From 323663bb115ef625642391a5a8e9b35fee8b2ae3 Mon Sep 17 00:00:00 2001 From: Hyeongseok Oh Date: Wed, 12 Apr 2023 15:42:02 +0900 Subject: [PATCH] Imported Upstream version 1.22.0 --- .ahub/tcchecker-tca/config.yaml | 1 + .gitattributes | 15 +- .github/workflows/check-format.yml | 6 +- .github/workflows/check-pr-commit.yml | 2 +- .github/workflows/deploy-github-pages.yml | 40 + Makefile.template | 107 +- compiler/CMakeLists.txt | 18 + compiler/circle-eval-diff/src/InputDataLoader.cpp | 3 +- compiler/circle-execution-plan/CMakeLists.txt | 1 + .../circle-execution-plan/pal/TargetPlatform.h | 12 + .../src/CircleExecutionPlan.cpp | 69 +- .../circle-execution-plan/src/ExecutionPlanner.cpp | 238 ++- .../circle-execution-plan/src/ExecutionPlanner.h | 68 +- compiler/circle-inspect/driver/Driver.cpp | 5 +- compiler/circle-inspect/src/Dump.cpp | 33 + compiler/circle-inspect/src/Dump.h | 9 + compiler/circle-interpreter-test/CMakeLists.txt | 17 + compiler/circle-interpreter-test/README.md | 9 + compiler/circle-interpreter-test/requires.cmake | 2 + .../src/circle-interpreter.test.cpp | 241 +++ .../circle-interpreter/src/CircleInterpreter.cpp | 4 +- compiler/circle-mpqsolver/CMakeLists.txt | 37 + compiler/circle-mpqsolver/README.md | 68 + compiler/circle-mpqsolver/requires.cmake | 6 + compiler/circle-mpqsolver/src/CircleMPQSolver.cpp | 168 +++ compiler/circle-mpqsolver/src/MPQSolver.cpp | 26 + compiler/circle-mpqsolver/src/MPQSolver.h | 54 + .../src/bisection/BisectionSolver.cpp | 244 +++ .../src/bisection/BisectionSolver.h | 80 + .../src/bisection/DepthParameterizer.cpp | 95 ++ .../src/bisection/DepthParameterizer.h | 49 + .../src/bisection/DepthParameterizer.test.cpp | 95 ++ .../src/bisection/ErrorApproximator.cpp | 387 +++++ .../src/bisection/ErrorApproximator.h | 37 + .../src/bisection/ErrorApproximator.test.cpp | 136 ++ .../circle-mpqsolver/src/bisection/ErrorMetric.cpp | 60 + .../circle-mpqsolver/src/bisection/ErrorMetric.h | 55 + .../circle-mpqsolver/src/bisection/Evaluator.cpp | 137 ++ .../circle-mpqsolver/src/bisection/Evaluator.h | 66 + .../circle-mpqsolver/src/bisection/Quantizer.cpp | 124 ++ .../circle-mpqsolver/src/bisection/Quantizer.h | 61 + .../src/bisection/Quantizer.test.cpp | 107 ++ .../circle-mpqsolver/src/bisection/TestHelper.h | 79 + .../src/bisection/VISQErrorApproximator.cpp | 48 + .../src/bisection/VISQErrorApproximator.h | 60 + .../src/circle-operator.test.cpp | 22 +- compiler/circle-opselector/CMakeLists.txt | 3 + compiler/circle-opselector/driver/Driver.cpp | 192 +-- compiler/circle-opselector/src/Driver.test.cpp | 66 - compiler/circle-opselector/src/Driver.test.h | 27 - compiler/circle-opselector/src/OpSelector.cpp | 387 +++++ compiler/circle-opselector/src/OpSelector.h | 54 + compiler/circle-opselector/src/OpSelector.test.cpp | 157 ++ compiler/circle-opselector/src/SelectType.h | 33 + compiler/circle-part-value-test/CMakeLists.txt | 16 +- .../CMakeLists.txt | 41 + .../circle-quantizer-dredd-recipe-test/README.md | 20 +- .../circle-quantizer-dredd-recipe-test/test.lst | 6 + compiler/circle-quantizer/src/CircleQuantizer.cpp | 10 +- compiler/circle2circle-dredd-recipe-test/README.md | 3 +- compiler/circle2circle-dredd-recipe-test/test.lst | 5 + .../circle2circle-dredd-recipe-test/testall.sh | 1 - compiler/circle2circle/src/Circle2Circle.cpp | 17 + compiler/common-artifacts/CMakeLists.txt | 47 +- compiler/common-artifacts/exclude.lst | 4 +- compiler/dalgona-test/.gitignore | 1 + compiler/dalgona-test/CMakeLists.txt | 69 + compiler/dalgona-test/GenH5RandomInputs.py | 66 + compiler/dalgona-test/RandomDataGenerator.py | 44 + compiler/dalgona-test/SingleOperatorTest.py | 210 +++ compiler/dalgona-test/TestSingleOp.sh | 97 ++ compiler/dalgona-test/TestUtil.py | 58 + compiler/dalgona-test/requires.cmake | 3 + compiler/dalgona-test/test.lst | 6 + compiler/dalgona/CMakeLists.txt | 63 + compiler/dalgona/README.md | 104 ++ compiler/dalgona/analysis/AnalysisTemplate.py | 125 ++ compiler/dalgona/driver/Driver.cpp | 90 ++ compiler/dalgona/include/Dalgona.h | 55 + compiler/dalgona/include/PythonHooks.h | 66 + compiler/dalgona/requires.cmake | 6 + compiler/dalgona/src/Dalgona.cpp | 275 ++++ compiler/dalgona/src/PostOperatorHook.h | 251 ++++ compiler/dalgona/src/PreOperatorHook.h | 221 +++ compiler/dalgona/src/PythonHooks.cpp | 109 ++ compiler/dalgona/src/RandomUtils.cpp | 43 + compiler/dalgona/src/RandomUtils.h | 49 + compiler/dalgona/src/RandomUtils.test.cpp | 65 + compiler/dalgona/src/StringUtils.cpp | 68 + compiler/dalgona/src/StringUtils.h | 34 + compiler/dalgona/src/StringUtils.test.cpp | 42 + compiler/dalgona/src/Utils.cpp | 175 +++ compiler/dalgona/src/Utils.h | 56 + compiler/dredd-rule-lib/rule-lib.sh | 18 + compiler/loco/include/loco/IR/Graph.h | 1 - compiler/loco/src/IR/Graph.test.cpp | 13 +- compiler/locomotiv/src/Node/TensorBroadcast.cpp | 4 + compiler/locomotiv/src/Node/TensorConstantPad.cpp | 1 + compiler/luci-compute/CMakeLists.txt | 23 + compiler/luci-compute/README.md | 3 + compiler/luci-eval-driver/CMakeLists.txt | 4 - compiler/luci-eval-driver/requires.cmake | 2 - compiler/luci-eval-driver/src/EvalDriver.cpp | 3 +- .../pal/cmsisnn/KernelsToBuild.lst | 1 + .../pal/cmsisnn/PALBatchToSpaceND.h | 2 +- .../luci-interpreter/pal/linux/KernelsToBuild.lst | 3 + .../luci-interpreter/pal/mcu/KernelsToBuild.lst | 1 + .../luci-interpreter/pal/mcu/PALBatchToSpaceND.h | 2 +- .../luci-interpreter/src/BuddyMemoryManager.cpp | 5 +- compiler/luci-interpreter/src/core/KernelParams.h | 9 + compiler/luci-interpreter/src/kernels/Abs.cpp | 63 + compiler/luci-interpreter/src/kernels/Abs.h | 46 + compiler/luci-interpreter/src/kernels/Abs.test.cpp | 81 + .../luci-interpreter/src/kernels/ReduceProd.cpp | 180 +++ compiler/luci-interpreter/src/kernels/ReduceProd.h | 50 + .../src/kernels/ReduceProd.test.cpp | 149 ++ compiler/luci-interpreter/src/kernels/SVDF.cpp | 28 +- compiler/luci-interpreter/src/kernels/Tanh.cpp | 1 + compiler/luci-interpreter/src/kernels/TestUtils.h | 1 + .../luci-interpreter/src/kernels/TransposeConv.cpp | 1 + .../src/kernels/UnidirectionalSequenceLSTM.cpp | 892 +++++++++++ .../src/kernels/UnidirectionalSequenceLSTM.h | 105 ++ .../kernels/UnidirectionalSequenceLSTM.test.cpp | 565 +++++++ compiler/luci-interpreter/src/kernels/Utils.cpp | 21 + compiler/luci-interpreter/src/kernels/Utils.h | 3 + .../luci-interpreter/src/loader/GraphLoader.cpp | 6 +- .../luci-interpreter/src/loader/KernelBuilder.cpp | 30 +- compiler/luci-interpreter/src/loader/nodes/Abs.cpp | 36 + .../src/loader/nodes/ReduceProd.cpp | 55 + .../loader/nodes/UnidirectionalSequenceLSTM.cpp | 106 ++ compiler/luci-micro/CMakeLists.txt | 58 - .../luci-micro/luci-interpreter/CMakeLists.txt | 15 - .../include/luci_interpreter/BuddyMemoryManager.h | 144 -- .../luci_interpreter/GraphBuilderRegistry.h | 35 - .../include/luci_interpreter/Interpreter.h | 84 -- .../include/luci_interpreter/MemoryManager.h | 37 - .../include/luci_interpreter/SimpleMemoryManager.h | 34 - .../include/luci_interpreter/StaticMemoryManager.h | 45 - .../include/luci_interpreter/TestMemoryManager.h | 47 - .../include/luci_interpreter/core/DataType.h | 36 - .../include/luci_interpreter/core/Tensor.h | 186 --- .../pal/cmsisnn/KernelsToBuild.lst | 62 - .../pal/cmsisnn/PALBatchToSpaceND.h | 37 - .../luci-interpreter/pal/cmsisnn/PALConv2d.h | 199 --- .../pal/cmsisnn/PALDepthwiseConv2d.h | 192 --- .../luci-interpreter/pal/cmsisnn/PALDequantize.h | 44 - .../luci-interpreter/pal/cmsisnn/PALQuantize.h | 44 - .../pal/cmsisnn/PALResizeBilinear.h | 37 - .../pal/cmsisnn/PALResizeNearestNeighbor.h | 37 - .../luci-interpreter/pal/cmsisnn/PALSVDF.h | 190 --- .../pal/cmsisnn/PALSpaceToBatchND.h | 38 - .../luci-interpreter/pal/cmsisnn/pal.cmake | 65 - .../luci-interpreter/pal/linux/KernelsToBuild.lst | 77 - .../luci-interpreter/pal/linux/PALSVDF.h | 90 -- .../luci-interpreter/pal/linux/pal.cmake | 82 - .../luci-interpreter/pal/mcu/KernelsToBuild.lst | 62 - .../luci-interpreter/pal/mcu/PALBatchToSpaceND.h | 37 - .../luci-interpreter/pal/mcu/PALConv2d.h | 85 -- .../luci-interpreter/pal/mcu/PALDequantize.h | 44 - .../luci-interpreter/pal/mcu/PALQuantize.h | 44 - .../luci-micro/luci-interpreter/pal/mcu/PALSVDF.h | 258 ---- .../luci-interpreter/pal/mcu/PALSoftmax.h | 62 - .../luci-micro/luci-interpreter/pal/mcu/pal.cmake | 56 - .../luci-micro/luci-interpreter/requires.cmake | 1 - .../luci-interpreter/src/BuddyMemoryManager.cpp | 96 -- .../src/BuddyMemoryManager.test.cpp | 69 - .../luci-micro/luci-interpreter/src/CMakeLists.txt | 61 - .../luci-interpreter/src/Interpreter.cpp | 145 -- .../luci-interpreter/src/SimpleMemoryManager.cpp | 51 - .../luci-interpreter/src/StaticMemoryManager.cpp | 39 - .../luci-interpreter/src/TestMemoryManager.cpp | 45 - .../luci-interpreter/src/core/CMakeLists.txt | 19 - .../luci-interpreter/src/core/EventNotifier.h | 36 - .../luci-micro/luci-interpreter/src/core/Kernel.h | 75 - .../luci-interpreter/src/core/KernelParams.h | 228 --- .../luci-interpreter/src/core/RuntimeGraph.cpp | 201 --- .../luci-interpreter/src/core/RuntimeGraph.h | 71 - .../luci-interpreter/src/core/RuntimeModule.h | 60 - .../luci-interpreter/src/core/Tensor.cpp | 58 - .../luci-interpreter/src/import/CMakeLists.txt | 15 - .../src/import/GraphBuilderRegistry.cpp | 33 - .../src/import/Nodes/CircleReferencingConst.cpp | 113 -- .../src/import/Nodes/CircleReferencingConst.h | 39 - .../luci-interpreter/src/kernels/Add.cpp | 220 --- .../luci-interpreter/src/kernels/ArgMax.cpp | 139 -- .../luci-interpreter/src/kernels/AveragePool2D.cpp | 194 --- .../luci-interpreter/src/kernels/BatchMatMul.cpp | 188 --- .../src/kernels/BatchToSpaceND.cpp | 104 -- .../luci-interpreter/src/kernels/CMakeLists.txt | 43 - .../luci-interpreter/src/kernels/Cast.cpp | 143 -- .../luci-interpreter/src/kernels/Concatenation.cpp | 149 -- .../luci-interpreter/src/kernels/Concatenation.h | 48 - .../src/kernels/Concatenation.test.cpp | 268 ---- .../luci-interpreter/src/kernels/Conv2D.cpp | 456 ------ .../luci-interpreter/src/kernels/Conv2D.h | 59 - .../luci-interpreter/src/kernels/Conv2D.test.cpp | 707 --------- .../luci-interpreter/src/kernels/DepthToSpace.cpp | 80 - .../src/kernels/DepthwiseConv2D.cpp | 451 ------ .../luci-interpreter/src/kernels/Dequantize.cpp | 79 - .../luci-interpreter/src/kernels/Div.cpp | 152 -- .../luci-interpreter/src/kernels/Elu.cpp | 52 - .../luci-interpreter/src/kernels/Equal.cpp | 142 -- .../luci-interpreter/src/kernels/Exp.cpp | 56 - .../luci-interpreter/src/kernels/ExpandDims.cpp | 88 -- .../luci-interpreter/src/kernels/ExpandDims.h | 44 - .../src/kernels/ExpandDims.test.cpp | 115 -- .../luci-interpreter/src/kernels/Fill.cpp | 117 -- .../luci-interpreter/src/kernels/Floor.cpp | 57 - .../luci-interpreter/src/kernels/FloorDiv.cpp | 85 -- .../src/kernels/FullyConnected.cpp | 192 --- .../luci-interpreter/src/kernels/FullyConnected.h | 51 - .../src/kernels/FullyConnected.test.cpp | 260 ---- .../luci-interpreter/src/kernels/Gather.cpp | 139 -- .../luci-interpreter/src/kernels/Greater.cpp | 142 -- .../luci-interpreter/src/kernels/GreaterEqual.cpp | 145 -- .../luci-interpreter/src/kernels/InstanceNorm.cpp | 121 -- .../luci-interpreter/src/kernels/L2Normalize.cpp | 75 - .../luci-interpreter/src/kernels/L2Pool2D.cpp | 88 -- .../luci-interpreter/src/kernels/LeakyRelu.cpp | 90 -- .../luci-interpreter/src/kernels/Less.cpp | 142 -- .../luci-interpreter/src/kernels/LessEqual.cpp | 142 -- .../src/kernels/LocalResponseNormalization.cpp | 65 - .../luci-interpreter/src/kernels/LogSoftmax.cpp | 92 -- .../luci-interpreter/src/kernels/LogicalAnd.cpp | 62 - .../luci-interpreter/src/kernels/LogicalNot.cpp | 60 - .../luci-interpreter/src/kernels/LogicalOr.cpp | 49 - .../luci-interpreter/src/kernels/Logistic.cpp | 94 -- .../luci-interpreter/src/kernels/Logistic.h | 52 - .../luci-interpreter/src/kernels/Logistic.test.cpp | 148 -- .../luci-interpreter/src/kernels/MaxPool2D.cpp | 150 -- .../luci-interpreter/src/kernels/MaxPool2D.h | 52 - .../src/kernels/MaxPool2D.test.cpp | 139 -- .../luci-interpreter/src/kernels/Maximum.cpp | 65 - .../luci-interpreter/src/kernels/Mean.cpp | 346 ----- .../luci-interpreter/src/kernels/Minimum.cpp | 65 - .../luci-interpreter/src/kernels/MirrorPad.cpp | 172 --- .../luci-interpreter/src/kernels/Mul.cpp | 150 -- .../luci-interpreter/src/kernels/Neg.cpp | 58 - .../luci-interpreter/src/kernels/NotEqual.cpp | 142 -- .../luci-interpreter/src/kernels/OneHot.cpp | 136 -- .../luci-interpreter/src/kernels/PRelu.cpp | 211 --- .../luci-interpreter/src/kernels/Pack.cpp | 142 -- .../luci-interpreter/src/kernels/Pad.cpp | 114 -- .../luci-interpreter/src/kernels/PadV2.cpp | 108 -- .../luci-interpreter/src/kernels/Pow.cpp | 79 - .../luci-interpreter/src/kernels/Quantize.cpp | 160 -- .../luci-interpreter/src/kernels/Relu.cpp | 114 -- .../luci-interpreter/src/kernels/Relu6.cpp | 88 -- .../luci-interpreter/src/kernels/Reshape.cpp | 90 -- .../luci-interpreter/src/kernels/Reshape.h | 43 - .../luci-interpreter/src/kernels/Reshape.test.cpp | 82 - .../src/kernels/ResizeBilinear.cpp | 74 - .../src/kernels/ResizeNearestNeighbor.cpp | 74 - .../luci-interpreter/src/kernels/ReverseV2.cpp | 81 - .../luci-interpreter/src/kernels/Rsqrt.cpp | 66 - .../luci-interpreter/src/kernels/SVDF.cpp | 241 --- .../luci-interpreter/src/kernels/Shape.cpp | 70 - .../luci-interpreter/src/kernels/Slice.cpp | 153 -- .../luci-interpreter/src/kernels/Softmax.cpp | 92 -- .../luci-interpreter/src/kernels/Softmax.h | 49 - .../luci-interpreter/src/kernels/Softmax.test.cpp | 117 -- .../src/kernels/SpaceToBatchND.cpp | 103 -- .../luci-interpreter/src/kernels/SpaceToDepth.cpp | 79 - .../luci-interpreter/src/kernels/Split.cpp | 81 - .../luci-interpreter/src/kernels/SplitV.cpp | 111 -- .../luci-interpreter/src/kernels/Sqrt.cpp | 66 - .../luci-interpreter/src/kernels/Square.cpp | 66 - .../src/kernels/SquaredDifference.cpp | 64 - .../luci-interpreter/src/kernels/Squeeze.cpp | 86 -- .../luci-interpreter/src/kernels/StridedSlice.cpp | 150 -- .../luci-interpreter/src/kernels/Sub.cpp | 164 -- .../luci-interpreter/src/kernels/Tanh.cpp | 93 -- .../luci-interpreter/src/kernels/TestUtils.cpp | 128 -- .../luci-interpreter/src/kernels/Transpose.cpp | 84 -- .../luci-interpreter/src/kernels/TransposeConv.cpp | 351 ----- .../luci-interpreter/src/kernels/Unpack.cpp | 84 -- .../luci-interpreter/src/kernels/Utils.cpp | 198 --- .../luci-interpreter/src/kernels/Utils.h | 293 ---- .../luci-interpreter/src/loader/CMakeLists.txt | 39 - .../luci-interpreter/src/loader/GraphLoader.cpp | 344 ----- .../luci-interpreter/src/loader/GraphLoader.h | 55 - .../luci-interpreter/src/loader/KernelBuilder.cpp | 104 -- .../luci-interpreter/src/loader/KernelBuilder.h | 52 - .../src/loader/KernelBuilder.test.cpp | 1376 ----------------- .../src/loader/KernelBuilderHelper.cpp | 64 - .../src/loader/KernelBuilderHelper.h | 84 -- .../luci-interpreter/src/loader/ModuleLoader.cpp | 53 - .../luci-interpreter/src/loader/ModuleLoader.h | 52 - .../luci-interpreter/src/loader/RuntimeToIR.h | 38 - .../luci-interpreter/src/loader/nodes/Add.cpp | 40 - .../luci-interpreter/src/loader/nodes/ArgMax.cpp | 39 - .../src/loader/nodes/AveragePool2D.cpp | 64 - .../src/loader/nodes/BatchMatMul.cpp | 70 - .../src/loader/nodes/BatchToSpaceND.cpp | 38 - .../luci-interpreter/src/loader/nodes/Builders.h | 37 - .../luci-interpreter/src/loader/nodes/Cast.cpp | 37 - .../src/loader/nodes/Concatenation.cpp | 42 - .../luci-interpreter/src/loader/nodes/Conv2D.cpp | 66 - .../src/loader/nodes/DepthToSpace.cpp | 39 - .../src/loader/nodes/DepthwiseConv2D.cpp | 67 - .../src/loader/nodes/Dequantize.cpp | 35 - .../luci-interpreter/src/loader/nodes/Div.cpp | 39 - .../luci-interpreter/src/loader/nodes/Elu.cpp | 35 - .../luci-interpreter/src/loader/nodes/Equal.cpp | 38 - .../luci-interpreter/src/loader/nodes/Exp.cpp | 36 - .../src/loader/nodes/ExpandDims.cpp | 37 - .../luci-interpreter/src/loader/nodes/Fill.cpp | 37 - .../luci-interpreter/src/loader/nodes/Floor.cpp | 36 - .../luci-interpreter/src/loader/nodes/FloorDiv.cpp | 37 - .../src/loader/nodes/FullyConnected.cpp | 42 - .../luci-interpreter/src/loader/nodes/Gather.cpp | 42 - .../luci-interpreter/src/loader/nodes/Greater.cpp | 37 - .../src/loader/nodes/GreaterEqual.cpp | 37 - .../luci-interpreter/src/loader/nodes/If.cpp | 47 - .../src/loader/nodes/InstanceNorm.cpp | 43 - .../src/loader/nodes/L2Normalize.cpp | 39 - .../luci-interpreter/src/loader/nodes/L2Pool2D.cpp | 44 - .../src/loader/nodes/LeakyRelu.cpp | 38 - .../luci-interpreter/src/loader/nodes/Less.cpp | 37 - .../src/loader/nodes/LessEqual.cpp | 37 - .../loader/nodes/LocalResponseNormalization.cpp | 42 - .../src/loader/nodes/LogSoftmax.cpp | 36 - .../src/loader/nodes/LogicalAnd.cpp | 37 - .../src/loader/nodes/LogicalNot.cpp | 36 - .../src/loader/nodes/LogicalOr.cpp | 37 - .../luci-interpreter/src/loader/nodes/Logistic.cpp | 36 - .../src/loader/nodes/MaxPool2D.cpp | 44 - .../luci-interpreter/src/loader/nodes/Maximum.cpp | 37 - .../luci-interpreter/src/loader/nodes/Mean.cpp | 61 - .../luci-interpreter/src/loader/nodes/Minimum.cpp | 37 - .../src/loader/nodes/MirrorPad.cpp | 40 - .../luci-interpreter/src/loader/nodes/Mul.cpp | 40 - .../luci-interpreter/src/loader/nodes/Neg.cpp | 36 - .../luci-interpreter/src/loader/nodes/NotEqual.cpp | 37 - .../luci-interpreter/src/loader/nodes/OneHot.cpp | 42 - .../luci-interpreter/src/loader/nodes/PRelu.cpp | 37 - .../luci-interpreter/src/loader/nodes/Pack.cpp | 44 - .../luci-interpreter/src/loader/nodes/Pad.cpp | 37 - .../luci-interpreter/src/loader/nodes/PadV2.cpp | 38 - .../luci-interpreter/src/loader/nodes/Pow.cpp | 38 - .../luci-interpreter/src/loader/nodes/Quantize.cpp | 36 - .../luci-interpreter/src/loader/nodes/Relu.cpp | 36 - .../luci-interpreter/src/loader/nodes/Relu6.cpp | 36 - .../luci-interpreter/src/loader/nodes/Reshape.cpp | 38 - .../src/loader/nodes/ResizeBilinear.cpp | 41 - .../src/loader/nodes/ResizeNearestNeighbor.cpp | 46 - .../src/loader/nodes/ReverseV2.cpp | 37 - .../luci-interpreter/src/loader/nodes/Rsqrt.cpp | 36 - .../luci-interpreter/src/loader/nodes/SVDF.cpp | 92 -- .../luci-interpreter/src/loader/nodes/Shape.cpp | 39 - .../luci-interpreter/src/loader/nodes/Slice.cpp | 39 - .../luci-interpreter/src/loader/nodes/Softmax.cpp | 39 - .../src/loader/nodes/SpaceToBatchND.cpp | 39 - .../src/loader/nodes/SpaceToDepth.cpp | 39 - .../luci-interpreter/src/loader/nodes/Split.cpp | 40 - .../luci-interpreter/src/loader/nodes/SplitV.cpp | 41 - .../luci-interpreter/src/loader/nodes/Sqrt.cpp | 36 - .../luci-interpreter/src/loader/nodes/Square.cpp | 36 - .../src/loader/nodes/SquaredDifference.cpp | 37 - .../luci-interpreter/src/loader/nodes/Squeeze.cpp | 39 - .../src/loader/nodes/StridedSlice.cpp | 47 - .../luci-interpreter/src/loader/nodes/Sub.cpp | 40 - .../luci-interpreter/src/loader/nodes/Tanh.cpp | 36 - .../src/loader/nodes/Transpose.cpp | 37 - .../src/loader/nodes/TransposeConv.cpp | 55 - .../luci-interpreter/src/loader/nodes/Unpack.cpp | 42 - .../luci-interpreter/src/loader/nodes/While.cpp | 47 - compiler/luci-micro/requires.cmake | 1 - compiler/luci-micro/standalone/CMakeLists.txt | 37 - compiler/luci-pass-value-test/CMakeLists.txt | 11 + compiler/luci-pass-value-test/test.lst | 7 + compiler/luci-value-test/CMakeLists.txt | 22 + compiler/luci-value-test/luci_eval_verifier.py | 33 +- compiler/luci-value-test/luci_eval_verifier_ref.py | 27 +- compiler/luci-value-test/test.lst | 7 +- compiler/luci/import/src/GraphBuilder.cpp | 3 - .../luci/import/src/GraphBuilderMultiOutput.cpp | 3 - .../src/Nodes/CircleBidirectionalSequenceLSTM.cpp | 20 +- compiler/luci/import/src/Nodes/CircleSVDF.cpp | 3 - .../luci/import/src/Nodes/CircleTransposeConv.cpp | 3 - .../src/Nodes/CircleUnidirectionalSequenceLSTM.cpp | 18 +- .../IR/Nodes/CircleUnidirectionalSequenceLSTM.h | 4 +- .../CircleUnidirectionalSequenceLSTM.test.cpp | 2 +- compiler/luci/log/src/Log.cpp | 22 - .../luci/logex/src/CircleNodeSummaryBuilders.cpp | 6 +- .../src/Nodes/CircleUnidirectionalSequenceLSTM.cpp | 5 +- .../CircleUnidirectionalSequenceLSTM.test.cpp | 2 +- compiler/luci/partition/src/PartitionPModules.cpp | 9 +- compiler/luci/pass/CMakeLists.txt | 2 +- compiler/luci/pass/include/luci/CircleOptimizer.h | 5 + .../include/luci/Pass/FoldFullyConnectedPass.h | 38 + .../include/luci/Pass/ForwardTransposeOpPass.h | 37 + .../luci/pass/include/luci/Pass/FusePReluPass.h | 40 + .../include/luci/Pass/QuantizeWithMinMaxPass.h | 21 +- .../include/luci/Pass/RemoveDuplicateConstPass.h | 45 + .../Pass/UnrollUnidirectionalSequenceLSTMPass.h | 37 + compiler/luci/pass/src/CircleOptimizer.cpp | 25 + compiler/luci/pass/src/CircleQuantizer.cpp | 191 ++- compiler/luci/pass/src/ConvertNCHWToNHWCPass.cpp | 180 ++- .../luci/pass/src/ConvertNCHWToNHWCPass.test.cpp | 252 +++- .../pass/src/ConvertToFakeQuantizedModelPass.cpp | 17 +- .../pass/src/ExpandBroadcastConstPass.test.cpp | 2 + compiler/luci/pass/src/FoldDepthwiseConv2DPass.cpp | 2 + .../luci/pass/src/FoldDepthwiseConv2DPass.test.cpp | 2 + compiler/luci/pass/src/FoldFullyConnectedPass.cpp | 198 +++ .../luci/pass/src/FoldFullyConnectedPass.test.cpp | 160 ++ .../luci/pass/src/ForwardReshapeToUnaryOpPass.cpp | 28 + compiler/luci/pass/src/ForwardTransposeOpPass.cpp | 366 +++++ .../luci/pass/src/ForwardTransposeOpPass.test.cpp | 524 +++++++ .../pass/src/FuseAddWithFullyConnectedPass.cpp | 8 + .../src/FuseAddWithFullyConnectedPass.test.cpp | 20 + compiler/luci/pass/src/FuseBCQPass.cpp | 1 - .../luci/pass/src/FuseBatchNormWithTConvPass.cpp | 53 +- compiler/luci/pass/src/FusePReluPass.cpp | 202 +++ compiler/luci/pass/src/FusePReluPass.test.cpp | 187 +++ compiler/luci/pass/src/QuantizationUtils.cpp | 6 +- compiler/luci/pass/src/QuantizationUtils.h | 3 + compiler/luci/pass/src/QuantizeActivation.cpp | 17 +- compiler/luci/pass/src/QuantizeActivation.h | 1 + compiler/luci/pass/src/QuantizeWeights.cpp | 173 ++- compiler/luci/pass/src/QuantizeWithMinMaxPass.cpp | 76 +- .../luci/pass/src/QuantizeWithMinMaxPass.test.cpp | 39 +- compiler/luci/pass/src/QuantizedModelVerifier.h | 17 +- .../luci/pass/src/QuantizedModelVerifier.test.cpp | 279 +++- .../luci/pass/src/RemoveDuplicateConstPass.cpp | 225 +++ .../pass/src/RemoveDuplicateConstPass.test.cpp | 159 ++ .../src/ReplaceNonConstFCWithBatchMatMulPass.cpp | 64 +- .../ReplaceNonConstFCWithBatchMatMulPass.test.cpp | 4 +- .../luci/pass/src/ResolveCustomOpMatMulPass.cpp | 1 - .../src/ResolveCustomOpMaxPoolWithArgmaxPass.cpp | 1 + .../luci/pass/src/ResolveCustomOpSplitVPass.cpp | 2 + .../src/UnrollUnidirectionalSequenceLSTMPass.cpp | 672 +++++++++ .../UnrollUnidirectionalSequenceLSTMPass.test.cpp | 211 +++ .../luci/pass/src/VerifyQuantizedNodeGranularity.h | 8 + compiler/luci/pass/src/VerifyQuantizedNodeType.cpp | 8 + compiler/luci/pass/src/helpers/NodeFiller.h | 26 + .../pass/src/helpers/SparsityFormatConverter.h | 1 + compiler/luci/pass/src/helpers/Strings.cpp | 9 + compiler/luci/pass/src/helpers/Strings.h | 2 + compiler/luci/pass/src/helpers/Strings.test.cpp | 23 + compiler/luci/pass/src/test/TestIOGraph.h | 161 -- compiler/luci/pass/src/test/TestIOGraph.test.cpp | 19 - compiler/luci/requires.cmake | 2 +- .../luci/service/src/CircleShapeInferenceRule.cpp | 24 - .../luci/service/src/CircleTypeInferenceRule.cpp | 9 +- .../service/src/Nodes/CircleFullyConnected.cpp | 1 + compiler/luci/service/src/Validate.cpp | 75 + compiler/luci/tests/test.lst | 2 + compiler/mio-tflite280/CMakeLists.txt | 10 - .../passes/optimizations/ConstantFoldTranspose.cpp | 2 +- compiler/nnc/tests/soft_backend/test_main.def | 3 +- compiler/one-cmds/CMakeLists.txt | 72 +- compiler/one-cmds/dummy-driver/CMakeLists.txt | 9 + .../one-cmds/dummy-driver/src/dummyEnv-compile.cpp | 45 + compiler/one-cmds/how-to-prepare-virtualenv.txt | 8 +- compiler/one-cmds/how-to-use-one-commands.txt | 1 + compiler/one-cmds/one-build | 34 +- compiler/one-cmds/one-codegen | 18 +- compiler/one-cmds/one-import | 6 +- compiler/one-cmds/one-import-bcq | 26 +- compiler/one-cmds/one-import-onnx | 28 +- compiler/one-cmds/one-import-pytorch | 30 +- compiler/one-cmds/one-import-tf | 26 +- compiler/one-cmds/one-import-tflite | 16 +- compiler/one-cmds/one-infer | 126 +- compiler/one-cmds/one-init | 139 +- compiler/one-cmds/one-optimize | 48 +- compiler/one-cmds/one-pack | 19 +- compiler/one-cmds/one-partition | 26 +- compiler/one-cmds/one-prepare-venv | 8 +- compiler/one-cmds/one-prepare-venv.u2204 | 97 ++ compiler/one-cmds/one-profile | 16 +- compiler/one-cmds/one-quantize | 133 +- compiler/one-cmds/onecc | 26 +- compiler/one-cmds/onecc.template.cfg | 8 + compiler/one-cmds/onelib/CfgRunner.py | 25 +- compiler/one-cmds/onelib/Command.py | 54 + compiler/one-cmds/onelib/WorkflowRunner.py | 4 +- compiler/one-cmds/onelib/constant.py | 60 +- compiler/one-cmds/onelib/export_constant.py | 76 + compiler/one-cmds/onelib/make_cmd.py | 28 +- compiler/one-cmds/onelib/utils.py | 273 ++++ compiler/one-cmds/tests/CMakeLists.txt | 39 + compiler/one-cmds/tests/one-build_001.test | 5 +- compiler/one-cmds/tests/one-build_002.test | 5 +- compiler/one-cmds/tests/one-build_003.test | 5 +- compiler/one-cmds/tests/one-build_004.test | 5 +- compiler/one-cmds/tests/one-build_005.test | 5 +- compiler/one-cmds/tests/one-build_006.test | 5 +- compiler/one-cmds/tests/one-build_007.test | 3 +- compiler/one-cmds/tests/one-build_008.test | 5 +- compiler/one-cmds/tests/one-build_009.test | 5 +- compiler/one-cmds/tests/one-build_010.test | 7 +- compiler/one-cmds/tests/one-build_011.test | 7 +- compiler/one-cmds/tests/one-build_012.test | 5 +- compiler/one-cmds/tests/one-build_013.test | 5 +- compiler/one-cmds/tests/one-build_014.test | 5 +- compiler/one-cmds/tests/one-build_neg_002.test | 2 + compiler/one-cmds/tests/one-build_neg_003.test | 2 + compiler/one-cmds/tests/one-build_neg_004.test | 2 + compiler/one-cmds/tests/one-build_neg_005.test | 1 + compiler/one-cmds/tests/one-build_neg_006.test | 1 + compiler/one-cmds/tests/one-build_neg_007.test | 2 + compiler/one-cmds/tests/one-build_neg_008.test | 2 + compiler/one-cmds/tests/one-build_neg_009.test | 2 + compiler/one-cmds/tests/one-codegen_001.test | 2 + compiler/one-cmds/tests/one-codegen_002.test | 1 + compiler/one-cmds/tests/one-codegen_003.test | 1 + compiler/one-cmds/tests/one-codegen_004.test | 2 + compiler/one-cmds/tests/one-codegen_neg_001.test | 2 + compiler/one-cmds/tests/one-import-bcq_001.test | 3 +- .../one-cmds/tests/one-import-bcq_neg_001.test | 2 +- .../one-cmds/tests/one-import-bcq_neg_002.test | 2 +- .../one-cmds/tests/one-import-bcq_neg_003.test | 2 +- .../one-cmds/tests/one-import-bcq_neg_004.test | 2 +- .../one-cmds/tests/one-import-bcq_neg_005.test | 2 +- .../one-cmds/tests/one-import-bcq_neg_006.test | 2 +- .../one-cmds/tests/one-import-bcq_neg_007.test | 2 +- .../one-cmds/tests/one-import-bcq_neg_008.test | 2 +- .../one-cmds/tests/one-import-bcq_neg_009.test | 2 +- compiler/one-cmds/tests/one-import-onnx_001.test | 4 +- compiler/one-cmds/tests/one-import-onnx_002.test | 6 +- compiler/one-cmds/tests/one-import_001.test | 3 +- compiler/one-cmds/tests/one-import_002.cfg | 2 +- compiler/one-cmds/tests/one-import_002.test | 11 +- compiler/one-cmds/tests/one-import_003.test | 3 +- compiler/one-cmds/tests/one-import_004.test | 3 +- compiler/one-cmds/tests/one-import_005.test | 1 + compiler/one-cmds/tests/one-import_006.test | 1 + compiler/one-cmds/tests/one-import_neg_001.test | 3 +- compiler/one-cmds/tests/one-import_neg_003.test | 3 +- compiler/one-cmds/tests/one-import_neg_004.test | 3 +- compiler/one-cmds/tests/one-import_neg_005.test | 2 +- compiler/one-cmds/tests/one-import_neg_006.test | 2 +- compiler/one-cmds/tests/one-import_neg_007.test | 2 +- compiler/one-cmds/tests/one-import_neg_008.test | 2 +- compiler/one-cmds/tests/one-import_neg_010.test | 2 +- compiler/one-cmds/tests/one-infer_001.test | 4 +- compiler/one-cmds/tests/one-infer_002.test | 2 + compiler/one-cmds/tests/one-infer_003.test | 18 +- compiler/one-cmds/tests/one-infer_004.cfg | 3 + compiler/one-cmds/tests/one-infer_004.test | 21 +- compiler/one-cmds/tests/one-infer_005.cfg | 3 - compiler/one-cmds/tests/one-infer_005.test | 14 +- compiler/one-cmds/tests/one-infer_006.test | 53 - compiler/one-cmds/tests/one-infer_neg_001.test | 4 +- compiler/one-cmds/tests/one-infer_neg_002.test | 4 +- compiler/one-cmds/tests/one-infer_neg_003.test | 28 +- compiler/one-cmds/tests/one-infer_neg_004.test | 41 - compiler/one-cmds/tests/one-infer_neg_005.test | 54 - compiler/one-cmds/tests/one-optimize_001.test | 12 +- compiler/one-cmds/tests/one-optimize_002.test | 12 +- compiler/one-cmds/tests/one-optimize_003.test | 62 + compiler/one-cmds/tests/one-optimize_neg_001.test | 2 +- compiler/one-cmds/tests/one-optimize_neg_002.test | 2 +- compiler/one-cmds/tests/one-optimize_neg_003.test | 9 +- compiler/one-cmds/tests/one-pack_001.test | 12 +- compiler/one-cmds/tests/one-partition_001.test | 3 +- compiler/one-cmds/tests/one-partition_neg_001.test | 5 + compiler/one-cmds/tests/one-profile_001.test | 2 + compiler/one-cmds/tests/one-profile_002.test | 2 + compiler/one-cmds/tests/one-profile_003.test | 2 + compiler/one-cmds/tests/one-profile_004.test | 2 + compiler/one-cmds/tests/one-profile_neg_001.test | 2 + compiler/one-cmds/tests/one-quantize_001.test | 12 +- compiler/one-cmds/tests/one-quantize_002.test | 14 +- compiler/one-cmds/tests/one-quantize_003.test | 5 +- compiler/one-cmds/tests/one-quantize_004.test | 3 +- compiler/one-cmds/tests/one-quantize_005.test | 3 +- compiler/one-cmds/tests/one-quantize_006.test | 3 +- compiler/one-cmds/tests/one-quantize_007.test | 12 +- compiler/one-cmds/tests/one-quantize_008.test | 12 +- compiler/one-cmds/tests/one-quantize_009.test | 12 +- compiler/one-cmds/tests/one-quantize_010.test | 10 +- compiler/one-cmds/tests/one-quantize_011.test | 1 + compiler/one-cmds/tests/one-quantize_012.test | 3 +- compiler/one-cmds/tests/one-quantize_013.test | 3 +- compiler/one-cmds/tests/one-quantize_014.test | 1 + compiler/one-cmds/tests/one-quantize_015.test | 1 + compiler/one-cmds/tests/one-quantize_016.test | 70 + compiler/one-cmds/tests/one-quantize_neg_001.test | 11 +- compiler/one-cmds/tests/one-quantize_neg_002.test | 11 +- compiler/one-cmds/tests/one-quantize_neg_003.test | 11 +- compiler/one-cmds/tests/one-quantize_neg_004.test | 11 +- compiler/one-cmds/tests/one-quantize_neg_005.test | 2 +- compiler/one-cmds/tests/one-quantize_neg_006.test | 2 +- compiler/one-cmds/tests/one-quantize_neg_007.test | 11 +- compiler/one-cmds/tests/one-quantize_neg_008.test | 11 +- compiler/one-cmds/tests/one-quantize_neg_009.test | 11 +- compiler/one-cmds/tests/one-quantize_neg_010.test | 11 +- compiler/one-cmds/tests/one-quantize_neg_011.test | 11 +- compiler/one-cmds/tests/one-quantize_neg_012.test | 11 +- compiler/one-cmds/tests/one-quantize_neg_013.test | 11 +- compiler/one-cmds/tests/one-quantize_neg_014.test | 2 +- compiler/one-cmds/tests/one-quantize_neg_015.test | 2 +- compiler/one-cmds/tests/one-quantize_neg_016.test | 2 +- compiler/one-cmds/tests/one-quantize_neg_017.test | 2 +- compiler/one-cmds/tests/one-quantize_neg_018.test | 2 +- compiler/one-cmds/tests/one-quantize_neg_019.test | 2 +- compiler/one-cmds/tests/one-quantize_neg_020.test | 2 +- compiler/one-cmds/tests/one-quantize_neg_021.test | 50 + compiler/one-cmds/tests/onecc_001.cfg | 4 +- compiler/one-cmds/tests/onecc_001.test | 5 +- compiler/one-cmds/tests/onecc_002.cfg | 4 +- compiler/one-cmds/tests/onecc_002.test | 5 +- compiler/one-cmds/tests/onecc_003.cfg | 4 +- compiler/one-cmds/tests/onecc_003.test | 3 +- compiler/one-cmds/tests/onecc_004.cfg | 4 +- compiler/one-cmds/tests/onecc_004.test | 3 +- compiler/one-cmds/tests/onecc_005.cfg | 4 +- compiler/one-cmds/tests/onecc_005.test | 3 +- compiler/one-cmds/tests/onecc_006.cfg | 4 +- compiler/one-cmds/tests/onecc_006.test | 3 +- compiler/one-cmds/tests/onecc_007.cfg | 4 +- compiler/one-cmds/tests/onecc_007.test | 3 +- compiler/one-cmds/tests/onecc_008.test | 3 +- compiler/one-cmds/tests/onecc_009.test | 3 +- compiler/one-cmds/tests/onecc_010.test | 3 +- compiler/one-cmds/tests/onecc_011.test | 3 +- compiler/one-cmds/tests/onecc_012.cfg | 4 +- compiler/one-cmds/tests/onecc_012.test | 3 +- compiler/one-cmds/tests/onecc_013.cfg | 2 +- compiler/one-cmds/tests/onecc_013.test | 5 +- compiler/one-cmds/tests/onecc_014.cfg | 2 +- compiler/one-cmds/tests/onecc_014.test | 5 +- compiler/one-cmds/tests/onecc_015.test | 3 +- compiler/one-cmds/tests/onecc_016.test | 3 +- compiler/one-cmds/tests/onecc_017.test | 3 +- compiler/one-cmds/tests/onecc_018.test | 3 +- compiler/one-cmds/tests/onecc_019.test | 3 +- compiler/one-cmds/tests/onecc_020.test | 3 +- compiler/one-cmds/tests/onecc_021.test | 4 +- compiler/one-cmds/tests/onecc_022.test | 3 +- compiler/one-cmds/tests/onecc_023.test | 3 +- compiler/one-cmds/tests/onecc_024.cfg | 4 +- compiler/one-cmds/tests/onecc_024.test | 3 +- compiler/one-cmds/tests/onecc_025.cfg | 4 +- compiler/one-cmds/tests/onecc_025.test | 5 +- compiler/one-cmds/tests/onecc_026.test | 1 + compiler/one-cmds/tests/onecc_027.cfg | 2 +- compiler/one-cmds/tests/onecc_027.test | 4 +- compiler/one-cmds/tests/onecc_028.test | 1 + compiler/one-cmds/tests/onecc_028.workflow.json | 4 +- compiler/one-cmds/tests/onecc_029.test | 1 + compiler/one-cmds/tests/onecc_029.workflow.json | 4 +- compiler/one-cmds/tests/onecc_030.test | 1 + compiler/one-cmds/tests/onecc_030.workflow.json | 4 +- compiler/one-cmds/tests/onecc_031.test | 1 + compiler/one-cmds/tests/onecc_031.workflow.json | 4 +- compiler/one-cmds/tests/onecc_032.test | 1 + compiler/one-cmds/tests/onecc_032.workflow.json | 6 +- compiler/one-cmds/tests/onecc_033.test | 1 + compiler/one-cmds/tests/onecc_033.workflow.json | 6 +- compiler/one-cmds/tests/onecc_034.test | 1 + compiler/one-cmds/tests/onecc_035.test | 1 + compiler/one-cmds/tests/onecc_036.test | 1 + compiler/one-cmds/tests/onecc_037.test | 1 + compiler/one-cmds/tests/onecc_037.workflow.json | 4 +- compiler/one-cmds/tests/onecc_038.test | 1 + compiler/one-cmds/tests/onecc_038.workflow.json | 4 +- compiler/one-cmds/tests/onecc_039.test | 1 + compiler/one-cmds/tests/onecc_040.cfg | 4 +- compiler/one-cmds/tests/onecc_040.test | 1 + compiler/one-cmds/tests/onecc_041.test | 1 + compiler/one-cmds/tests/onecc_041.workflow.json | 4 +- compiler/one-cmds/tests/onecc_042.cfg | 9 + compiler/one-cmds/tests/onecc_042.test | 55 + compiler/one-cmds/tests/onecc_043.cfg | 14 + compiler/one-cmds/tests/onecc_043.test | 61 + compiler/one-cmds/tests/onecc_044.cfg | 20 + compiler/one-cmds/tests/onecc_044.test | 74 + compiler/one-cmds/tests/onecc_045.cfg | 13 + compiler/one-cmds/tests/onecc_045.test | 65 + compiler/one-cmds/tests/onecc_neg_001.test | 2 + compiler/one-cmds/tests/onecc_neg_002.cfg | 4 +- compiler/one-cmds/tests/onecc_neg_002.test | 2 + compiler/one-cmds/tests/onecc_neg_003.cfg | 4 +- compiler/one-cmds/tests/onecc_neg_003.test | 2 + compiler/one-cmds/tests/onecc_neg_004.cfg | 4 +- compiler/one-cmds/tests/onecc_neg_004.test | 2 + compiler/one-cmds/tests/onecc_neg_005.test | 1 + compiler/one-cmds/tests/onecc_neg_006.test | 1 + compiler/one-cmds/tests/onecc_neg_007.test | 2 + compiler/one-cmds/tests/onecc_neg_008.test | 2 + compiler/one-cmds/tests/onecc_neg_009.test | 2 + compiler/one-cmds/tests/onecc_neg_010.test | 2 + compiler/one-cmds/tests/onecc_neg_011.test | 2 + compiler/one-cmds/tests/onecc_neg_012.cfg | 1 - compiler/one-cmds/tests/onecc_neg_012.test | 6 +- compiler/one-cmds/tests/onecc_neg_013.test | 2 + compiler/one-cmds/tests/onecc_neg_014.test | 2 + compiler/one-cmds/tests/onecc_neg_015.test | 2 + compiler/one-cmds/tests/onecc_neg_016.test | 2 + compiler/one-cmds/tests/onecc_neg_017.test | 2 + compiler/one-cmds/tests/onecc_neg_018.test | 2 + .../one-cmds/tests/onecc_neg_018.workflow.json | 2 +- compiler/one-cmds/tests/onecc_neg_019.test | 2 + .../one-cmds/tests/onecc_neg_019.workflow.json | 2 +- compiler/one-cmds/tests/onecc_neg_020.test | 2 + .../one-cmds/tests/onecc_neg_020.workflow.json | 2 +- compiler/one-cmds/tests/onecc_neg_021.test | 2 + .../one-cmds/tests/onecc_neg_021.workflow.json | 4 +- compiler/one-cmds/tests/onecc_neg_022.test | 2 + .../one-cmds/tests/onecc_neg_022.workflow.json | 4 +- compiler/one-cmds/tests/onecc_neg_023.test | 2 + .../one-cmds/tests/onecc_neg_023.workflow.json | 4 +- compiler/one-cmds/tests/onecc_neg_024.cfg | 20 + compiler/one-cmds/tests/onecc_neg_024.test | 43 + compiler/one-cmds/tests/onecc_neg_025.cfg | 20 + compiler/one-cmds/tests/onecc_neg_025.test | 43 + compiler/one-cmds/tests/onecc_neg_026.cfg | 13 + compiler/one-cmds/tests/onecc_neg_026.test | 44 + compiler/one-cmds/tests/prepare_test_materials.sh | 41 +- compiler/one-cmds/tests/rawdata2hdf5_001.test | 3 +- compiler/one-cmds/tests/rawdata2hdf5_neg_001.test | 1 + compiler/one-cmds/tests/rawdata2hdf5_neg_002.test | 2 + compiler/one-cmds/tests/rawdata2hdf5_neg_003.test | 2 + compiler/one-cmds/tests/rawdata2hdf5_neg_004.test | 2 + compiler/one-cmds/utils.py | 315 ---- compiler/onecc-docker/README.md | 36 + compiler/onecc-docker/debian/changelog | 6 + compiler/onecc-docker/debian/compat | 1 + compiler/onecc-docker/debian/control | 13 + compiler/onecc-docker/debian/copyright | 3 + compiler/onecc-docker/debian/onecc-docker.install | 2 + compiler/onecc-docker/debian/onecc-docker.links | 1 + compiler/onecc-docker/debian/rules | 8 + compiler/onecc-docker/docker/Dockerfile | 26 + compiler/onecc-docker/onecc-docker | 109 ++ compiler/pics/CMakeLists.txt | 33 + compiler/pics/README.md | 16 + .../pota-quantization-value-test/CMakeLists.txt | 43 +- .../pota-quantization-value-test/requires.cmake | 1 + .../test_parallel_record_minmax.sh | 95 ++ .../record-minmax-conversion-test/CMakeLists.txt | 11 + .../CMakeLists.txt | 73 + .../gen_h5_random_inputs.py | 54 + .../requires.cmake | 2 + compiler/record-minmax-thread-safety-test/test.lst | 16 + .../record-minmax-thread-safety-test/testall.sh | 83 + compiler/record-minmax/CMakeLists.txt | 27 + compiler/record-minmax/driver/Driver.cpp | 38 +- compiler/record-minmax/include/MinMaxObserver.h | 9 + compiler/record-minmax/include/RecordFunction.h | 10 +- compiler/record-minmax/include/RecordMinMax.h | 26 +- compiler/record-minmax/src/RecordMinMax.cpp | 227 ++- compiler/souschef/src/Gaussian.cpp | 1 + .../CMakeLists.txt | 2 +- .../tf2circle-value-pbtxt-remote-test/testall.sh | 2 +- compiler/tf2circle/src/CustomopConfLoader.cpp | 2 + compiler/tf2tflite/src/CustomopConfLoader.cpp | 2 + .../tf2tfliteV2-conversion-test/CMakeLists.txt | 7 +- compiler/tflite2circle/src/DataLookup.cpp | 11 +- compiler/tflite2circle/src/DataLookup.h | 1 + compiler/tflite2circle/src/TFLOperator.lst | 20 + compiler/vconone/CMakeLists.txt | 2 +- compiler/visq-unittest/CMakeLists.txt | 66 + compiler/visq-unittest/README.md | 3 + compiler/visq-unittest/requires.cmake | 3 + compiler/visq-unittest/test/__init__.py | 1 + compiler/visq-unittest/test/testDotBuilder.py | 44 + compiler/visq-unittest/test/testPalette.py | 42 + compiler/visq-unittest/test/testQErrorComputer.py | 150 ++ compiler/visq-unittest/test/testUtil.py | 55 + compiler/visq/CMakeLists.txt | 67 + compiler/visq/README.md | 32 + compiler/visq/requires.cmake | 2 + compiler/visq/visq | 381 +++++ compiler/visq/visqlib/DotBuilder.py | 165 ++ compiler/visq/visqlib/DumpFP32FM.py | 54 + compiler/visq/visqlib/DumpFakeQuantFM.py | 66 + compiler/visq/visqlib/Palette.py | 70 + compiler/visq/visqlib/QErrorComputer.py | 200 +++ compiler/visq/visqlib/Util.py | 37 + compute/cker/CMakeLists.txt | 2 +- compute/cker/include/cker/operation/Dequantize.h | 33 + compute/cker/include/cker/operation/Logistic.h | 29 +- compute/cker/include/cker/operation/Quantize.h | 159 ++ compute/cker/include/cker/operation/Reduce.h | 8 +- compute/cker/include/cker/operation/Round.h | 21 + docs/conf.py | 2 +- docs/howto/how-to-build-compiler.md | 10 + docs/howto/how-to-build-runtime-tizen-gbs-rpi4.md | 6 +- docs/howto/how-to-build-runtime.md | 95 +- docs/howto/how-to-contribute.md | 5 +- .../how-to-cross-build-runtime-for-aarch64.md | 4 +- docs/howto/how-to-cross-build-runtime-for-arm.md | 10 +- docs/howto/how-to-use-specific-backend.md | 6 +- docs/release/1.22/index.rst | 13 + docs/release/1.22/release-note-1.22.0.md | 12 + infra/cmake/modules/ExternalSourceTools.cmake | 5 +- infra/cmake/modules/IdentifyPlatform.cmake | 4 + .../packages/CMSIS-NN-4.0.0/CMSIS-NNConfig.cmake | 14 + .../CMSIS-NN-4.0.0/CMSIS-NNConfigVersion.cmake | 10 + infra/cmake/packages/GTestConfig.cmake | 6 +- infra/cmake/packages/TensorFlowGpuConfig.cmake | 22 - .../patch_for_gpu_cl_build.patch | 292 ---- .../cmake/packages/TensorFlowGpuSourceConfig.cmake | 75 - .../packages/TensorFlowLiteGpu/CMakeLists.txt | 72 - infra/command/format | 4 + infra/command/gen-coverage-report | 5 +- infra/debian/compiler/changelog | 13 + infra/debian/compiler/one-compiler-dev.install | 1 + infra/debian/compiler/one-compiler.install | 16 +- infra/debian/compiler/rules | 5 +- infra/debian/runtime/rules | 2 +- infra/docker/bionic/Dockerfile | 18 +- infra/docker/focal/Dockerfile | 14 +- infra/docker/jammy/Dockerfile | 60 + infra/doxygen/Doxyfile | 68 +- infra/nncc/CMakeLists.txt | 10 +- infra/nncc/Makefile.arm32 | 16 +- .../buildtool/config/config_armv7l-linux.cmake | 7 +- infra/nncc/config/docker.configuration | 2 +- infra/nnfw/CMakeLists.txt | 7 +- infra/nnfw/cmake/CfgOptionFlags.cmake | 11 +- .../cmake/options/options_aarch64-android.cmake | 14 +- .../nnfw/cmake/options/options_aarch64-tizen.cmake | 5 + .../nnfw/cmake/options/options_armv7hl-tizen.cmake | 10 +- .../nnfw/cmake/options/options_armv7l-linux.cmake | 4 +- .../nnfw/cmake/options/options_armv7l-tizen.cmake | 11 +- .../nnfw/cmake/options/options_x86_64-tizen.cmake | 4 + infra/nnfw/cmake/packages/ARMComputeConfig.cmake | 17 +- infra/nnfw/cmake/packages/EigenConfig.cmake | 2 +- infra/nnfw/cmake/packages/GObject2.0Config.cmake | 30 + infra/nnfw/cmake/packages/GTestConfig.cmake | 2 +- infra/nnfw/cmake/packages/Gio2.0Config.cmake | 32 + infra/nnfw/cmake/packages/Giounix2.0Config.cmake | 30 + .../TensorFlowEigenConfig.cmake | 19 - .../TensorFlowEigenConfigVersion.cmake | 9 - .../nnfw/cmake/packages/TensorFlowGpuConfig.cmake | 51 + .../TensorFlowLite/CMakeLists.txt | 64 - .../TensorFlowLiteConfig.cmake | 79 - .../TensorFlowLiteConfigVersion.cmake | 9 - .../TensorFlowLite/CMakeLists.txt | 252 ++-- .../TensorFlowLiteConfig.cmake | 58 +- .../packages/TensorFlowLiteGpu/CMakeLists.txt | 73 + infra/nnfw/command/prepare-model | 18 + infra/nnfw/config/gbs.conf | 11 +- infra/onert-micro/CMakeLists.txt | 61 + infra/onert-micro/cmake/ApplyCompileFlags.cmake | 35 + infra/onert-micro/cmake/CfgOptionFlags.cmake | 18 + .../cmake/buildtool/config/arm-none-eabi-gcc.cmake | 66 + .../cmake/buildtool/config/config_linux.cmake | 11 + .../buildtool/config/config_x86_64-linux.cmake | 12 + .../cmake/options/options_armv7-r-generic.cmake | 3 + .../cmake/options/options_armv7em-generic.cmake | 3 + .../cmake/options/options_armv8-m-generic.cmake | 3 + .../cmake/options/options_x86_64-linux.cmake | 3 + infra/onert-micro/utils.cmake | 53 + infra/packaging/build | 2 +- infra/packaging/preset/20220323 | 2 + infra/packaging/preset/20220323_windows | 2 + infra/packaging/preset/20221125 | 66 + infra/packaging/preset/20221125_windows | 80 + infra/packaging/res/tf2nnpkg.20221125 | 109 ++ infra/scripts/build_android_runtime_release.sh | 15 - infra/scripts/common.sh | 32 +- infra/scripts/compiler_modules.sh | 8 +- .../scripts/docker_build_cross_aarch64_runtime.sh | 50 - infra/scripts/docker_build_cross_arm_runtime.sh | 50 - .../docker_build_cross_arm_runtime_release.sh | 51 - infra/scripts/docker_build_cross_coverage.sh | 60 - infra/scripts/docker_build_test_x64.sh | 52 - infra/scripts/docker_build_tizen_cross.sh | 52 - infra/scripts/docker_build_tizen_gbs.sh | 31 - infra/scripts/docker_collect_nnpkg_resources.sh | 2 + infra/scripts/docker_coverage_report.sh | 34 - infra/scripts/test_arm_nnpkg.sh | 3 - infra/scripts/test_coverage.sh | 3 - infra/scripts/test_ubuntu_npud.sh | 59 + infra/scripts/test_ubuntu_runtime.sh | 50 +- infra/scripts/test_ubuntu_runtime_mixed.sh | 6 +- infra/scripts/tizen_xu4_test.sh | 17 +- onert-micro/CMakeLists.txt | 217 +++ {compiler/luci-micro => onert-micro}/README.md | 0 onert-micro/eval-driver/CMakeLists.txt | 13 + onert-micro/eval-driver/Driver.cpp | 153 ++ onert-micro/helpers/GenerateKernelsListHelper.cpp | 239 +++ onert-micro/luci-interpreter/CMakeLists.txt | 29 + .../luci-interpreter/README.md | 0 .../include/luci_interpreter/Interpreter.h | 67 + .../luci_interpreter/InterpreterConfigure.h | 80 + .../include/luci_interpreter/core/DataType.h | 180 +++ .../include/luci_interpreter/core/ParamsType.h | 57 + .../include/luci_interpreter/core/Tensor.h | 138 ++ .../core/reader/CircleMicroReader.h | 190 +++ .../core/reader/CircleMicroReaderHelper.h | 49 + .../pal/cmsisnn/KernelsToBuild.lst | 61 + .../luci-interpreter/pal/cmsisnn}/PALArgMax.h | 0 .../pal/cmsisnn/PALAveragePool2d.h | 0 .../pal/cmsisnn/PALBatchToSpaceND.h | 37 + .../luci-interpreter/pal/cmsisnn/PALConv2d.h | 199 +++ .../pal/cmsisnn}/PALDepthToSpace.h | 0 .../pal/cmsisnn/PALDepthwiseConv2d.h | 192 +++ .../luci-interpreter/pal/cmsisnn/PALDequantize.h | 44 + .../luci-interpreter/pal/cmsisnn}/PALElu.h | 0 onert-micro/luci-interpreter/pal/cmsisnn/PALFill.h | 22 + .../pal/cmsisnn/PALFullyConnected.h | 0 .../luci-interpreter/pal/cmsisnn}/PALL2Normalize.h | 0 .../luci-interpreter/pal/cmsisnn}/PALL2Pool2D.h | 0 .../luci-interpreter/pal/cmsisnn}/PALLeakyRelu.h | 0 .../luci-interpreter/pal/cmsisnn}/PALMul.h | 0 .../luci-interpreter/pal/cmsisnn}/PALNeg.h | 0 .../luci-interpreter/pal/cmsisnn/PALQuantize.h | 44 + .../pal/cmsisnn}/PALResizeBilinear.h | 0 .../pal/cmsisnn}/PALResizeNearestNeighbor.h | 0 onert-micro/luci-interpreter/pal/cmsisnn/PALSVDF.h | 190 +++ .../luci-interpreter/pal/cmsisnn/PALSoftmax.h | 0 .../pal/cmsisnn}/PALSpaceToBatchND.h | 0 .../pal/cmsisnn}/PALSpaceToDepth.h | 0 .../luci-interpreter/pal/cmsisnn}/PALSub.h | 0 .../pal/cmsisnn/PALUnidirectionalSequenceLSTM.h | 243 +++ .../pal/cmsisnn/PALreference_ops.h | 1556 +++++++++++++++++++ onert-micro/luci-interpreter/pal/cmsisnn/pal.cmake | 83 + .../luci-interpreter/pal/linux/KernelsToBuild.lst | 77 + .../luci-interpreter/pal/linux/PALArgMax.h | 0 .../luci-interpreter/pal/linux}/PALAveragePool2d.h | 0 .../luci-interpreter/pal/linux/PALBatchMatMul.h | 0 .../luci-interpreter/pal/linux/PALBatchToSpaceND.h | 0 .../luci-interpreter/pal/linux/PALConv2d.h | 0 .../luci-interpreter/pal/linux/PALDepthToSpace.h | 0 .../pal/linux}/PALDepthwiseConv2d.h | 0 .../luci-interpreter/pal/linux/PALDequantize.h | 0 .../luci-interpreter/pal/linux/PALElu.h | 0 onert-micro/luci-interpreter/pal/linux/PALFill.h | 22 + .../luci-interpreter/pal/linux/PALFullyConnected.h | 0 .../luci-interpreter/pal/linux/PALGather.h | 0 .../luci-interpreter/pal/linux/PALL2Normalize.h | 0 .../luci-interpreter/pal/linux/PALL2Pool2D.h | 0 .../luci-interpreter/pal/linux/PALLeakyRelu.h | 0 .../pal/linux/PALLocalResponseNormalization.h | 0 .../luci-interpreter/pal/linux/PALLogSoftmax.h | 0 .../luci-interpreter/pal/linux/PALMul.h | 0 .../luci-interpreter/pal/linux/PALNeg.h | 0 .../luci-interpreter/pal/linux/PALQuantize.h | 0 .../luci-interpreter/pal/linux/PALRelu.h | 0 .../luci-interpreter/pal/linux/PALRelu6.h | 0 .../luci-interpreter/pal/linux/PALResizeBilinear.h | 0 .../pal/linux/PALResizeNearestNeighbor.h | 0 onert-micro/luci-interpreter/pal/linux/PALSVDF.h | 90 ++ .../luci-interpreter/pal/linux/PALSlice.h | 0 .../luci-interpreter/pal/linux/PALSoftmax.h | 0 .../luci-interpreter/pal/linux/PALSpaceToBatchND.h | 0 .../luci-interpreter/pal/linux/PALSpaceToDepth.h | 0 .../luci-interpreter/pal/linux/PALSplit.h | 0 .../luci-interpreter/pal/linux/PALSub.h | 0 onert-micro/luci-interpreter/pal/linux/pal.cmake | 82 + .../luci-interpreter/pal/mcu/KernelsToBuild.lst | 9 + .../pal/mcu/PALApplyActivationToVector.h | 75 + .../luci-interpreter/pal/mcu}/PALArgMax.h | 0 .../luci-interpreter/pal/mcu}/PALAveragePool2d.h | 0 .../luci-interpreter/pal/mcu/PALBatchToSpaceND.h | 37 + onert-micro/luci-interpreter/pal/mcu/PALConv2d.h | 85 ++ .../luci-interpreter/pal/mcu}/PALDepthToSpace.h | 0 .../luci-interpreter/pal/mcu}/PALDepthwiseConv2d.h | 0 .../luci-interpreter/pal/mcu/PALDequantize.h | 44 + .../luci-interpreter/pal/mcu}/PALElu.h | 0 onert-micro/luci-interpreter/pal/mcu/PALFill.h | 22 + .../luci-interpreter/pal/mcu/PALFullyConnected.h | 0 .../luci-interpreter/pal/mcu}/PALL2Normalize.h | 0 .../luci-interpreter/pal/mcu}/PALL2Pool2D.h | 0 .../luci-interpreter/pal/mcu}/PALLeakyRelu.h | 0 .../luci-interpreter/pal/mcu}/PALMul.h | 0 .../luci-interpreter/pal/mcu}/PALNeg.h | 0 onert-micro/luci-interpreter/pal/mcu/PALQuantize.h | 44 + .../luci-interpreter/pal/mcu/PALResizeBilinear.h | 37 + .../pal/mcu/PALResizeNearestNeighbor.h | 38 + onert-micro/luci-interpreter/pal/mcu/PALSVDF.h | 258 ++++ onert-micro/luci-interpreter/pal/mcu/PALSoftmax.h | 58 + .../luci-interpreter/pal/mcu/PALSpaceToBatchND.h | 38 + .../luci-interpreter/pal/mcu}/PALSpaceToDepth.h | 0 .../luci-interpreter/pal/mcu}/PALSub.h | 0 .../pal/mcu/PALUnidirectionalSequenceLSTM.h | 678 +++++++++ .../luci-interpreter/pal/mcu/PALreference_ops.h | 1556 +++++++++++++++++++ onert-micro/luci-interpreter/pal/mcu/pal.cmake | 56 + .../luci-interpreter/requires.cmake | 0 onert-micro/luci-interpreter/src/CMakeLists.txt | 45 + onert-micro/luci-interpreter/src/Interpreter.cpp | 108 ++ .../luci-interpreter/src/core/CMakeLists.txt | 20 + .../luci-interpreter/src/core/RuntimeGraph.cpp | 344 +++++ .../luci-interpreter/src/core/RuntimeGraph.h | 113 ++ .../luci-interpreter/src/core/RuntimeModule.h | 63 + .../src/core/reader/CMakeLists.txt | 12 + .../src/core/reader/CircleMicroReader.cpp | 202 +++ .../src/core/reader/CircleMicroReaderHelper.cpp | 52 + onert-micro/luci-interpreter/src/kernels/Add.cpp | 219 +++ .../luci-interpreter/src/kernels/Add.h | 0 .../luci-interpreter/src/kernels/Add.test.cpp | 0 .../luci-interpreter/src/kernels/ArgMax.cpp | 140 ++ .../luci-interpreter/src/kernels/ArgMax.h | 0 .../luci-interpreter/src/kernels/ArgMax.test.cpp | 0 .../luci-interpreter/src/kernels/AveragePool2D.cpp | 193 +++ .../luci-interpreter/src/kernels/AveragePool2D.h | 0 .../src/kernels/AveragePool2D.test.cpp | 0 .../luci-interpreter/src/kernels/BatchMatMul.cpp | 186 +++ .../luci-interpreter/src/kernels/BatchMatMul.h | 0 .../src/kernels/BatchMatMul.test.cpp | 0 .../src/kernels/BatchToSpaceND.cpp | 104 ++ .../luci-interpreter/src/kernels/BatchToSpaceND.h | 0 .../src/kernels/BatchToSpaceND.test.cpp | 0 .../luci-interpreter/src/kernels/BinaryOpCommon.h | 0 .../luci-interpreter/src/kernels/Builders.h | 44 + .../luci-interpreter/src/kernels/CMakeLists.txt | 40 + onert-micro/luci-interpreter/src/kernels/Cast.cpp | 144 ++ .../luci-interpreter/src/kernels/Cast.h | 0 .../luci-interpreter/src/kernels/Cast.test.cpp | 0 .../luci-interpreter/src/kernels/Concatenation.cpp | 180 +++ .../src/kernels/Concatenation.test.cpp | 270 ++++ .../luci-interpreter/src/kernels/Conv2D.cpp | 524 +++++++ .../luci-interpreter/src/kernels/Conv2D.test.cpp | 710 +++++++++ .../luci-interpreter/src/kernels/DepthToSpace.cpp | 81 + .../luci-interpreter/src/kernels/DepthToSpace.h | 0 .../src/kernels/DepthToSpace.test.cpp | 0 .../src/kernels/DepthwiseConv2D.cpp | 450 ++++++ .../luci-interpreter/src/kernels/DepthwiseConv2D.h | 0 .../src/kernels/DepthwiseConv2D.test.cpp | 0 .../luci-interpreter/src/kernels/Dequantize.cpp | 80 + .../luci-interpreter/src/kernels/Dequantize.h | 0 .../src/kernels/Dequantize.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Div.cpp | 153 ++ .../luci-interpreter/src/kernels/Div.h | 0 .../luci-interpreter/src/kernels/Div.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Elu.cpp | 51 + .../luci-interpreter/src/kernels/Elu.h | 0 .../luci-interpreter/src/kernels/Elu.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Equal.cpp | 141 ++ .../luci-interpreter/src/kernels/Equal.h | 0 .../luci-interpreter/src/kernels/Equal.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Exp.cpp | 57 + .../luci-interpreter/src/kernels/Exp.h | 0 .../luci-interpreter/src/kernels/Exp.test.cpp | 0 .../luci-interpreter/src/kernels/ExpandDims.cpp | 96 ++ .../src/kernels/ExpandDims.test.cpp | 118 ++ onert-micro/luci-interpreter/src/kernels/Fill.cpp | 116 ++ .../luci-interpreter/src/kernels/Fill.h | 0 .../luci-interpreter/src/kernels/Fill.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Floor.cpp | 58 + .../luci-interpreter/src/kernels/Floor.h | 0 .../luci-interpreter/src/kernels/Floor.test.cpp | 0 .../luci-interpreter/src/kernels/FloorDiv.cpp | 86 ++ .../luci-interpreter/src/kernels/FloorDiv.h | 0 .../luci-interpreter/src/kernels/FloorDiv.test.cpp | 0 .../src/kernels/FullyConnected.cpp | 264 ++++ .../src/kernels/FullyConnected.test.cpp | 262 ++++ .../luci-interpreter/src/kernels/Gather.cpp | 139 ++ .../luci-interpreter/src/kernels/Gather.h | 0 .../luci-interpreter/src/kernels/Gather.test.cpp | 0 .../luci-interpreter/src/kernels/Greater.cpp | 141 ++ .../luci-interpreter/src/kernels/Greater.h | 0 .../luci-interpreter/src/kernels/Greater.test.cpp | 0 .../luci-interpreter/src/kernels/GreaterEqual.cpp | 144 ++ .../luci-interpreter/src/kernels/GreaterEqual.h | 0 .../src/kernels/GreaterEqual.test.cpp | 0 .../luci-interpreter/src/kernels/If.cpp | 0 .../luci-interpreter/src/kernels/If.h | 0 .../luci-interpreter/src/kernels/If.test.cpp | 0 .../luci-interpreter/src/kernels/InstanceNorm.cpp | 122 ++ .../luci-interpreter/src/kernels/InstanceNorm.h | 0 .../src/kernels/InstanceNorm.test.cpp | 0 .../luci-interpreter/src/kernels/KernelBuilder.cpp | 75 + .../luci-interpreter/src/kernels/KernelBuilder.h | 89 ++ .../luci-interpreter/src/kernels/L2Normalize.cpp | 74 + .../luci-interpreter/src/kernels/L2Normalize.h | 0 .../src/kernels/L2Normalize.test.cpp | 0 .../luci-interpreter/src/kernels/L2Pool2D.cpp | 87 ++ .../luci-interpreter/src/kernels/L2Pool2D.h | 0 .../luci-interpreter/src/kernels/L2Pool2D.test.cpp | 0 .../luci-interpreter/src/kernels/LeakyRelu.cpp | 89 ++ .../luci-interpreter/src/kernels/LeakyRelu.h | 0 .../src/kernels/LeakyRelu.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Less.cpp | 141 ++ .../luci-interpreter/src/kernels/Less.h | 0 .../luci-interpreter/src/kernels/Less.test.cpp | 0 .../luci-interpreter/src/kernels/LessEqual.cpp | 141 ++ .../luci-interpreter/src/kernels/LessEqual.h | 0 .../src/kernels/LessEqual.test.cpp | 0 .../src/kernels/LocalResponseNormalization.cpp | 64 + .../src/kernels/LocalResponseNormalization.h | 0 .../kernels/LocalResponseNormalization.test.cpp | 0 .../luci-interpreter/src/kernels/LogSoftmax.cpp | 93 ++ .../luci-interpreter/src/kernels/LogSoftmax.h | 0 .../src/kernels/LogSoftmax.test.cpp | 0 .../luci-interpreter/src/kernels/LogicalAnd.cpp | 63 + .../luci-interpreter/src/kernels/LogicalAnd.h | 0 .../src/kernels/LogicalAnd.test.cpp | 0 .../luci-interpreter/src/kernels/LogicalNot.cpp | 61 + .../luci-interpreter/src/kernels/LogicalNot.h | 0 .../src/kernels/LogicalNot.test.cpp | 0 .../luci-interpreter/src/kernels/LogicalOr.cpp | 50 + .../luci-interpreter/src/kernels/LogicalOr.h | 0 .../src/kernels/LogicalOr.test.cpp | 0 .../luci-interpreter/src/kernels/Logistic.cpp | 130 ++ .../luci-interpreter/src/kernels/Logistic.test.cpp | 150 ++ .../luci-interpreter/src/kernels/MaxPool2D.cpp | 220 +++ .../src/kernels/MaxPool2D.test.cpp | 141 ++ .../luci-interpreter/src/kernels/Maximum.cpp | 66 + .../luci-interpreter/src/kernels/Maximum.h | 0 .../luci-interpreter/src/kernels/Maximum.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Mean.cpp | 344 +++++ .../luci-interpreter/src/kernels/Mean.h | 0 .../luci-interpreter/src/kernels/Mean.test.cpp | 0 .../luci-interpreter/src/kernels/Minimum.cpp | 66 + .../luci-interpreter/src/kernels/Minimum.h | 0 .../luci-interpreter/src/kernels/Minimum.test.cpp | 0 .../luci-interpreter/src/kernels/MirrorPad.cpp | 172 +++ .../luci-interpreter/src/kernels/MirrorPad.h | 0 .../src/kernels/MirrorPad.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Mul.cpp | 148 ++ .../luci-interpreter/src/kernels/Mul.h | 0 .../luci-interpreter/src/kernels/Mul.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Neg.cpp | 56 + .../luci-interpreter/src/kernels/Neg.h | 0 .../luci-interpreter/src/kernels/Neg.test.cpp | 0 .../luci-interpreter/src/kernels/NotEqual.cpp | 141 ++ .../luci-interpreter/src/kernels/NotEqual.h | 0 .../luci-interpreter/src/kernels/NotEqual.test.cpp | 0 .../luci-interpreter/src/kernels/OneHot.cpp | 136 ++ .../luci-interpreter/src/kernels/OneHot.h | 0 .../luci-interpreter/src/kernels/OneHot.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/PRelu.cpp | 210 +++ .../luci-interpreter/src/kernels/PRelu.h | 0 .../luci-interpreter/src/kernels/PRelu.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Pack.cpp | 140 ++ .../luci-interpreter/src/kernels/Pack.h | 0 .../luci-interpreter/src/kernels/Pack.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Pad.cpp | 114 ++ .../luci-interpreter/src/kernels/Pad.h | 0 .../luci-interpreter/src/kernels/Pad.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/PadV2.cpp | 108 ++ .../luci-interpreter/src/kernels/PadV2.h | 0 .../luci-interpreter/src/kernels/PadV2.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Pow.cpp | 77 + .../luci-interpreter/src/kernels/Pow.h | 0 .../luci-interpreter/src/kernels/Pow.test.cpp | 0 .../luci-interpreter/src/kernels/Quantize.cpp | 160 ++ .../luci-interpreter/src/kernels/Quantize.h | 0 .../luci-interpreter/src/kernels/Quantize.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Relu.cpp | 113 ++ .../luci-interpreter/src/kernels/Relu.h | 0 .../luci-interpreter/src/kernels/Relu.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Relu6.cpp | 87 ++ .../luci-interpreter/src/kernels/Relu6.h | 0 .../luci-interpreter/src/kernels/Relu6.test.cpp | 0 .../luci-interpreter/src/kernels/Reshape.cpp | 60 + .../luci-interpreter/src/kernels/Reshape.test.cpp | 83 + .../src/kernels/ResizeBilinear.cpp | 75 + .../luci-interpreter/src/kernels/ResizeBilinear.h | 0 .../src/kernels/ResizeBilinear.test.cpp | 0 .../src/kernels/ResizeNearestNeighbor.cpp | 75 + .../src/kernels/ResizeNearestNeighbor.h | 0 .../src/kernels/ResizeNearestNeighbor.test.cpp | 0 .../luci-interpreter/src/kernels/ReverseV2.cpp | 82 + .../luci-interpreter/src/kernels/ReverseV2.h | 0 .../src/kernels/ReverseV2.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Rsqrt.cpp | 66 + .../luci-interpreter/src/kernels/Rsqrt.h | 0 .../luci-interpreter/src/kernels/Rsqrt.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/SVDF.cpp | 242 +++ .../luci-interpreter/src/kernels/SVDF.h | 0 .../luci-interpreter/src/kernels/SVDF.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Shape.cpp | 70 + .../luci-interpreter/src/kernels/Shape.h | 0 .../luci-interpreter/src/kernels/Shape.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Slice.cpp | 153 ++ .../luci-interpreter/src/kernels/Slice.h | 0 .../luci-interpreter/src/kernels/Slice.test.cpp | 0 .../luci-interpreter/src/kernels/Softmax.cpp | 132 ++ .../luci-interpreter/src/kernels/Softmax.test.cpp | 119 ++ .../src/kernels/SpaceToBatchND.cpp | 102 ++ .../luci-interpreter/src/kernels/SpaceToBatchND.h | 0 .../src/kernels/SpaceToBatchND.test.cpp | 0 .../luci-interpreter/src/kernels/SpaceToDepth.cpp | 79 + .../luci-interpreter/src/kernels/SpaceToDepth.h | 0 .../src/kernels/SpaceToDepth.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Split.cpp | 81 + .../luci-interpreter/src/kernels/Split.h | 0 .../luci-interpreter/src/kernels/Split.test.cpp | 0 .../luci-interpreter/src/kernels/SplitV.cpp | 112 ++ .../luci-interpreter/src/kernels/SplitV.h | 0 .../luci-interpreter/src/kernels/SplitV.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Sqrt.cpp | 66 + .../luci-interpreter/src/kernels/Sqrt.h | 0 .../luci-interpreter/src/kernels/Sqrt.test.cpp | 0 .../luci-interpreter/src/kernels/Square.cpp | 66 + .../luci-interpreter/src/kernels/Square.h | 0 .../luci-interpreter/src/kernels/Square.test.cpp | 0 .../src/kernels/SquaredDifference.cpp | 65 + .../src/kernels/SquaredDifference.h | 0 .../src/kernels/SquaredDifference.test.cpp | 0 .../luci-interpreter/src/kernels/Squeeze.cpp | 85 ++ .../luci-interpreter/src/kernels/Squeeze.h | 0 .../luci-interpreter/src/kernels/Squeeze.test.cpp | 0 .../luci-interpreter/src/kernels/StridedSlice.cpp | 149 ++ .../luci-interpreter/src/kernels/StridedSlice.h | 0 .../src/kernels/StridedSlice.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Sub.cpp | 163 ++ .../luci-interpreter/src/kernels/Sub.h | 0 .../luci-interpreter/src/kernels/Sub.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Tanh.cpp | 94 ++ .../luci-interpreter/src/kernels/Tanh.h | 0 .../luci-interpreter/src/kernels/Tanh.test.cpp | 0 .../luci-interpreter/src/kernels/TestUtils.cpp | 126 ++ .../luci-interpreter/src/kernels/TestUtils.h | 0 .../luci-interpreter/src/kernels/Transpose.cpp | 83 + .../luci-interpreter/src/kernels/Transpose.h | 0 .../src/kernels/Transpose.test.cpp | 0 .../luci-interpreter/src/kernels/TransposeConv.cpp | 351 +++++ .../luci-interpreter/src/kernels/TransposeConv.h | 0 .../src/kernels/TransposeConv.test.cpp | 0 .../src/kernels/UnidirectionalSequenceLSTM.cpp | 441 ++++++ .../src/kernels/UnidirectionalSequenceLSTM.h | 249 +++ .../kernels/UnidirectionalSequenceLSTM.test.cpp | 565 +++++++ .../luci-interpreter/src/kernels/Unpack.cpp | 83 + .../luci-interpreter/src/kernels/Unpack.h | 0 .../luci-interpreter/src/kernels/Unpack.test.cpp | 0 onert-micro/luci-interpreter/src/kernels/Utils.cpp | 235 +++ onert-micro/luci-interpreter/src/kernels/Utils.h | 305 ++++ .../luci-interpreter/src/kernels/While.cpp | 0 .../luci-interpreter/src/kernels/While.h | 0 .../luci-interpreter/src/kernels/While.test.cpp | 0 .../luci-interpreter/src/loader/CMakeLists.txt | 15 + .../luci-interpreter/src/loader/GraphLoader.cpp | 90 ++ .../luci-interpreter/src/loader/GraphLoader.h | 36 + .../luci-interpreter/src/loader/ModuleLoader.cpp | 62 + .../luci-interpreter/src/loader/ModuleLoader.h | 37 + .../luci-interpreter/src/loader/nodes/Add.cpp | 44 + .../luci-interpreter/src/loader/nodes/ArgMax.cpp | 44 + .../src/loader/nodes/AveragePool2D.cpp | 57 + .../src/loader/nodes/BatchMatMul.cpp | 55 + .../src/loader/nodes/BatchToSpaceND.cpp | 39 + .../luci-interpreter/src/loader/nodes/Cast.cpp | 36 + .../src/loader/nodes/Concatenation.cpp | 48 + .../luci-interpreter/src/loader/nodes/Conv2D.cpp | 58 + .../src/loader/nodes/DepthToSpace.cpp | 44 + .../src/loader/nodes/DepthwiseConv2D.cpp | 60 + .../src/loader/nodes/Dequantize.cpp | 35 + .../luci-interpreter/src/loader/nodes/Div.cpp | 43 + .../luci-interpreter/src/loader/nodes/Elu.cpp | 35 + .../luci-interpreter/src/loader/nodes/Equal.cpp | 38 + .../luci-interpreter/src/loader/nodes/Exp.cpp | 36 + .../src/loader/nodes/ExpandDims.cpp | 37 + .../luci-interpreter/src/loader/nodes/Fill.cpp | 36 + .../luci-interpreter/src/loader/nodes/Floor.cpp | 36 + .../luci-interpreter/src/loader/nodes/FloorDiv.cpp | 36 + .../src/loader/nodes/FullyConnected.cpp | 47 + .../luci-interpreter/src/loader/nodes/Gather.cpp | 45 + .../luci-interpreter/src/loader/nodes/Greater.cpp | 36 + .../src/loader/nodes/GreaterEqual.cpp | 37 + .../luci-interpreter/src/loader/nodes/If.cpp | 32 + .../src/loader/nodes/InstanceNorm.cpp | 48 + .../src/loader/nodes/L2Normalize.cpp | 44 + .../luci-interpreter/src/loader/nodes/L2Pool2D.cpp | 48 + .../src/loader/nodes/LeakyRelu.cpp | 44 + .../luci-interpreter/src/loader/nodes/Less.cpp | 37 + .../src/loader/nodes/LessEqual.cpp | 38 + .../loader/nodes/LocalResponseNormalization.cpp | 46 + .../src/loader/nodes/LogSoftmax.cpp | 36 + .../src/loader/nodes/LogicalAnd.cpp | 38 + .../src/loader/nodes/LogicalNot.cpp | 36 + .../src/loader/nodes/LogicalOr.cpp | 38 + .../luci-interpreter/src/loader/nodes/Logistic.cpp | 36 + .../src/loader/nodes/MaxPool2D.cpp | 49 + .../luci-interpreter/src/loader/nodes/Maximum.cpp | 37 + .../luci-interpreter/src/loader/nodes/Mean.cpp | 57 + .../luci-interpreter/src/loader/nodes/Minimum.cpp | 37 + .../src/loader/nodes/MirrorPad.cpp | 45 + .../luci-interpreter/src/loader/nodes/Mul.cpp | 44 + .../luci-interpreter/src/loader/nodes/Neg.cpp | 35 + .../luci-interpreter/src/loader/nodes/NotEqual.cpp | 37 + .../luci-interpreter/src/loader/nodes/OneHot.cpp | 46 + .../luci-interpreter/src/loader/nodes/PRelu.cpp | 37 + .../luci-interpreter/src/loader/nodes/Pack.cpp | 46 + .../luci-interpreter/src/loader/nodes/Pad.cpp | 37 + .../luci-interpreter/src/loader/nodes/PadV2.cpp | 38 + .../luci-interpreter/src/loader/nodes/Pow.cpp | 37 + .../luci-interpreter/src/loader/nodes/Quantize.cpp | 35 + .../luci-interpreter/src/loader/nodes/Relu.cpp | 35 + .../luci-interpreter/src/loader/nodes/Relu6.cpp | 35 + .../luci-interpreter/src/loader/nodes/Reshape.cpp | 38 + .../src/loader/nodes/ResizeBilinear.cpp | 46 + .../src/loader/nodes/ResizeNearestNeighbor.cpp | 50 + .../src/loader/nodes/ReverseV2.cpp | 38 + .../luci-interpreter/src/loader/nodes/Rsqrt.cpp | 35 + .../luci-interpreter/src/loader/nodes/SVDF.cpp | 89 ++ .../luci-interpreter/src/loader/nodes/Shape.cpp | 42 + .../luci-interpreter/src/loader/nodes/Slice.cpp | 38 + .../luci-interpreter/src/loader/nodes/Softmax.cpp | 42 + .../src/loader/nodes/SpaceToBatchND.cpp | 39 + .../src/loader/nodes/SpaceToDepth.cpp | 43 + .../luci-interpreter/src/loader/nodes/Split.cpp | 43 + .../luci-interpreter/src/loader/nodes/SplitV.cpp | 44 + .../luci-interpreter/src/loader/nodes/Sqrt.cpp | 35 + .../luci-interpreter/src/loader/nodes/Square.cpp | 35 + .../src/loader/nodes/SquaredDifference.cpp | 38 + .../luci-interpreter/src/loader/nodes/Squeeze.cpp | 42 + .../src/loader/nodes/StridedSlice.cpp | 51 + .../luci-interpreter/src/loader/nodes/Sub.cpp | 44 + .../luci-interpreter/src/loader/nodes/Tanh.cpp | 35 + .../src/loader/nodes/Transpose.cpp | 38 + .../src/loader/nodes/TransposeConv.cpp | 57 + .../loader/nodes/UnidirectionalSequenceLSTM.cpp | 145 ++ .../luci-interpreter/src/loader/nodes/Unpack.cpp | 49 + .../luci-interpreter/src/loader/nodes/While.cpp | 32 + .../src/memory_managers/BuddyMemoryManager.cpp | 100 ++ .../src/memory_managers/BuddyMemoryManager.h | 148 ++ .../memory_managers/BuddyMemoryManager.test.cpp | 73 + .../src/memory_managers/CMakeLists.txt | 21 + .../src/memory_managers/SimpleMemoryManager.cpp | 44 + .../src/memory_managers/SimpleMemoryManager.h | 39 + .../src/memory_managers/StaticMemoryManager.cpp | 83 + .../src/memory_managers/StaticMemoryManager.h | 90 ++ .../src/memory_managers/TestMemoryManager.cpp | 51 + .../src/memory_managers/TestMemoryManager.h | 53 + onert-micro/requires.cmake | 1 + onert-micro/standalone/CMakeLists.txt | 10 + onert-micro/tests/mbed-os/CMakeLists.txt | 194 +++ onert-micro/tests/mbed-os/main.cpp | 28 + onert-micro/tests/mbed-os/mbed-sources.cmake | 1589 ++++++++++++++++++++ onert-micro/tests/mbed-os/mbed_config.h | 488 ++++++ onert-micro/tests/mbed-os/startup_stm32h743xx.S | 675 +++++++++ packaging/EGL_HEADERS.tar.gz | Bin 0 -> 74033 bytes packaging/FLATBUFFERS-1.12.tar.gz | Bin 1171889 -> 0 bytes packaging/FLATBUFFERS-2.0.tar.gz | Bin 0 -> 345691 bytes packaging/OPENGL_HEADERS.tar.gz | Bin 0 -> 313232 bytes packaging/TENSORFLOW-2.3.0-EIGEN.tar.gz | Bin 2664733 -> 0 bytes packaging/TENSORFLOW-2.8.0-EIGEN.tar.gz | Bin 0 -> 1777666 bytes packaging/TENSORFLOW-2.8.0-GEMMLOWP.tar.gz | Bin 0 -> 847663 bytes packaging/TENSORFLOW-2.8.0.tar.gz | Bin 0 -> 13387359 bytes packaging/TENSORFLOW_GPU.tar.gz | Bin 579811 -> 0 bytes packaging/VULKAN.tar.gz | Bin 0 -> 870795 bytes packaging/XNNPACK.tar.gz | Bin 2241657 -> 1583418 bytes packaging/nnfw.spec | 106 +- .../examples/PReLUwConv1d/__init__.py | 19 + .../examples/PReLUwConv2d/__init__.py | 19 + res/PyTorchExamples/ptem.py | 12 +- .../FullyConnected_008/test.recipe | 29 + .../FullyConnected_008/test.reverse | 0 .../FullyConnected_008/test.rule | 8 + .../FullyConnected_009/test.recipe | 43 + .../FullyConnected_009/test.reverse | 0 .../Net_Conv_PReluGraph_000/test.recipe | 145 ++ .../Net_Conv_PReluGraph_000/test.rule | 10 + .../Net_Duplicate_Weights_000/test.recipe | 77 + .../Net_Duplicate_Weights_000/test.rule | 8 + .../Net_FullyConnected_Add_000/test.recipe | 66 + .../Net_FullyConnected_Add_000/test.rule | 6 + .../Net_TConv_BN_005/test.recipe | 126 ++ .../Net_TConv_BN_005/test.rule | 8 + .../Net_Transpose_Abs_000/test.recipe | 34 + .../Net_Transpose_Abs_000/test.reverse | 0 .../Net_Transpose_Add_000/test.recipe | 48 + .../Net_Transpose_Add_000/test.reverse | 0 .../Quant_Conv_005/test.recipe | 44 + res/TensorFlowLiteRecipes/Quant_Conv_005/test.rule | 8 + .../Quant_Conv_006/test.recipe | 44 + res/TensorFlowLiteRecipes/Quant_Conv_006/test.rule | 8 + .../Quant_DepthToSpace_000/test.recipe | 22 + .../Quant_DepthToSpace_000/test.rule | 12 + .../Quant_SpaceToDepth_000/test.recipe | 22 + .../Quant_SpaceToDepth_000/test.rule | 12 + .../UnidirectionalSequenceLSTM_002/test.recipe | 236 +++ .../UnidirectionalSequenceLSTM_002/test.reverse | 0 .../UnidirectionalSequenceLSTM_003/test.recipe | 193 +++ .../UnidirectionalSequenceLSTM_003/test.rule | 7 + .../UnidirectionalSequenceLSTM_004/test.recipe | 425 ++++++ .../UnidirectionalSequenceLSTM_004/test.rule | 6 + res/TensorFlowPythonExamples/.gitignore | 1 + res/TensorFlowPythonExamples/README.md | 14 +- .../examples/GRU_unroll/__init__.py | 8 + .../examples/LSTM_batsize/__init__.py | 10 + .../examples/LSTM_retseq/__init__.py | 10 + .../examples/LSTM_unroll/__init__.py | 10 + .../examples/RNN_GRUCell_unroll/__init__.py | 9 + .../examples/RNN_LSTMCell_unroll/__init__.py | 11 + .../examples/SimpleRNN_unroll/__init__.py | 10 + res/TensorFlowPythonExamples/tfpem.py | 43 +- runtime/contrib/android/api/build.gradle | 2 +- runtime/contrib/android_tflite/CMakeLists.txt | 31 - runtime/contrib/android_tflite/builtin_ops_jni.cc | 30 - .../contrib/labs/opencl_test/src/opencl_test.cc | 2 +- runtime/libs/benchmark/include/benchmark/Result.h | 2 +- runtime/libs/benchmark/src/MemoryPoller.cpp | 2 +- runtime/libs/benchmark/src/Phases.cpp | 8 +- runtime/libs/misc/CMakeLists.txt | 2 +- runtime/libs/ndarray/CMakeLists.txt | 2 +- runtime/libs/profiling/src/profiling/time.cpp | 8 +- runtime/libs/tflite/CMakeLists.txt | 4 +- .../tflite/include/tflite/CopyInputInitializer.h | 47 - runtime/libs/tflite/include/tflite/Diff.h | 10 +- runtime/libs/tflite/include/tflite/FeatureView.h | 108 -- .../tflite/include/tflite/InterpreterSession.h | 12 +- runtime/libs/tflite/include/tflite/NNAPISession.h | 98 -- runtime/libs/tflite/include/tflite/OutputIndex.h | 60 - .../libs/tflite/include/tflite/OutputResetter.h | 44 - runtime/libs/tflite/include/tflite/Quantization.h | 44 - .../tflite/include/tflite/RandomInputInitializer.h | 7 +- .../libs/tflite/include/tflite/RandomTestRunner.h | 103 -- runtime/libs/tflite/include/tflite/Session.h | 4 +- runtime/libs/tflite/include/tflite/TensorLogger.h | 167 -- runtime/libs/tflite/include/tflite/TensorUtils.h | 54 - runtime/libs/tflite/include/tflite/TensorView.h | 14 +- .../libs/tflite/include/tflite/interp/Builder.h | 53 - .../include/tflite/interp/FlatBufferBuilder.h | 64 - .../tflite/include/tflite/interp/FunctionBuilder.h | 67 - runtime/libs/tflite/src/CopyInputInitializer.cpp | 68 - runtime/libs/tflite/src/Diff.cpp | 60 +- runtime/libs/tflite/src/FeatureView.cpp | 70 - runtime/libs/tflite/src/OutputResetter.cpp | 64 - runtime/libs/tflite/src/Quantization.cpp | 22 - runtime/libs/tflite/src/RandomInputInitializer.cpp | 42 +- runtime/libs/tflite/src/RandomTestRunner.cpp | 132 -- .../libs/tflite/src/interp/FlatBufferBuilder.cpp | 40 - runtime/libs/tflite/src/interp/FunctionBuilder.cpp | 34 - runtime/onert/api/include/nnfw_version.h | 2 +- runtime/onert/api/src/nnfw_api_internal.cc | 370 ++--- runtime/onert/api/src/nnfw_api_internal.h | 9 +- runtime/onert/backend/acl_cl/KernelGenerator.cc | 2 +- .../onert/backend/acl_common/AclTensorBuilder.h | 20 +- .../cl_common/include/cl_common/BackendContext.h | 2 +- runtime/onert/backend/cpu/BackendContext.cc | 2 +- runtime/onert/backend/cpu/KernelGenerator.cc | 18 +- runtime/onert/backend/gpu_cl/Backend.h | 16 +- runtime/onert/backend/gpu_cl/BackendContext.cc | 26 + runtime/onert/backend/gpu_cl/BackendContext.h | 2 + runtime/onert/backend/gpu_cl/CMakeLists.txt | 43 +- runtime/onert/backend/gpu_cl/ClFunction.h | 32 +- runtime/onert/backend/gpu_cl/Config.h | 3 - runtime/onert/backend/gpu_cl/KernelGenerator.cc | 612 +++++--- runtime/onert/backend/gpu_cl/KernelGenerator.h | 15 +- runtime/onert/backend/gpu_cl/MemoryManager.h | 115 +- runtime/onert/backend/gpu_cl/TensorBuilder.cc | 24 +- runtime/onert/backend/gpu_cl/TensorBuilder.h | 7 +- runtime/onert/backend/gpu_cl/TensorBuilderHelper.h | 44 - runtime/onert/backend/gpu_cl/TensorManager.cc | 38 +- runtime/onert/backend/gpu_cl/TensorManager.h | 22 +- runtime/onert/backend/gpu_cl/TensorRegistry.h | 2 +- runtime/onert/backend/gpu_cl/Utils.h | 155 ++ .../onert/backend/gpu_cl/ex/InferenceContextEx.h | 108 -- runtime/onert/backend/gpu_cl/operand/CLTensor.cc | 8 +- runtime/onert/backend/gpu_cl/operand/CLTensor.h | 4 +- runtime/onert/backend/gpu_cl/operand/ICLTensor.cc | 40 +- runtime/onert/backend/gpu_cl/operand/ICLTensor.h | 49 +- runtime/onert/backend/ruy/BackendContext.cc | 2 +- runtime/onert/backend/trix/BackendContext.cc | 2 +- runtime/onert/backend/trix/BatchThreadPool.cc | 69 + runtime/onert/backend/trix/BatchThreadPool.h | 130 ++ runtime/onert/backend/trix/Convert.cc | 54 + runtime/onert/backend/trix/Convert.h | 93 ++ runtime/onert/backend/trix/DevContext.cc | 307 ++++ runtime/onert/backend/trix/DevContext.h | 197 +-- runtime/onert/backend/trix/KernelGenerator.cc | 4 +- runtime/onert/backend/trix/ops/BulkLayer.cc | 137 +- runtime/onert/backend/trix/ops/BulkLayer.h | 3 +- runtime/onert/backend/xnnpack/BackendContext.cc | 2 +- runtime/onert/core/CMakeLists.txt | 2 +- .../include/backend/basic/BackendContextHelpers.h | 18 +- runtime/onert/core/include/compiler/Compiler.h | 107 +- .../onert/core/include/compiler/CompilerFactory.h | 47 + .../onert/core/include/compiler/CompilerOptions.h | 91 ++ runtime/onert/core/include/compiler/ICompiler.h | 63 + runtime/onert/core/include/compiler/LoweredGraph.h | 5 - .../core/include/compiler/StaticShapeInferer.h | 9 + runtime/onert/core/include/exec/Execution.h | 119 +- runtime/onert/core/include/exec/Executors.h | 71 - runtime/onert/core/include/exec/FunctionSequence.h | 2 +- runtime/onert/core/include/exec/IExecutor.h | 17 +- runtime/onert/core/include/exec/IExecutors.h | 98 ++ runtime/onert/core/include/ir/Graph.h | 36 - runtime/onert/core/include/ir/Index.h | 4 +- runtime/onert/core/include/ir/NNPkg.h | 102 +- .../onert/core/include/ir/OperandIndexSequence.h | 7 - runtime/onert/core/include/ir/Shape.h | 6 +- runtime/onert/core/include/util/Config.lst | 1 - runtime/onert/core/include/util/Index.h | 7 + runtime/onert/core/include/util/ObjectManager.h | 4 +- runtime/onert/core/include/util/Utils.h | 37 +- .../onert/core/src/backend/basic/MemoryManager.cc | 2 +- .../onert/core/src/backend/basic/MemoryPlanner.cc | 2 +- .../core/src/backend/basic/StaticTensorManager.cc | 2 +- .../core/src/backend/builtin/BackendContext.cc | 2 +- runtime/onert/core/src/backend/builtin/IOTensor.h | 2 +- .../core/src/backend/builtin/KernelGenerator.cc | 8 +- .../core/src/backend/builtin/KernelGenerator.h | 9 +- .../core/src/backend/builtin/kernel/IfLayer.cc | 8 +- .../core/src/backend/builtin/kernel/IfLayer.h | 8 +- .../src/backend/builtin/kernel/PermuteLayer.cc | 8 +- .../core/src/backend/builtin/kernel/WhileLayer.cc | 10 +- .../core/src/backend/builtin/kernel/WhileLayer.h | 8 +- runtime/onert/core/src/compiler/Compiler.cc | 772 +--------- runtime/onert/core/src/compiler/CompilerFactory.cc | 45 + runtime/onert/core/src/compiler/CompilerOptions.cc | 145 ++ runtime/onert/core/src/compiler/ExecutorFactory.cc | 51 +- runtime/onert/core/src/compiler/ExecutorFactory.h | 25 +- .../onert/core/src/compiler/Fp32ToFp16Converter.cc | 24 +- runtime/onert/core/src/compiler/HEScheduler.cc | 2 +- .../onert/core/src/compiler/HEScheduler.test.cc | 4 +- runtime/onert/core/src/compiler/LoweredGraph.cc | 8 - runtime/onert/core/src/compiler/ManualScheduler.cc | 4 +- .../onert/core/src/compiler/MultiModelCompiler.cc | 214 +++ .../onert/core/src/compiler/MultiModelCompiler.h | 75 + .../onert/core/src/compiler/StaticShapeInferer.cc | 98 +- runtime/onert/core/src/compiler/TensorRegistries.h | 2 +- .../onert/core/src/compiler/pass/OddOutputPass.cc | 4 +- runtime/onert/core/src/compiler/pass/PassRunner.cc | 2 +- .../src/compiler/pass/PermutationInsertionPass.cc | 4 +- runtime/onert/core/src/exec/Execution.cc | 247 +-- runtime/onert/core/src/exec/Execution.test.cc | 337 ++++- runtime/onert/core/src/exec/ExecutionObservee.cc | 8 +- runtime/onert/core/src/exec/ExecutionObservers.h | 2 +- runtime/onert/core/src/exec/ExecutorBase.cc | 57 +- runtime/onert/core/src/exec/ExecutorBase.h | 13 +- runtime/onert/core/src/exec/Executors.cc | 672 +++++++-- runtime/onert/core/src/exec/Executors.h | 169 +++ runtime/onert/core/src/exec/IPermuteFunction.cc | 320 ++++ runtime/onert/core/src/exec/IPermuteFunction.h | 99 +- .../onert/core/src/exec/IPermuteFunction.test.cc | 902 +++++++++++ runtime/onert/core/src/exec/ParallelScheduler.cc | 2 +- .../onert/core/src/exec/SingleModelExecutors.cc | 61 + runtime/onert/core/src/exec/SingleModelExecutors.h | 70 + runtime/onert/core/src/exec/ThreadPool.cc | 2 +- runtime/onert/core/src/interp/Buffer.h | 91 -- runtime/onert/core/src/interp/ExecEnv.h | 212 --- runtime/onert/core/src/interp/InterpExecutor.cc | 127 -- runtime/onert/core/src/interp/InterpExecutor.h | 89 -- .../onert/core/src/interp/InterpExecutor.test.cc | 355 ----- runtime/onert/core/src/interp/InterpOps.lst | 73 - runtime/onert/core/src/interp/Interpreter.cc | 184 --- runtime/onert/core/src/interp/Interpreter.h | 64 - runtime/onert/core/src/interp/Registration.h | 43 - runtime/onert/core/src/interp/Tensor.cc | 57 - runtime/onert/core/src/interp/Tensor.h | 189 --- .../src/interp/operations/BinaryArithmeticOps.cc | 204 --- runtime/onert/core/src/interp/operations/Concat.cc | 147 -- runtime/onert/core/src/interp/operations/Conv2D.cc | 151 -- .../core/src/interp/operations/DepthwiseConv2D.cc | 156 -- .../interp/operations/ElementwiseActivations.cc | 160 -- .../core/src/interp/operations/FullyConnected.cc | 134 -- runtime/onert/core/src/interp/operations/Gather.cc | 138 -- .../core/src/interp/operations/InstanceNorm.cc | 121 -- .../core/src/interp/operations/OperationUtil.h | 203 --- runtime/onert/core/src/interp/operations/Pad.cc | 106 -- runtime/onert/core/src/interp/operations/Pool2D.cc | 140 -- .../onert/core/src/interp/operations/Reshape.cc | 63 - .../onert/core/src/interp/operations/Softmax.cc | 123 -- .../core/src/interp/operations/TransposeConv.cc | 141 -- runtime/onert/core/src/ir/Shape.cc | 8 +- runtime/onert/core/src/ir/Shape.test.cc | 2 +- .../core/src/util/ChromeTracingEventWriter.cc | 6 +- runtime/onert/core/src/util/MDTableEventWriter.cc | 8 +- runtime/onert/core/src/util/SNPEEventWriter.cc | 22 +- runtime/onert/core/src/util/ShapeInference.cc | 6 +- .../frontend/base_loader/include/base_loader.h | 30 +- runtime/onert/frontend/circle/src/circle_loader.cc | 4 +- runtime/onert/frontend/nnapi/CMakeLists.txt | 2 +- runtime/onert/frontend/nnapi/compilation.cc | 4 +- runtime/onert/frontend/nnapi/execution.cc | 2 +- .../nnapi/wrapper/ANeuralNetworksCompilation.cc | 5 +- .../nnapi/wrapper/ANeuralNetworksCompilation.h | 6 +- .../nnapi/wrapper/ANeuralNetworksExecution.h | 2 +- runtime/onert/frontend/tflite/src/tflite_loader.cc | 5 +- runtime/service/npud/CMakeLists.txt | 47 +- runtime/service/npud/backend/CMakeLists.txt | 2 + runtime/service/npud/backend/trix/CMakeLists.txt | 19 + runtime/service/npud/backend/trix/TrixBackend.cc | 418 +++++ runtime/service/npud/backend/trix/TrixBackend.h | 130 ++ runtime/service/npud/core/Backend.h | 174 +++ runtime/service/npud/core/CMakeLists.txt | 29 + runtime/service/npud/core/ContextManager.cc | 92 ++ runtime/service/npud/core/ContextManager.h | 62 + runtime/service/npud/core/Core.cc | 168 +++ runtime/service/npud/core/Core.h | 61 + runtime/service/npud/core/DBus.cc | 323 ++++ runtime/service/npud/core/DBus.h | 78 + runtime/service/npud/core/DevManager.cc | 179 +++ runtime/service/npud/core/DevManager.h | 63 + runtime/service/npud/core/DynamicLoader.cc | 62 + runtime/service/npud/core/DynamicLoader.h | 54 + runtime/service/npud/core/Server.cc | 30 +- runtime/service/npud/core/Server.h | 12 + runtime/service/npud/core/Signal.cc | 5 +- runtime/service/npud/core/ir/DataType.h | 49 + runtime/service/npud/core/ir/Layout.h | 42 + runtime/service/npud/core/main.cc | 3 +- runtime/service/npud/core/util/Config.lst | 23 + .../service/npud/{ => core}/util/ConfigSource.cc | 0 .../service/npud/{ => core}/util/ConfigSource.h | 0 runtime/service/npud/{ => core}/util/Logging.h | 0 runtime/service/npud/org.tizen.npud.conf | 9 + runtime/service/npud/org.tizen.npud.xml | 162 ++ runtime/service/npud/tests/CMakeLists.txt | 17 + runtime/service/npud/tests/core/DBus.test.cc | 608 ++++++++ runtime/service/npud/tests/core/Server.test.cc | 75 + runtime/service/npud/tests/core/Signal.test.cc | 104 ++ runtime/service/npud/util/Config.lst | 22 - tests/nnapi/CMakeLists.txt | 16 +- tests/nnapi/include/NeuralNetworksWrapper.h | 11 +- tests/nnapi/nnapi_gtest.skip.noarch.interp | 743 --------- tests/nnapi/src/TestNeuralNetworksWrapper.h | 4 +- tests/nnfw_api/CMakeLists.txt | 6 +- .../src/ValidationTestAddModelLoaded.test.cc | 2 - .../src/ValidationTestPipelineSession.test.cc | 541 ------- tests/scripts/README.md | 6 +- tests/scripts/benchmark.sh | 2 +- tests/scripts/benchmark_nnapi.sh | 245 --- tests/scripts/benchmark_nnpkg.sh | 98 +- tests/scripts/benchmark_ops.sh | 183 +++ tests/scripts/command/nnpkg-test | 6 +- tests/scripts/command/prepare-model | 113 +- tests/scripts/command/unittest | 4 +- tests/scripts/command/verify-tflite | 36 +- tests/scripts/common.sh | 47 +- tests/scripts/list/nnapi_test.aarch64.list | 45 - tests/scripts/list/nnapi_test.armv7l.list | 45 - tests/scripts/list/nnpkg_test_list.noarch.interp | 42 - .../list/tflite_comparator.aarch64.acl_cl.list | 6 +- .../list/tflite_comparator.aarch64.acl_neon.list | 2 + .../list/tflite_comparator.armv7l.acl_cl.list | 6 +- .../list/tflite_comparator.armv7l.acl_neon.list | 2 + .../scripts/list/tflite_comparator.armv7l.cpu.list | 1 - .../list/tflite_comparator.noarch.interp.list | 16 - .../scripts/list/tflite_comparator.x86_64.cpu.list | 1 - tests/scripts/merge_result_of_benchmark_nnpkg.py | 2 +- tests/scripts/models/run_test.sh | 121 +- tests/scripts/onert-test | 11 +- tests/scripts/test-driver.sh | 163 -- tests/scripts/test_framework.sh | 104 -- tests/scripts/test_scheduler_with_profiling.sh | 32 +- tests/tools/nnapi_test/CMakeLists.txt | 14 - tests/tools/nnapi_test/src/args.cc | 116 -- tests/tools/nnapi_test/src/args.h | 53 - tests/tools/nnapi_test/src/nnapi_test.cc | 62 - tests/tools/nnpackage_run/CMakeLists.txt | 45 - tests/tools/nnpackage_run/README.md | 22 - tests/tools/nnpackage_run/src/allocation.h | 38 - tests/tools/nnpackage_run/src/args.cc | 318 ---- tests/tools/nnpackage_run/src/args.h | 102 -- tests/tools/nnpackage_run/src/formatter.h | 47 - tests/tools/nnpackage_run/src/h5formatter.cc | 258 ---- tests/tools/nnpackage_run/src/h5formatter.h | 41 - tests/tools/nnpackage_run/src/nnfw_util.cc | 49 - tests/tools/nnpackage_run/src/nnfw_util.h | 37 - tests/tools/nnpackage_run/src/nnpackage_run.cc | 327 ---- tests/tools/nnpackage_run/src/randomgen.cc | 77 - tests/tools/nnpackage_run/src/randomgen.h | 40 - tests/tools/nnpackage_run/src/rawformatter.cc | 97 -- tests/tools/nnpackage_run/src/rawformatter.h | 40 - tests/tools/nnpackage_run/src/types.h | 27 - tests/tools/onert_run/CMakeLists.txt | 45 + tests/tools/onert_run/README.md | 22 + tests/tools/onert_run/src/allocation.h | 38 + tests/tools/onert_run/src/args.cc | 389 +++++ tests/tools/onert_run/src/args.h | 106 ++ tests/tools/onert_run/src/formatter.h | 47 + tests/tools/onert_run/src/h5formatter.cc | 258 ++++ tests/tools/onert_run/src/h5formatter.h | 41 + tests/tools/onert_run/src/nnfw_util.cc | 49 + tests/tools/onert_run/src/nnfw_util.h | 37 + tests/tools/onert_run/src/onert_run.cc | 331 ++++ tests/tools/onert_run/src/randomgen.cc | 77 + tests/tools/onert_run/src/randomgen.h | 40 + tests/tools/onert_run/src/rawformatter.cc | 97 ++ tests/tools/onert_run/src/rawformatter.h | 40 + tests/tools/onert_run/src/types.h | 27 + tests/tools/tflite_benchmark_model/CMakeLists.txt | 24 - tests/tools/tflite_benchmark_model/README.md | 197 --- .../benchmark_tflite_model.cc | 419 ------ .../tflite_benchmark_model/profile_summarizer.cc | 161 -- .../tflite_benchmark_model/stats_calculator.cc | 317 ---- .../tflite_comparator/src/tflite_comparator.cc | 103 +- tests/tools/tflite_run/CMakeLists.txt | 2 +- tests/tools/tflite_run/src/tensor_dumper.cc | 52 +- tests/tools/tflite_run/src/tensor_dumper.h | 8 +- tests/tools/tflite_run/src/tensor_loader.cc | 69 +- tests/tools/tflite_run/src/tensor_loader.h | 20 +- tests/tools/tflite_run/src/tflite_run.cc | 122 +- tests/tools/tflite_vanilla_run/CMakeLists.txt | 24 - tests/tools/tflite_vanilla_run/src/args.cc | 208 --- tests/tools/tflite_vanilla_run/src/args.h | 73 - tests/tools/tflite_vanilla_run/src/tensor_view.h | 117 -- .../tflite_vanilla_run/src/tflite_vanilla_run.cc | 284 ---- tools/nnpackage_tool/model2nnpkg/README.md | 48 +- tools/nnpackage_tool/model2nnpkg/model2nnpkg.py | 287 ++++ tools/nnpackage_tool/model2nnpkg/model2nnpkg.sh | 22 +- tools/nnpackage_tool/nncc-tc-to-nnpkg-tc/README.md | 8 +- .../nncc-tc-to-nnpkg-tc/nncc-tc-to-nnpkg-tc.sh | 4 +- tools/nnpackage_tool/qnf/qnf.md | 35 + tools/nnpackage_tool/qnf/qnf.py | 147 ++ tools/nnpackage_tool/qnf/requirements.txt | 3 + tools/nnpackage_tool/sth2nnpkgtc/pb2nnpkgtc.sh | 2 +- tools/nnpackage_tool/sth2nnpkgtc/tflite2nnpkgtc.md | 2 +- tools/nnpackage_tool/sth2nnpkgtc/tflite2nnpkgtc.sh | 2 +- tools/pareto_profiler/README.md | 68 +- .../estimator/brute_force_profiler.py | 2 +- tools/pareto_profiler/estimator/profile_args.py | 2 +- tools/pareto_profiler/estimator/runner.py | 4 +- tools/stab/remote.py | 2 +- tools/tflitefile_tool/README.md | 49 + .../tflitefile_tool/parser/tflite/tflite_tensor.py | 2 +- tools/tflitefile_tool/select_operator.py | 68 +- 1700 files changed, 62677 insertions(+), 39645 deletions(-) create mode 100644 .github/workflows/deploy-github-pages.yml create mode 100644 compiler/circle-interpreter-test/CMakeLists.txt create mode 100644 compiler/circle-interpreter-test/README.md create mode 100644 compiler/circle-interpreter-test/requires.cmake create mode 100644 compiler/circle-interpreter-test/src/circle-interpreter.test.cpp create mode 100644 compiler/circle-mpqsolver/CMakeLists.txt create mode 100644 compiler/circle-mpqsolver/README.md create mode 100644 compiler/circle-mpqsolver/requires.cmake create mode 100644 compiler/circle-mpqsolver/src/CircleMPQSolver.cpp create mode 100644 compiler/circle-mpqsolver/src/MPQSolver.cpp create mode 100644 compiler/circle-mpqsolver/src/MPQSolver.h create mode 100644 compiler/circle-mpqsolver/src/bisection/BisectionSolver.cpp create mode 100644 compiler/circle-mpqsolver/src/bisection/BisectionSolver.h create mode 100644 compiler/circle-mpqsolver/src/bisection/DepthParameterizer.cpp create mode 100644 compiler/circle-mpqsolver/src/bisection/DepthParameterizer.h create mode 100644 compiler/circle-mpqsolver/src/bisection/DepthParameterizer.test.cpp create mode 100644 compiler/circle-mpqsolver/src/bisection/ErrorApproximator.cpp create mode 100644 compiler/circle-mpqsolver/src/bisection/ErrorApproximator.h create mode 100644 compiler/circle-mpqsolver/src/bisection/ErrorApproximator.test.cpp create mode 100644 compiler/circle-mpqsolver/src/bisection/ErrorMetric.cpp create mode 100644 compiler/circle-mpqsolver/src/bisection/ErrorMetric.h create mode 100644 compiler/circle-mpqsolver/src/bisection/Evaluator.cpp create mode 100644 compiler/circle-mpqsolver/src/bisection/Evaluator.h create mode 100644 compiler/circle-mpqsolver/src/bisection/Quantizer.cpp create mode 100644 compiler/circle-mpqsolver/src/bisection/Quantizer.h create mode 100644 compiler/circle-mpqsolver/src/bisection/Quantizer.test.cpp create mode 100644 compiler/circle-mpqsolver/src/bisection/TestHelper.h create mode 100644 compiler/circle-mpqsolver/src/bisection/VISQErrorApproximator.cpp create mode 100644 compiler/circle-mpqsolver/src/bisection/VISQErrorApproximator.h delete mode 100644 compiler/circle-opselector/src/Driver.test.cpp delete mode 100644 compiler/circle-opselector/src/Driver.test.h create mode 100644 compiler/circle-opselector/src/OpSelector.cpp create mode 100644 compiler/circle-opselector/src/OpSelector.h create mode 100644 compiler/circle-opselector/src/OpSelector.test.cpp create mode 100644 compiler/circle-opselector/src/SelectType.h create mode 100644 compiler/dalgona-test/.gitignore create mode 100644 compiler/dalgona-test/CMakeLists.txt create mode 100644 compiler/dalgona-test/GenH5RandomInputs.py create mode 100644 compiler/dalgona-test/RandomDataGenerator.py create mode 100644 compiler/dalgona-test/SingleOperatorTest.py create mode 100755 compiler/dalgona-test/TestSingleOp.sh create mode 100644 compiler/dalgona-test/TestUtil.py create mode 100644 compiler/dalgona-test/requires.cmake create mode 100644 compiler/dalgona-test/test.lst create mode 100644 compiler/dalgona/CMakeLists.txt create mode 100644 compiler/dalgona/README.md create mode 100644 compiler/dalgona/analysis/AnalysisTemplate.py create mode 100644 compiler/dalgona/driver/Driver.cpp create mode 100644 compiler/dalgona/include/Dalgona.h create mode 100644 compiler/dalgona/include/PythonHooks.h create mode 100644 compiler/dalgona/requires.cmake create mode 100644 compiler/dalgona/src/Dalgona.cpp create mode 100644 compiler/dalgona/src/PostOperatorHook.h create mode 100644 compiler/dalgona/src/PreOperatorHook.h create mode 100644 compiler/dalgona/src/PythonHooks.cpp create mode 100644 compiler/dalgona/src/RandomUtils.cpp create mode 100644 compiler/dalgona/src/RandomUtils.h create mode 100644 compiler/dalgona/src/RandomUtils.test.cpp create mode 100644 compiler/dalgona/src/StringUtils.cpp create mode 100644 compiler/dalgona/src/StringUtils.h create mode 100644 compiler/dalgona/src/StringUtils.test.cpp create mode 100644 compiler/dalgona/src/Utils.cpp create mode 100644 compiler/dalgona/src/Utils.h create mode 100644 compiler/luci-compute/CMakeLists.txt create mode 100644 compiler/luci-compute/README.md create mode 100644 compiler/luci-interpreter/src/kernels/Abs.cpp create mode 100644 compiler/luci-interpreter/src/kernels/Abs.h create mode 100644 compiler/luci-interpreter/src/kernels/Abs.test.cpp create mode 100644 compiler/luci-interpreter/src/kernels/ReduceProd.cpp create mode 100644 compiler/luci-interpreter/src/kernels/ReduceProd.h create mode 100644 compiler/luci-interpreter/src/kernels/ReduceProd.test.cpp create mode 100644 compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.cpp create mode 100644 compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.h create mode 100644 compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.test.cpp create mode 100644 compiler/luci-interpreter/src/loader/nodes/Abs.cpp create mode 100644 compiler/luci-interpreter/src/loader/nodes/ReduceProd.cpp create mode 100644 compiler/luci-interpreter/src/loader/nodes/UnidirectionalSequenceLSTM.cpp delete mode 100644 compiler/luci-micro/CMakeLists.txt delete mode 100644 compiler/luci-micro/luci-interpreter/CMakeLists.txt delete mode 100644 compiler/luci-micro/luci-interpreter/include/luci_interpreter/BuddyMemoryManager.h delete mode 100644 compiler/luci-micro/luci-interpreter/include/luci_interpreter/GraphBuilderRegistry.h delete mode 100644 compiler/luci-micro/luci-interpreter/include/luci_interpreter/Interpreter.h delete mode 100644 compiler/luci-micro/luci-interpreter/include/luci_interpreter/MemoryManager.h delete mode 100644 compiler/luci-micro/luci-interpreter/include/luci_interpreter/SimpleMemoryManager.h delete mode 100644 compiler/luci-micro/luci-interpreter/include/luci_interpreter/StaticMemoryManager.h delete mode 100644 compiler/luci-micro/luci-interpreter/include/luci_interpreter/TestMemoryManager.h delete mode 100644 compiler/luci-micro/luci-interpreter/include/luci_interpreter/core/DataType.h delete mode 100644 compiler/luci-micro/luci-interpreter/include/luci_interpreter/core/Tensor.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst delete mode 100644 compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALConv2d.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALDepthwiseConv2d.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALDequantize.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALQuantize.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALResizeBilinear.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALResizeNearestNeighbor.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSVDF.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSpaceToBatchND.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/cmsisnn/pal.cmake delete mode 100644 compiler/luci-micro/luci-interpreter/pal/linux/KernelsToBuild.lst delete mode 100644 compiler/luci-micro/luci-interpreter/pal/linux/PALSVDF.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/linux/pal.cmake delete mode 100644 compiler/luci-micro/luci-interpreter/pal/mcu/KernelsToBuild.lst delete mode 100644 compiler/luci-micro/luci-interpreter/pal/mcu/PALBatchToSpaceND.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/mcu/PALConv2d.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/mcu/PALDequantize.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/mcu/PALQuantize.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/mcu/PALSVDF.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/mcu/PALSoftmax.h delete mode 100644 compiler/luci-micro/luci-interpreter/pal/mcu/pal.cmake delete mode 100644 compiler/luci-micro/luci-interpreter/requires.cmake delete mode 100644 compiler/luci-micro/luci-interpreter/src/BuddyMemoryManager.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/BuddyMemoryManager.test.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/CMakeLists.txt delete mode 100644 compiler/luci-micro/luci-interpreter/src/Interpreter.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/SimpleMemoryManager.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/StaticMemoryManager.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/TestMemoryManager.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/core/CMakeLists.txt delete mode 100644 compiler/luci-micro/luci-interpreter/src/core/EventNotifier.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/core/Kernel.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/core/KernelParams.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/core/RuntimeGraph.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/core/RuntimeGraph.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/core/RuntimeModule.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/core/Tensor.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/import/CMakeLists.txt delete mode 100644 compiler/luci-micro/luci-interpreter/src/import/GraphBuilderRegistry.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/import/Nodes/CircleReferencingConst.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/import/Nodes/CircleReferencingConst.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Add.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/ArgMax.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/AveragePool2D.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/BatchMatMul.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/BatchToSpaceND.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/CMakeLists.txt delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Cast.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.test.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.test.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/DepthToSpace.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/DepthwiseConv2D.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Dequantize.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Div.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Elu.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Equal.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Exp.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.test.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Fill.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Floor.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/FloorDiv.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.test.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Gather.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Greater.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/GreaterEqual.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/InstanceNorm.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/L2Normalize.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/L2Pool2D.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/LeakyRelu.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Less.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/LessEqual.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/LocalResponseNormalization.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/LogSoftmax.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/LogicalAnd.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/LogicalNot.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/LogicalOr.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Logistic.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Logistic.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Logistic.test.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.test.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Maximum.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Mean.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Minimum.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/MirrorPad.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Mul.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Neg.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/NotEqual.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/OneHot.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/PRelu.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Pack.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Pad.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/PadV2.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Pow.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Quantize.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Relu.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Relu6.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Reshape.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Reshape.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Reshape.test.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/ResizeBilinear.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/ReverseV2.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Rsqrt.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/SVDF.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Shape.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Slice.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Softmax.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Softmax.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Softmax.test.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/SpaceToBatchND.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/SpaceToDepth.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Split.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/SplitV.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Sqrt.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Square.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/SquaredDifference.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Squeeze.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/StridedSlice.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Sub.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Tanh.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/TestUtils.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Transpose.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/TransposeConv.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Unpack.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Utils.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/kernels/Utils.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/CMakeLists.txt delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/GraphLoader.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/GraphLoader.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.test.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/KernelBuilderHelper.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/KernelBuilderHelper.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/ModuleLoader.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/ModuleLoader.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/RuntimeToIR.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Add.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/ArgMax.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/AveragePool2D.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/BatchMatMul.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/BatchToSpaceND.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Builders.h delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Cast.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Concatenation.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Conv2D.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/DepthToSpace.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/DepthwiseConv2D.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Dequantize.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Div.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Elu.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Equal.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Exp.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/ExpandDims.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Fill.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Floor.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/FloorDiv.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/FullyConnected.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Gather.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Greater.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/GreaterEqual.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/If.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/InstanceNorm.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/L2Normalize.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/L2Pool2D.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/LeakyRelu.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Less.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/LessEqual.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/LocalResponseNormalization.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/LogSoftmax.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalAnd.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalNot.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalOr.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Logistic.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/MaxPool2D.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Maximum.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Mean.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Minimum.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/MirrorPad.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Mul.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Neg.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/NotEqual.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/OneHot.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/PRelu.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Pack.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Pad.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/PadV2.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Pow.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Quantize.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Relu.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Relu6.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Reshape.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/ResizeBilinear.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/ResizeNearestNeighbor.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/ReverseV2.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Rsqrt.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/SVDF.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Shape.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Slice.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Softmax.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/SpaceToBatchND.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/SpaceToDepth.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Split.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/SplitV.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Sqrt.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Square.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/SquaredDifference.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Squeeze.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/StridedSlice.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Sub.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Tanh.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Transpose.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/TransposeConv.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/Unpack.cpp delete mode 100644 compiler/luci-micro/luci-interpreter/src/loader/nodes/While.cpp delete mode 100644 compiler/luci-micro/requires.cmake delete mode 100644 compiler/luci-micro/standalone/CMakeLists.txt create mode 100644 compiler/luci/pass/include/luci/Pass/FoldFullyConnectedPass.h create mode 100644 compiler/luci/pass/include/luci/Pass/ForwardTransposeOpPass.h create mode 100644 compiler/luci/pass/include/luci/Pass/FusePReluPass.h create mode 100644 compiler/luci/pass/include/luci/Pass/RemoveDuplicateConstPass.h create mode 100644 compiler/luci/pass/include/luci/Pass/UnrollUnidirectionalSequenceLSTMPass.h create mode 100644 compiler/luci/pass/src/FoldFullyConnectedPass.cpp create mode 100644 compiler/luci/pass/src/FoldFullyConnectedPass.test.cpp create mode 100644 compiler/luci/pass/src/ForwardTransposeOpPass.cpp create mode 100644 compiler/luci/pass/src/ForwardTransposeOpPass.test.cpp create mode 100644 compiler/luci/pass/src/FusePReluPass.cpp create mode 100644 compiler/luci/pass/src/FusePReluPass.test.cpp create mode 100644 compiler/luci/pass/src/RemoveDuplicateConstPass.cpp create mode 100644 compiler/luci/pass/src/RemoveDuplicateConstPass.test.cpp create mode 100644 compiler/luci/pass/src/UnrollUnidirectionalSequenceLSTMPass.cpp create mode 100644 compiler/luci/pass/src/UnrollUnidirectionalSequenceLSTMPass.test.cpp delete mode 100644 compiler/luci/pass/src/test/TestIOGraph.h delete mode 100644 compiler/luci/pass/src/test/TestIOGraph.test.cpp create mode 100644 compiler/one-cmds/dummy-driver/src/dummyEnv-compile.cpp create mode 100644 compiler/one-cmds/one-prepare-venv.u2204 create mode 100644 compiler/one-cmds/onelib/Command.py create mode 100644 compiler/one-cmds/onelib/export_constant.py create mode 100644 compiler/one-cmds/onelib/utils.py create mode 100644 compiler/one-cmds/tests/one-infer_004.cfg delete mode 100644 compiler/one-cmds/tests/one-infer_005.cfg delete mode 100644 compiler/one-cmds/tests/one-infer_006.test delete mode 100644 compiler/one-cmds/tests/one-infer_neg_004.test delete mode 100644 compiler/one-cmds/tests/one-infer_neg_005.test create mode 100644 compiler/one-cmds/tests/one-optimize_003.test create mode 100644 compiler/one-cmds/tests/one-quantize_016.test create mode 100644 compiler/one-cmds/tests/one-quantize_neg_021.test create mode 100644 compiler/one-cmds/tests/onecc_042.cfg create mode 100644 compiler/one-cmds/tests/onecc_042.test create mode 100644 compiler/one-cmds/tests/onecc_043.cfg create mode 100644 compiler/one-cmds/tests/onecc_043.test create mode 100644 compiler/one-cmds/tests/onecc_044.cfg create mode 100644 compiler/one-cmds/tests/onecc_044.test create mode 100644 compiler/one-cmds/tests/onecc_045.cfg create mode 100644 compiler/one-cmds/tests/onecc_045.test create mode 100644 compiler/one-cmds/tests/onecc_neg_024.cfg create mode 100644 compiler/one-cmds/tests/onecc_neg_024.test create mode 100644 compiler/one-cmds/tests/onecc_neg_025.cfg create mode 100644 compiler/one-cmds/tests/onecc_neg_025.test create mode 100644 compiler/one-cmds/tests/onecc_neg_026.cfg create mode 100644 compiler/one-cmds/tests/onecc_neg_026.test delete mode 100644 compiler/one-cmds/utils.py create mode 100644 compiler/onecc-docker/README.md create mode 100644 compiler/onecc-docker/debian/changelog create mode 100644 compiler/onecc-docker/debian/compat create mode 100644 compiler/onecc-docker/debian/control create mode 100644 compiler/onecc-docker/debian/copyright create mode 100644 compiler/onecc-docker/debian/onecc-docker.install create mode 100644 compiler/onecc-docker/debian/onecc-docker.links create mode 100644 compiler/onecc-docker/debian/rules create mode 100644 compiler/onecc-docker/docker/Dockerfile create mode 100644 compiler/onecc-docker/onecc-docker create mode 100644 compiler/pics/CMakeLists.txt create mode 100644 compiler/pics/README.md create mode 100755 compiler/pota-quantization-value-test/test_parallel_record_minmax.sh create mode 100644 compiler/record-minmax-thread-safety-test/CMakeLists.txt create mode 100644 compiler/record-minmax-thread-safety-test/gen_h5_random_inputs.py create mode 100644 compiler/record-minmax-thread-safety-test/requires.cmake create mode 100644 compiler/record-minmax-thread-safety-test/test.lst create mode 100755 compiler/record-minmax-thread-safety-test/testall.sh create mode 100644 compiler/visq-unittest/CMakeLists.txt create mode 100644 compiler/visq-unittest/README.md create mode 100644 compiler/visq-unittest/requires.cmake create mode 100644 compiler/visq-unittest/test/__init__.py create mode 100644 compiler/visq-unittest/test/testDotBuilder.py create mode 100644 compiler/visq-unittest/test/testPalette.py create mode 100644 compiler/visq-unittest/test/testQErrorComputer.py create mode 100644 compiler/visq-unittest/test/testUtil.py create mode 100644 compiler/visq/CMakeLists.txt create mode 100644 compiler/visq/README.md create mode 100644 compiler/visq/requires.cmake create mode 100644 compiler/visq/visq create mode 100644 compiler/visq/visqlib/DotBuilder.py create mode 100644 compiler/visq/visqlib/DumpFP32FM.py create mode 100644 compiler/visq/visqlib/DumpFakeQuantFM.py create mode 100644 compiler/visq/visqlib/Palette.py create mode 100644 compiler/visq/visqlib/QErrorComputer.py create mode 100644 compiler/visq/visqlib/Util.py create mode 100644 docs/release/1.22/index.rst create mode 100644 docs/release/1.22/release-note-1.22.0.md create mode 100644 infra/cmake/packages/CMSIS-NN-4.0.0/CMSIS-NNConfig.cmake create mode 100644 infra/cmake/packages/CMSIS-NN-4.0.0/CMSIS-NNConfigVersion.cmake delete mode 100644 infra/cmake/packages/TensorFlowGpuConfig.cmake delete mode 100644 infra/cmake/packages/TensorFlowGpuSource/patch_for_gpu_cl_build.patch delete mode 100644 infra/cmake/packages/TensorFlowGpuSourceConfig.cmake delete mode 100644 infra/cmake/packages/TensorFlowLiteGpu/CMakeLists.txt create mode 100644 infra/docker/jammy/Dockerfile create mode 100644 infra/nnfw/cmake/packages/GObject2.0Config.cmake create mode 100644 infra/nnfw/cmake/packages/Gio2.0Config.cmake create mode 100644 infra/nnfw/cmake/packages/Giounix2.0Config.cmake delete mode 100644 infra/nnfw/cmake/packages/TensorFlowEigen-1.13.1/TensorFlowEigenConfig.cmake delete mode 100644 infra/nnfw/cmake/packages/TensorFlowEigen-1.13.1/TensorFlowEigenConfigVersion.cmake create mode 100644 infra/nnfw/cmake/packages/TensorFlowGpuConfig.cmake delete mode 100644 infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLite/CMakeLists.txt delete mode 100644 infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLiteConfig.cmake delete mode 100644 infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLiteConfigVersion.cmake create mode 100644 infra/nnfw/cmake/packages/TensorFlowLiteGpu/CMakeLists.txt create mode 100644 infra/nnfw/command/prepare-model create mode 100644 infra/onert-micro/CMakeLists.txt create mode 100644 infra/onert-micro/cmake/ApplyCompileFlags.cmake create mode 100644 infra/onert-micro/cmake/CfgOptionFlags.cmake create mode 100644 infra/onert-micro/cmake/buildtool/config/arm-none-eabi-gcc.cmake create mode 100644 infra/onert-micro/cmake/buildtool/config/config_linux.cmake create mode 100644 infra/onert-micro/cmake/buildtool/config/config_x86_64-linux.cmake create mode 100644 infra/onert-micro/cmake/options/options_armv7-r-generic.cmake create mode 100644 infra/onert-micro/cmake/options/options_armv7em-generic.cmake create mode 100644 infra/onert-micro/cmake/options/options_armv8-m-generic.cmake create mode 100644 infra/onert-micro/cmake/options/options_x86_64-linux.cmake create mode 100644 infra/onert-micro/utils.cmake create mode 100644 infra/packaging/preset/20221125 create mode 100644 infra/packaging/preset/20221125_windows create mode 100644 infra/packaging/res/tf2nnpkg.20221125 delete mode 100755 infra/scripts/build_android_runtime_release.sh delete mode 100755 infra/scripts/docker_build_cross_aarch64_runtime.sh delete mode 100755 infra/scripts/docker_build_cross_arm_runtime.sh delete mode 100755 infra/scripts/docker_build_cross_arm_runtime_release.sh delete mode 100755 infra/scripts/docker_build_cross_coverage.sh delete mode 100755 infra/scripts/docker_build_test_x64.sh delete mode 100755 infra/scripts/docker_build_tizen_cross.sh delete mode 100755 infra/scripts/docker_build_tizen_gbs.sh delete mode 100755 infra/scripts/docker_coverage_report.sh create mode 100755 infra/scripts/test_ubuntu_npud.sh create mode 100644 onert-micro/CMakeLists.txt rename {compiler/luci-micro => onert-micro}/README.md (100%) create mode 100644 onert-micro/eval-driver/CMakeLists.txt create mode 100644 onert-micro/eval-driver/Driver.cpp create mode 100644 onert-micro/helpers/GenerateKernelsListHelper.cpp create mode 100644 onert-micro/luci-interpreter/CMakeLists.txt rename {compiler/luci-micro => onert-micro}/luci-interpreter/README.md (100%) create mode 100644 onert-micro/luci-interpreter/include/luci_interpreter/Interpreter.h create mode 100644 onert-micro/luci-interpreter/include/luci_interpreter/InterpreterConfigure.h create mode 100644 onert-micro/luci-interpreter/include/luci_interpreter/core/DataType.h create mode 100644 onert-micro/luci-interpreter/include/luci_interpreter/core/ParamsType.h create mode 100644 onert-micro/luci-interpreter/include/luci_interpreter/core/Tensor.h create mode 100644 onert-micro/luci-interpreter/include/luci_interpreter/core/reader/CircleMicroReader.h create mode 100644 onert-micro/luci-interpreter/include/luci_interpreter/core/reader/CircleMicroReaderHelper.h create mode 100644 onert-micro/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALArgMax.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/cmsisnn/PALAveragePool2d.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h create mode 100644 onert-micro/luci-interpreter/pal/cmsisnn/PALConv2d.h rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALDepthToSpace.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/cmsisnn/PALDepthwiseConv2d.h create mode 100644 onert-micro/luci-interpreter/pal/cmsisnn/PALDequantize.h rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALElu.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/cmsisnn/PALFill.h rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/cmsisnn/PALFullyConnected.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALL2Normalize.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALL2Pool2D.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALLeakyRelu.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALMul.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALNeg.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/cmsisnn/PALQuantize.h rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALResizeBilinear.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALResizeNearestNeighbor.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/cmsisnn/PALSVDF.h rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/cmsisnn/PALSoftmax.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALSpaceToBatchND.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALSpaceToDepth.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/cmsisnn}/PALSub.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/cmsisnn/PALUnidirectionalSequenceLSTM.h create mode 100644 onert-micro/luci-interpreter/pal/cmsisnn/PALreference_ops.h create mode 100644 onert-micro/luci-interpreter/pal/cmsisnn/pal.cmake create mode 100644 onert-micro/luci-interpreter/pal/linux/KernelsToBuild.lst rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALArgMax.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/linux}/PALAveragePool2d.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALBatchMatMul.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALBatchToSpaceND.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALConv2d.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALDepthToSpace.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/mcu => onert-micro/luci-interpreter/pal/linux}/PALDepthwiseConv2d.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALDequantize.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALElu.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/linux/PALFill.h rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALFullyConnected.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALGather.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALL2Normalize.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALL2Pool2D.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALLeakyRelu.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALLocalResponseNormalization.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALLogSoftmax.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALMul.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALNeg.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALQuantize.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALRelu.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALRelu6.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALResizeBilinear.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALResizeNearestNeighbor.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/linux/PALSVDF.h rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALSlice.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALSoftmax.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALSpaceToBatchND.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALSpaceToDepth.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALSplit.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/linux/PALSub.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/linux/pal.cmake create mode 100644 onert-micro/luci-interpreter/pal/mcu/KernelsToBuild.lst create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALApplyActivationToVector.h rename {compiler/luci-micro/luci-interpreter/pal/cmsisnn => onert-micro/luci-interpreter/pal/mcu}/PALArgMax.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/linux => onert-micro/luci-interpreter/pal/mcu}/PALAveragePool2d.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALBatchToSpaceND.h create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALConv2d.h rename {compiler/luci-micro/luci-interpreter/pal/cmsisnn => onert-micro/luci-interpreter/pal/mcu}/PALDepthToSpace.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/linux => onert-micro/luci-interpreter/pal/mcu}/PALDepthwiseConv2d.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALDequantize.h rename {compiler/luci-micro/luci-interpreter/pal/cmsisnn => onert-micro/luci-interpreter/pal/mcu}/PALElu.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALFill.h rename {compiler/luci-micro => onert-micro}/luci-interpreter/pal/mcu/PALFullyConnected.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/cmsisnn => onert-micro/luci-interpreter/pal/mcu}/PALL2Normalize.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/cmsisnn => onert-micro/luci-interpreter/pal/mcu}/PALL2Pool2D.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/cmsisnn => onert-micro/luci-interpreter/pal/mcu}/PALLeakyRelu.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/cmsisnn => onert-micro/luci-interpreter/pal/mcu}/PALMul.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/cmsisnn => onert-micro/luci-interpreter/pal/mcu}/PALNeg.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALQuantize.h create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALResizeBilinear.h create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALResizeNearestNeighbor.h create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALSVDF.h create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALSoftmax.h create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALSpaceToBatchND.h rename {compiler/luci-micro/luci-interpreter/pal/cmsisnn => onert-micro/luci-interpreter/pal/mcu}/PALSpaceToDepth.h (100%) rename {compiler/luci-micro/luci-interpreter/pal/cmsisnn => onert-micro/luci-interpreter/pal/mcu}/PALSub.h (100%) create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALUnidirectionalSequenceLSTM.h create mode 100644 onert-micro/luci-interpreter/pal/mcu/PALreference_ops.h create mode 100644 onert-micro/luci-interpreter/pal/mcu/pal.cmake rename tests/tools/tflite_benchmark_model/.FORMATDENY => onert-micro/luci-interpreter/requires.cmake (100%) create mode 100644 onert-micro/luci-interpreter/src/CMakeLists.txt create mode 100644 onert-micro/luci-interpreter/src/Interpreter.cpp create mode 100644 onert-micro/luci-interpreter/src/core/CMakeLists.txt create mode 100644 onert-micro/luci-interpreter/src/core/RuntimeGraph.cpp create mode 100644 onert-micro/luci-interpreter/src/core/RuntimeGraph.h create mode 100644 onert-micro/luci-interpreter/src/core/RuntimeModule.h create mode 100644 onert-micro/luci-interpreter/src/core/reader/CMakeLists.txt create mode 100644 onert-micro/luci-interpreter/src/core/reader/CircleMicroReader.cpp create mode 100644 onert-micro/luci-interpreter/src/core/reader/CircleMicroReaderHelper.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Add.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Add.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Add.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/ArgMax.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/ArgMax.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/ArgMax.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/AveragePool2D.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/AveragePool2D.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/AveragePool2D.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/BatchMatMul.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/BatchMatMul.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/BatchMatMul.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/BatchToSpaceND.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/BatchToSpaceND.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/BatchToSpaceND.test.cpp (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/BinaryOpCommon.h (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Builders.h create mode 100644 onert-micro/luci-interpreter/src/kernels/CMakeLists.txt create mode 100644 onert-micro/luci-interpreter/src/kernels/Cast.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Cast.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Cast.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Concatenation.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Concatenation.test.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Conv2D.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Conv2D.test.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/DepthToSpace.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/DepthToSpace.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/DepthToSpace.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/DepthwiseConv2D.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/DepthwiseConv2D.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/DepthwiseConv2D.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Dequantize.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Dequantize.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Dequantize.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Div.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Div.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Div.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Elu.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Elu.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Elu.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Equal.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Equal.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Equal.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Exp.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Exp.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Exp.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/ExpandDims.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/ExpandDims.test.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Fill.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Fill.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Fill.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Floor.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Floor.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Floor.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/FloorDiv.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/FloorDiv.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/FloorDiv.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/FullyConnected.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/FullyConnected.test.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Gather.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Gather.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Gather.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Greater.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Greater.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Greater.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/GreaterEqual.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/GreaterEqual.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/GreaterEqual.test.cpp (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/If.cpp (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/If.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/If.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/InstanceNorm.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/InstanceNorm.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/InstanceNorm.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/KernelBuilder.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/KernelBuilder.h create mode 100644 onert-micro/luci-interpreter/src/kernels/L2Normalize.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/L2Normalize.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/L2Normalize.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/L2Pool2D.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/L2Pool2D.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/L2Pool2D.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/LeakyRelu.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LeakyRelu.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LeakyRelu.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Less.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Less.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Less.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/LessEqual.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LessEqual.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LessEqual.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/LocalResponseNormalization.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LocalResponseNormalization.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LocalResponseNormalization.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/LogSoftmax.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LogSoftmax.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LogSoftmax.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/LogicalAnd.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LogicalAnd.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LogicalAnd.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/LogicalNot.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LogicalNot.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LogicalNot.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/LogicalOr.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LogicalOr.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/LogicalOr.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Logistic.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Logistic.test.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/MaxPool2D.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/MaxPool2D.test.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Maximum.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Maximum.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Maximum.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Mean.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Mean.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Mean.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Minimum.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Minimum.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Minimum.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/MirrorPad.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/MirrorPad.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/MirrorPad.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Mul.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Mul.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Mul.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Neg.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Neg.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Neg.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/NotEqual.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/NotEqual.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/NotEqual.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/OneHot.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/OneHot.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/OneHot.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/PRelu.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/PRelu.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/PRelu.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Pack.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Pack.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Pack.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Pad.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Pad.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Pad.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/PadV2.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/PadV2.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/PadV2.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Pow.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Pow.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Pow.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Quantize.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Quantize.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Quantize.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Relu.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Relu.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Relu.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Relu6.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Relu6.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Relu6.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Reshape.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Reshape.test.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/ResizeBilinear.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/ResizeBilinear.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/ResizeBilinear.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/ResizeNearestNeighbor.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/ResizeNearestNeighbor.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/ReverseV2.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/ReverseV2.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/ReverseV2.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Rsqrt.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Rsqrt.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Rsqrt.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/SVDF.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/SVDF.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/SVDF.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Shape.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Shape.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Shape.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Slice.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Slice.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Slice.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Softmax.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Softmax.test.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/SpaceToBatchND.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/SpaceToBatchND.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/SpaceToBatchND.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/SpaceToDepth.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/SpaceToDepth.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/SpaceToDepth.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Split.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Split.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Split.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/SplitV.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/SplitV.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/SplitV.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Sqrt.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Sqrt.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Sqrt.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Square.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Square.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Square.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/SquaredDifference.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/SquaredDifference.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/SquaredDifference.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Squeeze.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Squeeze.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Squeeze.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/StridedSlice.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/StridedSlice.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/StridedSlice.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Sub.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Sub.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Sub.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Tanh.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Tanh.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Tanh.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/TestUtils.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/TestUtils.h (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Transpose.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Transpose.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Transpose.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/TransposeConv.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/TransposeConv.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/TransposeConv.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.h create mode 100644 onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.test.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Unpack.cpp rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Unpack.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/Unpack.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/kernels/Utils.cpp create mode 100644 onert-micro/luci-interpreter/src/kernels/Utils.h rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/While.cpp (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/While.h (100%) rename {compiler/luci-micro => onert-micro}/luci-interpreter/src/kernels/While.test.cpp (100%) create mode 100644 onert-micro/luci-interpreter/src/loader/CMakeLists.txt create mode 100644 onert-micro/luci-interpreter/src/loader/GraphLoader.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/GraphLoader.h create mode 100644 onert-micro/luci-interpreter/src/loader/ModuleLoader.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/ModuleLoader.h create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Add.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/ArgMax.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/AveragePool2D.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/BatchMatMul.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/BatchToSpaceND.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Cast.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Concatenation.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Conv2D.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/DepthToSpace.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/DepthwiseConv2D.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Dequantize.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Div.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Elu.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Equal.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Exp.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/ExpandDims.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Fill.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Floor.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/FloorDiv.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/FullyConnected.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Gather.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Greater.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/GreaterEqual.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/If.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/InstanceNorm.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/L2Normalize.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/L2Pool2D.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/LeakyRelu.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Less.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/LessEqual.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/LocalResponseNormalization.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/LogSoftmax.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/LogicalAnd.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/LogicalNot.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/LogicalOr.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Logistic.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/MaxPool2D.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Maximum.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Mean.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Minimum.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/MirrorPad.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Mul.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Neg.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/NotEqual.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/OneHot.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/PRelu.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Pack.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Pad.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/PadV2.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Pow.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Quantize.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Relu.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Relu6.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Reshape.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/ResizeBilinear.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/ResizeNearestNeighbor.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/ReverseV2.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Rsqrt.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/SVDF.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Shape.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Slice.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Softmax.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/SpaceToBatchND.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/SpaceToDepth.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Split.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/SplitV.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Sqrt.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Square.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/SquaredDifference.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Squeeze.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/StridedSlice.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Sub.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Tanh.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Transpose.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/TransposeConv.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/UnidirectionalSequenceLSTM.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/Unpack.cpp create mode 100644 onert-micro/luci-interpreter/src/loader/nodes/While.cpp create mode 100644 onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.cpp create mode 100644 onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.h create mode 100644 onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.test.cpp create mode 100644 onert-micro/luci-interpreter/src/memory_managers/CMakeLists.txt create mode 100644 onert-micro/luci-interpreter/src/memory_managers/SimpleMemoryManager.cpp create mode 100644 onert-micro/luci-interpreter/src/memory_managers/SimpleMemoryManager.h create mode 100644 onert-micro/luci-interpreter/src/memory_managers/StaticMemoryManager.cpp create mode 100644 onert-micro/luci-interpreter/src/memory_managers/StaticMemoryManager.h create mode 100644 onert-micro/luci-interpreter/src/memory_managers/TestMemoryManager.cpp create mode 100644 onert-micro/luci-interpreter/src/memory_managers/TestMemoryManager.h create mode 100644 onert-micro/requires.cmake create mode 100644 onert-micro/standalone/CMakeLists.txt create mode 100644 onert-micro/tests/mbed-os/CMakeLists.txt create mode 100644 onert-micro/tests/mbed-os/main.cpp create mode 100644 onert-micro/tests/mbed-os/mbed-sources.cmake create mode 100644 onert-micro/tests/mbed-os/mbed_config.h create mode 100644 onert-micro/tests/mbed-os/startup_stm32h743xx.S create mode 100644 packaging/EGL_HEADERS.tar.gz delete mode 100644 packaging/FLATBUFFERS-1.12.tar.gz create mode 100644 packaging/FLATBUFFERS-2.0.tar.gz create mode 100644 packaging/OPENGL_HEADERS.tar.gz delete mode 100644 packaging/TENSORFLOW-2.3.0-EIGEN.tar.gz create mode 100644 packaging/TENSORFLOW-2.8.0-EIGEN.tar.gz create mode 100644 packaging/TENSORFLOW-2.8.0-GEMMLOWP.tar.gz create mode 100644 packaging/TENSORFLOW-2.8.0.tar.gz delete mode 100644 packaging/TENSORFLOW_GPU.tar.gz create mode 100644 packaging/VULKAN.tar.gz create mode 100644 res/PyTorchExamples/examples/PReLUwConv1d/__init__.py create mode 100644 res/PyTorchExamples/examples/PReLUwConv2d/__init__.py create mode 100644 res/TensorFlowLiteRecipes/FullyConnected_008/test.recipe create mode 100644 res/TensorFlowLiteRecipes/FullyConnected_008/test.reverse create mode 100644 res/TensorFlowLiteRecipes/FullyConnected_008/test.rule create mode 100644 res/TensorFlowLiteRecipes/FullyConnected_009/test.recipe create mode 100644 res/TensorFlowLiteRecipes/FullyConnected_009/test.reverse create mode 100644 res/TensorFlowLiteRecipes/Net_Conv_PReluGraph_000/test.recipe create mode 100644 res/TensorFlowLiteRecipes/Net_Conv_PReluGraph_000/test.rule create mode 100644 res/TensorFlowLiteRecipes/Net_Duplicate_Weights_000/test.recipe create mode 100644 res/TensorFlowLiteRecipes/Net_Duplicate_Weights_000/test.rule create mode 100644 res/TensorFlowLiteRecipes/Net_FullyConnected_Add_000/test.recipe create mode 100644 res/TensorFlowLiteRecipes/Net_FullyConnected_Add_000/test.rule create mode 100644 res/TensorFlowLiteRecipes/Net_TConv_BN_005/test.recipe create mode 100644 res/TensorFlowLiteRecipes/Net_TConv_BN_005/test.rule create mode 100644 res/TensorFlowLiteRecipes/Net_Transpose_Abs_000/test.recipe create mode 100644 res/TensorFlowLiteRecipes/Net_Transpose_Abs_000/test.reverse create mode 100644 res/TensorFlowLiteRecipes/Net_Transpose_Add_000/test.recipe create mode 100644 res/TensorFlowLiteRecipes/Net_Transpose_Add_000/test.reverse create mode 100644 res/TensorFlowLiteRecipes/Quant_Conv_005/test.recipe create mode 100644 res/TensorFlowLiteRecipes/Quant_Conv_005/test.rule create mode 100644 res/TensorFlowLiteRecipes/Quant_Conv_006/test.recipe create mode 100644 res/TensorFlowLiteRecipes/Quant_Conv_006/test.rule create mode 100644 res/TensorFlowLiteRecipes/Quant_DepthToSpace_000/test.recipe create mode 100644 res/TensorFlowLiteRecipes/Quant_DepthToSpace_000/test.rule create mode 100644 res/TensorFlowLiteRecipes/Quant_SpaceToDepth_000/test.recipe create mode 100644 res/TensorFlowLiteRecipes/Quant_SpaceToDepth_000/test.rule create mode 100644 res/TensorFlowLiteRecipes/UnidirectionalSequenceLSTM_002/test.recipe create mode 100644 res/TensorFlowLiteRecipes/UnidirectionalSequenceLSTM_002/test.reverse create mode 100644 res/TensorFlowLiteRecipes/UnidirectionalSequenceLSTM_003/test.recipe create mode 100644 res/TensorFlowLiteRecipes/UnidirectionalSequenceLSTM_003/test.rule create mode 100644 res/TensorFlowLiteRecipes/UnidirectionalSequenceLSTM_004/test.recipe create mode 100644 res/TensorFlowLiteRecipes/UnidirectionalSequenceLSTM_004/test.rule create mode 100644 res/TensorFlowPythonExamples/examples/GRU_unroll/__init__.py create mode 100644 res/TensorFlowPythonExamples/examples/LSTM_batsize/__init__.py create mode 100644 res/TensorFlowPythonExamples/examples/LSTM_retseq/__init__.py create mode 100644 res/TensorFlowPythonExamples/examples/LSTM_unroll/__init__.py create mode 100644 res/TensorFlowPythonExamples/examples/RNN_GRUCell_unroll/__init__.py create mode 100644 res/TensorFlowPythonExamples/examples/RNN_LSTMCell_unroll/__init__.py create mode 100644 res/TensorFlowPythonExamples/examples/SimpleRNN_unroll/__init__.py delete mode 100644 runtime/contrib/android_tflite/CMakeLists.txt delete mode 100644 runtime/contrib/android_tflite/builtin_ops_jni.cc delete mode 100644 runtime/libs/tflite/include/tflite/CopyInputInitializer.h delete mode 100644 runtime/libs/tflite/include/tflite/FeatureView.h delete mode 100644 runtime/libs/tflite/include/tflite/NNAPISession.h delete mode 100644 runtime/libs/tflite/include/tflite/OutputIndex.h delete mode 100644 runtime/libs/tflite/include/tflite/OutputResetter.h delete mode 100644 runtime/libs/tflite/include/tflite/Quantization.h delete mode 100644 runtime/libs/tflite/include/tflite/RandomTestRunner.h delete mode 100644 runtime/libs/tflite/include/tflite/TensorLogger.h delete mode 100644 runtime/libs/tflite/include/tflite/TensorUtils.h delete mode 100644 runtime/libs/tflite/include/tflite/interp/Builder.h delete mode 100644 runtime/libs/tflite/include/tflite/interp/FlatBufferBuilder.h delete mode 100644 runtime/libs/tflite/include/tflite/interp/FunctionBuilder.h delete mode 100644 runtime/libs/tflite/src/CopyInputInitializer.cpp delete mode 100644 runtime/libs/tflite/src/FeatureView.cpp delete mode 100644 runtime/libs/tflite/src/OutputResetter.cpp delete mode 100644 runtime/libs/tflite/src/Quantization.cpp delete mode 100644 runtime/libs/tflite/src/RandomTestRunner.cpp delete mode 100644 runtime/libs/tflite/src/interp/FlatBufferBuilder.cpp delete mode 100644 runtime/libs/tflite/src/interp/FunctionBuilder.cpp delete mode 100644 runtime/onert/backend/gpu_cl/TensorBuilderHelper.h create mode 100644 runtime/onert/backend/gpu_cl/Utils.h delete mode 100644 runtime/onert/backend/gpu_cl/ex/InferenceContextEx.h create mode 100644 runtime/onert/backend/trix/BatchThreadPool.cc create mode 100644 runtime/onert/backend/trix/BatchThreadPool.h create mode 100644 runtime/onert/backend/trix/Convert.cc create mode 100644 runtime/onert/backend/trix/Convert.h create mode 100644 runtime/onert/backend/trix/DevContext.cc create mode 100644 runtime/onert/core/include/compiler/CompilerFactory.h create mode 100644 runtime/onert/core/include/compiler/CompilerOptions.h create mode 100644 runtime/onert/core/include/compiler/ICompiler.h delete mode 100644 runtime/onert/core/include/exec/Executors.h create mode 100644 runtime/onert/core/include/exec/IExecutors.h create mode 100644 runtime/onert/core/src/compiler/CompilerFactory.cc create mode 100644 runtime/onert/core/src/compiler/CompilerOptions.cc create mode 100644 runtime/onert/core/src/compiler/MultiModelCompiler.cc create mode 100644 runtime/onert/core/src/compiler/MultiModelCompiler.h create mode 100644 runtime/onert/core/src/exec/Executors.h create mode 100644 runtime/onert/core/src/exec/IPermuteFunction.cc create mode 100644 runtime/onert/core/src/exec/IPermuteFunction.test.cc create mode 100644 runtime/onert/core/src/exec/SingleModelExecutors.cc create mode 100644 runtime/onert/core/src/exec/SingleModelExecutors.h delete mode 100644 runtime/onert/core/src/interp/Buffer.h delete mode 100644 runtime/onert/core/src/interp/ExecEnv.h delete mode 100644 runtime/onert/core/src/interp/InterpExecutor.cc delete mode 100644 runtime/onert/core/src/interp/InterpExecutor.h delete mode 100644 runtime/onert/core/src/interp/InterpExecutor.test.cc delete mode 100644 runtime/onert/core/src/interp/InterpOps.lst delete mode 100644 runtime/onert/core/src/interp/Interpreter.cc delete mode 100644 runtime/onert/core/src/interp/Interpreter.h delete mode 100644 runtime/onert/core/src/interp/Registration.h delete mode 100644 runtime/onert/core/src/interp/Tensor.cc delete mode 100644 runtime/onert/core/src/interp/Tensor.h delete mode 100644 runtime/onert/core/src/interp/operations/BinaryArithmeticOps.cc delete mode 100644 runtime/onert/core/src/interp/operations/Concat.cc delete mode 100644 runtime/onert/core/src/interp/operations/Conv2D.cc delete mode 100644 runtime/onert/core/src/interp/operations/DepthwiseConv2D.cc delete mode 100644 runtime/onert/core/src/interp/operations/ElementwiseActivations.cc delete mode 100644 runtime/onert/core/src/interp/operations/FullyConnected.cc delete mode 100644 runtime/onert/core/src/interp/operations/Gather.cc delete mode 100644 runtime/onert/core/src/interp/operations/InstanceNorm.cc delete mode 100644 runtime/onert/core/src/interp/operations/OperationUtil.h delete mode 100644 runtime/onert/core/src/interp/operations/Pad.cc delete mode 100644 runtime/onert/core/src/interp/operations/Pool2D.cc delete mode 100644 runtime/onert/core/src/interp/operations/Reshape.cc delete mode 100644 runtime/onert/core/src/interp/operations/Softmax.cc delete mode 100644 runtime/onert/core/src/interp/operations/TransposeConv.cc create mode 100644 runtime/service/npud/backend/CMakeLists.txt create mode 100644 runtime/service/npud/backend/trix/CMakeLists.txt create mode 100644 runtime/service/npud/backend/trix/TrixBackend.cc create mode 100644 runtime/service/npud/backend/trix/TrixBackend.h create mode 100644 runtime/service/npud/core/Backend.h create mode 100644 runtime/service/npud/core/CMakeLists.txt create mode 100644 runtime/service/npud/core/ContextManager.cc create mode 100644 runtime/service/npud/core/ContextManager.h create mode 100644 runtime/service/npud/core/Core.cc create mode 100644 runtime/service/npud/core/Core.h create mode 100644 runtime/service/npud/core/DBus.cc create mode 100644 runtime/service/npud/core/DBus.h create mode 100644 runtime/service/npud/core/DevManager.cc create mode 100644 runtime/service/npud/core/DevManager.h create mode 100644 runtime/service/npud/core/DynamicLoader.cc create mode 100644 runtime/service/npud/core/DynamicLoader.h create mode 100644 runtime/service/npud/core/ir/DataType.h create mode 100644 runtime/service/npud/core/ir/Layout.h create mode 100644 runtime/service/npud/core/util/Config.lst rename runtime/service/npud/{ => core}/util/ConfigSource.cc (100%) rename runtime/service/npud/{ => core}/util/ConfigSource.h (100%) rename runtime/service/npud/{ => core}/util/Logging.h (100%) create mode 100644 runtime/service/npud/org.tizen.npud.conf create mode 100644 runtime/service/npud/org.tizen.npud.xml create mode 100644 runtime/service/npud/tests/CMakeLists.txt create mode 100644 runtime/service/npud/tests/core/DBus.test.cc create mode 100644 runtime/service/npud/tests/core/Server.test.cc create mode 100644 runtime/service/npud/tests/core/Signal.test.cc delete mode 100644 runtime/service/npud/util/Config.lst delete mode 100644 tests/nnapi/nnapi_gtest.skip.noarch.interp delete mode 100644 tests/nnfw_api/src/ValidationTestPipelineSession.test.cc delete mode 100755 tests/scripts/benchmark_nnapi.sh create mode 100755 tests/scripts/benchmark_ops.sh delete mode 100644 tests/scripts/list/nnapi_test.aarch64.list delete mode 100644 tests/scripts/list/nnapi_test.armv7l.list delete mode 100644 tests/scripts/list/nnpkg_test_list.noarch.interp delete mode 100644 tests/scripts/list/tflite_comparator.noarch.interp.list delete mode 100755 tests/scripts/test-driver.sh delete mode 100755 tests/scripts/test_framework.sh delete mode 100644 tests/tools/nnapi_test/CMakeLists.txt delete mode 100644 tests/tools/nnapi_test/src/args.cc delete mode 100644 tests/tools/nnapi_test/src/args.h delete mode 100644 tests/tools/nnapi_test/src/nnapi_test.cc delete mode 100644 tests/tools/nnpackage_run/CMakeLists.txt delete mode 100644 tests/tools/nnpackage_run/README.md delete mode 100644 tests/tools/nnpackage_run/src/allocation.h delete mode 100644 tests/tools/nnpackage_run/src/args.cc delete mode 100644 tests/tools/nnpackage_run/src/args.h delete mode 100644 tests/tools/nnpackage_run/src/formatter.h delete mode 100644 tests/tools/nnpackage_run/src/h5formatter.cc delete mode 100644 tests/tools/nnpackage_run/src/h5formatter.h delete mode 100644 tests/tools/nnpackage_run/src/nnfw_util.cc delete mode 100644 tests/tools/nnpackage_run/src/nnfw_util.h delete mode 100644 tests/tools/nnpackage_run/src/nnpackage_run.cc delete mode 100644 tests/tools/nnpackage_run/src/randomgen.cc delete mode 100644 tests/tools/nnpackage_run/src/randomgen.h delete mode 100644 tests/tools/nnpackage_run/src/rawformatter.cc delete mode 100644 tests/tools/nnpackage_run/src/rawformatter.h delete mode 100644 tests/tools/nnpackage_run/src/types.h create mode 100644 tests/tools/onert_run/CMakeLists.txt create mode 100644 tests/tools/onert_run/README.md create mode 100644 tests/tools/onert_run/src/allocation.h create mode 100644 tests/tools/onert_run/src/args.cc create mode 100644 tests/tools/onert_run/src/args.h create mode 100644 tests/tools/onert_run/src/formatter.h create mode 100644 tests/tools/onert_run/src/h5formatter.cc create mode 100644 tests/tools/onert_run/src/h5formatter.h create mode 100644 tests/tools/onert_run/src/nnfw_util.cc create mode 100644 tests/tools/onert_run/src/nnfw_util.h create mode 100644 tests/tools/onert_run/src/onert_run.cc create mode 100644 tests/tools/onert_run/src/randomgen.cc create mode 100644 tests/tools/onert_run/src/randomgen.h create mode 100644 tests/tools/onert_run/src/rawformatter.cc create mode 100644 tests/tools/onert_run/src/rawformatter.h create mode 100644 tests/tools/onert_run/src/types.h delete mode 100644 tests/tools/tflite_benchmark_model/CMakeLists.txt delete mode 100644 tests/tools/tflite_benchmark_model/README.md delete mode 100644 tests/tools/tflite_benchmark_model/benchmark_tflite_model.cc delete mode 100644 tests/tools/tflite_benchmark_model/profile_summarizer.cc delete mode 100644 tests/tools/tflite_benchmark_model/stats_calculator.cc delete mode 100644 tests/tools/tflite_vanilla_run/CMakeLists.txt delete mode 100644 tests/tools/tflite_vanilla_run/src/args.cc delete mode 100644 tests/tools/tflite_vanilla_run/src/args.h delete mode 100644 tests/tools/tflite_vanilla_run/src/tensor_view.h delete mode 100644 tests/tools/tflite_vanilla_run/src/tflite_vanilla_run.cc create mode 100755 tools/nnpackage_tool/model2nnpkg/model2nnpkg.py create mode 100644 tools/nnpackage_tool/qnf/qnf.md create mode 100644 tools/nnpackage_tool/qnf/qnf.py create mode 100644 tools/nnpackage_tool/qnf/requirements.txt diff --git a/.ahub/tcchecker-tca/config.yaml b/.ahub/tcchecker-tca/config.yaml index 73ec548..154fbf9 100644 --- a/.ahub/tcchecker-tca/config.yaml +++ b/.ahub/tcchecker-tca/config.yaml @@ -8,6 +8,7 @@ test: - /runtime/libs/misc - /runtime/libs/ndarray - /runtime/onert + - /runtime/service/npud/tests - /tests/nnfw_api testFile: diff --git a/.gitattributes b/.gitattributes index 3ef12ef..d106893 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,10 +7,11 @@ res/** linguist-detectable=false * text eol=lf # Binary - ignore text file setting -*.caffemodel -text -*.png -text -*.pdf -text -*.h5 -text -*.tar.gz -text -*.tflite -text -*.bmp -text +*.bmp binary +*.caffemodel binary +*.h5 binary +*.jar binary +*.pdf binary +*.png binary +*.tar.gz binary +*.tflite binary diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index bcbc3c5..dcfb8d5 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -21,10 +21,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.x' @@ -54,7 +54,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # Fetch all history and branch (default: 1) # Require all history to get file creation date diff --git a/.github/workflows/check-pr-commit.yml b/.github/workflows/check-pr-commit.yml index a3f4c1c..7fa84b1 100644 --- a/.github/workflows/check-pr-commit.yml +++ b/.github/workflows/check-pr-commit.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # Checkout PR head commit # Checkout Action use merge commit as default diff --git a/.github/workflows/deploy-github-pages.yml b/.github/workflows/deploy-github-pages.yml new file mode 100644 index 0000000..d474a27 --- /dev/null +++ b/.github/workflows/deploy-github-pages.yml @@ -0,0 +1,40 @@ +name: Build and deploy github page + +on: + schedule: + # Every weeks + - cron: '30 19 * * SUN' + workflow_dispatch: + inputs: + publish: + description: 'Push to github page branch or not' + required: true + default: false + type: boolean + +jobs: + build_and_deploy: + name: 'Deploy doxygen page' + runs-on: 'ubuntu-latest' + if: github.repository_owner == 'Samsung' + + steps: + - name: 'Checkout' + uses: actions/checkout@v3 + - name: 'Generate HTML' + uses: mattnotmitt/doxygen-action@v1.9 + with: + doxyfile-path: 'infra/doxygen/Doxyfile' + - name: 'Tar artifact' + run: tar -zcf doxygen.tar.gz -C doxygen/html ./ + - name: 'Generate artifact' + uses: actions/upload-artifact@v3 + with: + name: doxygen-html + path: doxygen.tar.gz + - name: 'Deploy' + if: ${{ github.event_name == 'schedule' || github.event.inputs.publish == 'true' }} + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: doxygen/html + branch: gh-pages diff --git a/Makefile.template b/Makefile.template index 1f194fa..2e5d0bd 100644 --- a/Makefile.template +++ b/Makefile.template @@ -1,3 +1,5 @@ +#!/usr/bin/make -f + HOST_ARCH?=$(shell uname -m) TARGET_ARCH?=$(shell uname -m) BUILD_TYPE?=Debug @@ -5,7 +7,6 @@ CROSS_BUILD?=0 HOST_OS?=linux TARGET_OS?=linux COVERAGE_BUILD?=0 -BENCHMARK_ACL_BUILD?=0 OPTIONS?= # make TARGET and TYPE to lowercase @@ -22,9 +23,8 @@ else ifneq (,$(findstring aarch64,$(TARGET_ARCH_BASE))) TARGET_ARCH_LC=aarch64 endif ifneq (,$(findstring android,$(TARGET_OS))) - # Anndroid only allow aarch64 target-arch + # Android only allow aarch64 target-arch TARGET_ARCH_LC=aarch64 - TARGET_OS=android endif # Set CROSS_BUILD=1 when ROOTFS_DIR is given, and TARGET_ARCH is different to HOST_ARCH. ifneq ($(ROOTFS_DIR),) @@ -38,19 +38,14 @@ ifeq ($(CROSS_BUILD),1) OPTIONS+= -DCMAKE_TOOLCHAIN_FILE=$(TOOLCHAIN_FILE) endif -ifeq ($(COVERAGE_BUILD),1) +ifneq ($(filter create-covsuite,$(MAKECMDGOALS)),) OPTIONS+= -DENABLE_COVERAGE=ON else - OPTIONS+= -DENABLE_COVERAGE=OFF -endif - -ifeq ($(BENCHMARK_ACL_BUILD),1) - OPTIONS+= -DBUILD_BENCHMARK_ACL=1 -endif - -ifneq ($(EXT_HDF5_DIR),) - $(info Hello $(EXT_HDF5_DIR)) - OPTIONS+= -DEXT_HDF5_DIR=$(EXT_HDF5_DIR) + ifeq ($(COVERAGE_BUILD),1) + OPTIONS+= -DENABLE_COVERAGE=ON + else + OPTIONS+= -DENABLE_COVERAGE=OFF + endif endif ifneq ($(EXTERNAL_VOLUME),) @@ -87,17 +82,23 @@ WORKHOME=$(CURDIR)/Product WORKFOLDER=$(TARGET_ARCH_LC)-$(TARGET_OS).$(BUILD_TYPE_LC) WORKSPACE=$(WORKHOME)/$(WORKFOLDER) -BUILD_FOLDER=$(WORKSPACE)/obj INSTALL_PATH?=$(WORKSPACE)/out OVERLAY_FOLDER?=$(WORKSPACE)/overlay -BUILD_ALIAS=$(WORKHOME)/obj INSTALL_ALIAS=$(WORKHOME)/out TIMESTAMP_CONFIGURE=$(WORKSPACE)/CONFIGURE TIMESTAMP_BUILD=$(WORKSPACE)/BUILD TIMESTAMP_INSTALL=$(WORKSPACE)/INSTALL -all: build +### +### Common environment variable +### +export NNFW_WORKSPACE=$(WORKSPACE) + +### +### Default target +### +all: install ### ### Command (public) @@ -106,78 +107,76 @@ configure: configure_internal build: build_internal -install: $(TIMESTAMP_INSTALL) +install: install_all_internal -create_package: runtime_tar_internal +create-package: runtime_tar_internal -create_acl_tar: acl_tar_internal +create-aclpack: acl_tar_internal + +create-testsuite: test_suite_internal + +create-covsuite: coverage_suite_internal clean: rm -rf $(WORKSPACE) distclean: - rm -rf $(WORKSPACE) - rm -rf externals/*.stamp + rm -rf Product + rm -rf externals rm -rf tests/nnapi/src/generated/ +# create_package, create_acl_tar: to be removed +create_package: runtime_tar_internal +create_acl_tar: acl_tar_internal + ### ### Command (internal) ### -configure_internal: -# TODO Remove setting EXT_ACL_FOLDER -# Construct overlay folder directly outside (with headers?) -ifneq ($(EXT_ACL_FOLDER),) - mkdir -p $(OVERLAY_FOLDER)/lib - cp $(EXT_ACL_FOLDER)/* $(OVERLAY_FOLDER)/lib -# Make stamp file - printf "21.02" > $(OVERLAY_FOLDER)/ARMCOMPUTE.stamp -endif +$(WORKSPACE): + mkdir -p $@ +configure_internal: $(WORKSPACE) ifneq ($(DEBIAN_BUILD),) test -d externals || mkdir -p externals find packaging/ -type f -name "*.tar.gz" | xargs -i tar xf {} -C externals endif - - NNFW_WORKSPACE="$(WORKSPACE)" NNFW_INSTALL_PREFIX=$(INSTALL_PATH) ./nnfw configure \ + NNFW_INSTALL_PREFIX=$(INSTALL_PATH) ./nnfw configure \ -DCMAKE_BUILD_TYPE=$(BUILD_TYPE_LC) \ -DNNFW_OVERLAY_DIR=$(OVERLAY_FOLDER) \ -DEXTERNALS_BUILD_THREADS=$(NPROCS) \ $(OPTIONS) - touch $(TIMESTAMP_CONFIGURE) -build_internal: $(BUILD_FOLDER) - NNFW_WORKSPACE="$(WORKSPACE)" ./nnfw build -j $(NPROCS) - rm -rf $(BUILD_ALIAS) - ln -s $(BUILD_FOLDER) $(BUILD_ALIAS) - touch $(TIMESTAMP_BUILD) +build_internal: configure_internal + ./nnfw build -j $(NPROCS) -install_internal: - NNFW_WORKSPACE="$(WORKSPACE)" ./nnfw install +install_internal: build_internal + ./nnfw install rm -rf $(INSTALL_ALIAS) ln -s $(INSTALL_PATH) $(INSTALL_ALIAS) - touch $(TIMESTAMP_INSTALL) -runtime_tar_internal: $(TIMESTAMP_BUILD) install_internal +runtime_tar_internal: build_internal install_internal tar -zcf $(WORKSPACE)/onert-package.tar.gz -C $(INSTALL_PATH) lib tar -zcf $(WORKSPACE)/onert-devel-package.tar.gz -C $(INSTALL_PATH) include/nnfw tar -zcf $(WORKSPACE)/onert-plugin-devel-package.tar.gz -C $(INSTALL_PATH) include/onert tar -zcf $(WORKSPACE)/onert-test-package.tar.gz -C $(INSTALL_PATH) $(shell ls $(INSTALL_PATH) -I lib -I include) -acl_tar_internal: $(BUILD_FOLDER) +acl_tar_internal: configure_internal tar -zcf $(WORKSPACE)/onert-acl.tar.gz -C ${OVERLAY_FOLDER} lib/libarm_compute.so lib/libarm_compute_core.so lib/libarm_compute_graph.so -install_internal_acl: +install_acl_internal: # Workaround to install acl for test (ignore error when there is no file to copy) cp $(OVERLAY_FOLDER)/lib/libarm_compute*.so $(INSTALL_ALIAS)/lib || true -build_test_suite: install_internal install_internal_acl +install_all_internal: install_internal install_acl_internal + +test_suite_internal: install_all_internal @echo "packaging test suite" @rm -rf $(INSTALL_PATH)/test-suite.tar.gz # TODO Divide runtime package, external library package, and test suite @tar -zcf test-suite.tar.gz tests/scripts infra Product/out --dereference @mv test-suite.tar.gz $(INSTALL_PATH)/. -build_coverage_suite: install_internal install_internal_acl +coverage_suite_internal: install_all_internal @echo "packaging test-coverage suite" @rm -rf $(INSTALL_PATH)/coverage-suite.tar.gz @find Product -name "*.gcno" > include_lists.txt @@ -185,17 +184,3 @@ build_coverage_suite: install_internal install_internal_acl @tar -zcf coverage-suite.tar.gz tests/scripts infra Product/out --dereference -T include_lists.txt @rm -rf include_lists.txt tests/scripts/build_path_depth.txt @mv coverage-suite.tar.gz $(INSTALL_PATH)/. - -### -### Timestamps -### -$(WORKSPACE): - mkdir -p $@ - -$(BUILD_FOLDER): $(WORKSPACE) configure_internal - -$(TIMESTAMP_CONFIGURE): configure_internal - -$(TIMESTAMP_BUILD): $(TIMESTAMP_CONFIGURE) build_internal - -$(TIMESTAMP_INSTALL): $(TIMESTAMP_BUILD) install_internal install_internal_acl diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index 7cf12f1..2588cf0 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -1,3 +1,21 @@ +# get CODENAME to perform per codename actions +# set focal as default +set(ONE_UBUNTU_CODENAME "focal") +find_program(LSB_RELEASE_EXEC lsb_release) +if(LSB_RELEASE_EXEC) + # output should be one of 'bionic', 'focal', 'jammy' + # others are not tested + execute_process(COMMAND "${LSB_RELEASE_EXEC}" --short --codename + OUTPUT_VARIABLE ONE_UBUNTU_CODENAME + OUTPUT_STRIP_TRAILING_WHITESPACE) +else() + message(STATUS "WARNING: lsb_release not found") +endif() + +if(${ONE_UBUNTU_CODENAME} STREQUAL "jammy") + set(ONE_UBUNTU_CODENAME_JAMMY TRUE) +endif() + # TODO Validate the argument of "requires" function(get_project_build_order VAR) # This file will describe the dependencies among projects diff --git a/compiler/circle-eval-diff/src/InputDataLoader.cpp b/compiler/circle-eval-diff/src/InputDataLoader.cpp index 99276f3..f15b2ba 100644 --- a/compiler/circle-eval-diff/src/InputDataLoader.cpp +++ b/compiler/circle-eval-diff/src/InputDataLoader.cpp @@ -55,6 +55,7 @@ std::vector getEachByteSizeOf(const std::vector &nodes) for (const auto node : nodes) { const auto input_node = loco::must_cast(node); + const auto dtype_size = loco::size(input_node->dtype()); size_t element_size = 1; for (uint32_t index = 0; index < input_node->rank(); index++) @@ -62,7 +63,7 @@ std::vector getEachByteSizeOf(const std::vector &nodes) element_size *= input_node->dim(index).value(); } - vec.push_back(element_size); + vec.push_back(element_size * dtype_size); } return vec; diff --git a/compiler/circle-execution-plan/CMakeLists.txt b/compiler/circle-execution-plan/CMakeLists.txt index da74e02..0320b52 100644 --- a/compiler/circle-execution-plan/CMakeLists.txt +++ b/compiler/circle-execution-plan/CMakeLists.txt @@ -26,6 +26,7 @@ target_link_libraries(circle_execution_plan luci_import) target_link_libraries(circle_execution_plan luci_export) target_link_libraries(circle_execution_plan luci_plan) target_link_libraries(circle_execution_plan arser) +target_link_libraries(circle_execution_plan luci_log) target_include_directories(circle_execution_plan PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/pal") install(TARGETS circle_execution_plan DESTINATION bin) diff --git a/compiler/circle-execution-plan/pal/TargetPlatform.h b/compiler/circle-execution-plan/pal/TargetPlatform.h index 538a502..7b210d6 100644 --- a/compiler/circle-execution-plan/pal/TargetPlatform.h +++ b/compiler/circle-execution-plan/pal/TargetPlatform.h @@ -27,6 +27,18 @@ enum SupportedPlatformType CMSISNN }; +enum RuntimeType +{ + ONERT_MICRO, + LUCI_INTERPRETER +}; + +enum AllocatingMode +{ + COMMON, + SPLIT +}; + struct TargetPlatform { SupportedPlatformType platform_type; diff --git a/compiler/circle-execution-plan/src/CircleExecutionPlan.cpp b/compiler/circle-execution-plan/src/CircleExecutionPlan.cpp index d5ddf0c..345bc05 100644 --- a/compiler/circle-execution-plan/src/CircleExecutionPlan.cpp +++ b/compiler/circle-execution-plan/src/CircleExecutionPlan.cpp @@ -36,6 +36,29 @@ int entry(int argc, char **argv) arser.add_argument("input").help("Input circle model"); arser.add_argument("output").help("Output circle model"); arser.add_argument("--platform").default_value("linux").help("Platform name: linux mcu cmsisnn"); + arser.add_argument("--allocating_mode") + .default_value("common") + .help("Buffer type name (only onert-micro option):" + "common - a single buffer is considered for all allocations" + "split - there are three buffers: for input," + " for output and for intermediate tensors"); + arser.add_argument("--runtime") + .default_value("onert_micro") + .help("Target runtime name: luci-interpreter onert-micro"); + arser.add_argument("--allocate_const") + .nargs(1) + .type(arser::DataType::BOOL) + .required(false) + .default_value(false) + .help("Whether or not to take into account constants in memory allocation. " + "Default value - false, constants are not counted when allocating memory"); + arser.add_argument("--allocate_input") + .nargs(1) + .type(arser::DataType::BOOL) + .required(false) + .default_value(true) + .help("Whether or not to take into account inputs in memory allocation. " + "Default value - true, inputs are counted when allocating memory"); arser.add_argument("--use_dsp") .nargs(1) .type(arser::DataType::BOOL) @@ -64,7 +87,11 @@ int entry(int argc, char **argv) const std::string input_path = arser.get("input"); const std::string output_path = arser.get("output"); const std::string platform_name = arser.get("--platform"); + const std::string allocating_mode_name = arser.get("--allocating_mode"); + const std::string runtime_name = arser.get("--runtime"); const bool use_dsp = arser.get("--use_dsp"); + const bool is_allocate_const = arser.get("--allocate_const"); + const bool is_allocate_input = arser.get("--allocate_input"); const std::string json_path = arser.get("--save_allocations"); if (platform_name != "cmsisnn" && use_dsp) @@ -92,6 +119,44 @@ int entry(int argc, char **argv) return EXIT_FAILURE; } + circle_planner::AllocatingMode allocating_mode; + if (allocating_mode_name == "split") + { + allocating_mode = circle_planner::AllocatingMode::SPLIT; + } + else if (allocating_mode_name == "common") + { + allocating_mode = circle_planner::AllocatingMode::COMMON; + } + else + { + std::cerr << "ERROR: Invalid allocation mode name '" << allocating_mode_name << "'" + << std::endl; + return EXIT_FAILURE; + } + + circle_planner::RuntimeType runtime_type; + if (runtime_name == "onert-micro") + { + runtime_type = circle_planner::RuntimeType::ONERT_MICRO; + } + else if (runtime_name == "luci-interpreter") + { + runtime_type = circle_planner::RuntimeType::LUCI_INTERPRETER; + } + else + { + std::cerr << "ERROR: Invalid runtime name '" << runtime_name << "'" << std::endl; + return EXIT_FAILURE; + } + + if (allocating_mode == circle_planner::AllocatingMode::SPLIT and + runtime_type == circle_planner::RuntimeType::LUCI_INTERPRETER) + { + std::cerr << "Split buffer type can only be used with onert-micro runtime" << std::endl; + return EXIT_FAILURE; + } + bool is_save_allocations = false; if (!json_path.empty()) @@ -131,7 +196,9 @@ int entry(int argc, char **argv) auto module = importer.importModule(circle_model); // Do main job - circle_planner::ExecutionPlanner execution_planner(module->graph(), {platform_type, use_dsp}); + circle_planner::ExecutionPlanner execution_planner(module->graph(), {platform_type, use_dsp}, + runtime_type, allocating_mode); + execution_planner.change_planning_mode(is_allocate_const, is_allocate_input, true); execution_planner.make_execution_plan(); if (is_save_allocations) diff --git a/compiler/circle-execution-plan/src/ExecutionPlanner.cpp b/compiler/circle-execution-plan/src/ExecutionPlanner.cpp index a1e6f7e..fe028c0 100644 --- a/compiler/circle-execution-plan/src/ExecutionPlanner.cpp +++ b/compiler/circle-execution-plan/src/ExecutionPlanner.cpp @@ -17,10 +17,13 @@ #include "ExecutionPlanner.h" #include #include +#include #include #include +#include // std::numeric_limits + namespace circle_planner { namespace @@ -84,10 +87,164 @@ void create_allocation_node(Json::Value &allocations_node, allocations_node.append(allocation_node); } +// TODO: Introduce inplace optimization +bool can_be_inplace_optimization_node(luci::CircleNode *node) +{ + switch (node->opcode()) + { + case luci::CircleOpcode::LOGISTIC: + case luci::CircleOpcode::RESHAPE: + case luci::CircleOpcode::EXPAND_DIMS: + return true; + default: + return false; + } +} + } // namespace -void ExecutionPlanner::make_execution_plan() +void ExecutionPlanner::make_execution_plan_onert_micro_base() +{ + switch (_allocating_mode) + { + case AllocatingMode::COMMON: + make_execution_plan_onert_micro_common_buffer(); + break; + case AllocatingMode::SPLIT: + make_execution_plan_onert_micro_split_buffer(); + break; + default: + throw std::runtime_error("Unsupported buffer type\n"); + } +} + +void ExecutionPlanner::write_execution_plan(uint32_t order_offset) +{ + _required_size = get_offsets_with_greedy_by_size(); + + int32_t counter_ops = 0; + for (uint32_t i = 0; i < _ordered_nodes.size(); i++) + { + const auto circle_node = dynamic_cast(_ordered_nodes[i]); + if (circle_node->opcode() != luci::CircleOpcode::CIRCLECONST and + circle_node->opcode() != luci::CircleOpcode::CIRCLEOUTPUTEXCLUDE) + { + luci::CircleNodeExecutionPlan execution_plan(counter_ops + order_offset, _offsets[i]); + luci::add_execution_plan(loco::must_cast(_ordered_nodes[i]), + execution_plan); + counter_ops++; + } + } +} + +void ExecutionPlanner::make_execution_plan_onert_micro_split_buffer() +{ + LOGGER(l); + + const auto input_size = _graph->inputs()->size(); + const auto output_size = _graph->outputs()->size(); + + // Make execution plan for inputs + _ordered_nodes = loco::input_nodes(_graph); + write_execution_plan(0); + dump_inform(); + VERBOSE(l, 0) << "Input graph buffer required memory = " << _required_size << std::endl; + + // Clear structures for next buffer + _ordered_nodes.clear(); + _alloc_node_inform_vector.clear(); + _dealloc_node.clear(); + _alloc_node.clear(); + _offsets.clear(); + _required_size = 0; + + // Make execution plan for outputs + _ordered_nodes = loco::output_nodes(_graph); + write_execution_plan(input_size); + dump_inform(); + VERBOSE(l, 0) << "Output graph buffer required memory = " << _required_size << std::endl; + + // Clear structures for next buffer + _ordered_nodes.clear(); + _alloc_node_inform_vector.clear(); + _dealloc_node.clear(); + _alloc_node.clear(); + _offsets.clear(); + _required_size = 0; + + // Make execution plan for intermediates calculations + get_default_execution_order_plan_without_inputs_and_outputs(); + write_execution_plan(input_size + output_size); + dump_inform(); + VERBOSE(l, 0) << "Main graph buffer required memory = " << _required_size << std::endl; +} + +void ExecutionPlanner::make_execution_plan_onert_micro_common_buffer() +{ + LOGGER(l); + + get_default_execution_order_plan(); + _required_size = get_offsets_with_greedy_by_size(); + + // Find prev nodes for output nodes (actual graph output node, not luci::CircleOutput) + const auto output_nodes = loco::output_nodes(const_cast(_graph)); + std::vector output_prev_nodes; + for (const auto output_node : output_nodes) + { + const auto prev_nodes = loco::preds(output_node); + std::copy(prev_nodes.begin(), prev_nodes.end(), std::back_inserter(output_prev_nodes)); + } + const auto output_nodes_size = output_prev_nodes.size(); + + const auto inputs_nodes = loco::input_nodes(_graph); + const auto input_nodes_size = inputs_nodes.size(); + + int32_t counter_ops = 0; + for (uint32_t i = 0; i < _ordered_nodes.size(); i++) + { + const auto circle_node = dynamic_cast(_ordered_nodes[i]); + // First write to input nodes + if (circle_node->opcode() == luci::CircleOpcode::CIRCLEINPUT) + { + // Find input_position for proper position in execution order + const auto input_position = std::distance( + inputs_nodes.begin(), std::find(inputs_nodes.begin(), inputs_nodes.end(), circle_node)); + luci::CircleNodeExecutionPlan execution_plan(input_position, _offsets[i]); + luci::add_execution_plan(loco::must_cast(_ordered_nodes[i]), + execution_plan); + } + // Second write to actual output nodes (not luci::CircleOutput) + else if (std::find(output_prev_nodes.begin(), output_prev_nodes.end(), circle_node) != + output_prev_nodes.end()) + { + // Find output_position for proper position in execution order + const auto output_position = + std::distance(output_prev_nodes.begin(), + std::find(output_prev_nodes.begin(), output_prev_nodes.end(), circle_node)); + luci::CircleNodeExecutionPlan execution_plan(input_nodes_size + output_position, _offsets[i]); + luci::add_execution_plan(loco::must_cast(_ordered_nodes[i]), + execution_plan); + } + // Finally write to all intermediate nodes + else if (circle_node->opcode() != luci::CircleOpcode::CIRCLECONST and + circle_node->opcode() != luci::CircleOpcode::CIRCLEOUTPUTEXCLUDE) + { + luci::CircleNodeExecutionPlan execution_plan( + counter_ops + input_nodes_size + output_nodes_size, _offsets[i]); + luci::add_execution_plan(loco::must_cast(_ordered_nodes[i]), + execution_plan); + counter_ops++; + } + } + + dump_inform(); + VERBOSE(l, 0) << "Buffer required memory = " << _required_size << std::endl; +} + +void ExecutionPlanner::make_execution_plan_luci_interpreter() { + LOGGER(l); + get_default_execution_order_plan(); _required_size = get_offsets_with_greedy_by_size(); for (uint32_t i = 0; i < _ordered_nodes.size(); i++) @@ -96,6 +253,25 @@ void ExecutionPlanner::make_execution_plan() luci::add_execution_plan(loco::must_cast(_ordered_nodes[i]), execution_plan); } + + VERBOSE(l, 0) << "Buffer required memory = " << _required_size << std::endl; + dump_inform(); +} + +void ExecutionPlanner::make_execution_plan() +{ + switch (_runtime_type) + { + case ONERT_MICRO: + make_execution_plan_onert_micro_base(); + break; + case LUCI_INTERPRETER: + make_execution_plan_luci_interpreter(); + break; + default: + throw std::runtime_error("Unsupported runtime platform\n"); + } + auto settings = luci::UserSettings::settings(); settings->set(luci::UserSettings::Key::ExecutionPlanGen, true); } @@ -150,6 +326,34 @@ void ExecutionPlanner::get_default_execution_order_plan() _ordered_nodes = loco::postorder_traversal(loco::output_nodes(const_cast(_graph))); } +void ExecutionPlanner::get_default_execution_order_plan_without_inputs_and_outputs() +{ + // Get all nodes + _ordered_nodes = loco::postorder_traversal(loco::output_nodes(const_cast(_graph))); + + // Get real output nodes (not luci::CircleOutput) + const auto output_nodes = loco::output_nodes(const_cast(_graph)); + std::vector output_prev_nodes; + for (const auto output_node : output_nodes) + { + const auto prev_nodes = loco::preds(output_node); + std::copy(prev_nodes.begin(), prev_nodes.end(), std::back_inserter(output_prev_nodes)); + } + + // Remove input and real output nodes from _ordered_nodes + _ordered_nodes.erase( + std::remove_if(_ordered_nodes.begin(), _ordered_nodes.end(), + [&output_prev_nodes](auto node) { + const auto circle_node = dynamic_cast(node); + + return circle_node->opcode() == luci::CircleOpcode::CIRCLEINPUT or + circle_node->opcode() == luci::CircleOpcode::CIRCLEOUTPUT or + std::find(output_prev_nodes.begin(), output_prev_nodes.end(), node) != + output_prev_nodes.end(); + }), + _ordered_nodes.end()); +} + void ExecutionPlanner::get_usage_interval() { // Initialize vectors of first and last nodes for usage interval @@ -177,6 +381,8 @@ void ExecutionPlanner::get_usage_interval() for (auto &output_node : output_nodes(_graph)) { auto it = std::find(_ordered_nodes.begin(), _ordered_nodes.end(), output_node); + if (it == _ordered_nodes.end()) + continue; size_t index = std::distance(_ordered_nodes.begin(), it); usages_counts[index]++; } @@ -184,6 +390,8 @@ void ExecutionPlanner::get_usage_interval() for (auto &input_node : input_nodes(_graph)) { auto it = std::find(_ordered_nodes.begin(), _ordered_nodes.end(), input_node); + if (it == _ordered_nodes.end()) + continue; size_t index = std::distance(_ordered_nodes.begin(), it); usages_counts[index]++; allocate(0, index); @@ -261,7 +469,7 @@ uint32_t ExecutionPlanner::get_offsets_with_greedy_by_size() uint32_t ExecutionPlanner::greedy_by_size_approach() { size_t result_size = 0; - create_alloc_node_inform_vector(_is_null_consts, _is_null_inputs, _is_null_scratchpads); + create_alloc_node_inform_vector(); std::vector ordered_alloc_inform; for (auto ¤t_node : _alloc_node_inform_vector) { @@ -307,8 +515,7 @@ uint32_t ExecutionPlanner::greedy_by_size_approach() return result_size; } -void ExecutionPlanner::create_alloc_node_inform_vector(bool null_consts, bool null_inputs, - bool null_scratchpad) +void ExecutionPlanner::create_alloc_node_inform_vector() { auto node_compare = [this](const AllocationNodeInformation &alloc_1, const AllocationNodeInformation &alloc_2) { @@ -355,11 +562,15 @@ void ExecutionPlanner::create_alloc_node_inform_vector(bool null_consts, bool nu _alloc_node_inform_vector[i].last_node = _dealloc_node[i]; const auto *const_node = dynamic_cast(circle_node); - if (i == 0 && null_inputs) + if (circle_node->opcode() == luci::CircleOpcode::CIRCLEINPUT && not _is_allocate_inputs) + { + _alloc_node_inform_vector[i].size = 0; + } + else if (circle_node->opcode() == luci::CircleOpcode::CIRCLEOUTPUTEXCLUDE) { _alloc_node_inform_vector[i].size = 0; } - else if (const_node && null_consts) + else if (const_node && not _is_allocate_consts) { _alloc_node_inform_vector[i].size = 0; } @@ -374,7 +585,7 @@ void ExecutionPlanner::create_alloc_node_inform_vector(bool null_consts, bool nu // Scratchpad If needed std::vector scratchpad_sizes; - if (!null_scratchpad) + if (_is_allocate_scratchpads) { switch (circle_node->opcode()) { @@ -440,6 +651,7 @@ void ExecutionPlanner::create_alloc_node_inform_vector(bool null_consts, bool nu void ExecutionPlanner::dump_inform() { + LOGGER(l); uint32_t max_breadth = 0; for (uint32_t i = 0; i < _ordered_nodes.size(); i++) @@ -466,12 +678,14 @@ void ExecutionPlanner::dump_inform() } auto node = loco::must_cast(_ordered_nodes.at(i)); - printf("node_num = %d node_name = %s node_size = %d node_offset = %d node_breadth = " - "%u node_first_node = %d node_last_node = %d\n", - i, node->name().c_str(), current_node_it->size, current_node_it->offset, - current_node_it->breadth, current_node_it->first_node, current_node_it->last_node); + VERBOSE(l, 0) << "node_num = " << i << " node_name = " << node->name().c_str() + << " node_size = " << current_node_it->size + << " node_offset = " << current_node_it->offset + << " node_breadth = " << current_node_it->breadth + << " node_first_node = " << current_node_it->first_node + << " node_last_node = " << current_node_it->last_node << std::endl; } - printf("Lower bound is = %u\n", max_breadth); + VERBOSE(l, 0) << "Lower bound = " << max_breadth << std::endl; std::sort(_alloc_node_inform_vector.begin(), _alloc_node_inform_vector.end(), [](const AllocationNodeInformation &first, const AllocationNodeInformation &second) { if (first.breadth != second.breadth) diff --git a/compiler/circle-execution-plan/src/ExecutionPlanner.h b/compiler/circle-execution-plan/src/ExecutionPlanner.h index af3fba3..1a25518 100644 --- a/compiler/circle-execution-plan/src/ExecutionPlanner.h +++ b/compiler/circle-execution-plan/src/ExecutionPlanner.h @@ -70,7 +70,9 @@ public: _scratchpad_helper = std::make_unique(); } - explicit ExecutionPlanner(loco::Graph *graph, TargetPlatform target_platform) : _graph(graph) + explicit ExecutionPlanner(loco::Graph *graph, TargetPlatform target_platform, + RuntimeType runtime_type, AllocatingMode allocating_mode) + : _graph(graph), _runtime_type(runtime_type), _allocating_mode(allocating_mode) { switch (target_platform.platform_type) { @@ -94,29 +96,52 @@ public: void make_execution_plan(); // Method change planning mode: - // is_null_consts = true - constants are no longer taken into account when planning - // is_null_inputs = true - input are no longer taken into account when planning - // is_null_scratchpads = true - scratchpads are no longer taken into account when planning - void change_planning_mode(bool is_null_consts, bool is_null_inputs, bool is_null_scratchpads) + // is_allocate_consts = false - constants are no longer taken into account when planning + // is_allocate_inputs = false - input are no longer taken into account when planning + // is_allocate_scratchpads = false - scratchpads are no longer taken into account when planning + void change_planning_mode(bool is_allocate_consts, bool is_allocate_inputs, + bool is_allocate_scratchpads) { - _is_null_consts = is_null_consts; - _is_null_inputs = is_null_inputs; - _is_null_scratchpads = is_null_scratchpads; + _is_allocate_consts = is_allocate_consts; + _is_allocate_inputs = is_allocate_inputs; + _is_allocate_scratchpads = is_allocate_scratchpads; }; void create_json_allocation_file(const std::string &json_path); private: + // Save execution plan for onert-micro runtime base function. + // + // NOTE: First, according to ordered_node, the input nodes are written, + // then all outputs, finally all nodes in execution order. + // Constants are not written. + void make_execution_plan_onert_micro_base(); + + // Save execution plan for luci-interpreter runtime base function. + void make_execution_plan_luci_interpreter(); + + // Save execution plan for onert-micro runtime for common buffer type. + void make_execution_plan_onert_micro_common_buffer(); + + // Save execution plan for onert-micro runtime for common split type. + void make_execution_plan_onert_micro_split_buffer(); + // Method gets default execution order plan and saves it in _ordered_nodes vector. // There can be different variants of execution order and this method provides main one. void get_default_execution_order_plan(); + // Method gets default execution order plan, + // but without inputs and output nodes and saves it in _ordered_nodes vector + void get_default_execution_order_plan_without_inputs_and_outputs(); + // Method provides nodes with usage interval information. void get_usage_interval(); // Method dumps execution plan information. void dump_inform(); + void write_execution_plan(uint32_t order_offset); + // Method finds required offsets for all nodes from _ordered_nodes, using greedy by size approach. // It saves offsets in _offsets vector. // Return: required size of buffer. @@ -127,14 +152,13 @@ private: uint32_t greedy_by_size_approach(); // Method creates and fills _alloc_node_inform_vector with usage interval inform and node's sizes. - // null_consts = true - size of const nodes will be equal 0; - // null_inputs = true - size of input nodes will be equal 0; - // null_scratchpad = true - size of scratchpad nodes will be equal 0; + // _is_allocate_const = true - size of const nodes will be equal 0; + // _is_allocate_input = true - size of input nodes will be equal 0; + // _is_allocate_scratchpad = true - size of scratchpad nodes will be equal 0; // It using if we don't want to take input(const or scratchpads) nodes into account // when determining offsets and calculating the required buffer size. This is uses for // experiments. - void create_alloc_node_inform_vector(bool null_consts = false, bool null_inputs = false, - bool null_scratchpad = false); + void create_alloc_node_inform_vector(); // Stores allocation additional information for the all nodes from _graph. std::vector _alloc_node_inform_vector; @@ -164,16 +188,22 @@ private: // Calculate size of scratchpad tensors for current platform std::unique_ptr _scratchpad_helper; + // Supported runtime type + RuntimeType _runtime_type; + + // Supported buffers type + AllocatingMode _allocating_mode; + // Required memory size. uint32_t _required_size = 0; // Flags for choosing different planning modes: - // _is_null_consts = true - constants are no longer taken into account when planning - // _is_null_inputs = true - input are no longer taken into account when planning - // _is_null_scratchpads = true - scratchpads are no longer taken into account when planning - bool _is_null_consts = false; - bool _is_null_inputs = false; - bool _is_null_scratchpads = false; + // _is_allocate_consts = false - constants are no longer taken into account when planning + // _is_allocate_inputs = false - input are no longer taken into account when planning + // _is_allocate_scratchpads = false - scratchpads are no longer taken into account when planning + bool _is_allocate_consts = true; + bool _is_allocate_inputs = true; + bool _is_allocate_scratchpads = true; }; } // namespace circle_planner diff --git a/compiler/circle-inspect/driver/Driver.cpp b/compiler/circle-inspect/driver/Driver.cpp index 318a582..4fa6069 100644 --- a/compiler/circle-inspect/driver/Driver.cpp +++ b/compiler/circle-inspect/driver/Driver.cpp @@ -34,6 +34,7 @@ int entry(int argc, char **argv) arser.add_argument("--conv2d_weight") .nargs(0) .help("Dump Conv2D series weight operators in circle file"); + arser.add_argument("--constants").nargs(0).help("Dump constant tensors name"); arser.add_argument("--op_version").nargs(0).help("Dump versions of the operators in circle file"); arser.add_argument("--tensor_dtype").nargs(0).help("Dump dtype of tensors"); arser.add_argument("circle").help("Circle file to inspect"); @@ -50,7 +51,7 @@ int entry(int argc, char **argv) } if (!arser["--operators"] && !arser["--conv2d_weight"] && !arser["--op_version"] && - !arser["--tensor_dtype"]) + !arser["--tensor_dtype"] && !arser["--constants"]) { std::cout << "At least one option must be specified" << std::endl; std::cout << arser; @@ -67,6 +68,8 @@ int entry(int argc, char **argv) dumps.push_back(std::make_unique()); if (arser["--tensor_dtype"]) dumps.push_back(std::make_unique()); + if (arser["--constants"]) + dumps.push_back(std::make_unique()); std::string model_file = arser.get("circle"); diff --git a/compiler/circle-inspect/src/Dump.cpp b/compiler/circle-inspect/src/Dump.cpp index aa8fed2..868fc2b 100644 --- a/compiler/circle-inspect/src/Dump.cpp +++ b/compiler/circle-inspect/src/Dump.cpp @@ -202,3 +202,36 @@ void DumpTensorDType::run(std::ostream &os, const circle::Model *model) } } // namespace circleinspect + +namespace circleinspect +{ + +void DumpConstants::run(std::ostream &os, const circle::Model *model) +{ + mio::circle::Reader reader(model); + + const uint32_t subgraph_size = reader.num_subgraph(); + + for (uint32_t g = 0; g < subgraph_size; g++) + { + reader.select_subgraph(g); + auto tensors = reader.tensors(); + + for (uint32_t i = 0; i < tensors->Length(); ++i) + { + const auto tensor = tensors->Get(i); + if (tensor->is_variable()) + continue; + + auto const buffer_id = tensor->buffer(); + + auto const buffer_size = reader.buffer_info(buffer_id, nullptr); + if (buffer_size == 0) + continue; + + os << reader.tensor_name(tensor) << std::endl; + } + } +} + +} // namespace circleinspect diff --git a/compiler/circle-inspect/src/Dump.h b/compiler/circle-inspect/src/Dump.h index 8ca6838..7ab1ebc 100644 --- a/compiler/circle-inspect/src/Dump.h +++ b/compiler/circle-inspect/src/Dump.h @@ -69,6 +69,15 @@ public: void run(std::ostream &os, const circle::Model *model); }; +class DumpConstants final : public DumpInterface +{ +public: + DumpConstants() = default; + +public: + void run(std::ostream &os, const circle::Model *model); +}; + } // namespace circleinspect #endif // __DUMP_H__ diff --git a/compiler/circle-interpreter-test/CMakeLists.txt b/compiler/circle-interpreter-test/CMakeLists.txt new file mode 100644 index 0000000..50cd0c6 --- /dev/null +++ b/compiler/circle-interpreter-test/CMakeLists.txt @@ -0,0 +1,17 @@ +if(NOT ENABLE_TEST) + return() +endif(NOT ENABLE_TEST) + +get_target_property(ARTIFACTS_PATH testDataGenerator BINARY_DIR) +get_target_property(CIRCLE_INTERPRETER_PATH circle-interpreter BINARY_DIR) +set(CIRCLE_INTERPRETER_PATH "${CIRCLE_INTERPRETER_PATH}/circle-interpreter") + +nnas_find_package(GTest REQUIRED) + +file(GLOB_RECURSE TESTS "src/*.test.cpp") +GTest_AddTest(circle-interpreter-test ${TESTS}) + +set_tests_properties(circle-interpreter-test + PROPERTIES + ENVIRONMENT "ARTIFACTS_PATH=${ARTIFACTS_PATH};CIRCLE_INTERPRETER_PATH=${CIRCLE_INTERPRETER_PATH}" + ) diff --git a/compiler/circle-interpreter-test/README.md b/compiler/circle-interpreter-test/README.md new file mode 100644 index 0000000..66f80e7 --- /dev/null +++ b/compiler/circle-interpreter-test/README.md @@ -0,0 +1,9 @@ +# circle-interpreter-test + +`circle-interpreter-test` checks if _circle-interpreter_ is working as expected. + +Current tests includes +- input arguments test +- output data test +- printing help message test +- validation of arguments and error message test diff --git a/compiler/circle-interpreter-test/requires.cmake b/compiler/circle-interpreter-test/requires.cmake new file mode 100644 index 0000000..8d8585b --- /dev/null +++ b/compiler/circle-interpreter-test/requires.cmake @@ -0,0 +1,2 @@ +require("common-artifacts") +require("circle-interpreter") diff --git a/compiler/circle-interpreter-test/src/circle-interpreter.test.cpp b/compiler/circle-interpreter-test/src/circle-interpreter.test.cpp new file mode 100644 index 0000000..4a5e81b --- /dev/null +++ b/compiler/circle-interpreter-test/src/circle-interpreter.test.cpp @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include + +#define READSIZE 4096 + +class circle_interpreter_test : public ::testing::Test +{ +protected: + bool initialize(void); + bool run(const std::string &command); + +protected: + bool compare(const std::string &file1, const std::string &file2); + +protected: + std::string _artifacts_path; + std::string _circle_interpreter_path; + std::string _result; +}; + +bool circle_interpreter_test::initialize(void) +{ + char *path = std::getenv("ARTIFACTS_PATH"); + if (path == nullptr) + { + std::cerr << "ARTIFACTS_PATH not found" << std::endl; + return false; + } + _artifacts_path = path; + + path = std::getenv("CIRCLE_INTERPRETER_PATH"); + if (path == nullptr) + { + std::cerr << "CIRCLE_INTERPRETER_PATH not found" << std::endl; + return false; + } + _circle_interpreter_path = path; + + return true; +} + +bool circle_interpreter_test::run(const std::string &command) +{ + std::vector buffer(READSIZE); + std::string result = ""; + std::string cmd_err = command + " 2>&1"; + FILE *pipe = popen(cmd_err.c_str(), "r"); + if (!pipe) + { + return false; + } + try + { + while (fgets(&buffer[0], buffer.size(), pipe) != NULL) + { + result += &buffer[0]; + } + } + catch (...) + { + pclose(pipe); + return false; + } + pclose(pipe); + _result = result; + + std::cout << _result << std::endl; + + return true; +} + +bool circle_interpreter_test::compare(const std::string &file1, const std::string &file2) +{ + std::ifstream f1(file1.c_str(), std::ifstream::in | std::ifstream::binary); + std::ifstream f2(file2.c_str(), std::ifstream::in | std::ifstream::binary); + + if (!f1.is_open() || !f2.is_open()) + { + return false; + } + + typedef unsigned char BYTE; + std::vector vBuffer1(READSIZE); + std::vector vBuffer2(READSIZE); + + do + { + f1.read((char *)&vBuffer1[0], READSIZE); + std::streamsize f1_bytes = f1.gcount(); + f2.read((char *)&vBuffer2[0], READSIZE); + std::streamsize f2_bytes = f2.gcount(); + + if (f1_bytes != f2_bytes) + { + return false; + } + + if (!std::equal(vBuffer1.begin(), vBuffer1.end(), vBuffer2.begin())) + { + return false; + } + } while (f1.good() || f2.good()); + return true; +} + +TEST_F(circle_interpreter_test, show_help_msg) +{ + if (!initialize()) + { + FAIL(); + return; + } + + std::string command = _circle_interpreter_path + " -h"; + if (!run(command)) + { + FAIL(); + return; + } + + const auto pos = _result.find("Usage: ./circle-interpreter"); + ASSERT_NE(std::string::npos, pos); +} + +TEST_F(circle_interpreter_test, valid_command) +{ + if (!initialize()) + { + FAIL(); + return; + } + + std::string model = _artifacts_path + "/Conv2D_000.circle"; + std::string input_prefix = _artifacts_path + "/Conv2D_000.circle.input"; + std::string output_prefix = "/tmp/Conv2D_000.circle.output"; + std::string generated_output = output_prefix + "0"; + std::remove(generated_output.c_str()); + std::string command = + _circle_interpreter_path + " " + model + " " + input_prefix + " " + output_prefix; + if (!run(command)) + { + FAIL(); + return; + } + + std::string expected_output = _artifacts_path + "/Conv2D_000.circle.output0"; + + if (!compare(generated_output, expected_output)) + { + FAIL(); + return; + } +} + +TEST_F(circle_interpreter_test, invalid_option_NEG) +{ + if (!initialize()) + { + FAIL(); + return; + } + + std::string model = _artifacts_path + "/Conv2D_000.circle"; + std::string command = _circle_interpreter_path + " " + model; + if (!run(command)) + { + FAIL(); + return; + } + + const auto pos = _result.find("Invalid argument"); + ASSERT_NE(std::string::npos, pos); +} + +TEST_F(circle_interpreter_test, not_existing_model_NEG) +{ + if (!initialize()) + { + FAIL(); + return; + } + + std::string not_existing_model = _artifacts_path + "/non_exist_file.foo"; + std::string input_prefix = _artifacts_path + "/Conv2D_000.circle.input"; + std::string output_prefix = "/tmp/Conv2D_000.circle.output"; + std::remove(output_prefix.c_str()); + std::string command = + _circle_interpreter_path + " " + not_existing_model + " " + input_prefix + " " + output_prefix; + if (!run(command)) + { + FAIL(); + return; + } + + const auto pos = _result.find("Failed to load"); + ASSERT_NE(std::string::npos, pos); +} + +TEST_F(circle_interpreter_test, invalid_input_prefix_NEG) +{ + if (!initialize()) + { + FAIL(); + return; + } + + std::string model = _artifacts_path + "/Conv2D_000.circle"; + std::string input_prefix = _artifacts_path + "/non_exist_file.foo"; + std::string output_prefix = "/tmp/Conv2D_000.circle.output"; + std::remove(output_prefix.c_str()); + std::string command = + _circle_interpreter_path + " " + model + " " + input_prefix + " " + output_prefix; + if (!run(command)) + { + FAIL(); + return; + } + + const auto pos = _result.find("Cannot open file"); + ASSERT_NE(std::string::npos, pos); +} diff --git a/compiler/circle-interpreter/src/CircleInterpreter.cpp b/compiler/circle-interpreter/src/CircleInterpreter.cpp index 1d24127..48c29a5 100644 --- a/compiler/circle-interpreter/src/CircleInterpreter.cpp +++ b/compiler/circle-interpreter/src/CircleInterpreter.cpp @@ -33,7 +33,9 @@ void readDataFromFile(const std::string &filename, char *data, size_t data_size) if (fs.fail()) throw std::runtime_error("Cannot open file \"" + filename + "\".\n"); if (fs.read(data, data_size).fail()) - throw std::runtime_error("Failed to read data from file \"" + filename + "\".\n"); + throw std::runtime_error("Input tensor size mismatches with \"" + filename + "\".\n"); + if (fs.peek() != EOF) + throw std::runtime_error("Input tensor size mismatches with \"" + filename + "\".\n"); } void writeDataToFile(const std::string &filename, const char *data, size_t data_size) diff --git a/compiler/circle-mpqsolver/CMakeLists.txt b/compiler/circle-mpqsolver/CMakeLists.txt new file mode 100644 index 0000000..25e318e --- /dev/null +++ b/compiler/circle-mpqsolver/CMakeLists.txt @@ -0,0 +1,37 @@ +file(GLOB_RECURSE SOURCES "src/*.cpp") +file(GLOB_RECURSE TESTS "src/*.test.cpp") +list(REMOVE_ITEM SOURCES ${TESTS}) + +add_executable(circle-mpqsolver "${SOURCES}") +target_include_directories(circle-mpqsolver PRIVATE src) +target_link_libraries(circle-mpqsolver arser) +target_link_libraries(circle-mpqsolver vconone) +target_link_libraries(circle-mpqsolver safemain) +target_link_libraries(circle-mpqsolver luci_lang) +target_link_libraries(circle-mpqsolver luci_service) +target_link_libraries(circle-mpqsolver luci_pass) +target_link_libraries(circle-mpqsolver luci_interpreter) +target_link_libraries(circle-mpqsolver dio_hdf5) +target_link_libraries(circle-mpqsolver luci_import) +target_link_libraries(circle-mpqsolver luci_export) +target_link_libraries(circle-mpqsolver luci_log) +target_link_libraries(circle-mpqsolver nncc_common) + +install(TARGETS circle-mpqsolver DESTINATION bin) + +if(NOT ENABLE_TEST) + return() +endif(NOT ENABLE_TEST) + +# circle-mpqsolver is executable, so we do not link it to the test. +# Instead, we use TEST_SOURCES to specify sources uesd for tests. +set(TEST_SOURCES + "src/bisection/DepthParameterizer.cpp" + "src/bisection/Quantizer.cpp" + "src/bisection/ErrorApproximator.cpp") + +nnas_find_package(GTest REQUIRED) +GTest_AddTest(circle_mpqsolver_test ${TESTS} ${TEST_SOURCES}) +target_link_libraries(circle_mpqsolver_test luci_lang) +target_link_libraries(circle_mpqsolver_test luci_service) +target_link_libraries(circle_mpqsolver_test luci_pass) diff --git a/compiler/circle-mpqsolver/README.md b/compiler/circle-mpqsolver/README.md new file mode 100644 index 0000000..9f4c2e7 --- /dev/null +++ b/compiler/circle-mpqsolver/README.md @@ -0,0 +1,68 @@ +# circle-mpqsolver +_circle-mpqsolver_ provides light-weight methods for finding a high-quality mixed-precision model +within a reasonable time. + +## Methods + +### Bisection +A model is split into two parts: front and back. One of them is quantized in uint8 and another in +int16. The precision of front and back is determined by our proxy metric, upperbound of total layer +errors. (See https://github.com/Samsung/ONE/pull/10170#discussion_r1042246598 for more details) + +The boundary between the front and the back is decided by the depth of operators (depth: distance +from input to the operator), i.e., given a depth d, layers with a depth less than d are included +in front, and the rest are included in back. Bisection performs binary search to find a proper +depth which achieves a qerror less than target_qerror. + +In case front is quantized into Q16 the pseudocode is the following: +``` + until |_depth_max_ - _depth_min_| <=1 do + _current_depth_ = 0.5 * (_depth_max_ + _depth_min_) + if Loss(_current_depth_) < _target_loss_ + _depth_max_ = _current_depth_ + else + _depth_min_ = _current_depth_ +``` +, where Loss(current_depth) is the qerror of the mixied-precision model split at current_depth. +As every iteration halves the remaining range (|depth_max - depth_min|), it converges in +_~log2(max_depth)_ iterations. + +## Usage +Run _circle-mpqsolver_ with the following arguments. + +--data: .h5 file with test data + +--input_model: Input float model initialized with min-max (recorded model) + +--output_model: Output qunatized mode + +--qerror_ratio: Target quantization error ratio. It should be in [0, 1]. 0 indicates qerror of full int16 model, 1 indicates qerror of full uint8 model. The lower `qerror_ratio` indicates the more accurate solution. + +--bisection _mode_: input nodes should be at Q16 precision ['auto', 'true', 'false'] + +``` +$ ./circle-mpqsolver + --data <.h5 data> + --input_model + --output_model + --qerror_ratio + --bisection +``` + +For example: +``` +$./circle-mpqsolver + --data dataset.h5 + --input_model model.recorded.circle + --output_model model.q_opt.circle + --qerror_ratio 0.4f + --bisection true +``` + +It will produce _model.q_opt.circle_, which is _model.recorded.circle_ quantized to mixed precision +using _dataset.h5_, with input nodes set to _Q16_ precision and quantization error (_qerror_) of +_model.q_opt.circle_ will be less than +``` + _qerror(full_q16) + qerror_ratio * (qerror(full_q8) - qerror(full_q16))_ + ``` + (_full_q16_ - model quantized using Q16 precision, _full_q8_ - model quantized using Q8 precision). diff --git a/compiler/circle-mpqsolver/requires.cmake b/compiler/circle-mpqsolver/requires.cmake new file mode 100644 index 0000000..73492a1 --- /dev/null +++ b/compiler/circle-mpqsolver/requires.cmake @@ -0,0 +1,6 @@ +require("safemain") +require("arser") +require("vconone") +require("luci") +require("luci-interpreter") +require("dio-hdf5") diff --git a/compiler/circle-mpqsolver/src/CircleMPQSolver.cpp b/compiler/circle-mpqsolver/src/CircleMPQSolver.cpp new file mode 100644 index 0000000..23e8fd4 --- /dev/null +++ b/compiler/circle-mpqsolver/src/CircleMPQSolver.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include "bisection/BisectionSolver.h" + +#include +#include +#include + +void print_version(void) +{ + std::cout << "circle-mpqsolver version " << vconone::get_string() << std::endl; + std::cout << vconone::get_copyright() << std::endl; +} + +int entry(int argc, char **argv) +{ + LOGGER(l); + + const std::string bisection_str = "--bisection"; + + arser::Arser arser("circle-mpqsolver provides light-weight methods for finding a high-quality " + "mixed-precision model within a reasonable time."); + + arser::Helper::add_version(arser, print_version); + arser::Helper::add_verbose(arser); + + arser.add_argument("--data").required(true).help("Path to the test data"); + arser.add_argument("--data_format").required(false).help("Test data format (default: h5)"); + + arser.add_argument("--qerror_ratio") + .type(arser::DataType::FLOAT) + .default_value(0.5f) + .help("quantization error ratio ([0, 1])"); + + arser.add_argument(bisection_str) + .nargs(1) + .type(arser::DataType::STR) + .help("Single optional argument for bisection method. " + "Whether input node should be quantized to Q16: 'auto', 'true', 'false'."); + + arser.add_argument("--input_model") + .required(true) + .help("Input float model with min max initialized"); + + arser.add_argument("--input_dtype") + .type(arser::DataType::STR) + .default_value("uint8") + .help("Data type of quantized model's inputs (default: uint8)"); + + arser.add_argument("--output_dtype") + .type(arser::DataType::STR) + .default_value("uint8") + .help("Data type of quantized model's outputs (default: uint8)"); + + arser.add_argument("--output_model").required(true).help("Output quantized model"); + + try + { + arser.parse(argc, argv); + } + catch (const std::runtime_error &err) + { + std::cerr << err.what() << std::endl; + std::cout << arser; + return EXIT_FAILURE; + } + + if (arser.get("--verbose")) + { + // The third parameter of setenv means REPLACE. + // If REPLACE is zero, it does not overwrite an existing value. + setenv("LUCI_LOG", "100", 0); + } + + auto data_path = arser.get("--data"); + auto input_model_path = arser.get("--input_model"); + auto output_model_path = arser.get("--output_model"); + auto input_dtype = arser.get("--input_dtype"); + auto output_dtype = arser.get("--output_dtype"); + + float qerror_ratio = arser.get("--qerror_ratio"); + if (qerror_ratio < 0.0 || qerror_ratio > 1.f) + { + std::cerr << "ERROR: quantization ratio must be in [0, 1]" << std::endl; + return EXIT_FAILURE; + } + auto start = std::chrono::high_resolution_clock::now(); + + if (arser[bisection_str]) + { + // optimize + using namespace mpqsolver::bisection; + + BisectionSolver solver(data_path, qerror_ratio, input_dtype, output_dtype); + { + auto value = arser.get(bisection_str); + if (value == "auto") + { + solver.algorithm(BisectionSolver::Algorithm::Auto); + } + else if (value == "true") + { + solver.algorithm(BisectionSolver::Algorithm::ForceQ16Front); + } + else if (value == "false") + { + solver.algorithm(BisectionSolver::Algorithm::ForceQ16Back); + } + else + { + std::cerr << "ERROR: Unrecognized option for bisection algortithm" << input_model_path + << std::endl; + return EXIT_FAILURE; + } + } + + auto optimized = solver.run(input_model_path); + if (optimized == nullptr) + { + std::cerr << "ERROR: Failed to build mixed precision model" << input_model_path << std::endl; + return EXIT_FAILURE; + } + + // save optimized + { + luci::CircleExporter exporter; + luci::CircleFileExpContract contract(optimized.get(), output_model_path); + if (!exporter.invoke(&contract)) + { + std::cerr << "ERROR: Failed to export mixed precision model" << input_model_path + << std::endl; + return EXIT_FAILURE; + } + } + } + else + { + std::cerr << "ERROR: Unrecognized solver" << std::endl; + return EXIT_FAILURE; + } + + auto duration = std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - start); + VERBOSE(l, 0) << "Elapsed Time: " << std::setprecision(5) << duration.count() / 60.f + << " minutes." << std::endl; + + return EXIT_SUCCESS; +} diff --git a/compiler/circle-mpqsolver/src/MPQSolver.cpp b/compiler/circle-mpqsolver/src/MPQSolver.cpp new file mode 100644 index 0000000..02732eb --- /dev/null +++ b/compiler/circle-mpqsolver/src/MPQSolver.cpp @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MPQSolver.h" + +using namespace mpqsolver; + +MPQSolver::MPQSolver(const std::string &input_data_path, float qerror_ratio, + const std::string &input_quantization, const std::string &output_quantization) + : _input_data_path(input_data_path), _qerror_ratio(qerror_ratio), + _input_quantization(input_quantization), _output_quantization(output_quantization) +{ +} diff --git a/compiler/circle-mpqsolver/src/MPQSolver.h b/compiler/circle-mpqsolver/src/MPQSolver.h new file mode 100644 index 0000000..7c018d0 --- /dev/null +++ b/compiler/circle-mpqsolver/src/MPQSolver.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __MPQSOLVER_MPQSOLEVR_SOLVER_H__ +#define __MPQSOLVER_MPQSOLEVR_SOLVER_H__ + +#include + +#include +#include + +namespace mpqsolver +{ + +class MPQSolver +{ +public: + /** + * @brief construct Solver using input_data_path for .h5 file, + * qerror_ratio to set target qerror, and input_quantization/output_quantization to set + * quantization type at input/output respectively + */ + MPQSolver(const std::string &input_data_path, float qerror_ratio, + const std::string &input_quantization, const std::string &output_quantization); + virtual ~MPQSolver() = default; + + /** + * @brief run solver for recorded float module at module_path + */ + virtual std::unique_ptr run(const std::string &module_path) = 0; + +protected: + std::string _input_data_path; + std::string _input_quantization; + std::string _output_quantization; + float _qerror_ratio = 0.f; // quantization error ratio +}; + +} // namespace mpqsolver + +#endif //__MPQSOLVER_MPQSOLEVR_SOLVER_H__ diff --git a/compiler/circle-mpqsolver/src/bisection/BisectionSolver.cpp b/compiler/circle-mpqsolver/src/bisection/BisectionSolver.cpp new file mode 100644 index 0000000..d506802 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/BisectionSolver.cpp @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BisectionSolver.h" +#include "DepthParameterizer.h" +#include "ErrorMetric.h" +#include "ErrorApproximator.h" + +#include +#include + +#include +#include + +using namespace mpqsolver::bisection; + +namespace +{ + +bool error_at_input_is_larger_than_at_output(const NodeDepthType &nodes_depth, float cut_depth) +{ + LOGGER(l); + + float error_at_input = 0; + float error_at_output = 0; + for (auto &iter : nodes_depth) + { + float cur_error = approximate(iter.first); + if (iter.second < cut_depth) + { + error_at_input += cur_error; + } + else + { + error_at_output += cur_error; + } + } + + if (error_at_input > error_at_output) + { + VERBOSE(l, 0) << "Q16 will be set at input due to "; + } + else + { + VERBOSE(l, 0) << "Q8 will be set at input due to "; + } + VERBOSE(l, 0) << error_at_input << " error at input vs "; + VERBOSE(l, 0) << error_at_output << " error at output." << std::endl; + + return error_at_input > error_at_output; +} + +std::unique_ptr read_module(const std::string &path) +{ + luci::ImporterEx importerex; + auto module = importerex.importVerifyModule(path); + if (module.get() == nullptr) + { + std::cerr << "ERROR: Failed to load " << path << std::endl; + return nullptr; + } + + return module; +} + +} // namespace + +BisectionSolver::BisectionSolver(const std::string &input_data_path, float qerror_ratio, + const std::string &input_quantization, + const std::string &output_quantization) + : MPQSolver(input_data_path, qerror_ratio, input_quantization, output_quantization) +{ + _quantizer = std::make_unique(_input_quantization, _output_quantization); +} + +float BisectionSolver::evaluate(const DatasetEvaluator &evaluator, const std::string &flt_path, + const std::string &def_quant, LayerParams &layers) +{ + auto model = read_module(flt_path); + // get fake quantized model for evaluation + if (!_quantizer->fake_quantize(model.get(), def_quant, layers)) + { + throw std::runtime_error("Failed to produce fake-quantized model."); + } + + return evaluator.evaluate(model.get()); +} + +void BisectionSolver::algorithm(Algorithm algorithm) { _algorithm = algorithm; } + +std::unique_ptr BisectionSolver::run(const std::string &module_path) +{ + LOGGER(l); + + auto module = read_module(module_path); + + float min_depth = 0.f; + float max_depth = 0.f; + NodeDepthType nodes_depth; + if (compute_depth(module.get(), nodes_depth, min_depth, max_depth) != + ParameterizerResult::SUCCESS) + { + std::cerr << "ERROR: Invalid graph for bisectioning" << std::endl; + return nullptr; + } + + std::unique_ptr metric = std::make_unique(); + DatasetEvaluator evaluator(module.get(), _input_data_path, *metric.get()); + + LayerParams layer_params; + float int16_qerror = + evaluate(evaluator, module_path, "int16" /* default quant_dtype */, layer_params); + VERBOSE(l, 0) << "Full int16 model quantization error " << int16_qerror << std::endl; + + float uint8_qerror = + evaluate(evaluator, module_path, "uint8" /* default quant_dtype */, layer_params); + VERBOSE(l, 0) << "Full uint8 model quantization error " << uint8_qerror << std::endl; + + if (int16_qerror > uint8_qerror) + { + throw std::runtime_error("Q8 model's qerror is less than Q16 model's qerror."); + } + + _qerror = int16_qerror + _qerror_ratio * std::fabs(uint8_qerror - int16_qerror); + VERBOSE(l, 0) << "Target quantization error " << _qerror << std::endl; + + if (uint8_qerror <= _qerror) + { + // no need for bisectioning just return Q8 model + if (!_quantizer->quantize(module.get(), "uint8", layer_params)) + { + std::cerr << "ERROR: Failed to quantize model" << std::endl; + return nullptr; + } + } + + int last_depth = -1; + float best_depth = -1; + LayerParams best_params; + if (module->size() != 1) + { + throw std::runtime_error("Unsupported module"); + } + auto graph = module->graph(0); + auto active_nodes = loco::active_nodes(loco::output_nodes(graph)); + // input and output nodes are not valid for quantization, so let's remove them + for (auto node : loco::input_nodes(graph)) + { + active_nodes.erase(node); + } + for (auto node : loco::output_nodes(graph)) + { + active_nodes.erase(node); + } + + // let's decide whether nodes at input are more suspectible to be quantized into Q16, than at + // output + bool int16_front = true; + switch (_algorithm) + { + case Algorithm::Auto: + int16_front = + error_at_input_is_larger_than_at_output(nodes_depth, 0.5f * (max_depth + min_depth)); + break; + case Algorithm::ForceQ16Front: + int16_front = true; + break; + case Algorithm::ForceQ16Back: + int16_front = true; + break; + } + + while (true) + { + int cut_depth = static_cast(std::floor(0.5f * (min_depth + max_depth))); + + if (last_depth == cut_depth) + { + break; + } + last_depth = cut_depth; + + LayerParams layer_params; + for (auto &node : active_nodes) + { + auto cur_node = loco::must_cast(node); + auto iter = nodes_depth.find(cur_node); + if (iter == nodes_depth.end()) + { + continue; // to filter out nodes like weights + } + + float depth = iter->second; + + if ((depth <= cut_depth && int16_front) || (depth >= cut_depth && !int16_front)) + { + auto layer_param = std::make_shared(); + { + layer_param->name = cur_node->name(); + layer_param->dtype = "int16"; + layer_param->granularity = "channel"; + } + + layer_params.emplace_back(layer_param); + } + } + + float cur_accuracy = evaluate(evaluator, module_path, "uint8", layer_params); + VERBOSE(l, 0) << cut_depth << " : " << cur_accuracy << std::endl; + + if (cur_accuracy < _qerror) + { + int16_front ? (max_depth = cut_depth) : (min_depth = cut_depth); + best_params = layer_params; + best_depth = cut_depth; + } + else + { + int16_front ? (min_depth = cut_depth) : (max_depth = cut_depth); + } + } + + VERBOSE(l, 0) << "Found the best configuration at " << best_depth << " depth." << std::endl; + if (!_quantizer->quantize(module.get(), "uint8", best_params)) + { + std::cerr << "ERROR: Failed to quantize model" << std::endl; + return nullptr; + } + + return module; +} diff --git a/compiler/circle-mpqsolver/src/bisection/BisectionSolver.h b/compiler/circle-mpqsolver/src/bisection/BisectionSolver.h new file mode 100644 index 0000000..7ec2e69 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/BisectionSolver.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __MPQSOLVER_BISECTION_SOLVER_H__ +#define __MPQSOLVER_BISECTION_SOLVER_H__ + +#include "Quantizer.h" +#include "Evaluator.h" +#include + +#include + +#include +#include + +namespace mpqsolver +{ +namespace bisection +{ + +class BisectionSolver final : public MPQSolver +{ +public: + /** + * @brief Algorithm options for running bisection algorithm + */ + enum Algorithm + { + Auto, + ForceQ16Front, + ForceQ16Back, + }; + +public: + /** + * @brief construct Solver using input_data_path for .h5 file, + * qerror_ratio to set target qerror, and input_quantization/output_quantization to set + * quantization type at input/output respectively + */ + BisectionSolver(const std::string &input_data_path, float qerror_ratio, + const std::string &input_quantization, const std::string &output_quantization); + BisectionSolver() = delete; + + /** + * @brief run bisection for recorded float module at module_path + */ + std::unique_ptr run(const std::string &module_path) override; + + /** + * @brief set used algorithm + */ + void algorithm(Algorithm algorithm); + +private: + float evaluate(const DatasetEvaluator &evaluator, const std::string &module_path, + const std::string &def_quant, LayerParams &layers); + +private: + float _qerror = 0.f; // quantization error + Algorithm _algorithm = Algorithm::ForceQ16Front; + std::unique_ptr _quantizer; +}; + +} // namespace bisection +} // namespace mpqsolver + +#endif //__MPQSOLVER_BISECTION_SOLVER_H__ diff --git a/compiler/circle-mpqsolver/src/bisection/DepthParameterizer.cpp b/compiler/circle-mpqsolver/src/bisection/DepthParameterizer.cpp new file mode 100644 index 0000000..cbf1b96 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/DepthParameterizer.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "DepthParameterizer.h" + +namespace mpqsolver +{ +namespace bisection +{ + +/** + * @brief compute maximal distance from graph inputs to graph nodes along with min/max values of + * distance and return status of computation (Assumes graph has no cycles) + */ +ParameterizerResult compute_depth(const luci::Module *module, NodeDepthType &nodes_depth, + float &min_depth, float &max_depth) +{ + if (module == nullptr) + return ParameterizerResult::FAILURE; + + if (module->size() != 1) + return ParameterizerResult::FAILURE; + + auto graph = module->graph(0); + if (!graph) + return ParameterizerResult::FAILURE; + + // initializing + std::vector to_process; + std::map named_depth; + { + auto inputs = loco::input_nodes(graph); + for (auto &node : inputs) + { + auto cnode = loco::must_cast(node); + to_process.emplace_back(cnode); + nodes_depth[cnode] = 0.f; + named_depth[cnode->name()] = 0.f; + } + } + + // enumerating + while (!to_process.empty()) + { + auto cur_node = to_process.back(); + to_process.pop_back(); + auto iter = nodes_depth.find(cur_node); + if (iter == nodes_depth.end()) + { + return ParameterizerResult::FAILURE; // unexpected + } + float cur_depth = iter->second + 1; + // processing children + auto children = loco::succs(cur_node); + for (auto &child : children) + { + auto cichild = loco::must_cast(child); + auto node_depth = nodes_depth.find(cichild); + if (node_depth == nodes_depth.end() || node_depth->second < cur_depth) + { + // initialize depth + nodes_depth[cichild] = cur_depth; + to_process.push_back(cichild); + named_depth[cichild->name()] = cur_depth; + } + } + } + + // compute min/max of depth parameter + auto minmax = std::minmax_element( + nodes_depth.begin(), nodes_depth.end(), + [=](const std::pair &el1, + const std::pair &el2) { return el1.second < el2.second; }); + + min_depth = minmax.first->second; + max_depth = minmax.second->second; + + return ParameterizerResult::SUCCESS; +} + +} // namespace bisection +} // namespace mpqsolver diff --git a/compiler/circle-mpqsolver/src/bisection/DepthParameterizer.h b/compiler/circle-mpqsolver/src/bisection/DepthParameterizer.h new file mode 100644 index 0000000..6a96aa1 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/DepthParameterizer.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __MPQSOLVER_DEPTH_PARAMETERIZER_H__ +#define __MPQSOLVER_DEPTH_PARAMETERIZER_H__ + +#include +#include + +namespace mpqsolver +{ +namespace bisection +{ + +using NodeDepthType = std::map; + +/** + * @brief status of parameterization + */ +enum class ParameterizerResult : int32_t +{ + SUCCESS = 0, + FAILURE = 1 +}; + +/** + * @brief compute maximal distance from graph inputs to graph nodes along with min/max values of + * distance and return status of compuation (success/failure) + */ +ParameterizerResult compute_depth(const luci::Module *module, NodeDepthType &nodes_depth, + float &min_depth, float &max_depth); + +} // namespace bisection +} // namespace mpqsolver + +#endif //__MPQSOLVER_DEPTH_PARAMETERIZER_H__ diff --git a/compiler/circle-mpqsolver/src/bisection/DepthParameterizer.test.cpp b/compiler/circle-mpqsolver/src/bisection/DepthParameterizer.test.cpp new file mode 100644 index 0000000..d3865e4 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/DepthParameterizer.test.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "DepthParameterizer.h" +#include "TestHelper.h" + +#include + +namespace +{ + +class NConvGraph final : public SimpleGraph +{ +protected: + loco::Node *insertGraphBody(loco::Node *input) override + { + _filter = _g->nodes()->create(); + _filter->dtype(loco::DataType::FLOAT32); + _filter->shape({_channel_size, 1, 1, _channel_size}); + _filter->name("conv_filter"); + + _bias = _g->nodes()->create(); + _bias->dtype(loco::DataType::FLOAT32); + _bias->shape({_channel_size}); + _bias->name("conv_bias"); + + _conv = _g->nodes()->create(); + _conv->padding(luci::Padding::SAME); + _conv->fusedActivationFunction(luci::FusedActFunc::NONE); + _conv->dtype(loco::DataType::FLOAT32); + _conv->shape({1, _width, _height, _channel_size}); + _conv->name("conv"); + _conv->filter(_filter); + _conv->bias(_bias); + _conv->input(input); + + return _conv; + } + +public: + luci::CircleConv2D *_conv = nullptr; + luci::CircleConst *_filter = nullptr; + luci::CircleConst *_bias = nullptr; +}; + +} // namespace + +TEST(CircleMPQSolverDepthParameteriserTest, verifyResultsTest) +{ + auto m = luci::make_module(); + NConvGraph g; + g.init(); + auto conv = g._conv; + auto input = g._input; + auto output = g._output; + + g.transfer_to(m.get()); + + mpqsolver::bisection::NodeDepthType nodes_depth; + float min_depth = std::numeric_limits().max(); + float max_depth = -std::numeric_limits().max(); + auto status = mpqsolver::bisection::compute_depth(m.get(), nodes_depth, min_depth, max_depth); + + EXPECT_TRUE(status == mpqsolver::bisection::ParameterizerResult::SUCCESS); + EXPECT_TRUE(max_depth == 2 && min_depth == 0); + EXPECT_TRUE(nodes_depth[input] == min_depth); + EXPECT_TRUE(nodes_depth[conv] == 1); + EXPECT_TRUE(nodes_depth[output] == max_depth); +} + +TEST(CircleMPQSolverDepthParameteriserTest, verifyResultsTest_NEG) +{ + auto m = luci::make_module(); + mpqsolver::bisection::NodeDepthType nodes_depth; + float min_depth = std::numeric_limits().max(); + float max_depth = -std::numeric_limits().max(); + auto status = mpqsolver::bisection::compute_depth(m.get(), nodes_depth, min_depth, max_depth); + + EXPECT_TRUE(status == mpqsolver::bisection::ParameterizerResult::FAILURE); +} diff --git a/compiler/circle-mpqsolver/src/bisection/ErrorApproximator.cpp b/compiler/circle-mpqsolver/src/bisection/ErrorApproximator.cpp new file mode 100644 index 0000000..ebd7258 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/ErrorApproximator.cpp @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ErrorApproximator.h" + +#include +#include +#include +#include +#include + +namespace +{ + +using namespace luci; +using IterFunc = std::function; + +inline bool has_min_max(const CircleNode *node) +{ + return node->quantparam() && !node->quantparam()->min.empty() && !node->quantparam()->max.empty(); +} + +inline uint32_t cal_offset(const loco::TensorShape &dimension, uint32_t *indices) +{ + return indices[0] * dimension.dim(1).value() * dimension.dim(2).value() * + dimension.dim(3).value() + + indices[1] * dimension.dim(2).value() * dimension.dim(3).value() + + indices[2] * dimension.dim(3).value() + indices[3]; +} + +uint32_t get_channel_dim_index(const CircleNode *node) +{ + uint32_t index = 0; + auto opcode = node->opcode(); + switch (opcode) + { + case CircleOpcode::CONV_2D: + case CircleOpcode::TRANSPOSE_CONV: + case CircleOpcode::FULLY_CONNECTED: + index = 0; + break; + case CircleOpcode::DEPTHWISE_CONV_2D: + index = 3; + break; + default: + throw std::runtime_error("Failed to find channel index in " + node->name()); + } + + return index; +} + +bool set_weight_dim(const CircleNode *node, const CircleConst *weights, + loco::TensorShape &dimension) +{ + auto opcode = node->opcode(); + switch (opcode) + { + case CircleOpcode::CONV_2D: + case CircleOpcode::TRANSPOSE_CONV: + case CircleOpcode::DEPTHWISE_CONV_2D: + assert(node->rank() == 4); + dimension.rank(node->rank()); + dimension.dim(0).set(weights->dim(0).value()); + dimension.dim(1).set(weights->dim(1).value()); + dimension.dim(2).set(weights->dim(2).value()); + dimension.dim(3).set(weights->dim(3).value()); + break; + case CircleOpcode::FULLY_CONNECTED: + assert(node->rank() == 2); + dimension.rank(4); + dimension.dim(0).set(weights->dim(0).value()); + dimension.dim(1).set(1); // Set FC layer like CONV + dimension.dim(2).set(1); + dimension.dim(3).set(weights->dim(1).value()); + break; + default: + return false; + } + + return true; +} + +loco::Node *get_weight(const CircleNode *node) +{ + loco::Node *weight = nullptr; + auto opcode = node->opcode(); + switch (opcode) + { + case CircleOpcode::CONV_2D: + { + auto conv = loco::must_cast(node); + weight = conv->filter(); + } + break; + case CircleOpcode::DEPTHWISE_CONV_2D: + { + auto dconv = loco::must_cast(node); + weight = dconv->filter(); + } + break; + case CircleOpcode::TRANSPOSE_CONV: + { + auto tconv = loco::must_cast(node); + weight = tconv->filter(); + } + break; + case CircleOpcode::FULLY_CONNECTED: + { + auto fc = loco::must_cast(node); + weight = fc->weights(); + } + break; + default: + break; + } + + return weight; +} + +inline CircleConst *get_constant_weight(const CircleNode *node) +{ + CircleConst *weight = dynamic_cast(get_weight(node)); + if (weight == nullptr) + { + throw std::runtime_error("Unsupported non-constant weights in convolution node " + + node->name()); + } + + return weight; +} + +void iterate_per_channel(const CircleNode *node, IterFunc func) +{ + CircleConst *weight = get_constant_weight(node); + + loco::TensorShape dimension; + set_weight_dim(node, weight, dimension); + uint32_t indices[4] = { + 0, + }; + + auto channel_dim_index = get_channel_dim_index(node); + + for (indices[0] = 0; indices[0] < dimension.dim(0).value(); indices[0]++) + { + for (indices[1] = 0; indices[1] < dimension.dim(1).value(); indices[1]++) + { + for (indices[2] = 0; indices[2] < dimension.dim(2).value(); indices[2]++) + { + for (indices[3] = 0; indices[3] < dimension.dim(3).value(); indices[3]++) + { + func(indices, dimension, channel_dim_index); + } + } + } + } +} + +void cal_minmax_per_channel(const CircleNode *node, std::vector &min, + std::vector &max) +{ + CircleConst *weight = get_constant_weight(node); + + loco::TensorShape dimension; + set_weight_dim(node, weight, dimension); + + auto channel_dim_index = get_channel_dim_index(node); + auto size = dimension.dim(channel_dim_index).value(); + + std::vector has_min_max_value(size, false); + min.resize(size); + max.resize(size); + + auto cal_minmax = [&](uint32_t *indices, loco::TensorShape &dimension, + uint32_t channel_dim_index) { + uint32_t channel_idx = indices[channel_dim_index]; + auto data = weight->at(cal_offset(dimension, indices)); + if (has_min_max_value[channel_idx]) + { + min[channel_idx] = data < min[channel_idx] ? data : min[channel_idx]; + max[channel_idx] = data > max[channel_idx] ? data : max[channel_idx]; + } + else + { + min[channel_idx] = data; + max[channel_idx] = data; + has_min_max_value[channel_idx] = true; + } + }; + + iterate_per_channel(node, cal_minmax); +} + +bool get_shape(const CircleNode *circle_node, std::vector &shape) +{ + if (circle_node->shape_status() == ShapeStatus::VALID) + { + auto rank = circle_node->rank(); + if (rank != 4) + return false; + + shape.resize(rank); + for (uint32_t i = 0; i < rank; i++) + { + shape[i] = circle_node->dim(i).value(); + } + return true; + } + + return false; +} + +/** + * @brief get_additions_per_channel computes W * H * CIN * KW * KH. + * + * W, H - width/height of OFM; KW, KH - convolution kernel width/height; + * CIN - number of channels in IFM (for depthwise its unity) + * See + * https://github.com/Samsung/ONE/pull/10170#discussion_r1065371638 + * for derivation. + */ +uint32_t get_additions_per_channel(const CircleNode *node) +{ + uint32_t adds_per_channel = 1; + std::vector ofm_shape; + if (!get_shape(node, ofm_shape)) // [BATCH, W, H, channels_out] + { + throw std::runtime_error("Failed to find correct shape " + node->name()); + } + + adds_per_channel *= ofm_shape[1] * ofm_shape[2]; // adds_per_channel *= W * H + + auto weights = loco::must_cast(get_weight(node)); + { + std::vector w_shape; + if (get_shape(weights, w_shape)) // [channels_out, k_x, k_y, channels_in] + { + adds_per_channel *= (w_shape[1] * w_shape[2]); // adds_per_channel *= k_x * k_y + } + if (node->opcode() != CircleOpcode::DEPTHWISE_CONV_2D) + { + // for not depthwise convolutions we need to scale it by CIN + adds_per_channel *= w_shape[3]; // adds_per_channel *= c_in + } + } + + return adds_per_channel; +} + +void get_min_max_ifm_values(const CircleNode *node, float &ci_min, float &ci_max) +{ + auto preds = loco::preds(node); + for (const auto &pred : preds) + { + auto parent_node = loco::must_cast(pred); + if (has_min_max(parent_node)) + { + auto quantparam = parent_node->quantparam(); + if (quantparam->min.size() > 0) + { + ci_min = quantparam->min[0]; + ci_max = quantparam->max[0]; + } + } + } +} + +/** + * @brief Return upper bound of quantization error for CONV, DCONV, TCONV. + * + * See + * https://github.com/Samsung/ONE/pull/10170#discussion_r1065371638 for details. + */ +float approximate_conv(const CircleNode *node) +{ + float volume_W_A_err = 0.f; + { + // activation min-max values + float ci_min = 0.f; + float ci_max = 0.f; + get_min_max_ifm_values(node, ci_min, ci_max); + + // channel-wise min, max + std::vector min_values; + std::vector max_values; + cal_minmax_per_channel(node, min_values, max_values); + assert(not min_values.empty()); + assert(not max_values.empty()); + + // ranges = (max_values - min_values) + std::vector ranges; + std::transform(max_values.begin(), max_values.end(), min_values.begin(), + std::back_inserter(ranges), std::minus()); + + // maximal weight value across all channels + float w_max = 0; + { + assert(max_values.size() == min_values.size()); + for (size_t i = 0; i < max_values.size(); ++i) + { + w_max = std::max(w_max, std::abs(max_values[i])); + w_max = std::max(w_max, std::abs(min_values[i])); + } + } + + // total weight quantization error across all channels + // so maximal error of quantization is ~ (max_value - min_value) / 255 + // omitting 255 term we get that maximal error of quantization is just its range + float sum_err = 0.f; + for (auto cur_err : ranges) + { + sum_err += cur_err; + } + + uint32_t adds_per_channel = get_additions_per_channel(node); + uint32_t num_of_channels = ranges.size(); + + // maximal error introduced by weights quantization (for all channels) + volume_W_A_err = sum_err * std::max(::fabs(ci_max), ::fabs(ci_min)); + // plus total error introduced by activation quantization (for all channels) + volume_W_A_err += w_max * num_of_channels * ::fabs(ci_max - ci_min); + // scale by volume of adds per channel + volume_W_A_err *= adds_per_channel; + // scale to get more readable output values + volume_W_A_err /= 1.e+6f; + } + + return volume_W_A_err; +} + +} // namespace + +namespace mpqsolver +{ +namespace bisection +{ + +/** + * How Approximate works? + * + * Currently it works just for convolution layers, but may be generalized for other types as well. + * See discussion at https://github.com/Samsung/ONE/pull/10170#discussion_r1042246598 + * Convolution can be expressed as a matrix multiplication. + * While quantizing we introduce quantization error into convolution operand (activations) as well + * as into convolution weights. A_q * W_q = (A + q_err(A)) * (W + q_err(W)) = A * W + A * q_err(W) + + * W * q_err(A) + q_err(A) * q_err(W), assuming q_err(A) * q_err(W) are negligible as quadratic + * terms, we get A_q * W_q ~ A * W + A * q_err(W) + W * q_err(A) , q_err - quantization error, + * W - weight matrix, A - activations from previous layer (IFM), so quantization error of matrix + * multiplication can be approximated as A * q_err(W) + W * q_err(A). Estimating its upper bound + * we get A * q_err(W) + W * q_err(A) <= + * number_of_additions * (A_max * (W_max - W_min) / 255 + W_max * (A_max - A_min) / 255) + * The following code tries to get total error for quantizing convolution node into Q8. + * It's just an heuristic (Metric sensitivity depends highly on derivatives as well). + */ +float approximate(const CircleNode *node) +{ + auto opcode = node->opcode(); + float qerror = 0.f; + switch (opcode) + { + case CircleOpcode::DEPTHWISE_CONV_2D: + case CircleOpcode::CONV_2D: + case CircleOpcode::TRANSPOSE_CONV: + qerror = approximate_conv(node); + break; + default: // TODO (FULLY_CONNECTED e.g.) + qerror = 0.f; + } + + return qerror; +} + +} // namespace bisection +} // namespace mpqsolver diff --git a/compiler/circle-mpqsolver/src/bisection/ErrorApproximator.h b/compiler/circle-mpqsolver/src/bisection/ErrorApproximator.h new file mode 100644 index 0000000..efbfa82 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/ErrorApproximator.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __MPQSOLVER_BISECTION_ERROR_APPROXIMATOR_H__ +#define __MPQSOLVER_BISECTION_ERROR_APPROXIMATOR_H__ + +#include + +#include + +namespace mpqsolver +{ +namespace bisection +{ + +/** + * @brief approximate error introduced while quantizing node into Q8 + */ +float approximate(const luci::CircleNode *node); + +} // namespace bisection +} // namespace mpqsolver + +#endif // __MPQSOLVER_BISECTION_ERROR_APPROXIMATOR_H__ diff --git a/compiler/circle-mpqsolver/src/bisection/ErrorApproximator.test.cpp b/compiler/circle-mpqsolver/src/bisection/ErrorApproximator.test.cpp new file mode 100644 index 0000000..015f83b --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/ErrorApproximator.test.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "ErrorApproximator.h" +#include "TestHelper.h" + +#include + +#include + +namespace +{ + +inline uint32_t cal_offset(uint32_t shape[4], uint32_t *indices) +{ + return indices[0] * shape[1] * shape[2] * shape[3] + indices[1] * shape[2] * shape[3] + + indices[2] * shape[3] + indices[3]; +} + +class NConvGraph final : public SimpleGraph +{ +protected: + void initInput(loco::Node *input) override + { + auto ci_input = loco::must_cast(input); + ci_input->shape_status(luci::ShapeStatus::VALID); + auto qparam = std::make_unique(); + qparam->min.assign(_channel_size, _a_min); + qparam->max.assign(_channel_size, _a_max); + ci_input->quantparam(std::move(qparam)); + } + + loco::Node *insertGraphBody(loco::Node *input) override + { + _filter = _g->nodes()->create(); + _filter->dtype(loco::DataType::FLOAT32); + _filter->shape({_channel_size, _f_w, _f_h, _channel_size}); + _filter->shape_status(luci::ShapeStatus::VALID); + _filter->name("conv_filter"); + uint32_t indices[4] = { + 0, + }; + + uint32_t w_shape[4] = {_filter->dim(0).value(), _filter->dim(1).value(), + _filter->dim(2).value(), _filter->dim(3).value()}; + + _filter->size(w_shape[0] * w_shape[1] * w_shape[2] * w_shape[3]); + + for (indices[0] = 0; indices[0] < w_shape[0]; ++indices[0]) + { + for (indices[1] = 0; indices[1] < w_shape[1]; ++indices[1]) + { + for (indices[2] = 0; indices[2] < w_shape[2]; ++indices[2]) + { + for (indices[3] = 0; indices[3] < w_shape[3]; ++indices[3]) + { + uint32_t offset = cal_offset(w_shape, indices); + _filter->at(offset) = (offset % 2 == 0) ? _w_max : _w_min; + } + } + } + } + + _bias = _g->nodes()->create(); + _bias->dtype(loco::DataType::FLOAT32); + _bias->shape({_channel_size}); + _bias->name("conv_bias"); + + _conv = _g->nodes()->create(); + _conv->padding(luci::Padding::SAME); + _conv->fusedActivationFunction(luci::FusedActFunc::NONE); + _conv->dtype(loco::DataType::FLOAT32); + _conv->shape({1, _width, _height, _channel_size}); + _conv->shape_status(luci::ShapeStatus::VALID); + _conv->name("conv"); + _conv->filter(_filter); + _conv->bias(_bias); + _conv->input(input); + + return _conv; + } + +public: + luci::CircleConv2D *_conv = nullptr; + luci::CircleConst *_filter = nullptr; + luci::CircleConst *_bias = nullptr; + uint32_t _f_w = 1; + uint32_t _f_h = 1; + float _w_min = -1.f; + float _w_max = 1.f; + float _a_min = -1.f; + float _a_max = 1.f; +}; + +} // namespace + +TEST(CircleMPQSolverErrorApproximatorTest, verifyResultsTest) +{ + NConvGraph g; + g.init(); + + auto value = mpqsolver::bisection::approximate(g._conv); + float expected = ((g._w_max - g._w_min) * g._channel_size * std::max(g._a_max, g._a_min) + + (g._a_max - g._a_min) * g._channel_size * std::max(g._w_max, g._w_min)) * + g._f_h * g._f_w * g._height * g._width * g._channel_size / 1.e+6f; + EXPECT_FLOAT_EQ(expected, value); +} + +TEST(CircleMPQSolverErrorApproximatorTest, verifyResultsTest_NEG) +{ + NConvGraph g; + g.init(); + + auto value = mpqsolver::bisection::approximate(g._input); + float expected = 0.f; + EXPECT_FLOAT_EQ(expected, value); + + value = mpqsolver::bisection::approximate(g._output); + expected = 0.f; + EXPECT_FLOAT_EQ(expected, value); +} diff --git a/compiler/circle-mpqsolver/src/bisection/ErrorMetric.cpp b/compiler/circle-mpqsolver/src/bisection/ErrorMetric.cpp new file mode 100644 index 0000000..74c7fd6 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/ErrorMetric.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ErrorMetric.h" + +#include +#include + +#include +#include + +using namespace mpqsolver::bisection; + +/** + * @brief compare first and second operands in MAE (Mean Average Error metric) + */ +float MAEMetric::compute(const WholeOutput &first, const WholeOutput &second) const +{ + assert(first.size() == second.size()); + + float error = 0.f; + size_t output_size = 0; + + for (size_t sample_index = 0; sample_index < first.size(); ++sample_index) + { + assert(first[sample_index].size() == second[sample_index].size()); + for (size_t out_index = 0; out_index < first[sample_index].size(); ++out_index) + { + const Buffer &first_elementary = first[sample_index][out_index]; + const Buffer &second_elementary = second[sample_index][out_index]; + assert(first_elementary.size() == second_elementary.size()); + size_t cur_size = first_elementary.size() / loco::size(loco::DataType::FLOAT32); + + const float *first_floats = reinterpret_cast(first_elementary.data()); + const float *second_floats = reinterpret_cast(second_elementary.data()); + for (size_t index = 0; index < cur_size; index++) + { + float ref_value = *(first_floats + index); + float cur_value = *(second_floats + index); + error += std::fabs(ref_value - cur_value); + } + output_size += cur_size; + } + } + + return error / output_size; +} diff --git a/compiler/circle-mpqsolver/src/bisection/ErrorMetric.h b/compiler/circle-mpqsolver/src/bisection/ErrorMetric.h new file mode 100644 index 0000000..9ef8662 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/ErrorMetric.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __MPQSOLVER_BISECTION_ERROR_METRIC_H__ +#define __MPQSOLVER_BISECTION_ERROR_METRIC_H__ + +#include + +namespace mpqsolver +{ +namespace bisection +{ + +using Buffer = std::vector; +using Output = std::vector; +using WholeOutput = std::vector; + +class ErrorMetric +{ +public: + virtual ~ErrorMetric() = default; + + /** + * @brief abstract method for comparing first and second operands + */ + virtual float compute(const WholeOutput &first, const WholeOutput &second) const = 0; +}; + +// Mean Absolute Error +class MAEMetric final : public ErrorMetric +{ +public: + /** + * @brief compare first and second operands in MAE (Mean Average Error metric) + */ + float compute(const WholeOutput &first, const WholeOutput &second) const; +}; + +} // namespace bisection +} // namespace mpqsolver + +#endif //__MPQSOLVER_BISECTION_ERROR_METRIC_H__ diff --git a/compiler/circle-mpqsolver/src/bisection/Evaluator.cpp b/compiler/circle-mpqsolver/src/bisection/Evaluator.cpp new file mode 100644 index 0000000..94d46a3 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/Evaluator.cpp @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Evaluator.h" + +#include + +#include + +using namespace mpqsolver::bisection; + +using Shape = std::vector; + +namespace +{ + +using namespace luci; + +template size_t get_tensor_size(const NodeT *node) +{ + uint32_t tensor_size = loco::size(node->dtype()); + for (uint32_t i = 0; i < node->rank(); ++i) + tensor_size *= node->dim(i).value(); + return tensor_size; +} + +WholeOutput compute_outputs(const luci::Module *module, const std::string &h5file) +{ + dio::hdf5::HDF5Importer importer{h5file}; + importer.importGroup("value"); + + bool is_raw_data = importer.isRawData(); + + const auto num_records = importer.numData(); + if (num_records == 0) + throw std::runtime_error("The input data file does not contain any record."); + const auto input_nodes = loco::input_nodes(module->graph()); + const auto num_inputs = input_nodes.size(); + + WholeOutput dataset_output; + + // Create interpreter. + luci_interpreter::Interpreter interpreter(module); + for (int32_t record_idx = 0; record_idx < num_records; record_idx++) + { + if (num_inputs != static_cast(importer.numInputs(record_idx))) + throw std::runtime_error("Wrong number of inputs."); + for (uint32_t input_idx = 0; input_idx < num_inputs; input_idx++) + { + const auto *input_node = loco::must_cast(input_nodes[input_idx]); + assert(input_node->index() == input_idx); + + std::vector input_data(get_tensor_size(input_node)); + + if (!is_raw_data) + { + loco::DataType dtype; + Shape shape; + importer.readTensor(record_idx, input_idx, &dtype, &shape, input_data.data()); + } + else + { + // Skip type/shape check for raw data + importer.readTensor(record_idx, input_idx, input_data.data()); + } + + interpreter.writeInputTensor(input_node, input_data.data(), input_data.size()); + } + + interpreter.interpret(); + + Output nn_output; + + // Get output. + const auto output_nodes = loco::output_nodes(module->graph()); + for (size_t i = 0; i < module->graph()->outputs()->size(); i++) + { + const auto *output_node = loco::must_cast(output_nodes[i]); + Buffer output_data(get_tensor_size(output_node)); + interpreter.readOutputTensor(output_node, output_data.data(), output_data.size()); + // output + nn_output.push_back(output_data); + } + dataset_output.push_back(nn_output); + } + + return dataset_output; +} + +} // namespace + +DatasetEvaluator::DatasetEvaluator(const luci::Module *ref_module, const std::string &h5file, + const ErrorMetric &metric) + : _ref_module(ref_module), _h5file(h5file), _metric(&metric) +{ + _ref_output = compute_outputs(_ref_module, _h5file); +} + +void DatasetEvaluator::validate(const luci::Module *trgt_fq_module) const +{ + const auto output_nodes = loco::output_nodes(trgt_fq_module->graph()); + for (size_t out_index = 0; out_index < output_nodes.size(); ++out_index) + { + const auto *output_node = loco::must_cast(output_nodes[out_index]); + loco::DataType out_dtype = output_node->dtype(); + if (out_dtype != loco::DataType::FLOAT32) + throw std::runtime_error("Unsupported output dtype " + output_node->name()); + } +} + +float DatasetEvaluator::evaluate(const luci::Module *trgt_fq_module) const +{ + if (trgt_fq_module == nullptr) + throw std::runtime_error("Invalid target module"); + + if (_metric == nullptr) + throw std::runtime_error("Invalid metric"); + + validate(trgt_fq_module); + + const WholeOutput &cur_output = compute_outputs(trgt_fq_module, _h5file); + float error = _metric->compute(_ref_output, cur_output); + return error; +} diff --git a/compiler/circle-mpqsolver/src/bisection/Evaluator.h b/compiler/circle-mpqsolver/src/bisection/Evaluator.h new file mode 100644 index 0000000..144c86c --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/Evaluator.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __MPQSOLVER_BISECTION_EVALUATOR_H__ +#define __MPQSOLVER_BISECTION_EVALUATOR_H__ + +#include "ErrorMetric.h" + +#include +#include + +#include +#include + +namespace mpqsolver +{ +namespace bisection +{ + +class DatasetEvaluator final +{ +public: + /** + * @brief create Evaluator for comparing output of ref_module on h5file + */ + DatasetEvaluator(const luci::Module *ref_module, const std::string &h5file, + const ErrorMetric &metric); + DatasetEvaluator() = delete; + ~DatasetEvaluator() = default; + + /** + * @brief evaluate trgt_fq_module (fake-quantized) + * returns error-metric + */ + float evaluate(const luci::Module *trgt_fq_module) const; + +private: + /** + * @brief throws if there is something wrong with the module + */ + void validate(const luci::Module *module) const; + +private: + const luci::Module *_ref_module = nullptr; + std::string _h5file; + WholeOutput _ref_output; + const ErrorMetric *_metric = nullptr; +}; + +} // namespace bisection +} // namespace mpqsolver + +#endif //__MPQSOLVER_BISECTION_EVALUATOR_H__ diff --git a/compiler/circle-mpqsolver/src/bisection/Quantizer.cpp b/compiler/circle-mpqsolver/src/bisection/Quantizer.cpp new file mode 100644 index 0000000..6fe1d56 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/Quantizer.cpp @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Quantizer.h" +#include + +#include + +using namespace mpqsolver::bisection; +using AlgorithmParameters = luci::CircleQuantizer::Options::AlgorithmParameters; +using Algorithms = luci::CircleQuantizer::Options::Algorithm; + +namespace +{ + +bool make_model_fake_quantized(luci::Module *module) +{ + luci::CircleQuantizer quantizer; + + auto options = quantizer.options(); + options->enable(Algorithms::ConvertToFakeQuantizedModel); + + for (size_t idx = 0; idx < module->size(); ++idx) + { + auto graph = module->graph(idx); + // quantize the graph + quantizer.quantize(graph); + if (!luci::validate(graph)) + { + return false; + } + } + + return true; +} + +} // namespace + +Quantizer::Quantizer(const std::string &input_dtype, const std::string &output_dtype) + : _input_dtype(input_dtype), _output_dtype(output_dtype) +{ +} + +/** + * @brief quantize recorded module (min/max initialized) with specified parameters + * returns true on success + */ +bool Quantizer::quantize(luci::Module *module, const std::string &quant_dtype, + LayerParams &layer_params) +{ + if (!module) + return false; + + static const std::string default_dtype = "float32"; + static const std::string granularity_type = "channel"; + + luci::CircleQuantizer quantizer; + + auto options = quantizer.options(); + options->enable(Algorithms::QuantizeWithMinMax); + + options->param(AlgorithmParameters::Quantize_input_model_dtype, default_dtype); + options->param(AlgorithmParameters::Quantize_output_model_dtype, quant_dtype); + options->param(AlgorithmParameters::Quantize_granularity, granularity_type); + options->param(AlgorithmParameters::Quantize_input_type, _input_dtype); + options->param(AlgorithmParameters::Quantize_output_type, _output_dtype); + options->param(AlgorithmParameters::Quantize_TF_style_maxpool, "False"); + + if (!layer_params.empty()) + { + try + { + options->layer_params(AlgorithmParameters::Quantize_layer_params, layer_params); + } + catch (const std::runtime_error &e) + { + std::cerr << e.what() << '\n'; + return false; + } + } + + for (size_t idx = 0; idx < module->size(); ++idx) + { + auto graph = module->graph(idx); + // quantize the graph + quantizer.quantize(graph); + if (!luci::validate(graph)) + { + std::cerr << "ERROR: Quantized graph is invalid" << std::endl; + return false; + } + } + + return true; +} + +/** + * @brief fake_quantize recorded module (min/max initialized) with specified parameters + * returns true on success + */ +bool Quantizer::fake_quantize(luci::Module *module, const std::string &quant_dtype, + LayerParams &layer_params) +{ + if (!quantize(module, quant_dtype, layer_params)) + return false; + + if (!make_model_fake_quantized(module)) + return false; + + return true; +} diff --git a/compiler/circle-mpqsolver/src/bisection/Quantizer.h b/compiler/circle-mpqsolver/src/bisection/Quantizer.h new file mode 100644 index 0000000..ceaabf5 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/Quantizer.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __MPQSOLVER_BISECTION_QUANTIZER_H__ +#define __MPQSOLVER_BISECTION_QUANTIZER_H__ + +#include +#include + +#include +#include +#include + +namespace mpqsolver +{ +namespace bisection +{ + +using LayerParam = luci::CircleQuantizer::Options::LayerParam; +using LayerParams = std::vector>; + +class Quantizer +{ +public: + Quantizer(const std::string &input_dtype, const std::string &output_type); + + /** + * @brief quantize recorded module (min/max initialized) with specified parameters + * returns true on success + */ + bool quantize(luci::Module *module, const std::string &quant_dtype, LayerParams &layer_params); + + /** + * @brief fake_quantize recorded module (min/max initialized) with specified parameters + * returns true on success + */ + bool fake_quantize(luci::Module *module, const std::string &quant_dtype, + LayerParams &layer_params); + +private: + std::string _input_dtype = "uint8"; + std::string _output_dtype = "uint8"; +}; + +} // namespace bisection +} // namespace mpqsolver + +#endif //__MPQSOLVER_BISECTION_QUANTIZER_H__ diff --git a/compiler/circle-mpqsolver/src/bisection/Quantizer.test.cpp b/compiler/circle-mpqsolver/src/bisection/Quantizer.test.cpp new file mode 100644 index 0000000..8575156 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/Quantizer.test.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include + +#include "Quantizer.h" +#include "TestHelper.h" + +#include + +#include + +namespace +{ + +class AddGraph final : public SimpleGraph +{ +protected: + void initInput(loco::Node *input) override + { + auto ci_input = loco::must_cast(input); + initMinMax(ci_input); + } + + void initMinMax(luci::CircleNode *node) + { + auto qparam = std::make_unique(); + qparam->min.assign(1, _a_min); + qparam->max.assign(1, _a_max); + node->quantparam(std::move(qparam)); + } + + loco::Node *insertGraphBody(loco::Node *input) override + { + _add = _g->nodes()->create(); + _beta = _g->nodes()->create(); + + _add->dtype(loco::DataType::FLOAT32); + _beta->dtype(loco::DataType::FLOAT32); + + uint32_t channel_size = 16; + _add->shape({1, _channel_size, _width, _height}); + _beta->shape({1, _channel_size, _width, _height}); + + _beta->size(channel_size); + _add->x(input); + _add->y(_beta); + _add->fusedActivationFunction(luci::FusedActFunc::NONE); + + _add->name("add"); + _beta->name("beta"); + initMinMax(_add); + + return _add; + } + +public: + float _a_min = -1.f; + float _a_max = 1.f; + luci::CircleAdd *_add = nullptr; + luci::CircleConst *_beta = nullptr; +}; + +} // namespace + +TEST(CircleMPQSolverQuantizerTest, verifyResultsTest) +{ + auto m = luci::make_module(); + AddGraph g; + g.init(); + auto add = g._add; + float range = g._a_max - g._a_min; + g.transfer_to(m.get()); + + std::string def_quant = "uint8"; + mpqsolver::bisection::Quantizer quantizer(def_quant, def_quant); + mpqsolver::bisection::LayerParams params; + auto res = quantizer.quantize(m.get(), def_quant, params); + EXPECT_TRUE(res); + auto quant_param = add->quantparam(); + EXPECT_TRUE(quant_param != nullptr); + EXPECT_TRUE(quant_param->scale.size() == 1); + EXPECT_FLOAT_EQ(quant_param->scale[0], range / 255.f); + EXPECT_TRUE(quant_param->zerop.size() == 1); + EXPECT_TRUE(quant_param->zerop[0] == 128); +} + +TEST(CircleMPQSolverQuantizerTest, verifyResultsTest_NEG) +{ + std::string def_quant = "uint8"; + mpqsolver::bisection::Quantizer quantizer(def_quant, def_quant); + mpqsolver::bisection::LayerParams params; + auto res = quantizer.quantize(nullptr, def_quant, params); + EXPECT_TRUE(!res); +} diff --git a/compiler/circle-mpqsolver/src/bisection/TestHelper.h b/compiler/circle-mpqsolver/src/bisection/TestHelper.h new file mode 100644 index 0000000..f930738 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/TestHelper.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __MPQSOLVER_TEST_HELPER_H__ +#define __MPQSOLVER_TEST_HELPER_H__ + +#include +#include + +class SimpleGraph +{ +public: + SimpleGraph() : _g(loco::make_graph()) {} + +public: + void init() + { + _input = _g->nodes()->create(); + _output = _g->nodes()->create(); + _input->name("input"); + _output->name("output"); + + auto graph_input = _g->inputs()->create(); + _input->index(graph_input->index()); + auto graph_output = _g->outputs()->create(); + _output->index(graph_output->index()); + + graph_input->dtype(loco::DataType::FLOAT32); + _input->dtype(loco::DataType::FLOAT32); + _output->dtype(loco::DataType::FLOAT32); + graph_output->dtype(loco::DataType::FLOAT32); + + graph_input->shape({1, _channel_size, _width, _height}); + _input->shape({1, _channel_size, _width, _height}); + _output->shape({1, _channel_size, _width, _height}); + graph_output->shape({1, _channel_size, _width, _height}); + + auto graph_body = insertGraphBody(_input); + _output->from(graph_body); + + initInput(_input); + } + + virtual ~SimpleGraph() = default; + void transfer_to(luci::Module *module) + { + // WARNING: after g is transfered, _graph_inputs, _inputs + // and _graph_outputs, _outputs in TestOsGraphlet will be invalid. + // arrays are not cleared as this is just helpers to unit tests + module->add(std::move(_g)); + } + +protected: + virtual loco::Node *insertGraphBody(loco::Node *input) = 0; + virtual void initInput(loco::Node *input){}; + +public: + std::unique_ptr _g; + luci::CircleInput *_input = nullptr; + luci::CircleOutput *_output = nullptr; + uint32_t _channel_size = 16; + uint32_t _width = 4; + uint32_t _height = 4; +}; + +#endif //__MPQSOLVER_TEST_HELPER_H__ diff --git a/compiler/circle-mpqsolver/src/bisection/VISQErrorApproximator.cpp b/compiler/circle-mpqsolver/src/bisection/VISQErrorApproximator.cpp new file mode 100644 index 0000000..822df89 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/VISQErrorApproximator.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "VISQErrorApproximator.h" + +#include + +using namespace mpqsolver::bisection; + +void VISQErrorApproximator::init(const std::string &visq_data_path) +{ + // read file + std::ifstream file(visq_data_path); + if (!init(file)) + { + throw std::runtime_error("Invalid visq file " + visq_data_path); + } +} + +bool VISQErrorApproximator::init(std::istream &) +{ + // TODO + return true; +} + +float VISQErrorApproximator::approximate(const std::string &node_name) const +{ + auto iter = _layer_errors.find(node_name); + if (iter == _layer_errors.end()) + { + return 0.f; + } + + return iter->second; +} diff --git a/compiler/circle-mpqsolver/src/bisection/VISQErrorApproximator.h b/compiler/circle-mpqsolver/src/bisection/VISQErrorApproximator.h new file mode 100644 index 0000000..855bca8 --- /dev/null +++ b/compiler/circle-mpqsolver/src/bisection/VISQErrorApproximator.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __MPQSOLVER_BISECTION_VISQ_ERROR_APPROXIMATOR_H__ +#define __MPQSOLVER_BISECTION_VISQ_ERROR_APPROXIMATOR_H__ + +#include +#include + +namespace mpqsolver +{ +namespace bisection +{ + +class VISQErrorApproximator final +{ +public: + /** + * @brief constructor of VISQErrorApproximator + */ + VISQErrorApproximator() = default; + + /** + * @brief initiliaze by visq_data_path (throws on failure) + */ + void init(const std::string &visq_data_path); + + /** + * @brief approximate error introduced while quantizing node into Q8 + */ + float approximate(const std::string &node_name) const; + +private: + /** + * @brief initiliaze by visq_data (returns success) + */ + bool init(std::istream &visq_data); + +private: + std::string _visq_data_path; + std::map _layer_errors; +}; + +} // namespace bisection +} // namespace mpqsolver + +#endif // __MPQSOLVER_BISECTION_VISQ_ERROR_APPROXIMATOR_H__ diff --git a/compiler/circle-operator-test/src/circle-operator.test.cpp b/compiler/circle-operator-test/src/circle-operator.test.cpp index 29c6f37..351eda8 100644 --- a/compiler/circle-operator-test/src/circle-operator.test.cpp +++ b/compiler/circle-operator-test/src/circle-operator.test.cpp @@ -20,7 +20,7 @@ #include #include -class cirlce_operator_test : public ::testing::Test +class circle_operator_test : public ::testing::Test { protected: bool initialize(void); @@ -35,7 +35,7 @@ protected: std::string _result; }; -bool cirlce_operator_test::initialize(void) +bool circle_operator_test::initialize(void) { char *path = std::getenv("ARTIFACTS_PATH"); if (path == nullptr) @@ -56,7 +56,7 @@ bool cirlce_operator_test::initialize(void) return true; } -bool cirlce_operator_test::run(const std::string &command) +bool circle_operator_test::run(const std::string &command) { std::vector buffer(260); std::string result = ""; @@ -86,7 +86,7 @@ bool cirlce_operator_test::run(const std::string &command) return true; } -bool cirlce_operator_test::load(const std::string &file) +bool circle_operator_test::load(const std::string &file) { std::ifstream tmp(file.c_str()); if (tmp.fail()) @@ -98,7 +98,7 @@ bool cirlce_operator_test::load(const std::string &file) return true; } -TEST_F(cirlce_operator_test, valid_names) +TEST_F(circle_operator_test, valid_names) { if (!initialize()) { @@ -118,7 +118,7 @@ TEST_F(cirlce_operator_test, valid_names) ASSERT_NE(std::string::npos, pos); } -TEST_F(cirlce_operator_test, valid_codes) +TEST_F(circle_operator_test, valid_codes) { if (!initialize()) { @@ -138,7 +138,7 @@ TEST_F(cirlce_operator_test, valid_codes) ASSERT_NE(std::string::npos, pos); } -TEST_F(cirlce_operator_test, invalid_option_NEG) +TEST_F(circle_operator_test, invalid_option_NEG) { if (!initialize()) { @@ -158,7 +158,7 @@ TEST_F(cirlce_operator_test, invalid_option_NEG) ASSERT_NE(std::string::npos, pos); } -TEST_F(cirlce_operator_test, check_code_name) +TEST_F(circle_operator_test, check_code_name) { if (!initialize()) { @@ -180,7 +180,7 @@ TEST_F(cirlce_operator_test, check_code_name) ASSERT_NE(std::string::npos, pos2); } -TEST_F(cirlce_operator_test, nonexist_file_NEG) +TEST_F(circle_operator_test, nonexist_file_NEG) { if (!initialize()) { @@ -200,7 +200,7 @@ TEST_F(cirlce_operator_test, nonexist_file_NEG) ASSERT_NE(std::string::npos, pos); } -TEST_F(cirlce_operator_test, invalid_file_NEG) +TEST_F(circle_operator_test, invalid_file_NEG) { if (!initialize()) { @@ -220,7 +220,7 @@ TEST_F(cirlce_operator_test, invalid_file_NEG) ASSERT_NE(std::string::npos, pos); } -TEST_F(cirlce_operator_test, output_file) +TEST_F(circle_operator_test, output_file) { if (!initialize()) { diff --git a/compiler/circle-opselector/CMakeLists.txt b/compiler/circle-opselector/CMakeLists.txt index 93ab84c..3f7f424 100644 --- a/compiler/circle-opselector/CMakeLists.txt +++ b/compiler/circle-opselector/CMakeLists.txt @@ -14,6 +14,7 @@ target_link_libraries(circle-opselector luci_export) target_link_libraries(circle-opselector arser) target_link_libraries(circle-opselector vconone) target_link_libraries(circle-opselector luci_service) +target_link_libraries(circle-opselector luci_partition) target_link_libraries(circle-opselector luci_profile) install(TARGETS circle-opselector DESTINATION bin) @@ -33,4 +34,6 @@ target_link_libraries(circle-opselector-test luci_export) target_link_libraries(circle-opselector-test arser) target_link_libraries(circle-opselector-test vconone) target_link_libraries(circle-opselector-test luci_service) +target_link_libraries(circle-opselector-test luci_partition) target_link_libraries(circle-opselector-test luci_profile) +target_link_libraries(circle-opselector-test luci_testhelper) diff --git a/compiler/circle-opselector/driver/Driver.cpp b/compiler/circle-opselector/driver/Driver.cpp index 4b39a6d..6549fc7 100644 --- a/compiler/circle-opselector/driver/Driver.cpp +++ b/compiler/circle-opselector/driver/Driver.cpp @@ -15,8 +15,11 @@ */ #include "ModuleIO.h" +#include "OpSelector.h" +#include #include +#include #include #include @@ -35,124 +38,6 @@ void print_version(void) std::cout << vconone::get_copyright() << std::endl; } -std::vector split_into_vector(const std::string &str, const char &delim) -{ - std::vector ret; - std::istringstream is(str); - for (std::string item; std::getline(is, item, delim);) - { - ret.push_back(item); - } - - // remove empty string - ret.erase(std::remove_if(ret.begin(), ret.end(), [](const std::string &s) { return s.empty(); }), - ret.end()); - - return ret; -} - -bool is_number(const std::string &s) -{ - return !s.empty() && std::find_if(s.begin(), s.end(), - [](unsigned char c) { return !std::isdigit(c); }) == s.end(); -} - -bool is_number(const std::vector &vec) -{ - for (const auto &s : vec) - { - if (not::is_number(s)) - { - return false; - } - } - return true; -} - -/** - * @brief Segmentation function for user's '--by_id' input - * - * @note This function tokenizes the input data.s - * First, divide it into ',', and if token has '-', devide it once more into '-'. - * For example, if user input is '12,34,56', it is devided into [12,34,56]. - * If input is '1-2,34,56', it is devided into [[1,2],34,56]. - * And '-' means range so, if input is '2-7', it means all integer between 2-7. - */ -std::vector split_id_input(const std::string &str) -{ - std::vector by_id; - - // tokenize colon-separated string - auto colon_tokens = ::split_into_vector(str, ','); - if (colon_tokens.empty()) // input empty line like "". - { - std::cerr << "ERROR: Nothing was entered." << std::endl; - exit(EXIT_FAILURE); - } - for (const auto &ctok : colon_tokens) - { - auto dash_tokens = ::split_into_vector(ctok, '-'); - if (not::is_number(dash_tokens)) - { - std::cerr << "ERROR: To select operator by id, please use these args: [0-9], '-', ','" - << std::endl; - exit(EXIT_FAILURE); - } - // convert string into integer - std::vector int_tokens; - try - { - std::transform(dash_tokens.begin(), dash_tokens.end(), std::back_inserter(int_tokens), - [](const std::string &str) { return static_cast(std::stoi(str)); }); - } - catch (const std::out_of_range &) - { - // if input is big integer like '123467891234', stoi throw this exception. - std::cerr << "ERROR: Argument is out of range." << std::endl; - exit(EXIT_FAILURE); - } - catch (...) - { - std::cerr << "ERROR: Unknown error" << std::endl; - exit(EXIT_FAILURE); - } - - switch (int_tokens.size()) - { - case 0: // inputs like "-" - { - std::cerr << "ERROR: Nothing was entered" << std::endl; - exit(EXIT_FAILURE); - } - case 1: // inputs like "1", "2" - { - by_id.push_back(int_tokens.at(0)); - break; - } - case 2: // inputs like "1-2", "11-50" - { - for (uint32_t i = int_tokens.at(0); i <= int_tokens.at(1); i++) - { - by_id.push_back(i); - } - break; - } - default: // inputs like "1-2-3" - { - std::cerr << "ERROR: Too many '-' in str." << std::endl; - exit(EXIT_FAILURE); - } - } - } - - return by_id; -} - -std::vector split_name_input(const std::string &str) -{ - return ::split_into_vector(str, ','); -} - int entry(int argc, char **argv) { // TODO Add new option names! @@ -184,11 +69,6 @@ int entry(int argc, char **argv) std::string input_path = arser.get("input"); std::string output_path = arser.get("output"); - std::string operator_input; - - std::vector by_id; - std::vector by_name; - if (!arser["--by_id"] && !arser["--by_name"] || arser["--by_id"] && arser["--by_name"]) { std::cerr << "ERROR: Either option '--by_id' or '--by_name' must be specified" << std::endl; @@ -196,69 +76,33 @@ int entry(int argc, char **argv) return EXIT_FAILURE; } - if (arser["--by_id"]) - { - operator_input = arser.get("--by_id"); - by_id = split_id_input(operator_input); - } - if (arser["--by_name"]) - { - operator_input = arser.get("--by_name"); - by_name = split_name_input(operator_input); - } - // Import original circle file. auto module = opselector::getModule(input_path); - // Select nodes from user input. - std::vector selected_nodes; - - // put selected nodes into vector. - if (by_id.size()) + // TODO support two or more subgraphs + if (module.get()->size() != 1) { - loco::Graph *graph = module.get()->graph(0); // get main subgraph. + std::cerr << "ERROR: Not support two or more subgraphs" << std::endl; + return EXIT_FAILURE; + } - for (auto node : loco::all_nodes(graph)) - { - auto cnode = loco::must_cast(node); + opselector::OpSelector op_selector{module.get()}; - try - { - auto node_id = luci::get_node_id(cnode); // if the node is not operator, throw runtime_error + std::unique_ptr new_module; + std::string operator_input; - for (auto selected_id : by_id) - if (selected_id == node_id) // find the selected id - selected_nodes.emplace_back(cnode); - } - catch (std::runtime_error) - { - continue; - } - } - } - if (by_name.size()) + if (arser["--by_id"]) { - loco::Graph *graph = module.get()->graph(0); // get main subgraph. - - for (auto node : loco::all_nodes(graph)) - { - auto cnode = loco::must_cast(node); - std::string node_name = cnode->name(); - - for (auto selected_name : by_name) - if (selected_name.compare(node_name) == 0) // find the selected name - selected_nodes.emplace_back(cnode); - } + operator_input = arser.get("--by_id"); + new_module = op_selector.select_by(operator_input); } - if (selected_nodes.size() == 0) + if (arser["--by_name"]) { - std::cerr << "ERROR: No operator selected" << std::endl; - exit(EXIT_FAILURE); + operator_input = arser.get("--by_name"); + new_module = op_selector.select_by(operator_input); } - // TODO implement node selections - // Export to output Circle file - assert(opselector::exportModule(module.get(), output_path)); + assert(opselector::exportModule(new_module.get(), output_path)); return 0; } diff --git a/compiler/circle-opselector/src/Driver.test.cpp b/compiler/circle-opselector/src/Driver.test.cpp deleted file mode 100644 index 6e56908..0000000 --- a/compiler/circle-opselector/src/Driver.test.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Driver.test.h" -#include "TestHelper.h" - -#include - -TEST(DriverTest, NoArg_NEG) -{ - Argv<1> argv; - argv.add("circle-opselector"); - - ::testing::internal::CaptureStderr(); - ::testing::internal::CaptureStdout(); - int result = entry(1, argv.argv()); - ::testing::internal::GetCapturedStdout(); - ASSERT_EQ(EXIT_FAILURE, result); -} - -TEST(DriverTest, Wrong_ID_NEG) -{ - std::string str1 = "1"; - std::string empty = ""; - std::string no_integer = "1531538X5"; - - ASSERT_EQ(true, is_number(str1)); - ASSERT_EQ(false, is_number(empty)); - ASSERT_EQ(false, is_number(no_integer)); -} - -TEST(DriverTest, Split) -{ - std::vector vec1; - std::vector vec2; - - std::string hyphen = "1-3,8-10"; - std::string comma = "1,2,3"; - - vec1.push_back(1); - vec1.push_back(2); - vec1.push_back(3); - vec1.push_back(8); - vec1.push_back(9); - vec1.push_back(10); - - vec2.push_back(1); - vec2.push_back(2); - vec2.push_back(3); - - ASSERT_EQ(vec1, split_id_input(hyphen)); - ASSERT_EQ(vec2, split_id_input(comma)); -} diff --git a/compiler/circle-opselector/src/Driver.test.h b/compiler/circle-opselector/src/Driver.test.h deleted file mode 100644 index 06f1516..0000000 --- a/compiler/circle-opselector/src/Driver.test.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __CIRCLE_OPSELECTOR_DRIVER_TEST_H__ -#define __CIRCLE_OPSELECTOR_DRIVER_TEST_H__ - -#include -#include - -int entry(int argc, char **argv); -bool is_number(const std::string &s); -std::vector split_id_input(const std::string &str); - -#endif // __CIRCLE_OPSELECTOR_DRIVER_TEST_H__ diff --git a/compiler/circle-opselector/src/OpSelector.cpp b/compiler/circle-opselector/src/OpSelector.cpp new file mode 100644 index 0000000..31cbf9c --- /dev/null +++ b/compiler/circle-opselector/src/OpSelector.cpp @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OpSelector.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ + +/** + * @brief Tokenize given string + * + * Assumes given string looks like below. + * + * - '1,2,5,7,9' + * - '1-5,6,7,9,12-14' + * - 'tensor_a,tensor_b,tensor_d' + * + * NOTE. 1-5 is same with '1,2,3,4,5'. + * + * WARNING. SelectType::NAME doesn't allow '-' like 'tensor_a-tensor_c'. + */ +std::vector split_into_vector(const std::string &str, const char &delim) +{ + std::vector ret; + std::istringstream is(str); + for (std::string item; std::getline(is, item, delim);) + { + ret.push_back(item); + } + + // Remove empty string + ret.erase(std::remove_if(ret.begin(), ret.end(), [](const std::string &s) { return s.empty(); }), + ret.end()); + + return ret; +} + +bool is_number(const std::string &s) +{ + return !s.empty() && std::find_if(s.begin(), s.end(), + [](unsigned char c) { return !std::isdigit(c); }) == s.end(); +} + +bool is_number(const std::vector &vec) +{ + for (const auto &s : vec) + { + if (not::is_number(s)) + { + return false; + } + } + return true; +} + +// TODO Move this class into a separate header for reuse +class IsMultiOutputNode final : public luci::CircleNodeVisitor +{ +public: + bool visit(const luci::CircleCustom *) final { return true; } + bool visit(const luci::CircleIf *) final { return true; } + bool visit(const luci::CircleNonMaxSuppressionV4 *) final { return true; } + bool visit(const luci::CircleNonMaxSuppressionV5 *) final { return true; } + bool visit(const luci::CircleSplit *) final { return true; } + bool visit(const luci::CircleSplitV *) final { return true; } + bool visit(const luci::CircleTopKV2 *) final { return true; } + bool visit(const luci::CircleUnique *) final { return true; } + bool visit(const luci::CircleUnpack *) final { return true; } + bool visit(const luci::CircleWhile *) final { return true; } + // default is false + bool visit(const luci::CircleNode *) final { return false; } +}; + +std::unique_ptr make_graph(const std::vector nodes) +{ + auto graph = loco::make_graph(); + + luci::CloneContext ctx; + // clone nodes + for (const auto &n : nodes) + { + auto clone = luci::clone_node(n, graph.get()); + ctx.emplace(n, clone); + } + // set graph input + for (const auto &n : nodes) + { + for (uint32_t i = 0; i < n->arity(); i++) + { + auto arg = n->arg(i); + auto input_node = dynamic_cast(arg); + auto ctx_it = ctx.find(input_node); + // check if the node already has been cloned + if (ctx_it != ctx.end()) + continue; + // the node isn't graph input if it is an other node's input + if (std::find(nodes.begin(), nodes.end(), arg) != nodes.end()) + continue; + auto circle_const = dynamic_cast(arg); + if (circle_const != nullptr) + { + auto clone = luci::clone_node(circle_const, graph.get()); + ctx.emplace(circle_const, clone); + } + else + { + // circle input + auto circle_input = graph->nodes()->create(); + input_node = dynamic_cast(arg); + if (not input_node) + { + throw std::runtime_error{"ERROR: Invalid graph"}; + } + luci::copy_common_attributes(input_node, circle_input); + ctx.emplace(input_node, circle_input); + // graph input + auto graph_input = graph->inputs()->create(); + graph_input->name(circle_input->name()); + graph_input->dtype(circle_input->dtype()); + // graph input shape + auto input_shape = std::make_unique(); + input_shape->rank(circle_input->rank()); + for (uint32_t i = 0; i < circle_input->rank(); i++) + { + if (circle_input->dim(i).known()) + { + circle_input->dim(i).set(circle_input->dim(i).value()); + } + } + graph_input->shape(std::move(input_shape)); + + circle_input->index(graph_input->index()); + } + } + } + // set graph output + for (auto &n : nodes) + { + auto outputs = loco::succs(n); + bool beingUsed = false; + for (const auto &o : outputs) + { + if (std::find(nodes.begin(), nodes.end(), o) != nodes.end()) + { + beingUsed = true; + break; + } + } + // the node isn't graph output if it is an other node's output + if (beingUsed) + continue; + + IsMultiOutputNode multiout_visitor; + bool isMultiOut = n->accept(&multiout_visitor); + for (auto &o : outputs) + { + const luci::CircleNode *output_node = nullptr; + if (isMultiOut) + { + output_node = dynamic_cast(o); + if (not output_node) + { + throw std::runtime_error{"ERROR: Invalid graph"}; + } + } + else + { + output_node = n; + } + // circle output + auto circle_output = graph->nodes()->create(); + luci::copy_common_attributes(output_node, circle_output); + // connect to cloned output node + circle_output->from(ctx.find(output_node)->second); + // graph output + auto graph_output = graph->outputs()->create(); + graph_output->name(output_node->name()); + graph_output->dtype(output_node->dtype()); + // graph output shape + auto output_shape = std::make_unique(); + output_shape->rank(circle_output->rank()); + for (uint32_t i = 0; i < output_shape->rank(); i++) + { + if (circle_output->dim(i).known()) + { + output_shape->dim(i).set(circle_output->dim(i).value()); + } + } + graph_output->shape(std::move(output_shape)); + + circle_output->index(graph_output->index()); + if (not isMultiOut) + break; + } + } + // connect nodes + for (const auto &n : nodes) + { + luci::clone_connect(n, ctx); + } + + return graph; +} + +} // namespace + +namespace opselector +{ + +OpSelector::OpSelector(const luci::Module *module) : _module{module} +{ + if (_module->size() != 1) + { + throw std::runtime_error{"ERROR: Not support two or more subgraphs"}; + } +} + +template <> +std::vector +OpSelector::select_by(const std::vector &comma_tokens) +{ + std::vector by_id; + + for (const auto &comma_token : comma_tokens) + { + auto dash_tokens = ::split_into_vector(comma_token, '-'); + if (not::is_number(dash_tokens)) + { + throw std::runtime_error{ + "ERROR: To select operator by id, please use these args: [0-9], '-', ','"}; + } + + // Convert string into integer + std::vector int_tokens; + try + { + std::transform(dash_tokens.begin(), dash_tokens.end(), std::back_inserter(int_tokens), + [](const std::string &str) { return static_cast(std::stoi(str)); }); + } + catch (const std::out_of_range &) + { + // Uf input is big integer like '123467891234', stoi throws this exception. + throw std::runtime_error{"ERROR: Argument is out of range."}; + } + catch (...) + { + throw std::runtime_error{"ERROR: Unknown error"}; + } + + switch (int_tokens.size()) + { + case 0: // inputs like "-" + { + throw std::runtime_error{"ERROR: Nothing was entered"}; + } + case 1: // inputs like "1", "2" + { + by_id.push_back(int_tokens.at(0)); + break; + } + case 2: // inputs like "1-2", "11-50" + { + for (uint32_t i = int_tokens.at(0); i <= int_tokens.at(1); i++) + { + by_id.push_back(i); + } + break; + } + default: // inputs like "1-2-3" + { + throw std::runtime_error{"ERROR: Too many '-' in str."}; + } + } + } + + loco::Graph *graph = _module->graph(0); + std::vector selected_nodes; + + for (auto node : loco::all_nodes(graph)) + { + auto cnode = loco::must_cast(node); + + try + { + auto node_id = luci::get_node_id(cnode); + for (auto selected_id : by_id) + { + if (selected_id == node_id) + { + selected_nodes.emplace_back(cnode); + } + } + } + catch (const std::runtime_error &) + { + continue; + } + } + + return selected_nodes; +} + +template <> +std::vector +OpSelector::select_by(const std::vector &tokens) +{ + loco::Graph *graph = _module->graph(0); + std::vector selected_nodes; + + for (auto node : loco::all_nodes(graph)) + { + auto cnode = loco::must_cast(node); + std::string node_name = cnode->name(); + + for (auto selected_name : tokens) + if (selected_name.compare(node_name) == 0) // find the selected name + selected_nodes.emplace_back(cnode); + } + + return selected_nodes; +} + +template +std::unique_ptr OpSelector::select_by(const std::string &str) +{ + auto colon_tokens = ::split_into_vector(str, ','); + if (colon_tokens.empty()) + { + throw std::runtime_error{"ERROR: Nothing was entered."}; + } + + assert(_module->size() == 1); + + auto selected_nodes = select_by(colon_tokens); + + // multiout node should be considered + IsMultiOutputNode multiout_visitor; + std::vector output_nodes; + for (const auto &node : selected_nodes) + { + if (node->accept(&multiout_visitor)) + { + auto outputs = loco::succs(node); + for (auto &o : outputs) + { + output_nodes.push_back(dynamic_cast(o)); + } + } + } + selected_nodes.insert(selected_nodes.end(), output_nodes.begin(), output_nodes.end()); + + auto new_module = std::make_unique(); + new_module->add(::make_graph(selected_nodes)); + + return new_module; +} + +template std::unique_ptr +OpSelector::select_by(const std::string &str); + +template std::unique_ptr +OpSelector::select_by(const std::string &str); + +} // namespace opselector diff --git a/compiler/circle-opselector/src/OpSelector.h b/compiler/circle-opselector/src/OpSelector.h new file mode 100644 index 0000000..c4366fa --- /dev/null +++ b/compiler/circle-opselector/src/OpSelector.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __CIRCLE_OPSELECTOR_OPSELECTOR_H__ +#define __CIRCLE_OPSELECTOR_OPSELECTOR_H__ + +#include "SelectType.h" + +#include +#include + +#include +#include + +namespace opselector +{ + +class OpSelector final +{ +private: + const luci::Module *_module; + +public: + OpSelector(const luci::Module *module); + +private: + template + std::vector select_by(const std::vector &tokens); + +public: + template std::unique_ptr select_by(const std::string &str); +}; + +extern template std::unique_ptr +OpSelector::select_by(const std::string &str); +extern template std::unique_ptr +OpSelector::select_by(const std::string &str); + +} // namespace opselector + +#endif // __CIRCLE_OPSELECTOR_OPSELECTOR_H__ diff --git a/compiler/circle-opselector/src/OpSelector.test.cpp b/compiler/circle-opselector/src/OpSelector.test.cpp new file mode 100644 index 0000000..a5ccc03 --- /dev/null +++ b/compiler/circle-opselector/src/OpSelector.test.cpp @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OpSelector.h" + +#include + +#include + +namespace +{ + +/** + * Conv-Donv graphlet + * + * [Conv] + * | + * [Donv] + * + */ +class ConvDonvGraphlet +{ +public: + void init(loco::Graph *g) + { + _conv_filter = g->nodes()->create(); + _conv_filter->dtype(loco::DataType::FLOAT32); + _conv_filter->shape({16, 1, 1, 16}); + _conv_filter->name("conv_filter"); + + _conv_bias = g->nodes()->create(); + _conv_bias->dtype(loco::DataType::FLOAT32); + _conv_bias->shape({16}); + _conv_bias->name("conv_bias"); + + _conv = g->nodes()->create(); + _conv->padding(luci::Padding::SAME); + _conv->fusedActivationFunction(luci::FusedActFunc::NONE); + _conv->dtype(loco::DataType::FLOAT32); + _conv->shape({1, 4, 4, 16}); + _conv->name("conv"); + _conv->filter(_conv_filter); + _conv->bias(_conv_bias); + + _dconv_filter = g->nodes()->create(); + _dconv_filter->dtype(loco::DataType::FLOAT32); + _dconv_filter->shape({16, 1, 1, 16}); + _dconv_filter->name("dconv_filter"); + + _dconv_bias = g->nodes()->create(); + _dconv_bias->dtype(loco::DataType::FLOAT32); + _dconv_bias->shape({16}); + _dconv_bias->name("dconv_bias"); + + _dconv = g->nodes()->create(); + _dconv->input(_conv); + _dconv->depthMultiplier(1); + _dconv->fusedActivationFunction(luci::FusedActFunc::NONE); + _dconv->dtype(loco::DataType::FLOAT32); + _dconv->shape({1, 4, 4, 16}); + _dconv->padding(luci::Padding::SAME); + _dconv->name("dconv"); + _dconv->filter(_dconv_filter); + _dconv->bias(_dconv_bias); + } + +protected: + luci::CircleConv2D *_conv{nullptr}; + luci::CircleConst *_conv_filter{nullptr}; + luci::CircleConst *_conv_bias{nullptr}; + luci::CircleDepthwiseConv2D *_dconv{nullptr}; + luci::CircleConst *_dconv_filter{nullptr}; + luci::CircleConst *_dconv_bias{nullptr}; +}; + +class ConvDonvGraph : public luci::test::TestIOGraph, public ConvDonvGraphlet +{ +public: + ConvDonvGraph() + { + luci::test::TestIOGraph::init({1, 4, 4, 16}, {1, 4, 4, 16}); + ConvDonvGraphlet::init(g()); + + _conv->input(input()); + + output()->from(_dconv); + } + + std::unique_ptr graph(void) { return std::move(_g); } +}; + +} // namespace + +TEST(OpSelectorTest, select_by_name) +{ + auto m = luci::make_module(); + + ConvDonvGraph g; + g.transfer_to(m.get()); + + opselector::OpSelector op_selector{m.get()}; + + // Select conv only + auto conv_module = op_selector.select_by("conv"); + ASSERT_EQ(1, conv_module->size()); + + auto conv_graph = conv_module->graph(0); + ASSERT_EQ(1, conv_graph->outputs()->size()); + + auto output_node1 = luci::output_node(conv_graph, 0); + auto conv = loco::must_cast(output_node1->from()); + EXPECT_STREQ("conv", conv->name().c_str()); + auto conv_filter = loco::must_cast(conv->filter()); + EXPECT_STREQ("conv_filter", conv_filter->name().c_str()); + auto conv_bias = loco::must_cast(conv->bias()); + EXPECT_STREQ("conv_bias", conv_bias->name().c_str()); + + // Select dconv only + auto dconv_module = op_selector.select_by("dconv"); + ASSERT_EQ(1, dconv_module->size()); + + auto dconv_graph = dconv_module->graph(0); + ASSERT_EQ(1, dconv_graph->outputs()->size()); + + auto output_node2 = luci::output_node(dconv_graph, 0); + auto dconv = loco::must_cast(output_node2->from()); + EXPECT_STREQ("dconv", dconv->name().c_str()); + auto dconv_filter = loco::must_cast(dconv->filter()); + EXPECT_STREQ("dconv_filter", dconv_filter->name().c_str()); + auto dconv_bias = loco::must_cast(dconv->bias()); + EXPECT_STREQ("dconv_bias", dconv_bias->name().c_str()); +} + +TEST(OpSelectorTest, select_by_name_NEG) +{ + auto m = luci::make_module(); + + ConvDonvGraph g; + g.transfer_to(m.get()); + + opselector::OpSelector op_selector{m.get()}; + + EXPECT_ANY_THROW(op_selector.select_by(",")); +} diff --git a/compiler/circle-opselector/src/SelectType.h b/compiler/circle-opselector/src/SelectType.h new file mode 100644 index 0000000..46d5f09 --- /dev/null +++ b/compiler/circle-opselector/src/SelectType.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __CIRCLE_OPSELECTOR_SELECT_TYPE_H__ +#define __CIRCLE_OPSELECTOR_SELECT_TYPE_H__ + +#include + +namespace opselector +{ + +enum class SelectType +{ + ID, + NAME, +}; + +} // namespace opselector + +#endif // __CIRCLE_OPSELECTOR_SELECT_TYPE_H__ diff --git a/compiler/circle-part-value-test/CMakeLists.txt b/compiler/circle-part-value-test/CMakeLists.txt index ffe1b89..c4bd200 100644 --- a/compiler/circle-part-value-test/CMakeLists.txt +++ b/compiler/circle-part-value-test/CMakeLists.txt @@ -49,7 +49,7 @@ foreach(IDX RANGE ${RECIPE_LENGTH_M1}) add_custom_command(OUTPUT ${TFLITE_DST_PATH} COMMAND ${CMAKE_COMMAND} -E copy "${TFLITE_SRC_PATH}" "${TFLITE_DST_PATH}" - DEPENDS ${TFLITE_SRC_PATH} ${PARTITIONER_OUTPUT_PATH} + DEPENDS ${TFLITE_SRC_PATH} COMMENT "Copy ${RECIPE_NAME}.tflite" ) list(APPEND TEST_DEPS ${TFLITE_DST_PATH}) @@ -60,7 +60,7 @@ foreach(IDX RANGE ${RECIPE_LENGTH_M1}) add_custom_command(OUTPUT ${CIRCLE_DST_PATH} COMMAND ${CMAKE_COMMAND} -E copy "${CIRCLE_SRC_PATH}" "${CIRCLE_DST_PATH}" - DEPENDS ${CIRCLE_SRC_PATH} ${PARTITIONER_OUTPUT_PATH} + DEPENDS ${CIRCLE_SRC_PATH} COMMENT "Copy ${RECIPE_NAME}.circle" ) list(APPEND TEST_DEPS ${CIRCLE_DST_PATH}) @@ -72,7 +72,7 @@ foreach(IDX RANGE ${RECIPE_LENGTH_M1}) add_custom_command(OUTPUT ${PART_DST_PATH} COMMAND ${CMAKE_COMMAND} -E copy "${PART_SRC_PATH}" "${PART_DST_PATH}" - DEPENDS ${PART_SRC_PATH} ${PARTITIONER_OUTPUT_PATH} + DEPENDS ${PART_SRC_PATH} COMMENT "Copy ${PART_FILE}" ) list(APPEND TEST_DEPS ${PART_DST_PATH}) @@ -111,3 +111,13 @@ add_test(NAME circle_part_value_test "$" ${PARTITION_LIST} ) + +if(ONE_UBUNTU_CODENAME_JAMMY) + add_test(NAME circle_part_value_210_test + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/part_eval_all.sh" + "${CMAKE_CURRENT_BINARY_DIR}" + "${NNCC_OVERLAY_DIR}/venv_2_10_1" + "$" + ${PARTITION_LIST} + ) +endif(ONE_UBUNTU_CODENAME_JAMMY) diff --git a/compiler/circle-quantizer-dredd-recipe-test/CMakeLists.txt b/compiler/circle-quantizer-dredd-recipe-test/CMakeLists.txt index a3a2902..a9420d4 100644 --- a/compiler/circle-quantizer-dredd-recipe-test/CMakeLists.txt +++ b/compiler/circle-quantizer-dredd-recipe-test/CMakeLists.txt @@ -82,6 +82,47 @@ macro(AddFakeQuant RECIPE) list(APPEND TEST_NAMES ${RECIPE}) endmacro(AddFakeQuant) +# Macro to quantize without quantize_dequantize_weights +macro(AddSkipQDQW RECIPE) + cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + set(QCONFIG_OPT "") + if(ARG_USE_QCONFIG) + set(QCONFIG_OPT "--config" "${ARTIFACTS_BIN_PATH}/${RECIPE}.qconf.json") + endif() + + set(INPUT_DTYPE_OPT "") + if(ARG_INPUT_DTYPE) + set(INPUT_DTYPE_OPT "--input_type" "${ARG_INPUT_DTYPE}") + endif() + + set(OUTPUT_DTYPE_OPT "") + if(ARG_OUTPUT_DTYPE) + set(OUTPUT_DTYPE_OPT "--output_type" "${ARG_OUTPUT_DTYPE}") + endif() + + set(CIRCLE_PATH "${ARTIFACTS_BIN_PATH}/${RECIPE}.circle") + set(RECORDED_CIRCLE_PATH "${CMAKE_CURRENT_BINARY_DIR}/${RECIPE}.recorded.circle") + set(QUANT_CIRCLE_PATH "${CMAKE_CURRENT_BINARY_DIR}/${RECIPE}.q.circle") + + # Generate quantized .circle + add_custom_command(OUTPUT ${QUANT_CIRCLE_PATH} + COMMAND $ --input_model ${CIRCLE_PATH} --output_model ${RECORDED_CIRCLE_PATH} + COMMAND $ + --quantize_with_minmax float32 ${ARG_DTYPE} ${ARG_GRANULARITY} + ${QCONFIG_OPT} ${RECORDED_CIRCLE_PATH} ${QUANT_CIRCLE_PATH} + ${INPUT_DTYPE_OPT} ${OUTPUT_DTYPE_OPT} + DEPENDS + circle-quantizer + record-minmax + ${CIRCLE_PATH} + COMMENT "Generate ${RECIPE}.q.circle" + ) + + list(APPEND TEST_DEPS ${QUANT_CIRCLE_PATH}) + list(APPEND TEST_NAMES ${RECIPE}) +endmacro(AddSkipQDQW) + # Read "test.lst" include("test.lst") diff --git a/compiler/circle-quantizer-dredd-recipe-test/README.md b/compiler/circle-quantizer-dredd-recipe-test/README.md index 6152549..396cf7c 100644 --- a/compiler/circle-quantizer-dredd-recipe-test/README.md +++ b/compiler/circle-quantizer-dredd-recipe-test/README.md @@ -8,18 +8,18 @@ It tests non-functional conditions of a quantized circle model generated by circ 2. Make a recipe (`test.recipe`) for fp32 model under the directory. -3. Make a rule (`test.rule`) you want to test under the directory. (For more information on dredd-test-rules, see _dredd-rule-lib_ module.) +3. Make a rule (`test.rule`) you want to test under the directory. +(For more information on dredd-test-rules, see _dredd-rule-lib_ module.) 4. Add test to `test.lst` in this module with `Add` macro. - -``` -Add(RECIPE_DIR DTYPE dtype GRANULARITY granularity USE_QCONFIG) -``` - -- `RECIPE_DIR`: Path to the directory where the recipe file is saved. -- `DTYPE`: Default quantization dtype (uint8, int16) -- `GRANULARITY`: Quantization granularity (channel, layer) -- `USE_QCONFIG`: (Optional) Whether to use a quantization configuration file or not. If this is set, `test.qconf.json` should exist under `RECIPE_DIR` + ``` + Add(RECIPE_DIR DTYPE dtype GRANULARITY granularity USE_QCONFIG) + ``` + - `RECIPE_DIR`: Path to the directory where the recipe file is saved. + - `DTYPE`: Default quantization dtype (uint8, int16) + - `GRANULARITY`: Quantization granularity (channel, layer) + - `USE_QCONFIG`: (Optional) Whether to use a quantization configuration file or not. + If this is set, `test.qconf.json` should exist under `RECIPE_DIR` ## Example diff --git a/compiler/circle-quantizer-dredd-recipe-test/test.lst b/compiler/circle-quantizer-dredd-recipe-test/test.lst index 58f89c7..1464083 100644 --- a/compiler/circle-quantizer-dredd-recipe-test/test.lst +++ b/compiler/circle-quantizer-dredd-recipe-test/test.lst @@ -70,6 +70,8 @@ Add(Quant_Conv_001 DTYPE uint8 GRANULARITY channel OUTPUT_DTYPE float32) Add(Quant_Conv_002 DTYPE uint8 GRANULARITY channel INPUT_DTYPE float32 OUTPUT_DTYPE float32) AddFakeQuant(Quant_Add_000) +AddFakeQuant(Quant_DepthToSpace_000) +AddFakeQuant(Quant_SpaceToDepth_000) ## CIRCLE RECIPE @@ -78,3 +80,7 @@ Add(Quant_InstanceNorm_000 DTYPE uint8 GRANULARITY channel USE_QCONFIG) # MPQ Test (default: s16, target: u8) Add(Quant_InstanceNorm_001 DTYPE int16 GRANULARITY channel USE_QCONFIG) + +# Test for quantization without QuantizeDequantizeWeights +AddSkipQDQW(Quant_Conv_005 DTYPE uint8 GRANULARITY channel) +AddSkipQDQW(Quant_Conv_006 DTYPE int16 GRANULARITY channel) diff --git a/compiler/circle-quantizer/src/CircleQuantizer.cpp b/compiler/circle-quantizer/src/CircleQuantizer.cpp index f1e31ed..33a845c 100644 --- a/compiler/circle-quantizer/src/CircleQuantizer.cpp +++ b/compiler/circle-quantizer/src/CircleQuantizer.cpp @@ -104,8 +104,6 @@ void print_version(void) int entry(int argc, char **argv) { - // Simple argument parser (based on map) - std::map argparse; luci::CircleQuantizer quantizer; auto options = quantizer.options(); @@ -177,10 +175,14 @@ int entry(int argc, char **argv) "destination_tensor_name(string)"); arser.add_argument("--input_type") - .help("Input type of quantized model (uint8, int16, or float32)"); + .help("Input type of quantized model (uint8, int16, int32, int64, float32, or bool). For " + "multiple inputs, " + "use comma-separated values. e.g., uint8,int16"); arser.add_argument("--output_type") - .help("Output type of quantized model (uint8, int16, or float32)"); + .help("Output type of quantized model (uint8, int16, int32, int64, float32, or bool). For " + "multiple outputs, " + "use comma-separated values. e.g., uint8,int16"); arser.add_argument(cfg).help("Path to the quantization configuration file"); diff --git a/compiler/circle2circle-dredd-recipe-test/README.md b/compiler/circle2circle-dredd-recipe-test/README.md index 85140a8..b6e03e1 100644 --- a/compiler/circle2circle-dredd-recipe-test/README.md +++ b/compiler/circle2circle-dredd-recipe-test/README.md @@ -2,7 +2,8 @@ It tests the non-functional conditions of the optimized circle binary resulting from circle2circle. -This test basically refers to the _TensorFlowLiteRecipes_ resource. So you should add what you want to test to both of the resource and `test.lst`. +This test basically refers to the _TensorFlowLiteRecipes_ resource. +So you should add what you want to test to both of the resource and `test.lst`. ## Example diff --git a/compiler/circle2circle-dredd-recipe-test/test.lst b/compiler/circle2circle-dredd-recipe-test/test.lst index a6f2786..4fb1e78 100644 --- a/compiler/circle2circle-dredd-recipe-test/test.lst +++ b/compiler/circle2circle-dredd-recipe-test/test.lst @@ -20,9 +20,12 @@ Add(Net_Conv_FakeQuant_000 PASS remove_fakequant) Add(Net_Conv_QuantDequant_000 PASS remove_quantdequant) Add(Net_Conv_Min_Max_000 PASS transform_min_max_to_relu6) Add(Net_Conv_Min_Relu_000 PASS transform_min_relu_to_relu6) +Add(Net_Conv_PReluGraph_000 PASS fuse_prelu) Add(Net_Conv_Relu6_000 PASS fuse_activation_function) +Add(Net_Duplicate_Weights_000 PASS remove_duplicate_const) Add(Net_DwConv_BN_000 PASS fuse_batchnorm_with_dwconv) Add(Net_DwConv_BN_001 PASS fuse_batchnorm_with_dwconv) +Add(Net_FullyConnected_Add_000 PASS fold_fully_connected) Add(Net_Reshape_Reshape_000 PASS remove_redundant_reshape) Add(Net_Squeeze_Squeeze_000 PASS substitute_squeeze_to_reshape) Add(Net_TConv_Add_000 PASS fuse_add_with_tconv) @@ -33,6 +36,7 @@ Add(Net_TConv_BN_001 PASS fuse_batchnorm_with_tconv) Add(Net_TConv_BN_002 PASS fuse_batchnorm_with_tconv) Add(Net_TConv_BN_003 PASS fuse_batchnorm_with_tconv) Add(Net_TConv_BN_004 PASS fuse_batchnorm_with_tconv) +Add(Net_TConv_BN_005 PASS fuse_batchnorm_with_tconv) Add(Net_InstanceNorm_001 PASS fuse_instnorm) Add(Net_InstanceNorm_003 PASS fuse_instnorm) Add(Net_InstanceNorm_004 PASS fuse_instnorm) @@ -49,6 +53,7 @@ Add(MaxPoolWithArgmax_000 PASS resolve_customop_max_pool_with_argmax) Add(MaxPoolWithArgmax_001 PASS resolve_customop_max_pool_with_argmax) Add(MaxPoolWithArgmax_002 PASS resolve_customop_max_pool_with_argmax) Add(FullyConnected_007 PASS replace_non_const_fc_with_batch_matmul) +Add(FullyConnected_008 PASS replace_non_const_fc_with_batch_matmul) ## CIRCLE RECIPE diff --git a/compiler/circle2circle-dredd-recipe-test/testall.sh b/compiler/circle2circle-dredd-recipe-test/testall.sh index 2899587..94831d1 100755 --- a/compiler/circle2circle-dredd-recipe-test/testall.sh +++ b/compiler/circle2circle-dredd-recipe-test/testall.sh @@ -21,7 +21,6 @@ source "${CONFIG_PATH}" echo "-- Found circle-inspect: ${CIRCLE_INSPECT_PATH}" echo "-- Found circle-verify: ${CIRCLE_VERIFY_PATH}" -echo "-- Found circle2circle: ${CIRCLE2CIRCLE_PATH}" echo "-- Found common-artifacts: ${RESOURCE_DIR}" TESTED=() diff --git a/compiler/circle2circle/src/Circle2Circle.cpp b/compiler/circle2circle/src/Circle2Circle.cpp index f5cf0d7..9afb67d 100644 --- a/compiler/circle2circle/src/Circle2Circle.cpp +++ b/compiler/circle2circle/src/Circle2Circle.cpp @@ -77,10 +77,14 @@ int entry(int argc, char **argv) add_switch(arser, "--fold_dequantize", "This will fold dequantize op"); add_switch(arser, "--fold_dwconv", "This will fold Depthwise Convolution operator with constant inputs"); + add_switch(arser, "--fold_fully_connected", + "This will fold FullyConnected operator with constant inputs"); add_switch(arser, "--fold_gather", "This will fold Gather operator"); add_switch(arser, "--fold_sparse_to_dense", "This will fold SparseToDense operator"); add_switch(arser, "--forward_reshape_to_unaryop", "This will move Reshape after UnaryOp for centain condition"); + add_switch(arser, "--forward_transpose_op", + "This will move Transpose Op forward if possible (for further optimization)"); add_switch(arser, "--fuse_activation_function", "This will fuse Activation function to a preceding operator"); add_switch(arser, "--fuse_add_with_fully_connected", @@ -106,6 +110,8 @@ int entry(int argc, char **argv) "when the impact is known to be acceptable."); add_switch(arser, "--fuse_preactivation_batchnorm", "This will fuse BatchNorm operators of pre-activations to Convolution operator"); + add_switch(arser, "--fuse_prelu", "This will fuse operators to PReLU operator"); + add_switch(arser, "--remove_duplicate_const", "This will remove all duplicate constant nodes"); add_switch(arser, "--remove_fakequant", "This will remove FakeQuant operators"); add_switch(arser, "--remove_quantdequant", "This will remove Quantize-Dequantize sequence"); add_switch(arser, "--remove_redundant_quantize", "This will remove redundant Quantize operators"); @@ -149,6 +155,7 @@ int entry(int argc, char **argv) add_switch(arser, "--substitute_transpose_to_reshape", "This will convert single input Transpose to Reshape"); add_switch(arser, "--expand_broadcast_const", "This will expand broadcastable constant inputs"); + add_switch(arser, "--unroll_unidirseqlstm", "Unroll UnidirectionalSequenceLSTM operator."); add_switch(arser, "--convert_nchw_to_nhwc", "Experimental: This will convert NCHW operators to NHWC under the assumption that " "input model is NCHW."); @@ -216,12 +223,16 @@ int entry(int argc, char **argv) options->enable(Algorithms::FoldDequantize); if (arser.get("--fold_dwconv")) options->enable(Algorithms::FoldDepthwiseConv2D); + if (arser.get("--fold_fully_connected")) + options->enable(Algorithms::FoldFullyConnected); if (arser.get("--fold_gather")) options->enable(Algorithms::FoldGather); if (arser.get("--fold_sparse_to_dense")) options->enable(Algorithms::FoldSparseToDense); if (arser.get("--forward_reshape_to_unaryop")) options->enable(Algorithms::ForwardReshapeToUnaryOp); + if (arser.get("--forward_transpose_op")) + options->enable(Algorithms::ForwardTransposeOp); if (arser.get("--fuse_activation_function")) options->enable(Algorithms::FuseActivationFunction); if (arser.get("--fuse_batchnorm_with_conv")) @@ -244,8 +255,12 @@ int entry(int argc, char **argv) options->enable(Algorithms::MakeBatchNormGammaPositive); if (arser.get("--fuse_preactivation_batchnorm")) options->enable(Algorithms::FusePreActivationBatchNorm); + if (arser.get("--fuse_prelu")) + options->enable(Algorithms::FusePRelu); if (arser.get("--fuse_transpose_with_mean")) options->enable(Algorithms::FuseTransposeWithMean); + if (arser.get("--remove_duplicate_const")) + options->enable(Algorithms::RemoveDuplicateConst); if (arser.get("--remove_fakequant")) options->enable(Algorithms::RemoveFakeQuant); if (arser.get("--remove_quantdequant")) @@ -300,6 +315,8 @@ int entry(int argc, char **argv) options->enable(Algorithms::TransformMinReluToRelu6Pass); if (arser.get("--expand_broadcast_const")) options->enable(Algorithms::ExpandBroadcastConst); + if (arser.get("--unroll_unidirseqlstm")) + options->enable(Algorithms::UnrollUnidirSeqLSTM); if (arser.get("--mute_warnings")) settings->set(luci::UserSettings::Key::MuteWarnings, true); diff --git a/compiler/common-artifacts/CMakeLists.txt b/compiler/common-artifacts/CMakeLists.txt index 34a3a4d..7d89d45 100644 --- a/compiler/common-artifacts/CMakeLists.txt +++ b/compiler/common-artifacts/CMakeLists.txt @@ -1,4 +1,9 @@ #[[ Generate common python virtual enviornment ]] +# NOTE find_package try to use at least python3.8 as follows depending on platform version +# Ubuntu18.04; explictly installed python3.8 (default is python3.6) +# Ubuntu20.04; default python3.8 +# Ubuntu22.04; default python3.10 +# refer https://github.com/Samsung/ONE/issues/9962 find_package(PythonInterp 3.8 QUIET) find_package(PythonLibs 3.8 QUIET) @@ -14,6 +19,10 @@ endif() # Create python virtual environment with tensorflow 2.8.0 set(VIRTUALENV_OVERLAY_TF_2_8_0 "${NNCC_OVERLAY_DIR}/venv_2_8_0") +# TensorFlow 2.10.1 for Ubuntu22.04 +if(ONE_UBUNTU_CODENAME_JAMMY) + set(VIRTUALENV_OVERLAY_TF_2_10_1 "${NNCC_OVERLAY_DIR}/venv_2_10_1") +endif(ONE_UBUNTU_CODENAME_JAMMY) add_custom_command( OUTPUT ${VIRTUALENV_OVERLAY_TF_2_8_0} @@ -24,6 +33,15 @@ add_custom_command( set(REQUIREMENTS_FILE "requirements.txt") set(REQUIREMENTS_OVERLAY_PATH_TF_2_8_0 "${VIRTUALENV_OVERLAY_TF_2_8_0}/${REQUIREMENTS_FILE}") +if(ONE_UBUNTU_CODENAME_JAMMY) + add_custom_command( + OUTPUT ${VIRTUALENV_OVERLAY_TF_2_10_1} + COMMAND ${PYTHON_EXECUTABLE} -m venv ${VIRTUALENV_OVERLAY_TF_2_10_1} + ) + set(REQUIREMENTS_FILE "requirements.txt") + set(REQUIREMENTS_OVERLAY_PATH_TF_2_10_1 "${VIRTUALENV_OVERLAY_TF_2_10_1}/${REQUIREMENTS_FILE}") +endif(ONE_UBUNTU_CODENAME_JAMMY) + set(PYTHON_OVERLAY python3) if(PYTHON_EXECUTABLE MATCHES python3.8) set(PYTHON_OVERLAY python3.8) @@ -43,6 +61,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E echo "tensorflow-cpu==2.8.0" >> ${REQUIREMENTS_OVERLAY_PATH_TF_2_8_0} COMMAND ${CMAKE_COMMAND} -E echo "flatbuffers==1.12" >> ${REQUIREMENTS_OVERLAY_PATH_TF_2_8_0} COMMAND ${CMAKE_COMMAND} -E echo "protobuf==3.20.1" >> ${REQUIREMENTS_OVERLAY_PATH_TF_2_8_0} + COMMAND ${CMAKE_COMMAND} -E echo "pydot==1.4.2" >> ${REQUIREMENTS_OVERLAY_PATH_TF_2_8_0} COMMAND ${VIRTUALENV_OVERLAY_TF_2_8_0}/bin/${PYTHON_OVERLAY} -m pip --default-timeout=1000 ${PIP_OPTION_TRUSTED_HOST} install --upgrade pip setuptools COMMAND ${VIRTUALENV_OVERLAY_TF_2_8_0}/bin/${PYTHON_OVERLAY} -m pip --default-timeout=1000 @@ -55,6 +74,27 @@ add_custom_target(common_artifacts_python_deps ALL ${REQUIREMENTS_OVERLAY_PATH_TF_2_8_0} ) +if(ONE_UBUNTU_CODENAME_JAMMY) + add_custom_command( + OUTPUT ${REQUIREMENTS_OVERLAY_PATH_TF_2_10_1} + COMMAND ${CMAKE_COMMAND} -E remove -f ${REQUIREMENTS_OVERLAY_PATH_TF_2_10_1} + COMMAND ${CMAKE_COMMAND} -E echo "tensorflow-cpu==2.10.1" >> ${REQUIREMENTS_OVERLAY_PATH_TF_2_10_1} + COMMAND ${CMAKE_COMMAND} -E echo "flatbuffers==23.1.21" >> ${REQUIREMENTS_OVERLAY_PATH_TF_2_10_1} + COMMAND ${CMAKE_COMMAND} -E echo "protobuf==3.19.6" >> ${REQUIREMENTS_OVERLAY_PATH_TF_2_10_1} + COMMAND ${CMAKE_COMMAND} -E echo "pydot==1.4.2" >> ${REQUIREMENTS_OVERLAY_PATH_TF_2_10_1} + COMMAND ${VIRTUALENV_OVERLAY_TF_2_10_1}/bin/${PYTHON_OVERLAY} -m pip --default-timeout=1000 + ${PIP_OPTION_TRUSTED_HOST} install --upgrade pip setuptools + COMMAND ${VIRTUALENV_OVERLAY_TF_2_10_1}/bin/${PYTHON_OVERLAY} -m pip --default-timeout=1000 + ${PIP_OPTION_TRUSTED_HOST} install -r ${REQUIREMENTS_OVERLAY_PATH_TF_2_10_1} --upgrade + DEPENDS ${VIRTUALENV_OVERLAY_TF_2_10_1} + ) + + add_custom_target(common_artifacts_python_u22_deps ALL + DEPENDS ${VIRTUALENV_OVERLAY_TF_2_10_1} + ${REQUIREMENTS_OVERLAY_PATH_TF_2_10_1} + ) +endif(ONE_UBUNTU_CODENAME_JAMMY) + #[[ Generate common resources ]] # TODO add pbtxt nnas_find_package(HDF5 QUIET) @@ -86,7 +126,7 @@ set(TEST_RECIPE_FILENAME "test.recipe") set(TEST_RULE_FILENAME "test.rule") set(TEST_QCONFIG_FILENAME "test.qconf.json") -set(MODEL2NNPKG "${NNAS_PROJECT_SOURCE_DIR}/tools/nnpackage_tool/model2nnpkg/model2nnpkg.sh") +set(MODEL2NNPKG "${NNAS_PROJECT_SOURCE_DIR}/tools/nnpackage_tool/model2nnpkg/model2nnpkg.py") # Get test case list unset(RECIPES) file(GLOB TFLITE_SUBDIR RELATIVE ${TFLITE_RECIPE_REPO} ${TFLITE_RECIPE_REPO}/*) @@ -270,8 +310,8 @@ foreach(RECIPE IN ITEMS ${RECIPES}) list(APPEND TEST_DEPS ${NNPKG_DIR}) add_custom_command(OUTPUT ${NNPKG_MODEL} - COMMAND ${MODEL2NNPKG} ${MODEL_PATH} - DEPENDS ${MODEL2NNPKG} ${MODEL_PATH} ${NNPKG_DIR} + COMMAND ${PYTHON_EXECUTABLE} ${MODEL2NNPKG} -m ${MODEL_PATH} + DEPENDS ${MODEL2NNPKG} ${MODEL_PATH} COMMENT "Generate ${RECIPE} nnpackage" ) list(APPEND TEST_DEPS ${NNPKG_MODEL}) @@ -281,7 +321,6 @@ foreach(RECIPE IN ITEMS ${RECIPES}) set(TC_DIRECTORY "${NNPKG_DIR}/metadata/tc") add_custom_command(OUTPUT ${TC_DIRECTORY} COMMAND ${CMAKE_COMMAND} -E make_directory ${TC_DIRECTORY} - DEPENDS ${NNPKG_DIR} COMMENT "Generate ${RECIPE} nnpackage test directory" ) list(APPEND TEST_DEPS ${TC_DIRECTORY}) diff --git a/compiler/common-artifacts/exclude.lst b/compiler/common-artifacts/exclude.lst index 2275a42..f238e79 100644 --- a/compiler/common-artifacts/exclude.lst +++ b/compiler/common-artifacts/exclude.lst @@ -5,7 +5,6 @@ #[[ optimize : Exclude from circle optimization(circle2circle) ]] ## TensorFlowLiteRecipes -optimize(UnidirectionalSequenceLSTM_001) # This recipe contains is_variable Tensor ## CircleRecipes @@ -136,8 +135,7 @@ tcgenerate(Tile_000) tcgenerate(Tile_U8_000) tcgenerate(TopKV2_000) tcgenerate(TopKV2_001) -tcgenerate(UnidirectionalSequenceLSTM_000) # runtime and luci-interpreter doesn't support UnidirectionalSequenceLSTM op yet -tcgenerate(UnidirectionalSequenceLSTM_001) # runtime and luci-interpreter doesn't support UnidirectionalSequenceLSTM op yet +tcgenerate(UnidirectionalSequenceLSTM_000) # This mode is just for Op creation, cannot run tcgenerate(Unique_000) tcgenerate(Unique_001) tcgenerate(Unique_002) diff --git a/compiler/dalgona-test/.gitignore b/compiler/dalgona-test/.gitignore new file mode 100644 index 0000000..19f2918 --- /dev/null +++ b/compiler/dalgona-test/.gitignore @@ -0,0 +1 @@ +test.local.lst diff --git a/compiler/dalgona-test/CMakeLists.txt b/compiler/dalgona-test/CMakeLists.txt new file mode 100644 index 0000000..a233831 --- /dev/null +++ b/compiler/dalgona-test/CMakeLists.txt @@ -0,0 +1,69 @@ +if(NOT ENABLE_TEST) + return() +endif(NOT ENABLE_TEST) + +unset(DALGONA_SINGLE_OP_TEST) + +macro(singleOpTest NAME) + list(APPEND DALGONA_SINGLE_OP_TEST ${NAME}) +endmacro(singleOpTest) + +# Read "test.lst" +include("test.lst") +# Read "test.local.lst" if exists +include("test.local.lst" OPTIONAL) + +unset(TEST_DEPS) + +get_target_property(ARTIFACTS_BIN_PATH testDataGenerator BINARY_DIR) + +# Place test scripts in one place +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/GenH5RandomInputs.py" "${CMAKE_CURRENT_BINARY_DIR}/GenH5RandomInputs.py" COPYONLY) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/SingleOperatorTest.py" "${CMAKE_CURRENT_BINARY_DIR}/SingleOperatorTest.py" COPYONLY) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/RandomDataGenerator.py" "${CMAKE_CURRENT_BINARY_DIR}/RandomDataGenerator.py" COPYONLY) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/TestUtil.py" "${CMAKE_CURRENT_BINARY_DIR}/TestUtil.py" COPYONLY) + +### +### Generate test.config +### +set(TEST_CONFIG "${CMAKE_CURRENT_BINARY_DIR}/test.config") + +add_custom_command( + OUTPUT ${TEST_CONFIG} + COMMAND ${CMAKE_COMMAND} -E remove -f ${TEST_CONFIG} + COMMAND ${CMAKE_COMMAND} -E echo 'DALGONA_PATH=\"$\"' >> ${TEST_CONFIG} + DEPENDS dalgona + COMMENT "Generate test configuration" +) + +# Import pics module +get_target_property(PICS_BIN_PATH pics BINARY_DIR) +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/circle + COMMAND ${CMAKE_COMMAND} -E create_symlink + ${PICS_BIN_PATH}/circle ${CMAKE_CURRENT_BINARY_DIR}/circle) + +list(APPEND TEST_DEPS "${TEST_CONFIG}" "${CMAKE_CURRENT_BINARY_DIR}/circle") + +# This enforces CMake to generate all the dependencies during "build" phase +add_custom_target(dalgona_test_deps ALL DEPENDS ${TEST_DEPS}) + +# Run tests +add_test( + NAME dalgona_single_op_test + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/TestSingleOp.sh" + "${TEST_CONFIG}" + "${ARTIFACTS_BIN_PATH}" + "${NNCC_OVERLAY_DIR}/venv_2_8_0" + ${DALGONA_SINGLE_OP_TEST} +) + +if(ONE_UBUNTU_CODENAME_JAMMY) + add_test( + NAME dalgona_single_op_210_test + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/TestSingleOp.sh" + "${TEST_CONFIG}" + "${ARTIFACTS_BIN_PATH}" + "${NNCC_OVERLAY_DIR}/venv_2_10_1" + ${DALGONA_SINGLE_OP_TEST} + ) +endif(ONE_UBUNTU_CODENAME_JAMMY) diff --git a/compiler/dalgona-test/GenH5RandomInputs.py b/compiler/dalgona-test/GenH5RandomInputs.py new file mode 100644 index 0000000..795cd5a --- /dev/null +++ b/compiler/dalgona-test/GenH5RandomInputs.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +# Copyright 2022 Samsung Electronics Co., Ltd. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import h5py as h5 +import numpy as np +import argparse + +from circle.Model import Model +from RandomDataGenerator import RandomDataGenerator + +# +# This script generates a pack of random input data (.h5) expected by the input circle model +# +# Basic usage: +# gen_h5_random_inputs.py --model --num_data --output +# ex: gen_h5_random_inputs.py --model add.circle --num_data 3 --output add.circle.input.h5 +# (This will create add.circle.input.h5 composed of three random inputs in the same directory as the model) +parser = argparse.ArgumentParser() +parser.add_argument('--model', type=str, required=True) +parser.add_argument('--num_data', type=int, required=True) +parser.add_argument('--output', type=str, required=True) +args = parser.parse_args() + +model = args.model +num_data = args.num_data +output_path = args.output + +with open(model, 'rb') as f: + buf = f.read() + circle_model = Model.GetRootAsModel(buf, 0) + +# Assume one subgraph +assert (circle_model.SubgraphsLength() == 1) +graph = circle_model.Subgraphs(0) +inputs = graph.InputsAsNumpy() + +# Create h5 file +h5_file = h5.File(output_path, 'w') +group = h5_file.create_group("value") +group.attrs['desc'] = "Input data for " + model + +# Generate random data +for i in range(num_data): + sample = group.create_group(str(i)) + for j in range(len(inputs)): + input_index = inputs[j] + tensor = graph.Tensors(input_index) + g = RandomDataGenerator(tensor.ShapeAsNumpy()) + input_data = g.gen(tensor.Type()) + sample.create_dataset(str(j), data=input_data) + +h5_file.close() diff --git a/compiler/dalgona-test/RandomDataGenerator.py b/compiler/dalgona-test/RandomDataGenerator.py new file mode 100644 index 0000000..6fa9ab0 --- /dev/null +++ b/compiler/dalgona-test/RandomDataGenerator.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +# Copyright 2022 Samsung Electronics Co., Ltd. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import numpy as np +from circle.TensorType import TensorType + + +class RandomDataGenerator: + def __init__(self, shape): + self.shape = shape + + def _unsupported_types(self): + raise RuntimeError('Unsupported data type') + + def _gen_uint8(self): + return np.random.randint(0, high=256, size=self.shape, dtype=np.uint8) + + def _gen_int16(self): + return np.random.randint(-32767, high=32768, size=self.shape, dtype=np.int16) + + def _gen_float32(self): + return np.array(10 * np.random.random_sample(self.shape) - 5, np.float32) + + def gen(self, dtype): + gen_book = dict() + gen_book[TensorType.UINT8] = self._gen_uint8 + gen_book[TensorType.INT16] = self._gen_int16 + gen_book[TensorType.FLOAT32] = self._gen_float32 + + return gen_book.get(dtype, self._unsupported_types)() diff --git a/compiler/dalgona-test/SingleOperatorTest.py b/compiler/dalgona-test/SingleOperatorTest.py new file mode 100644 index 0000000..9de77dc --- /dev/null +++ b/compiler/dalgona-test/SingleOperatorTest.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 + +# Copyright 2022 Samsung Electronics Co., Ltd. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +""""Test for a model with a single operator""" + +from TestUtil import * + +from circle import Model +from circle import BuiltinOptions +from circle import BuiltinOperator +from circle import Conv2DOptions +from circle import DepthwiseConv2DOptions +from circle import AddOptions +from circle import FullyConnectedOptions +from circle import TransposeConvOptions +from circle import InstanceNormOptions +from circle import SplitOptions + + +class SingleOperatorTest(object): + def StartAnalysis(self, args): + """Called when the analysis starts""" + with open(args, 'rb') as f: + buffer = f.read() + self._model = Model.Model.GetRootAsModel(buffer, 0) + + # Check model has one subgraph + assertTrue(self._model.SubgraphsLength() == 1, "Model has more than one subgraph") + graph = self._model.Subgraphs(0) + + # Check model has one operator + assertTrue(graph.OperatorsLength() == 1, "Model has more than one operator") + self._op = graph.Operators(0) + + def DefaultOpPost(self, name, opcode, inputs, output): + raise SystemExit('NYI operator: ' + str(opcode)) + + def testConv2D(self, padding, stride, dilation, fused_act): + # Check opcode + opcode = self._model.OperatorCodes(self._op.OpcodeIndex()) + checkOpcode(opcode.BuiltinCode(), BuiltinOperator.BuiltinOperator.CONV_2D) + + # Check option + checkBuiltinOptionType(self._op.BuiltinOptionsType(), + BuiltinOptions.BuiltinOptions.Conv2DOptions) + + self._opt = self._op.BuiltinOptions() + opt = Conv2DOptions.Conv2DOptions() + opt.Init(self._opt.Bytes, self._opt.Pos) + checkPadding(padding, opt.Padding()) + assertTrue(opt.StrideW() == stride['w'], "Stride_w mismatches") + assertTrue(opt.StrideH() == stride['h'], "Stride_h mismatches") + assertTrue(opt.DilationWFactor() == dilation['w'], "Dilation_w mismatches") + assertTrue(opt.DilationHFactor() == dilation['h'], "Dilation_w mismatches") + checkActivation(fused_act, opt.FusedActivationFunction()) + + def Conv2DPre(self, name, input, filter, bias, padding, stride, dilation, fused_act): + self.testConv2D(padding, stride, dilation, fused_act) + + def Conv2DPost(self, name, input, filter, bias, padding, stride, dilation, output, + fused_act): + self.testConv2D(padding, stride, dilation, fused_act) + + def testAdd(self, fused_act): + # Check opcode + opcode = self._model.OperatorCodes(self._op.OpcodeIndex()) + checkOpcode(opcode.BuiltinCode(), BuiltinOperator.BuiltinOperator.ADD) + + # Check option + checkBuiltinOptionType(self._op.BuiltinOptionsType(), + BuiltinOptions.BuiltinOptions.AddOptions) + + self._opt = self._op.BuiltinOptions() + opt = AddOptions.AddOptions() + opt.Init(self._opt.Bytes, self._opt.Pos) + checkActivation(fused_act, opt.FusedActivationFunction()) + + def AddPre(self, name, x, y, fused_act): + self.testAdd(fused_act) + + def AddPost(self, name, x, y, output, fused_act): + self.testAdd(fused_act) + + def testDepthwiseConv2D(self, padding, stride, depth_multiplier, dilation, fused_act): + # Check opcode + opcode = self._model.OperatorCodes(self._op.OpcodeIndex()) + checkOpcode(opcode.BuiltinCode(), + BuiltinOperator.BuiltinOperator.DEPTHWISE_CONV_2D) + + # Check option + checkBuiltinOptionType(self._op.BuiltinOptionsType(), + BuiltinOptions.BuiltinOptions.DepthwiseConv2DOptions) + + self._opt = self._op.BuiltinOptions() + opt = DepthwiseConv2DOptions.DepthwiseConv2DOptions() + opt.Init(self._opt.Bytes, self._opt.Pos) + checkPadding(padding, opt.Padding()) + assertTrue(opt.StrideW() == stride['w'], "Stride_w mismatches") + assertTrue(opt.StrideH() == stride['h'], "Stride_h mismatches") + assertTrue(opt.DepthMultiplier() == depth_multiplier, + "Depth multiplier mismatches") + assertTrue(opt.DilationWFactor() == dilation['w'], "Dilation_w mismatches") + assertTrue(opt.DilationHFactor() == dilation['h'], "Dilation_w mismatches") + checkActivation(fused_act, opt.FusedActivationFunction()) + + def DepthwiseConv2DPre(self, name, input, filter, bias, padding, stride, + depth_multiplier, dilation, fused_act): + self.testDepthwiseConv2D(padding, stride, depth_multiplier, dilation, fused_act) + + def DepthwiseConv2DPost(self, name, input, filter, bias, padding, stride, + depth_multiplier, dilation, output, fused_act): + self.testDepthwiseConv2D(padding, stride, depth_multiplier, dilation, fused_act) + + def testFullyConnected(self, fused_act): + # Check opcode + opcode = self._model.OperatorCodes(self._op.OpcodeIndex()) + checkOpcode(opcode.BuiltinCode(), BuiltinOperator.BuiltinOperator.FULLY_CONNECTED) + + # Check option + checkBuiltinOptionType(self._op.BuiltinOptionsType(), + BuiltinOptions.BuiltinOptions.FullyConnectedOptions) + + self._opt = self._op.BuiltinOptions() + opt = FullyConnectedOptions.FullyConnectedOptions() + opt.Init(self._opt.Bytes, self._opt.Pos) + checkActivation(fused_act, opt.FusedActivationFunction()) + + def FullyConnectedPre(self, name, input, weights, bias, fused_act): + self.testFullyConnected(fused_act) + + def FullyConnectedPost(self, name, input, weights, bias, output, fused_act): + self.testFullyConnected(fused_act) + + def testTransposeConv(self, padding, stride): + # Check opcode + opcode = self._model.OperatorCodes(self._op.OpcodeIndex()) + checkOpcode(opcode.BuiltinCode(), BuiltinOperator.BuiltinOperator.TRANSPOSE_CONV) + + # Check option + checkBuiltinOptionType(self._op.BuiltinOptionsType(), + BuiltinOptions.BuiltinOptions.TransposeConvOptions) + + self._opt = self._op.BuiltinOptions() + opt = TransposeConvOptions.TransposeConvOptions() + opt.Init(self._opt.Bytes, self._opt.Pos) + checkPadding(padding, opt.Padding()) + assertTrue(opt.StrideW() == stride['w'], "Stride_w mismatches") + assertTrue(opt.StrideH() == stride['h'], "Stride_h mismatches") + + def TransposeConvPre(self, name, input, filter, output_shape, bias, padding, stride): + self.testTransposeConv(padding, stride) + + def TransposeConvPost(self, name, input, filter, output_shape, bias, padding, stride, + output): + self.testTransposeConv(padding, stride) + + def testInstanceNorm(self, epsilon, fused_act): + # Check opcode + opcode = self._model.OperatorCodes(self._op.OpcodeIndex()) + checkOpcode(opcode.BuiltinCode(), BuiltinOperator.BuiltinOperator.INSTANCE_NORM) + + # Check option + checkBuiltinOptionType(self._op.BuiltinOptionsType(), + BuiltinOptions.BuiltinOptions.InstanceNormOptions) + + self._opt = self._op.BuiltinOptions() + opt = InstanceNormOptions.InstanceNormOptions() + opt.Init(self._opt.Bytes, self._opt.Pos) + assertTrue(opt.Epsilon() == epsilon, "epsilon mismatches") + checkActivation(fused_act, opt.FusedActivationFunction()) + + def InstanceNormPre(self, name, input, gamma, beta, epsilon, fused_act): + self.testInstanceNorm(epsilon, fused_act) + + def InstanceNormPost(self, name, input, gamma, beta, epsilon, output, fused_act): + self.testInstanceNorm(epsilon, fused_act) + + def testSplit(self, num_split): + # Check opcode + opcode = self._model.OperatorCodes(self._op.OpcodeIndex()) + checkOpcode(opcode.BuiltinCode(), BuiltinOperator.BuiltinOperator.SPLIT) + + # Check option + checkBuiltinOptionType(self._op.BuiltinOptionsType(), + BuiltinOptions.BuiltinOptions.SplitOptions) + + self._opt = self._op.BuiltinOptions() + opt = SplitOptions.SplitOptions() + opt.Init(self._opt.Bytes, self._opt.Pos) + assertTrue(opt.NumSplits() == num_split, "num_split mismatches") + + def SplitPre(self, name, split_dim, input, num_split): + self.testSplit(num_split) + + def SplitPost(self, name, split_dim, input, num_split, outputs): + self.testSplit(num_split) + assertTrue(num_split == len(outputs), "num_split mismatches with outputs") diff --git a/compiler/dalgona-test/TestSingleOp.sh b/compiler/dalgona-test/TestSingleOp.sh new file mode 100755 index 0000000..fad26fb --- /dev/null +++ b/compiler/dalgona-test/TestSingleOp.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +# Copyright 2022 Samsung Electronics Co., Ltd. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +# This script tests the basic behavior of dalgona +# +# HOW TO USE +# +# ./test_single_op.sh ... +# test.config : set ${DALGONA_PATH} +# work_dir : archive of common-artifacts (ex: build/compiler/common-artifacts) +# venv : virtual environment for python execution + +CONFIG_PATH="$1"; shift +BIN_PATH=$(dirname "$CONFIG_PATH") +GEN_SCRIPT_PATH="${BIN_PATH}/GenH5RandomInputs.py" +TEST_SCRIPT_PATH="${BIN_PATH}/SingleOperatorTest.py" +WORKDIR="$1"; shift +VIRTUALENV="$1"; shift + +source "${CONFIG_PATH}" + +echo "-- Found DALGONA: ${DALGONA_PATH}" +echo "-- Found workdir: ${WORKDIR}" + +TESTED=() +PASSED=() +FAILED=() + +pushd "${WORKDIR}" +for TESTCASE in "$@"; do + TESTED+=("${TESTCASE}") + + TESTCASE_FILE="${WORKDIR}/${TESTCASE}" + + PASSED_TAG="${BIN_PATH}/${TESTCASE}.passed" + rm -f "${PASSED_TAG}" + + cat > "${BIN_PATH}/${TESTCASE}.log" <( + exec 2>&1 + set -ex + + # Generate random h5 input data + source "${VIRTUALENV}/bin/activate" + "${VIRTUALENV}/bin/python" "${GEN_SCRIPT_PATH}" \ + --model "${TESTCASE_FILE}.circle" \ + --num_data 3 \ + --output "${BIN_PATH}/${TESTCASE}.circle.input.h5" + if [[ $? -ne 0 ]]; then + echo "FAILED TO GENERATE INPUT" + continue + fi + + # Run dalgona with test script(SingleOperatorTest.py) + "${DALGONA_PATH}" \ + --input_model "${TESTCASE_FILE}.circle" \ + --input_data "${BIN_PATH}/${TESTCASE}.circle.input.h5" \ + --analysis "${TEST_SCRIPT_PATH}" \ + --analysis_args "${TESTCASE_FILE}.circle" + + if [[ $? -eq 0 ]]; then + touch "${PASSED_TAG}" + fi + ) + + if [[ -f "${PASSED_TAG}" ]]; then + PASSED+=("$TESTCASE") + else + FAILED+=("$TESTCASE") + fi +done +popd + +if [[ ${#TESTED[@]} -ne ${#PASSED[@]} ]]; then + echo "FAILED" + for TEST in "${FAILED[@]}" + do + echo "- ${TEST}" + done + exit 255 +fi + +echo "PASSED" +exit 0 diff --git a/compiler/dalgona-test/TestUtil.py b/compiler/dalgona-test/TestUtil.py new file mode 100644 index 0000000..d646528 --- /dev/null +++ b/compiler/dalgona-test/TestUtil.py @@ -0,0 +1,58 @@ +# Copyright 2022 Samsung Electronics Co., Ltd. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from circle import ActivationFunctionType +from circle import BuiltinOptions +from circle import Padding + + +def assertTrue(cond, msg): + assert cond, msg + + +def checkPadding(pad, exp_pad): + if pad == 'SAME': + assertTrue(exp_pad == Padding.Padding.SAME, "Padding mismatches") + elif pad == 'VALID': + assertTrue(exp_pad == Padding.Padding.VALID, "Padding mismatches") + else: + raise SystemExit('Unsupported padding') + + +def checkActivation(act, exp_act): + act_functions = { + 'relu': ActivationFunctionType.ActivationFunctionType.RELU, + 'relu6': ActivationFunctionType.ActivationFunctionType.RELU6, + 'relu_n1_to_1': ActivationFunctionType.ActivationFunctionType.RELU_N1_TO_1, + 'tanh': ActivationFunctionType.ActivationFunctionType.TANH, + 'none': ActivationFunctionType.ActivationFunctionType.NONE, + 'sign_bit': ActivationFunctionType.ActivationFunctionType.SIGN_BIT, + } + + try: + assertTrue(act_functions[act] == exp_act, "Activation function mismatches") + except KeyError: + raise SystemExit('Unsupported activation functions') + + +def checkOpcode(opcode, exp_opcode): + assertTrue(opcode == exp_opcode, + "Opcode mismatches (" + str(opcode) + ", " + str(exp_opcode) + ")") + + +def checkBuiltinOptionType(option, exp_option): + assertTrue( + option == exp_option, + "Built-in option type mismatches (" + str(option) + ", " + str(exp_option) + ")") diff --git a/compiler/dalgona-test/requires.cmake b/compiler/dalgona-test/requires.cmake new file mode 100644 index 0000000..ab75e09 --- /dev/null +++ b/compiler/dalgona-test/requires.cmake @@ -0,0 +1,3 @@ +require("dalgona") +require("common-artifacts") +require("pics") diff --git a/compiler/dalgona-test/test.lst b/compiler/dalgona-test/test.lst new file mode 100644 index 0000000..36c4070 --- /dev/null +++ b/compiler/dalgona-test/test.lst @@ -0,0 +1,6 @@ +singleOpTest(Conv2D_000) +singleOpTest(Conv2D_001) +singleOpTest(Conv2D_002) +singleOpTest(Conv2D_003) +singleOpTest(Split_000) +singleOpTest(InstanceNorm_000) diff --git a/compiler/dalgona/CMakeLists.txt b/compiler/dalgona/CMakeLists.txt new file mode 100644 index 0000000..bd06424 --- /dev/null +++ b/compiler/dalgona/CMakeLists.txt @@ -0,0 +1,63 @@ +# NOTE find_package will try to use at least python3.8 as follows depending on platform version +# Ubuntu18.04; explictly installed python3.8 (default is python3.6) +# Ubuntu20.04; default python3.8 +# Ubuntu22.04; default python3.10 +# refer https://github.com/Samsung/ONE/issues/9962 +find_package(PythonInterp 3.8 QUIET) +find_package(PythonLibs 3.8 QUIET) + +if(NOT ${PYTHONINTERP_FOUND}) + message(STATUS "Build dalgona: FAILED (Python3 is missing)") + return() +endif() + +if(${PYTHON_VERSION_MINOR} LESS 8) + message(STATUS "Build dalgona: FAILED (Install Python version higher than or equal to 3.8)") + return() +endif() + +nnas_find_package(Pybind11) +if(NOT Pybind11_FOUND) + message(STATUS "Build dalgona: FAILED (Pybind11 is missing)") + return() +endif(NOT Pybind11_FOUND) + +set(DRIVER "driver/Driver.cpp") + +file(GLOB_RECURSE SOURCES "src/*.cpp") +file(GLOB_RECURSE TESTS "src/*.test.cpp") +list(REMOVE_ITEM SOURCES ${TESTS}) + +add_compile_options(-fvisibility=hidden) + +add_executable(dalgona ${DRIVER} ${SOURCES}) +target_include_directories(dalgona PRIVATE include) +target_include_directories(dalgona PRIVATE ${PYTHON_INCLUDE_DIRS}) +target_include_directories(dalgona PRIVATE ${Pybind11_INCLUDE_DIRS}) + +target_link_libraries(dalgona INTERFACE pybind11::embed) +target_link_libraries(dalgona PRIVATE ${PYTHON_LIBRARIES}) +target_link_libraries(dalgona PRIVATE arser) +target_link_libraries(dalgona PRIVATE safemain) +target_link_libraries(dalgona PRIVATE foder) +target_link_libraries(dalgona PRIVATE luci_import) +target_link_libraries(dalgona PRIVATE luci_interpreter) +target_link_libraries(dalgona PRIVATE dio_hdf5) +target_link_libraries(dalgona PRIVATE nncc_common) + +install(TARGETS dalgona DESTINATION bin) + +if(NOT ENABLE_TEST) + return() +endif(NOT ENABLE_TEST) + +# dalgona is executable, so we do not link it to the test. +# Instead, we use TEST_SOURCES to specify sources used for tests. +set(TEST_SOURCES + "src/StringUtils.cpp" + "src/RandomUtils.cpp") + +nnas_find_package(GTest REQUIRED) +GTest_AddTest(dalgona_unit_test ${TESTS} ${TEST_SOURCES}) +target_include_directories(dalgona_unit_test PRIVATE src) +target_link_libraries(dalgona_unit_test luci_lang) diff --git a/compiler/dalgona/README.md b/compiler/dalgona/README.md new file mode 100644 index 0000000..0fd0f0b --- /dev/null +++ b/compiler/dalgona/README.md @@ -0,0 +1,104 @@ +# dalgona + +## What is dalgona? + +_dalgona_ is a tool for dynamic analysis of deep neural network. + +## How it works? + +_dalgona_ runs a user's custom analysis code (written in "Python") while performing inference. The analysis code has the form of hooks, called before/after each operator is executed. Intermediate execution results (values of activations) are passed to the hooks, so users can analyze the distribution of activations inside the hooks. The analysis result can be exported as files, log messages or any other forms, used for various purposes (model compression, optimization, etc.). + +NOTE Inference is performed by `luci-interpreter`. + +## Possible applications +- Finding quantization parameters based on the distribution of activations +- Finding sparse activations by observing the portion of zero values +- Finding the distribution of conditional variables in If-statement and While-statement +- Visualization of activation data with Python libraries + +## Prerequisite +- Python 3.8 (python3.8, python3.8-dev packages) +- Circle model (target to analyze) +- Input data of the model (hdf5 format. See _rawdata2hdf5_ or _gen_h5_explicit_inputs.py_ for more details.) +- Analysis code (Python code) + +## Example +``` +dalgona \ + --input_model model.circle + --input_data data.h5 + --analysis analysis/AnalysisTemplate.py +``` + +## Arguments +``` + --help Show help message and exit + --input_model Input model filepath (.circle) + --input_data Input data filepath (.h5) (if not given, random data will be used) + --analysis Analysis code filepath (.py) + --analysis_args (optional) String argument passed to the analysis code +``` + +## How to write analysis code? + +_dalgona_ provides hooks which are called before/after an operator is executed. +Users can access tensors relevant to the corresponding operator inside the hooks. +The information of each operator is passed as the arguments of the hook. +For example, for a Conv2D operator, _dalgona_ provides following hooks. + +``` + def Conv2DPre(self, name, input, filter, bias, padding, stride, dilation, fused_act) + def Conv2DPost(self, name, input, filter, bias, padding, stride, dilation, output, fused_act) +``` + +`Conv2DPre`/`Conv2DPost` are called before/after Conv2D is executed, respectively. Users can write codes to analyze the distribution of intermediate tensors using the provided arguments. + +(Note that Conv2DPost has one more argument "output", which is the execution result of the operator) + +Details about the arguments of each hook can be found in the section "Arguments of Hooks". + +We proivde a template for the analysis code in `analysis/AnalysisTemplate.py`. Users can copy the template file and modify it to write their custom analysis codes. + +| List of hooks | Explanation | +| --------------|------------ | +| StartAnalysis(self) | Called when the analysis starts | +| EndAnalysis(self) | Called when the analysis ends | +| StartNetworkExecution(self, inputs) | Called when the execution of a network starts | +| EndNetworkExecution(self, outputs) | Called when the execution of a network ends | +| DefaultOpPre(self, name, opcode, inputs) | Default hook called before an operator is executed | +| DefaultOpPost(self, name, opcode, inputs, output) | Default hook called after an operator is executed | +| \Pre/Post | Hooks called before/after the corresponding operator is executed. | + +## Arguments of Hooks + +Arguments are implemented with built-in Python types. + +Tensor +- Type: dict +- {name:str, data: np.ndarray, quantparam: QuantParam, is_const: bool} + +QuantParam +- Type: dict +- {scale: list, zero_point: list, quantized_dimension: int} + +Padding +- Type: string +- Values: 'SAME', 'VALID' + +Stride +- Type: dict +- {w: int, h: int} + +Dilation +- Type: dict +- {w: int, h: int} + +FusedActivationFunction +- Type: string +- Values: 'none', 'relu', 'relu_n1_to_1', 'relu6' + +## What's different from Hook APIs in Tensorflow or Pytorch? + +Basically, dalgona works in the same way as Hooks in TF or Pytorch. It calls user-defined functions before/after each operator is executed. + +A major difference is that dalgona runs with a model desinged for inference (i.e., circle, which can be directly converted from tflite). diff --git a/compiler/dalgona/analysis/AnalysisTemplate.py b/compiler/dalgona/analysis/AnalysisTemplate.py new file mode 100644 index 0000000..b1ffac4 --- /dev/null +++ b/compiler/dalgona/analysis/AnalysisTemplate.py @@ -0,0 +1,125 @@ +# Copyright 2022 Samsung Electronics Co., Ltd. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +""" +A template for anlaysis code. +This template shows how to access the information of each operator inside hooks. +Users can write their own hooks by modifying this file. + +NOTE See "Arguments of Hooks" section in README to understand argument types (Tensor, Stride, ..) +NOTE See "tests/SingleOperatorTest.py" for more operators. +""" + + +class AnalysisTemplate(object): + def StartAnalysis(self, args: str): + """ + Called when the analysis starts + args: string given by --analysis_args option + """ + print("Analysis started.") + print("args", args) + + def EndAnalysis(self): + """ + Called when the analysis ends + """ + print("Analysis ended.") + + def StartNetworkExecution(self, inputs: list): + """ + Called when the execution of a network starts + inputs: list of Tensor + """ + print("Network execution started.") + + def EndNetworkExecution(self, outputs: list): + """ + Called when the execution of a network ends + outputs: list of Tensor + """ + print("Network execution ended.") + + def DefaultOpPre(self, name: str, opcode: str, inputs: list): + """ + Default hook called before an operator is executed + name: output tensor name (string) + opcode: opcode name (string) + inputs: list of Tensor + """ + print("name", name) + print("opcode", opcode) + print("inputs", inputs) + + def DefaultOpPost(self, name: str, opcode: str, inputs: list, output: dict): + """ + Default hook called after an operator is executed + name: output tensor name (string) + opcode: opcode name (string) + inputs: list of Tensor + output: Tensor + """ + print("name", name) + print("opcode", opcode) + print("inputs", inputs) + print("output", output) + + def Conv2DPre(self, name: str, input: dict, filter: dict, bias: dict, padding: str, + stride: dict, dilation: dict, fused_act: str): + """ + Called before Conv2D layer execution + name: output tensor name (string) + opcode: opcode name (string) + input: Tensor + filter: Tensor + bias: Tensor + padding: Padding (string) + stride: Stride + dilation: Dilation + fused_act: Fused activation functions (string) + """ + print("name", name) + print("input", input) + print("filter", filter) + print("bias", bias) + print("padding", padding) + print("stride", stride) + print("dilation", dilation) + print("fused activation", fused_act) + + def Conv2DPost(self, name: str, input: dict, filter: dict, bias: dict, padding: str, + stride: dict, dilation: dict, output: dict, fused_act: str): + """ + Called after Conv2D layer execution + name: output tensor name (string) + opcode: opcode name (string) + input: Tensor + filter: Tensor + bias: Tensor + padding: Padding (string) + stride: Stride + dilation: Dilation + output: Tensor + fused_act: Fused activation functions (string) + """ + print("name", name) + print("input", input) + print("filter", filter) + print("bias", bias) + print("padding", padding) + print("stride", stride) + print("dilation", dilation) + print("output shape", output['data'].shape) + print("output type", output['data'].dtype) + print("fused activation", fused_act) diff --git a/compiler/dalgona/driver/Driver.cpp b/compiler/dalgona/driver/Driver.cpp new file mode 100644 index 0000000..8bba0b7 --- /dev/null +++ b/compiler/dalgona/driver/Driver.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Dalgona.h" + +#include +#include + +namespace py = pybind11; + +using namespace dalgona; + +int entry(const int argc, char **argv) +{ + arser::Arser arser("Dalgona: Dynamic analysis tool for DNN"); + + arser.add_argument("--input_model") + .nargs(1) + .type(arser::DataType::STR) + .required(true) + .help("Input model filepath (.circle)"); + + arser.add_argument("--input_data") + .nargs(1) + .type(arser::DataType::STR) + .help("Input data filepath (.h5) (if not given, random data will be used)"); + + arser.add_argument("--analysis") + .nargs(1) + .type(arser::DataType::STR) + .required(true) + .help("Analysis code filepath (.py)"); + + arser.add_argument("--analysis_args") + .nargs(1) + .type(arser::DataType::STR) + .help("String argument passed to the analysis code"); + + try + { + arser.parse(argc, argv); + } + catch (const std::runtime_error &err) + { + std::cout << err.what() << std::endl; + std::cout << arser; + return EXIT_FAILURE; + } + + auto input_model_path = arser.get("--input_model"); + auto analysis_path = arser.get("--analysis"); + std::string analysis_args = ""; + if (arser["--analysis_args"]) + analysis_args = arser.get("--analysis_args"); + + // Initialize python interpreter + py::scoped_interpreter guard{}; + + Dalgona dalgona; + + // Initialize interpreter and operator hooks + dalgona.initialize(input_model_path); + + // Run analysis + if (arser["--input_data"]) + { + const auto input_data_path = arser.get("--input_data"); + dalgona.runAnalysisWithH5Input(input_data_path, analysis_path, analysis_args); + } + else + { + std::cout << "--input_data was not specified. Run with a random input." << std::endl; + dalgona.runAnalysisWithRandomInput(analysis_path, analysis_args); + } + + return EXIT_SUCCESS; +} diff --git a/compiler/dalgona/include/Dalgona.h b/compiler/dalgona/include/Dalgona.h new file mode 100644 index 0000000..353e268 --- /dev/null +++ b/compiler/dalgona/include/Dalgona.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DALGONA_H__ +#define __DALGONA_H__ + +#include +#include + +#include "PythonHooks.h" + +#include + +namespace dalgona +{ + +class Dalgona +{ +public: + explicit Dalgona() = default; + + ~Dalgona() = default; + + void initialize(const std::string &input_model_path); + + // Run analysis with hdf5 input + void runAnalysisWithH5Input(const std::string &input_data_path, const std::string &analysis_path, + const std::string &analysis_args); + + // Run analysis with random input + void runAnalysisWithRandomInput(const std::string &analysis_path, + const std::string &analysis_args); + +private: + std::unique_ptr _module{nullptr}; + std::unique_ptr _interpreter{nullptr}; + std::unique_ptr _hooks{nullptr}; +}; + +} // namespace dalgona + +#endif // __DALGONA_H__ diff --git a/compiler/dalgona/include/PythonHooks.h b/compiler/dalgona/include/PythonHooks.h new file mode 100644 index 0000000..8c100aa --- /dev/null +++ b/compiler/dalgona/include/PythonHooks.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DALGONA_PYTHON_HOOKS_H__ +#define __DALGONA_PYTHON_HOOKS_H__ + +#include +#include + +#include + +#include + +namespace py = pybind11; + +namespace dalgona +{ + +class PythonHooks : public luci_interpreter::ExecutionObserver +{ +public: + PythonHooks(luci_interpreter::Interpreter *interpreter) : _interpreter(interpreter) + { + // Do nothing + } + + // Called when the analysis starts + void importAnalysis(const std::string &analysis_path, py::object &globals, + const std::string &analysis_args); + + // Called after the analysis is done + void endAnalysis(); + + // Called before a network is started to be executed + void startNetworkExecution(loco::Graph *graph); + + // Called after a network is executed + void endNetworkExecution(loco::Graph *graph); + + // Called before an operator is executed + void preOperatorExecute(const luci::CircleNode *node) override; + + // Called after an operator is executed + void postOperatorExecute(const luci::CircleNode *node) override; + +private: + luci_interpreter::Interpreter *_interpreter = nullptr; + py::object _analysis; +}; + +} // namespace dalgona + +#endif // __DALGONA_PYTHON_HOOKS_H__ diff --git a/compiler/dalgona/requires.cmake b/compiler/dalgona/requires.cmake new file mode 100644 index 0000000..185476b --- /dev/null +++ b/compiler/dalgona/requires.cmake @@ -0,0 +1,6 @@ +require("safemain") +require("arser") +require("foder") +require("luci") +require("luci-interpreter") +require("dio-hdf5") diff --git a/compiler/dalgona/src/Dalgona.cpp b/compiler/dalgona/src/Dalgona.cpp new file mode 100644 index 0000000..1d060b4 --- /dev/null +++ b/compiler/dalgona/src/Dalgona.cpp @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Dalgona.h" +#include "PythonHooks.h" +#include "RandomUtils.h" + +#include +#include +#include + +#include + +#include +#include + +using Shape = std::vector; +using DataType = loco::DataType; + +namespace py = pybind11; + +namespace +{ + +uint32_t numElements(const luci::CircleNode *node) +{ + assert(node != nullptr); // FIX_CALLER_UNLESS + + uint32_t num_elements = 1; + for (uint32_t i = 0; i < node->rank(); i++) + num_elements *= node->dim(i).value(); + + return num_elements; +} + +// Return tensor's size in bytes +template size_t getByteSize(const NodeT *node) +{ + assert(node != nullptr); // FIX_CALLER_UNLESS + + uint32_t dtype_size = loco::size(node->dtype()); + return static_cast(dtype_size) * static_cast(numElements(node)); +} + +// Throw exception if input has one of the following conditions. +// 1. Have unknown dimension +// 2. Number of elements is 0 +void checkInputDimension(const luci::CircleInput *input) +{ + assert(input != nullptr); // FIX_CALLER_UNLESS + + for (uint32_t i = 0; i < input->rank(); i++) + if (!input->dim(i).known()) + throw std::runtime_error(input->name() + " has unknown dimension"); + + if (numElements(input) == 0) + throw std::runtime_error(input->name() + " is a zero-sized input"); +} + +// Check the type and the shape of CircleInput +// Throw an exception if type or shape does not match +void verifyTypeShape(const luci::CircleInput *input_node, const DataType &dtype, const Shape &shape) +{ + assert(input_node != nullptr); // FIX_CALLER_UNLESS + + // Type check + if (dtype != input_node->dtype()) + throw std::runtime_error("Wrong input type."); + + if (shape.size() != input_node->rank()) + throw std::runtime_error("Input rank mismatch."); + + for (uint32_t i = 0; i < shape.size(); i++) + { + if (not(shape.at(i) == input_node->dim(i))) + throw std::runtime_error("Input shape mismatch."); + } +} + +} // namespace + +namespace dalgona +{ + +void Dalgona::initialize(const std::string &input_model_path) +{ + // Load model from the file + foder::FileLoader loader{input_model_path}; + std::vector model_data = loader.load(); + + // Verify flatbuffers + flatbuffers::Verifier verifier{reinterpret_cast(model_data.data()), + model_data.size()}; + if (not circle::VerifyModelBuffer(verifier)) + throw std::runtime_error("Failed to verify circle '" + input_model_path + "'"); + + auto circle_model = circle::GetModel(model_data.data()); + + if (not circle_model) + throw std::runtime_error("Failed to load '" + input_model_path + "'"); + + _module = luci::Importer().importModule(circle_model); + + if (not _module) + throw std::runtime_error("ERROR: Failed to load '" + input_model_path + "'"); + + // Initialize interpreter + _interpreter = std::make_unique(_module.get()); + + _hooks = std::make_unique(_interpreter.get()); + + _interpreter->attachObserver(_hooks.get()); +} + +void Dalgona::runAnalysisWithH5Input(const std::string &input_data_path, + const std::string &analysis_path, + const std::string &analysis_args) +{ + py::object scope = py::module::import("__main__").attr("__dict__"); + _hooks->importAnalysis(analysis_path, scope, analysis_args); + + try + { + dio::hdf5::HDF5Importer importer(input_data_path); + importer.importGroup("value"); + + bool is_raw_data = importer.isRawData(); + + const auto num_records = importer.numData(); + if (num_records == 0) + throw std::runtime_error("The input data file does not contain any record."); + + const auto input_nodes = loco::input_nodes(_module->graph()); + const auto num_inputs = input_nodes.size(); + + for (int32_t record_idx = 0; record_idx < num_records; record_idx++) + { + if (num_inputs != static_cast(importer.numInputs(record_idx))) + throw std::runtime_error("Wrong number of inputs."); + + std::cout << "Running " << record_idx << "'th data" << std::endl; + + for (uint32_t input_idx = 0; input_idx < num_inputs; input_idx++) + { + const auto *input_node = loco::must_cast(input_nodes[input_idx]); + assert(input_node->index() == input_idx); + checkInputDimension(input_node); + std::vector input_data(getByteSize(input_node)); + + if (is_raw_data) + { + // Skip type/shape check for raw data + importer.readTensor(record_idx, input_idx, input_data.data()); + } + else + { + DataType dtype; + Shape shape; + importer.readTensor(record_idx, input_idx, &dtype, &shape, input_data.data()); + + // Check the type and the shape of the input data is valid + verifyTypeShape(input_node, dtype, shape); + } + + _interpreter->writeInputTensor(input_node, input_data.data(), input_data.size()); + } + + _hooks->startNetworkExecution(_module->graph()); + _interpreter->interpret(); + _hooks->endNetworkExecution(_module->graph()); + } + + std::cout << "Finished executing " << num_records << "'th data" << std::endl; + _hooks->endAnalysis(); + } + catch (const H5::Exception &e) + { + H5::Exception::printErrorStack(); + throw std::runtime_error("HDF5 error occurred."); + } +} + +void Dalgona::runAnalysisWithRandomInput(const std::string &analysis_path, + const std::string &analysis_args) +{ + py::object scope = py::module::import("__main__").attr("__dict__"); + _hooks->importAnalysis(analysis_path, scope, analysis_args); + + const auto input_nodes = loco::input_nodes(_module->graph()); + const auto num_inputs = input_nodes.size(); + + for (uint32_t input_idx = 0; input_idx < num_inputs; input_idx++) + { + const auto *input_node = loco::must_cast(input_nodes[input_idx]); + assert(input_node->index() == input_idx); + checkInputDimension(input_node); + + uint32_t num_elems = numElements(input_node); + switch (input_node->dtype()) + { + case DataType::FLOAT32: + { + // Synced with record-minmax (-5,5) + auto input_data = genRandomFloatData(num_elems, -5, 5); + _interpreter->writeInputTensor(input_node, input_data.data(), + input_data.size() * sizeof(float)); + break; + } + case DataType::U8: + { + auto input_data = genRandomIntData(num_elems, std::numeric_limits::min(), + std::numeric_limits::max()); + _interpreter->writeInputTensor(input_node, input_data.data(), + input_data.size() * sizeof(uint8_t)); + break; + } + case DataType::S16: + { + auto input_data = genRandomIntData(num_elems, std::numeric_limits::min(), + std::numeric_limits::max()); + _interpreter->writeInputTensor(input_node, input_data.data(), + input_data.size() * sizeof(int16_t)); + break; + } + case DataType::S32: + { + // Synced with record-minmax (0, 100) + auto input_data = genRandomIntData(num_elems, 0, 100); + _interpreter->writeInputTensor(input_node, input_data.data(), + input_data.size() * sizeof(int32_t)); + break; + } + case DataType::S64: + { + // Synced with record-minmax (0, 100) + auto input_data = genRandomIntData(num_elems, 0, 100); + _interpreter->writeInputTensor(input_node, input_data.data(), + input_data.size() * sizeof(int64_t)); + break; + } + case DataType::BOOL: + { + // Bool is represented as uint8 (0 or 1) + auto input_data = genRandomIntData(num_elems, 0, 1); + _interpreter->writeInputTensor(input_node, input_data.data(), + input_data.size() * sizeof(uint8_t)); + break; + } + default: + throw std::runtime_error("Unsupported input data type in " + input_node->name()); + } + } + + _hooks->startNetworkExecution(_module->graph()); + _interpreter->interpret(); + _hooks->endNetworkExecution(_module->graph()); + + std::cout << "Finished executing a random input" << std::endl; + _hooks->endAnalysis(); +} + +} // namespace dalgona diff --git a/compiler/dalgona/src/PostOperatorHook.h b/compiler/dalgona/src/PostOperatorHook.h new file mode 100644 index 0000000..daf32f6 --- /dev/null +++ b/compiler/dalgona/src/PostOperatorHook.h @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DALGONA_POST_OPERATOR_HOOK_H__ +#define __DALGONA_POST_OPERATOR_HOOK_H__ + +#include "Utils.h" +#include "StringUtils.h" + +#include +#include +#include + +#include +#include + +namespace py = pybind11; +using namespace py::literals; + +namespace dalgona +{ + +// Invoke a user-written Python hook after an operator is executed +class PostOperatorHook final : public luci::CircleNodeVisitor +{ + +// This macro creates three variables used for post-operator hooks. +// 1. hook: Python function to be invoked (type: py::object) +// 2. inputs: input data (type: std::vector of numpy array) +// 3. output: output data (type: numpy array) +#define POST_OPERATOR_HOOK_PROLOGUE(OP_NAME) \ + if (!py::hasattr(_analysis, #OP_NAME "Post")) \ + { \ + visit(loco::must_cast(node)); \ + return; \ + } \ + py::object hook = _analysis.attr(#OP_NAME "Post"); \ + auto inputs = inputsPyArray(node, _interpreter); \ + auto output = outputPyArray(node, _interpreter); + +// Multi-output version of POST_OPERATOR_HOOK_PROLOGUE +#define POST_OPERATOR_HOOK_PROLOGUE_MULTI_OUTS(OP_NAME) \ + if (!py::hasattr(_analysis, #OP_NAME "Post")) \ + { \ + visit(loco::must_cast(node)); \ + return; \ + } \ + py::object hook = _analysis.attr(#OP_NAME "Post"); \ + auto inputs = inputsPyArray(node, _interpreter); \ + auto outputs = outputsPyArray(node, _interpreter); + +private: + py::object _analysis; + luci_interpreter::Interpreter *_interpreter{nullptr}; + +public: + explicit PostOperatorHook(py::object analysis, luci_interpreter::Interpreter *interpreter) + : _analysis(analysis), _interpreter(interpreter) + { + // Do nothing + } + + // default + void visit(const luci::CircleNode *node) + { + if (not py::hasattr(_analysis, "DefaultOpPost")) + return; + + py::object hook = _analysis.attr("DefaultOpPost"); + auto inputs = inputsPyArray(node, _interpreter); + auto output = outputPyArray(node, _interpreter); + + py::list input_list; + for (uint32_t i = 0; i < inputs.size(); i++) + { + input_list.append(inputs[i]); + } + + pySafeCall(hook, + node->name(), // name + toString(node->opcode()), // opcode + input_list, // list of inputs + output // output + ); + } + + void visit(const luci::CircleConv2D *node) + { + POST_OPERATOR_HOOK_PROLOGUE(Conv2D) + + auto padding = node->padding(); + auto stride = node->stride(); + auto dilation = node->dilation(); + + auto py_stride = py::dict("w"_a = stride->w(), "h"_a = stride->h()); + auto py_dilation = py::dict("w"_a = dilation->w(), "h"_a = dilation->h()); + + auto fused_act = node->fusedActivationFunction(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // input + inputs[1], // filter + inputs[2], // bias + padding == luci::Padding::SAME ? "SAME" : "VALID", // padding + py_stride, // stride + py_dilation, // dilation + output, // output + toString(fused_act) // fused activation + ); + } + + void visit(const luci::CircleDepthwiseConv2D *node) + { + POST_OPERATOR_HOOK_PROLOGUE(DepthwiseConv2D) + + auto padding = node->padding(); + auto stride = node->stride(); + auto dilation = node->dilation(); + auto depthMultiplier = node->depthMultiplier(); + + auto py_stride = py::dict("w"_a = stride->w(), "h"_a = stride->h()); + auto py_dilation = py::dict("w"_a = dilation->w(), "h"_a = dilation->h()); + + auto fused_act = node->fusedActivationFunction(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // input + inputs[1], // filter + inputs[2], // bias + padding == luci::Padding::SAME ? "SAME" : "VALID", // padding + py_stride, // stride + depthMultiplier, // depthMultiplier + py_dilation, // dilation + output, // output + toString(fused_act) // fused activation + ); + } + + void visit(const luci::CircleAdd *node) + { + POST_OPERATOR_HOOK_PROLOGUE(Add) + + auto fused_act = node->fusedActivationFunction(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // x + inputs[1], // y + output, // output + toString(fused_act) // fused activation + ); + } + + void visit(const luci::CircleFullyConnected *node) + { + POST_OPERATOR_HOOK_PROLOGUE(FullyConnected) + + auto fused_act = node->fusedActivationFunction(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // input + inputs[1], // weights + inputs[2], // bias + output, // output + toString(fused_act) // fused activation + ); + } + + void visit(const luci::CircleTransposeConv *node) + { + POST_OPERATOR_HOOK_PROLOGUE(TransposeConv) + + auto padding = node->padding(); + auto stride = node->stride(); + + auto py_stride = py::dict("w"_a = stride->w(), "h"_a = stride->h()); + + pySafeCall(hook, + node->name(), // name + inputs[2], // input + inputs[1], // filter + inputs[0], // output shape + inputs.size() == 4 ? inputs[3] : none(), // bias + padding == luci::Padding::SAME ? "SAME" : "VALID", // padding + py_stride, // stride + output // output + ); + } + + void visit(const luci::CircleInstanceNorm *node) + { + POST_OPERATOR_HOOK_PROLOGUE(InstanceNorm) + + auto epsilon = node->epsilon(); + + auto fused_act = node->fusedActivationFunction(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // input + inputs[1], // gamma + inputs[2], // beta + epsilon, // epsilon + output, // output + toString(fused_act) // fused activation + ); + } + + void visit(const luci::CircleSplit *node) + { + POST_OPERATOR_HOOK_PROLOGUE_MULTI_OUTS(Split) + + py::list output_list; + for (uint32_t i = 0; i < outputs.size(); i++) + { + output_list.append(outputs[i]); + } + + auto num_split = node->num_split(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // split_dim + inputs[1], // input + num_split, // num_split + output_list // list of outputs + ); + } + +#undef POST_OPERATOR_HOOK_PROLOGUE_MULTI_OUTS +}; + +} // namespace dalgona + +#endif // __DALGONA_POST_OPERATOR_HOOK_H__ diff --git a/compiler/dalgona/src/PreOperatorHook.h b/compiler/dalgona/src/PreOperatorHook.h new file mode 100644 index 0000000..eb6a95e --- /dev/null +++ b/compiler/dalgona/src/PreOperatorHook.h @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DALGONA_PRE_OPERATOR_HOOK_H__ +#define __DALGONA_PRE_OPERATOR_HOOK_H__ + +#include "Utils.h" +#include "StringUtils.h" + +#include +#include +#include + +#include +#include + +namespace py = pybind11; +using namespace py::literals; + +namespace dalgona +{ + +// Invoke a user-written Python hook before an operator is executed +class PreOperatorHook final : public luci::CircleNodeVisitor +{ + +// This macro creates two variables used for pre-operator hooks. +// 1. hook: Python function to be invoked (type: py::object) +// 2. inputs: input data (type: std::vector of numpy array) +#define PRE_OPERATOR_HOOK_PROLOGUE(OP_NAME) \ + if (!py::hasattr(_analysis, #OP_NAME "Pre")) \ + { \ + visit(loco::must_cast(node)); \ + return; \ + } \ + py::object hook = _analysis.attr(#OP_NAME "Pre"); \ + auto inputs = inputsPyArray(node, _interpreter); + +private: + py::object _analysis; + luci_interpreter::Interpreter *_interpreter{nullptr}; + +public: + explicit PreOperatorHook(py::object analysis, luci_interpreter::Interpreter *interpreter) + : _analysis(analysis), _interpreter(interpreter) + { + // Do nothing + } + + // default + void visit(const luci::CircleNode *node) + { + if (not py::hasattr(_analysis, "DefaultOpPre")) + return; + + py::object hook = _analysis.attr("DefaultOpPre"); + auto inputs = inputsPyArray(node, _interpreter); + + py::list input_list; + for (uint32_t i = 0; i < inputs.size(); i++) + { + input_list.append(inputs[i]); + } + + pySafeCall(hook, + node->name(), // name + toString(node->opcode()), // opcode + input_list // list of inputs + ); + } + + void visit(const luci::CircleConv2D *node) + { + PRE_OPERATOR_HOOK_PROLOGUE(Conv2D) + + auto padding = node->padding(); + auto stride = node->stride(); + auto dilation = node->dilation(); + + auto py_stride = py::dict("w"_a = stride->w(), "h"_a = stride->h()); + auto py_dilation = py::dict("w"_a = dilation->w(), "h"_a = dilation->h()); + + auto fused_act = node->fusedActivationFunction(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // input + inputs[1], // filter + inputs[2], // bias + padding == luci::Padding::SAME ? "SAME" : "VALID", // padding + py_stride, // stride + py_dilation, // dilation + toString(fused_act) // fused activation + ); + } + + void visit(const luci::CircleDepthwiseConv2D *node) + { + PRE_OPERATOR_HOOK_PROLOGUE(DepthwiseConv2D) + + auto padding = node->padding(); + auto stride = node->stride(); + auto dilation = node->dilation(); + auto depthMultiplier = node->depthMultiplier(); + + auto py_stride = py::dict("w"_a = stride->w(), "h"_a = stride->h()); + auto py_dilation = py::dict("w"_a = dilation->w(), "h"_a = dilation->h()); + + auto fused_act = node->fusedActivationFunction(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // input + inputs[1], // filter + inputs[2], // bias + padding == luci::Padding::SAME ? "SAME" : "VALID", // padding + py_stride, // stride + depthMultiplier, // depthMultiplier + py_dilation, // dilation + toString(fused_act) // fused activation + ); + } + + void visit(const luci::CircleAdd *node) + { + PRE_OPERATOR_HOOK_PROLOGUE(Add) + + auto fused_act = node->fusedActivationFunction(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // x + inputs[1], // y + toString(fused_act) // fused activation + ); + } + + void visit(const luci::CircleFullyConnected *node) + { + PRE_OPERATOR_HOOK_PROLOGUE(FullyConnected) + + auto fused_act = node->fusedActivationFunction(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // input + inputs[1], // weights + inputs[2], // bias + toString(fused_act) // fused activation + ); + } + + void visit(const luci::CircleTransposeConv *node) + { + PRE_OPERATOR_HOOK_PROLOGUE(TransposeConv) + + auto padding = node->padding(); + auto stride = node->stride(); + + auto py_stride = py::dict("w"_a = stride->w(), "h"_a = stride->h()); + + pySafeCall(hook, + node->name(), // name + inputs[2], // input + inputs[1], // filter + inputs[0], // output shape + inputs.size() == 4 ? inputs[3] : none(), // bias + padding == luci::Padding::SAME ? "SAME" : "VALID", // padding + py_stride // stride + ); + } + + void visit(const luci::CircleInstanceNorm *node) + { + PRE_OPERATOR_HOOK_PROLOGUE(InstanceNorm) + + auto epsilon = node->epsilon(); + + auto fused_act = node->fusedActivationFunction(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // input + inputs[1], // gamma + inputs[2], // beta + epsilon, // epsilon + toString(fused_act) // fused activation + ); + } + + void visit(const luci::CircleSplit *node) + { + PRE_OPERATOR_HOOK_PROLOGUE(Split) + + auto num_split = node->num_split(); + + pySafeCall(hook, + node->name(), // name + inputs[0], // split_dim + inputs[1], // input + num_split // num_split + ); + } +}; + +} // namespace dalgona + +#endif // __DALGONA_PRE_OPERATOR_HOOK_H__ diff --git a/compiler/dalgona/src/PythonHooks.cpp b/compiler/dalgona/src/PythonHooks.cpp new file mode 100644 index 0000000..8447699 --- /dev/null +++ b/compiler/dalgona/src/PythonHooks.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PythonHooks.h" +#include "PostOperatorHook.h" +#include "PreOperatorHook.h" +#include "Utils.h" + +#include + +namespace dalgona +{ + +void PythonHooks::preOperatorExecute(const luci::CircleNode *node) +{ + PreOperatorHook hook(_analysis, _interpreter); + node->accept(&hook); +} + +void PythonHooks::postOperatorExecute(const luci::CircleNode *node) +{ + PostOperatorHook hook(_analysis, _interpreter); + node->accept(&hook); +} + +void PythonHooks::importAnalysis(const std::string &analysis_path, py::object &globals, + const std::string &analysis_args) +{ + const auto base_filename = analysis_path.substr(analysis_path.find_last_of("/\\") + 1); + // module name must be the same with the python code + // ex: base_filename = MyAnalysis.py -> module_name = MyAnalysis + const auto module_name = base_filename.substr(0, base_filename.find_last_of('.')); + + py::dict locals; + locals["path"] = py::cast(analysis_path); + + py::eval("import sys\n" + "import os\n" + "sys.path.append(os.path.dirname(path))\n" + "import " + + module_name + + "\n" + "analysis = " + + module_name + "." + module_name + "()", + globals, locals); + + _analysis = locals["analysis"]; + + if (py::hasattr(_analysis, "StartAnalysis")) + pySafeCall(_analysis.attr("StartAnalysis"), analysis_args); +} + +void PythonHooks::startNetworkExecution(loco::Graph *graph) +{ + if (!py::hasattr(_analysis, "StartNetworkExecution")) + return; + + assert(graph != nullptr); // FIX_CALLER_UNLESS + + const auto input_nodes = loco::input_nodes(graph); + py::list inputs; + // Assumption: input_nodes is iterated in the same order of model inputs + for (const auto input_node : input_nodes) + { + auto circle_node = loco::must_cast(input_node); + inputs.append(outputPyArray(circle_node, _interpreter)); + } + pySafeCall(_analysis.attr("StartNetworkExecution"), inputs); +} + +void PythonHooks::endNetworkExecution(loco::Graph *graph) +{ + if (!py::hasattr(_analysis, "EndNetworkExecution")) + return; + + assert(graph != nullptr); // FIX_CALLER_UNLESS + + const auto output_nodes = loco::output_nodes(graph); + py::list outputs; + // Assumption: output_nodes is iterated in the same order of model outputs + for (const auto output_node : output_nodes) + { + auto circle_node = loco::must_cast(output_node); + outputs.append( + outputPyArray(loco::must_cast(circle_node->from()), _interpreter)); + } + pySafeCall(_analysis.attr("EndNetworkExecution"), outputs); +} + +void PythonHooks::endAnalysis() +{ + if (py::hasattr(_analysis, "EndAnalysis")) + pySafeCall(_analysis.attr("EndAnalysis")); +} + +} // namespace dalgona diff --git a/compiler/dalgona/src/RandomUtils.cpp b/compiler/dalgona/src/RandomUtils.cpp new file mode 100644 index 0000000..a8e32b3 --- /dev/null +++ b/compiler/dalgona/src/RandomUtils.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "RandomUtils.h" + +#include +#include +#include + +namespace dalgona +{ + +std::vector genRandomFloatData(uint32_t num_elements, float min, float max) +{ + if (min > max) + throw std::invalid_argument("min is greater than max"); + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution<> dist(min, max); + std::vector buffer(num_elements); + + // Write random data + for (auto &iter : buffer) + iter = static_cast(dist(gen)); + + return buffer; +} + +} // namespace dalgona diff --git a/compiler/dalgona/src/RandomUtils.h b/compiler/dalgona/src/RandomUtils.h new file mode 100644 index 0000000..6f6f48f --- /dev/null +++ b/compiler/dalgona/src/RandomUtils.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DALGONA_RANDOM_UTILS_H__ +#define __DALGONA_RANDOM_UTILS_H__ + +#include +#include +#include +#include + +namespace dalgona +{ + +template std::vector genRandomIntData(uint32_t num_elements, T min, T max) +{ + if (min > max) + throw std::invalid_argument("min is greater than max"); + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution dist(min, max); + std::vector buffer(num_elements); + + // Write random data + for (auto &iter : buffer) + iter = dist(gen); + + return buffer; +} + +std::vector genRandomFloatData(uint32_t num_elements, float min, float max); + +} // namespace dalgona + +#endif // __DALGONA_RANDOM_UTILS_H__ diff --git a/compiler/dalgona/src/RandomUtils.test.cpp b/compiler/dalgona/src/RandomUtils.test.cpp new file mode 100644 index 0000000..b04d8b8 --- /dev/null +++ b/compiler/dalgona/src/RandomUtils.test.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "RandomUtils.h" + +#include + +using namespace dalgona; + +TEST(DalgonaUtilTest, gen_random_int32) +{ + const uint32_t num_elements = 10; + const int32_t min = -5; + const int32_t max = 5; + std::vector buffer = genRandomIntData(num_elements, min, max); + + EXPECT_EQ(num_elements, buffer.size()); + for (auto val : buffer) + { + EXPECT_TRUE(val >= min and val <= max); + } +} + +TEST(DalgonaUtilTest, gen_random_int32_NEG) +{ + const uint32_t num_elements = 10; + const int32_t min = 5; + const int32_t max = -5; + EXPECT_ANY_THROW(genRandomIntData(num_elements, min, max)); +} + +TEST(DalgonaUtilTest, gen_random_float) +{ + const uint32_t num_elements = 10; + const float min = -5; + const float max = 5; + std::vector buffer = genRandomFloatData(num_elements, min, max); + + EXPECT_EQ(num_elements, buffer.size()); + for (auto val : buffer) + { + EXPECT_TRUE(val >= min and val <= max); + } +} + +TEST(DalgonaUtilTest, gen_random_float_NEG) +{ + const uint32_t num_elements = 10; + const float min = 5; + const float max = -5; + EXPECT_ANY_THROW(genRandomFloatData(num_elements, min, max)); +} diff --git a/compiler/dalgona/src/StringUtils.cpp b/compiler/dalgona/src/StringUtils.cpp new file mode 100644 index 0000000..423b488 --- /dev/null +++ b/compiler/dalgona/src/StringUtils.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "StringUtils.h" + +#include + +#include + +namespace dalgona +{ + +const std::string toString(luci::CircleOpcode opcode) +{ + static const char *names[] = { +#define CIRCLE_NODE(OPCODE, CIRCLE_CLASS) #CIRCLE_CLASS, +#define CIRCLE_VNODE(OPCODE, CIRCLE_CLASS) #CIRCLE_CLASS, +#include +#undef CIRCLE_NODE +#undef CIRCLE_VNODE + }; + + auto const node_name = names[static_cast(opcode)]; + + assert(std::string(node_name).substr(0, 6) == "Circle"); // FIX_ME_UNLESS + + // Return substring of class name ("Circle" is sliced out) + // Ex: Return "Conv2D" for "CircleConv2D" node + return std::string(node_name).substr(6); +} + +const std::string toString(luci::FusedActFunc fused_act) +{ + switch (fused_act) + { + case (luci::FusedActFunc::UNDEFINED): + return std::string("undefined"); + case (luci::FusedActFunc::NONE): + return std::string("none"); + case (luci::FusedActFunc::RELU): + return std::string("relu"); + case (luci::FusedActFunc::RELU_N1_TO_1): + return std::string("relu_n1_to_1"); + case (luci::FusedActFunc::RELU6): + return std::string("relu6"); + case (luci::FusedActFunc::TANH): + return std::string("tanh"); + case (luci::FusedActFunc::SIGN_BIT): + return std::string("sign_bit"); + default: + throw std::runtime_error("Unsupported activation function"); + } +} + +} // namespace dalgona diff --git a/compiler/dalgona/src/StringUtils.h b/compiler/dalgona/src/StringUtils.h new file mode 100644 index 0000000..ad9d061 --- /dev/null +++ b/compiler/dalgona/src/StringUtils.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DALGONA_STRING_UTILS_H__ +#define __DALGONA_STRING_UTILS_H__ + +#include +#include + +#include + +namespace dalgona +{ + +const std::string toString(luci::CircleOpcode opcode); + +const std::string toString(luci::FusedActFunc fused_act); + +} // namespace dalgona + +#endif // __DALGONA_STRING_UTILS_H__ diff --git a/compiler/dalgona/src/StringUtils.test.cpp b/compiler/dalgona/src/StringUtils.test.cpp new file mode 100644 index 0000000..e795a47 --- /dev/null +++ b/compiler/dalgona/src/StringUtils.test.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "StringUtils.h" + +#include +#include + +#include + +using namespace dalgona; + +TEST(DalgonaUtilTest, toString_basic) +{ + luci::CircleConv2D node; + + EXPECT_EQ("Conv2D", toString(node.opcode())); +} + +TEST(DalgonaUtilTest, toString_fused_act_func) +{ + EXPECT_EQ("undefined", toString(luci::FusedActFunc::UNDEFINED)); + EXPECT_EQ("none", toString(luci::FusedActFunc::NONE)); + EXPECT_EQ("relu", toString(luci::FusedActFunc::RELU)); + EXPECT_EQ("relu6", toString(luci::FusedActFunc::RELU6)); + EXPECT_EQ("relu_n1_to_1", toString(luci::FusedActFunc::RELU_N1_TO_1)); + EXPECT_EQ("tanh", toString(luci::FusedActFunc::TANH)); + EXPECT_EQ("sign_bit", toString(luci::FusedActFunc::SIGN_BIT)); +} diff --git a/compiler/dalgona/src/Utils.cpp b/compiler/dalgona/src/Utils.cpp new file mode 100644 index 0000000..f65d9c2 --- /dev/null +++ b/compiler/dalgona/src/Utils.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Utils.h" +#include "StringUtils.h" + +#include +#include +#include + +#include +#include +#include + +using Tensor = luci_interpreter::Tensor; + +namespace py = pybind11; +using namespace py::literals; + +#define THROW_UNLESS(COND, MSG) \ + if (not(COND)) \ + throw std::runtime_error(MSG); + +namespace +{ + +py::array numpyArray(const Tensor *tensor) +{ + assert(tensor != nullptr); // FIX_CALLER_UNLESS + + const auto tensor_shape = tensor->shape(); + + uint32_t size = 1; + std::vector shape(tensor_shape.num_dims()); + for (int i = 0; i < tensor_shape.num_dims(); i++) + { + THROW_UNLESS(tensor_shape.dim(i) >= 0, "Negative dimension detected in " + tensor->name()); + + shape[i] = tensor_shape.dim(i); + size *= shape[i]; + } + + if (size == 0) + return py::none(); + + switch (tensor->element_type()) + { + case loco::DataType::FLOAT32: + return py::array_t(shape, tensor->data()); + case loco::DataType::S16: + return py::array_t(shape, tensor->data()); + case loco::DataType::S32: + return py::array_t(shape, tensor->data()); + case loco::DataType::S64: + return py::array_t(shape, tensor->data()); + case loco::DataType::U8: + return py::array_t(shape, tensor->data()); + default: + throw std::runtime_error("Unsupported data type"); + } +} + +py::dict quantparam(const Tensor *tensor) +{ + assert(tensor != nullptr); // FIX_CALLER_UNLESS + + auto scale = tensor->scales(); + auto zp = tensor->zero_points(); + + py::list py_scale; + for (auto s : scale) + { + py_scale.append(s); + } + + py::list py_zp; + for (auto z : zp) + { + py_zp.append(z); + } + + auto quantparam = py::dict("scale"_a = py_scale, "zero_point"_a = py_zp, + "quantized_dimension"_a = tensor->quantized_dimension()); + return quantparam; +} + +} // namespace + +namespace dalgona +{ + +py::object none() { return py::none(); } + +std::vector inputsPyArray(const luci::CircleNode *node, + luci_interpreter::Interpreter *interpreter) +{ + assert(node != nullptr); // FIX_CALLER_UNLESS + assert(interpreter != nullptr); // FIX_CALLER_UNLESS + + std::vector inputs; + for (uint32_t i = 0; i < node->arity(); ++i) + { + const auto input_tensor = interpreter->getTensor(node->arg(i)); + auto circle_node = static_cast(node->arg(i)); + + // skip invalid inputs (e.g., non-existing bias in TCONV) + if (circle_node->opcode() == luci::CircleOpcode::CIRCLEOUTPUTEXCLUDE) + continue; + + auto py_input = + py::dict("name"_a = circle_node->name(), "data"_a = numpyArray(input_tensor), + "quantparam"_a = quantparam(input_tensor), + "is_const"_a = circle_node->opcode() == luci::CircleOpcode::CIRCLECONST); + inputs.push_back(py_input); + } + return inputs; +} + +std::vector outputsPyArray(const luci::CircleNode *node, + luci_interpreter::Interpreter *interpreter) +{ + std::vector outputs; + for (auto succ : loco::succs(node)) + { + const auto output_tensor = interpreter->getTensor(succ); + auto circle_node = static_cast(succ); + + auto opcode_str = toString(circle_node->opcode()); + // Check if node is a multi-output node + // Assumption: Multi-output virtual nodes have 'Out' prefix + // TODO Fix this if the assumption changes + THROW_UNLESS(opcode_str.substr(opcode_str.length() - 3) == "Out", + "Invalid output detected in " + node->name()); + + auto py_output = + py::dict("name"_a = circle_node->name(), "data"_a = numpyArray(output_tensor), + "quantparam"_a = quantparam(output_tensor), + "is_const"_a = circle_node->opcode() == luci::CircleOpcode::CIRCLECONST); + outputs.push_back(py_output); + } + return outputs; +} + +// Note: Only returns 1 output +py::dict outputPyArray(const luci::CircleNode *node, luci_interpreter::Interpreter *interpreter) +{ + assert(node != nullptr); // FIX_CALLER_UNLESS + assert(interpreter != nullptr); // FIX_CALLER_UNLESS + + const auto tensor = interpreter->getTensor(node); + + THROW_UNLESS(tensor != nullptr, "Null tensor detected in " + node->name()); + + auto py_output = py::dict("name"_a = node->name(), "data"_a = numpyArray(tensor), + "quantparam"_a = quantparam(tensor), + "is_const"_a = node->opcode() == luci::CircleOpcode::CIRCLECONST); + return py_output; +} + +} // namespace dalgona + +#undef THROW_UNLESS diff --git a/compiler/dalgona/src/Utils.h b/compiler/dalgona/src/Utils.h new file mode 100644 index 0000000..9ca920f --- /dev/null +++ b/compiler/dalgona/src/Utils.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __DALGONA_UTILS_H__ +#define __DALGONA_UTILS_H__ + +#include + +#include + +namespace py = pybind11; + +namespace dalgona +{ + +template void pySafeCall(py::object func, Args... args) +{ + try + { + func(args...); + } + catch (py::error_already_set &e) + { + throw std::runtime_error(e.what()); + } +} + +py::dict outputPyArray(const luci::CircleNode *node, luci_interpreter::Interpreter *interpreter); + +// Return a vector of Tensors(py::dict) which correspond to node's inputs +std::vector inputsPyArray(const luci::CircleNode *node, + luci_interpreter::Interpreter *interpreter); + +// Return a vector of Tensors(py::dict) which correspond to the outputs of multi-out node (ex: +// SPLIT) +std::vector outputsPyArray(const luci::CircleNode *node, + luci_interpreter::Interpreter *interpreter); + +py::object none(); + +} // namespace dalgona + +#endif // __DALGONA_UTILS_H__ diff --git a/compiler/dredd-rule-lib/rule-lib.sh b/compiler/dredd-rule-lib/rule-lib.sh index c25dc5f..a920e08 100755 --- a/compiler/dredd-rule-lib/rule-lib.sh +++ b/compiler/dredd-rule-lib/rule-lib.sh @@ -234,4 +234,22 @@ tensor_dtype() echo ${ACTUAL} } +const_count() +{ + argc_check $# 1 + file_path_check ${COMPILED_FILE} + file_path_check ${INSPECT_PROG_PATH} + + set -o pipefail + + RESULT=`init_error_log ; ${INSPECT_PROG_PATH} --constants ${COMPILED_FILE}` + check_success_exit_code $? 0 + + # note : grep's exit code is 2 in case of error. + ACTUAL=`init_error_log ; echo "${RESULT}" | grep -wc "$1"` + check_error_exit_code $? 2 + + echo ${ACTUAL} +} + # TODO define more qullity test function diff --git a/compiler/loco/include/loco/IR/Graph.h b/compiler/loco/include/loco/IR/Graph.h index a820aba..176e6cc 100644 --- a/compiler/loco/include/loco/IR/Graph.h +++ b/compiler/loco/include/loco/IR/Graph.h @@ -264,7 +264,6 @@ struct GraphOutputIndexQueryService : public DialectService virtual GraphOutputIndex index(const Node *node) const = 0; }; -// TODO Use "const Graph *" std::vector output_nodes(Graph *); /** diff --git a/compiler/loco/src/IR/Graph.test.cpp b/compiler/loco/src/IR/Graph.test.cpp index 837d293..99de8fc 100644 --- a/compiler/loco/src/IR/Graph.test.cpp +++ b/compiler/loco/src/IR/Graph.test.cpp @@ -79,6 +79,18 @@ TEST(GraphTest, create_and_destroy_node) auto pull = g->nodes()->create(); ASSERT_NO_THROW(g->nodes()->destroy(pull)); +} + +TEST(GraphTest, DISABLED_create_and_destroy_node_again) +{ + auto g = loco::make_graph(); + + auto pull = g->nodes()->create(); + + ASSERT_NO_THROW(g->nodes()->destroy(pull)); + // NOTE calling destroy again raises Segmentation fault + // refer https://github.com/Samsung/ONE/issues/9968 + // TODO fix this ASSERT_THROW(g->nodes()->destroy(pull), std::invalid_argument); } @@ -139,7 +151,6 @@ TEST(GraphTest, consturctor_with_param_node) ASSERT_FLOAT_EQ(test_node->f(), 11.11); ASSERT_NO_THROW(g->nodes()->destroy(test_node)); - ASSERT_THROW(g->nodes()->destroy(test_node), std::invalid_argument); } TEST(GraphTest, getters_over_const_instance) diff --git a/compiler/locomotiv/src/Node/TensorBroadcast.cpp b/compiler/locomotiv/src/Node/TensorBroadcast.cpp index 38e5a7a..682237f 100644 --- a/compiler/locomotiv/src/Node/TensorBroadcast.cpp +++ b/compiler/locomotiv/src/Node/TensorBroadcast.cpp @@ -42,6 +42,10 @@ using namespace locomotiv; void execute_node(loco::TensorBroadcast *tensor_broadcast) { auto input_data = annot_data(tensor_broadcast->input()); + if (input_data == nullptr) + { + throw std::runtime_error("Annotation is required for TensorBroadcast input"); + } // Calculate output shape Shape input_shape = *(input_data->shape()); diff --git a/compiler/locomotiv/src/Node/TensorConstantPad.cpp b/compiler/locomotiv/src/Node/TensorConstantPad.cpp index 5d4ad5d..fb07128 100644 --- a/compiler/locomotiv/src/Node/TensorConstantPad.cpp +++ b/compiler/locomotiv/src/Node/TensorConstantPad.cpp @@ -53,6 +53,7 @@ void execute_node(loco::TensorConstantPad *pad) auto constant_node = pad->constant(); auto constant_data = annot_data(constant_node); + validate(constant_data != nullptr, "constant is not found"); validate(constant_data->dtype() == input_data->dtype(), "constant and input have same data type"); validate(constant_data->shape()->rank() == 1 && constant_data->shape()->dim(0) == 1, "constant should have one rank with one dimension at zero axis"); diff --git a/compiler/luci-compute/CMakeLists.txt b/compiler/luci-compute/CMakeLists.txt new file mode 100644 index 0000000..b7ddb44 --- /dev/null +++ b/compiler/luci-compute/CMakeLists.txt @@ -0,0 +1,23 @@ +nnas_find_package(TensorFlowSource EXACT 2.8.0 QUIET) +nnas_find_package(TensorFlowGEMMLowpSource EXACT 2.8.0 QUIET) +nnas_find_package(TensorFlowRuySource EXACT 2.8.0 QUIET) + +if(NOT TensorFlowSource_FOUND) + message(STATUS "Build luci-compute: FAILED (missing TensorFlowSource 2.8.0)") + return() +endif(NOT TensorFlowSource_FOUND) + +if(NOT TensorFlowGEMMLowpSource_FOUND) + message(STATUS "Build luci-compute: FAILED (missing TensorFlowGEMMLowpSource 2.8.0)") + return() +endif(NOT TensorFlowGEMMLowpSource_FOUND) + +if(NOT TensorFlowRuySource_FOUND) + message(STATUS "Build luci-compute: FAILED (missing TensorFlowRuySource 2.8.0)") + return() +endif(NOT TensorFlowRuySource_FOUND) + +add_library(luci_compute INTERFACE) +target_include_directories(luci_compute SYSTEM INTERFACE "${TensorFlowSource_DIR}") +target_include_directories(luci_compute SYSTEM INTERFACE "${TensorFlowGEMMLowpSource_DIR}") +target_include_directories(luci_compute SYSTEM INTERFACE "${TensorFlowRuySource_DIR}") diff --git a/compiler/luci-compute/README.md b/compiler/luci-compute/README.md new file mode 100644 index 0000000..caa4ab4 --- /dev/null +++ b/compiler/luci-compute/README.md @@ -0,0 +1,3 @@ +# luci-compute + +_luci-compute_ provides computation kernels for _luci_ and related modules. diff --git a/compiler/luci-eval-driver/CMakeLists.txt b/compiler/luci-eval-driver/CMakeLists.txt index c67754a..e8cd4f8 100644 --- a/compiler/luci-eval-driver/CMakeLists.txt +++ b/compiler/luci-eval-driver/CMakeLists.txt @@ -3,11 +3,7 @@ set(SRCS_EVAL_TESTER ) add_executable(luci_eval_driver ${SRCS_EVAL_TESTER}) -target_link_libraries(luci_eval_driver PRIVATE oops) -target_link_libraries(luci_eval_driver PRIVATE loco) target_link_libraries(luci_eval_driver PRIVATE luci_import) -target_link_libraries(luci_eval_driver PRIVATE luci_export) -target_link_libraries(luci_eval_driver PRIVATE luci_lang) target_link_libraries(luci_eval_driver PRIVATE luci_interpreter) target_link_libraries(luci_eval_driver PRIVATE safemain) diff --git a/compiler/luci-eval-driver/requires.cmake b/compiler/luci-eval-driver/requires.cmake index 2904d9d..847eb87 100644 --- a/compiler/luci-eval-driver/requires.cmake +++ b/compiler/luci-eval-driver/requires.cmake @@ -1,5 +1,3 @@ -require("oops") -require("loco") require("luci") require("luci-interpreter") require("safemain") diff --git a/compiler/luci-eval-driver/src/EvalDriver.cpp b/compiler/luci-eval-driver/src/EvalDriver.cpp index 0ed3543..8d844ab 100644 --- a/compiler/luci-eval-driver/src/EvalDriver.cpp +++ b/compiler/luci-eval-driver/src/EvalDriver.cpp @@ -16,11 +16,10 @@ #include #include -#include -#include #include #include +#include #include #include diff --git a/compiler/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst b/compiler/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst index f0df58d..fe3f73f 100644 --- a/compiler/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst +++ b/compiler/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst @@ -1,3 +1,4 @@ +REGISTER_KERNEL(Abs) REGISTER_KERNEL(Add) REGISTER_KERNEL(ArgMax) REGISTER_KERNEL(AveragePool2D) diff --git a/compiler/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h b/compiler/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h index 4dd77ff..f8a4a80 100644 --- a/compiler/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h +++ b/compiler/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h @@ -15,7 +15,7 @@ */ #ifndef LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H -#define LUCI_INTERPRETER_PAL_ARGMAX_H +#define LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H #include diff --git a/compiler/luci-interpreter/pal/linux/KernelsToBuild.lst b/compiler/luci-interpreter/pal/linux/KernelsToBuild.lst index 1e6c41e..db31cbf 100644 --- a/compiler/luci-interpreter/pal/linux/KernelsToBuild.lst +++ b/compiler/luci-interpreter/pal/linux/KernelsToBuild.lst @@ -1,3 +1,4 @@ +REGISTER_KERNEL(Abs) REGISTER_KERNEL(Add) REGISTER_KERNEL(ArgMax) REGISTER_KERNEL(AveragePool2D) @@ -50,6 +51,7 @@ REGISTER_KERNEL(Pow) REGISTER_KERNEL(PRelu) REGISTER_KERNEL(Quantize) REGISTER_KERNEL(ReduceMax) +REGISTER_KERNEL(ReduceProd) REGISTER_KERNEL(Relu) REGISTER_KERNEL(Relu6) REGISTER_KERNEL(Reshape) @@ -74,5 +76,6 @@ REGISTER_KERNEL(SVDF) REGISTER_KERNEL(Tanh) REGISTER_KERNEL(Transpose) REGISTER_KERNEL(TransposeConv) +REGISTER_KERNEL(UnidirectionalSequenceLSTM) REGISTER_KERNEL(Unpack) REGISTER_KERNEL(While) diff --git a/compiler/luci-interpreter/pal/mcu/KernelsToBuild.lst b/compiler/luci-interpreter/pal/mcu/KernelsToBuild.lst index f0df58d..fe3f73f 100644 --- a/compiler/luci-interpreter/pal/mcu/KernelsToBuild.lst +++ b/compiler/luci-interpreter/pal/mcu/KernelsToBuild.lst @@ -1,3 +1,4 @@ +REGISTER_KERNEL(Abs) REGISTER_KERNEL(Add) REGISTER_KERNEL(ArgMax) REGISTER_KERNEL(AveragePool2D) diff --git a/compiler/luci-interpreter/pal/mcu/PALBatchToSpaceND.h b/compiler/luci-interpreter/pal/mcu/PALBatchToSpaceND.h index 4dd77ff..f8a4a80 100644 --- a/compiler/luci-interpreter/pal/mcu/PALBatchToSpaceND.h +++ b/compiler/luci-interpreter/pal/mcu/PALBatchToSpaceND.h @@ -15,7 +15,7 @@ */ #ifndef LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H -#define LUCI_INTERPRETER_PAL_ARGMAX_H +#define LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H #include diff --git a/compiler/luci-interpreter/src/BuddyMemoryManager.cpp b/compiler/luci-interpreter/src/BuddyMemoryManager.cpp index 6ad1f32..14bc75e 100644 --- a/compiler/luci-interpreter/src/BuddyMemoryManager.cpp +++ b/compiler/luci-interpreter/src/BuddyMemoryManager.cpp @@ -53,7 +53,10 @@ void BuddyMemoryManager::allocate_memory(luci_interpreter::Tensor &tensor) while (l < 32 && !_free_blocks[l]) l++; - assert(l < 32); + if (l >= 32) + { + throw std::runtime_error{"Memory limit exceeded"}; + } Block *tmp; tmp = _free_blocks[l]; diff --git a/compiler/luci-interpreter/src/core/KernelParams.h b/compiler/luci-interpreter/src/core/KernelParams.h index 6c0220c..45f3bfe 100644 --- a/compiler/luci-interpreter/src/core/KernelParams.h +++ b/compiler/luci-interpreter/src/core/KernelParams.h @@ -218,6 +218,15 @@ struct TransposeConvParams int32_t stride_width; }; +struct UnidirectionalSequenceLSTMParams +{ + Activation activation; + float cell_clip; + float proj_clip; + bool time_major; + bool asymmetric_quantize_inputs; +}; + struct UnpackParams { int axis; diff --git a/compiler/luci-interpreter/src/kernels/Abs.cpp b/compiler/luci-interpreter/src/kernels/Abs.cpp new file mode 100644 index 0000000..5c63315 --- /dev/null +++ b/compiler/luci-interpreter/src/kernels/Abs.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Abs.h" + +#include "kernels/Utils.h" + +#include // abs for float + +namespace luci_interpreter +{ +namespace kernels +{ + +Abs::Abs(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Abs::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + + output()->resize(input()->shape()); +} + +void Abs::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + eval(); + break; + default: + throw std::runtime_error("Unsupported type."); + } +} + +template void Abs::eval() const +{ + const auto *input_data = input()->data(); + auto *output_data = output()->data(); + + const int size = tflite::MatchingFlatSize(getTensorShape(input()), getTensorShape(output())); + + for (int i = 0; i < size; ++i) + { + output_data[i] = std::abs(input_data[i]); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-interpreter/src/kernels/Abs.h b/compiler/luci-interpreter/src/kernels/Abs.h new file mode 100644 index 0000000..b5b874a --- /dev/null +++ b/compiler/luci-interpreter/src/kernels/Abs.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_KERNELS_ABS_H +#define LUCI_INTERPRETER_KERNELS_ABS_H + +#include "core/Kernel.h" +#include "core/KernelParams.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +class Abs : public Kernel +{ +public: + Abs(const Tensor *input, Tensor *output); + + const Tensor *input() const { return _inputs[0]; } + Tensor *output() const { return _outputs[0]; } + + void configure() override; + void execute() const override; + +private: + template void eval() const; +}; + +} // namespace kernels +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_KERNELS_ABS_H diff --git a/compiler/luci-interpreter/src/kernels/Abs.test.cpp b/compiler/luci-interpreter/src/kernels/Abs.test.cpp new file mode 100644 index 0000000..2c42ab7 --- /dev/null +++ b/compiler/luci-interpreter/src/kernels/Abs.test.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Abs.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +template +void Check(std::initializer_list input_shape, std::initializer_list output_shape, + std::initializer_list input_data, std::initializer_list output_data) +{ + std::unique_ptr memory_manager = std::make_unique(); + constexpr DataType element_type = getElementType(); + Tensor input_tensor = + makeInputTensor(input_shape, input_data, memory_manager.get()); + Tensor output_tensor = makeOutputTensor(element_type); + + Abs kernel(&input_tensor, &output_tensor); + + kernel.configure(); + memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorData(output_tensor), ::testing::ElementsAreArray(output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); +} + +TEST(AbsTest, FloatSimple) +{ + Check(/*input_shape=*/{2, 3}, + /*output_shape=*/{2, 3}, + /*input_data=*/ + { + 0.0f, -1.0f, 3.0f, // Row 1 + 1.0f, -1.0f, -2.0f, // Row 2 + }, + /*output_data=*/ + { + 0.0f, 1.0f, 3.0f, // Row 1 + 1.0f, 1.0f, 2.0f, // Row 2 + }); + + SUCCEED(); +} + +TEST(AbsTest, Type_Mismatch_NEG) +{ + std::unique_ptr memory_manager = std::make_unique(); + + Tensor input_tensor = makeInputTensor({3}, {1, -3, 2}, memory_manager.get()); + Tensor output_tensor = makeOutputTensor(loco::DataType::FLOAT32); + + Abs kernel(&input_tensor, &output_tensor); + EXPECT_ANY_THROW(kernel.configure()); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-interpreter/src/kernels/ReduceProd.cpp b/compiler/luci-interpreter/src/kernels/ReduceProd.cpp new file mode 100644 index 0000000..f3fc7d3 --- /dev/null +++ b/compiler/luci-interpreter/src/kernels/ReduceProd.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/ReduceProd.h" + +#include "kernels/Utils.h" + +#include + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +// Returns the number of axes that will be reduced. Removes duplicates. +static int getAxisReductionCount(const int32_t *axes_data, int num_axes, int input_num_dims) +{ + int reduction_count = num_axes; + for (int i = 0; i < num_axes; ++i) + { + int current = axes_data[i] >= 0 ? axes_data[i] : axes_data[i] + input_num_dims; + assert(current >= 0 && current < input_num_dims); + for (int j = 0; j < i; j++) + { + int previous = axes_data[j] >= 0 ? axes_data[j] : axes_data[j] + input_num_dims; + // This checks for duplicate axis + if (current == previous) + { + --reduction_count; + break; + } + } + } + return reduction_count; +} + +static Shape getOutputShape(const Shape &input_shape, const int32_t *axes_data, int num_axes, + bool keep_dims) +{ + int input_num_dims = input_shape.num_dims(); + if (input_num_dims == 0) + { + return Shape(0); + } + + if (keep_dims) + { + Shape output_shape(input_num_dims); + for (int idx = 0; idx < input_num_dims; ++idx) + { + bool is_axis = false; + for (int axis_idx = 0; axis_idx < num_axes; ++axis_idx) + { + if (axes_data[axis_idx] == idx || axes_data[axis_idx] + input_num_dims == idx) + { + is_axis = true; + break; + } + } + if (is_axis) + { + output_shape.dim(idx) = 1; + } + else + { + output_shape.dim(idx) = input_shape.dim(idx); + } + } + return output_shape; + } + else + { + int num_reduce_axes = getAxisReductionCount(axes_data, num_axes, input_num_dims); + Shape output_shape(input_num_dims - num_reduce_axes); + int num_skip_axes = 0; + for (int idx = 0; idx < input_num_dims; ++idx) + { + bool is_axis = false; + for (int axis_idx = 0; axis_idx < num_axes; ++axis_idx) + { + if (axes_data[axis_idx] == idx || axes_data[axis_idx] + input_num_dims == idx) + { + ++num_skip_axes; + is_axis = true; + break; + } + } + if (!is_axis) + { + output_shape.dim(idx - num_skip_axes) = input_shape.dim(idx); + } + } + return output_shape; + } +} + +ReduceProd::ReduceProd(const Tensor *input, const Tensor *axes, Tensor *output, Tensor *temp_index, + Tensor *resolved_axes, const ReducerParams ¶ms) + : KernelWithParams({input, axes}, {output, temp_index, resolved_axes}, params) +{ +} + +void ReduceProd::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + LUCI_INTERPRETER_CHECK(axes()->element_type() == DataType::S32); + + const Shape &input_shape = input()->shape(); + int input_num_dims = input_shape.num_dims(); + + const auto *axes_data = getTensorData(axes()); + int num_axes = axes()->shape().num_elements(); + LUCI_INTERPRETER_CHECK(num_axes <= 4); + + // We compute shapes of outputs in configure, assuming that outputs have + // static shape + // TODO Support dynamic shape + Shape output_shape = getOutputShape(input_shape, axes_data, num_axes, _params.keep_dims); + output()->resize(output_shape); + + auto temp_index = getOutputTensors()[1]; + auto resolved_axes = getOutputTensors()[2]; + + temp_index->resize(Shape(input_num_dims)); + resolved_axes->resize(Shape(num_axes)); +} + +void ReduceProd::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + // TODO Support quantized kernels + default: + throw std::runtime_error("Unsupported type."); + } +} + +void ReduceProd::evalFloat() const +{ + const auto *axes_data = getTensorData(axes()); + int num_axes = axes()->shape().num_elements(); + + auto temp_index = getOutputTensors()[1]; + auto resolved_axes = getOutputTensors()[2]; + + int num_resolved_axis = 0; + LUCI_INTERPRETER_CHECK( + tflite::reference_ops::ResolveAxis(input()->shape().num_dims(), axes_data, num_axes, + getTensorData(resolved_axes), &num_resolved_axis)); + + float init_value = 1.0; + tflite::reference_ops::ReduceGeneric( + getTensorData(input()), getTensorShape(input()).DimsData(), input()->shape().num_dims(), + getTensorData(output()), getTensorShape(output()).DimsData(), + output()->shape().num_dims(), axes_data, num_axes, _params.keep_dims, + getTensorData(temp_index), getTensorData(resolved_axes), init_value, + [](const float current, const float in) -> float { return current * in; }); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-interpreter/src/kernels/ReduceProd.h b/compiler/luci-interpreter/src/kernels/ReduceProd.h new file mode 100644 index 0000000..d2f58cc --- /dev/null +++ b/compiler/luci-interpreter/src/kernels/ReduceProd.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_KERNELS_REDUCE_PROD_H +#define LUCI_INTERPRETER_KERNELS_REDUCE_PROD_H + +#include "core/Kernel.h" +#include "core/KernelParams.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +class ReduceProd : public KernelWithParams +{ +public: + ReduceProd(const Tensor *input, const Tensor *axes, Tensor *output, Tensor *temp_index, + Tensor *resolved_axes, const ReducerParams ¶ms); + + const Tensor *input() const { return _inputs[0]; } + const Tensor *axes() const { return _inputs[1]; } + Tensor *output() const { return _outputs[0]; } + + void configure() override; + void execute() const override; + +private: + void evalFloat() const; +}; + +} // namespace kernels +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_KERNELS_REDUCE_PROD_H diff --git a/compiler/luci-interpreter/src/kernels/ReduceProd.test.cpp b/compiler/luci-interpreter/src/kernels/ReduceProd.test.cpp new file mode 100644 index 0000000..fa46f39 --- /dev/null +++ b/compiler/luci-interpreter/src/kernels/ReduceProd.test.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2018 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/ReduceProd.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +class ReduceProdTest : public ::testing::Test +{ +protected: + void SetUp() override { _memory_manager = std::make_unique(); } + + std::unique_ptr _memory_manager; +}; + +TEST_F(ReduceProdTest, FloatNotKeepDims) +{ + std::vector input_data = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, + 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0}; + + std::vector axis_data{1, 0, -3, -3}; + Tensor input_tensor = + makeInputTensor({4, 3, 2}, input_data, _memory_manager.get()); + Tensor axis_tensor = makeInputTensor({4}, axis_data, _memory_manager.get()); + Tensor temp_index(DataType::S32, Shape({}), {}, ""); + Tensor resolved_axes(DataType::S32, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + ReducerParams params{}; + params.keep_dims = false; + + ReduceProd kernel(&input_tensor, &axis_tensor, &output_tensor, &temp_index, &resolved_axes, + params); + kernel.configure(); + _memory_manager->allocate_memory(temp_index); + _memory_manager->allocate_memory(resolved_axes); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + std::vector ref_output_data{3.162341376e+11, 1.9619905536e+12}; + std::initializer_list ref_output_shape{2}; + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(ReduceProdTest, FloatKeepDims) +{ + std::vector input_data = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, + 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0}; + + std::vector axis_data{0, 2}; + Tensor input_tensor = + makeInputTensor({4, 3, 2}, input_data, _memory_manager.get()); + Tensor axis_tensor = makeInputTensor({2}, axis_data, _memory_manager.get()); + Tensor temp_index(DataType::S32, Shape({}), {}, ""); + Tensor resolved_axes(DataType::S32, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + ReducerParams params{}; + params.keep_dims = true; + + ReduceProd kernel(&input_tensor, &axis_tensor, &output_tensor, &temp_index, &resolved_axes, + params); + kernel.configure(); + _memory_manager->allocate_memory(temp_index); + _memory_manager->allocate_memory(resolved_axes); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + std::vector ref_output_data{7.74592e+06, 1.197504e+08, 6.6889152e+08}; + std::initializer_list ref_output_shape{1, 3, 1}; + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(ReduceProdTest, Input_Output_Type_NEG) +{ + std::vector input_data = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, + 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0}; + + std::vector axis_data{0, 2}; + Tensor input_tensor = + makeInputTensor({4, 3, 2}, input_data, _memory_manager.get()); + Tensor axis_tensor = makeInputTensor({2}, axis_data, _memory_manager.get()); + Tensor temp_index(DataType::S32, Shape({}), {}, ""); + Tensor resolved_axes(DataType::S32, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::U8); + + ReducerParams params{}; + params.keep_dims = true; + + ReduceProd kernel(&input_tensor, &axis_tensor, &output_tensor, &temp_index, &resolved_axes, + params); + + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(ReduceProdTest, Invalid_Axes_Type_NEG) +{ + std::vector input_data = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, + 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0}; + + std::vector axis_data{0, 2}; + Tensor input_tensor = + makeInputTensor({4, 3, 2}, input_data, _memory_manager.get()); + Tensor axis_tensor = makeInputTensor({2}, axis_data, _memory_manager.get()); + Tensor temp_index(DataType::S32, Shape({}), {}, ""); + Tensor resolved_axes(DataType::S32, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + ReducerParams params{}; + params.keep_dims = true; + + ReduceProd kernel(&input_tensor, &axis_tensor, &output_tensor, &temp_index, &resolved_axes, + params); + + EXPECT_ANY_THROW(kernel.configure()); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-interpreter/src/kernels/SVDF.cpp b/compiler/luci-interpreter/src/kernels/SVDF.cpp index 40d79aa..b124e24 100644 --- a/compiler/luci-interpreter/src/kernels/SVDF.cpp +++ b/compiler/luci-interpreter/src/kernels/SVDF.cpp @@ -26,30 +26,6 @@ namespace luci_interpreter namespace kernels { -namespace -{ -TfLiteFusedActivation get_tflite_activation(Activation activation) -{ - switch (activation) - { - case luci::FusedActFunc::RELU: - return kTfLiteActRelu; - case luci::FusedActFunc::RELU6: - return kTfLiteActRelu6; - case luci::FusedActFunc::RELU_N1_TO_1: - return kTfLiteActReluN1To1; - case luci::FusedActFunc::TANH: - return kTfLiteActTanh; - case luci::FusedActFunc::SIGN_BIT: - return kTfLiteActSignBit; - case luci::FusedActFunc::NONE: - return kTfLiteActNone; - default: - throw std::runtime_error("Unsupported activation type"); - } -} -} // namespace - SVDF::SVDF(const Tensor *input, const Tensor *weight_feature, const Tensor *weight_time, const Tensor *bias, const Tensor *input_activation_state, Tensor *output, Tensor *scratchpad_activation_state, Tensor *scratchpad_1, Tensor *scratchpad_2, @@ -191,7 +167,7 @@ void SVDF::evalInteger() const TfLiteSVDFParams params_svdf{}; params_svdf.asymmetric_quantize_inputs = params().asymmetric_quantize_inputs; params_svdf.rank = params().svdf_rank; - params_svdf.activation = get_tflite_activation(params().activation); + params_svdf.activation = getTfLiteActivation(params().activation); auto scratchpad_activation_state = getOutputTensors()[1]; // Note: it is expected that activation_state input variable tensor reset to zero, @@ -219,7 +195,7 @@ void SVDF::evalFloat() const TfLiteSVDFParams params_svdf{}; params_svdf.asymmetric_quantize_inputs = params().asymmetric_quantize_inputs; params_svdf.rank = params().svdf_rank; - params_svdf.activation = get_tflite_activation(params().activation); + params_svdf.activation = getTfLiteActivation(params().activation); auto scratchpad_activation_state = getOutputTensors()[1]; // Note: it is expected that activation_state input variable tensor reset to zero, diff --git a/compiler/luci-interpreter/src/kernels/Tanh.cpp b/compiler/luci-interpreter/src/kernels/Tanh.cpp index c4fa169..d47a0bd 100644 --- a/compiler/luci-interpreter/src/kernels/Tanh.cpp +++ b/compiler/luci-interpreter/src/kernels/Tanh.cpp @@ -17,6 +17,7 @@ #include "kernels/Tanh.h" #include "kernels/Utils.h" +#include // std::numeric_limits #include diff --git a/compiler/luci-interpreter/src/kernels/TestUtils.h b/compiler/luci-interpreter/src/kernels/TestUtils.h index 1f5a0c3..b9c942e 100644 --- a/compiler/luci-interpreter/src/kernels/TestUtils.h +++ b/compiler/luci-interpreter/src/kernels/TestUtils.h @@ -22,6 +22,7 @@ #include "luci_interpreter/MemoryManager.h" #include +#include // std::numeric_limits #include #include diff --git a/compiler/luci-interpreter/src/kernels/TransposeConv.cpp b/compiler/luci-interpreter/src/kernels/TransposeConv.cpp index 1b5f9d9..08bfbf3 100644 --- a/compiler/luci-interpreter/src/kernels/TransposeConv.cpp +++ b/compiler/luci-interpreter/src/kernels/TransposeConv.cpp @@ -22,6 +22,7 @@ #include #include +#include // std::numeric_limits namespace luci_interpreter { diff --git a/compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.cpp b/compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.cpp new file mode 100644 index 0000000..f049beec --- /dev/null +++ b/compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.cpp @@ -0,0 +1,892 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/UnidirectionalSequenceLSTM.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ +namespace lstm +{ +namespace +{ + +using namespace tflite; + +void UpdateLstmCellFloat(int n_batch, int n_cell, float *cell_state, const float *input_gate, + float *forget_gate, const float *cell_gate, bool use_cifg, float clip) +{ +// NOTE tflite source is as is but will fail build with gcc-8 and above +// TODO remove #pragma +#pragma GCC diagnostic ignored "-Wrestrict" + tensor_utils::VectorVectorCwiseProduct(forget_gate, cell_state, n_batch * n_cell, cell_state); + + if (use_cifg) + { + // With CIFG, input_gate = 1-forget_gate. Use the forget_gate array as + // scratch, as input_gate array is not allocated in this case. (Be careful + // not to write to the scratch before reading the forget gate data.) + float *scratch = forget_gate; + tensor_utils::Sub1Vector(forget_gate, n_batch * n_cell, scratch); + tensor_utils::VectorVectorCwiseProductAccumulate(cell_gate, scratch, n_batch * n_cell, + cell_state); + } + else + { + tensor_utils::VectorVectorCwiseProductAccumulate(cell_gate, input_gate, n_batch * n_cell, + cell_state); + } + if (clip > 0.0f) + { + tensor_utils::CwiseClipping(cell_state, n_batch * n_cell, clip); + } +} + +void CalculateLstmOutputFloat(int n_batch, int n_cell, int n_output, const float *cell_state, + const float *output_gate, TfLiteFusedActivation activation, + const float *projection_weights, const float *projection_bias, + const float proj_clip, float *output_state, float *scratch) +{ + tensor_utils::ApplyActivationToVector(cell_state, n_batch * n_cell, activation, scratch); + tensor_utils::VectorVectorCwiseProduct(output_gate, scratch, n_batch * n_cell, scratch); + + const bool use_projection = (projection_weights != nullptr); + const bool use_projection_bias = (projection_bias != nullptr); + + if (use_projection) + { + if (use_projection_bias) + { + tensor_utils::VectorBatchVectorAssign(projection_bias, n_output, n_batch, output_state); + } + else + { + std::fill_n(output_state, n_batch * n_output, 0.0f); + } + tensor_utils::MatrixBatchVectorMultiplyAccumulate(projection_weights, n_output, n_cell, scratch, + n_batch, output_state); + if (proj_clip > 0.0f) + { + tensor_utils::CwiseClipping(output_state, n_batch * n_output, proj_clip); + } + } + else + { + std::copy_n(scratch, n_batch * n_output, output_state); + } +} + +inline void CalculateLstmGateFloat(const float *input, const float *input_to_gate_weights, + const float *aux_input, const float *aux_input_to_gate_weights, + const float *output_state, + const float *recurrent_to_gate_weights, const float *cell_state, + const float *cell_to_gate_weights, + const float *layer_norm_coefficients, const float *gate_bias, + const int n_batch, const int n_input, const int n_aux_input, + const int n_output, const int n_cell, + const TfLiteFusedActivation activation, float *gate, + const bool is_input_all_zeros, const bool is_aux_input_all_zeros) +{ + const bool use_peephole = (cell_to_gate_weights != nullptr); + const bool use_layer_norm = (layer_norm_coefficients != nullptr); + + // Initialize scratch buffers with bias for regular lstm or initialize with + // zero for layer norm lstm. + if (use_layer_norm) + { + std::fill_n(gate, n_cell * n_batch, 0.0f); + } + else + { + tensor_utils::VectorBatchVectorAssign(gate_bias, n_cell, n_batch, gate); + } + // For each batch and cell: compute input_weight * input. + // Skip if input is all zeros. + if (!is_input_all_zeros) + { + tensor_utils::MatrixBatchVectorMultiplyAccumulate(input_to_gate_weights, n_cell, n_input, input, + n_batch, gate); + } + // For each batch and cell: compute aux_input_weight * aux_input. + // Skip if auxiliary input is not available or all zeros. + if (!is_aux_input_all_zeros) + { + tensor_utils::MatrixBatchVectorMultiplyAccumulate(aux_input_to_gate_weights, n_cell, + n_aux_input, aux_input, n_batch, gate); + } + // For each batch and cell: compute recurrent_weight * output_state. + tensor_utils::MatrixBatchVectorMultiplyAccumulate(recurrent_to_gate_weights, n_cell, n_output, + output_state, n_batch, gate); + // For each batch and cell: compute cell_weight .* cell_state (peephole LSTM) + if (use_peephole) + { + tensor_utils::VectorBatchVectorCwiseProductAccumulate(cell_to_gate_weights, n_cell, cell_state, + n_batch, gate); + } + // Do layer normalization (if layer norm LSTM) + if (use_layer_norm) + { + tensor_utils::MeanStddevNormalization(gate, gate, n_cell, n_batch); + tensor_utils::VectorBatchVectorCwiseProduct(layer_norm_coefficients, n_cell, gate, n_batch, + gate); + tensor_utils::VectorBatchVectorAdd(gate_bias, n_cell, n_batch, gate); + } + // Apply activation + tensor_utils::ApplyActivationToVector(gate, n_batch * n_cell, activation, gate); +} + +inline void LstmStepFloat( + const float *input_ptr, const float *input_to_input_weights_ptr, + const float *input_to_forget_weights_ptr, const float *input_to_cell_weights_ptr, + const float *input_to_output_weights_ptr, const float *aux_input_ptr, + const float *aux_input_to_input_weights_ptr, const float *aux_input_to_forget_weights_ptr, + const float *aux_input_to_cell_weights_ptr, const float *aux_input_to_output_weights_ptr, + const float *recurrent_to_input_weights_ptr, const float *recurrent_to_forget_weights_ptr, + const float *recurrent_to_cell_weights_ptr, const float *recurrent_to_output_weights_ptr, + const float *cell_to_input_weights_ptr, const float *cell_to_forget_weights_ptr, + const float *cell_to_output_weights_ptr, const float *input_layer_norm_coefficients_ptr, + const float *forget_layer_norm_coefficients_ptr, const float *cell_layer_norm_coefficients_ptr, + const float *output_layer_norm_coefficients_ptr, const float *input_gate_bias_ptr, + const float *forget_gate_bias_ptr, const float *cell_gate_bias_ptr, + const float *output_gate_bias_ptr, const float *projection_weights_ptr, + const float *projection_bias_ptr, const TfLiteLSTMParams *params, int n_batch, int n_cell, + int n_input, int n_aux_input, int n_output, int output_batch_leading_dim, float *output_state_ptr, + float *cell_state_ptr, float *scratch0, float *scratch1, float *scratch2, float *scratch3, + float *output_ptr) +{ + // Since we have already checked that weights are all there or none, we can + // check the existence of only one to the get the condition. + const bool use_cifg = (input_to_input_weights_ptr == nullptr); + + // Make named scratch buffers. + float *input_gate_scratch = scratch0; + float *forget_gate_scratch = scratch1; + float *cell_gate_scratch = scratch2; + float *output_gate_scratch = scratch3; + + // Check if inputs are all zeros so we can skip some computations. + const bool is_input_all_zeros = tensor_utils::IsZeroVector(input_ptr, n_batch * n_input); + const bool is_aux_input_all_zeros = + (aux_input_ptr == nullptr || tensor_utils::IsZeroVector(aux_input_ptr, n_batch * n_aux_input)); + if (!use_cifg) + { + // Calculate the input gate. (If not CIFG.) + CalculateLstmGateFloat(input_ptr, input_to_input_weights_ptr, aux_input_ptr, + aux_input_to_input_weights_ptr, output_state_ptr, + recurrent_to_input_weights_ptr, cell_state_ptr, + cell_to_input_weights_ptr, input_layer_norm_coefficients_ptr, + input_gate_bias_ptr, n_batch, n_input, n_aux_input, n_output, n_cell, + /*activation=*/kTfLiteActSigmoid, input_gate_scratch, is_input_all_zeros, + is_aux_input_all_zeros); + } + // Calculate the forget gate. + CalculateLstmGateFloat(input_ptr, input_to_forget_weights_ptr, aux_input_ptr, + aux_input_to_forget_weights_ptr, output_state_ptr, + recurrent_to_forget_weights_ptr, cell_state_ptr, + cell_to_forget_weights_ptr, forget_layer_norm_coefficients_ptr, + forget_gate_bias_ptr, n_batch, n_input, n_aux_input, n_output, n_cell, + /*activation=*/kTfLiteActSigmoid, forget_gate_scratch, is_input_all_zeros, + is_aux_input_all_zeros); + // Calculate the cell update gate. + CalculateLstmGateFloat( + input_ptr, input_to_cell_weights_ptr, aux_input_ptr, aux_input_to_cell_weights_ptr, + output_state_ptr, recurrent_to_cell_weights_ptr, /*cell_state=*/nullptr, + /*cell_to_gate_weights=*/nullptr, cell_layer_norm_coefficients_ptr, cell_gate_bias_ptr, n_batch, + n_input, n_aux_input, n_output, n_cell, params->activation, cell_gate_scratch, + is_input_all_zeros, is_aux_input_all_zeros); + // Update the cell state. + UpdateLstmCellFloat(n_batch, n_cell, cell_state_ptr, input_gate_scratch, forget_gate_scratch, + cell_gate_scratch, use_cifg, params->cell_clip); + // Calculate output gate. + CalculateLstmGateFloat(input_ptr, input_to_output_weights_ptr, aux_input_ptr, + aux_input_to_output_weights_ptr, output_state_ptr, + recurrent_to_output_weights_ptr, cell_state_ptr, + cell_to_output_weights_ptr, output_layer_norm_coefficients_ptr, + output_gate_bias_ptr, n_batch, n_input, n_aux_input, n_output, n_cell, + /*activation=*/kTfLiteActSigmoid, output_gate_scratch, is_input_all_zeros, + is_aux_input_all_zeros); + // Update the output state. + CalculateLstmOutputFloat(n_batch, n_cell, n_output, cell_state_ptr, output_gate_scratch, + params->activation, projection_weights_ptr, projection_bias_ptr, + params->proj_clip, output_state_ptr, scratch2); + // Copy output state to the output. Note that the output's rows may not be + // contiguous (output_batch_leading_dim != n_output). + for (int b = 0; b < n_batch; b++) + { + std::copy_n(output_state_ptr + b * n_output, n_output, + output_ptr + b * output_batch_leading_dim); + } +} + +} // namespace + +void EvalFloat(const Tensor *input, + + const Tensor *input_to_input_weights, const Tensor *input_to_forget_weights, + const Tensor *input_to_cell_weights, const Tensor *input_to_output_weights, + + const Tensor *recurrent_to_input_weights, const Tensor *recurrent_to_forget_weights, + const Tensor *recurrent_to_cell_weights, const Tensor *recurrent_to_output_weights, + + const Tensor *cell_to_input_weights, const Tensor *cell_to_forget_weights, + const Tensor *cell_to_output_weights, + + const Tensor *input_layer_norm_coefficients, + const Tensor *forget_layer_norm_coefficients, + const Tensor *cell_layer_norm_coefficients, + const Tensor *output_layer_norm_coefficients, + + const Tensor *aux_input, const Tensor *aux_input_to_input_weights, + const Tensor *aux_input_to_forget_weights, const Tensor *aux_input_to_cell_weights, + const Tensor *aux_input_to_output_weights, + + const Tensor *input_gate_bias, const Tensor *forget_gate_bias, + const Tensor *cell_gate_bias, const Tensor *output_gate_bias, + + const Tensor *projection_weights, const Tensor *projection_bias, + const TfLiteLSTMParams *params, + + bool forward_sequence, bool time_major, int output_offset, + + Tensor *scratch_buffer, Tensor *output_state, Tensor *cell_state, Tensor *output) +{ + const Shape &input_shape = input->shape(); + assert(input_shape.num_dims() >= 2 && input_shape.num_dims() <= 3); + int max_time, n_batch; + if (input_shape.num_dims() == 3) + { + max_time = (time_major) ? input_shape.dim(0) : input_shape.dim(1); + n_batch = (time_major) ? input_shape.dim(1) : input_shape.dim(0); + } + else + { + max_time = 1; + n_batch = input_shape.dim(0); + } + const int n_input = input_shape.dim(input_shape.num_dims() - 1); + + int aux_input_temp = 0; + if (aux_input) + { + const Shape &aux_input_shape = aux_input->shape(); + aux_input_temp = aux_input_shape.dim(aux_input_shape.num_dims() - 1); + } + const int aux_input_size = aux_input_temp; + + // n_cell and n_output will be the same size when there is no projection. + const Shape &input_to_output_weights_shape = input_to_output_weights->shape(); + const Shape &recurrent_to_output_weights_shape = recurrent_to_output_weights->shape(); + const int n_cell = input_to_output_weights_shape.dim(0); + const int n_output = recurrent_to_output_weights_shape.dim(1); + + // Since we have already checked that weights are all there or none, we can + // check the existence of only one to the get the condition. + const bool use_cifg = (input_to_input_weights == nullptr); + + // Index the scratch buffers pointers to the global scratch buffer. + float *scratch_buffer_ptr = getTensorData(scratch_buffer); + float *input_gate_scratch = nullptr; + float *cell_gate_scratch = nullptr; + float *forget_gate_scratch = nullptr; + float *output_gate_scratch = nullptr; + if (use_cifg) + { + cell_gate_scratch = scratch_buffer_ptr; + forget_gate_scratch = scratch_buffer_ptr + n_cell * n_batch; + output_gate_scratch = scratch_buffer_ptr + 2 * n_cell * n_batch; + } + else + { + input_gate_scratch = scratch_buffer_ptr; + cell_gate_scratch = scratch_buffer_ptr + n_cell * n_batch; + forget_gate_scratch = scratch_buffer_ptr + 2 * n_cell * n_batch; + output_gate_scratch = scratch_buffer_ptr + 3 * n_cell * n_batch; + } + + const Shape &output_shape = output->shape(); + const int output_batch_leading_dim = output_shape.dim(output_shape.num_dims() - 1); + if (time_major) + { + // Loop through the sequence. + const int input_step = n_batch * n_input; + const int output_step = n_batch * output_batch_leading_dim; + for (int t = 0; t < max_time; t++) + { + // If this is the forward_sequence, step forward, otherwise step + // backwards. + const int t_rel = forward_sequence ? t : max_time - t - 1; + const float *input_ptr = getTensorData(input) + t_rel * input_step; + const float *aux_input_ptr = nullptr; + if (aux_input) + { + aux_input_ptr = getTensorData(aux_input) + t_rel * input_step; + } + float *output_ptr = getTensorData(output) + t_rel * output_step + output_offset; + + LstmStepFloat( + input_ptr, getTensorData(input_to_input_weights), + getTensorData(input_to_forget_weights), getTensorData(input_to_cell_weights), + getTensorData(input_to_output_weights), aux_input_ptr, + getTensorData(aux_input_to_input_weights), + getTensorData(aux_input_to_forget_weights), + getTensorData(aux_input_to_cell_weights), + getTensorData(aux_input_to_output_weights), + getTensorData(recurrent_to_input_weights), + getTensorData(recurrent_to_forget_weights), + getTensorData(recurrent_to_cell_weights), + getTensorData(recurrent_to_output_weights), + getTensorData(cell_to_input_weights), getTensorData(cell_to_forget_weights), + getTensorData(cell_to_output_weights), + getTensorData(input_layer_norm_coefficients), + getTensorData(forget_layer_norm_coefficients), + getTensorData(cell_layer_norm_coefficients), + getTensorData(output_layer_norm_coefficients), getTensorData(input_gate_bias), + getTensorData(forget_gate_bias), getTensorData(cell_gate_bias), + getTensorData(output_gate_bias), getTensorData(projection_weights), + getTensorData(projection_bias), params, n_batch, n_cell, n_input, aux_input_size, + n_output, output_batch_leading_dim, getTensorData(output_state), + getTensorData(cell_state), input_gate_scratch, forget_gate_scratch, + cell_gate_scratch, output_gate_scratch, output_ptr); + } + } + else + { + for (int b = 0; b < n_batch; b++) + { + const int input_step = n_input; + const int output_step = output_batch_leading_dim; + for (int t = 0; t < max_time; t++) + { + // If this is the forward_sequence, step forward, otherwise step + // backwards. + const int t_rel = forward_sequence ? t : max_time - t - 1; + const int time_offset = b * max_time + t_rel; + const float *input_ptr = getTensorData(input) + time_offset * input_step; + const float *aux_input_ptr = nullptr; + if (aux_input) + { + aux_input_ptr = getTensorData(aux_input) + time_offset * input_step; + } + float *output_ptr = + getTensorData(output) + time_offset * output_step + output_offset; + + // Offset the {output,cell}_state pointers to the right batch. + float *output_state_ptr = getTensorData(output_state) + b * output_batch_leading_dim; + float *cell_state_ptr = getTensorData(cell_state) + b * n_cell; + // Offset the scratch pointers to the right batch. + float *input_gate_scratch_ptr = + input_gate_scratch ? input_gate_scratch + b * n_cell : nullptr; + float *forget_gate_scratch_ptr = forget_gate_scratch + b * n_cell; + float *cell_gate_scratch_ptr = cell_gate_scratch + b * n_cell; + float *output_gate_scratch_ptr = output_gate_scratch + b * n_cell; + + LstmStepFloat( + input_ptr, getTensorData(input_to_input_weights), + getTensorData(input_to_forget_weights), + getTensorData(input_to_cell_weights), + getTensorData(input_to_output_weights), aux_input_ptr, + getTensorData(aux_input_to_input_weights), + getTensorData(aux_input_to_forget_weights), + getTensorData(aux_input_to_cell_weights), + getTensorData(aux_input_to_output_weights), + getTensorData(recurrent_to_input_weights), + getTensorData(recurrent_to_forget_weights), + getTensorData(recurrent_to_cell_weights), + getTensorData(recurrent_to_output_weights), + getTensorData(cell_to_input_weights), getTensorData(cell_to_forget_weights), + getTensorData(cell_to_output_weights), + getTensorData(input_layer_norm_coefficients), + getTensorData(forget_layer_norm_coefficients), + getTensorData(cell_layer_norm_coefficients), + getTensorData(output_layer_norm_coefficients), + getTensorData(input_gate_bias), getTensorData(forget_gate_bias), + getTensorData(cell_gate_bias), getTensorData(output_gate_bias), + getTensorData(projection_weights), getTensorData(projection_bias), params, + /*n_batch=*/1, n_cell, n_input, aux_input_size, n_output, output_batch_leading_dim, + output_state_ptr, cell_state_ptr, input_gate_scratch_ptr, forget_gate_scratch_ptr, + cell_gate_scratch_ptr, output_gate_scratch_ptr, output_ptr); + } + } + } +} + +} // namespace lstm +} // namespace kernels +} // namespace luci_interpreter + +namespace luci_interpreter +{ +namespace kernels +{ + +UnidirectionalSequenceLSTM::UnidirectionalSequenceLSTM( + const Tensor *input, + + const Tensor *input_to_input_weights, const Tensor *input_to_forget_weights, + const Tensor *input_to_cell_weights, const Tensor *input_to_output_weights, + + const Tensor *recurrent_to_input_weights, const Tensor *recurrent_to_forget_weights, + const Tensor *recurrent_to_cell_weights, const Tensor *recurrent_to_output_weights, + + const Tensor *cell_to_input_weights, const Tensor *cell_to_forget_weights, + const Tensor *cell_to_output_weights, + + const Tensor *input_gate_bias, const Tensor *forget_gate_bias, const Tensor *cell_gate_bias, + const Tensor *output_gate_bias, + + const Tensor *projection_weights, const Tensor *projection_bias, + + const Tensor *output_state, const Tensor *cell_state, const Tensor *input_layer_norm_coefficients, + const Tensor *forget_layer_norm_coefficients, const Tensor *cell_layer_norm_coefficients, + const Tensor *output_layer_norm_coefficients, + + Tensor *output, Tensor *scratchpad_1, Tensor *scratchpad_2, Tensor *scratchpad_3, + const UnidirectionalSequenceLSTMParams ¶ms) + : KernelWithParams( + {input, + input_to_input_weights, + input_to_forget_weights, + input_to_cell_weights, + input_to_output_weights, + + recurrent_to_input_weights, + recurrent_to_forget_weights, + recurrent_to_cell_weights, + recurrent_to_output_weights, + + cell_to_input_weights, + cell_to_forget_weights, + cell_to_output_weights, + + input_gate_bias, + forget_gate_bias, + cell_gate_bias, + output_gate_bias, + + projection_weights, + projection_bias, + + output_state, + cell_state, + + input_layer_norm_coefficients, + forget_layer_norm_coefficients, + cell_layer_norm_coefficients, + output_layer_norm_coefficients}, + {output, scratchpad_1, scratchpad_2, scratchpad_3}, params) +{ + // Do nothing +} + +// Check that input tensor dimensions matches with each other. +void UnidirectionalSequenceLSTM::check_input_tensor_dimensions(int n_input, int n_output, + int n_cell, bool use_layer_norm, + bool is_integer) +{ + // Making sure clipping parameters have valid values. + // == 0 means no clipping + // > 0 means clipping + LUCI_INTERPRETER_CHECK(params().cell_clip >= 0); + LUCI_INTERPRETER_CHECK(params().proj_clip >= 0); + + if (input_to_input_weights() != nullptr) + { + const Shape &input_to_input_weights_shape = input_to_input_weights()->shape(); + LUCI_INTERPRETER_CHECK(input_to_input_weights_shape.num_dims() == 2); + LUCI_INTERPRETER_CHECK(input_to_input_weights_shape.dim(0) == n_cell); + LUCI_INTERPRETER_CHECK(input_to_input_weights_shape.dim(1) == n_input); + } + + const Shape &input_to_forget_weights_shape = input_to_forget_weights()->shape(); + LUCI_INTERPRETER_CHECK(input_to_forget_weights_shape.num_dims() == 2); + LUCI_INTERPRETER_CHECK(input_to_forget_weights_shape.dim(0) == n_cell); + LUCI_INTERPRETER_CHECK(input_to_forget_weights_shape.dim(1) == n_input); + + const Shape &input_to_cell_weights_shape = input_to_cell_weights()->shape(); + LUCI_INTERPRETER_CHECK(input_to_cell_weights_shape.num_dims() == 2); + LUCI_INTERPRETER_CHECK(input_to_cell_weights_shape.dim(0) == n_cell); + LUCI_INTERPRETER_CHECK(input_to_cell_weights_shape.dim(1) == n_input); + + if (recurrent_to_input_weights() != nullptr) + { + const Shape &recurrent_to_input_weights_shape = recurrent_to_input_weights()->shape(); + LUCI_INTERPRETER_CHECK(recurrent_to_input_weights_shape.num_dims() == 2); + LUCI_INTERPRETER_CHECK(recurrent_to_input_weights_shape.dim(0) == n_cell); + LUCI_INTERPRETER_CHECK(recurrent_to_input_weights_shape.dim(1) == n_output); + } + + const Shape &recurrent_to_forget_weights_shape = recurrent_to_forget_weights()->shape(); + LUCI_INTERPRETER_CHECK(recurrent_to_forget_weights_shape.num_dims() == 2); + LUCI_INTERPRETER_CHECK(recurrent_to_forget_weights_shape.dim(0) == n_cell); + LUCI_INTERPRETER_CHECK(recurrent_to_forget_weights_shape.dim(1) == n_output); + + const Shape &recurrent_to_cell_weights_shape = recurrent_to_cell_weights()->shape(); + LUCI_INTERPRETER_CHECK(recurrent_to_cell_weights_shape.num_dims() == 2); + LUCI_INTERPRETER_CHECK(recurrent_to_cell_weights_shape.dim(0) == n_cell); + LUCI_INTERPRETER_CHECK(recurrent_to_cell_weights_shape.dim(1) == n_output); + + // We make sure the input-gate's parameters are either both present (regular + // LSTM) or not at all (CIFG-LSTM). + const bool cifg_weights_all_or_none = + ((input_to_input_weights() != nullptr) && (recurrent_to_input_weights() != nullptr)) || + ((input_to_input_weights() == nullptr) && (recurrent_to_input_weights() == nullptr)); + LUCI_INTERPRETER_CHECK(cifg_weights_all_or_none == true); + + if (cell_to_input_weights() != nullptr) + { + const Shape &cell_to_input_weights_shape = cell_to_input_weights()->shape(); + LUCI_INTERPRETER_CHECK(cell_to_input_weights_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(cell_to_input_weights_shape.dim(0) == n_cell); + LUCI_INTERPRETER_CHECK(is_integer + ? cell_to_input_weights()->element_type() == loco::DataType::S16 + : cell_to_input_weights()->element_type() == + input_to_forget_weights()->element_type()); + } + + if (cell_to_forget_weights() != nullptr) + { + const Shape &cell_to_forget_weights_shape = cell_to_forget_weights()->shape(); + LUCI_INTERPRETER_CHECK(cell_to_forget_weights_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(cell_to_forget_weights_shape.dim(0) == n_cell); + LUCI_INTERPRETER_CHECK(is_integer + ? cell_to_forget_weights()->element_type() == loco::DataType::S16 + : cell_to_forget_weights()->element_type() == + input_to_forget_weights()->element_type()); + } + + if (cell_to_output_weights() != nullptr) + { + const Shape &cell_to_output_weights_shape = cell_to_output_weights()->shape(); + LUCI_INTERPRETER_CHECK(cell_to_output_weights_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(cell_to_output_weights_shape.dim(0) == n_cell); + LUCI_INTERPRETER_CHECK(is_integer + ? cell_to_output_weights()->element_type() == loco::DataType::S16 + : cell_to_output_weights()->element_type() == + input_to_forget_weights()->element_type()); + } + + // Making sure the peephole weights are there all or none. + const bool use_cifg = (input_to_input_weights() == nullptr); + const bool peephole_weights_all_or_none = + ((cell_to_input_weights() != nullptr || use_cifg) && (cell_to_forget_weights() != nullptr) && + (cell_to_output_weights() != nullptr)) || + ((cell_to_input_weights() == nullptr) && (cell_to_forget_weights() == nullptr) && + (cell_to_output_weights() == nullptr)); + LUCI_INTERPRETER_CHECK(peephole_weights_all_or_none == true); + + // Make sure the input gate bias is present only when not a CIFG-LSTM. + if (use_cifg) + { + LUCI_INTERPRETER_CHECK(input_gate_bias() == nullptr); + } + else + { + const Shape &input_gate_bias_shape = input_gate_bias()->shape(); + LUCI_INTERPRETER_CHECK(input_gate_bias_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(input_gate_bias_shape.dim(0) == n_cell); + if (is_integer) + { + LUCI_INTERPRETER_CHECK(input_gate_bias()->element_type() == loco::DataType::S32); + } + else + { + LUCI_INTERPRETER_CHECK(input_gate_bias()->element_type() == loco::DataType::FLOAT32); + } + } + + const Shape &forget_gate_bias_shape = forget_gate_bias()->shape(); + LUCI_INTERPRETER_CHECK(forget_gate_bias_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(forget_gate_bias_shape.dim(0) == n_cell); + if (is_integer) + { + LUCI_INTERPRETER_CHECK(forget_gate_bias()->element_type() == loco::DataType::S32); + } + else + { + LUCI_INTERPRETER_CHECK(forget_gate_bias()->element_type() == loco::DataType::FLOAT32); + } + + const Shape &cell_gate_bias_shape = cell_gate_bias()->shape(); + LUCI_INTERPRETER_CHECK(cell_gate_bias_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(cell_gate_bias_shape.dim(0) == n_cell); + if (is_integer) + { + LUCI_INTERPRETER_CHECK(cell_gate_bias()->element_type() == loco::DataType::S32); + } + else + { + LUCI_INTERPRETER_CHECK(cell_gate_bias()->element_type() == loco::DataType::FLOAT32); + } + + const Shape &output_gate_bias_shape = output_gate_bias()->shape(); + LUCI_INTERPRETER_CHECK(output_gate_bias_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(output_gate_bias_shape.dim(0) == n_cell); + if (is_integer) + { + LUCI_INTERPRETER_CHECK(output_gate_bias()->element_type() == loco::DataType::S32); + } + else + { + LUCI_INTERPRETER_CHECK(output_gate_bias()->element_type() == loco::DataType::FLOAT32); + } + + if (projection_weights() != nullptr) + { + const Shape &projection_weights_shape = projection_weights()->shape(); + LUCI_INTERPRETER_CHECK(projection_weights_shape.num_dims() == 2); + LUCI_INTERPRETER_CHECK(projection_weights_shape.dim(0) == n_output); + LUCI_INTERPRETER_CHECK(projection_weights_shape.dim(1) == n_cell); + } + + if (projection_bias() != nullptr) + { + const Shape &projection_bias_shape = projection_bias()->shape(); + LUCI_INTERPRETER_CHECK(projection_bias_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(projection_bias_shape.dim(0) == n_output); + if (is_integer) + { + LUCI_INTERPRETER_CHECK(projection_bias()->element_type() == loco::DataType::S32); + } + else + { + LUCI_INTERPRETER_CHECK(projection_bias()->element_type() == loco::DataType::FLOAT32); + } + } + + // Making sure the projection tensors are consistent: + // 1) If projection weight is not present, then projection bias should not be + // present. + // 2) If projection weight is present, then projection bias is optional. + // TODO(ghodrat): make sure this is correct. + const bool projecton_tensors_consistent = + ((projection_weights() != nullptr) || (projection_bias() == nullptr)); + LUCI_INTERPRETER_CHECK(projecton_tensors_consistent == true); + + if (use_layer_norm) + { + if (use_cifg) + { + LUCI_INTERPRETER_CHECK(input_layer_norm_coefficients() == nullptr); + } + else + { + LUCI_INTERPRETER_CHECK(input_layer_norm_coefficients() != nullptr) + + const Shape &input_layer_norm_coefficients_shape = input_layer_norm_coefficients()->shape(); + LUCI_INTERPRETER_CHECK(input_layer_norm_coefficients_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(input_layer_norm_coefficients_shape.dim(0) == n_cell); + if (is_integer) + { + LUCI_INTERPRETER_CHECK(input_layer_norm_coefficients()->element_type() == + loco::DataType::S16); + } + else + { + LUCI_INTERPRETER_CHECK(input_layer_norm_coefficients()->element_type() == + loco::DataType::FLOAT32); + } + } + + const Shape &forget_layer_norm_coefficients_shape = forget_layer_norm_coefficients()->shape(); + LUCI_INTERPRETER_CHECK(forget_layer_norm_coefficients_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(forget_layer_norm_coefficients_shape.dim(0) == n_cell); + if (is_integer) + { + LUCI_INTERPRETER_CHECK(forget_layer_norm_coefficients()->element_type() == + loco::DataType::S16); + } + else + { + LUCI_INTERPRETER_CHECK(forget_layer_norm_coefficients()->element_type() == + loco::DataType::FLOAT32); + } + + const Shape &cell_layer_norm_coefficients_shape = cell_layer_norm_coefficients()->shape(); + LUCI_INTERPRETER_CHECK(cell_layer_norm_coefficients_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(cell_layer_norm_coefficients_shape.dim(0) == n_cell); + if (is_integer) + { + LUCI_INTERPRETER_CHECK(cell_layer_norm_coefficients()->element_type() == loco::DataType::S16); + } + else + { + LUCI_INTERPRETER_CHECK(cell_layer_norm_coefficients()->element_type() == + loco::DataType::FLOAT32); + } + + const Shape &output_layer_norm_coefficients_shape = output_layer_norm_coefficients()->shape(); + LUCI_INTERPRETER_CHECK(output_layer_norm_coefficients_shape.num_dims() == 1); + LUCI_INTERPRETER_CHECK(output_layer_norm_coefficients_shape.dim(0) == n_cell); + if (is_integer) + { + LUCI_INTERPRETER_CHECK(output_layer_norm_coefficients()->element_type() == + loco::DataType::S16); + } + else + { + LUCI_INTERPRETER_CHECK(output_layer_norm_coefficients()->element_type() == + loco::DataType::FLOAT32); + } + } +} + +void UnidirectionalSequenceLSTM::configure() +{ + LUCI_INTERPRETER_CHECK(getInputTensors().size() == 24); + LUCI_INTERPRETER_CHECK(getOutputTensors().size() >= 1); + + // TODO support U8 + LUCI_INTERPRETER_CHECK(input()->element_type() == loco::DataType::FLOAT32); + const bool is_integer = false; + const bool use_layer_norm = (forget_layer_norm_coefficients() != nullptr); + + // Inferring batch size, number of outputs and sequence length and + // number of cells from the input tensors. + const Shape &input_shape = input()->shape(); + LUCI_INTERPRETER_CHECK(input_shape.num_dims() > 1); + const bool time_major = params().time_major; + const int n_batch = time_major ? input_shape.dim(1) : input_shape.dim(0); + // NOTE as dim(2) is accessed, we need to check this is valid + LUCI_INTERPRETER_CHECK(input_shape.num_dims() > 2); + const int n_input = input_shape.dim(2); + + const Shape &input_to_output_weights_shape = input_to_output_weights()->shape(); + const int n_cell = input_to_output_weights_shape.dim(0); + LUCI_INTERPRETER_CHECK(input_to_output_weights_shape.num_dims() == 2); + LUCI_INTERPRETER_CHECK(input_to_output_weights_shape.dim(1) == n_input); + + const Shape &recurrent_to_output_weights_shape = recurrent_to_output_weights()->shape(); + LUCI_INTERPRETER_CHECK(recurrent_to_output_weights_shape.num_dims() == 2); + LUCI_INTERPRETER_CHECK(recurrent_to_output_weights_shape.dim(0) == n_cell); + + const int n_output = recurrent_to_output_weights_shape.dim(1); + + // Check that input tensor dimensions matches with each other. + check_input_tensor_dimensions(n_input, n_output, n_cell, use_layer_norm, is_integer); + + // Check the shape of input state tensors. + // These tensor may be 1D or 2D. It's fine as long as the total size is + // correct. + const Shape &output_state_shape = output_state()->shape(); + const Shape &cell_state_shape = cell_state()->shape(); + LUCI_INTERPRETER_CHECK(output_state_shape.num_elements() == n_batch * n_output); + LUCI_INTERPRETER_CHECK(cell_state_shape.num_elements() == n_batch * n_cell); + + // Resize the output tensors. + Shape output_shape = Shape(input_shape.num_dims()); + for (int i = 0; i < input_shape.num_dims() - 1; i++) + { + output_shape.dim(i) = input_shape.dim(i); + } + output_shape.dim(input_shape.num_dims() - 1) = n_output; + output()->resize(output_shape); + + // TODO import integer + + // output_state and cell_state are variable tensor; use scratchpad. + getOutputTensors()[1]->resize(output_state_shape); + getOutputTensors()[2]->resize(cell_state_shape); + + const bool use_cifg = (input_to_input_weights() == nullptr); + if (use_cifg) + getOutputTensors()[3]->resize({n_batch, n_cell * 3}); + else + getOutputTensors()[3]->resize({n_batch, n_cell * 4}); + + // hybrid not supported + if (input_to_output_weights()->element_type() == loco::DataType::U8 && + input()->element_type() == loco::DataType::FLOAT32) + { + throw std::runtime_error("Hybrid type is not currently supported"); + } + // TODO support hybrid + // TODO support U8 +} + +void UnidirectionalSequenceLSTM::execute() const +{ + switch (input()->element_type()) + { + case loco::DataType::FLOAT32: + evalFloat(); + break; + default: + throw std::runtime_error("Unsupported type"); + } +} + +void UnidirectionalSequenceLSTM::evalFloat() const +{ + const bool time_major = params().time_major; + const bool use_layer_norm = (forget_layer_norm_coefficients() != nullptr); + + const Tensor *t_input_layer_norm_coefficients = + use_layer_norm ? input_layer_norm_coefficients() : nullptr; + const Tensor *t_forget_layer_norm_coefficients = + use_layer_norm ? forget_layer_norm_coefficients() : nullptr; + const Tensor *t_cell_layer_norm_coefficients = + use_layer_norm ? cell_layer_norm_coefficients() : nullptr; + const Tensor *t_output_layer_norm_coefficients = + use_layer_norm ? output_layer_norm_coefficients() : nullptr; + + Tensor *sp_output_state = getOutputTensors()[1]; + Tensor *sp_cell_state = getOutputTensors()[2]; + Tensor *sp_scratch_buffer = getOutputTensors()[3]; + + // Note: it is expected that output_state input variable tensor reset to zero, + // also expected that this variable tensor doesn't have buffer + auto scratchpad_data = getTensorData(sp_output_state); + std::fill_n(scratchpad_data, sp_output_state->shape().num_elements(), 0); + scratchpad_data = getTensorData(sp_cell_state); + std::fill_n(scratchpad_data, sp_cell_state->shape().num_elements(), 0); + scratchpad_data = getTensorData(sp_scratch_buffer); + std::fill_n(scratchpad_data, sp_scratch_buffer->shape().num_elements(), 0); + + TfLiteLSTMParams lstm_params{}; + lstm_params.activation = getTfLiteActivation(params().activation); + lstm_params.cell_clip = params().cell_clip; + lstm_params.proj_clip = params().proj_clip; + lstm_params.asymmetric_quantize_inputs = params().asymmetric_quantize_inputs; + + lstm::EvalFloat(input(), input_to_input_weights(), input_to_forget_weights(), + input_to_cell_weights(), input_to_output_weights(), + + recurrent_to_input_weights(), recurrent_to_forget_weights(), + recurrent_to_cell_weights(), recurrent_to_output_weights(), + + cell_to_input_weights(), cell_to_forget_weights(), cell_to_output_weights(), + + t_input_layer_norm_coefficients, t_forget_layer_norm_coefficients, + t_cell_layer_norm_coefficients, t_output_layer_norm_coefficients, + /*aux_input=*/nullptr, + /*aux_input_to_input_weights=*/nullptr, + /*aux_input_to_forget_weights=*/nullptr, + /*aux_input_to_cell_weights=*/nullptr, + /*aux_input_to_output_weights=*/nullptr, input_gate_bias(), forget_gate_bias(), + cell_gate_bias(), output_gate_bias(), + + projection_weights(), projection_bias(), &lstm_params, + /*forward_sequence=*/true, time_major, + /*output_offset=*/0, sp_scratch_buffer, sp_output_state, sp_cell_state, output()); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.h b/compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.h new file mode 100644 index 0000000..b812511 --- /dev/null +++ b/compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_KERNELS_UNIDIRECTIONALSEQUENCELSTM_H +#define LUCI_INTERPRETER_KERNELS_UNIDIRECTIONALSEQUENCELSTM_H + +#include "core/Kernel.h" +#include "core/KernelParams.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +class UnidirectionalSequenceLSTM : public KernelWithParams +{ +public: + UnidirectionalSequenceLSTM( + const Tensor *input, + + const Tensor *input_to_input_weights, const Tensor *input_to_forget_weights, + const Tensor *input_to_cell_weights, const Tensor *input_to_output_weights, + + const Tensor *recurrent_to_input_weights, const Tensor *recurrent_to_forget_weights, + const Tensor *recurrent_to_cell_weights, const Tensor *recurrent_to_output_weights, + + const Tensor *cell_to_input_weights, const Tensor *cell_to_forget_weights, + const Tensor *cell_to_output_weights, + + const Tensor *input_gate_bias, const Tensor *forget_gate_bias, const Tensor *cell_gate_bias, + const Tensor *output_gate_bias, + + const Tensor *projection_weights, const Tensor *projection_bias, + + const Tensor *output_state, const Tensor *cell_state, + + const Tensor *input_layer_norm_coefficients, const Tensor *forget_layer_norm_coefficients, + const Tensor *cell_layer_norm_coefficients, const Tensor *output_layer_norm_coefficients, + + Tensor *output, Tensor *scratchpad_1, Tensor *scratchpad_2, Tensor *scratchpad_3, + const UnidirectionalSequenceLSTMParams ¶ms); + + const Tensor *input() const { return _inputs[0]; } + + const Tensor *input_to_input_weights() const { return _inputs[1]; } + const Tensor *input_to_forget_weights() const { return _inputs[2]; } + const Tensor *input_to_cell_weights() const { return _inputs[3]; } + const Tensor *input_to_output_weights() const { return _inputs[4]; } + + const Tensor *recurrent_to_input_weights() const { return _inputs[5]; } + const Tensor *recurrent_to_forget_weights() const { return _inputs[6]; } + const Tensor *recurrent_to_cell_weights() const { return _inputs[7]; } + const Tensor *recurrent_to_output_weights() const { return _inputs[8]; } + + const Tensor *cell_to_input_weights() const { return _inputs[9]; } + const Tensor *cell_to_forget_weights() const { return _inputs[10]; } + const Tensor *cell_to_output_weights() const { return _inputs[11]; } + + const Tensor *input_gate_bias() const { return _inputs[12]; } + const Tensor *forget_gate_bias() const { return _inputs[13]; } + const Tensor *cell_gate_bias() const { return _inputs[14]; } + const Tensor *output_gate_bias() const { return _inputs[15]; } + + const Tensor *projection_weights() const { return _inputs[16]; } + const Tensor *projection_bias() const { return _inputs[17]; } + + const Tensor *output_state() const { return _inputs[18]; } + const Tensor *cell_state() const { return _inputs[19]; } + + const Tensor *input_layer_norm_coefficients() const { return _inputs[20]; } + const Tensor *forget_layer_norm_coefficients() const { return _inputs[21]; } + const Tensor *cell_layer_norm_coefficients() const { return _inputs[22]; } + const Tensor *output_layer_norm_coefficients() const { return _inputs[23]; } + + Tensor *output() const { return _outputs[0]; } + + void configure() override; + void execute() const override; + +private: + void evalFloat() const; + +private: + void check_input_tensor_dimensions(int n_input, int n_output, int n_cell, bool use_layer_norm, + bool is_integer); +}; + +} // namespace kernels +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_KERNELS_UNIDIRECTIONALSEQUENCELSTM_H diff --git a/compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.test.cpp b/compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.test.cpp new file mode 100644 index 0000000..df059cf --- /dev/null +++ b/compiler/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.test.cpp @@ -0,0 +1,565 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/UnidirectionalSequenceLSTM.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +class UnidirectionalSequenceLSTMTest : public ::testing::Test +{ +protected: + void SetUp() override { _memory_manager = std::make_unique(); } + + std::unique_ptr _memory_manager; +}; + +// NOTE from NoCifgNoPeepholeNoProjectionNoClippingUnidirectionalLstmTest +TEST_F(UnidirectionalSequenceLSTMTest, FloatTest) +{ + const int32_t n_batch = 1; + const int32_t n_input = 2; + const int32_t n_cell = 4; + const int32_t n_output = 4; + const int32_t sequence_length = 3; + + std::vector input_to_input_weights = {-0.45018822, -0.02338299, -0.0870589, -0.34550029, + 0.04266912, -0.15680569, -0.34856534, 0.43890524}; + + std::vector input_to_cell_weights = {-0.50013041, 0.1370284, 0.11810488, 0.2013163, + -0.20583314, 0.44344562, 0.22077113, -0.29909778}; + + std::vector input_to_forget_weights = {0.09701663, 0.20334584, -0.50592935, -0.31343272, + -0.40032279, 0.44781327, 0.01387155, -0.35593212}; + + std::vector input_to_output_weights = {-0.25065863, -0.28290087, 0.04613829, 0.40525138, + 0.44272184, 0.03897077, -0.1556896, 0.19487578}; + + std::vector input_gate_bias = {0., 0., 0., 0.}; + std::vector forget_gate_bias = {1., 1., 1., 1.}; + std::vector cell_gate_bias = {0., 0., 0., 0.}; + std::vector output_gate_bias = {0., 0., 0., 0.}; + + std::vector recurrent_to_input_weights = { + -0.0063535, -0.2042388, 0.31454784, -0.35746509, 0.28902304, 0.08183324, + -0.16555229, 0.02286911, -0.13566875, 0.03034258, 0.48091322, -0.12528998, + 0.24077177, -0.51332325, -0.33502164, 0.10629296}; + + std::vector recurrent_to_forget_weights = { + -0.48684245, -0.06655136, 0.42224967, 0.2112639, 0.27654213, 0.20864892, + -0.07646349, 0.45877004, 0.00141793, -0.14609534, 0.36447752, 0.09196436, + 0.28053468, 0.01560611, -0.20127171, -0.01140004}; + + std::vector recurrent_to_cell_weights = { + -0.3407414, 0.24443203, -0.2078532, 0.26320225, 0.05695659, -0.00123841, + -0.4744786, -0.35869038, -0.06418842, -0.13502428, -0.501764, 0.22830659, + -0.46367589, 0.26016325, -0.03894562, -0.16368064}; + + std::vector recurrent_to_output_weights = { + 0.43385774, -0.17194885, 0.2718237, 0.09215671, 0.24107647, -0.39835793, + 0.18212086, 0.01301402, 0.48572797, -0.50656658, 0.20047462, -0.20607421, + -0.51818722, -0.15390486, 0.0468148, 0.39922136}; + + Shape input_to_input_weights_shape{n_cell, n_input}; + Shape input_to_cell_weights_shape{n_cell, n_input}; + Shape input_to_forget_weights_shape{n_cell, n_input}; + Shape input_to_output_weights_shape{n_cell, n_input}; + + Shape input_gate_bias_shape{n_cell}; + Shape forget_gate_bias_shape{n_cell}; + Shape cell_gate_bias_shape{n_cell}; + Shape output_gate_bias_shape{n_cell}; + + Shape recurrent_to_input_weights_shape{n_cell, n_output}; + Shape recurrent_to_cell_weights_shape{n_cell, n_output}; + Shape recurrent_to_forget_weights_shape{n_cell, n_output}; + Shape recurrent_to_output_weights_shape{n_cell, n_output}; + + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + Tensor input_to_cell_weights_tensor = makeInputTensor( + input_to_cell_weights_shape, input_to_cell_weights, _memory_manager.get()); + Tensor input_to_forget_weights_tensor = makeInputTensor( + input_to_forget_weights_shape, input_to_forget_weights, _memory_manager.get()); + Tensor input_to_output_weights_tensor = makeInputTensor( + input_to_output_weights_shape, input_to_output_weights, _memory_manager.get()); + + Tensor input_gate_bias_tensor = makeInputTensor( + input_gate_bias_shape, input_gate_bias, _memory_manager.get()); + Tensor forget_gate_bias_tensor = makeInputTensor( + forget_gate_bias_shape, forget_gate_bias, _memory_manager.get()); + Tensor cell_gate_bias_tensor = + makeInputTensor(cell_gate_bias_shape, cell_gate_bias, _memory_manager.get()); + Tensor output_gate_bias_tensor = makeInputTensor( + output_gate_bias_shape, output_gate_bias, _memory_manager.get()); + + Tensor recurrent_to_input_weights_tensor = makeInputTensor( + recurrent_to_input_weights_shape, recurrent_to_input_weights, _memory_manager.get()); + Tensor recurrent_to_cell_weights_tensor = makeInputTensor( + recurrent_to_cell_weights_shape, recurrent_to_cell_weights, _memory_manager.get()); + Tensor recurrent_to_forget_weights_tensor = makeInputTensor( + recurrent_to_forget_weights_shape, recurrent_to_forget_weights, _memory_manager.get()); + Tensor recurrent_to_output_weights_tensor = makeInputTensor( + recurrent_to_output_weights_shape, recurrent_to_output_weights, _memory_manager.get()); + + std::vector input_data{2., 3., 3., 4., 1., 1.}; + Shape input_shape{sequence_length, n_batch, n_input}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + Shape output_state_shape{n_batch, n_output}; + Tensor output_state_tensor = makeOutputTensor(DataType::FLOAT32); + output_state_tensor.resize(output_state_shape); + + Shape cell_state_shape{n_batch, n_cell}; + Tensor cell_state_tensor = makeOutputTensor(DataType::FLOAT32); + cell_state_tensor.resize(cell_state_shape); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_2(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_3(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 0.0; + params.proj_clip = 0.0; + params.time_major = true; + params.asymmetric_quantize_inputs = false; + + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_forget_weights_tensor, + &input_to_cell_weights_tensor, &input_to_output_weights_tensor, + &recurrent_to_input_weights_tensor, &recurrent_to_forget_weights_tensor, + &recurrent_to_cell_weights_tensor, &recurrent_to_output_weights_tensor, nullptr, nullptr, + nullptr, &input_gate_bias_tensor, &forget_gate_bias_tensor, &cell_gate_bias_tensor, + &output_gate_bias_tensor, nullptr, nullptr, &output_state_tensor, &cell_state_tensor, nullptr, + nullptr, nullptr, nullptr, &output_tensor, &scratchpad_1, &scratchpad_2, &scratchpad_3, params); + + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(output_state_tensor); + _memory_manager->allocate_memory(cell_state_tensor); + _memory_manager->allocate_memory(scratchpad_1); + _memory_manager->allocate_memory(scratchpad_2); + _memory_manager->allocate_memory(scratchpad_3); + kernel.execute(); + + std::vector ref_output_data{-0.02973187, 0.1229473, 0.20885126, -0.15358765, + -0.03716109, 0.12507336, 0.41193449, -0.20860538, + -0.15053082, 0.09120187, 0.24278517, -0.12222792}; + + std::vector ref_output_shape{sequence_length, n_batch, n_output}; + const float tolerance = 1e-5; + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data, tolerance)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(UnidirectionalSequenceLSTMTest, FloatTest_batch) +{ + const int32_t n_batch = 1; + const int32_t n_input = 2; + const int32_t n_cell = 4; + const int32_t n_output = 4; + const int32_t sequence_length = 3; + + std::vector input_to_input_weights = {-0.45018822, -0.02338299, -0.0870589, -0.34550029, + 0.04266912, -0.15680569, -0.34856534, 0.43890524}; + + std::vector input_to_cell_weights = {-0.50013041, 0.1370284, 0.11810488, 0.2013163, + -0.20583314, 0.44344562, 0.22077113, -0.29909778}; + + std::vector input_to_forget_weights = {0.09701663, 0.20334584, -0.50592935, -0.31343272, + -0.40032279, 0.44781327, 0.01387155, -0.35593212}; + + std::vector input_to_output_weights = {-0.25065863, -0.28290087, 0.04613829, 0.40525138, + 0.44272184, 0.03897077, -0.1556896, 0.19487578}; + + std::vector input_gate_bias = {0., 0., 0., 0.}; + std::vector forget_gate_bias = {1., 1., 1., 1.}; + std::vector cell_gate_bias = {0., 0., 0., 0.}; + std::vector output_gate_bias = {0., 0., 0., 0.}; + + std::vector recurrent_to_input_weights = { + -0.0063535, -0.2042388, 0.31454784, -0.35746509, 0.28902304, 0.08183324, + -0.16555229, 0.02286911, -0.13566875, 0.03034258, 0.48091322, -0.12528998, + 0.24077177, -0.51332325, -0.33502164, 0.10629296}; + + std::vector recurrent_to_forget_weights = { + -0.48684245, -0.06655136, 0.42224967, 0.2112639, 0.27654213, 0.20864892, + -0.07646349, 0.45877004, 0.00141793, -0.14609534, 0.36447752, 0.09196436, + 0.28053468, 0.01560611, -0.20127171, -0.01140004}; + + std::vector recurrent_to_cell_weights = { + -0.3407414, 0.24443203, -0.2078532, 0.26320225, 0.05695659, -0.00123841, + -0.4744786, -0.35869038, -0.06418842, -0.13502428, -0.501764, 0.22830659, + -0.46367589, 0.26016325, -0.03894562, -0.16368064}; + + std::vector recurrent_to_output_weights = { + 0.43385774, -0.17194885, 0.2718237, 0.09215671, 0.24107647, -0.39835793, + 0.18212086, 0.01301402, 0.48572797, -0.50656658, 0.20047462, -0.20607421, + -0.51818722, -0.15390486, 0.0468148, 0.39922136}; + + Shape input_to_input_weights_shape{n_cell, n_input}; + Shape input_to_cell_weights_shape{n_cell, n_input}; + Shape input_to_forget_weights_shape{n_cell, n_input}; + Shape input_to_output_weights_shape{n_cell, n_input}; + + Shape input_gate_bias_shape{n_cell}; + Shape forget_gate_bias_shape{n_cell}; + Shape cell_gate_bias_shape{n_cell}; + Shape output_gate_bias_shape{n_cell}; + + Shape recurrent_to_input_weights_shape{n_cell, n_output}; + Shape recurrent_to_cell_weights_shape{n_cell, n_output}; + Shape recurrent_to_forget_weights_shape{n_cell, n_output}; + Shape recurrent_to_output_weights_shape{n_cell, n_output}; + + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + Tensor input_to_cell_weights_tensor = makeInputTensor( + input_to_cell_weights_shape, input_to_cell_weights, _memory_manager.get()); + Tensor input_to_forget_weights_tensor = makeInputTensor( + input_to_forget_weights_shape, input_to_forget_weights, _memory_manager.get()); + Tensor input_to_output_weights_tensor = makeInputTensor( + input_to_output_weights_shape, input_to_output_weights, _memory_manager.get()); + + Tensor input_gate_bias_tensor = makeInputTensor( + input_gate_bias_shape, input_gate_bias, _memory_manager.get()); + Tensor forget_gate_bias_tensor = makeInputTensor( + forget_gate_bias_shape, forget_gate_bias, _memory_manager.get()); + Tensor cell_gate_bias_tensor = + makeInputTensor(cell_gate_bias_shape, cell_gate_bias, _memory_manager.get()); + Tensor output_gate_bias_tensor = makeInputTensor( + output_gate_bias_shape, output_gate_bias, _memory_manager.get()); + + Tensor recurrent_to_input_weights_tensor = makeInputTensor( + recurrent_to_input_weights_shape, recurrent_to_input_weights, _memory_manager.get()); + Tensor recurrent_to_cell_weights_tensor = makeInputTensor( + recurrent_to_cell_weights_shape, recurrent_to_cell_weights, _memory_manager.get()); + Tensor recurrent_to_forget_weights_tensor = makeInputTensor( + recurrent_to_forget_weights_shape, recurrent_to_forget_weights, _memory_manager.get()); + Tensor recurrent_to_output_weights_tensor = makeInputTensor( + recurrent_to_output_weights_shape, recurrent_to_output_weights, _memory_manager.get()); + + std::vector input_data{2., 3., 3., 4., 1., 1.}; + Shape input_shape{n_batch, sequence_length, n_input}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + Shape output_state_shape{n_batch, n_output}; + Tensor output_state_tensor = makeOutputTensor(DataType::FLOAT32); + output_state_tensor.resize(output_state_shape); + + Shape cell_state_shape{n_batch, n_cell}; + Tensor cell_state_tensor = makeOutputTensor(DataType::FLOAT32); + cell_state_tensor.resize(cell_state_shape); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 0.0; + params.proj_clip = 0.0; + params.time_major = false; + params.asymmetric_quantize_inputs = false; + + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_forget_weights_tensor, + &input_to_cell_weights_tensor, &input_to_output_weights_tensor, + &recurrent_to_input_weights_tensor, &recurrent_to_forget_weights_tensor, + &recurrent_to_cell_weights_tensor, &recurrent_to_output_weights_tensor, nullptr, nullptr, + nullptr, &input_gate_bias_tensor, &forget_gate_bias_tensor, &cell_gate_bias_tensor, + &output_gate_bias_tensor, nullptr, nullptr, &output_state_tensor, &cell_state_tensor, nullptr, + nullptr, nullptr, nullptr, &output_tensor, &output_state_tensor, &cell_state_tensor, + &scratchpad_1, params); + + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(output_state_tensor); + _memory_manager->allocate_memory(cell_state_tensor); + _memory_manager->allocate_memory(scratchpad_1); + kernel.execute(); + + std::vector ref_output_data{-0.02973187, 0.1229473, 0.20885126, -0.15358765, + -0.03716109, 0.12507336, 0.41193449, -0.20860538, + -0.15053082, 0.09120187, 0.24278517, -0.12222792}; + + std::vector ref_output_shape{n_batch, sequence_length, n_output}; + const float tolerance = 1e-5; + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data, tolerance)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(UnidirectionalSequenceLSTMTest, FloatTest_simple) +{ + const int32_t n_batch = 1; + const int32_t n_input = 1; + const int32_t n_cell = 1; + const int32_t n_output = 1; + const int32_t sequence_length = 1; + + std::vector input_to_input_weights = {0.329067}; + std::vector input_to_forget_weights = {0.308059}; + std::vector input_to_cell_weights = {0.152916}; + std::vector input_to_output_weights = {-0.476033}; + + std::vector input_gate_bias = {0.}; + std::vector forget_gate_bias = {1.}; + std::vector cell_gate_bias = {0.}; + std::vector output_gate_bias = {0.}; + + std::vector recurrent_to_input_weights = {0.207806}; + std::vector recurrent_to_forget_weights = {0.028718}; + std::vector recurrent_to_cell_weights = {-0.182756}; + std::vector recurrent_to_output_weights = {-0.960517}; + + Shape input_to_input_weights_shape{n_cell, n_input}; + Shape input_to_cell_weights_shape{n_cell, n_input}; + Shape input_to_forget_weights_shape{n_cell, n_input}; + Shape input_to_output_weights_shape{n_cell, n_input}; + + Shape input_gate_bias_shape{n_cell}; + Shape forget_gate_bias_shape{n_cell}; + Shape cell_gate_bias_shape{n_cell}; + Shape output_gate_bias_shape{n_cell}; + + Shape recurrent_to_input_weights_shape{n_cell, n_output}; + Shape recurrent_to_cell_weights_shape{n_cell, n_output}; + Shape recurrent_to_forget_weights_shape{n_cell, n_output}; + Shape recurrent_to_output_weights_shape{n_cell, n_output}; + + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + Tensor input_to_cell_weights_tensor = makeInputTensor( + input_to_cell_weights_shape, input_to_cell_weights, _memory_manager.get()); + Tensor input_to_forget_weights_tensor = makeInputTensor( + input_to_forget_weights_shape, input_to_forget_weights, _memory_manager.get()); + Tensor input_to_output_weights_tensor = makeInputTensor( + input_to_output_weights_shape, input_to_output_weights, _memory_manager.get()); + + Tensor input_gate_bias_tensor = makeInputTensor( + input_gate_bias_shape, input_gate_bias, _memory_manager.get()); + Tensor forget_gate_bias_tensor = makeInputTensor( + forget_gate_bias_shape, forget_gate_bias, _memory_manager.get()); + Tensor cell_gate_bias_tensor = + makeInputTensor(cell_gate_bias_shape, cell_gate_bias, _memory_manager.get()); + Tensor output_gate_bias_tensor = makeInputTensor( + output_gate_bias_shape, output_gate_bias, _memory_manager.get()); + + Tensor recurrent_to_input_weights_tensor = makeInputTensor( + recurrent_to_input_weights_shape, recurrent_to_input_weights, _memory_manager.get()); + Tensor recurrent_to_cell_weights_tensor = makeInputTensor( + recurrent_to_cell_weights_shape, recurrent_to_cell_weights, _memory_manager.get()); + Tensor recurrent_to_forget_weights_tensor = makeInputTensor( + recurrent_to_forget_weights_shape, recurrent_to_forget_weights, _memory_manager.get()); + Tensor recurrent_to_output_weights_tensor = makeInputTensor( + recurrent_to_output_weights_shape, recurrent_to_output_weights, _memory_manager.get()); + + std::vector input_data{0.03653763}; + Shape input_shape{n_batch, sequence_length, n_input}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + Shape output_state_shape{n_batch, n_output}; + Tensor output_state_tensor = makeOutputTensor(DataType::FLOAT32); + output_state_tensor.resize(output_state_shape); + + Shape cell_state_shape{n_batch, n_cell}; + Tensor cell_state_tensor = makeOutputTensor(DataType::FLOAT32); + cell_state_tensor.resize(cell_state_shape); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 10.0; + params.proj_clip = 0.0; + params.time_major = false; + params.asymmetric_quantize_inputs = false; + + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_forget_weights_tensor, + &input_to_cell_weights_tensor, &input_to_output_weights_tensor, + &recurrent_to_input_weights_tensor, &recurrent_to_forget_weights_tensor, + &recurrent_to_cell_weights_tensor, &recurrent_to_output_weights_tensor, nullptr, nullptr, + nullptr, &input_gate_bias_tensor, &forget_gate_bias_tensor, &cell_gate_bias_tensor, + &output_gate_bias_tensor, nullptr, nullptr, &output_state_tensor, &cell_state_tensor, nullptr, + nullptr, nullptr, nullptr, &output_tensor, &output_state_tensor, &cell_state_tensor, + &scratchpad_1, params); + + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(output_state_tensor); + _memory_manager->allocate_memory(cell_state_tensor); + _memory_manager->allocate_memory(scratchpad_1); + kernel.execute(); + + std::vector ref_output_data{0.00139296}; + std::vector ref_output_shape{n_batch, sequence_length, n_output}; + const float tolerance = 1e-5; + auto aa = extractTensorData(output_tensor); + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data, tolerance)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(UnidirectionalSequenceLSTMTest, Unsupported_Type_Configure_NEG) +{ + const int32_t n_batch = 1; + const int32_t n_input = 2; + const int32_t n_cell = 4; + const int32_t n_output = 4; + const int32_t sequence_length = 3; + + std::vector input_data{2, 3, 3, 4, 1, 1}; // int8 is not support as of now + Shape input_shape{sequence_length, n_batch, n_input}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + std::vector input_to_input_weights = {-0.45018822, -0.02338299, -0.0870589, -0.34550029, + 0.04266912, -0.15680569, -0.34856534, 0.43890524}; + Shape input_to_input_weights_shape{n_cell, n_input}; + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_2(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_3(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 0.0; + params.proj_clip = 0.0; + params.time_major = true; + params.asymmetric_quantize_inputs = false; + + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + nullptr, nullptr, nullptr, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, nullptr, + nullptr, &output_tensor, &scratchpad_1, &scratchpad_2, &scratchpad_3, params); + + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(UnidirectionalSequenceLSTMTest, Invalid_Input_Shape_NEG) +{ + const int32_t n_batch = 1; + const int32_t n_input = 2; + const int32_t n_cell = 4; + const int32_t n_output = 4; + const int32_t sequence_length = 3; + + std::vector input_data{2., 3., 3., 4., 1., 1.}; + Shape input_shape{sequence_length, n_input}; // this is wrong + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + std::vector input_to_input_weights = {-0.45018822, -0.02338299, -0.0870589, -0.34550029, + 0.04266912, -0.15680569, -0.34856534, 0.43890524}; + Shape input_to_input_weights_shape{n_cell, n_input}; + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_2(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_3(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 0.0; + params.proj_clip = 0.0; + params.time_major = true; + params.asymmetric_quantize_inputs = false; + + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + nullptr, nullptr, nullptr, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, nullptr, + nullptr, &output_tensor, &scratchpad_1, &scratchpad_2, &scratchpad_3, params); + + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(UnidirectionalSequenceLSTMTest, Invalid_Input_Shape_2_NEG) +{ + const int32_t n_batch = 1; + const int32_t n_input = 2; + const int32_t n_cell = 4; + const int32_t n_output = 4; + const int32_t sequence_length = 3; + + std::vector input_data{2., 3., 3., 4., 1., 1.}; + Shape input_shape{sequence_length, n_batch, n_input}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + std::vector input_to_input_weights = {-0.45018822, -0.02338299, -0.0870589, -0.34550029, + 0.04266912, -0.15680569, -0.34856534, 0.43890524}; + Shape input_to_input_weights_shape{n_cell, n_input}; + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_2(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_3(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 0.0; + params.proj_clip = 0.0; + params.time_major = true; + params.asymmetric_quantize_inputs = false; + + // NOTE provide wrong shaped inputs + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + nullptr, nullptr, nullptr, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, nullptr, + nullptr, &output_tensor, &scratchpad_1, &scratchpad_2, &scratchpad_3, params); + + EXPECT_ANY_THROW(kernel.configure()); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-interpreter/src/kernels/Utils.cpp b/compiler/luci-interpreter/src/kernels/Utils.cpp index 5d8e5db..a04dbcc 100644 --- a/compiler/luci-interpreter/src/kernels/Utils.cpp +++ b/compiler/luci-interpreter/src/kernels/Utils.cpp @@ -27,6 +27,27 @@ namespace luci_interpreter namespace kernels { +TfLiteFusedActivation getTfLiteActivation(Activation activation) +{ + switch (activation) + { + case luci::FusedActFunc::RELU: + return kTfLiteActRelu; + case luci::FusedActFunc::RELU6: + return kTfLiteActRelu6; + case luci::FusedActFunc::RELU_N1_TO_1: + return kTfLiteActReluN1To1; + case luci::FusedActFunc::TANH: + return kTfLiteActTanh; + case luci::FusedActFunc::SIGN_BIT: + return kTfLiteActSignBit; + case luci::FusedActFunc::NONE: + return kTfLiteActNone; + default: + throw std::runtime_error("Unsupported activation type"); + } +} + template void calculateActivationRange(Activation activation, T *activation_min, T *activation_max) { diff --git a/compiler/luci-interpreter/src/kernels/Utils.h b/compiler/luci-interpreter/src/kernels/Utils.h index ebeb20e..e975585 100644 --- a/compiler/luci-interpreter/src/kernels/Utils.h +++ b/compiler/luci-interpreter/src/kernels/Utils.h @@ -21,6 +21,7 @@ #include "core/KernelParams.h" #include "luci_interpreter/core/Tensor.h" +#include #include #include @@ -76,6 +77,8 @@ inline int32_t calcOffset(const Shape &shape, int32_t d0, int32_t d1, int32_t d2 return ((d0 * shape.dim(1) + d1) * shape.dim(2) + d2) * shape.dim(3) + d3; } +TfLiteFusedActivation getTfLiteActivation(Activation activation); + template void calculateActivationRange(Activation activation, T *activation_min, T *activation_max); diff --git a/compiler/luci-interpreter/src/loader/GraphLoader.cpp b/compiler/luci-interpreter/src/loader/GraphLoader.cpp index 4020709..ba99a57 100644 --- a/compiler/luci-interpreter/src/loader/GraphLoader.cpp +++ b/compiler/luci-interpreter/src/loader/GraphLoader.cpp @@ -187,7 +187,11 @@ void GraphLoader::loadTensors() const auto *node = loco::must_cast(_graph->nodes()->at(i)); if (node->opcode() == luci::CircleOpcode::CUSTOM && !isSupportedCustomNode(node)) - throw std::runtime_error("Unsupported Custom operator. " + node->name()); + { + const auto *cnode = loco::must_cast(node); + throw std::runtime_error("Unsupported Custom operator. " + cnode->custom_code() + " in " + + node->name()); + } if (!isTensorProducingNode(node)) continue; diff --git a/compiler/luci-interpreter/src/loader/KernelBuilder.cpp b/compiler/luci-interpreter/src/loader/KernelBuilder.cpp index 8483a9a..c1e2c63 100644 --- a/compiler/luci-interpreter/src/loader/KernelBuilder.cpp +++ b/compiler/luci-interpreter/src/loader/KernelBuilder.cpp @@ -17,8 +17,36 @@ #include "loader/KernelBuilder.h" #include "loader/nodes/Builders.h" +#include +#include + #include +namespace +{ + +// TODO Extract this helper function +const std::string toString(luci::CircleOpcode opcode) +{ + static const char *names[] = { +#define CIRCLE_NODE(OPCODE, CIRCLE_CLASS) #CIRCLE_CLASS, +#define CIRCLE_VNODE(OPCODE, CIRCLE_CLASS) #CIRCLE_CLASS, +#include +#undef CIRCLE_NODE +#undef CIRCLE_VNODE + }; + + auto const node_name = names[static_cast(opcode)]; + + assert(std::string(node_name).substr(0, 6) == "Circle"); // FIX_ME_UNLESS + + // Return substring of class name ("Circle" is sliced out) + // Ex: Return "Conv2D" for "CircleConv2D" node + return std::string(node_name).substr(6); +} + +} // namespace + namespace luci_interpreter { @@ -97,7 +125,7 @@ std::unique_ptr KernelBuilder::build(const luci::CircleNode *node) return specific_builder(node, *this); std::string msg = "Unsupported operator: "; - msg += std::to_string(static_cast(node->opcode())) + " " + std::string(node->name()); + msg += toString(node->opcode()) + " in " + std::string(node->name()); throw std::invalid_argument(msg.c_str()); } diff --git a/compiler/luci-interpreter/src/loader/nodes/Abs.cpp b/compiler/luci-interpreter/src/loader/nodes/Abs.cpp new file mode 100644 index 0000000..3947111 --- /dev/null +++ b/compiler/luci-interpreter/src/loader/nodes/Abs.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Abs.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleAbs(const luci::CircleNode *circle_node, + KernelBuilderHelper &helper) +{ + const auto *node = loco::must_cast(circle_node); + assert(node->arity() == 1); + + const Tensor *input = helper.getInputTensor(node->x()); + Tensor *output = helper.getOutputTensor(node); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/compiler/luci-interpreter/src/loader/nodes/ReduceProd.cpp b/compiler/luci-interpreter/src/loader/nodes/ReduceProd.cpp new file mode 100644 index 0000000..1610e20 --- /dev/null +++ b/compiler/luci-interpreter/src/loader/nodes/ReduceProd.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/ReduceProd.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleReduceProd(const luci::CircleNode *circle_node, + KernelBuilderHelper &helper) +{ + const auto *node = loco::must_cast(circle_node); + assert(node->arity() == 2); + + const Tensor *input = helper.getInputTensor(node->input()); + const Tensor *axes = helper.getInputTensor(node->reduction_indices()); + Tensor *output = helper.getOutputTensor(node); + + auto temp_index_unique = + std::make_unique(DataType::S32, Shape({}), AffineQuantization{}, ""); + temp_index_unique->set_observable(false); + temp_index_unique->set_data_buffer(nullptr); + Tensor *temp_index = + helper.getRuntimeGraph(node->graph())->addTensor(std::move(temp_index_unique)); + + auto resolved_axes_unique = + std::make_unique(DataType::S32, Shape({}), AffineQuantization{}, ""); + resolved_axes_unique->set_observable(false); + resolved_axes_unique->set_data_buffer(nullptr); + Tensor *resolved_axes = + helper.getRuntimeGraph(node->graph())->addTensor(std::move(resolved_axes_unique)); + + ReducerParams params{}; + params.keep_dims = node->keep_dims(); + + return std::make_unique(input, axes, output, temp_index, resolved_axes, + params); +} + +} // namespace luci_interpreter diff --git a/compiler/luci-interpreter/src/loader/nodes/UnidirectionalSequenceLSTM.cpp b/compiler/luci-interpreter/src/loader/nodes/UnidirectionalSequenceLSTM.cpp new file mode 100644 index 0000000..f4cf0b8 --- /dev/null +++ b/compiler/luci-interpreter/src/loader/nodes/UnidirectionalSequenceLSTM.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/UnidirectionalSequenceLSTM.h" + +namespace luci_interpreter +{ + +std::unique_ptr +build_kernel_CircleUnidirectionalSequenceLSTM(const luci::CircleNode *circle_node, + KernelBuilderHelper &helper) +{ + const auto *node = loco::must_cast(circle_node); + assert(node->arity() == 24); + + const Tensor *input = helper.getInputTensor(node->input()); + const Tensor *input_to_input_weights = + helper.getOptionalInputTensor(node->input_to_input_weights()); + const Tensor *input_to_cell_weights = helper.getInputTensor(node->input_to_cell_weights()); + const Tensor *input_to_forget_weights = helper.getInputTensor(node->input_to_forget_weights()); + const Tensor *input_to_output_weights = helper.getInputTensor(node->input_to_output_weights()); + const Tensor *recurrent_to_input_weights = + helper.getOptionalInputTensor(node->recurrent_to_input_weights()); + const Tensor *recurrent_to_cell_weights = + helper.getInputTensor(node->recurrent_to_cell_weights()); + const Tensor *recurrent_to_forget_weights = + helper.getInputTensor(node->recurrent_to_forget_weights()); + const Tensor *recurrent_to_output_weights = + helper.getInputTensor(node->recurrent_to_output_weights()); + const Tensor *cell_to_input_weights = + helper.getOptionalInputTensor(node->cell_to_input_weights()); + const Tensor *cell_to_forget_weights = + helper.getOptionalInputTensor(node->cell_to_forget_weights()); + const Tensor *cell_to_output_weights = + helper.getOptionalInputTensor(node->cell_to_output_weights()); + const Tensor *input_gate_bias = helper.getOptionalInputTensor(node->input_gate_bias()); + const Tensor *forget_gate_bias = helper.getInputTensor(node->forget_gate_bias()); + const Tensor *cell_gate_bias = helper.getInputTensor(node->cell_gate_bias()); + const Tensor *output_gate_bias = helper.getInputTensor(node->output_gate_bias()); + const Tensor *projection_weights = helper.getOptionalInputTensor(node->projection_weights()); + const Tensor *projection_bias = helper.getOptionalInputTensor(node->projection_bias()); + const Tensor *output_state = helper.getInputTensor(node->output_state()); + const Tensor *cell_state = helper.getInputTensor(node->cell_state()); + const Tensor *input_layer_norm_coefficients = + helper.getOptionalInputTensor(node->input_layer_norm_coefficients()); + const Tensor *forget_layer_norm_coefficients = + helper.getOptionalInputTensor(node->forget_layer_norm_coefficients()); + const Tensor *cell_layer_norm_coefficients = + helper.getOptionalInputTensor(node->cell_layer_norm_coefficients()); + const Tensor *output_layer_norm_coefficients = + helper.getOptionalInputTensor(node->output_layer_norm_coefficients()); + Tensor *output = helper.getOutputTensor(node); + + // scratch pad tensor + // NOTE provide more scratch pads if support hybrid or integer + auto sp_output_state = + std::make_unique(output_state->element_type(), Shape({}), AffineQuantization{}, ""); + sp_output_state->set_observable(false); + sp_output_state->set_data_buffer(nullptr); + Tensor *tmp_1 = helper.getRuntimeGraph(node->graph())->addTensor(std::move(sp_output_state)); + + auto sp_cell_state = + std::make_unique(cell_state->element_type(), Shape({}), AffineQuantization{}, ""); + sp_cell_state->set_observable(false); + sp_cell_state->set_data_buffer(nullptr); + Tensor *tmp_2 = helper.getRuntimeGraph(node->graph())->addTensor(std::move(sp_cell_state)); + + auto sp_3 = std::make_unique(input->element_type(), Shape({}), AffineQuantization{}, ""); + sp_3->set_observable(false); + sp_3->set_data_buffer(nullptr); + Tensor *tmp_3 = helper.getRuntimeGraph(node->graph())->addTensor(std::move(sp_3)); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = node->fusedActivationFunction(); + params.cell_clip = node->cell_clip(); + params.proj_clip = node->proj_clip(); + params.time_major = node->time_major(); + params.asymmetric_quantize_inputs = node->asymmetric_quantize_inputs(); + + return std::make_unique( + input, input_to_input_weights, input_to_forget_weights, input_to_cell_weights, + input_to_output_weights, recurrent_to_input_weights, recurrent_to_forget_weights, + recurrent_to_cell_weights, recurrent_to_output_weights, cell_to_input_weights, + cell_to_forget_weights, cell_to_output_weights, input_gate_bias, forget_gate_bias, + cell_gate_bias, output_gate_bias, projection_weights, projection_bias, output_state, cell_state, + input_layer_norm_coefficients, forget_layer_norm_coefficients, cell_layer_norm_coefficients, + output_layer_norm_coefficients, output, tmp_1, tmp_2, tmp_3, params); +} + +} // namespace luci_interpreter diff --git a/compiler/luci-micro/CMakeLists.txt b/compiler/luci-micro/CMakeLists.txt deleted file mode 100644 index 642cf14..0000000 --- a/compiler/luci-micro/CMakeLists.txt +++ /dev/null @@ -1,58 +0,0 @@ -set(ARM_C_COMPILER "arm-none-eabi-gcc") -set(ARM_ASM_COMPILER "arm-none-eabi-gcc") -set(ARM_CXX_COMPILER "arm-none-eabi-g++") -set(ARM_OBJCOPY "arm-none-eabi-objcopy") - -find_program(ARM_C_COMPILER_PATH ${ARM_C_COMPILER}) - -if(NOT ARM_C_COMPILER_PATH) - message(STATUS "Build luci-micro: FALSE(ARM compiler is NOT FOUND)") - return() -endif() - -set(CMAKE_ARM_OPTIONS - -DLUCI_INTERPRETER_STATIC=ON - -DLUCI_STATIC=ON - -DBUILD_CMSIS_NN_FUNCTIONS=ON - -DTARGET_CPU=cortex-m7 - "-DCMAKE_TOOLCHAIN_FILE=${NNAS_PROJECT_SOURCE_DIR}/infra/nncc/cmake/buildtool/config/arm-none-eabi-gcc.cmake" - "-DLUCI_INTERPRETER_PAL_DIR=${CMAKE_CURRENT_SOURCE_DIR}/../luci-interpreter/pal/mcu" - "-DNNAS_PROJECT_SOURCE_DIR=${NNAS_PROJECT_SOURCE_DIR}" - "-DNNAS_EXTERNALS_DIR=${NNAS_EXTERNALS_DIR}" - -DC_COMPILER=${ARM_C_COMPILER} - -DCXX_COMPILER=${ARM_CXX_COMPILER} - -DASM_COMPILER=${ARM_ASM_COMPILER} - -DOBJCOPY=${ARM_OBJCOPY} - -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} - -DENABLE_TEST=OFF - -DBUILD_GTEST=OFF - "-DNNAS_ROOT=${NNAS_PROJECT_SOURCE_DIR}" - -DENABLE_STRICT_BUILD=OFF -) - -set(MICRO_ARM_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/standalone_arm") -file(MAKE_DIRECTORY "${MICRO_ARM_BUILD_DIR}") - -set(MICRO_ARM_BUILD_DEPENDENCY "${MICRO_ARM_BUILD_DIR}/CMakeCache.txt") - -add_custom_command( - OUTPUT "${MICRO_ARM_BUILD_DEPENDENCY}" - COMMAND "${CMAKE_COMMAND}" "${CMAKE_CURRENT_SOURCE_DIR}/standalone" ${CMAKE_ARM_OPTIONS} - WORKING_DIRECTORY "${MICRO_ARM_BUILD_DIR}" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/standalone/CMakeLists.txt" - VERBATIM -) - -add_custom_target(luci_interpreter_micro_arm_cmake DEPENDS "${MICRO_ARM_BUILD_DEPENDENCY}") - -set(MICRO_ARM_BINARY "${MICRO_ARM_BUILD_DIR}/compiler/luci-interpreter/src/libluci_interpreter.a") - -add_custom_command( - OUTPUT "${MICRO_ARM_BINARY}" - COMMAND "${CMAKE_MAKE_PROGRAM}" luci_interpreter -j ${CPU_COUNT} - WORKING_DIRECTORY "${MICRO_ARM_BUILD_DIR}" - DEPENDS luci_interpreter_micro_arm_cmake - VERBATIM -) - -add_custom_target(luci_interpreter_micro_arm DEPENDS "${MICRO_ARM_BINARY}") diff --git a/compiler/luci-micro/luci-interpreter/CMakeLists.txt b/compiler/luci-micro/luci-interpreter/CMakeLists.txt deleted file mode 100644 index 1f7acee..0000000 --- a/compiler/luci-micro/luci-interpreter/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -set(LUCI_INTERPRETER_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include") -set(LUCI_INTERPRETER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src") -if (NOT LUCI_INTERPRETER_PAL_DIR) - set(LUCI_INTERPRETER_PAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/pal/linux") -endif() - -set(KERNEL_REGISTER_FILE ${LUCI_INTERPRETER_PAL_DIR}/KernelsToBuild.lst) - -if (NOT DEFINED CUSTOM_LUCI_INTERPRETER_SUFFIX) - set(LUCI_INTERPRETER_SUFFIX "") -else() - set(LUCI_INTERPRETER_SUFFIX ${CUSTOM_LUCI_INTERPRETER_SUFFIX}) -endif() - -add_subdirectory(src) diff --git a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/BuddyMemoryManager.h b/compiler/luci-micro/luci-interpreter/include/luci_interpreter/BuddyMemoryManager.h deleted file mode 100644 index 205baa6..0000000 --- a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/BuddyMemoryManager.h +++ /dev/null @@ -1,144 +0,0 @@ -/* Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "luci_interpreter/MemoryManager.h" - -#ifndef LUCI_INTERPRETER_BUDDY_MEMORY_MANAGER_H -#define LUCI_INTERPRETER_BUDDY_MEMORY_MANAGER_H - -namespace luci_interpreter -{ - -class BuddyMemoryManager : public IMemoryManager -{ -public: - BuddyMemoryManager(uint8_t *memory_start, int32_t memSize); - - void allocate_memory(luci_interpreter::Tensor &tensor) final; - void release_memory(luci_interpreter::Tensor &tensor) final; - -private: - struct Block - { - Block *next_free; - bool is_free; - uint32_t size; - // debug field - Block *self; - }; - - Block *_start_block; - int32_t _num_blocks; - uint32_t _size; - Block *_free_blocks[32]{}; - - static int32_t lowerLog2(uint32_t val) - { - int32_t i = 0; - while (val >>= 1) - i++; - - return i; - } - - void addToBlocks(Block *block, int32_t l) - { - if (!block) - return; - - block->next_free = _free_blocks[l]; - _free_blocks[l] = block; - } - - void removeFromBlocks(const Block *block, int32_t l) - { - if (!block) - return; - - Block *tmp = _free_blocks[l]; - - if (block == tmp) - { - _free_blocks[l] = block->next_free; - return; - } - - while (tmp) - { - if (tmp->next_free == block) - { - tmp->next_free = block->next_free; - return; - } - - tmp = tmp->next_free; - } - } - - void divideBlock(Block *block, int32_t l) - { - int32_t size = ((block->size + sizeof(Block)) / 2) - sizeof(Block); - - removeFromBlocks(block, l); - - // there is no need to add to the free_blocks list here - block->is_free = true; - block->size = size; - block->self = block; - - Block *buddy; - buddy = (Block *)((uint8_t *)block + sizeof(Block) + size); - buddy->is_free = true; - buddy->size = size; - buddy->self = buddy; - - addToBlocks(buddy, l - 1); - } - - Block *mergeBlock(Block *block) - { - Block *buddy; - - const int32_t l = lowerLog2(block->size + sizeof(Block)); - - const int64_t address = ((uint8_t *)block - (uint8_t *)_start_block); - buddy = (Block *)((address ^ (1 << l)) + (uint8_t *)_start_block); - - if (!buddy->is_free || buddy->size != block->size) - return nullptr; - - if (block > buddy) - { - Block *x = block; - block = buddy; - buddy = x; - } - - removeFromBlocks(block, l); - removeFromBlocks(buddy, l); - - block->size = block->size * 2 + sizeof(Block); - block->is_free = true; - block->self = block; - - addToBlocks(block, l + 1); - - return block; - } -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_BUDDY_MEMORY_MANAGER_H diff --git a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/GraphBuilderRegistry.h b/compiler/luci-micro/luci-interpreter/include/luci_interpreter/GraphBuilderRegistry.h deleted file mode 100644 index 375b1ae..0000000 --- a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/GraphBuilderRegistry.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __LUCI_INTERPRETER_GRAPH_BUILDER_REGISTRY__ -#define __LUCI_INTERPRETER_GRAPH_BUILDER_REGISTRY__ - -#include - -namespace luci_interpreter -{ - -/** - * @brief Creates and returns GraphBuilderSource, which allows to not copy constant buffers from - * model's file. - * - * @warning Use this source only in case when model's buffer alive longer than Interpreter. - */ -std::unique_ptr source_without_constant_copying(); - -} // namespace luci_interpreter - -#endif // __LUCI_INTERPRETER_GRAPH_BUILDER_REGISTRY__ diff --git a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/Interpreter.h b/compiler/luci-micro/luci-interpreter/include/luci_interpreter/Interpreter.h deleted file mode 100644 index 8e2f457..0000000 --- a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/Interpreter.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_INTERPRETER_H -#define LUCI_INTERPRETER_INTERPRETER_H - -#include "luci_interpreter/core/Tensor.h" - -#include -#include - -#include "luci_interpreter/MemoryManager.h" -#include - -#include -#include -#include - -namespace luci_interpreter -{ - -class ExecutionObserver -{ -public: - virtual ~ExecutionObserver(); - - // Called when the value of a tensor has been updated during execution. - virtual void postTensorWrite(const luci::CircleNode *node, const Tensor *tensor); - - // Called before / after executing an operator. - // Note that these methods are not called for auxiliary operators (CircleInput, CircleOutput, - // CircleConst and Circle*Out). - virtual void preOperatorExecute(const luci::CircleNode *node); - virtual void postOperatorExecute(const luci::CircleNode *node); -}; - -class Interpreter -{ -public: - explicit Interpreter(const luci::Module *module); - - explicit Interpreter(const luci::Module *module, IMemoryManager *memory_manager); - - ~Interpreter(); - - void writeInputTensor(const luci::CircleInput *input_node, const void *data, size_t data_size); - - void readOutputTensor(const luci::CircleOutput *output_node, void *data, size_t data_size); - - void interpret(); - - void attachObserver(ExecutionObserver *observer); - - const Tensor *getTensor(const loco::Node *node) { return _node_to_tensor[node]; } - -private: - // _default_memory_manager should be before _runtime_module due to - // the order of deletion in the destructor - std::unique_ptr _default_memory_manager = nullptr; - std::unique_ptr _runtime_module; - - // Observer functionality support. - std::unique_ptr _runtime_to_ir; - std::unordered_map _node_to_tensor; - std::unique_ptr _event_notifier; - std::vector _observers; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_INTERPRETER_H diff --git a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/MemoryManager.h b/compiler/luci-micro/luci-interpreter/include/luci_interpreter/MemoryManager.h deleted file mode 100644 index f32c520..0000000 --- a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/MemoryManager.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_MEMORY_MANAGER_H -#define LUCI_INTERPRETER_MEMORY_MANAGER_H - -#include "luci_interpreter/core/DataType.h" -#include "luci_interpreter/core/Tensor.h" - -namespace luci_interpreter -{ - -class IMemoryManager -{ -public: - virtual void allocate_memory(luci_interpreter::Tensor &tensor) = 0; - virtual void release_memory(luci_interpreter::Tensor &tensor) = 0; - - virtual ~IMemoryManager() = default; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_MEMORY_MANAGER_H diff --git a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/SimpleMemoryManager.h b/compiler/luci-micro/luci-interpreter/include/luci_interpreter/SimpleMemoryManager.h deleted file mode 100644 index 658a1c6..0000000 --- a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/SimpleMemoryManager.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_SIMPLE_MEMORY_MANAGER_H -#define LUCI_INTERPRETER_SIMPLE_MEMORY_MANAGER_H - -#include "luci_interpreter/MemoryManager.h" - -namespace luci_interpreter -{ - -class SimpleMemoryManager : public IMemoryManager -{ -public: - void allocate_memory(luci_interpreter::Tensor &tensor) final; - void release_memory(luci_interpreter::Tensor &tensor) final; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_SIMPLE_MEMORY_MANAGER_H diff --git a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/StaticMemoryManager.h b/compiler/luci-micro/luci-interpreter/include/luci_interpreter/StaticMemoryManager.h deleted file mode 100644 index ded7bde..0000000 --- a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/StaticMemoryManager.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_STATIC_MEMORY_MANAGER_H -#define LUCI_INTERPRETER_STATIC_MEMORY_MANAGER_H - -#include "luci_interpreter/MemoryManager.h" - -namespace luci_interpreter -{ - -// Used for allocations in static buffer, using offsets defined in luci model. -class StaticMemoryManager : public IMemoryManager -{ -public: - StaticMemoryManager() = delete; - - explicit StaticMemoryManager(uint8_t *buffer_ptr) : _buffer_ptr(buffer_ptr) - { /* Do nothing */ - } - - void allocate_memory(luci_interpreter::Tensor &tensor) final; - void release_memory(luci_interpreter::Tensor &tensor) final; - -private: - // Stores a pointer to the beginning of the allocated memory buffer. - uint8_t *_buffer_ptr; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_STATIC_MEMORY_MANAGER_H diff --git a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/TestMemoryManager.h b/compiler/luci-micro/luci-interpreter/include/luci_interpreter/TestMemoryManager.h deleted file mode 100644 index 397bbed..0000000 --- a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/TestMemoryManager.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_TEST_MEMORY_MANAGER_H -#define LUCI_INTERPRETER_TEST_MEMORY_MANAGER_H - -#include "luci_interpreter/MemoryManager.h" - -namespace luci_interpreter -{ -// Memory Manager for using in kernels tests. This eliminates the need to manually delete the -// allocated memory in tests. This mem_manager remembers all its allocations and in destructor -// delete all allocations. -class TestMemoryManager : public IMemoryManager -{ -public: - void allocate_memory(luci_interpreter::Tensor &tensor) final; - void release_memory(luci_interpreter::Tensor &tensor) final; - - ~TestMemoryManager() override - { - for (auto allocation : allocations) - { - delete[] allocation; - } - } - -private: - std::vector allocations; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_TEST_MEMORY_MANAGER_H diff --git a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/core/DataType.h b/compiler/luci-micro/luci-interpreter/include/luci_interpreter/core/DataType.h deleted file mode 100644 index 27bf719..0000000 --- a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/core/DataType.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_CORE_DATATYPE_H -#define LUCI_INTERPRETER_CORE_DATATYPE_H - -#include -#include - -#include - -namespace luci_interpreter -{ - -using DataType = loco::DataType; - -template using DataTypeImpl = loco::DataTypeImpl
; - -inline size_t getDataTypeSize(DataType data_type) { return loco::size(data_type); } - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_CORE_DATATYPE_H diff --git a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/core/Tensor.h b/compiler/luci-micro/luci-interpreter/include/luci_interpreter/core/Tensor.h deleted file mode 100644 index bb9ff6d..0000000 --- a/compiler/luci-micro/luci-interpreter/include/luci_interpreter/core/Tensor.h +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_CORE_TENSOR_H -#define LUCI_INTERPRETER_CORE_TENSOR_H - -#include "luci_interpreter/core/DataType.h" - -#include -#include -#include -#include -#include -#include - -namespace luci_interpreter -{ - -class Shape -{ -public: - explicit Shape(int rank) : _dims(rank, 0) {} - - Shape(std::initializer_list dims) : _dims(dims.begin(), dims.end()) {} - - int num_dims() const { return _dims.size(); } - - int32_t dim(int i) const - { - assert(i >= 0 && i < static_cast(_dims.size())); - return _dims[i]; - } - - int32_t &dim(int i) - { - assert(i >= 0 && i < static_cast(_dims.size())); - return _dims[i]; - } - - int32_t num_elements() const - { - int32_t result = 1; - for (const int32_t dim : _dims) - { - result *= dim; - } - return result; - } - - bool operator==(const Shape &other) const { return _dims == other._dims; } - - bool operator!=(const Shape &other) const { return !operator==(other); } - -private: - std::vector _dims; -}; - -// Tensor affine quantization parameters. -// -// The relationship between real and quantized values: -// real_value = (quantized_value - zero_point) * scale -// -// In per-tensor case, 'scale' and 'zero_point' are one element each. -// In per-channel case, 'scale' and 'zero_point' are N elements each, where N is the size -// of the quantized dimension. -// -// Note that due to historical and performance reasons, per-tensor quantization uses unsigned -// integer types, while per-channel uses signed types assuming 'zero_point' == 0. -struct AffineQuantization -{ - std::vector scale; - std::vector zero_point; - int32_t quantized_dimension; -}; - -class Tensor -{ -public: - Tensor(DataType element_type, Shape shape, AffineQuantization quantization, std::string name); - - DataType element_type() const { return _element_type; } - - const Shape &shape() const { return _shape; } - - float scale() const - { - assert(_quantization.scale.size() == 1); - return _quantization.scale[0]; - } - - int32_t zero_point() const - { - assert(_quantization.zero_point.size() == 1); - return _quantization.zero_point[0]; - } - - const std::vector &scales() const { return _quantization.scale; } - - const std::vector &zero_points() const { return _quantization.zero_point; } - - int32_t quantized_dimension() const { return _quantization.quantized_dimension; } - - template const T *data() const - { - static_assert(std::is_same::value or - std::is_same::value); - return reinterpret_cast(_data); - } - - template T *data() - { - static_assert(std::is_same::value or - std::is_same::value); - return reinterpret_cast(_data); - } - - const std::string &name() const { return _name; } - - void readData(void *data_ptr, size_t data_size) const; - - void writeData(const void *data_ptr, size_t data_size); - - void resize(const Shape &new_shape); - - void set_data_buffer(uint8_t *buffer) - { - if (buffer == nullptr) - { - _data_allocated = false; - } - else - { - _data_allocated = true; - } - _data = buffer; - } - - bool is_observable() const { return _is_observable; } - - void set_observable(bool value) { _is_observable = value; } - - bool is_allocatable() const { return _is_allocatable; } - - void set_allocatable(bool value) { _is_allocatable = value; } - - bool is_data_allocated() const { return _data_allocated; } - - int32_t get_offset() const { return _offset; } - - void set_offset(int32_t offset) { _offset = offset; } - -private: - DataType _element_type; - Shape _shape; - AffineQuantization _quantization; - uint8_t *_data; - std::string _name; - bool _data_allocated; - // Write of tensor is reported to registered Observers only if this tensor is observable - // This is needed for tensors used in kernel implementation, but not present in original model. - bool _is_observable = true; - // Memory manager is called for tensor only if it is "allocatable". - // Kernel configuration could disable allocation of some tensors if they are not needed for - // particular operation. - bool _is_allocatable = true; - // Used by static memory manager. - // Stores the offset from the beginning of the allocated memory buffer. - int32_t _offset = -1; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_CORE_TENSOR_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst b/compiler/luci-micro/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst deleted file mode 100644 index f0df58d..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst +++ /dev/null @@ -1,62 +0,0 @@ -REGISTER_KERNEL(Add) -REGISTER_KERNEL(ArgMax) -REGISTER_KERNEL(AveragePool2D) -REGISTER_KERNEL(BatchToSpaceND) -REGISTER_KERNEL(Cast) -REGISTER_KERNEL(Concatenation) -REGISTER_KERNEL(Conv2D) -REGISTER_KERNEL(DepthToSpace) -REGISTER_KERNEL(DepthwiseConv2D) -REGISTER_KERNEL(Dequantize) -REGISTER_KERNEL(Div) -REGISTER_KERNEL(Elu) -REGISTER_KERNEL(Exp) -REGISTER_KERNEL(ExpandDims) -REGISTER_KERNEL(Fill) -REGISTER_KERNEL(Floor) -REGISTER_KERNEL(FloorDiv) -REGISTER_KERNEL(Equal) -REGISTER_KERNEL(FullyConnected) -REGISTER_KERNEL(Greater) -REGISTER_KERNEL(GreaterEqual) -REGISTER_KERNEL(If) -REGISTER_KERNEL(InstanceNorm) -REGISTER_KERNEL(L2Normalize) -REGISTER_KERNEL(L2Pool2D) -REGISTER_KERNEL(LeakyRelu) -REGISTER_KERNEL(Less) -REGISTER_KERNEL(LessEqual) -REGISTER_KERNEL(LogicalAnd) -REGISTER_KERNEL(LogicalNot) -REGISTER_KERNEL(LogicalOr) -REGISTER_KERNEL(Logistic) -REGISTER_KERNEL(Maximum) -REGISTER_KERNEL(MaxPool2D) -REGISTER_KERNEL(Minimum) -REGISTER_KERNEL(MirrorPad) -REGISTER_KERNEL(Mul) -REGISTER_KERNEL(Neg) -REGISTER_KERNEL(NotEqual) -REGISTER_KERNEL(Pad) -REGISTER_KERNEL(PadV2) -REGISTER_KERNEL(PRelu) -REGISTER_KERNEL(Quantize) -REGISTER_KERNEL(Reshape) -REGISTER_KERNEL(ResizeBilinear) -REGISTER_KERNEL(ResizeNearestNeighbor) -REGISTER_KERNEL(Rsqrt) -REGISTER_KERNEL(Shape) -REGISTER_KERNEL(Softmax) -REGISTER_KERNEL(SpaceToBatchND) -REGISTER_KERNEL(SpaceToDepth) -REGISTER_KERNEL(StridedSlice) -REGISTER_KERNEL(Sqrt) -REGISTER_KERNEL(Square) -REGISTER_KERNEL(SquaredDifference) -REGISTER_KERNEL(Squeeze) -REGISTER_KERNEL(Sub) -REGISTER_KERNEL(SVDF) -REGISTER_KERNEL(Tanh) -REGISTER_KERNEL(Transpose) -REGISTER_KERNEL(TransposeConv) -REGISTER_KERNEL(While) diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h b/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h deleted file mode 100644 index 4dd77ff..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H -#define LUCI_INTERPRETER_PAL_ARGMAX_H - -#include - -namespace luci_interpreter_pal -{ -template -static inline void -BatchToSpaceND(const tflite::RuntimeShape &unextended_input1_shape, const T *input1_data, - const tflite::RuntimeShape &unextended_input2_shape, const int32 *block_shape_data, - const tflite::RuntimeShape &unextended_input3_shape, const int32 *crops_data, - const tflite::RuntimeShape &unextended_output_shape, T *output_data) -{ - tflite::reference_ops::BatchToSpaceND( - unextended_input1_shape, input1_data, unextended_input2_shape, block_shape_data, - unextended_input3_shape, crops_data, unextended_output_shape, output_data); -} -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALConv2d.h b/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALConv2d.h deleted file mode 100644 index cfb84ea..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALConv2d.h +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_CONV2D_H -#define LUCI_INTERPRETER_PAL_CONV2D_H - -#include -#include -#include -#include - -namespace luci_interpreter_pal -{ -static inline void Conv(const tflite::ConvParams ¶ms, const tflite::RuntimeShape &input_shape, - const float *input_data, const tflite::RuntimeShape &filter_shape, - const float *filter_data, const tflite::RuntimeShape &bias_shape, - const float *bias_data, const tflite::RuntimeShape &output_shape, - float *output_data, const tflite::RuntimeShape &scratchpad_shape, - float *scratchpad_data) -{ - (void)scratchpad_shape; - (void)scratchpad_data; - tflite::reference_ops::Conv(params, input_shape, input_data, filter_shape, filter_data, - bias_shape, bias_data, output_shape, output_data, - tflite::RuntimeShape(), nullptr); -} - -static inline void Conv(const tflite::ConvParams ¶ms, const tflite::RuntimeShape &input_shape, - const uint8 *input_data, const tflite::RuntimeShape &filter_shape, - const uint8 *filter_data, const tflite::RuntimeShape &bias_shape, - const int32 *bias_data, const tflite::RuntimeShape &output_shape, - uint8 *output_data, const tflite::RuntimeShape &scratchpad_shape, - uint8 *scratchpad_data) -{ - (void)scratchpad_shape; - (void)scratchpad_data; - tflite::reference_ops::Conv(params, input_shape, input_data, filter_shape, filter_data, - bias_shape, bias_data, output_shape, output_data, scratchpad_shape, - scratchpad_data, nullptr); -} - -static inline void ConvPerChannel(const tflite::ConvParams ¶ms, const int32_t *mult, - const int32_t *shifts, const tflite::RuntimeShape &input_shape, - const int8 *input_data, const tflite::RuntimeShape &filter_shape, - const int8 *filter_data, const tflite::RuntimeShape &bias_shape, - const int32 *bias_data, const tflite::RuntimeShape &output_shape, - int8 *output_data, const tflite::RuntimeShape &scratchpad_shape, - int8 *scratchpad_data) -{ - if (scratchpad_data) - { - cmsis_nn_conv_params conv_params; - conv_params.dilation.h = params.dilation_height_factor; - conv_params.dilation.w = params.dilation_width_factor; - - assert(conv_params.dilation.h == 1); - assert(conv_params.dilation.w == 1); - - conv_params.input_offset = params.input_offset; - conv_params.output_offset = params.output_offset; - conv_params.stride.h = params.stride_height; - conv_params.stride.w = params.stride_width; - conv_params.padding.h = params.padding_values.height; - conv_params.padding.w = params.padding_values.width; - conv_params.activation.min = params.quantized_activation_min; - conv_params.activation.max = params.quantized_activation_max; - - cmsis_nn_per_channel_quant_params quant_params; - quant_params.multiplier = const_cast(mult); - quant_params.shift = const_cast(shifts); - - assert(conv_params.activation.min <= conv_params.activation.max); - assert(input_shape.DimensionsCount() == 4); - assert(filter_shape.DimensionsCount() == 4); - assert(output_shape.DimensionsCount() == 4); - const int batch_size = tflite::MatchingDim(input_shape, 0, output_shape, 0); - const int input_depth = tflite::MatchingDim(input_shape, 3, filter_shape, 3); - const int output_depth = tflite::MatchingDim(filter_shape, 0, output_shape, 3); - if (bias_data) - { - assert(bias_shape.FlatSize() == output_depth); - } - - cmsis_nn_dims input_dims; - input_dims.n = batch_size; - input_dims.h = input_shape.Dims(1); - input_dims.w = input_shape.Dims(2); - input_dims.c = input_depth; - - cmsis_nn_dims filter_dims; - filter_dims.n = output_depth; - filter_dims.h = filter_shape.Dims(1); - filter_dims.w = filter_shape.Dims(2); - filter_dims.c = input_depth; - - cmsis_nn_dims bias_dims; - bias_dims.n = 1; - bias_dims.h = 1; - bias_dims.w = 1; - bias_dims.c = output_depth; - - cmsis_nn_dims output_dims; - output_dims.n = batch_size; - output_dims.h = output_shape.Dims(1); - output_dims.w = output_shape.Dims(2); - output_dims.c = output_depth; - - cmsis_nn_context ctx; - ctx.buf = scratchpad_data; - ctx.size = scratchpad_shape.Dims(0); - - auto res = arm_convolve_wrapper_s8(&ctx, &conv_params, &quant_params, &input_dims, input_data, - &filter_dims, filter_data, &bias_dims, bias_data, - &output_dims, output_data); - assert(res == ARM_MATH_SUCCESS); - } - else - { - tflite::reference_integer_ops::ConvPerChannel(params, mult, shifts, input_shape, input_data, - filter_shape, filter_data, bias_shape, bias_data, - output_shape, output_data); - } -} - -static inline void SetupScratchpadTensor(luci_interpreter::Tensor *scratchpad, - const luci_interpreter::DataType &input_data_type, - const tflite::ConvParams ¶ms, - const tflite::RuntimeShape &input_shape, - const tflite::RuntimeShape &filter_shape, - const tflite::RuntimeShape &output_shape) -{ - cmsis_nn_conv_params conv_params; - conv_params.dilation.h = params.dilation_height_factor; - conv_params.dilation.w = params.dilation_width_factor; - - if (input_data_type == loco::DataType::S8 && conv_params.dilation.h == 1 && - conv_params.dilation.w == 1) - { - const int32_t batches = tflite::MatchingDim(input_shape, 0, output_shape, 0); - const int32_t input_depth = tflite::MatchingDim(input_shape, 3, filter_shape, 3); - const int32_t output_depth = tflite::MatchingDim(filter_shape, 0, output_shape, 3); - const int32_t filter_height = filter_shape.Dims(1); - const int32_t filter_width = filter_shape.Dims(2); - const int32_t output_height = output_shape.Dims(1); - const int32_t output_width = output_shape.Dims(2); - - conv_params.input_offset = params.input_offset; - conv_params.output_offset = params.output_offset; - conv_params.stride.h = params.stride_height; - conv_params.stride.w = params.stride_width; - conv_params.padding.h = params.padding_values.height; - conv_params.padding.w = params.padding_values.width; - - cmsis_nn_dims input_dims; - input_dims.n = batches; - input_dims.h = input_shape.Dims(1); - input_dims.w = input_shape.Dims(2); - input_dims.c = input_depth; - - cmsis_nn_dims filter_dims; - filter_dims.n = output_depth; - filter_dims.h = filter_height; - filter_dims.w = filter_width; - filter_dims.c = input_depth; - - cmsis_nn_dims output_dims; - output_dims.n = batches; - output_dims.h = output_height; - output_dims.w = output_width; - output_dims.c = output_depth; - - const int32_t buf_size = arm_convolve_wrapper_s8_get_buffer_size(&conv_params, &input_dims, - &filter_dims, &output_dims); - - luci_interpreter::Shape scratchpad_shape{buf_size}; - scratchpad->resize(scratchpad_shape); - } - else - { - scratchpad->set_allocatable(false); - } -} - -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_CONV2D_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALDepthwiseConv2d.h b/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALDepthwiseConv2d.h deleted file mode 100644 index 120dcd8..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALDepthwiseConv2d.h +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_DEPTHWISECONV2D_H -#define LUCI_INTERPRETER_PAL_DEPTHWISECONV2D_H - -#include -#include -#include -#include - -namespace luci_interpreter_pal -{ -template -static inline void -DepthwiseConvPerChannel(const tflite::DepthwiseParams ¶ms, const int32_t *output_multiplier, - const int32_t *output_shift, const tflite::RuntimeShape &input_shape, - const T *input_data, const tflite::RuntimeShape &filter_shape, - const T *filter_data, const tflite::RuntimeShape &bias_shape, - const int32_t *bias_data, const tflite::RuntimeShape &output_shape, - T *output_data, const tflite::RuntimeShape &scratchpad_shape, - T *scratchpad_data) -{ - { - // MARK: At this moment this operation is not supported - assert(false && "DepthwiseConvPerChannel NYI"); - (void)params; - (void)output_multiplier; - (void)output_shift; - (void)input_shape; - (void)output_data; - (void)input_data; - (void)filter_shape; - (void)filter_data; - (void)bias_shape; - (void)bias_data; - (void)output_shape; - (void)output_data; - (void)scratchpad_shape; - (void)scratchpad_data; - } -} - -template <> -inline void DepthwiseConvPerChannel( - const tflite::DepthwiseParams ¶ms, const int32_t *output_multiplier, - const int32_t *output_shift, const tflite::RuntimeShape &input_shape, const int8_t *input_data, - const tflite::RuntimeShape &filter_shape, const int8_t *filter_data, - const tflite::RuntimeShape &bias_shape, const int32_t *bias_data, - const tflite::RuntimeShape &output_shape, int8_t *output_data, - const tflite::RuntimeShape &scratchpad_shape, int8_t *scratchpad_data) -{ - if (scratchpad_data) - { - cmsis_nn_dw_conv_params dw_conv_params; - dw_conv_params.dilation.h = params.dilation_height_factor; - dw_conv_params.dilation.w = params.dilation_width_factor; - assert(dw_conv_params.dilation.h == 1); - assert(dw_conv_params.dilation.w == 1); - - dw_conv_params.input_offset = params.input_offset; - dw_conv_params.output_offset = params.output_offset; - dw_conv_params.stride.h = params.stride_height; - dw_conv_params.stride.w = params.stride_width; - dw_conv_params.padding.h = params.padding_values.height; - dw_conv_params.padding.w = params.padding_values.width; - - dw_conv_params.activation.min = params.quantized_activation_min; - dw_conv_params.activation.max = params.quantized_activation_max; - dw_conv_params.ch_mult = params.depth_multiplier; - - cmsis_nn_per_channel_quant_params quant_params; - int32_t output_multiplier = params.output_multiplier; - int32_t output_shift = params.output_shift; - - quant_params.multiplier = &output_multiplier; - quant_params.shift = &output_shift; - - assert(dw_conv_params.activation.min <= dw_conv_params.activation.max); - const int batch_size = tflite::MatchingDim(input_shape, 0, output_shape, 0); - const int output_depth = tflite::MatchingDim(filter_shape, 3, output_shape, 3); - if (bias_data) - { - assert(bias_shape.FlatSize() == output_depth); - } - - cmsis_nn_dims input_dims; - input_dims.n = batch_size; - input_dims.h = input_shape.Dims(1); - input_dims.w = input_shape.Dims(2); - input_dims.c = input_shape.Dims(3); - - cmsis_nn_dims filter_dims; - filter_dims.n = filter_shape.Dims(0); - filter_dims.h = filter_shape.Dims(1); - filter_dims.w = filter_shape.Dims(2); - filter_dims.c = output_depth; - - cmsis_nn_dims bias_dims; - bias_dims.n = 1; - bias_dims.h = 1; - bias_dims.w = 1; - bias_dims.c = output_depth; - - cmsis_nn_dims output_dims; - output_dims.n = batch_size; - output_dims.h = output_shape.Dims(1); - output_dims.w = output_shape.Dims(2); - output_dims.c = output_depth; - - cmsis_nn_context ctx; - ctx.buf = scratchpad_data; - ctx.size = scratchpad_shape.Dims(0); - - auto res = arm_depthwise_conv_wrapper_s8(&ctx, &dw_conv_params, &quant_params, &input_dims, - input_data, &filter_dims, filter_data, &bias_dims, - bias_data, &output_dims, output_data); - assert(res == ARM_MATH_SUCCESS); - } - else - { - tflite::reference_integer_ops::DepthwiseConvPerChannel( - params, output_multiplier, output_shift, input_shape, input_data, filter_shape, filter_data, - bias_shape, bias_data, output_shape, output_data); - } -} - -static inline void SetupScratchpadTensor(luci_interpreter::Tensor *scratchpad, - const tflite::DepthwiseParams ¶ms, - const luci_interpreter::DataType &input_data_type, - const tflite::RuntimeShape &input_shape, - const tflite::RuntimeShape &filter_shape, - const tflite::RuntimeShape &output_shape) -{ - cmsis_nn_dw_conv_params dw_conv_params; - dw_conv_params.dilation.h = params.dilation_height_factor; - dw_conv_params.dilation.w = params.dilation_width_factor; - - if (input_data_type == loco::DataType::S8 && dw_conv_params.dilation.h == 1 && - dw_conv_params.dilation.w == 1) - { - const int batch_size = tflite::MatchingDim(input_shape, 0, output_shape, 0); - const int output_depth = tflite::MatchingDim(filter_shape, 3, output_shape, 3); - - cmsis_nn_dims input_dims; - input_dims.n = batch_size; - input_dims.h = input_shape.Dims(1); - input_dims.w = input_shape.Dims(2); - input_dims.c = input_shape.Dims(3); - - cmsis_nn_dims filter_dims; - filter_dims.n = filter_shape.Dims(0); - filter_dims.h = filter_shape.Dims(1); - filter_dims.w = filter_shape.Dims(2); - filter_dims.c = output_depth; - - cmsis_nn_dims output_dims; - output_dims.n = batch_size; - output_dims.h = output_shape.Dims(1); - output_dims.w = output_shape.Dims(2); - output_dims.c = output_depth; - - const int32_t buf_size = arm_depthwise_conv_wrapper_s8_get_buffer_size( - &dw_conv_params, &input_dims, &filter_dims, &output_dims); - - auto data_type_size = static_cast(luci_interpreter::getDataTypeSize(input_data_type)); - - luci_interpreter::Shape scratchpad_shape{buf_size * data_type_size}; - scratchpad->resize(scratchpad_shape); - } - else - { - scratchpad->set_allocatable(false); - } -} - -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_DEPTHWISECONV2D_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALDequantize.h b/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALDequantize.h deleted file mode 100644 index 15ff032..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALDequantize.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_DEQUANTIZE_H -#define LUCI_INTERPRETER_PAL_DEQUANTIZE_H - -#include "tensorflow/lite/kernels/internal/reference/integer_ops/dequantize.h" -#include "tensorflow/lite/kernels/internal/reference/reference_ops.h" - -namespace luci_interpreter_pal -{ - -template -static inline void Dequantize(tflite::DequantizationParams ¶ms, - const tflite::RuntimeShape &input_shape, const T *input_data, - const tflite::RuntimeShape &output_shape, float *output_data) -{ - tflite::reference_integer_ops::Dequantize(params, input_shape, input_data, output_shape, - output_data); -} - -static inline void Dequantize(tflite::DequantizationParams ¶ms, - const tflite::RuntimeShape &input_shape, const uint8_t *input_data, - const tflite::RuntimeShape &output_shape, float *output_data) -{ - tflite::reference_ops::Dequantize(params, input_shape, input_data, output_shape, output_data); -} - -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_DEQUANTIZE_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALQuantize.h b/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALQuantize.h deleted file mode 100644 index 6046789..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALQuantize.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_QUANTIZE_H -#define LUCI_INTERPRETER_PAL_QUANTIZE_H - -#include "tensorflow/lite/kernels/internal/reference/reference_ops.h" - -namespace luci_interpreter_pal -{ -template -static inline void Quantize(tflite::QuantizationParams ¶ms, - const tflite::RuntimeShape &input_shape, const float *input_data, - const tflite::RuntimeShape &output_shape, T *output_data) -{ - tflite::reference_ops::AffineQuantize(params, input_shape, input_data, output_shape, output_data); -} - -template -static inline void Requantize(const Input *input_data, int32_t size, - int32_t effective_scale_multiplier, int32_t effective_scale_shift, - int32_t input_zero_point, int32_t output_zero_point, - Output *output_data) -{ - tflite::reference_ops::Requantize(input_data, size, effective_scale_multiplier, - effective_scale_shift, input_zero_point, output_zero_point, - output_data); -} -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_QUANTIZE_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALResizeBilinear.h b/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALResizeBilinear.h deleted file mode 100644 index cc9f0fd..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALResizeBilinear.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_RESIZEBILINEAR_H -#define LUCI_INTERPRETER_PAL_RESIZEBILINEAR_H - -#include - -namespace luci_interpreter_pal -{ -template -static inline void -ResizeBilinear(const tflite::ResizeBilinearParams &op_params, - const tflite::RuntimeShape &unextended_input_shape, const T *input_data, - const tflite::RuntimeShape &output_size_shape, const int32 *output_size_data, - const tflite::RuntimeShape &unextended_output_shape, T *output_data) -{ - tflite::reference_ops::ResizeBilinear(op_params, unextended_input_shape, input_data, - output_size_shape, output_size_data, - unextended_output_shape, output_data); -} -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_RESIZEBILINEAR_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALResizeNearestNeighbor.h b/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALResizeNearestNeighbor.h deleted file mode 100644 index f4d5a6e..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALResizeNearestNeighbor.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_RESIZENEARESTNEIGHBOR_H -#define LUCI_INTERPRETER_PAL_RESIZENEARESTNEIGHBOR_H - -#include - -namespace luci_interpreter_pal -{ -template -static inline void -ResizeNearestNeighbor(const tflite::ResizeNearestNeighborParams &op_params, - const tflite::RuntimeShape &unextended_input_shape, const T *input_data, - const tflite::RuntimeShape &output_size_shape, const int32 *output_size_data, - const tflite::RuntimeShape &unextended_output_shape, T *output_data) -{ - tflite::reference_ops::ResizeNearestNeighbor(op_params, unextended_input_shape, input_data, - output_size_shape, output_size_data, - unextended_output_shape, output_data); -} -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_RESIZENEARESTNEIGHBOR_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSVDF.h b/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSVDF.h deleted file mode 100644 index a4a5b2a..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSVDF.h +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2020 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_SVDF_H -#define LUCI_INTERPRETER_PAL_SVDF_H - -#include -#include - -namespace luci_interpreter_pal -{ -static inline void -IntegerSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, - const int8_t *input_data, const tflite::RuntimeShape &weight_feature_shape, - const int8_t *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, - const int16_t *weight_time_data, const tflite::RuntimeShape &bias_shape, - const int32_t *bias_data, int16_t *activation_state_data, - const tflite::RuntimeShape &output_shape, int8_t *output_data, int32_t *scratchpad_data, - int32_t *output_temp_data, int32_t scale_1_a, int scale_1_b, int32_t scale_2_a, - int scale_2_b, int32_t input_zp, int32_t output_zp) -{ - const int32_t rank = params.rank; - const int32_t batch_size = input_shape.Dims(0); - const int32_t num_filters = weight_feature_shape.Dims(0); - const int32_t memory_size = weight_time_shape.Dims(1); - - cmsis_nn_dims input_dims; - input_dims.n = input_shape.Dims(0); - input_dims.h = input_shape.Dims(1); - - cmsis_nn_dims weights_feature_dims; - weights_feature_dims.n = weight_feature_shape.Dims(0); - weights_feature_dims.h = weight_feature_shape.Dims(1); - - cmsis_nn_dims weights_time_dims; - weights_time_dims.n = weight_time_shape.Dims(0); - weights_time_dims.h = weight_time_shape.Dims(1); - - cmsis_nn_dims bias_dims; - bias_dims.n = bias_shape.Dims(0); - - cmsis_nn_dims state_dims; - state_dims.n = batch_size; - state_dims.h = memory_size * num_filters; - - cmsis_nn_dims output_dims; - output_dims.n = output_shape.Dims(0); - output_dims.h = output_shape.Dims(1); - - cmsis_nn_svdf_params svdf_params; - svdf_params.rank = params.rank; - svdf_params.input_offset = input_zp; - svdf_params.output_offset = output_zp; - - svdf_params.input_activation.min = INT16_MIN; - svdf_params.input_activation.max = INT16_MAX; - - svdf_params.output_activation.min = INT8_MIN; - svdf_params.output_activation.max = INT8_MAX; - - cmsis_nn_per_tensor_quant_params in_quant_params; - in_quant_params.multiplier = scale_1_a; - in_quant_params.shift = scale_1_b; - - cmsis_nn_per_tensor_quant_params out_quant_params; - out_quant_params.multiplier = scale_2_a; - out_quant_params.shift = scale_2_b; - - cmsis_nn_context scratch_ctx; - scratch_ctx.buf = scratchpad_data; - - cmsis_nn_context scratch_output_ctx; - scratch_output_ctx.buf = output_temp_data; - - arm_svdf_s8(&scratch_ctx, &scratch_output_ctx, &svdf_params, &in_quant_params, &out_quant_params, - &input_dims, input_data, &state_dims, activation_state_data, &weights_feature_dims, - weight_feature_data, &weights_time_dims, weight_time_data, &bias_dims, bias_data, - &output_dims, output_data); -} -static inline void -FloatSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, - const float *input_data, const tflite::RuntimeShape &weight_feature_shape, - const float *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, - const float *weight_time_data, const tflite::RuntimeShape &bias_shape, - const float *bias_data, float *scratchpad_data, float *activation_state_data, - const tflite::RuntimeShape &output_shape, float *output_data) -{ - const int32_t rank = params.rank; - const int32_t batch_size = input_shape.Dims(0); - const int32_t input_size = input_shape.Dims(1); - const int32_t num_filters = weight_feature_shape.Dims(0); - const int32_t num_units = num_filters / rank; - const int32_t memory_size = weight_time_shape.Dims(1); - - // Left shift the activation_state. - { - float *new_state_start = activation_state_data; - const float *old_state_start = activation_state_data + 1; - const float *old_state_end = activation_state_data + batch_size * num_filters * memory_size; - while (old_state_start != old_state_end) - { - *new_state_start++ = *old_state_start++; - } - } - - // Note: no need to clear the latest activation, matmul is not accumulative. - - // Compute conv1d(inputs, weights_feature). - // The activation_state's rightmost column is used to save current cycle - // activation. This is achieved by starting at state_ptr[memory_size - 1] and - // having the stride equal to memory_size. - - // Perform batched matrix vector multiply operation: - { - const float *matrix = weight_feature_data; - const float *vector = input_data; - float *result = &activation_state_data[memory_size - 1]; - float *result_in_batch = result; - for (int i = 0; i < batch_size; ++i) - { - const float *matrix_ptr = matrix; - for (int j = 0; j < num_filters; ++j) - { - float dot_prod = 0.0f; - const float *vector_in_batch = vector + i * input_size; - for (int k = 0; k < input_size; ++k) - { - dot_prod += *matrix_ptr++ * *vector_in_batch++; - } - *result_in_batch = dot_prod; - result_in_batch += memory_size; - } - } - } - - tflite::reference_ops::ApplyTimeWeightsBiasAndActivation( - batch_size, memory_size, num_filters, num_units, rank, weight_time_data, bias_data, - params.activation, activation_state_data, scratchpad_data, output_data); -} - -static inline void SetupScratchpadTensor( - const luci_interpreter::DataType &input_data_type, - const luci_interpreter::DataType &weight_feature_data_type, - luci_interpreter::Tensor *scratchpad_1, luci_interpreter::Tensor *scratchpad_2, - luci_interpreter::Tensor *scratchpad_3, luci_interpreter::Tensor *scratchpad_4, - luci_interpreter::Tensor *scratchpad_5, luci_interpreter::Tensor *scratchpad_6, - const luci_interpreter::Shape input_shape, const luci_interpreter::Shape weight_time_shape, - const int32_t batch_size, const int32_t num_filters, const int32_t num_units) -{ - if (input_data_type == loco::DataType::FLOAT32 && - (weight_feature_data_type == loco::DataType::S8 || - weight_feature_data_type == loco::DataType::U8)) - { - (void)input_shape; - (void)weight_time_shape; - (void)scratchpad_3; - (void)scratchpad_4; - (void)scratchpad_5; - (void)scratchpad_6; - - throw std::runtime_error("Hybrid type is not supported for cmsisnn"); - } - - // Resize scratchpad_1 tensor - scratchpad_1->resize({batch_size, num_filters}); - - if (input_data_type == loco::DataType::S8) - { - // Resize scratchpad_2 for full_integer op - scratchpad_2->resize({batch_size, num_units}); - } -} - -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_SVDF_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSpaceToBatchND.h b/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSpaceToBatchND.h deleted file mode 100644 index fdddaa9..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSpaceToBatchND.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_SPACETOBATCHND_H -#define LUCI_INTERPRETER_PAL_SPACETOBATCHND_H - -#include - -namespace luci_interpreter_pal -{ -template -static inline void -SpaceToBatchND(const tflite::SpaceToBatchParams ¶ms, - const tflite::RuntimeShape &unextended_input1_shape, const T *input1_data, - const tflite::RuntimeShape &unextended_input2_shape, const int32 *block_shape_data, - const tflite::RuntimeShape &unextended_input3_shape, const int32 *paddings_data, - const tflite::RuntimeShape &unextended_output_shape, T *output_data) -{ - tflite::reference_ops::SpaceToBatchND( - params, unextended_input1_shape, input1_data, unextended_input2_shape, block_shape_data, - unextended_input3_shape, paddings_data, unextended_output_shape, output_data); -} -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_SPACETOBATCHND_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/pal.cmake b/compiler/luci-micro/luci-interpreter/pal/cmsisnn/pal.cmake deleted file mode 100644 index a68b363..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/pal.cmake +++ /dev/null @@ -1,65 +0,0 @@ -macro(initialize_pal) - nnas_find_package(TensorFlowSource EXACT 2.6.0 QUIET) - nnas_find_package(TensorFlowGEMMLowpSource EXACT 2.6.0 QUIET) - nnas_find_package(TensorFlowEigenSource EXACT 2.6.0 QUIET) - nnas_find_package(TensorFlowRuySource EXACT 2.6.0 QUIET) - nnas_find_package(CMSISSource EXACT 5.8.0 QUIET) - - if (NOT TensorFlowSource_FOUND) - message(STATUS "Skipping luci-interpreter: TensorFlow not found") - return() - endif () - - if (NOT TensorFlowGEMMLowpSource_FOUND) - message(STATUS "Skipping luci-interpreter: gemmlowp not found") - return() - endif () - - if (NOT TensorFlowEigenSource_FOUND) - message(STATUS "Skipping luci-interpreter: Eigen not found") - return() - endif () - - if (NOT TensorFlowRuySource_FOUND) - message(STATUS "Skipping luci-interpreter: Ruy not found") - return() - endif () - - if (NOT CMSISSource_FOUND) - message(STATUS "Skipping luci-interpreter: CMSISSource not found") - return() - endif () - - set(PAL_INITIALIZED TRUE) -endmacro() - -macro(add_pal_to_target TGT) - target_include_directories(${TGT} PRIVATE "${PAL}") - target_include_directories(${TGT} PRIVATE - "${TensorFlowRuySource_DIR}" - "${TensorFlowGEMMLowpSource_DIR}" - "${TensorFlowEigenSource_DIR}" - "${TensorFlowSource_DIR}") - target_include_directories(${TGT} PRIVATE ${LUCI_INTERPRETER_PAL_DIR}) - - file(GLOB_RECURSE PAL_SOURCES "${CMSISSource_DIR}/CMSIS/NN/Source/*.c") - list(APPEND PAL_SOURCES ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/quantization_util.cc - ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/tensor_utils.cc - ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/reference/portable_tensor_utils.cc) - add_library(luci_interpreter_cmsisnn_pal STATIC ${PAL_SOURCES}) - set_property(TARGET luci_interpreter_cmsisnn_pal PROPERTY POSITION_INDEPENDENT_CODE ON) - target_include_directories(luci_interpreter_cmsisnn_pal PRIVATE - "${TensorFlowRuySource_DIR}" - "${TensorFlowGEMMLowpSource_DIR}" - "${TensorFlowEigenSource_DIR}" - "${TensorFlowSource_DIR}" - ) - - add_subdirectory(${CMSISSource_DIR}/CMSIS/NN ${CMAKE_CURRENT_BINARY_DIR}/CMSISNN) - target_include_directories(luci_interpreter_cmsisnn_pal PUBLIC - "${CMSISSource_DIR}/CMSIS/NN/Include" - "${CMSISSource_DIR}/CMSIS/DSP/Include" - "${CMSISSource_DIR}/CMSIS/Core/Include") - - target_link_libraries(${TGT} PRIVATE luci_interpreter_cmsisnn_pal) -endmacro() diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/KernelsToBuild.lst b/compiler/luci-micro/luci-interpreter/pal/linux/KernelsToBuild.lst deleted file mode 100644 index 8e20559..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/linux/KernelsToBuild.lst +++ /dev/null @@ -1,77 +0,0 @@ -REGISTER_KERNEL(Add) -REGISTER_KERNEL(ArgMax) -REGISTER_KERNEL(AveragePool2D) -REGISTER_KERNEL(BatchMatMul) -REGISTER_KERNEL(BatchToSpaceND) -REGISTER_KERNEL(Cast) -REGISTER_KERNEL(Concatenation) -REGISTER_KERNEL(Conv2D) -REGISTER_KERNEL(DepthToSpace) -REGISTER_KERNEL(DepthwiseConv2D) -REGISTER_KERNEL(Dequantize) -REGISTER_KERNEL(Div) -REGISTER_KERNEL(Elu) -REGISTER_KERNEL(Exp) -REGISTER_KERNEL(ExpandDims) -REGISTER_KERNEL(Fill) -REGISTER_KERNEL(Floor) -REGISTER_KERNEL(FloorDiv) -REGISTER_KERNEL(Equal) -REGISTER_KERNEL(FullyConnected) -REGISTER_KERNEL(Gather) -REGISTER_KERNEL(Greater) -REGISTER_KERNEL(GreaterEqual) -REGISTER_KERNEL(If) -REGISTER_KERNEL(InstanceNorm) -REGISTER_KERNEL(L2Normalize) -REGISTER_KERNEL(L2Pool2D) -REGISTER_KERNEL(LeakyRelu) -REGISTER_KERNEL(Less) -REGISTER_KERNEL(LessEqual) -REGISTER_KERNEL(LocalResponseNormalization) -REGISTER_KERNEL(LogicalAnd) -REGISTER_KERNEL(LogicalNot) -REGISTER_KERNEL(LogicalOr) -REGISTER_KERNEL(Logistic) -REGISTER_KERNEL(LogSoftmax) -REGISTER_KERNEL(Maximum) -REGISTER_KERNEL(MaxPool2D) -REGISTER_KERNEL(Mean) -REGISTER_KERNEL(Minimum) -REGISTER_KERNEL(MirrorPad) -REGISTER_KERNEL(Mul) -REGISTER_KERNEL(Neg) -REGISTER_KERNEL(NotEqual) -REGISTER_KERNEL(OneHot) -REGISTER_KERNEL(Pack) -REGISTER_KERNEL(Pad) -REGISTER_KERNEL(PadV2) -REGISTER_KERNEL(Pow) -REGISTER_KERNEL(PRelu) -REGISTER_KERNEL(Quantize) -REGISTER_KERNEL(Relu) -REGISTER_KERNEL(Relu6) -REGISTER_KERNEL(Reshape) -REGISTER_KERNEL(ResizeBilinear) -REGISTER_KERNEL(ResizeNearestNeighbor) -REGISTER_KERNEL(ReverseV2) -REGISTER_KERNEL(Rsqrt) -REGISTER_KERNEL(Shape) -REGISTER_KERNEL(Slice) -REGISTER_KERNEL(Softmax) -REGISTER_KERNEL(SpaceToBatchND) -REGISTER_KERNEL(SpaceToDepth) -REGISTER_KERNEL(Split) -REGISTER_KERNEL(SplitV) -REGISTER_KERNEL(StridedSlice) -REGISTER_KERNEL(Sqrt) -REGISTER_KERNEL(Square) -REGISTER_KERNEL(SquaredDifference) -REGISTER_KERNEL(Squeeze) -REGISTER_KERNEL(Sub) -REGISTER_KERNEL(SVDF) -REGISTER_KERNEL(Tanh) -REGISTER_KERNEL(Transpose) -REGISTER_KERNEL(TransposeConv) -REGISTER_KERNEL(Unpack) -REGISTER_KERNEL(While) diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALSVDF.h b/compiler/luci-micro/luci-interpreter/pal/linux/PALSVDF.h deleted file mode 100644 index 0ffba14..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/linux/PALSVDF.h +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_SVDF_H -#define LUCI_INTERPRETER_PAL_SVDF_H - -#include - -namespace luci_interpreter_pal -{ -static inline void -IntegerSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, - const int8_t *input_data, const tflite::RuntimeShape &weight_feature_shape, - const int8_t *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, - const int16_t *weight_time_data, const tflite::RuntimeShape &bias_shape, - const int32_t *bias_data, int16_t *activation_state_data, - const tflite::RuntimeShape &output_shape, int8_t *output_data, int32_t *scratchpad_data, - int32_t *output_temp_data, int32_t scale_1_a, int scale_1_b, int32_t scale_2_a, - int scale_2_b, int32_t input_zp, int32_t output_zp) -{ - tflite::reference_ops::EvalIntegerSVDF(¶ms, input_shape, input_data, weight_feature_shape, - weight_feature_data, weight_time_shape, weight_time_data, - bias_shape, bias_data, activation_state_data, output_shape, - output_data, scratchpad_data, output_temp_data, scale_1_a, - scale_1_b, scale_2_a, scale_2_b, input_zp, output_zp); -} -static inline void -FloatSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, - const float *input_data, const tflite::RuntimeShape &weight_feature_shape, - const float *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, - const float *weight_time_data, const tflite::RuntimeShape &bias_shape, - const float *bias_data, float *scratchpad_data, float *activation_state_data, - const tflite::RuntimeShape &output_shape, float *output_data) -{ - tflite::reference_ops::EvalFloatSVDF(¶ms, input_shape, input_data, weight_feature_shape, - weight_feature_data, weight_time_shape, weight_time_data, - bias_shape, bias_data, scratchpad_data, - activation_state_data, output_shape, output_data); -} - -static inline void SetupScratchpadTensor( - const luci_interpreter::DataType &input_data_type, - const luci_interpreter::DataType &weight_feature_data_type, - luci_interpreter::Tensor *scratchpad_1, luci_interpreter::Tensor *scratchpad_2, - luci_interpreter::Tensor *scratchpad_3, luci_interpreter::Tensor *scratchpad_4, - luci_interpreter::Tensor *scratchpad_5, luci_interpreter::Tensor *scratchpad_6, - const luci_interpreter::Shape input_shape, const luci_interpreter::Shape weight_time_shape, - const int32_t batch_size, const int32_t num_filters, const int32_t num_units) -{ - - if (input_data_type == loco::DataType::FLOAT32 && - (weight_feature_data_type == loco::DataType::S8 || - weight_feature_data_type == loco::DataType::U8)) - { - (void)input_shape; - (void)weight_time_shape; - (void)scratchpad_3; - (void)scratchpad_4; - (void)scratchpad_5; - (void)scratchpad_6; - - throw std::runtime_error("Hybrid type is not currently supported for linux platform"); - } - - // Resize scratchpad_1 tensor - scratchpad_1->resize({batch_size, num_filters}); - - if (input_data_type == loco::DataType::S8) - { - // Resize scratchpad_2 for full_integer op - scratchpad_2->resize({batch_size, num_units}); - } -} - -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_SVDF_H diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/pal.cmake b/compiler/luci-micro/luci-interpreter/pal/linux/pal.cmake deleted file mode 100644 index 185700c..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/linux/pal.cmake +++ /dev/null @@ -1,82 +0,0 @@ -macro(initialize_pal) - nnas_find_package(TensorFlowSource EXACT 2.6.0 QUIET) - nnas_find_package(TensorFlowGEMMLowpSource EXACT 2.6.0 QUIET) - nnas_find_package(TensorFlowEigenSource EXACT 2.6.0 QUIET) - nnas_find_package(TensorFlowRuySource EXACT 2.6.0 QUIET) - - if (NOT TensorFlowSource_FOUND) - message(STATUS "Skipping luci-interpreter: TensorFlow not found") - return() - endif () - - if (NOT TensorFlowGEMMLowpSource_FOUND) - message(STATUS "Skipping luci-interpreter: gemmlowp not found") - return() - endif () - - if (NOT TensorFlowEigenSource_FOUND) - message(STATUS "Skipping luci-interpreter: Eigen not found") - return() - endif () - - if (NOT TensorFlowRuySource_FOUND) - message(STATUS "Skipping luci-interpreter: Ruy not found") - return() - endif () - - find_package(Threads REQUIRED) - - set(PAL_INITIALIZED TRUE) -endmacro() - -macro(add_pal_to_target TGT) - target_include_directories(${TGT} PRIVATE "${PAL}") - target_include_directories(${TGT} SYSTEM PRIVATE - "${TensorFlowRuySource_DIR}" - "${TensorFlowGEMMLowpSource_DIR}" - "${TensorFlowEigenSource_DIR}" - "${TensorFlowSource_DIR}") - target_include_directories(${TGT} PRIVATE ${LUCI_INTERPRETER_PAL_DIR}) - - # TODO put it back, I changed my mind. - # instead add sources with visitors in this library - set(PAL_SOURCES ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/tensor_utils.cc - ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/reference/portable_tensor_utils.cc - ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/quantization_util.cc) - - if(BUILD_ARM32_NEON) - # NOTE may need to revise this list for version upgrade - set(PAL_SOURCES ${PAL_SOURCES} - ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/optimized/neon_tensor_utils.cc - ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/optimized/cpu_check.cc - ${TensorFlowRuySource_DIR}/ruy/allocator.cc - ${TensorFlowRuySource_DIR}/ruy/block_map.cc - ${TensorFlowRuySource_DIR}/ruy/blocking_counter.cc - ${TensorFlowRuySource_DIR}/ruy/context_get_ctx.cc - ${TensorFlowRuySource_DIR}/ruy/cpuinfo.cc - ${TensorFlowRuySource_DIR}/ruy/ctx.cc - ${TensorFlowRuySource_DIR}/ruy/denormal.cc - ${TensorFlowRuySource_DIR}/ruy/frontend.cc - ${TensorFlowRuySource_DIR}/ruy/pack_arm.cc - ${TensorFlowRuySource_DIR}/ruy/prepacked_cache.cc - ${TensorFlowRuySource_DIR}/ruy/prepare_packed_matrices.cc - ${TensorFlowRuySource_DIR}/ruy/system_aligned_alloc.cc - ${TensorFlowRuySource_DIR}/ruy/thread_pool.cc - ${TensorFlowRuySource_DIR}/ruy/trmul.cc - ${TensorFlowRuySource_DIR}/ruy/tune.cc - ${TensorFlowRuySource_DIR}/ruy/wait.cc - ${TensorFlowRuySource_DIR}/ruy/kernel_arm32.cc - ) - endif(BUILD_ARM32_NEON) - - add_library(luci_interpreter_linux_pal STATIC ${PAL_SOURCES}) - set_target_properties(luci_interpreter_linux_pal PROPERTIES POSITION_INDEPENDENT_CODE ON) - target_include_directories(luci_interpreter_linux_pal SYSTEM PRIVATE - "${TensorFlowRuySource_DIR}" - "${TensorFlowGEMMLowpSource_DIR}" - "${TensorFlowEigenSource_DIR}" - "${TensorFlowSource_DIR}" - ) - - target_link_libraries(${TGT} PRIVATE Threads::Threads luci_interpreter_linux_pal) -endmacro() diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/KernelsToBuild.lst b/compiler/luci-micro/luci-interpreter/pal/mcu/KernelsToBuild.lst deleted file mode 100644 index f0df58d..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/mcu/KernelsToBuild.lst +++ /dev/null @@ -1,62 +0,0 @@ -REGISTER_KERNEL(Add) -REGISTER_KERNEL(ArgMax) -REGISTER_KERNEL(AveragePool2D) -REGISTER_KERNEL(BatchToSpaceND) -REGISTER_KERNEL(Cast) -REGISTER_KERNEL(Concatenation) -REGISTER_KERNEL(Conv2D) -REGISTER_KERNEL(DepthToSpace) -REGISTER_KERNEL(DepthwiseConv2D) -REGISTER_KERNEL(Dequantize) -REGISTER_KERNEL(Div) -REGISTER_KERNEL(Elu) -REGISTER_KERNEL(Exp) -REGISTER_KERNEL(ExpandDims) -REGISTER_KERNEL(Fill) -REGISTER_KERNEL(Floor) -REGISTER_KERNEL(FloorDiv) -REGISTER_KERNEL(Equal) -REGISTER_KERNEL(FullyConnected) -REGISTER_KERNEL(Greater) -REGISTER_KERNEL(GreaterEqual) -REGISTER_KERNEL(If) -REGISTER_KERNEL(InstanceNorm) -REGISTER_KERNEL(L2Normalize) -REGISTER_KERNEL(L2Pool2D) -REGISTER_KERNEL(LeakyRelu) -REGISTER_KERNEL(Less) -REGISTER_KERNEL(LessEqual) -REGISTER_KERNEL(LogicalAnd) -REGISTER_KERNEL(LogicalNot) -REGISTER_KERNEL(LogicalOr) -REGISTER_KERNEL(Logistic) -REGISTER_KERNEL(Maximum) -REGISTER_KERNEL(MaxPool2D) -REGISTER_KERNEL(Minimum) -REGISTER_KERNEL(MirrorPad) -REGISTER_KERNEL(Mul) -REGISTER_KERNEL(Neg) -REGISTER_KERNEL(NotEqual) -REGISTER_KERNEL(Pad) -REGISTER_KERNEL(PadV2) -REGISTER_KERNEL(PRelu) -REGISTER_KERNEL(Quantize) -REGISTER_KERNEL(Reshape) -REGISTER_KERNEL(ResizeBilinear) -REGISTER_KERNEL(ResizeNearestNeighbor) -REGISTER_KERNEL(Rsqrt) -REGISTER_KERNEL(Shape) -REGISTER_KERNEL(Softmax) -REGISTER_KERNEL(SpaceToBatchND) -REGISTER_KERNEL(SpaceToDepth) -REGISTER_KERNEL(StridedSlice) -REGISTER_KERNEL(Sqrt) -REGISTER_KERNEL(Square) -REGISTER_KERNEL(SquaredDifference) -REGISTER_KERNEL(Squeeze) -REGISTER_KERNEL(Sub) -REGISTER_KERNEL(SVDF) -REGISTER_KERNEL(Tanh) -REGISTER_KERNEL(Transpose) -REGISTER_KERNEL(TransposeConv) -REGISTER_KERNEL(While) diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALBatchToSpaceND.h b/compiler/luci-micro/luci-interpreter/pal/mcu/PALBatchToSpaceND.h deleted file mode 100644 index 4dd77ff..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/mcu/PALBatchToSpaceND.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H -#define LUCI_INTERPRETER_PAL_ARGMAX_H - -#include - -namespace luci_interpreter_pal -{ -template -static inline void -BatchToSpaceND(const tflite::RuntimeShape &unextended_input1_shape, const T *input1_data, - const tflite::RuntimeShape &unextended_input2_shape, const int32 *block_shape_data, - const tflite::RuntimeShape &unextended_input3_shape, const int32 *crops_data, - const tflite::RuntimeShape &unextended_output_shape, T *output_data) -{ - tflite::reference_ops::BatchToSpaceND( - unextended_input1_shape, input1_data, unextended_input2_shape, block_shape_data, - unextended_input3_shape, crops_data, unextended_output_shape, output_data); -} -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALConv2d.h b/compiler/luci-micro/luci-interpreter/pal/mcu/PALConv2d.h deleted file mode 100644 index 1397687..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/mcu/PALConv2d.h +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_CONV2D_H -#define LUCI_INTERPRETER_PAL_CONV2D_H - -#include -#include - -namespace luci_interpreter_pal -{ -static inline void Conv(const tflite::ConvParams ¶ms, const tflite::RuntimeShape &input_shape, - const float *input_data, const tflite::RuntimeShape &filter_shape, - const float *filter_data, const tflite::RuntimeShape &bias_shape, - const float *bias_data, const tflite::RuntimeShape &output_shape, - float *output_data, const tflite::RuntimeShape &scratchpad_shape, - float *scratchpad_data) -{ - (void)scratchpad_shape; - (void)scratchpad_data; - tflite::reference_ops::Conv(params, input_shape, input_data, filter_shape, filter_data, - bias_shape, bias_data, output_shape, output_data, - tflite::RuntimeShape(), nullptr); -} - -static inline void Conv(const tflite::ConvParams ¶ms, const tflite::RuntimeShape &input_shape, - const uint8 *input_data, const tflite::RuntimeShape &filter_shape, - const uint8 *filter_data, const tflite::RuntimeShape &bias_shape, - const int32 *bias_data, const tflite::RuntimeShape &output_shape, - uint8 *output_data, const tflite::RuntimeShape &scratchpad_shape, - uint8 *scratchpad_data) -{ - (void)scratchpad_shape; - (void)scratchpad_data; - tflite::reference_ops::Conv(params, input_shape, input_data, filter_shape, filter_data, - bias_shape, bias_data, output_shape, output_data, scratchpad_shape, - scratchpad_data, nullptr); -} - -static inline void ConvPerChannel(const tflite::ConvParams ¶ms, const int32_t *mult, - const int32_t *shifts, const tflite::RuntimeShape &input_shape, - const int8 *input_data, const tflite::RuntimeShape &filter_shape, - const int8 *filter_data, const tflite::RuntimeShape &bias_shape, - const int32 *bias_data, const tflite::RuntimeShape &output_shape, - int8 *output_data, const tflite::RuntimeShape &scratchpad_shape, - int8 *scratchpad_data) -{ - (void)scratchpad_shape; - (void)scratchpad_data; - tflite::reference_integer_ops::ConvPerChannel(params, mult, shifts, input_shape, input_data, - filter_shape, filter_data, bias_shape, bias_data, - output_shape, output_data); -} - -static inline void SetupScratchpadTensor(luci_interpreter::Tensor *scratchpad, - const luci_interpreter::DataType &input_data_type, - const tflite::ConvParams ¶ms, - const tflite::RuntimeShape &input_shape, - const tflite::RuntimeShape &filter_shape, - const tflite::RuntimeShape &output_shape) -{ - (void)input_data_type; - (void)params; - (void)input_shape; - (void)filter_shape; - (void)output_shape; - scratchpad->set_allocatable(false); -} - -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_CONV2D_H diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALDequantize.h b/compiler/luci-micro/luci-interpreter/pal/mcu/PALDequantize.h deleted file mode 100644 index 15ff032..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/mcu/PALDequantize.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_DEQUANTIZE_H -#define LUCI_INTERPRETER_PAL_DEQUANTIZE_H - -#include "tensorflow/lite/kernels/internal/reference/integer_ops/dequantize.h" -#include "tensorflow/lite/kernels/internal/reference/reference_ops.h" - -namespace luci_interpreter_pal -{ - -template -static inline void Dequantize(tflite::DequantizationParams ¶ms, - const tflite::RuntimeShape &input_shape, const T *input_data, - const tflite::RuntimeShape &output_shape, float *output_data) -{ - tflite::reference_integer_ops::Dequantize(params, input_shape, input_data, output_shape, - output_data); -} - -static inline void Dequantize(tflite::DequantizationParams ¶ms, - const tflite::RuntimeShape &input_shape, const uint8_t *input_data, - const tflite::RuntimeShape &output_shape, float *output_data) -{ - tflite::reference_ops::Dequantize(params, input_shape, input_data, output_shape, output_data); -} - -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_DEQUANTIZE_H diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALQuantize.h b/compiler/luci-micro/luci-interpreter/pal/mcu/PALQuantize.h deleted file mode 100644 index 6046789..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/mcu/PALQuantize.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_QUANTIZE_H -#define LUCI_INTERPRETER_PAL_QUANTIZE_H - -#include "tensorflow/lite/kernels/internal/reference/reference_ops.h" - -namespace luci_interpreter_pal -{ -template -static inline void Quantize(tflite::QuantizationParams ¶ms, - const tflite::RuntimeShape &input_shape, const float *input_data, - const tflite::RuntimeShape &output_shape, T *output_data) -{ - tflite::reference_ops::AffineQuantize(params, input_shape, input_data, output_shape, output_data); -} - -template -static inline void Requantize(const Input *input_data, int32_t size, - int32_t effective_scale_multiplier, int32_t effective_scale_shift, - int32_t input_zero_point, int32_t output_zero_point, - Output *output_data) -{ - tflite::reference_ops::Requantize(input_data, size, effective_scale_multiplier, - effective_scale_shift, input_zero_point, output_zero_point, - output_data); -} -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_QUANTIZE_H diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALSVDF.h b/compiler/luci-micro/luci-interpreter/pal/mcu/PALSVDF.h deleted file mode 100644 index 3bba668..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/mcu/PALSVDF.h +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2020 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_SVDF_H -#define LUCI_INTERPRETER_PAL_SVDF_H - -#include - -namespace luci_interpreter_pal -{ -static inline void -IntegerSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, - const int8_t *input_data, const tflite::RuntimeShape &weight_feature_shape, - const int8_t *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, - const int16_t *weight_time_data, const tflite::RuntimeShape &bias_shape, - const int32_t *bias_data, int16_t *activation_state_data, - const tflite::RuntimeShape &output_shape, int8_t *output_data, int32_t *scratchpad_data, - int32_t *output_temp_data, int32_t scale_1_a, int scale_1_b, int32_t scale_2_a, - int scale_2_b, int32_t input_zp, int32_t output_zp) -{ - const int n_rank = params.rank; - const int n_batch = input_shape.Dims(0); - const int n_input = input_shape.Dims(1); - const int n_filter = weight_feature_shape.Dims(0); - const int n_unit = n_filter / n_rank; - const int n_memory = weight_time_shape.Dims(1); - - // Left shift the activation_state. - { - int16_t *new_state_start = activation_state_data; - const int16_t *old_state_start = activation_state_data + 1; - const int16_t *old_state_end = activation_state_data + n_batch * n_filter * n_memory; - while (old_state_start != old_state_end) - { - *new_state_start++ = *old_state_start++; - } - } - - // Note: no need to clear the latest activation, matmul is not accumulative. - - // Feature matmul. - { - const int32_t output_max = std::numeric_limits::max(); - const int32_t output_min = std::numeric_limits::min(); - int16_t *result_in_batch = activation_state_data + (n_memory - 1); - for (int b = 0; b < n_batch; b++) - { - const int8_t *matrix_ptr = weight_feature_data; - for (int r = 0; r < n_filter; r++) - { - int32_t dot_prod = 0; - const int8_t *vector_in_batch = input_data + b * n_input; - for (int c = 0; c < n_input; c++) - { - dot_prod += *matrix_ptr++ * (*vector_in_batch++ - input_zp); - } - dot_prod = tflite::MultiplyByQuantizedMultiplier(dot_prod, scale_1_a, scale_1_b); - dot_prod = std::min(std::max(output_min, dot_prod), output_max); - // This assumes state is symmetrically quantized. Otherwise last bit of - // state should be initialized to its zero point and accumulate the - // dot_prod. - // Equivalent as the following: - // result_in_batch = zero point, which happens to be zero. - // result_in_batch += dot_prod_56. - *result_in_batch = dot_prod; - result_in_batch += n_memory; - } - } - } - - // Time. - { - for (int b = 0; b < n_batch; ++b) - { - int32_t *scratch_ptr_batch = scratchpad_data + b * n_filter; - - // Perform batched vector dot product: - const int16_t *vector1_ptr = weight_time_data; - const int16_t *vector2_ptr = activation_state_data + b * n_memory * n_filter; - - for (int i = 0; i < n_filter; i++) - { - *scratch_ptr_batch = 0; - for (int j = 0; j < n_memory; j++) - { - *scratch_ptr_batch += *vector1_ptr++ * *vector2_ptr++; - } - scratch_ptr_batch++; - } - } - } - - // Reduce, add bias, rescale, activation. - { - // Add bias. - if (bias_data) - { - // Vector batch assign: - for (int i = 0; i < n_batch; ++i) - { - int32_t *output_ptr = output_temp_data + i * n_unit; - const int32_t *bias_ptr = bias_data; - for (int j = 0; j < n_unit; ++j) - { - *output_ptr++ = *bias_ptr++; - } - } - } - else - { - int32_t *output_ptr = output_temp_data; - for (int i = 0; i < n_batch * n_unit; ++i) - { - *output_ptr++ = 0; - } - } - - // Reduce. - for (int b = 0; b < n_batch; ++b) - { - int32_t *output_temp_ptr = output_temp_data + b * n_unit; - int32_t *scratch_ptr_batch = scratchpad_data + b * n_filter; - - // Reduction sum vector - for (int i = 0; i < n_unit; ++i) - { - for (int j = 0; j < n_rank; ++j) - { - output_temp_ptr[i] += *scratch_ptr_batch++; - } - } - } - - // Rescale. - const int32_t output_max = std::numeric_limits::max(); - const int32_t output_min = std::numeric_limits::min(); - for (int i = 0; i < n_batch * n_unit; ++i) - { - int32_t x1 = output_temp_data[i]; - int32_t x2 = tflite::MultiplyByQuantizedMultiplier(x1, scale_2_a, scale_2_b); - int32_t x3 = x2 + output_zp; - int32_t x4 = std::min(std::max(output_min, x3), output_max); - output_data[i] = static_cast(x4); - } - } -} -static inline void -FloatSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, - const float *input_data, const tflite::RuntimeShape &weight_feature_shape, - const float *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, - const float *weight_time_data, const tflite::RuntimeShape &bias_shape, - const float *bias_data, float *scratchpad_data, float *activation_state_data, - const tflite::RuntimeShape &output_shape, float *output_data) -{ - const int32_t rank = params.rank; - const int32_t batch_size = input_shape.Dims(0); - const int32_t input_size = input_shape.Dims(1); - const int32_t num_filters = weight_feature_shape.Dims(0); - const int32_t num_units = num_filters / rank; - const int32_t memory_size = weight_time_shape.Dims(1); - - // Left shift the activation_state. - { - float *new_state_start = activation_state_data; - const float *old_state_start = activation_state_data + 1; - const float *old_state_end = activation_state_data + batch_size * num_filters * memory_size; - while (old_state_start != old_state_end) - { - *new_state_start++ = *old_state_start++; - } - } - - // Note: no need to clear the latest activation, matmul is not accumulative. - - // Compute conv1d(inputs, weights_feature). - // The activation_state's rightmost column is used to save current cycle - // activation. This is achieved by starting at state_ptr[memory_size - 1] and - // having the stride equal to memory_size. - - // Perform batched matrix vector multiply operation: - { - const float *matrix = weight_feature_data; - const float *vector = input_data; - float *result = &activation_state_data[memory_size - 1]; - float *result_in_batch = result; - for (int i = 0; i < batch_size; ++i) - { - const float *matrix_ptr = matrix; - for (int j = 0; j < num_filters; ++j) - { - float dot_prod = 0.0f; - const float *vector_in_batch = vector + i * input_size; - for (int k = 0; k < input_size; ++k) - { - dot_prod += *matrix_ptr++ * *vector_in_batch++; - } - *result_in_batch = dot_prod; - result_in_batch += memory_size; - } - } - } - - tflite::reference_ops::ApplyTimeWeightsBiasAndActivation( - batch_size, memory_size, num_filters, num_units, rank, weight_time_data, bias_data, - params.activation, activation_state_data, scratchpad_data, output_data); -} - -static inline void SetupScratchpadTensor( - const luci_interpreter::DataType &input_data_type, - const luci_interpreter::DataType &weight_feature_data_type, - luci_interpreter::Tensor *scratchpad_1, luci_interpreter::Tensor *scratchpad_2, - luci_interpreter::Tensor *scratchpad_3, luci_interpreter::Tensor *scratchpad_4, - luci_interpreter::Tensor *scratchpad_5, luci_interpreter::Tensor *scratchpad_6, - const luci_interpreter::Shape input_shape, const luci_interpreter::Shape weight_time_shape, - const int32_t batch_size, const int32_t num_filters, const int32_t num_units) -{ - - if (input_data_type == loco::DataType::FLOAT32 && - (weight_feature_data_type == loco::DataType::S8 || - weight_feature_data_type == loco::DataType::U8)) - { - (void)input_shape; - (void)weight_time_shape; - (void)scratchpad_3; - (void)scratchpad_4; - (void)scratchpad_5; - (void)scratchpad_6; - - throw std::runtime_error("Hybrid type is not currently supported for mcu platform"); - } - - // Resize scratchpad_1 tensor - scratchpad_1->resize({batch_size, num_filters}); - - if (input_data_type == loco::DataType::S8) - { - // Resize scratchpad_2 for full_integer op - scratchpad_2->resize({batch_size, num_units}); - } -} - -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_SVDF_H diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALSoftmax.h b/compiler/luci-micro/luci-interpreter/pal/mcu/PALSoftmax.h deleted file mode 100644 index 9838b54..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/mcu/PALSoftmax.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_PAL_SOFTMAX_H -#define LUCI_INTERPRETER_PAL_SOFTMAX_H - -#include - -namespace luci_interpreter_pal -{ -static inline void PopulateSoftmaxLookupTable(tflite::SoftmaxParams *data, float input_scale, - float beta) -{ - // Do nothing for mcu - (void)data; - (void)input_scale; - (void)beta; -} - -static inline void InitializeParams(tflite::SoftmaxParams *params, float input_scale, float beta) -{ - int32 input_beta_multiplier; - int input_beta_left_shift; - static const int kScaledDiffIntegerBits = 5; - tflite::PreprocessSoftmaxScaling(beta, input_scale, kScaledDiffIntegerBits, - &input_beta_multiplier, &input_beta_left_shift); - - params->input_multiplier = input_beta_multiplier; - params->input_left_shift = input_beta_left_shift; - params->diff_min = - -tflite::CalculateInputRadius(kScaledDiffIntegerBits, params->input_left_shift); -} - -template -static inline void Softmax(const tflite::SoftmaxParams ¶ms, - const tflite::RuntimeShape &input_shape, const T *input_data, - const tflite::RuntimeShape &output_shape, T *output_data) -{ - // MARK: At this moment this operation doesn't support on mcu - assert(false && "Softmax NYI"); - (void)params; - (void)input_shape; - (void)input_data; - (void)output_shape; - (void)output_data; -} -} // namespace luci_interpreter_pal - -#endif // LUCI_INTERPRETER_PAL_SOFTMAX_H diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/pal.cmake b/compiler/luci-micro/luci-interpreter/pal/mcu/pal.cmake deleted file mode 100644 index 907d51d..0000000 --- a/compiler/luci-micro/luci-interpreter/pal/mcu/pal.cmake +++ /dev/null @@ -1,56 +0,0 @@ -macro(initialize_pal) - nnas_find_package(TensorFlowSource EXACT 2.6.0 QUIET) - nnas_find_package(TensorFlowGEMMLowpSource EXACT 2.6.0 QUIET) - nnas_find_package(TensorFlowEigenSource EXACT 2.6.0 QUIET) - nnas_find_package(TensorFlowRuySource EXACT 2.6.0 QUIET) - - if (NOT TensorFlowSource_FOUND) - message(STATUS "Skipping luci-interpreter: TensorFlow not found") - return() - endif () - - if (NOT TensorFlowGEMMLowpSource_FOUND) - message(STATUS "Skipping luci-interpreter: gemmlowp not found") - return() - endif () - - if (NOT TensorFlowEigenSource_FOUND) - message(STATUS "Skipping luci-interpreter: Eigen not found") - return() - endif () - - if (NOT TensorFlowRuySource_FOUND) - message(STATUS "Skipping luci-interpreter: Ruy not found") - return() - endif () - #find_package(Threads REQUIRED) - - set(PAL_INITIALIZED TRUE) -endmacro() - -macro(add_pal_to_target TGT) - target_include_directories(${TGT} PRIVATE "${PAL}") - target_include_directories(${TGT} PRIVATE - "${TensorFlowRuySource_DIR}" - "${TensorFlowGEMMLowpSource_DIR}" - "${TensorFlowEigenSource_DIR}" - "${TensorFlowSource_DIR}") - target_include_directories(${TGT} PRIVATE ${LUCI_INTERPRETER_PAL_DIR}) - - # TODO put it back, I changed my mind. - # instead add sources with visitors in this library - set(PAL_SOURCES ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/quantization_util.cc - ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/tensor_utils.cc - ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/reference/portable_tensor_utils.cc) - add_library(luci_interpreter_mcu_pal STATIC ${PAL_SOURCES}) - set_target_properties(luci_interpreter_mcu_pal PROPERTIES POSITION_INDEPENDENT_CODE ON) - target_include_directories(luci_interpreter_mcu_pal PRIVATE - "${TensorFlowRuySource_DIR}" - "${TensorFlowGEMMLowpSource_DIR}" - "${TensorFlowEigenSource_DIR}" - "${TensorFlowSource_DIR}" - ) - - target_link_libraries(${TGT} PRIVATE luci_interpreter_mcu_pal) - #target_link_libraries(${TGT} PRIVATE Threads::Threads luci_interpreter_mcu_pal) -endmacro() diff --git a/compiler/luci-micro/luci-interpreter/requires.cmake b/compiler/luci-micro/luci-interpreter/requires.cmake deleted file mode 100644 index f411f38..0000000 --- a/compiler/luci-micro/luci-interpreter/requires.cmake +++ /dev/null @@ -1 +0,0 @@ -require(luci) diff --git a/compiler/luci-micro/luci-interpreter/src/BuddyMemoryManager.cpp b/compiler/luci-micro/luci-interpreter/src/BuddyMemoryManager.cpp deleted file mode 100644 index 6ad1f32..0000000 --- a/compiler/luci-micro/luci-interpreter/src/BuddyMemoryManager.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "luci_interpreter/BuddyMemoryManager.h" - -namespace luci_interpreter -{ - -BuddyMemoryManager::BuddyMemoryManager(uint8_t *memory_start, int32_t memSize) -{ - int32_t p = lowerLog2(memSize); - - // We assume that the requested size of memory does not exceed 4 GB - assert(p < 32); - memSize = 1 << p; - - _start_block = reinterpret_cast(memory_start); - _start_block->size = memSize - sizeof(Block); - _start_block->is_free = true; - _start_block->self = _start_block; - _num_blocks = 0; - _size = _start_block->size; - - for (auto &_free_block : _free_blocks) - _free_block = nullptr; - - addToBlocks(_start_block, p); -} - -void BuddyMemoryManager::allocate_memory(luci_interpreter::Tensor &tensor) -{ - const size_t element_size = getDataTypeSize(tensor.element_type()); - const int32_t num_elements = tensor.shape().num_elements(); - auto size = num_elements * element_size; - auto footprint = size + sizeof(Block); - auto l = (footprint & (footprint - 1)) == 0 - ? lowerLog2(footprint) - : lowerLog2(footprint) + 1; // check footprint is pow_of_2 - - while (l < 32 && !_free_blocks[l]) - l++; - - assert(l < 32); - - Block *tmp; - tmp = _free_blocks[l]; - removeFromBlocks(tmp, l); - - while ((tmp->size + sizeof(Block)) / 2 >= size + sizeof(Block)) - { - divideBlock(tmp, l); - l--; - } - - tmp->is_free = false; - tmp->self = tmp; - _num_blocks++; - - auto *data = (uint8_t *)(tmp + 1); - tensor.set_data_buffer(data); -} - -void BuddyMemoryManager::release_memory(luci_interpreter::Tensor &tensor) -{ - auto data = tensor.data(); - auto *tmp = (Block *)((uint8_t *)data - sizeof(Block)); - - assert(tmp->self == tmp); - - tmp->is_free = true; - addToBlocks(tmp, lowerLog2(tmp->size + sizeof(Block))); - - while (tmp) - if (tmp->size == _size) - break; - else - tmp = mergeBlock(tmp); - - _num_blocks--; - tensor.set_data_buffer(nullptr); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/BuddyMemoryManager.test.cpp b/compiler/luci-micro/luci-interpreter/src/BuddyMemoryManager.test.cpp deleted file mode 100644 index 29fb767..0000000 --- a/compiler/luci-micro/luci-interpreter/src/BuddyMemoryManager.test.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "luci_interpreter/BuddyMemoryManager.h" -#include - -namespace luci_interpreter -{ -namespace -{ - -using namespace testing; - -TEST(BuddyMemoryManager, basic) -{ - auto mem_pool = std::make_unique(200); - auto buddy_memory_manager = std::make_unique(mem_pool.get(), 130); - Tensor first_tensor(DataType::U8, Shape({8}), AffineQuantization{}, "first_tensor"); - - buddy_memory_manager->allocate_memory(first_tensor); - - uint8_t data_1[] = {1, 2, 3, 4, 5, 6, 7, 8}; - - first_tensor.writeData(data_1, 8); - uint8_t array_1[8]; - first_tensor.readData(array_1, 8); - for (int i = 0; i < 8; i++) - { - EXPECT_EQ(data_1[i], array_1[i]); - } - - Tensor second_tensor(DataType::U8, Shape({2, 5}), AffineQuantization{}, "second_tensor"); - buddy_memory_manager->allocate_memory(second_tensor); - - uint8_t data_2[2][5] = {{11, 22, 33, 44, 55}, {12, 23, 34, 45, 56}}; - second_tensor.writeData(data_2, 10); - - uint8_t array_2[2][5]; - second_tensor.readData(array_2, 10); - for (int i = 0; i < 2; i++) - { - for (int j = 0; j < 5; j++) - { - EXPECT_EQ(data_2[i][j], array_2[i][j]); - } - } - - buddy_memory_manager->release_memory(first_tensor); - EXPECT_EQ(first_tensor.data(), nullptr); - - buddy_memory_manager->release_memory(second_tensor); - EXPECT_EQ(second_tensor.data(), nullptr); -} - -} // namespace -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/CMakeLists.txt b/compiler/luci-micro/luci-interpreter/src/CMakeLists.txt deleted file mode 100644 index 997b75a..0000000 --- a/compiler/luci-micro/luci-interpreter/src/CMakeLists.txt +++ /dev/null @@ -1,61 +0,0 @@ -include("${LUCI_INTERPRETER_PAL_DIR}/pal.cmake") - -initialize_pal() - -if (NOT PAL_INITIALIZED) - message("PAL Failed to initialize, skip luci-interpreter") - return() -endif() - -message(STATUS "LUCI INTERPRETER BEGIN") - -set(LUCI_INTERPRETER_BINARY "luci_interpreter${LUCI_INTERPRETER_SUFFIX}") -set(LUCI_INTERPRETER_CORE "luci_interpreter_core${LUCI_INTERPRETER_SUFFIX}") -set(LUCI_INTERPRETER_KERNELS "luci_interpreter_kernels${LUCI_INTERPRETER_SUFFIX}") -set(LUCI_INTERPRETER_LOADER "luci_interpreter_loader${LUCI_INTERPRETER_SUFFIX}") -set(LUCI_INTERPRETER_IMPORT "luci_interpreter_import${LUCI_INTERPRETER_SUFFIX}") - -add_subdirectory(core) -message(STATUS "LUCI INTERPRETER CORE") -add_subdirectory(kernels) -message(STATUS "LUCI INTERPRETER KERNELS") -add_subdirectory(loader) -message(STATUS "LUCI INTERPRETER LOADER") -add_subdirectory(import) -message(STATUS "LUCI INTERPRETER IMPORT") - -message(STATUS "LUCI INTERPTER INITALIZED") - -set(SOURCES - "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/Interpreter.h" - Interpreter.cpp "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/SimpleMemoryManager.h" SimpleMemoryManager.cpp - "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/TestMemoryManager.h" TestMemoryManager.cpp - "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/BuddyMemoryManager.h" BuddyMemoryManager.cpp - "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/StaticMemoryManager.h" StaticMemoryManager.cpp) - -if (NOT LUCI_INTERPRETER_STATIC) - add_library(${LUCI_INTERPRETER_BINARY} SHARED ${SOURCES}) -else () - add_library(${LUCI_INTERPRETER_BINARY} STATIC ${SOURCES}) -endif () - -set(TEST_SOURCES BuddyMemoryManager.test.cpp) - -target_include_directories(${LUCI_INTERPRETER_BINARY} PUBLIC "${LUCI_INTERPRETER_INCLUDE_DIR}") -target_include_directories(${LUCI_INTERPRETER_BINARY} PRIVATE "${LUCI_INTERPRETER_SOURCE_DIR}") -target_link_libraries(${LUCI_INTERPRETER_BINARY} - PUBLIC luci_lang ${LUCI_INTERPRETER_LOADER} ${LUCI_INTERPRETER_CORE} - PRIVATE nncc_common) - -install(TARGETS ${LUCI_INTERPRETER_BINARY} DESTINATION lib) -install(DIRECTORY include/ DESTINATION include - FILES_MATCHING PATTERN "*.h") - -if(NOT ENABLE_TEST) - return() -endif(NOT ENABLE_TEST) - -nnas_find_package(GTest REQUIRED) - -GTest_AddTest(buddy_manager_test ${TEST_SOURCES}) -target_link_libraries(buddy_manager_test ${LUCI_INTERPRETER_BINARY}) diff --git a/compiler/luci-micro/luci-interpreter/src/Interpreter.cpp b/compiler/luci-micro/luci-interpreter/src/Interpreter.cpp deleted file mode 100644 index 8cf272e..0000000 --- a/compiler/luci-micro/luci-interpreter/src/Interpreter.cpp +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "luci_interpreter/Interpreter.h" -#include "luci_interpreter/SimpleMemoryManager.h" - -#include "loader/ModuleLoader.h" - -#include - -namespace luci_interpreter -{ - -namespace -{ - -class EventNotifierImpl final : public EventNotifier -{ -public: - EventNotifierImpl(const RuntimeToIR &runtime_to_ir, - const std::vector &observers) - : _runtime_to_ir(runtime_to_ir), _observers(observers) - { - } - - void postTensorWrite(const Tensor *tensor) override - { - assert(tensor != nullptr); - for (const auto &observer : _observers) - { - observer->postTensorWrite(_runtime_to_ir.tensor_to_node.at(tensor), tensor); - } - } - - void preOperatorExecute(const Kernel *kernel) override - { - assert(kernel != nullptr); - for (const auto &observer : _observers) - { - observer->preOperatorExecute(_runtime_to_ir.kernel_to_node.at(kernel)); - } - } - - void postOperatorExecute(const Kernel *kernel) override - { - assert(kernel != nullptr); - for (const auto &observer : _observers) - { - observer->postOperatorExecute(_runtime_to_ir.kernel_to_node.at(kernel)); - } - } - -private: - const RuntimeToIR &_runtime_to_ir; - const std::vector &_observers; -}; - -} // namespace - -Interpreter::Interpreter(const luci::Module *module) -{ - _runtime_to_ir = std::make_unique(); - _event_notifier = std::make_unique(*_runtime_to_ir, _observers); - _runtime_module = std::make_unique(_event_notifier.get()); - - _default_memory_manager = std::make_unique(); - - ModuleLoader loader(module, _runtime_module.get(), *_runtime_to_ir, _node_to_tensor, - _default_memory_manager.get()); - loader.load(); -} - -Interpreter::Interpreter(const luci::Module *module, - luci_interpreter::IMemoryManager *memory_manager) -{ - assert(memory_manager && "Use Interpreter::Interpreter(module) constructor instead"); - - _runtime_to_ir = std::make_unique(); - _event_notifier = std::make_unique(*_runtime_to_ir, _observers); - _runtime_module = std::make_unique(_event_notifier.get()); - - ModuleLoader loader(module, _runtime_module.get(), *_runtime_to_ir, _node_to_tensor, - memory_manager); - loader.load(); -} - -Interpreter::~Interpreter() = default; - -void Interpreter::writeInputTensor(const luci::CircleInput *input_node, const void *data, - size_t data_size) -{ - Tensor *tensor = _runtime_module->getInputTensors()[input_node->index()]; - if (tensor == nullptr) - { - const std::string &name = input_node->name(); - throw std::runtime_error("Cannot find tensor for input node named \"" + name + "\"."); - } - if (data != nullptr) - tensor->writeData(data, data_size); -} - -void Interpreter::readOutputTensor(const luci::CircleOutput *output_node, void *data, - size_t data_size) -{ - Tensor *tensor = _runtime_module->getOutputTensors()[output_node->index()]; - if (tensor == nullptr) - { - const std::string &name = output_node->name(); - throw std::runtime_error("Cannot find tensor for output node named \"" + name + "\"."); - } - if (data != nullptr) - tensor->readData(data, data_size); -} - -void Interpreter::interpret() { _runtime_module->execute(); } - -void Interpreter::attachObserver(ExecutionObserver *observer) -{ - if (std::find(_observers.cbegin(), _observers.cend(), observer) != _observers.cend()) - throw std::runtime_error("Observer is already attached."); - _observers.push_back(observer); -} - -ExecutionObserver::~ExecutionObserver() = default; - -void ExecutionObserver::postTensorWrite(const luci::CircleNode *, const Tensor *) {} - -void ExecutionObserver::preOperatorExecute(const luci::CircleNode *) {} - -void ExecutionObserver::postOperatorExecute(const luci::CircleNode *) {} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/SimpleMemoryManager.cpp b/compiler/luci-micro/luci-interpreter/src/SimpleMemoryManager.cpp deleted file mode 100644 index 230e398..0000000 --- a/compiler/luci-micro/luci-interpreter/src/SimpleMemoryManager.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "luci_interpreter/SimpleMemoryManager.h" - -namespace luci_interpreter -{ - -void SimpleMemoryManager::allocate_memory(luci_interpreter::Tensor &tensor) -{ - if (!tensor.is_allocatable()) - { - return; - } - if (tensor.is_data_allocated()) - { - release_memory(tensor); - } - const auto element_size = getDataTypeSize(tensor.element_type()); - const auto num_elements = tensor.shape().num_elements(); - - auto *data = new uint8_t[num_elements * element_size]; - tensor.set_data_buffer(data); -} - -void SimpleMemoryManager::release_memory(luci_interpreter::Tensor &tensor) -{ - if (!tensor.is_data_allocated()) - { - tensor.set_data_buffer(nullptr); - return; - } - auto data = tensor.data(); - delete[] data; - tensor.set_data_buffer(nullptr); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/StaticMemoryManager.cpp b/compiler/luci-micro/luci-interpreter/src/StaticMemoryManager.cpp deleted file mode 100644 index 73a8199..0000000 --- a/compiler/luci-micro/luci-interpreter/src/StaticMemoryManager.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "luci_interpreter/StaticMemoryManager.h" - -namespace luci_interpreter -{ - -void StaticMemoryManager::allocate_memory(luci_interpreter::Tensor &tensor) -{ - if (!tensor.is_allocatable()) - { - return; - } - int32_t offset = tensor.get_offset(); - assert(offset >= 0); - auto tensor_ptr = _buffer_ptr + offset; - tensor.set_data_buffer(tensor_ptr); -} - -void StaticMemoryManager::release_memory(luci_interpreter::Tensor &tensor) -{ - tensor.set_data_buffer(nullptr); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/TestMemoryManager.cpp b/compiler/luci-micro/luci-interpreter/src/TestMemoryManager.cpp deleted file mode 100644 index 3beeee5..0000000 --- a/compiler/luci-micro/luci-interpreter/src/TestMemoryManager.cpp +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "luci_interpreter/TestMemoryManager.h" - -namespace luci_interpreter -{ - -void TestMemoryManager::allocate_memory(luci_interpreter::Tensor &tensor) -{ - if (!tensor.is_allocatable()) - { - return; - } - if (tensor.is_data_allocated()) - { - release_memory(tensor); - } - const auto element_size = getDataTypeSize(tensor.element_type()); - const auto num_elements = tensor.shape().num_elements(); - - auto *data = new uint8_t[num_elements * element_size]; - allocations.push_back(data); - tensor.set_data_buffer(data); -} - -void TestMemoryManager::release_memory(luci_interpreter::Tensor &tensor) -{ - tensor.set_data_buffer(nullptr); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/core/CMakeLists.txt b/compiler/luci-micro/luci-interpreter/src/core/CMakeLists.txt deleted file mode 100644 index c2471e0..0000000 --- a/compiler/luci-micro/luci-interpreter/src/core/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -set(SOURCES - "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/core/DataType.h" - "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/core/Tensor.h" - EventNotifier.h - Kernel.h - KernelParams.h - RuntimeGraph.h - RuntimeGraph.cpp - RuntimeModule.h - Tensor.cpp) - -add_library(${LUCI_INTERPRETER_CORE} STATIC ${SOURCES}) -if (NOT NNCC_LIBRARY_NO_PIC) - set_target_properties(${LUCI_INTERPRETER_CORE} PROPERTIES POSITION_INDEPENDENT_CODE ON) -endif(NOT NNCC_LIBRARY_NO_PIC) -target_include_directories(${LUCI_INTERPRETER_CORE} PUBLIC "${LUCI_INTERPRETER_INCLUDE_DIR}") -target_include_directories(${LUCI_INTERPRETER_CORE} PUBLIC "${LUCI_INTERPRETER_SOURCE_DIR}") -target_link_libraries(${LUCI_INTERPRETER_CORE} PUBLIC luci_lang) -target_link_libraries(${LUCI_INTERPRETER_CORE} PRIVATE nncc_common) diff --git a/compiler/luci-micro/luci-interpreter/src/core/EventNotifier.h b/compiler/luci-micro/luci-interpreter/src/core/EventNotifier.h deleted file mode 100644 index 5c4fbd3..0000000 --- a/compiler/luci-micro/luci-interpreter/src/core/EventNotifier.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_CORE_EVENTNOTIFIER_H -#define LUCI_INTERPRETER_CORE_EVENTNOTIFIER_H - -namespace luci_interpreter -{ - -// Used at execution stage to tell the interpreter that the runtime state has changed in some way. -class EventNotifier -{ -public: - virtual ~EventNotifier() = default; - - virtual void postTensorWrite(const Tensor *tensor) = 0; - virtual void preOperatorExecute(const Kernel *kernel) = 0; - virtual void postOperatorExecute(const Kernel *kernel) = 0; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_CORE_EVENTNOTIFIER_H diff --git a/compiler/luci-micro/luci-interpreter/src/core/Kernel.h b/compiler/luci-micro/luci-interpreter/src/core/Kernel.h deleted file mode 100644 index a7c4a42..0000000 --- a/compiler/luci-micro/luci-interpreter/src/core/Kernel.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_CORE_KERNEL_H -#define LUCI_INTERPRETER_CORE_KERNEL_H - -#include "luci_interpreter/core/Tensor.h" - -#include - -namespace luci_interpreter -{ - -// Base class for all kernels. -class Kernel -{ -protected: - Kernel(std::vector inputs, std::vector outputs) - : _inputs(std::move(inputs)), _outputs(std::move(outputs)) - { - } - -public: - virtual ~Kernel() = default; - - const std::vector &getInputTensors() const { return _inputs; } - const std::vector &getOutputTensors() const { return _outputs; } - - // Configures the kernel. - // This function is currently called once for each kernel during interpreter construction, - // which makes it a convenient place for preparing (resizing) output tensors. - virtual void configure() = 0; - - // Executes the kernel. - virtual void execute() const = 0; - -protected: - // NOTE Prefer not to use these in derived classes. - const std::vector _inputs; - const std::vector _outputs; -}; - -// Base class for kernels with parameters. -template class KernelWithParams : public Kernel -{ -protected: - KernelWithParams(std::vector inputs, std::vector outputs, - const Params ¶ms) - : Kernel(std::move(inputs), std::move(outputs)), _params(params) - { - } - -public: - const Params ¶ms() const { return _params; } - -protected: - const Params _params; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_CORE_KERNEL_H diff --git a/compiler/luci-micro/luci-interpreter/src/core/KernelParams.h b/compiler/luci-micro/luci-interpreter/src/core/KernelParams.h deleted file mode 100644 index 6c0220c..0000000 --- a/compiler/luci-micro/luci-interpreter/src/core/KernelParams.h +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_CORE_KERNELPARAMS_H -#define LUCI_INTERPRETER_CORE_KERNELPARAMS_H - -#include -#include -#include -#include - -#include -#include - -namespace luci_interpreter -{ - -// Inject commonly used types into `luci_interpreter` namespace for convenience. -using Activation = luci::FusedActFunc; -using Padding = luci::Padding; -using MirrorPadMode = luci::MirrorPadMode; - -struct AddParams -{ - Activation activation; -}; - -struct ArgMaxParams -{ - DataType output_type; -}; - -struct BatchMatMulParams -{ - bool adj_x; - bool adj_y; -}; - -struct ConcatenationParams -{ - int axis; - Activation activation; -}; - -struct Conv2DParams -{ - Padding padding; - int32_t stride_height; - int32_t stride_width; - int32_t dilation_height_factor; - int32_t dilation_width_factor; - Activation activation; -}; - -struct DepthToSpaceParams -{ - int block_size; -}; - -struct DepthwiseConv2DParams -{ - Padding padding; - int32_t depth_multiplier; // TODO Remove, as it can be calculated. - int32_t stride_height; - int32_t stride_width; - int32_t dilation_height_factor; - int32_t dilation_width_factor; - Activation activation; -}; - -struct DivParams -{ - Activation activation; -}; - -struct FullyConnectedParams -{ - Activation activation; - bool keep_num_dims = false; -}; - -struct GatherParams -{ - int32_t axis; - int32_t batch_dims; -}; - -struct InstanceNormParams -{ - float epsilon; - Activation activation; -}; - -struct L2NormParams -{ - Activation activation; -}; - -struct LeakyReluParams -{ - float alpha; -}; - -struct LocalResponseNormalizationParams -{ - int32_t radius; - float bias; - float alpha; - float beta; -}; - -struct MirrorPadParams -{ - MirrorPadMode mode; -}; - -struct MulParams -{ - Activation activation; -}; - -struct OneHotParams -{ - int32_t axis; -}; - -struct PackParams -{ - int32_t values_count; - int32_t axis; -}; - -struct Pool2DParams -{ - Padding padding; - int32_t filter_height; - int32_t filter_width; - int32_t stride_height; - int32_t stride_width; - Activation activation; -}; - -struct ReducerParams -{ - bool keep_dims; -}; - -struct ResizeBilinearParams -{ - bool align_corners; - bool half_pixel_centers; -}; - -struct ResizeNearestNeighborParams -{ - bool align_corners; - bool half_pixel_centers; -}; - -struct ShapeParams -{ - loco::DataType out_type; -}; - -struct SubParams -{ - Activation activation; -}; - -struct SVDFParams -{ - bool asymmetric_quantize_inputs; - int32_t svdf_rank; - Activation activation; -}; - -struct SpaceToDepthParams -{ - int block_size; -}; - -struct SoftmaxParams -{ - float beta; -}; - -struct StridedSliceParams -{ - int32_t begin_mask; - int32_t end_mask; - int32_t ellipsis_mask; - int32_t new_axis_mask; - int32_t shrink_axis_mask; -}; - -struct SqueezeParams -{ - std::vector squeeze_dims; -}; - -struct TransposeConvParams -{ - Padding padding; - int32_t stride_height; - int32_t stride_width; -}; - -struct UnpackParams -{ - int axis; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_CORE_KERNELPARAMS_H diff --git a/compiler/luci-micro/luci-interpreter/src/core/RuntimeGraph.cpp b/compiler/luci-micro/luci-interpreter/src/core/RuntimeGraph.cpp deleted file mode 100644 index c2f8d2e..0000000 --- a/compiler/luci-micro/luci-interpreter/src/core/RuntimeGraph.cpp +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "core/RuntimeGraph.h" - -#include "core/RuntimeModule.h" - -#include -#include - -namespace luci_interpreter -{ - -class RuntimeGraph::TensorAllocPlan -{ - std::vector> _alloc_plan; - std::vector> _dealloc_plan; - bool _valid = false; - IMemoryManager *_memory_manager; - -public: - explicit TensorAllocPlan(IMemoryManager *memory_manager); - void invalidate() { _valid = false; } - bool isValid() const { return _valid; } - void build(const RuntimeGraph &graph); - void allocate(size_t kernel_index) const; - void deallocate(size_t kernel_index) const; -}; - -RuntimeGraph::TensorAllocPlan::TensorAllocPlan(IMemoryManager *memory_manager) - : _memory_manager(memory_manager) -{ -} - -void RuntimeGraph::TensorAllocPlan::build(const RuntimeGraph &graph) -{ - invalidate(); - using Lifetime = std::pair; - std::unordered_map lifetimes; - const size_t num_kernels = graph._kernels.size(); - for (size_t index = 0; index < num_kernels; ++index) - { - const auto &kernel = graph._kernels[index]; - for (const Tensor *tensor : kernel->getInputTensors()) - { - auto nc_tensor = const_cast(tensor); - if (lifetimes.count(nc_tensor) > 0) - lifetimes.at(nc_tensor).second = index; - } - for (Tensor *tensor : kernel->getOutputTensors()) - { - assert(lifetimes.count(tensor) == 0); - lifetimes[tensor] = Lifetime(index, index); - } - } - for (const Tensor *tensor : graph.getOutputTensors()) - { - auto nc_tensor = const_cast(tensor); - if (lifetimes.count(nc_tensor) > 0) - lifetimes.at(nc_tensor).second = num_kernels; - } - _alloc_plan.assign(num_kernels, std::vector()); - _dealloc_plan.assign(num_kernels + 1, std::vector()); - for (const auto &item : lifetimes) - { - _alloc_plan[item.second.first].push_back(item.first); - _dealloc_plan[item.second.second].push_back(item.first); - } - _valid = true; -} - -void RuntimeGraph::TensorAllocPlan::allocate(size_t kernel_index) const -{ - assert(_valid && kernel_index < _alloc_plan.size()); - for (Tensor *tensor : _alloc_plan[kernel_index]) - { - _memory_manager->allocate_memory(*tensor); - } -} - -void RuntimeGraph::TensorAllocPlan::deallocate(size_t kernel_index) const -{ - assert(_valid && kernel_index < _dealloc_plan.size()); - for (Tensor *tensor : _dealloc_plan[kernel_index]) - { - _memory_manager->release_memory(*tensor); - } -} - -RuntimeGraph::RuntimeGraph(RuntimeModule *owning_module, IMemoryManager *memory_manager) - : _owning_module(owning_module), _memory_manager(memory_manager), - _tensor_alloc_plan(std::make_unique(memory_manager)) -{ -} - -RuntimeGraph::~RuntimeGraph() -{ - for (auto &tensor : _tensors) - { - if (tensor->is_data_allocated()) - _memory_manager->release_memory(*tensor); - } -} - -Tensor *RuntimeGraph::addTensor(std::unique_ptr &&tensor) -{ - assert(tensor != nullptr); - _tensors.push_back(std::move(tensor)); - return _tensors.back().get(); -} - -void RuntimeGraph::setInputTensors(const std::vector &input_tensors) -{ - assert(std::all_of(input_tensors.cbegin(), input_tensors.cend(), - [](Tensor *tensor) { return tensor != nullptr; })); - _input_tensors = input_tensors; -} - -void RuntimeGraph::setOutputTensors(const std::vector &output_tensors) -{ - assert(std::all_of(output_tensors.cbegin(), output_tensors.cend(), - [](Tensor *tensor) { return tensor != nullptr; })); - _output_tensors = output_tensors; -} - -void RuntimeGraph::configureAllocations(Tensor *tensor) -{ - _memory_manager->allocate_memory(*tensor); -} - -void RuntimeGraph::addKernel(std::unique_ptr &&kernel) -{ - assert(kernel != nullptr); - _kernels.push_back(std::move(kernel)); - _tensor_alloc_plan->invalidate(); -} - -void RuntimeGraph::execute() const -{ - if (!_tensor_alloc_plan->isValid()) - _tensor_alloc_plan->build(*this); - - EventNotifier *event_notifier = _owning_module->getEventNotifier(); - - // Notify the observers that the input tensors have changed. - if (event_notifier != nullptr) - { - for (const Tensor *input_tensor : getInputTensors()) - { - if (input_tensor->is_observable()) - event_notifier->postTensorWrite(input_tensor); - } - } - - for (size_t index = 0; index < _kernels.size(); ++index) - { - const auto &kernel = _kernels[index]; - if (event_notifier != nullptr) - { - event_notifier->preOperatorExecute(kernel.get()); - } - - // TODO The `configure` method should only be called if the outputs of an operator need to be - // resized. - kernel->configure(); - - // Preallocate outputs in advance instead of relying on automatic allocation - _tensor_alloc_plan->allocate(index); - - kernel->execute(); - - if (event_notifier != nullptr) - { - event_notifier->postOperatorExecute(kernel.get()); - } - - for (const Tensor *tensor : kernel->getOutputTensors()) - { - if (event_notifier != nullptr && tensor->is_observable()) - { - event_notifier->postTensorWrite(tensor); - } - } - _tensor_alloc_plan->deallocate(index); - } -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/core/RuntimeGraph.h b/compiler/luci-micro/luci-interpreter/src/core/RuntimeGraph.h deleted file mode 100644 index 8184e24..0000000 --- a/compiler/luci-micro/luci-interpreter/src/core/RuntimeGraph.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_CORE_RUNTIMEGRAPH_H -#define LUCI_INTERPRETER_CORE_RUNTIMEGRAPH_H - -#include "luci_interpreter/core/Tensor.h" -#include "luci_interpreter/MemoryManager.h" -#include "core/Kernel.h" - -#include -#include - -namespace luci_interpreter -{ - -class RuntimeModule; - -class RuntimeGraph -{ -private: - class TensorAllocPlan; - friend class TensorAllocPlan; - -public: - explicit RuntimeGraph(RuntimeModule *owning_module, IMemoryManager *memory_manager); - ~RuntimeGraph(); - - Tensor *addTensor(std::unique_ptr &&tensor); - - void setInputTensors(const std::vector &input_tensors); - void setOutputTensors(const std::vector &output_tensors); - - void configureAllocations(Tensor *tensor); - - const std::vector &getInputTensors() const { return _input_tensors; } - const std::vector &getOutputTensors() const { return _output_tensors; } - - void addKernel(std::unique_ptr &&kernel); - - void execute() const; - -private: - IMemoryManager *_memory_manager; - RuntimeModule *_owning_module; - std::vector> _tensors; - std::vector _input_tensors; - std::vector _output_tensors; - - // Kernels in execution order. - std::vector> _kernels; - // Tensors that are not used anymore after given op - std::unique_ptr _tensor_alloc_plan; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_CORE_RUNTIMEGRAPH_H diff --git a/compiler/luci-micro/luci-interpreter/src/core/RuntimeModule.h b/compiler/luci-micro/luci-interpreter/src/core/RuntimeModule.h deleted file mode 100644 index 78873b0..0000000 --- a/compiler/luci-micro/luci-interpreter/src/core/RuntimeModule.h +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_CORE_RUNTIMEMODULE_H -#define LUCI_INTERPRETER_CORE_RUNTIMEMODULE_H - -#include "core/RuntimeGraph.h" -#include "core/EventNotifier.h" -#include "luci_interpreter/MemoryManager.h" - -#include -#include - -namespace luci_interpreter -{ - -class RuntimeModule -{ -public: - explicit RuntimeModule(EventNotifier *event_notifier) : _event_notifier(event_notifier) {} - - EventNotifier *getEventNotifier() const { return _event_notifier; } - - RuntimeGraph *addGraph(IMemoryManager *memory_manager) - { - _graphs.push_back(std::make_unique(this, memory_manager)); - return _graphs.back().get(); - } - - const std::vector &getInputTensors() const { return getMainGraph()->getInputTensors(); } - const std::vector &getOutputTensors() const - { - return getMainGraph()->getOutputTensors(); - } - - void execute() const { getMainGraph()->execute(); } - -private: - RuntimeGraph *getMainGraph() const { return _graphs[0].get(); } - - EventNotifier *const _event_notifier; - std::vector> _graphs; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_CORE_RUNTIMEMODULE_H diff --git a/compiler/luci-micro/luci-interpreter/src/core/Tensor.cpp b/compiler/luci-micro/luci-interpreter/src/core/Tensor.cpp deleted file mode 100644 index 3c3c5ff..0000000 --- a/compiler/luci-micro/luci-interpreter/src/core/Tensor.cpp +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "luci_interpreter/core/Tensor.h" - -#include -#include - -namespace luci_interpreter -{ - -Tensor::Tensor(DataType element_type, Shape shape, AffineQuantization quantization, - std::string name) - : _element_type(element_type), _shape(std::move(shape)), _quantization(std::move(quantization)), - _name(std::move(name)), _data_allocated(false) -{ -} - -void Tensor::readData(void *data_ptr, size_t data_size) const -{ - const size_t element_size = getDataTypeSize(element_type()); - const int32_t num_elements = shape().num_elements(); - if (data_size != num_elements * element_size) - { - throw std::invalid_argument("Invalid data size."); - } - assert(data_ptr != nullptr); - std::memcpy(data_ptr, data(), data_size); -} - -void Tensor::writeData(const void *data_ptr, size_t data_size) -{ - const size_t element_size = getDataTypeSize(element_type()); - const int32_t num_elements = shape().num_elements(); - if (data_size != num_elements * element_size) - { - throw std::invalid_argument("Invalid data size."); - } - assert(data_ptr != nullptr); - std::memcpy(data(), data_ptr, data_size); -} - -void Tensor::resize(const Shape &new_shape) { _shape = new_shape; } - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/import/CMakeLists.txt b/compiler/luci-micro/luci-interpreter/src/import/CMakeLists.txt deleted file mode 100644 index dd9733f..0000000 --- a/compiler/luci-micro/luci-interpreter/src/import/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -set(SOURCES - "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/GraphBuilderRegistry.h" - GraphBuilderRegistry.cpp) - -# include specific builders -file(GLOB_RECURSE NODES "Nodes/*") -list(APPEND SOURCES ${NODES}) - -add_library(${LUCI_INTERPRETER_IMPORT} STATIC ${SOURCES}) -if (NOT NNCC_LIBRARY_NO_PIC) - set_target_properties(${LUCI_INTERPRETER_IMPORT} PROPERTIES POSITION_INDEPENDENT_CODE ON) -endif(NOT NNCC_LIBRARY_NO_PIC) - -target_include_directories(${LUCI_INTERPRETER_IMPORT} PUBLIC "${LUCI_INTERPRETER_INCLUDE_DIR}") -target_link_libraries(${LUCI_INTERPRETER_IMPORT} PUBLIC luci_import) diff --git a/compiler/luci-micro/luci-interpreter/src/import/GraphBuilderRegistry.cpp b/compiler/luci-micro/luci-interpreter/src/import/GraphBuilderRegistry.cpp deleted file mode 100644 index a33bca6..0000000 --- a/compiler/luci-micro/luci-interpreter/src/import/GraphBuilderRegistry.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include "luci_interpreter/GraphBuilderRegistry.h" -#include "Nodes/CircleReferencingConst.h" - -namespace luci_interpreter -{ - -std::unique_ptr source_without_constant_copying() -{ - auto builder = std::make_unique(); - { - // redefine NodeBuilder of BUFFER type - builder->add(std::make_unique()); - } - - return builder; -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/import/Nodes/CircleReferencingConst.cpp b/compiler/luci-micro/luci-interpreter/src/import/Nodes/CircleReferencingConst.cpp deleted file mode 100644 index 14e90f2..0000000 --- a/compiler/luci-micro/luci-interpreter/src/import/Nodes/CircleReferencingConst.cpp +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "CircleReferencingConst.h" - -#include - -namespace -{ - -// helper struct which describes data loaded to custom_options of CircleReferencingConst node -struct ConstDataReference -{ - const uint8_t *data = nullptr; - uint32_t size = 0; -}; - -} // namespace - -namespace luci_interpreter -{ -using namespace luci; - -CircleNode *CircleReferencingConstNodeBuilder::build(TensorIndex tensor_index, - GraphBuilderContext *context) const -{ - assert(tensor_index >= 0); - - const auto graph = context->graph(); - const auto reader = context->reader(); - const auto tensors = reader->tensors(); - auto const const_tensor = tensors[tensor_index]; - assert(const_tensor != nullptr); - if (const_tensor->is_variable()) - { - // Create CircleVariable for variable - return nullptr; - } - - auto const buffer = wrap(reader->buffers()[const_tensor->buffer()]->data()); - auto const const_dims = wrap(const_tensor->shape()); // in NHWC - if (const_dims.empty() && buffer.empty()) - { - // unknown shape tensor and scalar tensor - return nullptr; - } - - // if tensor_index is used as output to some other operator, this is not a constant - auto tensoroutputs = context->tensoroutputs(); - if (tensoroutputs->find(tensor_index)) - { - // other operator output tensor - return nullptr; - } - - uint32_t num_elements = 1; - for (uint32_t r = 0; r < const_dims.size(); ++r) - { - num_elements = num_elements * const_dims[r]; - } - - if (buffer.empty() && num_elements > 0) - { - // normal empty tensor - return nullptr; - } - - // create CircleReferencingConst - auto custom_node = graph->nodes()->create(0, 1); - { - custom_node->custom_code("CircleReferencingConst"); - - copy_tensor_attributes(const_tensor, custom_node); - custom_node->shape_status(luci::ShapeStatus::VALID); - - // custom options stores size of buffer and pointer's value to buffer's data - { - std::vector custom_options(sizeof(ConstDataReference)); - { - auto &const_data_ref = *reinterpret_cast(custom_options.data()); - const_data_ref = {buffer.data(), buffer.size()}; - } - custom_node->custom_options(custom_options); - } - } - - // Output of CircleCustom node presented with CircleConstNode - auto out_node = graph->nodes()->create(); - { - out_node->index(0); - out_node->input(custom_node); - - copy_tensor_attributes(const_tensor, out_node); - out_node->shape_status(luci::ShapeStatus::VALID); - } - - return out_node; -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/import/Nodes/CircleReferencingConst.h b/compiler/luci-micro/luci-interpreter/src/import/Nodes/CircleReferencingConst.h deleted file mode 100644 index ed8f951..0000000 --- a/compiler/luci-micro/luci-interpreter/src/import/Nodes/CircleReferencingConst.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __LUCI_INTERPRETER_IMPORT_OP_CIRCLE_REFERENCING_CONST_H__ -#define __LUCI_INTERPRETER_IMPORT_OP_CIRCLE_REFERENCING_CONST_H__ - -#include - -#include - -namespace luci_interpreter -{ -using namespace luci; - -/** - * @brief Builder creates CircleCustom node with pointer to constants data from Tensor with buffer. - */ -class CircleReferencingConstNodeBuilder : public TypedNodeBuilder -{ -public: - CircleNode *build(TensorIndex tensor_index, GraphBuilderContext *ctx) const final; -}; - -} // namespace luci_interpreter - -#endif // __LUCI_INTERPRETER_IMPORT_OP_CIRCLE_REFERENCING_CONST_H__ diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Add.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Add.cpp deleted file mode 100644 index d7bf308..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Add.cpp +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Add.h" - -#include "kernels/BinaryOpCommon.h" -#include "kernels/Utils.h" - -#include -#include - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Add::Add(const Tensor *input1, const Tensor *input2, Tensor *output, const AddParams ¶ms) - : KernelWithParams({input1, input2}, {output}, params) -{ -} - -void Add::configure() -{ - LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); - LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()); - if (input1()->element_type() == DataType::S16) - { - LUCI_INTERPRETER_CHECK(input1()->zero_points().size() == 1 && - input2()->zero_points().size() == 1); - LUCI_INTERPRETER_CHECK(input1()->zero_point() == 0 && input2()->zero_point() == 0 && - output()->zero_point() == 0); - } - - output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); -} - -void Add::execute() const -{ - switch (input1()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::S64: - evalInteger(); - break; - case DataType::S32: - evalInteger(); - break; - case DataType::U8: - evalQuantized(); - break; - case DataType::S16: - evalQuantizedS16(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Add::evalFloat() const -{ - tflite::ArithmeticParams params{}; - fillArithmeticActivationRange(params, _params.activation); - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - tflite::reference_ops::BroadcastAdd4DSlow( - params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), - getTensorData(input2()), getTensorShape(output()), getTensorData(output())); - } - else - { - tflite::reference_ops::Add(params, getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -template void Add::evalInteger() const -{ - tflite::ArithmeticParams params{}; - fillArithmeticActivationRange(params, _params.activation); - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - tflite::reference_ops::BroadcastAdd4DSlow( - params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), - getTensorData(input2()), getTensorShape(output()), getTensorData(output())); - } - else - { - tflite::reference_ops::Add(params, getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -void Add::evalQuantized() const -{ - const auto input1_scale = static_cast(input1()->scale()); - const auto input2_scale = static_cast(input2()->scale()); - const auto output_scale = static_cast(output()->scale()); - - const int left_shift = 20; - const double twice_max_input_scale = 2 * std::max(input1_scale, input2_scale); - const double real_input1_multiplier = input1_scale / twice_max_input_scale; - const double real_input2_multiplier = input2_scale / twice_max_input_scale; - const double real_output_multiplier = twice_max_input_scale / ((1 << left_shift) * output_scale); - - int32_t input1_multiplier{}, input2_multiplier{}, output_multiplier{}; - int input1_shift{}, input2_shift{}, output_shift{}; - quantizeMultiplierSmallerThanOneExp(real_input1_multiplier, &input1_multiplier, &input1_shift); - quantizeMultiplierSmallerThanOneExp(real_input2_multiplier, &input2_multiplier, &input2_shift); - quantizeMultiplierSmallerThanOneExp(real_output_multiplier, &output_multiplier, &output_shift); - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - tflite::ArithmeticParams params{}; - params.left_shift = left_shift; - // The kernel expects inputs' zero points to be negated. - params.input1_offset = -input1()->zero_point(); // Note the '-'. - params.input1_multiplier = input1_multiplier; - params.input1_shift = input1_shift; - params.input2_offset = -input2()->zero_point(); // Note the '-'. - params.input2_multiplier = input2_multiplier; - params.input2_shift = input2_shift; - params.output_offset = output()->zero_point(); - params.output_multiplier = output_multiplier; - params.output_shift = output_shift; - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - tflite::reference_ops::BroadcastAdd4DSlow( - params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), - getTensorData(input2()), getTensorShape(output()), getTensorData(output())); - } - else - { - tflite::reference_ops::Add(params, getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -void Add::evalQuantizedS16() const -{ - const auto input1_scale = static_cast(input1()->scale()); - const auto input2_scale = static_cast(input2()->scale()); - const auto output_scale = static_cast(output()->scale()); - - constexpr int left_shift = 12; - const double twice_max_input_scale = 2 * std::max(input1_scale, input2_scale); - const double real_input1_multiplier = input1_scale / twice_max_input_scale; - const double real_input2_multiplier = input2_scale / twice_max_input_scale; - const double real_output_multiplier = twice_max_input_scale / ((1 << left_shift) * output_scale); - - int32_t input1_multiplier{}, input2_multiplier{}, output_multiplier{}; - int input1_shift{}, input2_shift{}, output_shift{}; - quantizeMultiplierSmallerThanOneExp(real_input1_multiplier, &input1_multiplier, &input1_shift); - quantizeMultiplierSmallerThanOneExp(real_input2_multiplier, &input2_multiplier, &input2_shift); - quantizeMultiplierSmallerThanOneExp(real_output_multiplier, &output_multiplier, &output_shift); - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - auto fn = [input1_multiplier, input1_shift, // - input2_multiplier, input2_shift, // - output_multiplier, output_shift, // - activation_min, activation_max](int16_t input1_val, int16_t input2_val) { - const int32_t shifted_input1_val = static_cast(input1_val) << left_shift; - const int32_t shifted_input2_val = static_cast(input2_val) << left_shift; - const int32_t scaled_input1_val = tflite::MultiplyByQuantizedMultiplierSmallerThanOneExp( - shifted_input1_val, input1_multiplier, input1_shift); - const int32_t scaled_input2_val = tflite::MultiplyByQuantizedMultiplierSmallerThanOneExp( - shifted_input2_val, input2_multiplier, input2_shift); - const int32_t raw_sum = scaled_input1_val + scaled_input2_val; - const int32_t raw_output = tflite::MultiplyByQuantizedMultiplierSmallerThanOneExp( - raw_sum, output_multiplier, output_shift); - const int32_t clamped_output = std::min(activation_max, std::max(activation_min, raw_output)); - return static_cast(clamped_output); - }; - - BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output()), fn); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ArgMax.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/ArgMax.cpp deleted file mode 100644 index 6561a17..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/ArgMax.cpp +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/ArgMax.h" -#include "kernels/Utils.h" -#include "PALArgMax.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -ArgMax::ArgMax(const Tensor *input, const Tensor *axis, Tensor *output, const ArgMaxParams ¶ms) - : KernelWithParams({input, axis}, {output}, params) -{ -} - -void ArgMax::configure() -{ - assert(axis()->element_type() == DataType::S32 || axis()->element_type() == DataType::S64); - assert(input()->shape().num_dims() >= 1); - const Shape &input_shape = input()->shape(); - const int num_dims = input_shape.num_dims(); - Shape output_shape(num_dims - 1); - - // If axis value is negative, then update by adding input_shape's num_dims. - // If updated value also negative, then assert. - assert(axis()->shape().num_elements() == 1); - int axis_value = getTensorData(axis())[0]; - if (axis_value < 0) - axis_value = axis_value + num_dims; - assert(axis_value >= 0); - - int j = 0; - for (int i = 0; i < num_dims; i++) - { - if (i == axis_value) - continue; - output_shape.dim(j++) = input_shape.dim(i); - } - - assert(output()->element_type() == _params.output_type); - - output()->resize(output_shape); -} - -void ArgMax::execute() const -{ - -#define TF_LITE_ARG_MAX(data_type, axis_type, output_type) \ - luci_interpreter_pal::ArgMinMax(getTensorShape(input()), getTensorData(input()), \ - getTensorData(axis()), getTensorShape(output()), \ - getTensorData(output()), std::greater()) - if (axis()->element_type() == DataType::S32) - { - switch (_params.output_type) - { - case DataType::S32: - switch (input()->element_type()) - { - case DataType::FLOAT32: - TF_LITE_ARG_MAX(float, int32_t, int32_t); - break; - case DataType::U8: - TF_LITE_ARG_MAX(uint8_t, int32_t, int32_t); - break; - default: - throw std::runtime_error("Unsupported input type."); - } - break; - case DataType::S64: - switch (input()->element_type()) - { - case DataType::FLOAT32: - TF_LITE_ARG_MAX(float, int32_t, int64_t); - break; - case DataType::U8: - TF_LITE_ARG_MAX(uint8_t, int32_t, int64_t); - break; - default: - throw std::runtime_error("Unsupported input type."); - } - break; - default: - throw std::runtime_error("Unsupported output type."); - } - } - else - { - switch (_params.output_type) - { - case DataType::S32: - switch (input()->element_type()) - { - case DataType::FLOAT32: - TF_LITE_ARG_MAX(float, int64_t, int32_t); - break; - case DataType::U8: - TF_LITE_ARG_MAX(uint8_t, int64_t, int32_t); - break; - default: - throw std::runtime_error("Unsupported input type."); - } - break; - case DataType::S64: - switch (input()->element_type()) - { - case DataType::FLOAT32: - TF_LITE_ARG_MAX(float, int64_t, int64_t); - break; - case DataType::U8: - TF_LITE_ARG_MAX(uint8_t, int64_t, int64_t); - break; - default: - throw std::runtime_error("Unsupported input type."); - } - break; - default: - throw std::runtime_error("Unsupported output type."); - } - } -#undef TF_LITE_ARG_MAX -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/AveragePool2D.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/AveragePool2D.cpp deleted file mode 100644 index d3bade9..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/AveragePool2D.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/AveragePool2D.h" - -#include "kernels/Utils.h" - -#include "PALAveragePool2d.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -AveragePool2D::AveragePool2D(const Tensor *input, Tensor *output, Tensor *scratchpad, - const Pool2DParams ¶ms) - : KernelWithParams({input}, {output, scratchpad}, params) -{ -} - -void AveragePool2D::configure() -{ - if (input()->element_type() != output()->element_type()) - { - throw std::runtime_error("Input Tensor and Output Tensor Type must be same"); - } - if (input()->shape().num_dims() != 4) - { - throw std::runtime_error("Input Tensor Shape must be 4-D"); - } - const Shape &input_shape = input()->shape(); - - const int32_t batches = input_shape.dim(0); - const int32_t input_height = input_shape.dim(1); - const int32_t input_width = input_shape.dim(2); - const int32_t depth = input_shape.dim(3); - - const int32_t output_height = - computeOutputSize(_params.padding, input_height, _params.filter_height, _params.stride_height); - const int32_t output_width = - computeOutputSize(_params.padding, input_width, _params.filter_width, _params.stride_width); - - _padding_height = - computePadding(_params.stride_height, 1, input_height, _params.filter_height, output_height); - _padding_width = - computePadding(_params.stride_width, 1, input_width, _params.filter_width, output_width); - if (input()->element_type() == DataType::U8) - { - LUCI_INTERPRETER_CHECK(std::abs(output()->scale() - input()->scale()) <= 1.0e-6); - LUCI_INTERPRETER_CHECK(output()->zero_point() == input()->zero_point()); - } - else if (input()->element_type() == DataType::S16) - { - LUCI_INTERPRETER_CHECK(std::abs(output()->scale() - input()->scale()) <= 1.0e-6); - LUCI_INTERPRETER_CHECK(input()->zero_point() == 0 && output()->zero_point() == 0); - } - else if (input()->element_type() == DataType::S8) - { - LUCI_INTERPRETER_CHECK(std::abs(output()->scale() - input()->scale()) <= 1.0e-6); - LUCI_INTERPRETER_CHECK(output()->zero_point() == input()->zero_point()); - } - output()->resize({batches, output_height, output_width, depth}); - - auto scratchpad = getOutputTensors()[1]; - luci_interpreter_pal::SetupScratchpadTensor(scratchpad, input()->element_type(), - getTensorShape(input()), getTensorShape(output())); -} - -void AveragePool2D::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::U8: - evalQuantized(); - break; - case DataType::S16: - evalSInt16(); - break; - case DataType::S8: - evalSInt8(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void AveragePool2D::evalFloat() const -{ - float activation_min{}; - float activation_max{}; - calculateActivationRange(_params.activation, &activation_min, &activation_max); - - tflite::PoolParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.filter_height = _params.filter_height; - params.filter_width = _params.filter_width; - params.float_activation_min = activation_min; - params.float_activation_max = activation_max; - - tflite::reference_ops::AveragePool(params, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -void AveragePool2D::evalQuantized() const -{ - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - tflite::PoolParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.filter_height = _params.filter_height; - params.filter_width = _params.filter_width; - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - tflite::reference_ops::AveragePool(params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); -} - -void AveragePool2D::evalSInt8() const -{ - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - tflite::PoolParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.filter_height = _params.filter_height; - params.filter_width = _params.filter_width; - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - auto scratchpad = getOutputTensors()[1]; - int8_t *scratchpad_data = nullptr; - if (scratchpad->is_allocatable()) - scratchpad_data = scratchpad->data(); - - luci_interpreter_pal::AveragePool( - params, getTensorShape(input()), getTensorData(input()), getTensorShape(output()), - getTensorData(output()), getTensorShape(scratchpad), scratchpad_data); -} - -void AveragePool2D::evalSInt16() const -{ - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - tflite::PoolParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.filter_height = _params.filter_height; - params.filter_width = _params.filter_width; - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - tflite::reference_integer_ops::AveragePool( - params, getTensorShape(input()), getTensorData(input()), // - getTensorShape(output()), getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/BatchMatMul.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/BatchMatMul.cpp deleted file mode 100644 index 24ca229..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/BatchMatMul.cpp +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2020 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/BatchMatMul.h" -#include "kernels/Utils.h" - -#include "PALBatchMatMul.h" - -#include - -#include - -namespace -{ - -tflite::RuntimeShape SwapRowColumnDims(const tflite::RuntimeShape &shape) -{ - tflite::RuntimeShape swapped_shape(shape); - const int32_t dims = shape.DimensionsCount(); - swapped_shape.SetDim(dims - 2, shape.Dims(dims - 1)); - swapped_shape.SetDim(dims - 1, shape.Dims(dims - 2)); - return swapped_shape; -} - -} // namespace - -namespace luci_interpreter -{ -namespace kernels -{ - -BatchMatMul::BatchMatMul(const Tensor *x, const Tensor *y, Tensor *output, Tensor *x_tmp, - Tensor *y_tmp, const BatchMatMulParams ¶ms) - : KernelWithParams({x, y}, {output, x_tmp, y_tmp}, params) -{ -} - -void BatchMatMul::configure() -{ - auto lhs = x(); - auto rhs = y(); - auto adj_x = params().adj_x; - auto adj_y = params().adj_y; - - // TODO Support non-float types - if (lhs->element_type() != DataType::FLOAT32 || rhs->element_type() != DataType::FLOAT32) - throw std::runtime_error("Unsupported type."); - - LUCI_INTERPRETER_CHECK(lhs->element_type() == rhs->element_type()); - - auto lhs_rank = lhs->shape().num_dims(); - auto rhs_rank = rhs->shape().num_dims(); - LUCI_INTERPRETER_CHECK(lhs_rank >= 2 && lhs_rank <= 4); - LUCI_INTERPRETER_CHECK(rhs_rank >= 2 && rhs_rank <= 4); - - auto lhs_scratchpad = temp_lhs(); - auto rhs_scratchpad = temp_rhs(); - luci_interpreter_pal::SetupScratchpadTensor(lhs_scratchpad, rhs_scratchpad, getTensorShape(lhs), - getTensorShape(rhs)); - - auto output_rank = std::max(lhs_rank, rhs_rank); - - auto extended_lhs_shape = tflite::RuntimeShape::ExtendedShape(output_rank, getTensorShape(lhs)); - auto extended_rhs_shape = tflite::RuntimeShape::ExtendedShape(output_rank, getTensorShape(rhs)); - - // Ensure any batch dimensions obey broacasting rules. - for (int i = 0; i < output_rank - 2; ++i) - { - const int lhs_dim = extended_lhs_shape.Dims(i); - const int rhs_dim = extended_rhs_shape.Dims(i); - if (lhs_dim != rhs_dim) - { - if (lhs_dim != 1) - { - LUCI_INTERPRETER_CHECK(rhs_dim == 1); - } - } - } - - // Ensure other dimensions work for matrix multiplication. - int accum_dim_lhs = - adj_x ? extended_lhs_shape.Dims(output_rank - 2) : extended_lhs_shape.Dims(output_rank - 1); - int accum_dim_rhs = - adj_y ? extended_rhs_shape.Dims(output_rank - 1) : extended_rhs_shape.Dims(output_rank - 2); - LUCI_INTERPRETER_CHECK(accum_dim_lhs == accum_dim_rhs); - - Shape output_shape(output_rank); - // Fill in any broadcast dimensions. - for (int i = 0; i < output_rank - 2; ++i) - { - const int lhs_dim = extended_lhs_shape.Dims(i); - const int rhs_dim = extended_rhs_shape.Dims(i); - int broadcast_dim = lhs_dim; - if ((lhs_dim != rhs_dim) && (lhs_dim == 1)) - { - broadcast_dim = rhs_dim; - } - output_shape.dim(i) = broadcast_dim; - } - // Fill in the matmul dimensions. - int lhs_rows_index = adj_x ? output_rank - 1 : output_rank - 2; - int rhs_cols_index = adj_y ? output_rank - 2 : output_rank - 1; - - output_shape.dim(output_rank - 2) = extended_lhs_shape.Dims(lhs_rows_index); - output_shape.dim(output_rank - 1) = extended_rhs_shape.Dims(rhs_cols_index); - - output()->resize(output_shape); -} - -void TransposeRowsColumns(const Tensor *tensor_in, Tensor *tensor_out) -{ - tflite::RuntimeShape transposed_shape(getTensorShape(tensor_in)); - tflite::RuntimeShape shape(getTensorShape(tensor_in)); - tflite::TransposeParams params; - int rank = shape.DimensionsCount(); - params.perm_count = rank; - for (int i = 0; i < rank - 2; ++i) - { - params.perm[i] = i; - } - // Transpose the last two dimensions. - params.perm[rank - 2] = rank - 1; - params.perm[rank - 1] = rank - 2; - transposed_shape.SetDim(rank - 1, shape.Dims(rank - 2)); - transposed_shape.SetDim(rank - 2, shape.Dims(rank - 1)); - switch (tensor_in->element_type()) - { - case DataType::FLOAT32: - tflite::reference_ops::Transpose(params, shape, getTensorData(tensor_in), - transposed_shape, getTensorData(tensor_out)); - break; - default: - throw std::runtime_error("Only suppport fp32 BatchMatMul for now."); - } -} - -void BatchMatMul::execute() const -{ - auto lhs = x(); - auto rhs = y(); - - bool adj_x = params().adj_x; - bool adj_y = params().adj_y; - - auto orig_lhs_shape = getTensorShape(lhs); - auto orig_rhs_shape = getTensorShape(rhs); - - auto rhs_tensor = adj_y ? rhs : temp_rhs(); - auto lhs_tensor = adj_x ? temp_lhs() : lhs; - if (not adj_y) - { - TransposeRowsColumns(rhs, temp_rhs()); - } - if (adj_x) - { - TransposeRowsColumns(lhs, temp_lhs()); - } - tflite::RuntimeShape rhs_shape = adj_y ? orig_rhs_shape : SwapRowColumnDims(orig_rhs_shape); - tflite::RuntimeShape lhs_shape = adj_x ? orig_lhs_shape : SwapRowColumnDims(orig_lhs_shape); - - switch (x()->element_type()) - { - case DataType::FLOAT32: - luci_interpreter_pal::BatchMatMul(rhs_shape, getTensorData(rhs_tensor), lhs_shape, - getTensorData(lhs_tensor), getTensorShape(output()), - getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/BatchToSpaceND.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/BatchToSpaceND.cpp deleted file mode 100644 index bd315ff..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/BatchToSpaceND.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/BatchToSpaceND.h" -#include "kernels/Utils.h" - -#include "PALBatchToSpaceND.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -namespace -{ -const int kInputMinDimensionNum = 3; -const int kInputMaxDimensionNum = 4; -} // namespace - -BatchToSpaceND::BatchToSpaceND(const Tensor *input, const Tensor *block_shape, const Tensor *crops, - Tensor *output) - : Kernel({input, block_shape, crops}, {output}) -{ -} - -void BatchToSpaceND::configure() -{ - - const auto *block_shape_data = block_shape()->data(); - const auto *crops_data = crops()->data(); - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() >= kInputMinDimensionNum); - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() <= kInputMaxDimensionNum); - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - - int spatial_dims_num = input()->shape().num_dims() - 2; - - LUCI_INTERPRETER_CHECK(block_shape()->shape().num_dims() == 1); - LUCI_INTERPRETER_CHECK(block_shape()->shape().dim(0) == spatial_dims_num); - - LUCI_INTERPRETER_CHECK(crops()->shape().num_dims() == 2); - LUCI_INTERPRETER_CHECK(crops()->shape().dim(0) == spatial_dims_num); - LUCI_INTERPRETER_CHECK(crops()->shape().dim(1) == 2); - for (int i = 0; i < spatial_dims_num * 2; ++i) - { - LUCI_INTERPRETER_CHECK(crops_data[i] >= 0); - } - - Shape output_shape = Shape(input()->shape().num_dims()); - int output_batch_size = input()->shape().dim(0); - for (int i = 0; i < spatial_dims_num; ++i) - { - LUCI_INTERPRETER_CHECK(output_batch_size % block_shape_data[i] == 0); - output_batch_size = output_batch_size / block_shape_data[i]; - output_shape.dim(i + 1) = - input()->shape().dim(i + 1) * block_shape_data[i] - crops_data[i * 2] - crops_data[i * 2 + 1]; - } - - output_shape.dim(0) = output_batch_size; - output_shape.dim(input()->shape().num_dims() - 1) = - input()->shape().dim(input()->shape().num_dims() - 1); - output()->resize(output_shape); -} - -void BatchToSpaceND::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - luci_interpreter_pal::BatchToSpaceND( - getTensorShape(input()), getTensorData(input()), getTensorShape(block_shape()), - getTensorData(block_shape()), getTensorShape(crops()), - getTensorData(crops()), getTensorShape(output()), getTensorData(output())); - break; - case DataType::U8: - luci_interpreter_pal::BatchToSpaceND( - getTensorShape(input()), getTensorData(input()), getTensorShape(block_shape()), - getTensorData(block_shape()), getTensorShape(crops()), - getTensorData(crops()), getTensorShape(output()), - getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/CMakeLists.txt b/compiler/luci-micro/luci-interpreter/src/kernels/CMakeLists.txt deleted file mode 100644 index 9f4ba0e..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/CMakeLists.txt +++ /dev/null @@ -1,43 +0,0 @@ -set(SOURCES - BinaryOpCommon.h - Utils.h - Utils.cpp - "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/TestMemoryManager.h" - ${LUCI_INTERPRETER_SOURCE_DIR}/TestMemoryManager.cpp - "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/SimpleMemoryManager.h" - ${LUCI_INTERPRETER_SOURCE_DIR}/SimpleMemoryManager.cpp) - -macro(REGISTER_KERNEL NODE) - list(APPEND SOURCES "${NODE}.h") - list(APPEND SOURCES "${NODE}.cpp") -endmacro(REGISTER_KERNEL) - -include(${KERNEL_REGISTER_FILE}) - -add_library(${LUCI_INTERPRETER_KERNELS} STATIC ${SOURCES}) -if (NOT NNCC_LIBRARY_NO_PIC) - set_target_properties(${LUCI_INTERPRETER_KERNELS} PROPERTIES POSITION_INDEPENDENT_CODE ON) -endif(NOT NNCC_LIBRARY_NO_PIC) -target_include_directories(${LUCI_INTERPRETER_KERNELS} PUBLIC ${LUCI_INTERPRETER_SOURCE_DIR}) - -target_link_libraries(${LUCI_INTERPRETER_KERNELS} PUBLIC ${LUCI_INTERPRETER_CORE}) -target_link_libraries(${LUCI_INTERPRETER_KERNELS} PRIVATE nncc_common) - -add_pal_to_target(${LUCI_INTERPRETER_KERNELS}) - -if(NOT ENABLE_TEST) - return() -endif(NOT ENABLE_TEST) - -nnas_find_package(GTest REQUIRED) - -macro(REGISTER_KERNEL NODE) - list(APPEND TEST_SOURCES "${NODE}.test.cpp") -endmacro(REGISTER_KERNEL) - -include(${KERNEL_REGISTER_FILE}) - -list(APPEND TEST_SOURCES TestUtils.h TestUtils.cpp) - -GTest_AddTest(${LUCI_INTERPRETER_KERNELS}_test ${TEST_SOURCES}) -target_link_libraries(${LUCI_INTERPRETER_KERNELS}_test ${LUCI_INTERPRETER_KERNELS}) diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Cast.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Cast.cpp deleted file mode 100644 index 39ee725..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Cast.cpp +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2017 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Cast.h" -#include "kernels/Utils.h" - -namespace -{ - -using namespace luci_interpreter; -using namespace luci_interpreter::kernels; - -template -void cast_data(const InT *in_data, OutT *out_data, uint32_t elements_count) -{ - std::transform(in_data, in_data + elements_count, out_data, - [](InT a) { return static_cast(a); }); -} - -template void cast_from_pointer_to_tensor(const InT *in_data, Tensor *out_tensor) -{ - auto const out_type = out_tensor->element_type(); - auto const elements_count = out_tensor->shape().num_elements(); - - switch (out_type) - { - case loco::DataType::U8: - cast_data(in_data, getTensorData(out_tensor), elements_count); - break; - case loco::DataType::U16: - cast_data(in_data, getTensorData(out_tensor), elements_count); - break; - case loco::DataType::U32: - cast_data(in_data, getTensorData(out_tensor), elements_count); - break; - case loco::DataType::U64: - cast_data(in_data, getTensorData(out_tensor), elements_count); - break; - case loco::DataType::S8: - cast_data(in_data, getTensorData(out_tensor), elements_count); - break; - case loco::DataType::S16: - cast_data(in_data, getTensorData(out_tensor), elements_count); - break; - case loco::DataType::S32: - cast_data(in_data, getTensorData(out_tensor), elements_count); - break; - case loco::DataType::S64: - cast_data(in_data, getTensorData(out_tensor), elements_count); - break; - case loco::DataType::FLOAT32: - cast_data(in_data, getTensorData(out_tensor), elements_count); - break; - case loco::DataType::BOOL: - cast_data(in_data, getTensorData(out_tensor), elements_count); - break; - default: - throw std::runtime_error("Unsupported output type."); - } -} - -void cast_from_tensor_to_tensor(const Tensor *in_tensor, Tensor *out_tensor) -{ - auto in_type = in_tensor->element_type(); - - switch (in_type) - { - case loco::DataType::U8: - cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); - break; - case loco::DataType::U16: - cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); - break; - case loco::DataType::U32: - cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); - break; - case loco::DataType::U64: - cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); - break; - case loco::DataType::S8: - cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); - break; - case loco::DataType::S16: - cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); - break; - case loco::DataType::S32: - cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); - break; - case loco::DataType::S64: - cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); - break; - case loco::DataType::FLOAT32: - cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); - break; - case loco::DataType::BOOL: - cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); - break; - default: - throw std::runtime_error("Unsupported input type."); - } -} - -} // namespace - -namespace luci_interpreter -{ -namespace kernels -{ - -Cast::Cast(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Cast::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() != loco::DataType::Unknown); - LUCI_INTERPRETER_CHECK(output()->element_type() != loco::DataType::Unknown); - - const Shape &shape = input()->shape(); - output()->resize(shape); -} - -void Cast::execute() const -{ - assert(input()->shape().num_elements() == output()->shape().num_elements()); - - cast_from_tensor_to_tensor(input(), output()); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.cpp deleted file mode 100644 index 46ee594..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.cpp +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Concatenation.h" -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Concatenation::Concatenation(std::vector inputs, Tensor *output, - const ConcatenationParams ¶ms) - : KernelWithParams(std::move(inputs), {output}, params) -{ -} - -void Concatenation::configure() -{ - const int num_inputs = _inputs.size(); - LUCI_INTERPRETER_CHECK(num_inputs > 0); - const Tensor *t0 = _inputs[0]; - - // TODO: Support concat with fused activation function - LUCI_INTERPRETER_CHECK(params().activation == luci::FusedActFunc::NONE); - - int axis = _params.axis; - if (axis < 0) - axis += t0->shape().num_dims(); - LUCI_INTERPRETER_CHECK(axis >= 0 && axis < t0->shape().num_dims()); - - int32_t sum_axis = t0->shape().dim(axis); - for (int i = 1; i < num_inputs; ++i) - { - const Tensor *tensor = _inputs[i]; - LUCI_INTERPRETER_CHECK(tensor->element_type() == t0->element_type()); - LUCI_INTERPRETER_CHECK(tensor->shape().num_dims() == t0->shape().num_dims()); - for (int d = 0; d < t0->shape().num_dims(); ++d) - { - if (d == axis) - { - sum_axis += tensor->shape().dim(axis); - } - else - { - LUCI_INTERPRETER_CHECK(tensor->shape().dim(d) == t0->shape().dim(d)); - } - } - } - - Shape output_shape = t0->shape(); - output_shape.dim(axis) = sum_axis; - - // If input tensors are INT8 type then quantization parameters of all input tensors and the output - // should be the same - for (auto current_tensor : _inputs) - { - if (current_tensor->element_type() == DataType::S8) - { - LUCI_INTERPRETER_CHECK(current_tensor->quantized_dimension() == - output()->quantized_dimension()); - - LUCI_INTERPRETER_CHECK(current_tensor->zero_points().size() == - current_tensor->scales().size()); - LUCI_INTERPRETER_CHECK(current_tensor->zero_points() == output()->zero_points()); - LUCI_INTERPRETER_CHECK(current_tensor->scales() == output()->scales()); - } - } - output()->resize(output_shape); -} - -void Concatenation::execute() const -{ - switch (_inputs[0]->element_type()) - { - case DataType::FLOAT32: - evalGeneric(); - break; - case DataType::U8: - evalQuantized(); - break; - case DataType::S8: - evalGeneric(); - break; - case DataType::S32: - evalGeneric(); - break; - case DataType::S64: - evalGeneric(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -template void Concatenation::evalGeneric() const -{ - int axis = _params.axis; - if (axis < 0) - axis += output()->shape().num_dims(); - - VectorOfTensors inputs(_inputs); - tflite::ConcatenationParams params{}; - params.axis = axis; - params.inputs_count = _inputs.size(); - tflite::reference_ops::Concatenation(params, inputs.shapes(), inputs.data(), - getTensorShape(output()), getTensorData(output())); -} - -void Concatenation::evalQuantized() const -{ - int axis = _params.axis; - if (axis < 0) - axis += output()->shape().num_dims(); - - VectorOfQuantizedTensors inputs(_inputs); - tflite::ConcatenationParams params{}; - params.axis = axis; - params.input_zeropoint = inputs.zero_point(); - params.input_scale = inputs.scale(); - params.inputs_count = _inputs.size(); - params.output_zeropoint = output()->zero_point(); - params.output_scale = output()->scale(); - - tflite::reference_ops::ConcatenationWithScaling(params, inputs.shapes(), inputs.data(), - getTensorShape(output()), - getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.h b/compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.h deleted file mode 100644 index b48c8ed..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_KERNELS_CONCATENATION_H -#define LUCI_INTERPRETER_KERNELS_CONCATENATION_H - -#include "core/Kernel.h" -#include "core/KernelParams.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -class Concatenation : public KernelWithParams -{ -public: - Concatenation(std::vector inputs, Tensor *output, - const ConcatenationParams ¶ms); - - const Tensor *input(int index) const { return _inputs[index]; } - Tensor *output() const { return _outputs[0]; } - - void configure() override; - void execute() const override; - -private: - template void evalGeneric() const; - void evalQuantized() const; -}; - -} // namespace kernels -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_KERNELS_CONCATENATION_H diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.test.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.test.cpp deleted file mode 100644 index f893b38..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Concatenation.test.cpp +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Concatenation.h" -#include "kernels/TestUtils.h" -#include "luci_interpreter/TestMemoryManager.h" - -namespace luci_interpreter -{ -namespace kernels -{ -namespace -{ - -using namespace testing; - -class ConcatenationTest : public ::testing::Test -{ -protected: - void SetUp() override { _memory_manager = std::make_unique(); } - - std::unique_ptr _memory_manager; -}; - -TEST_F(ConcatenationTest, Float) -{ - std::vector input1_data{1, 2, 3, 4, 5, 6}; - std::vector input2_data{7, 8, 9, 10, 11, 12}; - Tensor input1_tensor = - makeInputTensor({2, 3}, input1_data, _memory_manager.get()); - Tensor input2_tensor = - makeInputTensor({2, 3}, input2_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - ConcatenationParams params{}; - - // Try different 'axis' and expect different results. - { - params.axis = 0; - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - kernel.configure(); - for (auto t : kernel.getOutputTensors()) - { - _memory_manager->allocate_memory(*t); - } - kernel.execute(); - - EXPECT_THAT(extractTensorData(output_tensor), - FloatArrayNear({1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12})); - } - { - params.axis = -2; // Same as '0'. - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorData(output_tensor), - FloatArrayNear({1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12})); - } - { - params.axis = 1; - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorData(output_tensor), - FloatArrayNear({1, 2, 3, 7, 8, 9, 4, 5, 6, 10, 11, 12})); - } - { - params.axis = -1; // Same as '1'. - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorData(output_tensor), - FloatArrayNear({1, 2, 3, 7, 8, 9, 4, 5, 6, 10, 11, 12})); - } -} - -TEST_F(ConcatenationTest, Input_Number_Check_NEG) -{ - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - ConcatenationParams params{}; - - params.axis = -1; - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({}, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(ConcatenationTest, Invalid_Axis_NEG) -{ - std::vector input1_data{1, 2, 3, 4, 5, 6}; - std::vector input2_data{7, 8, 9, 10, 11, 12}; - Tensor input1_tensor = - makeInputTensor({2, 3}, input1_data, _memory_manager.get()); - Tensor input2_tensor = - makeInputTensor({2, 3}, input2_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - ConcatenationParams params{}; - - params.axis = -3; - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(ConcatenationTest, Mismatching_Input_Type_NEG) -{ - std::vector input1_data{1, 2, 3, 4, 5, 6}; - std::vector input2_data{7, 8, 9, 10, 11, 12}; - Tensor input1_tensor = - makeInputTensor({2, 3}, input1_data, _memory_manager.get()); - Tensor input2_tensor = makeInputTensor({2, 3}, input2_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - ConcatenationParams params{}; - - params.axis = -1; - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(ConcatenationTest, Mismatching_Input_Dimension_Num_NEG) -{ - std::vector input1_data{1, 2, 3, 4, 5, 6}; - std::vector input2_data{7, 8, 9, 10, 11, 12}; - Tensor input1_tensor = - makeInputTensor({2, 3}, input1_data, _memory_manager.get()); - Tensor input2_tensor = - makeInputTensor({1, 2, 3}, input2_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - ConcatenationParams params{}; - - params.axis = -1; - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(ConcatenationTest, Mismatching_Input_Dimension_NEG) -{ - std::vector input1_data{1, 2, 3, 4, 5, 6}; - std::vector input2_data{7, 8, 9, 10, 11, 12, 13, 14, 15}; - Tensor input1_tensor = - makeInputTensor({2, 3}, input1_data, _memory_manager.get()); - Tensor input2_tensor = - makeInputTensor({3, 3}, input2_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - ConcatenationParams params{}; - - params.axis = -1; - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(ConcatenationTest, Int8_Mismatching_Input_Type_NEG) -{ - std::vector input1_data{1, 2, 3, 4}; - std::vector input2_data{5, 6, 7, 8}; - Tensor input1_tensor = makeInputTensor({2, 2}, input1_data, _memory_manager.get()); - Tensor input2_tensor = makeInputTensor({2, 2}, input2_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::S8); - ConcatenationParams params{}; - - params.axis = -1; - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(ConcatenationTest, Int8_Mismatching_Input_Output_Quant_Params_NEG) -{ - std::vector input1_data{1, 2, 3, 4, 5, 6}; - std::vector input2_data{7, 8, 9, 10, 11, 12}; - int quantized_dimension = 3; - std::vector scales{0.1, 0.2, 0.3}; - std::vector zero_points{1, -1, 1}; - - Tensor input1_tensor = makeInputTensor( - {1, 1, 2, 3}, scales, zero_points, quantized_dimension, input1_data, _memory_manager.get()); - Tensor input2_tensor = makeInputTensor( - {1, 1, 2, 3}, scales, zero_points, quantized_dimension, input2_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::S8, scales.at(0), zero_points.at(0)); - ConcatenationParams params{}; - - params.axis = -1; - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(ConcatenationTest, Int8_Mismatching_Zero_Point_NEG) -{ - std::vector input1_data{1, 2, 3, 4}; - std::vector input2_data{5, 6, 7, 8}; - float scale = 0.1; - int32_t zero_point_1 = 1; - int32_t zero_point_2 = -1; - - Tensor input1_tensor = - makeInputTensor({2, 2}, scale, zero_point_1, input1_data, _memory_manager.get()); - Tensor input2_tensor = - makeInputTensor({2, 2}, scale, zero_point_2, input2_data, _memory_manager.get()); - - Tensor output_tensor = makeOutputTensor(DataType::S8, scale, zero_point_1); - ConcatenationParams params{}; - - params.axis = -1; - params.activation = luci::FusedActFunc::NONE; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -// TODO: Remove this test when concat w/ fused_activation is supported -TEST_F(ConcatenationTest, With_Fused_Activation_NEG) -{ - std::vector input1_data{1, 2, 3, 4, 5, 6}; - std::vector input2_data{7, 8, 9, 10, 11, 12}; - Tensor input1_tensor = - makeInputTensor({2, 3}, input1_data, _memory_manager.get()); - Tensor input2_tensor = - makeInputTensor({2, 3}, input2_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - ConcatenationParams params{}; - - params.axis = 1; - params.activation = luci::FusedActFunc::RELU; - - Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -} // namespace -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.cpp deleted file mode 100644 index 234f954..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.cpp +++ /dev/null @@ -1,456 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Conv2D.h" - -#include "kernels/Utils.h" - -#include "PALConv2d.h" - -#include -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Conv2D::Conv2D(const Tensor *input, const Tensor *filter, const Tensor *bias, Tensor *output, - Tensor *scratchpad, const Conv2DParams ¶ms) - : KernelWithParams({input, filter, bias}, {output, scratchpad}, params) -{ -} - -void Conv2D::configure() -{ - // TensorFlow Lite (as of v2.2.0) supports the following combinations of types: - // | input filter bias output | - // ----+---------------------------+ - // (1) | float float float float | - // (2) | float int8 float float | hybrid - // (3) | uint8 uint8 int32 uint8 | quantized - // (4) | int8 int8 int32 int8 | quantized per channel - // - // We only support (1), (3) and (4) for now, and additionally the following: - // | input filter bias output | - // ----+---------------------------+ - // (5) | int16 int16 int64 int16 | - // - if (input()->element_type() == DataType::FLOAT32 && filter()->element_type() == DataType::FLOAT32) - { - LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::FLOAT32); - } - else if (input()->element_type() == DataType::U8 && filter()->element_type() == DataType::U8) - { - LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::S32); - } - else if (input()->element_type() == DataType::S8 && filter()->element_type() == DataType::S8) - { - LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::S32); - LUCI_INTERPRETER_CHECK(filter()->shape().num_dims() == 4); - LUCI_INTERPRETER_CHECK(filter()->scales().size() == - static_cast(filter()->shape().dim(0))); - for (auto zerop : filter()->zero_points()) - { - LUCI_INTERPRETER_CHECK(zerop == 0); - } - } - else if (input()->element_type() == DataType::S16 && filter()->element_type() == DataType::S16) - { - LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::S64); - } - else - { - throw std::runtime_error("Unsupported type."); - } - LUCI_INTERPRETER_CHECK(output()->element_type() == input()->element_type()); - - const Shape &input_shape = input()->shape(); - const Shape &filter_shape = filter()->shape(); - LUCI_INTERPRETER_CHECK(input_shape.num_dims() == 4 && filter_shape.num_dims() == 4); - - const int32_t batches = input_shape.dim(0); - const int32_t input_height = input_shape.dim(1); - const int32_t input_width = input_shape.dim(2); - const int32_t output_depth = filter_shape.dim(0); - const int32_t filter_height = filter_shape.dim(1); - const int32_t filter_width = filter_shape.dim(2); - LUCI_INTERPRETER_CHECK(filter_shape.dim(3) == input_shape.dim(3)); - - LUCI_INTERPRETER_CHECK(bias() == nullptr || (bias()->shape().num_dims() == 1 && - bias()->shape().dim(0) == output_depth)); - - const int32_t output_height = - computeOutputSize(_params.padding, input_height, filter_height, _params.stride_height, - _params.dilation_height_factor); - const int32_t output_width = - computeOutputSize(_params.padding, input_width, filter_width, _params.stride_width, - _params.dilation_width_factor); - - _padding_height = computePadding(_params.stride_height, _params.dilation_height_factor, - input_height, filter_height, output_height); - _padding_width = computePadding(_params.stride_width, _params.dilation_width_factor, input_width, - filter_width, output_width); - - output()->resize({batches, output_height, output_width, output_depth}); - - // Allocate tensor for scratchpad, if needed. - tflite::ConvParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.dilation_height_factor = _params.dilation_height_factor; - params.dilation_width_factor = _params.dilation_width_factor; - auto scratchpad = getOutputTensors()[1]; - luci_interpreter_pal::SetupScratchpadTensor(scratchpad, input()->element_type(), params, - getTensorShape(input()), getTensorShape(filter()), - getTensorShape(output())); - - switch (_params.activation) - { - case Activation::NONE: - case Activation::RELU: - case Activation::RELU6: - case Activation::RELU_N1_TO_1: - break; - default: - throw std::runtime_error("Unsupported fused activation"); - } -} - -void Conv2D::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - if (filter()->element_type() == DataType::FLOAT32) - { - evalFloat(); - break; - } - throw std::runtime_error("Unsupported type."); - case DataType::U8: - if (filter()->scales().size() == 1) - { - evalQuantized(); - } - else if (filter()->scales().size() > 1) - { - LUCI_INTERPRETER_CHECK(filter()->shape().num_dims() == 4); - LUCI_INTERPRETER_CHECK(filter()->scales().size() == - static_cast(filter()->shape().dim(0))); - evalQuantizedPerChannel(); - } - break; - case DataType::S8: - evalQuantizedS8PerChannel(); - break; - case DataType::S16: - evalQuantizedS16(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Conv2D::evalFloat() const -{ - float activation_min{}; - float activation_max{}; - calculateActivationRange(_params.activation, &activation_min, &activation_max); - - tflite::ConvParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.dilation_height_factor = _params.dilation_height_factor; - params.dilation_width_factor = _params.dilation_width_factor; - params.float_activation_min = activation_min; - params.float_activation_max = activation_max; - - auto scratchpad = getOutputTensors()[1]; - float *scratchpad_data = nullptr; - if (scratchpad->is_allocatable()) - scratchpad_data = scratchpad->data(); - - luci_interpreter_pal::Conv(params, getTensorShape(input()), getTensorData(input()), - getTensorShape(filter()), getTensorData(filter()), - getTensorShape(bias()), getTensorData(bias()), - getTensorShape(output()), getTensorData(output()), - getTensorShape(scratchpad), scratchpad_data); -} - -void Conv2D::evalQuantized() const -{ - const auto input_scale = static_cast(input()->scale()); - const auto filter_scale = static_cast(filter()->scale()); - const auto output_scale = static_cast(output()->scale()); - - const double real_multiplier = input_scale * filter_scale / output_scale; - int32_t output_multiplier{}; - int output_shift{}; - quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - tflite::ConvParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.dilation_height_factor = _params.dilation_height_factor; - params.dilation_width_factor = _params.dilation_width_factor; - // The kernel expects input and filter zero points to be negated. - params.input_offset = -input()->zero_point(); // Note the '-'. - params.weights_offset = -filter()->zero_point(); // Note the '-'. - params.output_offset = output()->zero_point(); - params.output_multiplier = output_multiplier; - params.output_shift = output_shift; - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - auto scratchpad = getOutputTensors()[1]; - luci_interpreter_pal::Conv(params, getTensorShape(input()), getTensorData(input()), - getTensorShape(filter()), getTensorData(filter()), - getTensorShape(bias()), getTensorData(bias()), - getTensorShape(output()), getTensorData(output()), - getTensorShape(scratchpad), getTensorData(scratchpad)); -} - -void Conv2D::evalQuantizedPerChannel() const -{ - const auto *input_data = getTensorData(input()); - const auto *filter_data = getTensorData(filter()); - const auto *bias_data = getTensorData(bias()); - auto *output_data = getTensorData(output()); - - const Shape &input_shape = input()->shape(); - const Shape &filter_shape = filter()->shape(); - const Shape &output_shape = output()->shape(); - - const int32_t batches = input_shape.dim(0); - const int32_t input_height = input_shape.dim(1); - const int32_t input_width = input_shape.dim(2); - const int32_t input_depth = input_shape.dim(3); - const int32_t output_depth = filter_shape.dim(0); - const int32_t filter_height = filter_shape.dim(1); - const int32_t filter_width = filter_shape.dim(2); - const int32_t output_height = output_shape.dim(1); - const int32_t output_width = output_shape.dim(2); - - const int32_t stride_height = _params.stride_height; - const int32_t stride_width = _params.stride_width; - const int32_t dilation_height_factor = _params.dilation_height_factor; - const int32_t dilation_width_factor = _params.dilation_width_factor; - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - const std::vector effective_output_scale = - getQuantizedConvolutionMultiplers(input()->scale(), filter()->scales(), output()->scale()); - - const std::vector multipliers_raw = - quantizeMultipliers(effective_output_scale); - BroadcastableWrapper quant_multipliers(multipliers_raw); - - for (int32_t batch = 0; batch < batches; ++batch) - { - for (int32_t out_y = 0; out_y < output_height; ++out_y) - { - for (int32_t out_x = 0; out_x < output_width; ++out_x) - { - for (int32_t out_c = 0; out_c < output_depth; ++out_c) - { - const int32_t in_y_origin = out_y * stride_height - _padding_height; - const int32_t in_x_origin = out_x * stride_width - _padding_width; - int32_t acc = 0; - for (int32_t filter_y = 0; filter_y < filter_height; ++filter_y) - { - for (int32_t filter_x = 0; filter_x < filter_width; ++filter_x) - { - const int32_t in_y = in_y_origin + dilation_height_factor * filter_y; - const int32_t in_x = in_x_origin + dilation_width_factor * filter_x; - if ((in_y >= 0 && in_y < input_height) && (in_x >= 0 && in_x < input_width)) - { - for (int32_t in_c = 0; in_c < input_depth; ++in_c) - { - const uint8_t input_val = - input_data[calcOffset(input_shape, batch, in_y, in_x, in_c)]; - const uint8_t filter_val = - filter_data[calcOffset(filter_shape, out_c, filter_y, filter_x, in_c)]; - acc += static_cast(input_val - input()->zero_point()) * - static_cast(filter_val - filter()->zero_points()[out_c]); - } - } - } - } - if (bias_data) - { - acc += bias_data[out_c]; - } - - int32_t scaled_acc = tflite::MultiplyByQuantizedMultiplier( - acc, quant_multipliers[out_c].multiplier, quant_multipliers[out_c].shift); - - scaled_acc += output()->zero_point(); - scaled_acc = std::max(scaled_acc, activation_min); - scaled_acc = std::min(scaled_acc, activation_max); - output_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] = scaled_acc; - } - } - } - } -} - -void Conv2D::evalQuantizedS8PerChannel() const -{ - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - tflite::ConvParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.dilation_height_factor = _params.dilation_height_factor; - params.dilation_width_factor = _params.dilation_width_factor; - // The kernel expects filter zero points to be negated. - params.input_offset = -input()->zero_point(); // Note the '-'. - params.weights_offset = 0; // Unused in tflite code - params.output_offset = output()->zero_point(); - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - const std::vector effective_output_scales = - getQuantizedConvolutionMultiplers(input()->scale(), filter()->scales(), output()->scale()); - - std::vector quant_multipliers = - quantizeMultipliers(effective_output_scales); - - std::vector shifts; - std::transform(quant_multipliers.begin(), quant_multipliers.end(), std::back_inserter(shifts), - [](ChannelQuantMultipliers cm) { return cm.shift; }); - std::vector multipliers; - std::transform(quant_multipliers.begin(), quant_multipliers.end(), - std::back_inserter(multipliers), - [](ChannelQuantMultipliers cm) { return cm.multiplier; }); - - auto scratchpad = getOutputTensors()[1]; - int8_t *scratchpad_data = nullptr; - if (scratchpad->is_allocatable()) - scratchpad_data = scratchpad->data(); - - luci_interpreter_pal::ConvPerChannel( - params, multipliers.data(), shifts.data(), getTensorShape(input()), - getTensorData(input()), getTensorShape(filter()), getTensorData(filter()), - getTensorShape(bias()), getTensorData(bias()), getTensorShape(output()), - getTensorData(output()), getTensorShape(scratchpad), scratchpad_data); -} - -void Conv2D::evalQuantizedS16() const -{ - const auto *input_data = getTensorData(input()); - const auto *filter_data = getTensorData(filter()); - const auto *bias_data = getTensorData(bias()); - auto *output_data = getTensorData(output()); - - const Shape &input_shape = input()->shape(); - const Shape &filter_shape = filter()->shape(); - const Shape &output_shape = output()->shape(); - - const int32_t batches = input_shape.dim(0); - const int32_t input_height = input_shape.dim(1); - const int32_t input_width = input_shape.dim(2); - const int32_t input_depth = input_shape.dim(3); - const int32_t output_depth = filter_shape.dim(0); - const int32_t filter_height = filter_shape.dim(1); - const int32_t filter_width = filter_shape.dim(2); - const int32_t output_height = output_shape.dim(1); - const int32_t output_width = output_shape.dim(2); - - const int32_t stride_height = _params.stride_height; - const int32_t stride_width = _params.stride_width; - const int32_t dilation_height_factor = _params.dilation_height_factor; - const int32_t dilation_width_factor = _params.dilation_width_factor; - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - const std::vector effective_output_scale = - getQuantizedConvolutionMultiplers(input()->scale(), filter()->scales(), output()->scale()); - - const std::vector multipliers_raw = - quantizeMultipliers(effective_output_scale); - BroadcastableWrapper multipliers(multipliers_raw); - - for (int32_t batch = 0; batch < batches; ++batch) - { - for (int32_t out_y = 0; out_y < output_height; ++out_y) - { - for (int32_t out_x = 0; out_x < output_width; ++out_x) - { - for (int32_t out_c = 0; out_c < output_depth; ++out_c) - { - const int32_t in_y_origin = out_y * stride_height - _padding_height; - const int32_t in_x_origin = out_x * stride_width - _padding_width; - int64_t acc = 0; - for (int32_t filter_y = 0; filter_y < filter_height; ++filter_y) - { - for (int32_t filter_x = 0; filter_x < filter_width; ++filter_x) - { - const int32_t in_y = in_y_origin + dilation_height_factor * filter_y; - const int32_t in_x = in_x_origin + dilation_width_factor * filter_x; - if ((in_y >= 0 && in_y < input_height) && (in_x >= 0 && in_x < input_width)) - { - for (int32_t in_c = 0; in_c < input_depth; ++in_c) - { - const int16_t input_val = - input_data[calcOffset(input_shape, batch, in_y, in_x, in_c)]; - const int16_t filter_val = - filter_data[calcOffset(filter_shape, out_c, filter_y, filter_x, in_c)]; - acc += static_cast(input_val) * static_cast(filter_val); - } - } - } - } - if (bias_data) - { - acc += bias_data[out_c]; - } - - int32_t scaled_acc = tflite::MultiplyByQuantizedMultiplier( - acc, multipliers[out_c].multiplier, multipliers[out_c].shift); - - scaled_acc = std::max(scaled_acc, activation_min); - scaled_acc = std::min(scaled_acc, activation_max); - - output_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] = scaled_acc; - } - } - } - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.h b/compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.h deleted file mode 100644 index 330bf3a..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_KERNELS_CONV2D_H -#define LUCI_INTERPRETER_KERNELS_CONV2D_H - -#include "core/Kernel.h" -#include "core/KernelParams.h" - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -class Conv2D : public KernelWithParams -{ -public: - Conv2D(const Tensor *input, const Tensor *filter, const Tensor *bias, Tensor *output, - Tensor *scratchpad, const Conv2DParams ¶ms); - - const Tensor *input() const { return _inputs[0]; } - const Tensor *filter() const { return _inputs[1]; } - const Tensor *bias() const { return _inputs[2]; } - Tensor *output() const { return _outputs[0]; } - - void configure() override; - void execute() const override; - -private: - void evalFloat() const; - void evalQuantized() const; - void evalQuantizedPerChannel() const; - void evalQuantizedS8PerChannel() const; - void evalQuantizedS16() const; - -private: - int32_t _padding_height{}; - int32_t _padding_width{}; -}; - -} // namespace kernels -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_KERNELS_CONV2D_H diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.test.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.test.cpp deleted file mode 100644 index 0fe6ef7..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Conv2D.test.cpp +++ /dev/null @@ -1,707 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Conv2D.h" -#include "kernels/TestUtils.h" -#include "luci_interpreter/TestMemoryManager.h" - -namespace luci_interpreter -{ -namespace kernels -{ -namespace -{ - -using namespace testing; - -class Conv2DTest : public ::testing::Test -{ -protected: - void SetUp() override { _memory_manager = std::make_unique(); } - - std::unique_ptr _memory_manager; -}; - -TEST_F(Conv2DTest, Float) -{ - Shape input_shape{1, 4, 3, 2}; - Shape filter_shape{2, 2, 2, 2}; - Shape bias_shape{2}; - std::vector input_data{ - 1, 2, 3, 4, 5, 6, // row = 0 - 7, 8, 9, 10, 11, 12, // row = 1 - 13, 14, 15, 16, 17, 18, // row = 2 - 19, 20, 21, 22, 23, 24, // row = 3 - }; - std::vector filter_data{ - 1, 2, -3, -4, // out = 0, row = 0 - -5, 6, -7, 8, // out = 1, row = 0 - 4, -2, 3, -1, // out = 0, row = 1 - -8, -6, 7, 5, // out = 1, row = 1 - }; - std::vector bias_data{1, 2}; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor filter_tensor = - makeInputTensor(filter_shape, filter_data, _memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, bias_data, _memory_manager.get()); - Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 2; - params.stride_width = 1; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::RELU; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - kernel.configure(); - _memory_manager->allocate_memory(im2col); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - std::vector ref_output_data{ - 11, 16, 7, 20, // row = 0 - 0, 40, 0, 44, // row = 1 - }; - std::vector ref_output_shape{1, 2, 2, 2}; - EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); -} - -TEST_F(Conv2DTest, FloatPointwise) -{ - Shape input_shape{1, 2, 2, 2}; - Shape filter_shape{2, 1, 1, 2}; - Shape bias_shape{2}; - std::vector input_data{ - 1, 2, // row = 0, col = 0 - 3, 4, // row = 0, col = 1 - 5, 6, // row = 1, col = 0 - 7, 8, // row = 1, col = 1 - }; - std::vector filter_data{ - -1, 2, // out = 0 - -3, 4, // out = 1 - }; - std::vector bias_data{1, 2}; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor filter_tensor = - makeInputTensor(filter_shape, filter_data, _memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, bias_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 1; - params.stride_width = 1; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::RELU; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - kernel.configure(); - _memory_manager->allocate_memory(im2col); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - std::vector ref_output_data{ - 4, 7, 6, 9, // row = 0 - 8, 11, 10, 13, // row = 1 - }; - std::vector ref_output_shape{1, 2, 2, 2}; - EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); -} - -TEST_F(Conv2DTest, FloatCheck) -{ - Shape input_shape{2, 2, 4, 1}; - Shape filter_shape{3, 2, 2, 1}; - Shape bias_shape{3}; - std::vector input_data{ - // First batch - 1, 1, 1, 1, // row = 1 - 2, 2, 2, 2, // row = 2 - // Second batch - 1, 2, 3, 4, // row = 1 - 1, 2, 3, 4, // row = 2 - }; - std::vector filter_data{ - 1, 2, 3, 4, // first 2x2 filter - -1, 1, -1, 1, // second 2x2 filter - -1, -1, 1, 1, // third 2x2 filter - }; - std::vector bias_data{1, 2, 3}; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor filter_tensor = - makeInputTensor(filter_shape, filter_data, _memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, bias_data, _memory_manager.get()); - Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 2; - params.stride_width = 2; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::NONE; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - _memory_manager->allocate_memory(im2col); - kernel.execute(); - - std::vector ref_output_data{ - 18, 2, 5, // first batch, left - 18, 2, 5, // first batch, right - 17, 4, 3, // second batch, left - 37, 4, 3, // second batch, right - }; - std::vector ref_output_shape{2, 1, 2, 3}; - EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); -} - -TEST_F(Conv2DTest, Uint8) -{ - std::vector input_data{ - // First batch - 1, 1, 1, 1, // row = 1 - 2, 2, 2, 2, // row = 2 - // Second batch - 1, 2, 3, 4, // row = 1 - 1, 2, 3, 4, // row = 2 - }; - std::vector filter_data{ - 1, 2, 3, 4, // first 2x2 filter - -1, 1, -1, 1, // second 2x2 filter - -1, -1, 1, 1, // third 2x2 filter - }; - std::vector bias_data{1, 2, 3}; - - std::pair input_quant_param = quantizationParams(-63.5, 64); - std::pair output_quant_param = quantizationParams(-127, 128); - - Tensor input_tensor = - makeInputTensor({2, 2, 4, 1}, input_quant_param.first, input_quant_param.second, - input_data, _memory_manager.get()); - Tensor filter_tensor = - makeInputTensor({3, 2, 2, 1}, input_quant_param.first, input_quant_param.second, - filter_data, _memory_manager.get()); - Tensor bias_tensor = makeInputTensor( - {3}, input_quant_param.first * input_quant_param.first, 0, bias_data, _memory_manager.get()); - Tensor im2col(DataType::U8, Shape({}), {}, ""); - Tensor output_tensor = - makeOutputTensor(DataType::U8, output_quant_param.first, output_quant_param.second); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 2; - params.stride_width = 2; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::NONE; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - _memory_manager->allocate_memory(im2col); - kernel.execute(); - - std::vector ref_output_data{ - 18, 2, 5, // first batch, left - 18, 2, 5, // first batch, right - 17, 4, 3, // second batch, left - 37, 4, 3, // second batch, right - }; - std::vector ref_output_shape{2, 1, 2, 3}; - EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); -} - -TEST_F(Conv2DTest, Uint8_CWQ) -{ - const int output_channels = 3; - std::vector input_data{ - // First batch - 1, 1, 1, 1, // row = 1 - 2, 2, 2, 2, // row = 2 - // Second batch - 1, 2, 3, 4, // row = 1 - 1, 2, 3, 4, // row = 2 - }; - std::vector filter_data{ - 1, 2, 3, 4, // first 2x2 filter - -1, 1, -1, 1, // second 2x2 filter - -1, -1, 1, 1, // third 2x2 filter - }; - std::vector bias_data{1, 2, 3}; - Shape filter_shape{output_channels, 2, 2, 1}; - - std::pair input_quant_param = quantizationParams(0, 4); - std::pair output_quant_param = quantizationParams(-127, 128); - - std::vector> filter_quant_params; - filter_quant_params.push_back(quantizationParams(0, 4)); - filter_quant_params.push_back(quantizationParams(-1, 1)); - filter_quant_params.push_back(quantizationParams(-1, 1)); - - std::vector filter_scales; - std::vector filter_zerops; - for (auto iter : filter_quant_params) - { - filter_scales.push_back(iter.first); - filter_zerops.push_back(iter.second); - } - - std::vector bias_scales; - for (int i = 0; i < output_channels; ++i) - bias_scales.push_back(filter_quant_params[i].first * input_quant_param.first); - std::vector zerop(output_channels, 0); - - Tensor input_tensor = - makeInputTensor({2, 2, 4, 1}, input_quant_param.first, input_quant_param.second, - input_data, _memory_manager.get()); - Tensor filter_tensor = makeInputTensor(filter_shape, filter_scales, filter_zerops, - 0, filter_data, _memory_manager.get()); - Tensor bias_tensor = makeInputTensor({output_channels}, bias_scales, zerop, 0, - bias_data, _memory_manager.get()); - Tensor im2col(DataType::U8, Shape({}), {}, ""); - Tensor output_tensor = - makeOutputTensor(DataType::U8, output_quant_param.first, output_quant_param.second); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 2; - params.stride_width = 2; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::NONE; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - _memory_manager->allocate_memory(im2col); - kernel.execute(); - - std::vector ref_output_data{ - 18, 2, 5, // first batch, left - 18, 2, 5, // first batch, right - 17, 4, 3, // second batch, left - 37, 4, 3, // second batch, right - }; - std::vector ref_output_shape{2, 1, 2, 3}; - EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); -} - -TEST_F(Conv2DTest, SInt8_CWQ) -{ - const int output_channels = 3; - std::vector input_data{ - // First batch - 1, 1, 1, 1, // row = 1 - 2, 2, 2, 2, // row = 2 - // Second batch - 1, 2, 3, 4, // row = 1 - 1, 2, 3, 4, // row = 2 - }; - std::vector filter_data{ - 1, 2, 3, 4, // first 2x2 filter - -1, 1, -1, 1, // second 2x2 filter - -1, -1, 1, 1, // third 2x2 filter - }; - std::vector bias_data{1, 2, 3}; - Shape filter_shape{output_channels, 2, 2, 1}; - - std::pair input_quant_param = quantizationParams(0, 4); - std::pair output_quant_param = quantizationParams(-127, 128); - - std::vector> filter_quant_params; - filter_quant_params.push_back(std::pair(0.5, 0)); - filter_quant_params.push_back(std::pair(0.25, 0)); - filter_quant_params.push_back(std::pair(0.125, 0)); - - std::vector filter_scales; - std::vector filter_zerops; - for (auto iter : filter_quant_params) - { - filter_scales.push_back(iter.first); - filter_zerops.push_back(iter.second); - } - - std::vector bias_scales; - for (int i = 0; i < output_channels; ++i) - bias_scales.push_back(filter_quant_params[i].first * input_quant_param.first); - std::vector zerop(output_channels, 0); - - Tensor input_tensor = - makeInputTensor({2, 2, 4, 1}, input_quant_param.first, input_quant_param.second, - input_data, _memory_manager.get()); - Tensor filter_tensor = makeInputTensor(filter_shape, filter_scales, filter_zerops, - 0, filter_data, _memory_manager.get()); - Tensor bias_tensor = makeInputTensor({output_channels}, bias_scales, zerop, 0, - bias_data, _memory_manager.get()); - Tensor im2col(DataType::S8, Shape({}), {}, ""); - Tensor output_tensor = - makeOutputTensor(DataType::S8, output_quant_param.first, output_quant_param.second); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 2; - params.stride_width = 2; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::NONE; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - _memory_manager->allocate_memory(im2col); - kernel.execute(); - - std::vector ref_output_data{ - 18, 2, 5, // first batch, left - 18, 2, 5, // first batch, right - 17, 4, 3, // second batch, left - 37, 4, 3, // second batch, right - }; - std::vector ref_output_shape{2, 1, 2, 3}; - EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); -} - -TEST_F(Conv2DTest, SInt16) -{ - Shape input_shape{1, 4, 3, 2}; - Shape filter_shape{2, 2, 2, 2}; - Shape bias_shape{2}; - std::vector ref_output_shape{1, 2, 2, 2}; - - std::vector input_data{ - 1, 2, 3, 4, 5, 6, // row = 0 - 7, 8, 9, 10, 11, 12, // row = 1 - 13, 14, 15, 16, 17, 18, // row = 2 - 19, 20, 21, 22, 23, 24, // row = 3 - }; - std::vector filter_data{ - 1, 2, -3, -4, // out = 0, row = 0 - -5, 6, -7, 8, // out = 1, row = 0 - 4, -2, 3, -1, // out = 0, row = 1 - -8, -6, 7, 5, // out = 1, row = 1 - }; - std::vector bias_data{1, 2}; - std::vector ref_output_data{ - 11, 16, 7, 20, // row = 0 - 0, 40, 0, 44, // row = 1 - }; - - Tensor input_tensor = - makeInputTensor(input_shape, 0.25, 0, input_data, _memory_manager.get()); - Tensor filter_tensor = - makeInputTensor(filter_shape, 0.2, 0, filter_data, _memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, 0.25 * 0.2, 0, bias_data, _memory_manager.get()); - Tensor im2col(DataType::S16, Shape({}), {}, ""); - Tensor output_tensor = makeOutputTensor(DataType::S16, 0.5, 0); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 2; - params.stride_width = 1; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::RELU; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - _memory_manager->allocate_memory(im2col); - kernel.execute(); - - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); - EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); -} - -TEST_F(Conv2DTest, SInt16_CWQ_weights) -{ - Shape input_shape{1, 2, 2, 2}; // Batch x H x W x C - Shape filter_shape{3, 1, 1, 2}; // Out channels x H x W x In Channels - Shape bias_shape{3}; - std::vector ref_output_shape{1, 2, 2, 3}; - - std::vector input_data{ - 1, 2, // row = 0, col 0 - 3, 4, // row = 0, col 1 - 5, 6, // row = 1, col 0 - 7, 8, // row = 1, col 1 - }; - std::vector filter_data{ - 4, -3, // out = 0 - 1, -3, // out = 1 - 5, -3, // out = 2 - }; - std::vector bias_data{1, 10, 5}; - std::vector ref_output_data{ - 0, 5, 4, // row 0, col 0 - 1, 1, 8, // row 0, col 1 - 3, 0, 12, // row 1, col 0 - 5, 0, 16, // row 1, col 1 - }; - - float input_scale = 0.25f; - float output_scale = 0.05f; - std::vector filter_scales = {0.25f, 0.2f, 0.1f}; - std::vector bias_scales; - for (int i = 0; i < filter_scales.size(); ++i) - bias_scales.push_back(filter_scales[i] * input_scale); - std::vector zerop = {0, 0, 0}; - - Tensor input_tensor = - makeInputTensor(input_shape, input_scale, 0, input_data, _memory_manager.get()); - Tensor filter_tensor = makeInputTensor(filter_shape, filter_scales, zerop, 0, - filter_data, _memory_manager.get()); - Tensor bias_tensor = makeInputTensor(bias_shape, bias_scales, zerop, 0, bias_data, - _memory_manager.get()); - Tensor im2col(DataType::S16, Shape({}), {}, ""); - Tensor output_tensor = makeOutputTensor(DataType::S16, output_scale, 0); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 1; - params.stride_width = 1; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::RELU; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - _memory_manager->allocate_memory(im2col); - kernel.execute(); - - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); - EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); -} - -TEST_F(Conv2DTest, Unsupported_Type_Configure_NEG) -{ - Shape input_shape{1, 4, 3, 2}; - Shape filter_shape{2, 2, 2, 2}; - Shape bias_shape{2}; - std::vector input_data{ - 1, 2, 3, 4, 5, 6, // row = 0 - 7, 8, 9, 10, 11, 12, // row = 1 - 13, 14, 15, 16, 17, 18, // row = 2 - 19, 20, 21, 22, 23, 24, // row = 3 - }; - std::vector filter_data{ - 1, 2, -3, -4, // out = 0, row = 0 - -5, 6, -7, 8, // out = 1, row = 0 - 4, -2, 3, -1, // out = 0, row = 1 - -8, -6, 7, 5, // out = 1, row = 1 - }; - std::vector bias_data{1, 2}; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor filter_tensor = - makeInputTensor(filter_shape, filter_data, _memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, bias_data, _memory_manager.get()); - Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 2; - params.stride_width = 1; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::RELU; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(Conv2DTest, Invalid_Bias_Type_NEG) -{ - Shape input_shape{1, 4, 3, 2}; - Shape filter_shape{2, 2, 2, 2}; - Shape bias_shape{2}; - std::vector input_data{ - 1, 2, 3, 4, 5, 6, // row = 0 - 7, 8, 9, 10, 11, 12, // row = 1 - 13, 14, 15, 16, 17, 18, // row = 2 - 19, 20, 21, 22, 23, 24, // row = 3 - }; - std::vector filter_data{ - 1, 2, -3, -4, // out = 0, row = 0 - -5, 6, -7, 8, // out = 1, row = 0 - 4, -2, 3, -1, // out = 0, row = 1 - -8, -6, 7, 5, // out = 1, row = 1 - }; - std::vector bias_data{1, 2}; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor filter_tensor = - makeInputTensor(filter_shape, filter_data, _memory_manager.get()); - Tensor bias_tensor = makeInputTensor(bias_shape, bias_data, _memory_manager.get()); - Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 2; - params.stride_width = 1; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::RELU; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(Conv2DTest, Invalid_Bias_Data_NEG) -{ - Shape input_shape{1, 4, 3, 2}; - Shape filter_shape{2, 2, 2, 2}; - Shape bias_shape{3}; - std::vector input_data{ - 1, 2, 3, 4, 5, 6, // row = 0 - 7, 8, 9, 10, 11, 12, // row = 1 - 13, 14, 15, 16, 17, 18, // row = 2 - 19, 20, 21, 22, 23, 24, // row = 3 - }; - std::vector filter_data{ - 1, 2, -3, -4, // out = 0, row = 0 - -5, 6, -7, 8, // out = 1, row = 0 - 4, -2, 3, -1, // out = 0, row = 1 - -8, -6, 7, 5, // out = 1, row = 1 - }; - std::vector bias_data{1, 2, 3}; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor filter_tensor = - makeInputTensor(filter_shape, filter_data, _memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, bias_data, _memory_manager.get()); - Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 2; - params.stride_width = 1; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::RELU; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(Conv2DTest, Invalid_Input_Shape_NEG) -{ - Shape input_shape{1, 4, 6, 1}; - Shape filter_shape{2, 2, 2, 2}; - Shape bias_shape{2}; - std::vector input_data{ - 1, 2, 3, 4, 5, 6, // row = 0 - 7, 8, 9, 10, 11, 12, // row = 1 - 13, 14, 15, 16, 17, 18, // row = 2 - 19, 20, 21, 22, 23, 24, // row = 3 - }; - std::vector filter_data{ - 1, 2, -3, -4, // out = 0, row = 0 - -5, 6, -7, 8, // out = 1, row = 0 - 4, -2, 3, -1, // out = 0, row = 1 - -8, -6, 7, 5, // out = 1, row = 1 - }; - std::vector bias_data{1, 2}; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor filter_tensor = - makeInputTensor(filter_shape, filter_data, _memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, bias_data, _memory_manager.get()); - Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 2; - params.stride_width = 1; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::RELU; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(Conv2DTest, Invalid_fused_act_tanh_NEG) -{ - Shape input_shape{1, 4, 3, 2}; - Shape filter_shape{2, 2, 2, 2}; - Shape bias_shape{2}; - std::vector input_data{ - 1, 2, 3, 4, 5, 6, // row = 0 - 7, 8, 9, 10, 11, 12, // row = 1 - 13, 14, 15, 16, 17, 18, // row = 2 - 19, 20, 21, 22, 23, 24, // row = 3 - }; - std::vector filter_data{ - 1, 2, -3, -4, // out = 0, row = 0 - -5, 6, -7, 8, // out = 1, row = 0 - 4, -2, 3, -1, // out = 0, row = 1 - -8, -6, 7, 5, // out = 1, row = 1 - }; - std::vector bias_data{1, 2}; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor filter_tensor = - makeInputTensor(filter_shape, filter_data, _memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, bias_data, _memory_manager.get()); - Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - Conv2DParams params{}; - params.padding = Padding::VALID; - params.stride_height = 2; - params.stride_width = 1; - params.dilation_height_factor = 1; - params.dilation_width_factor = 1; - params.activation = Activation::TANH; - - Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -} // namespace -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/DepthToSpace.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/DepthToSpace.cpp deleted file mode 100644 index 3a9acd1..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/DepthToSpace.cpp +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "DepthToSpace.h" -#include "Utils.h" -#include "PALDepthToSpace.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -DepthToSpace::DepthToSpace(const Tensor *input, Tensor *output, const DepthToSpaceParams ¶ms) - : KernelWithParams({input}, {output}, params) -{ -} - -void DepthToSpace::configure() -{ - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::FLOAT32 || - output()->element_type() == DataType::U8) - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()) - const int block_size = params().block_size; - const int32_t input_height = input()->shape().dim(1); - const int32_t input_width = input()->shape().dim(2); - const int32_t input_channels = input()->shape().dim(3); - int32_t output_height = input_height * block_size; - int32_t output_width = input_width * block_size; - int32_t output_channels = input_channels / block_size / block_size; - - LUCI_INTERPRETER_CHECK(input_height == output_height / block_size); - LUCI_INTERPRETER_CHECK(input_width == output_width / block_size); - LUCI_INTERPRETER_CHECK(input_channels == output_channels * block_size * block_size); - - Shape output_shape(4); - output_shape.dim(0) = input()->shape().dim(0); - output_shape.dim(1) = output_height; - output_shape.dim(2) = output_width; - output_shape.dim(3) = output_channels; - - output()->resize(output_shape); -} - -void DepthToSpace::execute() const -{ - tflite::DepthToSpaceParams op_params; - op_params.block_size = params().block_size; - switch (input()->element_type()) - { - case DataType::FLOAT32: - luci_interpreter_pal::DepthToSpace(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - case DataType::U8: - luci_interpreter_pal::DepthToSpace(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported Type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/DepthwiseConv2D.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/DepthwiseConv2D.cpp deleted file mode 100644 index c554c30..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/DepthwiseConv2D.cpp +++ /dev/null @@ -1,451 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/DepthwiseConv2D.h" - -#include "kernels/Utils.h" - -#include "PALDepthwiseConv2d.h" - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -DepthwiseConv2D::DepthwiseConv2D(const Tensor *input, const Tensor *filter, const Tensor *bias, - Tensor *output, Tensor *scratchpad, - const DepthwiseConv2DParams ¶ms) - : KernelWithParams({input, filter, bias}, {output, scratchpad}, params) -{ -} - -void DepthwiseConv2D::configure() -{ - // TensorFlow Lite (as of v2.2.0) supports the following combinations of types: - // | input filter bias output | - // ----+---------------------------+ - // (1) | float float float float | - // (2) | float int8 float float | hybrid - // (3) | uint8 uint8 int32 uint8 | quantized - // (4) | int8 int8 int32 int8 | quantized per channel - // (5) | int16 int8 int64 int16 | quantized per channel 16x8 - // - // We only support (1), (3) and (4) for now, and additionally the following: - // | input filter bias output | - // ----+---------------------------+ - // (5) | int16 int16 int64 int16 | - // - if (input()->element_type() == DataType::FLOAT32 && filter()->element_type() == DataType::FLOAT32) - { - LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::FLOAT32); - } - else if (input()->element_type() == DataType::U8 && filter()->element_type() == DataType::U8) - { - LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::S32); - } - else if (input()->element_type() == DataType::S8 && filter()->element_type() == DataType::S8) - { - LUCI_INTERPRETER_CHECK(filter()->shape().num_dims() == 4); - LUCI_INTERPRETER_CHECK(static_cast(filter()->shape().dim(3)) == - filter()->scales().size()); - for (auto zerop : filter()->zero_points()) - { - LUCI_INTERPRETER_CHECK(zerop == 0); - } - LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::S32); - } - else if (input()->element_type() == DataType::S16 && filter()->element_type() == DataType::S16) - { - LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::S64); - } - else - { - throw std::runtime_error("Unsupported type."); - } - LUCI_INTERPRETER_CHECK(output()->element_type() == input()->element_type()); - - const Shape &input_shape = input()->shape(); - const Shape &filter_shape = filter()->shape(); - LUCI_INTERPRETER_CHECK(input_shape.num_dims() == 4 && filter_shape.num_dims() == 4); - - const int32_t batches = input_shape.dim(0); - const int32_t input_height = input_shape.dim(1); - const int32_t input_width = input_shape.dim(2); - // Filter format: [1, H, W, O]. - LUCI_INTERPRETER_CHECK(filter_shape.dim(0) == 1); - const int32_t filter_height = filter_shape.dim(1); - const int32_t filter_width = filter_shape.dim(2); - const int32_t channels_out = filter_shape.dim(3); - - LUCI_INTERPRETER_CHECK(bias() == nullptr || (bias()->shape().num_dims() == 1 && - bias()->shape().dim(0) == channels_out)); - - const int32_t output_height = - computeOutputSize(_params.padding, input_height, filter_height, _params.stride_height, - _params.dilation_height_factor); - const int32_t output_width = - computeOutputSize(_params.padding, input_width, filter_width, _params.stride_width, - _params.dilation_width_factor); - - _padding_height = computePadding(_params.stride_height, _params.dilation_height_factor, - input_height, filter_height, output_height); - _padding_width = computePadding(_params.stride_width, _params.dilation_width_factor, input_width, - filter_width, output_width); - - output()->resize({batches, output_height, output_width, channels_out}); - - tflite::DepthwiseParams params{}; - - params.dilation_height_factor = _params.dilation_height_factor; - params.dilation_width_factor = _params.dilation_width_factor; - - auto scratchpad = getOutputTensors()[1]; - luci_interpreter_pal::SetupScratchpadTensor(scratchpad, params, input()->element_type(), - getTensorShape(input()), getTensorShape(filter()), - getTensorShape(output())); -} - -void DepthwiseConv2D::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - if (filter()->element_type() == DataType::FLOAT32) - { - evalFloat(); - break; - } - throw std::runtime_error("Unsupported type."); - case DataType::U8: - if (filter()->scales().size() == 1) - { - evalQuantized(); - } - else if (filter()->scales().size() > 1) - { - LUCI_INTERPRETER_CHECK(filter()->shape().num_dims() == 4); - LUCI_INTERPRETER_CHECK(filter()->scales().size() == - static_cast(filter()->shape().dim(3))); - evalQuantizedPerChannel(); - } - break; - case DataType::S8: - evalQuantizedS8PerChannel(); - break; - case DataType::S16: - evalQuantizedS16(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void DepthwiseConv2D::evalFloat() const -{ - float activation_min{}; - float activation_max{}; - calculateActivationRange(_params.activation, &activation_min, &activation_max); - - tflite::DepthwiseParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.dilation_height_factor = _params.dilation_height_factor; - params.dilation_width_factor = _params.dilation_width_factor; - params.depth_multiplier = _params.depth_multiplier; - params.float_activation_min = activation_min; - params.float_activation_max = activation_max; - - tflite::reference_ops::DepthwiseConv( - params, getTensorShape(input()), getTensorData(input()), getTensorShape(filter()), - getTensorData(filter()), getTensorShape(bias()), getTensorData(bias()), - getTensorShape(output()), getTensorData(output())); -} - -void DepthwiseConv2D::evalQuantizedPerChannel() const -{ - const auto *input_data = getTensorData(input()); - const auto *filter_data = getTensorData(filter()); - const auto *bias_data = getTensorData(bias()); - auto *output_data = getTensorData(output()); - - const Shape &input_shape = input()->shape(); - const Shape &filter_shape = filter()->shape(); - const Shape &output_shape = output()->shape(); - - const int32_t batches = input_shape.dim(0); - const int32_t input_height = input_shape.dim(1); - const int32_t input_width = input_shape.dim(2); - const int32_t input_depth = input_shape.dim(3); - const int32_t filter_height = filter_shape.dim(1); - const int32_t filter_width = filter_shape.dim(2); - const int32_t output_height = output_shape.dim(1); - const int32_t output_width = output_shape.dim(2); - - const int32_t stride_height = _params.stride_height; - const int32_t stride_width = _params.stride_width; - const int32_t dilation_height_factor = _params.dilation_height_factor; - const int32_t dilation_width_factor = _params.dilation_width_factor; - const int32_t depth_multiplier = _params.depth_multiplier; - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - const std::vector effective_output_scales = - getQuantizedConvolutionMultiplers(input()->scale(), filter()->scales(), output()->scale()); - - std::vector quant_multipliers_raw = - quantizeMultipliers(effective_output_scales); - BroadcastableWrapper quant_multipliers(quant_multipliers_raw); - - for (int batch = 0; batch < batches; ++batch) - { - for (int out_y = 0; out_y < output_height; ++out_y) - { - for (int out_x = 0; out_x < output_width; ++out_x) - { - for (int in_channel = 0; in_channel < input_depth; ++in_channel) - { - for (int m = 0; m < depth_multiplier; ++m) - { - const int output_channel = m + in_channel * depth_multiplier; - const int in_x_origin = (out_x * stride_width) - _padding_width; - const int in_y_origin = (out_y * stride_height) - _padding_height; - int32 acc = 0; - for (int filter_y = 0; filter_y < filter_height; ++filter_y) - { - for (int filter_x = 0; filter_x < filter_width; ++filter_x) - { - const int in_x = in_x_origin + dilation_width_factor * filter_x; - const int in_y = in_y_origin + dilation_height_factor * filter_y; - // Zero padding by omitting the areas outside the image. - const bool is_point_inside_image = - (in_x >= 0) && (in_x < input_width) && (in_y >= 0) && (in_y < input_height); - if (is_point_inside_image) - { - int32 input_val = - input_data[calcOffset(input_shape, batch, in_y, in_x, in_channel)]; - int32 filter_val = - filter_data[calcOffset(filter_shape, 0, filter_y, filter_x, output_channel)]; - acc += (filter_val - filter()->zero_points()[output_channel]) * - (input_val - input()->zero_point()); - } - } - } - if (bias_data) - { - acc += bias_data[output_channel]; - } - int32_t output_multiplier = quant_multipliers[output_channel].multiplier; - int output_shift = quant_multipliers[output_channel].shift; - int32_t scaled_acc = - tflite::MultiplyByQuantizedMultiplier(acc, output_multiplier, output_shift); - scaled_acc += output()->zero_point(); - scaled_acc = std::max(scaled_acc, activation_min); - scaled_acc = std::min(scaled_acc, activation_max); - output_data[calcOffset(output_shape, batch, out_y, out_x, output_channel)] = - static_cast(scaled_acc); - } - } - } - } - } -} - -void DepthwiseConv2D::evalQuantized() const -{ - const auto input_scale = static_cast(input()->scale()); - const auto filter_scale = static_cast(filter()->scale()); - const auto output_scale = static_cast(output()->scale()); - - const double real_multiplier = input_scale * filter_scale / output_scale; - int32_t output_multiplier{}; - int output_shift{}; - quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - tflite::DepthwiseParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.dilation_height_factor = _params.dilation_height_factor; - params.dilation_width_factor = _params.dilation_width_factor; - params.depth_multiplier = _params.depth_multiplier; - // The kernel expects input and filter zero points to be negated. - params.input_offset = -input()->zero_point(); // Note the '-'. - params.weights_offset = -filter()->zero_point(); // Note the '-'. - params.output_offset = output()->zero_point(); - params.output_multiplier = output_multiplier; - params.output_shift = output_shift; - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - tflite::reference_ops::DepthwiseConv( - params, getTensorShape(input()), getTensorData(input()), getTensorShape(filter()), - getTensorData(filter()), getTensorShape(bias()), getTensorData(bias()), - getTensorShape(output()), getTensorData(output())); -} - -void DepthwiseConv2D::evalQuantizedS8PerChannel() const -{ - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - tflite::DepthwiseParams params{}; - - params.padding_type = tflite::PaddingType::kSame; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.dilation_height_factor = _params.dilation_height_factor; - params.dilation_width_factor = _params.dilation_width_factor; - params.depth_multiplier = _params.depth_multiplier; - // The kernel expects input and filter zero points to be negated. - params.input_offset = -input()->zero_point(); // Note the '-'. - params.weights_offset = 0; - params.output_offset = output()->zero_point(); - params.output_multiplier = 1; // unused in tflite code - params.output_shift = 0; // unused in tflite code - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - const std::vector effective_output_scales = - getQuantizedConvolutionMultiplers(input()->scale(), filter()->scales(), output()->scale()); - - std::vector quant_multipliers = - quantizeMultipliers(effective_output_scales); - - std::vector shifts; - std::transform(quant_multipliers.begin(), quant_multipliers.end(), std::back_inserter(shifts), - [](ChannelQuantMultipliers cm) { return cm.shift; }); - std::vector multipliers; - std::transform(quant_multipliers.begin(), quant_multipliers.end(), - std::back_inserter(multipliers), - [](ChannelQuantMultipliers cm) { return cm.multiplier; }); - - auto scratchpad = getOutputTensors()[1]; - int8_t *scratchpad_data = nullptr; - if (scratchpad->is_allocatable()) - scratchpad_data = scratchpad->data(); - - luci_interpreter_pal::DepthwiseConvPerChannel( - params, multipliers.data(), shifts.data(), getTensorShape(input()), - getTensorData(input()), getTensorShape(filter()), getTensorData(filter()), - getTensorShape(bias()), getTensorData(bias()), getTensorShape(output()), - getTensorData(output()), getTensorShape(scratchpad), scratchpad_data); -} - -void DepthwiseConv2D::evalQuantizedS16() const -{ - const auto *input_data = getTensorData(input()); - const auto *filter_data = getTensorData(filter()); - const auto *bias_data = getTensorData(bias()); - auto *output_data = getTensorData(output()); - - const Shape &input_shape = input()->shape(); - const Shape &filter_shape = filter()->shape(); - const Shape &output_shape = output()->shape(); - - const int32_t batches = input_shape.dim(0); - const int32_t input_height = input_shape.dim(1); - const int32_t input_width = input_shape.dim(2); - const int32_t input_depth = input_shape.dim(3); - const int32_t filter_height = filter_shape.dim(1); - const int32_t filter_width = filter_shape.dim(2); - const int32_t output_height = output_shape.dim(1); - const int32_t output_width = output_shape.dim(2); - - const int32_t stride_height = _params.stride_height; - const int32_t stride_width = _params.stride_width; - const int32_t dilation_height_factor = _params.dilation_height_factor; - const int32_t dilation_width_factor = _params.dilation_width_factor; - const int32_t depth_multiplier = _params.depth_multiplier; - - const std::vector effective_output_scales = - getQuantizedConvolutionMultiplers(input()->scale(), filter()->scales(), output()->scale()); - - std::vector quant_multipliers_raw = - quantizeMultipliers(effective_output_scales); - - BroadcastableWrapper quant_multipliers(quant_multipliers_raw); - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - for (int32_t batch = 0; batch < batches; ++batch) - { - for (int32_t out_y = 0; out_y < output_height; ++out_y) - { - for (int32_t out_x = 0; out_x < output_width; ++out_x) - { - for (int32_t in_c = 0; in_c < input_depth; ++in_c) - { - for (int32_t m = 0; m < depth_multiplier; ++m) - { - const int32_t out_c = m + in_c * depth_multiplier; - const int32_t in_y_origin = out_y * stride_height - _padding_height; - const int32_t in_x_origin = out_x * stride_width - _padding_width; - int64_t acc = 0; - for (int32_t filter_y = 0; filter_y < filter_height; ++filter_y) - { - for (int32_t filter_x = 0; filter_x < filter_width; ++filter_x) - { - const int32_t in_y = in_y_origin + dilation_height_factor * filter_y; - const int32_t in_x = in_x_origin + dilation_width_factor * filter_x; - if ((in_y >= 0 && in_y < input_height) && (in_x >= 0 && in_x < input_width)) - { - const int16_t input_val = - input_data[calcOffset(input_shape, batch, in_y, in_x, in_c)]; - const int16_t filter_val = - filter_data[calcOffset(filter_shape, 0, filter_y, filter_x, out_c)]; - acc += static_cast(input_val) * static_cast(filter_val); - } - } - } - if (bias_data != nullptr) - { - acc += bias_data[out_c]; - } - - int32_t output_multiplier = quant_multipliers[out_c].multiplier; - int output_shift = quant_multipliers[out_c].shift; - int32_t scaled_acc = - tflite::MultiplyByQuantizedMultiplier(acc, output_multiplier, output_shift); - - scaled_acc = std::max(scaled_acc, activation_min); - scaled_acc = std::min(scaled_acc, activation_max); - - output_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] = scaled_acc; - } - } - } - } - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Dequantize.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Dequantize.cpp deleted file mode 100644 index 96399e5..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Dequantize.cpp +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Dequantize.h" -#include "kernels/Utils.h" -#include "PALDequantize.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -Dequantize::Dequantize(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Dequantize::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == loco::DataType::S8 || - input()->element_type() == loco::DataType::U8 || - input()->element_type() == loco::DataType::S16); - - LUCI_INTERPRETER_CHECK(input()->scales().size() == 1); - - if (input()->element_type() == loco::DataType::S16) - LUCI_INTERPRETER_CHECK(input()->zero_point() == 0); - - LUCI_INTERPRETER_CHECK(output()->element_type() == loco::DataType::FLOAT32); - - output()->resize(input()->shape()); -} - -void Dequantize::execute() const -{ - tflite::DequantizationParams op_params; - op_params.zero_point = input()->zero_point(); - op_params.scale = input()->scale(); - - switch (input()->element_type()) - { - case loco::DataType::U8: - { - luci_interpreter_pal::Dequantize(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - } - case loco::DataType::S8: - { - luci_interpreter_pal::Dequantize(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - } - case loco::DataType::S16: - { - luci_interpreter_pal::Dequantize(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - } - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Div.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Div.cpp deleted file mode 100644 index dd15322..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Div.cpp +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Div.h" - -#include "kernels/Utils.h" - -#include -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Div::Div(const Tensor *input1, const Tensor *input2, Tensor *output, const DivParams ¶ms) - : KernelWithParams({input1, input2}, {output}, params) -{ -} - -void Div::configure() -{ - LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); - LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()); - - output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); -} - -void Div::execute() const -{ - switch (input1()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::S64: - evalInteger(); - break; - case DataType::S32: - evalInteger(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Div::evalFloat() const -{ - tflite::ArithmeticParams params{}; - fillArithmeticActivationRange(params, _params.activation); - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - tflite::reference_ops::BroadcastDivSlow( - params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), - getTensorData(input2()), getTensorShape(output()), getTensorData(output())); - } - else - { - tflite::reference_ops::Div(params, getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -template void Div::evalInteger() const -{ - tflite::ArithmeticParams params{}; - fillArithmeticActivationRange(params, _params.activation); - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - tflite::reference_ops::BroadcastDivSlow( - params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), - getTensorData(input2()), getTensorShape(output()), getTensorData(output())); - } - else - { - tflite::reference_ops::Div(params, getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -void Div::evalQuantized() const -{ - const auto input1_scale = static_cast(input1()->scale()); - const auto input2_scale = static_cast(input2()->scale()); - const auto output_scale = static_cast(output()->scale()); - - const double real_output_multiplier = input1_scale / (input2_scale * output_scale); - - int32_t output_multiplier{}; - int output_shift{}; - - quantizeMultiplier(real_output_multiplier, &output_multiplier, &output_shift); - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - tflite::ArithmeticParams params{}; - - params.input1_offset = -input1()->zero_point(); // Note the '-'. - params.input2_offset = -input2()->zero_point(); // Note the '-'. - params.output_offset = output()->zero_point(); - params.output_multiplier = output_multiplier; - params.output_shift = output_shift; - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - tflite::reference_ops::BroadcastDivSlow( - params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), - getTensorData(input2()), getTensorShape(output()), getTensorData(output())); - } - else - { - tflite::reference_ops::Div(params, getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Elu.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Elu.cpp deleted file mode 100644 index 697d63b..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Elu.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Elu.h" -#include "kernels/Utils.h" - -#include "PALElu.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Elu::Elu(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Elu::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - output()->resize(input()->shape()); -} - -void Elu::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - luci_interpreter_pal::Elu(getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Equal.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Equal.cpp deleted file mode 100644 index a57e127..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Equal.cpp +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Equal.h" -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Equal::Equal(const Tensor *x, const Tensor *y, Tensor *output) : Kernel({x, y}, {output}) {} - -void Equal::configure() -{ - LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); - - if (x()->element_type() == DataType::U8) - { - quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); - quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); - } - output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); -} - -void Equal::execute() const -{ - switch (x()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::S64: - evalInteger(); - break; - case DataType::S32: - evalInteger(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Equal::evalFloat() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowEqual(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::Equal(op_params, getTensorShape(x()), x_data, getTensorShape(y()), - y_data, getTensorShape(output()), output_data); - } -} - -template void Equal::evalInteger() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowEqualNoScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::EqualNoScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } -} - -void Equal::evalQuantized() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.left_shift = 8; - op_params.input1_offset = -x()->zero_point(); // Note the '-' - op_params.input1_shift = _x_shift; - op_params.input1_multiplier = _x_multiplier; - op_params.input2_offset = -y()->zero_point(); // Note the '-' - op_params.input2_shift = _y_shift; - op_params.input2_multiplier = _y_multiplier; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowEqualWithScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::EqualWithScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Exp.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Exp.cpp deleted file mode 100644 index e7c560a..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Exp.cpp +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2018 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Exp.h" - -#include "kernels/Utils.h" - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Exp::Exp(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Exp::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - output()->resize(input()->shape()); -} - -void Exp::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Exp::evalFloat() const -{ - const int size = tflite::MatchingFlatSize(getTensorShape(input()), getTensorShape(output())); - tflite::reference_ops::Exp(getTensorData(input()), size, getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.cpp deleted file mode 100644 index ba35c99..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.cpp +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/ExpandDims.h" -#include "kernels/Utils.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -ExpandDims::ExpandDims(const Tensor *input, const Tensor *axis, Tensor *output) - : Kernel({input, axis}, {output}) -{ -} - -void ExpandDims::configure() -{ - int32_t axis_value; - - switch (axis()->element_type()) - { - case loco::DataType::S32: - axis_value = *getTensorData(axis()); - break; - case loco::DataType::S64: - axis_value = static_cast(*getTensorData(axis())); - break; - default: - throw std::runtime_error("Unsupported type."); - } - - const auto input_shape = input()->shape(); - - if (axis_value < 0) - { - axis_value += input_shape.num_dims() + 1; - } - - LUCI_INTERPRETER_CHECK(axis_value <= input_shape.num_dims() and axis_value >= 0); - - Shape output_shape(input_shape.num_dims() + 1); - for (int32_t i = 0; i < output_shape.num_dims(); ++i) - { - if (i < axis_value) - { - output_shape.dim(i) = input_shape.dim(i); - } - else if (i == axis_value) - { - output_shape.dim(i) = 1; - } - else - { - LUCI_INTERPRETER_CHECK(i >= 1); - output_shape.dim(i) = input_shape.dim(i - 1); - } - } - - output()->resize(output_shape); -} - -void ExpandDims::execute() const -{ - // Just copy input to output - const auto *input_data = input()->data(); - auto *output_data = output()->data(); - - const size_t element_size = getDataTypeSize(input()->element_type()); - const int32_t num_elements = input()->shape().num_elements(); - std::memcpy(output_data, input_data, num_elements * element_size); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.h b/compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.h deleted file mode 100644 index e510b11..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_KERNELS_EXPAND_DIMS_H -#define LUCI_INTERPRETER_KERNELS_EXPAND_DIMS_H - -#include "core/Kernel.h" -#include "core/KernelParams.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -class ExpandDims : public Kernel -{ -public: - ExpandDims(const Tensor *input, const Tensor *axis, Tensor *output); - - const Tensor *input() const { return _inputs[0]; } - const Tensor *axis() const { return _inputs[1]; } - Tensor *output() const { return _outputs[0]; } - - void configure() override; - void execute() const override; -}; - -} // namespace kernels -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_KERNELS_EXPAND_DIMS_H diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.test.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.test.cpp deleted file mode 100644 index df9eacc..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/ExpandDims.test.cpp +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2017 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/ExpandDims.h" -#include "kernels/TestUtils.h" -#include "luci_interpreter/TestMemoryManager.h" - -namespace luci_interpreter -{ -namespace kernels -{ -namespace -{ - -using namespace testing; - -class ExpandDimsTest : public ::testing::Test -{ -protected: - void SetUp() override { _memory_manager = std::make_unique(); } - - std::unique_ptr _memory_manager; -}; - -TEST_F(ExpandDimsTest, PositiveAxis) -{ - std::vector input_data{-1, 1, -2, 2}; - std::initializer_list input_shape = {2, 2}; - - std::initializer_list axis_value = {0}; - - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor axis_tensor = makeInputTensor({1}, axis_value, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::S32); - - ExpandDims kernel(&input_tensor, &axis_tensor, &output_tensor); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorData(output_tensor), ::testing::ElementsAreArray(input_data)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray({1, 2, 2})); -} - -TEST_F(ExpandDimsTest, NegAxis) -{ - std::vector input_data{-1, 1, -2, 2}; - std::initializer_list input_shape = {2, 2}; - - std::initializer_list axis_value = {-1}; - - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor axis_tensor = makeInputTensor({1}, axis_value, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::S32); - - ExpandDims kernel(&input_tensor, &axis_tensor, &output_tensor); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorData(output_tensor), ::testing::ElementsAreArray(input_data)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray({2, 2, 1})); -} - -TEST_F(ExpandDimsTest, InvalidAxisType_NEG) -{ - std::vector input_data{-1, 1, -2, 2}; - std::initializer_list input_shape = {2, 2}; - - std::initializer_list axis_value = {1.0}; - - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor axis_tensor = makeInputTensor({1}, axis_value, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::S32); - - ExpandDims kernel(&input_tensor, &axis_tensor, &output_tensor); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST_F(ExpandDimsTest, InvalidAxisValue_NEG) -{ - std::vector input_data{-1, 1, -2, 2}; - std::initializer_list input_shape = {2, 2}; - - std::initializer_list axis_value = {3}; - - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor axis_tensor = makeInputTensor({1}, axis_value, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::S32); - - ExpandDims kernel(&input_tensor, &axis_tensor, &output_tensor); - EXPECT_ANY_THROW(kernel.configure()); -} - -} // namespace -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Fill.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Fill.cpp deleted file mode 100644 index e09d633..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Fill.cpp +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Fill.h" -#include "kernels/Utils.h" -#include "tensorflow/lite/kernels/internal/reference/reference_ops.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -Fill::Fill(const Tensor *dims, const Tensor *value, Tensor *output) - : Kernel({dims, value}, {output}) -{ -} - -template void Fill::configureShape() -{ - const auto dims_data = getTensorData(dims()); - Shape output_shape(dims()->shape().dim(0)); - - for (int i = 0; i < output_shape.num_dims(); ++i) - { - T data = dims_data[i]; - if (data < 0) - throw std::runtime_error("Fill dimensions must be >= 0"); - - output_shape.dim(i) = data; - } - - output()->resize(output_shape); -} - -void Fill::configure() -{ - const auto dims_shape = dims()->shape(); - const auto value_shape = value()->shape(); - - // Make sure the 1st input tensor is 1-D - LUCI_INTERPRETER_CHECK(dims_shape.num_dims() == 1); - - // Make sure the 1st input tensor is int32 or int64 - LUCI_INTERPRETER_CHECK(dims()->element_type() == DataType::S32 or - dims()->element_type() == DataType::S64); - - // Make sure the 2nd input tensor is a scalar - LUCI_INTERPRETER_CHECK(value_shape.num_dims() == 0) - - // Check zero point and scale for S16 and S8 - if (value()->element_type() == loco::DataType::S16 or - value()->element_type() == loco::DataType::S8) - { - LUCI_INTERPRETER_CHECK(value()->scale() == output()->scale()); - LUCI_INTERPRETER_CHECK(value()->zero_point() == output()->zero_point()); - - if (value()->element_type() == loco::DataType::S16) - LUCI_INTERPRETER_CHECK(value()->zero_point() == 0); - } - // Resize output - switch (dims()->element_type()) - { - case DataType::S32: - configureShape(); - break; - case DataType::S64: - configureShape(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Fill::execute() const -{ - switch (output()->element_type()) - { - case DataType::S8: - tflite::reference_ops::Fill(getTensorShape(value()), getTensorData(value()), - getTensorShape(output()), getTensorData(output())); - break; - case DataType::S16: - tflite::reference_ops::Fill(getTensorShape(value()), getTensorData(value()), - getTensorShape(output()), getTensorData(output())); - break; - case DataType::S32: - tflite::reference_ops::Fill(getTensorShape(value()), getTensorData(value()), - getTensorShape(output()), getTensorData(output())); - break; - case DataType::S64: - tflite::reference_ops::Fill(getTensorShape(value()), getTensorData(value()), - getTensorShape(output()), getTensorData(output())); - break; - case DataType::FLOAT32: - tflite::reference_ops::Fill(getTensorShape(value()), getTensorData(value()), - getTensorShape(output()), getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Floor.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Floor.cpp deleted file mode 100644 index e3c4246..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Floor.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Floor.h" -#include "kernels/Utils.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Floor::Floor(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Floor::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - output()->resize(input()->shape()); -} - -void Floor::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Floor::evalFloat() const -{ - tflite::reference_ops::Floor(getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/FloorDiv.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/FloorDiv.cpp deleted file mode 100644 index a7a10a3..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/FloorDiv.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/FloorDiv.h" -#include "kernels/Utils.h" - -#include -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -FloorDiv::FloorDiv(const Tensor *input, const Tensor *alpha, Tensor *output) - : Kernel({input, alpha}, {output}) -{ -} - -void FloorDiv::configure() -{ - LUCI_INTERPRETER_CHECK(x()->element_type() == output()->element_type()); - LUCI_INTERPRETER_CHECK(y()->element_type() == output()->element_type()); - - output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); -} - -void FloorDiv::execute() const -{ - switch (x()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void FloorDiv::evalFloat() const -{ - auto FloorDivFunc = [](float x, float y) -> float { - return std::floor(static_cast(x) / static_cast(y)); - }; - - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - - // Check the denominator - for (int i = 0; i < getTensorShape(y()).FlatSize(); ++i) - { - LUCI_INTERPRETER_CHECK(y_data[i] != 0); - } - - if (x()->shape() != y()->shape()) - { - tflite::reference_ops::BroadcastBinaryFunction4DSlow( - getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), - getTensorData(output()), FloorDivFunc); - } - else - { - tflite::reference_ops::BinaryFunction( - getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), - getTensorData(output()), FloorDivFunc); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.cpp deleted file mode 100644 index bd2bb2f..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.cpp +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/FullyConnected.h" - -#include "kernels/Utils.h" - -#include "PALFullyConnected.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -FullyConnected::FullyConnected(const Tensor *input, const Tensor *weights, const Tensor *bias, - Tensor *output, const FullyConnectedParams ¶ms) - : KernelWithParams({input, weights, bias}, {output}, params) -{ -} - -void FullyConnected::configure() -{ - if (weights()->element_type() == DataType::U8) - { - LUCI_INTERPRETER_CHECK(input()->element_type() == DataType::U8); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::U8); - LUCI_INTERPRETER_CHECK(!bias() || bias()->element_type() == DataType::S32) - } - else if (weights()->element_type() == DataType::FLOAT32) - { - LUCI_INTERPRETER_CHECK(input()->element_type() == DataType::FLOAT32); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::FLOAT32); - LUCI_INTERPRETER_CHECK(!bias() || bias()->element_type() == DataType::FLOAT32) - } - else if (weights()->element_type() == DataType::S8) - { - LUCI_INTERPRETER_CHECK(input()->element_type() == DataType::S8); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::S8); - LUCI_INTERPRETER_CHECK(!bias() || bias()->element_type() == DataType::S32) - } - else - { - throw std::runtime_error("Unsupported type."); - } - - const Shape &input_shape = input()->shape(); - const Shape &weights_shape = weights()->shape(); - - LUCI_INTERPRETER_CHECK(weights_shape.num_dims() == 2); - LUCI_INTERPRETER_CHECK(bias() == nullptr || - bias()->shape().num_elements() == weights_shape.dim(0)); - - LUCI_INTERPRETER_CHECK(input_shape.num_elements() % weights_shape.dim(1) == 0); - const int32_t batch_size = input_shape.num_elements() / weights_shape.dim(1); - const int32_t num_units = weights_shape.dim(0); - - if (bias()) - LUCI_INTERPRETER_CHECK(bias()->shape().num_elements() == weights()->shape().dim(0)); - - if (params().keep_num_dims == false) - { - output()->resize({batch_size, num_units}); - } - else - { - luci_interpreter::Shape output_shape(input_shape.num_dims()); - for (int i = 0; i < input_shape.num_dims(); ++i) - output_shape.dim(i) = input_shape.dim(i); - output_shape.dim(input_shape.num_dims() - 1) = num_units; - output()->resize(output_shape); - } -} - -void FullyConnected::execute() const -{ - switch (input()->element_type()) - { - case DataType::U8: - evalQuantized(); - break; - case DataType::S8: - evalQuantizedS8(); - break; - case DataType::FLOAT32: - evalFloat(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void FullyConnected::evalFloat() const -{ - float activation_min{}; - float activation_max{}; - calculateActivationRange(_params.activation, &activation_min, &activation_max); - - tflite::FullyConnectedParams params{}; - params.float_activation_min = activation_min; - params.float_activation_max = activation_max; - params.weights_format = tflite::FullyConnectedWeightsFormat::kDefault; - - tflite::reference_ops::FullyConnected( - params, getTensorShape(input()), getTensorData(input()), getTensorShape(weights()), - getTensorData(weights()), getTensorShape(bias()), getTensorData(bias()), - getTensorShape(output()), getTensorData(output())); -} - -void FullyConnected::evalQuantized() const -{ - double real_multiplier = 0.0; - int output_shift; - int32_t output_activation_min; - int32_t output_activation_max; - int32_t output_multiplier; - real_multiplier = - getQuantizedConvolutionMultipler(input()->scale(), weights()->scale(), output()->scale()); - quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); - calculateActivationRangeQuantized(params().activation, output(), &output_activation_min, - &output_activation_max); - - int32_t input_offset = -input()->zero_point(); - int32_t filter_offset = -weights()->zero_point(); - int32_t output_offset = output()->zero_point(); - - tflite::FullyConnectedParams op_params{}; - op_params.input_offset = input_offset; - op_params.weights_offset = filter_offset; - op_params.output_offset = output_offset; - op_params.output_multiplier = output_multiplier; - op_params.output_shift = output_shift; - op_params.quantized_activation_min = output_activation_min; - op_params.quantized_activation_max = output_activation_max; - op_params.lhs_cacheable = false; - op_params.rhs_cacheable = false; - tflite::reference_ops::FullyConnected( - op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(weights()), - getTensorData(weights()), getTensorShape(bias()), getTensorData(bias()), - getTensorShape(output()), getTensorData(output())); -} - -void FullyConnected::evalQuantizedS8() const -{ - double real_multiplier = 0.0; - int output_shift; - int32_t output_activation_min; - int32_t output_activation_max; - int32_t output_multiplier; - real_multiplier = - getQuantizedConvolutionMultipler(input()->scale(), weights()->scale(), output()->scale()); - quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); - calculateActivationRangeQuantized(params().activation, output(), &output_activation_min, - &output_activation_max); - - int32_t input_offset = -input()->zero_point(); - int32_t filter_offset = -weights()->zero_point(); - int32_t output_offset = output()->zero_point(); - - tflite::FullyConnectedParams op_params{}; - op_params.input_offset = input_offset; - op_params.weights_offset = filter_offset; - op_params.output_offset = output_offset; - op_params.output_multiplier = output_multiplier; - op_params.output_shift = output_shift; - op_params.quantized_activation_min = output_activation_min; - op_params.quantized_activation_max = output_activation_max; - op_params.lhs_cacheable = false; - op_params.rhs_cacheable = false; - luci_interpreter_pal::FullyConnected( - op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(weights()), - getTensorData(weights()), getTensorShape(bias()), getTensorData(bias()), - getTensorShape(output()), getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.h b/compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.h deleted file mode 100644 index 2a7c068..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_KERNELS_FULLYCONNECTED_H -#define LUCI_INTERPRETER_KERNELS_FULLYCONNECTED_H - -#include "core/Kernel.h" -#include "core/KernelParams.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -class FullyConnected : public KernelWithParams -{ -public: - FullyConnected(const Tensor *input, const Tensor *weights, const Tensor *bias, Tensor *output, - const FullyConnectedParams ¶ms); - - const Tensor *input() const { return _inputs[0]; } - const Tensor *weights() const { return _inputs[1]; } - const Tensor *bias() const { return _inputs[2]; } - Tensor *output() const { return _outputs[0]; } - - void configure() override; - void execute() const override; - -private: - void evalFloat() const; - void evalQuantized() const; - void evalQuantizedS8() const; -}; - -} // namespace kernels -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_KERNELS_FULLYCONNECTED_H diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.test.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.test.cpp deleted file mode 100644 index 4474cc4..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/FullyConnected.test.cpp +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/FullyConnected.h" -#include "kernels/TestUtils.h" -#include "luci_interpreter/TestMemoryManager.h" - -namespace luci_interpreter -{ -namespace kernels -{ -namespace -{ - -using namespace testing; - -template -void Check(std::initializer_list input_shape, std::initializer_list weights_shape, - std::initializer_list bias_shape, std::initializer_list output_shape, - std::initializer_list input_data, std::initializer_list weights_data, - std::initializer_list bias_data, std::initializer_list output_data) -{ - std::unique_ptr memory_manager = std::make_unique(); - Tensor input_tensor = - makeInputTensor(input_shape, input_data, memory_manager.get()); - Tensor weights_tensor = - makeInputTensor(weights_shape, weights_data, memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, bias_data, memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - FullyConnectedParams params{}; - params.activation = Activation::RELU; - - FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); - kernel.configure(); - memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); - EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(output_data)); -} - -template <> -void Check(std::initializer_list input_shape, - std::initializer_list weights_shape, - std::initializer_list bias_shape, - std::initializer_list output_shape, - std::initializer_list input_data, - std::initializer_list weights_data, - std::initializer_list bias_data, std::initializer_list output_data) -{ - std::unique_ptr memory_manager = std::make_unique(); - const float quantized_tolerance = getTolerance(-127, 128, 255); - std::pair input_quant_param = quantizationParams(-63.5, 64); - std::pair output_quant_param = quantizationParams(-127, 128); - Tensor input_tensor = - makeInputTensor(input_shape, input_quant_param.first, input_quant_param.second, - input_data, memory_manager.get()); - Tensor weights_tensor = - makeInputTensor(weights_shape, input_quant_param.first, input_quant_param.second, - weights_data, memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, input_quant_param.first * input_quant_param.first, 0, - bias_data, memory_manager.get()); - Tensor output_tensor = - makeOutputTensor(DataType::S8, output_quant_param.first, output_quant_param.second); - - FullyConnectedParams params{}; - params.activation = Activation::RELU; - - FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); - kernel.configure(); - memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); - EXPECT_THAT(dequantizeTensorData(output_tensor), - FloatArrayNear(output_data, quantized_tolerance)); -} - -template <> -void Check( - std::initializer_list input_shape, std::initializer_list weights_shape, - std::initializer_list bias_shape, std::initializer_list output_shape, - std::initializer_list input_data, std::initializer_list weights_data, - std::initializer_list bias_data, std::initializer_list output_data) -{ - std::unique_ptr memory_manager = std::make_unique(); - const float quantized_tolerance = getTolerance(-127, 128, 255); - std::pair input_quant_param = quantizationParams(-63.5, 64); - std::pair output_quant_param = quantizationParams(-127, 128); - Tensor input_tensor = - makeInputTensor(input_shape, input_quant_param.first, input_quant_param.second, - input_data, memory_manager.get()); - Tensor weights_tensor = - makeInputTensor(weights_shape, input_quant_param.first, input_quant_param.second, - weights_data, memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, input_quant_param.first * input_quant_param.first, 0, - bias_data, memory_manager.get()); - Tensor output_tensor = - makeOutputTensor(DataType::U8, output_quant_param.first, output_quant_param.second); - - FullyConnectedParams params{}; - params.activation = Activation::RELU; - - FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); - kernel.configure(); - memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); - EXPECT_THAT(dequantizeTensorData(output_tensor), - FloatArrayNear(output_data, quantized_tolerance)); -} - -template class FullyConnectedTest : public ::testing::Test -{ -}; - -using DataTypes = ::testing::Types; -TYPED_TEST_SUITE(FullyConnectedTest, DataTypes); - -TYPED_TEST(FullyConnectedTest, Simple) -{ - Check({3, 2, 2, 1}, {3, 6}, {3}, {2, 3}, - { - -3, -5, 5, 4, 9, -2, // batch = 0 - -3, -2, -4, 9, -8, 1, // batch = 1 - }, - { - -3, -7, 4, -4, -6, 4, // unit = 0 - 3, 5, 2, 3, -3, -8, // unit = 1 - -3, 7, 4, 9, 0, -5, // unit = 2 - }, - {-1, -5, -8}, - { - 0, 0, 32, // batch = 0 - 22, 11, 47, // batch = 1 - }); -} - -TEST(FullyConnectedTest, InvalidBiasType_NEG) -{ - Shape input_shape{3, 2, 2, 1}; - std::vector input_data{ - -3, -5, 5, 4, 9, -2, // batch = 0 - -3, -2, -4, 9, -8, 1, // batch = 1 - }; - Shape weights_shape{3, 6}; - std::vector weights_data{ - -3, -7, 4, -4, -6, 4, // unit = 0 - 3, 5, 2, 3, -3, -8, // unit = 1 - -3, 7, 4, 9, 0, -5, // unit = 2 - }; - Shape bias_shape{3}; - std::vector bias_data{-1, -5, -8}; - - std::unique_ptr memory_manager = std::make_unique(); - - Tensor input_tensor = - makeInputTensor(input_shape, input_data, memory_manager.get()); - Tensor weights_tensor = - makeInputTensor(weights_shape, weights_data, memory_manager.get()); - Tensor bias_tensor = makeInputTensor(bias_shape, bias_data, memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - FullyConnectedParams params{}; - params.activation = Activation::RELU; - - FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST(FullyConnectedTest, InvalidWeightShapeDim_NEG) -{ - Shape input_shape{3, 2, 2, 1}; - std::vector input_data{ - -3, -5, 5, 4, 9, -2, // batch = 0 - -3, -2, -4, 9, -8, 1, // batch = 1 - }; - Shape weights_shape{1, 3, 6}; - std::vector weights_data{ - -3, -7, 4, -4, -6, 4, // unit = 0 - 3, 5, 2, 3, -3, -8, // unit = 1 - -3, 7, 4, 9, 0, -5, // unit = 2 - }; - Shape bias_shape{3}; - std::vector bias_data{-1, -5, -8}; - - std::unique_ptr memory_manager = std::make_unique(); - - Tensor input_tensor = - makeInputTensor(input_shape, input_data, memory_manager.get()); - Tensor weights_tensor = - makeInputTensor(weights_shape, weights_data, memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, bias_data, memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - FullyConnectedParams params{}; - params.activation = Activation::RELU; - - FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST(FullyConnectedTest, BiasElementNumWeightDimMismatch_NEG) -{ - Shape input_shape{3, 2, 2, 1}; - std::vector input_data{ - -3, -5, 5, 4, 9, -2, // batch = 0 - -3, -2, -4, 9, -8, 1, // batch = 1 - }; - Shape weights_shape{6, 3}; - std::vector weights_data{ - -3, -7, 4, // unit = 0 - -4, -6, 4, // unit = 1 - 3, 5, 2, // unit = 2 - 3, -3, -8, // unit = 3 - -3, 7, 4, // unit = 4 - 9, 0, -5, // unit = 5 - }; - Shape bias_shape{3}; - std::vector bias_data{-1, -5, -8}; - - std::unique_ptr memory_manager = std::make_unique(); - - Tensor input_tensor = - makeInputTensor(input_shape, input_data, memory_manager.get()); - Tensor weights_tensor = - makeInputTensor(weights_shape, weights_data, memory_manager.get()); - Tensor bias_tensor = - makeInputTensor(bias_shape, bias_data, memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - FullyConnectedParams params{}; - params.activation = Activation::RELU; - - FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); - EXPECT_ANY_THROW(kernel.configure()); -} - -} // namespace -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Gather.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Gather.cpp deleted file mode 100644 index f125666..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Gather.cpp +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2021 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Gather.h" -#include "kernels/Utils.h" -#include "PALGather.h" - -#include -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Gather::Gather(const Tensor *params, const Tensor *indices, Tensor *output, - const GatherParams &gparams) - : KernelWithParams({params, indices}, {output}, gparams) -{ -} - -void Gather::configure() -{ - if (params()->element_type() == DataType::FLOAT32) - { - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::FLOAT32); - } - else - { - throw std::runtime_error("Unsupported type."); - } - - LUCI_INTERPRETER_CHECK(indices()->element_type() == DataType::S32 || - indices()->element_type() == DataType::S64); - - // refer tensorflow/lite/kernels/gather.cc - - const Shape ¶ms_shape = params()->shape(); - const Shape &indices_shape = indices()->shape(); - - int axis = _params.axis; - if (axis < 0) - { - axis += params_shape.num_dims(); - } - LUCI_INTERPRETER_CHECK(0 <= axis && axis < params_shape.num_dims()); - - int batch_dims = _params.batch_dims; - // batch_dims should be in range: [-rank(indices), rank(indices)]. - // Negative batch_dims is added with rank of positions. - if (batch_dims < 0) - { - batch_dims += indices_shape.num_dims(); - } - LUCI_INTERPRETER_CHECK(batch_dims <= axis); - LUCI_INTERPRETER_CHECK(0 <= batch_dims && batch_dims < params_shape.num_dims()); - LUCI_INTERPRETER_CHECK(batch_dims <= indices_shape.num_dims()); - for (int i = 0; i < batch_dims; ++i) - { - LUCI_INTERPRETER_CHECK(params_shape.dim(i) == indices_shape.dim(i)); - } - - const int num_dimensions = params_shape.num_dims() + indices_shape.num_dims() - 1 - batch_dims; - - Shape output_shape(num_dimensions); - int output_index = 0; - for (int i = 0; i < axis; ++i) - { - output_shape.dim(output_index++) = params_shape.dim(i); - } - for (int i = batch_dims; i < indices_shape.num_dims(); ++i) - { - output_shape.dim(output_index++) = indices_shape.dim(i); - } - for (int i = axis + 1; i < params_shape.num_dims(); ++i) - { - output_shape.dim(output_index++) = params_shape.dim(i); - } - output()->resize(output_shape); -} - -void Gather::execute() const -{ - switch (params()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Gather::evalFloat() const -{ - assert(indices()->element_type() == DataType::S32 || indices()->element_type() == DataType::S64); - - const auto params_data = getTensorData(params()); - auto output_data = getTensorData(output()); - - tflite::GatherParams tparams; - tparams.axis = _params.axis; - tparams.batch_dims = _params.batch_dims; - - if (indices()->element_type() == DataType::S32) - { - const auto indices_data = getTensorData(indices()); - - luci_interpreter_pal::Gather(tparams, getTensorShape(params()), params_data, - getTensorShape(indices()), indices_data, - getTensorShape(output()), output_data); - } - else - { - const auto indices_data = getTensorData(indices()); - - luci_interpreter_pal::Gather(tparams, getTensorShape(params()), params_data, - getTensorShape(indices()), indices_data, - getTensorShape(output()), output_data); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Greater.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Greater.cpp deleted file mode 100644 index 5ccae3c..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Greater.cpp +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Greater.h" -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Greater::Greater(const Tensor *x, const Tensor *y, Tensor *output) : Kernel({x, y}, {output}) {} - -void Greater::configure() -{ - LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); - - if (x()->element_type() == DataType::U8) - { - quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); - quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); - } - output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); -} - -void Greater::execute() const -{ - switch (x()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::S64: - evalInteger(); - break; - case DataType::S32: - evalInteger(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Greater::evalFloat() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowGreater(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::Greater(op_params, getTensorShape(x()), x_data, getTensorShape(y()), - y_data, getTensorShape(output()), output_data); - } -} - -template void Greater::evalInteger() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowGreaterNoScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::GreaterNoScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } -} - -void Greater::evalQuantized() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.left_shift = 8; - op_params.input1_offset = -x()->zero_point(); // Note the '-' - op_params.input1_shift = _x_shift; - op_params.input1_multiplier = _x_multiplier; - op_params.input2_offset = -y()->zero_point(); // Note the '-' - op_params.input2_shift = _y_shift; - op_params.input2_multiplier = _y_multiplier; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowGreaterWithScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::GreaterWithScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/GreaterEqual.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/GreaterEqual.cpp deleted file mode 100644 index 27e42c9..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/GreaterEqual.cpp +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/GreaterEqual.h" -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -GreaterEqual::GreaterEqual(const Tensor *x, const Tensor *y, Tensor *output) - : Kernel({x, y}, {output}) -{ -} - -void GreaterEqual::configure() -{ - LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); - - if (x()->element_type() == DataType::U8) - { - quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); - quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); - } - output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); -} - -void GreaterEqual::execute() const -{ - switch (x()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::S64: - evalInteger(); - break; - case DataType::S32: - evalInteger(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void GreaterEqual::evalFloat() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowGreaterEqual(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::GreaterEqual(op_params, getTensorShape(x()), x_data, getTensorShape(y()), - y_data, getTensorShape(output()), output_data); - } -} - -template void GreaterEqual::evalInteger() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowGreaterEqualNoScaling( - op_params, getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } - else - { - tflite::reference_ops::GreaterEqualNoScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } -} - -void GreaterEqual::evalQuantized() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.left_shift = 8; - op_params.input1_offset = -x()->zero_point(); // Note the '-' - op_params.input1_shift = _x_shift; - op_params.input1_multiplier = _x_multiplier; - op_params.input2_offset = -y()->zero_point(); // Note the '-' - op_params.input2_shift = _y_shift; - op_params.input2_multiplier = _y_multiplier; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowGreaterEqualWithScaling( - op_params, getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } - else - { - tflite::reference_ops::GreaterEqualWithScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/InstanceNorm.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/InstanceNorm.cpp deleted file mode 100644 index 22a329b..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/InstanceNorm.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/InstanceNorm.h" - -#include "kernels/Utils.h" - -#include -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -InstanceNorm::InstanceNorm(const Tensor *input, const Tensor *gamma, const Tensor *beta, - Tensor *output, const InstanceNormParams ¶ms) - : KernelWithParams({input, gamma, beta}, {output}, params) -{ -} - -void InstanceNorm::configure() -{ - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - LUCI_INTERPRETER_CHECK(gamma()->element_type() == input()->element_type()); - LUCI_INTERPRETER_CHECK(gamma()->shape().num_dims() == 1); - LUCI_INTERPRETER_CHECK(gamma()->shape().dim(0) == input()->shape().dim(3) || - gamma()->shape().dim(0) == 1); - LUCI_INTERPRETER_CHECK(beta()->element_type() == input()->element_type()); - LUCI_INTERPRETER_CHECK(beta()->shape().num_dims() == 1); - LUCI_INTERPRETER_CHECK(beta()->shape().dim(0) == input()->shape().dim(3) || - beta()->shape().dim(0) == 1); - output()->resize(input()->shape()); -} - -void InstanceNorm::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void InstanceNorm::evalFloat() const -{ - float activation_min, activation_max; - calculateActivationRange(params().activation, &activation_min, &activation_max); - auto input_shape = getTensorShape(input()); - auto output_shape = getTensorShape(output()); - const int32_t batches = tflite::MatchingDim(input_shape, 0, output_shape, 0); - const int32_t heights = tflite::MatchingDim(input_shape, 1, output_shape, 1); - const int32_t widths = tflite::MatchingDim(input_shape, 2, output_shape, 2); - const int32_t channels = tflite::MatchingDim(input_shape, 3, output_shape, 3); - const float *input_data = getTensorData(input()); - const float *gamma_data = getTensorData(gamma()); - auto gamma_shape = getTensorShape(gamma()); - bool single_gamma = gamma_shape.DimensionsCount() == 1 && gamma_shape.Dims(0) == 1; - const float *beta_data = getTensorData(beta()); - auto beta_shape = getTensorShape(beta()); - bool single_beta = beta_shape.DimensionsCount() == 1 && beta_shape.Dims(0) == 1; - float *output_data = getTensorData(output()); - for (int32_t batch = 0; batch < batches; batch++) - { - for (int32_t channel = 0; channel < channels; channel++) - { - double sum = 0.0f; - double square_sum = 0.0f; - int32_t size = heights * widths; - for (int32_t height = 0; height < heights; height++) - { - for (int32_t width = 0; width < widths; width++) - { - double input_val = input_data[tflite::Offset(input_shape, batch, height, width, channel)]; - sum += input_val; - square_sum += (input_val * input_val); - } - } - double mean = sum / size; - double var = square_sum / size - mean * mean; - - double gamma = single_gamma ? gamma_data[0] : gamma_data[channel]; - double beta = single_beta ? beta_data[0] : beta_data[channel]; - double a = gamma / (std::sqrt(var + params().epsilon)); - double b = -mean * a + beta; - - for (int32_t height = 0; height < heights; height++) - { - for (int32_t width = 0; width < widths; width++) - { - double input_value = - input_data[tflite::Offset(output_shape, batch, height, width, channel)]; - double output_value = input_value * a + b; - output_data[tflite::Offset(output_shape, batch, height, width, channel)] = - tflite::ActivationFunctionWithMinMax((float)output_value, activation_min, - activation_max); - } - } - } - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/L2Normalize.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/L2Normalize.cpp deleted file mode 100644 index 6422295..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/L2Normalize.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/L2Normalize.h" -#include "kernels/Utils.h" - -#include "PALL2Normalize.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -L2Normalize::L2Normalize(const Tensor *input, Tensor *output, const L2NormParams ¶ms) - : KernelWithParams({input}, {output}, params) -{ -} - -void L2Normalize::configure() -{ - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() <= 4); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::FLOAT32 || - output()->element_type() == DataType::U8); - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - if (output()->element_type() == DataType::U8) - { - LUCI_INTERPRETER_CHECK(output()->scale() == (1. / 128.)); - LUCI_INTERPRETER_CHECK(output()->zero_point() == 128); - } - LUCI_INTERPRETER_CHECK(params().activation == Activation::NONE); - output()->resize(input()->shape()); -} - -void L2Normalize::execute() const -{ - switch (output()->element_type()) - { - case DataType::FLOAT32: - eval(0); - break; - case DataType::U8: - eval(input()->zero_point()); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -template void L2Normalize::eval(int32_t zero_point) const -{ - tflite::L2NormalizationParams op_params{}; - op_params.input_zero_point = zero_point; - luci_interpreter_pal::L2Normalization(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/L2Pool2D.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/L2Pool2D.cpp deleted file mode 100644 index 5a88808..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/L2Pool2D.cpp +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2017 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/L2Pool2D.h" - -#include "kernels/Utils.h" - -#include "PALL2Pool2D.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -L2Pool2D::L2Pool2D(const Tensor *input, Tensor *output, const Pool2DParams ¶ms) - : KernelWithParams({input}, {output}, params) -{ -} - -void L2Pool2D::configure() -{ - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - - int batches = input()->shape().dim(0); - int height = input()->shape().dim(1); - int width = input()->shape().dim(2); - int channels_out = input()->shape().dim(3); - - // Matching GetWindowedOutputSize in TensorFlow. - auto padding = params().padding; - int out_width, out_height; - out_width = computeOutputSize(padding, width, params().filter_width, params().stride_width, 1); - out_height = - computeOutputSize(padding, height, params().filter_height, params().stride_height, 1); - _padding_width = - computePadding(params().stride_width, 1, width, params().filter_width, out_width); - _padding_height = - computePadding(params().stride_height, 1, height, params().filter_height, out_height); - - LUCI_INTERPRETER_CHECK(input()->element_type() == DataType::FLOAT32); - output()->resize({batches, out_height, out_width, channels_out}); -} - -void L2Pool2D::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - float activation_min, activation_max; - calculateActivationRange(params().activation, &activation_min, &activation_max); - tflite::PoolParams op_params; - op_params.stride_height = params().stride_height; - op_params.stride_width = params().stride_width; - op_params.filter_height = params().filter_height; - op_params.filter_width = params().filter_width; - op_params.padding_values.height = _padding_height; - op_params.padding_values.width = _padding_width; - op_params.float_activation_min = activation_min; - op_params.float_activation_max = activation_max; - luci_interpreter_pal::L2Pool(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LeakyRelu.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/LeakyRelu.cpp deleted file mode 100644 index 3833a55..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/LeakyRelu.cpp +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/LeakyRelu.h" - -#include "kernels/Utils.h" - -#include - -#include "PALLeakyRelu.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -LeakyRelu::LeakyRelu(const Tensor *input, Tensor *output, const LeakyReluParams ¶ms) - : KernelWithParams({input}, {output}, params) -{ -} - -void LeakyRelu::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - if (input()->element_type() == DataType::U8) - { - double alpha_multiplier = input()->scale() * params().alpha / output()->scale(); - quantizeMultiplier(alpha_multiplier, &_output_multiplier_alpha, &_output_shift_alpha); - double identity_multiplier = input()->scale() / output()->scale(); - quantizeMultiplier(identity_multiplier, &_output_multiplier_identity, &_output_shift_identity); - } - output()->resize(input()->shape()); -} - -void LeakyRelu::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void LeakyRelu::evalFloat() const -{ - tflite::LeakyReluParams op_params{}; - op_params.alpha = params().alpha; - luci_interpreter_pal::LeakyRelu(op_params, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -void LeakyRelu::evalQuantized() const -{ - tflite::LeakyReluParams op_params{}; - op_params.input_offset = input()->zero_point(); - op_params.output_offset = output()->zero_point(); - op_params.output_multiplier_alpha = _output_multiplier_alpha; - op_params.output_shift_alpha = _output_shift_alpha; - op_params.output_multiplier_identity = _output_multiplier_identity; - op_params.output_shift_identity = _output_shift_identity; - - tflite::reference_ops::QuantizeLeakyRelu( - op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(output()), - getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Less.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Less.cpp deleted file mode 100644 index 8d26ff2..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Less.cpp +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Less.h" -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Less::Less(const Tensor *x, const Tensor *y, Tensor *output) : Kernel({x, y}, {output}) {} - -void Less::configure() -{ - LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); - - if (x()->element_type() == DataType::U8) - { - quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); - quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); - } - output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); -} - -void Less::execute() const -{ - switch (x()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::S64: - evalInteger(); - break; - case DataType::S32: - evalInteger(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Less::evalFloat() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowLess(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::Less(op_params, getTensorShape(x()), x_data, getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } -} - -template void Less::evalInteger() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowLessNoScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::LessNoScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } -} - -void Less::evalQuantized() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.left_shift = 8; - op_params.input1_offset = -x()->zero_point(); // Note the '-' - op_params.input1_shift = _x_shift; - op_params.input1_multiplier = _x_multiplier; - op_params.input2_offset = -y()->zero_point(); // Note the '-' - op_params.input2_shift = _y_shift; - op_params.input2_multiplier = _y_multiplier; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowLessWithScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::LessWithScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LessEqual.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/LessEqual.cpp deleted file mode 100644 index b474bc4..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/LessEqual.cpp +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/LessEqual.h" -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -LessEqual::LessEqual(const Tensor *x, const Tensor *y, Tensor *output) : Kernel({x, y}, {output}) {} - -void LessEqual::configure() -{ - LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); - - if (x()->element_type() == DataType::U8) - { - quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); - quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); - } - output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); -} - -void LessEqual::execute() const -{ - switch (x()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::S64: - evalInteger(); - break; - case DataType::S32: - evalInteger(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void LessEqual::evalFloat() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowLessEqual(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::LessEqual(op_params, getTensorShape(x()), x_data, getTensorShape(y()), - y_data, getTensorShape(output()), output_data); - } -} - -template void LessEqual::evalInteger() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowLessEqualNoScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::LessEqualNoScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } -} - -void LessEqual::evalQuantized() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.left_shift = 8; - op_params.input1_offset = -x()->zero_point(); // Note the '-' - op_params.input1_shift = _x_shift; - op_params.input1_multiplier = _x_multiplier; - op_params.input2_offset = -y()->zero_point(); // Note the '-' - op_params.input2_shift = _y_shift; - op_params.input2_multiplier = _y_multiplier; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowLessEqualWithScaling( - op_params, getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } - else - { - tflite::reference_ops::LessEqualWithScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LocalResponseNormalization.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/LocalResponseNormalization.cpp deleted file mode 100644 index a2bf442..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/LocalResponseNormalization.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/LocalResponseNormalization.h" - -#include "kernels/Utils.h" - -#include "PALLocalResponseNormalization.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -LocalResponseNormalization::LocalResponseNormalization( - const Tensor *input, Tensor *output, const LocalResponseNormalizationParams ¶ms) - : KernelWithParams({input}, {output}, params) -{ -} - -void LocalResponseNormalization::configure() -{ - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::FLOAT32); - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - output()->resize(input()->shape()); -} - -void LocalResponseNormalization::execute() const -{ - switch (output()->element_type()) - { - case DataType::FLOAT32: - tflite::LocalResponseNormalizationParams op_params; - op_params.range = params().radius; - op_params.bias = params().bias; - op_params.alpha = params().alpha; - op_params.beta = params().beta; - luci_interpreter_pal::LocalResponseNormalization( - op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogSoftmax.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/LogSoftmax.cpp deleted file mode 100644 index 79c3153..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/LogSoftmax.cpp +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/LogSoftmax.h" - -#include "kernels/Utils.h" - -#include - -#include "PALLogSoftmax.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -LogSoftmax::LogSoftmax(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void LogSoftmax::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - if (input()->element_type() == DataType::U8) - { - LUCI_INTERPRETER_CHECK(output()->scale() == 16. / 256); - LUCI_INTERPRETER_CHECK(output()->zero_point() == 255); - - tflite::SoftmaxParams params{}; - - params.table = _table; - params.beta = 1.0; - luci_interpreter_pal::PopulateSoftmaxLookupTable(¶ms, input()->scale(), params.beta); - } - output()->resize(input()->shape()); -} - -void LogSoftmax::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void LogSoftmax::evalFloat() const -{ - tflite::SoftmaxParams params{}; - tflite::reference_ops::LogSoftmax(params, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -void LogSoftmax::evalQuantized() const -{ - const auto input_shape = getTensorShape(input()); - const auto output_shape = getTensorShape(output()); - const auto input_scale = input()->scale(); - uint8_t *output_data = getTensorData(output()); - const uint8_t *input_data = getTensorData(input()); - const float beta = 1.0; - - tflite::SoftmaxParams params{}; - - params.table = const_cast(_table); - params.zero_point = output()->zero_point(); - params.scale = output()->scale(); - - luci_interpreter_pal::InitializeParams(¶ms, input_scale, beta); - luci_interpreter_pal::LogSoftmax(params, input_scale, input_shape, input_data, output_shape, - output_data); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalAnd.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/LogicalAnd.cpp deleted file mode 100644 index 8e72632..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalAnd.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2018 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/LogicalAnd.h" - -#include "kernels/Utils.h" - -#include "kernels/BinaryOpCommon.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -LogicalAnd::LogicalAnd(const Tensor *input1, const Tensor *input2, Tensor *output) - : Kernel({input1, input2}, {output}) -{ -} - -void LogicalAnd::configure() -{ - LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); - LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()); - output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); -} - -void LogicalAnd::execute() const -{ - switch (input1()->element_type()) - { - case DataType::BOOL: - evalLogicalAnd(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -inline void LogicalAnd::evalLogicalAnd() const -{ - BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output()), - [](bool x, bool y) { return x && y; }); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalNot.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/LogicalNot.cpp deleted file mode 100644 index 65ab961..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalNot.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/LogicalNot.h" - -#include "kernels/Utils.h" - -#include "kernels/BinaryOpCommon.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -LogicalNot::LogicalNot(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void LogicalNot::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - output()->resize(input()->shape()); -} - -void LogicalNot::execute() const -{ - switch (input()->element_type()) - { - case DataType::BOOL: - evalLogicalNot(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -inline void LogicalNot::evalLogicalNot() const -{ - const int size = tflite::MatchingFlatSize(getTensorShape(input()), getTensorShape(output())); - bool *output_data = getTensorData(output()); - const bool *input_data = getTensorData(input()); - for (int i = 0; i < size; ++i) - { - output_data[i] = !input_data[i]; - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalOr.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/LogicalOr.cpp deleted file mode 100644 index f289ca6..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalOr.cpp +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/LogicalOr.h" - -#include "kernels/Utils.h" -#include "kernels/BinaryOpCommon.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -LogicalOr::LogicalOr(const Tensor *input1, const Tensor *input2, Tensor *output) - : Kernel({input1, input2}, {output}) -{ -} - -void LogicalOr::configure() -{ - LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); - LUCI_INTERPRETER_CHECK(input1()->element_type() == DataType::BOOL); - output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); -} - -void LogicalOr::execute() const -{ - BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output()), - [](bool x, bool y) { return x || y; }); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Logistic.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Logistic.cpp deleted file mode 100644 index 58e4f18..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Logistic.cpp +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Logistic.h" - -#include "kernels/Utils.h" - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Logistic::Logistic(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Logistic::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - if (input()->element_type() == DataType::U8) - { - LUCI_INTERPRETER_CHECK(output()->scale() == 1. / 256); - populateLookupTable(); - } - output()->resize(input()->shape()); -} - -void Logistic::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Logistic::evalFloat() const -{ - tflite::reference_ops::Logistic(getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -void Logistic::evalQuantized() const -{ - const int size = tflite::MatchingFlatSize(getTensorShape(input()), getTensorShape(output())); - uint8_t *output_data = getTensorData(output()); - const uint8_t *input_data = getTensorData(input()); - for (int i = 0; i < size; ++i) - { - output_data[i] = getTableValue(input_data[i]); - } -} - -void Logistic::populateLookupTable() -{ - const auto input_scale = static_cast(input()->scale()); - const auto input_zero_point = static_cast(input()->zero_point()); - const auto output_scale = static_cast(output()->scale()); - const auto output_zero_point = static_cast(output()->zero_point()); - const float inverse_scale = 1 / output_scale; - int32_t maxval = std::numeric_limits::max(); - int32_t minval = std::numeric_limits::min(); - for (int32_t val = minval; val <= maxval; ++val) - { - const float dequantized = input_scale * (val - input_zero_point); - const float transformed = 1.0f / (1.0f + std::exp(-dequantized)); - const float rescaled = std::round(transformed * inverse_scale); - const int32_t quantized = static_cast(rescaled + output_zero_point); - setTableValue(static_cast(std::max(std::min(maxval, quantized), minval)), - static_cast(val)); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Logistic.h b/compiler/luci-micro/luci-interpreter/src/kernels/Logistic.h deleted file mode 100644 index 31de6ad..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Logistic.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_KERNELS_LOGISTIC_H -#define LUCI_INTERPRETER_KERNELS_LOGISTIC_H - -#include "core/Kernel.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -class Logistic : public Kernel -{ -public: - Logistic(const Tensor *input, Tensor *output); - - const Tensor *input() const { return _inputs[0]; } - Tensor *output() const { return _outputs[0]; } - - void configure() override; - void execute() const override; - -private: - void evalFloat() const; - void evalQuantized() const; - void populateLookupTable(); - void setTableValue(uint8_t value, uint8_t idx) { _table[idx] = value; }; - uint8_t getTableValue(uint8_t idx) const { return _table[idx]; }; - -private: - uint8_t _table[256]{}; -}; - -} // namespace kernels -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_KERNELS_LOGISTIC_H diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Logistic.test.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Logistic.test.cpp deleted file mode 100644 index 5a1ea66..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Logistic.test.cpp +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Logistic.h" -#include "kernels/TestUtils.h" -#include "luci_interpreter/TestMemoryManager.h" - -namespace luci_interpreter -{ -namespace kernels -{ -namespace -{ - -using namespace testing; - -template -void Check(std::initializer_list input_shape, std::initializer_list output_shape, - std::initializer_list input_data, std::initializer_list output_data) -{ - std::unique_ptr memory_manager = std::make_unique(); - - Tensor input_tensor = - makeInputTensor()>(input_shape, input_data, memory_manager.get()); - Tensor output_tensor = makeOutputTensor(getElementType()); - - Logistic kernel(&input_tensor, &output_tensor); - kernel.configure(); - memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(output_data)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); -} - -template <> -void Check(std::initializer_list input_shape, - std::initializer_list output_shape, - std::initializer_list input_data, - std::initializer_list output_data) -{ - std::unique_ptr memory_manager = std::make_unique(); - - std::pair input_quant_param = - quantizationParams(std::min(input_data), std::max(input_data)); - Tensor input_tensor = - makeInputTensor(input_shape, input_quant_param.first, input_quant_param.second, - input_data, memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::U8, 1. / 256, 0); - - Logistic kernel(&input_tensor, &output_tensor); - kernel.configure(); - memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(dequantizeTensorData(output_tensor), - FloatArrayNear(output_data, output_tensor.scale() * 2)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); -} - -template class LogisticTest : public ::testing::Test -{ -}; - -using DataTypes = ::testing::Types; -TYPED_TEST_SUITE(LogisticTest, DataTypes); - -TYPED_TEST(LogisticTest, Simple) -{ - Check( - {89}, {89}, - {-10.0000000000, -9.7727272727, -9.5454545455, -9.3181818182, -9.0909090909, -8.8636363636, - -8.6363636364, -8.4090909091, -8.1818181818, -7.9545454545, -7.7272727273, -7.5000000000, - -7.2727272727, -7.0454545455, -6.8181818182, -6.5909090909, -6.3636363636, -6.1363636364, - -5.9090909091, -5.6818181818, -5.4545454545, -5.2272727273, -5.0000000000, -4.7727272727, - -4.5454545455, -4.3181818182, -4.0909090909, -3.8636363636, -3.6363636364, -3.4090909091, - -3.1818181818, -2.9545454545, -2.7272727273, -2.5000000000, -2.2727272727, -2.0454545455, - -1.8181818182, -1.5909090909, -1.3636363636, -1.1363636364, -0.9090909091, -0.6818181818, - -0.4545454545, -0.2272727273, 0.0000000000, 0.2272727273, 0.4545454545, 0.6818181818, - 0.9090909091, 1.1363636364, 1.3636363636, 1.5909090909, 1.8181818182, 2.0454545455, - 2.2727272727, 2.5000000000, 2.7272727273, 2.9545454545, 3.1818181818, 3.4090909091, - 3.6363636364, 3.8636363636, 4.0909090909, 4.3181818182, 4.5454545455, 4.7727272727, - 5.0000000000, 5.2272727273, 5.4545454545, 5.6818181818, 5.9090909091, 6.1363636364, - 6.3636363636, 6.5909090909, 6.8181818182, 7.0454545455, 7.2727272727, 7.5000000000, - 7.7272727273, 7.9545454545, 8.1818181818, 8.4090909091, 8.6363636364, 8.8636363636, - 9.0909090909, 9.3181818182, 9.5454545455, 9.7727272727, 10.0000000000}, - {0.0000453979, 0.0000569815, 0.0000715205, 0.0000897689, 0.0001126729, 0.0001414198, - 0.0001774998, 0.0002227827, 0.0002796147, 0.0003509396, 0.0004404502, 0.0005527786, - 0.0006937345, 0.0008706021, 0.0010925128, 0.0013709094, 0.0017201256, 0.0021581065, - 0.0027073042, 0.0033957870, 0.0042586071, 0.0053394826, 0.0066928509, 0.0083863576, - 0.0105038445, 0.0131488902, 0.0164489307, 0.0205599431, 0.0256715863, 0.0320125562, - 0.0398556989, 0.0495221198, 0.0613831074, 0.0758581800, 0.0934070047, 0.1145124805, - 0.1396521834, 0.1692560327, 0.2036499335, 0.2429886272, 0.2871859014, 0.3358556241, - 0.3882805886, 0.4434251301, 0.5000000000, 0.5565748699, 0.6117194114, 0.6641443759, - 0.7128140986, 0.7570113728, 0.7963500665, 0.8307439673, 0.8603478166, 0.8854875195, - 0.9065929953, 0.9241418200, 0.9386168926, 0.9504778802, 0.9601443011, 0.9679874438, - 0.9743284137, 0.9794400569, 0.9835510693, 0.9868511098, 0.9894961555, 0.9916136424, - 0.9933071491, 0.9946605174, 0.9957413929, 0.9966042130, 0.9972926958, 0.9978418935, - 0.9982798744, 0.9986290906, 0.9989074872, 0.9991293979, 0.9993062655, 0.9994472214, - 0.9995595498, 0.9996490604, 0.9997203853, 0.9997772173, 0.9998225002, 0.9998585802, - 0.9998873271, 0.9999102311, 0.9999284795, 0.9999430185, 0.9999546021}); -} - -TEST(LogisticTest, IvalidInputOutputType_NEG) -{ - std::unique_ptr memory_manager = std::make_unique(); - - Shape input_shape = {1}; - std::vector input_data{10}; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::U8, 1. / 256, 0); - - Logistic kernel(&input_tensor, &output_tensor); - EXPECT_ANY_THROW(kernel.configure()); -} - -TEST(LogisticTest, IvalidQuantParam_NEG) -{ - std::unique_ptr memory_manager = std::make_unique(); - Shape input_shape = {2}; - std::vector input_data{-10, 10}; - std::pair input_quant_param = quantizationParams(-10, 10); - Tensor input_tensor = - makeInputTensor(input_shape, input_quant_param.first, input_quant_param.second, - input_data, memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::U8, 1. / 255, 0); - - Logistic kernel(&input_tensor, &output_tensor); - EXPECT_ANY_THROW(kernel.configure()); -} - -} // namespace -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.cpp deleted file mode 100644 index 8d9760f..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.cpp +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/MaxPool2D.h" - -#include "kernels/Utils.h" - -#include -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -MaxPool2D::MaxPool2D(const Tensor *input, Tensor *output, const Pool2DParams ¶ms) - : KernelWithParams({input}, {output}, params) -{ -} - -void MaxPool2D::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - assert(input()->shape().num_dims() == 4); - const Shape &input_shape = input()->shape(); - const int32_t batches = input_shape.dim(0); - const int32_t input_height = input_shape.dim(1); - const int32_t input_width = input_shape.dim(2); - const int32_t depth = input_shape.dim(3); - - const int32_t output_height = - computeOutputSize(_params.padding, input_height, _params.filter_height, _params.stride_height); - const int32_t output_width = - computeOutputSize(_params.padding, input_width, _params.filter_width, _params.stride_width); - - _padding_height = - computePadding(_params.stride_height, 1, input_height, _params.filter_height, output_height); - _padding_width = - computePadding(_params.stride_width, 1, input_width, _params.filter_width, output_width); - - output()->resize({batches, output_height, output_width, depth}); - if (input()->element_type() == DataType::U8) - { - LUCI_INTERPRETER_CHECK(std::abs(output()->scale() - input()->scale()) <= 1.0e-6); - LUCI_INTERPRETER_CHECK(output()->zero_point() == input()->zero_point()); - } - else if (input()->element_type() == DataType::S16) - { - LUCI_INTERPRETER_CHECK(std::abs(output()->scale() - input()->scale()) <= 1.0e-6); - LUCI_INTERPRETER_CHECK(input()->zero_point() == 0 && output()->zero_point() == 0); - } -} - -void MaxPool2D::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::U8: - evalQuantized(); - break; - case DataType::S16: - evalSInt16(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void MaxPool2D::evalFloat() const -{ - float activation_min{}; - float activation_max{}; - calculateActivationRange(_params.activation, &activation_min, &activation_max); - - tflite::PoolParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.filter_height = _params.filter_height; - params.filter_width = _params.filter_width; - params.float_activation_min = activation_min; - params.float_activation_max = activation_max; - - tflite::reference_ops::MaxPool(params, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -void MaxPool2D::evalQuantized() const -{ - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - tflite::PoolParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.filter_height = _params.filter_height; - params.filter_width = _params.filter_width; - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - tflite::reference_ops::MaxPool(params, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -void MaxPool2D::evalSInt16() const -{ - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - tflite::PoolParams params{}; - params.padding_values.height = _padding_height; - params.padding_values.width = _padding_width; - params.stride_height = _params.stride_height; - params.stride_width = _params.stride_width; - params.filter_height = _params.filter_height; - params.filter_width = _params.filter_width; - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - tflite::reference_integer_ops::MaxPool( - params, getTensorShape(input()), getTensorData(input()), // - getTensorShape(output()), getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.h b/compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.h deleted file mode 100644 index bb76663..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_KERNELS_MAXPOOL2D_H -#define LUCI_INTERPRETER_KERNELS_MAXPOOL2D_H - -#include "core/Kernel.h" -#include "core/KernelParams.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -class MaxPool2D : public KernelWithParams -{ -public: - MaxPool2D(const Tensor *input, Tensor *output, const Pool2DParams ¶ms); - - const Tensor *input() const { return _inputs[0]; } - Tensor *output() const { return _outputs[0]; } - - void configure() override; - void execute() const override; - -private: - void evalFloat() const; - void evalQuantized() const; - void evalSInt16() const; - -private: - int32_t _padding_height{}; - int32_t _padding_width{}; -}; - -} // namespace kernels -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_KERNELS_MAXPOOL2D_H diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.test.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.test.cpp deleted file mode 100644 index 44f2a22..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/MaxPool2D.test.cpp +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/MaxPool2D.h" -#include "kernels/TestUtils.h" -#include "luci_interpreter/TestMemoryManager.h" - -namespace luci_interpreter -{ -namespace kernels -{ -namespace -{ - -using namespace testing; - -class MaxPool2DTest : public ::testing::Test -{ -protected: - void SetUp() override { _memory_manager = std::make_unique(); } - - std::unique_ptr _memory_manager; -}; - -TEST_F(MaxPool2DTest, Float) -{ - Shape input_shape{1, 3, 5, 1}; - std::vector input_data{ - 1, -1, 0, -2, 2, // - -7, -6, -5, -4, -3, // - 5, 4, 3, 6, 7, // - }; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - Pool2DParams params{}; - params.padding = Padding::VALID; - params.filter_height = 2; - params.filter_width = 3; - params.stride_height = 1; - params.stride_width = 2; - params.activation = Activation::RELU6; - - MaxPool2D kernel(&input_tensor, &output_tensor, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - std::vector ref_output_data{ - 1, 2, // - 5, 6, // - }; - std::initializer_list ref_output_shape{1, 2, 2, 1}; - EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); -} - -TEST_F(MaxPool2DTest, Uint8) -{ - std::pair quant_param = quantizationParams(-15.9375, 15.9375); - std::vector input_data{ - 0, -6, 12, 4, // - -3, -2, 10, 7, // - }; - Tensor input_tensor = makeInputTensor( - {1, 2, 4, 1}, quant_param.first, quant_param.second, input_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::U8, quant_param.first, quant_param.second); - - Pool2DParams params{}; - params.padding = Padding::VALID; - params.filter_height = 2; - params.filter_width = 2; - params.stride_height = 2; - params.stride_width = 2; - params.activation = Activation::RELU6; - - MaxPool2D kernel(&input_tensor, &output_tensor, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - std::vector ref_output_data{0.0, 6.0}; - std::initializer_list ref_output_shape{1, 1, 2, 1}; - EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); -} - -TEST_F(MaxPool2DTest, SInt16) -{ - Shape input_shape{1, 3, 5, 1}; - std::vector ref_output_shape{1, 2, 2, 1}; - std::vector input_data{ - 1, -1, 0, -2, 2, // - -7, -6, -5, -4, -3, // - 5, 4, 3, 6, 7, // - }; - std::vector ref_output_data{ - 1, 2, // - 5, 6, // - }; - - Tensor input_tensor = - makeInputTensor(input_shape, 0.2, 0, input_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::S16, 0.2, 0); - - Pool2DParams params{}; - params.padding = Padding::VALID; - params.filter_height = 2; - params.filter_width = 3; - params.stride_height = 1; - params.stride_width = 2; - params.activation = Activation::RELU6; - - MaxPool2D kernel(&input_tensor, &output_tensor, params); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); - EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); -} - -} // namespace -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Maximum.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Maximum.cpp deleted file mode 100644 index b102b5e..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Maximum.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2018 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Maximum.h" - -#include "kernels/Utils.h" - -#include "kernels/BinaryOpCommon.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -Maximum::Maximum(const Tensor *input1, const Tensor *input2, Tensor *output) - : Kernel({input1, input2}, {output}) -{ -} - -void Maximum::configure() -{ - LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()) - LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()) - output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); -} - -void Maximum::execute() const -{ - switch (input1()->element_type()) - { - case DataType::FLOAT32: - evalMaximum(); - break; - case DataType::U8: - evalMaximum(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -template inline void Maximum::evalMaximum() const -{ - BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output()), - [](T x, T y) { return std::max(x, y); }); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Mean.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Mean.cpp deleted file mode 100644 index 8e65e0d..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Mean.cpp +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Mean.h" - -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -static void resolveAxes(const int32_t *axes_data, int num_axes, tflite::MeanParams *params) -{ - params->axis_count = num_axes; - for (int i = 0; i < num_axes; ++i) - { - params->axis[i] = static_cast(axes_data[i]); - } - for (int i = num_axes; i < 4; ++i) - { - params->axis[i] = 1; - } -} - -// Returns the number of axes that will be reduced. Removes duplicates. -static int getAxisReductionCount(const int32_t *axes_data, int num_axes, int input_num_dims) -{ - int reduction_count = num_axes; - for (int i = 0; i < num_axes; ++i) - { - int current = axes_data[i] >= 0 ? axes_data[i] : axes_data[i] + input_num_dims; - assert(current >= 0 && current < input_num_dims); - for (int j = 0; j < i; j++) - { - int previous = axes_data[j] >= 0 ? axes_data[j] : axes_data[j] + input_num_dims; - // This checks for duplicate axis - if (current == previous) - { - --reduction_count; - break; - } - } - } - return reduction_count; -} - -static Shape getOutputShape(const Shape &input_shape, const int32_t *axes_data, int num_axes, - bool keep_dims) -{ - int input_num_dims = input_shape.num_dims(); - if (input_num_dims == 0) - { - return Shape(0); - } - - if (keep_dims) - { - Shape output_shape(input_num_dims); - for (int idx = 0; idx < input_num_dims; ++idx) - { - bool is_axis = false; - for (int axis_idx = 0; axis_idx < num_axes; ++axis_idx) - { - if (axes_data[axis_idx] == idx || axes_data[axis_idx] + input_num_dims == idx) - { - is_axis = true; - break; - } - } - if (is_axis) - { - output_shape.dim(idx) = 1; - } - else - { - output_shape.dim(idx) = input_shape.dim(idx); - } - } - return output_shape; - } - else - { - int num_reduce_axes = getAxisReductionCount(axes_data, num_axes, input_num_dims); - Shape output_shape(input_num_dims - num_reduce_axes); - int num_skip_axes = 0; - for (int idx = 0; idx < input_num_dims; ++idx) - { - bool is_axis = false; - for (int axis_idx = 0; axis_idx < num_axes; ++axis_idx) - { - if (axes_data[axis_idx] == idx || axes_data[axis_idx] + input_num_dims == idx) - { - ++num_skip_axes; - is_axis = true; - break; - } - } - if (!is_axis) - { - output_shape.dim(idx - num_skip_axes) = input_shape.dim(idx); - } - } - return output_shape; - } -} - -Mean::Mean(const Tensor *input, const Tensor *axes, Tensor *output, Tensor *temp_index, - Tensor *resolved_axes, Tensor *temp_sum, const ReducerParams ¶ms) - : KernelWithParams({input, axes}, {output, temp_index, resolved_axes, temp_sum}, - params) -{ -} - -void Mean::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - LUCI_INTERPRETER_CHECK(axes()->element_type() == DataType::S32); - if (input()->element_type() == DataType::S16) - { - LUCI_INTERPRETER_CHECK(input()->zero_point() == 0 && output()->zero_point() == 0); - } - - const Shape &input_shape = input()->shape(); - int input_num_dims = input_shape.num_dims(); - - const auto *axes_data = getTensorData(axes()); - int num_axes = axes()->shape().num_elements(); - assert(num_axes <= 4); - - Shape output_shape = getOutputShape(input_shape, axes_data, num_axes, _params.keep_dims); - output()->resize(output_shape); - - tflite::MeanParams params{}; - resolveAxes(axes_data, num_axes, ¶ms); - _need_temporaries = !( - _params.keep_dims && input_num_dims == 4 && params.axis_count == 2 && - ((params.axis[0] == 1 && params.axis[1] == 2) || (params.axis[0] == 2 && params.axis[1] == 1))); - if (_need_temporaries) - { - auto temp_index = getOutputTensors()[1]; - auto resolved_axes = getOutputTensors()[2]; - auto temp_sum = getOutputTensors()[3]; - - temp_index->resize(Shape(input_num_dims)); - resolved_axes->resize(Shape(num_axes)); - temp_sum->resize(output()->shape()); - } - else - { - auto temp_index = getOutputTensors()[1]; - auto resolved_axes = getOutputTensors()[2]; - auto temp_sum = getOutputTensors()[3]; - - temp_index->set_allocatable(false); - resolved_axes->set_allocatable(false); - temp_sum->set_allocatable(false); - } -} - -void Mean::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::U8: - evalQuantized(); - break; - case DataType::S16: - evalQuantizedS16(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Mean::evalFloat() const -{ - const Shape &input_shape = input()->shape(); - int input_num_dims = input_shape.num_dims(); - const auto *axes_data = getTensorData(axes()); - int num_axes = axes()->shape().num_elements(); - - tflite::MeanParams params{}; - resolveAxes(axes_data, num_axes, ¶ms); - - auto temp_index = getOutputTensors()[1]; - auto resolved_axes = getOutputTensors()[2]; - auto temp_sum = getOutputTensors()[3]; - - // Defer to specialized implementation for 4D Mean across axes 1 & 2. - if (_params.keep_dims && input_num_dims == 4 && params.axis_count == 2 && - ((params.axis[0] == 1 && params.axis[1] == 2) || - (params.axis[0] == 2 && params.axis[1] == 1))) - { - tflite::reference_ops::Mean(params, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); - } - else - { - tflite::reference_ops::Mean(getTensorData(input()), getTensorShape(input()).DimsData(), - input()->shape().num_dims(), getTensorData(output()), - getTensorShape(output()).DimsData(), output()->shape().num_dims(), - axes_data, num_axes, _params.keep_dims, - getTensorData(temp_index), getTensorData(resolved_axes), - getTensorData(temp_sum)); - } -} - -void Mean::evalQuantized() const -{ - const Shape &input_shape = input()->shape(); - int input_num_dims = input_shape.num_dims(); - const auto *axes_data = getTensorData(axes()); - int num_axes = axes()->shape().num_elements(); - - tflite::MeanParams params{}; - resolveAxes(axes_data, num_axes, ¶ms); - - auto temp_index = getOutputTensors()[1]; - auto resolved_axes = getOutputTensors()[2]; - auto temp_sum = getOutputTensors()[3]; - - // Defer to specialized implementation for 4D Mean across axes 1 & 2. - if (_params.keep_dims && input_num_dims == 4 && params.axis_count == 2 && - ((params.axis[0] == 1 && params.axis[1] == 2) || - (params.axis[0] == 2 && params.axis[1] == 1))) - { - tflite::reference_ops::Mean(params, getTensorShape(input()), getTensorData(input()), - input()->zero_point(), input()->scale(), getTensorShape(output()), - getTensorData(output()), output()->zero_point(), - output()->scale()); - } - else if (input()->zero_point() == output()->zero_point() && input()->scale() == output()->scale()) - { - tflite::reference_ops::Mean(getTensorData(input()), getTensorShape(input()).DimsData(), - input()->shape().num_dims(), getTensorData(output()), - getTensorShape(output()).DimsData(), output()->shape().num_dims(), - axes_data, num_axes, _params.keep_dims, - getTensorData(temp_index), getTensorData(resolved_axes), - getTensorData(temp_sum)); - } - else - { - tflite::reference_ops::QuantizedMeanOrSum<>( - getTensorData(input()), input()->zero_point(), input()->scale(), - getTensorShape(input()).DimsData(), input()->shape().num_dims(), - getTensorData(output()), output()->zero_point(), output()->scale(), - getTensorShape(output()).DimsData(), output()->shape().num_dims(), axes_data, num_axes, - _params.keep_dims, getTensorData(temp_index), getTensorData(resolved_axes), - getTensorData(temp_sum), - /*compute_sum=*/false); - } -} - -void Mean::evalQuantizedS16() const -{ - const auto *input_data = getTensorData(input()); - auto *output_data = getTensorData(output()); - - const Shape &input_shape = input()->shape(); - const Shape &output_shape = output()->shape(); - - const auto *axes_data = getTensorData(axes()); - const int num_axes = axes()->shape().num_elements(); - - constexpr int32_t output_min = -std::numeric_limits::max(); - constexpr int32_t output_max = std::numeric_limits::max(); - - // Defer to specialized implementation for 4D Mean across axes 1 & 2. - if (_params.keep_dims && input_shape.num_dims() == 4 && num_axes == 2 && - ((axes_data[0] == 1 && axes_data[1] == 2) || (axes_data[0] == 2 && axes_data[1] == 1))) - { - const int32_t batches = input_shape.dim(0); - const int32_t input_height = input_shape.dim(1); - const int32_t input_width = input_shape.dim(2); - const int32_t depth = input_shape.dim(3); - assert(output_shape.num_dims() == 4); - assert(output_shape.dim(0) == batches); - assert(output_shape.dim(1) == 1); - assert(output_shape.dim(2) == 1); - assert(output_shape.dim(3) == depth); - - const double real_multiplier = - static_cast(input()->scale()) / static_cast(output()->scale()); - - int32_t output_multiplier{}; - int output_shift{}; - quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); - - const int32_t num_elements_in_axes = input_height * input_width; - - for (int32_t batch = 0; batch < batches; ++batch) - { - for (int32_t c = 0; c < depth; ++c) - { - int32_t acc = 0; - for (int32_t in_y = 0; in_y < input_height; ++in_y) - { - for (int32_t in_x = 0; in_x < input_width; ++in_x) - { - acc += input_data[calcOffset(input_shape, batch, in_y, in_x, c)]; - } - } - int32_t scaled_acc = - tflite::MultiplyByQuantizedMultiplier(acc, output_multiplier, output_shift); - // Divide by the number of elements rounding to the nearest integer. - scaled_acc = scaled_acc > 0 - ? (scaled_acc + num_elements_in_axes / 2) / num_elements_in_axes - : (scaled_acc - num_elements_in_axes / 2) / num_elements_in_axes; - - scaled_acc = std::max(scaled_acc, output_min); - scaled_acc = std::min(scaled_acc, output_max); - - output_data[calcOffset(output_shape, batch, 0, 0, c)] = scaled_acc; - } - } - } - else - { - throw std::runtime_error("Unsupported configuration."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Minimum.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Minimum.cpp deleted file mode 100644 index 5d3dcde..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Minimum.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2018 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Minimum.h" - -#include "kernels/Utils.h" - -#include "kernels/BinaryOpCommon.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -Minimum::Minimum(const Tensor *input1, const Tensor *input2, Tensor *output) - : Kernel({input1, input2}, {output}) -{ -} - -void Minimum::configure() -{ - LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()) - LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()) - output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); -} - -void Minimum::execute() const -{ - switch (input1()->element_type()) - { - case DataType::FLOAT32: - evalMinimum(); - break; - case DataType::U8: - evalMinimum(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -template inline void Minimum::evalMinimum() const -{ - BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output()), - [](T x, T y) { return std::min(x, y); }); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/MirrorPad.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/MirrorPad.cpp deleted file mode 100644 index bae1eac..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/MirrorPad.cpp +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/MirrorPad.h" - -#include "kernels/Utils.h" - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -MirrorPad::MirrorPad(const Tensor *input, const Tensor *paddings, Tensor *output, - const MirrorPadParams ¶ms) - : KernelWithParams({input, paddings}, {output}, params) -{ -} - -void MirrorPad::configure() -{ - const Shape &input_shape = input()->shape(); - const int num_dims = input_shape.num_dims(); - - if (num_dims > 4) - throw std::runtime_error("Unsupported number of dimensions."); - - assert(output()->element_type() == input()->element_type()); - assert(paddings()->element_type() == DataType::S32); - // Paddings shape should be [N, 2]. - assert(paddings()->shape().num_dims() == 2); - assert(paddings()->shape().dim(0) == num_dims); - assert(paddings()->shape().dim(1) == 2); - - Shape output_shape(num_dims); - const auto *paddings_data = getTensorData(paddings()); - for (int i = 0; i < num_dims; ++i) - { - const int32_t padding_before = paddings_data[i * 2]; - const int32_t padding_after = paddings_data[i * 2 + 1]; - assert(padding_before >= 0 && padding_after >= 0); - output_shape.dim(i) = input_shape.dim(i) + padding_before + padding_after; - } - - output()->resize(output_shape); -} - -template -inline void MirrorPadImpl(const Tensor &input, const Tensor &paddings, MirrorPadMode mode, - Tensor &output); - -void MirrorPad::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - { - MirrorPadImpl(*input(), *paddings(), params().mode, *output()); - break; - } - case DataType::U8: - { - assert(output()->zero_point() >= std::numeric_limits::min()); - assert(output()->zero_point() <= std::numeric_limits::max()); - - MirrorPadImpl(*input(), *paddings(), params().mode, *output()); - break; - } - default: - throw std::runtime_error("Unsupported type."); - } -} - -template -inline void MirrorPadImpl(const Tensor &input, const Tensor &paddings, MirrorPadMode mode, - Tensor &output) -{ - auto const input_dims = input.shape().num_dims(); - auto const input_data = input.data(); - auto const paddings_data = paddings.data(); - auto const output_data = output.data(); - - auto const input_b = input_dims > 3 ? input.shape().dim(input_dims - 4) : 1; - auto const input_h = input_dims > 2 ? input.shape().dim(input_dims - 3) : 1; - auto const input_w = input_dims > 1 ? input.shape().dim(input_dims - 2) : 1; - auto const input_d = input.shape().dim(input_dims - 1); - - auto const input_h_offset = input_d * input_w; - auto const input_b_offset = input_h_offset * input_h; - - auto const output_b = input_dims > 3 ? output.shape().dim(input_dims - 4) : 1; - auto const output_h = input_dims > 2 ? output.shape().dim(input_dims - 3) : 1; - auto const output_w = input_dims > 1 ? output.shape().dim(input_dims - 2) : 1; - auto const output_d = output.shape().dim(input_dims - 1); - - auto const left_b_pad = paddings_data[2 * (input_dims - 4)]; - auto const left_h_pad = paddings_data[2 * (input_dims - 3)]; - auto const left_w_pad = paddings_data[2 * (input_dims - 2)]; - auto const left_d_pad = paddings_data[2 * (input_dims - 1)]; - - auto const right_b_pad = paddings_data[2 * (input_dims - 4) + 1]; - auto const right_h_pad = paddings_data[2 * (input_dims - 3) + 1]; - auto const right_w_pad = paddings_data[2 * (input_dims - 2) + 1]; - auto const right_d_pad = paddings_data[2 * (input_dims - 1) + 1]; - - const auto positive_mod = [](auto a, auto b) { return (a % b + b) % b; }; - const auto offset_index = [input_d, input_h_offset, input_b_offset](auto d, auto w, auto h, - auto b) { - return d + w * input_d + h * input_h_offset + b * input_b_offset; - }; - - const auto symmetric_dim = [&positive_mod](auto i, auto left_pad, auto input) { - bool reflected = (((i < left_pad ? i + 1 - input : i) - left_pad) / input & 1) == 1; - return positive_mod(reflected ? input + left_pad - i - 1 : i - left_pad, input); - }; - - const T *in_ptr = input_data; - T *out_ptr = output_data; - - for (int32_t b = 0; b < output_b; ++b) - { - for (int32_t h = 0; h < output_h; ++h) - { - for (int32_t w = 0; w < output_w; ++w) - { - for (int32_t d = 0; d < output_d; ++d) - { - if (b < left_b_pad || b >= output_b - right_b_pad || // - h < left_h_pad || h >= output_h - right_h_pad || // - w < left_w_pad || w >= output_w - right_w_pad || // - d < left_d_pad || d >= output_d - right_d_pad) - { - if (mode == MirrorPadMode::REFLECT) - { - *out_ptr++ = input_data[offset_index( - positive_mod(d - left_d_pad, input_d), positive_mod(w - left_w_pad, input_w), - positive_mod(h - left_h_pad, input_h), positive_mod(b - left_b_pad, input_b))]; - } - else - { - *out_ptr++ = input_data[offset_index( - symmetric_dim(d, left_d_pad, input_d), symmetric_dim(w, left_w_pad, input_w), - symmetric_dim(h, left_h_pad, input_h), symmetric_dim(b, left_b_pad, input_b))]; - } - } - else - { - *out_ptr++ = *in_ptr++; - } - } - } - } - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Mul.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Mul.cpp deleted file mode 100644 index 531fb4f..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Mul.cpp +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Mul.h" - -#include "kernels/BinaryOpCommon.h" -#include "kernels/Utils.h" - -#include "PALMul.h" - -#include - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Mul::Mul(const Tensor *input1, const Tensor *input2, Tensor *output, const MulParams ¶ms) - : KernelWithParams({input1, input2}, {output}, params) -{ -} - -void Mul::configure() -{ - LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); - LUCI_INTERPRETER_CHECK(output()->element_type() == input1()->element_type()); - if (input1()->element_type() == DataType::S16) - { - LUCI_INTERPRETER_CHECK(input1()->zero_points().size() == 1 && - input2()->zero_points().size() == 1) - LUCI_INTERPRETER_CHECK(input1()->zero_point() == 0 && input2()->zero_point() == 0 && - output()->zero_point() == 0); - } - - output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); -} - -void Mul::execute() const -{ - switch (input1()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::S64: - evalInteger(); - break; - case DataType::S32: - evalInteger(); - break; - case DataType::S16: - evalQuantizedS16(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Mul::evalFloat() const -{ - tflite::ArithmeticParams params{}; - fillArithmeticActivationRange(params, _params.activation); - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - luci_interpreter_pal::BroadcastMul4DSlow( - params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), - getTensorData(input2()), getTensorShape(output()), getTensorData(output())); - } - else - { - luci_interpreter_pal::Mul(params, getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -template void Mul::evalInteger() const -{ - tflite::ArithmeticParams params{}; - fillArithmeticActivationRange(params, _params.activation); - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - luci_interpreter_pal::BroadcastMul4DSlow( - params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), - getTensorData(input2()), getTensorShape(output()), getTensorData(output())); - } - else - { - luci_interpreter_pal::Mul(params, getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -void Mul::evalQuantizedS16() const -{ - const auto input1_scale = static_cast(input1()->scale()); - const auto input2_scale = static_cast(input2()->scale()); - const auto output_scale = static_cast(output()->scale()); - - const double real_multiplier = input1_scale * input2_scale / output_scale; - - int32_t output_multiplier; - int output_shift; - quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - auto fn = [output_multiplier, output_shift, activation_min, activation_max](int16_t input1_val, - int16_t input2_val) { - int32_t output = static_cast(input1_val) * static_cast(input2_val); - output = tflite::MultiplyByQuantizedMultiplier(output, output_multiplier, output_shift); - output = std::max(output, activation_min); - output = std::min(output, activation_max); - return static_cast(output); - }; - - BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output()), fn); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Neg.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Neg.cpp deleted file mode 100644 index c6fe08a..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Neg.cpp +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Neg.h" -#include "kernels/Utils.h" - -#include "PALNeg.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Neg::Neg(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Neg::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - - output()->resize(input()->shape()); -} - -void Neg::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Neg::evalFloat() const -{ - luci_interpreter_pal::Negate(getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/NotEqual.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/NotEqual.cpp deleted file mode 100644 index 54e5eee..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/NotEqual.cpp +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/NotEqual.h" -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -NotEqual::NotEqual(const Tensor *x, const Tensor *y, Tensor *output) : Kernel({x, y}, {output}) {} - -void NotEqual::configure() -{ - LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); - - if (x()->element_type() == DataType::U8) - { - quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); - quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); - } - output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); -} - -void NotEqual::execute() const -{ - switch (x()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::S64: - evalInteger(); - break; - case DataType::S32: - evalInteger(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void NotEqual::evalFloat() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowNotEqual(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::NotEqual(op_params, getTensorShape(x()), x_data, getTensorShape(y()), - y_data, getTensorShape(output()), output_data); - } -} - -template void NotEqual::evalInteger() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowNotEqualNoScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } - else - { - tflite::reference_ops::NotEqualNoScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } -} - -void NotEqual::evalQuantized() const -{ - const auto x_data = getTensorData(x()); - const auto y_data = getTensorData(y()); - auto output_data = getTensorData(output()); - - tflite::ComparisonParams op_params; - op_params.left_shift = 8; - op_params.input1_offset = -x()->zero_point(); // Note the '-' - op_params.input1_shift = _x_shift; - op_params.input1_multiplier = _x_multiplier; - op_params.input2_offset = -y()->zero_point(); // Note the '-' - op_params.input2_shift = _y_shift; - op_params.input2_multiplier = _y_multiplier; - op_params.is_broadcast = x()->shape() != y()->shape(); - - if (op_params.is_broadcast) - { - tflite::reference_ops::Broadcast4DSlowNotEqualWithScaling( - op_params, getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), - output_data); - } - else - { - tflite::reference_ops::NotEqualWithScaling(op_params, getTensorShape(x()), x_data, - getTensorShape(y()), y_data, - getTensorShape(output()), output_data); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/OneHot.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/OneHot.cpp deleted file mode 100644 index 4d3e5f2..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/OneHot.cpp +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2017 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/OneHot.h" -#include "kernels/Utils.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -namespace -{ - -template -void OneHotComputeImpl(const Tensor *indices_tensor, const Tensor *on_value_tensor, - const Tensor *off_value_tensor, int32_t depth, int32_t axis, - Tensor *output_tensor) -{ - // define input shape and correct axis - auto const &input_shape = indices_tensor->shape(); - axis = axis == -1 ? input_shape.num_dims() : axis; - - // TODO support other integer input types - auto const *indices = getTensorData(indices_tensor); - auto const on_value = getTensorData(on_value_tensor)[0]; - auto const off_value = getTensorData(off_value_tensor)[0]; - auto *output = getTensorData(output_tensor); - - // prefix_dim_size == # of elements before the axis - // depth == # of elements per axis - // suffix_dim_size == # of elements after the axis - auto prefix_dim_size = 1; - for (int32_t i = 0; i < axis; ++i) - { - prefix_dim_size *= input_shape.dim(i); - } - assert(prefix_dim_size > 0); - auto const suffix_dim_size = input_shape.num_elements() / prefix_dim_size; - - // View the indices as a matrix of size: - // prefix_dim_size x suffix_dim_size - // View the output as a matrix of size: - // prefix_dim_size x depth x suffix_dim_size - // Then the output is: - // output(i, j, k) == (indices(i, k) == j) ? on : off - for (int32_t i = 0; i < prefix_dim_size; ++i) - for (int32_t j = 0; j < depth; ++j) - for (int32_t k = 0; k < suffix_dim_size; ++k, ++output) - *output = indices[i * suffix_dim_size + k] == j ? on_value : off_value; -} - -} // namespace - -OneHot::OneHot(const Tensor *indices, const Tensor *depth, const Tensor *on_value, - const Tensor *off_value, Tensor *output, const OneHotParams ¶ms) - : KernelWithParams({indices, depth, on_value, off_value}, {output}, params) -{ - // Do nothing -} - -void OneHot::configure() -{ - // check types - LUCI_INTERPRETER_CHECK(indices()->element_type() == DataType::S32); - LUCI_INTERPRETER_CHECK(depth()->element_type() == DataType::S32); - LUCI_INTERPRETER_CHECK(on_value()->element_type() == off_value()->element_type()); - LUCI_INTERPRETER_CHECK(output()->element_type() == on_value()->element_type()); - - // check shape dependent parameters - LUCI_INTERPRETER_CHECK(on_value()->shape().num_elements() == 1); - LUCI_INTERPRETER_CHECK(off_value()->shape().num_elements() == 1); - LUCI_INTERPRETER_CHECK(depth()->shape().num_elements() == 1); - LUCI_INTERPRETER_CHECK(params().axis >= -1 && params().axis <= indices()->shape().num_dims()); - - // define parameters that affect the output shape - auto const depth_value = getTensorData(depth())[0]; - auto const &input_shape = indices()->shape(); - auto const input_dims = input_shape.num_dims(); - auto const axis = params().axis == -1 ? input_dims : params().axis; - - // define output shape - Shape output_shape(input_shape.num_dims() + 1); - { - for (int32_t d = 0; d < axis; ++d) - output_shape.dim(d) = input_shape.dim(d); - - output_shape.dim(axis) = depth_value; - - for (int32_t d = axis + 1; d < output_shape.num_dims(); ++d) - output_shape.dim(d) = input_shape.dim(d - 1); - } - - // reshape output - output()->resize(output_shape); -} - -void OneHot::execute() const -{ - auto const depth_value = getTensorData(depth())[0]; - auto const axis = params().axis; - - switch (output()->element_type()) - { - case loco::DataType::FLOAT32: - OneHotComputeImpl(indices(), on_value(), off_value(), depth_value, axis, output()); - break; - case loco::DataType::U8: - OneHotComputeImpl(indices(), on_value(), off_value(), depth_value, axis, output()); - break; - case loco::DataType::S16: - OneHotComputeImpl(indices(), on_value(), off_value(), depth_value, axis, output()); - break; - default: - // TODO Support other data types - throw std::runtime_error("Not supported, yet!"); - break; - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/PRelu.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/PRelu.cpp deleted file mode 100644 index 5a6b05c..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/PRelu.cpp +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/PRelu.h" - -#include "kernels/BinaryOpCommon.h" -#include "kernels/Utils.h" - -#include -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -PRelu::PRelu(const Tensor *input, const Tensor *alpha, Tensor *output) - : Kernel({input, alpha}, {output}) -{ -} - -PRelu::~PRelu() -{ - // Destructor declared to delete vector of alpha quantized data properly -} - -void PRelu::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - LUCI_INTERPRETER_CHECK(alpha()->element_type() == output()->element_type()); - LUCI_INTERPRETER_CHECK(input()->scales().size() <= 1); - LUCI_INTERPRETER_CHECK(output()->scales().size() <= 1); - - if (input()->element_type() == DataType::U8) - { - LUCI_INTERPRETER_CHECK(alpha()->scales().size() <= 1); // remove when CWQ kernel arrives - _alpha_multipliers.resize(1); - double alpha_multiplier = input()->scale() * alpha()->scale() / output()->scale(); - quantizeMultiplier(alpha_multiplier, &_alpha_multipliers[0].multiplier, - &_alpha_multipliers[0].shift); - double identity_multiplier = input()->scale() / output()->scale(); - quantizeMultiplier(identity_multiplier, &_output_multiplier_identity, &_output_shift_identity); - } - else if (input()->element_type() == DataType::S16) - { - // Common check for correctness of quant params - LUCI_INTERPRETER_CHECK(input()->zero_point() == 0 && output()->zero_point() == 0); - for (size_t channel = 0; channel < alpha()->zero_points().size(); ++channel) - { - LUCI_INTERPRETER_CHECK(alpha()->zero_points()[channel] == 0); - } - // PRelu specific checks for CWQ - LUCI_INTERPRETER_CHECK(alpha()->quantized_dimension() == alpha()->shape().num_dims() - 1); - LUCI_INTERPRETER_CHECK(static_cast(alpha()->scales().size()) == - alpha()->shape().dim(alpha()->quantized_dimension())); - LUCI_INTERPRETER_CHECK(alpha()->shape().num_elements() == - input()->shape().dim(input()->shape().num_dims() - 1)); - - // all dimension of alpha except last one should be size 1 - for (int dim = 0; dim < alpha()->shape().num_dims() - 1; ++dim) - { - LUCI_INTERPRETER_CHECK(alpha()->shape().dim(dim) == 1); - } - - std::vector real_multipliers = - getQuantizedConvolutionMultiplers(input()->scale(), alpha()->scales(), output()->scale()); - - _alpha_multipliers = quantizeMultipliers(real_multipliers); - - double identity_multiplier = input()->scale() / output()->scale(); - quantizeMultiplier(identity_multiplier, &_output_multiplier_identity, &_output_shift_identity); - } - output()->resize(calculateShapeForBroadcast(input()->shape(), alpha()->shape())); -} - -void PRelu::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::U8: - evalQuantized(); - break; - case DataType::S16: - evalQuantizedS16(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void PRelu::evalFloat() const -{ - const auto input_data = getTensorData(input()); - const auto alpha_data = getTensorData(alpha()); - const auto size = getTensorShape(input()).FlatSize(); - auto output_data = getTensorData(output()); - - auto PReluFunc = [](float input, float alpha) { return input >= 0.0 ? input : input * alpha; }; - - if (input()->shape() != alpha()->shape()) - { - tflite::reference_ops::BroadcastBinaryFunction4DSlow( - getTensorShape(input()), getTensorData(input()), getTensorShape(alpha()), - getTensorData(alpha()), getTensorShape(output()), getTensorData(output()), - PReluFunc); - } - else - { - for (auto i = decltype(size){0}; i < size; ++i) - { - if (input_data[i] >= 0) - output_data[i] = input_data[i]; - else - output_data[i] = input_data[i] * alpha_data[i]; - } - } -} - -void PRelu::evalQuantized() const -{ - tflite::PreluParams op_params{}; - - op_params.input_offset = -input()->zero_point(); // Note the '-'. - op_params.alpha_offset = -alpha()->zero_point(); // Note the '-'. - op_params.output_offset = output()->zero_point(); - op_params.output_shift_1 = _output_shift_identity; - op_params.output_multiplier_1 = _output_multiplier_identity; - op_params.output_shift_2 = _alpha_multipliers[0].shift; - op_params.output_multiplier_2 = _alpha_multipliers[0].multiplier; - - if (input()->shape() != alpha()->shape()) - { - tflite::reference_ops::BroadcastPrelu4DSlow( - op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(alpha()), - getTensorData(alpha()), getTensorShape(output()), getTensorData(output())); - } - else - { - tflite::reference_ops::Prelu( - op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(alpha()), - getTensorData(alpha()), getTensorShape(output()), getTensorData(output())); - } -} - -static inline int16_t evalElemS16PRelu(int16_t input_val, int16_t alpha_val, - const ChannelQuantMultipliers &identity_mult, - const ChannelQuantMultipliers &alpha_mult) -{ - constexpr int32_t quantized_min = std::numeric_limits::min(); - constexpr int32_t quantized_max = std::numeric_limits::max(); - - const int32_t output_val = - input_val >= 0 - ? tflite::MultiplyByQuantizedMultiplier(static_cast(input_val), - identity_mult.multiplier, identity_mult.shift) - : tflite::MultiplyByQuantizedMultiplier(static_cast(input_val * alpha_val), - alpha_mult.multiplier, alpha_mult.shift); - const int32_t clamped_output = std::min(quantized_max, std::max(quantized_min, output_val)); - return clamped_output; -} - -void PRelu::evalQuantizedS16() const -{ - // Note that this kernel assumes alpha is CWQ - tflite::RuntimeShape input_shape = getTensorShape(input()); - const int16_t *input_data = input()->data(); - const int16_t *alpha_data = alpha()->data(); - int16_t *output_data = output()->data(); - - const ChannelQuantMultipliers pos_mult{_output_shift_identity, _output_multiplier_identity}; - - const int last_dim = input()->shape().num_dims() - 1; - - int32_t outer_dims_size = 1; - for (int i = 0; i < last_dim; ++i) - outer_dims_size *= input_shape.Dims(i); - int32_t quant_dim_size = input_shape.Dims(last_dim); - - for (int32_t outer_dims = 0; outer_dims < outer_dims_size; ++outer_dims) - for (int32_t quant_channel = 0; quant_channel < quant_dim_size; ++quant_channel) - { - const ChannelQuantMultipliers &neg_mult = _alpha_multipliers[quant_channel]; - size_t offset = static_cast(outer_dims) * static_cast(quant_dim_size); - offset += quant_channel; - - output_data[offset] = - evalElemS16PRelu(input_data[offset], alpha_data[quant_channel], pos_mult, neg_mult); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Pack.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Pack.cpp deleted file mode 100644 index 42aab33..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Pack.cpp +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Pack.h" -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Pack::Pack(std::vector inputs, Tensor *output, const PackParams ¶ms) - : KernelWithParams(std::move(inputs), {output}, params) -{ -} - -void Pack::configure() -{ - LUCI_INTERPRETER_CHECK(_inputs.size() == static_cast(params().values_count)); - const Tensor *t0 = _inputs[0]; - const int dimension_size = t0->shape().num_dims() + 1; - int axis = params().axis; - if (axis < 0) - { - axis += dimension_size; - } - LUCI_INTERPRETER_CHECK(axis >= 0 && axis <= t0->shape().num_dims()); - - if (t0->element_type() != DataType::S32 && t0->element_type() != DataType::FLOAT32 && - t0->element_type() != DataType::U8 && t0->element_type() != DataType::S8 && - t0->element_type() != DataType::S16 && t0->element_type() != DataType::S64) - { - throw std::runtime_error("Unsupported type."); - } - - for (uint32_t i = 1; i < _inputs.size(); ++i) - { - const Tensor *tensor = _inputs[i]; - LUCI_INTERPRETER_CHECK(tensor->element_type() == t0->element_type()); - LUCI_INTERPRETER_CHECK(tensor->shape().num_dims() == t0->shape().num_dims()); - for (int d = 0; d < t0->shape().num_dims(); ++d) - { - LUCI_INTERPRETER_CHECK(tensor->shape().dim(d) == t0->shape().dim(d)); - } - } - - Shape output_shape(dimension_size); - int i = 0; - for (int index = 0; index < dimension_size; ++index) - { - if (index == axis) - { - output_shape.dim(index) = params().values_count; - } - else - { - output_shape.dim(index) = t0->shape().dim(i++); - } - } - - if (t0->element_type() == DataType::U8 || t0->element_type() == DataType::S8 || - t0->element_type() == DataType::S16) - { - LUCI_INTERPRETER_CHECK(output()->zero_point() == t0->zero_point()); - LUCI_INTERPRETER_CHECK(output()->scale() == t0->scale()); - // Guarantee input/output quantization params match as we do not support - // packing quantized tensors. - for (int i = 0; i < params().values_count; i++) - { - LUCI_INTERPRETER_CHECK(_inputs[i]->zero_point() == t0->zero_point()); - LUCI_INTERPRETER_CHECK(_inputs[i]->scale() == t0->scale()); - } - } - - output()->resize(output_shape); -} - -void Pack::execute() const -{ - switch (_inputs[0]->element_type()) - { - case DataType::FLOAT32: - evalGeneric(); - break; - case DataType::U8: - evalGeneric(); - break; - case DataType::S8: - evalGeneric(); - break; - case DataType::S16: - evalGeneric(); - break; - case DataType::S32: - evalGeneric(); - break; - case DataType::S64: - evalGeneric(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -template void Pack::evalGeneric() const -{ - const Tensor *t0 = _inputs[0]; - const int dimension_size = t0->shape().num_dims() + 1; - int axis = params().axis; - if (axis < 0) - { - axis += dimension_size; - } - - VectorOfTensors inputs(_inputs); - tflite::PackParams params{}; - params.axis = axis; - params.inputs_count = _inputs.size(); - tflite::reference_ops::Pack(params, inputs.shapes(), inputs.data(), getTensorShape(output()), - getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Pad.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Pad.cpp deleted file mode 100644 index c07f6e3..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Pad.cpp +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Pad.h" - -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Pad::Pad(const Tensor *input, const Tensor *paddings, Tensor *output) - : Kernel({input, paddings}, {output}) -{ -} - -void Pad::configure() -{ - const Shape &input_shape = input()->shape(); - const int num_dims = input_shape.num_dims(); - - if (num_dims > 4) - throw std::runtime_error("Unsupported number of dimensions."); - - assert(output()->element_type() == input()->element_type()); - assert(paddings()->element_type() == DataType::S32); - // Paddings shape should be [N, 2]. - assert(paddings()->shape().num_dims() == 2); - assert(paddings()->shape().dim(0) == num_dims); - assert(paddings()->shape().dim(1) == 2); - - Shape output_shape(num_dims); - const auto *paddings_data = getTensorData(paddings()); - for (int i = 0; i < num_dims; ++i) - { - const int32_t padding_before = paddings_data[i * 2]; - const int32_t padding_after = paddings_data[i * 2 + 1]; - assert(padding_before >= 0 && padding_after >= 0); - output_shape.dim(i) = input_shape.dim(i) + padding_before + padding_after; - } - - output()->resize(output_shape); -} - -void Pad::execute() const -{ - const int num_dims = input()->shape().num_dims(); - - tflite::PadParams params{}; - params.left_padding_count = num_dims; - params.right_padding_count = num_dims; - - const auto *paddings_data = getTensorData(paddings()); - for (int i = num_dims - 1; i >= 0; --i) - { - params.left_padding[i] = paddings_data[i * 2]; - params.right_padding[i] = paddings_data[i * 2 + 1]; - } - - switch (input()->element_type()) - { - case DataType::FLOAT32: - { - const float pad_value = 0.0f; - tflite::reference_ops::Pad(params, getTensorShape(input()), getTensorData(input()), - &pad_value, getTensorShape(output()), - getTensorData(output())); - break; - } - case DataType::U8: - { - assert(output()->zero_point() >= std::numeric_limits::min()); - assert(output()->zero_point() <= std::numeric_limits::max()); - const auto pad_value = static_cast(output()->zero_point()); - tflite::reference_ops::Pad(params, getTensorShape(input()), getTensorData(input()), - &pad_value, getTensorShape(output()), - getTensorData(output())); - break; - } - case DataType::S8: - { - assert(output()->zero_point() >= std::numeric_limits::min()); - assert(output()->zero_point() <= std::numeric_limits::max()); - const auto pad_value = static_cast(output()->zero_point()); - tflite::reference_ops::Pad(params, getTensorShape(input()), getTensorData(input()), - &pad_value, getTensorShape(output()), - getTensorData(output())); - break; - } - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/PadV2.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/PadV2.cpp deleted file mode 100644 index 197cdaa..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/PadV2.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/PadV2.h" - -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -PadV2::PadV2(const Tensor *input, const Tensor *paddings, const Tensor *constant_values, - Tensor *output) - : Kernel({input, paddings, constant_values}, {output}) -{ -} - -void PadV2::configure() -{ - const Shape &input_shape = input()->shape(); - const int num_dims = input_shape.num_dims(); - - if (num_dims > 4) - throw std::runtime_error("Unsupported number of dimensions."); - - assert(output()->element_type() == input()->element_type()); - assert(paddings()->element_type() == DataType::S32); - assert(constant_values()->element_type() == output()->element_type()); - // Paddings shape should be [N, 2]. - assert(paddings()->shape().num_dims() == 2); - assert(paddings()->shape().dim(0) == num_dims); - assert(paddings()->shape().dim(1) == 2); - // Constant values elements number should be 1. - assert(constant_values()->shape().num_elements() == 1); - - Shape output_shape(num_dims); - const auto *paddings_data = getTensorData(paddings()); - for (int i = 0; i < num_dims; ++i) - { - const int32_t padding_before = paddings_data[i * 2]; - const int32_t padding_after = paddings_data[i * 2 + 1]; - assert(padding_before >= 0 && padding_after >= 0); - output_shape.dim(i) = input_shape.dim(i) + padding_before + padding_after; - } - - output()->resize(output_shape); -} - -void PadV2::execute() const -{ - const int num_dims = input()->shape().num_dims(); - - tflite::PadParams params{}; - params.left_padding_count = num_dims; - params.right_padding_count = num_dims; - - const auto *paddings_data = getTensorData(paddings()); - for (int i = num_dims - 1; i >= 0; --i) - { - params.left_padding[i] = paddings_data[i * 2]; - params.right_padding[i] = paddings_data[i * 2 + 1]; - } - - switch (input()->element_type()) - { - case DataType::FLOAT32: - { - const auto pad_value = getTensorData(constant_values())[0]; - tflite::reference_ops::Pad(params, getTensorShape(input()), getTensorData(input()), - &pad_value, getTensorShape(output()), - getTensorData(output())); - break; - } - case DataType::U8: - { - assert(output()->zero_point() >= std::numeric_limits::min()); - assert(output()->zero_point() <= std::numeric_limits::max()); - const auto pad_value = getTensorData(constant_values())[0]; - tflite::reference_ops::Pad(params, getTensorShape(input()), getTensorData(input()), - &pad_value, getTensorShape(output()), - getTensorData(output())); - break; - } - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Pow.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Pow.cpp deleted file mode 100644 index 722c640..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Pow.cpp +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Pow.h" -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Pow::Pow(const Tensor *input1, const Tensor *input2, Tensor *output) - : Kernel({input1, input2}, {output}) -{ -} - -void Pow::configure() -{ - LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); - LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()); - - output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); -} - -void Pow::execute() const -{ - switch (input1()->element_type()) - { - case DataType::FLOAT32: - eval(); - break; - case DataType::S32: - eval(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -template void Pow::eval() const -{ - tflite::ArithmeticParams params{}; - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - tflite::reference_ops::BroadcastPow4DSlow(getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } - else - { - tflite::reference_ops::Pow(getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Quantize.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Quantize.cpp deleted file mode 100644 index 0c8544a..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Quantize.cpp +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Quantize.h" -#include "kernels/Utils.h" -#include "PALQuantize.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -namespace -{ - -template void call_requantize(const Tensor *input, Tensor *output) -{ - int32_t multiplier; - int shift; - - const double effective_output_scale = input->scale() / output->scale(); - quantizeMultiplier(effective_output_scale, &multiplier, &shift); - - const auto input_shape = getTensorShape(input); - const auto output_shape = getTensorShape(output); - const auto size = tflite::MatchingFlatSize(input_shape, output_shape); - - const auto input_data = getTensorData(input); - - switch (output->element_type()) - { - case loco::DataType::S8: - luci_interpreter_pal::Requantize(input_data, size, multiplier, shift, input->zero_point(), - output->zero_point(), getTensorData(output)); - break; - case loco::DataType::U8: - luci_interpreter_pal::Requantize(input_data, size, multiplier, shift, input->zero_point(), - output->zero_point(), getTensorData(output)); - break; - case loco::DataType::S16: - luci_interpreter_pal::Requantize(input_data, size, multiplier, shift, input->zero_point(), - output->zero_point(), getTensorData(output)); - break; - default: - throw std::runtime_error("Unsupported quantized type, yet!"); - } -} - -} // namespace - -Quantize::Quantize(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Quantize::configure() -{ - - if (input()->element_type() == loco::DataType::S16) - LUCI_INTERPRETER_CHECK(input()->zero_point() == 0); - - switch (input()->element_type()) - { - case loco::DataType::FLOAT32: - { - LUCI_INTERPRETER_CHECK(output()->element_type() == loco::DataType::U8 || - output()->element_type() == loco::DataType::S8 || - output()->element_type() == loco::DataType::S16); - break; - } - case loco::DataType::S16: - case loco::DataType::S8: - case loco::DataType::U8: - { - LUCI_INTERPRETER_CHECK(output()->element_type() == loco::DataType::S8 || - output()->element_type() == loco::DataType::U8 || - output()->element_type() == loco::DataType::S16); - if (output()->element_type() == loco::DataType::S16) - { - LUCI_INTERPRETER_CHECK(output()->zero_point() == 0); - } - break; - } - default: - throw std::runtime_error("Unsupported type"); - } - - output()->resize(input()->shape()); -} - -void Quantize::execute() const -{ - switch (input()->element_type()) - { - case loco::DataType::FLOAT32: - { - tflite::QuantizationParams op_params; - op_params.zero_point = output()->zero_point(); - op_params.scale = output()->scale(); - const auto input_data = getTensorData(input()); - - switch (output()->element_type()) - { - case loco::DataType::S8: - { - luci_interpreter_pal::Quantize(op_params, getTensorShape(input()), input_data, - getTensorShape(output()), getTensorData(output())); - break; - } - case loco::DataType::U8: - { - luci_interpreter_pal::Quantize(op_params, getTensorShape(input()), input_data, - getTensorShape(output()), - getTensorData(output())); - break; - } - case loco::DataType::S16: - { - luci_interpreter_pal::Quantize(op_params, getTensorShape(input()), input_data, - getTensorShape(output()), - getTensorData(output())); - break; - } - default: - throw std::runtime_error("Unsupported type."); - } - break; - } - case loco::DataType::S16: - { - call_requantize(input(), output()); - break; - } - case loco::DataType::S8: - { - call_requantize(input(), output()); - break; - } - case loco::DataType::U8: - { - call_requantize(input(), output()); - break; - } - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Relu.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Relu.cpp deleted file mode 100644 index 747ec6c..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Relu.cpp +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Relu.h" -#include "kernels/Utils.h" - -#include "PALRelu.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Relu::Relu(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Relu::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - if (input()->element_type() == DataType::S16) - { - LUCI_INTERPRETER_CHECK(input()->zero_point() == 0 && output()->zero_point() == 0); - } - - if (input()->element_type() == DataType::U8 || input()->element_type() == DataType::S16) - { - double multiplier = input()->scale() / output()->scale(); - quantizeMultiplier(multiplier, &_output_multiplier, &_output_shift); - } - output()->resize(input()->shape()); -} - -void Relu::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::U8: - evalQuantized(); - break; - case DataType::S16: - evalQuantizedS16(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Relu::evalFloat() const -{ - const auto input_data = getTensorData(input()); - const auto input_shape = getTensorShape(input()); - auto output_data = getTensorData(output()); - auto output_shape = getTensorShape(output()); - - luci_interpreter_pal::Relu(input_shape, input_data, output_shape, output_data); -} - -void Relu::evalQuantized() const -{ - tflite::ReluParams params; - params.input_offset = input()->zero_point(); - params.output_offset = output()->zero_point(); - params.output_multiplier = _output_multiplier; - params.output_shift = _output_shift; - - params.quantized_activation_min = - std::max(static_cast(std::numeric_limits::min()), params.output_offset); - params.quantized_activation_max = static_cast(std::numeric_limits::max()); - - luci_interpreter_pal::ReluX(params, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -void Relu::evalQuantizedS16() const -{ - const auto *input_data = getTensorData(input()); - auto *output_data = getTensorData(output()); - - constexpr int32_t output_min = 0; - constexpr int32_t output_max = std::numeric_limits::max(); - - const int32_t num_elements = input()->shape().num_elements(); - - for (int32_t i = 0; i < num_elements; ++i) - { - const int32_t input_val = input_data[i]; - int32_t output_val = - tflite::MultiplyByQuantizedMultiplier(input_val, _output_multiplier, _output_shift); - output_val = std::max(output_val, output_min); - output_val = std::min(output_val, output_max); - output_data[i] = static_cast(output_val); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Relu6.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Relu6.cpp deleted file mode 100644 index 07205ed..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Relu6.cpp +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Relu6.h" -#include "kernels/Utils.h" - -#include "PALRelu6.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Relu6::Relu6(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Relu6::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - - if (input()->element_type() == DataType::U8) - { - double multiplier = input()->scale() / output()->scale(); - quantizeMultiplier(multiplier, &_output_multiplier, &_output_shift); - } - output()->resize(input()->shape()); -} - -void Relu6::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Relu6::evalFloat() const -{ - const auto input_data = getTensorData(input()); - const auto input_shape = getTensorShape(input()); - auto output_data = getTensorData(output()); - auto output_shape = getTensorShape(output()); - - luci_interpreter_pal::Relu6(input_shape, input_data, output_shape, output_data); -} - -void Relu6::evalQuantized() const -{ - tflite::ReluParams params; - params.input_offset = input()->zero_point(); - params.output_offset = output()->zero_point(); - params.output_multiplier = _output_multiplier; - params.output_shift = _output_shift; - - params.quantized_activation_min = - std::max(static_cast(std::numeric_limits::min()), params.output_offset); - params.quantized_activation_max = - std::min(static_cast(std::numeric_limits::max()), - params.output_offset + static_cast(roundf(6.f / output()->scale()))); - - luci_interpreter_pal::ReluX(params, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Reshape.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Reshape.cpp deleted file mode 100644 index 61d3300..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Reshape.cpp +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2017 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Reshape.h" - -#include -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -static Shape extractShapeFromTensor(const Tensor *tensor) -{ - assert(tensor->element_type() == DataType::S32); - Shape shape(tensor->shape().num_elements()); - const auto *shape_data = tensor->data(); - for (int i = 0; i < tensor->shape().num_elements(); ++i) - { - shape.dim(i) = shape_data[i]; - } - return shape; -} - -static void resolveUnknownDimension(const Shape &input_shape, Shape *output_shape) -{ - const int32_t num_input_elements = input_shape.num_elements(); - int32_t num_output_elements = 1; - int unknown_dim_index = -1; - for (int i = 0; i < output_shape->num_dims(); ++i) - { - const int32_t value = output_shape->dim(i); - if (value == -1) - { - assert(unknown_dim_index == -1); - unknown_dim_index = i; - } - else - { - num_output_elements *= value; - } - } - if (unknown_dim_index != -1) - { - output_shape->dim(unknown_dim_index) = num_input_elements / num_output_elements; - num_output_elements *= output_shape->dim(unknown_dim_index); - } - assert(num_output_elements == num_input_elements); -} - -Reshape::Reshape(const Tensor *input, const Tensor *shape, Tensor *output) - : Kernel({input, shape}, {output}) -{ -} - -void Reshape::configure() -{ - Shape output_shape = extractShapeFromTensor(shape()); - resolveUnknownDimension(input()->shape(), &output_shape); - output()->resize(output_shape); -} - -void Reshape::execute() const -{ - const auto *input_data = input()->data(); - auto *output_data = output()->data(); - - const size_t element_size = getDataTypeSize(input()->element_type()); - const int32_t num_elements = input()->shape().num_elements(); - std::memcpy(output_data, input_data, num_elements * element_size); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Reshape.h b/compiler/luci-micro/luci-interpreter/src/kernels/Reshape.h deleted file mode 100644 index 99b947f..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Reshape.h +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_KERNELS_RESHAPE_H -#define LUCI_INTERPRETER_KERNELS_RESHAPE_H - -#include "core/Kernel.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -class Reshape : public Kernel -{ -public: - Reshape(const Tensor *input, const Tensor *shape, Tensor *output); - - const Tensor *input() const { return _inputs[0]; } - const Tensor *shape() const { return _inputs[1]; } - Tensor *output() const { return _outputs[0]; } - - void configure() override; - void execute() const override; -}; - -} // namespace kernels -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_KERNELS_RESHAPE_H diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Reshape.test.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Reshape.test.cpp deleted file mode 100644 index c2ff3ea..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Reshape.test.cpp +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Reshape.h" -#include "kernels/TestUtils.h" -#include "luci_interpreter/TestMemoryManager.h" - -namespace luci_interpreter -{ -namespace kernels -{ -namespace -{ - -using namespace testing; - -class ReshapeTest : public ::testing::Test -{ -protected: - void SetUp() override { _memory_manager = std::make_unique(); } - - std::unique_ptr _memory_manager; -}; - -// TODO Test types other than FLOAT32. - -TEST_F(ReshapeTest, Regular) -{ - Shape input_shape{1, 2, 2, 3}; - std::vector input_data{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; - Shape shape_shape{2}; - std::vector shape_data{3, 4}; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor shape_tensor = - makeInputTensor(shape_shape, shape_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - Reshape kernel(&input_tensor, &shape_tensor, &output_tensor); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(input_data)); -} - -TEST_F(ReshapeTest, UnknownDimension) -{ - Shape input_shape{2, 1, 2, 3}; - std::vector input_data{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; - Shape shape_shape{3}; - std::vector shape_data{2, -1, 2}; - Tensor input_tensor = - makeInputTensor(input_shape, input_data, _memory_manager.get()); - Tensor shape_tensor = - makeInputTensor(shape_shape, shape_data, _memory_manager.get()); - Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); - - Reshape kernel(&input_tensor, &shape_tensor, &output_tensor); - kernel.configure(); - _memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(input_data)); -} - -} // namespace -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ResizeBilinear.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/ResizeBilinear.cpp deleted file mode 100644 index e2ddd6a..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/ResizeBilinear.cpp +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/ResizeBilinear.h" - -#include "kernels/Utils.h" - -#include "PALResizeBilinear.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -ResizeBilinear::ResizeBilinear(const Tensor *input, const Tensor *size, Tensor *output, - const ResizeBilinearParams ¶ms) - : KernelWithParams({input, size}, {output}, params) -{ -} - -void ResizeBilinear::configure() -{ - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); - LUCI_INTERPRETER_CHECK(size()->shape().num_dims() == 1); - LUCI_INTERPRETER_CHECK(size()->element_type() == DataType::S32); - if (params().half_pixel_centers && params().align_corners) - throw std::runtime_error("If half_pixel_centers is True, align_corners must be False."); - LUCI_INTERPRETER_CHECK(size()->shape().dim(0) == 2); - Shape output_shape(4); - output_shape.dim(0) = input()->shape().dim(0); - output_shape.dim(1) = getTensorData(size())[0]; - output_shape.dim(2) = getTensorData(size())[1]; - output_shape.dim(3) = input()->shape().dim(3); - output()->resize(output_shape); -} - -void ResizeBilinear::execute() const -{ - tflite::ResizeBilinearParams op_params{}; - op_params.align_corners = params().align_corners; - op_params.half_pixel_centers = params().half_pixel_centers; - switch (output()->element_type()) - { - case DataType::FLOAT32: - luci_interpreter_pal::ResizeBilinear( - op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(size()), - getTensorData(size()), getTensorShape(output()), getTensorData(output())); - break; - case DataType::U8: - luci_interpreter_pal::ResizeBilinear( - op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(size()), - getTensorData(size()), getTensorShape(output()), getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.cpp deleted file mode 100644 index 306cefb..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.cpp +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/ResizeNearestNeighbor.h" - -#include "kernels/Utils.h" - -#include -#include "PALResizeNearestNeighbor.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -ResizeNearestNeighbor::ResizeNearestNeighbor(const Tensor *input, const Tensor *size, - Tensor *output, - const ResizeNearestNeighborParams ¶ms) - : KernelWithParams({input, size}, {output}, params) -{ -} - -void ResizeNearestNeighbor::configure() -{ - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); - LUCI_INTERPRETER_CHECK(size()->shape().num_dims() == 1); - LUCI_INTERPRETER_CHECK(size()->element_type() == DataType::S32); - LUCI_INTERPRETER_CHECK(size()->shape().dim(0) == 2); - Shape output_shape(4); - output_shape.dim(0) = input()->shape().dim(0); - output_shape.dim(1) = getTensorData(size())[0]; - output_shape.dim(2) = getTensorData(size())[1]; - output_shape.dim(3) = input()->shape().dim(3); - output()->resize(output_shape); -} - -void ResizeNearestNeighbor::execute() const -{ - tflite::ResizeNearestNeighborParams op_params{}; - op_params.align_corners = params().align_corners; - op_params.half_pixel_centers = params().half_pixel_centers; - switch (output()->element_type()) - { - case DataType::FLOAT32: - tflite::reference_ops::ResizeNearestNeighbor( - op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(size()), - getTensorData(size()), getTensorShape(output()), getTensorData(output())); - break; - case DataType::U8: - luci_interpreter_pal::ResizeNearestNeighbor( - op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(size()), - getTensorData(size()), getTensorShape(output()), getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ReverseV2.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/ReverseV2.cpp deleted file mode 100644 index 1b6a5cc..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/ReverseV2.cpp +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/ReverseV2.h" -#include "kernels/Utils.h" -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -ReverseV2::ReverseV2(const Tensor *input, const Tensor *axes, Tensor *output) - : Kernel({input, axes}, {output}) -{ -} - -void ReverseV2::configure() -{ - assert(axes()->shape().num_dims() == 1); - assert(input()->shape().num_dims() >= axes()->shape().num_elements()); - if (input()->element_type() != DataType::S32 && input()->element_type() != DataType::FLOAT32 && - input()->element_type() != DataType::U8 && input()->element_type() != DataType::S16 && - input()->element_type() != DataType::S64) - { - throw std::runtime_error("Unsupported input type."); - } - if (axes()->element_type() != DataType::S32) - { - throw std::runtime_error("Unsupported axes type."); - } - if (axes()->shape().num_elements() > 1) - { - throw std::runtime_error("Current implementation does not support more than 1 axis."); - } - int axis_value = getTensorData(axes())[0]; - if (axis_value < 0 || axis_value >= input()->shape().num_dims()) - { - throw std::runtime_error("Invalid axes value"); - } - assert(input()->element_type() == output()->element_type()); - - output()->resize(input()->shape()); -} - -void ReverseV2::execute() const -{ - int axis_value = getTensorData(axes())[0]; - switch (output()->element_type()) - { - case DataType::FLOAT32: - tflite::reference_ops::Reverse(axis_value, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - case DataType::U8: - tflite::reference_ops::Reverse( - axis_value, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported output type"); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Rsqrt.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Rsqrt.cpp deleted file mode 100644 index 6dd92dc..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Rsqrt.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Rsqrt.h" -#include "kernels/Utils.h" - -#include -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Rsqrt::Rsqrt(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Rsqrt::configure() -{ - if (input()->element_type() != output()->element_type()) - { - throw std::runtime_error("Input/output tensor data type mismatch."); - } - output()->resize(input()->shape()); -} - -void Rsqrt::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Rsqrt::evalFloat() const -{ - auto in = getTensorData(input()); - auto out = getTensorData(output()); - auto size = getTensorShape(input()).FlatSize(); - for (auto i = in; i != in + size; ++i) - { - *out = 1.f / std::sqrt(*i); - ++out; - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SVDF.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/SVDF.cpp deleted file mode 100644 index 40d79aa..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/SVDF.cpp +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2017 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/SVDF.h" -#include "kernels/Utils.h" -#include "PALSVDF.h" - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -namespace -{ -TfLiteFusedActivation get_tflite_activation(Activation activation) -{ - switch (activation) - { - case luci::FusedActFunc::RELU: - return kTfLiteActRelu; - case luci::FusedActFunc::RELU6: - return kTfLiteActRelu6; - case luci::FusedActFunc::RELU_N1_TO_1: - return kTfLiteActReluN1To1; - case luci::FusedActFunc::TANH: - return kTfLiteActTanh; - case luci::FusedActFunc::SIGN_BIT: - return kTfLiteActSignBit; - case luci::FusedActFunc::NONE: - return kTfLiteActNone; - default: - throw std::runtime_error("Unsupported activation type"); - } -} -} // namespace - -SVDF::SVDF(const Tensor *input, const Tensor *weight_feature, const Tensor *weight_time, - const Tensor *bias, const Tensor *input_activation_state, Tensor *output, - Tensor *scratchpad_activation_state, Tensor *scratchpad_1, Tensor *scratchpad_2, - Tensor *scratchpad_3, Tensor *scratchpad_4, Tensor *scratchpad_5, Tensor *scratchpad_6, - const SVDFParams ¶ms) - : KernelWithParams({input, weight_feature, weight_time, bias, input_activation_state}, - {output, scratchpad_activation_state, scratchpad_1, scratchpad_2, - scratchpad_3, scratchpad_4, scratchpad_5, scratchpad_6}, - params) -{ - // Do nothing -} - -void SVDF::configure() -{ - const Shape &input_shape = input()->shape(); - const Shape &weight_features_shape = weight_feature()->shape(); - const Shape &weight_time_shape = weight_time()->shape(); - - // Validate Input Tensor: - LUCI_INTERPRETER_CHECK(input()->element_type() == loco::DataType::FLOAT32 || - input()->element_type() == loco::DataType::S8); - LUCI_INTERPRETER_CHECK(input_shape.num_dims() == 2); - - // Validate inputs and output types - if (input()->element_type() == loco::DataType::S8) - { - LUCI_INTERPRETER_CHECK(weight_feature()->element_type() == loco::DataType::S8); - LUCI_INTERPRETER_CHECK(weight_time()->element_type() == loco::DataType::S16 || - weight_time()->element_type() == loco::DataType::S8); - if (bias()) - LUCI_INTERPRETER_CHECK(bias()->element_type() == loco::DataType::S32); - - LUCI_INTERPRETER_CHECK(input_activation_state()->element_type() == loco::DataType::S16 || - input_activation_state()->element_type() == loco::DataType::S8); - LUCI_INTERPRETER_CHECK(output()->element_type() == loco::DataType::S8); - - // Note: now tflite support only ReLU activation for integer SVDF - LUCI_INTERPRETER_CHECK(params().activation == luci::FusedActFunc::RELU); - } - else if (weight_feature()->element_type() == loco::DataType::FLOAT32) - { - LUCI_INTERPRETER_CHECK(weight_feature()->element_type() == loco::DataType::FLOAT32); - LUCI_INTERPRETER_CHECK(weight_time()->element_type() == loco::DataType::FLOAT32); - LUCI_INTERPRETER_CHECK(input_activation_state()->element_type() == loco::DataType::FLOAT32); - if (bias()) - LUCI_INTERPRETER_CHECK(bias()->element_type() == loco::DataType::FLOAT32); - LUCI_INTERPRETER_CHECK(output()->element_type() == loco::DataType::FLOAT32); - } - else if ((weight_feature()->element_type() == loco::DataType::U8 || - weight_feature()->element_type() == loco::DataType::S8) && - input()->element_type() == loco::DataType::FLOAT32) - { - // TODO:: support hybrid SVDF op - throw std::runtime_error("Hybrid type is not currently supported"); - } - else - { - throw std::runtime_error("Unsupported type."); - } - - // Check all the parameters of tensor match within themselves and match the - // input configuration. - const int rank = params().svdf_rank; - const int batch_size = input_shape.dim(0); - const int num_filters = weight_features_shape.dim(0); - LUCI_INTERPRETER_CHECK(rank != 0); - LUCI_INTERPRETER_CHECK(num_filters % rank == 0); - - const int num_units = num_filters / rank; - const int memory_size = weight_time_shape.dim(1); - - // Validate Weight_Feature Input Tensor: - LUCI_INTERPRETER_CHECK(weight_features_shape.num_dims() == 2); - LUCI_INTERPRETER_CHECK(weight_features_shape.dim(1) == input_shape.dim(1)); - - // Validate Weight_Time Input Tensor: - LUCI_INTERPRETER_CHECK(weight_time_shape.num_dims() == 2); - LUCI_INTERPRETER_CHECK(weight_time_shape.dim(0) == num_filters); - - // Validate Bias - if (bias()) - LUCI_INTERPRETER_CHECK(bias()->shape().dim(0) == num_units); - - // Validate Input Activation State - LUCI_INTERPRETER_CHECK(input_activation_state()->shape().num_dims() == 2); - LUCI_INTERPRETER_CHECK(input_activation_state()->shape().dim(0) == batch_size); - LUCI_INTERPRETER_CHECK(input_activation_state()->shape().dim(1) == memory_size * num_filters); - - // Resize scratchpad_state to input_activation_state - auto scratchpad_activation_state = getOutputTensors()[1]; - scratchpad_activation_state->resize({batch_size, memory_size * num_filters}); - - // Resize output tensor - output()->resize({batch_size, num_units}); - - luci_interpreter_pal::SetupScratchpadTensor( - input()->element_type(), weight_feature()->element_type(), getOutputTensors()[2], - getOutputTensors()[3], getOutputTensors()[4], getOutputTensors()[5], getOutputTensors()[6], - getOutputTensors()[7], input_shape, weight_time_shape, batch_size, num_filters, num_units); -} - -void SVDF::execute() const -{ - switch (weight_feature()->element_type()) - { - case loco::DataType::FLOAT32: - evalFloat(); - break; - case loco::DataType::S8: - { - if (input()->element_type() == loco::DataType::S8) - evalInteger(); - else - // TODO:: support hybrid SVDF op - throw std::runtime_error("Hybrid type is not currently supported"); - break; - } - default: - throw std::runtime_error("Unsupported type"); - } -} - -void SVDF::evalInteger() const -{ - const auto effective_scale_1 = static_cast(input()->scale() * weight_feature()->scale() / - input_activation_state()->scale()); - const auto effective_scale_2 = static_cast(input_activation_state()->scale() * - weight_time()->scale() / output()->scale()); - - int32_t effective_scale_1_a; - int effective_scale_1_b; - int32_t effective_scale_2_a; - int effective_scale_2_b; - - tflite::QuantizeMultiplier(effective_scale_1, &effective_scale_1_a, &effective_scale_1_b); - tflite::QuantizeMultiplier(effective_scale_2, &effective_scale_2_a, &effective_scale_2_b); - - TfLiteSVDFParams params_svdf{}; - params_svdf.asymmetric_quantize_inputs = params().asymmetric_quantize_inputs; - params_svdf.rank = params().svdf_rank; - params_svdf.activation = get_tflite_activation(params().activation); - - auto scratchpad_activation_state = getOutputTensors()[1]; - // Note: it is expected that activation_state input variable tensor reset to zero, - // also expected that this variable tensor doesn't have buffer - auto scratchpad_data = getTensorData(scratchpad_activation_state); - std::fill_n(scratchpad_data, scratchpad_activation_state->shape().num_elements(), 0); - - auto scratchpad = getOutputTensors()[2]; - auto output_temp = getOutputTensors()[3]; - - int32_t input_zp = input()->zero_point(); - int32_t output_zp = output()->zero_point(); - luci_interpreter_pal::IntegerSVDF( - params_svdf, getTensorShape(input()), getTensorData(input()), - getTensorShape(weight_feature()), getTensorData(weight_feature()), - getTensorShape(weight_time()), getTensorData(weight_time()), getTensorShape(bias()), - getTensorData(bias()), scratchpad_data, getTensorShape(output()), - getTensorData(output()), getTensorData(scratchpad), - getTensorData(output_temp), effective_scale_1_a, effective_scale_1_b, - effective_scale_2_a, effective_scale_2_b, input_zp, output_zp); -} - -void SVDF::evalFloat() const -{ - TfLiteSVDFParams params_svdf{}; - params_svdf.asymmetric_quantize_inputs = params().asymmetric_quantize_inputs; - params_svdf.rank = params().svdf_rank; - params_svdf.activation = get_tflite_activation(params().activation); - - auto scratchpad_activation_state = getOutputTensors()[1]; - // Note: it is expected that activation_state input variable tensor reset to zero, - // also expected that this variable tensor doesn't have buffer - auto scratchpad_data = getTensorData(scratchpad_activation_state); - std::fill_n(scratchpad_data, scratchpad_activation_state->shape().num_elements(), 0); - - auto scratchpad_1 = getOutputTensors()[2]; - - luci_interpreter_pal::FloatSVDF( - params_svdf, getTensorShape(input()), getTensorData(input()), - getTensorShape(weight_feature()), getTensorData(weight_feature()), - getTensorShape(weight_time()), getTensorData(weight_time()), getTensorShape(bias()), - getTensorData(bias()), getTensorData(scratchpad_1), scratchpad_data, - getTensorShape(output()), getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Shape.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Shape.cpp deleted file mode 100644 index 0429fe1..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Shape.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Shape.h" -#include "kernels/Utils.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -ShapeKernel::ShapeKernel(const Tensor *input, Tensor *output, const ShapeParams ¶ms) - : KernelWithParams({input}, {output}, params) -{ -} - -void ShapeKernel::configure() -{ - LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::S32 or - output()->element_type() == DataType::S64); - const auto input_shape = input()->shape(); - - Shape output_shape(1); - output_shape.dim(0) = input_shape.num_dims(); - - output()->resize(output_shape); -} - -void ShapeKernel::execute() const -{ - switch (params().out_type) - { - case DataType::S32: - evalInt(); - break; - case DataType::S64: - evalInt(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -template void ShapeKernel::evalInt() const -{ - const auto input_shape = input()->shape(); - - auto output_data = getTensorData(output()); - - for (int i = 0; i < input_shape.num_dims(); ++i) - { - output_data[i] = input_shape.dim(i); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Slice.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Slice.cpp deleted file mode 100644 index 2fe2c54..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Slice.cpp +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Slice.h" -#include "Utils.h" -#include "PALSlice.h" - -#include -#include - -namespace luci_interpreter -{ - -namespace kernels -{ -const int max_dim = 4; - -Slice::Slice(const Tensor *input, const Tensor *begin, const Tensor *size, Tensor *output) - : Kernel({input, begin, size}, {output}) -{ -} - -template -Shape calculateOutputShape(const Tensor *input, const Tensor *begin, const Tensor *size) -{ - Shape output_shape = Shape(input->shape().num_dims()); - for (int idx = 0; idx < input->shape().num_dims(); idx++) - { - T size_value = getTensorData(size)[idx]; - if (size_value < 0) - { - if (size_value != -1) - { - throw std::runtime_error("Invalid size."); - } - size_value = input->shape().dim(idx) - getTensorData(begin)[idx]; - } - else - { - if (input->shape().dim(idx) < getTensorData(begin)[idx] + size_value) - { - throw std::runtime_error("Invalid begin and size."); - } - } - output_shape.dim(idx) = static_cast(size_value); - } - return output_shape; -} - -template -void getBeginAndSizeVectors(int dimensions, const Tensor *begin, const Tensor *size, - std::vector *begins, std::vector *sizes) -{ - for (int idx = dimensions - 1; idx >= 0; --idx) - { - begins->push_back(getTensorData(begin)[idx]); - sizes->push_back(getTensorData(size)[idx]); - } -} - -void Slice::configure() -{ - assert(input()->element_type() == output()->element_type()); - assert(begin()->element_type() == DataType::S32 || begin()->element_type() == DataType::S64); - assert(size()->element_type() == DataType::S32 || size()->element_type() == DataType::S64); - assert(begin()->shape().num_dims() == 1); - assert(size()->shape().num_dims() == 1); - assert(input()->shape().num_dims() <= max_dim); - - if (begin()->element_type() == DataType::S32) - { - output()->resize(calculateOutputShape(input(), begin(), size())); - } - else if (begin()->element_type() == DataType::S64) - { - output()->resize(calculateOutputShape(input(), begin(), size())); - } - else - { - throw std::runtime_error("Unsupported type."); - } -} - -void Slice::execute() const -{ - std::vector begins; - begins.reserve(max_dim); - std::vector sizes; - sizes.reserve(max_dim); - if (begin()->element_type() == DataType::S32) - { - getBeginAndSizeVectors(input()->shape().num_dims(), begin(), size(), &begins, &sizes); - } - else if (begin()->element_type() == DataType::S64) - { - getBeginAndSizeVectors(input()->shape().num_dims(), begin(), size(), &begins, &sizes); - } - else - { - throw std::runtime_error("Unsupported begin type."); - } - for (int i = input()->shape().num_dims(); i < max_dim; ++i) - { - begins.push_back(0); - sizes.push_back(1); - } - - assert(begins.size() == 4); - assert(sizes.size() == 4); - tflite::SliceParams op_params{}; - op_params.begin_count = 4; - op_params.size_count = 4; - for (int i = 0; i < 4; i++) - { - op_params.begin[i] = begins[3 - i]; - op_params.size[i] = sizes[3 - i]; - } - switch (input()->element_type()) - { - case DataType::FLOAT32: - luci_interpreter_pal::Slice(op_params, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); - break; - case DataType::U8: - luci_interpreter_pal::Slice(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - case DataType::S8: - luci_interpreter_pal::Slice(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported input type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Softmax.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Softmax.cpp deleted file mode 100644 index c230aaa..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Softmax.cpp +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Softmax.h" - -#include "kernels/Utils.h" - -#include -#include "PALSoftmax.h" - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Softmax::Softmax(const Tensor *input, Tensor *output, const SoftmaxParams ¶ms) - : KernelWithParams({input}, {output}, params) -{ -} - -void Softmax::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() >= 1); - if (input()->element_type() == DataType::U8 || input()->element_type() == DataType::S8) - { - LUCI_INTERPRETER_CHECK(input()->element_type() == DataType::S8 || output()->zero_point() == 0); - LUCI_INTERPRETER_CHECK(input()->element_type() == DataType::U8 || - output()->zero_point() == std::numeric_limits::min()); - tflite::SoftmaxParams op_params{}; - op_params.table = _table; - luci_interpreter_pal::PopulateSoftmaxLookupTable(&op_params, input()->scale(), params().beta); - } - output()->resize(input()->shape()); -} - -void Softmax::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::S8: - evalQuantized(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Softmax::evalFloat() const -{ - tflite::SoftmaxParams op_params{}; - op_params.beta = params().beta; - - tflite::reference_ops::Softmax(op_params, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -template void Softmax::evalQuantized() const -{ - tflite::SoftmaxParams op_params{}; - op_params.table = const_cast(_table); - op_params.zero_point = output()->zero_point(); - op_params.scale = output()->scale(); - luci_interpreter_pal::InitializeParams(&op_params, input()->scale(), params().beta); - luci_interpreter_pal::Softmax(op_params, getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Softmax.h b/compiler/luci-micro/luci-interpreter/src/kernels/Softmax.h deleted file mode 100644 index 1f281df..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Softmax.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_KERNELS_SOFTMAX_H -#define LUCI_INTERPRETER_KERNELS_SOFTMAX_H - -#include "core/Kernel.h" -#include "core/KernelParams.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -class Softmax : public KernelWithParams -{ -public: - Softmax(const Tensor *input, Tensor *output, const SoftmaxParams ¶ms); - - const Tensor *input() const { return _inputs[0]; } - Tensor *output() const { return _outputs[0]; } - - void configure() override; - void execute() const override; - -private: - void evalFloat() const; - template void evalQuantized() const; - - float _table[256]; -}; - -} // namespace kernels -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_KERNELS_SOFTMAX_H diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Softmax.test.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Softmax.test.cpp deleted file mode 100644 index 08e7067..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Softmax.test.cpp +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Softmax.h" -#include "kernels/TestUtils.h" -#include "luci_interpreter/TestMemoryManager.h" - -namespace luci_interpreter -{ -namespace kernels -{ -namespace -{ - -using namespace testing; - -template constexpr loco::DataType toLocoDataType(); - -template <> constexpr loco::DataType toLocoDataType() { return loco::DataType::FLOAT32; } - -template <> constexpr loco::DataType toLocoDataType() { return loco::DataType::U8; } - -template <> constexpr loco::DataType toLocoDataType() { return loco::DataType::S8; } - -template ::value, bool> = true> -void Check(std::initializer_list input_shape, std::initializer_list output_shape, - std::initializer_list input_data, std::initializer_list output_data) -{ - std::unique_ptr memory_manager = std::make_unique(); - - Tensor input_tensor = - makeInputTensor()>(input_shape, input_data, memory_manager.get()); - Tensor output_tensor = makeOutputTensor(toLocoDataType()); - - SoftmaxParams params{}; - params.beta = 0.1; - - Softmax kernel(&input_tensor, &output_tensor, params); - kernel.configure(); - memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(output_data)); - EXPECT_THAT(extractTensorShape(output_tensor), output_shape); -} - -template ::value, bool> = true> -void Check(std::initializer_list input_shape, std::initializer_list output_shape, - std::initializer_list input_data, std::initializer_list output_data) -{ - std::unique_ptr memory_manager = std::make_unique(); - - std::pair input_quant_param = - quantizationParams(std::min(std::min(input_data), 0.f), - std::max(std::max(input_data), 0.f)); - std::pair output_quant_param = - quantizationParams(std::min(std::min(output_data), 0.f), - std::max(std::max(output_data), 0.f)); - Tensor input_tensor = makeInputTensor()>(input_shape, input_quant_param.first, - input_quant_param.second, input_data, - memory_manager.get()); - Tensor output_tensor = - makeOutputTensor(toLocoDataType(), output_quant_param.first, output_quant_param.second); - - SoftmaxParams params{}; - params.beta = 0.1; - - Softmax kernel(&input_tensor, &output_tensor, params); - kernel.configure(); - memory_manager->allocate_memory(output_tensor); - kernel.execute(); - - EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); - EXPECT_THAT(dequantizeTensorData(output_tensor), - FloatArrayNear(output_data, output_tensor.scale())); -} - -template class SoftmaxTest : public ::testing::Test -{ -}; - -using DataTypes = ::testing::Types; -TYPED_TEST_SUITE(SoftmaxTest, DataTypes); - -TYPED_TEST(SoftmaxTest, Simple) -{ - Check({2, 1, 2, 3}, {2, 1, 2, 3}, - { - 5, -9, 8, // - -7, 2, -4, // - 1, -2, 9, // - 3, -6, -1, // - }, - { - 0.38514, 0.09497, 0.51989, // - 0.20792, 0.51141, 0.28067, // - 0.25212, 0.18678, 0.56110, // - 0.48149, 0.19576, 0.32275, // - }); -} - -} // namespace -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SpaceToBatchND.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/SpaceToBatchND.cpp deleted file mode 100644 index 630cd38..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/SpaceToBatchND.cpp +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/SpaceToBatchND.h" -#include "kernels/Utils.h" - -#include "PALSpaceToBatchND.h" - -#include - -namespace luci_interpreter -{ -namespace kernels -{ -namespace -{ - -const int kInputMinDimensionNum = 3; -const int kInputMaxDimensionNum = 4; - -} // namespace - -SpaceToBatchND::SpaceToBatchND(const Tensor *input, const Tensor *block_shape, - const Tensor *paddings, Tensor *output) - : Kernel({input, block_shape, paddings}, {output}) -{ -} - -void SpaceToBatchND::configure() -{ - const auto *block_shape_data = block_shape()->data(); - const auto *paddings_data = paddings()->data(); - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() >= kInputMinDimensionNum); - LUCI_INTERPRETER_CHECK(input()->shape().num_dims() <= kInputMaxDimensionNum); - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - - int spatial_dims_num = input()->shape().num_dims() - 2; - - LUCI_INTERPRETER_CHECK(block_shape()->shape().num_dims() == 1); - LUCI_INTERPRETER_CHECK(block_shape()->shape().dim(0) == spatial_dims_num); - - LUCI_INTERPRETER_CHECK(paddings()->shape().num_dims() == 2); - LUCI_INTERPRETER_CHECK(paddings()->shape().dim(0) == spatial_dims_num); - LUCI_INTERPRETER_CHECK(paddings()->shape().dim(1) == 2); - - Shape output_shape = Shape(input()->shape().num_dims()); - int output_batch_size = input()->shape().dim(0); - for (int i = 0; i < spatial_dims_num; ++i) - { - int final_dim_size = - (input()->shape().dim(i + 1) + paddings_data[i * 2] + paddings_data[i * 2 + 1]); - LUCI_INTERPRETER_CHECK(final_dim_size % block_shape_data[i] == 0); - output_shape.dim(i + 1) = final_dim_size / block_shape_data[i]; - output_batch_size = output_batch_size * block_shape_data[i]; - } - output_shape.dim(0) = output_batch_size; - output_shape.dim(input()->shape().num_dims() - 1) = - input()->shape().dim(input()->shape().num_dims() - 1); - output()->resize(output_shape); -} - -void SpaceToBatchND::execute() const -{ - switch (input()->element_type()) - { - tflite::SpaceToBatchParams op_params; - case DataType::FLOAT32: - op_params.output_offset = 0; - luci_interpreter_pal::SpaceToBatchND( - op_params, getTensorShape(input()), getTensorData(input()), - getTensorShape(block_shape()), getTensorData(block_shape()), - getTensorShape(paddings()), getTensorData(paddings()), getTensorShape(output()), - getTensorData(output())); - break; - case DataType::U8: - op_params.output_offset = output()->zero_point(); - luci_interpreter_pal::SpaceToBatchND( - op_params, getTensorShape(input()), getTensorData(input()), - getTensorShape(block_shape()), getTensorData(block_shape()), - getTensorShape(paddings()), getTensorData(paddings()), getTensorShape(output()), - getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SpaceToDepth.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/SpaceToDepth.cpp deleted file mode 100644 index 7c29e8c..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/SpaceToDepth.cpp +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "SpaceToDepth.h" -#include "Utils.h" -#include "PALSpaceToDepth.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -SpaceToDepth::SpaceToDepth(const Tensor *input, Tensor *output, const SpaceToDepthParams ¶ms) - : KernelWithParams({input}, {output}, params) -{ -} - -void SpaceToDepth::configure() -{ - assert(input()->shape().num_dims() == 4); - assert(output()->element_type() == DataType::FLOAT32 || - output()->element_type() == DataType::U8 || output()->element_type() == DataType::S8 || - output()->element_type() == DataType::S32 || output()->element_type() == DataType::S64); - assert(input()->element_type() == output()->element_type()); - - const int block_size = params().block_size; - const int32_t input_height = input()->shape().dim(1); - const int32_t input_width = input()->shape().dim(2); - int32_t output_height = input_height / block_size; - int32_t output_width = input_width / block_size; - - assert(input_height == output_height * block_size); - assert(input_width == output_width * block_size); - - Shape output_shape(4); - output_shape.dim(0) = input()->shape().dim(0); - output_shape.dim(1) = output_height; - output_shape.dim(2) = output_width; - output_shape.dim(3) = input()->shape().dim(3) * block_size * block_size; - - output()->resize(output_shape); -} - -void SpaceToDepth::execute() const -{ - tflite::SpaceToDepthParams op_params{}; - op_params.block_size = params().block_size; - switch (input()->element_type()) - { - case DataType::FLOAT32: - luci_interpreter_pal::SpaceToDepth(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - case DataType::U8: - luci_interpreter_pal::SpaceToDepth(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Split.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Split.cpp deleted file mode 100644 index 1a563f3..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Split.cpp +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Split.h" - -#include "Utils.h" - -#include "PALSplit.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -Split::Split(const Tensor *axis, const Tensor *input, std::vector outputs) - : Kernel({axis, input}, std::move(outputs)) -{ -} - -void Split::configure() -{ - assert(axis()->shape().num_elements() == 1); - _axis_value = getTensorData(axis())[0]; - if (_axis_value < 0) - _axis_value += input()->shape().num_dims(); - assert(_axis_value >= 0 && _axis_value < input()->shape().num_dims()); - - const int32_t input_size = input()->shape().dim(_axis_value); - assert(input_size % _outputs.size() == 0); - const int32_t slice_size = input_size / _outputs.size(); - - Shape output_shape = input()->shape(); - output_shape.dim(_axis_value) = slice_size; - for (Tensor *output : _outputs) - { - output->resize(output_shape); - } -} - -void Split::execute() const -{ - tflite::SplitParams params{}; - params.num_split = _outputs.size(); - params.axis = _axis_value; - -#define TF_LITE_SPLIT(scalar) \ - { \ - VectorOfTensors all_outputs(_outputs); \ - luci_interpreter_pal::Split(params, getTensorShape(input()), getTensorData(input()), \ - all_outputs.shapes(), all_outputs.data()); \ - } - - switch (input()->element_type()) - { - case DataType::FLOAT32: - TF_LITE_SPLIT(float); - break; - case DataType::U8: - TF_LITE_SPLIT(uint8_t); - break; - default: - throw std::runtime_error("Unsupported type."); - } -#undef TF_LITE_SPLIT -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SplitV.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/SplitV.cpp deleted file mode 100644 index aa68208..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/SplitV.cpp +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "SplitV.h" - -#include "Utils.h" - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -SplitV::SplitV(const Tensor *input, const Tensor *size_splits, const Tensor *axis, - std::vector outputs) - : Kernel({input, size_splits, axis}, std::move(outputs)) -{ -} - -void SplitV::configure() -{ - assert(axis()->shape().num_elements() == 1); - _axis_value = getTensorData(axis())[0]; - if (_axis_value < 0) - _axis_value += input()->shape().num_dims(); - assert(_axis_value >= 0 && _axis_value < input()->shape().num_dims()); - - auto num_split = static_cast(_outputs.size()); - auto sizes_data = getTensorData(size_splits()); - - assert(size_splits()->shape().num_dims() == 1); - - int32_t sum = 0; - const auto num_dims_size_spits = size_splits()->shape().dim(0); - int32_t count_neg_dim = 0; - - for (int32_t i = 0; i < num_dims_size_spits - 1; ++i) - { - if (sizes_data[i] != -1) - { - sum += sizes_data[i]; - } - else - { - count_neg_dim++; - } - } - assert(count_neg_dim < 2); - assert(size_splits()->shape().num_elements() == num_split); - - auto output_shape = input()->shape(); - for (int32_t i = 0; i < num_split; ++i) - { - if (sizes_data[i] == -1) - { - output_shape.dim(_axis_value) = input()->shape().dim(_axis_value) - sum; - } - else - { - output_shape.dim(_axis_value) = sizes_data[i]; - } - _outputs[i]->resize(output_shape); - } -} - -void SplitV::execute() const -{ - tflite::SplitParams params{}; - params.num_split = _outputs.size(); - params.axis = _axis_value; - -#define TF_LITE_SPLIT(scalar) \ - { \ - VectorOfTensors all_outputs(_outputs); \ - tflite::optimized_ops::Split(params, getTensorShape(input()), getTensorData(input()), \ - all_outputs.shapes(), all_outputs.data()); \ - } - - switch (input()->element_type()) - { - case DataType::FLOAT32: - TF_LITE_SPLIT(float); - break; - case DataType::U8: - TF_LITE_SPLIT(uint8_t); - break; - case DataType::S16: - TF_LITE_SPLIT(int16_t); - break; - default: - throw std::runtime_error("Unsupported type."); - } -#undef TF_LITE_SPLIT -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Sqrt.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Sqrt.cpp deleted file mode 100644 index 46e9fc9..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Sqrt.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Sqrt.h" -#include "kernels/Utils.h" - -#include -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Sqrt::Sqrt(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Sqrt::configure() -{ - if (input()->element_type() != output()->element_type()) - { - throw std::runtime_error("Input/output tensor data type mismatch."); - } - output()->resize(input()->shape()); -} - -void Sqrt::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Sqrt::evalFloat() const -{ - auto in = getTensorData(input()); - auto out = getTensorData(output()); - auto size = getTensorShape(input()).FlatSize(); - for (auto i = in; i != in + size; ++i) - { - *out = std::sqrt(*i); - ++out; - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Square.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Square.cpp deleted file mode 100644 index bc71905..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Square.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Square.h" -#include "kernels/Utils.h" - -#include -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Square::Square(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Square::configure() -{ - if (input()->element_type() != output()->element_type()) - { - throw std::runtime_error("Input/output tensor data type mismatch."); - } - output()->resize(input()->shape()); -} - -void Square::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Square::evalFloat() const -{ - auto in = getTensorData(input()); - auto out = getTensorData(output()); - auto size = getTensorShape(input()).FlatSize(); - for (auto i = in; i != in + size; ++i) - { - *out = (*i) * (*i); - ++out; - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SquaredDifference.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/SquaredDifference.cpp deleted file mode 100644 index 3bafeba..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/SquaredDifference.cpp +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2018 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/SquaredDifference.h" - -#include "kernels/Utils.h" - -#include "kernels/BinaryOpCommon.h" - -namespace luci_interpreter -{ -namespace kernels -{ - -SquaredDifference::SquaredDifference(const Tensor *input1, const Tensor *input2, Tensor *output) - : Kernel({input1, input2}, {output}) -{ -} - -void SquaredDifference::configure() -{ - LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()) - LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()) - output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); -} - -void SquaredDifference::execute() const -{ - switch (input1()->element_type()) - { - case DataType::FLOAT32: - evalSquaredDifference(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -template inline void SquaredDifference::evalSquaredDifference() const -{ - BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output()), [](T x, T y) { - const T difference = x - y; - return difference * difference; - }); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Squeeze.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Squeeze.cpp deleted file mode 100644 index 4a75518..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Squeeze.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2018 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Squeeze.h" - -#include "kernels/Utils.h" - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Squeeze::Squeeze(const Tensor *input, Tensor *output, const SqueezeParams ¶ms) - : KernelWithParams({input}, {output}, params) -{ -} - -void Squeeze::configure() -{ - int input_num_dims = input()->shape().num_dims(); - int num_squeeze_dims = params().squeeze_dims.size(); - assert(input_num_dims <= 8); - bool should_squeeze[8] = {false}; - int num_squeezed_dims = 0; - if (num_squeeze_dims == 0) - { - for (int idx = 0; idx < input_num_dims; ++idx) - { - if (input()->shape().dim(idx) == 1) - { - should_squeeze[idx] = true; - ++num_squeezed_dims; - } - } - } - else - { - for (int idx = 0; idx < num_squeeze_dims; ++idx) - { - int current = params().squeeze_dims[idx] < 0 ? params().squeeze_dims[idx] + input_num_dims - : params().squeeze_dims[idx]; - assert(current >= 0 && current < input_num_dims && input()->shape().dim(current) == 1); - if (!should_squeeze[current]) - ++num_squeezed_dims; - should_squeeze[current] = true; - } - } - Shape output_shape(input_num_dims - num_squeezed_dims); - for (int in_idx = 0, out_idx = 0; in_idx < input_num_dims; ++in_idx) - { - if (!should_squeeze[in_idx]) - { - output_shape.dim(out_idx++) = input()->shape().dim(in_idx); - } - } - output()->resize(output_shape); -} - -void Squeeze::execute() const -{ - assert(input()->shape().num_elements() == output()->shape().num_elements()); - - const auto *input_data = input()->data(); - auto *output_data = output()->data(); - std::memcpy(output_data, input_data, - getDataTypeSize(input()->element_type()) * input()->shape().num_elements()); -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/StridedSlice.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/StridedSlice.cpp deleted file mode 100644 index a8730d8..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/StridedSlice.cpp +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2017 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/StridedSlice.h" - -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -StridedSlice::StridedSlice(const Tensor *input, const Tensor *begin, const Tensor *end, - const Tensor *strides, Tensor *output, const StridedSliceParams ¶ms) - : KernelWithParams({input, begin, end, strides}, {output}, params) -{ -} - -void StridedSlice::configure() -{ - assert(begin()->shape().num_dims() == 1); - assert(end()->shape().num_dims() == 1); - assert(strides()->shape().num_dims() == 1); - assert(input()->element_type() == output()->element_type()); - assert(begin()->element_type() == DataType::S32); - assert(end()->element_type() == DataType::S32); - assert(strides()->element_type() == DataType::S32); - assert(input()->shape().num_dims() <= 4); - if (params().ellipsis_mask != 0) - { - throw std::runtime_error("ellipsis_mask is not implemented yet."); - } - if (params().new_axis_mask != 0) - { - throw std::runtime_error("new_axis_mask is not implemented yet."); - } - if (input()->element_type() == DataType::U8) - { - assert(input()->scale() == output()->scale()); - assert(input()->zero_point() == output()->zero_point()); - } - tflite::StridedSliceParams op_params{}; - op_params.start_indices_count = input()->shape().num_dims(); - op_params.stop_indices_count = input()->shape().num_dims(); - op_params.strides_count = input()->shape().num_dims(); - - for (int i = 0; i < input()->shape().num_dims(); i++) - { - op_params.start_indices[i] = getTensorData(begin())[i]; - op_params.stop_indices[i] = getTensorData(end())[i]; - op_params.strides[i] = getTensorData(strides())[i]; - } - op_params.begin_mask = params().begin_mask; - op_params.ellipsis_mask = 0; - op_params.end_mask = params().end_mask; - op_params.new_axis_mask = 0; - op_params.shrink_axis_mask = params().shrink_axis_mask; - std::vector output_shape_vector; - for (int i = 0; i < input()->shape().num_dims(); i++) - { - int idx = input()->shape().num_dims() - i - 1; - int32_t stride = getTensorData(strides())[idx]; - assert(stride != 0); - int32_t begin = ::tflite::strided_slice::StartForAxis(op_params, getTensorShape(input()), idx); - int32_t end = - ::tflite::strided_slice::StopForAxis(op_params, getTensorShape(input()), idx, begin); - - const bool shrink_axis = params().shrink_axis_mask & (1 << idx); - if (shrink_axis) - { - end = begin + 1; - } - - int32_t dim_shape = std::ceil((end - begin) / static_cast(stride)); - dim_shape = dim_shape < 0 ? 0 : dim_shape; - if (!shrink_axis) - { - output_shape_vector.push_back(dim_shape); - } - } - Shape output_shape = Shape(output_shape_vector.size()); - for (size_t i = 0; i < output_shape_vector.size(); i++) - { - output_shape.dim(i) = output_shape_vector[output_shape_vector.size() - i - 1]; - } - output()->resize(output_shape); -} - -void StridedSlice::execute() const -{ - tflite::StridedSliceParams op_params{}; - op_params.start_indices_count = input()->shape().num_dims(); - op_params.stop_indices_count = input()->shape().num_dims(); - op_params.strides_count = input()->shape().num_dims(); - - for (int i = 0; i < input()->shape().num_dims(); i++) - { - op_params.start_indices[i] = getTensorData(begin())[i]; - op_params.stop_indices[i] = getTensorData(end())[i]; - op_params.strides[i] = getTensorData(strides())[i]; - } - op_params.begin_mask = params().begin_mask; - op_params.ellipsis_mask = 0; - op_params.end_mask = params().end_mask; - op_params.new_axis_mask = 0; - op_params.shrink_axis_mask = params().shrink_axis_mask; - - switch (input()->element_type()) - { - case DataType::FLOAT32: - tflite::reference_ops::StridedSlice(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - case DataType::U8: - tflite::reference_ops::StridedSlice(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - case DataType::S32: - tflite::reference_ops::StridedSlice(op_params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Sub.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Sub.cpp deleted file mode 100644 index 24b6a72..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Sub.cpp +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2019 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Sub.h" -#include "kernels/Utils.h" - -#include "PALSub.h" - -#include - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Sub::Sub(const Tensor *input1, const Tensor *input2, Tensor *output, const SubParams ¶ms) - : KernelWithParams({input1, input2}, {output}, params) -{ -} - -void Sub::configure() -{ - LUCI_INTERPRETER_CHECK(!(input1()->element_type() != input2()->element_type())) - LUCI_INTERPRETER_CHECK(!(input1()->element_type() != output()->element_type())) - output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); -} - -void Sub::execute() const -{ - switch (input1()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::S64: - evalInteger(); - break; - case DataType::S32: - evalInteger(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Sub::evalFloat() const -{ - tflite::ArithmeticParams params{}; - fillArithmeticActivationRange(params, _params.activation); - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - tflite::reference_ops::BroadcastSubSlow( - params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), - getTensorData(input2()), getTensorShape(output()), getTensorData(output())); - } - else - { - luci_interpreter_pal::Sub(params, getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -template void Sub::evalInteger() const -{ - tflite::ArithmeticParams params{}; - fillArithmeticActivationRange(params, _params.activation); - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - tflite::reference_ops::BroadcastSubSlow( - params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), - getTensorData(input2()), getTensorShape(output()), getTensorData(output())); - } - else - { - tflite::reference_ops::Sub(params, getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -void Sub::evalQuantized() const -{ - const auto input1_scale = static_cast(input1()->scale()); - const auto input2_scale = static_cast(input2()->scale()); - const auto output_scale = static_cast(output()->scale()); - - const int left_shift = 20; - const double twice_max_input_scale = 2 * std::max(input1_scale, input2_scale); - const double real_input1_multiplier = input1_scale / twice_max_input_scale; - const double real_input2_multiplier = input2_scale / twice_max_input_scale; - const double real_output_multiplier = twice_max_input_scale / ((1 << left_shift) * output_scale); - - int32_t input1_multiplier{}, input2_multiplier{}, output_multiplier{}; - int input1_shift{}, input2_shift{}, output_shift{}; - quantizeMultiplierSmallerThanOneExp(real_input1_multiplier, &input1_multiplier, &input1_shift); - quantizeMultiplierSmallerThanOneExp(real_input2_multiplier, &input2_multiplier, &input2_shift); - quantizeMultiplierSmallerThanOneExp(real_output_multiplier, &output_multiplier, &output_shift); - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); - - tflite::ArithmeticParams params{}; - params.left_shift = left_shift; - // The kernel expects inputs' zero points to be negated. - params.input1_offset = -input1()->zero_point(); // Note the '-'. - params.input1_multiplier = input1_multiplier; - params.input1_shift = input1_shift; - params.input2_offset = -input2()->zero_point(); // Note the '-'. - params.input2_multiplier = input2_multiplier; - params.input2_shift = input2_shift; - params.output_offset = output()->zero_point(); - params.output_multiplier = output_multiplier; - params.output_shift = output_shift; - params.quantized_activation_min = activation_min; - params.quantized_activation_max = activation_max; - - const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( - getTensorShape(input1()), getTensorShape(input2()), ¶ms); - - if (need_broadcast) - { - tflite::reference_ops::BroadcastSubSlow( - params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), - getTensorData(input2()), getTensorShape(output()), getTensorData(output())); - } - else - { - tflite::reference_ops::Sub(params, getTensorShape(input1()), getTensorData(input1()), - getTensorShape(input2()), getTensorData(input2()), - getTensorShape(output()), getTensorData(output())); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Tanh.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Tanh.cpp deleted file mode 100644 index c4fa169..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Tanh.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Tanh.h" - -#include "kernels/Utils.h" - -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -Tanh::Tanh(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} - -void Tanh::configure() -{ - LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); - if (input()->element_type() == DataType::U8) - { - populateLookupTable(); - } - output()->resize(input()->shape()); -} - -void Tanh::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::U8: - evalQuantized(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void Tanh::evalFloat() const -{ - tflite::reference_ops::Tanh(getTensorShape(input()), getTensorData(input()), - getTensorShape(output()), getTensorData(output())); -} - -void Tanh::evalQuantized() const -{ - const int size = tflite::MatchingFlatSize(getTensorShape(input()), getTensorShape(output())); - uint8_t *output_data = getTensorData(output()); - const uint8_t *input_data = getTensorData(input()); - for (int i = 0; i < size; ++i) - { - output_data[i] = getTableValue(input_data[i]); - } -} - -void Tanh::populateLookupTable() -{ - const auto input_scale = static_cast(input()->scale()); - const auto input_zero_point = static_cast(input()->zero_point()); - const auto output_scale = static_cast(output()->scale()); - const auto output_zero_point = static_cast(output()->zero_point()); - const float inverse_scale = 1 / output_scale; - int32_t maxval = std::numeric_limits::max(); - int32_t minval = std::numeric_limits::min(); - for (int32_t val = minval; val <= maxval; ++val) - { - const float dequantized = input_scale * (val - input_zero_point); - const float transformed = std::tanh(dequantized); - const float rescaled = std::round(transformed * inverse_scale); - const int32_t quantized = static_cast(rescaled + output_zero_point); - setTableValue(static_cast(std::max(std::min(maxval, quantized), minval)), - static_cast(val)); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/TestUtils.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/TestUtils.cpp deleted file mode 100644 index 4d983ad..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/TestUtils.cpp +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2017 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/TestUtils.h" - -#include - -namespace luci_interpreter -{ -namespace kernels -{ -namespace testing -{ - -using ::testing::FloatNear; -using ::testing::Matcher; - -Tensor makeOutputTensor(DataType element_type) { return Tensor(element_type, {}, {}, ""); } - -Tensor makeOutputTensor(DataType element_type, float scale, int32_t zero_point) -{ - return Tensor(element_type, {}, {{scale}, {zero_point}}, ""); -} - -std::vector dequantizeTensorData(const Tensor &tensor) -{ - if (tensor.element_type() == DataType::U8) - { - std::vector data = extractTensorData(tensor); - return dequantize(data.data(), data.size(), tensor.scale(), tensor.zero_point()); - } - if (tensor.element_type() == DataType::S8) - { - std::vector data = extractTensorData(tensor); - return dequantize(data.data(), data.size(), tensor.scale(), tensor.zero_point()); - } - else if (tensor.element_type() == DataType::S16) - { - // S16 quantization is symmetric, so zero point should be zero. - for (auto zp : tensor.zero_points()) - { - (void)zp; - assert(zp == 0); - } - - std::vector data = extractTensorData(tensor); - if (tensor.scales().size() == 1) - { - return dequantize(data.data(), data.size(), tensor.scale(), 0); - } - - // quantize_dimension breaks shape into two parts: - // inner dimensions that contains continuous data with one quantization type - // outer dimensions that contains other dimensions - const Shape shape = tensor.shape(); - const int32_t quantized_dimension = tensor.quantized_dimension(); - assert(quantized_dimension < shape.num_dims()); - size_t outer_dims_size = 1; - int32_t quant_dim_size = shape.dim(quantized_dimension); - size_t inner_dims_size = 1; - assert(quant_dim_size == tensor.scales().size()); - - for (int i = 0; i < quantized_dimension; ++i) - outer_dims_size *= shape.dim(i); - for (int i = quantized_dimension + 1; i < shape.num_dims(); ++i) - inner_dims_size *= shape.dim(i); - - assert(shape.num_elements() == outer_dims_size * quant_dim_size * inner_dims_size); - - std::vector dequantized_data; - dequantized_data.reserve(shape.num_elements()); - for (size_t outer_it = 0; outer_it < outer_dims_size; ++outer_it) - for (int32_t channel = 0; channel < quant_dim_size; ++channel) - { - float scale = tensor.scales()[channel]; - size_t offset = inner_dims_size * (quant_dim_size * outer_it + channel); - std::vector part_dequantized_data = - dequantize(data.data() + offset, inner_dims_size, scale, 0); - dequantized_data.insert(dequantized_data.end(), part_dequantized_data.begin(), - part_dequantized_data.end()); - } - return dequantized_data; - } - else - { - throw std::runtime_error("Unsupported type."); - } -} - -Matcher> FloatArrayNear(const std::vector &values, float max_abs_error) -{ - std::vector> matchers; - matchers.reserve(values.size()); - for (const float v : values) - { - matchers.emplace_back(FloatNear(v, max_abs_error)); - } - return ElementsAreArray(matchers); -} - -std::vector extractTensorShape(const Tensor &tensor) -{ - std::vector result; - int dims = tensor.shape().num_dims(); - for (int i = 0; i < dims; i++) - { - result.push_back(tensor.shape().dim(i)); - } - return result; -} - -} // namespace testing -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Transpose.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Transpose.cpp deleted file mode 100644 index 802d872..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Transpose.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Transpose.h" - -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Transpose::Transpose(const Tensor *input, const Tensor *perm, Tensor *output) - : Kernel({input, perm}, {output}) -{ -} - -void Transpose::configure() -{ - // Transpose op only supports 1D-4D input arrays. - int dims = input()->shape().num_dims(); - const int32_t *perm_data = getTensorData(perm()); - - assert(input()->shape().num_dims() <= 4); - assert(input()->element_type() == output()->element_type()); - - assert(perm()->shape().num_dims() == 1); - assert(perm()->shape().dim(0) == dims); - - Shape output_shape(dims); - for (int i = 0; i < dims; i++) - { - assert(perm_data[i] < dims && perm_data[i] >= 0); - output_shape.dim(i) = input()->shape().dim(perm_data[i]); - } - - output()->resize(output_shape); -} - -void Transpose::execute() const -{ - tflite::TransposeParams params{}; - const int32_t *perm_data = getTensorData(perm()); - const int32_t size = perm()->shape().dim(0); - params.perm_count = size; - for (int i = 0; i < size; i++) - params.perm[i] = perm_data[i]; - switch (input()->element_type()) - { - case DataType::FLOAT32: - tflite::reference_ops::Transpose(params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - case DataType::U8: - tflite::reference_ops::Transpose(params, getTensorShape(input()), - getTensorData(input()), getTensorShape(output()), - getTensorData(output())); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/TransposeConv.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/TransposeConv.cpp deleted file mode 100644 index 1b5f9d9..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/TransposeConv.cpp +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2017 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/TransposeConv.h" - -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -TransposeConv::TransposeConv(const Tensor *output_shape, const Tensor *filter, const Tensor *input, - const Tensor *bias, Tensor *output, Tensor *scratch_tensor, - const TransposeConvParams ¶ms) - : KernelWithParams({output_shape, filter, input, bias}, - {output, scratch_tensor}, params) -{ -} - -TransposeConv::~TransposeConv() -{ - // Define destructor here, to delete vector of qunatized multipliers properly -} - -void TransposeConv::configure() -{ - assert(output_shape()->shape().num_dims() == 1); - assert(input()->shape().num_dims() == 4); - assert(filter()->shape().num_dims() == 4); - assert(input()->element_type() == DataType::FLOAT32 || input()->element_type() == DataType::U8 || - input()->element_type() == DataType::S16); - assert(input()->element_type() == output()->element_type()); - assert(input()->shape().dim(3) == filter()->shape().dim(3)); - - const int num_dims = output_shape()->shape().dim(0); - Shape out_shape(num_dims); - const auto *shape_data = getTensorData(output_shape()); - for (int i = 0; i < num_dims; i++) - out_shape.dim(i) = shape_data[i]; - output()->resize(out_shape); - - const int32_t filter_height = filter()->shape().dim(1); - const int32_t filter_width = filter()->shape().dim(2); - const int32_t output_height = out_shape.dim(1); - const int32_t output_width = out_shape.dim(2); - - const int32_t unused_output_height = - computeOutputSize(params().padding, output_height, filter_height, params().stride_height, 1); - const int32_t unused_output_width = - computeOutputSize(params().padding, output_width, filter_width, params().stride_width, 1); - - _padding_height = - computePadding(params().stride_height, 1, output_height, filter_height, unused_output_height); - _padding_width = - computePadding(params().stride_width, 1, output_width, filter_width, unused_output_width); - - if (input()->element_type() == DataType::U8 || input()->element_type() == DataType::S16) - { - auto scratch_tensor = getOutputTensors()[1]; - scratch_tensor->resize(output()->shape()); - const std::vector real_multipliers = - getQuantizedConvolutionMultiplers(input()->scale(), filter()->scales(), output()->scale()); - - _quant_multipliers = quantizeMultipliers(real_multipliers); - } - else - { - auto scratch_tensor = getOutputTensors()[1]; - scratch_tensor->set_allocatable(false); - } -} - -void TransposeConv::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - evalFloat(); - break; - case DataType::U8: - if (filter()->scales().size() == 1) - { - evalQuantized(); - } - else if (filter()->scales().size() > 1) - { - LUCI_INTERPRETER_CHECK(filter()->shape().num_dims() == 4); - LUCI_INTERPRETER_CHECK(filter()->scales().size() == - static_cast(filter()->shape().dim(0))); - evalQuantizedPerChannel(); - } - break; - case DataType::S16: - evalQuantizedS16(); - break; - default: - throw std::runtime_error("Unsupported type."); - } -} - -void TransposeConv::evalFloat() const -{ - tflite::ConvParams op_params{}; - op_params.padding_type = tflite::PaddingType::kSame; - op_params.padding_values.height = _padding_height; - op_params.padding_values.width = _padding_width; - op_params.stride_height = params().stride_height; - op_params.stride_width = params().stride_width; - tflite::reference_ops::TransposeConv(op_params, // - getTensorShape(input()), getTensorData(input()), // - getTensorShape(filter()), getTensorData(filter()), // - getTensorShape(bias()), getTensorData(bias()), // - getTensorShape(output()), getTensorData(output()), // - tflite::RuntimeShape(), nullptr); -} - -void TransposeConv::evalQuantized() const -{ - tflite::ConvParams op_params{}; - op_params.padding_type = tflite::PaddingType::kSame; - op_params.padding_values.height = _padding_height; - op_params.padding_values.width = _padding_width; - op_params.stride_height = params().stride_height; - op_params.stride_width = params().stride_width; - // The kernel expects input and filter zero points to be negated. - op_params.input_offset = -input()->zero_point(); // Note the '-'. - op_params.weights_offset = -filter()->zero_point(); // Note the '-'. - op_params.output_offset = output()->zero_point(); - op_params.output_multiplier = _quant_multipliers[0].multiplier; - op_params.output_shift = _quant_multipliers[0].shift; - op_params.quantized_activation_min = std::numeric_limits::min(); - op_params.quantized_activation_max = std::numeric_limits::max(); - - auto scratch_tensor = getOutputTensors()[1]; - - tflite::reference_ops::TransposeConv(op_params, // - getTensorShape(input()), getTensorData(input()), // - getTensorShape(filter()), getTensorData(filter()), // - getTensorShape(bias()), getTensorData(bias()), // - getTensorShape(output()), getTensorData(output()), // - tflite::RuntimeShape(), nullptr, // - getTensorData(scratch_tensor)); -} - -void TransposeConv::evalQuantizedPerChannel() const -{ - const auto *input_data = getTensorData(input()); - const auto *filter_data = getTensorData(filter()); - const auto *bias_data = getTensorData(bias()); - auto *output_data = getTensorData(output()); - - auto scratch_tensor = getOutputTensors()[1]; - auto *scratch_data = getTensorData(scratch_tensor); - - const Shape &input_shape = input()->shape(); - const Shape &filter_shape = filter()->shape(); - const Shape &output_shape = output()->shape(); - - const int32_t batches = input_shape.dim(0); - const int32_t input_height = input_shape.dim(1); - const int32_t input_width = input_shape.dim(2); - const int32_t input_depth = input_shape.dim(3); - const int32_t output_depth = filter_shape.dim(0); - const int32_t filter_height = filter_shape.dim(1); - const int32_t filter_width = filter_shape.dim(2); - const int32_t output_height = output_shape.dim(1); - const int32_t output_width = output_shape.dim(2); - - const int32_t stride_height = _params.stride_height; - const int32_t stride_width = _params.stride_width; - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(Activation::NONE, output(), &activation_min, &activation_max); - - std::memset(scratch_data, 0, scratch_tensor->shape().num_elements() * sizeof(int32_t)); - - BroadcastableWrapper output_multipliers(_quant_multipliers); - for (int32_t batch = 0; batch < batches; ++batch) - { - for (int32_t in_y = 0; in_y < input_height; ++in_y) - { - for (int32_t in_x = 0; in_x < input_width; ++in_x) - { - for (int32_t in_c = 0; in_c < input_depth; ++in_c) - { - const int32_t out_y_origin = in_y * stride_height - _padding_height; - const int32_t out_x_origin = in_x * stride_width - _padding_width; - for (int32_t filter_y = 0; filter_y < filter_height; ++filter_y) - { - for (int32_t filter_x = 0; filter_x < filter_width; ++filter_x) - { - const int32_t out_x = out_x_origin + filter_x; - const int32_t out_y = out_y_origin + filter_y; - if ((out_y >= 0 && out_y < output_height) && (out_x >= 0 && out_x < output_width)) - { - for (int32_t out_c = 0; out_c < output_depth; ++out_c) - { - const uint8_t input_val = - input_data[calcOffset(input_shape, batch, in_y, in_x, in_c)]; - const uint8_t filter_val = - filter_data[calcOffset(filter_shape, out_c, filter_y, filter_x, in_c)]; - scratch_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] += - static_cast(input_val - input()->zero_point()) * - static_cast(filter_val - filter()->zero_points()[out_c]); - } - } - } - } - } - } - } - for (int32_t out_y = 0; out_y < output_height; ++out_y) - { - for (int32_t out_x = 0; out_x < output_width; ++out_x) - { - for (int32_t out_c = 0; out_c < output_depth; ++out_c) - { - int32_t acc = scratch_data[calcOffset(output_shape, batch, out_y, out_x, out_c)]; - if (bias_data) - { - acc += bias_data[out_c]; - } - - int32_t scaled_acc = tflite::MultiplyByQuantizedMultiplier( - acc, output_multipliers[out_c].multiplier, output_multipliers[out_c].shift); - - scaled_acc += output()->zero_point(); - scaled_acc = std::max(scaled_acc, activation_min); - scaled_acc = std::min(scaled_acc, activation_max); - - output_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] = scaled_acc; - } - } - } - } -} - -void TransposeConv::evalQuantizedS16() const -{ - const auto *input_data = getTensorData(input()); - const auto *filter_data = getTensorData(filter()); - const auto *bias_data = getTensorData(bias()); - auto *output_data = getTensorData(output()); - - auto scratch_tensor = getOutputTensors()[1]; - auto *scratch_data = getTensorData(scratch_tensor); - - const Shape &input_shape = input()->shape(); - const Shape &filter_shape = filter()->shape(); - const Shape &output_shape = output()->shape(); - - const int32_t batches = input_shape.dim(0); - const int32_t input_height = input_shape.dim(1); - const int32_t input_width = input_shape.dim(2); - const int32_t input_depth = input_shape.dim(3); - const int32_t output_depth = filter_shape.dim(0); - const int32_t filter_height = filter_shape.dim(1); - const int32_t filter_width = filter_shape.dim(2); - const int32_t output_height = output_shape.dim(1); - const int32_t output_width = output_shape.dim(2); - - const int32_t stride_height = _params.stride_height; - const int32_t stride_width = _params.stride_width; - - int32_t activation_min{}; - int32_t activation_max{}; - calculateActivationRangeQuantized(Activation::NONE, output(), &activation_min, &activation_max); - - std::memset(scratch_data, 0, scratch_tensor->shape().num_elements() * sizeof(int64_t)); - - BroadcastableWrapper output_multipliers(_quant_multipliers); - for (int32_t batch = 0; batch < batches; ++batch) - { - for (int32_t in_y = 0; in_y < input_height; ++in_y) - { - for (int32_t in_x = 0; in_x < input_width; ++in_x) - { - for (int32_t in_c = 0; in_c < input_depth; ++in_c) - { - const int32_t out_y_origin = in_y * stride_height - _padding_height; - const int32_t out_x_origin = in_x * stride_width - _padding_width; - for (int32_t filter_y = 0; filter_y < filter_height; ++filter_y) - { - for (int32_t filter_x = 0; filter_x < filter_width; ++filter_x) - { - const int32_t out_x = out_x_origin + filter_x; - const int32_t out_y = out_y_origin + filter_y; - if ((out_y >= 0 && out_y < output_height) && (out_x >= 0 && out_x < output_width)) - { - for (int32_t out_c = 0; out_c < output_depth; ++out_c) - { - const int16_t input_val = - input_data[calcOffset(input_shape, batch, in_y, in_x, in_c)]; - const int16_t filter_val = - filter_data[calcOffset(filter_shape, out_c, filter_y, filter_x, in_c)]; - scratch_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] += - static_cast(input_val) * static_cast(filter_val); - } - } - } - } - } - } - } - for (int32_t out_y = 0; out_y < output_height; ++out_y) - { - for (int32_t out_x = 0; out_x < output_width; ++out_x) - { - for (int32_t out_c = 0; out_c < output_depth; ++out_c) - { - int64_t acc = scratch_data[calcOffset(output_shape, batch, out_y, out_x, out_c)]; - if (bias_data) - { - acc += bias_data[out_c]; - } - int32_t scaled_acc = tflite::MultiplyByQuantizedMultiplier( - acc, output_multipliers[out_c].multiplier, output_multipliers[out_c].shift); - - scaled_acc = std::max(scaled_acc, activation_min); - scaled_acc = std::min(scaled_acc, activation_max); - - output_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] = scaled_acc; - } - } - } - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Unpack.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Unpack.cpp deleted file mode 100644 index 9127241..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Unpack.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Unpack.h" - -#include "kernels/Utils.h" - -#include - -#include - -namespace luci_interpreter -{ - -namespace kernels -{ - -Unpack::Unpack(const Tensor *input, std::vector outputs, const UnpackParams ¶ms) - : KernelWithParams({input}, std::move(outputs), params) -{ -} - -void Unpack::configure() -{ - const Shape &input_shape = input()->shape(); - - int axis = _params.axis; - if (axis < 0) - axis += input()->shape().num_dims(); - assert(axis >= 0 && axis < input_shape.num_dims()); - - Shape output_shape(input_shape.num_dims() - 1); - int out_index = 0; - for (int in_index = 0; in_index < input_shape.num_dims(); ++in_index) - { - if (in_index != axis) - output_shape.dim(out_index++) = input_shape.dim(in_index); - } - - for (Tensor *output : _outputs) - { - assert(output->element_type() == input()->element_type()); - output->resize(output_shape); - } -} - -template void Unpack::executeImpl() const -{ - tflite::UnpackParams params{}; - params.axis = _params.axis; - params.num_split = _outputs.size(); - VectorOfTensors all_outputs(_outputs); - tflite::reference_ops::Unpack(params, getTensorShape(input()), getTensorData(input()), - **all_outputs.shapes(), all_outputs.data()); -} - -void Unpack::execute() const -{ - switch (input()->element_type()) - { - case DataType::FLOAT32: - return executeImpl(); - case DataType::U8: - return executeImpl(); - default: - throw std::runtime_error("Unsupported type."); - } -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Utils.cpp b/compiler/luci-micro/luci-interpreter/src/kernels/Utils.cpp deleted file mode 100644 index 5d8e5db..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Utils.cpp +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2017 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "kernels/Utils.h" - -#include -#include -#include -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -template -void calculateActivationRange(Activation activation, T *activation_min, T *activation_max) -{ - switch (activation) - { - case Activation::NONE: - *activation_min = std::numeric_limits::lowest(); - *activation_max = std::numeric_limits::max(); - break; - case Activation::RELU: - *activation_min = 0; - *activation_max = std::numeric_limits::max(); - break; - case Activation::RELU_N1_TO_1: - *activation_min = -1; - *activation_max = 1; - break; - case Activation::RELU6: - *activation_min = 0; - *activation_max = 6; - break; - default: - throw std::runtime_error("Unsupported activation."); - } -} - -template void calculateActivationRange(Activation activation, float *activation_min, - float *activation_max); -template void calculateActivationRange(Activation activation, int32_t *activation_min, - int32_t *activation_max); -template void calculateActivationRange(Activation activation, int64_t *activation_min, - int64_t *activation_max); - -static void calculateActivationRangeQuantizedImpl(Activation activation, int32_t qmin, int32_t qmax, - const Tensor *output, int32_t *activation_min, - int32_t *activation_max) -{ - const float scale = output->scale(); - const int32_t zero_point = output->zero_point(); - - auto quantize = [scale, zero_point](float x) { - return zero_point + static_cast(std::round(x / scale)); - }; - - switch (activation) - { - case Activation::NONE: - case Activation::TANH: - *activation_min = qmin; - *activation_max = qmax; - break; - case Activation::RELU: - *activation_min = std::max(qmin, quantize(0.0f)); - *activation_max = qmax; - break; - case Activation::RELU_N1_TO_1: - *activation_min = std::max(qmin, quantize(-1.0f)); - *activation_max = std::min(qmax, quantize(1.0f)); - break; - case Activation::RELU6: - *activation_min = std::max(qmin, quantize(0.0f)); - *activation_max = std::min(qmax, quantize(6.0f)); - break; - default: - throw std::runtime_error("Unsupported activation."); - } -} - -void calculateActivationRangeQuantized(Activation activation, const Tensor *output, - int32_t *activation_min, int32_t *activation_max) -{ - assert(output->zero_points().size() == 1); - int32_t qmin{}; - int32_t qmax{}; - switch (output->element_type()) - { - case DataType::U8: - qmin = 0; - qmax = std::numeric_limits::max(); - break; - case DataType::S8: - qmin = -std::numeric_limits::max(); - qmax = std::numeric_limits::max(); - break; - case DataType::S16: - // For now, assume that signed int16 type implies signed symmetric quantization. - assert(output->zero_point() == 0); - qmin = -std::numeric_limits::max(); - qmax = std::numeric_limits::max(); - break; - default: - throw std::runtime_error("Unsupported type."); - } - - calculateActivationRangeQuantizedImpl(activation, qmin, qmax, output, activation_min, - activation_max); -} - -void quantizeMultiplier(double double_multiplier, int32_t *quantized_multiplier, int *shift) -{ - if (double_multiplier == 0.0) - { - *quantized_multiplier = 0; - *shift = 0; - return; - } - - const double q = std::frexp(double_multiplier, shift); - auto q_fixed = static_cast(std::round(q * (INT64_C(1) << 31))); - - if (q_fixed == (INT64_C(1) << 31)) - { - q_fixed /= 2; - ++*shift; - } - assert(q_fixed <= std::numeric_limits::max()); - // A shift amount smaller than -31 would cause all bits to be shifted out - // and thus all results would be zero. We implement that instead with - // q_fixed==0, so as to avoid hitting issues with right-shift - // operations with shift amounts greater than 31. Note that this happens - // roughly when abs(double_multiplier) < 2^-31 and the present handling means - // that we're effectively flushing tiny double_multiplier's to zero. - // We could conceivably handle values in the range (roughly) [32, 63] - // as 'denormals' i.e. (shift==0, q_fixed < 2^30). In that point of view - // the present handling is just doing 'flush denormals to zero'. We could - // reconsider and actually generate nonzero denormals if a need arises. - if (*shift < -31) - { - *shift = 0; - q_fixed = 0; - } - *quantized_multiplier = static_cast(q_fixed); -} - -void quantizeMultiplierSmallerThanOneExp(double double_multiplier, int32_t *quantized_multiplier, - int *left_shift) -{ - assert(double_multiplier < 1.0); - assert(double_multiplier > 0.0); - int shift; - quantizeMultiplier(double_multiplier, quantized_multiplier, &shift); - assert(shift <= 0); - *left_shift = shift; -} - -Shape calculateShapeForBroadcast(const Shape &input1_shape, const Shape &input2_shape) -{ - const int num_input1_dims = input1_shape.num_dims(); - const int num_input2_dims = input2_shape.num_dims(); - const int num_out_dims = std::max(num_input1_dims, num_input2_dims); - Shape output_shape(num_out_dims); - - for (int i = 0; i < num_out_dims; ++i) - { - const int32_t input1_dim = i < num_input1_dims ? input1_shape.dim(num_input1_dims - i - 1) : 1; - const int32_t input2_dim = i < num_input2_dims ? input2_shape.dim(num_input2_dims - i - 1) : 1; - - bool need_broadcast = input1_dim != input2_dim; - bool can_broadcast = input1_dim == 1 || input2_dim == 1; - LUCI_INTERPRETER_CHECK(!need_broadcast || can_broadcast); - - output_shape.dim(num_out_dims - i - 1) = std::max(input1_dim, input2_dim); - } - - return output_shape; -} - -} // namespace kernels -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Utils.h b/compiler/luci-micro/luci-interpreter/src/kernels/Utils.h deleted file mode 100644 index ebeb20e..0000000 --- a/compiler/luci-micro/luci-interpreter/src/kernels/Utils.h +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * Copyright 2017 The TensorFlow Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_KERNELS_UTILS_H -#define LUCI_INTERPRETER_KERNELS_UTILS_H - -#include "core/KernelParams.h" -#include "luci_interpreter/core/Tensor.h" - -#include - -#include -#include -#include - -namespace luci_interpreter -{ -namespace kernels -{ - -#define LUCI_INTERPRETER_CHECK(cond) \ - if (!(cond)) \ - throw std::runtime_error(std::string(__FILE__) + ":" + std::to_string(__LINE__) + +"(" + \ - std::string(#cond) + ") was not true."); - -inline int32_t computePadding(int32_t stride, int32_t dilation_rate, int32_t in_size, - int32_t filter_size, int32_t out_size) -{ - const int32_t effective_filter_size = (filter_size - 1) * dilation_rate + 1; - const int32_t padding = ((out_size - 1) * stride + effective_filter_size - in_size) / 2; - return padding > 0 ? padding : 0; -} - -inline int32_t computePaddingWithOffset(int32_t stride, int32_t dilation_rate, int32_t in_size, - int32_t filter_size, int32_t out_size, int32_t *offset) -{ - int32_t effective_filter_size = (filter_size - 1) * dilation_rate + 1; - int32_t total_padding = ((out_size - 1) * stride + effective_filter_size - in_size); - total_padding = total_padding > 0 ? total_padding : 0; - *offset = total_padding % 2; - return total_padding / 2; -} - -inline int32_t computeOutputSize(Padding padding, int32_t image_size, int32_t filter_size, - int32_t stride, int32_t dilation_rate = 1) -{ - const int32_t effective_filter_size = (filter_size - 1) * dilation_rate + 1; - switch (padding) - { - case Padding::SAME: - return (image_size + stride - 1) / stride; - case Padding::VALID: - return (image_size + stride - effective_filter_size) / stride; - default: - assert(false); - return 0; - } -} - -inline int32_t calcOffset(const Shape &shape, int32_t d0, int32_t d1, int32_t d2, int32_t d3) -{ - return ((d0 * shape.dim(1) + d1) * shape.dim(2) + d2) * shape.dim(3) + d3; -} - -template -void calculateActivationRange(Activation activation, T *activation_min, T *activation_max); - -void calculateActivationRangeQuantized(Activation activation, const Tensor *output, - int32_t *activation_min, int32_t *activation_max); - -template constexpr bool one_of_types() { return false; } - -// Checks if T is equal to one of {U,Other} types -template constexpr bool one_of_types() -{ - return std::is_same::value || one_of_types(); -} - -/** - * Fills activation min and max parameters depending on given data type and activation - * - * T is a template parameter, so after optimization this code left with only required if case - * - * @tparam T data type of arithmetic operation output tensor - * @param params tflite params to fill - * @param activation luci_interpreter::Activation of arithmetic operation - */ -template -void fillArithmeticActivationRange(tflite::ArithmeticParams &p, Activation act) -{ - static_assert(one_of_types(), "Unsupported dtype"); - - if (std::is_same::value) - calculateActivationRange(act, &p.float_activation_min, &p.float_activation_max); - if (std::is_same::value) - calculateActivationRange(act, &p.quantized_activation_min, &p.quantized_activation_max); - else - calculateActivationRange(act, &p.int64_activation_min, &p.int64_activation_max); -} - -// Decompose a double multiplier into a Q0.31 int32 representation of its -// significand, and shift representation of its exponent. -// -// Handles an arbitrary positive multiplier. The 'shift' output-value is -// basically the 'floating-point exponent' of the multiplier: -// Negative for a right-shift (when the multiplier is <1), positive for a -// left-shift (when the multiplier is >1) -void quantizeMultiplier(double double_multiplier, int32_t *quantized_multiplier, int *shift); - -// Decompose a double multiplier into a Q0.31 int32 representation of its -// significand, and shift representation of NEGATIVE its exponent --- -// this is intended as a RIGHT-shift. -// -// Restricted to the case where the multiplier < 1 (and non-negative). -void quantizeMultiplierSmallerThanOneExp(double double_multiplier, int32_t *quantized_multiplier, - int *left_shift); - -Shape calculateShapeForBroadcast(const Shape &input1_shape, const Shape &input2_shape); - -inline double getQuantizedConvolutionMultipler(float input_scale, float filter_scale, - float output_scale) -{ - const double input_product_scale = static_cast(input_scale * filter_scale); - LUCI_INTERPRETER_CHECK(input_product_scale >= 0); - return input_product_scale / static_cast(output_scale); -} - -// TODO rename getQuantizedConvolutionMultiplers to something more general -// it is used for non conv operators too -inline std::vector getQuantizedConvolutionMultiplers(float input_scale, - const std::vector &filter_scale, - float output_scale) -{ - std::vector effective_output_scales; - size_t n = filter_scale.size(); - effective_output_scales.reserve(n); - for (size_t i = 0; i < n; ++i) - { - effective_output_scales.push_back( - getQuantizedConvolutionMultipler(input_scale, filter_scale[i], output_scale)); - } - return effective_output_scales; -} - -struct ChannelQuantMultipliers -{ - int shift; - int32_t multiplier; - ChannelQuantMultipliers() = default; -}; - -inline std::vector -quantizeMultipliers(const std::vector &effective_scale) -{ - size_t n = effective_scale.size(); - std::vector params(n); - for (size_t i = 0; i < n; ++i) - { - quantizeMultiplier(effective_scale[i], ¶ms[i].multiplier, ¶ms[i].shift); - } - return params; -} - -// Helper wrapper to hide broadcast logic -template class BroadcastableWrapper -{ -public: - BroadcastableWrapper(const std::vector &v) : _v(v), _stride(v.size() == 1 ? 0 : 1) {} - - T operator[](int idx) { return _v[idx * _stride]; } - -private: - const std::vector &_v; - int _stride; -}; - -inline tflite::RuntimeShape getTensorShape(const Tensor *tensor) -{ - if (tensor == nullptr) - return tflite::RuntimeShape(); - - const Shape &shape = tensor->shape(); - tflite::RuntimeShape runtime_shape(shape.num_dims()); - for (int i = 0; i < shape.num_dims(); ++i) - { - runtime_shape.SetDim(i, shape.dim(i)); - } - return runtime_shape; -} - -template const T *getTensorData(const Tensor *tensor) -{ - return tensor != nullptr ? tensor->data() : nullptr; -} - -template T *getTensorData(Tensor *tensor) -{ - return tensor != nullptr ? tensor->data() : nullptr; -} - -// A list of tensors in a format that can be used by kernels like split and -// concatenation. -template class VectorOfTensors -{ -public: - using ElementT = typename std::conditional::type; - using TensorT = typename std::conditional::type; - - // Build with the tensors in 'tensor_list'. - explicit VectorOfTensors(const std::vector &tensor_list) - { - const int num_tensors = tensor_list.size(); - - all_data_.reserve(num_tensors); - all_shape_.reserve(num_tensors); - all_shape_ptr_.reserve(num_tensors); - - for (TensorT *tensor : tensor_list) - { - all_data_.push_back(getTensorData(tensor)); - all_shape_.push_back(getTensorShape(tensor)); - } - - // Taking the pointer from inside a std::vector is only OK if the vector is - // never modified, so we populate all_shape in the previous loop and then we - // are free to grab iterators here. - for (tflite::RuntimeShape &shape : all_shape_) - { - all_shape_ptr_.push_back(&shape); - } - } - // Return a pointer to the data pointers of all tensors in the list. For - // example: - // float* const* f = v.data(); - // f[0][1] is the second element of the first tensor. - ElementT *const *data() const { return all_data_.data(); } - - // Return a pointer the shape pointers of all tensors in the list. For - // example: - // const RuntimeShape* const* d = v.dims(); - // dims[1] are the dimensions of the second tensor in the list. - const tflite::RuntimeShape *const *shapes() const { return all_shape_ptr_.data(); } - -private: - std::vector all_data_; - std::vector all_shape_; - std::vector all_shape_ptr_; -}; - -// A list of quantized tensors in a format that can be used by kernels like -// split and concatenation. -template class VectorOfQuantizedTensors : public VectorOfTensors -{ -public: - using typename VectorOfTensors::TensorT; - - // Build with the tensors in 'tensor_list'. - explicit VectorOfQuantizedTensors(const std::vector &tensor_list) - : VectorOfTensors(tensor_list) - { - for (TensorT *tensor : tensor_list) - { - zero_point_.push_back(tensor->zero_point()); - scale_.push_back(tensor->scale()); - } - } - - const float *scale() const { return scale_.data(); } - const int32_t *zero_point() const { return zero_point_.data(); } - -private: - std::vector zero_point_; - std::vector scale_; -}; - -} // namespace kernels -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_KERNELS_UTILS_H diff --git a/compiler/luci-micro/luci-interpreter/src/loader/CMakeLists.txt b/compiler/luci-micro/luci-interpreter/src/loader/CMakeLists.txt deleted file mode 100644 index 2927715..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/CMakeLists.txt +++ /dev/null @@ -1,39 +0,0 @@ -set(SOURCES - GraphLoader.h - GraphLoader.cpp - KernelBuilderHelper.h - KernelBuilderHelper.cpp - KernelBuilder.h - KernelBuilder.cpp - ModuleLoader.h - ModuleLoader.cpp - RuntimeToIR.h - nodes/Builders.h) - -# include kernel specific builders -macro(REGISTER_KERNEL NODE) - list(APPEND SOURCES "nodes/${NODE}.cpp") -endmacro(REGISTER_KERNEL) -include(${KERNEL_REGISTER_FILE}) - -add_library(${LUCI_INTERPRETER_LOADER} STATIC ${SOURCES}) -if (NOT NNCC_LIBRARY_NO_PIC) - set_target_properties(${LUCI_INTERPRETER_LOADER} PROPERTIES POSITION_INDEPENDENT_CODE ON) -endif(NOT NNCC_LIBRARY_NO_PIC) -target_include_directories(${LUCI_INTERPRETER_LOADER} PUBLIC "${LUCI_INTERPRETER_PAL_DIR}") -target_include_directories(${LUCI_INTERPRETER_LOADER} PUBLIC "${LUCI_INTERPRETER_SOURCE_DIR}") - -target_link_libraries(${LUCI_INTERPRETER_LOADER} - PUBLIC luci_lang ${LUCI_INTERPRETER_CORE} - PRIVATE ${LUCI_INTERPRETER_KERNELS} nncc_common luci_plan) - -if(NOT ENABLE_TEST) - return() -endif(NOT ENABLE_TEST) - -nnas_find_package(GTest REQUIRED) - -set(TEST_SOURCES KernelBuilder.test.cpp) - -GTest_AddTest(${LUCI_INTERPRETER_LOADER}_test ${TEST_SOURCES}) -target_link_libraries(${LUCI_INTERPRETER_LOADER}_test ${LUCI_INTERPRETER_LOADER}) diff --git a/compiler/luci-micro/luci-interpreter/src/loader/GraphLoader.cpp b/compiler/luci-micro/luci-interpreter/src/loader/GraphLoader.cpp deleted file mode 100644 index 4020709..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/GraphLoader.cpp +++ /dev/null @@ -1,344 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "loader/GraphLoader.h" - -#include "loader/KernelBuilder.h" - -#include -#include - -namespace luci_interpreter -{ -namespace -{ - -template Shape getNodeShape(const NodeT *node) -{ - Shape shape(node->rank()); - for (uint32_t i = 0; i < node->rank(); ++i) - { - shape.dim(i) = node->dim(i).value(); - } - return shape; -} - -template const void *getNodeDataImpl(const luci::CircleConst *node, size_t *data_size) -{ - const size_t element_size = getDataTypeSize(DT); - const int32_t num_elements = node->size
(); - - *data_size = num_elements * element_size; - if (*data_size > 0) - { - // FIXME There is no good way to get the pointer to the data currently. - return &node->at
(0); - } - return nullptr; -} - -const void *getNodeData(const luci::CircleConst *node, size_t *data_size) -{ - switch (node->dtype()) - { - case DataType::U8: - return getNodeDataImpl(node, data_size); - case DataType::FLOAT32: - return getNodeDataImpl(node, data_size); - case DataType::S8: - return getNodeDataImpl(node, data_size); - case DataType::S16: - return getNodeDataImpl(node, data_size); - case DataType::S32: - return getNodeDataImpl(node, data_size); - case DataType::S64: - return getNodeDataImpl(node, data_size); - case DataType::BOOL: - return getNodeDataImpl(node, data_size); - default: - throw std::runtime_error("Unsupported type."); - } -} - -const void *getNodeData(const luci::CircleCustom *node, size_t *data_size) -{ - if (node->custom_code() != "CircleReferencingConst") - return nullptr; - - // helper struct which describes data loaded to custom_options of CircleReferencingConst node - // TODO move this struct to header - struct ConstDataReference - { - const uint8_t *data = nullptr; - uint32_t size = 0; - }; - - const auto &custom_options = node->custom_options(); - const auto &const_data_ref = *reinterpret_cast(custom_options.data()); - - *data_size = const_data_ref.size; - return const_data_ref.data; -} - -bool isExecutableNode(const luci::CircleNode *node) -{ - switch (node->opcode()) - { - // These nodes denote inputs / outputs of a graph. - case luci::CircleOpcode::CIRCLECONST: - case luci::CircleOpcode::CIRCLEINPUT: - case luci::CircleOpcode::CIRCLEOUTPUT: - case luci::CircleOpcode::CIRCLEOUTPUTEXCLUDE: - // The following nodes denote outputs of multiple-output nodes. - case luci::CircleOpcode::CIRCLEBIDIRECTIONAL_SEQUENCE_LSTM_OUT: - case luci::CircleOpcode::CIRCLECUSTOMOUT: - case luci::CircleOpcode::CIRCLEIFOUT: - case luci::CircleOpcode::CIRCLENONMAXSUPPRESSIONV4OUT: - case luci::CircleOpcode::CIRCLENONMAXSUPPRESSIONV5OUT: - case luci::CircleOpcode::CIRCLESPLITOUT: - case luci::CircleOpcode::CIRCLESPLITVOUT: - case luci::CircleOpcode::CIRCLETOPKV2OUT: - case luci::CircleOpcode::CIRCLEUNIQUEOUT: - case luci::CircleOpcode::CIRCLEUNPACKOUT: - case luci::CircleOpcode::CIRCLEVARIABLE: - case luci::CircleOpcode::CIRCLEWHILEOUT: - return false; - // Custom nodes may be executable and non-executable - case luci::CircleOpcode::CUSTOM: - { - auto const custom_node = loco::must_cast(node); - - // TODO handle more non-executable Custom ops here - if (custom_node->custom_code() == "CircleReferencingConst") - return false; - - return true; - } - default: - return true; - } -} - -bool isTensorProducingNode(const luci::CircleNode *node) -{ - switch (node->opcode()) - { - // Output nodes do not produce tensors. - case luci::CircleOpcode::CIRCLEOUTPUT: - // The following nodes are multiple-output nodes. They do not produce tensors, the tensors - // are produced by the corresponding *Out nodes instead. - case luci::CircleOpcode::BIDIRECTIONAL_SEQUENCE_LSTM: - case luci::CircleOpcode::CUSTOM: - case luci::CircleOpcode::IF: - case luci::CircleOpcode::NON_MAX_SUPPRESSION_V4: - case luci::CircleOpcode::NON_MAX_SUPPRESSION_V5: - case luci::CircleOpcode::SPLIT: - case luci::CircleOpcode::SPLIT_V: - case luci::CircleOpcode::TOPK_V2: - case luci::CircleOpcode::UNIQUE: - case luci::CircleOpcode::UNPACK: - case luci::CircleOpcode::WHILE: - return false; - default: - return true; - } -} - -bool isSupportedCustomNode(const luci::CircleNode *node) -{ - const auto custom_node = loco::must_cast(node); - - // TODO handle more Custom ops here - if (custom_node->custom_code() == "CircleReferencingConst") - return true; - - return false; -} - -} // namespace - -GraphLoader::GraphLoader( - const loco::Graph *graph, RuntimeGraph *runtime_graph, RuntimeToIR &runtime_to_ir, - const std::unordered_map &graph_to_runtime_graph, - std::unordered_map &node_to_tensor, IMemoryManager *memory_manager) - : _graph(graph), _runtime_graph(runtime_graph), _runtime_to_ir(runtime_to_ir), - _graph_to_runtime_graph(graph_to_runtime_graph), _node_to_tensor(node_to_tensor), - _memory_manager(memory_manager) -{ -} - -void GraphLoader::loadTensors() -{ - for (uint32_t i = 0; i < _graph->nodes()->size(); ++i) - { - const auto *node = loco::must_cast(_graph->nodes()->at(i)); - - if (node->opcode() == luci::CircleOpcode::CUSTOM && !isSupportedCustomNode(node)) - throw std::runtime_error("Unsupported Custom operator. " + node->name()); - - if (!isTensorProducingNode(node)) - continue; - - // Only Input, Const, Custom and Variable nodes have shapes. Shapes of intermediate tensors will - // be inferred. - Shape shape{}; - switch (node->opcode()) - { - case luci::CircleOpcode::CIRCLECONST: - case luci::CircleOpcode::CIRCLECUSTOMOUT: - case luci::CircleOpcode::CIRCLEINPUT: - case luci::CircleOpcode::CIRCLEVARIABLE: - shape = getNodeShape(node); - break; - default: - break; - } - - AffineQuantization quantization; - if (node->quantparam() != nullptr) - { - const luci::CircleQuantParam *params = node->quantparam(); - assert(params->scale.size() == params->zerop.size()); - quantization.scale.assign(params->scale.cbegin(), params->scale.cend()); - quantization.zero_point.assign(params->zerop.cbegin(), params->zerop.cend()); - quantization.quantized_dimension = params->quantized_dimension; - } - - auto tensor = std::make_unique(node->dtype(), std::move(shape), std::move(quantization), - node->name()); - - // If node has execution plan then read memory offsets for nodes - // from the beginning of shared memory buffer. Used in Static Memory Manager. - if (luci::has_execution_plan(node)) - { - auto execution_plan = luci::get_execution_plan(node); - assert(!execution_plan.offsets().empty()); - tensor->set_offset(execution_plan.offsets().front()); - } - - if (const auto *const_node = dynamic_cast(node)) - { - size_t data_size{}; - const void *const_data = getNodeData(const_node, &data_size); - if (const_data != nullptr) - { - _memory_manager->allocate_memory(*tensor); - tensor->writeData(const_data, data_size); - } - } - else if (const auto *custom_out_node = dynamic_cast(node)) - { - const auto *custom_node = - loco::must_cast(custom_out_node->input()); - - if (custom_node->custom_code() == "CircleReferencingConst") - { - size_t data_size{}; - const void *const_data = getNodeData(custom_node, &data_size); - if (const_data != nullptr) - { - _memory_manager->allocate_memory(*tensor); - tensor->writeData(const_data, data_size); - } - } - } - - _node_to_tensor.emplace(node, tensor.get()); - _runtime_to_ir.tensor_to_node.emplace(tensor.get(), node); - - _runtime_graph->addTensor(std::move(tensor)); - } -} - -void GraphLoader::initInputOutputTensors() const -{ - auto input_nodes = loco::input_nodes(_graph); - std::vector input_tensors(input_nodes.size()); - for (size_t i = 0; i < input_nodes.size(); ++i) - { - input_tensors[i] = _node_to_tensor.at(input_nodes[i]); - _memory_manager->allocate_memory(*input_tensors[i]); - } - _runtime_graph->setInputTensors(input_tensors); - - auto output_nodes = loco::output_nodes(const_cast(_graph)); - std::vector output_tensors(output_nodes.size()); - for (size_t i = 0; i < output_nodes.size(); ++i) - { - const auto *node = loco::must_cast(output_nodes[i]); - output_tensors[i] = _node_to_tensor.at(node->from()); - } - _runtime_graph->setOutputTensors(output_tensors); -} - -void GraphLoader::loadOperators() -{ - KernelBuilder kernel_builder(_graph_to_runtime_graph, _node_to_tensor); - - // Create kernels for executable nodes. This has to be done in execution order. - auto graph = const_cast(_graph); - - auto const graph_nodes = loco::all_nodes(graph); - - // Checking for execution plan in node annotations. - bool has_execution_annotation = true; - auto const checking_exec_plan = [&has_execution_annotation](auto const node) { - const auto *circle_node = loco::must_cast(node); - if (!luci::has_execution_plan(circle_node)) - has_execution_annotation = false; - }; - std::for_each(begin(graph_nodes), end(graph_nodes), checking_exec_plan); - - if (has_execution_annotation) - { - // Build ordered_nodes vector that stores the order of execution of graph nodes. - std::vector ordered_nodes(graph_nodes.size()); - - auto const filler = [&ordered_nodes](auto const node) { - const auto *circle_node = loco::must_cast(node); - auto const position = luci::get_execution_plan(circle_node).order_in_plan(); - ordered_nodes.at(position) = circle_node; - }; - std::for_each(begin(graph_nodes), end(graph_nodes), filler); - - for (auto node : ordered_nodes) - { - if (isExecutableNode(node)) - { - std::unique_ptr kernel = kernel_builder.build(node); - _runtime_to_ir.kernel_to_node.emplace(kernel.get(), node); - _runtime_graph->addKernel(std::move(kernel)); - } - } - } - else - { - // If it is impossible to build the execution order plan, - // then we use the default postorder_traversal approach. - for (const loco::Node *loco_node : loco::postorder_traversal(loco::output_nodes(graph))) - { - const auto *node = loco::must_cast(loco_node); - if (isExecutableNode(node)) - { - std::unique_ptr kernel = kernel_builder.build(node); - _runtime_to_ir.kernel_to_node.emplace(kernel.get(), node); - _runtime_graph->addKernel(std::move(kernel)); - } - } - } -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/GraphLoader.h b/compiler/luci-micro/luci-interpreter/src/loader/GraphLoader.h deleted file mode 100644 index fe066ec..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/GraphLoader.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_LOADER_GRAPHLOADER_H -#define LUCI_INTERPRETER_LOADER_GRAPHLOADER_H - -#include "core/RuntimeGraph.h" -#include "loader/RuntimeToIR.h" -#include "luci_interpreter/MemoryManager.h" - -#include - -#include - -namespace luci_interpreter -{ - -class GraphLoader -{ -public: - GraphLoader(const loco::Graph *graph, RuntimeGraph *runtime_graph, RuntimeToIR &runtime_to_ir, - const std::unordered_map &graph_to_runtime_graph, - std::unordered_map &node_to_tensor, - IMemoryManager *memory_manager); - - void loadTensors(); - void initInputOutputTensors() const; - void loadOperators(); - -private: - const loco::Graph *_graph; - RuntimeGraph *_runtime_graph; - RuntimeToIR &_runtime_to_ir; - IMemoryManager *_memory_manager; - - const std::unordered_map &_graph_to_runtime_graph; - std::unordered_map &_node_to_tensor; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_LOADER_GRAPHLOADER_H diff --git a/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.cpp b/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.cpp deleted file mode 100644 index 8483a9a..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "loader/KernelBuilder.h" -#include "loader/nodes/Builders.h" - -#include - -namespace luci_interpreter -{ - -#define CIRCLE_NODE(OPCODE, CLASS) CLASS, -#define CIRCLE_VNODE(OPCODE, CLASS) CLASS, - -// This enum is auxiliary. -// It is duplicate of luci::CircleOpcode but initialized with CLASS instead of OPCODE, -// because list of target operators is in format of CLASS names -enum class BuilderId -{ -#include - Size // casts to count of values in BuilderId enum -}; - -#undef CIRCLE_VNODE -#undef CIRCLE_NODE - -/** - * @brief Registry of kernel builders - * - * This class contains mapping from Opcodes to kernel builder functions - */ - -class KernelBuilderRegistry -{ -public: - using KernelBuilderFunc = std::unique_ptr(const luci::CircleNode *, - KernelBuilderHelper &); - - KernelBuilderRegistry() : _operator_builders(size_t(BuilderId::Size), nullptr) - { -#define REGISTER_KERNEL(name) \ - register_kernel_builder(BuilderId::Circle##name, build_kernel_Circle##name); - -#include "KernelsToBuild.lst" - -#undef REGISTER_KERNEL - } - - KernelBuilderFunc *get_kernel_builder_func(luci::CircleOpcode opcode) const - { - return _operator_builders.at(size_t(opcode)); - } - -private: - std::vector _operator_builders; - - void register_kernel_builder(BuilderId id, KernelBuilderFunc *func) - { - // Using BuilderId is a duplicate of luci::CirclreOpcode, - // size_t(id) is equal to size_t(corresponding operation opcode). - assert(size_t(id) < _operator_builders.size()); - _operator_builders[size_t(id)] = func; - } -}; - -KernelBuilder::KernelBuilder( - const std::unordered_map &graph_to_runtime_graph, - const std::unordered_map &node_to_tensor) - : KernelBuilderHelper(graph_to_runtime_graph, node_to_tensor) -{ - _builder_registry = std::make_unique(); -} - -KernelBuilder::~KernelBuilder() -{ - // Need to define in this CPP to hide KernelBuilderRegistry internals. - // This destructor deletes _builder_registry -} - -std::unique_ptr KernelBuilder::build(const luci::CircleNode *node) -{ - auto specific_builder = _builder_registry->get_kernel_builder_func(node->opcode()); - if (specific_builder != nullptr) - return specific_builder(node, *this); - - std::string msg = "Unsupported operator: "; - msg += std::to_string(static_cast(node->opcode())) + " " + std::string(node->name()); - throw std::invalid_argument(msg.c_str()); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.h b/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.h deleted file mode 100644 index b1f3833..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_LOADER_KERNELBUILDER_H -#define LUCI_INTERPRETER_LOADER_KERNELBUILDER_H - -#include "loader/KernelBuilderHelper.h" - -#include "core/Kernel.h" -#include "core/RuntimeGraph.h" - -#include - -#include -#include - -namespace luci_interpreter -{ - -class KernelBuilderRegistry; - -class KernelBuilder : public KernelBuilderHelper -{ -public: - KernelBuilder( - const std::unordered_map &graph_to_runtime_graph, - const std::unordered_map &node_to_tensor); - - ~KernelBuilder(); - - std::unique_ptr build(const luci::CircleNode *node); - -private: - std::unique_ptr _builder_registry; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_LOADER_KERNELBUILDER_H diff --git a/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.test.cpp b/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.test.cpp deleted file mode 100644 index b221b69..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilder.test.cpp +++ /dev/null @@ -1,1376 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "loader/GraphLoader.h" -#include "loader/KernelBuilder.h" -#include "luci_interpreter/SimpleMemoryManager.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace luci_interpreter -{ -namespace -{ - -using namespace testing; - -class KernelBuilderTest : public Test -{ -protected: - luci::CircleInput *createInputNode() { return createNode(); } - void SetUp() override { _memory_manager = std::make_unique(); } - - std::unique_ptr _memory_manager; - - template NodeT *createNode(Args &&... args) - { - auto *node = _graph.nodes()->create(std::forward(args)...); - // The actual type does not matter for the purpose of the tests. - // NOTE The type is meaningless for nodes with multiple outputs (corresponding *Out nodes carry - // actual output types). - node->dtype(loco::DataType::FLOAT32); - return node; - } - - template NodeOutT *createNodeOut(loco::Node *node, int index) - { - auto *node_out = createNode(); - node_out->input(node); - node_out->index(index); - return node_out; - } - - template std::unique_ptr buildKernel(const luci::CircleNode *op) - { - std::unordered_map graph_to_runtime_graph; - - RuntimeGraph runtime_graph(nullptr, _memory_manager.get()); - graph_to_runtime_graph[&_graph] = &runtime_graph; - RuntimeToIR runtime_to_ir; - GraphLoader graph_loader(&_graph, &runtime_graph, runtime_to_ir, graph_to_runtime_graph, - _node_to_tensor, _memory_manager.get()); - graph_loader.loadTensors(); - - KernelBuilder kernel_builder(graph_to_runtime_graph, _node_to_tensor); - - auto kernel = kernel_builder.build(op); - return std::unique_ptr(dynamic_cast(kernel.release())); - } - - void checkTensor(const Tensor *tensor, const loco::Node *node) - { - EXPECT_THAT(tensor, Eq(_node_to_tensor.at(node))); - } - -private: - loco::Graph _graph; - std::unordered_map _node_to_tensor; -}; - -TEST_F(KernelBuilderTest, Add) -{ - auto *input1 = createInputNode(); - auto *input2 = createInputNode(); - - auto *op = createNode(); - op->x(input1); - op->y(input2); - - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input1(), input1); - checkTensor(kernel->input2(), input2); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, ArgMax) -{ - auto *input = createInputNode(); - auto *axis = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->dimension(axis); - - op->output_type(loco::DataType::FLOAT32); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->axis(), axis); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().output_type, Eq(op->output_type())); -} - -TEST_F(KernelBuilderTest, AveragePool2D) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->value(input); - - op->padding(luci::Padding::SAME); - op->filter()->h(11); - op->filter()->w(13); - op->stride()->h(17); - op->stride()->w(19); - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().padding, Eq(op->padding())); - EXPECT_THAT(kernel->params().filter_height, Eq(op->filter()->h())); - EXPECT_THAT(kernel->params().filter_width, Eq(op->filter()->w())); - EXPECT_THAT(kernel->params().stride_height, Eq(op->stride()->h())); - EXPECT_THAT(kernel->params().stride_width, Eq(op->stride()->w())); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, BatchMatMul) -{ - auto *lhs = createInputNode(); - auto *rhs = createInputNode(); - - auto *op = createNode(); - op->x(lhs); - op->y(rhs); - op->adj_x(false); - op->adj_y(false); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->x(), lhs); - checkTensor(kernel->y(), rhs); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().adj_x, Eq(op->adj_x())); - EXPECT_THAT(kernel->params().adj_y, Eq(op->adj_y())); -} - -TEST_F(KernelBuilderTest, Cast) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->x(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Concatenation) -{ - auto *input1 = createInputNode(); - auto *input2 = createInputNode(); - - auto *op = createNode(2); - op->values(0, input1); - op->values(1, input2); - op->axis(11); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(0), input1); - checkTensor(kernel->input(1), input2); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().axis, Eq(op->axis())); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, Conv2D) -{ - auto *input = createInputNode(); - auto *filter = createInputNode(); - auto *bias = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->filter(filter); - op->bias(bias); - - op->padding(luci::Padding::SAME); - op->stride()->h(11); - op->stride()->w(13); - op->dilation()->h(17); - op->dilation()->w(19); - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->filter(), filter); - checkTensor(kernel->bias(), bias); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().padding, Eq(op->padding())); - EXPECT_THAT(kernel->params().stride_height, Eq(op->stride()->h())); - EXPECT_THAT(kernel->params().stride_width, Eq(op->stride()->w())); - EXPECT_THAT(kernel->params().dilation_height_factor, Eq(op->dilation()->h())); - EXPECT_THAT(kernel->params().dilation_width_factor, Eq(op->dilation()->w())); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, DepthToSpace) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->input(input); - - op->block_size(11); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().block_size, Eq(op->block_size())); -} - -TEST_F(KernelBuilderTest, DepthwiseConv2D) -{ - auto *input = createInputNode(); - auto *filter = createInputNode(); - auto *bias = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->filter(filter); - op->bias(bias); - - op->padding(luci::Padding::SAME); - op->depthMultiplier(11); - op->stride()->h(13); - op->stride()->w(17); - op->dilation()->h(19); - op->dilation()->w(23); - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->filter(), filter); - checkTensor(kernel->bias(), bias); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().padding, Eq(op->padding())); - EXPECT_THAT(kernel->params().depth_multiplier, Eq(op->depthMultiplier())); - EXPECT_THAT(kernel->params().stride_height, Eq(op->stride()->h())); - EXPECT_THAT(kernel->params().stride_width, Eq(op->stride()->w())); - EXPECT_THAT(kernel->params().dilation_height_factor, Eq(op->dilation()->h())); - EXPECT_THAT(kernel->params().dilation_width_factor, Eq(op->dilation()->w())); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, Div) -{ - auto *input1 = createInputNode(); - auto *input2 = createInputNode(); - - auto *op = createNode(); - op->x(input1); - op->y(input2); - - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input1(), input1); - checkTensor(kernel->input2(), input2); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, Elu) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->features(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Exp) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->x(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Floor) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->x(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, FloorDiv) -{ - auto *x = createInputNode(); - auto *y = createInputNode(); - - auto *op = createNode(); - op->x(x); - op->y(y); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->x(), x); - checkTensor(kernel->y(), y); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Equal) -{ - auto *x_input = createInputNode(); - auto *y_input = createInputNode(); - - auto *op = createNode(); - op->x(x_input); - op->y(y_input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->x(), x_input); - checkTensor(kernel->y(), y_input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, FullyConnected) -{ - auto *input = createInputNode(); - auto *weights = createInputNode(); - auto *bias = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->weights(weights); - op->bias(bias); - - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->weights(), weights); - checkTensor(kernel->bias(), bias); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, Greater) -{ - auto *x_input = createInputNode(); - auto *y_input = createInputNode(); - - auto *op = createNode(); - op->x(x_input); - op->y(y_input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->x(), x_input); - checkTensor(kernel->y(), y_input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, GreaterEqual) -{ - auto *x_input = createInputNode(); - auto *y_input = createInputNode(); - - auto *op = createNode(); - op->x(x_input); - op->y(y_input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->x(), x_input); - checkTensor(kernel->y(), y_input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, InstanceNorm) -{ - auto *input = createInputNode(); - auto *gamma = createInputNode(); - auto *beta = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->gamma(gamma); - op->beta(beta); - - op->epsilon(1e-05); - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->gamma(), gamma); - checkTensor(kernel->beta(), beta); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().epsilon, Eq(op->epsilon())); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, L2Normalize) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->x(input); - - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, L2Pool2D) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->value(input); - - op->padding(luci::Padding::SAME); - op->filter()->h(11); - op->filter()->w(13); - op->stride()->h(17); - op->stride()->w(19); - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().padding, Eq(op->padding())); - EXPECT_THAT(kernel->params().filter_height, Eq(op->filter()->h())); - EXPECT_THAT(kernel->params().filter_width, Eq(op->filter()->w())); - EXPECT_THAT(kernel->params().stride_height, Eq(op->stride()->h())); - EXPECT_THAT(kernel->params().stride_width, Eq(op->stride()->w())); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, LeakyRelu) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->features(input); - - op->alpha(11.0f); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().alpha, Eq(op->alpha())); -} - -TEST_F(KernelBuilderTest, Less) -{ - auto *x_input = createInputNode(); - auto *y_input = createInputNode(); - - auto *op = createNode(); - op->x(x_input); - op->y(y_input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->x(), x_input); - checkTensor(kernel->y(), y_input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, LessEqual) -{ - auto *x_input = createInputNode(); - auto *y_input = createInputNode(); - - auto *op = createNode(); - op->x(x_input); - op->y(y_input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->x(), x_input); - checkTensor(kernel->y(), y_input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, LocalResponseNormalization) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->input(input); - - op->radius(11); - op->bias(13.0f); - op->alpha(15.0f); - op->beta(17.0f); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().radius, Eq(op->radius())); - EXPECT_THAT(kernel->params().bias, Eq(op->bias())); - EXPECT_THAT(kernel->params().alpha, Eq(op->alpha())); - EXPECT_THAT(kernel->params().beta, Eq(op->beta())); -} - -TEST_F(KernelBuilderTest, LogicalAnd) -{ - auto *input1 = createInputNode(); - auto *input2 = createInputNode(); - - auto *op = createNode(); - op->x(input1); - op->y(input2); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input1(), input1); - checkTensor(kernel->input2(), input2); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, LogicalNot) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->x(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, LogicalOr) -{ - auto *input1 = createInputNode(); - auto *input2 = createInputNode(); - - auto *op = createNode(); - op->x(input1); - op->y(input2); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input1(), input1); - checkTensor(kernel->input2(), input2); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Logistic) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->x(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, LogSoftmax) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->logits(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Maximum) -{ - auto *input1 = createInputNode(); - auto *input2 = createInputNode(); - - auto *op = createNode(); - op->x(input1); - op->y(input2); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input1(), input1); - checkTensor(kernel->input2(), input2); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, MaxPool2D) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->value(input); - - op->padding(luci::Padding::SAME); - op->filter()->h(11); - op->filter()->w(13); - op->stride()->h(17); - op->stride()->w(19); - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().padding, Eq(op->padding())); - EXPECT_THAT(kernel->params().filter_height, Eq(op->filter()->h())); - EXPECT_THAT(kernel->params().filter_width, Eq(op->filter()->w())); - EXPECT_THAT(kernel->params().stride_height, Eq(op->stride()->h())); - EXPECT_THAT(kernel->params().stride_width, Eq(op->stride()->w())); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, Mean) -{ - auto *input = createInputNode(); - auto *axes = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->reduction_indices(axes); - - op->keep_dims(true); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->axes(), axes); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().keep_dims, Eq(op->keep_dims())); -} - -TEST_F(KernelBuilderTest, Minimum) -{ - auto *input1 = createInputNode(); - auto *input2 = createInputNode(); - - auto *op = createNode(); - op->x(input1); - op->y(input2); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input1(), input1); - checkTensor(kernel->input2(), input2); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Mul) -{ - auto *input1 = createInputNode(); - auto *input2 = createInputNode(); - - auto *op = createNode(); - op->x(input1); - op->y(input2); - - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input1(), input1); - checkTensor(kernel->input2(), input2); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, Neg) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->x(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, NotEqual) -{ - auto *x_input = createInputNode(); - auto *y_input = createInputNode(); - - auto *op = createNode(); - op->x(x_input); - op->y(y_input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->x(), x_input); - checkTensor(kernel->y(), y_input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, OneHot) -{ - auto *indices = createInputNode(); - auto *depth = createInputNode(); - auto *on_value = createInputNode(); - auto *off_value = createInputNode(); - auto axis = 1; - - auto *op = createNode(); - op->indices(indices); - op->depth(depth); - op->on_value(on_value); - op->off_value(off_value); - op->axis(axis); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->indices(), indices); - checkTensor(kernel->depth(), depth); - checkTensor(kernel->on_value(), on_value); - checkTensor(kernel->off_value(), off_value); - EXPECT_THAT(kernel->params().axis, Eq(op->axis())); -} - -TEST_F(KernelBuilderTest, Pad) -{ - auto *input = createInputNode(); - auto *paddings = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->paddings(paddings); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->paddings(), paddings); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, PadV2) -{ - auto *input = createInputNode(); - auto *paddings = createInputNode(); - auto *constant_values = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->paddings(paddings); - op->constant_values(constant_values); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->paddings(), paddings); - checkTensor(kernel->constant_values(), constant_values); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Pow) -{ - auto *input1 = createInputNode(); - auto *input2 = createInputNode(); - - auto *op = createNode(); - op->x(input1); - op->y(input2); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input1(), input1); - checkTensor(kernel->input2(), input2); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, PRelu) -{ - auto *input = createInputNode(); - auto *alpha = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->alpha(alpha); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->alpha(), alpha); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Relu) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->features(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Relu6) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->features(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Reshape) -{ - auto *input = createInputNode(); - auto *shape = createInputNode(); - - auto *op = createNode(); - op->tensor(input); - op->shape(shape); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->shape(), shape); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, ResizeBilinear) -{ - auto *input = createInputNode(); - auto *size = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->size(size); - op->align_corners(true); - op->half_pixel_centers(true); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->size(), size); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().align_corners, Eq(op->align_corners())); - EXPECT_THAT(kernel->params().half_pixel_centers, Eq(op->half_pixel_centers())); -} - -TEST_F(KernelBuilderTest, ResizeNearestNeighbor) -{ - auto *input = createInputNode(); - auto *size = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->size(size); - op->align_corners(true); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->size(), size); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().align_corners, Eq(op->align_corners())); - // TODO currently half_pixel_centers are not implemented on CircleResizeNearestNeighbor - // after adding, need to be updated. -} - -TEST_F(KernelBuilderTest, ReverseV2) -{ - auto *input = createInputNode(); - auto *axes = createInputNode(); - - auto *op = createNode(); - op->tensor(input); - op->axis(axes); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->axes(), axes); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Rsqrt) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->x(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Slice) -{ - auto *input = createInputNode(); - auto *begin = createInputNode(); - auto *size = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->begin(begin); - op->size(size); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->begin(), begin); - checkTensor(kernel->size(), size); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Softmax) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->logits(input); - - op->beta(11.0f); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().beta, Eq(op->beta())); -} - -TEST_F(KernelBuilderTest, SpaceToDepth) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->input(input); - - op->block_size(11); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().block_size, op->block_size()); -} - -TEST_F(KernelBuilderTest, Split) -{ - auto *axis = createInputNode(); - auto *input = createInputNode(); - auto *op = createNode(); - auto *output1 = createNodeOut(op, 0); - auto *output2 = createNodeOut(op, 1); - - op->split_dim(axis); - op->input(input); - - op->num_split(2); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->axis(), axis); - checkTensor(kernel->input(), input); - checkTensor(kernel->output(0), output1); - checkTensor(kernel->output(1), output2); -} - -TEST_F(KernelBuilderTest, SplitV) -{ - auto *input = createInputNode(); - auto *size_splits = createInputNode(); - auto *axis = createInputNode(); - auto *op = createNode(); - auto *output0 = createNodeOut(op, 0); - auto *output1 = createNodeOut(op, 1); - - op->input(input); - op->size_splits(size_splits); - op->split_dim(axis); - - op->num_split(2); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->size_splits(), size_splits); - checkTensor(kernel->axis(), axis); - checkTensor(kernel->output(0), output0); - checkTensor(kernel->output(1), output1); -} - -TEST_F(KernelBuilderTest, Sqrt) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->x(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, SquaredDifference) -{ - auto *input1 = createInputNode(); - auto *input2 = createInputNode(); - - auto *op = createNode(); - op->x(input1); - op->y(input2); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input1(), input1); - checkTensor(kernel->input2(), input2); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Squeeze) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->input(input); - - op->squeeze_dims({11, 13}); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().squeeze_dims, ElementsAreArray(op->squeeze_dims())); -} - -TEST_F(KernelBuilderTest, StridedSlice) -{ - auto *input = createInputNode(); - auto *begin = createInputNode(); - auto *end = createInputNode(); - auto *strides = createInputNode(); - - auto *op = createNode(); - op->input(input); - op->begin(begin); - op->end(end); - op->strides(strides); - - op->begin_mask(11); - op->ellipsis_mask(13); - op->end_mask(17); - op->new_axis_mask(19); - op->shrink_axis_mask(23); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->begin(), begin); - checkTensor(kernel->end(), end); - checkTensor(kernel->strides(), strides); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().begin_mask, Eq(op->begin_mask())); - EXPECT_THAT(kernel->params().ellipsis_mask, Eq(op->ellipsis_mask())); - EXPECT_THAT(kernel->params().end_mask, Eq(op->end_mask())); - EXPECT_THAT(kernel->params().new_axis_mask, Eq(op->new_axis_mask())); - EXPECT_THAT(kernel->params().shrink_axis_mask, Eq(op->shrink_axis_mask())); -} - -TEST_F(KernelBuilderTest, Sub) -{ - auto *input1 = createInputNode(); - auto *input2 = createInputNode(); - - auto *op = createNode(); - op->x(input1); - op->y(input2); - - op->fusedActivationFunction(luci::FusedActFunc::RELU); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input1(), input1); - checkTensor(kernel->input2(), input2); - checkTensor(kernel->output(), op); - EXPECT_THAT(kernel->params().activation, Eq(op->fusedActivationFunction())); -} - -TEST_F(KernelBuilderTest, Tanh) -{ - auto *input = createInputNode(); - - auto *op = createNode(); - op->x(input); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, Transpose) -{ - auto *input = createInputNode(); - auto *perm = createInputNode(); - - auto *op = createNode(); - op->a(input); - op->perm(perm); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->perm(), perm); - checkTensor(kernel->output(), op); -} - -TEST_F(KernelBuilderTest, TransposeConv) -{ - auto *output_shape = createInputNode(); - auto *filter = createInputNode(); - auto *input = createInputNode(); - auto *bias = createInputNode(); - - auto *op = createNode(); - op->inputSizes(output_shape); - op->filter(filter); - op->outBackprop(input); - op->bias(bias); - - op->padding(luci::Padding::SAME); - op->stride()->h(11); - op->stride()->w(13); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->output_shape(), output_shape); - checkTensor(kernel->filter(), filter); - checkTensor(kernel->input(), input); - checkTensor(kernel->output(), op); - checkTensor(kernel->bias(), bias); - EXPECT_THAT(kernel->params().padding, Eq(op->padding())); - EXPECT_THAT(kernel->params().stride_height, Eq(op->stride()->h())); - EXPECT_THAT(kernel->params().stride_width, Eq(op->stride()->w())); -} - -TEST_F(KernelBuilderTest, Unpack) -{ - auto *input = createInputNode(); - auto *op = createNode(); - auto *output1 = createNodeOut(op, 0); - auto *output2 = createNodeOut(op, 1); - - op->value(input); - - op->num(2); - op->axis(11); - - auto kernel = buildKernel(op); - ASSERT_THAT(kernel, NotNull()); - - checkTensor(kernel->input(), input); - checkTensor(kernel->output(0), output1); - checkTensor(kernel->output(1), output2); - EXPECT_THAT(kernel->params().axis, Eq(op->axis())); -} - -TEST_F(KernelBuilderTest, NonExisting1_NEG) -{ - auto *op = createNode(); - ASSERT_ANY_THROW(buildKernel(op)); -} - -TEST_F(KernelBuilderTest, NonExisting2_NEG) -{ - auto *op = createNode(); - ASSERT_ANY_THROW(buildKernel(op)); -} - -TEST_F(KernelBuilderTest, NonExisting3_NEG) -{ - auto *op = createNode(); - ASSERT_ANY_THROW(buildKernel(op)); -} - -} // namespace -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilderHelper.cpp b/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilderHelper.cpp deleted file mode 100644 index 23c96a6..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilderHelper.cpp +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "loader/KernelBuilderHelper.h" - -#include - -namespace luci_interpreter -{ - -const Tensor *KernelBuilderHelper::getInputTensor(const loco::Node *node) const -{ - const Tensor *tensor = _node_to_tensor.at(node); - assert(tensor != nullptr); - return tensor; -} - -const Tensor *KernelBuilderHelper::getOptionalInputTensor(const loco::Node *node) const -{ - if (dynamic_cast(node)) - { - return nullptr; - } - return getInputTensor(node); -} - -Tensor *KernelBuilderHelper::getOutputTensor(const loco::Node *node) const -{ - Tensor *tensor = _node_to_tensor.at(node); - assert(tensor != nullptr); - return tensor; -} - -std::vector -KernelBuilderHelper::getOutputTensors(const std::vector &nodes) const -{ - std::vector tensors; - tensors.reserve(nodes.size()); - for (const loco::Node *node : nodes) - tensors.push_back(getOutputTensor(node)); - return tensors; -} - -RuntimeGraph *KernelBuilderHelper::getRuntimeGraph(const loco::Graph *graph) const -{ - RuntimeGraph *runtime_graph = _graph_to_runtime_graph.at(graph); - assert(runtime_graph != nullptr); - return runtime_graph; -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilderHelper.h b/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilderHelper.h deleted file mode 100644 index d6fb253..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/KernelBuilderHelper.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_LOADER_KERNELBUILDER_HELPER_H -#define LUCI_INTERPRETER_LOADER_KERNELBUILDER_HELPER_H - -#include "core/Kernel.h" -#include "core/RuntimeGraph.h" - -#include -#include - -#include -#include - -namespace luci_interpreter -{ - -class KernelBuilderHelper -{ -public: - KernelBuilderHelper( - const std::unordered_map &graph_to_runtime_graph, - const std::unordered_map &node_to_tensor) - : _graph_to_runtime_graph(graph_to_runtime_graph), _node_to_tensor(node_to_tensor) - { - } - -public: - const Tensor *getInputTensor(const loco::Node *node) const; - const Tensor *getOptionalInputTensor(const loco::Node *node) const; - - Tensor *getOutputTensor(const loco::Node *node) const; - std::vector getOutputTensors(const std::vector &nodes) const; - - RuntimeGraph *getRuntimeGraph(const loco::Graph *graph) const; - -public: - const std::unordered_map &graph_to_runtime_graph() const - { - return _graph_to_runtime_graph; - } - - const std::unordered_map &node_to_tensor() const - { - return _node_to_tensor; - } - -private: - const std::unordered_map &_graph_to_runtime_graph; - const std::unordered_map &_node_to_tensor; -}; - -template -std::vector collectOutputNodes(const loco::Node *node) -{ - std::vector output_nodes; - for (const loco::Node *loco_node : loco::succs(node)) - { - output_nodes.push_back(loco::must_cast(loco_node)); - } - std::sort(output_nodes.begin(), output_nodes.end(), - [](const CircleNodeOut *node1, const CircleNodeOut *node2) { - return node1->index() < node2->index(); - }); - return {output_nodes.cbegin(), output_nodes.cend()}; -} - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_LOADER_KERNELBUILDER_HELPER_H diff --git a/compiler/luci-micro/luci-interpreter/src/loader/ModuleLoader.cpp b/compiler/luci-micro/luci-interpreter/src/loader/ModuleLoader.cpp deleted file mode 100644 index 2f278b0..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/ModuleLoader.cpp +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "ModuleLoader.h" - -#include "GraphLoader.h" - -namespace luci_interpreter -{ - -ModuleLoader::ModuleLoader(const luci::Module *module, RuntimeModule *runtime_module, - RuntimeToIR &runtime_to_ir, - std::unordered_map &node_to_tensor, - IMemoryManager *memory_manager) - : _module(module), _runtime_module(runtime_module), _runtime_to_ir(runtime_to_ir), - _node_to_tensor(node_to_tensor), _memory_manager(memory_manager) -{ -} - -void ModuleLoader::load() -{ - // Runtime graphs have to be created in advance, because they will be needed during the loading - // process for control flow nodes. - for (size_t i = 0; i < _module->size(); ++i) - { - _graph_to_runtime_graph.emplace(_module->graph(i), _runtime_module->addGraph(_memory_manager)); - } - for (size_t i = 0; i < _module->size(); ++i) - { - const loco::Graph *graph = _module->graph(i); - RuntimeGraph *runtime_graph = _graph_to_runtime_graph.at(graph); - GraphLoader loader(graph, runtime_graph, _runtime_to_ir, _graph_to_runtime_graph, - _node_to_tensor, _memory_manager); - loader.loadTensors(); - loader.initInputOutputTensors(); - loader.loadOperators(); - } -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/ModuleLoader.h b/compiler/luci-micro/luci-interpreter/src/loader/ModuleLoader.h deleted file mode 100644 index 11326a2..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/ModuleLoader.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_LOADER_MODULELOADER_H -#define LUCI_INTERPRETER_LOADER_MODULELOADER_H - -#include "core/RuntimeModule.h" -#include "loader/RuntimeToIR.h" -#include "luci_interpreter/MemoryManager.h" - -#include - -#include - -namespace luci_interpreter -{ - -class ModuleLoader -{ -public: - ModuleLoader(const luci::Module *module, RuntimeModule *runtime_module, - RuntimeToIR &runtime_to_ir, - std::unordered_map &node_to_tensor, - IMemoryManager *memory_manager); - - void load(); - -private: - IMemoryManager *_memory_manager; - const luci::Module *_module; - RuntimeModule *_runtime_module; - RuntimeToIR &_runtime_to_ir; - std::unordered_map &_node_to_tensor; - std::unordered_map _graph_to_runtime_graph; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_LOADER_MODULELOADER_H diff --git a/compiler/luci-micro/luci-interpreter/src/loader/RuntimeToIR.h b/compiler/luci-micro/luci-interpreter/src/loader/RuntimeToIR.h deleted file mode 100644 index 9ea8b1f..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/RuntimeToIR.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_LOADER_RUNTIMETOIR_H -#define LUCI_INTERPRETER_LOADER_RUNTIMETOIR_H - -#include "luci_interpreter/core/Tensor.h" - -#include - -#include - -namespace luci_interpreter -{ - -// Maps runtime entities back to IR entities. It is used to implement observing functionality. -struct RuntimeToIR -{ - std::unordered_map tensor_to_node; - std::unordered_map kernel_to_node; -}; - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_LOADER_RUNTIMETOIR_H diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Add.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Add.cpp deleted file mode 100644 index 501e847..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Add.cpp +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Add.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleAdd(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input1 = helper.getInputTensor(node->x()); - const Tensor *input2 = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - AddParams params{}; - params.activation = node->fusedActivationFunction(); - - return std::make_unique(input1, input2, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/ArgMax.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/ArgMax.cpp deleted file mode 100644 index f3ca557..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/ArgMax.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/ArgMax.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleArgMax(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *axis = helper.getInputTensor(node->dimension()); - Tensor *output = helper.getOutputTensor(node); - - ArgMaxParams params{}; - params.output_type = node->output_type(); - - return std::make_unique(input, axis, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/AveragePool2D.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/AveragePool2D.cpp deleted file mode 100644 index a813570..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/AveragePool2D.cpp +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/AveragePool2D.h" -#include - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleAveragePool2D(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->value()); - Tensor *output = helper.getOutputTensor(node); - - Pool2DParams params{}; - params.padding = node->padding(); - params.filter_height = node->filter()->h(); - params.filter_width = node->filter()->w(); - params.stride_height = node->stride()->h(); - params.stride_width = node->stride()->w(); - params.activation = node->fusedActivationFunction(); - - // It is unknown what data will be stored in scratchpad tensor, - // using UINT8 as a most general option - auto scratchpad = std::make_unique(DataType::U8, Shape({}), AffineQuantization{}, ""); - scratchpad->set_observable(false); - scratchpad->set_data_buffer(nullptr); - // If node has execution plan then read memory offsets for scratchpad temporary tensor - // from the beginning of shared memory buffer. - // Used in Static Memory Manager. - // TODO move tensors offset initialization to one place - if (luci::has_execution_plan(node)) - { - const auto execution_plan = luci::get_execution_plan(node); - // Check whether the offset for the current CircleConv2D temporary was found. - if (execution_plan.offsets().size() > 1) - // If this is true, then we keep this offset in scratchpad. - scratchpad->set_offset(execution_plan.offsets().at(1)); - } - Tensor *tmp = helper.getRuntimeGraph(node->graph())->addTensor(std::move(scratchpad)); - - return std::make_unique(input, output, tmp, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/BatchMatMul.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/BatchMatMul.cpp deleted file mode 100644 index 9da2f6d..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/BatchMatMul.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/BatchMatMul.h" -#include - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleBatchMatMul(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *lhs = helper.getInputTensor(node->x()); - const Tensor *rhs = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - auto lhs_scratchpad = - std::make_unique(lhs->element_type(), Shape({}), AffineQuantization{}, ""); - lhs_scratchpad->set_observable(false); - lhs_scratchpad->set_data_buffer(nullptr); - auto rhs_scratchpad = - std::make_unique(rhs->element_type(), Shape({}), AffineQuantization{}, ""); - rhs_scratchpad->set_observable(false); - rhs_scratchpad->set_data_buffer(nullptr); - // If node has execution plan then read memory offsets for scratchpad temporary tensor - // from the beginning of shared memory buffer. - // Used in Static Memory Manager. - // TODO move tensors offset initialization to one place - if (luci::has_execution_plan(node)) - { - const auto execution_plan = luci::get_execution_plan(node); - // Check whether the offset for the current BatchMatMul temporary was found. - if (execution_plan.offsets().size() > 1) - { - assert(execution_plan.offsets().size() == 3); - - // If this is true, then we keep this offset in scratchpad. - lhs_scratchpad->set_offset(execution_plan.offsets().at(1)); - rhs_scratchpad->set_offset(execution_plan.offsets().at(2)); - } - } - Tensor *lhs_tmp = helper.getRuntimeGraph(node->graph())->addTensor(std::move(lhs_scratchpad)); - Tensor *rhs_tmp = helper.getRuntimeGraph(node->graph())->addTensor(std::move(rhs_scratchpad)); - - BatchMatMulParams params; - params.adj_x = node->adj_x(); - params.adj_y = node->adj_y(); - - return std::make_unique(lhs, rhs, output, lhs_tmp, rhs_tmp, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/BatchToSpaceND.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/BatchToSpaceND.cpp deleted file mode 100644 index ac6ebb3..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/BatchToSpaceND.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/BatchToSpaceND.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleBatchToSpaceND(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 3); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *block_shape = helper.getInputTensor(node->block_shape()); - const Tensor *crops = helper.getInputTensor(node->crops()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, block_shape, crops, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Builders.h b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Builders.h deleted file mode 100644 index eab2840..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Builders.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef LUCI_INTERPRETER_LOADER_NODES_BUILDERS_H -#define LUCI_INTERPRETER_LOADER_NODES_BUILDERS_H - -#include "loader/KernelBuilderHelper.h" - -#include "luci/IR/CircleNodes.h" - -namespace luci_interpreter -{ - -#define REGISTER_KERNEL(name) \ - std::unique_ptr build_kernel_Circle##name(const luci::CircleNode *circle_node, \ - KernelBuilderHelper &helper); - -#include "KernelsToBuild.lst" - -#undef REGISTER_KERNEL - -} // namespace luci_interpreter - -#endif // LUCI_INTERPRETER_LOADER_NODES_BUILDERS_H diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Cast.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Cast.cpp deleted file mode 100644 index a16354c..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Cast.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Cast.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleCast(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->x()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Concatenation.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Concatenation.cpp deleted file mode 100644 index ba2564e..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Concatenation.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Concatenation.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleConcatenation(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - std::vector inputs(node->numValues()); - for (uint32_t i = 0; i < node->numValues(); ++i) - { - inputs[i] = helper.getInputTensor(node->values(i)); - } - Tensor *output = helper.getOutputTensor(node); - - ConcatenationParams params{}; - params.axis = node->axis(); - params.activation = node->fusedActivationFunction(); - - return std::make_unique(std::move(inputs), output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Conv2D.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Conv2D.cpp deleted file mode 100644 index 218165e..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Conv2D.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Conv2D.h" -#include - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleConv2D(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 3); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *filter = helper.getInputTensor(node->filter()); - const Tensor *bias = helper.getOptionalInputTensor(node->bias()); - Tensor *output = helper.getOutputTensor(node); - - // It is unknown what data will be stored in scratchpad tensor, - // using UINT8 as a most general option - auto scratchpad = std::make_unique(DataType::U8, Shape({}), AffineQuantization{}, ""); - scratchpad->set_observable(false); - scratchpad->set_data_buffer(nullptr); - // If node has execution plan then read memory offsets for scratchpad temporary tensor - // from the beginning of shared memory buffer. - // Used in Static Memory Manager. - // TODO move tensors offset initialization to one place - if (luci::has_execution_plan(node)) - { - const auto execution_plan = luci::get_execution_plan(node); - // Check whether the offset for the current CircleConv2D temporary was found. - if (execution_plan.offsets().size() > 1) - // If this is true, then we keep this offset in scratchpad. - scratchpad->set_offset(execution_plan.offsets().at(1)); - } - Tensor *tmp = helper.getRuntimeGraph(node->graph())->addTensor(std::move(scratchpad)); - - Conv2DParams params{}; - params.padding = node->padding(); - params.stride_height = node->stride()->h(); - params.stride_width = node->stride()->w(); - params.dilation_height_factor = node->dilation()->h(); - params.dilation_width_factor = node->dilation()->w(); - params.activation = node->fusedActivationFunction(); - - return std::make_unique(input, filter, bias, output, tmp, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/DepthToSpace.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/DepthToSpace.cpp deleted file mode 100644 index 1749463..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/DepthToSpace.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/DepthToSpace.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleDepthToSpace(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->input()); - Tensor *output = helper.getOutputTensor(node); - - DepthToSpaceParams params{}; - params.block_size = node->block_size(); - - return std::make_unique(input, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/DepthwiseConv2D.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/DepthwiseConv2D.cpp deleted file mode 100644 index 8af1e3b..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/DepthwiseConv2D.cpp +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/DepthwiseConv2D.h" -#include - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleDepthwiseConv2D(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 3); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *filter = helper.getInputTensor(node->filter()); - const Tensor *bias = helper.getInputTensor(node->bias()); - Tensor *output = helper.getOutputTensor(node); - - DepthwiseConv2DParams params{}; - params.padding = node->padding(); - params.depth_multiplier = node->depthMultiplier(); - params.stride_height = node->stride()->h(); - params.stride_width = node->stride()->w(); - params.dilation_height_factor = node->dilation()->h(); - params.dilation_width_factor = node->dilation()->w(); - params.activation = node->fusedActivationFunction(); - - // It is unknown what data will be stored in scratchpad tensor, - // using UINT8 as a most general option - auto scratchpad = std::make_unique(DataType::U8, Shape({}), AffineQuantization{}, ""); - scratchpad->set_observable(false); - scratchpad->set_data_buffer(nullptr); - // If node has execution plan then read memory offsets for scratchpad temporary tensor - // from the beginning of shared memory buffer. - // Used in Static Memory Manager. - // TODO move tensors offset initialization to one place - if (luci::has_execution_plan(node)) - { - const auto execution_plan = luci::get_execution_plan(node); - // Check whether the offset for the current CircleConv2D temporary was found. - if (execution_plan.offsets().size() > 1) - // If this is true, then we keep this offset in scratchpad. - scratchpad->set_offset(execution_plan.offsets().at(1)); - } - Tensor *tmp = helper.getRuntimeGraph(node->graph())->addTensor(std::move(scratchpad)); - - return std::make_unique(input, filter, bias, output, tmp, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Dequantize.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Dequantize.cpp deleted file mode 100644 index 787322e..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Dequantize.cpp +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Dequantize.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleDequantize(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - - const Tensor *input = helper.getInputTensor(node->input()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Div.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Div.cpp deleted file mode 100644 index 0611dfd..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Div.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Div.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleDiv(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - const Tensor *input1 = helper.getInputTensor(node->x()); - const Tensor *input2 = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - DivParams params{}; - params.activation = node->fusedActivationFunction(); - - return std::make_unique(input1, input2, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Elu.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Elu.cpp deleted file mode 100644 index a79985e..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Elu.cpp +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Elu.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleElu(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->features()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Equal.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Equal.cpp deleted file mode 100644 index 5969288..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Equal.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Equal.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleEqual(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) - -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *x = helper.getInputTensor(node->x()); - const Tensor *y = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(x, y, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Exp.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Exp.cpp deleted file mode 100644 index 30d11cb..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Exp.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Exp.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleExp(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->x()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/ExpandDims.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/ExpandDims.cpp deleted file mode 100644 index 9840c34..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/ExpandDims.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/ExpandDims.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleExpandDims(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *axis = helper.getInputTensor(node->axis()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, axis, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Fill.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Fill.cpp deleted file mode 100644 index 3aefdf1..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Fill.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Fill.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleFill(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const auto dims = helper.getInputTensor(node->dims()); - const auto value = helper.getInputTensor(node->value()); - auto output = helper.getOutputTensor(node); - - return std::make_unique(dims, value, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Floor.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Floor.cpp deleted file mode 100644 index e0a2231..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Floor.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Floor.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleFloor(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->x()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/FloorDiv.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/FloorDiv.cpp deleted file mode 100644 index a45d89e..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/FloorDiv.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/FloorDiv.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleFloorDiv(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *x = helper.getInputTensor(node->x()); - const Tensor *y = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(x, y, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/FullyConnected.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/FullyConnected.cpp deleted file mode 100644 index b7b742b..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/FullyConnected.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/FullyConnected.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleFullyConnected(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 3); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *weights = helper.getInputTensor(node->weights()); - const Tensor *bias = helper.getOptionalInputTensor(node->bias()); - Tensor *output = helper.getOutputTensor(node); - - FullyConnectedParams params{}; - params.activation = node->fusedActivationFunction(); - params.keep_num_dims = node->keep_num_dims(); - - return std::make_unique(input, weights, bias, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Gather.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Gather.cpp deleted file mode 100644 index 2ee2906..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Gather.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Gather.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleGather(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *params = helper.getInputTensor(node->params()); - const Tensor *indices = helper.getInputTensor(node->indices()); - Tensor *output = helper.getOutputTensor(node); - - GatherParams gparams{}; - gparams.axis = node->axis(); - // TODO support batch_dims - gparams.batch_dims = 0; - - return std::make_unique(params, indices, output, gparams); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Greater.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Greater.cpp deleted file mode 100644 index 80aa63c..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Greater.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Greater.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleGreater(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *x = helper.getInputTensor(node->x()); - const Tensor *y = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(x, y, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/GreaterEqual.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/GreaterEqual.cpp deleted file mode 100644 index 272f284..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/GreaterEqual.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/GreaterEqual.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleGreaterEqual(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *x = helper.getInputTensor(node->x()); - const Tensor *y = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(x, y, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/If.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/If.cpp deleted file mode 100644 index 3ac7d49..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/If.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/If.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleIf(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - auto output_nodes = collectOutputNodes(node); - assert(node->arity() == 1 + node->input_count()); - assert(output_nodes.size() == static_cast(node->output_count())); - - const Tensor *cond = helper.getInputTensor(node->cond()); - std::vector inputs(node->input_count()); - for (uint32_t i = 0; i < node->input_count(); ++i) - { - inputs[i] = helper.getInputTensor(node->input(i)); - } - std::vector outputs = helper.getOutputTensors(output_nodes); - - RuntimeGraph *then_graph = helper.getRuntimeGraph(node->then_graph()); - RuntimeGraph *else_graph = helper.getRuntimeGraph(node->else_graph()); - - return std::make_unique(cond, std::move(inputs), std::move(outputs), then_graph, - else_graph); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/InstanceNorm.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/InstanceNorm.cpp deleted file mode 100644 index 06031e5..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/InstanceNorm.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/InstanceNorm.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleInstanceNorm(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 3); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *gamma = helper.getInputTensor(node->gamma()); - const Tensor *beta = helper.getInputTensor(node->beta()); - - Tensor *output = helper.getOutputTensor(node); - - InstanceNormParams params{}; - params.epsilon = node->epsilon(); - params.activation = node->fusedActivationFunction(); - - return std::make_unique(input, gamma, beta, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/L2Normalize.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/L2Normalize.cpp deleted file mode 100644 index 6e22e6d..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/L2Normalize.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/L2Normalize.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleL2Normalize(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->x()); - Tensor *output = helper.getOutputTensor(node); - - L2NormParams params{}; - params.activation = node->fusedActivationFunction(); - - return std::make_unique(input, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/L2Pool2D.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/L2Pool2D.cpp deleted file mode 100644 index 95b5589..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/L2Pool2D.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/L2Pool2D.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleL2Pool2D(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->value()); - Tensor *output = helper.getOutputTensor(node); - - Pool2DParams params{}; - params.padding = node->padding(); - params.filter_height = node->filter()->h(); - params.filter_width = node->filter()->w(); - params.stride_height = node->stride()->h(); - params.stride_width = node->stride()->w(); - params.activation = node->fusedActivationFunction(); - - return std::make_unique(input, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LeakyRelu.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/LeakyRelu.cpp deleted file mode 100644 index bbf5067..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LeakyRelu.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/LeakyRelu.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleLeakyRelu(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - const Tensor *input = helper.getInputTensor(node->features()); - Tensor *output = helper.getOutputTensor(node); - - LeakyReluParams params{}; - params.alpha = node->alpha(); - - return std::make_unique(input, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Less.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Less.cpp deleted file mode 100644 index ae914ec..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Less.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Less.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleLess(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *x = helper.getInputTensor(node->x()); - const Tensor *y = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(x, y, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LessEqual.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/LessEqual.cpp deleted file mode 100644 index f1b424b..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LessEqual.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/LessEqual.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleLessEqual(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *x = helper.getInputTensor(node->x()); - const Tensor *y = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(x, y, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LocalResponseNormalization.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/LocalResponseNormalization.cpp deleted file mode 100644 index 962ca2d..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LocalResponseNormalization.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/LocalResponseNormalization.h" - -namespace luci_interpreter -{ - -std::unique_ptr -build_kernel_CircleLocalResponseNormalization(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - const Tensor *input = helper.getInputTensor(node->input()); - Tensor *output = helper.getOutputTensor(node); - - LocalResponseNormalizationParams params{}; - params.radius = node->radius(); - params.bias = node->bias(); - params.alpha = node->alpha(); - params.beta = node->beta(); - - return std::make_unique(input, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogSoftmax.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogSoftmax.cpp deleted file mode 100644 index 4322041..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogSoftmax.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/LogSoftmax.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleLogSoftmax(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->logits()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalAnd.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalAnd.cpp deleted file mode 100644 index bf3cb67..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalAnd.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/LogicalAnd.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleLogicalAnd(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input1 = helper.getInputTensor(node->x()); - const Tensor *input2 = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input1, input2, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalNot.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalNot.cpp deleted file mode 100644 index fefcd9a..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalNot.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/LogicalNot.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleLogicalNot(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->x()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalOr.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalOr.cpp deleted file mode 100644 index a416cb4..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/LogicalOr.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/LogicalOr.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleLogicalOr(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input1 = helper.getInputTensor(node->x()); - const Tensor *input2 = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input1, input2, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Logistic.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Logistic.cpp deleted file mode 100644 index 4a69dee..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Logistic.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Logistic.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleLogistic(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->x()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/MaxPool2D.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/MaxPool2D.cpp deleted file mode 100644 index f66a206..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/MaxPool2D.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/MaxPool2D.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleMaxPool2D(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->value()); - Tensor *output = helper.getOutputTensor(node); - - Pool2DParams params{}; - params.padding = node->padding(); - params.filter_height = node->filter()->h(); - params.filter_width = node->filter()->w(); - params.stride_height = node->stride()->h(); - params.stride_width = node->stride()->w(); - params.activation = node->fusedActivationFunction(); - - return std::make_unique(input, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Maximum.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Maximum.cpp deleted file mode 100644 index d0bff77..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Maximum.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Maximum.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleMaximum(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input1 = helper.getInputTensor(node->x()); - const Tensor *input2 = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input1, input2, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Mean.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Mean.cpp deleted file mode 100644 index 0dec63e..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Mean.cpp +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Mean.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleMean(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *axes = helper.getInputTensor(node->reduction_indices()); - Tensor *output = helper.getOutputTensor(node); - - auto temp_index_unique = - std::make_unique(DataType::S32, Shape({}), AffineQuantization{}, ""); - temp_index_unique->set_observable(false); - temp_index_unique->set_data_buffer(nullptr); - Tensor *temp_index = - helper.getRuntimeGraph(node->graph())->addTensor(std::move(temp_index_unique)); - - auto resolved_axes_unique = - std::make_unique(DataType::S32, Shape({}), AffineQuantization{}, ""); - resolved_axes_unique->set_observable(false); - resolved_axes_unique->set_data_buffer(nullptr); - Tensor *resolved_axes = - helper.getRuntimeGraph(node->graph())->addTensor(std::move(resolved_axes_unique)); - - auto temp_sum_unique = - std::make_unique(input->element_type(), Shape({}), AffineQuantization{}, ""); - temp_sum_unique->set_observable(false); - temp_sum_unique->set_data_buffer(nullptr); - Tensor *temp_sum = helper.getRuntimeGraph(node->graph())->addTensor(std::move(temp_sum_unique)); - - ReducerParams params{}; - params.keep_dims = node->keep_dims(); - - return std::make_unique(input, axes, output, temp_index, resolved_axes, temp_sum, - params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Minimum.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Minimum.cpp deleted file mode 100644 index 1a49c10..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Minimum.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Minimum.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleMinimum(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input1 = helper.getInputTensor(node->x()); - const Tensor *input2 = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input1, input2, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/MirrorPad.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/MirrorPad.cpp deleted file mode 100644 index b221b45..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/MirrorPad.cpp +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/MirrorPad.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleMirrorPad(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *paddings = helper.getInputTensor(node->paddings()); - Tensor *output = helper.getOutputTensor(node); - - MirrorPadParams params{}; - params.mode = node->mode(); - - return std::make_unique(input, paddings, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Mul.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Mul.cpp deleted file mode 100644 index f998485..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Mul.cpp +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Mul.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleMul(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input1 = helper.getInputTensor(node->x()); - const Tensor *input2 = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - MulParams params{}; - params.activation = node->fusedActivationFunction(); - - return std::make_unique(input1, input2, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Neg.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Neg.cpp deleted file mode 100644 index 9a9ecf9..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Neg.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Neg.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleNeg(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->x()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/NotEqual.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/NotEqual.cpp deleted file mode 100644 index 3916a58..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/NotEqual.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/NotEqual.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleNotEqual(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *x = helper.getInputTensor(node->x()); - const Tensor *y = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(x, y, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/OneHot.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/OneHot.cpp deleted file mode 100644 index a401609..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/OneHot.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/OneHot.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleOneHot(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 4); - - const Tensor *indices = helper.getInputTensor(node->indices()); - const Tensor *depth = helper.getInputTensor(node->depth()); - const Tensor *on_value = helper.getInputTensor(node->on_value()); - const Tensor *off_value = helper.getInputTensor(node->off_value()); - Tensor *output = helper.getOutputTensor(node); - - OneHotParams params{}; - params.axis = node->axis(); - - return std::make_unique(indices, depth, on_value, off_value, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/PRelu.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/PRelu.cpp deleted file mode 100644 index f3d700c..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/PRelu.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/PRelu.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CirclePRelu(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *alpha = helper.getInputTensor(node->alpha()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, alpha, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Pack.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Pack.cpp deleted file mode 100644 index efc5850..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Pack.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Pack.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CirclePack(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == node->values_count()); - - std::vector inputs(node->values_count()); - for (uint32_t i = 0; i < node->values_count(); ++i) - { - inputs[i] = helper.getInputTensor(node->values(i)); - } - Tensor *output = helper.getOutputTensor(node); - - PackParams params{}; - params.axis = node->axis(); - params.values_count = node->values_count(); - - return std::make_unique(std::move(inputs), output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Pad.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Pad.cpp deleted file mode 100644 index 67ce997..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Pad.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Pad.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CirclePad(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *paddings = helper.getInputTensor(node->paddings()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, paddings, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/PadV2.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/PadV2.cpp deleted file mode 100644 index e378a97..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/PadV2.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/PadV2.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CirclePadV2(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 3); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *paddings = helper.getInputTensor(node->paddings()); - const Tensor *constant_values = helper.getInputTensor(node->constant_values()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, paddings, constant_values, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Pow.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Pow.cpp deleted file mode 100644 index d32fc3d..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Pow.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Pow.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CirclePow(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input1 = helper.getInputTensor(node->x()); - const Tensor *input2 = helper.getInputTensor(node->y()); - - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input1, input2, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Quantize.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Quantize.cpp deleted file mode 100644 index cb36fb6..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Quantize.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Quantize.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleQuantize(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->input()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Relu.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Relu.cpp deleted file mode 100644 index 1d64c1c..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Relu.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Relu.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleRelu(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->features()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Relu6.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Relu6.cpp deleted file mode 100644 index e50cd25..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Relu6.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Relu6.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleRelu6(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->features()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Reshape.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Reshape.cpp deleted file mode 100644 index 76ddd88..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Reshape.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Reshape.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleReshape(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input = helper.getInputTensor(node->tensor()); - const Tensor *shape = helper.getInputTensor(node->shape()); - Tensor *output = helper.getOutputTensor(node); - - // NOTE 'newShape' attribute is ignored. - return std::make_unique(input, shape, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/ResizeBilinear.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/ResizeBilinear.cpp deleted file mode 100644 index dc2b88a..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/ResizeBilinear.cpp +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/ResizeBilinear.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleResizeBilinear(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *size = helper.getInputTensor(node->size()); - Tensor *output = helper.getOutputTensor(node); - - ResizeBilinearParams params{}; - params.align_corners = node->align_corners(); - params.half_pixel_centers = node->half_pixel_centers(); - - return std::make_unique(input, size, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/ResizeNearestNeighbor.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/ResizeNearestNeighbor.cpp deleted file mode 100644 index c7058ae..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/ResizeNearestNeighbor.cpp +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/ResizeNearestNeighbor.h" - -namespace luci_interpreter -{ - -std::unique_ptr -build_kernel_CircleResizeNearestNeighbor(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *size = helper.getInputTensor(node->size()); - Tensor *output = helper.getOutputTensor(node); - - ResizeNearestNeighborParams params{}; - params.align_corners = node->align_corners(); - // TODO update half_pixel_centers after CircleResizeNearestNeighbor updated - // Current CircleResizeNearestNeighbor don't have half_pixel_centers. - // default value on current is false. - // it need to be updated when CircleResizeNearestNeighbor updated. - params.half_pixel_centers = false; - - return std::make_unique(input, size, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/ReverseV2.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/ReverseV2.cpp deleted file mode 100644 index c1a7f53..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/ReverseV2.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/ReverseV2.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleReverseV2(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input = helper.getInputTensor(node->tensor()); - const Tensor *axes = helper.getInputTensor(node->axis()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, axes, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Rsqrt.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Rsqrt.cpp deleted file mode 100644 index 0714a5d..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Rsqrt.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Rsqrt.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleRsqrt(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->x()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/SVDF.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/SVDF.cpp deleted file mode 100644 index d172ef4..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/SVDF.cpp +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/SVDF.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSVDF(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 5); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *feature = helper.getInputTensor(node->weight_feature()); - const Tensor *time = helper.getInputTensor(node->weight_time()); - const Tensor *bias = helper.getOptionalInputTensor(node->bias()); - const Tensor *input_activation_state = helper.getInputTensor(node->input_activation_state()); - Tensor *output = helper.getOutputTensor(node); - - auto scratchpad_tensor = std::make_unique(input_activation_state->element_type(), - Shape({}), AffineQuantization{}, ""); - scratchpad_tensor->set_observable(false); - scratchpad_tensor->set_data_buffer(nullptr); - Tensor *tmp = helper.getRuntimeGraph(node->graph())->addTensor(std::move(scratchpad_tensor)); - - DataType data_type = input->element_type() == DataType::S8 ? DataType::S32 : DataType::FLOAT32; - - scratchpad_tensor = std::make_unique(data_type, Shape({}), AffineQuantization{}, ""); - scratchpad_tensor->set_observable(false); - scratchpad_tensor->set_data_buffer(nullptr); - Tensor *tmp_1 = helper.getRuntimeGraph(node->graph())->addTensor(std::move(scratchpad_tensor)); - - if (data_type == DataType::FLOAT32 && - (feature->element_type() == DataType::S8 || feature->element_type() == DataType::U8)) - { - data_type = feature->element_type(); - } - - scratchpad_tensor = std::make_unique(data_type, Shape({}), AffineQuantization{}, ""); - scratchpad_tensor->set_observable(false); - scratchpad_tensor->set_data_buffer(nullptr); - Tensor *tmp_2 = helper.getRuntimeGraph(node->graph())->addTensor(std::move(scratchpad_tensor)); - - data_type = DataType::FLOAT32; - - scratchpad_tensor = std::make_unique(data_type, Shape({}), AffineQuantization{}, ""); - scratchpad_tensor->set_observable(false); - scratchpad_tensor->set_data_buffer(nullptr); - Tensor *tmp_3 = helper.getRuntimeGraph(node->graph())->addTensor(std::move(scratchpad_tensor)); - - scratchpad_tensor = std::make_unique(data_type, Shape({}), AffineQuantization{}, ""); - scratchpad_tensor->set_observable(false); - scratchpad_tensor->set_data_buffer(nullptr); - Tensor *tmp_4 = helper.getRuntimeGraph(node->graph())->addTensor(std::move(scratchpad_tensor)); - - scratchpad_tensor = std::make_unique(data_type, Shape({}), AffineQuantization{}, ""); - scratchpad_tensor->set_observable(false); - scratchpad_tensor->set_data_buffer(nullptr); - Tensor *tmp_5 = helper.getRuntimeGraph(node->graph())->addTensor(std::move(scratchpad_tensor)); - - scratchpad_tensor = std::make_unique(data_type, Shape({}), AffineQuantization{}, ""); - scratchpad_tensor->set_observable(false); - scratchpad_tensor->set_data_buffer(nullptr); - Tensor *tmp_6 = helper.getRuntimeGraph(node->graph())->addTensor(std::move(scratchpad_tensor)); - - SVDFParams params{}; - params.activation = node->fusedActivationFunction(); - params.svdf_rank = node->svdf_rank(); - params.asymmetric_quantize_inputs = node->asymmetric_quantize_inputs(); - - return std::make_unique(input, feature, time, bias, input_activation_state, output, - tmp, tmp_1, tmp_2, tmp_3, tmp_4, tmp_5, tmp_6, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Shape.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Shape.cpp deleted file mode 100644 index d1edbc7..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Shape.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Shape.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleShape(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const auto input = helper.getInputTensor(node->input()); - auto output = helper.getOutputTensor(node); - - ShapeParams shape_params{}; - shape_params.out_type = node->out_type(); - - return std::make_unique(input, output, shape_params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Slice.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Slice.cpp deleted file mode 100644 index 60ac641..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Slice.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Slice.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSlice(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 3); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *begin = helper.getInputTensor(node->begin()); - const Tensor *size = helper.getInputTensor(node->size()); - - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, begin, size, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Softmax.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Softmax.cpp deleted file mode 100644 index f41f63f..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Softmax.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Softmax.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSoftmax(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->logits()); - Tensor *output = helper.getOutputTensor(node); - - SoftmaxParams params{}; - params.beta = node->beta(); - - return std::make_unique(input, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/SpaceToBatchND.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/SpaceToBatchND.cpp deleted file mode 100644 index b6e6cf5..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/SpaceToBatchND.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/SpaceToBatchND.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSpaceToBatchND(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 3); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *block_shape = helper.getInputTensor(node->block_shape()); - const Tensor *paddings = helper.getInputTensor(node->paddings()); - - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, block_shape, paddings, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/SpaceToDepth.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/SpaceToDepth.cpp deleted file mode 100644 index 63fdb95..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/SpaceToDepth.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/SpaceToDepth.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSpaceToDepth(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - const Tensor *input = helper.getInputTensor(node->input()); - - Tensor *output = helper.getOutputTensor(node); - - SpaceToDepthParams params{}; - params.block_size = node->block_size(); - - return std::make_unique(input, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Split.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Split.cpp deleted file mode 100644 index 3f6d4a7..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Split.cpp +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Split.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSplit(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - auto output_nodes = collectOutputNodes(node); - assert(node->arity() == 2); - assert(output_nodes.size() == static_cast(node->num_split())); - - const Tensor *axis = helper.getInputTensor(node->split_dim()); - const Tensor *input = helper.getInputTensor(node->input()); - std::vector outputs = helper.getOutputTensors(output_nodes); - - // NOTE 'num_splits' attribute is ignored. - return std::make_unique(axis, input, std::move(outputs)); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/SplitV.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/SplitV.cpp deleted file mode 100644 index 0788822..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/SplitV.cpp +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/SplitV.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSplitV(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - auto output_nodes = collectOutputNodes(node); - assert(node->arity() == 3); - assert(output_nodes.size() == static_cast(node->num_split())); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *sizes_data = helper.getInputTensor(node->size_splits()); - const Tensor *axis = helper.getInputTensor(node->split_dim()); - std::vector outputs = helper.getOutputTensors(output_nodes); - - // NOTE 'num_splits' attribute is ignored. - return std::make_unique(input, sizes_data, axis, std::move(outputs)); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Sqrt.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Sqrt.cpp deleted file mode 100644 index b9843fe..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Sqrt.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Sqrt.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSqrt(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->x()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Square.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Square.cpp deleted file mode 100644 index 0ad7c17..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Square.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Square.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSquare(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->x()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/SquaredDifference.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/SquaredDifference.cpp deleted file mode 100644 index e4c6fd8..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/SquaredDifference.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/SquaredDifference.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSquaredDifference(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input1 = helper.getInputTensor(node->x()); - const Tensor *input2 = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input1, input2, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Squeeze.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Squeeze.cpp deleted file mode 100644 index 6885f80..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Squeeze.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Squeeze.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSqueeze(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->input()); - Tensor *output = helper.getOutputTensor(node); - - SqueezeParams params{}; - params.squeeze_dims = node->squeeze_dims(); - - return std::make_unique(input, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/StridedSlice.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/StridedSlice.cpp deleted file mode 100644 index 359b4e3..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/StridedSlice.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/StridedSlice.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleStridedSlice(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 4); - - const Tensor *input = helper.getInputTensor(node->input()); - const Tensor *begin = helper.getInputTensor(node->begin()); - const Tensor *end = helper.getInputTensor(node->end()); - const Tensor *strides = helper.getInputTensor(node->strides()); - - Tensor *output = helper.getOutputTensor(node); - - StridedSliceParams params{}; - params.begin_mask = node->begin_mask(); - params.ellipsis_mask = node->ellipsis_mask(); - params.end_mask = node->end_mask(); - params.new_axis_mask = node->new_axis_mask(); - params.shrink_axis_mask = node->shrink_axis_mask(); - - return std::make_unique(input, begin, end, strides, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Sub.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Sub.cpp deleted file mode 100644 index a6252cb..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Sub.cpp +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Sub.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleSub(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input1 = helper.getInputTensor(node->x()); - const Tensor *input2 = helper.getInputTensor(node->y()); - Tensor *output = helper.getOutputTensor(node); - - SubParams params{}; - params.activation = node->fusedActivationFunction(); - - return std::make_unique(input1, input2, output, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Tanh.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Tanh.cpp deleted file mode 100644 index a58ef60..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Tanh.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Tanh.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleTanh(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 1); - - const Tensor *input = helper.getInputTensor(node->x()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Transpose.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Transpose.cpp deleted file mode 100644 index ea17d83..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Transpose.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Transpose.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleTranspose(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 2); - - const Tensor *input = helper.getInputTensor(node->a()); - const Tensor *perm = helper.getInputTensor(node->perm()); - Tensor *output = helper.getOutputTensor(node); - - return std::make_unique(input, perm, output); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/TransposeConv.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/TransposeConv.cpp deleted file mode 100644 index d773e30..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/TransposeConv.cpp +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/TransposeConv.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleTransposeConv(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - assert(node->arity() == 4); - - const Tensor *input_sizes = helper.getInputTensor(node->inputSizes()); - const Tensor *filter = helper.getInputTensor(node->filter()); - const Tensor *out_backprop = helper.getInputTensor(node->outBackprop()); - const Tensor *bias = helper.getOptionalInputTensor(node->bias()); - - Tensor *output = helper.getOutputTensor(node); - - DataType scratch_data_type = - helper.getInputTensor(node)->element_type() == DataType::S16 ? DataType::S64 : DataType::S32; - - auto scratch_tensor = - std::make_unique(scratch_data_type, Shape({}), AffineQuantization{}, ""); - scratch_tensor->set_observable(false); - scratch_tensor->set_data_buffer(nullptr); - Tensor *tmp = helper.getRuntimeGraph(node->graph())->addTensor(std::move(scratch_tensor)); - - TransposeConvParams params{}; - params.padding = node->padding(); - params.stride_height = node->stride()->h(); - params.stride_width = node->stride()->w(); - - return std::make_unique(input_sizes, filter, out_backprop, bias, output, - tmp, params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Unpack.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/Unpack.cpp deleted file mode 100644 index a1c0d32..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/Unpack.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/Unpack.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleUnpack(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - auto output_nodes = collectOutputNodes(node); - assert(node->arity() == 1); - assert(output_nodes.size() == static_cast(node->num())); - - const Tensor *input = helper.getInputTensor(node->value()); - std::vector outputs = helper.getOutputTensors(output_nodes); - - UnpackParams params{}; - params.axis = node->axis(); - - // NOTE 'num' attribute is ignored. - return std::make_unique(input, std::move(outputs), params); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/loader/nodes/While.cpp b/compiler/luci-micro/luci-interpreter/src/loader/nodes/While.cpp deleted file mode 100644 index 8fde6ec..0000000 --- a/compiler/luci-micro/luci-interpreter/src/loader/nodes/While.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Builders.h" - -#include "kernels/While.h" - -namespace luci_interpreter -{ - -std::unique_ptr build_kernel_CircleWhile(const luci::CircleNode *circle_node, - KernelBuilderHelper &helper) -{ - const auto *node = loco::must_cast(circle_node); - - auto output_nodes = collectOutputNodes(node); - assert(node->arity() == node->input_count()); - assert(output_nodes.size() == static_cast(node->output_count())); - - std::vector inputs(node->input_count()); - for (uint32_t i = 0; i < node->input_count(); ++i) - { - inputs[i] = helper.getInputTensor(node->input(i)); - } - std::vector outputs = helper.getOutputTensors(output_nodes); - - RuntimeGraph *cond_graph = helper.getRuntimeGraph(node->cond_graph()); - RuntimeGraph *body_graph = helper.getRuntimeGraph(node->body_graph()); - - return std::make_unique(std::move(inputs), std::move(outputs), cond_graph, - body_graph); -} - -} // namespace luci_interpreter diff --git a/compiler/luci-micro/requires.cmake b/compiler/luci-micro/requires.cmake deleted file mode 100644 index 5913aa9..0000000 --- a/compiler/luci-micro/requires.cmake +++ /dev/null @@ -1 +0,0 @@ -require(luci-interpreter) diff --git a/compiler/luci-micro/standalone/CMakeLists.txt b/compiler/luci-micro/standalone/CMakeLists.txt deleted file mode 100644 index d304826..0000000 --- a/compiler/luci-micro/standalone/CMakeLists.txt +++ /dev/null @@ -1,37 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(luci_interpreter_micro_standalone) - -# Add fake target, so nothing is build -set(BUILD_WHITELIST "dummy") - -add_subdirectory(${NNAS_ROOT}/infra/nncc ${CMAKE_CURRENT_BINARY_DIR}/nncc) - -set(ONE_COMPILER_SRC_DIR "${NNAS_PROJECT_SOURCE_DIR}/compiler") -nnas_find_package(FlatBuffersSource EXACT 2.0 QUIET) - -include_directories(${FlatBuffersSource_DIR}/include) - -add_subdirectory(${ONE_COMPILER_SRC_DIR}/loco ${CMAKE_CURRENT_BINARY_DIR}/loco) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/angkor ${CMAKE_CURRENT_BINARY_DIR}/angkor) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/oops ${CMAKE_CURRENT_BINARY_DIR}/oops) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/pepper-str ${CMAKE_CURRENT_BINARY_DIR}/pepper-str) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/logo ${CMAKE_CURRENT_BINARY_DIR}/logo) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/logo-core ${CMAKE_CURRENT_BINARY_DIR}/logo-core) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/hermes-std ${CMAKE_CURRENT_BINARY_DIR}/hermes-std) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/hermes ${CMAKE_CURRENT_BINARY_DIR}/hermes) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/pepper-strcast ${CMAKE_CURRENT_BINARY_DIR}/pepper-strcast) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/foder ${CMAKE_CURRENT_BINARY_DIR}/foder) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/mio-circle04 ${CMAKE_CURRENT_BINARY_DIR}/mio-circle04) - -add_subdirectory(${ONE_COMPILER_SRC_DIR}/locomotiv ${CMAKE_CURRENT_BINARY_DIR}/locomotiv) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/luci/lang ${CMAKE_CURRENT_BINARY_DIR}/luci/lang) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/luci/import ${CMAKE_CURRENT_BINARY_DIR}/luci/import) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/luci/profile ${CMAKE_CURRENT_BINARY_DIR}/luci/profile) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/luci/env ${CMAKE_CURRENT_BINARY_DIR}/luci/env) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/luci/plan ${CMAKE_CURRENT_BINARY_DIR}/luci/plan) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/luci/log ${CMAKE_CURRENT_BINARY_DIR}/luci/log) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/luci/logex ${CMAKE_CURRENT_BINARY_DIR}/luci/logex) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/locop ${CMAKE_CURRENT_BINARY_DIR}/locop) -add_subdirectory(${ONE_COMPILER_SRC_DIR}/pp ${CMAKE_CURRENT_BINARY_DIR}/pp) - -add_subdirectory(${ONE_COMPILER_SRC_DIR}/luci-micro/luci-interpreter ${CMAKE_CURRENT_BINARY_DIR}/luci-interpreter) diff --git a/compiler/luci-pass-value-test/CMakeLists.txt b/compiler/luci-pass-value-test/CMakeLists.txt index 3489f1e..c86d2cd 100644 --- a/compiler/luci-pass-value-test/CMakeLists.txt +++ b/compiler/luci-pass-value-test/CMakeLists.txt @@ -53,3 +53,14 @@ add_test(NAME luci_pass_value_test "$" ${LUCI_PASS_VALUE_TESTS} ) + +if(ONE_UBUNTU_CODENAME_JAMMY) + add_test(NAME luci_pass_value_210_test + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/eval_driver.sh" + "${CMAKE_CURRENT_BINARY_DIR}" + "${ARTIFACTS_BIN_PATH}" + "${NNCC_OVERLAY_DIR}/venv_2_10_1" + "$" + ${LUCI_PASS_VALUE_TESTS} + ) +endif(ONE_UBUNTU_CODENAME_JAMMY) diff --git a/compiler/luci-pass-value-test/test.lst b/compiler/luci-pass-value-test/test.lst index cdff159..b0b938c 100644 --- a/compiler/luci-pass-value-test/test.lst +++ b/compiler/luci-pass-value-test/test.lst @@ -13,11 +13,13 @@ addeval(Net_Conv_Add_Mul_001 fuse_batchnorm_with_conv) addeval(Net_Conv_Add_Mul_002 fuse_batchnorm_with_conv) addeval(Net_Conv_Min_Max_000 transform_min_max_to_relu6) addeval(Net_Conv_Min_Relu_000 transform_min_relu_to_relu6) +addeval(Net_Conv_PReluGraph_000 fuse_prelu) addeval(Net_Conv_Relu6_000 fuse_activation_function) addeval(Net_Densify_Add_000 fold_densify) addeval(Net_Dequantize_Add_000 fold_dequantize) addeval(Net_DwConv_BN_000 fuse_batchnorm_with_dwconv) addeval(Net_DwConv_BN_001 fuse_batchnorm_with_dwconv) +addeval(Net_FullyConnected_Add_000 fold_fully_connected) addeval(Net_Reshape_Neg_000 forward_reshape_to_unaryop) addeval(Net_Reshape_Reshape_000 remove_redundant_reshape) addeval(Net_Squeeze_Squeeze_000 substitute_squeeze_to_reshape) @@ -29,11 +31,16 @@ addeval(Net_TConv_BN_001 fuse_batchnorm_with_tconv) addeval(Net_TConv_BN_002 fuse_batchnorm_with_tconv) addeval(Net_TConv_BN_003 fuse_batchnorm_with_tconv) addeval(Net_TConv_BN_004 fuse_batchnorm_with_tconv) +addeval(Net_TConv_BN_005 fuse_batchnorm_with_tconv) addeval(Net_InstanceNorm_001 fuse_instnorm) addeval(Net_InstanceNorm_002 fuse_instnorm) addeval(Net_InstanceNorm_003 fuse_instnorm) addeval(Net_StridedSlice_StridedSlice_000 remove_unnecessary_strided_slice) addeval(FullyConnected_007 replace_non_const_fc_with_batch_matmul) +addeval(Net_Transpose_Add_000 forward_transpose_op) +addeval(Net_Transpose_Abs_000 forward_transpose_op) +addeval(UnidirectionalSequenceLSTM_003 unroll_unidirseqlstm) +addeval(UnidirectionalSequenceLSTM_004 unroll_unidirseqlstm) # test for limited support for FLOAT16 addeval(Net_Dequantize_Add_000 fold_dequantize) diff --git a/compiler/luci-value-test/CMakeLists.txt b/compiler/luci-value-test/CMakeLists.txt index ebf9c592..b55f602 100644 --- a/compiler/luci-value-test/CMakeLists.txt +++ b/compiler/luci-value-test/CMakeLists.txt @@ -45,6 +45,28 @@ if(NOT CMAKE_CROSSCOMPILING) ) endif() + if(ONE_UBUNTU_CODENAME_JAMMY) + add_test(NAME luci_value_210_test + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/evalverify.sh" + "${CMAKE_CURRENT_BINARY_DIR}" + "${ARTIFACTS_BIN_PATH}" + "${NNCC_OVERLAY_DIR}/venv_2_10_1" + "$" + ${LUCI_VALUE_TESTS} + ) + + if(DEFINED LUCI_VALUE_TESTS_TOL) + add_test(NAME luci_value_tol_210_test + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/evalverifytol.sh" + "${CMAKE_CURRENT_BINARY_DIR}" + "${ARTIFACTS_BIN_PATH}" + "${NNCC_OVERLAY_DIR}/venv_2_10_1" + "$" + ${LUCI_VALUE_TESTS_TOL} + ) + endif() + endif(ONE_UBUNTU_CODENAME_JAMMY) + else(NOT CMAKE_CROSSCOMPILING) # NOTE target test is carried out using reference input/output data from host # test results. this is because it would be difficult to prepare diff --git a/compiler/luci-value-test/luci_eval_verifier.py b/compiler/luci-value-test/luci_eval_verifier.py index 560e34f..f74b220 100755 --- a/compiler/luci-value-test/luci_eval_verifier.py +++ b/compiler/luci-value-test/luci_eval_verifier.py @@ -24,11 +24,16 @@ circle_model = args.model + ".circle" rtolf32 = 1e-5 atolf32 = 1e-5 +# NOTE reuse f32 value as int value too +rtolint = 0 +atolint = 0 try: if args.rtolf32 != None: rtolf32 = float(args.rtolf32) + rtolint = int(rtolf32) if args.atolf32 != None: atolf32 = float(args.atolf32) + atolint = int(atolf32) except ValueError: print("rtolf32 or atolf32 is not a number") quit(128) @@ -117,7 +122,11 @@ for idx in range(len(inpt_output_details)): intp_output_data = interpreter.get_tensor(output_tensor) try: if output_details["dtype"] == np.uint8: - if np.allclose(luci_output_data, intp_output_data, rtol=0, atol=0) == False: + if np.allclose( + luci_output_data, intp_output_data, rtol=rtolint, + atol=atolint) == False: + print("intp_output_data", intp_output_data) + print("luci_output_data", luci_output_data) raise SystemExit("Execution result of " + tflite_model + " does not match with " + circle_model) output_dtype = "uint8" @@ -125,26 +134,42 @@ for idx in range(len(inpt_output_details)): if np.allclose( luci_output_data, intp_output_data, rtol=rtolf32, atol=atolf32) == False: + print("intp_output_data", intp_output_data) + print("luci_output_data", luci_output_data) raise SystemExit("Execution result of " + tflite_model + " does not match with " + circle_model) output_dtype = "float32" elif output_details["dtype"] == np.int64: - if np.allclose(luci_output_data, intp_output_data, rtol=0, atol=0) == False: + if np.allclose( + luci_output_data, intp_output_data, rtol=rtolint, + atol=atolint) == False: + print("intp_output_data", intp_output_data) + print("luci_output_data", luci_output_data) raise SystemExit("Execution result of " + tflite_model + " does not match with " + circle_model) output_dtype = "int64" elif output_details["dtype"] == np.int32: - if np.allclose(luci_output_data, intp_output_data, rtol=0, atol=0) == False: + if np.allclose( + luci_output_data, intp_output_data, rtol=rtolint, + atol=atolint) == False: + print("intp_output_data", intp_output_data) + print("luci_output_data", luci_output_data) raise SystemExit("Execution result of " + tflite_model + " does not match with " + circle_model) output_dtype = "int32" elif output_details["dtype"] == np.int16: - if np.allclose(luci_output_data, intp_output_data, rtol=0, atol=0) == False: + if np.allclose( + luci_output_data, intp_output_data, rtol=rtolint, + atol=atolint) == False: + print("intp_output_data", intp_output_data) + print("luci_output_data", luci_output_data) raise SystemExit("Execution result of " + tflite_model + " does not match with " + circle_model) output_dtype = "int16" elif output_details["dtype"] == np.bool_: if np.allclose(luci_output_data, intp_output_data, rtol=0, atol=0) == False: + print("intp_output_data", intp_output_data) + print("luci_output_data", luci_output_data) raise SystemExit("Execution result of " + tflite_model + " does not match with " + circle_model) output_dtype = "bool" diff --git a/compiler/luci-value-test/luci_eval_verifier_ref.py b/compiler/luci-value-test/luci_eval_verifier_ref.py index 5313e33..3e4d938 100755 --- a/compiler/luci-value-test/luci_eval_verifier_ref.py +++ b/compiler/luci-value-test/luci_eval_verifier_ref.py @@ -55,11 +55,16 @@ circle_model = args.work_path + ".circle" rtolf32 = 1e-5 atolf32 = 1e-5 +# NOTE reuse f32 value as int value too +rtolint = 0 +atolint = 0 try: if args.rtolf32 != None: rtolf32 = float(args.rtolf32) + rtolint = int(rtolf32) if args.atolf32 != None: atolf32 = float(args.atolf32) + atolint = int(atolf32) except ValueError: print("rtolf32 or atolf32 is not a number") quit(128) @@ -113,28 +118,42 @@ for idx in range(num_outputs): try: if output_dtype == np.uint8: if np.allclose( - luci_output_data, luci_output_data_ref, rtol=0, atol=0) == False: + luci_output_data, luci_output_data_ref, rtol=rtolint, + atol=atolint) == False: + print("luci_output_data_ref", luci_output_data_ref) + print("luci_output_data", luci_output_data) raise SystemExit("Execution result of " + circle_model_ref + " does not match with " + circle_model) elif output_dtype == np.float32: if np.allclose( luci_output_data, luci_output_data_ref, rtol=rtolf32, atol=atolf32) == False: + print("luci_output_data_ref", luci_output_data_ref) + print("luci_output_data", luci_output_data) raise SystemExit("Execution result of " + circle_model_ref + " does not match with " + circle_model) elif output_dtype == np.int64: if np.allclose( - luci_output_data, luci_output_data_ref, rtol=0, atol=0) == False: + luci_output_data, luci_output_data_ref, rtol=rtolint, + atol=atolint) == False: + print("luci_output_data_ref", luci_output_data_ref) + print("luci_output_data", luci_output_data) raise SystemExit("Execution result of " + circle_model_ref + " does not match with " + circle_model) elif output_dtype == np.int32: if np.allclose( - luci_output_data, luci_output_data_ref, rtol=0, atol=0) == False: + luci_output_data, luci_output_data_ref, rtol=rtolint, + atol=atolint) == False: + print("luci_output_data_ref", luci_output_data_ref) + print("luci_output_data", luci_output_data) raise SystemExit("Execution result of " + circle_model_ref + " does not match with " + circle_model) elif output_dtype == np.int16: if np.allclose( - luci_output_data, luci_output_data_ref, rtol=0, atol=0) == False: + luci_output_data, luci_output_data_ref, rtol=rtolint, + atol=atolint) == False: + print("luci_output_data_ref", luci_output_data_ref) + print("luci_output_data", luci_output_data) raise SystemExit("Execution result of " + circle_model_ref + " does not match with " + circle_model) elif output_dtype == np.bool_: diff --git a/compiler/luci-value-test/test.lst b/compiler/luci-value-test/test.lst index 932da95..70c3fb1 100644 --- a/compiler/luci-value-test/test.lst +++ b/compiler/luci-value-test/test.lst @@ -174,17 +174,20 @@ addeval(Tanh_000) #addeval(TopKV2_001) addeval(Transpose_000) addeval(TransposeConv_000) +addeval(UnidirectionalSequenceLSTM_002) +addeval(UnidirectionalSequenceLSTM_003) +addeval(UnidirectionalSequenceLSTM_004) addeval(Unpack_000) addeval(Unpack_001) addeval(Unpack_002) addeval(Unpack_003) +addeval(UnidirectionalSequenceLSTM_002) #addeval(Where_000) #addeval(Where_001) #addeval(While_000) #addeval(While_001) #addeval(While_002) #addeval(While_003) -addeval(YUV_TO_RGB_U8_000) #addeval(ZerosLike_000) # Simple Network test @@ -194,3 +197,5 @@ addeval(Part_While_001) # Tests with tolerance addevaltol(SVDF_000 8e-3 8e-3) addevaltol(SVDF_001 8e-3 8e-3) +# refer https://github.com/Samsung/ONE/issues/10438 +addevaltol(YUV_TO_RGB_U8_000 1 1) diff --git a/compiler/luci/import/src/GraphBuilder.cpp b/compiler/luci/import/src/GraphBuilder.cpp index 59a08b5..4634be7 100644 --- a/compiler/luci/import/src/GraphBuilder.cpp +++ b/compiler/luci/import/src/GraphBuilder.cpp @@ -47,9 +47,6 @@ CircleNode *GraphBuilder::build(const circle::OperatorT &op, GraphBuilderContext { // If there is no tensor, insert CircleOutputExclude. auto *node = context->graph()->nodes()->create(); - // CircleOutputExclude doesn't need a type, but since all nodes must have a type, - // a dummy type is inserted. - node->dtype(loco::DataType::FLOAT32); input_nodes.push_back(node); } } diff --git a/compiler/luci/import/src/GraphBuilderMultiOutput.cpp b/compiler/luci/import/src/GraphBuilderMultiOutput.cpp index 4df8d1e..7bcb57a 100644 --- a/compiler/luci/import/src/GraphBuilderMultiOutput.cpp +++ b/compiler/luci/import/src/GraphBuilderMultiOutput.cpp @@ -48,9 +48,6 @@ CircleNode *GraphBuilderMultiOutput::build(const circle::OperatorT &op, { // If there is no tensor, insert CircleOutputExclude. auto *node = context->graph()->nodes()->create(); - // CircleOutputExclude doesn't need a type, but since all nodes must have a type, - // a dummy type is inserted. - node->dtype(loco::DataType::FLOAT32); input_nodes.push_back(node); } } diff --git a/compiler/luci/import/src/Nodes/CircleBidirectionalSequenceLSTM.cpp b/compiler/luci/import/src/Nodes/CircleBidirectionalSequenceLSTM.cpp index f8bdcff..c04b957 100644 --- a/compiler/luci/import/src/Nodes/CircleBidirectionalSequenceLSTM.cpp +++ b/compiler/luci/import/src/Nodes/CircleBidirectionalSequenceLSTM.cpp @@ -39,40 +39,51 @@ CircleNode *CircleBidirectionalSequenceLSTMGraphBuilder::build_node(const BuildN auto *node = bna.context->graph()->nodes()->create(); auto &inputs = bna.input_nodes; node->input(inputs.at(0)); + node->fw_input_to_input_weights(inputs.at(1)); // Optional node->fw_input_to_cell_weights(inputs.at(2)); node->fw_input_to_forget_weights(inputs.at(3)); node->fw_input_to_output_weights(inputs.at(4)); + node->fw_recurrent_to_input_weights(inputs.at(5)); // Optional node->fw_recurrent_to_cell_weights(inputs.at(6)); node->fw_recurrent_to_forget_weights(inputs.at(7)); node->fw_recurrent_to_output_weights(inputs.at(8)); + node->fw_cell_to_input_weights(inputs.at(9)); // Optional node->fw_cell_to_forget_weights(inputs.at(10)); // Optional node->fw_cell_to_output_weights(inputs.at(11)); // Optional - node->fw_input_gate_bias(inputs.at(12)); // Optional + + node->fw_input_gate_bias(inputs.at(12)); // Optional node->fw_forget_gate_bias(inputs.at(13)); node->fw_cell_gate_bias(inputs.at(14)); node->fw_output_gate_bias(inputs.at(15)); - node->fw_projection_weights(inputs.at(16)); // Optional - node->fw_projection_bias(inputs.at(17)); // Optional + + node->fw_projection_weights(inputs.at(16)); // Optional + node->fw_projection_bias(inputs.at(17)); // Optional + node->bw_input_to_input_weights(inputs.at(18)); // Optional node->bw_input_to_cell_weights(inputs.at(19)); node->bw_input_to_forget_weights(inputs.at(20)); node->bw_input_to_output_weights(inputs.at(21)); + node->bw_recurrent_to_input_weights(inputs.at(22)); // Optional node->bw_recurrent_to_cell_weights(inputs.at(23)); node->bw_recurrent_to_forget_weights(inputs.at(24)); node->bw_recurrent_to_output_weights(inputs.at(25)); + node->bw_cell_to_input_weights(inputs.at(26)); // Optional node->bw_cell_to_forget_weights(inputs.at(27)); // Optional node->bw_cell_to_output_weights(inputs.at(28)); // Optional - node->bw_input_gate_bias(inputs.at(29)); // Optional + + node->bw_input_gate_bias(inputs.at(29)); // Optional node->bw_forget_gate_bias(inputs.at(30)); node->bw_cell_gate_bias(inputs.at(31)); node->bw_output_gate_bias(inputs.at(32)); + node->bw_projection_weights(inputs.at(33)); // Optional node->bw_projection_bias(inputs.at(34)); // Optional + node->fw_activation_state(inputs.at(35)); node->fw_cell_state(inputs.at(36)); node->bw_activation_state(inputs.at(37)); @@ -83,6 +94,7 @@ CircleNode *CircleBidirectionalSequenceLSTMGraphBuilder::build_node(const BuildN node->fw_auxillary_input_to_forget_weights(inputs.at(41)); // Optional node->fw_auxillary_input_to_cell_weights(inputs.at(42)); // Optional node->fw_auxillary_input_to_output_weights(inputs.at(43)); // Optional + node->bw_auxillary_input_to_input_weights(inputs.at(44)); // Optional node->bw_auxillary_input_to_forget_weights(inputs.at(45)); // Optional node->bw_auxillary_input_to_cell_weights(inputs.at(46)); // Optional diff --git a/compiler/luci/import/src/Nodes/CircleSVDF.cpp b/compiler/luci/import/src/Nodes/CircleSVDF.cpp index 83a0251..ef57a13 100644 --- a/compiler/luci/import/src/Nodes/CircleSVDF.cpp +++ b/compiler/luci/import/src/Nodes/CircleSVDF.cpp @@ -43,9 +43,6 @@ CircleNode *CircleSVDFBuilder::build_node(const circle::OperatorT &op, if (inputs.size() == 4) { auto *bias = graph->nodes()->create(); - // CircleOutputExclude doesn't need a type, but since all nodes must have a type, - // a dummy type is inserted. - bias->dtype(inputs.at(0)->dtype()); node->bias(bias); node->input_activation_state(inputs.at(3)); diff --git a/compiler/luci/import/src/Nodes/CircleTransposeConv.cpp b/compiler/luci/import/src/Nodes/CircleTransposeConv.cpp index 041983d..01a28cb 100644 --- a/compiler/luci/import/src/Nodes/CircleTransposeConv.cpp +++ b/compiler/luci/import/src/Nodes/CircleTransposeConv.cpp @@ -65,9 +65,6 @@ CircleNode *CircleTransposeConvGraphBuilder::build_node(const circle::OperatorT if (inputs.size() == 3) { auto *bias = graph->nodes()->create(); - // CircleOutputExclude doesn't need a type, but since all nodes must have a type, - // a dummy type is inserted. - bias->dtype(loco::DataType::FLOAT32); node->bias(bias); } else diff --git a/compiler/luci/import/src/Nodes/CircleUnidirectionalSequenceLSTM.cpp b/compiler/luci/import/src/Nodes/CircleUnidirectionalSequenceLSTM.cpp index d9cc3f8..7ab6d68 100644 --- a/compiler/luci/import/src/Nodes/CircleUnidirectionalSequenceLSTM.cpp +++ b/compiler/luci/import/src/Nodes/CircleUnidirectionalSequenceLSTM.cpp @@ -34,24 +34,30 @@ CircleNode *CircleUnidirectionalSequenceLSTMGraphBuilder::build_node( auto *node = graph->nodes()->create(); node->input(inputs.at(0)); node->input_to_input_weights(inputs.at(1)); // Optional - node->input_to_cell_weights(inputs.at(2)); - node->input_to_forget_weights(inputs.at(3)); + node->input_to_forget_weights(inputs.at(2)); + node->input_to_cell_weights(inputs.at(3)); node->input_to_output_weights(inputs.at(4)); + node->recurrent_to_input_weights(inputs.at(5)); // Optional - node->recurrent_to_cell_weights(inputs.at(6)); - node->recurrent_to_forget_weights(inputs.at(7)); + node->recurrent_to_forget_weights(inputs.at(6)); + node->recurrent_to_cell_weights(inputs.at(7)); node->recurrent_to_output_weights(inputs.at(8)); + node->cell_to_input_weights(inputs.at(9)); // Optional node->cell_to_forget_weights(inputs.at(10)); // Optional node->cell_to_output_weights(inputs.at(11)); // Optional - node->input_gate_bias(inputs.at(12)); // Optional + + node->input_gate_bias(inputs.at(12)); // Optional node->forget_gate_bias(inputs.at(13)); node->cell_gate_bias(inputs.at(14)); node->output_gate_bias(inputs.at(15)); + node->projection_weights(inputs.at(16)); // Optional node->projection_bias(inputs.at(17)); // Optional - node->activation_state(inputs.at(18)); + + node->output_state(inputs.at(18)); node->cell_state(inputs.at(19)); + node->input_layer_norm_coefficients(inputs.at(20)); // Optional node->forget_layer_norm_coefficients(inputs.at(21)); // Optional node->cell_layer_norm_coefficients(inputs.at(22)); // Optional diff --git a/compiler/luci/lang/include/luci/IR/Nodes/CircleUnidirectionalSequenceLSTM.h b/compiler/luci/lang/include/luci/IR/Nodes/CircleUnidirectionalSequenceLSTM.h index faf0ec9..7b9e445 100644 --- a/compiler/luci/lang/include/luci/IR/Nodes/CircleUnidirectionalSequenceLSTM.h +++ b/compiler/luci/lang/include/luci/IR/Nodes/CircleUnidirectionalSequenceLSTM.h @@ -76,8 +76,8 @@ public: loco::Node *projection_bias(void) const { return at(17)->node(); } void projection_bias(loco::Node *node) { at(17)->node(node); } - loco::Node *activation_state(void) const { return at(18)->node(); } - void activation_state(loco::Node *node) { at(18)->node(node); } + loco::Node *output_state(void) const { return at(18)->node(); } + void output_state(loco::Node *node) { at(18)->node(node); } loco::Node *cell_state(void) const { return at(19)->node(); } void cell_state(loco::Node *node) { at(19)->node(node); } diff --git a/compiler/luci/lang/src/Nodes/CircleUnidirectionalSequenceLSTM.test.cpp b/compiler/luci/lang/src/Nodes/CircleUnidirectionalSequenceLSTM.test.cpp index 6b00d6f..2b10930 100644 --- a/compiler/luci/lang/src/Nodes/CircleUnidirectionalSequenceLSTM.test.cpp +++ b/compiler/luci/lang/src/Nodes/CircleUnidirectionalSequenceLSTM.test.cpp @@ -52,7 +52,7 @@ TEST(CircleUnidirectionalSequenceLSTMTest, constructor_P) ASSERT_EQ(nullptr, trc_node.projection_weights()); ASSERT_EQ(nullptr, trc_node.projection_bias()); - ASSERT_EQ(nullptr, trc_node.activation_state()); + ASSERT_EQ(nullptr, trc_node.output_state()); ASSERT_EQ(nullptr, trc_node.cell_state()); ASSERT_EQ(nullptr, trc_node.input_layer_norm_coefficients()); diff --git a/compiler/luci/log/src/Log.cpp b/compiler/luci/log/src/Log.cpp index 0cc45e8..27049be 100644 --- a/compiler/luci/log/src/Log.cpp +++ b/compiler/luci/log/src/Log.cpp @@ -79,28 +79,6 @@ void LoggerConfig::configure(const hermes::Source *source, hermes::Source::Setti void LoggerConfig::configure(const Logger *, hermes::Source::Setting &setting) const { - // TODO remove deprecated codes -#if 0 - setting.filter(hermes::SeverityCategory::FATAL).reject_all(); - setting.filter(hermes::SeverityCategory::ERROR).reject_all(); - setting.filter(hermes::SeverityCategory::WARN).reject_all(); - setting.filter(hermes::SeverityCategory::INFO).reject_all(); - setting.filter(hermes::SeverityCategory::VERBOSE).reject_all(); - - // TODO enable FATAL and ERROR - if (_show_warn) - { - setting.filter(hermes::SeverityCategory::WARN).accept_all(); - } - if (_show_info) - { - setting.filter(hermes::SeverityCategory::INFO).accept_all(); - } - if (_show_verbose) - { - setting.filter(hermes::SeverityCategory::VERBOSE).accept_upto(_show_verbose); - } -#endif setting.reject_all(); setting.filter(hermes::SeverityCategory::FATAL).accept_upto(_show_verbose); setting.filter(hermes::SeverityCategory::ERROR).accept_upto(_show_verbose); diff --git a/compiler/luci/logex/src/CircleNodeSummaryBuilders.cpp b/compiler/luci/logex/src/CircleNodeSummaryBuilders.cpp index 48e4579..42f11be 100644 --- a/compiler/luci/logex/src/CircleNodeSummaryBuilders.cpp +++ b/compiler/luci/logex/src/CircleNodeSummaryBuilders.cpp @@ -63,6 +63,10 @@ std::string to_str(loco::DataType type) } } +std::string to_str(float value) { return std::to_string(value); } + +std::string to_str(int32_t value) { return std::to_string(value); } + std::string to_str(bool value) { return value ? "true" : "false"; } std::string to_str(luci::FusedActFunc fused) @@ -1047,7 +1051,7 @@ CircleUnidirectionalSequenceLSTMSummaryBuilder::get_input_names(const luci::Circ "output_gate_bias", "projection_weights", "projection_bias", - "activation_state", + "output_state", "cell_state", "input_layer_norm_coefficients", "forget_layer_norm_coefficients", diff --git a/compiler/luci/partition/src/Nodes/CircleUnidirectionalSequenceLSTM.cpp b/compiler/luci/partition/src/Nodes/CircleUnidirectionalSequenceLSTM.cpp index 3323014..e8c834b 100644 --- a/compiler/luci/partition/src/Nodes/CircleUnidirectionalSequenceLSTM.cpp +++ b/compiler/luci/partition/src/Nodes/CircleUnidirectionalSequenceLSTM.cpp @@ -61,8 +61,7 @@ void connect(luci::ConnectNode *cn, const luci::CircleUnidirectionalSequenceLSTM loco::must_cast(node->projection_weights()); luci::CircleNode *projection_bias = loco::must_cast(node->projection_bias()); - luci::CircleNode *activation_state = - loco::must_cast(node->activation_state()); + luci::CircleNode *output_state = loco::must_cast(node->output_state()); luci::CircleNode *cell_state = loco::must_cast(node->cell_state()); luci::CircleNode *input_layer_norm_coefficients = @@ -98,7 +97,7 @@ void connect(luci::ConnectNode *cn, const luci::CircleUnidirectionalSequenceLSTM cloned->projection_weights(cn->find_clone(projection_weights)); cloned->projection_bias(cn->find_clone(projection_bias)); - cloned->activation_state(cn->find_clone(activation_state)); + cloned->output_state(cn->find_clone(output_state)); cloned->cell_state(cn->find_clone(cell_state)); cloned->input_layer_norm_coefficients(cn->find_clone(input_layer_norm_coefficients)); diff --git a/compiler/luci/partition/src/Nodes/CircleUnidirectionalSequenceLSTM.test.cpp b/compiler/luci/partition/src/Nodes/CircleUnidirectionalSequenceLSTM.test.cpp index 2630461..6472b58 100644 --- a/compiler/luci/partition/src/Nodes/CircleUnidirectionalSequenceLSTM.test.cpp +++ b/compiler/luci/partition/src/Nodes/CircleUnidirectionalSequenceLSTM.test.cpp @@ -79,7 +79,7 @@ public: node()->projection_weights(input(16)); node()->projection_bias(input(17)); - node()->activation_state(input(18)); + node()->output_state(input(18)); node()->cell_state(input(19)); node()->input_layer_norm_coefficients(input(20)); diff --git a/compiler/luci/partition/src/PartitionPModules.cpp b/compiler/luci/partition/src/PartitionPModules.cpp index 251dbea..9912305 100644 --- a/compiler/luci/partition/src/PartitionPModules.cpp +++ b/compiler/luci/partition/src/PartitionPModules.cpp @@ -22,6 +22,8 @@ #include +#include + namespace { @@ -88,11 +90,14 @@ std::unique_ptr clone_graph(loco::Graph *graph_org, luci::CloneCont { auto graph = loco::make_graph(); auto graph_clone = graph.get(); + auto &graph_name = graph_org->name(); - graph_clone->name(graph_org->name()); + graph_clone->name(graph_name); // clone inputs - for (uint32_t n = 0; n < graph_org->inputs()->size(); ++n) + auto inputs = graph_org->inputs(); + assert(inputs); + for (uint32_t n = 0; n < inputs->size(); ++n) { auto input_org = luci::input_node(graph_org, n); assert(input_org != nullptr); diff --git a/compiler/luci/pass/CMakeLists.txt b/compiler/luci/pass/CMakeLists.txt index d9d004d..ac18a5f 100644 --- a/compiler/luci/pass/CMakeLists.txt +++ b/compiler/luci/pass/CMakeLists.txt @@ -31,7 +31,7 @@ target_link_libraries(luci_pass PRIVATE luci_log) target_link_libraries(luci_pass PRIVATE luci_service) target_link_libraries(luci_pass PRIVATE luci_logex) target_link_libraries(luci_pass PRIVATE luci_profile) -target_link_libraries(luci_pass PRIVATE mio_tflite280_inc) +target_link_libraries(luci_pass PRIVATE luci_compute) target_link_libraries(luci_pass PRIVATE nncc_common) target_link_libraries(luci_pass PRIVATE pepper_csv2vec) target_link_libraries(luci_pass PRIVATE oops) diff --git a/compiler/luci/pass/include/luci/CircleOptimizer.h b/compiler/luci/pass/include/luci/CircleOptimizer.h index b94822c..d77e89d 100644 --- a/compiler/luci/pass/include/luci/CircleOptimizer.h +++ b/compiler/luci/pass/include/luci/CircleOptimizer.h @@ -52,14 +52,17 @@ public: FoldCast, FoldDensify, FoldDepthwiseConv2D, + FoldFullyConnected, FoldDequantize, FoldGather, FoldSparseToDense, ForwardReshapeToUnaryOp, + ForwardTransposeOp, SparsifyTensorPass, FusePreActivationBatchNorm, MakeBatchNormGammaPositive, FuseActivationFunction, + FusePRelu, ShuffleWeightTo16x1Float32, RemoveRedundantTranspose, ReplaceMulAddWithDepthwiseConv, @@ -83,6 +86,8 @@ public: RemoveRedundantReshape, RemoveFakeQuant, RemoveQuantDequantSeq, + RemoveDuplicateConst, + UnrollUnidirSeqLSTM, }; enum AlgorithmParameters diff --git a/compiler/luci/pass/include/luci/Pass/FoldFullyConnectedPass.h b/compiler/luci/pass/include/luci/Pass/FoldFullyConnectedPass.h new file mode 100644 index 0000000..bd36ff1 --- /dev/null +++ b/compiler/luci/pass/include/luci/Pass/FoldFullyConnectedPass.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __LUCI_FOLD_FULLY_CONNECTED_PASS_H__ +#define __LUCI_FOLD_FULLY_CONNECTED_PASS_H__ + +#include + +namespace luci +{ + +/** + * @brief Class to fold FullyConnected with constant input and filter into a + * constant tensor + */ +struct FoldFullyConnectedPass final : public logo::Pass +{ + const char *name(void) const final { return "luci::FoldFullyConnectedPass"; } + + bool run(loco::Graph *g) final; +}; + +} // namespace luci + +#endif // __LUCI_FOLD_FULLY_CONNECTED_PASS_H__ diff --git a/compiler/luci/pass/include/luci/Pass/ForwardTransposeOpPass.h b/compiler/luci/pass/include/luci/Pass/ForwardTransposeOpPass.h new file mode 100644 index 0000000..b44b1bd --- /dev/null +++ b/compiler/luci/pass/include/luci/Pass/ForwardTransposeOpPass.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __LUCI_FORWARD_TRANSPOSE_OP_PASS_H__ +#define __LUCI_FORWARD_TRANSPOSE_OP_PASS_H__ + +#include + +namespace luci +{ + +/** + * @brief Class to Forward Transpose Ops for further optimization. + */ +struct ForwardTransposeOpPass final : public logo::Pass +{ + const char *name(void) const final { return "luci::ForwardTransposeOpPass"; } + + bool run(loco::Graph *g) final; +}; + +} // namespace luci + +#endif // __LUCI_FORWARD_TRANSPOSE_OP_PASS_H__ diff --git a/compiler/luci/pass/include/luci/Pass/FusePReluPass.h b/compiler/luci/pass/include/luci/Pass/FusePReluPass.h new file mode 100644 index 0000000..a21acf4 --- /dev/null +++ b/compiler/luci/pass/include/luci/Pass/FusePReluPass.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __LUCI_FUSE_PRELU_PASS_H__ +#define __LUCI_FUSE_PRELU_PASS_H__ + +#include + +namespace luci +{ + +/** + * @brief Class to fuse certain pattern of subgraph into CirclePRelu + * with auxiliary nodes + * + * For detailed subgraph pattern to be fused, please check its implementation. + */ +struct FusePReluPass final : public logo::Pass +{ + const char *name(void) const final { return "luci::FusePReluPass"; } + + bool run(loco::Graph *g) final; +}; + +} // namespace luci + +#endif // __LUCI_FUSE_PRELU_PASS_H__ diff --git a/compiler/luci/pass/include/luci/Pass/QuantizeWithMinMaxPass.h b/compiler/luci/pass/include/luci/Pass/QuantizeWithMinMaxPass.h index ea6db85..6874046 100644 --- a/compiler/luci/pass/include/luci/Pass/QuantizeWithMinMaxPass.h +++ b/compiler/luci/pass/include/luci/Pass/QuantizeWithMinMaxPass.h @@ -39,29 +39,12 @@ public: loco::DataType input_model_dtype = loco::DataType::Unknown; loco::DataType output_model_dtype = loco::DataType::Unknown; QuantizationGranularity granularity = QuantizationGranularity::ChannelWise; - loco::DataType input_type = loco::DataType::Unknown; - loco::DataType output_type = loco::DataType::Unknown; + std::vector input_types; + std::vector output_types; bool TF_style_maxpool = false; std::vector layers_info; }; - // For backward-compatibility - // TODO Remove this constructor -public: - QuantizeWithMinMaxPass(loco::DataType input_model_dtype, loco::DataType output_model_dtype, - QuantizationGranularity granularity) - { - _ctx = std::make_unique(); - { - _ctx->input_model_dtype = input_model_dtype; - _ctx->output_model_dtype = output_model_dtype; - _ctx->granularity = granularity; - _ctx->input_type = output_model_dtype; - _ctx->output_type = output_model_dtype; - _ctx->TF_style_maxpool = false; - } - } - public: QuantizeWithMinMaxPass(std::unique_ptr &&ctx) : _ctx{std::move(ctx)} { diff --git a/compiler/luci/pass/include/luci/Pass/RemoveDuplicateConstPass.h b/compiler/luci/pass/include/luci/Pass/RemoveDuplicateConstPass.h new file mode 100644 index 0000000..000cdcc --- /dev/null +++ b/compiler/luci/pass/include/luci/Pass/RemoveDuplicateConstPass.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __LUCI_REMOVE_DUPLICATE_CONST_PASS_H__ +#define __LUCI_REMOVE_DUPLICATE_CONST_PASS_H__ + +#include +#include + +namespace luci +{ + +/** + * @brief Class to remove duplicate Const nodes. + */ +struct RemoveDuplicateConstPass final : public logo::Pass +{ + const char *name(void) const final { return "luci::RemoveDuplicateConstPass"; } + + bool run(loco::Graph *g) final; + +private: + bool remove_duplicate_const(); + + template void add_to_map(luci::CircleConst *const_node); + + std::map> _sum_to_const; +}; + +} // namespace luci + +#endif // __LUCI_REMOVE_DUPLICATE_CONST_PASS_H__ diff --git a/compiler/luci/pass/include/luci/Pass/UnrollUnidirectionalSequenceLSTMPass.h b/compiler/luci/pass/include/luci/Pass/UnrollUnidirectionalSequenceLSTMPass.h new file mode 100644 index 0000000..fd5a708 --- /dev/null +++ b/compiler/luci/pass/include/luci/Pass/UnrollUnidirectionalSequenceLSTMPass.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __LUCI_UNROLL_UNIDIRECTIONALSEQUENCELSTM_PASS_H__ +#define __LUCI_UNROLL_UNIDIRECTIONALSEQUENCELSTM_PASS_H__ + +#include + +namespace luci +{ + +/** + * @brief Class to Unroll UnidirectionalSequenceLSTM + */ +struct UnrollUnidirectionalSequenceLSTMPass final : public logo::Pass +{ + const char *name(void) const final { return "luci::UnrollUnidirectionalSequenceLSTMPass"; } + + bool run(loco::Graph *g) final; +}; + +} // namespace luci + +#endif // __LUCI_UNROLL_UNIDIRECTIONALSEQUENCELSTM_PASS_H__ diff --git a/compiler/luci/pass/src/CircleOptimizer.cpp b/compiler/luci/pass/src/CircleOptimizer.cpp index 74c569d..5e1613a 100644 --- a/compiler/luci/pass/src/CircleOptimizer.cpp +++ b/compiler/luci/pass/src/CircleOptimizer.cpp @@ -23,9 +23,11 @@ #include "luci/Pass/FoldDensifyPass.h" #include "luci/Pass/FoldDepthwiseConv2DPass.h" #include "luci/Pass/FoldDequantizePass.h" +#include "luci/Pass/FoldFullyConnectedPass.h" #include "luci/Pass/FoldGatherPass.h" #include "luci/Pass/FoldSparseToDensePass.h" #include "luci/Pass/ForwardReshapeToUnaryOpPass.h" +#include "luci/Pass/ForwardTransposeOpPass.h" #include "luci/Pass/FuseActivationFunctionPass.h" #include "luci/Pass/FuseAddWithFullyConnectedPass.h" #include "luci/Pass/FuseAddWithTConvPass.h" @@ -36,8 +38,10 @@ #include "luci/Pass/FuseInstanceNormPass.h" #include "luci/Pass/FuseMeanWithMeanPass.h" #include "luci/Pass/FusePreActivationBatchNormPass.h" +#include "luci/Pass/FusePReluPass.h" #include "luci/Pass/FuseTransposeWithMeanPass.h" #include "luci/Pass/MakeBatchNormGammaPositivePass.h" +#include "luci/Pass/RemoveDuplicateConstPass.h" #include "luci/Pass/RemoveFakeQuantPass.h" #include "luci/Pass/RemoveQuantDequantSeqPass.h" #include "luci/Pass/RemoveRedundantReshapePass.h" @@ -66,6 +70,7 @@ #include "luci/Pass/SubstituteTransposeToReshapePass.h" #include "luci/Pass/TransformMinMaxToRelu6Pass.h" #include "luci/Pass/TransformMinReluToRelu6Pass.h" +#include "luci/Pass/UnrollUnidirectionalSequenceLSTMPass.h" // TODO add more passes #include "luci/Pass/CircleShapeInferencePass.h" @@ -274,6 +279,10 @@ void CircleOptimizer::optimize(loco::Graph *g) const { phase.emplace_back(std::make_unique()); } + if (_options->query(Options::Algorithm::FusePRelu)) + { + phase.emplace_back(std::make_unique()); + } if (_options->query(Options::Algorithm::FuseTransposeWithMean)) { phase.emplace_back(std::make_unique()); @@ -298,6 +307,10 @@ void CircleOptimizer::optimize(loco::Graph *g) const { phase.emplace_back(std::make_unique()); } + if (_options->query(Options::Algorithm::FoldFullyConnected)) + { + phase.emplace_back(std::make_unique()); + } if (_options->query(Options::Algorithm::FoldGather)) { phase.emplace_back(std::make_unique()); @@ -310,6 +323,10 @@ void CircleOptimizer::optimize(loco::Graph *g) const { phase.emplace_back(std::make_unique()); } + if (_options->query(Options::Algorithm::ForwardTransposeOp)) + { + phase.emplace_back(std::make_unique()); + } if (_options->query(Options::Algorithm::FusePreActivationBatchNorm)) { phase.emplace_back(std::make_unique()); @@ -326,6 +343,10 @@ void CircleOptimizer::optimize(loco::Graph *g) const { phase.emplace_back(std::make_unique()); } + if (_options->query(Options::Algorithm::RemoveDuplicateConst)) + { + phase.emplace_back(std::make_unique()); + } if (_options->query(Options::Algorithm::RemoveFakeQuant)) { phase.emplace_back(std::make_unique()); @@ -407,6 +428,10 @@ void CircleOptimizer::optimize(loco::Graph *g) const { phase.emplace_back(std::make_unique()); } + if (_options->query(Options::Algorithm::UnrollUnidirSeqLSTM)) + { + phase.emplace_back(std::make_unique()); + } /* TRANSFORM DECLARATION END */ diff --git a/compiler/luci/pass/src/CircleQuantizer.cpp b/compiler/luci/pass/src/CircleQuantizer.cpp index 9a6550b..3ffa118 100644 --- a/compiler/luci/pass/src/CircleQuantizer.cpp +++ b/compiler/luci/pass/src/CircleQuantizer.cpp @@ -40,6 +40,7 @@ #include #include +#include #include @@ -49,6 +50,154 @@ namespace using namespace luci; using LayerParam = luci::CircleQuantizer::Options::LayerParam; +// This function updates user-given input_type to match with the input signature of graph +// If user gives only one input_type, it will be expanded to the number of graph inputs +void canonicalize_input_type(loco::Graph *g, std::vector &input_type) +{ + if (g == nullptr) + return; + + const auto inputs = g->inputs(); + + assert(inputs); // FIX_CALLER_UNLESS + + // Check validity of the number of input dtype given by a user + if (input_type.size() != 1 and input_type.size() != inputs->size()) + { + throw std::runtime_error( + "Invalid number of input dtype. The number of input dtype should be 1 or " + "the same as the number of graph inputs."); + } + + // Handle the case when a user gives only one input dtype + if (input_type.size() == 1) + { + const auto user_given_dtype = input_type[0]; + input_type.clear(); + + // Expand input dtype to the number of graph inputs + // Since quantizer can only quantize float32, user_given_dtype is set only for float32 inputs + auto input_nodes = loco::input_nodes(g); + for (uint32_t i = 0; i < input_nodes.size(); i++) + { + auto input = loco::must_cast(input_nodes[i]); + + if (input->dtype() == loco::DataType::FLOAT32) + input_type.push_back(user_given_dtype); + else + input_type.push_back(input->dtype()); + } + } + + // Finally, check validity of input_type + // input_type is valid if + // C1. for non-float32 model input, input_type == model's input dtype + // or + // C2. for float32 model input, input_type == uint8, int16, or float32 + auto input_nodes = loco::input_nodes(g); + for (uint32_t i = 0; i < input_nodes.size(); i++) + { + auto input = loco::must_cast(input_nodes[i]); + assert(i == input->index()); // FIX_ME_UNLESS + + if (input->dtype() != loco::DataType::FLOAT32) + { + // C1 + if (input->dtype() != input_type[i]) + throw std::runtime_error( + "Input dtype of " + input->name() + + " is invalid. It has to be the same with the model's input dtype."); + } + else + { + // C2 + if (input_type[i] != loco::DataType::FLOAT32 and input_type[i] != loco::DataType::U8 and + input_type[i] != loco::DataType::S16) + { + throw std::runtime_error("Input dtype of " + input->name() + + " is invalid. For float32 input, the input dtype after " + "quantization must be one of uint8, int16, or float32."); + } + } + } +} + +// This function updates user-given output_type to match with the output signature of graph +// If user gives only one output_type, it will be expanded to the number of graph outputs +// NOTE This function is almost same with canonicalize_input_type, but it is written as a +// separate function for more precise error messaging. +// TODO Find a way to reduce duplicate codes +void canonicalize_output_type(loco::Graph *g, std::vector &output_type) +{ + if (g == nullptr) + return; + + const auto outputs = g->outputs(); + + assert(outputs); // FIX_CALLER_UNLESS + + // Check validity of the number of output dtype given by a user + if (output_type.size() != 1 and output_type.size() != outputs->size()) + { + throw std::runtime_error( + "Invalid number of output dtype. The number of output dtype should be 1 or " + "the same as the number of graph outputs."); + } + + // Handle the case when a user gives only one output dtype + if (output_type.size() == 1) + { + const auto user_given_dtype = output_type[0]; + output_type.clear(); + + // Expand output dtype to the number of graph outputs + // If dtype of graph output is float32, it will be replaced with user_given_dtype + // Otherwise, it will not change + auto output_nodes = loco::output_nodes(g); + for (uint32_t i = 0; i < output_nodes.size(); i++) + { + auto output = loco::must_cast(output_nodes[i]); + + if (output->dtype() == loco::DataType::FLOAT32) + output_type.push_back(user_given_dtype); + else + output_type.push_back(output->dtype()); + } + } + + // Finally, check validity of output_type + // output_type is valid if + // C1. for non-float32 model output, output_type == model's output dtype + // or + // C2. for float32 model output, output_type == uint8, int16, or float32 + auto output_nodes = loco::output_nodes(g); + for (uint32_t i = 0; i < output_nodes.size(); i++) + { + auto output = loco::must_cast(output_nodes[i]); + assert(i == output->index()); // FIX_ME_UNLESS + + if (output->dtype() != loco::DataType::FLOAT32) + { + // C1 + if (output->dtype() != output_type[i]) + throw std::runtime_error( + "Output dtype of " + output->name() + + " is invalid. It has to be the same with the model's output dtype."); + } + else + { + // C2 + if (output_type[i] != loco::DataType::FLOAT32 and output_type[i] != loco::DataType::U8 and + output_type[i] != loco::DataType::S16) + { + throw std::runtime_error("Output dtype of " + output->name() + + " is invalid. For float32 output, the output dtype after " + "quantization must be one of uint8, int16, or float32."); + } + } + } +} + template T lexical_cast(const std::string &str) { std::istringstream ss; @@ -253,8 +402,10 @@ void CircleQuantizer::quantize(loco::Graph *g) const static const std::vector qwmm_supported_input_model_dtype{"float32"}; static const std::vector qwmm_supported_output_model_dtype{"uint8", "int16"}; static const std::vector qwmm_supported_granularity{"layer", "channel"}; - static const std::vector qwmm_supported_input_type{"uint8", "int16", "float32"}; - static const std::vector qwmm_supported_output_type{"uint8", "int16", "float32"}; + static const std::vector qwmm_supported_input_type{"uint8", "int16", "int32", + "int64", "float32", "bool"}; + static const std::vector qwmm_supported_output_type{"uint8", "int16", "int32", + "int64", "float32", "bool"}; auto input_model_dtype = _options->param(Options::AlgorithmParameters::Quantize_input_model_dtype); @@ -268,6 +419,9 @@ void CircleQuantizer::quantize(loco::Graph *g) const if (output_type.empty()) output_type = output_model_dtype; + auto input_type_vec = pepper::csv_to_vector(input_type); + auto output_type_vec = pepper::csv_to_vector(output_type); + bool TF_style_maxpool = _options->param(Options::AlgorithmParameters::Quantize_TF_style_maxpool) == "True"; @@ -285,13 +439,19 @@ void CircleQuantizer::quantize(loco::Graph *g) const throw std::runtime_error("Unsupported granularity. List of supported granularity: " + to_string(qwmm_supported_granularity)); - if (!in_array(to_lower_case(input_type), qwmm_supported_input_type)) - throw std::runtime_error("Unsupported input type. List of supported input types: " + - to_string(qwmm_supported_input_type)); + for (auto dtype : input_type_vec) + { + if (!in_array(to_lower_case(dtype), qwmm_supported_input_type)) + throw std::runtime_error("Unsupported input type. List of supported input types: " + + to_string(qwmm_supported_input_type)); + } - if (!in_array(to_lower_case(output_type), qwmm_supported_output_type)) - throw std::runtime_error("Unsupported output type. List of supported output types: " + - to_string(qwmm_supported_output_type)); + for (auto dtype : output_type_vec) + { + if (!in_array(to_lower_case(dtype), qwmm_supported_output_type)) + throw std::runtime_error("Unsupported output type. List of supported output types: " + + to_string(qwmm_supported_output_type)); + } if (str_to_granularity(granularity) == QuantizationGranularity::LayerWise && str_to_dtype(output_model_dtype) != loco::DataType::U8) @@ -314,6 +474,13 @@ void CircleQuantizer::quantize(loco::Graph *g) const } } + auto input_types = str_vec_to_dtype_vec(input_type_vec); + auto output_types = str_vec_to_dtype_vec(output_type_vec); + + // Canonicalize user-given input/output_type (match with # of inputs/outputs) + canonicalize_input_type(g, input_types); + canonicalize_output_type(g, output_types); + // Input model checker for quantization luci::QuantizePreCheckerPass input_model_checker{}; input_model_checker.run(g); @@ -323,8 +490,8 @@ void CircleQuantizer::quantize(loco::Graph *g) const ctx->input_model_dtype = str_to_dtype(input_model_dtype); ctx->output_model_dtype = str_to_dtype(output_model_dtype); ctx->granularity = str_to_granularity(granularity); - ctx->input_type = str_to_dtype(input_type); - ctx->output_type = str_to_dtype(output_type); + ctx->input_types = input_types; + ctx->output_types = output_types; ctx->TF_style_maxpool = TF_style_maxpool; for (auto layer_param : layer_params) @@ -347,8 +514,8 @@ void CircleQuantizer::quantize(loco::Graph *g) const { verify_ctx->output_model_dtype = str_to_dtype(output_model_dtype); verify_ctx->granularity = str_to_granularity(granularity); - verify_ctx->input_type = str_to_dtype(input_type); - verify_ctx->output_type = str_to_dtype(output_type); + verify_ctx->input_types = input_types; + verify_ctx->output_types = output_types; verify_ctx->TF_style_maxpool = TF_style_maxpool; for (auto layer_param : layer_params) diff --git a/compiler/luci/pass/src/ConvertNCHWToNHWCPass.cpp b/compiler/luci/pass/src/ConvertNCHWToNHWCPass.cpp index 55a29d1..99e1e29 100644 --- a/compiler/luci/pass/src/ConvertNCHWToNHWCPass.cpp +++ b/compiler/luci/pass/src/ConvertNCHWToNHWCPass.cpp @@ -503,43 +503,30 @@ bool is_NCHW(const luci::CirclePadV2 *node) return true; } -// NOTE Following conditions can be extended later -// NOTE Used for Maximum, Miminum as ReLU/ReLU6 -// -// Find T with an NCHW pattern described below -// - Input (non-constant) shape : [N, C, H, W] -// - Input (constant) shape : [1] or [] -// - Output shape : [N, C, H, W] -template -bool is_NCHW_with_s_const(const T *node, luci::CircleNode *&pred_node, - luci::CircleConst *&comp_const) +bool is_const(const loco::Node *node) { - auto x = dynamic_cast(node->x()); - auto y = dynamic_cast(node->y()); - - if (x != nullptr && y == nullptr) - { - pred_node = loco::must_cast(node->y()); - comp_const = x; - } - else if (x == nullptr && y != nullptr) - { - pred_node = loco::must_cast(node->x()); - comp_const = y; - } - else - { - // Ignore if T does not have a comp_const input. + if (not dynamic_cast(node)) return false; - } - if (pred_node->rank() != 4) + return true; +} + +bool is_scalar_const(const loco::Node *node) +{ + auto const_node = dynamic_cast(node); + if (not const_node) return false; - // Check if scalar - const auto const_rank = comp_const->rank(); - if (const_rank == 0 || (const_rank == 1 && comp_const->dim(0).value() == 1)) + const auto const_rank = const_node->rank(); + // shape of scalar + // 1. rank = 0 + // 2. rank = 1, dimension = 1 + if (const_rank == 0) + return true; + + if (const_rank == 1 && const_node->dim(0).value() == 1) return true; + return false; } @@ -854,22 +841,30 @@ class ConvertNCHWToNHWC final : public luci::CircleNodeMutableVisitor bool visit(luci::CircleLogistic *node) { return convert_unary_x(node); } - bool visit(luci::CircleLogSoftmax *node) - { - return convert_unary_logits(node); - } - bool visit(luci::CircleMaximum *node) { - luci::CircleNode *pred_node = nullptr; - luci::CircleConst *comp_constant = nullptr; - - if (is_NCHW_with_s_const(node, pred_node, comp_constant)) + if ((not is_const(node->x())) and is_scalar_const(node->y())) { auto pre_trans = create_pre_transpose(node); - pre_trans->a(pred_node); + pre_trans->a(node->x()); node->x(pre_trans); } + else if (is_scalar_const(node->x()) and (not is_const(node->y()))) + { + auto pre_trans = create_pre_transpose(node); + pre_trans->a(node->y()); + node->y(pre_trans); + } + else if ((not is_const(node->x())) and (not is_const(node->y()))) + { + auto pre_trans_x = create_pre_transpose(node); + pre_trans_x->a(node->x()); + node->x(pre_trans_x); + + auto pre_trans_y = create_pre_transpose(node); + pre_trans_y->a(node->y()); + node->y(pre_trans_y); + } else { // TODO support other cases @@ -963,15 +958,18 @@ class ConvertNCHWToNHWC final : public luci::CircleNodeMutableVisitor bool visit(luci::CircleMinimum *node) { - luci::CircleNode *pred_node = nullptr; - luci::CircleConst *comp_constant = nullptr; - - if (is_NCHW_with_s_const(node, pred_node, comp_constant)) + if ((not is_const(node->x())) and is_scalar_const(node->y())) { auto pre_trans = create_pre_transpose(node); - pre_trans->a(pred_node); + pre_trans->a(node->x()); node->x(pre_trans); } + else if (is_scalar_const(node->x()) and (not is_const(node->y()))) + { + auto pre_trans = create_pre_transpose(node); + pre_trans->a(node->y()); + node->y(pre_trans); + } else { // TODO support other cases @@ -1168,14 +1166,88 @@ class ConvertNCHWToNHWC final : public luci::CircleNodeMutableVisitor return true; } + // TODO Reduce duplicate codes with CircleReduceMax + bool visit(luci::CircleReduceMin *node) + { + auto input = loco::must_cast(node->input()); + if (input->rank() != 4) + return false; + + auto rindices = dynamic_cast(node->reduction_indices()); + if (not rindices) + return false; + + auto nhwc_rindices = create_NHWC_rindices(rindices); + if (not nhwc_rindices) + return false; + + auto pre_trans = create_pre_transpose(node); + pre_trans->a(input); + node->input(pre_trans); + + // Do shape inference for this node again. + node->shape_status(luci::ShapeStatus::UNDEFINED); + + node->reduction_indices(nhwc_rindices); + + if (node->keep_dims()) + { + auto post_trans = create_post_transpose(node); + loco::replace(node).with(post_trans); + + post_trans->a(node); + + return true; + } + + // The below codes handle the cases where node->keep_dims() == false + // 1D output never needs a transpose + if (node->rank() <= 1) + return true; + + std::vector reduced_dims_nhwc(4, false); + uint32_t num_reduced_indices = nhwc_rindices->size(); + + for (uint32_t ri = 0; ri < num_reduced_indices; ++ri) + { + reduced_dims_nhwc[nhwc_rindices->at(ri)] = true; + } + + // if channel dimension has been reduced, we don't need a transpose + if (reduced_dims_nhwc[3]) + return true; + + // likewise, if both space dimensions are reduced, no transpose is needed + if (reduced_dims_nhwc[1] && reduced_dims_nhwc[2]) + return true; + + std::vector post_trans_ind; + // case 1: only N is reduced + if (num_reduced_indices == 1 && reduced_dims_nhwc[0]) + post_trans_ind = {2, 0, 1}; + + // case 2: only H or W is reduced + if (num_reduced_indices == 1 && (reduced_dims_nhwc[1] || reduced_dims_nhwc[2])) + post_trans_ind = {0, 2, 1}; + + // case 3: N and either H or W are reduced + if (num_reduced_indices == 2) + post_trans_ind = {1, 0}; + + auto post_trans = create_Nd_transpose(node, post_trans_ind); + loco::replace(node).with(post_trans); + + post_trans->a(node); + + return true; + } + bool visit(luci::CircleRelu *node) { return convert_unary_features(node); } bool visit(luci::CircleRelu6 *node) { return convert_unary_features(node); } bool visit(luci::CircleRsqrt *node) { return convert_unary_x(node); } - bool visit(luci::CircleSoftmax *node) { return convert_unary_logits(node); } - bool visit(luci::CircleSplitV *node) { // Change split dimension @@ -1375,6 +1447,10 @@ bool ConvertNCHWToNHWCPass::run(loco::Graph *g) collect_intermediate = [&](loco::Node *n) { for (auto succ : loco::succs(n)) { + // Skip unnecessary traversal + if (intermediate.find(succ) != intermediate.end()) + continue; + // Exit condition if (is_post_transpose(succ) || is_post_reshape(succ)) continue; @@ -1429,12 +1505,13 @@ bool ConvertNCHWToNHWCPass::run(loco::Graph *g) set_data_format(node, DataFormat::NCHW); } break; + // SOFTMAX, LOG_SOFTMAX are not converted, because + // tflite/circle assumes the last channel is always axis case luci::CircleOpcode::ADD: case luci::CircleOpcode::CONCATENATION: case luci::CircleOpcode::ELU: case luci::CircleOpcode::LEAKY_RELU: case luci::CircleOpcode::LOGISTIC: - case luci::CircleOpcode::LOG_SOFTMAX: case luci::CircleOpcode::MAXIMUM: case luci::CircleOpcode::MEAN: case luci::CircleOpcode::MINIMUM: @@ -1443,10 +1520,10 @@ bool ConvertNCHWToNHWCPass::run(loco::Graph *g) case luci::CircleOpcode::PAD: case luci::CircleOpcode::PADV2: case luci::CircleOpcode::REDUCE_MAX: + case luci::CircleOpcode::REDUCE_MIN: case luci::CircleOpcode::RELU: case luci::CircleOpcode::RELU6: case luci::CircleOpcode::RSQRT: - case luci::CircleOpcode::SOFTMAX: case luci::CircleOpcode::SPLIT_V: case luci::CircleOpcode::SQUARED_DIFFERENCE: case luci::CircleOpcode::SUB: @@ -1487,7 +1564,8 @@ bool ConvertNCHWToNHWCPass::run(loco::Graph *g) { // TODO replace the check above with the input rank check, and remove the condition below if (not dynamic_cast(node) and - not dynamic_cast(node)) + not dynamic_cast(node) and + not dynamic_cast(node)) continue; } diff --git a/compiler/luci/pass/src/ConvertNCHWToNHWCPass.test.cpp b/compiler/luci/pass/src/ConvertNCHWToNHWCPass.test.cpp index 6bb3d32..fd32651 100644 --- a/compiler/luci/pass/src/ConvertNCHWToNHWCPass.test.cpp +++ b/compiler/luci/pass/src/ConvertNCHWToNHWCPass.test.cpp @@ -483,22 +483,6 @@ public: luci::CircleLogistic *logistic = nullptr; }; -class LogSoftmaxGraph final : public SimpleGraph -{ -protected: - loco::Node *insertGraphBody(loco::Node *input) override - { - log_softmax = g.nodes()->create(); - log_softmax->logits(input); - log_softmax->name("log_softmax"); - - return log_softmax; - } - -public: - luci::CircleLogSoftmax *log_softmax = nullptr; -}; - class MaximumGraph final : public SimpleGraph { protected: @@ -530,6 +514,27 @@ public: luci::CircleConst *limit = nullptr; }; +class MaximumNonConstGraph final : public SimpleGraph +{ +protected: + loco::Node *insertGraphBody(loco::Node *input) override + { + max = g.nodes()->create(); + max->dtype(loco::DataType::FLOAT32); + max->shape({1, 16, 4, 4}); + + max->x(input); + max->y(input); + + max->name("max"); + + return max; + } + +public: + luci::CircleMaximum *max = nullptr; +}; + class MeanGraph final : public SimpleGraph { protected: @@ -874,6 +879,51 @@ private: std::initializer_list _shape = {1, 16, 1, 1}; }; +class ReduceMinGraph final : public SimpleGraph +{ +protected: + loco::Node *insertGraphBody(loco::Node *input) override + { + rm = g.nodes()->create(); + rindices = g.nodes()->create(); + + rm->dtype(loco::DataType::FLOAT32); + rindices->dtype(loco::DataType::S32); + + rm->shape(_shape); + rindices->shape({static_cast(_axes.size())}); + + rindices->size(_axes.size()); + for (uint32_t i = 0; i < _axes.size(); ++i) + { + rindices->at(i) = _axes[i]; + } + + rm->input(input); + rm->reduction_indices(rindices); + rm->keep_dims(_keep_dims); + + rm->name("reduce_max"); + rindices->name("rindices"); + + return rm; + } + +public: + void keep_dims(bool val) { _keep_dims = val; } + void axes(std::vector val) { _axes = val; } + void shape(std::initializer_list val) { _shape = val; } + +public: + luci::CircleReduceMin *rm = nullptr; + luci::CircleConst *rindices = nullptr; + +private: + bool _keep_dims = true; + std::vector _axes = {2, 3}; + std::initializer_list _shape = {1, 16, 1, 1}; +}; + class ReluGraph final : public SimpleGraph { protected: @@ -922,22 +972,6 @@ public: luci::CircleRsqrt *rsqrt = nullptr; }; -class SoftmaxGraph final : public SimpleGraph -{ -protected: - loco::Node *insertGraphBody(loco::Node *input) override - { - softmax = g.nodes()->create(); - softmax->logits(input); - softmax->name("softmax"); - - return softmax; - } - -public: - luci::CircleSoftmax *softmax = nullptr; -}; - class SplitVGraphlet { public: @@ -1357,44 +1391,50 @@ TEST(ConvertNCHWToNHWC, Logistic) EXPECT_EQ(16, g.logistic->dim(3).value()); } -TEST(ConvertNCHWToNHWC, LogSoftmax) +TEST(ConvertNCHWToNHWC, Maximum) { - LogSoftmaxGraph g; + MaximumGraph g; g.init(); - run_phase(&g.g, true, true); + run_phase(&g.g, false, false); + + auto input_succs = loco::succs(g.input); + EXPECT_EQ(1, input_succs.size()); + check_post_trans(*input_succs.begin()); - check_pre_trans(g.log_softmax->logits()); + check_pre_trans(g.max->x()); - auto log_softmax_succs = loco::succs(g.log_softmax); - EXPECT_EQ(1, log_softmax_succs.size()); - check_post_trans(*log_softmax_succs.begin()); + auto max_succs = loco::succs(g.max); + EXPECT_EQ(1, max_succs.size()); + check_post_trans(*max_succs.begin()); - // Check log_softmax shape - EXPECT_EQ(1, g.log_softmax->dim(0).value()); - EXPECT_EQ(4, g.log_softmax->dim(1).value()); - EXPECT_EQ(4, g.log_softmax->dim(2).value()); - EXPECT_EQ(16, g.log_softmax->dim(3).value()); + check_pre_trans(g.output->from()); } -TEST(ConvertNCHWToNHWC, Maximum) +TEST(ConvertNCHWToNHWC, Maximum_non_scalar_NEG) { MaximumGraph g; g.init(); - run_phase(&g.g, false, false); + g.limit->shape({3}); - auto input_succs = loco::succs(g.input); - EXPECT_EQ(1, input_succs.size()); - check_post_trans(*input_succs.begin()); + luci::ConvertNCHWToNHWCPass pass(true, true); + EXPECT_FALSE(pass.run(&g.g)); +} + +TEST(ConvertNCHWToNHWC, MaximumNonConst) +{ + MaximumNonConstGraph g; + g.init(); + + run_phase(&g.g, true, true); check_pre_trans(g.max->x()); + check_pre_trans(g.max->y()); auto max_succs = loco::succs(g.max); EXPECT_EQ(1, max_succs.size()); check_post_trans(*max_succs.begin()); - - check_pre_trans(g.output->from()); } TEST(ConvertNCHWToNHWC, Mean) @@ -1553,6 +1593,17 @@ TEST(ConvertNCHWToNHWC, Minimum) check_pre_trans(g.output->from()); } +TEST(ConvertNCHWToNHWC, Minimum_non_scalar_NEG) +{ + MinimumGraph g; + g.init(); + + g.limit->shape({3}); + + luci::ConvertNCHWToNHWCPass pass(true, true); + EXPECT_FALSE(pass.run(&g.g)); +} + TEST(ConvertNCHWToNHWC, Mul) { MulGraph g; @@ -1893,6 +1944,85 @@ TEST(ConvertNCHWToNHWC, ReduceMax_keep_dims_false) } } +TEST(ConvertNCHWToNHWC, ReduceMin) +{ + ReduceMinGraph g; + g.init(); + + run_phase(&g.g, true, true); + + check_pre_trans(g.rm->input()); + + auto rm_succs = loco::succs(g.rm); + EXPECT_EQ(1, rm_succs.size()); + check_post_trans(*rm_succs.begin()); + + auto new_rindices = dynamic_cast(g.rm->reduction_indices()); + EXPECT_NE(nullptr, new_rindices); + EXPECT_EQ(1, new_rindices->rank()); + EXPECT_EQ(2, new_rindices->dim(0).value()); + EXPECT_EQ(2, new_rindices->size()); + EXPECT_EQ(1, new_rindices->at(0)); + EXPECT_EQ(2, new_rindices->at(1)); +} + +TEST(ConvertNCHWToNHWC, ReduceMin_keep_dims_false) +{ + struct TC + { + std::vector nchw_ind; + std::vector nhwc_ind; + std::initializer_list shape; + bool needs_transpose = false; + }; + + uint32_t n = 1; + uint32_t c = 16; + uint32_t h = 4; + uint32_t w = 4; + + std::vector test_cases{{{0}, {0}, {c, h, w}, true}, {{1}, {3}, {n, h, w}, false}, + {{2}, {1}, {n, c, w}, true}, {{3}, {2}, {n, c, h}, true}, + {{0, 1}, {0, 3}, {h, w}, false}, {{0, 2}, {0, 1}, {c, w}, true}, + {{0, 3}, {0, 2}, {c, h}, true}, {{1, 2}, {3, 1}, {n, w}, false}, + {{1, 3}, {3, 2}, {n, h}, false}, {{2, 3}, {1, 2}, {n, c}, false}, + {{0, 1, 2}, {0, 3, 1}, {w}, false}}; + + for (auto &tc : test_cases) + { + ReduceMinGraph g; + g.keep_dims(false); + g.axes(tc.nchw_ind); + g.shape(tc.shape); + g.init(); + + run_phase(&g.g, true, true); + + check_pre_trans(g.rm->input()); + + auto rm_succs = loco::succs(g.rm); + EXPECT_EQ(1, rm_succs.size()); + if (tc.needs_transpose) + { + EXPECT_NE(nullptr, dynamic_cast(*rm_succs.begin())); + } + else + { + EXPECT_NE(nullptr, dynamic_cast(*rm_succs.begin())); + } + + auto new_rindices = dynamic_cast(g.rm->reduction_indices()); + EXPECT_NE(nullptr, new_rindices); + EXPECT_EQ(1, new_rindices->rank()); + EXPECT_EQ(tc.nhwc_ind.size(), new_rindices->dim(0).value()); + EXPECT_EQ(tc.nhwc_ind.size(), new_rindices->size()); + for (uint32_t i = 0; i < tc.nhwc_ind.size(); ++i) + { + EXPECT_EQ(tc.nhwc_ind[i], new_rindices->at(i)); + } + } +} + TEST(ConvertNCHWToNHWC, Relu) { ReluGraph g; @@ -1953,26 +2083,6 @@ TEST(ConvertNCHWToNHWC, Rsqrt) EXPECT_EQ(16, g.rsqrt->dim(3).value()); } -TEST(ConvertNCHWToNHWC, Softmax) -{ - SoftmaxGraph g; - g.init(); - - run_phase(&g.g, true, true); - - check_pre_trans(g.softmax->logits()); - - auto softmax_succs = loco::succs(g.softmax); - EXPECT_EQ(1, softmax_succs.size()); - check_post_trans(*softmax_succs.begin()); - - // Check softmax shape - EXPECT_EQ(1, g.softmax->dim(0).value()); - EXPECT_EQ(4, g.softmax->dim(1).value()); - EXPECT_EQ(4, g.softmax->dim(2).value()); - EXPECT_EQ(16, g.softmax->dim(3).value()); -} - TEST(ConvertNCHWToNHWC, SplitV) { SplitVGraph g; diff --git a/compiler/luci/pass/src/ConvertToFakeQuantizedModelPass.cpp b/compiler/luci/pass/src/ConvertToFakeQuantizedModelPass.cpp index 72f5901..aacfce3 100644 --- a/compiler/luci/pass/src/ConvertToFakeQuantizedModelPass.cpp +++ b/compiler/luci/pass/src/ConvertToFakeQuantizedModelPass.cpp @@ -31,7 +31,10 @@ namespace luci::CircleQuantize *create_quantize(luci::CircleNode *node) { auto quantize = node->graph()->nodes()->create(); - quantize->name(node->name() + "_Quantize"); + // DESIGN NOTE: Why use '_FQ_Quantize' instead of '_Quantize'? + // '_Quantize' is used in mixed-precision quantization + // We add '_FQ' to distinguish Op from mixed-precision quantization + quantize->name(node->name() + "_FQ_Quantize"); quantize->dtype(node->dtype()); quantize->rank(node->rank()); for (uint32_t i = 0; i < node->rank(); i++) @@ -50,7 +53,10 @@ luci::CircleQuantize *create_quantize(luci::CircleNode *node) luci::CircleDequantize *create_dequantize(luci::CircleNode *node) { auto dequantize = node->graph()->nodes()->create(); - dequantize->name(node->name() + "_Dequantize"); + // DESIGN NOTE: Why use '_FQ_Dequantize' instead of '_Dequantize'? + // '_Dequantize' is used in mixed-precision quantization + // We add '_FQ' to distinguish Op from mixed-precision quantization + dequantize->name(node->name() + "_FQ_Dequantize"); dequantize->dtype(loco::DataType::FLOAT32); dequantize->rank(node->rank()); for (uint32_t i = 0; i < node->rank(); i++) @@ -184,6 +190,7 @@ struct FakeQuantize final : public luci::CircleNodeMutableVisitor // For non-const activation, insert Quantize-Dequantize Ops // and dequantize the node + void visit(luci::CircleAbs *node) { fq_activation(node); } void visit(luci::CircleAdd *node) { fq_activation(node); } void visit(luci::CircleAveragePool2D *node) { fq_activation(node); } void visit(luci::CircleBatchMatMul *node) { fq_activation(node); } @@ -201,6 +208,7 @@ struct FakeQuantize final : public luci::CircleNodeMutableVisitor void visit(luci::CirclePad *node) { fq_activation(node); } void visit(luci::CirclePRelu *node) { fq_activation(node); } void visit(luci::CircleMean *node) { fq_activation(node); } + void visit(luci::CircleReduceProd *node) { fq_activation(node); } void visit(luci::CircleReduceMax *node) { fq_activation(node); } void visit(luci::CircleRelu *node) { fq_activation(node); } void visit(luci::CircleRelu6 *node) { fq_activation(node); } @@ -216,15 +224,20 @@ struct FakeQuantize final : public luci::CircleNodeMutableVisitor // (dtype will be automatically updated by type inference) void visit(luci::CircleCast *) {} void visit(luci::CircleConcatenation *) {} + void visit(luci::CircleDepthToSpace *) {} void visit(luci::CircleGather *) {} void visit(luci::CircleSlice *) {} void visit(luci::CircleStridedSlice *) {} void visit(luci::CircleReshape *) {} + void visit(luci::CircleSpaceToDepth *) {} void visit(luci::CircleSplit *) {} void visit(luci::CircleSplitOut *) {} void visit(luci::CircleSplitV *) {} void visit(luci::CircleSplitVOut *) {} void visit(luci::CircleTranspose *) {} + void visit(luci::CirclePack *) {} + void visit(luci::CircleUnpack *) {} + void visit(luci::CircleUnpackOut *) {} // For Ops that return index, fake quantization is unnecessary void visit(luci::CircleArgMax *) {} diff --git a/compiler/luci/pass/src/ExpandBroadcastConstPass.test.cpp b/compiler/luci/pass/src/ExpandBroadcastConstPass.test.cpp index 0734e07..5df1b72 100644 --- a/compiler/luci/pass/src/ExpandBroadcastConstPass.test.cpp +++ b/compiler/luci/pass/src/ExpandBroadcastConstPass.test.cpp @@ -19,6 +19,8 @@ #include +#include // std::numeric_limits + #include namespace diff --git a/compiler/luci/pass/src/FoldDepthwiseConv2DPass.cpp b/compiler/luci/pass/src/FoldDepthwiseConv2DPass.cpp index 6e423e3..33f9f1d 100644 --- a/compiler/luci/pass/src/FoldDepthwiseConv2DPass.cpp +++ b/compiler/luci/pass/src/FoldDepthwiseConv2DPass.cpp @@ -23,6 +23,8 @@ #include +#include // std::numeric_limits + namespace { diff --git a/compiler/luci/pass/src/FoldDepthwiseConv2DPass.test.cpp b/compiler/luci/pass/src/FoldDepthwiseConv2DPass.test.cpp index b1ef568..36cae04 100644 --- a/compiler/luci/pass/src/FoldDepthwiseConv2DPass.test.cpp +++ b/compiler/luci/pass/src/FoldDepthwiseConv2DPass.test.cpp @@ -19,6 +19,8 @@ #include +#include // std::numeric_limits + #include namespace diff --git a/compiler/luci/pass/src/FoldFullyConnectedPass.cpp b/compiler/luci/pass/src/FoldFullyConnectedPass.cpp new file mode 100644 index 0000000..a3bca7e --- /dev/null +++ b/compiler/luci/pass/src/FoldFullyConnectedPass.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/FoldFullyConnectedPass.h" + +#include + +#include +#include + +#include + +#include // std::numeric_limits + +namespace +{ + +bool set_kernel_parameters(tflite::FullyConnectedParams *params, luci::CircleFullyConnected *node) +{ + switch (node->fusedActivationFunction()) + { + case luci::FusedActFunc::NONE: + case luci::FusedActFunc::TANH: + params->float_activation_min = std::numeric_limits::lowest(); + params->float_activation_max = std::numeric_limits::max(); + break; + case luci::FusedActFunc::RELU: + params->float_activation_min = 0; + params->float_activation_max = std::numeric_limits::max(); + break; + case luci::FusedActFunc::RELU_N1_TO_1: + params->float_activation_min = -1; + params->float_activation_max = 1; + break; + case luci::FusedActFunc::RELU6: + params->float_activation_min = 0; + params->float_activation_max = 6; + break; + default: + { + LOGGER(l); + WARN(l) << "Unsupported activation: " << uint32_t(node->fusedActivationFunction()); + return false; + } + } + + assert(node->weights_format() == + luci::CircleFullyConnected::WeightsFormat::DEFAULT); // FIX_CALLER_UNLESS + params->weights_format = tflite::FullyConnectedWeightsFormat::kDefault; + + return true; +} + +#define RETURN_FALSE_UNLESS(cond) \ + if (not(cond)) \ + return false; + +/** + * Fold FullyConnected with constant input and filter into a constant tensor + * + * BEFORE + * + * [CircleConst] [CircleConst] + * | | + * [CircleFullyConnected] + * + * AFTER + * + * [CircleConst] + */ +bool fold_fully_connected(luci::CircleFullyConnected *node) +{ + RETURN_FALSE_UNLESS(node != nullptr); + + LOGGER(l); + + auto const input = dynamic_cast(node->input()); + auto const weights = dynamic_cast(node->weights()); + auto const bias = dynamic_cast(node->bias()); + auto const no_bias = dynamic_cast(node->bias()); + + RETURN_FALSE_UNLESS(input != nullptr); + RETURN_FALSE_UNLESS(weights != nullptr); + RETURN_FALSE_UNLESS(node->weights_format() == luci::CircleFullyConnected::WeightsFormat::DEFAULT); + RETURN_FALSE_UNLESS(bias != nullptr or no_bias != nullptr); + + RETURN_FALSE_UNLESS(input->dtype() == loco::DataType::FLOAT32); + RETURN_FALSE_UNLESS(weights->dtype() == loco::DataType::FLOAT32); + if (bias) + RETURN_FALSE_UNLESS(bias->dtype() == loco::DataType::FLOAT32); + + auto const input_elems = input->size(); + + RETURN_FALSE_UNLESS(weights->rank() == 2); + RETURN_FALSE_UNLESS(input_elems % weights->dim(1).value() == 0); + auto const batch_size = input_elems / weights->dim(1).value(); + auto const num_units = weights->dim(0).value(); + + if (bias) + RETURN_FALSE_UNLESS(bias->size() == num_units); + + tflite::FullyConnectedParams params{}; + if (!set_kernel_parameters(¶ms, node)) + return false; // Unsupported kernel parameter values + + std::vector output_shape; + if (node->keep_num_dims() == false) + { + output_shape.push_back(batch_size); + output_shape.push_back(num_units); + } + else + { + output_shape.resize(input->rank()); + for (uint32_t i = 0; i < input->rank(); i++) + output_shape[i] = input->dim(i).value(); + output_shape[input->rank() - 1] = num_units; + } + + auto constant = node->graph()->nodes()->create(); + { + constant->name(node->name()); + constant->dtype(node->dtype()); + constant->rank(node->rank()); + constant->shape_status(luci::ShapeStatus::VALID); + uint32_t num_elem = 1; + for (uint32_t i = 0; i < node->rank(); ++i) + { + constant->dim(i).set(node->dim(i).value()); + num_elem *= node->dim(i).value(); + } + constant->size(num_elem); + } + + auto tensor_shape = [](luci::CircleNode *node) { + if (node == nullptr) + return tflite::RuntimeShape(); + + tflite::RuntimeShape runtime_shape(node->rank()); + for (uint32_t i = 0; i < node->rank(); ++i) + runtime_shape.SetDim(i, node->dim(i).value()); + return runtime_shape; + }; + + auto tensor_data = [](luci::CircleConst *node) -> float * { + if (node == nullptr) + return nullptr; + + return &node->at(0); + }; + + tflite::reference_ops::FullyConnected( + params, tensor_shape(input), tensor_data(input), tensor_shape(weights), tensor_data(weights), + tensor_shape(bias), tensor_data(bias), tensor_shape(constant), tensor_data(constant)); + + loco::replace(node).with(constant); + + return true; +} + +} // namespace + +namespace luci +{ + +/** + * Constant Folding for FullyConnected Op + **/ +bool FoldFullyConnectedPass::run(loco::Graph *g) +{ + bool changed = false; + for (auto node : loco::active_nodes(loco::output_nodes(g))) + { + auto fc = dynamic_cast(node); + + if (fold_fully_connected(fc)) + changed = true; + } + + return changed; +} + +} // namespace luci + +#undef RETURN_FALSE_UNLESS diff --git a/compiler/luci/pass/src/FoldFullyConnectedPass.test.cpp b/compiler/luci/pass/src/FoldFullyConnectedPass.test.cpp new file mode 100644 index 0000000..a8e64a2 --- /dev/null +++ b/compiler/luci/pass/src/FoldFullyConnectedPass.test.cpp @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/FoldFullyConnectedPass.h" +#include "PassTestGraphs.h" + +#include + +#include // std::numeric_limits + +#include + +namespace +{ + +/** + * Graph has an FullyConnected Op with constant inputs + * + * BEFORE + * + * [CircleConst] [CircleConst] + * | | + * [CircleFullyConnected] + * + * AFTER + * + * [CircleConst] + */ +class FoldFullyConnectedTest : public luci::ConstantFoldingTestGraph, public ::testing::Test +{ +#define INPUT_DIM 80 +#define NUM_UNITS 32 + +public: + FoldFullyConnectedTest() : luci::ConstantFoldingTestGraph({INPUT_DIM}, loco::DataType::FLOAT32) + { + _fc = _g.nodes()->create(); + _fc_input = _g.nodes()->create(); + _fc_weights = _g.nodes()->create(); + _fc_bias = _g.nodes()->create(); + + _fc->dtype(loco::DataType::FLOAT32); + _fc->fusedActivationFunction(luci::FusedActFunc::NONE); + _fc->input(_fc_input); + _fc->weights(_fc_weights); + _fc->bias(_fc_bias); + _fc->shape({NUM_UNITS}); + _fc->weights_format(luci::CircleFullyConnected::WeightsFormat::DEFAULT); + _fc->keep_num_dims(true); + + _fc_input->dtype(loco::DataType::FLOAT32); + _fc_input->shape({INPUT_DIM}); + _fc_input->size(INPUT_DIM); + + _fc_weights->dtype(loco::DataType::FLOAT32); + _fc_weights->shape({NUM_UNITS, INPUT_DIM}); + _fc_weights->size(NUM_UNITS * INPUT_DIM); + + _fc_bias->dtype(loco::DataType::FLOAT32); + _fc_bias->shape({1, NUM_UNITS}); + _fc_bias->size(NUM_UNITS); + + for (uint32_t i = 0; i < INPUT_DIM; ++i) + _fc_input->at(i) = 1.0; + + for (uint32_t i = 0; i < INPUT_DIM * NUM_UNITS; ++i) + _fc_weights->at(i) = 1.0; + + for (uint32_t i = 0; i < NUM_UNITS; ++i) + _fc_bias->at(i) = 0.0; + + _output->from(_fc); + } + +protected: + void init() final {} + +protected: + loco::Node *createFoldedPattern() final { return nullptr; } + +protected: + luci::CircleConst *getFoldedPattern() final + { + return loco::must_cast(_output->from()); + } + +protected: + luci::CircleFullyConnected *_fc = nullptr; + luci::CircleConst *_fc_input = nullptr; + luci::CircleConst *_fc_weights = nullptr; + luci::CircleConst *_fc_bias = nullptr; +#undef INPUT_DIM +#undef NUM_UNITS +}; + +} // namespace + +TEST_F(FoldFullyConnectedTest, fold_fc) +{ + luci::FoldFullyConnectedPass pass; + ASSERT_TRUE(pass.run(&_g)); + + auto folded_const = getFoldedPattern(); + EXPECT_EQ(folded_const->dtype(), loco::DataType::FLOAT32); + EXPECT_EQ(1, folded_const->rank()); + EXPECT_EQ(32, folded_const->dim(0)); + EXPECT_EQ(32, folded_const->size()); + for (uint32_t i = 0; i < 32; ++i) + EXPECT_NEAR(folded_const->at(i), 80, + std::numeric_limits::min()); +} + +TEST_F(FoldFullyConnectedTest, fold_fc_no_bias) +{ + auto no_bias = _g.nodes()->create(); + _fc->bias(no_bias); + + luci::FoldFullyConnectedPass pass; + ASSERT_TRUE(pass.run(&_g)); + + auto folded_const = getFoldedPattern(); + EXPECT_EQ(loco::DataType::FLOAT32, folded_const->dtype()); + EXPECT_EQ(1, folded_const->rank()); + EXPECT_EQ(32, folded_const->dim(0)); + EXPECT_EQ(32, folded_const->size()); + for (uint32_t i = 0; i < 32; ++i) + EXPECT_NEAR(folded_const->at(i), 80, + std::numeric_limits::min()); +} + +TEST_F(FoldFullyConnectedTest, fold_fc_NEG) +{ + auto new_fc = _g.nodes()->create(); + _fc->input(new_fc); + + luci::FoldFullyConnectedPass pass; + ASSERT_FALSE(pass.run(&_g)); +} + +TEST_F(FoldFullyConnectedTest, fold_fc_weight_format_NEG) +{ + auto new_fc = _g.nodes()->create(); + _fc->weights_format(luci::CircleFullyConnected::WeightsFormat::SHUFFLED4x16INT8); + + luci::FoldFullyConnectedPass pass; + ASSERT_FALSE(pass.run(&_g)); +} diff --git a/compiler/luci/pass/src/ForwardReshapeToUnaryOpPass.cpp b/compiler/luci/pass/src/ForwardReshapeToUnaryOpPass.cpp index bc09abe..3494a6e 100644 --- a/compiler/luci/pass/src/ForwardReshapeToUnaryOpPass.cpp +++ b/compiler/luci/pass/src/ForwardReshapeToUnaryOpPass.cpp @@ -76,6 +76,26 @@ luci::CircleReshape *create_cloned_reshape(luci::CircleReshape *reshape) return new_reshape; } +bool forward_reshape(luci::CircleReshape *reshape, luci::CircleAbs *abs) +{ + assert(reshape != nullptr); // FIX_CALLER_UNLESS + assert(abs != nullptr); // FIX_CALLER_UNLESS + + auto new_reshape = create_cloned_reshape(reshape); + if (not new_reshape) + return false; + + // reconnect network + loco::replace(abs).with(new_reshape); + abs->x(reshape->tensor()); + new_reshape->tensor(abs); + + // Do shape inference for this node again. + abs->shape_status(luci::ShapeStatus::UNDEFINED); + + return true; +} + bool forward_reshape(luci::CircleReshape *reshape, luci::CircleNeg *neg) { assert(reshape != nullptr); @@ -136,6 +156,14 @@ protected: return false; } + bool visit(luci::CircleAbs *node) + { + auto reshape = as_reshape(node->x()); + if (reshape == nullptr) + return false; + return forward_reshape(reshape, node); + } + bool visit(luci::CircleNeg *node) { auto reshape = as_reshape(node->x()); diff --git a/compiler/luci/pass/src/ForwardTransposeOpPass.cpp b/compiler/luci/pass/src/ForwardTransposeOpPass.cpp new file mode 100644 index 0000000..c76d733 --- /dev/null +++ b/compiler/luci/pass/src/ForwardTransposeOpPass.cpp @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/ForwardTransposeOpPass.h" + +#include +#include +#include +#include +#include + +using namespace luci; + +namespace +{ + +// Create new Transpose Op including perm +// Return nullptr if failed +CircleTranspose *create_cloned_transpose(CircleTranspose *transpose) +{ + assert(transpose != nullptr); // FIX_CALLER_UNLESS + + auto perm = dynamic_cast(transpose->perm()); + if (not perm) + return nullptr; + + CircleConst *cloned_perm = clone(perm); + if (cloned_perm == nullptr) + return nullptr; + + cloned_perm->name(perm->name() + "_C"); + luci::add_origin(cloned_perm, luci::get_origin(perm)); + + auto cloned_node = clone_node(transpose, transpose->graph()); + if (cloned_node == nullptr) + return nullptr; + + auto new_transpose = loco::must_cast(cloned_node); + new_transpose->perm(cloned_perm); + new_transpose->name(transpose->name() + "_C"); + luci::add_origin(new_transpose, luci::get_origin(transpose)); + + return new_transpose; +} + +uint32_t cal_offset(const std::vector &shape, const std::vector &indices) +{ + assert(shape.size() == indices.size()); // FIX_CALLER_UNLESS + + uint32_t offset = 0; + for (uint32_t i = 0; i < indices.size(); i++) + { + uint32_t index = indices[i]; + for (uint32_t j = shape.size() - 1; j > i; j--) + { + index *= shape[j]; + } + offset += index; + } + return offset; +} + +// Return reverse-transpose of 'node' +// i.e., Transpose(return value) = node +CircleConst *reverse_transposed(CircleConst *node, std::vector &t) +{ + assert(node->rank() == t.size()); // FIX_CALLER_UNLESS + assert(node->rank() == 4); // FIX_CALLER_UNLESS + + std::vector orig_shape(node->rank()); + std::vector new_shape(node->rank()); + + for (uint32_t i = 0; i < node->rank(); i++) + { + assert(t[i] < node->rank()); // FIX_CALLER_UNLESS + + orig_shape[i] = node->dim(i).value(); + new_shape[t[i]] = node->dim(i).value(); + } + + auto clone_const = clone(node); + for (uint32_t i = 0; i < node->rank(); i++) + clone_const->dim(i).set(new_shape[i]); + + clone_const->name(clone_const->name() + "_r_transposed"); + add_origin(clone_const, luci::get_origin(node)); + + for (uint32_t n = 0; n < clone_const->dim(0).value(); n++) + { + for (uint32_t h = 0; h < clone_const->dim(1).value(); h++) + { + for (uint32_t w = 0; w < clone_const->dim(2).value(); w++) + { + for (uint32_t c = 0; c < clone_const->dim(3).value(); c++) + { + std::vector new_indices{n, h, w, c}; + std::vector orig_indices{new_indices[t[0]], new_indices[t[1]], + new_indices[t[2]], new_indices[t[3]]}; + + const auto data = node->at(cal_offset(orig_shape, orig_indices)); + clone_const->at(cal_offset(new_shape, new_indices)) = data; + } + } + } + } + + return clone_const; +} + +bool check_rank_four(const CircleConst *c) { return c->rank() == 4; } + +// Return true if below conditions are met +// 1. t->perm() is CircleConst +// 2. t->perm() is S32 +bool check_perm(const CircleTranspose *t) +{ + auto perm = dynamic_cast(t->perm()); + if (not perm) + return false; + + switch (perm->dtype()) + { + case loco::DataType::S32: + for (uint32_t i = 0; i < perm->size(); i++) + { + auto data = perm->at(i); + // TODO Support not normalized index + if (data < 0 or data >= static_cast(t->rank())) + return false; + } + break; + // TODO Support S64 data type + default: + return false; + } + + return true; +} + +#define RETURN_FALSE_UNLESS(COND) \ + if (not(COND)) \ + return false; + +// Elementwise Binary Operator with const +class EBOWithConstPattern final : public CircleNodeMutableVisitor +{ +private: + template bool has_pattern(CIRCLE_OP_PTR node) + { + if (auto x = dynamic_cast(node->x())) + { + if (auto y = dynamic_cast(node->y())) + { + RETURN_FALSE_UNLESS(check_rank_four(x)); + RETURN_FALSE_UNLESS(check_perm(y)); + + auto new_const = gen_new_const(y, x); + assert(new_const); // FIX_ME_UNLESS + + auto new_transpose = create_cloned_transpose(y); + assert(new_transpose); // FIX_ME_UNLESS + + // Reconnect network + node->x(new_const); + node->y(y->a()); + loco::replace(node).with(new_transpose); + new_transpose->a(node); + + // Do shape inference for this node again. + node->shape_status(luci::ShapeStatus::UNDEFINED); + + return true; + } + } + + if (auto y = dynamic_cast(node->y())) + { + if (auto x = dynamic_cast(node->x())) + { + RETURN_FALSE_UNLESS(check_rank_four(y)); + RETURN_FALSE_UNLESS(check_perm(x)); + + auto new_const = gen_new_const(x, y); + assert(new_const); // FIX_ME_UNLESS + + auto new_transpose = create_cloned_transpose(x); + assert(new_transpose); // FIX_ME_UNLESS + + // Reconnect network + node->y(new_const); + node->x(x->a()); + loco::replace(node).with(new_transpose); + new_transpose->a(node); + + // Do shape inference for this node again. + node->shape_status(luci::ShapeStatus::UNDEFINED); + + return true; + } + } + + return false; + } + +public: + // Default + bool visit(luci::CircleNode *) { return false; } + + bool visit(luci::CircleAdd *node) { return has_pattern(node); } + + bool visit(luci::CircleMul *node) { return has_pattern(node); } + +private: + // Return a new const node after Tranpose Op is forwarded + // Return nullptr if unsupported cases + CircleConst *gen_new_const(CircleTranspose *t, CircleConst *c) + { + const auto perm = dynamic_cast(t->perm()); + + // Only support constant perm + if (not perm) + return nullptr; + + std::vector perm_data; + switch (perm->dtype()) + { + case loco::DataType::S32: + for (uint32_t i = 0; i < perm->size(); i++) + { + auto data = perm->at(i); + assert(data >= 0 and data < static_cast(t->rank())); + perm_data.emplace_back(static_cast(data)); + } + break; + // TODO Support S64 data type + default: + return nullptr; + } + + assert(perm_data.size() == t->rank()); // FIX_CALLER_UNLESS + + return reverse_transposed(c, perm_data); + } +}; + +// Elementwise Unary Operator +class EwUnaryPattern final : public CircleNodeMutableVisitor +{ +private: + // input is 'x' + template bool has_pattern_x(CIRCLE_OP_PTR node) + { + if (auto x = dynamic_cast(node->x())) + { + RETURN_FALSE_UNLESS(check_perm(x)); + + auto new_transpose = create_cloned_transpose(x); + assert(new_transpose); // FIX_ME_UNLESS + + // Reconnect network + node->x(x->a()); + loco::replace(node).with(new_transpose); + new_transpose->a(node); + + // Do shape inference for this node again. + node->shape_status(luci::ShapeStatus::UNDEFINED); + + return true; + } + + return false; + } + +public: + // Default + bool visit(luci::CircleNode *) { return false; } + + bool visit(luci::CircleAbs *node) { return has_pattern_x(node); } +}; + +} // namespace + +namespace luci +{ + +/** + * BEFORE + * | + * [CircleNode] [CircleConst] + * | / + * [CircleTranspose] [CircleConst] + * / | / + * [CircleNode] [(BinaryOp)] + * | | \ + * | | [CircleNode] + * | | | + * + * BinaryOp: CircleAdd, CircleMul, ... + * + * | + * [CircleNode] [CircleConst] + * | / + * [CircleTranspose] + * / | + * [CircleNode] [(UnaryOp)] + * | | \ + * | | [CircleNode] + * | | | + * + * UnaryOp: CircleAbs, ... + * + * AFTER + * | + * [CircleConst] [CircleNode] [CircleConst(updated)] + * | / | / + * [CircleTranspose] [(BinaryOp)] [CircleConst] + * | | / + * [CircleNode] [CircleTranspose] + * | | \ + * | | [CircleNode] + * | | | + * + * | + * [CircleConst] [CircleNode] + * | / | + * [CircleTranspose] [(UnaryOp)] [CircleConst] + * | | / + * [CircleNode] [CircleTranspose] + * | | \ + * | | [CircleNode] + * | | | + * + * Note: new [CircleTranspose] is added after [(BinaryOp)] + */ +bool ForwardTransposeOpPass::run(loco::Graph *g) +{ + bool changed = false; + EBOWithConstPattern eboc; + EwUnaryPattern ewu; + for (auto node : loco::active_nodes(loco::output_nodes(g))) + { + auto circle_node = loco::must_cast(node); + if (circle_node->accept(&eboc)) + changed = true; + else if (circle_node->accept(&ewu)) + changed = true; + } + return changed; +} + +#undef RETURN_FALSE_UNLESS + +} // namespace luci diff --git a/compiler/luci/pass/src/ForwardTransposeOpPass.test.cpp b/compiler/luci/pass/src/ForwardTransposeOpPass.test.cpp new file mode 100644 index 0000000..2d061c2 --- /dev/null +++ b/compiler/luci/pass/src/ForwardTransposeOpPass.test.cpp @@ -0,0 +1,524 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/ForwardTransposeOpPass.h" +#include "luci/Pass/CircleShapeInferencePass.h" + +#include +#include +#include + +#include + +#include + +namespace +{ + +using namespace luci::test; + +template class TransposeBinaryOpGraphlet +{ +public: + TransposeBinaryOpGraphlet() = default; + +public: + virtual ~TransposeBinaryOpGraphlet() = default; + +public: + void init(loco::Graph *g, const ShapeU32 shape_in, const ShapeU32 perm) + { + std::vector shape_in_v = shape_in; + std::vector perm_v = perm; + + assert(shape_in_v.size() == perm_v.size()); // FIX_CALLER_UNLESS + + _perm = g->nodes()->create(); + _const = g->nodes()->create(); + _transpose = g->nodes()->create(); + _binary = g->nodes()->create(); + + _perm->dtype(loco::DataType::S32); + _perm->rank(1); + _perm->dim(0).set(perm_v.size()); + _perm->shape_status(luci::ShapeStatus::VALID); + + _const->dtype(loco::DataType::FLOAT32); + _const->rank(shape_in_v.size()); + for (uint32_t i = 0; i < shape_in_v.size(); i++) + _const->dim(i).set(shape_in_v[perm_v[i]]); + _const->shape_status(luci::ShapeStatus::VALID); + + // values + const auto size = perm_v.size(); + _perm->size(size); + for (uint32_t i = 0; i < size; i++) + _perm->at(i) = perm_v[i]; + + uint32_t elems = 1; + for (uint32_t i = 0; i < size; i++) + elems *= shape_in_v[i]; + + _const->size(elems); + for (uint32_t i = 0; i < elems; i++) + _const->at(i) = i; + + _perm->name("transpose_perm"); + _transpose->name("transpose"); + _binary->name("binary"); + } + + luci::CircleTranspose *transpose(void) { return _transpose; } + + void switch_xy(void) + { + assert(_binary); // FIX_CALLER_UNLESS + auto temp = _binary->x(); + _binary->x(_binary->y()); + _binary->y(temp); + } + +protected: + luci::CircleTranspose *_transpose = nullptr; + T *_binary = nullptr; + luci::CircleConst *_perm = nullptr; + luci::CircleConst *_const = nullptr; +}; + +using TransposeAddGraphlet = TransposeBinaryOpGraphlet; +using TransposeMulGraphlet = TransposeBinaryOpGraphlet; + +class ForwardTransposeToAddGraph : public TestIOGraph, public TransposeAddGraphlet +{ +public: + void init(const ShapeU32 shape_in, const ShapeU32 shape_out) + { + TestIOGraph::init(shape_in, shape_out); + TransposeAddGraphlet::init(g(), shape_in, shape_out); + + // connect network + _transpose->a(input()); + _transpose->perm(_perm); + _binary->x(_transpose); + _binary->y(_const); + + output()->from(_binary); + } +}; + +class ForwardTransposeToAddInvalidGraph : public TestIOGraph, public TransposeAddGraphlet +{ +public: + void init(const ShapeU32 shape_in, const ShapeU32 shape_out) + { + TestIOGraph::init(shape_in, shape_out); + TransposeAddGraphlet::init(g(), shape_in, shape_out); + + // connect network + _transpose->a(input()); + _transpose->perm(_perm); + _binary->x(_transpose); + _binary->y(_transpose); + + output()->from(_binary); + } +}; + +class ForwardTransposeToMulGraph : public TestIOGraph, public TransposeMulGraphlet +{ +public: + void init(const ShapeU32 shape_in, const ShapeU32 shape_out) + { + TestIOGraph::init(shape_in, shape_out); + TransposeMulGraphlet::init(g(), shape_in, shape_out); + + // connect network + _transpose->a(input()); + _transpose->perm(_perm); + _binary->x(_transpose); + _binary->y(_const); + + output()->from(_binary); + } +}; + +void run_phase(loco::Graph *g) +{ + logo::Phase phase; + + // Default passes. + phase.emplace_back(std::make_unique()); + + // Pass to test + phase.emplace_back(std::make_unique()); + + logo::PhaseRunner phase_runner{g}; + phase_runner.run(phase); +} + +class ForwardTransposeToAddGraphTest : public ::testing::Test +{ +public: + void run_pass(void) { run_phase(_graph.g()); } + +protected: + ForwardTransposeToAddGraph _graph; +}; + +class ForwardTransposeToAddGraphNegTest : public ::testing::Test +{ +public: + void run_pass(void) { run_phase(_graph.g()); } + +protected: + ForwardTransposeToAddInvalidGraph _graph; +}; + +class ForwardTransposeToMulGraphTest : public ::testing::Test +{ +public: + void run_pass(void) { run_phase(_graph.g()); } + +protected: + ForwardTransposeToMulGraph _graph; +}; + +} // namespace + +TEST_F(ForwardTransposeToAddGraphTest, forward_add_xy) +{ + _graph.init({1, 64, 51, 1}, {0, 3, 2, 1}); + + run_pass(); + + auto transpose = dynamic_cast(_graph.output()->from()); + EXPECT_NE(nullptr, transpose); + EXPECT_EQ(4, transpose->rank()); + EXPECT_EQ(1, transpose->dim(0).value()); + EXPECT_EQ(1, transpose->dim(1).value()); + EXPECT_EQ(51, transpose->dim(2).value()); + EXPECT_EQ(64, transpose->dim(3).value()); + + auto add = dynamic_cast(transpose->a()); + EXPECT_NE(nullptr, add); + EXPECT_EQ(4, add->rank()); + EXPECT_EQ(1, add->dim(0).value()); + EXPECT_EQ(64, add->dim(1).value()); + EXPECT_EQ(51, add->dim(2).value()); + EXPECT_EQ(1, add->dim(3).value()); + + auto add_const = dynamic_cast(add->y()); + EXPECT_NE(nullptr, add_const); + EXPECT_EQ(4, add_const->rank()); + EXPECT_EQ(1, add_const->dim(0).value()); + EXPECT_EQ(64, add_const->dim(1).value()); + EXPECT_EQ(51, add_const->dim(2).value()); + EXPECT_EQ(1, add_const->dim(3).value()); +} + +TEST_F(ForwardTransposeToAddGraphTest, forward_add_yx) +{ + _graph.init({1, 64, 51, 1}, {0, 3, 2, 1}); + _graph.switch_xy(); + + run_pass(); + + auto transpose = dynamic_cast(_graph.output()->from()); + EXPECT_NE(nullptr, transpose); + EXPECT_EQ(4, transpose->rank()); + EXPECT_EQ(1, transpose->dim(0).value()); + EXPECT_EQ(1, transpose->dim(1).value()); + EXPECT_EQ(51, transpose->dim(2).value()); + EXPECT_EQ(64, transpose->dim(3).value()); + + auto mul = dynamic_cast(transpose->a()); + EXPECT_NE(nullptr, mul); + EXPECT_EQ(4, mul->rank()); + EXPECT_EQ(1, mul->dim(0).value()); + EXPECT_EQ(64, mul->dim(1).value()); + EXPECT_EQ(51, mul->dim(2).value()); + EXPECT_EQ(1, mul->dim(3).value()); + + auto mul_const = dynamic_cast(mul->x()); + EXPECT_NE(nullptr, mul_const); + EXPECT_EQ(4, mul_const->rank()); + EXPECT_EQ(1, mul_const->dim(0).value()); + EXPECT_EQ(64, mul_const->dim(1).value()); + EXPECT_EQ(51, mul_const->dim(2).value()); + EXPECT_EQ(1, mul_const->dim(3).value()); +} + +TEST_F(ForwardTransposeToMulGraphTest, forward_mul_xy) +{ + _graph.init({1, 64, 51, 1}, {0, 3, 2, 1}); + + run_pass(); + + auto transpose = dynamic_cast(_graph.output()->from()); + EXPECT_NE(nullptr, transpose); + EXPECT_EQ(4, transpose->rank()); + EXPECT_EQ(1, transpose->dim(0).value()); + EXPECT_EQ(1, transpose->dim(1).value()); + EXPECT_EQ(51, transpose->dim(2).value()); + EXPECT_EQ(64, transpose->dim(3).value()); + + auto mul = dynamic_cast(transpose->a()); + EXPECT_NE(nullptr, mul); + EXPECT_EQ(4, mul->rank()); + EXPECT_EQ(1, mul->dim(0).value()); + EXPECT_EQ(64, mul->dim(1).value()); + EXPECT_EQ(51, mul->dim(2).value()); + EXPECT_EQ(1, mul->dim(3).value()); + + auto mul_const = dynamic_cast(mul->y()); + EXPECT_NE(nullptr, mul_const); + EXPECT_EQ(4, mul_const->rank()); + EXPECT_EQ(1, mul_const->dim(0).value()); + EXPECT_EQ(64, mul_const->dim(1).value()); + EXPECT_EQ(51, mul_const->dim(2).value()); + EXPECT_EQ(1, mul_const->dim(3).value()); +} + +TEST_F(ForwardTransposeToMulGraphTest, forward_mul_yx) +{ + _graph.init({1, 64, 51, 1}, {0, 3, 2, 1}); + _graph.switch_xy(); + + run_pass(); + + auto transpose = dynamic_cast(_graph.output()->from()); + EXPECT_NE(nullptr, transpose); + EXPECT_EQ(4, transpose->rank()); + EXPECT_EQ(1, transpose->dim(0).value()); + EXPECT_EQ(1, transpose->dim(1).value()); + EXPECT_EQ(51, transpose->dim(2).value()); + EXPECT_EQ(64, transpose->dim(3).value()); + + auto mul = dynamic_cast(transpose->a()); + EXPECT_NE(nullptr, mul); + EXPECT_EQ(4, mul->rank()); + EXPECT_EQ(1, mul->dim(0).value()); + EXPECT_EQ(64, mul->dim(1).value()); + EXPECT_EQ(51, mul->dim(2).value()); + EXPECT_EQ(1, mul->dim(3).value()); + + auto mul_const = dynamic_cast(mul->x()); + EXPECT_NE(nullptr, mul_const); + EXPECT_EQ(4, mul_const->rank()); + EXPECT_EQ(1, mul_const->dim(0).value()); + EXPECT_EQ(64, mul_const->dim(1).value()); + EXPECT_EQ(51, mul_const->dim(2).value()); + EXPECT_EQ(1, mul_const->dim(3).value()); +} + +TEST_F(ForwardTransposeToAddGraphTest, forward_transpose_add_NEG) +{ + _graph.init({1, 64, 51, 1}, {0, 3, 2, 1}); + + // Remove add + _graph.output()->from(_graph.transpose()); + + luci::ForwardTransposeOpPass pass; + EXPECT_FALSE(pass.run(_graph.g())); +} + +TEST_F(ForwardTransposeToAddGraphNegTest, forward_transpose_add_non_const_NEG) +{ + _graph.init({1, 64, 51, 1}, {0, 3, 2, 1}); + + luci::ForwardTransposeOpPass pass; + EXPECT_FALSE(pass.run(_graph.g())); +} + +TEST_F(ForwardTransposeToMulGraphTest, forward_transpose_mul_NEG) +{ + _graph.init({1, 64, 51, 1}, {0, 3, 2, 1}); + + // Remove mul + _graph.output()->from(_graph.transpose()); + + luci::ForwardTransposeOpPass pass; + EXPECT_FALSE(pass.run(_graph.g())); +} + +// Unary + +namespace +{ + +template class TransposeUnaryOpGraphlet +{ +public: + TransposeUnaryOpGraphlet() = default; + +public: + virtual ~TransposeUnaryOpGraphlet() = default; + +public: + void init(loco::Graph *g, const ShapeU32 shape_in, const ShapeU32 perm) + { + std::vector shape_in_v = shape_in; + std::vector perm_v = perm; + + assert(shape_in_v.size() == perm_v.size()); // FIX_CALLER_UNLESS + + _perm = g->nodes()->create(); + _const = g->nodes()->create(); + _transpose = g->nodes()->create(); + _unary = g->nodes()->create(); + + _perm->dtype(loco::DataType::S32); + _perm->rank(1); + _perm->dim(0).set(perm_v.size()); + _perm->shape_status(luci::ShapeStatus::VALID); + + _const->dtype(loco::DataType::FLOAT32); + _const->rank(shape_in_v.size()); + for (uint32_t i = 0; i < shape_in_v.size(); i++) + _const->dim(i).set(shape_in_v[perm_v[i]]); + _const->shape_status(luci::ShapeStatus::VALID); + + // values + const auto size = perm_v.size(); + _perm->size(size); + for (uint32_t i = 0; i < size; i++) + _perm->at(i) = perm_v[i]; + + uint32_t elems = 1; + for (uint32_t i = 0; i < size; i++) + elems *= shape_in_v[i]; + + _const->size(elems); + for (uint32_t i = 0; i < elems; i++) + _const->at(i) = i; + + _perm->name("transpose_perm"); + _transpose->name("transpose"); + _unary->name("_unary"); + } + + luci::CircleTranspose *transpose(void) { return _transpose; } + +protected: + luci::CircleTranspose *_transpose = nullptr; + T *_unary = nullptr; + luci::CircleConst *_perm = nullptr; + luci::CircleConst *_const = nullptr; +}; + +using TransposeAbsGraphlet = TransposeUnaryOpGraphlet; + +class ForwardTransposeToAbsGraph : public TestIOGraph, public TransposeAbsGraphlet +{ +public: + void init(const ShapeU32 shape_in, const ShapeU32 shape_out) + { + TestIOGraph::init(shape_in, shape_out); + TransposeAbsGraphlet::init(g(), shape_in, shape_out); + + // connect network + _transpose->a(input()); + _transpose->perm(_perm); + _unary->x(_transpose); + + output()->from(_unary); + } +}; + +class ForwardTransposeToAbsInvalidGraph : public TestIOGraph, public TransposeAbsGraphlet +{ +public: + void init(const ShapeU32 shape_in, const ShapeU32 shape_out) + { + TestIOGraph::init(shape_in, shape_out); + TransposeAbsGraphlet::init(g(), shape_in, shape_out); + + _relu = g()->nodes()->create(); + _relu->dtype(loco::DataType::FLOAT32); + _relu->name("relu"); + + // connect network + _relu->features(input()); + _unary->x(_relu); + + output()->from(_unary); + } + +protected: + luci::CircleRelu *_relu = nullptr; +}; + +class ForwardTransposeToAbsGraphTest : public ::testing::Test +{ +public: + void run_pass(void) { run_phase(_graph.g()); } + +protected: + ForwardTransposeToAbsGraph _graph; +}; + +class ForwardTransposeToAbsGraphNegTest : public ::testing::Test +{ +public: + void run_pass(void) { run_phase(_graph.g()); } + +protected: + ForwardTransposeToAbsInvalidGraph _graph; +}; + +} // namespace + +TEST_F(ForwardTransposeToAbsGraphTest, forward_abs_x) +{ + _graph.init({1, 64, 51, 1}, {0, 3, 2, 1}); + + run_pass(); + + auto transpose = dynamic_cast(_graph.output()->from()); + EXPECT_NE(nullptr, transpose); + EXPECT_EQ(4, transpose->rank()); + EXPECT_EQ(1, transpose->dim(0).value()); + EXPECT_EQ(1, transpose->dim(1).value()); + EXPECT_EQ(51, transpose->dim(2).value()); + EXPECT_EQ(64, transpose->dim(3).value()); + + auto abs = dynamic_cast(transpose->a()); + EXPECT_NE(nullptr, abs); + EXPECT_EQ(4, abs->rank()); + EXPECT_EQ(1, abs->dim(0).value()); + EXPECT_EQ(64, abs->dim(1).value()); + EXPECT_EQ(51, abs->dim(2).value()); + EXPECT_EQ(1, abs->dim(3).value()); +} + +TEST_F(ForwardTransposeToAbsGraphTest, forward_transpose_abs_NEG) +{ + _graph.init({1, 64, 51, 1}, {0, 3, 2, 1}); + + // Remove abs + _graph.output()->from(_graph.transpose()); + + luci::ForwardTransposeOpPass pass; + EXPECT_FALSE(pass.run(_graph.g())); +} + +TEST_F(ForwardTransposeToAbsGraphNegTest, forward_transpose_abs_non_transpose_NEG) +{ + _graph.init({1, 64, 51, 1}, {0, 3, 2, 1}); + + luci::ForwardTransposeOpPass pass; + EXPECT_FALSE(pass.run(_graph.g())); +} diff --git a/compiler/luci/pass/src/FuseAddWithFullyConnectedPass.cpp b/compiler/luci/pass/src/FuseAddWithFullyConnectedPass.cpp index 3cf31ed..1d4a2e3 100644 --- a/compiler/luci/pass/src/FuseAddWithFullyConnectedPass.cpp +++ b/compiler/luci/pass/src/FuseAddWithFullyConnectedPass.cpp @@ -86,6 +86,14 @@ bool fuse_add_with_fc(luci::CircleFullyConnected *fc) if (not(addition->dim(rank - 1) == weights->dim(0))) return false; + auto bias = loco::must_cast(fc->bias()); + + // We only support (1) constant bias (2) no bias + // If bias is neither (1) nor (2), it would be a feature map + if (bias->opcode() != luci::CircleOpcode::CIRCLECONST and + bias->opcode() != luci::CircleOpcode::CIRCLEOUTPUTEXCLUDE) + return false; + auto fused_bias = luci::clone(addition); // Add existing bias values diff --git a/compiler/luci/pass/src/FuseAddWithFullyConnectedPass.test.cpp b/compiler/luci/pass/src/FuseAddWithFullyConnectedPass.test.cpp index 4cc2eb5..3007965 100644 --- a/compiler/luci/pass/src/FuseAddWithFullyConnectedPass.test.cpp +++ b/compiler/luci/pass/src/FuseAddWithFullyConnectedPass.test.cpp @@ -125,6 +125,15 @@ public: public: luci::CircleFullyConnected *fc() { return _fc; } +public: + void to_fm_bias(void) + { + assert(_fc != nullptr); // FIX_ME_UNLESS + + auto new_fc = _fc->graph()->nodes()->create(); + _fc->bias(new_fc); + } + protected: luci::CircleFullyConnected *_fc = nullptr; luci::CircleAdd *_add = nullptr; @@ -174,3 +183,14 @@ TEST_F(FuseAddWithFullyConnectedPassTest, simple_test) EXPECT_EQ(i, bias->at(i)); } } + +TEST_F(FuseAddWithFullyConnectedPassTest, fm_bias_NEG) +{ + g.init(); + + // Bias is a feature map. Add is not fused. + g.to_fm_bias(); + + auto ret = pass.run(g.g()); + EXPECT_EQ(false, ret); +} diff --git a/compiler/luci/pass/src/FuseBCQPass.cpp b/compiler/luci/pass/src/FuseBCQPass.cpp index 09180d8..3f8f700 100644 --- a/compiler/luci/pass/src/FuseBCQPass.cpp +++ b/compiler/luci/pass/src/FuseBCQPass.cpp @@ -679,7 +679,6 @@ bool FuseBCQPass::run(luci::Module *m) if (output_node->index() == 0 || (int)output_node->index() > original_output_cnt) { auto noOp = main_graph->nodes()->create(); - noOp->dtype(loco::DataType::FLOAT32); // TODO Remove this setting output_node->from(noOp); changed = true; } diff --git a/compiler/luci/pass/src/FuseBatchNormWithTConvPass.cpp b/compiler/luci/pass/src/FuseBatchNormWithTConvPass.cpp index e6b54df..265a839 100644 --- a/compiler/luci/pass/src/FuseBatchNormWithTConvPass.cpp +++ b/compiler/luci/pass/src/FuseBatchNormWithTConvPass.cpp @@ -23,6 +23,26 @@ namespace { + +template +void replace_with_relu(luci::CircleNode *target, luci::CircleNode *feature, + const std::string &relu_name) +{ + assert(target != nullptr); + assert(feature != nullptr); + + auto relu = target->graph()->nodes()->create(); + relu->features(feature); + relu->name(relu_name); + luci::add_origin(relu, luci::get_origin(target)); + + replace(target).with(relu); +} + +} // namespace + +namespace +{ /** * Fuse Mul-Add to TransposeConv if possible. * @@ -49,10 +69,10 @@ namespace * | / / | / * [CircleTransposeConv] [CircleAdd] * | - * ([CircleRelu6]) + * ([CircleRelu]/[CircleRelu6]) * | * - * Note: CircleRelu6 is inserted if Add activation is ReLU6 + * Note: CircleRelu or CircleRelu6 is inserted if Add activation is ReLU/ReLU6 */ bool fused_batch_norm_with_tconv(luci::CircleAdd *add) { @@ -80,7 +100,8 @@ bool fused_batch_norm_with_tconv(luci::CircleAdd *add) if (add->dtype() != loco::DataType::FLOAT32) return false; if (add->fusedActivationFunction() != luci::FusedActFunc::NONE && - add->fusedActivationFunction() != luci::FusedActFunc::RELU6) + add->fusedActivationFunction() != luci::FusedActFunc::RELU6 && + add->fusedActivationFunction() != luci::FusedActFunc::RELU) return false; // tconv bias is optional @@ -202,19 +223,23 @@ bool fused_batch_norm_with_tconv(luci::CircleAdd *add) luci::add_origin(fused_tconv, luci::get_origin(bias)); } - if (add->fusedActivationFunction() == luci::FusedActFunc::RELU6) + switch (add->fusedActivationFunction()) { - // separate relu op from add op - auto relu = add->graph()->nodes()->create(); - relu->features(fused_tconv); - relu->name(name + "/Relu6"); - luci::add_origin(relu, luci::get_origin(add)); + case luci::FusedActFunc::RELU6: + replace_with_relu(add, fused_tconv, name + "/Relu6"); + break; - replace(add).with(relu); - } - else - { - replace(add).with(fused_tconv); + case luci::FusedActFunc::RELU: + replace_with_relu(add, fused_tconv, name + "/Relu"); + break; + + case luci::FusedActFunc::NONE: + replace(add).with(fused_tconv); + break; + + default: + assert(false); + break; } return true; diff --git a/compiler/luci/pass/src/FusePReluPass.cpp b/compiler/luci/pass/src/FusePReluPass.cpp new file mode 100644 index 0000000..a5ce60e --- /dev/null +++ b/compiler/luci/pass/src/FusePReluPass.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/FusePReluPass.h" +#include "helpers/NodeFiller.h" + +#include + +#include +#include + +#include + +// Helper to fuse PRelu +namespace +{ + +/** + * Below diagram shows PRelu pattern to fuse. + * - this pattern will be replaced with one PRelu + * + * [In] + * | + * V + * +---- ifm ----+ + * | | | + * | | V + * | | abs + * | V | + * | sub <---+ + * | | + * | V + * | mul_alpha (alpha of PRelu) + * | | + * V V + * relu mul_half (0.5) + * | | + * | V + * +---> add + * | + * V + * [Out] + * + */ +class PReluPattern final +{ +public: + PReluPattern(luci::CircleAdd *candidate) + { + assert(candidate); + _add_ofm = candidate; + } + +public: + bool matched(); + +public: + luci::CircleNode *_ifm = nullptr; + luci::CircleRelu *_relu = nullptr; + luci::CircleAbs *_abs = nullptr; + luci::CircleSub *_sub = nullptr; + luci::CircleMul *_mul_alpha = nullptr; + luci::CircleMul *_mul_half = nullptr; + luci::CircleAdd *_add_ofm = nullptr; + luci::CircleConst *_const_alpha = nullptr; + luci::CircleConst *_const_half = nullptr; +}; + +#define CHECK_OR_FALSE(condition) \ + if (not(condition)) \ + return false; + +bool PReluPattern::matched() +{ + // check pattern + CHECK_OR_FALSE(luci::fill(&_relu, &_mul_half).with_commutative_args_of(_add_ofm)); + CHECK_OR_FALSE(luci::fill(&_mul_alpha, &_const_half).with_commutative_args_of(_mul_half)); + CHECK_OR_FALSE(luci::fill(&_sub, &_const_alpha).with_commutative_args_of(_mul_alpha)); + + CHECK_OR_FALSE(luci::fill(&_ifm, &_abs).with_args_of(_sub)); + + CHECK_OR_FALSE(_relu->features() == _ifm); + CHECK_OR_FALSE(_abs->x() == _ifm); + + // Check Activation to be NONE + CHECK_OR_FALSE(_sub->fusedActivationFunction() == luci::FusedActFunc::NONE); + CHECK_OR_FALSE(_mul_alpha->fusedActivationFunction() == luci::FusedActFunc::NONE); + CHECK_OR_FALSE(_mul_half->fusedActivationFunction() == luci::FusedActFunc::NONE); + CHECK_OR_FALSE(_add_ofm->fusedActivationFunction() == luci::FusedActFunc::NONE); + + // TODO support other types? + // check if _const_half is really FLOAT32 & 0.5 + CHECK_OR_FALSE(_const_half->dtype() == loco::DataType::FLOAT32); + CHECK_OR_FALSE(_const_half->size() == 1); + CHECK_OR_FALSE(_const_half->at(0) == 0.5); + + // check _const_alpha condition + CHECK_OR_FALSE(_const_alpha->dtype() == loco::DataType::FLOAT32); + // TODO add more if needed + + return true; +} + +#undef CHECK_OR_FALSE + +class FusePRelu final +{ +public: + FusePRelu(const PReluPattern &p) : _p(p) {} + +public: + void apply(void); + +private: + luci::CirclePRelu *create_prelu(loco::Graph *graph); + +private: + const PReluPattern &_p; +}; + +luci::CirclePRelu *FusePRelu::create_prelu(loco::Graph *graph) +{ + assert(graph); + + auto prelu = graph->nodes()->create(); + prelu->input(_p._ifm); + prelu->alpha(_p._const_alpha); + prelu->name(_p._add_ofm->name() + "_prelu"); + return prelu; +} + +void FusePRelu::apply() +{ + auto graph = _p._add_ofm->graph(); + + auto prelu = create_prelu(graph); + + // set origin + std::vector> origin_vec{ + luci::get_origin(_p._relu), luci::get_origin(_p._abs), luci::get_origin(_p._sub), + luci::get_origin(_p._mul_alpha), luci::get_origin(_p._mul_half), luci::get_origin(_p._add_ofm)}; + + luci::add_origin(prelu, luci::composite_origin(origin_vec)); + + replace(_p._add_ofm).with(prelu); +} + +} // namespace + +namespace +{ + +bool fuse_prelu(luci::CircleAdd *add) +{ + assert(add); + + PReluPattern pattern(add); + if (pattern.matched()) + { + FusePRelu fuse(pattern); + fuse.apply(); + return true; + } + return false; +} + +} // namespace + +namespace luci +{ + +bool FusePReluPass::run(loco::Graph *g) +{ + bool changed = false; + + for (auto node : loco::active_nodes(loco::output_nodes(g))) + { + auto add = dynamic_cast(node); + if (not add) + continue; + + if (fuse_prelu(add)) + changed = true; + } + + return changed; +} + +} // namespace luci diff --git a/compiler/luci/pass/src/FusePReluPass.test.cpp b/compiler/luci/pass/src/FusePReluPass.test.cpp new file mode 100644 index 0000000..209fe39 --- /dev/null +++ b/compiler/luci/pass/src/FusePReluPass.test.cpp @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/FusePReluPass.h" + +#include + +#include + +#include + +namespace +{ + +using namespace luci::test; + +class PReluGraphlet +{ +public: + PReluGraphlet() = default; + + void init(loco::Graph *g) + { + _abs = g->nodes()->create(); + _sub = g->nodes()->create(); + _mul_alpha = g->nodes()->create(); + _mul_half = g->nodes()->create(); + _relu = g->nodes()->create(); + _add = g->nodes()->create(); + _const_alpha = g->nodes()->create(); + _const_half = g->nodes()->create(); + + _sub->fusedActivationFunction(luci::FusedActFunc::NONE); + _mul_alpha->fusedActivationFunction(luci::FusedActFunc::NONE); + _mul_half->fusedActivationFunction(luci::FusedActFunc::NONE); + _add->fusedActivationFunction(luci::FusedActFunc::NONE); + + _abs->name("abs"); + _sub->name("sub"); + _mul_alpha->name("mul_alpha"); + _mul_half->name("mul_half"); + _relu->name("relu"); + _add->name("add"); + _const_alpha->name("const_alpha"); + _const_half->name("const_half"); + + _const_alpha->dtype(loco::DataType::FLOAT32); + _const_alpha->size(1); + _const_alpha->shape({1}); + _const_alpha->at(0) = 0.1; + _const_alpha->shape_status(luci::ShapeStatus::VALID); + + _const_half->dtype(loco::DataType::FLOAT32); + _const_half->size(1); + _const_half->shape({1}); + _const_half->at(0) = 0.5; + _const_half->shape_status(luci::ShapeStatus::VALID); + } + + void invalid_half() { _const_half->at(0) = 0.1; } + void invalid_act() { _add->fusedActivationFunction(luci::FusedActFunc::RELU); } + +protected: + luci::CircleAbs *_abs = nullptr; + luci::CircleSub *_sub = nullptr; + luci::CircleMul *_mul_alpha = nullptr; + luci::CircleMul *_mul_half = nullptr; + luci::CircleRelu *_relu = nullptr; + luci::CircleAdd *_add = nullptr; + luci::CircleConst *_const_alpha = nullptr; + luci::CircleConst *_const_half = nullptr; +}; + +class FusePReluTestGraph : public TestIOGraph, public PReluGraphlet +{ +public: + FusePReluTestGraph() = default; + + void init(void) + { + TestIOGraph::init({1}, {1}); + PReluGraphlet::init(g()); + + _relu->features(input()); + _abs->x(input()); + _sub->x(input()); + _sub->y(_abs); + _mul_alpha->x(_sub); + _mul_alpha->y(_const_alpha); + _mul_half->x(_mul_alpha); + _mul_half->y(_const_half); + _add->x(_relu); + _add->y(_mul_half); + + output()->from(_add); + } +}; + +class FusePReluTestNegGraph : public TestIOGraph, public PReluGraphlet +{ +public: + FusePReluTestNegGraph() = default; + + void init(void) + { + TestIOGraph::init({1}, {1}); + PReluGraphlet::init(g()); + + _relu->features(input()); + _abs->x(input()); + // NOTE x and y are incorrect + _sub->x(_abs); + _sub->y(input()); + _mul_alpha->x(_sub); + _mul_alpha->y(_const_alpha); + _mul_half->x(_mul_alpha); + _mul_half->y(_const_half); + _add->x(_relu); + _add->y(_mul_half); + + output()->from(_add); + } +}; + +} // namespace + +TEST(FusePReluPassTest, name) +{ + luci::FusePReluPass pass; + auto const name = pass.name(); + ASSERT_NE(nullptr, name); +} + +TEST(FusePReluPassTest, fuse) +{ + FusePReluTestGraph g; + luci::FusePReluPass pass; + + g.init(); + + EXPECT_TRUE(pass.run(g.g())); +} + +TEST(FusePReluPassTest, fuse_invalid_half_NEG) +{ + FusePReluTestNegGraph g; + luci::FusePReluPass pass; + + g.init(); + g.invalid_half(); + + EXPECT_FALSE(pass.run(g.g())); +} + +TEST(FusePReluPassTest, fuse_invalid_act_NEG) +{ + FusePReluTestNegGraph g; + luci::FusePReluPass pass; + + g.init(); + g.invalid_act(); + + EXPECT_FALSE(pass.run(g.g())); +} + +TEST(FusePReluPassTest, fuse_NEG) +{ + FusePReluTestNegGraph g; + luci::FusePReluPass pass; + + g.init(); + + EXPECT_FALSE(pass.run(g.g())); +} diff --git a/compiler/luci/pass/src/QuantizationUtils.cpp b/compiler/luci/pass/src/QuantizationUtils.cpp index 06a4ae9..45d229a 100644 --- a/compiler/luci/pass/src/QuantizationUtils.cpp +++ b/compiler/luci/pass/src/QuantizationUtils.cpp @@ -34,6 +34,8 @@ bool is_quantized(const CircleNode *node) node->dtype() == loco::DataType::S64); // bias (int16 quant) } +bool is_fp32(const CircleNode *node) { return node->dtype() == loco::DataType::FLOAT32; } + uint8_t fp32_to_uint8_cast(float f) { assert(std::numeric_limits::min() <= f); @@ -124,8 +126,8 @@ void compute_sym_scale_zp(float min, float max, float &scaling_factor, int64_t & : scale_factor_from_max_side; // protect scale from being very low to avoid overflow/underflow - if (scaling_factor < 1e-9) - scaling_factor = 1e-9; + if (scaling_factor < 1e-8) + scaling_factor = 1e-8; zp = 0; nudged_min = static_cast(qmin_double * scaling_factor); diff --git a/compiler/luci/pass/src/QuantizationUtils.h b/compiler/luci/pass/src/QuantizationUtils.h index 4d5316c..0720c98 100644 --- a/compiler/luci/pass/src/QuantizationUtils.h +++ b/compiler/luci/pass/src/QuantizationUtils.h @@ -60,6 +60,9 @@ void propagate_pad_v2_quantparam(luci::CirclePadV2 *pad_v2); // Return true if the node is quantized bool is_quantized(const CircleNode *node); +// Return true if the node is fp32 +bool is_fp32(const CircleNode *node); + enum ActivationQType { MinMax, // Quantize using recorded min/max diff --git a/compiler/luci/pass/src/QuantizeActivation.cpp b/compiler/luci/pass/src/QuantizeActivation.cpp index 95251a8..214e61c 100644 --- a/compiler/luci/pass/src/QuantizeActivation.cpp +++ b/compiler/luci/pass/src/QuantizeActivation.cpp @@ -44,12 +44,8 @@ void QuantizeActivation::visit(luci::CircleNode *node) LOGGER(l); INFO(l) << "QuantizeActivation visit node: " << node->name() << std::endl; - // Check if this is already quantized - if (is_quantized(node)) - return; - - // Check if this is bool type (bool type is not quantized) - if (node->dtype() == loco::DataType::BOOL) + // Check if node is fp32 + if (not is_fp32(node)) return; // Check if this is const (const activation is handled by QuantizeConstInputActivation) @@ -185,7 +181,7 @@ void QuantizeConstInputActivation::visit(luci::CircleNode *node) { \ auto input = node->INPUT_NAME(); \ auto const_node = dynamic_cast(input); \ - if (const_node && !is_quantized(const_node)) \ + if (const_node && is_fp32(const_node)) \ { \ auto new_const = luci::clone(const_node); \ quant_const(new_const, _output_type); \ @@ -199,7 +195,7 @@ void QuantizeConstInputActivation::visit(luci::CircleNode *node) { \ auto input1 = node->INPUT_NAME1(); \ auto const_node1 = dynamic_cast(input1); \ - if (const_node1 && !is_quantized(const_node1)) \ + if (const_node1 && is_fp32(const_node1)) \ { \ auto new_const1 = luci::clone(const_node1); \ quant_const(new_const1, _output_type); \ @@ -207,7 +203,7 @@ void QuantizeConstInputActivation::visit(luci::CircleNode *node) } \ auto input2 = node->INPUT_NAME2(); \ auto const_node2 = dynamic_cast(input2); \ - if (const_node2 && !is_quantized(const_node2)) \ + if (const_node2 && is_fp32(const_node2)) \ { \ auto new_const2 = luci::clone(const_node2); \ quant_const(new_const2, _output_type); \ @@ -216,6 +212,7 @@ void QuantizeConstInputActivation::visit(luci::CircleNode *node) } // Ops that receive a single activation as an input +QUANTIZE_SINGLE_CONST_INPUT(luci::CircleAbs, x) QUANTIZE_SINGLE_CONST_INPUT(luci::CircleArgMax, input) QUANTIZE_SINGLE_CONST_INPUT(luci::CircleArgMin, input) QUANTIZE_SINGLE_CONST_INPUT(luci::CircleBatchToSpaceND, input) @@ -278,7 +275,7 @@ void QuantizeConstInputActivation::visit(luci::CircleAddN *node) { auto input_node = node->inputs(i); auto const_node = dynamic_cast(input_node); - if (const_node && !is_quantized(const_node)) + if (const_node && is_fp32(const_node)) { auto new_const = luci::clone(const_node); quant_const(new_const, _output_type); diff --git a/compiler/luci/pass/src/QuantizeActivation.h b/compiler/luci/pass/src/QuantizeActivation.h index fc32d1c..c6c991a 100644 --- a/compiler/luci/pass/src/QuantizeActivation.h +++ b/compiler/luci/pass/src/QuantizeActivation.h @@ -102,6 +102,7 @@ private: void visit(luci::CircleNode *node); // Ops that receive a single activation as an input + void visit(luci::CircleAbs *node); void visit(luci::CircleArgMax *node); void visit(luci::CircleArgMin *node); void visit(luci::CircleBatchToSpaceND *node); diff --git a/compiler/luci/pass/src/QuantizeWeights.cpp b/compiler/luci/pass/src/QuantizeWeights.cpp index 500ae12..29cdaff 100644 --- a/compiler/luci/pass/src/QuantizeWeights.cpp +++ b/compiler/luci/pass/src/QuantizeWeights.cpp @@ -90,6 +90,118 @@ void asym_wquant_per_channel(CircleConst *node, std::vector &min, } } +// TODO Reduce duplicate code with QuantizeDequantizeWeights +void sym_wquant_per_channel(CircleConst *node, std::vector &min, std::vector &max, + std::vector &scaling_factor, std::vector &zp, + std::vector &nudged_min, std::vector &nudged_max, + int32_t &channel_dim_index) +{ + assert(node->dtype() == loco::DataType::FLOAT32); + const int32_t kMaxScale = std::numeric_limits::max(); + const int32_t kMinScale = -kMaxScale; + + uint32_t size = node->size(); + std::vector quantized_values(size); + + for (size_t i = 0; i < min.size(); ++i) + { + compute_sym_scale_zp(min[i], max[i], scaling_factor[i], zp[i], nudged_min[i], nudged_max[i]); + } + + auto quantize = [&](uint32_t *indices, loco::TensorShape &dimension, int channel_dim_index) { + int channel_idx = indices[channel_dim_index]; + const float scaling_factor_inv = 1.0 / scaling_factor[channel_idx]; + auto data = node->at(cal_offset(dimension, indices)); + data = data < nudged_min[channel_idx] ? nudged_min[channel_idx] : data; + data = data > nudged_max[channel_idx] ? nudged_max[channel_idx] : data; + quantized_values[cal_offset(dimension, indices)] = + static_cast(std::round(data * scaling_factor_inv)); + }; + + iterate_per_channel(node, channel_dim_index, quantize); + + node->dtype(loco::DataType::S16); // change the type of tensor + node->size(size); // resize tensor + for (uint32_t i = 0; i < size; ++i) + { + node->at(i) = + std::min(kMaxScale, std::max(kMinScale, quantized_values[i])); + } +} + +void cal_minmax_per_channel(CircleConst *node, std::vector &min, std::vector &max, + int32_t &channel_dim_index) +{ + loco::TensorShape dimension; + dimension.rank(4); + + if (!get_channel_dim_index(node, dimension, channel_dim_index)) + { + throw std::runtime_error("Failed to find channel index in " + node->name()); + } + auto size = dimension.dim(channel_dim_index).value(); + + std::vector has_min_max_value(size, false); + min.resize(size); + max.resize(size); + + auto cal_minmax = [&](uint32_t *indices, loco::TensorShape &dimension, int channel_dim_index) { + int channel_idx = indices[channel_dim_index]; + auto data = node->at(cal_offset(dimension, indices)); + if (has_min_max_value[channel_idx]) + { + min[channel_idx] = data < min[channel_idx] ? data : min[channel_idx]; + max[channel_idx] = data > max[channel_idx] ? data : max[channel_idx]; + } + else + { + min[channel_idx] = data; + max[channel_idx] = data; + has_min_max_value[channel_idx] = true; + } + }; + + iterate_per_channel(node, channel_dim_index, cal_minmax); +} + +void asymmetric_wquant_per_channel(CircleConst *node, std::vector &min, + std::vector &max, std::vector &scaling_factor, + std::vector &zp, std::vector &nudged_min, + std::vector &nudged_max, int32_t &channel_dim_index) +{ + assert(node->dtype() == loco::DataType::FLOAT32); + + const int32_t kMinScale = 0; + const int32_t kMaxScale = 255; + + uint32_t size = node->size(); + std::vector quantized_values(size); + + for (size_t i = 0; i < min.size(); ++i) + { + compute_asym_scale_zp(min[i], max[i], scaling_factor[i], zp[i], nudged_min[i], nudged_max[i]); + } + + auto quantize = [&](uint32_t *indices, loco::TensorShape &dimension, int channel_dim_index) { + int channel_idx = indices[channel_dim_index]; + const float scaling_factor_inv = 1.0 / scaling_factor[channel_idx]; + auto data = node->at(cal_offset(dimension, indices)); + data = data < nudged_min[channel_idx] ? nudged_min[channel_idx] : data; + data = data > nudged_max[channel_idx] ? nudged_max[channel_idx] : data; + quantized_values[cal_offset(dimension, indices)] = + static_cast(std::round((data - nudged_min[channel_idx]) * scaling_factor_inv)); + }; + + iterate_per_channel(node, channel_dim_index, quantize); + + node->dtype(loco::DataType::U8); // change the type of tensor + node->size(size); // resize tensor + for (uint32_t i = 0; i < size; ++i) + { + node->at(i) = std::min(kMaxScale, std::max(kMinScale, quantized_values[i])); + } +} + void sym_wquant_per_channel(CircleConst *node, std::vector &scaling_factor, int32_t &channel_dim_index) { @@ -250,7 +362,37 @@ void QuantizeWeights::quantize_weights(luci::CircleConst *weights) auto quantparam = weights->quantparam(); if (quantparam == nullptr) { - assert(false && "quantparam is nullptr"); + // Find min/max on the fly + // NOTE This is for the case when QuantizeDequantizeWeights is skipped + // TODO Reduce duplicate codes + std::vector min; + std::vector max; + int32_t channel_dim_index = 0; + + cal_minmax_per_channel(weights, min, max, channel_dim_index); + + std::vector nudged_min(min.size()); + std::vector nudged_max(min.size()); + std::vector scaling_factor(min.size()); + std::vector zp(min.size()); + + if (output_type == loco::DataType::U8) + { + asymmetric_wquant_per_channel(weights, min, max, scaling_factor, zp, nudged_min, nudged_max, + channel_dim_index); + } + else + { + sym_wquant_per_channel(weights, min, max, scaling_factor, zp, nudged_min, nudged_max, + channel_dim_index); + } + + auto quantparam = std::make_unique(); + quantparam->scale = scaling_factor; + quantparam->zerop = zp; + quantparam->quantized_dimension = channel_dim_index; + weights->quantparam(std::move(quantparam)); + return; } @@ -273,8 +415,35 @@ void QuantizeWeights::quantize_weights(luci::CircleConst *weights) // Find min/max per layer-wise else { - // Quantize using recorded quantparam auto quantparam = weights->quantparam(); + if (quantparam == nullptr) + { + // Find min/max on the fly + // NOTE This is for the case when QuantizeDequantizeWeights is skipped + // TODO Reduce duplicate codes + float min = std::numeric_limits::max(); + float max = std::numeric_limits::lowest(); + for (uint32_t i = 0; i < weights->size(); i++) + { + auto data = weights->at(i); + min = data < min ? data : min; + max = data > max ? data : max; + } + float scaling_factor{0}; + int64_t zp{0}; + float nudged_min{0}; + float nudged_max{0}; + + asymmetric_wquant_with_minmax_per_layer(weights, min, max, scaling_factor, zp, nudged_min, + nudged_max); + auto quantparam = std::make_unique(); + quantparam->scale.push_back(scaling_factor); + quantparam->zerop.push_back(zp); + weights->quantparam(std::move(quantparam)); + return; + } + + // Quantize using recorded quantparam assert(quantparam != nullptr); assert(quantparam->min.size() == 1); // only support layer-wise quant assert(quantparam->scale.size() == 1); // only support layer-wise quant diff --git a/compiler/luci/pass/src/QuantizeWithMinMaxPass.cpp b/compiler/luci/pass/src/QuantizeWithMinMaxPass.cpp index 0051445..c68e067 100644 --- a/compiler/luci/pass/src/QuantizeWithMinMaxPass.cpp +++ b/compiler/luci/pass/src/QuantizeWithMinMaxPass.cpp @@ -32,8 +32,6 @@ #include #include -#include - #include #include @@ -154,8 +152,8 @@ namespace * 2. After output feature map * * For example, if default_dtype = U8 and op_dtype = S16, - * 1. Quantize Op for U8->S16 is inserted before ifm - * 2. Quantize Op for S16->U8 is inserted after ofm + * 1. Quantize (U8->S16) is inserted before ifm + * 2. Quantize (S16->U8) is inserted after ofm * * Why not insert Quantize Op for const ifm? * We quantize const tensor at once to preserve precision. @@ -181,6 +179,10 @@ private: if (input->opcode() == luci::CircleOpcode::CIRCLECONST) return nullptr; + // input is not quantizable (ex: index) + if (input->quantparam() == nullptr) + return nullptr; + auto input_quant = create_quantize_op(input, _op_dtype); input_quant->input(input); auto origin_node = loco::must_cast(origin); @@ -192,6 +194,11 @@ private: { auto output = loco::must_cast(node); assert(output->opcode() != luci::CircleOpcode::CIRCLECONST); // FIX_CALLER_UNLESS + + // output is not quantizable (ex: index) + if (output->quantparam() == nullptr) + return; + auto output_quant = create_quantize_op(output, _default_dtype); luci::add_origin(output_quant, luci::get_origin(output)); @@ -253,6 +260,7 @@ private: void visit(luci::CircleUnpackOut *) {} // Ops that receive a single activation as an input + INSERT_QUANTIZE_TO_UNARY_OP(luci::CircleAbs, x) INSERT_QUANTIZE_TO_UNARY_OP(luci::CircleAveragePool2D, value) INSERT_QUANTIZE_TO_UNARY_OP(luci::CircleBatchToSpaceND, input) INSERT_QUANTIZE_TO_UNARY_OP(luci::CircleConv2D, input) @@ -365,10 +373,20 @@ private: void QuantizeWithMinMaxPass::set_input_type(loco::Graph *g) const { auto inputs = g->inputs(); - for (auto node : loco::input_nodes(g)) + + assert(inputs); // FIX_CALLER_UNLESS + assert(inputs->size() == _ctx->input_types.size()); // FIX_CALLER_UNLESS + + // NOTE loco::input_nodes returns input nodes following the order of InputIndex + auto input_nodes = loco::input_nodes(g); + for (uint32_t i = 0; i < input_nodes.size(); i++) { - auto input = loco::must_cast(node); - if (input->dtype() == _ctx->input_type) + auto input = loco::must_cast(input_nodes[i]); + assert(i == input->index()); // Fix input_type logic + + const auto user_given_dtype = _ctx->input_types[i]; + + if (input->dtype() == user_given_dtype) continue; // Bool type is not quantizable @@ -394,7 +412,7 @@ void QuantizeWithMinMaxPass::set_input_type(loco::Graph *g) const // Update qparam of input // This step is skipped if input_type is float32 - if (_ctx->input_type != loco::DataType::FLOAT32) + if (user_given_dtype != loco::DataType::FLOAT32) { auto quantparam = input->quantparam(); assert(quantparam); @@ -408,13 +426,13 @@ void QuantizeWithMinMaxPass::set_input_type(loco::Graph *g) const float nudged_min{0}; float nudged_max{0}; - if (_ctx->input_type == loco::DataType::U8) + if (user_given_dtype == loco::DataType::U8) { compute_asym_scale_zp(min, max, scaling_factor, zp, nudged_min, nudged_max); } else { - assert(_ctx->input_type == loco::DataType::S16); + assert(user_given_dtype == loco::DataType::S16); compute_sym_scale_zp(min, max, scaling_factor, zp, nudged_min, nudged_max); } input->quantparam()->scale[0] = scaling_factor; @@ -422,20 +440,29 @@ void QuantizeWithMinMaxPass::set_input_type(loco::Graph *g) const } // Update dtype of input - input->dtype(_ctx->input_type); + input->dtype(user_given_dtype); auto graph_input = inputs->at(input->index()); - graph_input->dtype(_ctx->input_type); + graph_input->dtype(user_given_dtype); } } void QuantizeWithMinMaxPass::set_output_type(loco::Graph *g) const { auto outputs = g->outputs(); - for (auto node : loco::output_nodes(g)) + assert(outputs); // FIX_CALLER_UNLESS + assert(outputs->size() == _ctx->output_types.size()); // Fix CircleQuantizer unless + + // NOTE loco::output_nodes returns output nodes following the order of OutputIndex + auto output_nodes = loco::output_nodes(g); + for (uint32_t i = 0; i < output_nodes.size(); i++) { - auto output = loco::must_cast(node); - if (output->dtype() == _ctx->output_type) + auto output = loco::must_cast(output_nodes[i]); + assert(i == output->index()); // Fix output_type logic + + const auto user_given_dtype = _ctx->output_types[i]; + + if (output->dtype() == user_given_dtype) continue; // Bool type is not quantizable @@ -444,12 +471,12 @@ void QuantizeWithMinMaxPass::set_output_type(loco::Graph *g) const auto from = loco::must_cast(output->from()); - // The last Op is not quantizable Op (ex: ArgMax) + // The last Op is not quantizable (ex: ArgMax) if (not from->quantparam()) continue; // Insert Dequantize Op for float32 output_type - if (_ctx->output_type == loco::DataType::FLOAT32) + if (user_given_dtype == loco::DataType::FLOAT32) { auto dequant_op = create_dequantize(from); loco::replace(from).with(dequant_op); @@ -458,7 +485,7 @@ void QuantizeWithMinMaxPass::set_output_type(loco::Graph *g) const else { // Insert Quantize Op for non-float32 output_type - auto quant_op = create_quantize_op(from, _ctx->output_type); + auto quant_op = create_quantize_op(from, user_given_dtype); loco::replace(from).with(quant_op); quant_op->input(from); @@ -467,10 +494,10 @@ void QuantizeWithMinMaxPass::set_output_type(loco::Graph *g) const } // Update dtype of output - output->dtype(_ctx->output_type); + output->dtype(user_given_dtype); auto graph_output = outputs->at(output->index()); - graph_output->dtype(_ctx->output_type); + graph_output->dtype(user_given_dtype); } } @@ -493,9 +520,9 @@ void QuantizeWithMinMaxPass::set_output_type(loco::Graph *g) const * Weights is quantized using min/max of its value * * Bias is quantized using input scale (s_i) and weights scale (s_w) - * - Activation and weights should be quantized earlier than bias + * - Therefore, activation and weights should be quantized earlier than bias * - * Quantization Steps + * Overall Quantization Steps * 1. Quantize Activation * - Quantize using recorded min/max (QuantizeActivation) * - Insert Quantize Ops for mixed-precision quantization (InsertQuantizeOp) @@ -550,7 +577,10 @@ bool QuantizeWithMinMaxPass::run(loco::Graph *g) }; // Quantize activation - for (auto node : loco::active_nodes(loco::output_nodes(g))) + // Why all_nodes? + // Models can have inactive (unused) inputs. + // We do not reject such models, but quantize them too + for (auto node : loco::all_nodes(g)) { auto circle_node = loco::must_cast(node); QuantizeActivation qa(_ctx->input_model_dtype, quantize_dtype(circle_node)); diff --git a/compiler/luci/pass/src/QuantizeWithMinMaxPass.test.cpp b/compiler/luci/pass/src/QuantizeWithMinMaxPass.test.cpp index d5fa21f..49c2d46 100644 --- a/compiler/luci/pass/src/QuantizeWithMinMaxPass.test.cpp +++ b/compiler/luci/pass/src/QuantizeWithMinMaxPass.test.cpp @@ -53,8 +53,14 @@ public: TEST(QuantizeWithMinMaxPassTest, name) { - luci::QuantizeWithMinMaxPass pass(loco::DataType::FLOAT32, loco::DataType::U8, - luci::QuantizationGranularity::LayerWise); + auto ctx = std::make_unique(); + { + ctx->input_model_dtype = loco::DataType::FLOAT32; + ctx->output_model_dtype = loco::DataType::U8; + ctx->granularity = luci::QuantizationGranularity::LayerWise; + } + + luci::QuantizeWithMinMaxPass pass(std::move(ctx)); auto const name = pass.name(); ASSERT_NE(nullptr, name); } @@ -65,8 +71,14 @@ TEST(QuantizeWithMinMaxPassTest, int_concat) { SimpleConcatGraph g(loco::DataType::S32); - luci::QuantizeWithMinMaxPass qwmm(loco::DataType::FLOAT32, loco::DataType::U8, - luci::QuantizationGranularity::LayerWise); + auto ctx = std::make_unique(); + { + ctx->input_model_dtype = loco::DataType::FLOAT32; + ctx->output_model_dtype = loco::DataType::U8; + ctx->granularity = luci::QuantizationGranularity::LayerWise; + } + + luci::QuantizeWithMinMaxPass qwmm(std::move(ctx)); qwmm.run(&g.g); @@ -74,3 +86,22 @@ TEST(QuantizeWithMinMaxPassTest, int_concat) EXPECT_EQ(nullptr, g.input_1->quantparam()); EXPECT_EQ(nullptr, g.input_2->quantparam()); } + +TEST(QuantizeWithMinMaxPassTest, inactive_input) +{ + SimpleConcatGraph g(loco::DataType::FLOAT32); + + // Unused input + g.g.nodes()->create(); + + auto ctx = std::make_unique(); + { + ctx->input_model_dtype = loco::DataType::FLOAT32; + ctx->output_model_dtype = loco::DataType::U8; + ctx->granularity = luci::QuantizationGranularity::LayerWise; + } + + luci::QuantizeWithMinMaxPass qwmm(std::move(ctx)); + + EXPECT_NO_THROW(qwmm.run(&g.g)); +} diff --git a/compiler/luci/pass/src/QuantizedModelVerifier.h b/compiler/luci/pass/src/QuantizedModelVerifier.h index 7409a51..d9bea43 100644 --- a/compiler/luci/pass/src/QuantizedModelVerifier.h +++ b/compiler/luci/pass/src/QuantizedModelVerifier.h @@ -38,26 +38,13 @@ public: { loco::DataType output_model_dtype = loco::DataType::Unknown; QuantizationGranularity granularity = QuantizationGranularity::ChannelWise; - loco::DataType input_type = loco::DataType::Unknown; - loco::DataType output_type = loco::DataType::Unknown; + std::vector input_types; + std::vector output_types; bool TF_style_maxpool = false; std::vector layers_info; }; public: - QuantizedModelVerifier(loco::DataType quantized_dtype, QuantizationGranularity granularity) - { - _ctx = std::make_unique(); - { - _ctx->output_model_dtype = quantized_dtype; - _ctx->granularity = granularity; - _ctx->input_type = quantized_dtype; - _ctx->output_type = quantized_dtype; - _ctx->TF_style_maxpool = false; - } - } - -public: QuantizedModelVerifier(std::unique_ptr &&ctx) : _ctx{std::move(ctx)} { // DO NOTHING diff --git a/compiler/luci/pass/src/QuantizedModelVerifier.test.cpp b/compiler/luci/pass/src/QuantizedModelVerifier.test.cpp index 21b4fe1..05ec317 100644 --- a/compiler/luci/pass/src/QuantizedModelVerifier.test.cpp +++ b/compiler/luci/pass/src/QuantizedModelVerifier.test.cpp @@ -18,7 +18,9 @@ #include "luci/Pass/QuantizeWithMinMaxPass.h" #include "luci/Pass/QuantizationParameters.h" +#include "luci/Pass/CircleTypeInferencePass.h" +#include #include #include @@ -104,12 +106,56 @@ void insert_scale_zp(luci::CircleNode *node, float scale, int64_t zp) qparam->zerop.push_back(zp); } +void run_phase(loco::Graph *g, Type quantized_dtype, Granularity granularity) +{ + logo::Phase phase; + + // Default passes. + phase.emplace_back(std::make_unique()); + + auto ctx = std::make_unique(); + { + ctx->input_model_dtype = loco::DataType::FLOAT32; + ctx->output_model_dtype = quantized_dtype; + ctx->granularity = granularity; + // Test graph has only one input/output + ctx->input_types = {quantized_dtype}; + ctx->output_types = {quantized_dtype}; + } + + phase.emplace_back(std::make_unique(std::move(ctx))); + + logo::PhaseRunner phase_runner{g}; + phase_runner.run(phase); +} + +void run_phase(loco::Graph *g, std::unique_ptr &&ctx) +{ + logo::Phase phase; + + // Default passes. + phase.emplace_back(std::make_unique()); + + phase.emplace_back(std::make_unique(std::move(ctx))); + + logo::PhaseRunner phase_runner{g}; + phase_runner.run(phase); +} + void quantize_and_verify(loco::Graph *g, Type quantized_dtype, Granularity granularity) { - luci::QuantizeWithMinMaxPass pass(Type::FLOAT32, quantized_dtype, granularity); - pass.run(g); + run_phase(g, quantized_dtype, granularity); - luci::QuantizedModelVerifier verifier(quantized_dtype, granularity); + auto ctx = std::make_unique(); + { + ctx->output_model_dtype = quantized_dtype; + ctx->granularity = granularity; + // Test graph has only one input/output + ctx->input_types = {quantized_dtype}; + ctx->output_types = {quantized_dtype}; + } + + luci::QuantizedModelVerifier verifier(std::move(ctx)); verifier.verify(g); } @@ -132,14 +178,14 @@ void quantize_and_verify_with_layer_info(loco::Graph *g, Type quantized_dtype, ctx->input_model_dtype = Type::FLOAT32; ctx->output_model_dtype = quantized_dtype; ctx->granularity = granularity; - ctx->input_type = quantized_dtype; - ctx->output_type = quantized_dtype; + // Test graph has only one input/output + ctx->input_types = {quantized_dtype}; + ctx->output_types = {quantized_dtype}; ctx->TF_style_maxpool = false; ctx->layers_info.push_back(info); } - luci::QuantizeWithMinMaxPass pass(std::move(ctx)); - pass.run(g); + run_phase(g, std::move(ctx)); } // Do verification @@ -148,8 +194,8 @@ void quantize_and_verify_with_layer_info(loco::Graph *g, Type quantized_dtype, { ctx->output_model_dtype = quantized_dtype; ctx->granularity = granularity; - ctx->input_type = quantized_dtype; - ctx->output_type = quantized_dtype; + ctx->input_types = {quantized_dtype}; + ctx->output_types = {quantized_dtype}; ctx->TF_style_maxpool = false; ctx->layers_info.push_back(info); } @@ -164,13 +210,21 @@ void quantize_and_verify_with_layer_info(loco::Graph *g, Type quantized_dtype, void quantize_and_verify_with_wrong_type(luci::test::TestIOGraph *g, Type quantized_dtype, Granularity granularity, Type wrong_dtype) { - luci::QuantizeWithMinMaxPass pass(Type::FLOAT32, quantized_dtype, granularity); - pass.run(g->g()); + run_phase(g->g(), quantized_dtype, granularity); auto node = loco::must_cast(g->output()->from()); node->dtype(wrong_dtype); - luci::QuantizedModelVerifier verifier(quantized_dtype, granularity); + auto ctx = std::make_unique(); + { + ctx->output_model_dtype = quantized_dtype; + ctx->granularity = granularity; + // Test graph has only one input/output + ctx->input_types = {quantized_dtype}; + ctx->output_types = {quantized_dtype}; + } + + luci::QuantizedModelVerifier verifier(std::move(ctx)); verifier.verify(g->g()); } @@ -179,13 +233,21 @@ void quantize_and_verify_with_wrong_type(luci::test::TestIOGraph *g, Type quanti void quantize_and_verify_with_wrong_granularity(luci::test::TestIOGraph *g, Type quantized_dtype, Granularity granularity) { - luci::QuantizeWithMinMaxPass pass(Type::FLOAT32, quantized_dtype, granularity); - pass.run(g->g()); + run_phase(g->g(), quantized_dtype, granularity); auto node = loco::must_cast(g->output()->from()); insert_scale_zp(node, 1.0, 1); - luci::QuantizedModelVerifier verifier(quantized_dtype, granularity); + auto ctx = std::make_unique(); + { + ctx->output_model_dtype = quantized_dtype; + ctx->granularity = granularity; + // Test graph has only one input/output + ctx->input_types = {quantized_dtype}; + ctx->output_types = {quantized_dtype}; + } + + luci::QuantizedModelVerifier verifier(std::move(ctx)); verifier.verify(g->g()); } @@ -238,6 +300,24 @@ public: virtual void init(void) = 0; }; +class TypedTestGraph : public luci::test::TestIOGraph +{ +protected: + void init(Type T, const luci::test::ShapeU32 shape_in, const luci::test::ShapeU32 shape_out) + { + TestIOGraph::init(shape_in, shape_out); + + input()->dtype(T); + output()->dtype(T); + + g()->inputs()->at(0)->dtype(T); + g()->outputs()->at(0)->dtype(T); + } + +public: + virtual void init(void) = 0; +}; + class InstanceNormTestGraph final : public SimpleTestGraph { public: @@ -603,6 +683,9 @@ public: output()->from(_argmax); set_minmax_to_non_const(g(), -1, 1); + + // Sync output dtype with graph's output dtype + g()->outputs()->at(0)->dtype(output()->dtype()); } public: @@ -904,6 +987,9 @@ public: output()->from(_op); set_minmax_to_non_const(g(), -1, 1); + + // Sync output dtype with graph's output dtype + g()->outputs()->at(0)->dtype(output()->dtype()); } loco::Node *x(void) const { return _op->x(); } @@ -934,6 +1020,9 @@ public: output()->from(_op); set_minmax_to_non_const(g(), -1, 1); + + // Sync output dtype with graph's output dtype + g()->outputs()->at(0)->dtype(output()->dtype()); } loco::Node *x(void) const { return _op->x(); } @@ -1218,6 +1307,33 @@ private: luci::CircleConst *_const = nullptr; }; +template class IntMulTestGraph final : public TypedTestGraph +{ +public: + void init(void) override + { + TypedTestGraph::init(T, {32}, {32}); + + _const = create_dummy_const(g(), {32}); + _mul = g()->nodes()->create(); + { + _mul->x(input()); + _mul->y(_const); + _mul->fusedActivationFunction(luci::FusedActFunc::NONE); + _mul->name("test"); + _mul->dtype(T); + } + output()->from(_mul); + } + + loco::Node *x() { return _mul->x(); } + loco::Node *y() { return _mul->y(); } + +private: + luci::CircleMul *_mul = nullptr; + luci::CircleConst *_const = nullptr; +}; + class AddTestGraph final : public SimpleTestGraph { public: @@ -1246,6 +1362,33 @@ private: luci::CircleConst *_const = nullptr; }; +template class IntAddTestGraph final : public TypedTestGraph +{ +public: + void init(void) override + { + TypedTestGraph::init(T, {32}, {32}); + + _const = create_dummy_const(g(), {32}); + _add = g()->nodes()->create(); + { + _add->x(input()); + _add->y(_const); + _add->fusedActivationFunction(luci::FusedActFunc::NONE); + _add->name("test"); + _add->dtype(T); + } + output()->from(_add); + } + + loco::Node *x() { return _add->x(); } + loco::Node *y() { return _add->y(); } + +private: + luci::CircleAdd *_add = nullptr; + luci::CircleConst *_const = nullptr; +}; + } // namespace // Quantize and verify with given configurations @@ -1286,34 +1429,46 @@ private: // Quantize and verify with wrong type // Users can specify the test target -#define TEST_WITH_WRONG_TYPE_TARGET(graph, type, granularity, wrong_dtype, target) \ - do \ - { \ - graph g; \ - g.init(); \ - auto node = loco::must_cast(target); \ - luci::QuantizeWithMinMaxPass pass(Type::FLOAT32, type, granularity); \ - pass.run(g.g()); \ - auto after_node = loco::must_cast(target); \ - after_node->dtype(wrong_dtype); \ - luci::QuantizedModelVerifier verifier(type, granularity); \ - EXPECT_ANY_THROW(verifier.verify(g.g())); \ +#define TEST_WITH_WRONG_TYPE_TARGET(graph, type, granularity_, wrong_dtype, target) \ + do \ + { \ + graph g; \ + g.init(); \ + auto node = loco::must_cast(target); \ + run_phase(g.g(), type, granularity_); \ + auto after_node = loco::must_cast(target); \ + after_node->dtype(wrong_dtype); \ + auto ctx = std::make_unique(); \ + { \ + ctx->output_model_dtype = type; \ + ctx->granularity = granularity_; \ + ctx->input_types = {type}; \ + ctx->output_types = {type}; \ + } \ + luci::QuantizedModelVerifier verifier(std::move(ctx)); \ + EXPECT_ANY_THROW(verifier.verify(g.g())); \ } while (0) // Quantize and verify with wrong granularity // Users can specify the test target -#define TEST_WITH_WRONG_GRANULARITY_TARGET(graph, type, granularity, target) \ - do \ - { \ - graph g; \ - g.init(); \ - auto node = loco::must_cast(target); \ - luci::QuantizeWithMinMaxPass pass(Type::FLOAT32, type, granularity); \ - pass.run(g.g()); \ - auto after_node = loco::must_cast(target); \ - insert_scale_zp(after_node, 1.0, 1); \ - luci::QuantizedModelVerifier verifier(type, granularity); \ - EXPECT_ANY_THROW(verifier.verify(g.g())); \ +#define TEST_WITH_WRONG_GRANULARITY_TARGET(graph, type, granularity_, target) \ + do \ + { \ + graph g; \ + g.init(); \ + auto node = loco::must_cast(target); \ + run_phase(g.g(), type, granularity_); \ + auto after_node = loco::must_cast(target); \ + insert_scale_zp(after_node, 1.0, 1); \ + auto ctx = std::make_unique(); \ + { \ + ctx->output_model_dtype = type; \ + ctx->granularity = granularity_; \ + ctx->input_types = {type}; \ + ctx->output_types = {type}; \ + } \ + luci::QuantizedModelVerifier verifier(std::move(ctx)); \ + EXPECT_ANY_THROW(verifier.verify(g.g())); \ } while (0) // Test a local helper function @@ -2512,6 +2667,29 @@ TEST(QuantizedModelVerifierTest, Add_wrong_granularity_NEG) SUCCEED(); } +TEST(QuantizedModelVerifierTest, Add_inttype) +{ + // Tests for S32 + TEST_WITH_GRAPH(IntAddTestGraph, Type::U8, Granularity::LayerWise); + TEST_WITH_GRAPH(IntAddTestGraph, Type::U8, Granularity::ChannelWise); + TEST_WITH_GRAPH(IntAddTestGraph, Type::S16, Granularity::ChannelWise); + + TEST_WITH_LAYER_INFO(IntAddTestGraph, Type::U8, Granularity::LayerWise); + TEST_WITH_LAYER_INFO(IntAddTestGraph, Type::U8, Granularity::ChannelWise); + TEST_WITH_LAYER_INFO(IntAddTestGraph, Type::S16, Granularity::ChannelWise); + + // Tests for S64 + TEST_WITH_GRAPH(IntAddTestGraph, Type::U8, Granularity::LayerWise); + TEST_WITH_GRAPH(IntAddTestGraph, Type::U8, Granularity::ChannelWise); + TEST_WITH_GRAPH(IntAddTestGraph, Type::S16, Granularity::ChannelWise); + + TEST_WITH_LAYER_INFO(IntAddTestGraph, Type::U8, Granularity::LayerWise); + TEST_WITH_LAYER_INFO(IntAddTestGraph, Type::U8, Granularity::ChannelWise); + TEST_WITH_LAYER_INFO(IntAddTestGraph, Type::S16, Granularity::ChannelWise); + + SUCCEED(); +} + TEST(QuantizedModelVerifierTest, Mul) { TEST_WITH_GRAPH(MulTestGraph, Type::U8, Granularity::LayerWise); @@ -2544,6 +2722,29 @@ TEST(QuantizedModelVerifierTest, Mul_wrong_granularity_NEG) SUCCEED(); } +TEST(QuantizedModelVerifierTest, Mul_inttype) +{ + // Tests for S32 + TEST_WITH_GRAPH(IntMulTestGraph, Type::U8, Granularity::LayerWise); + TEST_WITH_GRAPH(IntMulTestGraph, Type::U8, Granularity::ChannelWise); + TEST_WITH_GRAPH(IntMulTestGraph, Type::S16, Granularity::ChannelWise); + + TEST_WITH_LAYER_INFO(IntMulTestGraph, Type::U8, Granularity::LayerWise); + TEST_WITH_LAYER_INFO(IntMulTestGraph, Type::U8, Granularity::ChannelWise); + TEST_WITH_LAYER_INFO(IntMulTestGraph, Type::S16, Granularity::ChannelWise); + + // Tests for S64 + TEST_WITH_GRAPH(IntMulTestGraph, Type::U8, Granularity::LayerWise); + TEST_WITH_GRAPH(IntMulTestGraph, Type::U8, Granularity::ChannelWise); + TEST_WITH_GRAPH(IntMulTestGraph, Type::S16, Granularity::ChannelWise); + + TEST_WITH_LAYER_INFO(IntMulTestGraph, Type::U8, Granularity::LayerWise); + TEST_WITH_LAYER_INFO(IntMulTestGraph, Type::U8, Granularity::ChannelWise); + TEST_WITH_LAYER_INFO(IntMulTestGraph, Type::S16, Granularity::ChannelWise); + + SUCCEED(); +} + // TODO Add following testcases // // CircleConv2D diff --git a/compiler/luci/pass/src/RemoveDuplicateConstPass.cpp b/compiler/luci/pass/src/RemoveDuplicateConstPass.cpp new file mode 100644 index 0000000..e50dda9 --- /dev/null +++ b/compiler/luci/pass/src/RemoveDuplicateConstPass.cpp @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/RemoveDuplicateConstPass.h" + +#include + +namespace +{ + +bool compare_quant_params(luci::CircleConst *left, luci::CircleConst *right) +{ + const auto left_quant_param = left->quantparam(); + const auto right_quant_param = right->quantparam(); + + if (left_quant_param == right_quant_param) + return true; + + if (left_quant_param != nullptr and right_quant_param != nullptr) + { + if (left_quant_param->scale == right_quant_param->scale and + left_quant_param->quantized_dimension == right_quant_param->quantized_dimension and + left_quant_param->zerop == right_quant_param->zerop and + left_quant_param->min == right_quant_param->min and + left_quant_param->max == right_quant_param->max) + { + return true; + } + } + return false; +} + +bool compare_dim_values(luci::CircleConst *left, luci::CircleConst *right) +{ + const auto left_rank = left->rank(); + const auto right_rank = right->rank(); + + if (left_rank != right_rank) + return false; + + for (uint32_t i = 0; i < left_rank; ++i) + { + if (left->dim(i).value() != right->dim(i).value()) + return false; + } + + return true; +} + +template bool is_equal_consts(luci::CircleConst *left, luci::CircleConst *right) +{ + if (not compare_quant_params(left, right)) + return false; + + if (not compare_dim_values(left, right)) + return false; + + for (uint32_t i = 0; i < left->size
(); ++i) + { + if (left->at
(i) != right->at
(i)) + return false; + } + + return true; +} + +} // namespace + +namespace luci +{ + +bool RemoveDuplicateConstPass::remove_duplicate_const() +{ + bool changed = false; + + for (auto &cur_pair : _sum_to_const) + { + // if single const - continue + if (cur_pair.second.size() == 1) + continue; + + for (auto reference_const : cur_pair.second) + { + if (reference_const == nullptr) + continue; + + for (uint32_t i = 0; i < cur_pair.second.size(); ++i) + { + auto cur_const = cur_pair.second.at(i); + if (cur_const == nullptr or cur_const == reference_const) + continue; + + if (cur_const->dtype() != reference_const->dtype()) + continue; + + bool is_equal = false; + + switch (cur_const->dtype()) + { + case loco::DataType::FLOAT32: + is_equal = is_equal_consts(reference_const, cur_const); + break; + case loco::DataType::S32: + is_equal = is_equal_consts(reference_const, cur_const); + break; + case loco::DataType::S16: + is_equal = is_equal_consts(reference_const, cur_const); + break; + case loco::DataType::S8: + is_equal = is_equal_consts(reference_const, cur_const); + break; + case loco::DataType::U8: + is_equal = is_equal_consts(reference_const, cur_const); + break; + default: + continue; + } + + if (not is_equal) + continue; + + loco::replace(cur_const).with(reference_const); + + // Remove from next checking + cur_pair.second[i] = nullptr; + + changed = true; + } + } + } + + return changed; +} + +template +void RemoveDuplicateConstPass::add_to_map(luci::CircleConst *const_node) +{ + const auto const_size = const_node->size
(); + float sum = 0.0; + + for (uint32_t i = 0; i < const_size; ++i) + { + sum += const_node->at
(i); + } + + if (_sum_to_const.find(sum) == _sum_to_const.end()) + { + _sum_to_const[sum] = {const_node}; + } + else + { + _sum_to_const.at(sum).push_back(const_node); + } +} + +/** + * Remove duplicate Const nodes. + * + * BEFORE + * [CircleNode] [CircleConst] + * | / + * | / + * [CircleNode] [CircleConst] + * | / + * | / + * [CircleNode] + * + * AFTER + * + * [CircleNode] [CircleConst] + * | / / + * | / / + * [CircleNode] / + * | / + * | / + * [CircleNode] + * + */ +bool RemoveDuplicateConstPass::run(loco::Graph *g) +{ + for (auto node : loco::active_nodes(loco::output_nodes(g))) + { + auto const_node = dynamic_cast(node); + if (const_node == nullptr) + continue; + + switch (const_node->dtype()) + { + case loco::DataType::FLOAT32: + add_to_map(const_node); + break; + case loco::DataType::S32: + add_to_map(const_node); + break; + case loco::DataType::S16: + add_to_map(const_node); + break; + case loco::DataType::S8: + add_to_map(const_node); + break; + case loco::DataType::U8: + add_to_map(const_node); + break; + default: + continue; + } + } + + return remove_duplicate_const(); +} + +} // namespace luci diff --git a/compiler/luci/pass/src/RemoveDuplicateConstPass.test.cpp b/compiler/luci/pass/src/RemoveDuplicateConstPass.test.cpp new file mode 100644 index 0000000..5052a3e --- /dev/null +++ b/compiler/luci/pass/src/RemoveDuplicateConstPass.test.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/RemoveDuplicateConstPass.h" + +#include +#include +#include + +namespace +{ +using namespace luci::test; + +class DuplicateConstsGraphlet +{ +public: + DuplicateConstsGraphlet() = default; + +public: + void init(loco::Graph *g, bool is_duplicate) + { + _reshape_shape = g->nodes()->create(); + _reshape_shape->rank(1); + _reshape_shape->dim(0).set(1); + _reshape_shape->shape_status(luci::ShapeStatus::VALID); + _reshape_shape->dtype(loco::DataType::S32); + + _reshape_shape->size(1); + _reshape_shape->at(0) = 5; + _reshape_shape->name("reshape_shape_1"); + + _reshape_shape_duplicate = g->nodes()->create(); + _reshape_shape_duplicate->rank(1); + _reshape_shape_duplicate->dim(0).set(1); + _reshape_shape_duplicate->shape_status(luci::ShapeStatus::VALID); + _reshape_shape_duplicate->dtype(loco::DataType::S32); + if (is_duplicate) + { + _reshape_shape_duplicate->size(1); + _reshape_shape_duplicate->at(0) = 5; + } + else + { + _reshape_shape_duplicate->size(2); + _reshape_shape_duplicate->at(0) = 1; + _reshape_shape_duplicate->at(1) = 5; + } + _reshape_shape_duplicate->name("reshape_shape_2"); + + _reshape_f = g->nodes()->create(); + _reshape_f->newShape()->rank(1); + _reshape_f->newShape()->dim(0) = 5; + _reshape_f->name("reshape_f"); + + _reshape_s = g->nodes()->create(); + if (is_duplicate) + { + _reshape_s->newShape()->rank(1); + _reshape_s->newShape()->dim(0) = 5; + } + else + { + _reshape_s->newShape()->rank(2); + _reshape_s->newShape()->dim(0) = 1; + _reshape_s->newShape()->dim(1) = 5; + } + _reshape_s->name("reshape_s"); + } + +protected: + luci::CircleReshape *_reshape_f = nullptr; + luci::CircleReshape *_reshape_s = nullptr; + luci::CircleConst *_reshape_shape = nullptr; + luci::CircleConst *_reshape_shape_duplicate = nullptr; +}; + +class DuplicateConstsGraph : public TestIOGraph, public DuplicateConstsGraphlet +{ +public: + DuplicateConstsGraph() = default; + +public: + void init(const ShapeU32 in_shape, const ShapeU32 out_shape, bool is_duplicate) + { + TestIOGraph::init(in_shape, out_shape); + + DuplicateConstsGraphlet::init(g(), is_duplicate); + + // connect graph + _reshape_f->tensor(input()); + _reshape_f->shape(_reshape_shape); + + _reshape_s->tensor(_reshape_f); + _reshape_s->shape(_reshape_shape_duplicate); + + output()->from(_reshape_s); + } +}; +} // namespace + +TEST(RemoveDuplicateConstPass, name) +{ + luci::RemoveDuplicateConstPass pass; + auto const name = pass.name(); + ASSERT_NE(nullptr, name); +} + +TEST(RemoveDuplicateConstPass, remove_duplicate) +{ + DuplicateConstsGraph g; + g.init({1, 5}, {5}, true); + + luci::RemoveDuplicateConstPass pass; + while (pass.run(g.g())) + ; + + uint32_t const_num = 0; + for (auto node : loco::active_nodes(loco::output_nodes(g.g()))) + { + auto target_node = dynamic_cast(node); + if (target_node != nullptr) + const_num++; + } + + ASSERT_EQ(const_num, 1); +} + +TEST(RemoveDuplicateConstPass, remove_duplicate_NEG) +{ + DuplicateConstsGraph g; + g.init({1, 5}, {1, 5}, false); + + luci::RemoveDuplicateConstPass pass; + while (pass.run(g.g())) + ; + + uint32_t const_num = 0; + for (auto node : loco::active_nodes(loco::output_nodes(g.g()))) + { + auto target_node = dynamic_cast(node); + if (target_node != nullptr) + const_num++; + } + + ASSERT_EQ(const_num, 2); +} diff --git a/compiler/luci/pass/src/ReplaceNonConstFCWithBatchMatMulPass.cpp b/compiler/luci/pass/src/ReplaceNonConstFCWithBatchMatMulPass.cpp index 741b709..07457c1 100644 --- a/compiler/luci/pass/src/ReplaceNonConstFCWithBatchMatMulPass.cpp +++ b/compiler/luci/pass/src/ReplaceNonConstFCWithBatchMatMulPass.cpp @@ -64,6 +64,40 @@ luci::CircleNode *fromActivation(luci::CircleNode *inp, luci::FusedActFunc act) } } +// Create CircleReshape where +// - dtype is same with node +// - shape is same with node +// NOTE: User should set input(tensor) of the returned Op. +luci::CircleReshape *create_reshape(luci::CircleFullyConnected *node) +{ + assert(node); // FIX_CALLER_UNLESS + + auto g = node->graph(); + + auto reshape = g->nodes()->create(); + reshape->name(node->name() + "/reshape"); + reshape->dtype(node->dtype()); + luci::add_origin(reshape, luci::get_origin(node)); + + auto shape_const = g->nodes()->create(); + shape_const->dtype(loco::DataType::S32); + shape_const->rank(1); + shape_const->dim(0).set(node->rank()); + shape_const->size(node->rank()); + for (uint32_t i = 0; i < node->rank(); i++) + { + assert(node->dim(i).known()); // FIX_CALLER_UNLESS + shape_const->at(i) = node->dim(i).value(); + } + shape_const->shape_status(luci::ShapeStatus::VALID); + shape_const->name(node->name() + "/shape"); + luci::add_origin(shape_const, luci::get_origin(node)); + + reshape->shape(shape_const); + + return reshape; +} + /** * Replace Fully Connected with Batched MatMul * @@ -79,19 +113,23 @@ luci::CircleNode *fromActivation(luci::CircleNode *inp, luci::FusedActFunc act) * * [Node1] [Node2] * \ / - * [BatchMatMul] [BiasValue]? + * [BatchMatMul] + * | + * [Reshape] [BiasValue]? * \ / * [Add]? * | * [Activation]? * * Nodes with "?" denote optional elements + * NOTE Reshape Op is inserted to keep the original shape of FullyConnected Op + * Reshape Op can be redundant (input shape == output shape). This can be removed + * by RemoveUnnecessaryReshapePass. */ bool replace_fc_with_matmul(luci::CircleFullyConnected *fc) { luci::CircleNode *x = nullptr; luci::CircleNode *y = nullptr; - luci::CircleNode *b = nullptr; luci::CircleTranspose *ty = nullptr; luci::CircleTranspose *tx = nullptr; bool adj_x = false; @@ -122,10 +160,13 @@ bool replace_fc_with_matmul(luci::CircleFullyConnected *fc) x = loco::must_cast(fc->input()); } - b = loco::must_cast(fc->bias()); + if (x->dtype() != loco::DataType::FLOAT32 || y->dtype() != loco::DataType::FLOAT32) + return false; - if (x->dtype() != loco::DataType::FLOAT32 || y->dtype() != loco::DataType::FLOAT32 || - b->dtype() != loco::DataType::FLOAT32) + auto bc = dynamic_cast(fc->bias()); + // NOTE bias can be empty as CircleOutputExclude type + // NOTE we can only handle bias as FLOAT32 type as of now + if (nullptr != bc && bc->dtype() != loco::DataType::FLOAT32) return false; auto name = fc->name(); @@ -141,6 +182,9 @@ bool replace_fc_with_matmul(luci::CircleFullyConnected *fc) luci::add_origin(matmul, luci::get_origin(fc)); + auto reshape = create_reshape(fc); + reshape->tensor(matmul); + auto all_zero = [](const luci::CircleConst *c) { bool ac = true; for (uint32_t i = 0; i < c->size() && ac; i++) @@ -150,12 +194,11 @@ bool replace_fc_with_matmul(luci::CircleFullyConnected *fc) return ac; }; - auto bc = dynamic_cast(b); - if ((nullptr != bc) && !all_zero(bc)) + if (nullptr != bc && !all_zero(bc)) { auto bias_add = fc->graph()->nodes()->create(); - bias_add->x(matmul); - bias_add->y(b); + bias_add->x(reshape); + bias_add->y(bc); bias_add->name(fc->name() + "/bias_add"); bias_add->dtype(fc->dtype()); add_origin(bias_add, get_origin(fc)); @@ -164,7 +207,8 @@ bool replace_fc_with_matmul(luci::CircleFullyConnected *fc) } else { - auto n = fromActivation(matmul, fc->fusedActivationFunction()); + // NOTE bias doesn't exist or bias is all zero + auto n = fromActivation(reshape, fc->fusedActivationFunction()); add_origin(n, luci::get_origin(fc)); n->name(fc->name() + "fusedActivation"); n->dtype(fc->dtype()); diff --git a/compiler/luci/pass/src/ReplaceNonConstFCWithBatchMatMulPass.test.cpp b/compiler/luci/pass/src/ReplaceNonConstFCWithBatchMatMulPass.test.cpp index 7606a61..93024f3 100644 --- a/compiler/luci/pass/src/ReplaceNonConstFCWithBatchMatMulPass.test.cpp +++ b/compiler/luci/pass/src/ReplaceNonConstFCWithBatchMatMulPass.test.cpp @@ -159,8 +159,8 @@ TEST_F(ReplaceNonConstFCWithBatchMatMulPassTest, simple_test) auto ret = pass.run(g.g()); EXPECT_EQ(true, ret); - auto mm = dynamic_cast(g.output()->from()); - EXPECT_NE(nullptr, mm); + auto res = dynamic_cast(g.output()->from()); + EXPECT_NE(nullptr, res); } TEST_F(ReplaceNonConstFCWithBatchMatMulPassTest, nonzero_bias_test) diff --git a/compiler/luci/pass/src/ResolveCustomOpMatMulPass.cpp b/compiler/luci/pass/src/ResolveCustomOpMatMulPass.cpp index 1e8f681..f618827 100644 --- a/compiler/luci/pass/src/ResolveCustomOpMatMulPass.cpp +++ b/compiler/luci/pass/src/ResolveCustomOpMatMulPass.cpp @@ -153,7 +153,6 @@ bool resolve_matmul(luci::CircleCustom *cop) } auto empty_bias = graph->nodes()->create(); - empty_bias->dtype(loco::DataType::FLOAT32); // Needed for type inference auto fc_node = graph->nodes()->create(); fc_node->input(lhs); diff --git a/compiler/luci/pass/src/ResolveCustomOpMaxPoolWithArgmaxPass.cpp b/compiler/luci/pass/src/ResolveCustomOpMaxPoolWithArgmaxPass.cpp index f37f277..7c038d5 100644 --- a/compiler/luci/pass/src/ResolveCustomOpMaxPoolWithArgmaxPass.cpp +++ b/compiler/luci/pass/src/ResolveCustomOpMaxPoolWithArgmaxPass.cpp @@ -23,6 +23,7 @@ #include #include +#include // std::numeric_limits #include diff --git a/compiler/luci/pass/src/ResolveCustomOpSplitVPass.cpp b/compiler/luci/pass/src/ResolveCustomOpSplitVPass.cpp index a650658..5a09e39 100644 --- a/compiler/luci/pass/src/ResolveCustomOpSplitVPass.cpp +++ b/compiler/luci/pass/src/ResolveCustomOpSplitVPass.cpp @@ -20,6 +20,8 @@ #include #include +#include // std::numeric_limits + namespace { diff --git a/compiler/luci/pass/src/UnrollUnidirectionalSequenceLSTMPass.cpp b/compiler/luci/pass/src/UnrollUnidirectionalSequenceLSTMPass.cpp new file mode 100644 index 0000000..b73efaf --- /dev/null +++ b/compiler/luci/pass/src/UnrollUnidirectionalSequenceLSTMPass.cpp @@ -0,0 +1,672 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/UnrollUnidirectionalSequenceLSTMPass.h" + +#include "helpers/NodeFiller.h" +#include "helpers/TypeMapper.h" + +#include +#include + +#include +#include + +/** + * BEFORE + * [CircleNode] + * | + * [UnidirectionalSequenceLSTM] + * | + * [CircleNode] + * + * AFTER + * + * [CircleNode] + * | + * [CircleTranspose] + * | + * [CircleUnpack] + * | + * [CircleUnpackOut] + * | + * (Unrolled sub network) + * | + * [CirclePack] + * | | + * [CircleTranspose] [UnidirectionalSequenceLSTM] + * | | + * [CircleNode] + * + * NOTE for timesteps = 1, + * first [CircleTranspose] is not added and + * last [CirclePack] + [CircleTranspose] is replaced with [CircleReshape] + * + * First unrolled sub network is as follows + * - [] and 'Circle' are omitted + * - all FC has one or two Const for Weight/Bias + * + * (input) + * | + * FC + * | + * Split + * +---------+----------+----------+ + * | | | | + * | Logistic Logistic Tanh + * | Const | | | + * | | | | | + * | +-- Mul +-- Mul ---+ + * | | | + * | +---- Add ------+ + * | | + * | +----+----+ + * | | | + * Logistic Tanh | + * | | | + * +-- Mul ----+ | + * | | + * (output) (A) + * + * and following unrolled sub networks are; + * + * (prev-output) (input) + * | | + * FC FC + * | | + * +--- Add --+ + * Const | + * | | + * +------ Add + * | + * Split + * | + * +---------+----------+----------+ + * SplitOut SplitOut SplitOut SplitOut + * | | | | + * | Logistic Logistic Tanh + * | (A') | | | + * | | | | | + * | +--- Mul +-- Mul ---+ + * | | | + * | +---- Add ------+ + * | | + * | +----+----+ + * | | | + * Logistic Tanh | + * | | | + * +-- Mul ----+ | + * | | + * (output) (next) + * + * where (A) and (A') are connected + * + */ + +namespace +{ + +struct UnrollLSTM +{ + luci::CircleConst *transpose_perm(void); + luci::CircleTranspose *first_transpose(luci::CircleNode *input); + std::vector input_unpacks(luci::CircleNode *input); + luci::CircleConst *merged_weights(luci::CircleConst *iw, luci::CircleConst *fw, + luci::CircleConst *cw, luci::CircleConst *ow); + luci::CircleFullyConnected *create_input_matmul(luci::CircleNode *input); + luci::CircleAdd *create_input_matmul(luci::CircleNode *input, luci::CircleMul *mul, + uint32_t step); + std::vector matmul_splits(luci::CircleNode *input, uint32_t step); + luci::CircleConst *forget_zero(void); + luci::CircleMul *forget_gate_cell(std::vector &splits, + luci::CircleNode *prev, uint32_t step, + luci::CircleNode **retadd); + luci::CircleReshape *last_reshape(luci::CircleNode *input); + luci::CircleTranspose *last_transpose(std::vector &output_muls); + + luci::CircleUnidirectionalSequenceLSTM *_lstm{nullptr}; + loco::Graph::NodeContext *_nctx{nullptr}; + std::string _name; + uint32_t _batch{0}; + uint32_t _timesteps{0}; + uint32_t _units{0}; // output space dim +}; + +luci::CircleConst *UnrollLSTM::transpose_perm(void) +{ + auto perm = _nctx->create(); + perm->dtype(loco::DataType::S32); + perm->rank(1); + perm->dim(0) = 3; + perm->size(3); + perm->at(0) = 1; + perm->at(1) = 0; + perm->at(2) = 2; + perm->shape_status(luci::ShapeStatus::VALID); + + return perm; +} + +luci::CircleTranspose *UnrollLSTM::first_transpose(luci::CircleNode *input) +{ + assert(input != nullptr); + + auto perm = transpose_perm(); + perm->name(_name + "_perm1"); + luci::add_origin(perm, luci::get_origin(_lstm)); + + auto transpose = _nctx->create(); + transpose->a(input); + transpose->perm(perm); + transpose->name(_name + "_trans1"); + luci::add_origin(transpose, luci::get_origin(_lstm)); + + return transpose; +} + +std::vector UnrollLSTM::input_unpacks(luci::CircleNode *input) +{ + assert(input != nullptr); + + // NOTE unpack input can be LSTM or Transpose + auto unpack = _nctx->create(); + unpack->num(_timesteps); + unpack->axis(0); + unpack->value(input); + unpack->name(_name + "_unpack"); + luci::add_origin(unpack, luci::get_origin(_lstm)); + + std::vector outs; + for (uint32_t idx = 0; idx < _timesteps; ++idx) + { + auto unpackout = _nctx->create(); + unpackout->input(unpack); + unpackout->index(idx); + unpackout->name(_name + "_unpackout_" + std::to_string(idx)); + luci::add_origin(unpackout, luci::get_origin(_lstm)); + outs.push_back(unpackout); + } + + return outs; +} + +luci::CircleConst *UnrollLSTM::merged_weights(luci::CircleConst *iw, luci::CircleConst *fw, + luci::CircleConst *cw, luci::CircleConst *ow) +{ + assert(iw != nullptr); + assert(fw != nullptr); + assert(cw != nullptr); + assert(ow != nullptr); + + auto iw_rank = iw->rank(); + assert(iw_rank == fw->rank()); + assert(iw_rank == cw->rank()); + assert(iw_rank == ow->rank()); + + uint32_t ne_w = 1; + for (uint32_t i = 0; i < iw_rank; i++) + ne_w *= iw->dim(i).value(); + + assert(iw->dtype() == loco::DataType::FLOAT32); + assert(fw->dtype() == loco::DataType::FLOAT32); + assert(cw->dtype() == loco::DataType::FLOAT32); + assert(ow->dtype() == loco::DataType::FLOAT32); + + // merged weights + auto mw = _nctx->create(); + mw->dtype(iw->dtype()); + mw->rank(iw_rank); + mw->dim(0) = 4u * iw->dim(0).value(); + for (uint32_t i = 1; i < iw_rank; i++) + mw->dim(i) = iw->dim(i); + mw->size(4 * ne_w); + mw->shape_status(luci::ShapeStatus::VALID); + for (uint32_t i = 0; i < ne_w; ++i) + { + mw->at(i + ne_w * 0) = iw->at(i); + mw->at(i + ne_w * 1) = fw->at(i); + mw->at(i + ne_w * 2) = cw->at(i); + mw->at(i + ne_w * 3) = ow->at(i); + } + return mw; +} + +luci::CircleFullyConnected *UnrollLSTM::create_input_matmul(luci::CircleNode *input) +{ + assert(input != nullptr); + + // weights + auto iw = loco::must_cast(_lstm->input_to_input_weights()); + auto fw = loco::must_cast(_lstm->input_to_forget_weights()); + auto cw = loco::must_cast(_lstm->input_to_cell_weights()); + auto ow = loco::must_cast(_lstm->input_to_output_weights()); + + auto fcw = merged_weights(iw, fw, cw, ow); + fcw->name(_name + "_fc_w"); + luci::add_origin(fcw, luci::get_origin(_lstm)); + + // bias + auto ib = loco::must_cast(_lstm->input_gate_bias()); + auto fb = loco::must_cast(_lstm->forget_gate_bias()); + auto cb = loco::must_cast(_lstm->cell_gate_bias()); + auto ob = loco::must_cast(_lstm->output_gate_bias()); + + auto fcb = merged_weights(ib, fb, cb, ob); + fcb->name(_name + "_fc_b"); + luci::add_origin(fcb, luci::get_origin(_lstm)); + + auto fc = _nctx->create(); + fc->input(input); + fc->weights(fcw); + fc->bias(fcb); + fc->fusedActivationFunction(luci::FusedActFunc::NONE); + fc->name(_name + "_fc"); + luci::add_origin(fc, luci::get_origin(_lstm)); + + return fc; +} + +luci::CircleAdd *UnrollLSTM::create_input_matmul(luci::CircleNode *input, luci::CircleMul *mul, + uint32_t step) +{ + assert(input != nullptr); + assert(mul != nullptr); + assert(step < _timesteps); + + auto base_name = _name + "_matmul" + std::to_string(step); + + // input weights + auto iw = loco::must_cast(_lstm->input_to_input_weights()); + auto fw = loco::must_cast(_lstm->input_to_forget_weights()); + auto cw = loco::must_cast(_lstm->input_to_cell_weights()); + auto ow = loco::must_cast(_lstm->input_to_output_weights()); + + auto fcw = merged_weights(iw, fw, cw, ow); + fcw->name(base_name + "_fc_w"); + luci::add_origin(fcw, luci::get_origin(_lstm)); + + auto fcb = _nctx->create(); + + auto fc = _nctx->create(); + fc->input(input); + fc->weights(fcw); + fc->bias(fcb); + fc->fusedActivationFunction(luci::FusedActFunc::NONE); + fc->name(base_name + "_fc"); + luci::add_origin(fc, luci::get_origin(_lstm)); + + // recurrent weights + auto ri = loco::must_cast(_lstm->recurrent_to_input_weights()); + auto rf = loco::must_cast(_lstm->recurrent_to_forget_weights()); + auto rc = loco::must_cast(_lstm->recurrent_to_cell_weights()); + auto ro = loco::must_cast(_lstm->recurrent_to_output_weights()); + + auto fcrw = merged_weights(ri, rf, rc, ro); + fcrw->name(base_name + "_fcr_w"); + luci::add_origin(fcrw, luci::get_origin(_lstm)); + + auto fcrb = _nctx->create(); + + auto fcr = _nctx->create(); + fcr->input(mul); + fcr->weights(fcrw); + fcr->bias(fcrb); + fcr->fusedActivationFunction(luci::FusedActFunc::NONE); + fcr->name(base_name + "_fcr"); + luci::add_origin(fcr, luci::get_origin(_lstm)); + + auto add_fc = _nctx->create(); + add_fc->x(fcr); + add_fc->y(fc); + add_fc->fusedActivationFunction(luci::FusedActFunc::NONE); + add_fc->name(base_name + "_addfc"); + luci::add_origin(add_fc, luci::get_origin(_lstm)); + + // bias + auto ib = loco::must_cast(_lstm->input_gate_bias()); + auto fb = loco::must_cast(_lstm->forget_gate_bias()); + auto cb = loco::must_cast(_lstm->cell_gate_bias()); + auto ob = loco::must_cast(_lstm->output_gate_bias()); + + auto bias = merged_weights(ib, fb, cb, ob); + bias->name(base_name + "_bias"); + + auto add_bias = _nctx->create(); + add_bias->x(add_fc); + add_bias->y(bias); + add_bias->fusedActivationFunction(luci::FusedActFunc::NONE); + add_bias->name(base_name + "_addbias"); + luci::add_origin(add_bias, luci::get_origin(_lstm)); + + return add_bias; +} + +std::vector UnrollLSTM::matmul_splits(luci::CircleNode *input, + uint32_t step) +{ + assert(input != nullptr); + assert(step < _timesteps); + + std::string split_name = _name + "_sp" + std::to_string(step); + + auto split_dim = _nctx->create(); + split_dim->dtype(loco::DataType::S32); + split_dim->rank(1); + split_dim->dim(0) = 1; + split_dim->size(1); + split_dim->at(0) = 1; + split_dim->shape_status(luci::ShapeStatus::VALID); + split_dim->name(split_name + "_dim"); + luci::add_origin(split_dim, luci::get_origin(_lstm)); + + auto split = _nctx->create(); + split->num_split(4); + split->split_dim(split_dim); + split->input(input); + split->name(split_name); + luci::add_origin(split, luci::get_origin(_lstm)); + + auto split_o0 = _nctx->create(); + split_o0->input(split); + split_o0->index(0); + split_o0->name(split_name + "_spo0"); + luci::add_origin(split_o0, luci::get_origin(_lstm)); + + auto split_o1 = _nctx->create(); + split_o1->input(split); + split_o1->index(1); + split_o1->name(split_name + "_spo1"); + luci::add_origin(split_o1, luci::get_origin(_lstm)); + + auto split_o2 = _nctx->create(); + split_o2->input(split); + split_o2->index(2); + split_o2->name(split_name + "_spo2"); + luci::add_origin(split_o2, luci::get_origin(_lstm)); + + auto split_o3 = _nctx->create(); + split_o3->input(split); + split_o3->index(3); + split_o3->name(split_name + "_spo3"); + luci::add_origin(split_o3, luci::get_origin(_lstm)); + + std::vector outs; + outs.push_back(split_o0); + outs.push_back(split_o1); + outs.push_back(split_o2); + outs.push_back(split_o3); + return outs; +} + +luci::CircleConst *UnrollLSTM::forget_zero(void) +{ + uint32_t amount = _batch * _units; + + auto zero = _nctx->create(); + zero->dtype(loco::DataType::FLOAT32); + zero->rank(2); + zero->dim(0) = _batch; + zero->dim(1) = _units; + zero->size(amount); + for (uint32_t idx = 0; idx < amount; ++idx) + zero->at(idx) = 0.0; + zero->shape_status(luci::ShapeStatus::VALID); + zero->name(_name + "_zero"); + luci::add_origin(zero, luci::get_origin(_lstm)); + return zero; +} + +luci::CircleMul *UnrollLSTM::forget_gate_cell(std::vector &splits, + luci::CircleNode *prev, uint32_t step, + luci::CircleNode **retadd) +{ + assert(splits.size() > 0); + assert(prev != nullptr); + assert(step < _timesteps); + + std::string net_name = _name + "_net" + std::to_string(step); + + auto split_0 = splits[0]; // input-input : Logistic - Mul(c) - Add - Tanh - Mul + auto split_1 = splits[1]; // input-forget : Logistic - Mul(p) - Add - Tanh - Mul + auto split_2 = splits[2]; // input-cell : Tanh - Mul(c) - Add - Tanh - Mul + auto split_3 = splits[3]; // input-output : Logistic - Mul + + auto logis_0 = _nctx->create(); + logis_0->x(split_0); + logis_0->name(net_name + "_log0"); + luci::add_origin(logis_0, luci::get_origin(_lstm)); + + auto logis_1 = _nctx->create(); + logis_1->x(split_1); + logis_1->name(net_name + "_log1"); + luci::add_origin(logis_1, luci::get_origin(_lstm)); + + auto tanh_2 = _nctx->create(); + tanh_2->x(split_2); + tanh_2->name(net_name + "_tanh2"); + luci::add_origin(tanh_2, luci::get_origin(_lstm)); + + auto logis_3 = _nctx->create(); + logis_3->x(split_3); + logis_3->name(net_name + "_log3"); + luci::add_origin(logis_3, luci::get_origin(_lstm)); + + auto mul_c = _nctx->create(); + mul_c->x(logis_0); + mul_c->y(tanh_2); + mul_c->fusedActivationFunction(luci::FusedActFunc::NONE); + mul_c->name(net_name + "_mul1"); + luci::add_origin(mul_c, luci::get_origin(_lstm)); + + auto mul_p = _nctx->create(); + mul_p->x(logis_1); + mul_p->y(prev); + mul_p->fusedActivationFunction(luci::FusedActFunc::NONE); + mul_p->name(net_name + "_mul2"); + luci::add_origin(mul_p, luci::get_origin(_lstm)); + + auto add_cp = _nctx->create(); + add_cp->x(mul_c); + add_cp->y(mul_p); + add_cp->fusedActivationFunction(luci::FusedActFunc::NONE); + add_cp->name(net_name + "_add1"); + luci::add_origin(add_cp, luci::get_origin(_lstm)); + + if (retadd != nullptr) + *retadd = add_cp; + + auto tanh_cp = _nctx->create(); + tanh_cp->x(add_cp); + tanh_cp->name(net_name + "_tanh3"); + luci::add_origin(tanh_cp, luci::get_origin(_lstm)); + + auto mul_out = _nctx->create(); + mul_out->x(logis_3); + mul_out->y(tanh_cp); + mul_out->fusedActivationFunction(luci::FusedActFunc::NONE); + mul_out->name(net_name + "_mul3"); + luci::add_origin(mul_out, luci::get_origin(_lstm)); + + return mul_out; +} + +luci::CircleReshape *UnrollLSTM::last_reshape(luci::CircleNode *input) +{ + assert(input != nullptr); + + auto reshape_s = _nctx->create(); + reshape_s->dtype(loco::DataType::S32); + reshape_s->rank(1); + reshape_s->dim(0) = 3; + reshape_s->size(3); + reshape_s->at(0) = _batch; + reshape_s->at(1) = _timesteps; + reshape_s->at(2) = _units; + reshape_s->shape_status(luci::ShapeStatus::VALID); + reshape_s->name(_name + "_reshape_s"); + luci::add_origin(reshape_s, luci::get_origin(_lstm)); + + auto reshape = _nctx->create(); + reshape->tensor(input); + reshape->shape(reshape_s); + reshape->newShape()->rank(3); + reshape->newShape()->dim(0) = _batch; + reshape->newShape()->dim(1) = _timesteps; + reshape->newShape()->dim(2) = _units; + reshape->name(_name + "_reshape"); + luci::add_origin(reshape, luci::get_origin(_lstm)); + + return reshape; +} + +luci::CircleTranspose *UnrollLSTM::last_transpose(std::vector &output_muls) +{ + assert(output_muls.size() == _timesteps); + + auto pack = _nctx->create(_timesteps); + pack->axis(0); + for (uint32_t idx = 0; idx < _timesteps; ++idx) + pack->values(idx, output_muls[idx]); + pack->name(_name + "_pack"); + luci::add_origin(pack, luci::get_origin(_lstm)); + + auto perm = transpose_perm(); + perm->name(_name + "_perm2"); + luci::add_origin(perm, luci::get_origin(_lstm)); + + auto transpose = _nctx->create(); + transpose->a(pack); + transpose->perm(perm); + transpose->name(_name + "_trans2"); + luci::add_origin(transpose, luci::get_origin(_lstm)); + + return transpose; +} + +bool unroll_lstm(luci::CircleUnidirectionalSequenceLSTM *lstm) +{ + // NOTE shape of input of lstm is interpreted as [batch, timesteps, feature] + // shape of output of lstm is interpreted as [batch, timesteps, units] + // TODO add more conditions to check LSTM + assert(lstm != nullptr); + assert(lstm->rank() == 3); // use assert to findout when this happens + if (lstm->rank() != 3) + return false; + if (!(lstm->dim(0).known() and lstm->dim(1).known() and lstm->dim(2).known())) + return false; + + UnrollLSTM ulstm; + ulstm._lstm = lstm; + ulstm._nctx = lstm->graph()->nodes(); + ulstm._name = lstm->name(); + ulstm._batch = lstm->dim(0).value(); + ulstm._timesteps = lstm->dim(1).value(); + ulstm._units = lstm->dim(2).value(); // output space dim + + luci::CircleNode *input = loco::must_cast(lstm->input()); + assert(input->rank() == 3); // use assert to findout when this happens + if (input->rank() != 3) + return false; + assert(input->dim(0).value() == ulstm._batch); + assert(input->dim(1).value() == ulstm._timesteps); + + if (ulstm._timesteps > 1) + { + // Transpose to switch batch <-> timesteps + // NOTE TF uses Reshape when batch is 1 but as there is Transpose->Reshape + // Pass, we can just use Transpose for both cases + auto transpose = ulstm.first_transpose(input); + input = transpose; + } + + auto unpacks = ulstm.input_unpacks(input); + assert(unpacks.size() == ulstm._timesteps); + uint32_t step = 0; + auto unpackout = unpacks[step]; + + // First FC + auto fc_1 = ulstm.create_input_matmul(unpackout); + assert(fc_1 != nullptr); + auto splits = ulstm.matmul_splits(fc_1, step); + assert(splits.size() == 4); + + luci::CircleNode *prev = nullptr; // prev step CircleAdd + luci::CircleNode *this_add = nullptr; + + prev = ulstm.forget_zero(); // provide all zero constant for first step + + std::vector output_muls; + auto mul_gc = ulstm.forget_gate_cell(splits, prev, step, &this_add); + assert(mul_gc != nullptr); + assert(this_add != nullptr); + // gather all Muls for last Pack + output_muls.push_back(mul_gc); + + for (step = 1; step < ulstm._timesteps; ++step) + { + auto unpackout = unpacks[step]; + auto add_n = ulstm.create_input_matmul(unpackout, mul_gc, step); + + auto splits = ulstm.matmul_splits(add_n, step); + assert(splits.size() == 4); + + prev = this_add; + mul_gc = ulstm.forget_gate_cell(splits, prev, step, &this_add); + assert(mul_gc != nullptr); + assert(this_add != nullptr); + + output_muls.push_back(mul_gc); + } + assert(output_muls.size() == ulstm._timesteps); + + if (ulstm._timesteps == 1) + { + // Reshape for single step + auto reshape = ulstm.last_reshape(mul_gc); + loco::replace(lstm).with(reshape); + } + else + { + // Pack + Transpose for two or more steps + auto transpose = ulstm.last_transpose(output_muls); + loco::replace(lstm).with(transpose); + } + + return true; +} + +} // namespace + +namespace luci +{ + +bool UnrollUnidirectionalSequenceLSTMPass::run(loco::Graph *g) +{ + bool changed = false; + + for (auto node : loco::active_nodes(loco::output_nodes(g))) + { + if (auto lstm = dynamic_cast(node)) + { + if (unroll_lstm(lstm)) + changed = true; + } + } + + return changed; +} + +} // namespace luci diff --git a/compiler/luci/pass/src/UnrollUnidirectionalSequenceLSTMPass.test.cpp b/compiler/luci/pass/src/UnrollUnidirectionalSequenceLSTMPass.test.cpp new file mode 100644 index 0000000..3f273cb --- /dev/null +++ b/compiler/luci/pass/src/UnrollUnidirectionalSequenceLSTMPass.test.cpp @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci/Pass/UnrollUnidirectionalSequenceLSTMPass.h" + +#include + +#include + +#include + +namespace +{ + +using namespace luci::test; + +class UniSeqLSTMGraphlet +{ +public: + UniSeqLSTMGraphlet() = default; + + void init(loco::Graph *g, const ShapeU32 oshape) + { + _uslstm = g->nodes()->create(); + _uslstm->dtype(loco::DataType::FLOAT32); + _uslstm->shape(oshape); + _uslstm->name("uslstm"); + + _uslstm->fusedActivationFunction(luci::FusedActFunc::TANH); + _uslstm->cell_clip(0.0); + _uslstm->proj_clip(0.0); + _uslstm->time_major(false); + _uslstm->asymmetric_quantize_inputs(false); + + _iw = weight_1x1(g); + _rw = weight_1x1(g); + _gb = weight_1(g); + _ex = g->nodes()->create(); + } + +protected: + luci::CircleConst *weight_1x1(loco::Graph *g) + { + auto w = g->nodes()->create(); + w->dtype(loco::DataType::FLOAT32); + w->rank(2); + w->dim(0) = 1; + w->dim(1) = 1; + w->size(1); + w->at(0) = 1.0; + w->shape_status(luci::ShapeStatus::VALID); + return w; + } + + luci::CircleConst *weight_1(loco::Graph *g) + { + auto w = g->nodes()->create(); + w->dtype(loco::DataType::FLOAT32); + w->rank(1); + w->dim(0) = 1; + w->size(1); + w->at(0) = 1.0; + w->shape_status(luci::ShapeStatus::VALID); + return w; + } + +protected: + luci::CircleUnidirectionalSequenceLSTM *_uslstm = nullptr; + luci::CircleConst *_iw = nullptr; + luci::CircleConst *_rw = nullptr; + luci::CircleConst *_gb = nullptr; + luci::CircleOutputExclude *_ex = nullptr; +}; + +class UnrollUniSeqLSTMPassTestGraph : public TestIOGraph, public UniSeqLSTMGraphlet +{ +public: + UnrollUniSeqLSTMPassTestGraph() = default; + + void init(const ShapeU32 ishape, const ShapeU32 oshape) + { + TestIOGraph::init(ishape, oshape); + UniSeqLSTMGraphlet::init(g(), oshape); + + auto inode = input(); + _uslstm->input(inode); + + _uslstm->input_to_input_weights(_iw); + _uslstm->input_to_forget_weights(_iw); + _uslstm->input_to_cell_weights(_iw); + _uslstm->input_to_output_weights(_iw); + + _uslstm->recurrent_to_input_weights(_rw); + _uslstm->recurrent_to_forget_weights(_rw); + _uslstm->recurrent_to_cell_weights(_rw); + _uslstm->recurrent_to_output_weights(_rw); + + _uslstm->cell_to_input_weights(_ex); + _uslstm->cell_to_forget_weights(_ex); + _uslstm->cell_to_output_weights(_ex); + + _uslstm->input_gate_bias(_gb); + _uslstm->forget_gate_bias(_gb); + _uslstm->cell_gate_bias(_gb); + _uslstm->output_gate_bias(_gb); + + _uslstm->projection_weights(_ex); + _uslstm->projection_bias(_ex); + + _uslstm->output_state(_ex); + _uslstm->cell_state(_ex); + + _uslstm->input_layer_norm_coefficients(_ex); + _uslstm->forget_layer_norm_coefficients(_ex); + _uslstm->cell_layer_norm_coefficients(_ex); + _uslstm->output_layer_norm_coefficients(_ex); + + output()->from(_uslstm); + } +}; + +} // namespace + +namespace +{ + +using namespace luci::test; + +// FakeQuantGraphlet is for simple negative test +class FakeQuantGraphlet +{ +public: + FakeQuantGraphlet() = default; + +public: + void init(loco::Graph *g) + { + _fq = g->nodes()->create(); + _fq->name("fq"); + } + +protected: + luci::CircleFakeQuant *_fq = nullptr; +}; + +class FakeQuantGraph : public TestIOGraph, public FakeQuantGraphlet +{ +public: + FakeQuantGraph() = default; + +public: + void init(void) + { + TestIOGraph::init({1, 1, 1}, {1, 1, 1}); + FakeQuantGraphlet::init(g()); + + _fq->inputs(input()); + + output()->from(_fq); + } +}; + +} // namespace + +TEST(UnrollUnidirectionalSequenceLSTMPassTestName, name) +{ + luci::UnrollUnidirectionalSequenceLSTMPass pass; + auto const name = pass.name(); + ASSERT_NE(nullptr, name); +} + +class UnrollUnidirectionalSequenceLSTMPassTest : public ::testing::Test +{ +public: + UnrollUniSeqLSTMPassTestGraph g; + luci::UnrollUnidirectionalSequenceLSTMPass pass; +}; + +TEST_F(UnrollUnidirectionalSequenceLSTMPassTest, simple_run) +{ + g.init({1, 1, 1}, {1, 1, 1}); + + EXPECT_TRUE(pass.run(g.g())); +} + +class UnrollUnidirectionalSequenceLSTMPassTestN : public ::testing::Test +{ +public: + FakeQuantGraph g; + luci::UnrollUnidirectionalSequenceLSTMPass pass; +}; + +TEST_F(UnrollUnidirectionalSequenceLSTMPassTestN, simple_run_NEG) +{ + g.init(); + + EXPECT_FALSE(pass.run(g.g())); +} diff --git a/compiler/luci/pass/src/VerifyQuantizedNodeGranularity.h b/compiler/luci/pass/src/VerifyQuantizedNodeGranularity.h index 408e6b8..6bf7ff6 100644 --- a/compiler/luci/pass/src/VerifyQuantizedNodeGranularity.h +++ b/compiler/luci/pass/src/VerifyQuantizedNodeGranularity.h @@ -133,6 +133,10 @@ private: bool visit(const luci::CircleAdd *node) { + // Skip granularity check for indices + if (node->dtype() == loco::DataType::S32 or node->dtype() == loco::DataType::S64) + return true; + RETURN_FALSE_UNLESS(is_lwq(node)); RETURN_FALSE_UNLESS(is_lwq(node->x())); RETURN_FALSE_UNLESS(is_lwq(node->y())); @@ -176,6 +180,10 @@ private: bool visit(const luci::CircleMul *node) { + // Skip granularity check for indices + if (node->dtype() == loco::DataType::S32 or node->dtype() == loco::DataType::S64) + return true; + RETURN_FALSE_UNLESS(is_lwq(node)); RETURN_FALSE_UNLESS(is_lwq(node->x())); RETURN_FALSE_UNLESS(is_lwq(node->y())); diff --git a/compiler/luci/pass/src/VerifyQuantizedNodeType.cpp b/compiler/luci/pass/src/VerifyQuantizedNodeType.cpp index cf86aca..3ce3255 100644 --- a/compiler/luci/pass/src/VerifyQuantizedNodeType.cpp +++ b/compiler/luci/pass/src/VerifyQuantizedNodeType.cpp @@ -47,6 +47,10 @@ namespace luci template bool VerifyQuantizedNodeTypeBase::visit(const luci::CircleAdd *node) { + // Allow add of indices + if (group_has_type(node, loco::DataType::S32) or group_has_type(node, loco::DataType::S64)) + return true; + return group_has_type(node, Qtype); } @@ -240,6 +244,10 @@ bool VerifyQuantizedNodeTypeBase::visit(const luci::CircleMirrorPa template bool VerifyQuantizedNodeTypeBase::visit(const luci::CircleMul *node) { + // Allow mul of indices + if (group_has_type(node, loco::DataType::S32) or group_has_type(node, loco::DataType::S64)) + return true; + return group_has_type(node, Qtype); } diff --git a/compiler/luci/pass/src/helpers/NodeFiller.h b/compiler/luci/pass/src/helpers/NodeFiller.h index b80f085..10113e8 100644 --- a/compiler/luci/pass/src/helpers/NodeFiller.h +++ b/compiler/luci/pass/src/helpers/NodeFiller.h @@ -57,6 +57,12 @@ public: */ template bool with_commutative_args_of(const COMM_NODE *node); + /** + * @note Similar as with_commutative_args_of but not commutative. + * _arg_1 and _arg_2 must match that of ARG_TYPE_1 and ARG_TYPE_2. + */ + template bool with_args_of(const COMM_NODE *node); + private: ARG_TYPE_1 **_arg_1; ARG_TYPE_2 **_arg_2; @@ -101,4 +107,24 @@ bool NodeFiller::with_commutative_args_of(const COMM_NOD return false; } +template +template +bool NodeFiller::with_args_of(const COMM_NODE *node) +{ + // X == ARG_TYPE_1 / Y == ARG_TYPE_2 + { + auto x = dynamic_cast(node->x()); + auto y = dynamic_cast(node->y()); + + if (x && y) + { + *_arg_1 = x; + *_arg_2 = y; + return true; + } + } + + return false; +} + } // namespace luci diff --git a/compiler/luci/pass/src/helpers/SparsityFormatConverter.h b/compiler/luci/pass/src/helpers/SparsityFormatConverter.h index fcd9bbc..e014304 100644 --- a/compiler/luci/pass/src/helpers/SparsityFormatConverter.h +++ b/compiler/luci/pass/src/helpers/SparsityFormatConverter.h @@ -18,6 +18,7 @@ #ifndef __LUCI_PASS_HELPERS_SPARSITY_FORMAT_CONVERTER_H__ #define __LUCI_PASS_HELPERS_SPARSITY_FORMAT_CONVERTER_H__ +#include #include #include diff --git a/compiler/luci/pass/src/helpers/Strings.cpp b/compiler/luci/pass/src/helpers/Strings.cpp index d020f6d..2628726 100644 --- a/compiler/luci/pass/src/helpers/Strings.cpp +++ b/compiler/luci/pass/src/helpers/Strings.cpp @@ -77,6 +77,15 @@ loco::DataType str_to_dtype(const std::string &str) return loco::DataType::Unknown; } +// Convert string to a vector of loco::DataType +std::vector str_vec_to_dtype_vec(std::vector &vec) +{ + std::vector res; + std::transform(vec.begin(), vec.end(), std::back_inserter(res), + [](std::string s) -> loco::DataType { return str_to_dtype(to_lower_case(s)); }); + return res; +} + QuantizationGranularity str_to_granularity(const std::string &str) { if (to_lower_case(str).compare("layer") == 0) diff --git a/compiler/luci/pass/src/helpers/Strings.h b/compiler/luci/pass/src/helpers/Strings.h index 0e78185..485f379 100644 --- a/compiler/luci/pass/src/helpers/Strings.h +++ b/compiler/luci/pass/src/helpers/Strings.h @@ -36,6 +36,8 @@ std::string to_lower_case(std::string); loco::DataType str_to_dtype(const std::string &); +std::vector str_vec_to_dtype_vec(std::vector &); + QuantizationGranularity str_to_granularity(const std::string &); } // namespace luci diff --git a/compiler/luci/pass/src/helpers/Strings.test.cpp b/compiler/luci/pass/src/helpers/Strings.test.cpp index d77b650..6d854ad 100644 --- a/compiler/luci/pass/src/helpers/Strings.test.cpp +++ b/compiler/luci/pass/src/helpers/Strings.test.cpp @@ -48,3 +48,26 @@ TEST(StringsTest, str_to_granularity) EXPECT_THROW(luci::str_to_granularity("foo"), std::runtime_error); } + +TEST(StringsTest, str_vec_to_dtype_vec) +{ + std::vector input1 = {"uint8", "int16", "float32"}; + auto result1 = luci::str_vec_to_dtype_vec(input1); + ASSERT_EQ(3, result1.size()); + ASSERT_EQ(loco::DataType::U8, result1[0]); + ASSERT_EQ(loco::DataType::S16, result1[1]); + ASSERT_EQ(loco::DataType::FLOAT32, result1[2]); + + std::vector input2 = {"uint8", "int16", "float32", ""}; + auto result2 = luci::str_vec_to_dtype_vec(input2); + ASSERT_EQ(4, result2.size()); + ASSERT_EQ(loco::DataType::U8, result2[0]); + ASSERT_EQ(loco::DataType::S16, result2[1]); + ASSERT_EQ(loco::DataType::FLOAT32, result2[2]); + ASSERT_EQ(loco::DataType::Unknown, result2[3]); + + std::vector input3 = {"uint8"}; + auto result3 = luci::str_vec_to_dtype_vec(input3); + ASSERT_EQ(1, result3.size()); + ASSERT_EQ(loco::DataType::U8, result3[0]); +} diff --git a/compiler/luci/pass/src/test/TestIOGraph.h b/compiler/luci/pass/src/test/TestIOGraph.h deleted file mode 100644 index b1fc41f..0000000 --- a/compiler/luci/pass/src/test/TestIOGraph.h +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __LUCI_PASS_TEST_IO_GRAPH_H__ -#define __LUCI_PASS_TEST_IO_GRAPH_H__ - -#include "TestShape.h" - -#include - -namespace luci -{ -namespace test -{ - -/** - * @brief Graphlet with Inputs and loco::Graph for multiple inputs - * @note Every Graph will have Input(s) and Output(s) - * We put loco::Graph only in IsGraphlet not to declare separate - * class for loco::Graph - */ -template class TestIsGraphlet -{ -public: - TestIsGraphlet() - { - for (uint32_t n = 0; n < N; ++n) - { - _graph_inputs[n] = nullptr; - _inputs[n] = nullptr; - } - } - -public: - virtual void init(loco::Graph *g, const ShapeU32 shape_in) - { - for (uint32_t n = 0; n < N; ++n) - { - _graph_inputs[n] = g->inputs()->create(); - - _inputs[n] = g->nodes()->create(); - _inputs[n]->shape(shape_in); - _inputs[n]->shape_status(luci::ShapeStatus::VALID); - _inputs[n]->dtype(loco::DataType::FLOAT32); - _inputs[n]->name("input_" + std::to_string(n)); - - _inputs[n]->index(_graph_inputs[n]->index()); - - auto input_shape = std::make_unique(); - set_shape_vector(input_shape.get(), shape_in); - _graph_inputs[n]->shape(std::move(input_shape)); - _graph_inputs[n]->dtype(loco::DataType::FLOAT32); - } - } - -public: - loco::Graph *g(void) { return &_g; } - luci::CircleInput *input(int idx) { return _inputs[idx]; } - -protected: - loco::Graph _g; - std::array _graph_inputs; - std::array _inputs; -}; - -/** - * @brief Graphlet with one Input - */ -class TestIGraphlet : public TestIsGraphlet<1> -{ -public: - luci::CircleInput *input() { return _inputs[0]; } -}; - -/** - * @brief Graphlet with Outputs for multiple outputs - */ -template class TestOsGraphlet -{ -public: - TestOsGraphlet() - { - for (uint32_t n = 0; n < N; ++n) - { - _graph_outputs[n] = nullptr; - _outputs[n] = nullptr; - } - } - -public: - virtual void init(loco::Graph *g, const ShapeU32 shape_out) - { - for (uint32_t n = 0; n < N; ++n) - { - _graph_outputs[n] = g->outputs()->create(); - - _outputs[n] = g->nodes()->create(); - _outputs[n]->shape(shape_out); - _outputs[n]->shape_status(luci::ShapeStatus::VALID); - _outputs[n]->dtype(loco::DataType::FLOAT32); - _outputs[n]->name("output_" + std::to_string(n)); - - _outputs[n]->index(_graph_outputs[n]->index()); - - auto output_shape = std::make_unique(); - set_shape_vector(output_shape.get(), shape_out); - _graph_outputs[n]->shape(std::move(output_shape)); - _graph_outputs[n]->dtype(loco::DataType::FLOAT32); - } - } - -public: - luci::CircleOutput *output(int idx) { return _outputs[idx]; } - -protected: - std::array _graph_outputs; - std::array _outputs; -}; - -/** - * @brief Graphlet with one Output - */ -class TestOGraphlet : public TestOsGraphlet<1> -{ -public: - luci::CircleOutput *output() { return _outputs[0]; } -}; - -/** - * @brief Graph with Input and Output - */ -class TestIOGraph : public TestIGraphlet, public TestOGraphlet -{ -public: - TestIOGraph() = default; - -public: - virtual void init(const ShapeU32 shape_in, const ShapeU32 shape_out) - { - TestIsGraphlet<1>::init(g(), shape_in); - TestOsGraphlet<1>::init(g(), shape_out); - } -}; - -} // namespace test -} // namespace luci - -#endif // __LUCI_PASS_TEST_IO_GRAPH_H__ diff --git a/compiler/luci/pass/src/test/TestIOGraph.test.cpp b/compiler/luci/pass/src/test/TestIOGraph.test.cpp deleted file mode 100644 index e58a13f..0000000 --- a/compiler/luci/pass/src/test/TestIOGraph.test.cpp +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "TestIOGraph.h" - -// This file validates "TestIOGraph.h". Pleaes DO NOT remove this file. diff --git a/compiler/luci/requires.cmake b/compiler/luci/requires.cmake index 0a5e6a5..f3546b0 100644 --- a/compiler/luci/requires.cmake +++ b/compiler/luci/requires.cmake @@ -5,7 +5,7 @@ require("locop") require("logo") require("logo-core") require("mio-circle04") -require("mio-tflite280") +require("luci-compute") require("oops") require("hermes") require("hermes-std") diff --git a/compiler/luci/service/src/CircleShapeInferenceRule.cpp b/compiler/luci/service/src/CircleShapeInferenceRule.cpp index a368fae..9d4dbab 100644 --- a/compiler/luci/service/src/CircleShapeInferenceRule.cpp +++ b/compiler/luci/service/src/CircleShapeInferenceRule.cpp @@ -682,30 +682,6 @@ loco::NodeShape infer_fully_connected(const luci::CircleFullyConnected *node) auto input_shape = luci::shape_get(node->input()).as(); auto weights_shape = luci::shape_get(node->weights()).as(); -// TODO Remove following unused code -#if 0 - // Checking shape capability for fully connected layer - // Input: a tensor of at least rank 2 [D1, D2, ... Dn] - // Weight: [# of units, K] - // Output: [D1 * D2 * ... * Dn / K, # of units] - if (input_shape.rank() < 2 || weights_shape.rank() != 2) - { - // Return node own shape if shape inference is not possible - return use_own(node); - } - - uint32_t input_size = 1; - for (uint32_t i = 0; i < input_shape.rank(); i++) - { - input_size = input_size * input_shape.dim(i).value(); - } - const uint32_t batch_size = input_size / weights_shape.dim(1).value(); - loco::TensorShape out_shape; - out_shape.rank(2); - out_shape.dim(0) = batch_size; - out_shape.dim(1) = weights_shape.dim(0); -#endif - loco::TensorShape out_shape; // NOTE Some recipes in some repositories are using rank 4 input for FullyConnected. diff --git a/compiler/luci/service/src/CircleTypeInferenceRule.cpp b/compiler/luci/service/src/CircleTypeInferenceRule.cpp index 7616390..44c9330 100644 --- a/compiler/luci/service/src/CircleTypeInferenceRule.cpp +++ b/compiler/luci/service/src/CircleTypeInferenceRule.cpp @@ -573,7 +573,14 @@ struct TypeInferenceAlgorithm final : public luci::CircleNodeVisitordtype(); } - loco::DataType visit(const luci::CircleOutputExclude *node) final { return node->dtype(); } + loco::DataType visit(const luci::CircleOutputExclude *node) final + { + // NOTE We don't care CircleOutputExclude dtype, but set to FLOAT32 + // if it's Unknown to make type inference happy. + if (node->dtype() == loco::DataType::Unknown) + return loco::DataType::FLOAT32; + return node->dtype(); + } loco::DataType visit(const luci::CircleCustomOut *node) final { return node->dtype(); } diff --git a/compiler/luci/service/src/Nodes/CircleFullyConnected.cpp b/compiler/luci/service/src/Nodes/CircleFullyConnected.cpp index 277107b..7c37060 100644 --- a/compiler/luci/service/src/Nodes/CircleFullyConnected.cpp +++ b/compiler/luci/service/src/Nodes/CircleFullyConnected.cpp @@ -31,6 +31,7 @@ luci::CircleNode *CloneNodeLet::visit(const luci::CircleFullyConnected { cloned->fusedActivationFunction(node->fusedActivationFunction()); cloned->weights_format(node->weights_format()); + cloned->keep_num_dims(node->keep_num_dims()); } return cloned; } diff --git a/compiler/luci/service/src/Validate.cpp b/compiler/luci/service/src/Validate.cpp index fbd86df..2f78e05 100644 --- a/compiler/luci/service/src/Validate.cpp +++ b/compiler/luci/service/src/Validate.cpp @@ -148,6 +148,78 @@ bool validate_shape_dtype(loco::Graph *g) return true; } +class MultiOutNodeValidate final : public luci::CircleNodeVisitor +{ +public: + MultiOutNodeValidate() {} + +private: + template bool check(const luci::CircleNode *node) + { + auto succs = loco::succs(node); + if (succs.size() < 1) + return false; + for (const auto &cnode : succs) + { + auto const child = dynamic_cast(cnode); + if (child == nullptr) + return false; + } + return true; + } + +public: + bool visit(const luci::CircleBidirectionalSequenceLSTM *node) final + { + return check(node); + } + bool visit(const luci::CircleCustom *node) final { return check(node); } + bool visit(const luci::CircleIf *node) final { return check(node); } + bool visit(const luci::CircleNonMaxSuppressionV4 *node) final + { + return check(node); + } + bool visit(const luci::CircleNonMaxSuppressionV5 *node) final + { + return check(node); + } + bool visit(const luci::CircleSplit *node) final { return check(node); } + bool visit(const luci::CircleSplitV *node) final { return check(node); } + bool visit(const luci::CircleTopKV2 *node) final { return check(node); } + bool visit(const luci::CircleUnique *node) final { return check(node); } + bool visit(const luci::CircleUnpack *node) final { return check(node); } + bool visit(const luci::CircleWhile *node) final { return check(node); } + + // default true for other nodes + bool visit(const luci::CircleNode *) final { return true; } +}; + +/** + * @brief Validate sequence of multi-output nodes are followed for specific + * IRs such as CircleIfOut. + */ +bool validate_multi_outs(loco::Graph *g) +{ + LOGGER(l); + + for (auto node : loco::active_nodes(loco::output_nodes(g))) + { + auto const cnode = loco::must_cast(node); + + MultiOutNodeValidate d; + if (cnode->accept(&d)) + continue; + + auto const name = cnode->name(); + INFO(l) << "Node: " << name << ", " << (uint32_t)(cnode->opcode()) << " has invalid successor." + << std::endl; + + return false; + } + + return true; +} + class VirtualNodeDetector final : public luci::CircleNodeVisitor { public: @@ -185,6 +257,9 @@ bool validate(loco::Graph *g) if (!validate_shape_dtype(g)) return false; + if (!validate_multi_outs(g)) + return false; + // TODO add more validation return true; diff --git a/compiler/luci/tests/test.lst b/compiler/luci/tests/test.lst index 09a25ff..8291f20 100644 --- a/compiler/luci/tests/test.lst +++ b/compiler/luci/tests/test.lst @@ -193,6 +193,7 @@ addread(Transpose_000) addread(TransposeConv_000) addread(UnidirectionalSequenceLSTM_000) addread(UnidirectionalSequenceLSTM_001) +addread(UnidirectionalSequenceLSTM_002) addread(Unique_000) addread(Unique_001) addread(Unique_002) @@ -419,6 +420,7 @@ addwrite(Transpose_000) addwrite(TransposeConv_000) addwrite(UnidirectionalSequenceLSTM_000) addwrite(UnidirectionalSequenceLSTM_001) +addwrite(UnidirectionalSequenceLSTM_002) addwrite(Unique_000) addwrite(Unique_001) addwrite(Unique_002) diff --git a/compiler/mio-tflite280/CMakeLists.txt b/compiler/mio-tflite280/CMakeLists.txt index f48711e..edf75f4 100644 --- a/compiler/mio-tflite280/CMakeLists.txt +++ b/compiler/mio-tflite280/CMakeLists.txt @@ -38,16 +38,6 @@ target_link_libraries(mio_tflite280_example mio_tflite280) add_executable(mio_tflite280_validate example.cpp) target_link_libraries(mio_tflite280_validate mio_tflite280) -nnas_find_package(TensorFlowGEMMLowpSource EXACT 2.8.0 QUIET) - -if(NOT TensorFlowGEMMLowpSource_FOUND) - return() -endif(NOT TensorFlowGEMMLowpSource_FOUND) - -add_library(mio_tflite280_inc INTERFACE) -target_include_directories(mio_tflite280_inc SYSTEM INTERFACE "${TensorFlowSource_DIR}") -target_include_directories(mio_tflite280_inc SYSTEM INTERFACE "${TensorFlowGEMMLowpSource_DIR}") - file(GLOB_RECURSE SOURCES "src/*.cpp") file(GLOB_RECURSE TESTS "src/*.test.cpp") list(REMOVE_ITEM SOURCES ${TESTS}) diff --git a/compiler/nnc/passes/optimizations/ConstantFoldTranspose.cpp b/compiler/nnc/passes/optimizations/ConstantFoldTranspose.cpp index 47a3147..ce99cdb 100644 --- a/compiler/nnc/passes/optimizations/ConstantFoldTranspose.cpp +++ b/compiler/nnc/passes/optimizations/ConstantFoldTranspose.cpp @@ -62,7 +62,7 @@ PassData ConstantFoldTranspose::run(PassData data) auto matches = matcher.matchEdge(is_constant, is_transpose); while (!matches.empty()) { - for (const auto match : matches) + for (const auto &match : matches) { auto constant_op = dynamic_cast(match.first); auto transpose_op = dynamic_cast(match.second); diff --git a/compiler/nnc/tests/soft_backend/test_main.def b/compiler/nnc/tests/soft_backend/test_main.def index 6a464f8..c508cca 100644 --- a/compiler/nnc/tests/soft_backend/test_main.def +++ b/compiler/nnc/tests/soft_backend/test_main.def @@ -1,8 +1,9 @@ +#include int main() { Shape s{1, 2, 3}; Tensor in_t(s); - NNModel model("nnmodel.params"); + NNModel model(std::string("nnmodel.params")); model.set_in(in_t); model.doInference(); std::shared_ptr out_t = model.get_out(); diff --git a/compiler/one-cmds/CMakeLists.txt b/compiler/one-cmds/CMakeLists.txt index 90e989a..7772b53 100644 --- a/compiler/one-cmds/CMakeLists.txt +++ b/compiler/one-cmds/CMakeLists.txt @@ -1,3 +1,23 @@ +# NOTE find_package try to use at least python3.8 as follows depending on platform version +# Ubuntu18.04; explictly installed python3.8 (default is python3.6) +# Ubuntu20.04; default python3.8 +# Ubuntu22.04; default python3.10 +# refer https://github.com/Samsung/ONE/issues/9962 +find_package(PythonInterp 3.8 QUIET) +find_package(PythonLibs 3.8 QUIET) + +if(NOT ${PYTHONINTERP_FOUND}) + message(STATUS "Build one-cmds: FALSE (Python3 is missing)") + return() +endif() + +if(${PYTHON_VERSION_MINOR} LESS 8) + message(STATUS "Build one-cmds: FALSE (You need to install Python version higher than 3.8)") + return() +endif() + +# NOTE these files should not have extensions. +# below code will remove extension when copy and install. set(ONE_COMMAND_FILES one-build one-import @@ -12,10 +32,18 @@ set(ONE_COMMAND_FILES one-profile one-infer one-codegen - one-prepare-venv onecc ) +# TODO find better way for per-platform files +if(ONE_UBUNTU_CODENAME_JAMMY) + # NOTE copy one-prepare-venv.u2204 as build/../one-prepare-venv + # and install build/../one-prepare-venv file + list(APPEND ONE_COMMAND_FILES one-prepare-venv.u2204) +else() + list(APPEND ONE_COMMAND_FILES one-prepare-venv) +endif() + # pytorch importer is an experimental feature, it is not used in default configuration if(ENABLE_ONE_IMPORT_PYTORCH) list(APPEND ONE_COMMAND_FILES one-import-pytorch) @@ -25,7 +53,9 @@ foreach(ONE_COMMAND IN ITEMS ${ONE_COMMAND_FILES}) set(ONE_COMMAND_FILE ${ONE_COMMAND}) set(ONE_COMMAND_SRC "${CMAKE_CURRENT_SOURCE_DIR}/${ONE_COMMAND_FILE}") - set(ONE_COMMAND_BIN "${CMAKE_CURRENT_BINARY_DIR}/${ONE_COMMAND_FILE}") + # strip extension from the name + get_filename_component(ONE_COMMNAD_FILE_NAME ${ONE_COMMAND} NAME_WE) + set(ONE_COMMAND_BIN "${CMAKE_CURRENT_BINARY_DIR}/${ONE_COMMNAD_FILE_NAME}") set(ONE_COMMAND_TARGET "${ONE_COMMAND}_target") add_custom_command(OUTPUT ${ONE_COMMAND_BIN} @@ -36,7 +66,7 @@ foreach(ONE_COMMAND IN ITEMS ${ONE_COMMAND_FILES}) add_custom_target(${ONE_COMMAND_TARGET} ALL DEPENDS ${ONE_COMMAND_BIN}) - install(FILES ${ONE_COMMAND} + install(FILES ${ONE_COMMAND_BIN} PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE @@ -47,7 +77,6 @@ endforeach(ONE_COMMAND) set(ONE_UTILITY_FILES one-build.template.cfg onecc.template.cfg - utils.py onnx_legalizer.py ) @@ -74,13 +103,24 @@ foreach(ONE_UTILITY IN ITEMS ${ONE_UTILITY_FILES}) endforeach(ONE_UTILITY) +# one-pack internally uses model2nnpkg tool +set(MODEL2NNPKG "${NNAS_PROJECT_SOURCE_DIR}/tools/nnpackage_tool/model2nnpkg/model2nnpkg.py") +install(FILES ${MODEL2NNPKG} + PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE + WORLD_READ WORLD_EXECUTE + DESTINATION bin + RENAME "model2nnpkg") + # make python directory set(ONE_PYTHON_FILES constant.py + export_constant.py make_cmd.py CfgRunner.py OptionBuilder.py TopologicalSortHelper.py - WorkflowRunner.py) + WorkflowRunner.py + utils.py) foreach(ONE_PYTHON_FILE IN ITEMS ${ONE_PYTHON_FILES}) @@ -111,6 +151,28 @@ foreach(ONE_PYTHON_FILE IN ITEMS ${ONE_PYTHON_FILES}) endforeach(ONE_PYTHON_FILE) +set(CONSTANT_EXPORTING_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/${ONE_PYTHON_DIR}/export_constant.py") +set(O1_OPTION "O1") +set(O1_CFG_FILE "${O1_OPTION}.cfg") +set(O1_CFG_FILE_BIN "${CMAKE_CURRENT_BINARY_DIR}/${ONE_PYTHON_DIR}/${O1_CFG_FILE}") + +add_custom_command(OUTPUT ${O1_CFG_FILE_BIN} + COMMAND ${PYTHON_EXECUTABLE} ${CONSTANT_EXPORTING_SCRIPT} + --constant ${O1_OPTION} + --format cfg + --output_path ${O1_CFG_FILE_BIN} + DEPENDS ${CONSTANT_EXPORTING_SCRIPT} + COMMENT "Generate ${O1_CFG_FILE}" +) + +add_custom_target("O1_cfg_target" ALL DEPENDS ${O1_CFG_FILE_BIN}) + +install(FILES ${O1_CFG_FILE_BIN} + PERMISSIONS OWNER_WRITE OWNER_READ + GROUP_READ + WORLD_READ + DESTINATION optimization) + set(ONE_DOCUMENT_FILES how-to-use-one-commands.txt how-to-prepare-virtualenv.txt diff --git a/compiler/one-cmds/dummy-driver/CMakeLists.txt b/compiler/one-cmds/dummy-driver/CMakeLists.txt index 2552a02..912798e 100644 --- a/compiler/one-cmds/dummy-driver/CMakeLists.txt +++ b/compiler/one-cmds/dummy-driver/CMakeLists.txt @@ -6,6 +6,7 @@ set(DUMMY_INFER_V2_SRC src/dummy-inferV2.cpp) set(HELP_INFER_SRC src/help-infer.cpp) set(DUMMY_PROFILE_SRC src/dummy-profile.cpp) set(HELP_PROFILE_SRC src/help-profile.cpp) +set(DUMMY_ENV_SRC src/dummyEnv-compile.cpp) add_executable(dummy-compile ${DUMMY_DRIVER_SRC}) add_executable(help-compile ${HELP_DRIVER_SRC}) @@ -14,6 +15,7 @@ add_executable(dummy-inferV2 ${DUMMY_INFER_V2_SRC}) add_executable(help-infer ${HELP_INFER_SRC}) add_executable(dummy-profile ${DUMMY_PROFILE_SRC}) add_executable(help-profile ${HELP_PROFILE_SRC}) +add_executable(dummyEnv-compile ${DUMMY_ENV_SRC}) set(DUMMY_DRIVER "${CMAKE_CURRENT_BINARY_DIR}/dummy-compile") set(HELP_DRIVER "${CMAKE_CURRENT_BINARY_DIR}/help-compile") @@ -22,6 +24,7 @@ set(DUMMY_INFER_V2 "${CMAKE_CURRENT_BINARY_DIR}/dummy-inferV2") set(HELP_INFER "${CMAKE_CURRENT_BINARY_DIR}/help-infer") set(DUMMY_PROFILE "${CMAKE_CURRENT_BINARY_DIR}/dummy-profile") set(HELP_PROFILE "${CMAKE_CURRENT_BINARY_DIR}/help-profile") +set(DUMMY_ENV "${CMAKE_CURRENT_BINARY_DIR}/dummyEnv-compile") install(FILES ${DUMMY_DRIVER} PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE @@ -64,3 +67,9 @@ install(FILES ${HELP_PROFILE} GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE DESTINATION test) + +install(FILES ${DUMMY_ENV} + PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE + WORLD_READ WORLD_EXECUTE + DESTINATION test) diff --git a/compiler/one-cmds/dummy-driver/src/dummyEnv-compile.cpp b/compiler/one-cmds/dummy-driver/src/dummyEnv-compile.cpp new file mode 100644 index 0000000..b16ea30 --- /dev/null +++ b/compiler/one-cmds/dummy-driver/src/dummyEnv-compile.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * dummyEnv-compile only tests its interface rather than its functionality. + * + * ./dummyEnv-compile ${DUMMY_OUTPUT} + */ + +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + if (argc != 2) + return EXIT_FAILURE; + + std::string spm_size; + + if (const char *env_p = std::getenv("SPM_SIZE")) + spm_size = std::string(env_p); + + std::ofstream outfile(argv[1]); + + outfile << "SPM_SIZE=" << spm_size; + + outfile.close(); + + return EXIT_SUCCESS; +} diff --git a/compiler/one-cmds/how-to-prepare-virtualenv.txt b/compiler/one-cmds/how-to-prepare-virtualenv.txt index 8d6007f..c36650a 100644 --- a/compiler/one-cmds/how-to-prepare-virtualenv.txt +++ b/compiler/one-cmds/how-to-prepare-virtualenv.txt @@ -1,15 +1,15 @@ About ----- -Last update: 2020-09-15 +Last update: 2023-01-30 This document explains about 'one-prepare-venv' command. -'one-prepare-venv' will prepare python3.8 virtual environment with tensorflow-cpu -version 2.3.0, recommanded 2.x version as of now, so that 'one-import-tf' +'one-prepare-venv' will prepare python3 virtual environment with tensorflow-cpu +version 2.8.0, recommanded 2.x version as of now, so that 'one-import-tf' command can execute properly. -'one-prepare-venv' will also prepare onnx and onnx-tensorflow version 1.7.0 so +'one-prepare-venv' will also prepare onnx and onnx-tensorflow version 1.10.0 so that 'one-import-onnx' command can execute properly. diff --git a/compiler/one-cmds/how-to-use-one-commands.txt b/compiler/one-cmds/how-to-use-one-commands.txt index 2352bbd..028cde4 100644 --- a/compiler/one-cmds/how-to-use-one-commands.txt +++ b/compiler/one-cmds/how-to-use-one-commands.txt @@ -169,6 +169,7 @@ Current transformation options are - fuse_instnorm: This will convert instance normalization related operators to one InstanceNormalization operator that our onert provides for faster execution. +- fuse_prelu: This will fuse operators to PReLU operator - fuse_preactivation_batchnorm: This fuses batch normalization operators of pre-activations to Conv operators. - fuse_activation_function: This fuses Activation function to a preceding operator. - fuse_mean_with_mean: This fuses two consecutive ReduceMean operations into one. diff --git a/compiler/one-cmds/one-build b/compiler/one-cmds/one-build index 4b1f980..b21a162 100644 --- a/compiler/one-cmds/one-build +++ b/compiler/one-cmds/one-build @@ -24,7 +24,7 @@ import configparser import os import sys -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error # This suppression is applied only to `one-build` @@ -35,9 +35,9 @@ def _get_parser(): parser = argparse.ArgumentParser( description='command line tool to run ONE drivers in customized order') - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) - opt_name_list = _utils._get_optimization_list(get_name=True) + opt_name_list = oneutils.get_optimization_list(get_name=True) opt_name_list = ['-' + s for s in opt_name_list] if not opt_name_list: opt_help_message = '(No available optimization options)' @@ -54,7 +54,7 @@ def _parse_arg(parser): args = parser.parse_args() # print version if args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return args @@ -62,12 +62,12 @@ def _parse_arg(parser): def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given - if not _utils._is_valid_attr(args, 'config'): + if not oneutils.is_valid_attr(args, 'config'): parser.error('-C/--config argument is required') # check if given optimization option exists - opt_name_list = _utils._get_optimization_list(get_name=True) - opt_name_list = [_utils._remove_prefix(s, 'O') for s in opt_name_list] - if _utils._is_valid_attr(args, 'O'): + opt_name_list = oneutils.get_optimization_list(get_name=True) + opt_name_list = [oneutils.remove_prefix(s, 'O') for s in opt_name_list] + if oneutils.is_valid_attr(args, 'O'): if ' ' in getattr(args, 'O'): parser.error('Not allowed to have space in the optimization name') if not getattr(args, 'O') in opt_name_list: @@ -88,7 +88,7 @@ def _get_driver_name(driver_name): }[driver_name] -def _parse_cfg(args): +def parse_cfg(args): config = configparser.ConfigParser() config.optionxform = str parsed = config.read(os.path.expanduser(getattr(args, 'config'))) @@ -121,12 +121,12 @@ def _verify_cfg(driver_list, config): # verify given optimization option file def _verify_opt(args): - if _utils._is_valid_attr(args, 'O'): + if oneutils.is_valid_attr(args, 'O'): config = configparser.ConfigParser() config.optionxform = str opt_name_path_dic = dict( - zip(_utils._get_optimization_list(get_name=True), - _utils._get_optimization_list())) + zip(oneutils.get_optimization_list(get_name=True), + oneutils.get_optimization_list())) parsed = config.read(opt_name_path_dic['O' + getattr(args, 'O')]) # check if given optimization option file exists if not parsed: @@ -151,11 +151,11 @@ def main(): _verify_arg(parser, args) # parse configuration file - config = _parse_cfg(args) + config = parse_cfg(args) # verify configuration file bin_dir = os.path.dirname(os.path.realpath(__file__)) - import_drivers_dict = _utils._detect_one_import_drivers(bin_dir) + import_drivers_dict = oneutils.detect_one_import_drivers(bin_dir) transform_drivers = [ 'one-optimize', 'one-quantize', 'one-pack', 'one-codegen', 'one-profile', 'one-partition' @@ -181,10 +181,10 @@ def main(): driver_name = _get_driver_name(section) driver_path = os.path.join(dir_path, driver_name) cmd = [driver_path, '--config', getattr(args, 'config'), '--section', section] - if section == 'one-optimize' and _utils._is_valid_attr(args, 'O'): + if section == 'one-optimize' and oneutils.is_valid_attr(args, 'O'): cmd += ['-O', getattr(args, 'O')] - _utils._run(cmd) + oneutils.run(cmd) if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-codegen b/compiler/one-cmds/one-codegen index 86e1632..5ccff0f 100644 --- a/compiler/one-cmds/one-codegen +++ b/compiler/one-cmds/one-codegen @@ -28,7 +28,7 @@ import os import sys import shutil -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 @@ -79,7 +79,7 @@ def _get_parser(backends_list): parser = argparse.ArgumentParser( description='command line tool for code generation', usage=codegen_usage) - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) # get backend list in the directory backends_name = [ntpath.basename(f) for f in backends_list] @@ -98,7 +98,7 @@ def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'backend'): + if not oneutils.is_valid_attr(args, 'backend'): missing.append('-b/--backend') if len(missing): parser.error('the following arguments are required: ' + ' '.join(missing)) @@ -113,6 +113,8 @@ def _parse_arg(parser): del argv[0] # split by '--' args = [list(y) for x, y in itertools.groupby(argv, lambda z: z == '--') if not x] + if len(args) == 0: + codegen_args = parser.parse_args(codegen_args) # one-codegen has two interfaces # 1. one-codegen [-h] [-v] [-C CONFIG] [-b BACKEND] [COMMANDS FOR BACKEND] if len(args) == 1: @@ -125,7 +127,7 @@ def _parse_arg(parser): codegen_args = parser.parse_args(codegen_args) # print version if len(args) and codegen_args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return codegen_args, backend_args, unknown_args @@ -139,7 +141,7 @@ def main(): args, backend_args, unknown_args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-codegen') + oneutils.parse_cfg(args.config, 'one-codegen', args) # verify arguments _verify_arg(parser, args) @@ -157,12 +159,12 @@ def main(): if not codegen_path: raise FileNotFoundError(backend_base + ' not found') codegen_cmd = [codegen_path] + backend_args + unknown_args - if _utils._is_valid_attr(args, 'command'): + if oneutils.is_valid_attr(args, 'command'): codegen_cmd += getattr(args, 'command').split() # run backend driver - _utils._run(codegen_cmd, err_prefix=backend_base) + oneutils.run(codegen_cmd, err_prefix=backend_base) if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-import b/compiler/one-cmds/one-import index efd60bb..880b8311d 100644 --- a/compiler/one-cmds/one-import +++ b/compiler/one-cmds/one-import @@ -24,7 +24,7 @@ import os import subprocess import sys -import utils as _utils +import onelib.utils as oneutils def _get_parser(): @@ -79,7 +79,7 @@ def _convert(args, unknown_args): dir_path = os.path.dirname(os.path.realpath(__file__)) # make cmd cmd = [sys.executable, os.path.join(dir_path, _get_driver_name(args.driver))] - if _utils._is_valid_attr(args, 'config'): + if oneutils.is_valid_attr(args, 'config'): cmd.append('--config') cmd.append(os.path.expanduser(args.config)) return_code = subprocess.call(cmd + unknown_args) @@ -100,4 +100,4 @@ def main(): if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-import-bcq b/compiler/one-cmds/one-import-bcq index c3ef0b2..fc0f75c 100644 --- a/compiler/one-cmds/one-import-bcq +++ b/compiler/one-cmds/one-import-bcq @@ -25,7 +25,7 @@ import sys import tempfile import onelib.make_cmd as _make_cmd -import utils as _utils +import onelib.utils as oneutils import generate_bcq_output_arrays as _bcq_info_gen # TODO Find better way to suppress trackback on error @@ -40,7 +40,7 @@ def _get_parser(): parser = argparse.ArgumentParser( description='command line tool to convert TensorFlow with BCQ to circle') - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) ## tf2tfliteV2 arguments tf2tfliteV2_group = parser.add_argument_group('converter arguments') @@ -94,9 +94,9 @@ def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'input_path'): + if not oneutils.is_valid_attr(args, 'input_path'): missing.append('-i/--input_path') - if not _utils._is_valid_attr(args, 'output_path'): + if not oneutils.is_valid_attr(args, 'output_path'): missing.append('-o/--output_path') if len(missing): parser.error('the following arguments are required: ' + ' '.join(missing)) @@ -106,7 +106,7 @@ def _parse_arg(parser): args = parser.parse_args() # print version if args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return args @@ -115,15 +115,15 @@ def _make_generate_bcq_metadata_cmd(args, driver_path, output_path): """make a command for running generate_bcq_metadata""" cmd = [sys.executable, driver_path] # input_path - if _utils._is_valid_attr(args, 'input_path'): + if oneutils.is_valid_attr(args, 'input_path'): cmd.append('--input_path') cmd.append(os.path.expanduser(getattr(args, 'input_path'))) # output_path - if _utils._is_valid_attr(args, 'output_path'): + if oneutils.is_valid_attr(args, 'output_path'): cmd.append('--output_path') cmd.append(os.path.expanduser(output_path)) # output_arrays - if _utils._is_valid_attr(args, 'output_arrays'): + if oneutils.is_valid_attr(args, 'output_arrays'): cmd.append('--output_arrays') cmd.append(getattr(args, 'output_arrays')) @@ -147,7 +147,7 @@ def _convert(args): f.write((' '.join(generate_bcq_metadata_cmd) + '\n').encode()) # generate BCQ information metadata - _utils._run(generate_bcq_metadata_cmd, logfile=f) + oneutils.run(generate_bcq_metadata_cmd, logfile=f) # get output_arrays with BCQ bcq_output_arrays = _bcq_info_gen.get_bcq_output_arrays( @@ -171,7 +171,7 @@ def _convert(args): f.write((' '.join(tf2tfliteV2_cmd) + '\n').encode()) # convert tf to tflite - _utils._run(tf2tfliteV2_cmd, logfile=f) + oneutils.run(tf2tfliteV2_cmd, logfile=f) # make a command to convert from tflite to circle tflite2circle_path = os.path.join(dir_path, 'tflite2circle') @@ -182,7 +182,7 @@ def _convert(args): f.write((' '.join(tflite2circle_cmd) + '\n').encode()) # convert tflite to circle - _utils._run(tflite2circle_cmd, logfile=f) + oneutils.run(tflite2circle_cmd, logfile=f) def main(): @@ -191,7 +191,7 @@ def main(): args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-import-bcq') + oneutils.parse_cfg(args.config, 'one-import-bcq', args) # verify arguments _verify_arg(parser, args) @@ -201,4 +201,4 @@ def main(): if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-import-onnx b/compiler/one-cmds/one-import-onnx index ad19c2f..24edea6 100644 --- a/compiler/one-cmds/one-import-onnx +++ b/compiler/one-cmds/one-import-onnx @@ -35,7 +35,7 @@ except ImportError: _onnx_legalizer_enabled = False import onelib.make_cmd as _make_cmd -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 @@ -49,7 +49,7 @@ def _get_parser(): parser = argparse.ArgumentParser( description='command line tool to convert ONNX to circle') - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) ## tf2tfliteV2 arguments tf2tfliteV2_group = parser.add_argument_group('converter arguments') @@ -105,9 +105,9 @@ def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'input_path'): + if not oneutils.is_valid_attr(args, 'input_path'): missing.append('-i/--input_path') - if not _utils._is_valid_attr(args, 'output_path'): + if not oneutils.is_valid_attr(args, 'output_path'): missing.append('-o/--output_path') if len(missing): parser.error('the following arguments are required: ' + ' '.join(missing)) @@ -117,7 +117,7 @@ def _parse_arg(parser): args = parser.parse_args() # print version if args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return args @@ -203,18 +203,18 @@ def _convert(args): with open(logfile_path, 'wb') as f, tempfile.TemporaryDirectory() as tmpdir: # save intermediate - if _utils._is_valid_attr(args, 'save_intermediate'): + if oneutils.is_valid_attr(args, 'save_intermediate'): tmpdir = os.path.dirname(logfile_path) # convert onnx to tf saved model onnx_model = onnx.load(getattr(args, 'input_path')) if _onnx_legalizer_enabled: options = onnx_legalizer.LegalizeOptions - options.unroll_rnn = _utils._is_valid_attr(args, 'unroll_rnn') - options.unroll_lstm = _utils._is_valid_attr(args, 'unroll_lstm') + options.unroll_rnn = oneutils.is_valid_attr(args, 'unroll_rnn') + options.unroll_lstm = oneutils.is_valid_attr(args, 'unroll_lstm') onnx_legalizer.legalize(onnx_model, options) - if _utils._is_valid_attr(args, 'keep_io_order'): + if oneutils.is_valid_attr(args, 'keep_io_order'): _remap_io_names(onnx_model) - if _utils._is_valid_attr(args, 'save_intermediate'): + if oneutils.is_valid_attr(args, 'save_intermediate'): basename = os.path.basename(getattr(args, 'input_path')) fixed_path = os.path.join(tmpdir, os.path.splitext(basename)[0] + '~.onnx') @@ -238,7 +238,7 @@ def _convert(args): f.write((' '.join(tf2tfliteV2_cmd) + '\n').encode()) # convert tf to tflite - _utils._run(tf2tfliteV2_cmd, logfile=f) + oneutils.run(tf2tfliteV2_cmd, logfile=f) # make a command to convert from tflite to circle tflite2circle_path = os.path.join(dir_path, 'tflite2circle') @@ -249,7 +249,7 @@ def _convert(args): f.write((' '.join(tflite2circle_cmd) + '\n').encode()) # convert tflite to circle - _utils._run(tflite2circle_cmd, err_prefix="tflite2circle", logfile=f) + oneutils.run(tflite2circle_cmd, err_prefix="tflite2circle", logfile=f) def main(): @@ -258,7 +258,7 @@ def main(): args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-import-onnx') + oneutils.parse_cfg(args.config, 'one-import-onnx', args) # verify arguments _verify_arg(parser, args) @@ -268,4 +268,4 @@ def main(): if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-import-pytorch b/compiler/one-cmds/one-import-pytorch index 7f39e61..3a61d22 100644 --- a/compiler/one-cmds/one-import-pytorch +++ b/compiler/one-cmds/one-import-pytorch @@ -33,21 +33,21 @@ import zipfile import onnx_legalizer import onelib.make_cmd as _make_cmd -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 def get_driver_spec(): - return ("one-import-pytorch", _utils.DriverType.IMPORTER) + return ("one-import-pytorch", oneutils.DriverType.IMPORTER) def _get_parser(): parser = argparse.ArgumentParser( description='command line tool to convert PyTorch to Circle') - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) ## converter arguments converter_group = parser.add_argument_group('converter arguments') @@ -96,13 +96,13 @@ def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'input_path'): + if not oneutils.is_valid_attr(args, 'input_path'): missing.append('-i/--input_path') - if not _utils._is_valid_attr(args, 'output_path'): + if not oneutils.is_valid_attr(args, 'output_path'): missing.append('-o/--output_path') - if not _utils._is_valid_attr(args, 'input_shapes'): + if not oneutils.is_valid_attr(args, 'input_shapes'): missing.append('-s/--input_shapes') - if not _utils._is_valid_attr(args, 'input_types'): + if not oneutils.is_valid_attr(args, 'input_types'): missing.append('-t/--input_types') if len(missing): @@ -113,7 +113,7 @@ def _parse_arg(parser): args = parser.parse_args() # print version if args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return args @@ -255,7 +255,7 @@ def _convert(args): logfile_path = os.path.realpath(args.output_path) + '.log' with open(logfile_path, 'wb') as f, tempfile.TemporaryDirectory() as tmpdir: # save intermediate - if _utils._is_valid_attr(args, 'save_intermediate'): + if oneutils.is_valid_attr(args, 'save_intermediate'): tmpdir = os.path.dirname(logfile_path) # convert pytorch to onnx model input_path = getattr(args, 'input_path') @@ -310,8 +310,8 @@ def _convert(args): onnx_model = onnx.load(onnx_output_path) options = onnx_legalizer.LegalizeOptions() - options.unroll_rnn = _utils._is_valid_attr(args, 'unroll_rnn') - options.unroll_lstm = _utils._is_valid_attr(args, 'unroll_lstm') + options.unroll_rnn = oneutils.is_valid_attr(args, 'unroll_rnn') + options.unroll_lstm = oneutils.is_valid_attr(args, 'unroll_lstm') onnx_legalizer.legalize(onnx_model, options) tf_savedmodel = onnx_tf.backend.prepare(onnx_model) @@ -334,7 +334,7 @@ def _convert(args): f.write((' '.join(tf2tfliteV2_cmd) + '\n').encode()) # convert tf to tflite - _utils._run(tf2tfliteV2_cmd, logfile=f) + oneutils.run(tf2tfliteV2_cmd, logfile=f) # make a command to convert from tflite to circle tflite2circle_path = os.path.join(dir_path, 'tflite2circle') @@ -345,7 +345,7 @@ def _convert(args): f.write((' '.join(tflite2circle_cmd) + '\n').encode()) # convert tflite to circle - _utils._run(tflite2circle_cmd, err_prefix="tflite2circle", logfile=f) + oneutils.run(tflite2circle_cmd, err_prefix="tflite2circle", logfile=f) def main(): @@ -354,7 +354,7 @@ def main(): args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-import-pytorch') + oneutils.parse_cfg(args.config, 'one-import-pytorch', args) # verify arguments _verify_arg(parser, args) @@ -364,4 +364,4 @@ def main(): if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-import-tf b/compiler/one-cmds/one-import-tf index 6623fa6..75d1968 100644 --- a/compiler/one-cmds/one-import-tf +++ b/compiler/one-cmds/one-import-tf @@ -24,7 +24,7 @@ import os import tempfile import onelib.make_cmd as _make_cmd -import utils as _utils +import onelib.utils as oneutils def get_driver_cfg_section(): @@ -35,7 +35,7 @@ def _get_parser(): parser = argparse.ArgumentParser( description='command line tool to convert TensorFlow to circle') - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) ## tf2tfliteV2 arguments tf2tfliteV2_group = parser.add_argument_group('converter arguments') @@ -111,6 +111,12 @@ def _get_parser(): action='store_true', help='Save intermediate files to output folder') + # experimental options + parser.add_argument( + '--experimental_disable_batchmatmul_unfold', + action='store_true', + help='Experimental disable BatchMatMul unfold') + return parser @@ -118,9 +124,9 @@ def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'input_path'): + if not oneutils.is_valid_attr(args, 'input_path'): missing.append('-i/--input_path') - if not _utils._is_valid_attr(args, 'output_path'): + if not oneutils.is_valid_attr(args, 'output_path'): missing.append('-o/--output_path') if len(missing): parser.error('the following arguments are required: ' + ' '.join(missing)) @@ -130,7 +136,7 @@ def _parse_arg(parser): args = parser.parse_args() # print version if args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return args @@ -142,7 +148,7 @@ def _convert(args): with open(logfile_path, 'wb') as f, tempfile.TemporaryDirectory() as tmpdir: # save intermediate - if _utils._is_valid_attr(args, 'save_intermediate'): + if oneutils.is_valid_attr(args, 'save_intermediate'): tmpdir = os.path.dirname(logfile_path) # make a command to convert from tf to tflite tf2tfliteV2_path = os.path.join(dir_path, 'tf2tfliteV2.py') @@ -156,7 +162,7 @@ def _convert(args): f.write((' '.join(tf2tfliteV2_cmd) + '\n').encode()) # convert tf to tflite - _utils._run(tf2tfliteV2_cmd, logfile=f) + oneutils.run(tf2tfliteV2_cmd, logfile=f) # make a command to convert from tflite to circle tflite2circle_path = os.path.join(dir_path, 'tflite2circle') @@ -167,7 +173,7 @@ def _convert(args): f.write((' '.join(tflite2circle_cmd) + '\n').encode()) # convert tflite to circle - _utils._run(tflite2circle_cmd, err_prefix="tflite2circle", logfile=f) + oneutils.run(tflite2circle_cmd, err_prefix="tflite2circle", logfile=f) def main(): @@ -176,7 +182,7 @@ def main(): args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-import-tf') + oneutils.parse_cfg(args.config, 'one-import-tf', args) # verify arguments _verify_arg(parser, args) @@ -186,4 +192,4 @@ def main(): if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-import-tflite b/compiler/one-cmds/one-import-tflite index 3d96b11..8eba46d 100644 --- a/compiler/one-cmds/one-import-tflite +++ b/compiler/one-cmds/one-import-tflite @@ -24,7 +24,7 @@ import os import sys import onelib.make_cmd as _make_cmd -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 @@ -38,7 +38,7 @@ def _get_parser(): parser = argparse.ArgumentParser( description='command line tool to convert TensorFlow lite to circle') - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) ## tflite2circle arguments tflite2circle_group = parser.add_argument_group('converter arguments') @@ -56,9 +56,9 @@ def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'input_path'): + if not oneutils.is_valid_attr(args, 'input_path'): missing.append('-i/--input_path') - if not _utils._is_valid_attr(args, 'output_path'): + if not oneutils.is_valid_attr(args, 'output_path'): missing.append('-o/--output_path') if len(missing): parser.error('the following arguments are required: ' + ' '.join(missing)) @@ -68,7 +68,7 @@ def _parse_arg(parser): args = parser.parse_args() # print version if args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return args @@ -88,7 +88,7 @@ def _convert(args): f.write((' '.join(tflite2circle_cmd) + '\n').encode()) # convert tflite to circle - _utils._run(tflite2circle_cmd, err_prefix="tflite2circle", logfile=f) + oneutils.run(tflite2circle_cmd, err_prefix="tflite2circle", logfile=f) def main(): @@ -97,7 +97,7 @@ def main(): args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-import-tflite') + oneutils.parse_cfg(args.config, 'one-import-tflite', args) # verify arguments _verify_arg(parser, args) @@ -107,4 +107,4 @@ def main(): if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-infer b/compiler/one-cmds/one-infer index c7fcd8a..125db3e 100644 --- a/compiler/one-cmds/one-infer +++ b/compiler/one-cmds/one-infer @@ -27,51 +27,12 @@ import ntpath import os import sys -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 -def _get_backends_list(): - """ - [one hierarchy] - one - ├── backends - ├── bin - ├── doc - ├── include - ├── lib - ├── optimization - └── test - - The list where `one-infer` finds its backends - - `bin` folder where `one-infer` exists - - `backends` folder - - NOTE If there are backends of the same name in different places, - the closer to the top in the list, the higher the priority. - """ - dir_path = os.path.dirname(os.path.realpath(__file__)) - backend_set = set() - - # bin folder - files = [f for f in glob.glob(dir_path + '/*-infer')] - # backends folder - files += [f for f in glob.glob(dir_path + '/../backends/**/*-infer', recursive=True)] - # TODO find backends in `$PATH` - - backends_list = [] - for cand in files: - base = ntpath.basename(cand) - if (not base in backend_set) and os.path.isfile(cand) and os.access( - cand, os.X_OK): - backend_set.add(base) - backends_list.append(cand) - - return backends_list - - def _search_backend_driver(driver): """ [one hierarchy] @@ -108,28 +69,25 @@ def _search_backend_driver(driver): return None -def _get_parser(backends_list): - infer_usage = 'one-infer [-h] [-v] [-C CONFIG] [-d DRIVER | -b BACKEND] [--post-process POST_PROCESS] [--] [COMMANDS FOR BACKEND DRIVER]' +def _get_parser(): + infer_usage = 'one-infer [-h] [-v] [-C CONFIG] [-d DRIVER] [--post-process POST_PROCESS] [--] [COMMANDS FOR BACKEND DRIVER]' + infer_detail = """ +one-infer provides post-processing after invoking backend inference driver +use python script and its arguments to '--post-process' argument as below +one-infer -d dummy-infer --post-process "script.py arg1 arg2" -- [arguments for dummy-infer] +""" parser = argparse.ArgumentParser( - description='command line tool to infer model', usage=infer_usage) + description='command line tool to infer model', + usage=infer_usage, + epilog=infer_detail, + formatter_class=argparse.RawTextHelpFormatter) - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) - # TODO: add tflite/onnx-infer driver to helper message when it is implemented driver_help_message = 'backend inference driver name to execute' parser.add_argument('-d', '--driver', type=str, help=driver_help_message) - # get backend list in the directory - backends_name = [ntpath.basename(f) for f in backends_list] - if not backends_name: - backends_name_message = '(There is no available backend drivers)' - else: - backends_name_message = '(available backend drivers: ' + ', '.join( - backends_name) + ')' - backend_help_message = 'backend name to use ' + backends_name_message - parser.add_argument('-b', '--backend', type=str, help=backend_help_message) - - post_process_help_message = 'post processing script to convert I/O data to standard format' + post_process_help_message = 'post processing python script and arguments which can be used to convert I/O data to standard format' parser.add_argument('--post-process', type=str, help=post_process_help_message) return parser @@ -137,15 +95,9 @@ def _get_parser(backends_list): def _verify_arg(parser, args): """verify given arguments""" - # `-d/--driver` and `-b/--backend` are mutually exclusive arguments. - if _utils._is_valid_attr(args, 'driver') and _utils._is_valid_attr(args, 'backend'): - parser.error( - '-d and -b options are mutually exclusive. Please use only one of them') - missing = [] - if not _utils._is_valid_attr(args, 'driver') and not _utils._is_valid_attr( - args, 'backend'): - missing.append('{-d/--driver | -b/--backend}') + if not oneutils.is_valid_attr(args, 'driver'): + missing.append('-d/--driver') if len(missing): parser.error('the following arguments are required: ' + ' '.join(missing)) @@ -159,66 +111,58 @@ def _parse_arg(parser): # split by '--' args = [list(y) for x, y in itertools.groupby(argv, lambda z: z == '--') if not x] - # one-infer [-h] [-v] [-C CONFIG] [-d DRIVER] [-b BACKEND] [--post-process POST_PROCESS] -- [COMMANDS FOR BACKEND DRIVER] + # one-infer [-h] [-v] [-C CONFIG] [-d DRIVER] [--post-process POST_PROCESS] -- [COMMANDS FOR BACKEND DRIVER] if len(args): infer_args = args[0] infer_args = parser.parse_args(infer_args) backend_args = backend_args if len(args) < 2 else args[1] + else: + infer_args = parser.parse_args(infer_args) # print version if len(args) and infer_args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return infer_args, backend_args -def _get_executable(args, backends_list): - driver = _utils._is_valid_attr(args, 'driver') - if driver: - executable = _search_backend_driver(driver) - if executable: - return executable - else: - raise FileNotFoundError(driver + ' not found') +def _get_executable(args): + driver = oneutils.is_valid_attr(args, 'driver') - if _utils._is_valid_attr(args, 'backend'): - backend_base = getattr(args, 'backend') + '-infer' - for cand in backends_list: - if ntpath.basename(cand) == backend_base: - return cand - raise FileNotFoundError(backend_base + ' not found') + executable = _search_backend_driver(driver) + if executable: + return executable + else: + raise FileNotFoundError(driver + ' not found') def main(): - # get backend list - backends_list = _get_backends_list() - # parse arguments - parser = _get_parser(backends_list) + parser = _get_parser() args, backend_args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-infer') + oneutils.parse_cfg(args.config, 'one-infer', args) # verify arguments _verify_arg(parser, args) # make a command to run given backend driver - driver_path = _get_executable(args, backends_list) + driver_path = _get_executable(args) infer_cmd = [driver_path] + backend_args - if _utils._is_valid_attr(args, 'command'): + if oneutils.is_valid_attr(args, 'command'): infer_cmd += getattr(args, 'command').split() # run backend driver - _utils._run(infer_cmd, err_prefix=ntpath.basename(driver_path)) + oneutils.run(infer_cmd, err_prefix=ntpath.basename(driver_path)) # run post process script if it's given - if _utils._is_valid_attr(args, 'post_process'): + if oneutils.is_valid_attr(args, 'post_process'): # NOTE: the given python script will be executed by venv of ONE python_path = sys.executable post_process_command = [python_path] + getattr(args, 'post_process').strip().split(' ') - _utils._run(post_process_command, err_prefix='one-infer') + oneutils.run(post_process_command, err_prefix='one-infer') if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-init b/compiler/one-cmds/one-init index 04c4534..299255c 100644 --- a/compiler/one-cmds/one-init +++ b/compiler/one-cmds/one-init @@ -28,12 +28,44 @@ import os import sys import configparser -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 +class InputOutputPath: + ''' + Class that remembers input circle file and output circle file of section k, + + After calling enter_new_section(), + output path in section k will be used as input path of section k+1 + ''' + + def __init__(self, initial_input_path: str): + self._first_step = True + self._input_path = initial_input_path + self._output_path = '' + + def enter_new_section(self, section_output_path: str): + ''' + Call this when starting a section + ''' + if self._first_step == True: + self._output_path = section_output_path + else: + self._input_path = self._output_path + self._output_path = section_output_path + + self._first_step = False + + def input_path(self): + return self._input_path + + def output_path(self): + return self._output_path + + class CommentableConfigParser(configparser.ConfigParser): """ ConfigParser where comment can be stored @@ -120,7 +152,7 @@ def _get_parser(backends_list): 'Currently tflite and onnx models are supported', usage=init_usage) - _utils._add_default_arg_no_CS(parser) + oneutils.add_default_arg_no_CS(parser) parser.add_argument( '-i', '--input_path', type=str, help='full filepath of the input model file') @@ -165,23 +197,23 @@ def _get_parser(backends_list): def _verify_arg(parser, args): # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'input_path'): + if not oneutils.is_valid_attr(args, 'input_path'): missing.append('-i/--input_path') - if not _utils._is_valid_attr(args, 'output_path'): + if not oneutils.is_valid_attr(args, 'output_path'): missing.append('-o/--output_path') - if not _utils._is_valid_attr(args, 'backend'): + if not oneutils.is_valid_attr(args, 'backend'): missing.append('-b/--backend') - if _utils._is_valid_attr(args, 'model_type'): + if oneutils.is_valid_attr(args, 'model_type'): # TODO Support model types other than onnx and tflite (e.g., TF) if getattr(args, 'model_type') not in ['onnx', 'tflite']: parser.error('Allowed value for --model_type: "onnx" or "tflite"') - if _utils._is_valid_attr(args, 'nchw_to_nhwc_input_shape'): - if not _utils._is_valid_attr(args, 'convert_nchw_to_nhwc'): + if oneutils.is_valid_attr(args, 'nchw_to_nhwc_input_shape'): + if not oneutils.is_valid_attr(args, 'convert_nchw_to_nhwc'): missing.append('--convert_nchw_to_nhwc') - if _utils._is_valid_attr(args, 'nchw_to_nhwc_output_shape'): - if not _utils._is_valid_attr(args, 'convert_nchw_to_nhwc'): + if oneutils.is_valid_attr(args, 'nchw_to_nhwc_output_shape'): + if not oneutils.is_valid_attr(args, 'convert_nchw_to_nhwc'): missing.append('--convert_nchw_to_nhwc') if len(missing): @@ -204,13 +236,13 @@ def _parse_arg(parser): backend_args = backend_args if len(args) < 2 else args[1] # print version if len(args) and init_args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return init_args, backend_args def _get_executable(args, backends_list): - if _utils._is_valid_attr(args, 'backend'): + if oneutils.is_valid_attr(args, 'backend'): backend_base = getattr(args, 'backend') + '-init' for cand in backends_list: if ntpath.basename(cand) == backend_base: @@ -219,24 +251,65 @@ def _get_executable(args, backends_list): # TODO Support workflow format (https://github.com/Samsung/ONE/pull/9354) -def _generate(): +def _generate(args, model_type: str, inout_path: InputOutputPath): # generate cfg file config = CommentableConfigParser() + model_dir = os.path.dirname(args.input_path) + model_name = os.path.basename(args.input_path).split('.')[0] + + def _assert_section(section: str): + if not config.has_section(section): + raise RuntimeError(f'Cannot find section: {section}') def _add_onecc_sections(): - pass # NYI + ''' + This adds all sections + ''' + config.add_section('onecc') + sections = [ + f'one-import-{model_type}', 'one-optimize', 'one-quantize', 'one-codegen' + ] + + for section in sections: + config['onecc'][section] = 'True' + # add empty section as a preperation of next procedure + config.add_section(section) def _gen_import(): - pass # NYI + section = f'one-import-{model_type}' + _assert_section(section) + + output_path = os.path.join(model_dir, f'{model_name}.circle') + inout_path.enter_new_section(section_output_path=output_path) + config[section]['input_path'] = inout_path.input_path() + config[section]['output_path'] = inout_path.output_path() def _gen_optimize(): - pass # NYI + section = 'one-optimize' + _assert_section(section) + + output_path = os.path.join(model_dir, f'{model_name}.opt.circle') + inout_path.enter_new_section(section_output_path=output_path) + config[section]['input_path'] = inout_path.input_path() + config[section]['output_path'] = inout_path.output_path() + + # TODO Add optimization optinos def _gen_quantize(): - pass # NYI + section = 'one-quantize' + _assert_section(section) + + output_path = os.path.join(model_dir, f'{model_name}.q.circle') + inout_path.enter_new_section(section_output_path=output_path) + config[section]['input_path'] = inout_path.input_path() + config[section]['output_path'] = inout_path.output_path() def _gen_codegen(): - pass # NYI + section = 'one-codegen' + _assert_section(section) + + # [backend]-init must provide default value for 'command' + config[section]['backend'] = args.backend # # NYI: one-profile, one-partition, one-pack, one-infer @@ -253,6 +326,26 @@ def _generate(): config.write(f) +def _get_model_type(parser, args): + if oneutils.is_valid_attr(args, 'model_type'): + return args.model_type + + if oneutils.is_valid_attr(args, 'input_path'): + _, ext = os.path.splitext(args.input_path) + + # ext would be, e.g., '.tflite' or '.onnx'. + # Note: when args.input_path does not have an extension, e.g., '/home/foo' + # ext after os.path.splitext() is '' and ''[1:] is still ''. + # TODO support tensorflow model + ext = ext[1:] + if ext in ["tflite", "onnx"]: + return ext + else: + parser.error(f'following file extensions are supported: ".onnx" ".tflite"') + + parser.error(f'the following argument is required: --input_path') + + def main(): # get backend list backends_list = _get_backends_list() @@ -264,17 +357,19 @@ def main(): # verify arguments _verify_arg(parser, args) + model_type = _get_model_type(parser, args) + inout_path = InputOutputPath(args.input_path) + _generate(args, model_type, inout_path) + # make a command to run given backend driver driver_path = _get_executable(args, backends_list) init_cmd = [driver_path] + backend_args # run backend driver - _utils._run(init_cmd, err_prefix=ntpath.basename(driver_path)) - - #TODO generate cfg file + oneutils.run(init_cmd, err_prefix=ntpath.basename(driver_path)) raise NotImplementedError("NYI") if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-optimize b/compiler/one-cmds/one-optimize index 481fc84..51668a8 100644 --- a/compiler/one-cmds/one-optimize +++ b/compiler/one-cmds/one-optimize @@ -25,7 +25,7 @@ import sys import onelib.constant as _constant import onelib.make_cmd as _make_cmd -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 @@ -35,7 +35,7 @@ def _get_parser(): parser = argparse.ArgumentParser( description='command line tool to optimize circle model') - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) ## utility arguments utility_group = parser.add_argument_group('arguments for utility') @@ -75,9 +75,9 @@ def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'input_path'): + if not oneutils.is_valid_attr(args, 'input_path'): missing.append('-i/--input_path') - if not _utils._is_valid_attr(args, 'output_path'): + if not oneutils.is_valid_attr(args, 'output_path'): missing.append('-o/--output_path') if len(missing): parser.error('the following arguments are required: ' + ' '.join(missing)) @@ -95,7 +95,7 @@ def _parse_arg(parser): args = parser.parse_args() # print version if args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return args @@ -113,34 +113,54 @@ def _optimize(args): getattr(args, 'output_path')) # verbose - if _utils._is_valid_attr(args, 'verbose'): + if oneutils.is_valid_attr(args, 'verbose'): circle2circle_cmd.append('--verbose') - if _utils._is_valid_attr(args, 'change_outputs'): + if oneutils.is_valid_attr(args, 'change_outputs'): circle2circle_cmd.append('--change_outputs') circle2circle_cmd.append(getattr(args, 'change_outputs')) f.write((' '.join(circle2circle_cmd) + '\n').encode()) # optimize - _utils._run(circle2circle_cmd, err_prefix="circle2circle", logfile=f) + oneutils.run(circle2circle_cmd, err_prefix="circle2circle", logfile=f) def _parse_opt(args): - if _utils._is_valid_attr(args, 'O'): + if oneutils.is_valid_attr(args, 'O'): opt_name_path_dic = dict( - zip(_utils._get_optimization_list(get_name=True), - _utils._get_optimization_list())) + zip(oneutils.get_optimization_list(get_name=True), + oneutils.get_optimization_list())) config_path = opt_name_path_dic['O' + getattr(args, 'O')] - _utils._parse_cfg_and_overwrite(config_path, 'one-optimize', args) + # group option do not overwrite existing args + oneutils.parse_cfg(config_path, 'one-optimize', args) +# There are several cases to receive the optimization options: +# - Indivisual option +# 1. From command line +# 2. From cfg file +# - Group option +# 3. From command line +# +# Their priority is as follows, since each option can be given simultaneously: +# 1. Indivisual option from command line +# 2. Indivisual option from cfg file +# 3. Group option from command line +# +# To follow their priority, options with higher priority should be parsed first. +# +# DO NOT MODIFY the order of below function calls. +# +# NOTE. Assume all the optimization options must follow 'store_true' only. +# NOTE. Group option from cfg file (`include` in `[onecc]` section) is passed +# as a command line argument. def main(): # parse arguments parser = _get_parser() args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-optimize') + oneutils.parse_cfg(args.config, 'one-optimize', args) # parse optimization file # NOTE if there is a `one-optimize` section in above configuration file as well, @@ -155,4 +175,4 @@ def main(): if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-pack b/compiler/one-cmds/one-pack index 5cab7c7..db42466 100644 --- a/compiler/one-cmds/one-pack +++ b/compiler/one-cmds/one-pack @@ -23,7 +23,7 @@ import argparse import os import sys -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 @@ -33,7 +33,7 @@ def _get_parser(): parser = argparse.ArgumentParser( description='command line tool to package circle and metadata into nnpackage') - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) ## model2nnpkg arguments model2nnpkg_group = parser.add_argument_group('arguments for packaging') @@ -51,9 +51,9 @@ def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'input_path'): + if not oneutils.is_valid_attr(args, 'input_path'): missing.append('-i/--input_path') - if not _utils._is_valid_attr(args, 'output_path'): + if not oneutils.is_valid_attr(args, 'output_path'): missing.append('-o/--output_path') if len(missing): parser.error('the following arguments are required: ' + ' '.join(missing)) @@ -63,7 +63,7 @@ def _parse_arg(parser): args = parser.parse_args() # print version if args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return args @@ -73,6 +73,7 @@ def _make_model2nnpkg_cmd(driver_path, input_path, output_path): cmd = [os.path.expanduser(driver_path)] cmd.append('-o') cmd.append(os.path.expanduser(output_path)) + cmd.append('-m') cmd.append(os.path.expanduser(input_path)) return cmd @@ -84,7 +85,7 @@ def _pack(args): with open(logfile_path, 'wb') as f: # make a command to package circle model and metadata into nnpackage - model2nnpkg_path = os.path.join(dir_path, 'model2nnpkg.sh') + model2nnpkg_path = os.path.join(dir_path, 'model2nnpkg') model2nnpkg_cmd = _make_model2nnpkg_cmd(model2nnpkg_path, getattr(args, 'input_path'), getattr(args, 'output_path')) @@ -92,7 +93,7 @@ def _pack(args): f.write((' '.join(model2nnpkg_cmd) + '\n').encode()) # convert tflite to circle - _utils._run(model2nnpkg_cmd, err_prefix="model2nnpkg.sh", logfile=f) + oneutils.run(model2nnpkg_cmd, err_prefix="model2nnpkg", logfile=f) def main(): @@ -101,7 +102,7 @@ def main(): args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-pack') + oneutils.parse_cfg(args.config, 'one-pack', args) # verify arguments _verify_arg(parser, args) @@ -111,4 +112,4 @@ def main(): if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-partition b/compiler/one-cmds/one-partition index c0d71e5..62ab13d 100644 --- a/compiler/one-cmds/one-partition +++ b/compiler/one-cmds/one-partition @@ -24,7 +24,7 @@ import configparser import os import sys -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 @@ -34,7 +34,7 @@ def _get_parser(): parser = argparse.ArgumentParser( description='command line tool to partition circle model by multiple backends') - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) parser.add_argument( '--backends', type=str, help='backends in CSV to use for partitioning') @@ -55,7 +55,7 @@ def _parse_arg(parser): args = parser.parse_args() # print version if args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return args @@ -64,9 +64,9 @@ def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'part_file'): + if not oneutils.is_valid_attr(args, 'part_file'): missing.append('part_file') - if not _utils._is_valid_attr(args, 'input_file'): + if not oneutils.is_valid_attr(args, 'input_file'): missing.append('input_file') if len(missing): parser.error('the following arguments are required: ' + ' '.join(missing)) @@ -86,13 +86,13 @@ def _partition(args): cmd = [os.path.expanduser(circle_partitioner_path)] - if _utils._is_valid_attr(args, 'backends'): + if oneutils.is_valid_attr(args, 'backends'): cmd.append('--backends') cmd.append(getattr(args, 'backends')) - if _utils._is_valid_attr(args, 'default'): + if oneutils.is_valid_attr(args, 'default'): cmd.append('--default') cmd.append(getattr(args, 'default')) - if _utils._is_valid_attr(args, 'work_path'): + if oneutils.is_valid_attr(args, 'work_path'): cmd.append('--work_path') cmd.append(getattr(args, 'work_path')) @@ -104,7 +104,7 @@ def _partition(args): f.write((' '.join(cmd) + '\n').encode()) # run circle-partitoner - _utils._run(cmd, err_prefix='circle-partitioner', logfile=f) + oneutils.run(cmd, err_prefix='circle-partitioner', logfile=f) def main(): @@ -113,11 +113,7 @@ def main(): args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-partition') - - if _utils._is_valid_attr(args, 'config'): - config_path = getattr(args, 'config') - _utils._parse_cfg_and_overwrite(config_path, 'one-partition', args) + oneutils.parse_cfg(args.config, 'one-partition', args) # verify arguments _verify_arg(parser, args) @@ -127,4 +123,4 @@ def main(): if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/one-prepare-venv b/compiler/one-cmds/one-prepare-venv index b435671..baa88be 100644 --- a/compiler/one-cmds/one-prepare-venv +++ b/compiler/one-cmds/one-prepare-venv @@ -37,12 +37,13 @@ VER_TENSORFLOW=2.8.0 VER_ONNX=1.11.0 VER_ONNXRUNTIME=1.11.0 VER_ONNX_TF=1.10.0 +VER_PYDOT=1.4.2 # Install tensorflow PIP_TRUSTED_HOST="--trusted-host pypi.org " PIP_TRUSTED_HOST+="--trusted-host pypi.python.org " -PIP_TRUSTED_HOST+="--trusted-host files.pythonhost.org " +PIP_TRUSTED_HOST+="--trusted-host files.pythonhosted.org " PIP_TRUSTED_HOST+="--trusted-host download.pytorch.org " PIP_TIMEOUT="--default-timeout=1000 " @@ -65,6 +66,8 @@ fi ${VENV_PYTHON} -m pip ${PIP_OPTIONS} install Pillow # TODO remove version fix, https://github.com/Samsung/ONE/issues/9240 ${VENV_PYTHON} -m pip ${PIP_OPTIONS} install tensorflow_probability==0.16.0 +# TODO remove version fix, https://github.com/Samsung/ONE/issues/10481 +${VENV_PYTHON} -m pip ${PIP_OPTIONS} install tensorflow_addons==0.16.1 # Install PyTorch and ONNX related # NOTE set ONE_PREPVENV_TORCH_STABLE to override 'torch_stable.html' URL. @@ -92,3 +95,6 @@ fi # NOTE refer https://github.com/protocolbuffers/protobuf/issues/10051 # TODO remove this when issue is resolved ${VENV_PYTHON} -m pip ${PIP_OPTIONS} install --upgrade protobuf==3.20.1 + +# Install pydot for visq +${VENV_PYTHON} -m pip ${PIP_OPTIONS} install pydot==${VER_PYDOT} diff --git a/compiler/one-cmds/one-prepare-venv.u2204 b/compiler/one-cmds/one-prepare-venv.u2204 new file mode 100644 index 0000000..d4b0467 --- /dev/null +++ b/compiler/one-cmds/one-prepare-venv.u2204 @@ -0,0 +1,97 @@ +#!/bin/bash + +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e + +DRIVER_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +VENV_ACTIVATE=${DRIVER_PATH}/venv/bin/activate +# NOTE please use venv's python instead of python after `source activation`. +# This script is called by debian maintainer script, i.e. `postinst`. +# Since debian maintainer script is called with sudo, `source activation` is ignored. +VENV_PYTHON=${DRIVER_PATH}/venv/bin/python + +if [ ! -f ${VENV_ACTIVATE} ]; then + # Create python virtual enviornment + python3 -m venv "${DRIVER_PATH}/venv" +fi + +# NOTE version +# - https://github.com/onnx/onnx/blob/master/docs/Versioning.md +# - https://github.com/onnx/onnx-tensorflow/blob/master/Versioning.md + +VER_TENSORFLOW=2.10.1 +VER_ONNX=1.12.0 +VER_ONNXRUNTIME=1.12.1 +VER_ONNX_TF=1.10.0 +VER_PYDOT=1.4.2 + +# Install tensorflow + +PIP_TRUSTED_HOST="--trusted-host pypi.org " +PIP_TRUSTED_HOST+="--trusted-host pypi.python.org " +PIP_TRUSTED_HOST+="--trusted-host files.pythonhosted.org " +PIP_TRUSTED_HOST+="--trusted-host download.pytorch.org " + +PIP_TIMEOUT="--default-timeout=1000 " + +PIP_OPTIONS="${PIP_TIMEOUT} ${PIP_TRUSTED_HOST}" + +# NOTE $ONE_PREPVENV_PIP_OPTION is to provide additional PIP options +# such as ceritificate file behind firewall +# ex) ONE_PREPVENV_PIP_OPTION="--cert SomePrivateCetificate.crt" ./one-prepare-venv +if [[ ! -z "$ONE_PREPVENV_PIP_OPTION" ]]; then + PIP_OPTIONS+=" ${ONE_PREPVENV_PIP_OPTION} " +fi + +${VENV_PYTHON} -m pip ${PIP_OPTIONS} install --upgrade pip setuptools +if [ -n "${EXT_TENSORFLOW_WHL}" ]; then + ${VENV_PYTHON} -m pip ${PIP_OPTIONS} install ${EXT_TENSORFLOW_WHL} +else + ${VENV_PYTHON} -m pip ${PIP_OPTIONS} install tensorflow-cpu==${VER_TENSORFLOW} +fi +${VENV_PYTHON} -m pip ${PIP_OPTIONS} install Pillow +${VENV_PYTHON} -m pip ${PIP_OPTIONS} install tensorflow_probability + +# Install PyTorch and ONNX related +# NOTE set ONE_PREPVENV_TORCH_STABLE to override 'torch_stable.html' URL. +# torch_stable.html points to download URL of torch wheel file(s) +# but sometimes the server gets unstable, especially from in-house CI. +TORCH_STABLE_URL="https://download.pytorch.org/whl/torch_stable.html" +if [[ ! -z "$ONE_PREPVENV_TORCH_STABLE" ]]; then + TORCH_STABLE_URL="${ONE_PREPVENV_TORCH_STABLE}" +fi +# TODO remove torch message +echo "Torch from '${ONE_PREPVENV_TORCH_STABLE}' -> '${TORCH_STABLE_URL}'" +${VENV_PYTHON} -m pip ${PIP_OPTIONS} install torch==1.13.1+cpu -f ${TORCH_STABLE_URL} + +${VENV_PYTHON} -m pip ${PIP_OPTIONS} install onnx==${VER_ONNX} + +${VENV_PYTHON} -m pip ${PIP_OPTIONS} install onnxruntime==${VER_ONNXRUNTIME} + +# Provide install of custom onnx-tf +if [ -n "${EXT_ONNX_TF_WHL}" ]; then + ${VENV_PYTHON} -m pip ${PIP_OPTIONS} install ${EXT_ONNX_TF_WHL} +else + ${VENV_PYTHON} -m pip ${PIP_OPTIONS} install onnx-tf==${VER_ONNX_TF} +fi + +# NOTE refer https://github.com/protocolbuffers/protobuf/issues/10051 +# TODO remove this when issue is resolved +${VENV_PYTHON} -m pip ${PIP_OPTIONS} install --upgrade protobuf==3.19.6 + +# Install pydot for visq +${VENV_PYTHON} -m pip ${PIP_OPTIONS} install pydot==${VER_PYDOT} diff --git a/compiler/one-cmds/one-profile b/compiler/one-cmds/one-profile index b19c215..18a0456 100644 --- a/compiler/one-cmds/one-profile +++ b/compiler/one-cmds/one-profile @@ -27,7 +27,7 @@ import ntpath import os import sys -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 @@ -77,7 +77,7 @@ def _get_parser(backends_list): parser = argparse.ArgumentParser( description='command line tool for profiling backend model', usage=profile_usage) - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) # get backend list in the directory backends_name = [ntpath.basename(f) for f in backends_list] @@ -96,7 +96,7 @@ def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'backend'): + if not oneutils.is_valid_attr(args, 'backend'): missing.append('-b/--backend') if len(missing): parser.error('the following arguments are required: ' + ' '.join(missing)) @@ -111,6 +111,8 @@ def _parse_arg(parser): del argv[0] # split by '--' args = [list(y) for x, y in itertools.groupby(argv, lambda z: z == '--') if not x] + if len(args) == 0: + profile_args = parser.parse_args(profile_args) # one-profile has two interfaces # 1. one-profile [-h] [-v] [-C CONFIG] [-b BACKEND] [COMMANDS FOR BACKEND] if len(args) == 1: @@ -123,7 +125,7 @@ def _parse_arg(parser): profile_args = parser.parse_args(profile_args) # print version if len(args) and profile_args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return profile_args, backend_args, unknown_args @@ -137,7 +139,7 @@ def main(): args, backend_args, unknown_args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-profile') + oneutils.parse_cfg(args.config, 'one-profile', args) # verify arguments _verify_arg(parser, args) @@ -151,11 +153,11 @@ def main(): if not profile_path: raise FileNotFoundError(backend_base + ' not found') profile_cmd = [profile_path] + backend_args + unknown_args - if _utils._is_valid_attr(args, 'command'): + if oneutils.is_valid_attr(args, 'command'): profile_cmd += getattr(args, 'command').split() # run backend driver - _utils._run(profile_cmd, err_prefix=backend_base) + oneutils.run(profile_cmd, err_prefix=backend_base) if __name__ == '__main__': diff --git a/compiler/one-cmds/one-quantize b/compiler/one-cmds/one-quantize index 9282007..060892d 100644 --- a/compiler/one-cmds/one-quantize +++ b/compiler/one-cmds/one-quantize @@ -25,8 +25,8 @@ import sys import tempfile import json -import utils as _utils -from utils import Command +import onelib.utils as oneutils +from onelib.Command import Command # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 @@ -36,7 +36,7 @@ def _get_parser(): parser = argparse.ArgumentParser( description='command line tool to quantize circle model') - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) # input and output path. parser.add_argument( @@ -221,12 +221,13 @@ def _get_parser(): def _set_default_values(args): - if not _utils._is_valid_attr(args, 'input_model_dtype') and not _utils._is_valid_attr( - args, 'input_dtype'): + if not oneutils.is_valid_attr(args, + 'input_model_dtype') and not oneutils.is_valid_attr( + args, 'input_dtype'): setattr(args, 'input_model_dtype', 'float32') - if not _utils._is_valid_attr(args, 'quantized_dtype'): + if not oneutils.is_valid_attr(args, 'quantized_dtype'): setattr(args, 'quantized_dtype', 'uint8') - if _utils._is_valid_attr(args, 'quant_config'): + if oneutils.is_valid_attr(args, 'quant_config'): # Get quantized_dtype from qconfig file try: with open(getattr(args, 'quant_config')) as f: @@ -237,9 +238,9 @@ def _set_default_values(args): except json.decoder.JSONDecodeError: print('Failed to decode ' + getattr(args, 'quant_config') + '. Please check it is a json file.') - if not _utils._is_valid_attr(args, 'granularity'): + if not oneutils.is_valid_attr(args, 'granularity'): setattr(args, 'granularity', 'layer') - if _utils._is_valid_attr(args, 'quant_config'): + if oneutils.is_valid_attr(args, 'quant_config'): # Get granularity from qconfig file try: with open(getattr(args, 'quant_config')) as f: @@ -249,11 +250,11 @@ def _set_default_values(args): except json.decoder.JSONDecodeError: print('Failed to decode ' + getattr(args, 'quant_config') + '. Please check it is a json file.') - if not _utils._is_valid_attr(args, 'mode'): + if not oneutils.is_valid_attr(args, 'mode'): setattr(args, 'mode', 'percentile') - if not _utils._is_valid_attr(args, 'min_percentile'): + if not oneutils.is_valid_attr(args, 'min_percentile'): setattr(args, 'min_percentile', '1.0') - if not _utils._is_valid_attr(args, 'max_percentile'): + if not oneutils.is_valid_attr(args, 'max_percentile'): setattr(args, 'max_percentile', '99.0') @@ -261,32 +262,32 @@ def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given missing = [] - if not _utils._is_valid_attr(args, 'input_path'): + if not oneutils.is_valid_attr(args, 'input_path'): missing.append('-i/--input_path') - if not _utils._is_valid_attr(args, 'output_path'): + if not oneutils.is_valid_attr(args, 'output_path'): missing.append('-o/--output_path') - if _utils._is_valid_attr(args, 'force_quantparam'): - if not _utils._is_valid_attr(args, 'tensor_name'): + if oneutils.is_valid_attr(args, 'force_quantparam'): + if not oneutils.is_valid_attr(args, 'tensor_name'): missing.append('--tensor_name') - if not _utils._is_valid_attr(args, 'scale'): + if not oneutils.is_valid_attr(args, 'scale'): missing.append('--scale') - if not _utils._is_valid_attr(args, 'zero_point'): + if not oneutils.is_valid_attr(args, 'zero_point'): missing.append('--zero_point') - if _utils._is_valid_attr(args, 'copy_quantparam'): - if not _utils._is_valid_attr(args, 'src_tensor_name'): + if oneutils.is_valid_attr(args, 'copy_quantparam'): + if not oneutils.is_valid_attr(args, 'src_tensor_name'): missing.append('--src_tensor_name') - if not _utils._is_valid_attr(args, 'dst_tensor_name'): + if not oneutils.is_valid_attr(args, 'dst_tensor_name'): missing.append('--dst_tensor_name') if len(missing): parser.error('the following arguments are required: ' + ' '.join(missing)) - if _utils._is_valid_attr(args, 'force_quantparam'): + if oneutils.is_valid_attr(args, 'force_quantparam'): tensors = getattr(args, 'tensor_name') scales = getattr(args, 'scale') zerops = getattr(args, 'zero_point') if len(tensors) != len(scales) or len(tensors) != len(zerops): parser.error( 'The same number of tensor_name, scale, and zero_point should be given.') - if _utils._is_valid_attr(args, 'copy_quantparam'): + if oneutils.is_valid_attr(args, 'copy_quantparam'): src_tensors = getattr(args, 'src_tensor_name') dst_tensors = getattr(args, 'dst_tensor_name') if len(src_tensors) != len(dst_tensors): @@ -298,23 +299,23 @@ def _parse_arg(parser): args = parser.parse_args() # print version if args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return args def _quantize(args): - if _utils._is_valid_attr(args, 'force_quantparam'): + if oneutils.is_valid_attr(args, 'force_quantparam'): # write quantization parameters _write_qparam(args) return - if _utils._is_valid_attr(args, 'copy_quantparam'): + if oneutils.is_valid_attr(args, 'copy_quantparam'): # copy quantization parameters _copy_qparam(args) return - if _utils._is_valid_attr(args, 'fake_quantize'): + if oneutils.is_valid_attr(args, 'fake_quantize'): # fake-quantize model _fake_quantize(args) return @@ -324,7 +325,7 @@ def _quantize(args): logfile_path = os.path.realpath(args.output_path) + '.log' with open(logfile_path, 'wb') as f, tempfile.TemporaryDirectory() as tmpdir: - if _utils._is_valid_attr(args, 'save_intermediate'): + if oneutils.is_valid_attr(args, 'save_intermediate'): tmpdir = os.path.dirname(logfile_path) # get driver path circle_quantizer_path = os.path.join(dir_path, 'circle-quantizer') @@ -333,26 +334,26 @@ def _quantize(args): ## make a command to quantize and dequantize the weights of the model circle_quantizer_cmd = [circle_quantizer_path] # verbose - if _utils._is_valid_attr(args, 'verbose'): + if oneutils.is_valid_attr(args, 'verbose'): circle_quantizer_cmd.append('--verbose') # quantize_dequantize_weights circle_quantizer_cmd.append('--quantize_dequantize_weights') # Use input_model_dtype if it exists. Use input_dtype otherwise. - if _utils._is_valid_attr(args, 'input_model_dtype'): + if oneutils.is_valid_attr(args, 'input_model_dtype'): circle_quantizer_cmd.append(getattr(args, 'input_model_dtype')) - elif _utils._is_valid_attr(args, 'input_dtype'): + elif oneutils.is_valid_attr(args, 'input_dtype'): circle_quantizer_cmd.append(getattr(args, 'input_dtype')) - if _utils._is_valid_attr(args, 'quantized_dtype'): + if oneutils.is_valid_attr(args, 'quantized_dtype'): circle_quantizer_cmd.append(getattr(args, 'quantized_dtype')) - if _utils._is_valid_attr(args, 'granularity'): + if oneutils.is_valid_attr(args, 'granularity'): circle_quantizer_cmd.append(getattr(args, 'granularity')) - if _utils._is_valid_attr(args, 'quant_config'): + if oneutils.is_valid_attr(args, 'quant_config'): # NOTE --config conflicts with --config option in onecc, so # we use quant_config for one-quantize circle_quantizer_cmd.append('--config') circle_quantizer_cmd.append(getattr(args, 'quant_config')) # input and output path - if _utils._is_valid_attr(args, 'input_path'): + if oneutils.is_valid_attr(args, 'input_path'): circle_quantizer_cmd.append(getattr(args, 'input_path')) tmp_weights_fake_quant_path = os.path.join( tmpdir, @@ -360,13 +361,13 @@ def _quantize(args): args.input_path))[0]) + '.weights_fake_quant.circle' circle_quantizer_cmd.append(tmp_weights_fake_quant_path) # profiling - if _utils._is_valid_attr(args, 'generate_profile_data'): + if oneutils.is_valid_attr(args, 'generate_profile_data'): circle_quantizer_cmd.append('--generate_profile_data') f.write((' '.join(circle_quantizer_cmd) + '\n').encode()) # run circle-quantizer - _utils._run(circle_quantizer_cmd, err_prefix="circle_quantizer", logfile=f) + oneutils.run(circle_quantizer_cmd, err_prefix="circle_quantizer", logfile=f) tmp_minmax_recorded_path = os.path.join( tmpdir, @@ -389,50 +390,50 @@ def _quantize(args): ## make a second command to quantize the model using the embedded information circle_quantizer_cmd = [circle_quantizer_path] # verbose - if _utils._is_valid_attr(args, 'verbose'): + if oneutils.is_valid_attr(args, 'verbose'): circle_quantizer_cmd.append('--verbose') # quantize_dequantize_weights circle_quantizer_cmd.append('--quantize_with_minmax') # Use input_model_dtype if it exists. Use input_dtype otherwise. - if _utils._is_valid_attr(args, 'input_model_dtype'): + if oneutils.is_valid_attr(args, 'input_model_dtype'): circle_quantizer_cmd.append(getattr(args, 'input_model_dtype')) - elif _utils._is_valid_attr(args, 'input_dtype'): + elif oneutils.is_valid_attr(args, 'input_dtype'): circle_quantizer_cmd.append(getattr(args, 'input_dtype')) - if _utils._is_valid_attr(args, 'quantized_dtype'): + if oneutils.is_valid_attr(args, 'quantized_dtype'): circle_quantizer_cmd.append(getattr(args, 'quantized_dtype')) - if _utils._is_valid_attr(args, 'granularity'): + if oneutils.is_valid_attr(args, 'granularity'): circle_quantizer_cmd.append(getattr(args, 'granularity')) - if _utils._is_valid_attr(args, 'TF-style_maxpool'): + if oneutils.is_valid_attr(args, 'TF-style_maxpool'): circle_quantizer_cmd.append('--TF-style_maxpool') - if _utils._is_valid_attr(args, 'input_type'): + if oneutils.is_valid_attr(args, 'input_type'): circle_quantizer_cmd.append('--input_type') circle_quantizer_cmd.append(getattr(args, 'input_type')) - if _utils._is_valid_attr(args, 'output_type'): + if oneutils.is_valid_attr(args, 'output_type'): circle_quantizer_cmd.append('--output_type') circle_quantizer_cmd.append(getattr(args, 'output_type')) - if _utils._is_valid_attr(args, 'quant_config'): + if oneutils.is_valid_attr(args, 'quant_config'): # NOTE --config conflicts with --config option in onecc, so # we use quant_config for one-quantize circle_quantizer_cmd.append('--config') circle_quantizer_cmd.append(getattr(args, 'quant_config')) # input and output path circle_quantizer_cmd.append(tmp_minmax_recorded_path) - if _utils._is_valid_attr(args, 'output_path'): + if oneutils.is_valid_attr(args, 'output_path'): circle_quantizer_cmd.append(getattr(args, 'output_path')) # profiling - if _utils._is_valid_attr(args, 'generate_profile_data'): + if oneutils.is_valid_attr(args, 'generate_profile_data'): circle_quantizer_cmd.append('--generate_profile_data') f.write((' '.join(circle_quantizer_cmd) + '\n').encode()) # run circle-quantizer - _utils._run(circle_quantizer_cmd, err_prefix="circle_quantizer", logfile=f) + oneutils.run(circle_quantizer_cmd, err_prefix="circle_quantizer", logfile=f) # evaluate - if _utils._is_valid_attr(args, 'evaluate_result'): + if oneutils.is_valid_attr(args, 'evaluate_result'): circle_eval_diff_path = os.path.join(dir_path, 'circle-eval-diff') quant_model = "" - if _utils._is_valid_attr(args, 'output_path'): + if oneutils.is_valid_attr(args, 'output_path'): quant_model = getattr(args, 'output_path') tmp_fake_quant_model = os.path.join( tmpdir, @@ -473,13 +474,13 @@ def _write_qparam(args): # make a command to write qparams to the tensors circle_quantizer_cmd = [circle_quantizer_path] # verbose - if _utils._is_valid_attr(args, 'verbose'): + if oneutils.is_valid_attr(args, 'verbose'): circle_quantizer_cmd.append('--verbose') - if _utils._is_valid_attr(args, 'tensor_name'): + if oneutils.is_valid_attr(args, 'tensor_name'): tensor_name = getattr(args, 'tensor_name') - if _utils._is_valid_attr(args, 'scale'): + if oneutils.is_valid_attr(args, 'scale'): scale = getattr(args, 'scale') - if _utils._is_valid_attr(args, 'zero_point'): + if oneutils.is_valid_attr(args, 'zero_point'): zero_point = getattr(args, 'zero_point') for (t, s, zp) in zip(tensor_name, scale, zero_point): circle_quantizer_cmd.append('--force_quantparam') @@ -487,15 +488,15 @@ def _write_qparam(args): circle_quantizer_cmd.append(str(s)) circle_quantizer_cmd.append(str(zp)) # input and output path - if _utils._is_valid_attr(args, 'input_path'): + if oneutils.is_valid_attr(args, 'input_path'): circle_quantizer_cmd.append(getattr(args, 'input_path')) - if _utils._is_valid_attr(args, 'output_path'): + if oneutils.is_valid_attr(args, 'output_path'): circle_quantizer_cmd.append(getattr(args, 'output_path')) f.write((' '.join(circle_quantizer_cmd) + '\n').encode()) # run circle-quantizer - _utils._run(circle_quantizer_cmd, err_prefix="circle_quantizer", logfile=f) + oneutils.run(circle_quantizer_cmd, err_prefix="circle_quantizer", logfile=f) def _copy_qparam(args): @@ -510,26 +511,26 @@ def _copy_qparam(args): # make a command to write qparams to the tensors circle_quantizer_cmd = [circle_quantizer_path] # verbose - if _utils._is_valid_attr(args, 'verbose'): + if oneutils.is_valid_attr(args, 'verbose'): circle_quantizer_cmd.append('--verbose') - if _utils._is_valid_attr(args, 'src_tensor_name'): + if oneutils.is_valid_attr(args, 'src_tensor_name'): src_tensor_name = getattr(args, 'src_tensor_name') - if _utils._is_valid_attr(args, 'dst_tensor_name'): + if oneutils.is_valid_attr(args, 'dst_tensor_name'): dst_tensor_name = getattr(args, 'dst_tensor_name') for (src, dst) in zip(src_tensor_name, dst_tensor_name): circle_quantizer_cmd.append('--copy_quantparam') circle_quantizer_cmd.append(src) circle_quantizer_cmd.append(dst) # input and output path - if _utils._is_valid_attr(args, 'input_path'): + if oneutils.is_valid_attr(args, 'input_path'): circle_quantizer_cmd.append(getattr(args, 'input_path')) - if _utils._is_valid_attr(args, 'output_path'): + if oneutils.is_valid_attr(args, 'output_path'): circle_quantizer_cmd.append(getattr(args, 'output_path')) f.write((' '.join(circle_quantizer_cmd) + '\n').encode()) # run circle-quantizer - _utils._run(circle_quantizer_cmd, err_prefix="circle_quantizer", logfile=f) + oneutils.run(circle_quantizer_cmd, err_prefix="circle_quantizer", logfile=f) def _fake_quantize(args): @@ -556,7 +557,7 @@ def main(): args = _parse_arg(parser) # parse configuration file - _utils._parse_cfg(args, 'one-quantize') + oneutils.parse_cfg(args.config, 'one-quantize', args) # set default values _set_default_values(args) @@ -569,4 +570,4 @@ def main(): if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/onecc b/compiler/one-cmds/onecc index a5ba636..4cc7740 100644 --- a/compiler/one-cmds/onecc +++ b/compiler/one-cmds/onecc @@ -27,7 +27,7 @@ import sys from onelib.CfgRunner import CfgRunner from onelib.WorkflowRunner import WorkflowRunner -import utils as _utils +import onelib.utils as oneutils # TODO Find better way to suppress trackback on error sys.tracebacklimit = 0 @@ -53,7 +53,7 @@ def _call_driver(driver_name, options): dir_path = os.path.dirname(os.path.realpath(__file__)) driver_path = os.path.join(dir_path, driver_name) cmd = [driver_path] + options - _utils._run(cmd) + oneutils.run(cmd) def _check_subtool_exists(): @@ -71,9 +71,9 @@ def _get_parser(): onecc_desc = 'Run ONE driver via several commands or configuration file' parser = argparse.ArgumentParser(description=onecc_desc, usage=onecc_usage) - _utils._add_default_arg(parser) + oneutils.add_default_arg(parser) - opt_name_list = _utils._get_optimization_list(get_name=True) + opt_name_list = oneutils.get_optimization_list(get_name=True) opt_name_list = ['-' + s for s in opt_name_list] if not opt_name_list: opt_help_message = '(No available optimization options)' @@ -106,7 +106,7 @@ def _parse_arg(parser): args = parser.parse_args() # print version if args.version: - _utils._print_version_and_exit(__file__) + oneutils.print_version_and_exit(__file__) return args @@ -114,13 +114,13 @@ def _parse_arg(parser): def _verify_arg(parser, args): """verify given arguments""" # check if required arguments is given - if not _utils._is_valid_attr(args, 'config') and not _utils._is_valid_attr( + if not oneutils.is_valid_attr(args, 'config') and not oneutils.is_valid_attr( args, 'workflow'): parser.error('-C/--config or -W/--workflow argument is required') # check if given optimization option exists - opt_name_list = _utils._get_optimization_list(get_name=True) - opt_name_list = [_utils._remove_prefix(s, 'O') for s in opt_name_list] - if _utils._is_valid_attr(args, 'O'): + opt_name_list = oneutils.get_optimization_list(get_name=True) + opt_name_list = [oneutils.remove_prefix(s, 'O') for s in opt_name_list] + if oneutils.is_valid_attr(args, 'O'): if ' ' in getattr(args, 'O'): parser.error('Not allowed to have space in the optimization name') if not getattr(args, 'O') in opt_name_list: @@ -147,16 +147,16 @@ def main(): _verify_arg(parser, args) bin_dir = os.path.dirname(os.path.realpath(__file__)) - if _utils._is_valid_attr(args, 'config'): + if oneutils.is_valid_attr(args, 'config'): runner = CfgRunner(args.config) runner.detect_import_drivers(bin_dir) - if _utils._is_valid_attr(args, 'O'): + if oneutils.is_valid_attr(args, 'O'): runner.add_opt(getattr(args, 'O')) runner.run(bin_dir) - elif _utils._is_valid_attr(args, 'workflow'): + elif oneutils.is_valid_attr(args, 'workflow'): runner = WorkflowRunner(args.workflow) runner.run(bin_dir) if __name__ == '__main__': - _utils._safemain(main, __file__) + oneutils.safemain(main, __file__) diff --git a/compiler/one-cmds/onecc.template.cfg b/compiler/one-cmds/onecc.template.cfg index 6f6a4e2..c9968b4 100644 --- a/compiler/one-cmds/onecc.template.cfg +++ b/compiler/one-cmds/onecc.template.cfg @@ -1,3 +1,7 @@ +; set environment variables +[Environment] +ONECC_ENV="ONECC" + ; To activate a step (or task), ; set True for the step in [onecc] section and fill options in the corresponding section [onecc] @@ -20,6 +24,10 @@ one-codegen=False one-profile=False ; infer one-infer=False +; group option +; multiple group options are allowed +include=O1 +# include=O1 O2 OMY_OPT [one-import-tf] # mandatory diff --git a/compiler/one-cmds/onelib/CfgRunner.py b/compiler/one-cmds/onelib/CfgRunner.py index c66e5b4..fe1c41a 100644 --- a/compiler/one-cmds/onelib/CfgRunner.py +++ b/compiler/one-cmds/onelib/CfgRunner.py @@ -18,7 +18,7 @@ import configparser import os import warnings -import utils as oneutils +import onelib.utils as oneutils def _simple_warning(message, category, filename, lineno, file=None, line=None): @@ -46,6 +46,15 @@ class CfgRunner: self.import_drivers = [ 'one-import-bcq', 'one-import-onnx', 'one-import-tf', 'one-import-tflite' ] + # parse group option + GROUP_OPTION_KEY = 'include' + if self.cfgparser.has_option('onecc', GROUP_OPTION_KEY): + groups = self.cfgparser['onecc'][GROUP_OPTION_KEY].split() + for o in groups: + if o == 'O' or not o.startswith('O'): + raise ValueError('Invalid group option') + # add_opt receives group name except first 'O' + self.add_opt(o[1:]) def _verify_cfg(self, cfgparser): if not cfgparser.has_section('onecc'): @@ -67,8 +76,8 @@ class CfgRunner: # make option names case sensitive self.optparser.optionxform = str opt_book = dict( - zip(oneutils._get_optimization_list(get_name=True), - oneutils._get_optimization_list())) + zip(oneutils.get_optimization_list(get_name=True), + oneutils.get_optimization_list())) parsed = self.optparser.read(opt_book['O' + opt]) if not parsed: raise FileNotFoundError('Not found given optimization configuration file') @@ -80,9 +89,15 @@ class CfgRunner: self.opt = opt def detect_import_drivers(self, dir): - self.import_drivers = list(oneutils._detect_one_import_drivers(dir).keys()) + self.import_drivers = list(oneutils.detect_one_import_drivers(dir).keys()) def run(self, working_dir, verbose=False): + # set environment + CFG_ENV_SECTION = 'Environment' + if self.cfgparser.has_section(CFG_ENV_SECTION): + for key in self.cfgparser[CFG_ENV_SECTION]: + os.environ[key] = self.cfgparser[CFG_ENV_SECTION][key] + section_to_run = [] for d in self.import_drivers + self.driver_sequence: if self._is_available(d): @@ -96,4 +111,4 @@ class CfgRunner: options.append('--verbose') driver_path = os.path.join(working_dir, section) cmd = [driver_path] + options - oneutils._run(cmd) + oneutils.run(cmd) diff --git a/compiler/one-cmds/onelib/Command.py b/compiler/one-cmds/onelib/Command.py new file mode 100644 index 0000000..35a9567 --- /dev/null +++ b/compiler/one-cmds/onelib/Command.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +# Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import onelib.utils as oneutils + + +class Command: + def __init__(self, driver, args, log_file): + self.cmd = [driver] + self.driver = driver + self.args = args + self.log_file = log_file + + # Add option if attrs are valid + # Option values are collected from self.args + def add_option_with_valid_args(self, option, attrs): + for attr in attrs: + if not oneutils.is_valid_attr(self.args, attr): + return self + self.cmd.append(option) + for attr in attrs: + self.cmd.append(getattr(self.args, attr)) + return self + + # Add option and values without any condition + def add_option_with_values(self, option, values): + self.cmd.append(option) + for value in values: + self.cmd.append(value) + return self + + # Add option with no argument (ex: --verbose) if attr is valid + def add_noarg_option_if_valid_arg(self, option, attr): + if oneutils.is_valid_attr(self.args, attr): + self.cmd.append(option) + return self + + # Run cmd and save logs + def run(self): + self.log_file.write((' '.join(self.cmd) + '\n').encode()) + oneutils.run(self.cmd, err_prefix=self.driver, logfile=self.log_file) diff --git a/compiler/one-cmds/onelib/WorkflowRunner.py b/compiler/one-cmds/onelib/WorkflowRunner.py index 0482dd9..52bd253 100644 --- a/compiler/one-cmds/onelib/WorkflowRunner.py +++ b/compiler/one-cmds/onelib/WorkflowRunner.py @@ -20,7 +20,7 @@ import os from onelib.OptionBuilder import OptionBuilder from onelib.TopologicalSortHelper import TopologicalSortHelper from onelib.CfgRunner import CfgRunner -import utils as oneutils +import onelib.utils as oneutils class WorkflowRunner: @@ -124,7 +124,7 @@ class WorkflowRunner: # get the absolute path of the caller driver_path = os.path.join(working_dir, driver_name) cmd = [driver_path] + options - oneutils._run(cmd) + oneutils.run(cmd) elif self.CFG_REFERENCE_K in workflow: cfg_path = workflow[self.CFG_REFERENCE_K]['path'] runner = CfgRunner(cfg_path) diff --git a/compiler/one-cmds/onelib/constant.py b/compiler/one-cmds/onelib/constant.py index 7dd79b6..0f338fc 100644 --- a/compiler/one-cmds/onelib/constant.py +++ b/compiler/one-cmds/onelib/constant.py @@ -17,6 +17,60 @@ class CONSTANT: __slots__ = () # This prevents access via __dict__. + + # Basic optimization passes + # These passes do not change the execution result of the model + O1 = ( + # Constant folding + 'fold_add_v2', + 'fold_cast', + 'fold_densify', + 'fold_dequantize', + 'fold_dwconv', + 'fold_fully_connected', + 'fold_gather', + 'fold_sparse_to_dense', + + # Operator fusion + 'fuse_add_with_tconv', + 'fuse_add_with_fully_connected', + 'fuse_batchnorm_with_conv', + 'fuse_batchnorm_with_dwconv', + 'fuse_batchnorm_with_tconv', + 'fuse_activation_function', + 'fuse_instnorm', + 'fuse_prelu', + 'fuse_mean_with_mean', + 'fuse_transpose_with_mean', + 'transform_min_max_to_relu6', + 'transform_min_relu_to_relu6', + + # Remove redundant operators + 'remove_redundant_reshape', + 'remove_redundant_transpose', + 'remove_unnecessary_reshape', + 'remove_unnecessary_slice', + 'remove_unnecessary_strided_slice', + 'remove_unnecessary_split', + + # Canonicalization + # (passes to help further optimization) + 'resolve_customop_add', + 'resolve_customop_batchmatmul', + 'resolve_customop_matmul', + 'resolve_customop_max_pool_with_argmax', + 'resolve_customop_splitv', + 'substitute_pack_to_reshape', + 'substitute_padv2_to_pad', + 'substitute_splitv_to_split', + 'substitute_squeeze_to_reshape', + 'substitute_strided_slice_to_reshape', + 'substitute_transpose_to_reshape', + 'forward_reshape_to_unaryop', + 'forward_transpose_op', + 'replace_non_const_fc_with_batch_matmul', # For quantization + ) + OPTIMIZATION_OPTS = ( # (OPTION_NAME, HELP_MESSAGE) ('convert_nchw_to_nhwc', @@ -32,9 +86,11 @@ class CONSTANT: ('fold_densify', 'fold Densify op with sparse constant input'), ('fold_dequantize', 'fold Dequantize op'), ('fold_dwconv', 'fold Depthwise Convolution op with constant inputs'), + ('fold_fully_connected', 'fold FullyConnected op with constant inputs'), ('fold_gather', 'fold Gather op'), ('fold_sparse_to_dense', 'fold SparseToDense op'), ('forward_reshape_to_unaryop', 'Forward Reshape op'), + ('forward_transpose_op', 'Forward Transpose op'), ('fuse_add_with_tconv', 'fuse Add op to Transposed'), ('fuse_add_with_fully_connected', 'fuse Add op to FullyConnected op'), ('fuse_batchnorm_with_conv', 'fuse BatchNorm op to Convolution op'), @@ -52,6 +108,7 @@ class CONSTANT: ' So, use it only when the impact is known to be acceptable.'), ('fuse_activation_function', 'fuse Activation function to a preceding operator'), ('fuse_instnorm', 'fuse ops to InstanceNorm operator'), + ('fuse_prelu', 'fuse ops to PReLU operator'), ('replace_cw_mul_add_with_depthwise_conv', 'replace channel-wise Mul/Add with DepthwiseConv2D'), ('remove_fakequant', 'remove FakeQuant ops'), @@ -85,7 +142,8 @@ class CONSTANT: ('substitute_transpose_to_reshape', 'convert certain condition Transpose to Reshape'), ('transform_min_max_to_relu6', 'transform Minimum-Maximum pattern to Relu6 op'), - ('transform_min_relu_to_relu6', 'transform Minimum(6)-Relu pattern to Relu6 op')) + ('transform_min_relu_to_relu6', 'transform Minimum(6)-Relu pattern to Relu6 op'), + ('unroll_unidirseqlstm', 'unroll UnidirectionalSequenceLSTM op')) CONSTANT = CONSTANT() diff --git a/compiler/one-cmds/onelib/export_constant.py b/compiler/one-cmds/onelib/export_constant.py new file mode 100644 index 0000000..7a2de1e --- /dev/null +++ b/compiler/one-cmds/onelib/export_constant.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +# Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from constant import CONSTANT + +import argparse +import configparser + + +def main(): + parser = argparse.ArgumentParser( + description='Export CONSTANT value with given file format.') + parser.add_argument( + '-c', '--constant', type=str, required=True, help='Constant name to export') + parser.add_argument( + '-f', + '--format', + type=str, + required=True, + choices=['cfg', 'txt'], + help= + 'File format to export. The created cfg file contains CONSTANT under the one-optimize section.' + ) + parser.add_argument( + '--exclusive', + action='store_true', + help='Exports the rest of the options except for the given constant') + parser.add_argument( + '-o', '--output_path', type=str, required=True, help='Path to output') + + args = parser.parse_args() + + if not hasattr(CONSTANT, args.constant): + raise NameError('Not found given constant name') + + if args.exclusive: + constant_to_exclude = getattr(CONSTANT, args.constant) + constant_to_export = [] + for opt in CONSTANT.OPTIMIZATION_OPTS: + if opt[0] in constant_to_exclude: + continue + constant_to_export.append(opt[0]) + else: + constant_to_export = getattr(CONSTANT, args.constant) + + if args.format == 'cfg': + SECTION_TO_EXPORT = 'one-optimize' + config = configparser.ConfigParser() + config[SECTION_TO_EXPORT] = dict() + for constant in constant_to_export: + config[SECTION_TO_EXPORT][constant] = 'True' + + with open(args.output_path, 'w') as f: + config.write(f) + + if args.format == 'txt': + with open(args.output_path, 'w') as f: + for constant in constant_to_export: + f.write(f"{constant}\n") + + +if __name__ == '__main__': + main() diff --git a/compiler/one-cmds/onelib/make_cmd.py b/compiler/one-cmds/onelib/make_cmd.py index 0015e83..068b549 100644 --- a/compiler/one-cmds/onelib/make_cmd.py +++ b/compiler/one-cmds/onelib/make_cmd.py @@ -20,7 +20,7 @@ import sys import onelib.constant as _constant -def _is_valid_attr(args, attr): +def is_valid_attr(args, attr): return hasattr(args, attr) and getattr(args, attr) @@ -28,45 +28,45 @@ def make_tf2tfliteV2_cmd(args, driver_path, input_path, output_path): """make a command for running tf2tfliteV2.py""" cmd = [sys.executable, os.path.expanduser(driver_path)] # verbose - if _is_valid_attr(args, 'verbose'): + if is_valid_attr(args, 'verbose'): cmd.append('--verbose') # model_format - if _is_valid_attr(args, 'model_format_cmd'): + if is_valid_attr(args, 'model_format_cmd'): cmd.append(getattr(args, 'model_format_cmd')) - elif _is_valid_attr(args, 'model_format'): + elif is_valid_attr(args, 'model_format'): cmd.append('--' + getattr(args, 'model_format')) else: cmd.append('--graph_def') # default value # converter version - if _is_valid_attr(args, 'converter_version_cmd'): + if is_valid_attr(args, 'converter_version_cmd'): cmd.append(getattr(args, 'converter_version_cmd')) - elif _is_valid_attr(args, 'converter_version'): + elif is_valid_attr(args, 'converter_version'): cmd.append('--' + getattr(args, 'converter_version')) else: cmd.append('--v1') # default value # input_path - if _is_valid_attr(args, 'input_path'): + if is_valid_attr(args, 'input_path'): cmd.append('--input_path') cmd.append(os.path.expanduser(input_path)) # output_path - if _is_valid_attr(args, 'output_path'): + if is_valid_attr(args, 'output_path'): cmd.append('--output_path') cmd.append(os.path.expanduser(output_path)) # input_arrays - if _is_valid_attr(args, 'input_arrays'): + if is_valid_attr(args, 'input_arrays'): cmd.append('--input_arrays') cmd.append(getattr(args, 'input_arrays')) # input_shapes - if _is_valid_attr(args, 'input_shapes'): + if is_valid_attr(args, 'input_shapes'): cmd.append('--input_shapes') cmd.append(getattr(args, 'input_shapes')) # output_arrays - if _is_valid_attr(args, 'output_arrays'): + if is_valid_attr(args, 'output_arrays'): cmd.append('--output_arrays') cmd.append(getattr(args, 'output_arrays')) # experimental options - if _is_valid_attr(args, 'experimental_disable_batchmatmul_unfold'): + if is_valid_attr(args, 'experimental_disable_batchmatmul_unfold'): cmd.append('--experimental_disable_batchmatmul_unfold') return cmd @@ -82,12 +82,12 @@ def make_circle2circle_cmd(args, driver_path, input_path, output_path): """make a command for running circle2circle""" cmd = [os.path.expanduser(c) for c in [driver_path, input_path, output_path]] # profiling - if _is_valid_attr(args, 'generate_profile_data'): + if is_valid_attr(args, 'generate_profile_data'): cmd.append('--generate_profile_data') # optimization pass(only true/false options) # TODO support options whose number of arguments is more than zero for opt in _constant.CONSTANT.OPTIMIZATION_OPTS: - if _is_valid_attr(args, opt[0]): + if is_valid_attr(args, opt[0]): # ./driver --opt[0] if type(getattr(args, opt[0])) is bool: cmd.append('--' + opt[0]) diff --git a/compiler/one-cmds/onelib/utils.py b/compiler/one-cmds/onelib/utils.py new file mode 100644 index 0000000..3d37f9d --- /dev/null +++ b/compiler/one-cmds/onelib/utils.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python + +# Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import configparser +import glob +import importlib +import ntpath +import os +import subprocess +import sys + +from typing import Union + +import onelib.constant as _constant + + +def add_default_arg(parser): + # version + parser.add_argument( + '-v', + '--version', + action='store_true', + help='show program\'s version number and exit') + + # verbose + parser.add_argument( + '-V', + '--verbose', + action='store_true', + help='output additional information to stdout or stderr') + + # configuration file + parser.add_argument('-C', '--config', type=str, help='run with configuation file') + # section name that you want to run in configuration file + parser.add_argument('-S', '--section', type=str, help=argparse.SUPPRESS) + + +def add_default_arg_no_CS(parser): + """ + This adds -v -V args only (no -C nor -S) + """ + # version + parser.add_argument( + '-v', + '--version', + action='store_true', + help='show program\'s version number and exit') + + # verbose + parser.add_argument( + '-V', + '--verbose', + action='store_true', + help='output additional information to stdout or stderr') + + +def is_accumulated_arg(arg, driver): + if driver == "one-quantize": + accumulables = [ + "tensor_name", "scale", "zero_point", "src_tensor_name", "dst_tensor_name" + ] + if arg in accumulables: + return True + + return False + + +def is_valid_attr(args, attr): + return hasattr(args, attr) and getattr(args, attr) + + +def parse_cfg_and_overwrite(config_path, section, args): + """ + parse given section of configuration file and set the values of args. + Even if the values parsed from the configuration file already exist in args, + the values are overwritten. + """ + if config_path == None: + # DO NOTHING + return + config = configparser.ConfigParser() + # make option names case sensitive + config.optionxform = str + parsed = config.read(config_path) + if not parsed: + raise FileNotFoundError('Not found given configuration file') + if not config.has_section(section): + raise AssertionError('configuration file doesn\'t have \'' + section + + '\' section') + for key in config[section]: + setattr(args, key, config[section][key]) + # TODO support accumulated arguments + + +def parse_cfg(config_path: Union[str, None], section_to_parse: str, args): + """ + parse configuration file and store the information to args + + :param config_path: path to configuration file + :param section_to_parse: section name to parse + :param args: object to store the parsed information + """ + if config_path is None: + return + + parser = configparser.ConfigParser() + parser.optionxform = str + parser.read(config_path) + + if not parser.has_section(section_to_parse): + raise AssertionError('configuration file must have \'' + section_to_parse + + '\' section') + + for key in parser[section_to_parse]: + if is_accumulated_arg(key, section_to_parse): + if not is_valid_attr(args, key): + setattr(args, key, [parser[section_to_parse][key]]) + else: + getattr(args, key).append(parser[section_to_parse][key]) + continue + if hasattr(args, key) and getattr(args, key): + continue + setattr(args, key, parser[section_to_parse][key]) + + +def print_version_and_exit(file_path): + """print version of the file located in the file_path""" + script_path = os.path.realpath(file_path) + dir_path = os.path.dirname(script_path) + script_name = os.path.splitext(os.path.basename(script_path))[0] + # run one-version + subprocess.call([os.path.join(dir_path, 'one-version'), script_name]) + sys.exit() + + +def safemain(main, mainpath): + """execute given method and print with program name for all uncaught exceptions""" + try: + main() + except Exception as e: + prog_name = os.path.basename(mainpath) + print(f"{prog_name}: {type(e).__name__}: " + str(e), file=sys.stderr) + sys.exit(255) + + +def run(cmd, err_prefix=None, logfile=None): + """Execute command in subprocess + + Args: + cmd: command to be executed in subprocess + err_prefix: prefix to be put before every stderr lines + logfile: file stream to which both of stdout and stderr lines will be written + """ + with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p: + import select + inputs = set([p.stdout, p.stderr]) + while inputs: + readable, _, _ = select.select(inputs, [], []) + for x in readable: + line = x.readline() + if len(line) == 0: + inputs.discard(x) + continue + if x == p.stdout: + out = sys.stdout + if x == p.stderr: + out = sys.stderr + if err_prefix: + line = f"{err_prefix}: ".encode() + line + out.buffer.write(line) + out.buffer.flush() + if logfile != None: + logfile.write(line) + if p.returncode != 0: + sys.exit(p.returncode) + + +def remove_prefix(str, prefix): + if str.startswith(prefix): + return str[len(prefix):] + return str + + +def remove_suffix(str, suffix): + if str.endswith(suffix): + return str[:-len(suffix)] + return str + + +def get_optimization_list(get_name=False): + """ + returns a list of optimization. If `get_name` is True, + only basename without extension is returned rather than full file path. + + [one hierarchy] + one + ├── backends + ├── bin + ├── doc + ├── include + ├── lib + ├── optimization + └── test + + Optimization options must be placed in `optimization` folder + """ + dir_path = os.path.dirname(os.path.realpath(__file__)) + + # optimization folder + files = [ + f for f in glob.glob(dir_path + '/../../optimization/O*.cfg', recursive=True) + ] + # exclude if the name has space + files = [s for s in files if not ' ' in s] + + opt_list = [] + for cand in files: + base = ntpath.basename(cand) + if os.path.isfile(cand) and os.access(cand, os.R_OK): + opt_list.append(cand) + + if get_name == True: + # NOTE the name includes prefix 'O' + # e.g. O1, O2, ONCHW not just 1, 2, NCHW + opt_list = [ntpath.basename(f) for f in opt_list] + opt_list = [remove_suffix(s, '.cfg') for s in opt_list] + + return opt_list + + +def detect_one_import_drivers(search_path): + """Looks for import drivers in given directory + + Args: + search_path: path to the directory where to search import drivers + + Returns: + dict: each entry is related to single detected driver, + key is a config section name, value is a driver name + + """ + import_drivers_dict = {} + for module_name in os.listdir(search_path): + full_path = os.path.join(search_path, module_name) + if not os.path.isfile(full_path): + continue + if module_name.find("one-import-") != 0: + continue + module_loader = importlib.machinery.SourceFileLoader(module_name, full_path) + module_spec = importlib.util.spec_from_loader(module_name, module_loader) + module = importlib.util.module_from_spec(module_spec) + try: + module_loader.exec_module(module) + if hasattr(module, "get_driver_cfg_section"): + section = module.get_driver_cfg_section() + import_drivers_dict[section] = module_name + except: + pass + return import_drivers_dict diff --git a/compiler/one-cmds/tests/CMakeLists.txt b/compiler/one-cmds/tests/CMakeLists.txt index 17f55ec..8c006c1 100644 --- a/compiler/one-cmds/tests/CMakeLists.txt +++ b/compiler/one-cmds/tests/CMakeLists.txt @@ -111,3 +111,42 @@ add_subdirectory(onnx-operations) if(ENABLE_ONE_IMPORT_PYTORCH) add_subdirectory(pytorch-operations) endif(ENABLE_ONE_IMPORT_PYTORCH) + +# Generate group option list for tests +get_filename_component(ONE_CMDS_DIR ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) +set(ONE_PYTHON_DIR "onelib") +set(CONSTANT_EXPORTING_SCRIPT "${ONE_CMDS_DIR}/${ONE_PYTHON_DIR}/export_constant.py") +set(O1_OPTION "O1") +set(O1_TXT_FILE "${O1_OPTION}.list") +set(O1_TXT_FILE_BIN "${CMAKE_CURRENT_BINARY_DIR}/${O1_TXT_FILE}") +set(NON_O1_TXT_FILE "non-${O1_OPTION}.list") +set(NON_O1_TXT_FILE_BIN "${CMAKE_CURRENT_BINARY_DIR}/${NON_O1_TXT_FILE}") + +add_custom_command(OUTPUT ${O1_TXT_FILE_BIN} + COMMAND ${PYTHON_EXECUTABLE} ${CONSTANT_EXPORTING_SCRIPT} --constant ${O1_OPTION} + --format txt --output_path ${O1_TXT_FILE_BIN} + DEPENDS ${CONSTANT_EXPORTING_SCRIPT} + COMMENT "Generate ${O1_TXT_FILE}" +) + +add_custom_command(OUTPUT ${NON_O1_TXT_FILE_BIN} + COMMAND ${PYTHON_EXECUTABLE} ${CONSTANT_EXPORTING_SCRIPT} --constant ${O1_OPTION} + --format txt --output_path ${NON_O1_TXT_FILE_BIN} + --exclusive + DEPENDS ${CONSTANT_EXPORTING_SCRIPT} + COMMENT "Generate ${NON_O1_TXT_FILE}" +) + +add_custom_target("O1_txt_target" ALL DEPENDS ${O1_TXT_FILE_BIN} ${NON_O1_TXT_FILE_BIN}) + +install(FILES ${O1_TXT_FILE_BIN} + PERMISSIONS OWNER_WRITE OWNER_READ + GROUP_READ + WORLD_READ + DESTINATION test) + +install(FILES ${NON_O1_TXT_FILE_BIN} + PERMISSIONS OWNER_WRITE OWNER_READ + GROUP_READ + WORLD_READ + DESTINATION test) diff --git a/compiler/one-cmds/tests/one-build_001.test b/compiler/one-cmds/tests/one-build_001.test index 30f48e1..e6c6ee7 100644 --- a/compiler/one-cmds/tests/one-build_001.test +++ b/compiler/one-cmds/tests/one-build_001.test @@ -30,8 +30,11 @@ trap trap_err_onexit ERR configfile="one-build_001.cfg" outputfile="inception_v3.opt.circle" +rm -f ${filename}.log +rm -f ${outputfile} + # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_002.test b/compiler/one-cmds/tests/one-build_002.test index 1485df9..c29d422 100644 --- a/compiler/one-cmds/tests/one-build_002.test +++ b/compiler/one-cmds/tests/one-build_002.test @@ -30,8 +30,11 @@ trap trap_err_onexit ERR configfile="one-build_002.cfg" outputfile="inception_v3_pkg" +rm -f ${filename}.log +rm -rf ${outputfile} + # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_003.test b/compiler/one-cmds/tests/one-build_003.test index fd9585b..6337b50 100644 --- a/compiler/one-cmds/tests/one-build_003.test +++ b/compiler/one-cmds/tests/one-build_003.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="one-build_003.cfg" outputfile="inception_v3.quantized.circle" -rm -rf ${outputfile} +rm -f ${filename}.log +rm -f ${outputfile} # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_004.test b/compiler/one-cmds/tests/one-build_004.test index 3e72978..5406355 100644 --- a/compiler/one-cmds/tests/one-build_004.test +++ b/compiler/one-cmds/tests/one-build_004.test @@ -31,13 +31,14 @@ trap trap_err_onexit ERR configfile="one-build_004.cfg" outputfile="sample.tvn" -rm -rf ${outputfile} +rm -f ${filename}.log +rm -f ${outputfile} # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_005.test b/compiler/one-cmds/tests/one-build_005.test index 3a69cc3..f003be5 100644 --- a/compiler/one-cmds/tests/one-build_005.test +++ b/compiler/one-cmds/tests/one-build_005.test @@ -31,13 +31,14 @@ trap trap_err_onexit ERR configfile="one-build_005.cfg" outputfile="sample.tvn" -rm -rf ${outputfile} +rm -f ${filename}.log +rm -f ${outputfile} # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_006.test b/compiler/one-cmds/tests/one-build_006.test index ff389a8..0777015 100644 --- a/compiler/one-cmds/tests/one-build_006.test +++ b/compiler/one-cmds/tests/one-build_006.test @@ -31,13 +31,14 @@ trap trap_err_onexit ERR configfile="one-build_006.cfg" outputfile="sample.tvn" -rm -rf ${outputfile} +rm -f ${filename}.log +rm -f ${outputfile} # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_007.test b/compiler/one-cmds/tests/one-build_007.test index 4ad68b5..4fd6a2a 100644 --- a/compiler/one-cmds/tests/one-build_007.test +++ b/compiler/one-cmds/tests/one-build_007.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="one-build_007.cfg" outputfile="inception_v3_pkg" +rm -f ${filename}.log rm -rf ${outputfile} # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_008.test b/compiler/one-cmds/tests/one-build_008.test index 2f8c4c2..d19b95f 100644 --- a/compiler/one-cmds/tests/one-build_008.test +++ b/compiler/one-cmds/tests/one-build_008.test @@ -31,13 +31,14 @@ trap trap_err_onexit ERR configfile="one-build_008.cfg" outputfile="test_onnx_model.bin" -rm -rf ${outputfile} +rm -f ${filename}.log +rm -f ${outputfile} # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_009.test b/compiler/one-cmds/tests/one-build_009.test index 56c0505..ae53519 100644 --- a/compiler/one-cmds/tests/one-build_009.test +++ b/compiler/one-cmds/tests/one-build_009.test @@ -31,13 +31,14 @@ trap trap_err_onexit ERR configfile="one-build_009.cfg" outputfile="onnx_conv2d_conv2d.bin" -rm -rf ${outputfile} +rm -f ${filename}.log +rm -f ${outputfile} # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_010.test b/compiler/one-cmds/tests/one-build_010.test index ec31639..b76e81c 100644 --- a/compiler/one-cmds/tests/one-build_010.test +++ b/compiler/one-cmds/tests/one-build_010.test @@ -31,11 +31,12 @@ configfile="one-build_010.cfg" outputfile="inception_v3.alt.circle" intermfile="inception_v3.alt.tflite" -rm -rf ${outputfile} -rm -rf ${intermfile} +rm -f ${filename}.log +rm -f ${outputfile} +rm -f ${intermfile} # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_011.test b/compiler/one-cmds/tests/one-build_011.test index 29b4213..efd7143 100644 --- a/compiler/one-cmds/tests/one-build_011.test +++ b/compiler/one-cmds/tests/one-build_011.test @@ -31,11 +31,12 @@ configfile="one-build_011.cfg" outputfile="test_onnx_model.circle" intermfile="test_onnx_model.tflite" -rm -rf ${outputfile} -rm -rf ${intermfile} +rm -f ${filename}.log +rm -f ${outputfile} +rm -f ${intermfile} # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_012.test b/compiler/one-cmds/tests/one-build_012.test index 445a948..5f123cd 100644 --- a/compiler/one-cmds/tests/one-build_012.test +++ b/compiler/one-cmds/tests/one-build_012.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="one-build_012.cfg" outputfile="inception_v3.list.quantized.circle" -rm -rf ${outputfile} +rm -f ${filename}.log +rm -f ${outputfile} # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_013.test b/compiler/one-cmds/tests/one-build_013.test index 1b405fb..9a71f02 100644 --- a/compiler/one-cmds/tests/one-build_013.test +++ b/compiler/one-cmds/tests/one-build_013.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="one-build_013.cfg" outputfile="inception_v3.dir.quantized.circle" -rm -rf ${outputfile} +rm -f ${filename}.log +rm -f ${outputfile} # run test -one-build -C ${configfile} > /dev/null 2>&1 +one-build -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-build_014.test b/compiler/one-cmds/tests/one-build_014.test index b3acbf5..10e6cc6 100644 --- a/compiler/one-cmds/tests/one-build_014.test +++ b/compiler/one-cmds/tests/one-build_014.test @@ -55,7 +55,8 @@ trap trap_err_onexit ERR configfile="one-build_014.cfg" outputfile="inception_v3.opt.circle" -rm -rf ${outputfile} +rm -f ${filename}.log +rm -f ${outputfile} if [ ! -d "../optimization" ]; then mkdir -p ../optimization @@ -69,7 +70,7 @@ LUCI_LOG=5 one-build -C ${configfile} -OONE-BUILD_014 > ${filename}.log 2>&1 clean_envir -if grep -q "MakeBatchNormGammaPositivePass" "${filename}.log"; then +if ! grep -q "MakeBatchNormGammaPositivePass" "${filename}.log"; then echo "${filename_ext} SUCCESS" exit 0 fi diff --git a/compiler/one-cmds/tests/one-build_neg_002.test b/compiler/one-cmds/tests/one-build_neg_002.test index 15550d8..dece9c0 100644 --- a/compiler/one-cmds/tests/one-build_neg_002.test +++ b/compiler/one-cmds/tests/one-build_neg_002.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile="one-build_neg_002.cfg" +rm -f ${filename}.log + # run test one-build -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-build_neg_003.test b/compiler/one-cmds/tests/one-build_neg_003.test index bcbd2f9..c793d13 100644 --- a/compiler/one-cmds/tests/one-build_neg_003.test +++ b/compiler/one-cmds/tests/one-build_neg_003.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile="one-build_neg_003.cfg" +rm -f ${filename}.log + # run test one-build -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-build_neg_004.test b/compiler/one-cmds/tests/one-build_neg_004.test index aebdfe5..b0d4e61 100644 --- a/compiler/one-cmds/tests/one-build_neg_004.test +++ b/compiler/one-cmds/tests/one-build_neg_004.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile="one-build_neg_004.cfg" +rm -f ${filename}.log + # run test one-build -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-build_neg_005.test b/compiler/one-cmds/tests/one-build_neg_005.test index b4e43d0..9b25111 100644 --- a/compiler/one-cmds/tests/one-build_neg_005.test +++ b/compiler/one-cmds/tests/one-build_neg_005.test @@ -33,6 +33,7 @@ intermfile="inception_v3.alt.tflite" rm -rf ${outputfile} rm -rf ${intermfile} +rm -f ${filename}.log # run test one-build -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-build_neg_006.test b/compiler/one-cmds/tests/one-build_neg_006.test index 8723df2..d228f8f 100644 --- a/compiler/one-cmds/tests/one-build_neg_006.test +++ b/compiler/one-cmds/tests/one-build_neg_006.test @@ -33,6 +33,7 @@ intermfile="test_onnx_model.tflite" rm -rf ${outputfile} rm -rf ${intermfile} +rm -f ${filename}.log # run test one-build -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-build_neg_007.test b/compiler/one-cmds/tests/one-build_neg_007.test index 5c5d9af..59a5d25 100644 --- a/compiler/one-cmds/tests/one-build_neg_007.test +++ b/compiler/one-cmds/tests/one-build_neg_007.test @@ -62,6 +62,8 @@ touch ../optimization/OONE_BUILD_NEG_007.cfg configfile=".." +rm -f ${filename}.log + # run test one-build -C ${configfile} -OONE_BUILD_NEG_007 > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-build_neg_008.test b/compiler/one-cmds/tests/one-build_neg_008.test index 8ed2871..2b41222 100644 --- a/compiler/one-cmds/tests/one-build_neg_008.test +++ b/compiler/one-cmds/tests/one-build_neg_008.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile=".." +rm -f ${filename}.log + # run test one-build -C ${configfile} -OONE_BUILD_NEG_008 > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-build_neg_009.test b/compiler/one-cmds/tests/one-build_neg_009.test index 8d9c831..5e3698d 100644 --- a/compiler/one-cmds/tests/one-build_neg_009.test +++ b/compiler/one-cmds/tests/one-build_neg_009.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile=".." +rm -f ${filename}.log + # run test one-build -C ${configfile} "-O SPACE OPTION" > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-codegen_001.test b/compiler/one-cmds/tests/one-codegen_001.test index 80d149e..dbc4ad7 100644 --- a/compiler/one-cmds/tests/one-codegen_001.test +++ b/compiler/one-cmds/tests/one-codegen_001.test @@ -25,6 +25,8 @@ trap_err_onexit() trap trap_err_onexit ERR +rm -f ${filename}.log + # copy help-compile to bin folder cp help-compile ../bin/help-compile diff --git a/compiler/one-cmds/tests/one-codegen_002.test b/compiler/one-cmds/tests/one-codegen_002.test index 8af539d..95e7b7b 100644 --- a/compiler/one-cmds/tests/one-codegen_002.test +++ b/compiler/one-cmds/tests/one-codegen_002.test @@ -31,6 +31,7 @@ trap trap_err_onexit ERR outputfile="sample.tvn" rm -rf ${outputfile} +rm -f ${filename}.log # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile diff --git a/compiler/one-cmds/tests/one-codegen_003.test b/compiler/one-cmds/tests/one-codegen_003.test index 889c389..f283ec3 100644 --- a/compiler/one-cmds/tests/one-codegen_003.test +++ b/compiler/one-cmds/tests/one-codegen_003.test @@ -31,6 +31,7 @@ trap trap_err_onexit ERR outputfile="sample.tvn" rm -rf ${outputfile} +rm -f ${filename}.log # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile diff --git a/compiler/one-cmds/tests/one-codegen_004.test b/compiler/one-cmds/tests/one-codegen_004.test index 384689f..485f591 100644 --- a/compiler/one-cmds/tests/one-codegen_004.test +++ b/compiler/one-cmds/tests/one-codegen_004.test @@ -27,6 +27,8 @@ trap_err_onexit() trap trap_err_onexit ERR +rm -f ${filename}.log + # run test one-codegen -h > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-codegen_neg_001.test b/compiler/one-cmds/tests/one-codegen_neg_001.test index fd5d0cb..27db028 100644 --- a/compiler/one-cmds/tests/one-codegen_neg_001.test +++ b/compiler/one-cmds/tests/one-codegen_neg_001.test @@ -32,6 +32,8 @@ trap_err_onexit() trap trap_err_onexit ERR +rm -f ${filename}.log + # run test one-codegen > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-import-bcq_001.test b/compiler/one-cmds/tests/one-import-bcq_001.test index fcca10e..a414eed 100644 --- a/compiler/one-cmds/tests/one-import-bcq_001.test +++ b/compiler/one-cmds/tests/one-import-bcq_001.test @@ -28,6 +28,7 @@ trap trap_err_onexit ERR inputfile="./bcq.pb" outputfile="./bcq.circle" +rm -f ${filename}.log rm -rf $outputfile # run test @@ -35,7 +36,7 @@ one-import-bcq \ --input_path ${inputfile} \ --output_path ${outputfile} \ --input_arrays Placeholder \ ---output_arrays MatMul > /dev/null 2>&1 +--output_arrays MatMul > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-import-bcq_neg_001.test b/compiler/one-cmds/tests/one-import-bcq_neg_001.test index 6d0d9b2..cb8fe9e 100644 --- a/compiler/one-cmds/tests/one-import-bcq_neg_001.test +++ b/compiler/one-cmds/tests/one-import-bcq_neg_001.test @@ -36,7 +36,7 @@ inputfile="./bcq.pb" outputfile="./bcq.circle" rm -rf ${outputfile} -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-import-bcq \ diff --git a/compiler/one-cmds/tests/one-import-bcq_neg_002.test b/compiler/one-cmds/tests/one-import-bcq_neg_002.test index b50c72c..25c772f 100644 --- a/compiler/one-cmds/tests/one-import-bcq_neg_002.test +++ b/compiler/one-cmds/tests/one-import-bcq_neg_002.test @@ -36,7 +36,7 @@ inputfile="./bcq.pb" outputfile="./bcq.circle" rm -rf ${outputfile} -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-import-bcq \ diff --git a/compiler/one-cmds/tests/one-import-bcq_neg_003.test b/compiler/one-cmds/tests/one-import-bcq_neg_003.test index 0f75af8..20ef67a 100644 --- a/compiler/one-cmds/tests/one-import-bcq_neg_003.test +++ b/compiler/one-cmds/tests/one-import-bcq_neg_003.test @@ -36,7 +36,7 @@ inputfile="./bcq_null.pb" outputfile="./bcq.circle" rm -rf ${outputfile} -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-import-bcq \ diff --git a/compiler/one-cmds/tests/one-import-bcq_neg_004.test b/compiler/one-cmds/tests/one-import-bcq_neg_004.test index 62bfa44..44a8ae1 100644 --- a/compiler/one-cmds/tests/one-import-bcq_neg_004.test +++ b/compiler/one-cmds/tests/one-import-bcq_neg_004.test @@ -36,7 +36,7 @@ inputfile="./while_3.pbtxt" outputfile="./bcq.circle" rm -rf ${outputfile} -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-import-bcq \ diff --git a/compiler/one-cmds/tests/one-import-bcq_neg_005.test b/compiler/one-cmds/tests/one-import-bcq_neg_005.test index 0e6d52c..550804f 100644 --- a/compiler/one-cmds/tests/one-import-bcq_neg_005.test +++ b/compiler/one-cmds/tests/one-import-bcq_neg_005.test @@ -35,7 +35,7 @@ trap trap_err_onexit ERR inputfile="./bcq.pb" outputfile="." -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-import-bcq \ diff --git a/compiler/one-cmds/tests/one-import-bcq_neg_006.test b/compiler/one-cmds/tests/one-import-bcq_neg_006.test index df7116b..7b872eb 100644 --- a/compiler/one-cmds/tests/one-import-bcq_neg_006.test +++ b/compiler/one-cmds/tests/one-import-bcq_neg_006.test @@ -35,7 +35,7 @@ trap trap_err_onexit ERR inputfile="./bcq.pb" outputfile="./bcq.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-import-bcq \ diff --git a/compiler/one-cmds/tests/one-import-bcq_neg_007.test b/compiler/one-cmds/tests/one-import-bcq_neg_007.test index 521238b..50d47c0 100644 --- a/compiler/one-cmds/tests/one-import-bcq_neg_007.test +++ b/compiler/one-cmds/tests/one-import-bcq_neg_007.test @@ -35,7 +35,7 @@ trap trap_err_onexit ERR inputfile="./bcq.pb" outputfile="./bcq.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-import-bcq \ diff --git a/compiler/one-cmds/tests/one-import-bcq_neg_008.test b/compiler/one-cmds/tests/one-import-bcq_neg_008.test index 7729caf..2edd33d 100644 --- a/compiler/one-cmds/tests/one-import-bcq_neg_008.test +++ b/compiler/one-cmds/tests/one-import-bcq_neg_008.test @@ -35,7 +35,7 @@ trap trap_err_onexit ERR inputfile="./bcq.pb" outputfile="./bcq.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-import-bcq \ diff --git a/compiler/one-cmds/tests/one-import-bcq_neg_009.test b/compiler/one-cmds/tests/one-import-bcq_neg_009.test index e48c86d..72c1a87 100644 --- a/compiler/one-cmds/tests/one-import-bcq_neg_009.test +++ b/compiler/one-cmds/tests/one-import-bcq_neg_009.test @@ -35,7 +35,7 @@ trap trap_err_onexit ERR inputfile="./bcq.pb" outputfile="./bcq.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-import-bcq \ diff --git a/compiler/one-cmds/tests/one-import-onnx_001.test b/compiler/one-cmds/tests/one-import-onnx_001.test index 6119b68..39a9607 100644 --- a/compiler/one-cmds/tests/one-import-onnx_001.test +++ b/compiler/one-cmds/tests/one-import-onnx_001.test @@ -29,12 +29,12 @@ inputfile="./test_onnx_model.onnx" outputfile="./test_onnx_model.circle" rm -rf ${outputfile} -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-import-onnx \ --input_path ${inputfile} \ ---output_path ${outputfile} > ${outputfile}.log 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-import-onnx_002.test b/compiler/one-cmds/tests/one-import-onnx_002.test index a6a38ee..0430a8e 100644 --- a/compiler/one-cmds/tests/one-import-onnx_002.test +++ b/compiler/one-cmds/tests/one-import-onnx_002.test @@ -28,7 +28,7 @@ trap_err_onexit() trap trap_err_onexit ERR inputfile="./reshape_matmul.onnx" -outputfile="./reshape_matmul.circle" +outputfile="./reshape_matmul.one-import-onnx_002.circle" rm -rf ${outputfile} rm -rf ${outputfile}.log @@ -42,7 +42,7 @@ if [[ ! -s "${outputfile}" ]]; then trap_err_onexit fi -circle-operator --code reshape_matmul.circle > ${outputfile}.log 2>&1 +circle-operator --code ${outputfile} > ${outputfile}.log 2>&1 if ! grep -q "FULLY_CONNECTED" "${outputfile}.log"; then trap_err_onexit @@ -61,7 +61,7 @@ if [[ ! -s "${outputfile}" ]]; then trap_err_onexit fi -circle-operator --code reshape_matmul.circle > ${outputfile}.log 2>&1 +circle-operator --code ${outputfile} > ${outputfile}.log 2>&1 if ! grep -q "BATCH_MATMUL" "${outputfile}.log"; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-import_001.test b/compiler/one-cmds/tests/one-import_001.test index 35c9c3c..2647337 100644 --- a/compiler/one-cmds/tests/one-import_001.test +++ b/compiler/one-cmds/tests/one-import_001.test @@ -29,13 +29,14 @@ inputfile="./inception_v3.pb" outputfile="./inception_v3.circle" # Note: Do not remove output circle file as it's used for quantize tests +rm -f ${filename}.log # run test one-import tf \ --input_path ${inputfile} \ --output_path ${outputfile} \ --input_arrays input --input_shapes "1,299,299,3" \ ---output_arrays InceptionV3/Predictions/Reshape_1 > /dev/null 2>&1 +--output_arrays InceptionV3/Predictions/Reshape_1 > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-import_002.cfg b/compiler/one-cmds/tests/one-import_002.cfg index 8d6ae2c..e7ede7b 100644 --- a/compiler/one-cmds/tests/one-import_002.cfg +++ b/compiler/one-cmds/tests/one-import_002.cfg @@ -9,7 +9,7 @@ one-codegen=False [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3_cfg.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 diff --git a/compiler/one-cmds/tests/one-import_002.test b/compiler/one-cmds/tests/one-import_002.test index 232becf..24f673c 100644 --- a/compiler/one-cmds/tests/one-import_002.test +++ b/compiler/one-cmds/tests/one-import_002.test @@ -28,13 +28,18 @@ trap_err_onexit() trap trap_err_onexit ERR configfile="one-import_002.cfg" -outputfile="inception_v3_cmd.circle" +outputfile_cmd="inception_v3_cmd.circle" +outputfile_cfg="inception_v3_cfg.circle" + +rm -f ${filename}.log +rm -f ${outputfile_cmd} +rm -f ${outputfile_cfg} # run test one-import tf -C ${configfile} \ ---output_path=inception_v3_cmd.circle > /dev/null 2>&1 +--output_path=${outputfile_cmd} > ${filename}.log 2>&1 -if [[ ! -s "${outputfile}" ]]; then +if [[ ! -s "${outputfile_cmd}" ]]; then trap_err_onexit fi diff --git a/compiler/one-cmds/tests/one-import_003.test b/compiler/one-cmds/tests/one-import_003.test index cd10698..92e4b15 100644 --- a/compiler/one-cmds/tests/one-import_003.test +++ b/compiler/one-cmds/tests/one-import_003.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="one-import_003.cfg" outputfile="test_saved_model.circle" +rm -f ${filename}.log rm -f ${outputfile} # run test -one-import tf -C ${configfile} > /dev/null 2>&1 +one-import tf -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-import_004.test b/compiler/one-cmds/tests/one-import_004.test index fb30d40..42c1692 100644 --- a/compiler/one-cmds/tests/one-import_004.test +++ b/compiler/one-cmds/tests/one-import_004.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="one-import_004.cfg" outputfile="test_keras_model.circle" +rm -f ${filename}.log rm -f ${outputfile} # run test -one-import tf -C ${configfile} > /dev/null 2>&1 +one-import tf -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-import_005.test b/compiler/one-cmds/tests/one-import_005.test index 385afb5..75122d6 100644 --- a/compiler/one-cmds/tests/one-import_005.test +++ b/compiler/one-cmds/tests/one-import_005.test @@ -31,6 +31,7 @@ configfile="one-import_005.cfg" outputfile="test_onnx_model.circle" rm -f ${outputfile} +rm -f ${filename}.log # run test one-import onnx -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-import_006.test b/compiler/one-cmds/tests/one-import_006.test index 88c6da5..a9be173 100644 --- a/compiler/one-cmds/tests/one-import_006.test +++ b/compiler/one-cmds/tests/one-import_006.test @@ -31,6 +31,7 @@ inputfile="test_onnx_model.onnx" outputfile="test_onnx_model.circle" rm -f ${outputfile} +rm -f ${filename}.log # run test one-import onnx -i ${inputfile} -o ${outputfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-import_neg_001.test b/compiler/one-cmds/tests/one-import_neg_001.test index 3986ee4..82a3e33 100644 --- a/compiler/one-cmds/tests/one-import_neg_001.test +++ b/compiler/one-cmds/tests/one-import_neg_001.test @@ -35,7 +35,8 @@ trap trap_err_onexit ERR inputfile="./inception_v3.tflite" outputfile="./inception_v3.circle" -rm -rf ${outputfile} +# do not remove output file +# rm -rf ${outputfile} rm -rf ${filename}.log # run test diff --git a/compiler/one-cmds/tests/one-import_neg_003.test b/compiler/one-cmds/tests/one-import_neg_003.test index df83033..9561cea 100644 --- a/compiler/one-cmds/tests/one-import_neg_003.test +++ b/compiler/one-cmds/tests/one-import_neg_003.test @@ -43,7 +43,8 @@ trap trap_err_onexit ERR inputfile="./inception_v3.pb" outputfile="./inception_v3.circle" -rm -rf ${outputfile} +# do not remove output file +# rm -rf ${outputfile} rm -rf ${filename}.log # run test diff --git a/compiler/one-cmds/tests/one-import_neg_004.test b/compiler/one-cmds/tests/one-import_neg_004.test index 99badad..8626eb9 100644 --- a/compiler/one-cmds/tests/one-import_neg_004.test +++ b/compiler/one-cmds/tests/one-import_neg_004.test @@ -35,7 +35,8 @@ trap trap_err_onexit ERR inputfile="./inception_v3.pb" outputfile="./inception_v3.circle" -rm -rf ${outputfile} +# do not remove output file +# rm -rf ${outputfile} rm -rf ${filename}.log # run test diff --git a/compiler/one-cmds/tests/one-import_neg_005.test b/compiler/one-cmds/tests/one-import_neg_005.test index 5e0b9a5..f73826f 100644 --- a/compiler/one-cmds/tests/one-import_neg_005.test +++ b/compiler/one-cmds/tests/one-import_neg_005.test @@ -33,7 +33,7 @@ trap_err_onexit() trap trap_err_onexit ERR inputfile="./inception_v3.pb" -outputfile="./inception_v3.circle" +outputfile="./inception_v3.imp_neg_005.circle" rm -rf ${outputfile} rm -rf ${filename}.log diff --git a/compiler/one-cmds/tests/one-import_neg_006.test b/compiler/one-cmds/tests/one-import_neg_006.test index 3fb5c7d..985bd1c 100644 --- a/compiler/one-cmds/tests/one-import_neg_006.test +++ b/compiler/one-cmds/tests/one-import_neg_006.test @@ -33,7 +33,7 @@ trap_err_onexit() trap trap_err_onexit ERR inputfile="./inception_v3.pb" -outputfile="./inception_v3.circle" +outputfile="./inception_v3.imp_neg_006.circle" rm -rf ${outputfile} rm -rf ${filename}.log diff --git a/compiler/one-cmds/tests/one-import_neg_007.test b/compiler/one-cmds/tests/one-import_neg_007.test index 09e9ed8..2552157 100644 --- a/compiler/one-cmds/tests/one-import_neg_007.test +++ b/compiler/one-cmds/tests/one-import_neg_007.test @@ -33,7 +33,7 @@ trap_err_onexit() trap trap_err_onexit ERR inputfile="./inception_v3.pb" -outputfile="./inception_v3.circle" +outputfile="./inception_v3.imp_neg_007.circle" rm -rf ${outputfile} rm -rf ${filename}.log diff --git a/compiler/one-cmds/tests/one-import_neg_008.test b/compiler/one-cmds/tests/one-import_neg_008.test index 7ad678b..d62899d 100644 --- a/compiler/one-cmds/tests/one-import_neg_008.test +++ b/compiler/one-cmds/tests/one-import_neg_008.test @@ -33,7 +33,7 @@ trap_err_onexit() trap trap_err_onexit ERR inputfile="./inception_v3.pb" -outputfile="./inception_v3.circle" +outputfile="./inception_v3.imp_neg_008.circle" rm -rf ${outputfile} rm -rf ${filename}.log diff --git a/compiler/one-cmds/tests/one-import_neg_010.test b/compiler/one-cmds/tests/one-import_neg_010.test index ab039ee..11512fd 100644 --- a/compiler/one-cmds/tests/one-import_neg_010.test +++ b/compiler/one-cmds/tests/one-import_neg_010.test @@ -33,7 +33,7 @@ trap_err_onexit() trap trap_err_onexit ERR inputfile="./inception_v3.pb" -outputfile="./inception_v3.circle" +outputfile="./inception_v3.imp_neg_010.circle" rm -rf ${outputfile} rm -rf ${filename}.log diff --git a/compiler/one-cmds/tests/one-infer_001.test b/compiler/one-cmds/tests/one-infer_001.test index e7b5695..e8f1bc7 100644 --- a/compiler/one-cmds/tests/one-infer_001.test +++ b/compiler/one-cmds/tests/one-infer_001.test @@ -29,8 +29,10 @@ trap trap_err_onexit ERR # copy help-infer to bin folder cp help-infer ../bin/help-infer +rm -f ${filename}.log + # run test -one-infer -b help -- -h > ${filename}.log +one-infer -d help-infer -- -h > ${filename}.log rm -rf ../bin/help-infer diff --git a/compiler/one-cmds/tests/one-infer_002.test b/compiler/one-cmds/tests/one-infer_002.test index 22070de..6d22fb3 100644 --- a/compiler/one-cmds/tests/one-infer_002.test +++ b/compiler/one-cmds/tests/one-infer_002.test @@ -35,6 +35,8 @@ fi # copy dummy-infer to bin folder cp dummy-infer ../bin/dummy-infer +rm -f ${filename}.log + # run test one-infer -d dummy-infer -- ${inputfile} > ${filename}.log diff --git a/compiler/one-cmds/tests/one-infer_003.test b/compiler/one-cmds/tests/one-infer_003.test index e2aa459..b8fbe93 100644 --- a/compiler/one-cmds/tests/one-infer_003.test +++ b/compiler/one-cmds/tests/one-infer_003.test @@ -14,33 +14,25 @@ # See the License for the specific language governing permissions and # limitations under the License. +# print one-infer's help message + filename_ext="$(basename -- $0)" filename="${filename_ext%.*}" trap_err_onexit() { echo "${filename_ext} FAILED" - rm -rf ../bin/dummy-infer exit 255 } trap trap_err_onexit ERR -inputfile="sample.tvn" - -if [[ ! -s "${inputfile}" ]]; then - touch ${inputfile} -fi - -# copy dummy-infer to bin folder -cp dummy-infer ../bin/dummy-infer +rm -f ${filename}.log # run test -one-infer -b dummy -- ${inputfile} > ${filename}.log - -rm -rf ../bin/dummy-infer +one-infer -h > ${filename}.log -if grep -q "dummy-infer dummy output!!!" "${filename}.log"; then +if grep -q "command line tool to infer model" "${filename}.log"; then echo "${filename_ext} SUCCESS" exit 0 fi diff --git a/compiler/one-cmds/tests/one-infer_004.cfg b/compiler/one-cmds/tests/one-infer_004.cfg new file mode 100644 index 0000000..fd5353b --- /dev/null +++ b/compiler/one-cmds/tests/one-infer_004.cfg @@ -0,0 +1,3 @@ +[one-infer] +driver=dummy-infer +command=sample.tvn diff --git a/compiler/one-cmds/tests/one-infer_004.test b/compiler/one-cmds/tests/one-infer_004.test index a4cb76c..249728c 100644 --- a/compiler/one-cmds/tests/one-infer_004.test +++ b/compiler/one-cmds/tests/one-infer_004.test @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# print one-infer's help message +# one-infer with configuration input filename_ext="$(basename -- $0)" filename="${filename_ext%.*}" @@ -22,15 +22,30 @@ filename="${filename_ext%.*}" trap_err_onexit() { echo "${filename_ext} FAILED" + rm -rf ../bin/dummy-infer exit 255 } trap trap_err_onexit ERR +configfile="one-infer_004.cfg" +inputfile="sample.tvn" + +if [[ ! -s "${inputfile}" ]]; then + touch ${inputfile} +fi + +# copy dummy-infer to bin folder +cp dummy-infer ../bin/dummy-infer + +rm -f ${filename}.log + # run test -one-infer -h > ${filename}.log +one-infer -C ${configfile} > ${filename}.log + +rm -rf ../bin/dummy-infer -if grep -q "command line tool to infer model" "${filename}.log"; then +if grep -q "dummy-infer dummy output!!!" "${filename}.log"; then echo "${filename_ext} SUCCESS" exit 0 fi diff --git a/compiler/one-cmds/tests/one-infer_005.cfg b/compiler/one-cmds/tests/one-infer_005.cfg deleted file mode 100644 index aca6878..0000000 --- a/compiler/one-cmds/tests/one-infer_005.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[one-infer] -backend=dummy -command=sample.tvn diff --git a/compiler/one-cmds/tests/one-infer_005.test b/compiler/one-cmds/tests/one-infer_005.test index a44dd0e..7a921fb 100644 --- a/compiler/one-cmds/tests/one-infer_005.test +++ b/compiler/one-cmds/tests/one-infer_005.test @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# one-infer with configuration input +# one-infer with post process script filename_ext="$(basename -- $0)" filename="${filename_ext%.*}" @@ -28,7 +28,6 @@ trap_err_onexit() trap trap_err_onexit ERR -configfile="one-infer_005.cfg" inputfile="sample.tvn" if [[ ! -s "${inputfile}" ]]; then @@ -38,14 +37,19 @@ fi # copy dummy-infer to bin folder cp dummy-infer ../bin/dummy-infer +rm -f ${filename}.log + # run test -one-infer -C ${configfile} > ${filename}.log +one-infer -d dummy-infer --post-process "./one-infer-test-post-process.py TOKEN" -- ${inputfile} > ${filename}.log 2>&1 +return_code=$? rm -rf ../bin/dummy-infer if grep -q "dummy-infer dummy output!!!" "${filename}.log"; then - echo "${filename_ext} SUCCESS" - exit 0 + if [ "$return_code" -eq "0" ]; then + echo "${filename_ext} SUCCESS" + exit 0 + fi fi trap_err_onexit diff --git a/compiler/one-cmds/tests/one-infer_006.test b/compiler/one-cmds/tests/one-infer_006.test deleted file mode 100644 index 2612133..0000000 --- a/compiler/one-cmds/tests/one-infer_006.test +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# one-infer with post process script - -filename_ext="$(basename -- $0)" -filename="${filename_ext%.*}" - -trap_err_onexit() -{ - echo "${filename_ext} FAILED" - rm -rf ../bin/dummy-infer - exit 255 -} - -trap trap_err_onexit ERR - -inputfile="sample.tvn" - -if [[ ! -s "${inputfile}" ]]; then - touch ${inputfile} -fi - -# copy dummy-infer to bin folder -cp dummy-infer ../bin/dummy-infer - -# run test -one-infer -b dummy --post-process "./one-infer-test-post-process.py TOKEN" -- ${inputfile} > ${filename}.log 2>&1 -return_code=$? - -rm -rf ../bin/dummy-infer - -if grep -q "dummy-infer dummy output!!!" "${filename}.log"; then - if [ "$return_code" -eq "0" ]; then - echo "${filename_ext} SUCCESS" - exit 0 - fi -fi - -trap_err_onexit diff --git a/compiler/one-cmds/tests/one-infer_neg_001.test b/compiler/one-cmds/tests/one-infer_neg_001.test index 62e7211..15df58a 100644 --- a/compiler/one-cmds/tests/one-infer_neg_001.test +++ b/compiler/one-cmds/tests/one-infer_neg_001.test @@ -21,7 +21,7 @@ filename="${filename_ext%.*}" trap_err_onexit() { - if grep -q "error: the following arguments are required: {-d/--driver | -b/--backend}" "${filename}.log"; then + if grep -q "error: the following arguments are required: -d/--driver" "${filename}.log"; then echo "${filename_ext} SUCCESS" exit 0 fi @@ -32,6 +32,8 @@ trap_err_onexit() trap trap_err_onexit ERR +rm -f ${filename}.log + # run test one-infer > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-infer_neg_002.test b/compiler/one-cmds/tests/one-infer_neg_002.test index fa88876..1fd16f2 100644 --- a/compiler/one-cmds/tests/one-infer_neg_002.test +++ b/compiler/one-cmds/tests/one-infer_neg_002.test @@ -33,8 +33,10 @@ trap_err_onexit() trap trap_err_onexit ERR +rm -f ${filename}.log + # run test -one-infer -d ${driver_name} -- -h> ${filename}.log 2>&1 +one-infer -d ${driver_name} -- -h > ${filename}.log 2>&1 echo "${filename_ext} FAILED" exit 255 diff --git a/compiler/one-cmds/tests/one-infer_neg_003.test b/compiler/one-cmds/tests/one-infer_neg_003.test index a000552..1de4d68 100644 --- a/compiler/one-cmds/tests/one-infer_neg_003.test +++ b/compiler/one-cmds/tests/one-infer_neg_003.test @@ -14,27 +14,43 @@ # See the License for the specific language governing permissions and # limitations under the License. -# passed backend is not found +# one-infer with invalid post process script filename_ext="$(basename -- $0)" filename="${filename_ext%.*}" -backend_name="neg" trap_err_onexit() { - if grep -q "FileNotFoundError: ${backend_name}-infer not found" "${filename}.log"; then - echo "${filename_ext} SUCCESS" - exit 0 + return_code=$? + if grep -q "dummy-infer dummy output!!!" "${filename}.log"; then + # Case of succeed of inference driver but error after it + if [ "$return_code" -ne "0" ]; then + echo "${filename_ext} SUCCESS" + exit 0 + fi fi echo "${filename_ext} FAILED" + rm -rf ../bin/dummy-infer exit 255 } trap trap_err_onexit ERR +inputfile="sample.tvn" + +if [[ ! -s "${inputfile}" ]]; then + touch ${inputfile} +fi + +# copy dummy-infer to bin folder +cp dummy-infer ../bin/dummy-infer + +rm -f ${filename}.log + # run test -one-infer -b ${backend_name} -- -h> ${filename}.log 2>&1 +one-infer -d dummy-infer --post-process "./one-infer-test-post-process.py" -- ${inputfile} > ${filename}.log 2>&1 +rm -rf ../bin/dummy-infer echo "${filename_ext} FAILED" exit 255 diff --git a/compiler/one-cmds/tests/one-infer_neg_004.test b/compiler/one-cmds/tests/one-infer_neg_004.test deleted file mode 100644 index b9130d0..0000000 --- a/compiler/one-cmds/tests/one-infer_neg_004.test +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# both -b and -d option drivers are given as argument - -filename_ext="$(basename -- $0)" -filename="${filename_ext%.*}" -backend_name="neg" -driver_name="neg2" - -trap_err_onexit() -{ - if grep -q "\-d and -b options are mutually exclusive. Please use only one of them" "${filename}.log"; then - echo "${filename_ext} SUCCESS" - exit 0 - fi - - echo "${filename_ext} FAILED" - exit 255 -} - -trap trap_err_onexit ERR - -# run test -one-infer -d ${driver_name} -b ${backend_name} -- -h> ${filename}.log 2>&1 - -echo "${filename_ext} FAILED" -exit 255 diff --git a/compiler/one-cmds/tests/one-infer_neg_005.test b/compiler/one-cmds/tests/one-infer_neg_005.test deleted file mode 100644 index 9074deb..0000000 --- a/compiler/one-cmds/tests/one-infer_neg_005.test +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# one-infer with invalid post process script - -filename_ext="$(basename -- $0)" -filename="${filename_ext%.*}" - -trap_err_onexit() -{ - return_code=$? - if grep -q "dummy-infer dummy output!!!" "${filename}.log"; then - # Case of succeed of inference driver but error after it - if [ "$return_code" -ne "0" ]; then - echo "${filename_ext} SUCCESS" - exit 0 - fi - fi - - echo "${filename_ext} FAILED" - rm -rf ../bin/dummy-infer - exit 255 -} - -trap trap_err_onexit ERR - -inputfile="sample.tvn" - -if [[ ! -s "${inputfile}" ]]; then - touch ${inputfile} -fi - -# copy dummy-infer to bin folder -cp dummy-infer ../bin/dummy-infer - -# run test -one-infer -b dummy --post-process "./one-infer-test-post-process.py" -- ${inputfile} > ${filename}.log 2>&1 - -rm -rf ../bin/dummy-infer -echo "${filename_ext} FAILED" -exit 255 diff --git a/compiler/one-cmds/tests/one-optimize_001.test b/compiler/one-cmds/tests/one-optimize_001.test index 4152fa3..94c9062 100644 --- a/compiler/one-cmds/tests/one-optimize_001.test +++ b/compiler/one-cmds/tests/one-optimize_001.test @@ -28,21 +28,13 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3-opt.circle" +rm -f ${filename}.log rm -rf ${outputfile} -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test > /dev/null 2>&1 - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi - # run test one-optimize --resolve_customop_add \ --input_path ${inputfile} \ ---output_path ${outputfile} > /dev/null 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-optimize_002.test b/compiler/one-cmds/tests/one-optimize_002.test index 58f792b..f0a8336 100644 --- a/compiler/one-cmds/tests/one-optimize_002.test +++ b/compiler/one-cmds/tests/one-optimize_002.test @@ -28,22 +28,14 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3-opt.circle" +rm -f ${filename}.log rm -rf ${outputfile} -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test > /dev/null 2>&1 - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi - # run test one-optimize --resolve_customop_add \ --change_outputs InceptionV3/Logits/SpatialSqueeze1 \ --input_path ${inputfile} \ ---output_path ${outputfile} > /dev/null 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-optimize_003.test b/compiler/one-cmds/tests/one-optimize_003.test new file mode 100644 index 0000000..11a9fb4 --- /dev/null +++ b/compiler/one-cmds/tests/one-optimize_003.test @@ -0,0 +1,62 @@ +#!/bin/bash + +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +filename_ext="$(basename -- $0)" +filename="${filename_ext%.*}" + +trap_err_onexit() +{ + echo "${filename_ext} FAILED" + exit 255 +} + +trap trap_err_onexit ERR + +inputfile="./UnidirSeqLSTM.tflite" +intermfile="./UnidirSeqLSTM.circle" +outputfile="./UnidirSeqLSTM-opt.circle" + +rm -f ${intermfile} +rm -f ${outputfile} +rm -f ${intermfile}.log +rm -f ${outputfile}.log + +# run test +one-import-tflite \ +--input_path ${inputfile} \ +--output_path ${intermfile} > /dev/null 2>&1 + +one-optimize --unroll_unidirseqlstm \ +--input_path ${intermfile} \ +--output_path ${outputfile} > /dev/null 2>&1 + +if [[ ! -s "${outputfile}" ]]; then + trap_err_onexit +fi + +# check UNIDIRECTIONAL_SEQUENCE_LSTM exist +circle-operator --code ${intermfile} > ${intermfile}.log 2>&1 +if ! grep -q "UNIDIRECTIONAL_SEQUENCE_LSTM" "${intermfile}.log"; then + trap_err_onexit +fi + +# check UNIDIRECTIONAL_SEQUENCE_LSTM absent +circle-operator --code ${outputfile} > ${outputfile}.log 2>&1 +if grep -q "UNIDIRECTIONAL_SEQUENCE_LSTM" "${outputfile}.log"; then + trap_err_onexit +fi + +echo "${filename_ext} SUCCESS" diff --git a/compiler/one-cmds/tests/one-optimize_neg_001.test b/compiler/one-cmds/tests/one-optimize_neg_001.test index c67e3d4..f88b4f8 100644 --- a/compiler/one-cmds/tests/one-optimize_neg_001.test +++ b/compiler/one-cmds/tests/one-optimize_neg_001.test @@ -36,7 +36,7 @@ inputfile="./inception_v3.pb" outputfile="./inception_v3-opt.circle" rm -rf ${outputfile} -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-optimize --resolve_customop_add \ diff --git a/compiler/one-cmds/tests/one-optimize_neg_002.test b/compiler/one-cmds/tests/one-optimize_neg_002.test index a1ef702..3f37e62 100644 --- a/compiler/one-cmds/tests/one-optimize_neg_002.test +++ b/compiler/one-cmds/tests/one-optimize_neg_002.test @@ -36,7 +36,7 @@ inputfile="./inception_v3.circletxt" outputfile="./inception_v3-opt.circle" rm -rf ${outputfile} -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-optimize --resolve_customop_add \ diff --git a/compiler/one-cmds/tests/one-optimize_neg_003.test b/compiler/one-cmds/tests/one-optimize_neg_003.test index 668a6c2..9d6483c 100644 --- a/compiler/one-cmds/tests/one-optimize_neg_003.test +++ b/compiler/one-cmds/tests/one-optimize_neg_003.test @@ -34,14 +34,7 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi +rm -f ${filename}.log # run test one-optimize --resolve_customop_add \ diff --git a/compiler/one-cmds/tests/one-pack_001.test b/compiler/one-cmds/tests/one-pack_001.test index 858d3c7..b05d0fe 100644 --- a/compiler/one-cmds/tests/one-pack_001.test +++ b/compiler/one-cmds/tests/one-pack_001.test @@ -28,21 +28,13 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfolder="nnpack" +rm -f ${filename}.log rm -rf ${outputfolder} -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test > /dev/null 2>&1 - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi - # run test one-pack \ -i ${inputfile} \ --o ${outputfolder} > /dev/null 2>&1 +-o ${outputfolder} > ${filename}.log 2>&1 if [[ ! -d "${outputfolder}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-partition_001.test b/compiler/one-cmds/tests/one-partition_001.test index a6fba07..ddd2ae0 100644 --- a/compiler/one-cmds/tests/one-partition_001.test +++ b/compiler/one-cmds/tests/one-partition_001.test @@ -30,6 +30,7 @@ inputfile="${testmodel}.circle" partfile="${testmodel}.part" outputfile="${testmodel}.conn.json" +rm -f ${filename}.log rm -rf ${testmodel}.000* rm -rf ${testmodel}.conn.* rm -rf ${testmodel}.*.log @@ -37,7 +38,7 @@ rm -rf ${testmodel}.*.log # run test one-partition \ --input_file ${inputfile} \ ---part_file ${partfile} > /dev/null 2>&1 +--part_file ${partfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-partition_neg_001.test b/compiler/one-cmds/tests/one-partition_neg_001.test index d54a94f..b594eba 100644 --- a/compiler/one-cmds/tests/one-partition_neg_001.test +++ b/compiler/one-cmds/tests/one-partition_neg_001.test @@ -26,6 +26,11 @@ trap_err_onexit() echo "${filename_ext} SUCCESS" exit 0 fi + # for debug build test + if grep -1 "std::runtime_error" "${filename}.log"; then + echo "${filename_ext} SUCCESS" + exit 0 + fi echo "${filename_ext} FAILED" exit 255 diff --git a/compiler/one-cmds/tests/one-profile_001.test b/compiler/one-cmds/tests/one-profile_001.test index 823a731..b4bdc72 100644 --- a/compiler/one-cmds/tests/one-profile_001.test +++ b/compiler/one-cmds/tests/one-profile_001.test @@ -29,6 +29,8 @@ trap trap_err_onexit ERR # copy help-profile to bin folder cp help-profile ../bin/help-profile +rm -f ${filename}.log + # run test one-profile -b help -- -h > ${filename}.log diff --git a/compiler/one-cmds/tests/one-profile_002.test b/compiler/one-cmds/tests/one-profile_002.test index 1365f9d..3b85dee 100644 --- a/compiler/one-cmds/tests/one-profile_002.test +++ b/compiler/one-cmds/tests/one-profile_002.test @@ -37,6 +37,8 @@ fi # copy dummy-profile to bin folder cp dummy-profile ../bin/dummy-profile +rm -f ${filename}.log + # run test one-profile -b dummy ${inputfile} > ${filename}.log diff --git a/compiler/one-cmds/tests/one-profile_003.test b/compiler/one-cmds/tests/one-profile_003.test index 39e2f06..ad98bb4 100644 --- a/compiler/one-cmds/tests/one-profile_003.test +++ b/compiler/one-cmds/tests/one-profile_003.test @@ -27,6 +27,8 @@ trap_err_onexit() trap trap_err_onexit ERR +rm -f ${filename}.log + # run test one-profile -h > ${filename}.log diff --git a/compiler/one-cmds/tests/one-profile_004.test b/compiler/one-cmds/tests/one-profile_004.test index 0c9e15f..7b77caa 100644 --- a/compiler/one-cmds/tests/one-profile_004.test +++ b/compiler/one-cmds/tests/one-profile_004.test @@ -38,6 +38,8 @@ fi # copy dummy-profile to bin folder cp dummy-profile ../bin/dummy-profile +rm -f ${filename}.log + # run test one-profile -C ${configfile} > ${filename}.log diff --git a/compiler/one-cmds/tests/one-profile_neg_001.test b/compiler/one-cmds/tests/one-profile_neg_001.test index 0de1e3b..f5b2dce 100644 --- a/compiler/one-cmds/tests/one-profile_neg_001.test +++ b/compiler/one-cmds/tests/one-profile_neg_001.test @@ -32,6 +32,8 @@ trap_err_onexit() trap trap_err_onexit ERR +rm -f ${filename}.log + # run test one-profile > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/one-quantize_001.test b/compiler/one-cmds/tests/one-quantize_001.test index 60a9de2..96804bf 100644 --- a/compiler/one-cmds/tests/one-quantize_001.test +++ b/compiler/one-cmds/tests/one-quantize_001.test @@ -28,24 +28,16 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3.quantized.circle" +rm -f ${filename}.log rm -rf ${outputfile} -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test > /dev/null 2>&1 - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi - # run test one-quantize \ --input_dtype float32 \ --quantized_dtype uint8 \ --input_path ./inception_v3.circle \ --input_data ./inception_v3_test_data.h5 \ ---output_path ./inception_v3.quantized.circle > /dev/null 2>&1 +--output_path ./inception_v3.quantized.circle > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-quantize_002.test b/compiler/one-cmds/tests/one-quantize_002.test index 08a8305..0ddaad1 100644 --- a/compiler/one-cmds/tests/one-quantize_002.test +++ b/compiler/one-cmds/tests/one-quantize_002.test @@ -28,23 +28,15 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3.random.quantized.circle" +rm -f ${filename}.log rm -rf ${outputfile} -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test > /dev/null 2>&1 - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi - # run test without input data one-quantize \ --input_dtype float32 \ --quantized_dtype uint8 \ ---input_path ./inception_v3.circle \ ---output_path ./inception_v3.random.quantized.circle > /dev/null 2>&1 +--input_path ${inputfile} \ +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-quantize_003.test b/compiler/one-cmds/tests/one-quantize_003.test index a387113..b96263e 100644 --- a/compiler/one-cmds/tests/one-quantize_003.test +++ b/compiler/one-cmds/tests/one-quantize_003.test @@ -28,16 +28,17 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3.list.quantized.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test with list-format input data (datalist.txt) one-quantize \ --input_dtype float32 \ --quantized_dtype uint8 \ ---input_path ./inception_v3.circle \ +--input_path ${inputfile} \ --input_data ./datalist.txt \ --input_data_format list \ ---output_path ${outputfile} > /dev/null 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-quantize_004.test b/compiler/one-cmds/tests/one-quantize_004.test index a489e8c..afb4080 100644 --- a/compiler/one-cmds/tests/one-quantize_004.test +++ b/compiler/one-cmds/tests/one-quantize_004.test @@ -28,6 +28,7 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3.directory.quantized.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test with directory-format input data (raw_files) @@ -37,7 +38,7 @@ one-quantize \ --input_path ${inputfile} \ --input_data ./raw_files \ --input_data_format directory \ ---output_path ${outputfile} > /dev/null 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-quantize_005.test b/compiler/one-cmds/tests/one-quantize_005.test index 8449df6..e5403e2 100644 --- a/compiler/one-cmds/tests/one-quantize_005.test +++ b/compiler/one-cmds/tests/one-quantize_005.test @@ -28,6 +28,7 @@ trap trap_err_onexit ERR inputfile="./inception_v3.mat.q8.circle" outputfile="./inception_v3.one-quantize_005.q8.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test with force_quantparam option @@ -37,7 +38,7 @@ one-quantize \ --scale 2.3 \ --zero_point 33 \ --input_path ${inputfile} \ ---output_path ${outputfile} > /dev/null 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-quantize_006.test b/compiler/one-cmds/tests/one-quantize_006.test index 92b9ebe..2521a96 100644 --- a/compiler/one-cmds/tests/one-quantize_006.test +++ b/compiler/one-cmds/tests/one-quantize_006.test @@ -28,6 +28,7 @@ trap trap_err_onexit ERR inputfile="./inception_v3.mat.q8.circle" outputfile="./inception_v3.one-quantize_006.q8.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test with force_quantparam option (multi tensors) @@ -40,7 +41,7 @@ one-quantize \ --scale 2.3 \ --zero_point 33 \ --input_path ${inputfile} \ ---output_path ${outputfile} > /dev/null 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-quantize_007.test b/compiler/one-cmds/tests/one-quantize_007.test index 34ae92d..771b857 100644 --- a/compiler/one-cmds/tests/one-quantize_007.test +++ b/compiler/one-cmds/tests/one-quantize_007.test @@ -28,17 +28,9 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3.random.quantized.q16.iq8.circle" +rm -f ${filename}.log rm -rf ${outputfile} -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test > /dev/null 2>&1 - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi - # run test without input data one-quantize \ --input_dtype float32 \ @@ -46,7 +38,7 @@ one-quantize \ --granularity channel \ --input_type uint8 \ --input_path ${inputfile} \ ---output_path ${outputfile} > /dev/null 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-quantize_008.test b/compiler/one-cmds/tests/one-quantize_008.test index aff6bcf..fcc4141 100644 --- a/compiler/one-cmds/tests/one-quantize_008.test +++ b/compiler/one-cmds/tests/one-quantize_008.test @@ -28,17 +28,9 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3.random.quantized.q16.oq8.circle" +rm -f ${filename}.log rm -rf ${outputfile} -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test > /dev/null 2>&1 - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi - # run test without input data one-quantize \ --input_dtype float32 \ @@ -46,7 +38,7 @@ one-quantize \ --granularity channel \ --output_type uint8 \ --input_path ${inputfile} \ ---output_path ${outputfile} > /dev/null 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-quantize_009.test b/compiler/one-cmds/tests/one-quantize_009.test index aa06703..0c13292 100644 --- a/compiler/one-cmds/tests/one-quantize_009.test +++ b/compiler/one-cmds/tests/one-quantize_009.test @@ -28,17 +28,9 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3.random.quantized.mixed.circle" +rm -f ${filename}.log rm -rf ${outputfile} -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test > /dev/null 2>&1 - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi - # run test without input data one-quantize \ --input_dtype float32 \ @@ -46,7 +38,7 @@ one-quantize \ --granularity channel \ --quant_config one-quantize_009.qconf.json \ --input_path ${inputfile} \ ---output_path ${outputfile} > /dev/null 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-quantize_010.test b/compiler/one-cmds/tests/one-quantize_010.test index 1095ba0..83d5f37 100644 --- a/compiler/one-cmds/tests/one-quantize_010.test +++ b/compiler/one-cmds/tests/one-quantize_010.test @@ -39,17 +39,9 @@ inputfile="./inception_v3.circle" outputfile="./inception_v3.one-quantize_010.q.circle" datafile="./inception_v3_test_data.h5" +rm -f ${filename}.log rm -rf ${outputfile} -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test > /dev/null 2>&1 - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi - # run test one-quantize \ --input_dtype float32 \ diff --git a/compiler/one-cmds/tests/one-quantize_011.test b/compiler/one-cmds/tests/one-quantize_011.test index 34d7f57..88abe4e 100644 --- a/compiler/one-cmds/tests/one-quantize_011.test +++ b/compiler/one-cmds/tests/one-quantize_011.test @@ -39,6 +39,7 @@ inputfile="./inception_v3.circle" outputfile="./inception_v3.one-quantize_011.q.circle" datafile="./inception_v3_test_data.h5" +rm -f ${filename}.log rm -rf ${outputfile} # run test diff --git a/compiler/one-cmds/tests/one-quantize_012.test b/compiler/one-cmds/tests/one-quantize_012.test index fba18ac..db3bb17 100644 --- a/compiler/one-cmds/tests/one-quantize_012.test +++ b/compiler/one-cmds/tests/one-quantize_012.test @@ -28,6 +28,7 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3.one-quantize_012.q.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test without input data @@ -37,7 +38,7 @@ one-quantize \ --granularity channel \ --quant_config one-quantize_012.qconf.json \ --input_path ${inputfile} \ ---output_path ${outputfile} > /dev/null 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-quantize_013.test b/compiler/one-cmds/tests/one-quantize_013.test index fd443d6..0d985ff 100644 --- a/compiler/one-cmds/tests/one-quantize_013.test +++ b/compiler/one-cmds/tests/one-quantize_013.test @@ -31,6 +31,7 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3.one-quantize_013.q.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test without input data @@ -39,7 +40,7 @@ one-quantize \ --input_dtype float32 \ --quant_config one-quantize_013.qconf.json \ --input_path ${inputfile} \ ---output_path ${outputfile} > /dev/null 2>&1 +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/one-quantize_014.test b/compiler/one-cmds/tests/one-quantize_014.test index 518c328..40b79fa 100644 --- a/compiler/one-cmds/tests/one-quantize_014.test +++ b/compiler/one-cmds/tests/one-quantize_014.test @@ -41,6 +41,7 @@ inputfile="./inception_v3.circle" outputfile="./inception_v3.one-quantize_014.q.circle" datadir="./raw_files/" +rm -f ${filename}.log rm -rf ${outputfile} # run test diff --git a/compiler/one-cmds/tests/one-quantize_015.test b/compiler/one-cmds/tests/one-quantize_015.test index bb45b57..a069601 100644 --- a/compiler/one-cmds/tests/one-quantize_015.test +++ b/compiler/one-cmds/tests/one-quantize_015.test @@ -30,6 +30,7 @@ trap trap_err_onexit ERR inputfile="./inception_v3.mat.q8.circle" outputfile="./inception_v3.one-quantize_015.fq.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test diff --git a/compiler/one-cmds/tests/one-quantize_016.test b/compiler/one-cmds/tests/one-quantize_016.test new file mode 100644 index 0000000..8bf0dd0 --- /dev/null +++ b/compiler/one-cmds/tests/one-quantize_016.test @@ -0,0 +1,70 @@ +#!/bin/bash + +# Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +filename_ext="$(basename -- $0)" +filename="${filename_ext%.*}" + +# TODO Resolve circledump not found +# https://github.com/Samsung/ONE/issues/10550 +if ! command -v circledump &> /dev/null +then + echo "${filename_ext} SKIPPED" + exit 0 +fi + +trap_err_onexit() +{ + echo "${filename_ext} FAILED" + exit 255 +} + +trap trap_err_onexit ERR + +inputfile="./reshape_matmul.circle" +outputfile="./reshape_matmul.one-quantize_016.circle" + +rm -f ${filename}.log +rm -f ${filename}.first.cdump +rm -f ${filename}.second.cdump +rm -f ${outputfile} + +# run test with different input_type +one-quantize \ +--input_dtype float32 \ +--quantized_dtype uint8 \ +--granularity channel \ +--input_type uint8,int16 \ +--input_path ${inputfile} \ +--output_path ${outputfile} > ${filename}.log 2>&1 + +if [[ ! -s "${outputfile}" ]]; then + trap_err_onexit +fi + +circledump ${outputfile} | grep serving_default_l.1:0$ > ${filename}.first.cdump +circledump ${outputfile} | grep serving_default_r.1:0$ > ${filename}.second.cdump + +# check dtype of the first input (uint8) +if ! grep -q "UINT8" "${filename}.first.cdump"; then + trap_err_onexit +fi + +# check dtype of the second input (int16) +if ! grep -q "INT16" "${filename}.second.cdump"; then + trap_err_onexit +fi + +echo "${filename_ext} SUCCESS" diff --git a/compiler/one-cmds/tests/one-quantize_neg_001.test b/compiler/one-cmds/tests/one-quantize_neg_001.test index 4e42faa..dd2617f 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_001.test +++ b/compiler/one-cmds/tests/one-quantize_neg_001.test @@ -37,19 +37,10 @@ inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" rm -rf ${outputfile} -rm -rf ${outputfile}.log +rm -f ${filename}.log # test begin -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi - one-quantize \ --input_dtype float64 \ --quantized_dtype uint8 \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_002.test b/compiler/one-cmds/tests/one-quantize_neg_002.test index 3c0d676..c5ad693 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_002.test +++ b/compiler/one-cmds/tests/one-quantize_neg_002.test @@ -37,16 +37,7 @@ inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" rm -rf ${outputfile} -rm -rf ${outputfile}.log - -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_003.test b/compiler/one-cmds/tests/one-quantize_neg_003.test index 156ed84..e8b9648 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_003.test +++ b/compiler/one-cmds/tests/one-quantize_neg_003.test @@ -36,16 +36,7 @@ inputfile="./inception_v3.circle" outputfile="./inception_v3.quantized.circle" rm -rf ${outputfile} -rm -rf ${outputfile}.log - -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_004.test b/compiler/one-cmds/tests/one-quantize_neg_004.test index 5a78a81..eb50564 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_004.test +++ b/compiler/one-cmds/tests/one-quantize_neg_004.test @@ -36,16 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./inception_v3_test_data.h5" outputfile="." -rm -rf ${outputfile}.log - -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_005.test b/compiler/one-cmds/tests/one-quantize_neg_005.test index adcaf66..6f5f489 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_005.test +++ b/compiler/one-cmds/tests/one-quantize_neg_005.test @@ -36,7 +36,7 @@ inputfile="./while_3.pbtxt" inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_006.test b/compiler/one-cmds/tests/one-quantize_neg_006.test index 42890a8..f5c965f 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_006.test +++ b/compiler/one-cmds/tests/one-quantize_neg_006.test @@ -36,7 +36,7 @@ inputfile="./inception_v2.circle" inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_007.test b/compiler/one-cmds/tests/one-quantize_neg_007.test index f096dd6..50b5495 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_007.test +++ b/compiler/one-cmds/tests/one-quantize_neg_007.test @@ -36,16 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./inception_v3.circle" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log - -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_008.test b/compiler/one-cmds/tests/one-quantize_neg_008.test index 70752cf..f2675f4 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_008.test +++ b/compiler/one-cmds/tests/one-quantize_neg_008.test @@ -36,16 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log - -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_009.test b/compiler/one-cmds/tests/one-quantize_neg_009.test index 595dbdc..2190160 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_009.test +++ b/compiler/one-cmds/tests/one-quantize_neg_009.test @@ -36,16 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log - -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_010.test b/compiler/one-cmds/tests/one-quantize_neg_010.test index 6236caf..bd7c0dd 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_010.test +++ b/compiler/one-cmds/tests/one-quantize_neg_010.test @@ -36,16 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log - -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_011.test b/compiler/one-cmds/tests/one-quantize_neg_011.test index cf6a3c0..14552f0 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_011.test +++ b/compiler/one-cmds/tests/one-quantize_neg_011.test @@ -36,16 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log - -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_012.test b/compiler/one-cmds/tests/one-quantize_neg_012.test index 646c9d3..963783c 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_012.test +++ b/compiler/one-cmds/tests/one-quantize_neg_012.test @@ -36,16 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log - -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_013.test b/compiler/one-cmds/tests/one-quantize_neg_013.test index 768c032..4b81c87 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_013.test +++ b/compiler/one-cmds/tests/one-quantize_neg_013.test @@ -36,16 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log - -# to create inception_v3.circle -if [[ ! -s ${inputfile} ]]; then - /bin/bash one-import_001.test >> /dev/null - return_code=$? - if [[ ${return_code} != 0 ]]; then - trap_err_onexit - fi -fi +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_014.test b/compiler/one-cmds/tests/one-quantize_neg_014.test index af1f8bb..9115370 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_014.test +++ b/compiler/one-cmds/tests/one-quantize_neg_014.test @@ -36,7 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./datalist.txt" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_015.test b/compiler/one-cmds/tests/one-quantize_neg_015.test index 73864cc..1acb20f 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_015.test +++ b/compiler/one-cmds/tests/one-quantize_neg_015.test @@ -36,7 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_016.test b/compiler/one-cmds/tests/one-quantize_neg_016.test index 4090b15..b419ff5 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_016.test +++ b/compiler/one-cmds/tests/one-quantize_neg_016.test @@ -36,7 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./datalist.txt" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_017.test b/compiler/one-cmds/tests/one-quantize_neg_017.test index b666ace..5326f18 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_017.test +++ b/compiler/one-cmds/tests/one-quantize_neg_017.test @@ -36,7 +36,7 @@ inputfile="./inception_v3.circle" inputdata="./inception_v3_test_data.h5" outputfile="./inception_v3.quantized.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_018.test b/compiler/one-cmds/tests/one-quantize_neg_018.test index 6937caf..6470efc 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_018.test +++ b/compiler/one-cmds/tests/one-quantize_neg_018.test @@ -35,7 +35,7 @@ trap trap_err_onexit ERR inputfile="./inception_v3.mat.q8.circle" outputfile="./inception_v3.neg_018.q8.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_019.test b/compiler/one-cmds/tests/one-quantize_neg_019.test index e182edf..9f6e35f 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_019.test +++ b/compiler/one-cmds/tests/one-quantize_neg_019.test @@ -35,7 +35,7 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3.quantized.neg_019.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_020.test b/compiler/one-cmds/tests/one-quantize_neg_020.test index 27b11c3..46a4f9d 100644 --- a/compiler/one-cmds/tests/one-quantize_neg_020.test +++ b/compiler/one-cmds/tests/one-quantize_neg_020.test @@ -35,7 +35,7 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./inception_v3.quantized.neg_020.circle" -rm -rf ${outputfile}.log +rm -f ${filename}.log # run test one-quantize \ diff --git a/compiler/one-cmds/tests/one-quantize_neg_021.test b/compiler/one-cmds/tests/one-quantize_neg_021.test new file mode 100644 index 0000000..31a3182 --- /dev/null +++ b/compiler/one-cmds/tests/one-quantize_neg_021.test @@ -0,0 +1,50 @@ +#!/bin/bash + +# Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Wrong number of input_type in one-quantize + +filename_ext="$(basename -- $0)" +filename="${filename_ext%.*}" + +trap_err_onexit() +{ + if grep -q "Invalid number of input dtype" "${filename}.log"; then + echo "${filename_ext} SUCCESS" + exit 0 + fi + + echo "${filename_ext} FAILED" + exit 255 +} + +trap trap_err_onexit ERR + +inputfile="./reshape_matmul.circle" +outputfile="./reshape_matmul.quantized.neg_021.circle" + +rm -f ${filename}.log + +# run test with wrong number of input_type +one-quantize \ +--input_dtype float32 \ +--quantized_dtype uint8 \ +--granularity channel \ +--input_type uint8,int16,uint8 \ +--input_path ${inputfile} \ +--output_path ${outputfile} > ${filename}.log 2>&1 + +echo "${filename_ext} FAILED" +exit 255 diff --git a/compiler/one-cmds/tests/onecc_001.cfg b/compiler/one-cmds/tests/onecc_001.cfg index 4776ea8..f331010 100644 --- a/compiler/one-cmds/tests/onecc_001.cfg +++ b/compiler/one-cmds/tests/onecc_001.cfg @@ -9,12 +9,12 @@ one-codegen=False [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_001.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v2 [one-optimize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_001.circle output_path=inception_v3.opt.circle diff --git a/compiler/one-cmds/tests/onecc_001.test b/compiler/one-cmds/tests/onecc_001.test index ece0aa2..73fbdd6 100644 --- a/compiler/one-cmds/tests/onecc_001.test +++ b/compiler/one-cmds/tests/onecc_001.test @@ -30,8 +30,11 @@ trap trap_err_onexit ERR configfile="onecc_001.cfg" outputfile="inception_v3.opt.circle" +rm -f ${filename}.log +rm -f ${outputfile} + # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_002.cfg b/compiler/one-cmds/tests/onecc_002.cfg index fedaf11..0338ccb 100644 --- a/compiler/one-cmds/tests/onecc_002.cfg +++ b/compiler/one-cmds/tests/onecc_002.cfg @@ -9,14 +9,14 @@ one-codegen=False [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_002.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v2 [one-optimize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_002.circle output_path=inception_v3.opt.circle [one-pack] diff --git a/compiler/one-cmds/tests/onecc_002.test b/compiler/one-cmds/tests/onecc_002.test index 4075b79..f154f66 100644 --- a/compiler/one-cmds/tests/onecc_002.test +++ b/compiler/one-cmds/tests/onecc_002.test @@ -30,8 +30,11 @@ trap trap_err_onexit ERR configfile="onecc_002.cfg" outputfile="inception_v3_pkg" +rm -f ${filename}.log +rm -rf ${outputfile} + # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_003.cfg b/compiler/one-cmds/tests/onecc_003.cfg index d0b3a0c..da5c73e 100644 --- a/compiler/one-cmds/tests/onecc_003.cfg +++ b/compiler/one-cmds/tests/onecc_003.cfg @@ -9,13 +9,13 @@ one-codegen=False [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_003.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v1 [one-quantize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_003.circle output_path=inception_v3.quantized.circle input_data=inception_v3_test_data.h5 diff --git a/compiler/one-cmds/tests/onecc_003.test b/compiler/one-cmds/tests/onecc_003.test index 9ee6877..140a99f 100644 --- a/compiler/one-cmds/tests/onecc_003.test +++ b/compiler/one-cmds/tests/onecc_003.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="onecc_003.cfg" outputfile="inception_v3.quantized.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_004.cfg b/compiler/one-cmds/tests/onecc_004.cfg index 614ba13..e155430 100644 --- a/compiler/one-cmds/tests/onecc_004.cfg +++ b/compiler/one-cmds/tests/onecc_004.cfg @@ -9,7 +9,7 @@ one-codegen=True [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_004.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 @@ -17,4 +17,4 @@ converter_version=v1 [one-codegen] backend=dummy -command=-o sample.tvn inception_v3.circle +command=-o sample.tvn inception_v3.onecc_004.circle diff --git a/compiler/one-cmds/tests/onecc_004.test b/compiler/one-cmds/tests/onecc_004.test index 2eaffa6..b532d19 100644 --- a/compiler/one-cmds/tests/onecc_004.test +++ b/compiler/one-cmds/tests/onecc_004.test @@ -31,13 +31,14 @@ trap trap_err_onexit ERR configfile="onecc_004.cfg" outputfile="sample.tvn" +rm -f ${filename}.log rm -rf ${outputfile} # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_005.cfg b/compiler/one-cmds/tests/onecc_005.cfg index 83a8230..ff4ed84 100644 --- a/compiler/one-cmds/tests/onecc_005.cfg +++ b/compiler/one-cmds/tests/onecc_005.cfg @@ -9,10 +9,10 @@ one-codegen=True [one-import-tflite] input_path=inception_v3.tflite -output_path=inception_v3.circle +output_path=inception_v3.onecc_005.circle [one-optimize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_005.circle output_path=inception_v3.opt.circle [one-codegen] diff --git a/compiler/one-cmds/tests/onecc_005.test b/compiler/one-cmds/tests/onecc_005.test index a98c3e4..48c81e5 100644 --- a/compiler/one-cmds/tests/onecc_005.test +++ b/compiler/one-cmds/tests/onecc_005.test @@ -31,13 +31,14 @@ trap trap_err_onexit ERR configfile="onecc_005.cfg" outputfile="sample.tvn" +rm -f ${filename}.log rm -rf ${outputfile} # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_006.cfg b/compiler/one-cmds/tests/onecc_006.cfg index a7c8938..dd58e6b 100644 --- a/compiler/one-cmds/tests/onecc_006.cfg +++ b/compiler/one-cmds/tests/onecc_006.cfg @@ -9,14 +9,14 @@ one-codegen=True [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_006.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v1 [one-optimize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_006.circle output_path=inception_v3.opt.circle [one-quantize] diff --git a/compiler/one-cmds/tests/onecc_006.test b/compiler/one-cmds/tests/onecc_006.test index 096c861..451d34f 100644 --- a/compiler/one-cmds/tests/onecc_006.test +++ b/compiler/one-cmds/tests/onecc_006.test @@ -31,13 +31,14 @@ trap trap_err_onexit ERR configfile="onecc_006.cfg" outputfile="sample.tvn" +rm -f ${filename}.log rm -rf ${outputfile} # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_007.cfg b/compiler/one-cmds/tests/onecc_007.cfg index 8775f8e..2d3ecac 100644 --- a/compiler/one-cmds/tests/onecc_007.cfg +++ b/compiler/one-cmds/tests/onecc_007.cfg @@ -9,14 +9,14 @@ one-codegen=False [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_007.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v1 [one-optimize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_007.circle output_path=inception_v3.opt.circle [one-quantize] diff --git a/compiler/one-cmds/tests/onecc_007.test b/compiler/one-cmds/tests/onecc_007.test index 883a9a7..ee3c262 100644 --- a/compiler/one-cmds/tests/onecc_007.test +++ b/compiler/one-cmds/tests/onecc_007.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="onecc_007.cfg" outputfile="inception_v3_pkg" +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_008.test b/compiler/one-cmds/tests/onecc_008.test index 5bd7831..f2d25f1 100644 --- a/compiler/one-cmds/tests/onecc_008.test +++ b/compiler/one-cmds/tests/onecc_008.test @@ -31,13 +31,14 @@ trap trap_err_onexit ERR configfile="onecc_008.cfg" outputfile="test_onnx_model.bin" +rm -f ${filename}.log rm -rf ${outputfile} # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_009.test b/compiler/one-cmds/tests/onecc_009.test index 921ebeb..0b4537e 100644 --- a/compiler/one-cmds/tests/onecc_009.test +++ b/compiler/one-cmds/tests/onecc_009.test @@ -31,13 +31,14 @@ trap trap_err_onexit ERR configfile="onecc_009.cfg" outputfile="onnx_conv2d_conv2d.bin" +rm -f ${filename}.log rm -rf ${outputfile} # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_010.test b/compiler/one-cmds/tests/onecc_010.test index 87d7436..85192ed 100644 --- a/compiler/one-cmds/tests/onecc_010.test +++ b/compiler/one-cmds/tests/onecc_010.test @@ -31,11 +31,12 @@ configfile="onecc_010.cfg" outputfile="inception_v3.alt.circle" intermfile="inception_v3.alt.tflite" +rm -f ${filename}.log rm -rf ${outputfile} rm -rf ${intermfile} # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_011.test b/compiler/one-cmds/tests/onecc_011.test index ff8c109..eeb59b4 100644 --- a/compiler/one-cmds/tests/onecc_011.test +++ b/compiler/one-cmds/tests/onecc_011.test @@ -31,11 +31,12 @@ configfile="onecc_011.cfg" outputfile="test_onnx_model.circle" intermfile="test_onnx_model.tflite" +rm -f ${filename}.log rm -rf ${outputfile} rm -rf ${intermfile} # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_012.cfg b/compiler/one-cmds/tests/onecc_012.cfg index 498b602..92f61a1 100644 --- a/compiler/one-cmds/tests/onecc_012.cfg +++ b/compiler/one-cmds/tests/onecc_012.cfg @@ -9,14 +9,14 @@ one-codegen=False [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_012.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v1 [one-quantize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_012.circle output_path=inception_v3.list.quantized.circle input_data=datalist.txt input_data_format=list diff --git a/compiler/one-cmds/tests/onecc_012.test b/compiler/one-cmds/tests/onecc_012.test index 2c71e30..f5abe94 100644 --- a/compiler/one-cmds/tests/onecc_012.test +++ b/compiler/one-cmds/tests/onecc_012.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="onecc_012.cfg" outputfile="inception_v3.list.quantized.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_013.cfg b/compiler/one-cmds/tests/onecc_013.cfg index f909ab7..4ed6871 100644 --- a/compiler/one-cmds/tests/onecc_013.cfg +++ b/compiler/one-cmds/tests/onecc_013.cfg @@ -1,6 +1,6 @@ [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_13.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 diff --git a/compiler/one-cmds/tests/onecc_013.test b/compiler/one-cmds/tests/onecc_013.test index bcb2720..bca0cbe 100644 --- a/compiler/one-cmds/tests/onecc_013.test +++ b/compiler/one-cmds/tests/onecc_013.test @@ -28,12 +28,13 @@ trap_err_onexit() trap trap_err_onexit ERR configfile="onecc_013.cfg" -outputfile="inception_v3.circle" +outputfile="inception_v3.onecc_13.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc import tf -C ${configfile} > /dev/null 2>&1 +onecc import tf -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_014.cfg b/compiler/one-cmds/tests/onecc_014.cfg index c9a37f1..8478be7 100644 --- a/compiler/one-cmds/tests/onecc_014.cfg +++ b/compiler/one-cmds/tests/onecc_014.cfg @@ -1,6 +1,6 @@ [one-import-tflite] input_path=inception_v3.tflite -output_path=inception_v3.circle +output_path=inception_v3.onecc_014.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 diff --git a/compiler/one-cmds/tests/onecc_014.test b/compiler/one-cmds/tests/onecc_014.test index 9d42cc3..3e93a69 100644 --- a/compiler/one-cmds/tests/onecc_014.test +++ b/compiler/one-cmds/tests/onecc_014.test @@ -28,12 +28,13 @@ trap_err_onexit() trap trap_err_onexit ERR configfile="onecc_014.cfg" -outputfile="inception_v3.circle" +outputfile="inception_v3.onecc_014.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc import tflite -C ${configfile} > /dev/null 2>&1 +onecc import tflite -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_015.test b/compiler/one-cmds/tests/onecc_015.test index d74ba32..d92bf78 100644 --- a/compiler/one-cmds/tests/onecc_015.test +++ b/compiler/one-cmds/tests/onecc_015.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="onecc_015.cfg" outputfile="bcq.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc import bcq -C ${configfile} > /dev/null 2>&1 +onecc import bcq -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_016.test b/compiler/one-cmds/tests/onecc_016.test index 582cd06..09e5143 100644 --- a/compiler/one-cmds/tests/onecc_016.test +++ b/compiler/one-cmds/tests/onecc_016.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="onecc_016.cfg" outputfile="test_onnx_model.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc import onnx -C ${configfile} > /dev/null 2>&1 +onecc import onnx -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_017.test b/compiler/one-cmds/tests/onecc_017.test index d996a28..359a6e7 100644 --- a/compiler/one-cmds/tests/onecc_017.test +++ b/compiler/one-cmds/tests/onecc_017.test @@ -35,10 +35,11 @@ if [[ ! -s "${inputfile}" ]]; then trap_err_onexit fi +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc optimize -i ${inputfile} -o ${outputfile} > /dev/null 2>&1 +onecc optimize -i ${inputfile} -o ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_018.test b/compiler/one-cmds/tests/onecc_018.test index ed97603..cd2ac59 100644 --- a/compiler/one-cmds/tests/onecc_018.test +++ b/compiler/one-cmds/tests/onecc_018.test @@ -36,10 +36,11 @@ if [[ ! -s "${inputfile}" && ! -s "${inputdata}" ]]; then trap_err_onexit fi +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc quantize -i ${inputfile} -o ${outputfile} -d ${inputdata} > /dev/null 2>&1 +onecc quantize -i ${inputfile} -o ${outputfile} -d ${inputdata} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_019.test b/compiler/one-cmds/tests/onecc_019.test index 9d66e3a..23e0e3f 100644 --- a/compiler/one-cmds/tests/onecc_019.test +++ b/compiler/one-cmds/tests/onecc_019.test @@ -35,10 +35,11 @@ if [[ ! -s "${inputfile}" ]]; then trap_err_onexit fi +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc pack -i ${inputfile} -o ${outputfile} > /dev/null 2>&1 +onecc pack -i ${inputfile} -o ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_020.test b/compiler/one-cmds/tests/onecc_020.test index 9afa910..d914e9b 100644 --- a/compiler/one-cmds/tests/onecc_020.test +++ b/compiler/one-cmds/tests/onecc_020.test @@ -38,13 +38,14 @@ if [[ ! -f "${inputfile}" ]]; then trap_err_onexit fi +rm -f ${filename}.log rm -rf ${outputfile} # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile # run test -onecc codegen -b dummy -o ${outputfile} ${inputfile} > /dev/null 2>&1 +onecc codegen -b dummy -o ${outputfile} ${inputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_021.test b/compiler/one-cmds/tests/onecc_021.test index 906ed18..85e9aa7 100644 --- a/compiler/one-cmds/tests/onecc_021.test +++ b/compiler/one-cmds/tests/onecc_021.test @@ -33,8 +33,10 @@ configfile="onecc_021.cfg" # copy dummy-profile to bin folder cp dummy-profile ../bin/dummy-profile +rm -f ${filename}.log + # run test -onecc -C ${configfile} > ${filename}.log +onecc -C ${configfile} > ${filename}.log 2>&1 rm -rf ../bin/dummy-profile diff --git a/compiler/one-cmds/tests/onecc_022.test b/compiler/one-cmds/tests/onecc_022.test index 3aaa26f..6f5d565 100644 --- a/compiler/one-cmds/tests/onecc_022.test +++ b/compiler/one-cmds/tests/onecc_022.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="onecc_022.cfg" outputfile="inception_v3.onecc_022.q8.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_023.test b/compiler/one-cmds/tests/onecc_023.test index 50b3b1c..9448f40 100644 --- a/compiler/one-cmds/tests/onecc_023.test +++ b/compiler/one-cmds/tests/onecc_023.test @@ -30,10 +30,11 @@ trap trap_err_onexit ERR configfile="onecc_023.cfg" outputfile="inception_v3.onecc_023.q16.iq8.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_024.cfg b/compiler/one-cmds/tests/onecc_024.cfg index 7b4b1a8..053758f 100644 --- a/compiler/one-cmds/tests/onecc_024.cfg +++ b/compiler/one-cmds/tests/onecc_024.cfg @@ -10,13 +10,13 @@ one-codegen=False [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_024.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v1 [one-optimize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_024.circle output_path=inception_v3.opt.circle make_batchnorm_gamma_positive=False diff --git a/compiler/one-cmds/tests/onecc_024.test b/compiler/one-cmds/tests/onecc_024.test index 1f5daa1..d1e1d92 100644 --- a/compiler/one-cmds/tests/onecc_024.test +++ b/compiler/one-cmds/tests/onecc_024.test @@ -55,6 +55,7 @@ trap trap_err_onexit ERR configfile="onecc_024.cfg" outputfile="inception_v3.opt.circle" +rm -f ${filename}.log rm -rf ${outputfile} if [ ! -d "../optimization" ]; then @@ -69,7 +70,7 @@ LUCI_LOG=5 onecc -C ${configfile} -OONECC_024 > ${filename}.log 2>&1 clean_envir -if grep -q "MakeBatchNormGammaPositivePass" "${filename}.log"; then +if ! grep -q "MakeBatchNormGammaPositivePass" "${filename}.log"; then echo "${filename_ext} SUCCESS" exit 0 fi diff --git a/compiler/one-cmds/tests/onecc_025.cfg b/compiler/one-cmds/tests/onecc_025.cfg index 4776ea8..02e54b0 100644 --- a/compiler/one-cmds/tests/onecc_025.cfg +++ b/compiler/one-cmds/tests/onecc_025.cfg @@ -9,12 +9,12 @@ one-codegen=False [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_025.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v2 [one-optimize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_025.circle output_path=inception_v3.opt.circle diff --git a/compiler/one-cmds/tests/onecc_025.test b/compiler/one-cmds/tests/onecc_025.test index 396f40c..fff9440 100644 --- a/compiler/one-cmds/tests/onecc_025.test +++ b/compiler/one-cmds/tests/onecc_025.test @@ -30,8 +30,11 @@ trap trap_err_onexit ERR configfile="onecc_001.cfg" outputfile="inception_v3.opt.circle" +rm -f ${filename}.log +rm -f ${outputfile} + # run test -onecc -C ${configfile} > /dev/null 2>&1 +onecc -C ${configfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_026.test b/compiler/one-cmds/tests/onecc_026.test index 84cfa41..cd09e11 100644 --- a/compiler/one-cmds/tests/onecc_026.test +++ b/compiler/one-cmds/tests/onecc_026.test @@ -38,6 +38,7 @@ trap trap_err_onexit ERR configfile="onecc_026.cfg" outputfile="inception_v3.onecc_026.q.circle" +rm -f ${filename}.log rm -rf ${outputfile} # run test diff --git a/compiler/one-cmds/tests/onecc_027.cfg b/compiler/one-cmds/tests/onecc_027.cfg index d3f6b5e..1b9a2b9 100644 --- a/compiler/one-cmds/tests/onecc_027.cfg +++ b/compiler/one-cmds/tests/onecc_027.cfg @@ -11,5 +11,5 @@ one-profile=False one-infer=True [one-infer] -backend=dummy +driver=dummy-infer command=test_onnx_model.bin diff --git a/compiler/one-cmds/tests/onecc_027.test b/compiler/one-cmds/tests/onecc_027.test index e727359..54f0d25 100644 --- a/compiler/one-cmds/tests/onecc_027.test +++ b/compiler/one-cmds/tests/onecc_027.test @@ -33,8 +33,10 @@ configfile="onecc_027.cfg" # copy dummy-infer to bin folder cp dummy-infer ../bin/dummy-infer +rm -f ${filename}.log + # run test -onecc -C ${configfile} > ${filename}.log +onecc -C ${configfile} > ${filename}.log 2>&1 rm -rf ../bin/dummy-infer diff --git a/compiler/one-cmds/tests/onecc_028.test b/compiler/one-cmds/tests/onecc_028.test index 10ce158..ea23579 100644 --- a/compiler/one-cmds/tests/onecc_028.test +++ b/compiler/one-cmds/tests/onecc_028.test @@ -30,6 +30,7 @@ trap trap_err_onexit ERR workflowfile="onecc_028.workflow.json" outputfile="inception_v3_pkg" +rm -f ${filename}.log rm -rf ${outputfile} # run test diff --git a/compiler/one-cmds/tests/onecc_028.workflow.json b/compiler/one-cmds/tests/onecc_028.workflow.json index 84bfd01..9836482 100644 --- a/compiler/one-cmds/tests/onecc_028.workflow.json +++ b/compiler/one-cmds/tests/onecc_028.workflow.json @@ -12,7 +12,7 @@ "one-cmd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_028.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", @@ -22,7 +22,7 @@ "OPTIMIZE": { "one-cmd": "one-optimize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_028.circle", "output_path": "inception_v3.opt.circle" } }, diff --git a/compiler/one-cmds/tests/onecc_029.test b/compiler/one-cmds/tests/onecc_029.test index 9bab1a1..9fb0ec3 100644 --- a/compiler/one-cmds/tests/onecc_029.test +++ b/compiler/one-cmds/tests/onecc_029.test @@ -31,6 +31,7 @@ workflowfile="onecc_029.workflow.json" outputfile="inception_v3.quantized.circle" rm -rf ${outputfile} +rm -f ${filename}.log # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_029.workflow.json b/compiler/one-cmds/tests/onecc_029.workflow.json index 65c9ea6..826888d 100644 --- a/compiler/one-cmds/tests/onecc_029.workflow.json +++ b/compiler/one-cmds/tests/onecc_029.workflow.json @@ -11,7 +11,7 @@ "one-cmd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_029.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", @@ -21,7 +21,7 @@ "QUANTIZE": { "one-cmd": "one-quantize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_029.circle", "output_path": "inception_v3.quantized.circle", "input_data": "inception_v3_test_data.h5" } diff --git a/compiler/one-cmds/tests/onecc_030.test b/compiler/one-cmds/tests/onecc_030.test index c0aa56a..88f0ab8 100644 --- a/compiler/one-cmds/tests/onecc_030.test +++ b/compiler/one-cmds/tests/onecc_030.test @@ -32,6 +32,7 @@ workflowfile="onecc_030.workflow.json" outputfile="sample.tvn" rm -rf ${outputfile} +rm -f ${filename}.log # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile diff --git a/compiler/one-cmds/tests/onecc_030.workflow.json b/compiler/one-cmds/tests/onecc_030.workflow.json index 111a1b0..e4a2467 100644 --- a/compiler/one-cmds/tests/onecc_030.workflow.json +++ b/compiler/one-cmds/tests/onecc_030.workflow.json @@ -11,7 +11,7 @@ "one-cmd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_030.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", @@ -22,7 +22,7 @@ "one-cmd": "one-codegen", "commands": { "backend": "dummy", - "command": "-o sample.tvn inception_v3.circle" + "command": "-o sample.tvn inception_v3.onecc_030.circle" } } } diff --git a/compiler/one-cmds/tests/onecc_031.test b/compiler/one-cmds/tests/onecc_031.test index 7a1c670..33c9664 100644 --- a/compiler/one-cmds/tests/onecc_031.test +++ b/compiler/one-cmds/tests/onecc_031.test @@ -32,6 +32,7 @@ workflowfile="onecc_031.workflow.json" outputfile="sample.tvn" rm -rf ${outputfile} +rm -f ${filename}.log # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile diff --git a/compiler/one-cmds/tests/onecc_031.workflow.json b/compiler/one-cmds/tests/onecc_031.workflow.json index 83d52b9..7018ca6 100644 --- a/compiler/one-cmds/tests/onecc_031.workflow.json +++ b/compiler/one-cmds/tests/onecc_031.workflow.json @@ -12,13 +12,13 @@ "one-cmd": "one-import-tflite", "commands": { "input_path": "inception_v3.tflite", - "output_path": "inception_v3.circle" + "output_path": "inception_v3.onecc_031.circle" } }, "optimize": { "one-cmd": "one-optimize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_031.circle", "output_path": "inception_v3.opt.circle" } }, diff --git a/compiler/one-cmds/tests/onecc_032.test b/compiler/one-cmds/tests/onecc_032.test index 89b6c41..5884f57 100644 --- a/compiler/one-cmds/tests/onecc_032.test +++ b/compiler/one-cmds/tests/onecc_032.test @@ -32,6 +32,7 @@ workflowfile="onecc_032.workflow.json" outputfile="sample.tvn" rm -rf ${outputfile} +rm -f ${filename}.log # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile diff --git a/compiler/one-cmds/tests/onecc_032.workflow.json b/compiler/one-cmds/tests/onecc_032.workflow.json index 08d3f0f..7a794c8 100644 --- a/compiler/one-cmds/tests/onecc_032.workflow.json +++ b/compiler/one-cmds/tests/onecc_032.workflow.json @@ -13,20 +13,20 @@ "one-cmd": "one-import-tflite", "commands": { "input_path": "inception_v3.tflite", - "output_path": "inception_v3.circle" + "output_path": "inception_v3.onecc_032.circle" } }, "optimize": { "one-cmd": "one-optimize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_032.circle", "output_path": "inception_v3.opt.circle" } }, "quantize": { "one-cmd": "one-quantize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_032.circle", "output_path": "inception_v3.quantized.circle", "input_data": "inception_v3_test_data.h5" } diff --git a/compiler/one-cmds/tests/onecc_033.test b/compiler/one-cmds/tests/onecc_033.test index 635582f..b2655fe 100644 --- a/compiler/one-cmds/tests/onecc_033.test +++ b/compiler/one-cmds/tests/onecc_033.test @@ -31,6 +31,7 @@ workflowfile="onecc_033.workflow.json" outputfile="inception_v3_pkg" rm -rf ${outputfile} +rm -f ${filename}.log # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_033.workflow.json b/compiler/one-cmds/tests/onecc_033.workflow.json index 01233ff..2edd5a8 100644 --- a/compiler/one-cmds/tests/onecc_033.workflow.json +++ b/compiler/one-cmds/tests/onecc_033.workflow.json @@ -13,20 +13,20 @@ "one-cmd": "one-import-tflite", "commands": { "input_path": "inception_v3.tflite", - "output_path": "inception_v3.circle" + "output_path": "inception_v3.onecc_033.circle" } }, "optimize": { "one-cmd": "one-optimize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_033.circle", "output_path": "inception_v3.opt.circle" } }, "quantize": { "one-cmd": "one-quantize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_033.circle", "output_path": "inception_v3.quantized.circle", "input_data": "inception_v3_test_data.h5" } diff --git a/compiler/one-cmds/tests/onecc_034.test b/compiler/one-cmds/tests/onecc_034.test index e766548..9c88dff 100644 --- a/compiler/one-cmds/tests/onecc_034.test +++ b/compiler/one-cmds/tests/onecc_034.test @@ -32,6 +32,7 @@ workflowfile="onecc_034.workflow.json" outputfile="onnx_conv2d_conv2d.bin" rm -rf ${outputfile} +rm -f ${filename}.log # copy dummy-compile to bin folder cp dummy-compile ../bin/dummy-compile diff --git a/compiler/one-cmds/tests/onecc_035.test b/compiler/one-cmds/tests/onecc_035.test index 762cdd3..851da65 100644 --- a/compiler/one-cmds/tests/onecc_035.test +++ b/compiler/one-cmds/tests/onecc_035.test @@ -33,6 +33,7 @@ intermfile="inception_v3.alt.tflite" rm -rf ${outputfile} rm -rf ${intermfile} +rm -f ${filename}.log # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_036.test b/compiler/one-cmds/tests/onecc_036.test index 865255e..00f4641 100644 --- a/compiler/one-cmds/tests/onecc_036.test +++ b/compiler/one-cmds/tests/onecc_036.test @@ -33,6 +33,7 @@ intermfile="test_onnx_model.tflite" rm -rf ${outputfile} rm -rf ${intermfile} +rm -f ${filename}.log # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_037.test b/compiler/one-cmds/tests/onecc_037.test index 52ea9e4..596bbfd 100644 --- a/compiler/one-cmds/tests/onecc_037.test +++ b/compiler/one-cmds/tests/onecc_037.test @@ -31,6 +31,7 @@ workflowfile="onecc_037.workflow.json" outputfile="inception_v3.opt.circle" rm -rf ${outputfile} +rm -f ${filename}.log # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_037.workflow.json b/compiler/one-cmds/tests/onecc_037.workflow.json index 3317fb2..ebd6b34 100644 --- a/compiler/one-cmds/tests/onecc_037.workflow.json +++ b/compiler/one-cmds/tests/onecc_037.workflow.json @@ -11,7 +11,7 @@ "one-cmd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_037.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", @@ -21,7 +21,7 @@ "OPTIMIZE": { "one-cmd": "one-optimize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_037.circle", "output_path": "inception_v3.opt.circle" } } diff --git a/compiler/one-cmds/tests/onecc_038.test b/compiler/one-cmds/tests/onecc_038.test index 6b8f7cf..41f6333 100644 --- a/compiler/one-cmds/tests/onecc_038.test +++ b/compiler/one-cmds/tests/onecc_038.test @@ -31,6 +31,7 @@ workflowfile="onecc_038.workflow.json" outputfile="inception_v3.list.quantized.circle" rm -rf ${outputfile} +rm -f ${filename}.log # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_038.workflow.json b/compiler/one-cmds/tests/onecc_038.workflow.json index 5ac515d..d31045e 100644 --- a/compiler/one-cmds/tests/onecc_038.workflow.json +++ b/compiler/one-cmds/tests/onecc_038.workflow.json @@ -11,7 +11,7 @@ "one-cmd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_038.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", @@ -21,7 +21,7 @@ "QUANTIZE": { "one-cmd": "one-quantize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_038.circle", "output_path": "inception_v3.list.quantized.circle", "input_data": "datalist.txt", "input_data_format": "list" diff --git a/compiler/one-cmds/tests/onecc_039.test b/compiler/one-cmds/tests/onecc_039.test index 7db9d90..b922636 100644 --- a/compiler/one-cmds/tests/onecc_039.test +++ b/compiler/one-cmds/tests/onecc_039.test @@ -41,6 +41,7 @@ workflowfile="onecc_039.workflow.json" outputfile="inception_v3.onecc_039.q.circle" rm -rf ${outputfile} +rm -f ${filename}.log # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_040.cfg b/compiler/one-cmds/tests/onecc_040.cfg index 4776ea8..b9f39fd 100644 --- a/compiler/one-cmds/tests/onecc_040.cfg +++ b/compiler/one-cmds/tests/onecc_040.cfg @@ -9,12 +9,12 @@ one-codegen=False [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_040.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v2 [one-optimize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_040.circle output_path=inception_v3.opt.circle diff --git a/compiler/one-cmds/tests/onecc_040.test b/compiler/one-cmds/tests/onecc_040.test index 2f75677..4d23025 100644 --- a/compiler/one-cmds/tests/onecc_040.test +++ b/compiler/one-cmds/tests/onecc_040.test @@ -31,6 +31,7 @@ workflowfile="onecc_040.workflow.json" outputfile="inception_v3.opt.circle" rm -rf ${outputfile} +rm -f ${filename}.log # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_041.test b/compiler/one-cmds/tests/onecc_041.test index 791dd12..c504e69 100644 --- a/compiler/one-cmds/tests/onecc_041.test +++ b/compiler/one-cmds/tests/onecc_041.test @@ -45,6 +45,7 @@ outputfile2="inception_v3.opt.circle" cp dummy-inferV2 ../bin/dummy-inferV2 rm -rf ${outputfile1} {outputfile2} +rm -f ${filename}.log # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_041.workflow.json b/compiler/one-cmds/tests/onecc_041.workflow.json index 7dfc1c6..e19494c 100644 --- a/compiler/one-cmds/tests/onecc_041.workflow.json +++ b/compiler/one-cmds/tests/onecc_041.workflow.json @@ -42,7 +42,7 @@ "one-cmd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_041.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", @@ -52,7 +52,7 @@ "OPTIMIZE": { "one-cmd": "one-optimize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_041.circle", "output_path": "inception_v3.opt.circle" } } diff --git a/compiler/one-cmds/tests/onecc_042.cfg b/compiler/one-cmds/tests/onecc_042.cfg new file mode 100644 index 0000000..988c768 --- /dev/null +++ b/compiler/one-cmds/tests/onecc_042.cfg @@ -0,0 +1,9 @@ +[Environment] +SPM_SIZE=256KB + +[onecc] +one-codegen=True + +[one-codegen] +backend=dummyEnv +command=dummy_env.bin diff --git a/compiler/one-cmds/tests/onecc_042.test b/compiler/one-cmds/tests/onecc_042.test new file mode 100644 index 0000000..c8a15fa --- /dev/null +++ b/compiler/one-cmds/tests/onecc_042.test @@ -0,0 +1,55 @@ +#!/bin/bash + +# Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# one-codegen with Environment section + +filename_ext="$(basename -- $0)" +filename="${filename_ext%.*}" +outputfile="dummy_env.bin" + +trap_err_onexit() +{ + echo "${filename_ext} FAILED" + rm -rf ../bin/dummyEnv-compile + rm -rf ${outputfile} + exit 255 +} + +trap trap_err_onexit ERR + +configfile="onecc_042.cfg" + +rm -rf ${outputfile} +rm -rf ${filename}.log + +# copy dummyEnv-compile to bin folder +cp dummyEnv-compile ../bin/dummyEnv-compile + +# run test +onecc -C ${configfile} > ${filename}.log 2>&1 + +if [[ ! -s "${outputfile}" ]]; then + trap_err_onexit +fi + +if grep -q "SPM_SIZE=256KB" "${outputfile}"; then + echo "${filename_ext} SUCCESS" + rm -rf ../bin/dummyEnv-compile + rm -rf ${outputfile} + exit 0 +fi + +trap_err_onexit diff --git a/compiler/one-cmds/tests/onecc_043.cfg b/compiler/one-cmds/tests/onecc_043.cfg new file mode 100644 index 0000000..7d5ee87 --- /dev/null +++ b/compiler/one-cmds/tests/onecc_043.cfg @@ -0,0 +1,14 @@ +[onecc] +one-import-tf=False +one-import-tflite=False +one-import-bcq=False +one-import-onnx=False +one-optimize=True +one-quantize=False +one-pack=False +one-codegen=False +include=O1 + +[one-optimize] +input_path=inception_v3.circle +output_path=inception_v3.onecc_043.opt.circle diff --git a/compiler/one-cmds/tests/onecc_043.test b/compiler/one-cmds/tests/onecc_043.test new file mode 100644 index 0000000..f99039e --- /dev/null +++ b/compiler/one-cmds/tests/onecc_043.test @@ -0,0 +1,61 @@ +#!/bin/bash + +# Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test for "O1=True" option in onecc config file + +filename_ext="$(basename -- $0)" +filename="${filename_ext%.*}" + +trap_err_onexit() +{ + echo "${filename_ext} FAILED" + exit 255 +} + +trap trap_err_onexit ERR + +configfile="onecc_043.cfg" +outputfile="inception_v3.onecc_043.opt.circle" + +rm -f ${filename}.log +rm -rf ${outputfile} + +# run test +onecc -C ${configfile} > ${filename}.log 2>&1 + +if [[ ! -s "${outputfile}" ]]; then + trap_err_onexit +fi + +# O1.list is dynamically created from onelib/export_constant.py +readarray -t O1_OPTS < "O1.list" +readarray -t NO_O1_OPTS < "non-O1.list" + +for opt in "${O1_OPTS[@]}" +do + if ! grep -q ${opt} ${outputfile}.log; then + trap_err_onexit + fi +done + +for no_opt in "${NO_O1_OPTS[@]}" +do + if grep -q ${no_opt} ${outputfile}.log; then + trap_err_onexit + fi +done + +echo "${filename_ext} SUCCESS" diff --git a/compiler/one-cmds/tests/onecc_044.cfg b/compiler/one-cmds/tests/onecc_044.cfg new file mode 100644 index 0000000..f7cdde8 --- /dev/null +++ b/compiler/one-cmds/tests/onecc_044.cfg @@ -0,0 +1,20 @@ +[onecc] +one-import-tf=False +one-import-tflite=False +one-import-bcq=False +one-import-onnx=True +one-optimize=True +one-quantize=False +one-pack=False +one-codegen=False +include=O1 + +[one-import-onnx] +input_path=test_onnx_model.onnx +output_path=test_onnx_model.circle + +[one-optimize] +input_path=test_onnx_model.circle +output_path=test_onnx_model.onecc_044.opt.circle +convert_nchw_to_nhwc=True +fold_add_v2=False diff --git a/compiler/one-cmds/tests/onecc_044.test b/compiler/one-cmds/tests/onecc_044.test new file mode 100644 index 0000000..1706cf9 --- /dev/null +++ b/compiler/one-cmds/tests/onecc_044.test @@ -0,0 +1,74 @@ +#!/bin/bash + +# Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test for "O1=True" option with other options + +filename_ext="$(basename -- $0)" +filename="${filename_ext%.*}" + +trap_err_onexit() +{ + echo "${filename_ext} FAILED" + exit 255 +} + +trap trap_err_onexit ERR + +configfile="onecc_044.cfg" +outputfile="test_onnx_model.onecc_044.opt.circle" + +rm -f ${filename}.log +rm -rf ${outputfile} + +# run test +onecc -C ${configfile} > ${filename}.log 2>&1 + +if [[ ! -s "${outputfile}" ]]; then + trap_err_onexit +fi + +readarray -t OPTS < "O1.list" +readarray -t NO_OPTS < "non-O1.list" + +OPTS+=("convert_nchw_to_nhwc") +for i in "${!NO_OPTS[@]}"; do + if [[ ${NO_OPTS[i]} = "convert_nchw_to_nhwc" ]]; then + unset 'NO_OPTS[i]' + fi +done + +NO_OPTS+=("fold_add_v2") +for i in "${!OPTS[@]}"; do + if [[ ${OPTS[i]} = "fold_add_v2" ]]; then + unset 'OPTS[i]' + fi +done + +for opt in "${OPTS[@]}" +do + if ! grep -q ${opt} ${outputfile}.log; then + trap_err_onexit + fi +done + +for no_opt in "${NO_OPTS[@]}" +do + if grep -q ${no_opt} ${outputfile}.log; then + trap_err_onexit + fi +done + +echo "${filename_ext} SUCCESS" diff --git a/compiler/one-cmds/tests/onecc_045.cfg b/compiler/one-cmds/tests/onecc_045.cfg new file mode 100644 index 0000000..d0ee39f --- /dev/null +++ b/compiler/one-cmds/tests/onecc_045.cfg @@ -0,0 +1,13 @@ +[onecc] +one-import-tf=False +one-import-tflite=False +one-import-bcq=False +one-optimize=False +one-quantize=True +one-pack=False +one-codegen=False + +[one-quantize] +input_path=reshape_matmul.circle +output_path=reshape_matmul.onecc_045.q.circle +input_type=uint8,int16 diff --git a/compiler/one-cmds/tests/onecc_045.test b/compiler/one-cmds/tests/onecc_045.test new file mode 100644 index 0000000..74e21f6 --- /dev/null +++ b/compiler/one-cmds/tests/onecc_045.test @@ -0,0 +1,65 @@ +#!/bin/bash + +# Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +filename_ext="$(basename -- $0)" +filename="${filename_ext%.*}" + +# TODO Resolve circledump not found +# https://github.com/Samsung/ONE/issues/10550 +if ! command -v circledump &> /dev/null +then + echo "${filename_ext} SKIPPED" + exit 0 +fi + +trap_err_onexit() +{ + echo "${filename_ext} FAILED" + exit 255 +} + +trap trap_err_onexit ERR + +inputfile="./reshape_matmul.circle" +configfile="onecc_045.cfg" +outputfile="reshape_matmul.onecc_045.q.circle" + +rm -f ${filename}.log +rm -f ${filename}.first.cdump +rm -f ${filename}.second.cdump +rm -f ${outputfile} + +# run test +onecc -C ${configfile} > ${filename}.log 2>&1 + +if [[ ! -s "${outputfile}" ]]; then + trap_err_onexit +fi + +circledump ${outputfile} | grep serving_default_l.1:0$ > ${filename}.first.cdump +circledump ${outputfile} | grep serving_default_r.1:0$ > ${filename}.second.cdump + +# check dtype of the first input (uint8) +if ! grep -q "UINT8" "${filename}.first.cdump"; then + trap_err_onexit +fi + +# check dtype of the second input (int16) +if ! grep -q "INT16" "${filename}.second.cdump"; then + trap_err_onexit +fi + +echo "${filename_ext} SUCCESS" diff --git a/compiler/one-cmds/tests/onecc_neg_001.test b/compiler/one-cmds/tests/onecc_neg_001.test index 0456a42..fe83e35 100644 --- a/compiler/one-cmds/tests/onecc_neg_001.test +++ b/compiler/one-cmds/tests/onecc_neg_001.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile="onecc_neg_001.cfg" +rm -f ${filename}.log + # run test onecc -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_002.cfg b/compiler/one-cmds/tests/onecc_neg_002.cfg index 46347ea..e597a39 100644 --- a/compiler/one-cmds/tests/onecc_neg_002.cfg +++ b/compiler/one-cmds/tests/onecc_neg_002.cfg @@ -9,12 +9,12 @@ one-codegen=False [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_neg_002.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v2 [one-optimize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_neg_002.circle output_path=inception_v3.opt.circle diff --git a/compiler/one-cmds/tests/onecc_neg_002.test b/compiler/one-cmds/tests/onecc_neg_002.test index 7c81bab..04918ab 100644 --- a/compiler/one-cmds/tests/onecc_neg_002.test +++ b/compiler/one-cmds/tests/onecc_neg_002.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile="onecc_neg_002.cfg" +rm -f ${filename}.log + # run test onecc -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_003.cfg b/compiler/one-cmds/tests/onecc_neg_003.cfg index fa027cb..e4718ef 100644 --- a/compiler/one-cmds/tests/onecc_neg_003.cfg +++ b/compiler/one-cmds/tests/onecc_neg_003.cfg @@ -1,13 +1,13 @@ [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_neg_003.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v2 [one-optimize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_neg_003.circle output_path=inception_v3.opt.circle [one-pack] diff --git a/compiler/one-cmds/tests/onecc_neg_003.test b/compiler/one-cmds/tests/onecc_neg_003.test index b36bf8c..4c64c5a 100644 --- a/compiler/one-cmds/tests/onecc_neg_003.test +++ b/compiler/one-cmds/tests/onecc_neg_003.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile="onecc_neg_003.cfg" +rm -f ${filename}.log + # run test onecc -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_004.cfg b/compiler/one-cmds/tests/onecc_neg_004.cfg index dac8cb7..53e8191 100644 --- a/compiler/one-cmds/tests/onecc_neg_004.cfg +++ b/compiler/one-cmds/tests/onecc_neg_004.cfg @@ -9,14 +9,14 @@ one-codegen=False [one-import-tf] input_path=inception_v3.pb -output_path=inception_v3.circle +output_path=inception_v3.onecc_neg_004.circle input_arrays=input input_shapes=1,299,299,3 output_arrays=InceptionV3/Predictions/Reshape_1 converter_version=v2 [one-optimize] -input_path=inception_v3.circle +input_path=inception_v3.onecc_neg_004.circle output_path=inception_v3.opt.circle [one-optimize] diff --git a/compiler/one-cmds/tests/onecc_neg_004.test b/compiler/one-cmds/tests/onecc_neg_004.test index 5efb7c6..30fb345 100644 --- a/compiler/one-cmds/tests/onecc_neg_004.test +++ b/compiler/one-cmds/tests/onecc_neg_004.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile="onecc_neg_004.cfg" +rm -f ${filename}.log + # run test onecc -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_005.test b/compiler/one-cmds/tests/onecc_neg_005.test index 684d940..1d22485 100644 --- a/compiler/one-cmds/tests/onecc_neg_005.test +++ b/compiler/one-cmds/tests/onecc_neg_005.test @@ -33,6 +33,7 @@ intermfile="inception_v3.alt.tflite" rm -rf ${outputfile} rm -rf ${intermfile} +rm -f ${filename}.log # run test onecc -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_006.test b/compiler/one-cmds/tests/onecc_neg_006.test index 27b4f50..194a7ea 100644 --- a/compiler/one-cmds/tests/onecc_neg_006.test +++ b/compiler/one-cmds/tests/onecc_neg_006.test @@ -33,6 +33,7 @@ intermfile="test_onnx_model.tflite" rm -rf ${outputfile} rm -rf ${intermfile} +rm -f ${filename}.log # run test onecc -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_007.test b/compiler/one-cmds/tests/onecc_neg_007.test index bc32f23..264a74e 100644 --- a/compiler/one-cmds/tests/onecc_neg_007.test +++ b/compiler/one-cmds/tests/onecc_neg_007.test @@ -32,6 +32,8 @@ trap_err_onexit() trap trap_err_onexit ERR +rm -f ${filename}.log + # run test onecc wronginput > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_008.test b/compiler/one-cmds/tests/onecc_neg_008.test index ff39302..67b2c6f 100644 --- a/compiler/one-cmds/tests/onecc_neg_008.test +++ b/compiler/one-cmds/tests/onecc_neg_008.test @@ -32,6 +32,8 @@ trap_err_onexit() trap trap_err_onexit ERR +rm -f ${filename}.log + # run test onecc > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_009.test b/compiler/one-cmds/tests/onecc_neg_009.test index 54dd129..26ad7da 100644 --- a/compiler/one-cmds/tests/onecc_neg_009.test +++ b/compiler/one-cmds/tests/onecc_neg_009.test @@ -62,6 +62,8 @@ touch ../optimization/OONECC_NEG_009.cfg configfile=".." +rm -f ${filename}.log + # run test onecc -C ${configfile} -OONECC_NEG_009 > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_010.test b/compiler/one-cmds/tests/onecc_neg_010.test index ddad5e6..9860590 100644 --- a/compiler/one-cmds/tests/onecc_neg_010.test +++ b/compiler/one-cmds/tests/onecc_neg_010.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile=".." +rm -f ${filename}.log + # run test onecc -C ${configfile} -OONECC_NEG_010 > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_011.test b/compiler/one-cmds/tests/onecc_neg_011.test index 3f043a7..5df6fc8 100644 --- a/compiler/one-cmds/tests/onecc_neg_011.test +++ b/compiler/one-cmds/tests/onecc_neg_011.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile="onecc_neg_011.cfg" +rm -f ${filename}.log + # run test onecc -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_012.cfg b/compiler/one-cmds/tests/onecc_neg_012.cfg index fdc73ef..7324091 100644 --- a/compiler/one-cmds/tests/onecc_neg_012.cfg +++ b/compiler/one-cmds/tests/onecc_neg_012.cfg @@ -11,5 +11,4 @@ one-infer=True [one-infer] driver=dummy-infer -backend=dummy command="dummy arguments" diff --git a/compiler/one-cmds/tests/onecc_neg_012.test b/compiler/one-cmds/tests/onecc_neg_012.test index 9feca5f..45ed13d 100644 --- a/compiler/one-cmds/tests/onecc_neg_012.test +++ b/compiler/one-cmds/tests/onecc_neg_012.test @@ -14,14 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Check driver and backend option is mutually exclusive +# Check the case when driver does not exist filename_ext="$(basename -- $0)" filename="${filename_ext%.*}" trap_err_onexit() { - if grep -q "\-d and -b options are mutually exclusive" "${filename}.log"; then + if grep -q "dummy-infer not found" "${filename}.log"; then echo "${filename_ext} SUCCESS" exit 0 fi @@ -34,6 +34,8 @@ trap trap_err_onexit ERR configfile="onecc_neg_012.cfg" +rm -f ${filename}.log + # run test onecc -C ${configfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_013.test b/compiler/one-cmds/tests/onecc_neg_013.test index 0dd8a0f..95ac3c9 100644 --- a/compiler/one-cmds/tests/onecc_neg_013.test +++ b/compiler/one-cmds/tests/onecc_neg_013.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR workflowfile="onecc_neg_013.workflow.json" +rm -f ${filename}.log + # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_014.test b/compiler/one-cmds/tests/onecc_neg_014.test index 2ed5dcb..704acfa 100644 --- a/compiler/one-cmds/tests/onecc_neg_014.test +++ b/compiler/one-cmds/tests/onecc_neg_014.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR workflowfile="onecc_neg_014.workflow.json" +rm -f ${filename}.log + # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_015.test b/compiler/one-cmds/tests/onecc_neg_015.test index 079ba67..d15a2f3 100644 --- a/compiler/one-cmds/tests/onecc_neg_015.test +++ b/compiler/one-cmds/tests/onecc_neg_015.test @@ -35,6 +35,8 @@ trap trap_err_onexit ERR workflowfile="onecc_neg_015.workflow.json" +rm -f ${filename}.log + # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_016.test b/compiler/one-cmds/tests/onecc_neg_016.test index c52763f..23964e9 100644 --- a/compiler/one-cmds/tests/onecc_neg_016.test +++ b/compiler/one-cmds/tests/onecc_neg_016.test @@ -35,6 +35,8 @@ trap trap_err_onexit ERR workflowfile="onecc_neg_016.workflow.json" +rm -f ${filename}.log + # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_017.test b/compiler/one-cmds/tests/onecc_neg_017.test index 2f173d2..a345d62 100644 --- a/compiler/one-cmds/tests/onecc_neg_017.test +++ b/compiler/one-cmds/tests/onecc_neg_017.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR workflowfile="onecc_neg_017.workflow.json" +rm -f ${filename}.log + # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_018.test b/compiler/one-cmds/tests/onecc_neg_018.test index bc2297e..b70fae4 100644 --- a/compiler/one-cmds/tests/onecc_neg_018.test +++ b/compiler/one-cmds/tests/onecc_neg_018.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR workflowfile="onecc_neg_018.workflow.json" +rm -f ${filename}.log + # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_018.workflow.json b/compiler/one-cmds/tests/onecc_neg_018.workflow.json index 58cb88e..e0754d3 100644 --- a/compiler/one-cmds/tests/onecc_neg_018.workflow.json +++ b/compiler/one-cmds/tests/onecc_neg_018.workflow.json @@ -13,7 +13,7 @@ "one-cmd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_neg_018.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", diff --git a/compiler/one-cmds/tests/onecc_neg_019.test b/compiler/one-cmds/tests/onecc_neg_019.test index 11ef3a9..430438d 100644 --- a/compiler/one-cmds/tests/onecc_neg_019.test +++ b/compiler/one-cmds/tests/onecc_neg_019.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR workflowfile="onecc_neg_019.workflow.json" +rm -f ${filename}.log + # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_019.workflow.json b/compiler/one-cmds/tests/onecc_neg_019.workflow.json index aedeeec..995c6bf 100644 --- a/compiler/one-cmds/tests/onecc_neg_019.workflow.json +++ b/compiler/one-cmds/tests/onecc_neg_019.workflow.json @@ -10,7 +10,7 @@ "one-cmddddddddd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_neg_019.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", diff --git a/compiler/one-cmds/tests/onecc_neg_020.test b/compiler/one-cmds/tests/onecc_neg_020.test index 7f5073d..b86a231 100644 --- a/compiler/one-cmds/tests/onecc_neg_020.test +++ b/compiler/one-cmds/tests/onecc_neg_020.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR workflowfile="onecc_neg_020.workflow.json" +rm -f ${filename}.log + # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_020.workflow.json b/compiler/one-cmds/tests/onecc_neg_020.workflow.json index d3446d3..89f0d59 100644 --- a/compiler/one-cmds/tests/onecc_neg_020.workflow.json +++ b/compiler/one-cmds/tests/onecc_neg_020.workflow.json @@ -10,7 +10,7 @@ "one-cmd": "one-import-tf", "commandssssssssss": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_neg_020.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", diff --git a/compiler/one-cmds/tests/onecc_neg_021.test b/compiler/one-cmds/tests/onecc_neg_021.test index e9d4baa..ef023b1 100644 --- a/compiler/one-cmds/tests/onecc_neg_021.test +++ b/compiler/one-cmds/tests/onecc_neg_021.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR workflowfile="onecc_neg_021.workflow.json" +rm -f ${filename}.log + # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_021.workflow.json b/compiler/one-cmds/tests/onecc_neg_021.workflow.json index 6d21111..c326d41 100644 --- a/compiler/one-cmds/tests/onecc_neg_021.workflow.json +++ b/compiler/one-cmds/tests/onecc_neg_021.workflow.json @@ -14,7 +14,7 @@ "one-cmd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_neg_021.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", @@ -33,7 +33,7 @@ "one-cmd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_neg_021.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", diff --git a/compiler/one-cmds/tests/onecc_neg_022.test b/compiler/one-cmds/tests/onecc_neg_022.test index 5400717..002908a 100644 --- a/compiler/one-cmds/tests/onecc_neg_022.test +++ b/compiler/one-cmds/tests/onecc_neg_022.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR workflowfile="onecc_neg_022.workflow.json" +rm -f ${filename}.log + # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_022.workflow.json b/compiler/one-cmds/tests/onecc_neg_022.workflow.json index 2e056ac..1f45081 100644 --- a/compiler/one-cmds/tests/onecc_neg_022.workflow.json +++ b/compiler/one-cmds/tests/onecc_neg_022.workflow.json @@ -45,7 +45,7 @@ "one-cmd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_neg_022.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", @@ -55,7 +55,7 @@ "OPTIMIZE": { "one-cmd": "one-optimize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_neg_022.circle", "output_path": "inception_v3.opt.circle" } } diff --git a/compiler/one-cmds/tests/onecc_neg_023.test b/compiler/one-cmds/tests/onecc_neg_023.test index 09717e8..436c7c3 100644 --- a/compiler/one-cmds/tests/onecc_neg_023.test +++ b/compiler/one-cmds/tests/onecc_neg_023.test @@ -34,6 +34,8 @@ trap trap_err_onexit ERR workflowfile="onecc_neg_023.workflow.json" +rm -f ${filename}.log + # run test onecc -W ${workflowfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/onecc_neg_023.workflow.json b/compiler/one-cmds/tests/onecc_neg_023.workflow.json index 056e704..2d763f0 100644 --- a/compiler/one-cmds/tests/onecc_neg_023.workflow.json +++ b/compiler/one-cmds/tests/onecc_neg_023.workflow.json @@ -11,7 +11,7 @@ "one-cmd": "one-import-tf", "commands": { "input_path": "inception_v3.pb", - "output_path": "inception_v3.circle", + "output_path": "inception_v3.onecc_neg_023.circle", "input_arrays": "input", "input_shapes": "1,299,299,3", "output_arrays": "InceptionV3/Predictions/Reshape_1", @@ -21,7 +21,7 @@ "OPTIMIZE": { "one-cmd": "one-optimize", "commands": { - "input_path": "inception_v3.circle", + "input_path": "inception_v3.onecc_neg_023.circle", "output_path": "inception_v3.opt.circle", "change_outputs": "non_existing_node_name" } diff --git a/compiler/one-cmds/tests/onecc_neg_024.cfg b/compiler/one-cmds/tests/onecc_neg_024.cfg new file mode 100644 index 0000000..0e9ebc6 --- /dev/null +++ b/compiler/one-cmds/tests/onecc_neg_024.cfg @@ -0,0 +1,20 @@ +[onecc] +one-import-tf=False +one-import-tflite=False +one-import-bcq=False +one-import-onnx=True +one-optimize=True +one-quantize=False +one-pack=False +one-codegen=False +include=O # invalid (too short group option) + +[one-import-onnx] +input_path=test_onnx_model.onnx +output_path=test_onnx_model.circle + +[one-optimize] +input_path=test_onnx_model.circle +output_path=test_onnx_model.onecc_neg_024.opt.circle +convert_nchw_to_nhwc=True +fold_add_v2=False diff --git a/compiler/one-cmds/tests/onecc_neg_024.test b/compiler/one-cmds/tests/onecc_neg_024.test new file mode 100644 index 0000000..16952ba --- /dev/null +++ b/compiler/one-cmds/tests/onecc_neg_024.test @@ -0,0 +1,43 @@ +#!/bin/bash + +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# invalid group option + +filename_ext="$(basename -- $0)" +filename="${filename_ext%.*}" + +trap_err_onexit() +{ + if grep -q "Invalid group option" "${filename}.log"; then + echo "${filename_ext} SUCCESS" + exit 0 + fi + + echo "${filename_ext} FAILED" + exit 255 +} + +trap trap_err_onexit ERR + +configfile="onecc_neg_024.cfg" + +rm -f ${filename}.log + +# run test +onecc -C ${configfile} > ${filename}.log 2>&1 + +echo "${filename_ext} FAILED" +exit 255 diff --git a/compiler/one-cmds/tests/onecc_neg_025.cfg b/compiler/one-cmds/tests/onecc_neg_025.cfg new file mode 100644 index 0000000..e41c6e6 --- /dev/null +++ b/compiler/one-cmds/tests/onecc_neg_025.cfg @@ -0,0 +1,20 @@ +[onecc] +one-import-tf=False +one-import-tflite=False +one-import-bcq=False +one-import-onnx=True +one-optimize=True +one-quantize=False +one-pack=False +one-codegen=False +include=A1 # invalid (must start with 'O') + +[one-import-onnx] +input_path=test_onnx_model.onnx +output_path=test_onnx_model.circle + +[one-optimize] +input_path=test_onnx_model.circle +output_path=test_onnx_model.onecc_neg_024.opt.circle +convert_nchw_to_nhwc=True +fold_add_v2=False diff --git a/compiler/one-cmds/tests/onecc_neg_025.test b/compiler/one-cmds/tests/onecc_neg_025.test new file mode 100644 index 0000000..4ddc310 --- /dev/null +++ b/compiler/one-cmds/tests/onecc_neg_025.test @@ -0,0 +1,43 @@ +#!/bin/bash + +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# invalid group option + +filename_ext="$(basename -- $0)" +filename="${filename_ext%.*}" + +trap_err_onexit() +{ + if grep -q "Invalid group option" "${filename}.log"; then + echo "${filename_ext} SUCCESS" + exit 0 + fi + + echo "${filename_ext} FAILED" + exit 255 +} + +trap trap_err_onexit ERR + +configfile="onecc_neg_025.cfg" + +rm -f ${filename}.log + +# run test +onecc -C ${configfile} > ${filename}.log 2>&1 + +echo "${filename_ext} FAILED" +exit 255 diff --git a/compiler/one-cmds/tests/onecc_neg_026.cfg b/compiler/one-cmds/tests/onecc_neg_026.cfg new file mode 100644 index 0000000..2efddb1 --- /dev/null +++ b/compiler/one-cmds/tests/onecc_neg_026.cfg @@ -0,0 +1,13 @@ +[onecc] +one-import-tf=False +one-import-tflite=False +one-import-bcq=False +one-optimize=False +one-quantize=True +one-pack=False +one-codegen=False + +[one-quantize] +input_path=reshape_matmul.circle +output_path=reshape_matmul.onecc_045.q.circle +input_type=uint8,int16,uint8 diff --git a/compiler/one-cmds/tests/onecc_neg_026.test b/compiler/one-cmds/tests/onecc_neg_026.test new file mode 100644 index 0000000..f90c1ec --- /dev/null +++ b/compiler/one-cmds/tests/onecc_neg_026.test @@ -0,0 +1,44 @@ +#!/bin/bash + +# Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Wrong number of input_type in one-quantize + +filename_ext="$(basename -- $0)" +filename="${filename_ext%.*}" + +trap_err_onexit() +{ + if grep -q "Invalid number of input dtype" "${filename}.log"; then + echo "${filename_ext} SUCCESS" + exit 0 + fi + + echo "${filename_ext} FAILED" + exit 255 +} + +trap trap_err_onexit ERR + +inputfile="./reshape_matmul.circle" +configfile="onecc_neg_026.cfg" + +rm -f ${filename}.log + +# run test +onecc -C ${configfile} > ${filename}.log 2>&1 + +echo "${filename_ext} FAILED" +exit 255 diff --git a/compiler/one-cmds/tests/prepare_test_materials.sh b/compiler/one-cmds/tests/prepare_test_materials.sh index c171cfe..97bc6eb 100644 --- a/compiler/one-cmds/tests/prepare_test_materials.sh +++ b/compiler/one-cmds/tests/prepare_test_materials.sh @@ -21,34 +21,34 @@ pushd $SCRIPT_PATH > /dev/null if [[ ! -s "inception_v3.pb" ]]; then rm -rf inception_v3_2018_04_27.tgz - wget https://storage.googleapis.com/download.tensorflow.org/models/tflite/model_zoo/upload_20180427/inception_v3_2018_04_27.tgz + wget -nv https://storage.googleapis.com/download.tensorflow.org/models/tflite/model_zoo/upload_20180427/inception_v3_2018_04_27.tgz tar zxvf inception_v3_2018_04_27.tgz fi if [[ ! -s "while_3.pbtxt" ]]; then rm -rf while_3.zip - wget https://github.com/Samsung/ONE/files/5095630/while_3.zip + wget -nv https://github.com/Samsung/ONE/files/5095630/while_3.zip unzip while_3.zip # https://github.com/Samsung/ONE/issues/4155#issuecomment-689320297 fi if [[ ! -s "mobilenet_test_data.h5" ]]; then rm -rf mobilenet_test_data.zip - wget https://github.com/Samsung/ONE/files/5139460/mobilenet_test_data.zip + wget -nv https://github.com/Samsung/ONE/files/5139460/mobilenet_test_data.zip unzip mobilenet_test_data.zip # https://github.com/Samsung/ONE/issues/4155#issuecomment-689321538 fi if [[ ! -s "bcq.pb" ]]; then rm -rf bcq.pb.zip - wget https://github.com/Samsung/ONE/files/5153842/bcq.pb.zip + wget -nv https://github.com/Samsung/ONE/files/5153842/bcq.pb.zip unzip bcq.pb.zip # https://github.com/Samsung/ONE/issues/4155#issuecomment-689324597 fi if [[ ! -s "img_files" ]]; then rm -rf img_files.zip - wget https://github.com/Samsung/ONE/files/5499172/img_files.zip + wget -nv https://github.com/Samsung/ONE/files/5499172/img_files.zip unzip img_files.zip # https://github.com/Samsung/ONE/issues/3213#issuecomment-722757499 fi @@ -65,46 +65,61 @@ fi if [[ ! -d "test_saved_model" ]]; then rm -rf test_saved_model.zip - wget https://github.com/Samsung/ONE/files/5516226/test_saved_model.zip + wget -nv https://github.com/Samsung/ONE/files/5516226/test_saved_model.zip unzip test_saved_model.zip # https://github.com/Samsung/ONE/issues/4268#issuecomment-724578237 fi if [[ ! -s "test_keras_model.h5" ]]; then rm -rf test_keras_model.zip - wget https://github.com/Samsung/ONE/files/5520777/test_keras_model.zip + wget -nv https://github.com/Samsung/ONE/files/5520777/test_keras_model.zip unzip test_keras_model.zip # https://github.com/Samsung/ONE/issues/4268#issuecomment-725025805 fi if [[ ! -s "test_onnx_model.onnx" ]]; then rm -rf test_onnx_model.zip - wget https://github.com/Samsung/ONE/files/5768243/test_onnx_model.zip + wget -nv https://github.com/Samsung/ONE/files/5768243/test_onnx_model.zip unzip test_onnx_model.zip # https://github.com/Samsung/ONE/issues/5548#issuecomment-754373360 fi if [[ ! -s "onnx_conv2d_conv2d.onnx" ]]; then rm -rf onnx_conv2d_conv2d.zip - wget https://github.com/Samsung/ONE/files/5774648/onnx_conv2d_conv2d.zip + wget -nv https://github.com/Samsung/ONE/files/5774648/onnx_conv2d_conv2d.zip unzip onnx_conv2d_conv2d.zip # https://github.com/Samsung/ONE/issues/5577#issuecomment-755078444 fi if [[ ! -s "reshape_matmul.onnx" ]]; then rm -rf reshape_matmul.zip - wget https://github.com/Samsung/ONE/files/9082878/reshape_matmul.zip + wget -nv https://github.com/Samsung/ONE/files/9082878/reshape_matmul.zip unzip reshape_matmul.zip # https://github.com/Samsung/ONE/issues/9405#issuecomment-1180198137 fi +# prepare 'reshape_matmul.circle' file used for tests +if [[ ! -s "reshape_matmul.circle" ]]; then + ../bin/one-import onnx \ + --experimental_disable_batchmatmul_unfold \ + -i reshape_matmul.onnx \ + -o reshape_matmul.circle +fi + if [[ ! -s "Net_InstanceNorm_003.part" ]]; then rm -rf Net_InstanceNorm_003.zip - wget https://github.com/Samsung/ONE/files/8608844/Net_InstanceNorm_003.zip + wget -nv https://github.com/Samsung/ONE/files/8608844/Net_InstanceNorm_003.zip unzip Net_InstanceNorm_003.zip # https://github.com/Samsung/ONE/issues/8570#issuecomment-1115804257 fi +if [[ ! -s "UnidirSeqLSTM.tflite" ]]; then + rm -rf UnidirSeqLSTM.zip + wget -nv https://github.com/Samsung/ONE/files/10055255/UnidirSeqLSTM.zip + unzip UnidirSeqLSTM.zip + # https://github.com/Samsung/ONE/issues/9940#issuecomment-1293282484 +fi + function files_missing() { condition="test " @@ -124,7 +139,7 @@ declare -a TEST_RECCURENT_MODELS=(\ if files_missing "${TEST_RECCURENT_MODELS[@]}"; then rm -rf test_onnx_recurrent_models.zip - wget https://github.com/Samsung/ONE/files/8067909/test_onnx_recurrent_models.zip + wget -nv https://github.com/Samsung/ONE/files/8067909/test_onnx_recurrent_models.zip unzip test_onnx_recurrent_models.zip # https://github.com/Samsung/ONE/issues/8395#issuecomment-1040072097 fi @@ -133,7 +148,7 @@ declare -a NEG_TEST_RECCURENT_MODELS=("rnn_variable.onnx" "lstm_variable.onnx") if files_missing "${NEG_TEST_RECCURENT_MODELS[@]}"; then rm -rf neg_test_onnx_recurrent_models.zip - wget https://github.com/Samsung/ONE/files/8137183/neg_test_onnx_recurrent_models.zip + wget -nv https://github.com/Samsung/ONE/files/8137183/neg_test_onnx_recurrent_models.zip unzip neg_test_onnx_recurrent_models.zip # https://github.com/Samsung/ONE/issues/8395#issuecomment-1050364375 fi diff --git a/compiler/one-cmds/tests/rawdata2hdf5_001.test b/compiler/one-cmds/tests/rawdata2hdf5_001.test index 887d81b..ceefcf7 100644 --- a/compiler/one-cmds/tests/rawdata2hdf5_001.test +++ b/compiler/one-cmds/tests/rawdata2hdf5_001.test @@ -27,12 +27,13 @@ trap trap_err_onexit ERR outputfile="./output_testdata.h5" +rm -f ${filename}.log rm -rf ${outputfile} # run test rawdata2hdf5 \ --data_list datalist.txt \ ---output_path ./output_testdata.h5 >> /dev/null +--output_path ${outputfile} > ${filename}.log 2>&1 if [[ ! -s "${outputfile}" ]]; then trap_err_onexit diff --git a/compiler/one-cmds/tests/rawdata2hdf5_neg_001.test b/compiler/one-cmds/tests/rawdata2hdf5_neg_001.test index 45ad88f..fb78037 100644 --- a/compiler/one-cmds/tests/rawdata2hdf5_neg_001.test +++ b/compiler/one-cmds/tests/rawdata2hdf5_neg_001.test @@ -34,6 +34,7 @@ inputfile="./wronglist.txt" outputfile="./output_testdata.h5" rm -rf ${inputfile} +rm -f ${filename}.log touch ${inputfile} echo "non-existing-file.data" >> ${inputfile} diff --git a/compiler/one-cmds/tests/rawdata2hdf5_neg_002.test b/compiler/one-cmds/tests/rawdata2hdf5_neg_002.test index 6bd078f..7a9e231 100644 --- a/compiler/one-cmds/tests/rawdata2hdf5_neg_002.test +++ b/compiler/one-cmds/tests/rawdata2hdf5_neg_002.test @@ -33,6 +33,8 @@ trap trap_err_onexit ERR inputfile="./inception_v3.circle" outputfile="./output_testdata.h5" +rm -f ${filename}.log + # run test rawdata2hdf5 \ --data_list ${inputfile} \ diff --git a/compiler/one-cmds/tests/rawdata2hdf5_neg_003.test b/compiler/one-cmds/tests/rawdata2hdf5_neg_003.test index 64559c2..c69f935 100644 --- a/compiler/one-cmds/tests/rawdata2hdf5_neg_003.test +++ b/compiler/one-cmds/tests/rawdata2hdf5_neg_003.test @@ -32,6 +32,8 @@ trap trap_err_onexit ERR outputfile="./output_testdata.h5" +rm -f ${filename}.log + # run test rawdata2hdf5 \ --output_path ${outputfile} > ${filename}.log 2>&1 diff --git a/compiler/one-cmds/tests/rawdata2hdf5_neg_004.test b/compiler/one-cmds/tests/rawdata2hdf5_neg_004.test index 277bc0e..df06201 100644 --- a/compiler/one-cmds/tests/rawdata2hdf5_neg_004.test +++ b/compiler/one-cmds/tests/rawdata2hdf5_neg_004.test @@ -32,6 +32,8 @@ trap trap_err_onexit ERR outputfile="./non_existing_dir/output_testdata.h5" +rm -f ${filename}.log + # run test rawdata2hdf5 \ --data_list datalist.txt \ diff --git a/compiler/one-cmds/utils.py b/compiler/one-cmds/utils.py deleted file mode 100644 index d204447..0000000 --- a/compiler/one-cmds/utils.py +++ /dev/null @@ -1,315 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import configparser -import glob -import importlib -import ntpath -import os -import subprocess -import sys - -import onelib.constant as _constant - - -def _add_default_arg(parser): - # version - parser.add_argument( - '-v', - '--version', - action='store_true', - help='show program\'s version number and exit') - - # verbose - parser.add_argument( - '-V', - '--verbose', - action='store_true', - help='output additional information to stdout or stderr') - - # configuration file - parser.add_argument('-C', '--config', type=str, help='run with configuation file') - # section name that you want to run in configuration file - parser.add_argument('-S', '--section', type=str, help=argparse.SUPPRESS) - - -def _add_default_arg_no_CS(parser): - """ - This adds -v -V args only (no -C nor -S) - """ - # version - parser.add_argument( - '-v', - '--version', - action='store_true', - help='show program\'s version number and exit') - - # verbose - parser.add_argument( - '-V', - '--verbose', - action='store_true', - help='output additional information to stdout or stderr') - - -def is_accumulated_arg(arg, driver): - if driver == "one-quantize": - accumulables = [ - "tensor_name", "scale", "zero_point", "src_tensor_name", "dst_tensor_name" - ] - if arg in accumulables: - return True - - return False - - -def _is_valid_attr(args, attr): - return hasattr(args, attr) and getattr(args, attr) - - -class Command: - def __init__(self, driver, args, log_file): - self.cmd = [driver] - self.driver = driver - self.args = args - self.log_file = log_file - - # Add option if attrs are valid - # Option values are collected from self.args - def add_option_with_valid_args(self, option, attrs): - for attr in attrs: - if not _is_valid_attr(self.args, attr): - return self - self.cmd.append(option) - for attr in attrs: - self.cmd.append(getattr(self.args, attr)) - return self - - # Add option and values without any condition - def add_option_with_values(self, option, values): - self.cmd.append(option) - for value in values: - self.cmd.append(value) - return self - - # Add option with no argument (ex: --verbose) if attr is valid - def add_noarg_option_if_valid_arg(self, option, attr): - if _is_valid_attr(self.args, attr): - self.cmd.append(option) - return self - - # Run cmd and save logs - def run(self): - self.log_file.write((' '.join(self.cmd) + '\n').encode()) - _run(self.cmd, err_prefix=self.driver, logfile=self.log_file) - - -def _parse_cfg_and_overwrite(config_path, section, args): - """ - parse given section of configuration file and set the values of args. - Even if the values parsed from the configuration file already exist in args, - the values are overwritten. - """ - if config_path == None: - # DO NOTHING - return - config = configparser.ConfigParser() - # make option names case sensitive - config.optionxform = str - parsed = config.read(config_path) - if not parsed: - raise FileNotFoundError('Not found given configuration file') - if not config.has_section(section): - raise AssertionError('configuration file doesn\'t have \'' + section + - '\' section') - for key in config[section]: - setattr(args, key, config[section][key]) - # TODO support accumulated arguments - - -def _parse_cfg(args, driver_name): - """parse configuration file. If the option is directly given to the command line, - the option is processed prior to the configuration file. - That is, if the values parsed from the configuration file already exist in args, - the values are ignored.""" - if _is_valid_attr(args, 'config'): - config = configparser.ConfigParser() - config.optionxform = str - config.read(args.config) - # if section is given, verify given section - if _is_valid_attr(args, 'section'): - if not config.has_section(args.section): - raise AssertionError('configuration file must have \'' + driver_name + - '\' section') - for key in config[args.section]: - if is_accumulated_arg(key, driver_name): - if not _is_valid_attr(args, key): - setattr(args, key, [config[args.section][key]]) - else: - getattr(args, key).append(config[args.section][key]) - continue - if not _is_valid_attr(args, key): - setattr(args, key, config[args.section][key]) - # if section is not given, section name is same with its driver name - else: - if not config.has_section(driver_name): - raise AssertionError('configuration file must have \'' + driver_name + - '\' section') - secton_to_run = driver_name - for key in config[secton_to_run]: - if is_accumulated_arg(key, driver_name): - if not _is_valid_attr(args, key): - setattr(args, key, [config[secton_to_run][key]]) - else: - getattr(args, key).append(config[secton_to_run][key]) - continue - if not _is_valid_attr(args, key): - setattr(args, key, config[secton_to_run][key]) - - -def _print_version_and_exit(file_path): - """print version of the file located in the file_path""" - script_path = os.path.realpath(file_path) - dir_path = os.path.dirname(script_path) - script_name = os.path.splitext(os.path.basename(script_path))[0] - # run one-version - subprocess.call([os.path.join(dir_path, 'one-version'), script_name]) - sys.exit() - - -def _safemain(main, mainpath): - """execute given method and print with program name for all uncaught exceptions""" - try: - main() - except Exception as e: - prog_name = os.path.basename(mainpath) - print(f"{prog_name}: {type(e).__name__}: " + str(e), file=sys.stderr) - sys.exit(255) - - -def _run(cmd, err_prefix=None, logfile=None): - """Execute command in subprocess - - Args: - cmd: command to be executed in subprocess - err_prefix: prefix to be put before every stderr lines - logfile: file stream to which both of stdout and stderr lines will be written - """ - with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p: - import select - inputs = set([p.stdout, p.stderr]) - while inputs: - readable, _, _ = select.select(inputs, [], []) - for x in readable: - line = x.readline() - if len(line) == 0: - inputs.discard(x) - continue - if x == p.stdout: - out = sys.stdout - if x == p.stderr: - out = sys.stderr - if err_prefix: - line = f"{err_prefix}: ".encode() + line - out.buffer.write(line) - out.buffer.flush() - if logfile != None: - logfile.write(line) - if p.returncode != 0: - sys.exit(p.returncode) - - -def _remove_prefix(str, prefix): - if str.startswith(prefix): - return str[len(prefix):] - return str - - -def _remove_suffix(str, suffix): - if str.endswith(suffix): - return str[:-len(suffix)] - return str - - -def _get_optimization_list(get_name=False): - """ - returns a list of optimization. If `get_name` is True, - only basename without extension is returned rather than full file path. - - [one hierarchy] - one - ├── backends - ├── bin - ├── doc - ├── include - ├── lib - ├── optimization - └── test - - Optimization options must be placed in `optimization` folder - """ - dir_path = os.path.dirname(os.path.realpath(__file__)) - - # optimization folder - files = [f for f in glob.glob(dir_path + '/../optimization/O*.cfg', recursive=True)] - # exclude if the name has space - files = [s for s in files if not ' ' in s] - - opt_list = [] - for cand in files: - base = ntpath.basename(cand) - if os.path.isfile(cand) and os.access(cand, os.R_OK): - opt_list.append(cand) - - if get_name == True: - # NOTE the name includes prefix 'O' - # e.g. O1, O2, ONCHW not just 1, 2, NCHW - opt_list = [ntpath.basename(f) for f in opt_list] - opt_list = [_remove_suffix(s, '.cfg') for s in opt_list] - - return opt_list - - -def _detect_one_import_drivers(search_path): - """Looks for import drivers in given directory - - Args: - search_path: path to the directory where to search import drivers - - Returns: - dict: each entry is related to single detected driver, - key is a config section name, value is a driver name - - """ - import_drivers_dict = {} - for module_name in os.listdir(search_path): - full_path = os.path.join(search_path, module_name) - if not os.path.isfile(full_path): - continue - if module_name.find("one-import-") != 0: - continue - module_loader = importlib.machinery.SourceFileLoader(module_name, full_path) - module_spec = importlib.util.spec_from_loader(module_name, module_loader) - module = importlib.util.module_from_spec(module_spec) - try: - module_loader.exec_module(module) - if hasattr(module, "get_driver_cfg_section"): - section = module.get_driver_cfg_section() - import_drivers_dict[section] = module_name - except: - pass - return import_drivers_dict diff --git a/compiler/onecc-docker/README.md b/compiler/onecc-docker/README.md new file mode 100644 index 0000000..3d7aa89 --- /dev/null +++ b/compiler/onecc-docker/README.md @@ -0,0 +1,36 @@ +# onecc-docker + +_onecc-docker_ broadens ONE tools to be used in other platforms. + +## Description + +For now, ONE tools only support Ubuntu 18.04 and 20.04(not officially). +So, it is difficult for people in different environments to use our tools without using ubuntu 18.04. + +To overcome this limitation, we provide _onecc-docker_ that runs using a Docker so that users can use ONE tools more widely. + +This tool aims at the following objectives. + +- Unsupported Ubuntu OS supports ONE tools +- Install and use ONE tools lightly and quickly using Docker + +## Requirements + +- Any Linux distribution +- Docker + - Requires root privileges. + - _onecc-docker_ requires the current `user ID` to be included in the `Docker group` because it requires the Docker-related commands to be executed without `sudo` privileges. + - See "[Post-installation steps for Linux](https://docs.docker.com/engine/install/linux-postinstall/)" +- Python 3.8 + - requests + +## Note + +_onecc-docker_ is currently in incubation stage. + +The onecc-docker debian package should be created with one-compiler debian package when ONE +compiler project builds. To this end, it is correct to configure the onecc-docker debian codes in +./infra/debian/compiler directory. However, we are currently working on the code, so we will +temporarily implement it in this location. + +TODO: Merge this debian directory into ./infra/debian/compiler code. diff --git a/compiler/onecc-docker/debian/changelog b/compiler/onecc-docker/debian/changelog new file mode 100644 index 0000000..501d0ec --- /dev/null +++ b/compiler/onecc-docker/debian/changelog @@ -0,0 +1,6 @@ +onecc-docker (0.1.0) bionic; urgency=medium + + * Introduce onecc-docker + + -- Seunghui Lee Wed, 23 Sep 2022 12:00:00 +0900 + diff --git a/compiler/onecc-docker/debian/compat b/compiler/onecc-docker/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/compiler/onecc-docker/debian/compat @@ -0,0 +1 @@ +9 diff --git a/compiler/onecc-docker/debian/control b/compiler/onecc-docker/debian/control new file mode 100644 index 0000000..4687d10 --- /dev/null +++ b/compiler/onecc-docker/debian/control @@ -0,0 +1,13 @@ +Source: onecc-docker +Section: devel +Priority: extra +Maintainer: Neural Network Acceleration Solution Developers +Build-Depends: debhelper (>=9) +Standards-Version: 4.5.1 +Homepage: https://github.com/Samsung/ONE + +Package: onecc-docker +Architecture: amd64 +Multi-Arch: foreign +Depends: ${misc:Depends}, ${shlibs:Depends}, python3.8 +Description: On-device Neural Engine docker package diff --git a/compiler/onecc-docker/debian/copyright b/compiler/onecc-docker/debian/copyright new file mode 100644 index 0000000..837bb7d --- /dev/null +++ b/compiler/onecc-docker/debian/copyright @@ -0,0 +1,3 @@ +Files: * +License: Proprietary +Copyright (c) <2022> diff --git a/compiler/onecc-docker/debian/onecc-docker.install b/compiler/onecc-docker/debian/onecc-docker.install new file mode 100644 index 0000000..4036253 --- /dev/null +++ b/compiler/onecc-docker/debian/onecc-docker.install @@ -0,0 +1,2 @@ +compiler/onecc-docker/onecc-docker /usr/share/one/bin/ +compiler/onecc-docker/docker/Dockerfile /usr/share/one/bin/docker/ diff --git a/compiler/onecc-docker/debian/onecc-docker.links b/compiler/onecc-docker/debian/onecc-docker.links new file mode 100644 index 0000000..2374663 --- /dev/null +++ b/compiler/onecc-docker/debian/onecc-docker.links @@ -0,0 +1 @@ +/usr/share/one/bin/onecc-docker /usr/bin/onecc-docker diff --git a/compiler/onecc-docker/debian/rules b/compiler/onecc-docker/debian/rules new file mode 100644 index 0000000..cfd26cf --- /dev/null +++ b/compiler/onecc-docker/debian/rules @@ -0,0 +1,8 @@ +#!/usr/bin/make -f + +%: + dh $@ + +override_dh_fixperms: + dh_fixperms + chmod +x debian/onecc-docker/usr/share/one/bin/onecc-docker diff --git a/compiler/onecc-docker/docker/Dockerfile b/compiler/onecc-docker/docker/Dockerfile new file mode 100644 index 0000000..9752985 --- /dev/null +++ b/compiler/onecc-docker/docker/Dockerfile @@ -0,0 +1,26 @@ +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License + +FROM ubuntu:18.04 + +ARG VERSION + +RUN apt-get update && apt-get install -qqy --no-install-recommends \ + wget \ + ca-certificates \ + && wget https://github.com/Samsung/ONE/releases/download/${VERSION}/one-compiler_${VERSION}_amd64.deb \ + && apt-get install -y ./one-compiler_${VERSION}_amd64.deb \ + && rm -rf /var/lib/apt/lists/* + +ENTRYPOINT ["onecc"] diff --git a/compiler/onecc-docker/onecc-docker b/compiler/onecc-docker/onecc-docker new file mode 100644 index 0000000..ae3b31c --- /dev/null +++ b/compiler/onecc-docker/onecc-docker @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import subprocess +import json +import requests +import os +import argparse + + +def _run(cmd, is_shell=False): + result = subprocess.Popen( + cmd, shell=is_shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + stdout, stderr = result.communicate() + stdout = stdout.decode('utf-8') + stderr = stderr.decode('utf-8') + + if result.returncode: + print(stderr, end='') + exit(result.returncode) + else: + return stdout + + +def _image_exists(name): + cmd = ['docker', 'images', '-q', name] + lines = _run(cmd).splitlines() + return lines + + +def main(): + script_path = os.path.dirname(os.path.realpath(__file__)) + dockerfile_path = os.path.join(script_path, 'docker') + + onecc_docker_usage = 'onecc-docker [-h] [-t TOKEN] [COMMAND ]' + onecc_docker_desc = 'Run onecc via docker' + parser = argparse.ArgumentParser( + usage=onecc_docker_usage, description=onecc_docker_desc) + parser.add_argument( + "-t", + "--token", + help= + "Token for authentication to GitHub. This is a workaround for Rate limit exceeded error" + ) + + args, onecc_arguments = parser.parse_known_args() + authorization_token = args.token + + LATEST_URL = "https://api.github.com/repos/Samsung/ONE/releases/latest" + headers = {} + if authorization_token: + headers = {"Authorization": "Bearer {}".format(authorization_token)} + try: + response = requests.get(LATEST_URL, headers=headers) + response.raise_for_status() + except requests.exceptions.RequestException as e: + raise SystemExit('onecc-docker: error: {}'.format(e)) + + versions_str = response.content + versions_json = json.loads(versions_str) + recent_version = versions_json["tag_name"] + + image_name = f"onecc:{recent_version}" + build_arg = f"VERSION={recent_version}" + + if not _image_exists(image_name): + build_cmd = [ + "docker", "build", "-t", image_name, "--build-arg", build_arg, dockerfile_path + ] + print('build Docker image ...') + _run(build_cmd) + print('Dockerfile successfully built.') + + contianer_name = f"onecc_{recent_version.replace('.','_')}" + user_cmd = ' '.join(onecc_arguments) + + run_cmd = [ + "docker", "run", "--rm", "-u", "$(id -u):$(id -g)", "--name", contianer_name, + "-v", "${HOME}:${HOME}", "-e", "HOME=${HOME}", "-w", "${PWD}", image_name, + user_cmd + ] + + cmd = ' '.join(run_cmd) + output = _run(cmd, is_shell=True) + print(output, end='') + + +if __name__ == "__main__": + try: + main() + except Exception as e: + prog_name = os.path.basename(__file__) + print(f"{prog_name}: {type(e).__name__}: " + str(e), file=sys.stderr) + sys.exit(255) diff --git a/compiler/pics/CMakeLists.txt b/compiler/pics/CMakeLists.txt new file mode 100644 index 0000000..80e6a1c --- /dev/null +++ b/compiler/pics/CMakeLists.txt @@ -0,0 +1,33 @@ +nnas_find_package(FlatBuffers EXACT 2.0 QUIET) +if(NOT FlatBuffers_FOUND) + message(STATUS "Configure pics: FAILED (missing FlatBuffers)") + return() +endif(NOT FlatBuffers_FOUND) + +unset(PICS_DEPS) + +### +### Generate python interface for circle schema +### +set(CIRCLE_SCHEMA_PYTHON_DIR "${CMAKE_CURRENT_BINARY_DIR}/circle") + +get_target_property(SCHEMA_BIN_PATH mio_circle04 BINARY_DIR) + +add_custom_command( + OUTPUT ${CIRCLE_SCHEMA_PYTHON_DIR} + COMMAND "$" --python + -o "${CMAKE_CURRENT_BINARY_DIR}" "${SCHEMA_BIN_PATH}/schema.fbs" + DEPENDS flatbuffers::flatc + COMMENT "Generate python interface for circle schema" +) + +list(APPEND PICS_DEPS "${CIRCLE_SCHEMA_PYTHON_DIR}") + +# This enforces CMake to generate all the dependencies during "build" phase +add_custom_target(pics ALL DEPENDS ${PICS_DEPS}) + +install(DIRECTORY ${CIRCLE_SCHEMA_PYTHON_DIR} + FILE_PERMISSIONS OWNER_WRITE OWNER_READ + GROUP_READ + WORLD_READ + DESTINATION bin) diff --git a/compiler/pics/README.md b/compiler/pics/README.md new file mode 100644 index 0000000..248d1b8 --- /dev/null +++ b/compiler/pics/README.md @@ -0,0 +1,16 @@ +# pics + +_pics_ is flatbuffer Python interface for circle schema. + +## How to use pics in your module? + +Add below lines to your module's `CMakeLists.txt`. It will create a symbolic link to `circle` directory under your module's binary directory. + +``` +get_target_property(PICS_BIN_PATH pics BINARY_DIR) +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/circle + COMMAND ${CMAKE_COMMAND} -E create_symlink + ${PICS_BIN_PATH}/circle ${CMAKE_CURRENT_BINARY_DIR}/circle) + +# Add dependency to ${CMAKE_CURRENT_BINARY_DIR}/circle +``` diff --git a/compiler/pota-quantization-value-test/CMakeLists.txt b/compiler/pota-quantization-value-test/CMakeLists.txt index 96dfc86..53233cd 100644 --- a/compiler/pota-quantization-value-test/CMakeLists.txt +++ b/compiler/pota-quantization-value-test/CMakeLists.txt @@ -7,12 +7,6 @@ unset(QUANTIZATION_VALUE_TEST_WITH_PARAM) unset(QUANTIZATION_CONFIG_VALUE_TEST) unset(QUANTIZATION_CONFIG_VALUE_TEST_WITH_PARAM) -nnas_find_package(FlatBuffers EXACT 2.0 QUIET) -if(NOT FlatBuffers_FOUND) - message(STATUS "Build pota-quantization-value-test: FAILED (missing FlatBuffers)") - return() -endif(NOT FlatBuffers_FOUND) - macro(addTest NAME GRANULARITY DTYPE) list(APPEND QUANTIZATION_VALUE_TEST ${NAME}) list(APPEND QUANTIZATION_VALUE_TEST_WITH_PARAM ${NAME} ${GRANULARITY} ${DTYPE}) @@ -31,12 +25,16 @@ include("test.local.lst" OPTIONAL) unset(TEST_DEPS) get_target_property(ARTIFACTS_BIN_PATH testDataGenerator BINARY_DIR) -get_target_property(SCHEMA_BIN_PATH mio_circle04 BINARY_DIR) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/gen_h5_explicit_inputs.py" "${CMAKE_CURRENT_BINARY_DIR}/gen_h5_explicit_inputs.py" COPYONLY) -set(VIRTUALENV "${NNCC_OVERLAY_DIR}/venv_2_8_0") +# TODO Run both 2.8.0 and 2.10.1 test for jammy +if(ONE_UBUNTU_CODENAME_JAMMY) + set(VIRTUALENV "${NNCC_OVERLAY_DIR}/venv_2_10_1") +else(ONE_UBUNTU_CODENAME_JAMMY) + set(VIRTUALENV "${NNCC_OVERLAY_DIR}/venv_2_8_0") +endif(ONE_UBUNTU_CODENAME_JAMMY) ### ### Generate test.config @@ -56,21 +54,13 @@ add_custom_command( COMMENT "Generate test configuration" ) -### -### Generate python interface for circle schema -### -set(CIRCLE_SCHEMA_PYTHON_DIR "${CMAKE_CURRENT_BINARY_DIR}/circle") - -add_custom_command( - OUTPUT ${CIRCLE_SCHEMA_PYTHON_DIR} - COMMAND ${CMAKE_COMMAND} -E remove_directory "${CIRCLE_SCHEMA_PYTHON_DIR}" - COMMAND "$" --python - -o "${CMAKE_CURRENT_BINARY_DIR}" "${SCHEMA_BIN_PATH}/schema.fbs" - DEPENDS flatbuffers::flatc - COMMENT "Generate python interface for circle schema" -) +# Import pics module +get_target_property(PICS_BIN_PATH pics BINARY_DIR) +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/circle + COMMAND ${CMAKE_COMMAND} -E create_symlink + ${PICS_BIN_PATH}/circle ${CMAKE_CURRENT_BINARY_DIR}/circle) -list(APPEND TEST_DEPS "${TEST_CONFIG}" "${CIRCLE_SCHEMA_PYTHON_DIR}") +list(APPEND TEST_DEPS "${TEST_CONFIG}" "${CMAKE_CURRENT_BINARY_DIR}/circle") # This enforces CMake to generate all the dependencies during "build" phase add_custom_target(pota_quantization_value_test_deps ALL DEPENDS ${TEST_DEPS}) @@ -109,6 +99,14 @@ add_test( ) add_test( + NAME pota_parallel_record_minmax_test + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_parallel_record_minmax.sh" + "${TEST_CONFIG}" + "${ARTIFACTS_BIN_PATH}" + ${QUANTIZATION_VALUE_TEST_WITH_PARAM} +) + +add_test( NAME pota_quantization_test_with_config COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test_quantization_with_config.sh" "${TEST_CONFIG}" @@ -118,4 +116,5 @@ add_test( set_tests_properties(pota_record_minmax_test PROPERTIES DEPENDS pota_fake_wquant_test) set_tests_properties(pota_quantization_test PROPERTIES DEPENDS pota_record_minmax_test) +set_tests_properties(pota_parallel_record_minmax_test PROPERTIES DEPENDS pota_record_minmax_test) set_tests_properties(pota_quantization_test_with_config PROPERTIES DEPENDS pota_fake_wquant_test_with_config) diff --git a/compiler/pota-quantization-value-test/requires.cmake b/compiler/pota-quantization-value-test/requires.cmake index 5ce8dfb..e8acc81 100644 --- a/compiler/pota-quantization-value-test/requires.cmake +++ b/compiler/pota-quantization-value-test/requires.cmake @@ -3,3 +3,4 @@ require("circle-quantizer") require("circle-tensordump") require("common-artifacts") require("mio-circle04") +require("pics") diff --git a/compiler/pota-quantization-value-test/test_parallel_record_minmax.sh b/compiler/pota-quantization-value-test/test_parallel_record_minmax.sh new file mode 100755 index 0000000..0af2c01 --- /dev/null +++ b/compiler/pota-quantization-value-test/test_parallel_record_minmax.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +# This script tests the parallel behavior of record-minmax +# +# HOW TO USE +# +# ./test_parallel_record_minmax.sh ... +# test.config : set ${RECORD_MINMAX_PATH} and ${CIRCLE2CIRCLE_PATH} +# work_dir : build directory of quantization-value-test (ex: build/compiler/quantization-value-test) + +SOURCE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPARE_SCRIPT_PATH="${SOURCE_PATH}/compare_tensors.py" +CONFIG_PATH="$1"; shift +BIN_PATH=$(dirname "${CONFIG_PATH}") +TEST_INPUT_PATH="${SOURCE_PATH}/test_inputs" +GEN_SCRIPT_PATH="${BIN_PATH}/gen_h5_explicit_inputs.py" +WORKDIR="$1"; shift + +source "${CONFIG_PATH}" + +echo "-- Found RECORD-MINMAX: ${RECORD_MINMAX_PATH}" +echo "-- Found CIRCLE_TENSORDUMP: ${CIRCLE_TENSORDUMP_PATH}" +echo "-- Found workdir: ${WORKDIR}" + +TESTED=() +PASSED=() +FAILED=() + +pushd "${WORKDIR}" +while [ "$1" != "" ]; do + MODELNAME=$1; shift + GRANULARITY=$1; shift + DTYPE=$1; shift + TESTCASE="${MODELNAME}.${GRANULARITY}.${DTYPE}" + + TESTED+=("${TESTCASE}") + + TESTCASE_FILE="${WORKDIR}/${TESTCASE}" + TEST_RESULT_FILE="${BIN_PATH}/${TESTCASE}" + + PASSED_TAG="${TEST_RESULT_FILE}.parallel_record_minmax.passed" + rm -f "${PASSED_TAG}" + + cat > "${TEST_RESULT_FILE}_parallel_record_minmax.log" <( + exec 2>&1 + set -ex + # Generate h5 input data + source "${VIRTUALENV}/bin/activate" + "${VIRTUALENV}/bin/python" "${GEN_SCRIPT_PATH}" \ + --model "${WORKDIR}/${MODELNAME}.circle" \ + --input "${TEST_INPUT_PATH}/${MODELNAME}/${GRANULARITY}/${DTYPE}" \ + --output "${TESTCASE_FILE}.input.h5" + if [[ $? -ne 0 ]]; then + echo "FAILED TO GENERATE INPUT" + continue + fi + # Run parallel record-minmax + "${RECORD_MINMAX_PATH}" \ + --input_model "${TEST_RESULT_FILE}.fake_quantized.circle" \ + --input_data "${TESTCASE_FILE}.input.h5" \ + --output_model "${TEST_RESULT_FILE}.parallel_minmax_recorded.circle" \ + --num_threads 4 + # Dump min/max values (circle-tensordump) + "${CIRCLE_TENSORDUMP_PATH}" \ + "${TEST_RESULT_FILE}.parallel_minmax_recorded.circle" \ + --tensors_to_hdf5 "${TEST_RESULT_FILE}.parallel_minmax_recorded.circle.h5" + # Compare result + "${VIRTUALENV}/bin/python" "${COMPARE_SCRIPT_PATH}" \ + --input_h5 "${TEST_RESULT_FILE}.parallel_minmax_recorded.circle.h5" \ + --expect_dir "${SOURCE_PATH}/expected_outputs/${MODELNAME}/${GRANULARITY}/${DTYPE}/record_minmax" \ + --mode record_minmax + if [[ $? -eq 0 ]]; then + touch "${PASSED_TAG}" + fi + ) + + if [[ -f "${PASSED_TAG}" ]]; then + PASSED+=("$TESTCASE") + else + FAILED+=("$TESTCASE") + fi +done +popd + +if [[ ${#TESTED[@]} -ne ${#PASSED[@]} ]]; then + echo "FAILED" + for TEST in "${FAILED[@]}" + do + echo "- ${TEST}" + done + exit 255 +fi + +echo "PASSED" +exit 0 diff --git a/compiler/record-minmax-conversion-test/CMakeLists.txt b/compiler/record-minmax-conversion-test/CMakeLists.txt index 6363614..63fe33d 100644 --- a/compiler/record-minmax-conversion-test/CMakeLists.txt +++ b/compiler/record-minmax-conversion-test/CMakeLists.txt @@ -44,3 +44,14 @@ add_test( "${NNCC_OVERLAY_DIR}/venv_2_8_0" ${RECORD_MINMAX_CONVERSION_TEST} ) + +if(ONE_UBUNTU_CODENAME_JAMMY) + add_test( + NAME record_minmax_conversion_210_test + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/testall.sh" + "${TEST_CONFIG}" + "${ARTIFACTS_BIN_PATH}" + "${NNCC_OVERLAY_DIR}/venv_2_10_1" + ${RECORD_MINMAX_CONVERSION_TEST} + ) +endif(ONE_UBUNTU_CODENAME_JAMMY) diff --git a/compiler/record-minmax-thread-safety-test/CMakeLists.txt b/compiler/record-minmax-thread-safety-test/CMakeLists.txt new file mode 100644 index 0000000..ba0f082 --- /dev/null +++ b/compiler/record-minmax-thread-safety-test/CMakeLists.txt @@ -0,0 +1,73 @@ +if(NOT ENABLE_TEST) + return() +endif(NOT ENABLE_TEST) + +# Build record-minmax-for-thread-test if target arch is 64bit +# Thread sanitizer is only available on 64bit machine +# (https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual#supported-platforms) +if(NOT "${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") + return() +endif(NOT "${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") + +unset(RECORD_MINMAX_THREAD_SAFETY_TEST) + +macro(addTest NAME) + list(APPEND RECORD_MINMAX_THREAD_SAFETY_TEST ${NAME}) +endmacro(addTest) + +# Read "test.lst" +include("test.lst") +# Read "test.local.lst" if exists +include("test.local.lst" OPTIONAL) + +unset(TEST_DEPS) + +get_target_property(ARTIFACTS_BIN_PATH testDataGenerator BINARY_DIR) + +### +### Generate test.config +### +set(TEST_CONFIG "${CMAKE_CURRENT_BINARY_DIR}/test.config") + +add_custom_command( + OUTPUT "${MICRO_ARM_BUILD_DEPENDENCY}" + COMMAND "${CMAKE_COMMAND}" "${CMAKE_CURRENT_SOURCE_DIR}/standalone" ${CMAKE_ARM_OPTIONS} + WORKING_DIRECTORY "${MICRO_ARM_BUILD_DIR}" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/standalone/CMakeLists.txt" + VERBATIM +) +set(RECORD_MINMAX_PATH "$") + +add_custom_command( + OUTPUT ${TEST_CONFIG} + COMMAND ${CMAKE_COMMAND} -E remove -f ${TEST_CONFIG} + COMMAND ${CMAKE_COMMAND} -E echo 'RECORD_MINMAX_PATH=\"$\"' >> ${TEST_CONFIG} + DEPENDS record-minmax-for-thread-test + COMMENT "Generate test configuration" +) + +list(APPEND TEST_DEPS "${TEST_CONFIG}") + +# This enforces CMake to generate all the dependencies during "build" phase +add_custom_target(record_minmax_thread_safety_test_deps ALL DEPENDS ${TEST_DEPS}) + +# Run tests +add_test( + NAME record_minmax_thread_safety_test + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/testall.sh" + "${TEST_CONFIG}" + "${ARTIFACTS_BIN_PATH}" + "${NNCC_OVERLAY_DIR}/venv_2_8_0" + ${RECORD_MINMAX_THREAD_SAFETY_TEST} +) + +if(ONE_UBUNTU_CODENAME_JAMMY) + add_test( + NAME record_minmax_thread_safety_210_test + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/testall.sh" + "${TEST_CONFIG}" + "${ARTIFACTS_BIN_PATH}" + "${NNCC_OVERLAY_DIR}/venv_2_10_1" + ${RECORD_MINMAX_THREAD_SAFETY_TEST} + ) +endif(ONE_UBUNTU_CODENAME_JAMMY) diff --git a/compiler/record-minmax-thread-safety-test/gen_h5_random_inputs.py b/compiler/record-minmax-thread-safety-test/gen_h5_random_inputs.py new file mode 100644 index 0000000..d57289a --- /dev/null +++ b/compiler/record-minmax-thread-safety-test/gen_h5_random_inputs.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import h5py as h5 +import numpy as np +import tensorflow as tf +import argparse + +# +# This script generates a pack of random input data (.h5) expected by the input tflite model +# +# Basic usage: +# gen_h5_inputs.py --model --num_data --output +# ex: gen_h5_inputs.py --model add.tflite --num_data 3 --output add.tflite.input.h5 +# (This will create add.tflite.input.h5 composed of three random inputs in the same directory as the model) +parser = argparse.ArgumentParser() +parser.add_argument('--model', type=str, required=True) +parser.add_argument('--num_data', type=int, required=True) +parser.add_argument('--output', type=str, required=True) +args = parser.parse_args() + +model = args.model + +num_data = args.num_data + +output_path = args.output + +# Build TFLite interpreter. (to get the information of model input) +interpreter = tf.lite.Interpreter(model) +input_details = interpreter.get_input_details() + +# Create h5 file +h5_file = h5.File(output_path, 'w') +group = h5_file.create_group("value") +group.attrs['desc'] = "Input data for " + model + +# Generate random data +for i in range(num_data): + sample = group.create_group(str(i)) + sample.attrs['desc'] = "Input data " + str(i) + + for j in range(len(input_details)): + input_detail = input_details[j] + print(input_detail["dtype"]) + if input_detail["dtype"] == np.bool_: + # Generate random bool [0, 1] + input_data = np.array( + np.random.random_integers(0, 1, input_detail["shape"]), + input_detail["dtype"]) + elif input_detail["dtype"] == np.float32: + # Generate random input [-5, 5) + input_data = np.array(10 * np.random.random_sample(input_detail["shape"]) - 5, + input_detail["dtype"]) + sample.create_dataset(str(j), data=input_data) + +h5_file.close() diff --git a/compiler/record-minmax-thread-safety-test/requires.cmake b/compiler/record-minmax-thread-safety-test/requires.cmake new file mode 100644 index 0000000..9105c3e --- /dev/null +++ b/compiler/record-minmax-thread-safety-test/requires.cmake @@ -0,0 +1,2 @@ +require("common-artifacts") +require("record-minmax") diff --git a/compiler/record-minmax-thread-safety-test/test.lst b/compiler/record-minmax-thread-safety-test/test.lst new file mode 100644 index 0000000..771c3bd --- /dev/null +++ b/compiler/record-minmax-thread-safety-test/test.lst @@ -0,0 +1,16 @@ +addTest(Add_000) +addTest(AveragePool2D_000) +addTest(Concatenation_000) +addTest(Conv2D_000) +addTest(Conv2D_001) +addTest(Conv2D_002) +addTest(DepthwiseConv2D_000) +addTest(FullyConnected_000) +addTest(FullyConnected_001) +addTest(MaxPool2D_000) +addTest(Mul_000) +addTest(Pad_000) +addTest(Reshape_000) +addTest(Reshape_001) +addTest(Reshape_002) +addTest(Softmax_000) diff --git a/compiler/record-minmax-thread-safety-test/testall.sh b/compiler/record-minmax-thread-safety-test/testall.sh new file mode 100755 index 0000000..4b47b3e --- /dev/null +++ b/compiler/record-minmax-thread-safety-test/testall.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# This script tests the parallel behavior of record-minmax +# +# HOW TO USE +# +# ./testall.sh ... +# test.config : set ${RECORD_MINMAX_PATH} +# work_dir : build directory of record-minmax-thread-safety-test (ex: build/compiler/record-minmax-thread-safety-test) + +GEN_SOURCE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GEN_SCRIPT_PATH="${GEN_SOURCE_PATH}/gen_h5_random_inputs.py" +CONFIG_PATH="$1"; shift +BIN_PATH=$(dirname "$CONFIG_PATH") +WORKDIR="$1"; shift +VIRTUALENV="$1"; shift + +source "${CONFIG_PATH}" + +echo "-- Found RECORD-MINMAX: ${RECORD_MINMAX_PATH}" +echo "-- Found workdir: ${WORKDIR}" + +TESTED=() +PASSED=() +FAILED=() + +pushd "${WORKDIR}" +for TESTCASE in "$@"; do + TESTED+=("${TESTCASE}") + + TESTCASE_FILE="${WORKDIR}/${TESTCASE}" + + PASSED_TAG="${BIN_PATH}/${TESTCASE}.passed" + rm -f "${PASSED_TAG}" + + cat > "${BIN_PATH}/${TESTCASE}.log" <( + exec 2>&1 + set -ex + # Generate h5 input data + source "${VIRTUALENV}/bin/activate" + "${VIRTUALENV}/bin/python" "${GEN_SCRIPT_PATH}" \ + --model "${TESTCASE_FILE}.tflite" \ + --num_data 8 \ + --output "${BIN_PATH}/${TESTCASE}.tflite.input.h5" + if [[ $? -ne 0 ]]; then + echo "FAILED TO GENERATE INPUT" + continue + fi + # Run record-minmax in parallel mode + "${RECORD_MINMAX_PATH}" \ + --input_model "${TESTCASE_FILE}.circle" \ + --input_data "${BIN_PATH}/${TESTCASE}.tflite.input.h5" \ + --output_model "${BIN_PATH}/${TESTCASE}.out.circle" \ + --num_threads 4 + if [[ $? -ne 0 ]]; then + echo "FAILED TO GENERATE CIRCLE OUTPUT" + continue + fi + ) + + if ! grep -q "ThreadSanitizer: data race" "${BIN_PATH}/${TESTCASE}.log"; then + touch "${PASSED_TAG}" + fi + + if [[ -f "${PASSED_TAG}" ]]; then + PASSED+=("$TESTCASE") + else + FAILED+=("$TESTCASE") + fi +done +popd + +if [[ ${#TESTED[@]} -ne ${#PASSED[@]} ]]; then + echo "FAILED" + for TEST in "${FAILED[@]}" + do + echo "- ${TEST}" + done + exit 255 +fi + +echo "PASSED" +exit 0 diff --git a/compiler/record-minmax/CMakeLists.txt b/compiler/record-minmax/CMakeLists.txt index b9c08f4..391e35b 100644 --- a/compiler/record-minmax/CMakeLists.txt +++ b/compiler/record-minmax/CMakeLists.txt @@ -11,9 +11,11 @@ target_link_libraries(record-minmax luci_import) target_link_libraries(record-minmax luci_env) target_link_libraries(record-minmax luci_export) target_link_libraries(record-minmax luci_interpreter) +target_link_libraries(record-minmax luci_log) target_link_libraries(record-minmax dio_hdf5) target_link_libraries(record-minmax vconone) target_link_libraries(record-minmax nncc_coverage) +target_link_libraries(record-minmax nncc_common) install(TARGETS record-minmax DESTINATION bin) @@ -21,6 +23,31 @@ if(NOT ENABLE_TEST) return() endif(NOT ENABLE_TEST) +# Build record-minmax-for-thread-test if target arch is 64bit +# Thread sanitizer is only available on 64bit machine +# (https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual#supported-platforms) +if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") + # create record-minmax-for-thread-test target + # Note: record-minmax-for-thread-test is built with -fsanitize=thread so that thread sanitizer can check memory bugs, + # record-minmax is built without the option for performance. + add_executable(record-minmax-for-thread-test ${DRIVER} ${SOURCES}) + target_include_directories(record-minmax-for-thread-test PRIVATE include) + + target_link_libraries(record-minmax-for-thread-test arser) + target_link_libraries(record-minmax-for-thread-test safemain) + target_link_libraries(record-minmax-for-thread-test luci_import) + target_link_libraries(record-minmax-for-thread-test luci_env) + target_link_libraries(record-minmax-for-thread-test luci_export) + target_link_libraries(record-minmax-for-thread-test luci_interpreter) + target_link_libraries(record-minmax-for-thread-test dio_hdf5) + target_link_libraries(record-minmax-for-thread-test vconone) + target_link_libraries(record-minmax-for-thread-test nncc_coverage) + target_link_libraries(record-minmax-for-thread-test luci_log) + + target_compile_options(record-minmax-for-thread-test PUBLIC -fsanitize=thread) + target_link_libraries(record-minmax-for-thread-test -fsanitize=thread) +endif("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") + file(GLOB_RECURSE TESTS "tests/*.test.cpp") nnas_find_package(GTest REQUIRED) diff --git a/compiler/record-minmax/driver/Driver.cpp b/compiler/record-minmax/driver/Driver.cpp index faa402f..d8ed95e 100644 --- a/compiler/record-minmax/driver/Driver.cpp +++ b/compiler/record-minmax/driver/Driver.cpp @@ -21,6 +21,9 @@ #include +// TODO declare own log signature of record-minmax +#include + void print_version(void) { std::cout << "record-minmax version " << vconone::get_string() << std::endl; @@ -31,6 +34,8 @@ int entry(const int argc, char **argv) { using namespace record_minmax; + LOGGER(l); + arser::Arser arser( "Embedding min/max values of activations to the circle model for post-training quantization"); @@ -50,6 +55,10 @@ int entry(const int argc, char **argv) .type(arser::DataType::FLOAT) .help("Record n'th percentile of min"); + arser.add_argument("--num_threads") + .type(arser::DataType::INT32) + .help("Number of threads (default: 1)"); + arser.add_argument("--max_percentile") .type(arser::DataType::FLOAT) .help("Record n'th percentile of max"); @@ -92,10 +101,17 @@ int entry(const int argc, char **argv) float min_percentile = 1.0; float max_percentile = 99.0; std::string input_data_format("h5"); + uint32_t num_threads = 1; if (arser["--min_percentile"]) min_percentile = arser.get("--min_percentile"); + if (arser["--num_threads"]) + num_threads = arser.get("--num_threads"); + + if (num_threads < 1) + throw std::runtime_error("The number of threads must be greater than zero"); + if (arser["--max_percentile"]) max_percentile = arser.get("--max_percentile"); @@ -111,7 +127,13 @@ int entry(const int argc, char **argv) if (arser["--input_data_format"]) input_data_format = arser.get("--input_data_format"); - RecordMinMax rmm; + RecordMinMax rmm(num_threads); + + // TODO: support parallel record for profile with random data + if (num_threads > 1 and not arser["--input_data"]) + { + throw std::runtime_error("Input data must be given for parallel recording"); + } // Initialize interpreter and observer rmm.initialize(input_model_path); @@ -120,10 +142,22 @@ int entry(const int argc, char **argv) { auto input_data_path = arser.get("--input_data"); + // TODO: support parallel record from file and dir input data format + if (num_threads > 1 and not(input_data_format == "h5") and not(input_data_format == "hdf5")) + { + throw std::runtime_error("Parallel recording is used only for h5 now"); + } + if (input_data_format == "h5" || input_data_format == "hdf5") { // Profile min/max while executing the H5 data - rmm.profileData(mode, input_data_path, min_percentile, max_percentile); + if (num_threads == 1) + rmm.profileData(mode, input_data_path, min_percentile, max_percentile); + else + { + INFO(l) << "Using parallel recording" << std::endl; + rmm.profileDataInParallel(mode, input_data_path, min_percentile, max_percentile); + } } // input_data is a text file having a file path in each line. // Each data file is composed of inputs of a model, concatenated in diff --git a/compiler/record-minmax/include/MinMaxObserver.h b/compiler/record-minmax/include/MinMaxObserver.h index ce63438..c34ac8e 100644 --- a/compiler/record-minmax/include/MinMaxObserver.h +++ b/compiler/record-minmax/include/MinMaxObserver.h @@ -43,6 +43,15 @@ public: vectors.max_vector.push_back(max); } + void appendMinMaxVector(const luci::CircleNode *node, const MinMaxVectors &minmax_vector) + { + MinMaxVectors &vectors = _minmax_map[node]; + vectors.min_vector.insert(vectors.min_vector.end(), minmax_vector.min_vector.begin(), + minmax_vector.min_vector.end()); + vectors.max_vector.insert(vectors.max_vector.end(), minmax_vector.max_vector.begin(), + minmax_vector.max_vector.end()); + } + const std::unordered_map *getMap() const { return &_minmax_map; diff --git a/compiler/record-minmax/include/RecordFunction.h b/compiler/record-minmax/include/RecordFunction.h index 5b993e4..d5da01c 100644 --- a/compiler/record-minmax/include/RecordFunction.h +++ b/compiler/record-minmax/include/RecordFunction.h @@ -69,13 +69,13 @@ float getMovingAverage(const std::vector &vector, const float alpha, assert(alpha >= 0.0 && alpha <= 1.0); assert(batch_size > 0); - auto getBatchMinOrMax = [&](int start_index) { - assert(start_index >= 0 && start_index < vector.size()); + auto getBatchMinOrMax = [&](uint32_t start_index) { + assert(start_index < vector.size()); float res = is_min ? std::numeric_limits::max() : std::numeric_limits::lowest(); - for (int offset = 0; offset < batch_size; offset++) + for (uint32_t offset = 0; offset < batch_size; offset++) { - int index = start_index + offset; + uint32_t index = start_index + offset; if (index >= vector.size()) break; @@ -92,7 +92,7 @@ float getMovingAverage(const std::vector &vector, const float alpha, }; float curr_avg = getBatchMinOrMax(0); - for (size_t i = batch_size; i < vector.size(); i += batch_size) + for (uint32_t i = batch_size; i < vector.size(); i += batch_size) { curr_avg = curr_avg * alpha + getBatchMinOrMax(i) * (1.0 - alpha); } diff --git a/compiler/record-minmax/include/RecordMinMax.h b/compiler/record-minmax/include/RecordMinMax.h index aaf1161..2a7b88f 100644 --- a/compiler/record-minmax/include/RecordMinMax.h +++ b/compiler/record-minmax/include/RecordMinMax.h @@ -23,14 +23,22 @@ #include "MinMaxObserver.h" #include +#include namespace record_minmax { +using Buffer = std::vector; +using Output = std::vector; +using WholeOutput = std::vector; + class RecordMinMax { public: - explicit RecordMinMax() = default; + explicit RecordMinMax(uint32_t num_threads) : _threads_size(num_threads) + { + assert(_threads_size > 0); + } ~RecordMinMax() = default; @@ -39,6 +47,9 @@ public: void profileData(const std::string &mode, const std::string &input_data_path, float min_percentile, float max_percentile); + void profileDataInParallel(const std::string &mode, const std::string &input_data_path, + float min_percentile, float max_percentile); + void profileRawData(const std::string &mode, const std::string &input_data_path, float min_percentile, float max_percentile); @@ -51,9 +62,18 @@ public: void saveModel(const std::string &output_model_path); private: + luci_interpreter::Interpreter *getInterpreter() const { return _interpreters[0].get(); } + MinMaxObserver *getObserver() const { return _observers[0].get(); } + + WholeOutput importH5Data(const std::string &input_data_path); + std::unique_ptr _module; - std::unique_ptr _interpreter; - std::unique_ptr _observer; + + // Multiple interpreters are used for parallel execution + std::vector> _interpreters; + std::vector> _observers; + + uint32_t _threads_size = 0; }; } // namespace record_minmax diff --git a/compiler/record-minmax/src/RecordMinMax.cpp b/compiler/record-minmax/src/RecordMinMax.cpp index 6dbf98d..c3da83a 100644 --- a/compiler/record-minmax/src/RecordMinMax.cpp +++ b/compiler/record-minmax/src/RecordMinMax.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -39,6 +40,17 @@ using DataType = loco::DataType; namespace { +// Max h5 file size for parallel recording in bytes = 1 GB +const long h5_max_size_bytes = 1000000000; + +long getH5FileSize(const std::string &input_data_path) +{ + std::ifstream in_file(input_data_path, std::ios::binary); + in_file.seekg(0, std::ios::end); + + return in_file.tellg(); +} + uint32_t numElements(const luci::CircleNode *node) { uint32_t num_elements = 1; @@ -70,6 +82,8 @@ void readDataFromFile(const std::string &filename, std::vector &data, size throw std::runtime_error("Cannot open file \"" + filename + "\".\n"); if (fs.read(data.data(), data_size).fail()) throw std::runtime_error("Failed to read data from file \"" + filename + "\".\n"); + if (fs.peek() != EOF) + throw std::runtime_error("Input tensor size mismatches with \"" + filename + "\".\n"); } std::vector genRandomBoolData(std::mt19937 &gen, uint32_t num_elements) @@ -169,6 +183,8 @@ namespace record_minmax void RecordMinMax::initialize(const std::string &input_model_path) { + assert(_threads_size > 0); + // Load model from the file std::ifstream fs(input_model_path, std::ifstream::binary); if (fs.fail()) @@ -199,12 +215,20 @@ void RecordMinMax::initialize(const std::string &input_model_path) throw std::runtime_error("Failed to load '" + input_model_path + "'"); } - // Initialize interpreter - _interpreter = std::make_unique(_module.get()); + // Create and initialize interpreters and observers + _interpreters.resize(_threads_size); + _observers.resize(_threads_size); - _observer = std::make_unique(); + for (uint32_t thread_idx = 0; thread_idx < _threads_size; ++thread_idx) + { + auto interpreter = std::make_unique(_module.get()); + auto observer = std::make_unique(); - _interpreter->attachObserver(_observer.get()); + interpreter->attachObserver(observer.get()); + + _observers[thread_idx] = std::move(observer); + _interpreters[thread_idx] = std::move(interpreter); + } } // input_data_path is a path to the directory @@ -235,7 +259,7 @@ void RecordMinMax::profileRawDataDirectory(const std::string &mode, total_input_size += getTensorSize(input_node); } - while (entry = readdir(dp)) + while ((entry = readdir(dp))) { // Skip if the entry is not a regular file if (entry->d_type != DT_REG) @@ -256,12 +280,12 @@ void RecordMinMax::profileRawDataDirectory(const std::string &mode, { const auto *input_node = loco::must_cast(input); const auto input_size = getTensorSize(input_node); - _interpreter->writeInputTensor(input_node, input_data.data() + offset, input_size); + getInterpreter()->writeInputTensor(input_node, input_data.data() + offset, input_size); offset += input_size; } - _interpreter->interpret(); + getInterpreter()->interpret(); num_records++; } @@ -273,7 +297,7 @@ void RecordMinMax::profileRawDataDirectory(const std::string &mode, std::cout << "Recording finished. Number of recorded data: " << num_records << std::endl; - update_quantparam(_observer.get(), mode, min_percentile, max_percentile); + update_quantparam(getObserver(), mode, min_percentile, max_percentile); } // input_data_path is a text file which specifies the representative data @@ -318,12 +342,12 @@ void RecordMinMax::profileRawData(const std::string &mode, const std::string &in { const auto *input_node = loco::must_cast(input); const auto input_size = getTensorSize(input_node); - _interpreter->writeInputTensor(input_node, input_data.data() + offset, input_size); + getInterpreter()->writeInputTensor(input_node, input_data.data() + offset, input_size); offset += input_size; } - _interpreter->interpret(); + getInterpreter()->interpret(); num_records++; } @@ -333,7 +357,65 @@ void RecordMinMax::profileRawData(const std::string &mode, const std::string &in std::cout << "Recording finished. Number of recorded data: " << num_records << std::endl; - update_quantparam(_observer.get(), mode, min_percentile, max_percentile); + update_quantparam(getObserver(), mode, min_percentile, max_percentile); +} + +WholeOutput RecordMinMax::importH5Data(const std::string &input_data_path) +{ + try + { + dio::hdf5::HDF5Importer importer(input_data_path); + importer.importGroup("value"); + + bool is_raw_data = importer.isRawData(); + + const auto num_records = importer.numData(); + if (num_records == 0) + throw std::runtime_error("The input data file does not contain any record."); + + const auto input_nodes = loco::input_nodes(_module->graph()); + const auto num_inputs = input_nodes.size(); + + WholeOutput whole_output(num_records); + + // Read inputs to whole_output + for (int i = 0; i < num_records; ++i) + { + if (num_inputs != static_cast(importer.numInputs(i))) + throw std::runtime_error("Wrong number of inputs."); + + for (uint32_t input_idx = 0; input_idx < num_inputs; input_idx++) + { + const auto *input_node = loco::must_cast(input_nodes[input_idx]); + assert(input_node->index() == input_idx); + checkInputDimension(input_node); + Buffer input_data(getTensorSize(input_node)); + + if (!is_raw_data) + { + DataType dtype; + Shape shape; + importer.readTensor(i, input_idx, &dtype, &shape, input_data.data()); + + // Check the type and the shape of the input data is valid + verifyTypeShape(input_node, dtype, shape); + } + else + { + // Skip type/shape check for raw data + importer.readTensor(i, input_idx, input_data.data()); + } + whole_output[i].emplace_back(std::move(input_data)); + } + } + + return whole_output; + } + catch (const H5::Exception &e) + { + H5::Exception::printErrorStack(); + throw std::runtime_error("HDF5 error occurred."); + } } void RecordMinMax::profileData(const std::string &mode, const std::string &input_data_path, @@ -355,12 +437,12 @@ void RecordMinMax::profileData(const std::string &mode, const std::string &input for (int32_t record_idx = 0; record_idx < num_records; record_idx++) { - if (num_inputs != importer.numInputs(record_idx)) + if (num_inputs != static_cast(importer.numInputs(record_idx))) throw std::runtime_error("Wrong number of inputs."); std::cout << "Recording " << record_idx << "'th data" << std::endl; - for (int32_t input_idx = 0; input_idx < num_inputs; input_idx++) + for (uint32_t input_idx = 0; input_idx < num_inputs; input_idx++) { const auto *input_node = loco::must_cast(input_nodes[input_idx]); assert(input_node->index() == input_idx); @@ -384,10 +466,10 @@ void RecordMinMax::profileData(const std::string &mode, const std::string &input // TODO: Input data is copied twice (file -> buffer (input_data) -> interpreter inputs) // We can redcue the copy by directly writing data from file to interpreter inputs - _interpreter->writeInputTensor(input_node, input_data.data(), input_data.size()); + getInterpreter()->writeInputTensor(input_node, input_data.data(), input_data.size()); } - _interpreter->interpret(); + getInterpreter()->interpret(); } std::cout << "Recording finished. Number of recorded data: " << num_records << std::endl; @@ -398,7 +480,96 @@ void RecordMinMax::profileData(const std::string &mode, const std::string &input throw std::runtime_error("HDF5 error occurred."); } - update_quantparam(_observer.get(), mode, min_percentile, max_percentile); + update_quantparam(getObserver(), mode, min_percentile, max_percentile); +} + +void RecordMinMax::profileDataInParallel(const std::string &mode, + const std::string &input_data_path, float min_percentile, + float max_percentile) +{ + LOGGER(l); + + assert(_interpreters.size() == _threads_size); + assert(_observers.size() == _threads_size); + + const long h5_file_size = getH5FileSize(input_data_path); + + if (h5_file_size > h5_max_size_bytes) + throw std::runtime_error("H5 file size is too large for parallel recording"); + + WholeOutput whole_output; + try + { + whole_output = importH5Data(input_data_path); + } + catch (const std::bad_alloc &e) + { + throw std::runtime_error("Out of memory during h5 data load."); + } + + const auto num_records = whole_output.size(); + const auto input_nodes = loco::input_nodes(_module->graph()); + + // Start parallel part + INFO(l) << _threads_size << " concurrent threads are supported." << std::endl; + + const auto run_threads = num_records < _threads_size ? num_records : _threads_size; + + const auto records_batch = static_cast(num_records / run_threads); + + auto interpret_batch = [&whole_output, &input_nodes](int first_record, int last_record, + luci_interpreter::Interpreter *interpreter) { + for (int record_index = first_record; record_index < last_record; ++record_index) + { + for (uint32_t input_idx = 0; input_idx < input_nodes.size(); input_idx++) + { + const auto *input_node = loco::must_cast(input_nodes[input_idx]); + + const auto &cur_input_data = whole_output[record_index][input_idx]; + interpreter->writeInputTensor(input_node, cur_input_data.data(), cur_input_data.size()); + } + interpreter->interpret(); + } + }; + + std::vector threads; + for (uint32_t t = 0; t < run_threads; ++t) + { + if (t < run_threads - 1) + { + threads.emplace_back(interpret_batch, records_batch * t, records_batch * (t + 1), + _interpreters[t].get()); + } + else + { + threads.emplace_back(interpret_batch, records_batch * t, num_records, _interpreters[t].get()); + } + } + + for (uint32_t i = 0; i < run_threads; ++i) + threads.at(i).join(); + + // End parallel part + + // Copy all min, max values to one observer + auto observer = std::make_unique(); + auto main_min_max_map = const_cast(observer->minMaxData()); + + for (const auto &obs : _observers) + { + const auto cur_minmax_map = obs->minMaxData()->getMap(); + for (auto &iter : *cur_minmax_map) + { + const auto node = iter.first; + const auto &minmax = iter.second; + + main_min_max_map->appendMinMaxVector(node, minmax); + } + } + + std::cout << "Recording finished. Number of recorded data: " << num_records << std::endl; + + update_quantparam(observer.get(), mode, min_percentile, max_percentile); } void RecordMinMax::profileDataWithRandomInputs(const std::string &mode, float min_percentile, @@ -414,11 +585,11 @@ void RecordMinMax::profileDataWithRandomInputs(const std::string &mode, float mi std::mt19937 gen(rd()); std::uniform_real_distribution<> dist(-5, 5); - for (int32_t record_idx = 0; record_idx < num_records; record_idx++) + for (uint32_t record_idx = 0; record_idx < num_records; record_idx++) { std::cout << "Recording " << record_idx << "'th data" << std::endl; - for (int32_t input_idx = 0; input_idx < num_inputs; input_idx++) + for (uint32_t input_idx = 0; input_idx < num_inputs; input_idx++) { const auto *input_node = loco::must_cast(input_nodes[input_idx]); assert(input_node->index() == input_idx); @@ -442,35 +613,35 @@ void RecordMinMax::profileDataWithRandomInputs(const std::string &mode, float mi // TODO: Input data is copied twice (file -> buffer (input_data) -> interpreter inputs) // We can redcue the copy by directly writing data from file to interpreter inputs - _interpreter->writeInputTensor(input_node, input_data.data(), - input_data.size() * sizeof(float)); + getInterpreter()->writeInputTensor(input_node, input_data.data(), + input_data.size() * sizeof(float)); } else if (input_node->dtype() == DataType::BOOL) { auto input_data = genRandomBoolData(gen, num_elements); - _interpreter->writeInputTensor(input_node, input_data.data(), - input_data.size() * sizeof(uint8_t)); + getInterpreter()->writeInputTensor(input_node, input_data.data(), + input_data.size() * sizeof(uint8_t)); } else if (input_node->dtype() == DataType::S32) { auto input_data = genRandomIntData(gen, num_elements, 0, 100); - _interpreter->writeInputTensor(input_node, input_data.data(), - input_data.size() * sizeof(int32_t)); + getInterpreter()->writeInputTensor(input_node, input_data.data(), + input_data.size() * sizeof(int32_t)); } else if (input_node->dtype() == DataType::S64) { auto input_data = genRandomIntData(gen, num_elements, 0, 100); - _interpreter->writeInputTensor(input_node, input_data.data(), - input_data.size() * sizeof(int64_t)); + getInterpreter()->writeInputTensor(input_node, input_data.data(), + input_data.size() * sizeof(int64_t)); } } - _interpreter->interpret(); + getInterpreter()->interpret(); } std::cout << "Recording finished. Number of recorded data: " << num_records << std::endl; - update_quantparam(_observer.get(), mode, min_percentile, max_percentile); + update_quantparam(getObserver(), mode, min_percentile, max_percentile); } void RecordMinMax::saveModel(const std::string &output_model_path) diff --git a/compiler/souschef/src/Gaussian.cpp b/compiler/souschef/src/Gaussian.cpp index 53a62ca..a180e24 100644 --- a/compiler/souschef/src/Gaussian.cpp +++ b/compiler/souschef/src/Gaussian.cpp @@ -22,6 +22,7 @@ #include #include +#include // std::numeric_limits #include diff --git a/compiler/tf2circle-value-pbtxt-remote-test/CMakeLists.txt b/compiler/tf2circle-value-pbtxt-remote-test/CMakeLists.txt index 852018e..f0ba921 100644 --- a/compiler/tf2circle-value-pbtxt-remote-test/CMakeLists.txt +++ b/compiler/tf2circle-value-pbtxt-remote-test/CMakeLists.txt @@ -140,7 +140,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E echo 'RANDOMIZE_ACTION_PATH=\"$\"' >> ${TEST_CONFIG} COMMAND ${CMAKE_COMMAND} -E echo 'HDF5_EXPORT_ACTION_PATH=\"$\"' >> ${TEST_CONFIG} COMMAND ${CMAKE_COMMAND} -E echo 'HDF5_IMPORT_ACTION_PATH=\"$\"' >> ${TEST_CONFIG} - COMMAND ${CMAKE_COMMAND} -E echo 'MODEL2NNPKG_PATH=\"${NNAS_PROJECT_SOURCE_DIR}/tools/nnpackage_tool/model2nnpkg/model2nnpkg.sh\"' >> ${TEST_CONFIG} + COMMAND ${CMAKE_COMMAND} -E echo 'MODEL2NNPKG_PATH=\"${NNAS_PROJECT_SOURCE_DIR}/tools/nnpackage_tool/model2nnpkg/model2nnpkg.py\"' >> ${TEST_CONFIG} COMMAND ${CMAKE_COMMAND} -E echo 'RUNTIME_LIBRARY_PATH=\"${NNAS_PROJECT_SOURCE_DIR}/Product/out/\"' >> ${TEST_CONFIG} DEPENDS nnkit-run diff --git a/compiler/tf2circle-value-pbtxt-remote-test/testall.sh b/compiler/tf2circle-value-pbtxt-remote-test/testall.sh index c80b00a..56ef070 100755 --- a/compiler/tf2circle-value-pbtxt-remote-test/testall.sh +++ b/compiler/tf2circle-value-pbtxt-remote-test/testall.sh @@ -102,7 +102,7 @@ while [[ $# -ne 0 ]]; do --post-arg "${WORKDIR}/${PREFIX}.expected.h5" # Generate nnpackage model - "${MODEL2NNPKG_PATH}" -o "${WORKDIR}" "${WORKDIR}/${PREFIX}.circle" + "${MODEL2NNPKG_PATH}" -o "${WORKDIR}" -m "${WORKDIR}/${PREFIX}.circle" # Copy h5 files into nnpackage mkdir -p "${WORKDIR}/${PREFIX}/metadata/tc" diff --git a/compiler/tf2circle/src/CustomopConfLoader.cpp b/compiler/tf2circle/src/CustomopConfLoader.cpp index 4124058..0520ad0 100644 --- a/compiler/tf2circle/src/CustomopConfLoader.cpp +++ b/compiler/tf2circle/src/CustomopConfLoader.cpp @@ -27,6 +27,8 @@ #include +#include // std::numeric_limits + namespace { bool load_text(const cwrap::Fildes &fildes, tf2circle::CustomOpInfoDef &def) diff --git a/compiler/tf2tflite/src/CustomopConfLoader.cpp b/compiler/tf2tflite/src/CustomopConfLoader.cpp index 7399a43..c50c17f 100644 --- a/compiler/tf2tflite/src/CustomopConfLoader.cpp +++ b/compiler/tf2tflite/src/CustomopConfLoader.cpp @@ -27,6 +27,8 @@ #include +#include // std::numeric_limits + namespace { bool load_text(const cwrap::Fildes &fildes, tf2tflite::CustomOpInfoDef &def) diff --git a/compiler/tf2tfliteV2-conversion-test/CMakeLists.txt b/compiler/tf2tfliteV2-conversion-test/CMakeLists.txt index 97aa07f..ded4162 100644 --- a/compiler/tf2tfliteV2-conversion-test/CMakeLists.txt +++ b/compiler/tf2tfliteV2-conversion-test/CMakeLists.txt @@ -76,7 +76,12 @@ list(APPEND TEST_DEPS "${TEST_RUNNER}") get_target_property(ARTIFACTS_BIN_PATH testDataGenerator BINARY_DIR) -set(VIRTUALENV "${NNCC_OVERLAY_DIR}/venv_2_8_0") +# TODO Run both 2.8.0 and 2.10.1 test for jammy +if(ONE_UBUNTU_CODENAME_JAMMY) + set(VIRTUALENV "${NNCC_OVERLAY_DIR}/venv_2_10_1") +else() + set(VIRTUALENV "${NNCC_OVERLAY_DIR}/venv_2_8_0") +endif() ### ### Generate test.config diff --git a/compiler/tflite2circle/src/DataLookup.cpp b/compiler/tflite2circle/src/DataLookup.cpp index 7c3aab0..c16e601 100644 --- a/compiler/tflite2circle/src/DataLookup.cpp +++ b/compiler/tflite2circle/src/DataLookup.cpp @@ -36,15 +36,22 @@ circle::BuiltinOperator get_circle_builtin_code(tflite::BuiltinOperator tfl_bop) int8_t get_circle_builtin_code(int8_t tfl_bop_i8) { - tflite::BuiltinOperator tfl_bop = static_cast(tfl_bop_i8); + return get_circle_builtin_code(static_cast(tfl_bop_i8)); +} + +int32_t get_circle_builtin_code(int32_t tfl_bop_i32) +{ + tflite::BuiltinOperator tfl_bop = static_cast(tfl_bop_i32); switch (tfl_bop) { #define TFL_OPERATOR(OP) \ case tflite::BuiltinOperator_##OP: \ - return static_cast(circle::BuiltinOperator_##OP); + return static_cast(circle::BuiltinOperator_##OP); #include "TFLOperator.lst" #undef TFL_OPERATOR + case tflite::BuiltinOperator_PLACEHOLDER_FOR_GREATER_OP_CODES: + return static_cast(circle::BuiltinOperator_PLACEHOLDER_FOR_GREATER_OP_CODES); default: throw std::runtime_error("tflite2circle: wrong op"); } diff --git a/compiler/tflite2circle/src/DataLookup.h b/compiler/tflite2circle/src/DataLookup.h index 5aeeb6e..f346b01 100644 --- a/compiler/tflite2circle/src/DataLookup.h +++ b/compiler/tflite2circle/src/DataLookup.h @@ -31,6 +31,7 @@ namespace tflite2circle circle::BuiltinOperator get_circle_builtin_code(tflite::BuiltinOperator tfl_bop); int8_t get_circle_builtin_code(int8_t tfl_bop_i8); +int32_t get_circle_builtin_code(int32_t tfl_bop_i32); /** * @brief Returns circle TensorType according to tflite. diff --git a/compiler/tflite2circle/src/TFLOperator.lst b/compiler/tflite2circle/src/TFLOperator.lst index 942c846..72a29fc 100644 --- a/compiler/tflite2circle/src/TFLOperator.lst +++ b/compiler/tflite2circle/src/TFLOperator.lst @@ -131,3 +131,23 @@ TFL_OPERATOR(SELECT_V2) TFL_OPERATOR(DENSIFY) TFL_OPERATOR(SEGMENT_SUM) TFL_OPERATOR(BATCH_MATMUL) +// PLACEHOLDER_FOR_GREATER_OP_CODES = 127 +TFL_OPERATOR(CUMSUM) +TFL_OPERATOR(CALL_ONCE) +TFL_OPERATOR(BROADCAST_TO) +TFL_OPERATOR(RFFT2D) +TFL_OPERATOR(CONV_3D) +TFL_OPERATOR(IMAG) +TFL_OPERATOR(REAL) +TFL_OPERATOR(COMPLEX_ABS) +TFL_OPERATOR(HASHTABLE) +TFL_OPERATOR(HASHTABLE_FIND) +TFL_OPERATOR(HASHTABLE_IMPORT) +TFL_OPERATOR(HASHTABLE_SIZE) +TFL_OPERATOR(REDUCE_ALL) +TFL_OPERATOR(CONV_3D_TRANSPOSE) +TFL_OPERATOR(VAR_HANDLE) +TFL_OPERATOR(READ_VARIABLE) +TFL_OPERATOR(ASSIGN_VARIABLE) +TFL_OPERATOR(BROADCAST_ARGS) +TFL_OPERATOR(RANDOM_STANDARD_NORMAL) diff --git a/compiler/vconone/CMakeLists.txt b/compiler/vconone/CMakeLists.txt index 93c33cd..38e8686 100644 --- a/compiler/vconone/CMakeLists.txt +++ b/compiler/vconone/CMakeLists.txt @@ -1,5 +1,5 @@ if (NOT VCONONE_VERSION) - set(VCONONE_VERSION 0x0000000000150001) + set(VCONONE_VERSION 0x0000000000160001) # NOTE order is [build patch minor major] # if VCONONE_VERSION is set with -D option, it will be cached # you may have to remove cache file if you remove -D option diff --git a/compiler/visq-unittest/CMakeLists.txt b/compiler/visq-unittest/CMakeLists.txt new file mode 100644 index 0000000..4eefa8d --- /dev/null +++ b/compiler/visq-unittest/CMakeLists.txt @@ -0,0 +1,66 @@ +if(NOT ENABLE_TEST) + return() +endif(NOT ENABLE_TEST) + +unset(VISQ_TEST_DEPS) + +### +### Copy test files +### +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/test + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/test ${CMAKE_CURRENT_BINARY_DIR}/test) + +list(APPEND VISQ_TEST_DEPS ${CMAKE_CURRENT_BINARY_DIR}/test) + +### +### Import visqlib module +### +get_target_property(VISQ_BIN_PATH visq BINARY_DIR) +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/visqlib + COMMAND ${CMAKE_COMMAND} -E create_symlink + ${VISQ_BIN_PATH}/visqlib ${CMAKE_CURRENT_BINARY_DIR}/visqlib) + +list(APPEND VISQ_TEST_DEPS ${CMAKE_CURRENT_BINARY_DIR}/visqlib) + +### +### Import pics module +### +get_target_property(PICS_BIN_PATH pics BINARY_DIR) +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/circle + COMMAND ${CMAKE_COMMAND} -E create_symlink + ${PICS_BIN_PATH}/circle ${CMAKE_CURRENT_BINARY_DIR}/circle) + +list(APPEND VISQ_TEST_DEPS ${CMAKE_CURRENT_BINARY_DIR}/circle) + +### +### Generate Resources.py +### +set(RESOURCE_FILE "${CMAKE_CURRENT_BINARY_DIR}/test/Resources.py") + +get_target_property(FP32_MODEL_DIR testDataGenerator BINARY_DIR) + +add_custom_command( + OUTPUT ${RESOURCE_FILE} + COMMAND ${CMAKE_COMMAND} -E echo 'fp32_model_dir=\"${FP32_MODEL_DIR}\"' >> ${RESOURCE_FILE} + COMMENT "Generate file to specify resource location" +) + +list(APPEND VISQ_TEST_DEPS ${RESOURCE_FILE}) + +add_custom_target(visq_unittest ALL DEPENDS ${VISQ_TEST_DEPS}) + +# Use Python in venv to run unittest with pydot module +add_test( + NAME visq_unittest + COMMAND ${NNCC_OVERLAY_DIR}/venv_2_8_0/bin/python -m unittest + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) + +if(ONE_UBUNTU_CODENAME_JAMMY) + add_test( + NAME visq_210_unittest + COMMAND ${NNCC_OVERLAY_DIR}/venv_2_10_1/bin/python -m unittest + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) +endif(ONE_UBUNTU_CODENAME_JAMMY) diff --git a/compiler/visq-unittest/README.md b/compiler/visq-unittest/README.md new file mode 100644 index 0000000..e90837b --- /dev/null +++ b/compiler/visq-unittest/README.md @@ -0,0 +1,3 @@ +# visq-unittest + +_visq-unittest_ is a module to test visq diff --git a/compiler/visq-unittest/requires.cmake b/compiler/visq-unittest/requires.cmake new file mode 100644 index 0000000..bf7a41f --- /dev/null +++ b/compiler/visq-unittest/requires.cmake @@ -0,0 +1,3 @@ +require("pics") +require("common-artifacts") +require("visq") diff --git a/compiler/visq-unittest/test/__init__.py b/compiler/visq-unittest/test/__init__.py new file mode 100644 index 0000000..0c29109 --- /dev/null +++ b/compiler/visq-unittest/test/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE THIS FILE diff --git a/compiler/visq-unittest/test/testDotBuilder.py b/compiler/visq-unittest/test/testDotBuilder.py new file mode 100644 index 0000000..b657d60 --- /dev/null +++ b/compiler/visq-unittest/test/testDotBuilder.py @@ -0,0 +1,44 @@ +# Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Test visqlib.DotBuilder module""" + +import unittest +import pydot +from pathlib import Path + +from visqlib.DotBuilder import DotBuilder +from test.Resources import fp32_model_dir + + +class VisqDotBuilderTest(unittest.TestCase): + def test_dot_builder_wrong_input_file(self): + self.assertRaises(FileNotFoundError, DotBuilder, "wrong", "wrong", "wrong", + "wrong") + + def test_dot_builder(self): + test_colors = [{"b": 0, "e": 0.5, "c": "green"}, {"b": 0.5, "e": 1, "c": "red"}] + test_qerror_map = dict() + test_qerror_map["ofm"] = 0.1 + builder = DotBuilder(fp32_model_dir + "/Add_000.circle", "Add_000.dot", "MPEIR", + test_colors) + builder.save(test_qerror_map) + + graph = pydot.graph_from_dot_file("Add_000.dot")[0] + # Why 1? 0 is output + ofm_node = graph.get_node("\"ofm\"")[1] + self.assertEqual("green", ofm_node.get_fillcolor()) + + +if __name__ == "__main__": + unittest.main() diff --git a/compiler/visq-unittest/test/testPalette.py b/compiler/visq-unittest/test/testPalette.py new file mode 100644 index 0000000..bf5fbb4 --- /dev/null +++ b/compiler/visq-unittest/test/testPalette.py @@ -0,0 +1,42 @@ +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Test visqlib.Palette module''' + +import unittest + +from visqlib.Palette import YLORRD9Palette + + +class VisqPaletteTest(unittest.TestCase): + def test_ylorrd9(self): + min_test = [0.0, 0, -100, -100] + max_test = [1.0, 500, 100, -10] + + for min_val, max_val in zip(min_test, max_test): + palette = YLORRD9Palette(qerror_min=min_val, qerror_max=max_val) + cs = palette.colorscheme() + self.assertEqual(9, len(cs)) + + def test_ylorrd9_wrong_minmax(self): + min_test = [0.0, 10] + max_test = [0.0, 0] + + for min_val, max_val in zip(min_test, max_test): + # min must be less than max + self.assertRaises( + RuntimeError, YLORRD9Palette, qerror_min=min_val, qerror_max=max_val) + + +if __name__ == '__main__': + unittest.main() diff --git a/compiler/visq-unittest/test/testQErrorComputer.py b/compiler/visq-unittest/test/testQErrorComputer.py new file mode 100644 index 0000000..1c6b185 --- /dev/null +++ b/compiler/visq-unittest/test/testQErrorComputer.py @@ -0,0 +1,150 @@ +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Test visqlib.QErrorComputer module''' + +import unittest +import tempfile +import numpy as np +import os + +from visqlib.QErrorComputer import MPEIRComputer +from visqlib.QErrorComputer import MSEComputer +from visqlib.QErrorComputer import TAEComputer + + +class VisqQErrorComputerTest(unittest.TestCase): + def setUp(self): + "Called before running each test" + self.fp32_dir = tempfile.TemporaryDirectory() + self.fq_dir = tempfile.TemporaryDirectory() + + def tearDown(self): + "Called after running each test" + self.fp32_dir.cleanup() + self.fq_dir.cleanup() + + def _setUpSingleTensorData(self): + with open(self.fp32_dir.name + '/tensors.txt', 'w') as f: + f.write('test') + with open(self.fq_dir.name + '/tensors.txt', 'w') as f: + f.write('test') + os.mkdir(self.fp32_dir.name + '/0') + os.mkdir(self.fq_dir.name + '/0') + test_data = np.zeros(16) + np.save(self.fp32_dir.name + '/0/test.npy', test_data) + np.save(self.fq_dir.name + '/0/test.npy', test_data) + + def _setUpTwoTensorData(self): + with open(self.fp32_dir.name + '/tensors.txt', 'w') as f: + f.write('test') + with open(self.fq_dir.name + '/tensors.txt', 'w') as f: + f.write('test') + os.mkdir(self.fp32_dir.name + '/0') + os.mkdir(self.fp32_dir.name + '/1') + os.mkdir(self.fq_dir.name + '/0') + os.mkdir(self.fq_dir.name + '/1') + test_data_one = np.ones(16) + test_data_zero = np.zeros(16) + np.save(self.fp32_dir.name + '/0/test.npy', test_data_one) + np.save(self.fp32_dir.name + '/1/test.npy', test_data_zero) + np.save(self.fq_dir.name + '/0/test.npy', test_data_zero) + np.save(self.fq_dir.name + '/1/test.npy', test_data_zero) + # Golden: (1 + 0) / 2 = 0.5 for MSE + + def _setUpDifferentTensorData(self): + # Two fp32 data (test, test2) + # One fq data (test) + # NOTE When does this happen? + # This case can happen because visq ignores nodes that do not affect qerrors. + # For example, RESHAPE Op does not affect qerrors, so its fq data is not dumped, + # although it is listed in 'tensors.txt'. + with open(self.fp32_dir.name + '/tensors.txt', 'w') as f: + f.writelines(['test\n', 'test2']) + with open(self.fq_dir.name + '/tensors.txt', 'w') as f: + f.writelines(['test\n', 'test2']) + os.mkdir(self.fp32_dir.name + '/0') + os.mkdir(self.fq_dir.name + '/0') + test_data = np.zeros(16) + np.save(self.fp32_dir.name + '/0/test.npy', test_data) + np.save(self.fp32_dir.name + '/0/test2.npy', test_data) + np.save(self.fq_dir.name + '/0/test.npy', test_data) + + def test_MPEIR(self): + self._setUpSingleTensorData() + + computer = MPEIRComputer(self.fp32_dir.name, self.fq_dir.name) + qmap = computer.run() + self.assertAlmostEqual(0.0, qmap['test']) + + def test_MPEIR_different_tensors(self): + self._setUpDifferentTensorData() + + computer = MPEIRComputer(self.fp32_dir.name, self.fq_dir.name) + qmap = computer.run() + self.assertAlmostEqual(0.0, qmap['test']) + + def test_MSE(self): + self._setUpSingleTensorData() + + computer = MSEComputer(self.fp32_dir.name, self.fq_dir.name) + qmap, qmin, qmax = computer.run() + self.assertAlmostEqual(0.0, qmap['test']) + self.assertAlmostEqual(0.0, qmin) + self.assertAlmostEqual(0.0, qmax) + + def test_MSE_two(self): + self._setUpTwoTensorData() + + computer = MSEComputer(self.fp32_dir.name, self.fq_dir.name) + qmap, qmin, qmax = computer.run() + self.assertAlmostEqual(0.5, qmap['test']) + self.assertAlmostEqual(0.0, qmin) + self.assertAlmostEqual(1.0, qmax) + + def test_MSE_different_tensors(self): + self._setUpDifferentTensorData() + + computer = MSEComputer(self.fp32_dir.name, self.fq_dir.name) + qmap, qmin, qmax = computer.run() + self.assertAlmostEqual(0.0, qmap['test']) + self.assertAlmostEqual(0.0, qmin) + self.assertAlmostEqual(0.0, qmax) + + def test_TAE(self): + self._setUpSingleTensorData() + + computer = TAEComputer(self.fp32_dir.name, self.fq_dir.name) + qmap, qmin, qmax = computer.run() + self.assertAlmostEqual(0.0, qmap['test']) + + def test_TAE_different_options(self): + self._setUpDifferentTensorData() + + computer = TAEComputer(self.fp32_dir.name, self.fq_dir.name) + qmap, qmin, qmax = computer.run() + self.assertAlmostEqual(0.0, qmap['test']) + self.assertAlmostEqual(0.0, qmin) + self.assertAlmostEqual(0.0, qmax) + + def test_TAE_two(self): + self._setUpTwoTensorData() + computer = TAEComputer(self.fp32_dir.name, self.fq_dir.name) + qmap, qmin, qmax = computer.run() + self.assertAlmostEqual(0.0, qmin) + self.assertAlmostEqual(8.0, qmap['test']) + self.assertAlmostEqual(16.0, qmax) + + +if __name__ == '__main__': + unittest.main() diff --git a/compiler/visq-unittest/test/testUtil.py b/compiler/visq-unittest/test/testUtil.py new file mode 100644 index 0000000..51f6eb9 --- /dev/null +++ b/compiler/visq-unittest/test/testUtil.py @@ -0,0 +1,55 @@ +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Test visqlib.Util module''' + +import unittest + +from visqlib.Util import to_filename +from visqlib.Util import valid_attr +from visqlib.Util import pretty_float + + +class VisqUtilTest(unittest.TestCase): + def test_to_filename(self): + data = 'abc/d/e' + self.assertEqual('abc_d_e', to_filename(data)) + + long_data = 'x' * 300 + self.assertEqual('x' * 255, to_filename(long_data)) + + def test_valid_attr(self): + class Test: + def __init__(self): + self.a = 'a' + + test = Test() + self.assertTrue(valid_attr(test, 'a')) + self.assertFalse(valid_attr(test, 'b')) + + def test_pretty_float(self): + test_configs = [0.123456, 12.3456, [0.123456], {'test': [0.123456]}] + three_digits_ans = [0.123, 12.346, [0.123], {'test': [0.123]}] + for test_data, ans in zip(test_configs, three_digits_ans): + res = pretty_float(test_data, ndigits=3) + self.assertEqual(res, ans) + + test_configs = [0.123456, 12.3456, [0.123456], {'test': [0.123456]}] + four_digits_ans = [0.1235, 12.3456, [0.1235], {'test': [0.1235]}] + for test_data, ans in zip(test_configs, four_digits_ans): + res = pretty_float(test_data, ndigits=4) + self.assertEqual(res, ans) + + +if __name__ == '__main__': + unittest.main() diff --git a/compiler/visq/CMakeLists.txt b/compiler/visq/CMakeLists.txt new file mode 100644 index 0000000..0dd1b78 --- /dev/null +++ b/compiler/visq/CMakeLists.txt @@ -0,0 +1,67 @@ +unset(VISQ_DEPS) + +### +### Set up visq executable +### +set(VISQ_FILE "visq") +set(VISQ_SRC "${CMAKE_CURRENT_SOURCE_DIR}/${VISQ_FILE}") +set(VISQ_BIN "${CMAKE_CURRENT_BINARY_DIR}/${VISQ_FILE}") + +add_custom_command(OUTPUT ${VISQ_BIN} + COMMAND ${CMAKE_COMMAND} -E copy "${VISQ_SRC}" "${VISQ_BIN}" + DEPENDS ${VISQ_SRC} + COMMENT "Generate ${VISQ_BIN}" +) + +list(APPEND VISQ_DEPS ${VISQ_BIN}) + +### +### Set up visqlib directory +### +set(VISQ_PYTHON_DIR "visqlib") +set(VISQ_PYTHON_DIR_BIN "${CMAKE_CURRENT_BINARY_DIR}/${VISQ_PYTHON_DIR}") + +add_custom_command(OUTPUT ${VISQ_PYTHON_DIR_BIN} + COMMAND ${CMAKE_COMMAND} -E make_directory "${VISQ_PYTHON_DIR_BIN}" + COMMENT "Generate ${VISQ_PYTHON_DIR_BIN}" +) + +list(APPEND VISQ_DEPS ${VISQ_PYTHON_DIR_BIN}) + +### +### Set up Python files +### +set(VISQ_PYTHON_FILES DumpFakeQuantFM.py + DumpFP32FM.py + Palette.py + QErrorComputer.py + DotBuilder.py + Util.py) + +foreach(VISQ_PYTHON_FILE IN ITEMS ${VISQ_PYTHON_FILES}) + set(VISQ_PYTHON_FILE_SRC "${CMAKE_CURRENT_SOURCE_DIR}/${VISQ_PYTHON_DIR}/${VISQ_PYTHON_FILE}") + set(VISQ_PYTHON_FILE_BIN "${CMAKE_CURRENT_BINARY_DIR}/${VISQ_PYTHON_DIR}/${VISQ_PYTHON_FILE}") + + add_custom_command(OUTPUT ${VISQ_PYTHON_FILE_BIN} + COMMAND ${CMAKE_COMMAND} -E copy "${VISQ_PYTHON_FILE_SRC}" "${VISQ_PYTHON_FILE_BIN}" + DEPENDS ${VISQ_PYTHON_SRC} + COMMENT "Generate ${VISQ_PYTHON_FILE_BIN}" + ) + + list(APPEND VISQ_DEPS ${VISQ_PYTHON_FILE_BIN}) + +endforeach(VISQ_PYTHON_FILE) + +add_custom_target(visq ALL DEPENDS ${VISQ_DEPS}) + +install(FILES ${VISQ_FILE} + PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE + WORLD_READ WORLD_EXECUTE + DESTINATION bin) + +install(DIRECTORY ${VISQ_PYTHON_DIR} + FILE_PERMISSIONS OWNER_WRITE OWNER_READ + GROUP_READ + WORLD_READ + DESTINATION bin) diff --git a/compiler/visq/README.md b/compiler/visq/README.md new file mode 100644 index 0000000..0d0a838 --- /dev/null +++ b/compiler/visq/README.md @@ -0,0 +1,32 @@ +# visq + +_visq_ is a module to generate a json file used to visualize layer-wise quantization errors +(https://github.com/Samsung/ONE/issues/9694). + +## Example +```bash +$ ./visq --fp32_circle sample.circle \ + --q_circle sample.q.circle \ + --data test.h5 \ + --mpeir_output sample.mpeir.visq.json \ + --mse_output sample.mse.visq.json \ + --tae_output sample.tae.visq.json \ + --dump_dot_graph +``` + +The above command will generate +- `sample.mpeir.visq.json`: Json file that contains layer-wise mpeir. +- `sample.mse.visq.json`: Json file that conatins layer-wise mse. +- `sample.mpeir.visq.json.dot`: Dot graph for layer-wise mpeir. +- `sample.tae.visq.json.dot`: Dot graph for layer-wise tae. +- `sample.mse.visq.json.dot`: Dot graph for layer-wise mse. + +## Quantization error metrics + +f: Result of fp32 model +q: Result of quantized model + +- MPEIR: Mean Peak Error to Interval Ratio = Average(max(|f - q|) / (max(f) - min(f) + epsilon)) +epsilon: 1e-6 +- MSE: Mean Squared Error = Average(square(f - q)) +- TAE: Total Absolute Error = Sum(|f - q|) diff --git a/compiler/visq/requires.cmake b/compiler/visq/requires.cmake new file mode 100644 index 0000000..fdf32c6 --- /dev/null +++ b/compiler/visq/requires.cmake @@ -0,0 +1,2 @@ +require("dalgona") +require("circle-quantizer") diff --git a/compiler/visq/visq b/compiler/visq/visq new file mode 100644 index 0000000..02f63ab --- /dev/null +++ b/compiler/visq/visq @@ -0,0 +1,381 @@ +#!/usr/bin/env bash +''''export SCRIPT_PATH="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" # ''' +''''export PY_PATH=${SCRIPT_PATH}/venv/bin/python # ''' +''''test -f ${PY_PATH} && exec ${PY_PATH} "$0" "$@" # ''' +''''echo "Error: Virtual environment not found. Please run 'one-prepare-venv' command." # ''' +''''exit 255 # ''' + +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import subprocess +import tempfile +import json +import os +import math +import sys + +import h5py as h5 +import numpy as np + +from shutil import copyfile +from pathlib import Path + +from visqlib.Palette import YLORRD9Palette +from visqlib.QErrorComputer import MPEIRComputer, MSEComputer, TAEComputer +from visqlib.Util import valid_attr, pretty_float +from visqlib.DotBuilder import DotBuilder + + +def _get_parser(): + parser = argparse.ArgumentParser( + description='Command line tool to visualize layer-wise quantization errors') + parser.add_argument( + "-f", + "--fp32_circle", + type=str, + help="Path to the fp32 circle model.", + required=True) + parser.add_argument( + "-q", + "--q_circle", + type=str, + help="Path to the quantized circle model.", + required=True) + parser.add_argument( + "-d", + "--data", + type=str, + help= + "Path to the data used for inference. Random data will be used if this option is not given.", + required=False) + parser.add_argument( + "--mpeir_output", + type=str, + help="Path to the output json file (qerror metric = MPEIR).", + required=False) + parser.add_argument( + "--mse_output", + type=str, + help="Path to the output json file (qerror metric = MSE).", + required=False) + parser.add_argument( + "--tae_output", + type=str, + help="Path to the output json file (qerror metric = TAE).", + required=False) + parser.add_argument( + "--dump_dot_graph", action="store_true", help="Dump dot graph.", required=False) + parser.add_argument( + "-b", + "--batch_size", + type=int, + help="Batch size to process large datasets.", + required=False) + + return parser + + +def _verify_args(args): + """Verify the given arguments""" + + valid_outputs = ['mpeir_output', 'mse_output', 'tae_output'] + + # Check if at least one output option is given + num_outputs = 0 + for output_name in valid_outputs: + if valid_attr(args, output_name): + num_outputs += 1 + + if num_outputs == 0: + raise RuntimeError("At least one output should be given.") + + +def _run_dalgona(model, data, analysis, save_dir): + dir_path = Path(__file__).parent.resolve() + dalgona_path = os.path.join(dir_path, 'dalgona') + cmd = [dalgona_path] + cmd += ['--input_model', str(model)] + cmd += ['--analysis', str(analysis)] + if data != None: + cmd += ['--input_data', str(data)] + cmd += ['--analysis_args', str(save_dir)] + + try: + subprocess.run(cmd, capture_output=True, check=True) + except subprocess.CalledProcessError as e: + print('Error raised while running the below command') + print(' '.join(cmd)) + print(e.output) + raise + + +# Generate h5 file that contains a dataset of a single batch +# This is for batch execution of visq +def gen_batch_h5(inputs_data, inputs_path): + # Create h5 file + output_path = inputs_path + "/inputs.h5" + h5_file = h5.File(output_path, 'w') + group = h5_file.create_group("value") + group.attrs['desc'] = "Input data" + + for i in range(len(inputs_data)): + sample = group.create_group(str(i)) + for j in range(len(inputs_data[i])): + sample.create_dataset(str(j), data=inputs_data[i][j]) + + h5_file.close() + return output_path + + +# Aggregate intermediate results for a given data +def advance_on_data(fp32_model, fq_model, data, computers): + + curr_dir = Path(__file__).parent.resolve() + dump_fp32_py = curr_dir / 'visqlib' / 'DumpFP32FM.py' + dump_fq_py = curr_dir / 'visqlib' / 'DumpFakeQuantFM.py' + + with tempfile.TemporaryDirectory() as fp32_dir, \ + tempfile.TemporaryDirectory() as fq_dir: + + _run_dalgona(fp32_model, data, dump_fp32_py, fp32_dir) + copyfile(fp32_dir + '/tensors.txt', fq_dir + '/tensors.txt') + _run_dalgona(fq_model, data, dump_fq_py, fq_dir) + + for metric_key in computers: + computers[metric_key][0].advance_on(fp32_dir, fq_dir) + + +def _run_batch(fp32_model, fq_model, data, computers, batch_size): + with tempfile.TemporaryDirectory() as inputs_dir: + with h5.File(data, 'r') as f: + dataset = f['value'] + + inputs = [] + for data_index in dataset: + cur_inputs = [] + for input_index in dataset[data_index]: + d = dataset[data_index][input_index][:] + cur_inputs.append(np.array(d, np.float32)) + + inputs.append(cur_inputs) + if len(inputs) >= batch_size: + input_path = gen_batch_h5(inputs, inputs_dir) + advance_on_data(fp32_model, fq_model, input_path, computers) + inputs = [] + + if len(inputs) > 0: + input_path = gen_batch_h5(inputs, inputs_dir) + advance_on_data(fp32_model, fq_model, input_path, computers) + + +def _fake_quantize(input_model, output_model): + dir_path = Path(__file__).parent.resolve() + circle_quantizer_path = os.path.join(dir_path, 'circle-quantizer') + cmd = [circle_quantizer_path] + cmd += ['--fake_quantize'] + cmd += [str(input_model)] + cmd += [str(output_model)] + + try: + subprocess.run(cmd, check=True) + except subprocess.CalledProcessError as e: + print('Error raised while running the below command') + print(' '.join(cmd)) + print(e.output) + raise + + +# Recursively visit items and check if there is Infinity or NaN +def _check_float(item): + if isinstance(item, dict): + for v in item.values(): + _check_float(v) + if isinstance(item, list): + for v in item: + _check_float(v) + if isinstance(item, float): + if item == -float('inf') or item == float('inf'): + raise RuntimeError('Infinite value detected. Value must be float') + if math.isnan(item): + raise RuntimeError('NaN value detected. Value must be float') + + +def _build_json(model, metric, colorscheme, error): + # model: string + # metric: string + # colorscheme: list ['b': begin, 'e': end, 'c':color] + # error: dict {tensor_name:error} + + meta = {} + meta["model"] = model + meta["metric"] = metric + meta["colorscheme"] = pretty_float(colorscheme) + result = {} + result["meta"] = meta + # Why list? To support multiple subgraphs + result["error"] = [pretty_float(error)] + + # Invariants + _check_float(meta["colorscheme"]) + _check_float(result["error"]) + return result + + +def _save_dot(circle_path: str, dot_path: str, metric: str, colors: list, qerror: dict): + # circle_path: Path to the circle model (required to build graph) + # dot_path: Path to the output dot file + # metric: Metric name (ex: MPEIR, MSE) + # colors: list [{'b': begin, 'e': end, 'c':color}, ..] + # qerror: dict {tensor_name (str) -> qerror (float)} + builder = DotBuilder( + circle_path=circle_path, dot_path=dot_path, metric=metric, colors=colors) + + builder.save(qerror) + + +def run_on_data_batchwise(fp32_model, q_model, data, dump_dot_graph, computers, + batch_size): + + with tempfile.TemporaryDirectory() as model_dir: + fq_model = model_dir + '/fq_model.circle' + + # Step 1. Fake quantize quantized circle model + _fake_quantize(q_model, fq_model) + + # process the whole dataset batch by batch + _run_batch(fp32_model, fq_model, data, computers, batch_size) + + #compute the final results + for metric_key in computers: + cur_computer = computers[metric_key][0] + output = computers[metric_key][1] + if metric_key == 'MPEIR': + qerror_map = cur_computer.get_final_result() + q_min = 0.0 + q_max = 1.0 + elif metric_key == 'MSE' or metric_key == 'TAE': + qerror_map, q_min, q_max = cur_computer.get_final_result() + + palette = YLORRD9Palette(qerror_min=q_min, qerror_max=q_max) + result = _build_json( + metric=metric_key, + model=Path(fp32_model).name, + colorscheme=palette.colorscheme(), + error=qerror_map) + with open(output, "w") as f: + json.dump(result, f) + + if dump_dot_graph: + _save_dot( + circle_path=fp32_model, + dot_path=output + '.dot', + metric=metric_key, + colors=palette.colorscheme(), + qerror=qerror_map) + + +def run_on_data(fp32_model, q_model, data, dump_dot_graph, computers): + curr_dir = Path(__file__).parent.resolve() + dump_fp32_py = curr_dir / 'visqlib' / 'DumpFP32FM.py' + dump_fq_py = curr_dir / 'visqlib' / 'DumpFakeQuantFM.py' + + with tempfile.TemporaryDirectory() as model_dir, \ + tempfile.TemporaryDirectory() as fp32_dir, \ + tempfile.TemporaryDirectory() as fq_dir: + fq_model = model_dir + '/fq_model.circle' + + # Step 1. Fake quantize quantized circle model + _fake_quantize(q_model, fq_model) + + # Step 2. Run dalgona to dump intermediate FMs in FP32 model + _run_dalgona(fp32_model, data, dump_fp32_py, fp32_dir) + + # Copy list of dumped tensors + copyfile(fp32_dir + '/tensors.txt', fq_dir + '/tensors.txt') + + # Step 3. Run dalgona to dump intermediate FMs in fq model + _run_dalgona(fq_model, data, dump_fq_py, fq_dir) + + # Step 4. Read results and compute qerror + for metric_key in computers: + cur_computer = computers[metric_key][0] + output = computers[metric_key][1] + cur_computer.advance_on(fp32_dir, fq_dir) + if metric_key == 'MPEIR': + qerror_map = cur_computer.get_final_result() + q_min = 0.0 + q_max = 1.0 + elif metric_key == 'MSE' or metric_key == 'TAE': + qerror_map, q_min, q_max = cur_computer.get_final_result() + + palette = YLORRD9Palette(qerror_min=q_min, qerror_max=q_max) + result = _build_json( + metric=metric_key, + model=Path(fp32_model).name, + colorscheme=palette.colorscheme(), + error=qerror_map) + with open(output, "w") as f: + json.dump(result, f) + + if dump_dot_graph: + _save_dot( + circle_path=fp32_model, + dot_path=output + '.dot', + metric=metric_key, + colors=palette.colorscheme(), + qerror=qerror_map) + + +def main(): + # parse arguments + parser = _get_parser() + args = parser.parse_args() + _verify_args(args) + + fp32_model = args.fp32_circle + q_model = args.q_circle + data = None + if valid_attr(args, 'data'): + data = args.data + dump_dot_graph = args.dump_dot_graph + batch_size = None + if valid_attr(args, 'batch_size'): + batch_size = args.batch_size + + computers = {} + if args.mpeir_output: + computers['MPEIR'] = (MPEIRComputer(None, None), args.mpeir_output) + + if args.mse_output: + computers['MSE'] = (MSEComputer(None, None), args.mse_output) + + if args.tae_output: + computers['TAE'] = (TAEComputer(None, None), args.tae_output) + + if batch_size == None: + run_on_data(fp32_model, q_model, data, dump_dot_graph, computers) + else: + run_on_data_batchwise(fp32_model, q_model, data, dump_dot_graph, computers, + batch_size) + + +if __name__ == '__main__': + try: + main() + except Exception as e: + prog_name = os.path.basename(__file__) + print(f"{prog_name}: {type(e).__name__}: " + str(e), file=sys.stderr) + sys.exit(255) diff --git a/compiler/visq/visqlib/DotBuilder.py b/compiler/visq/visqlib/DotBuilder.py new file mode 100644 index 0000000..a6afb96 --- /dev/null +++ b/compiler/visq/visqlib/DotBuilder.py @@ -0,0 +1,165 @@ +# Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pydot +import math + +from circle import Model + +from pathlib import Path + + +# Return the name of the tensor +def _tensor_name(graph, tid): + return graph.Tensors(tid).Name().decode('utf-8') + + +# Return double-quoted string +def _quote(string: str): + return '"' + string + '"' + + +# Class to build dot graph from qerror_map +class DotBuilder: + def __init__(self, circle_path: str, dot_path: str, metric: str, colors: str): + ''' + circle_path: Path to the fp32 circle model (required to build graph) + dot_path: Path to the saved dot file + metric: Metric name (ex: MPEIR, MSE) + colors: List of color slots [{'b': begin, 'e': end, 'c':color}, ..] + ''' + with open(circle_path, 'rb') as f: + self._model = Model.Model.GetRootAsModel(f.read()) + + if self._model.SubgraphsLength() != 1: + raise RuntimeError("Only one subgraph is supported") + + self._name = Path(circle_path).name + self._dot_path = dot_path + self._metric = metric + self._colors = colors + + # Return color (RGB) for the given qerror + def _get_color(self, qerror: float): + # Find a slot where qerror is in the range of [begin, end] + for slot in self._colors: + begin = slot['b'] + end = slot['e'] + if (qerror > begin or math.isclose( + qerror, begin)) and (qerror < end or math.isclose(qerror, end)): + return slot['c'] + + # Use the first color if qerror is smaller than the first begin + if qerror < self._colors[0]['b']: + return self._colors[0]['c'] + + # Use the last color if qerror is larger than the last end + if qerror > self._colors[-1]['e']: + return self._colors[-1]['c'] + + raise RuntimeError("Color ID not found. QError: " + str(qerror)) + + # Generate a pydot.Node object which represents the color table + def _gen_color_table(self): + color_table = "< " + for slot in self._colors: + begin = slot['b'] + end = slot['e'] + color = slot['c'] + color_table += "" + color_table += "
" + color_table += self._metric + ": {:.4f}".format( + begin) + " ~ " + "{:.4f}".format(end) + color_table += "
>" + return pydot.Node("color_table", shape='none', label=color_table) + + # Save dot graph to self._dot_path + def save(self, qerror_map: dict): + ''' + qerror_map: Dictionary of {op_name (str) -> qerror (float)} + ''' + # Build graph + DOT = pydot.Dot(self._name, graph_type="digraph") + + # Add color table + DOT.add_node(self._gen_color_table()) + + # Dictionary from output tensor name to Op name {str -> str} + # This dict is for handling Ops with multiple output tensors. + # We use the first output tensor's name as the Op name, following + # the implementation of luci IR + output_to_op = dict() + + graph = self._model.Subgraphs(0) + + # Add Input nodes + for i in range(graph.InputsLength()): + name = _tensor_name(graph, graph.Inputs(i)) + output_to_op[name] = name + DOT.add_node(pydot.Node(_quote(name))) + + # Add Output nodes + for i in range(graph.OutputsLength()): + name = _tensor_name(graph, graph.Outputs(i)) + output_to_op[name] = name + DOT.add_node(pydot.Node(_quote(name))) + + # Add Edges + for i in range(graph.OperatorsLength()): + op = graph.Operators(i) + # Name of the first output tensor + op_name = _tensor_name(graph, op.Outputs(0)) + if op.OutputsLength() == 0: + print(op_name) + continue + + if op_name in qerror_map: + qerror = qerror_map[op_name] + node = pydot.Node( + _quote(op_name), + style="filled", + fillcolor=self._get_color(qerror), + xlabel=self._metric + ": {:.4f}".format(qerror)) + else: + # qerror_map does not have qerror info for the op. Color gray. + # When this happen? visq does not collect qerror info of some Ops + # For example, Reshape Op does not change values, so its qerror + # info is not collected. + node = pydot.Node(_quote(op_name), style="filled", fillcolor='gray') + + DOT.add_node(node) + + for output_idx in range(op.OutputsLength()): + output_name = _tensor_name(graph, op.Outputs(output_idx)) + # Set Op name as the first output tensor name (op_name) + output_to_op[output_name] = op_name + + for j in range(op.InputsLength()): + op_input = op.Inputs(j) + + # Optional input case (ex: For TConv with no bias, bias is -1) + if op_input == -1: + continue + + op_input_name = _tensor_name(graph, op_input) + if op_input_name not in output_to_op: + continue + + # Use the saved name to handle multiple outputs + op_input_name = output_to_op[op_input_name] + DOT.add_edge(pydot.Edge(_quote(op_input_name), _quote(op_name))) + + DOT.write(self._dot_path) diff --git a/compiler/visq/visqlib/DumpFP32FM.py b/compiler/visq/visqlib/DumpFP32FM.py new file mode 100644 index 0000000..d5b7545 --- /dev/null +++ b/compiler/visq/visqlib/DumpFP32FM.py @@ -0,0 +1,54 @@ +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Script that dumps FM of FP32 model +# NOTE This script runs on dalgona + +import numpy as np + +from pathlib import Path +from Util import to_filename + + +# Dump FP32 model's intermediate FM data and their names +# +# Before +# self._dir/ +# +# After +# self._dir/ +# tensors.txt +# .npy +# NOTE TENSOR_NAME is transformed by to_filename +class DumpFP32FM: + def StartAnalysis(self, args): + self._dir = Path(args) + self._num_data = 0 + self._tensor_names = set() + + def EndNetworkExecution(self, outputs): + self._num_data += 1 + + def DefaultOpPost(self, name, opcode, inputs, output): + # Save intermediate FM into tensor_name.npy + data_path = self._dir / str(self._num_data) + data_path.mkdir(parents=False, exist_ok=True) + np.save(str(data_path / to_filename(name)), output['data']) + self._tensor_names.add(name) + + def EndAnalysis(self): + # Save tensor names line by line + with open(self._dir / 'tensors.txt', 'w') as f: + for name in self._tensor_names: + f.write("%s\n" % name) diff --git a/compiler/visq/visqlib/DumpFakeQuantFM.py b/compiler/visq/visqlib/DumpFakeQuantFM.py new file mode 100644 index 0000000..0248428 --- /dev/null +++ b/compiler/visq/visqlib/DumpFakeQuantFM.py @@ -0,0 +1,66 @@ +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Script that dumps dequantized FM +# NOTE This script runs on dalgona + +import numpy as np + +from pathlib import Path +from Util import to_filename + +# Fake-quantized Op has the postfix of fq_postfix +# TODO Remove coupling with fake quantization codes +fq_postfix = '_FQ_Quantize_FQ_Dequantize' + + +# Return the original name before fake quantization +# Return None if name is not from fake quantization (Dequantize Op in original model) +# TODO Handle the case when the original node's name contains fq_postfix +def _name_before_fq(name): + if not name.endswith(fq_postfix): + return None + + return name[0:name.find(fq_postfix)] + + +# Dump fake-quantized model's intermediate FM data according to tensors.txt +# +# Before +# self._dir/ +# tensors.txt +# +# After +# self._dir/ +# tensors.txt +# .npy +# NOTE TENSOR_NAME is transformed by to_filename +class DumpFakeQuantFM: + def StartAnalysis(self, args): + self._dir = Path(args) + self._num_data = 0 + with open(self._dir / 'tensors.txt') as f: + self._target_tensors = set([line.rstrip() for line in f]) + + def EndNetworkExecution(self, outputs: list): + self._num_data += 1 + + # TODO Use DequantizePost when dalgona supports it + def DefaultOpPost(self, name, opcode, inputs, output): + if opcode == 'Dequantize': + orig_name = _name_before_fq(name) + if orig_name in self._target_tensors: + data_path = self._dir / str(self._num_data) + data_path.mkdir(parents=False, exist_ok=True) + np.save(str(data_path / to_filename(orig_name)), output['data']) diff --git a/compiler/visq/visqlib/Palette.py b/compiler/visq/visqlib/Palette.py new file mode 100644 index 0000000..9df7223 --- /dev/null +++ b/compiler/visq/visqlib/Palette.py @@ -0,0 +1,70 @@ +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# Class to save colorscheme +class Palette: + # Child class must implement __init__ to fill the below members + def __init__(self): + # Element of self._slots has [lower bound, upper bound] of qerrors to decide a color + self._slots = [] + # Element of self._colors has rgb values in string format + self._colors = [] + raise NotImplementedError('Child class must implement __init__') + + # Return color scheme as a list of objects + # Each object has the following attributes + # b: begin qerror + # e: end qerror + # c: color (in RGB string) + def colorscheme(self): + cs = [] + for slot, color in zip(self._slots, self._colors): + cs.append({"b": slot[0], "e": slot[1], "c": color}) + return cs + + +# Ranges of slots are defined by qerror_min/qerror_max +# Each slot has a uniform range +# For example, if qerror_min = 0.0, qerror_max = 1.0, number of colors = 10 +# Ranges of slots will be as follows. +# [0.0, 0.1], [0.1, 0.2], [0.2, 0.3] ... [0.8, 0.9], [0.9, 1.0] +class UniformPalette(Palette): + def __init__(self, qerror_min, qerror_max, colors): + self._colors = colors + self._slots = [] + qerror_range = qerror_max - qerror_min + num_colors = len(self._colors) + for i in range(num_colors): + lower_bound = qerror_min + i * (qerror_range / num_colors) + upper_bound = qerror_min + (i + 1) * (qerror_range / num_colors) + + self._slots.append([lower_bound, upper_bound]) + + # Invariant + assert len(self._slots) == num_colors + + +# Palette for ylorrd9 colorscheme +class YLORRD9Palette(UniformPalette): + def __init__(self, qerror_min, qerror_max): + if qerror_min >= qerror_max: + raise RuntimeError('min must be less than max') + + # From https://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=9 + colors = [ + "#ffffcc", "#ffeda0", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", + "#bd0026", "#800026" + ] + super().__init__(qerror_min, qerror_max, colors) diff --git a/compiler/visq/visqlib/QErrorComputer.py b/compiler/visq/visqlib/QErrorComputer.py new file mode 100644 index 0000000..c60556f --- /dev/null +++ b/compiler/visq/visqlib/QErrorComputer.py @@ -0,0 +1,200 @@ +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import glob +import numpy as np + +from pathlib import Path +from visqlib.Util import to_filename + + +class QErrorComputer: + def __init__(self, fp32_dir, fq_dir): + self._fp32_dir = fp32_dir + self._fq_dir = fq_dir + self.qerror_map = dict() + self._num_processed_data = 0 + + def collect_data_path(self, fp32_dir, fq_dir): + # Assumption: FM data are saved as follows + # + # fp32_dir/ + # tensors.txt + # / + # .npy + # + # fq_dir/ + # tensors.txt + # / + # .npy + self._num_data = len(list(filter(os.path.isdir, glob.glob(fp32_dir + '/*')))) + if self._num_data != len(list(filter(os.path.isdir, glob.glob(fq_dir + '/*')))): + raise RuntimeError("Number of data mistmatches") + + self._num_processed_data += self._num_data + + self._filename_to_tensor = dict() + with open(Path(fp32_dir) / 'tensors.txt') as f: + tensors = set([line.rstrip() for line in f]) + for tensor in tensors: + # Check if filename is unique + # Fix name finding logic unless + assert to_filename(tensor) not in self._filename_to_tensor + self._filename_to_tensor[to_filename(tensor)] = tensor + + # Save paths to fp32 data and fq data for each tensor + # dict + # { + # : (fp32_path, fq_path), + # : (fp32_path, fq_path), + # ... + # } + data_paths = dict() + for data_idx in range(self._num_data): + fp32_results = glob.glob(fp32_dir + '/' + str(data_idx) + '/*.npy') + for fp32_data_path in fp32_results: + fp32_path = Path(fp32_data_path) + fq_data_path = fq_dir + '/' + str(data_idx) + '/' + fp32_path.with_suffix( + '.npy').name + fq_path = Path(fq_data_path) + filename = fp32_path.stem + tensor_name = self._filename_to_tensor[filename] + + # Only save the tensors which have both fp32 data and fq data + if fq_path.is_file() and fp32_path.is_file(): + if tensor_name in data_paths: + data_paths[tensor_name].append((fp32_data_path, fq_data_path)) + else: + data_paths[tensor_name] = [(fp32_data_path, fq_data_path)] + + return data_paths + + def run(self): + '''Return qerror map (dict: tensor_name(string) -> qerror(float)).''' + raise NotImplementedError # Child must implement this + + +class MPEIRComputer(QErrorComputer): + def __init__(self, fp32_dir, fq_dir): + super().__init__(fp32_dir, fq_dir) + + # Incrementally compute Qerror while traversing all data in fp32_dir and fq_dir + def advance_on(self, fp32_dir, fq_dir): + data_paths = self.collect_data_path(fp32_dir, fq_dir) + for tensor_name, data_path in data_paths.items(): + for (fp32_data_path, fq_data_path) in data_path: + fp32_data = np.load(fp32_data_path) + fq_data = np.load(fq_data_path) + + diff = np.absolute(fp32_data - fq_data).reshape(-1) + + fp32_min = np.min(fp32_data.reshape(-1)) + fp32_max = np.max(fp32_data.reshape(-1)) + + # Peak Error-to-Interval Ratio (PEIR) + # NOTE: PEIR is an analogue of PSNR (Peak Signal to Noise Ratio) + PEAK_ERROR = np.max(diff) + INTERVAL = fp32_max - fp32_min + + # If INTERVAL is 0, PEIR becomes NaN. + # To prevent this, relaxed PEIR with epsilon(10^(-6)) is used. + rPEIR = PEAK_ERROR / (INTERVAL + 0.000001) + + if tensor_name in self.qerror_map: + self.qerror_map[tensor_name] += rPEIR + else: + self.qerror_map[tensor_name] = rPEIR + + def get_final_result(self): + qerror_map = dict() + for tensor_name, acc in self.qerror_map.items(): + qerror_map[tensor_name] = acc / self._num_processed_data + + return qerror_map + + def run(self): + self.advance_on(self._fp32_dir, self._fq_dir) + return self.get_final_result() + + +class MSEComputer(QErrorComputer): + def __init__(self, fp32_dir, fq_dir): + super().__init__(fp32_dir, fq_dir) + self.qerror_min = float('inf') + self.qerror_max = -self.qerror_min + + # Incrementally compute Qerror while traversing all data in fp32_dir and fq_dir + def advance_on(self, fp32_dir, fq_dir): + data_paths = self.collect_data_path(fp32_dir, fq_dir) + for tensor_name, data_path in data_paths.items(): + for (fp32_data_path, fq_data_path) in data_path: + fp32_data = np.load(fp32_data_path) + fq_data = np.load(fq_data_path) + + MSE = np.square(fp32_data - fq_data).mean() + + if tensor_name in self.qerror_map: + self.qerror_map[tensor_name] += MSE + else: + self.qerror_map[tensor_name] = MSE + + self.qerror_min = min(MSE, self.qerror_min) + self.qerror_max = max(MSE, self.qerror_max) + + def get_final_result(self): + qerror_map = dict() + for tensor_name, acc in self.qerror_map.items(): + qerror_map[tensor_name] = acc / self._num_processed_data + + return qerror_map, self.qerror_min, self.qerror_max + + def run(self): + self.advance_on(self._fp32_dir, self._fq_dir) + return self.get_final_result() + + +class TAEComputer(QErrorComputer): #total absolute error + def __init__(self, fp32_dir, fq_dir): + super().__init__(fp32_dir, fq_dir) + self.total_error = 0 + self.qerror_min = float('inf') + self.qerror_max = -self.qerror_min + + def advance_on(self, fp32_dir, fq_dir): + data_paths = self.collect_data_path(fp32_dir, fq_dir) + for tensor_name, data_path in data_paths.items(): + for (fp32_data_path, fq_data_path) in data_path: + fp32_data = np.load(fp32_data_path) + fq_data = np.load(fq_data_path) + + total_error = np.sum(np.abs(fp32_data - fq_data)) + + if tensor_name in self.qerror_map: + self.qerror_map[tensor_name] += total_error + else: + self.qerror_map[tensor_name] = total_error + + self.qerror_min = min(total_error, self.qerror_min) + self.qerror_max = max(total_error, self.qerror_max) + + def get_final_result(self): + qerror_map = dict() + for tensor_name, acc in self.qerror_map.items(): + qerror_map[tensor_name] = acc / self._num_processed_data + return qerror_map, self.qerror_min, self.qerror_max + + def run(self): + self.advance_on(self._fp32_dir, self._fq_dir) + return self.get_final_result() diff --git a/compiler/visq/visqlib/Util.py b/compiler/visq/visqlib/Util.py new file mode 100644 index 0000000..f5b6c9a --- /dev/null +++ b/compiler/visq/visqlib/Util.py @@ -0,0 +1,37 @@ +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# Change tensor name into the one compatible with Linux file system +# '/' is replaced with '_' +# Too long name is sliced to 255 characters +def to_filename(tensor_name): + assert isinstance(tensor_name, str) + return tensor_name.replace('/', '_')[-255:] + + +# Check if attr is valid +def valid_attr(args, attr): + return hasattr(args, attr) and getattr(args, attr) + + +# Recursively visit items and round floats with ndigits +def pretty_float(item, ndigits=4): + if isinstance(item, dict): + return {k: pretty_float(v, ndigits) for k, v in item.items()} + if isinstance(item, list): + return [pretty_float(x, ndigits) for x in item] + if isinstance(item, float): + return round(item, ndigits) + return item diff --git a/compute/cker/CMakeLists.txt b/compute/cker/CMakeLists.txt index 9b3cd4f..ce328b6 100644 --- a/compute/cker/CMakeLists.txt +++ b/compute/cker/CMakeLists.txt @@ -33,4 +33,4 @@ target_link_libraries(${TEST_CKER} nnfw_coverage) target_link_libraries(${TEST_CKER} gtest gtest_main ${LIB_PTHREAD}) add_test(${TEST_CKER} ${TEST_CKER}) -install(TARGETS ${TEST_CKER} DESTINATION unittest_standalone) +install(TARGETS ${TEST_CKER} DESTINATION unittest) diff --git a/compute/cker/include/cker/operation/Dequantize.h b/compute/cker/include/cker/operation/Dequantize.h index c487581..c8c2fd9 100644 --- a/compute/cker/include/cker/operation/Dequantize.h +++ b/compute/cker/include/cker/operation/Dequantize.h @@ -112,6 +112,39 @@ inline void Dequantize(const Shape &input_shape, const int8_t *input_data, } } +inline void Dequantize(const Shape &input_shape, const int16_t *input_data, + const Shape &output_shape, float *output_data, const float scale, + const int32_t zero_point) +{ + const int flat_size = MatchingFlatSize(input_shape, output_shape); + + int i = 0; +#ifdef USE_NEON + const float32x4_t scale_dup = vdupq_n_f32(static_cast(scale)); + const float32x4_t zero_times_scale_dup = vdupq_n_f32(static_cast(-zero_point * scale)); + for (; i <= flat_size - 8; i += 8) + { + const int16x4_t input_s16_low = vld1_s16(input_data + i); + const int16x4_t input_s16_high = vld1_s16(input_data + i + 4); + const int32x4_t val_low = vmovl_s16(input_s16_low); + const int32x4_t val_high = vmovl_s16(input_s16_high); + + float32x4_t result_low, result_high; + ScaleWithNewZeroPoint(val_low, scale_dup, zero_times_scale_dup, &result_low); + ScaleWithNewZeroPoint(val_high, scale_dup, zero_times_scale_dup, &result_high); + + vst1q_f32(output_data + i, result_low); + vst1q_f32(output_data + i + 4, result_high); + } +#endif // NEON + for (; i < flat_size; ++i) + { + const int32_t val = input_data[i]; + const float result = static_cast(scale * (val - zero_point)); + output_data[i] = result; + } +} + } // namespace cker } // namespace nnfw diff --git a/compute/cker/include/cker/operation/Logistic.h b/compute/cker/include/cker/operation/Logistic.h index 3d3e59e..e990772 100644 --- a/compute/cker/include/cker/operation/Logistic.h +++ b/compute/cker/include/cker/operation/Logistic.h @@ -29,12 +29,39 @@ namespace nnfw namespace cker { +/** + * @brief Internal scalar_logistic_op operation struct + * + * @note Recent Eigen3 scalar_logistic_op return invalid value on ARM32 if + * input value is float type 88 (expected: 1, actual: 0) + * As a workaround, we use old version scalar_logistic_op internal struct + * TODO Remove this workaround + */ +template struct scalar_logistic_op +{ + EIGEN_EMPTY_STRUCT_CTOR(scalar_logistic_op) + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE T operator()(const T &x) const + { + const T one = T(1); + return one / (one + Eigen::numext::exp(-x)); + } + + template + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Packet packetOp(const Packet &x) const + { + const Packet one = Eigen::internal::pset1(T(1)); + return pdiv(one, padd(one, pexp(pnegate(x)))); + } +}; + inline void Logistic(const Shape &input_shape, const float *input_data, const Shape &output_shape, float *output_data) { auto input_map = MapAsVector(input_data, input_shape); auto output_map = MapAsVector(output_data, output_shape); - output_map.array() = input_map.array().unaryExpr(Eigen::internal::scalar_logistic_op()); + + // Use old version scalar_logistic_op + output_map.array() = input_map.array().unaryExpr(nnfw::cker::scalar_logistic_op()); } } // namespace cker diff --git a/compute/cker/include/cker/operation/Quantize.h b/compute/cker/include/cker/operation/Quantize.h index 8e5fc22..7292a19 100644 --- a/compute/cker/include/cker/operation/Quantize.h +++ b/compute/cker/include/cker/operation/Quantize.h @@ -18,6 +18,7 @@ #ifndef __NNFW_CKER_QUANTIZE_H__ #define __NNFW_CKER_QUANTIZE_H__ +#include "cker/operation/Round.h" #include "cker/Shape.h" #include "cker/Types.h" #include "cker/Utils.h" @@ -45,6 +46,164 @@ inline void Quantize(const Shape &input_shape, const InputT *input_data, const S } } +template <> +inline void Quantize(const Shape &input_shape, const float *input_data, const Shape &output_shape, + int8_t *output_data, const float scale, const int32_t zero_point) +{ + const int flat_size = MatchingFlatSize(input_shape, output_shape); + static constexpr int32_t min_val = std::numeric_limits::min(); + static constexpr int32_t max_val = std::numeric_limits::max(); + + int i = 0; +#ifdef USE_NEON + const float32x4_t reverse_scale_dup = vdupq_n_f32(1.0f / scale); + const int32x4_t zero_point_dup = vdupq_n_s32(zero_point); + const int32x4_t min_val_dup = vdupq_n_s32(min_val); + const int32x4_t max_val_dup = vdupq_n_s32(max_val); + + for (; i <= flat_size - 8; i += 8) + { + const float *src_data_ptr = input_data + i; + float32x4_t input_val_0 = vld1q_f32(src_data_ptr); + float32x4_t input_val_1 = vld1q_f32(src_data_ptr + 4); + + input_val_0 = vmulq_f32(input_val_0, reverse_scale_dup); + input_val_1 = vmulq_f32(input_val_1, reverse_scale_dup); + + int32x4_t casted_val_0 = RoundToNearest(input_val_0); + int32x4_t casted_val_1 = RoundToNearest(input_val_1); + + casted_val_0 = vaddq_s32(casted_val_0, zero_point_dup); + casted_val_1 = vaddq_s32(casted_val_1, zero_point_dup); + + // Clamp the values to fit the target type's range. + casted_val_0 = vmaxq_s32(casted_val_0, min_val_dup); + casted_val_1 = vmaxq_s32(casted_val_1, min_val_dup); + casted_val_0 = vminq_s32(casted_val_0, max_val_dup); + casted_val_1 = vminq_s32(casted_val_1, max_val_dup); + + const int16x4_t narrowed_val_0 = vmovn_s32(casted_val_0); + const int16x4_t narrowed_val_1 = vmovn_s32(casted_val_1); + const int16x8_t combined_val = vcombine_s16(narrowed_val_0, narrowed_val_1); + const int8x8_t combined_val_narrowed = vmovn_s16(combined_val); + vst1_s8(output_data + i, combined_val_narrowed); + } +#endif // NEON + + for (; i < flat_size; ++i) + { + const float val = input_data[i]; + const int32_t unclamped = static_cast(round(val / scale)) + zero_point; + const int32_t clamped = std::min(std::max(unclamped, min_val), max_val); + output_data[i] = clamped; + } +} + +template <> +inline void Quantize(const Shape &input_shape, const float *input_data, const Shape &output_shape, + uint8_t *output_data, const float scale, const int32_t zero_point) +{ + const int flat_size = MatchingFlatSize(input_shape, output_shape); + static constexpr int32_t min_val = std::numeric_limits::min(); + static constexpr int32_t max_val = std::numeric_limits::max(); + + int i = 0; +#ifdef USE_NEON + const float32x4_t reverse_scale_dup = vdupq_n_f32(1.0f / scale); + const int32x4_t zero_point_dup = vdupq_n_s32(zero_point); + const int32x4_t min_val_dup = vdupq_n_s32(min_val); + const int32x4_t max_val_dup = vdupq_n_s32(max_val); + + for (; i <= flat_size - 8; i += 8) + { + const float *src_data_ptr = input_data + i; + float32x4_t input_val_0 = vld1q_f32(src_data_ptr); + float32x4_t input_val_1 = vld1q_f32(src_data_ptr + 4); + + input_val_0 = vmulq_f32(input_val_0, reverse_scale_dup); + input_val_1 = vmulq_f32(input_val_1, reverse_scale_dup); + + int32x4_t casted_val_0 = RoundToNearest(input_val_0); + int32x4_t casted_val_1 = RoundToNearest(input_val_1); + + casted_val_0 = vaddq_s32(casted_val_0, zero_point_dup); + casted_val_1 = vaddq_s32(casted_val_1, zero_point_dup); + + // Clamp the values to fit the target type's range. + casted_val_0 = vmaxq_s32(casted_val_0, min_val_dup); + casted_val_1 = vmaxq_s32(casted_val_1, min_val_dup); + casted_val_0 = vminq_s32(casted_val_0, max_val_dup); + casted_val_1 = vminq_s32(casted_val_1, max_val_dup); + + const uint16x4_t narrowed_val_0 = vqmovun_s32(casted_val_0); + const uint16x4_t narrowed_val_1 = vqmovun_s32(casted_val_1); + const uint16x8_t combined_val = vcombine_u16(narrowed_val_0, narrowed_val_1); + const uint8x8_t combined_val_narrowed = vmovn_u16(combined_val); + vst1_u8(output_data + i, combined_val_narrowed); + } +#endif // NEON + + for (; i < flat_size; ++i) + { + const float val = input_data[i]; + const int32_t unclamped = static_cast(round(val / scale)) + zero_point; + const int32_t clamped = std::min(std::max(unclamped, min_val), max_val); + output_data[i] = clamped; + } +} + +template <> +inline void Quantize(const Shape &input_shape, const float *input_data, const Shape &output_shape, + int16_t *output_data, const float scale, const int32_t zero_point) +{ + const int flat_size = MatchingFlatSize(input_shape, output_shape); + static constexpr int32_t min_val = std::numeric_limits::min(); + static constexpr int32_t max_val = std::numeric_limits::max(); + + int i = 0; +#ifdef USE_NEON + const float32x4_t reverse_scale_dup = vdupq_n_f32(1.0f / scale); + const int32x4_t zero_point_dup = vdupq_n_s32(zero_point); + const int32x4_t min_val_dup = vdupq_n_s32(min_val); + const int32x4_t max_val_dup = vdupq_n_s32(max_val); + + for (; i <= flat_size - 8; i += 8) + { + const float *src_data_ptr = input_data + i; + float32x4_t input_val_0 = vld1q_f32(src_data_ptr); + float32x4_t input_val_1 = vld1q_f32(src_data_ptr + 4); + + input_val_0 = vmulq_f32(input_val_0, reverse_scale_dup); + input_val_1 = vmulq_f32(input_val_1, reverse_scale_dup); + + int32x4_t casted_val_0 = RoundToNearest(input_val_0); + int32x4_t casted_val_1 = RoundToNearest(input_val_1); + + casted_val_0 = vaddq_s32(casted_val_0, zero_point_dup); + casted_val_1 = vaddq_s32(casted_val_1, zero_point_dup); + + // Clamp the values to fit the target type's range. + casted_val_0 = vmaxq_s32(casted_val_0, min_val_dup); + casted_val_1 = vmaxq_s32(casted_val_1, min_val_dup); + casted_val_0 = vminq_s32(casted_val_0, max_val_dup); + casted_val_1 = vminq_s32(casted_val_1, max_val_dup); + + const int16x4_t narrowed_val_0 = vmovn_s32(casted_val_0); + const int16x4_t narrowed_val_1 = vmovn_s32(casted_val_1); + vst1_s16(output_data + i, narrowed_val_0); + vst1_s16(output_data + i + 4, narrowed_val_1); + } +#endif // NEON + + for (; i < flat_size; ++i) + { + const float val = input_data[i]; + const int32_t unclamped = static_cast(round(val / scale)) + zero_point; + const int32_t clamped = std::min(std::max(unclamped, min_val), max_val); + output_data[i] = clamped; + } +} + inline void Quantize(const int32_t *multiplier, const int32_t *shift, int32_t channel_size, int32_t total_size, int32_t output_zp, int32_t output_min, int32_t output_max, int32_t *scratch, int8_t *output) diff --git a/compute/cker/include/cker/operation/Reduce.h b/compute/cker/include/cker/operation/Reduce.h index f54f2e6..02a9eac 100644 --- a/compute/cker/include/cker/operation/Reduce.h +++ b/compute/cker/include/cker/operation/Reduce.h @@ -312,12 +312,12 @@ public: } // Calculate mean by dividing output_data by num of aggregated element. - U num_elements_in_axis = 1; + size_t num_elements_in_axis = 1; for (int idx = 0; idx < num_resolved_axis; ++idx) { size_t current = static_cast(input_shape.Dims(resolved_axis_data()[idx])); // Overflow prevention. - if (current > static_cast(std::numeric_limits::max() / num_elements_in_axis)) + if (current > static_cast(std::numeric_limits::max() / num_elements_in_axis)) { return false; } @@ -330,7 +330,7 @@ public: if (compute_sum) { // TODO(b/116341117): Eliminate float and do this completely in 8bit. - const float bias = -input_zero_point * scale * num_elements_in_axis + 0.5f; + const float bias = -input_zero_point * scale * num_elements_in_axis; for (size_t idx = 0; idx < num_outputs; ++idx) { const U value = @@ -340,7 +340,7 @@ public: } else { - const float bias = -input_zero_point * scale + 0.5f; + const float bias = -input_zero_point * scale; for (size_t idx = 0; idx < num_outputs; ++idx) { float float_mean = diff --git a/compute/cker/include/cker/operation/Round.h b/compute/cker/include/cker/operation/Round.h index a04a741..d677145 100644 --- a/compute/cker/include/cker/operation/Round.h +++ b/compute/cker/include/cker/operation/Round.h @@ -19,6 +19,7 @@ #define __NNFW_CKER_ROUND_H__ #include "cker/Shape.h" +#include "cker/Utils.h" #include @@ -41,6 +42,26 @@ inline float RoundToNearest(float value) } } +#ifdef USE_NEON + +inline int32x4_t RoundToNearest(const float32x4_t input) +{ +#if defined(__aarch64__) || defined(__SSSE3__) + // Note: vcvtnq_s32_f32 is not available in ARMv7 + return vcvtnq_s32_f32(input); +#else + static const float32x4_t zero_val_dup = vdupq_n_f32(0.0f); + static const float32x4_t point5_val_dup = vdupq_n_f32(0.5f); + static const float32x4_t minus_point5_val_dup = vdupq_n_f32(-0.5f); + + const uint32x4_t mask = vcltq_f32(input, zero_val_dup); + const float32x4_t round = vbslq_f32(mask, minus_point5_val_dup, point5_val_dup); + return vcvtq_s32_f32(vaddq_f32(input, round)); +#endif // defined(__aarch64__) || defined(__SSSE3__) +} + +#endif // NEON + inline void Round(const Shape &input_shape, const float *input_data, const Shape &output_shape, float *output_data) { diff --git a/docs/conf.py b/docs/conf.py index 409e5f7..c49ea1f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ copyright = '2020, Samsung Research & contributors' author = 'Samsung Research & contributors' # The full version, including alpha/beta/rc tags -release = '1.21.0' +release = '1.22.0' # -- General configuration --------------------------------------------------- diff --git a/docs/howto/how-to-build-compiler.md b/docs/howto/how-to-build-compiler.md index 29e300b..7a1fa1c 100644 --- a/docs/howto/how-to-build-compiler.md +++ b/docs/howto/how-to-build-compiler.md @@ -45,6 +45,9 @@ pylint \ python3 \ python3-pip \ python3-venv \ +python3.8 \ +python3.8-dev \ +python3.8-venv \ scons \ software-properties-common \ unzip \ @@ -177,6 +180,13 @@ First `make` will run above steps (1), (2) and (3). Second `make` will run (4). ### Test +Preprequisite for testing in ARM32 device. +``` +# numpy is required for value match in ARM32 target device +sudo apt-get install python3-pip +python3 -m pip install numpy +``` + You can also run unit tests in ARM32 Ubuntu device with cross build results. First you need to run the test in host to prepare files that are currently complicated in target device. diff --git a/docs/howto/how-to-build-runtime-tizen-gbs-rpi4.md b/docs/howto/how-to-build-runtime-tizen-gbs-rpi4.md index 57b2b78..5479a34 100644 --- a/docs/howto/how-to-build-runtime-tizen-gbs-rpi4.md +++ b/docs/howto/how-to-build-runtime-tizen-gbs-rpi4.md @@ -2,7 +2,7 @@ This document describes how to build runtime with GBS for Tizen AARCH64. As a real example, we'll also describe how to prepare Tizen on Raspberry Pi 4 -and show you how to run our test package runner `nnpackage_run`. +and show you how to run our test package runner `onert_run`. For ARM32, there would be not much difference with some changes. @@ -70,7 +70,7 @@ $ cd ONE $ gbs -c infra/nnfw/config/gbs.conf build --include-all -A aarch64 --define 'test_build 1' ``` - `-A aarch64` is to set architecture to AARCH64. Use `arm32` for ARM32 target. -- `--define 'test_build 1'` is to enable test build so that we can use `nnpackage_run` +- `--define 'test_build 1'` is to enable test build so that we can use `onert_run` Now take a cup of coffee. @@ -300,7 +300,7 @@ Assume `mobilenet_v2_1.4_224` nnpackage is already copied to `/opt/usr/home/owner/media/models` folder with `sdb` command. ``` -sh-3.2# BACKENDS="cpu" Product/out/bin/nnpackage_run \ +sh-3.2# BACKENDS="cpu" Product/out/bin/onert_run \ --nnpackage /opt/usr/home/owner/media/models/mobilenet_v2_1.4_224 Package Filename /opt/usr/home/owner/media/models/mobilenet_v2_1.4_224 diff --git a/docs/howto/how-to-build-runtime.md b/docs/howto/how-to-build-runtime.md index bf524d7..f83ec5b 100644 --- a/docs/howto/how-to-build-runtime.md +++ b/docs/howto/how-to-build-runtime.md @@ -70,13 +70,13 @@ Unfortunately, the debug build on the x86_64 architecture currently has an error ``` $ export BUILD_TYPE=release -$ make -f Makefile.template install +$ make -f Makefile.template ``` Or you can simply do something like this: ``` -$ BUILD_TYPE=release make -f Makefile.template install +$ BUILD_TYPE=release make -f Makefile.template ``` The build method described here is a `native build` in which the build environment and execution environment are same. So, this command creates a runtime binary targeting the current build architecture, probably x86_64, as the execution environment. You can find the build output in the ./Product folder as follows: @@ -84,12 +84,8 @@ The build method described here is a `native build` in which the build environme ``` $ tree -L 2 ./Product ./Product -├── obj -> /home/sjlee/star/one/Product/x86_64-linux.release/obj ├── out -> /home/sjlee/star/one/Product/x86_64-linux.release/out └── x86_64-linux.release - ├── BUILD - ├── CONFIGURE - ├── INSTALL ├── obj └── out @@ -98,17 +94,16 @@ $ tree -L 2 ./Product $ tree -L 3 ./Product/out ./Product/out ├── bin -│   ├── nnapi_test -│   ├── nnpackage_run +│   ├── onert_run │   ├── tflite_comparator │   └── tflite_run ├── include │   ├── nnfw -│   │   ├── NeuralNetworks.h │   │   ├── NeuralNetworksEx.h │   │   ├── NeuralNetworksExtensions.h -│   │   ├── nnfw.h -│   │   └── nnfw_experimental.h +│   │   ├── NeuralNetworks.h +│   │   ├── nnfw_experimental.h +│   │   └── nnfw.h │   └── onert │   ├── backend │   ├── compiler @@ -117,55 +112,59 @@ $ tree -L 3 ./Product/out │   └── util ├── lib │   ├── libbackend_cpu.so -│   ├── libcircle_loader.so +│   ├── libbackend_ruy.so │   ├── libneuralnetworks.so │   ├── libnnfw-dev.so -│   ├── libonert_core.so -│   └── libtflite_loader.so +│   └── libonert_core.so +├── nnapi-gtest +│   ├── nnapi_gtest +│   ├── nnapi_gtest.skip +│   ├── nnapi_gtest.skip.noarch.interp +│   └── nnapi_gtest.skip.x86_64-linux.cpu ├── test -│   ├── FillFrom_runner │   ├── command │   │   ├── nnpkg-test │   │   ├── prepare-model │   │   ├── unittest │   │   └── verify-tflite +│   ├── FillFrom_runner │   ├── list │   │   ├── benchmark_nnpkg_model_list.txt -│   │   ├── frameworktest_list.aarch64.acl_cl.txt -│   │   ├── frameworktest_list.aarch64.acl_neon.txt -│   │   ├── frameworktest_list.aarch64.cpu.txt -│   │   ├── frameworktest_list.armv7l.acl_cl.txt -│   │   ├── frameworktest_list.armv7l.acl_neon.txt -│   │   ├── frameworktest_list.armv7l.cpu.txt -│   │   ├── frameworktest_list.noarch.interp.txt -│   │   ├── frameworktest_list.x86_64.cpu.txt │   │   ├── nnpkg_test_list.armv7l-linux.acl_cl │   │   ├── nnpkg_test_list.armv7l-linux.acl_neon │   │   ├── nnpkg_test_list.armv7l-linux.cpu │   │   ├── nnpkg_test_list.noarch.interp -│   │   ├── tflite_loader_list.aarch64.txt -│   │   └── tflite_loader_list.armv7l.txt +│   │   ├── tflite_comparator.aarch64.acl_cl.list +│   │   ├── tflite_comparator.aarch64.acl_neon.list +│   │   ├── tflite_comparator.aarch64.cpu.list +│   │   ├── tflite_comparator.armv7l.acl_cl.list +│   │   ├── tflite_comparator.armv7l.acl_neon.list +│   │   ├── tflite_comparator.armv7l.cpu.list +│   │   ├── tflite_comparator.noarch.interp.list +│   │   └── tflite_comparator.x86_64.cpu.list │   ├── models -│   │   ├── nnfw_api_gtest │   │   ├── run_test.sh │   │   └── tflite │   ├── nnpkgs │   │   └── FillFrom │   └── onert-test -├── unittest -│   ├── nnapi_gtest -│   ├── nnapi_gtest.skip -│   ├── nnapi_gtest.skip.noarch.interp -│   └── nnapi_gtest.skip.x86_64-linux.cpu -└── unittest_standalone +└── unittest + ├── ndarray_test ├── nnfw_api_gtest - ├── test_compute - ├── test_onert - ├── test_onert_backend_cpu_common + ├── nnfw_api_gtest_models + │   ├── add + │   ├── add_invalid_manifest + │   ├── add_no_manifest + │   ├── if_dynamic + │   ├── mobilenet_v1_1.0_224 + │   └── while_dynamic + ├── nnfw_lib_misc_test + ├── test_cker + ├── test_onert_core ├── test_onert_frontend_nnapi └── tflite_test -20 directories, 47 files +26 directories, 46 files ``` @@ -185,24 +184,8 @@ inception_v3.tflite The result of running the inception_v3 model using runtime is as follows. Please consider that this is a test that simply checks execution latency without considering the accuracy of the model. ``` -$ USE_NNAPI=1 ./Product/out/bin/tflite_run ./inception_v3.tflite -nnapi function 'ANeuralNetworksModel_create' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksModel_addOperand' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksModel_setOperandValue' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksModel_addOperation' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksModel_identifyInputsAndOutputs' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksModel_finish' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksCompilation_create' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksCompilation_finish' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -input tensor indices = [317,] -nnapi function 'ANeuralNetworksExecution_create' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksExecution_setInput' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksExecution_setOutput' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksExecution_startCompute' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksEvent_wait' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksEvent_free' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksExecution_free' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -output tensor indices = [316(max:905),] +$ ./Product/out/bin/onert_run --modelfile ./inception_v3.tflite +Model Filename ./inception_v3.tflite =================================== MODEL_LOAD takes 1.108 ms PREPARE takes 0.190 ms @@ -212,10 +195,8 @@ EXECUTE takes 183.895 ms - MIN : 183.895 ms - GEOMEAN : 183.895 ms =================================== -nnapi function 'ANeuralNetworksCompilation_free' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' -nnapi function 'ANeuralNetworksModel_free' is loaded from '/home/sjlee/star/one/Product/x86_64-linux.release/out/bin/../lib/libneuralnetworks.so' ``` -Here, `USE_NNAPI=1` means that **ONE** runtime is used for model inference. If omitted, the model will be executed using Tensorflow lite, the basic framework for verification. From the previous build result, you can see that it is the path to the directory where `libneuralnetworks.so` and `libonert_core.so` are located. +If you use `tflite_run` instead of `onert_run`, the model will be executed using Tensorflow lite, the basic framework for verification. From the previous build result, you can see that it is the path to the directory where `tflite_run` and `onert_run` are located. If you come here without any problems, you have all of the basic environments for runtime development. diff --git a/docs/howto/how-to-contribute.md b/docs/howto/how-to-contribute.md index fe7b140..7c68554 100644 --- a/docs/howto/how-to-contribute.md +++ b/docs/howto/how-to-contribute.md @@ -34,8 +34,9 @@ This section explains the steps to create a pull request (PR). 1. Create a commit It is time to create a commit for submission once you are convinced that your contribution is - ready to go. Please include signed-off message at the end of commit message. If not, your pull - request will be **rejected** by CI. + ready to go. Please include + [signed-off message](https://github.com/Samsung/ONE/wiki/ONE-Developer's-Certificate-of-Origin) + at the end of commit message. If not, your pull request will be **rejected** by CI. 1. Check code format locally diff --git a/docs/howto/how-to-cross-build-runtime-for-aarch64.md b/docs/howto/how-to-cross-build-runtime-for-aarch64.md index 4c8fe1d..3e27057 100644 --- a/docs/howto/how-to-cross-build-runtime-for-aarch64.md +++ b/docs/howto/how-to-cross-build-runtime-for-aarch64.md @@ -89,8 +89,8 @@ normal build and cross build as follows. ``` $ export ROOTFS_DIR=xxx ... -$ make all install # do normal build -$ TARGET_ARCH=aarch64 make all install # do cross build +$ make # do normal build +$ TARGET_ARCH=aarch64 make # do cross build ``` ### Run test diff --git a/docs/howto/how-to-cross-build-runtime-for-arm.md b/docs/howto/how-to-cross-build-runtime-for-arm.md index 32c64f8..91481a9 100644 --- a/docs/howto/how-to-cross-build-runtime-for-arm.md +++ b/docs/howto/how-to-cross-build-runtime-for-arm.md @@ -106,11 +106,11 @@ Give `TARGET_ARCH` variable to set the target architecture. If you used `ROOTFS_DIR` to prepare in alternative folder, you should also give this to makefile. ``` -$ CROSS_BUILD=1 TARGET_ARCH=armv7l make all install +$ CROSS_BUILD=1 TARGET_ARCH=armv7l make # If ROOTFS_DIR is in alternative folder $ ROOTFS_DIR=/path/to/your/rootfs/arm \ -CROSS_BUILD=1 TARGET_ARCH=armv7l make all install +CROSS_BUILD=1 TARGET_ARCH=armv7l make ``` You can also omit the `CROSS_BUILD=1` option if you explicitly pass `ROOTFS_DIR`. In that case, if @@ -121,8 +121,8 @@ normal build and cross build as follows. ``` $ export ROOTFS_DIR=xxx ... -$ make all install # do normal build -$ TARGET_ARCH=armv7l make all install # do cross build +$ make # do normal build +$ TARGET_ARCH=armv7l make # do cross build ``` ### Run test @@ -151,12 +151,10 @@ $ ssh odroid sjlee@odroid's password: ... $ cd ~/one/Product -$ ln ${PWD}/armv7l-linux.debug/obj obj $ ln ${PWD}/armv7l-linux.debug/out out $ cd .. $ ls -la Product drwxrwxr-x 5 sjlee sjlee 4096 Jun 4 20:55 armv7l-linux.debug -lrwxrwxrwx 1 sjlee sjlee 51 Jun 4 20:54 obj -> /home/sjlee/one/Product/armv7l-linux.debug/obj lrwxrwxrwx 1 sjlee sjlee 51 Jun 4 20:55 out -> /home/sjlee/one/Product/armv7l-linux.debug/out ``` diff --git a/docs/howto/how-to-use-specific-backend.md b/docs/howto/how-to-use-specific-backend.md index 32e1b83..a839777 100644 --- a/docs/howto/how-to-use-specific-backend.md +++ b/docs/howto/how-to-use-specific-backend.md @@ -18,7 +18,7 @@ ONE runtime has many ways to use specific backend during inference - Same as `nnfw_set_available_backends` - Example ```bash -BACKENDS=cpu ./Product/out/bin/nnpackage_run ... +BACKENDS=cpu ./Product/out/bin/onert_run ... ``` ### 2. OP_BACKEND_[OP_TYPE] @@ -27,7 +27,7 @@ BACKENDS=cpu ./Product/out/bin/nnpackage_run ... - Example - Execute `Conv2D` operator on ruy backend and others on cpu backend ```bash -OP_BACKEND_Conv2D=ruy BACKENDS="cpu;ruy" ./Product/out/bin/nnpackage_run ... +OP_BACKEND_Conv2D=ruy BACKENDS="cpu;ruy" ./Product/out/bin/onert_run ... ``` ### 3. OP_BACKEND_MAP @@ -36,5 +36,5 @@ OP_BACKEND_Conv2D=ruy BACKENDS="cpu;ruy" ./Product/out/bin/nnpackage_run ... - Example - Execute `operator 10` on `acl_cl` backend and others on `acl_neon` backend ```bash -OP_BACKEND_MAP="10=acl_cl" BACKENDS="acl_neon;acl_cl" ./Product/out/bin/nnpackage_run ... +OP_BACKEND_MAP="10=acl_cl" BACKENDS="acl_neon;acl_cl" ./Product/out/bin/onert_run ... ``` diff --git a/docs/release/1.22/index.rst b/docs/release/1.22/index.rst new file mode 100644 index 0000000..e3c330d --- /dev/null +++ b/docs/release/1.22/index.rst @@ -0,0 +1,13 @@ +.. ONE documentation master file, created by + sphinx-quickstart on Fri Mar 24 14:03:12 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +1.22 +==== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + ./release-note-1.22.0.md diff --git a/docs/release/1.22/release-note-1.22.0.md b/docs/release/1.22/release-note-1.22.0.md new file mode 100644 index 0000000..2e6dee7 --- /dev/null +++ b/docs/release/1.22/release-note-1.22.0.md @@ -0,0 +1,12 @@ +# Release Note 1.22.0 + +## ONE Compiler + +- Introduce new optimization options: `unroll_unidirseqlstm`, `forward_transpose_op`, `fold_fully_connected`, `fuse_prelu` +- Support more Ops for fake quantization: `Depth2Space`, `Space2Depth`, `Pack`, `Unpack`, `Abs` +- Support more Ops for quantization: `Abs`, `ReduceProd` +- Introduce _visq_ tool for quantization error visualization +- Introduce _Environment_ section into configuration file +- Improve speed of `convert_nchw_to_nhwc` option +- Support `Add`, `Mul` of index-type (int32, int64) tensors in _one-quantize_ +- Support ubuntu 20.04 diff --git a/infra/cmake/modules/ExternalSourceTools.cmake b/infra/cmake/modules/ExternalSourceTools.cmake index f71eb5d..5671ae0 100644 --- a/infra/cmake/modules/ExternalSourceTools.cmake +++ b/infra/cmake/modules/ExternalSourceTools.cmake @@ -51,8 +51,11 @@ function(ExternalSource_Download PREFIX) foreach(retry_count RANGE 5) message(STATUS "(Trial Count : ${retry_count})") + # For external mirror server + envoption(EXTERNAL_SERVER_USERPWD "") file(DOWNLOAD ${URL} "${DOWNLOAD_PATH}" STATUS status + USERPWD "${EXTERNAL_SERVER_USERPWD}" LOG log) list(GET status 0 status_code) @@ -72,7 +75,7 @@ function(ExternalSource_Download PREFIX) if(retry_count EQUAL 5) message(FATAL_ERROR "Download ${PREFIX} from ${URL} - failed") endif() - + # Retry after 10 seconds when download fails execute_process(COMMAND sleep 10) endforeach() diff --git a/infra/cmake/modules/IdentifyPlatform.cmake b/infra/cmake/modules/IdentifyPlatform.cmake index 890055f..0240611 100644 --- a/infra/cmake/modules/IdentifyPlatform.cmake +++ b/infra/cmake/modules/IdentifyPlatform.cmake @@ -51,6 +51,10 @@ endif() if("${TARGET_ARCH}" STREQUAL "x86_64") set(TARGET_ARCH_BASE ${TARGET_ARCH}) +elseif("${TARGET_ARCH}" STREQUAL "armv8-m") + set(TARGET_ARCH_BASE "arm") +elseif("${TARGET_ARCH}" STREQUAL "armv7-r") + set(TARGET_ARCH_BASE "arm") elseif("${TARGET_ARCH}" STREQUAL "armv7em") set(TARGET_ARCH_BASE "arm") elseif("${TARGET_ARCH}" STREQUAL "armv7l") diff --git a/infra/cmake/packages/CMSIS-NN-4.0.0/CMSIS-NNConfig.cmake b/infra/cmake/packages/CMSIS-NN-4.0.0/CMSIS-NNConfig.cmake new file mode 100644 index 0000000..4c82af2 --- /dev/null +++ b/infra/cmake/packages/CMSIS-NN-4.0.0/CMSIS-NNConfig.cmake @@ -0,0 +1,14 @@ +function(_CMSIS_NN_import) + nnas_include(ExternalSourceTools) + nnas_include(OptionTools) + + envoption(EXTERNAL_DOWNLOAD_SERVER "https://github.com") + envoption(CMSIS_NN_4_0_0_URL ${EXTERNAL_DOWNLOAD_SERVER}/ARM-software/CMSIS-NN/archive/refs/tags/v4.0.0.tar.gz) + + ExternalSource_Download(CMSIS_NN DIRNAME CMSIS-NN-4.0.0 ${CMSIS_NN_4_0_0_URL}) + + set(CMSIS_NNSource_DIR ${CMSIS_NN_SOURCE_DIR} PARENT_SCOPE) + set(CMSIS_NNSource_FOUND TRUE PARENT_SCOPE) +endfunction(_CMSIS_NN_import) + +_CMSIS_NN_import() diff --git a/infra/cmake/packages/CMSIS-NN-4.0.0/CMSIS-NNConfigVersion.cmake b/infra/cmake/packages/CMSIS-NN-4.0.0/CMSIS-NNConfigVersion.cmake new file mode 100644 index 0000000..5fa88e6 --- /dev/null +++ b/infra/cmake/packages/CMSIS-NN-4.0.0/CMSIS-NNConfigVersion.cmake @@ -0,0 +1,10 @@ +set(PACKAGE_VERSION "4.0.0") +set(PACKAGE_VERSION_EXACT FALSE) +set(PACKAGE_VERSION_COMPATIBLE FALSE) +set(PACKAGE_VERSION_UNSUITABLE TRUE) + +if(PACKAGE_FIND_VERSION VERSION_EQUAL PACKAGE_VERSION) + set(PACKAGE_VERSION_EXACT TRUE) + set(PACKAGE_VERSION_COMPATIBLE TRUE) + set(PACKAGE_VERSION_UNSUITABLE FALSE) +endif(PACKAGE_FIND_VERSION VERSION_EQUAL PACKAGE_VERSION) diff --git a/infra/cmake/packages/GTestConfig.cmake b/infra/cmake/packages/GTestConfig.cmake index 872ff72..c844f4c 100644 --- a/infra/cmake/packages/GTestConfig.cmake +++ b/infra/cmake/packages/GTestConfig.cmake @@ -30,14 +30,12 @@ _GTest_build() # Note: cmake supports GTest and does not find GTestConfig.cmake or GTest-config.cmake. # Refer to "https://cmake.org/cmake/help/v3.5/module/FindGTest.html" # find_package(GTest) creates options like GTEST_FOUND, not GTest_FOUND. -if(GTEST_FOUND) - message(STATUS "Found GTest: true") -else(GTEST_FOUND) +if(NOT GTEST_FOUND) message(STATUS "GTEST_FOUND false: call find_package(GTest)") # Reset package config directory cache to prevent recursive find unset(GTest_DIR CACHE) find_package(GTest) -endif(GTEST_FOUND) +endif(NOT GTEST_FOUND) find_package(Threads) if(${GTEST_FOUND} AND TARGET Threads::Threads) diff --git a/infra/cmake/packages/TensorFlowGpuConfig.cmake b/infra/cmake/packages/TensorFlowGpuConfig.cmake deleted file mode 100644 index 7a7f786..0000000 --- a/infra/cmake/packages/TensorFlowGpuConfig.cmake +++ /dev/null @@ -1,22 +0,0 @@ -# TensorFlowGpuConfig.cmake - -function(_Build_TfliteGpuDelagate_) - nnas_find_package(TensorFlowGpuSource REQUIRED) - nnas_find_package(AbseilSource REQUIRED) - nnas_find_package(Farmhash REQUIRED) - nnas_find_package(Fp16Source REQUIRED) - - if(NOT TARGET TensorFlowGpu) - nnas_include(ExternalProjectTools) - add_extdirectory("${CMAKE_CURRENT_LIST_DIR}/TensorFlowLiteGpu" TensorFlowLiteGpu) - endif() - set(TENSORFLOWGPU_SOURCE_DIR ${TENSORFLOWGPU_SOURCE_DIR} PARENT_SCOPE) - set(TensorFlowGpu_DIR ${TensorFlowGpu_DIR} PARENT_SCOPE) -endfunction(_Build_TfliteGpuDelagate_) - -if(BUILD_TENSORFLOW_LITE_GPU) - _Build_TfliteGpuDelagate_() - set(TensorFlowGpu_FOUND TRUE PARENT_SCOPE) -else(BUILD_TENSORFLOW_LITE_GPU) - set(TensorFlowGpu_FOUND FALSE PARENT_SCOPE) -endif(BUILD_TENSORFLOW_LITE_GPU) diff --git a/infra/cmake/packages/TensorFlowGpuSource/patch_for_gpu_cl_build.patch b/infra/cmake/packages/TensorFlowGpuSource/patch_for_gpu_cl_build.patch deleted file mode 100644 index bf423dc..0000000 --- a/infra/cmake/packages/TensorFlowGpuSource/patch_for_gpu_cl_build.patch +++ /dev/null @@ -1,292 +0,0 @@ -diff --git a/tensorflow/lite/delegates/gpu/api.h b/tensorflow/lite/delegates/gpu/api.h -index 7892d0ce..fae4fb69 100644 ---- a/tensorflow/lite/delegates/gpu/api.h -+++ b/tensorflow/lite/delegates/gpu/api.h -@@ -43,11 +43,18 @@ limitations under the License. - #include "tensorflow/lite/delegates/gpu/common/data_type.h" - #include "tensorflow/lite/delegates/gpu/common/status.h" - #include "tensorflow/lite/delegates/gpu/common/util.h" -+ -+#ifdef TFLITE_GPU_LIB_FIX - #include -+#endif - - #define GL_NO_PROTOTYPES - #define EGL_NO_PROTOTYPES -+ -+#ifdef TFLITE_GPU_LIB_FIX - #include "tensorflow/lite/delegates/gpu/gl/portable_gl31.h" -+#endif -+ - #undef GL_NO_PROTOTYPES - #undef EGL_NO_PROTOTYPES - -@@ -80,6 +87,7 @@ enum class ObjectType { - VULKAN_TEXTURE - }; - -+#ifdef TFLITE_GPU_LIB_FIX - struct OpenGlBuffer { - OpenGlBuffer() = default; - explicit OpenGlBuffer(GLuint new_id) : id(new_id) {} -@@ -95,6 +103,7 @@ struct OpenGlTexture { - GLuint id = GL_INVALID_INDEX; - GLenum format = GL_INVALID_ENUM; - }; -+#endif - - struct OpenClBuffer { - OpenClBuffer() = default; -@@ -111,6 +120,7 @@ struct OpenClTexture { - // TODO(akulik): should it specify texture format? - }; - -+#ifdef TFLITE_GPU_LIB_FIX - struct VulkanBuffer { - VulkanBuffer() = default; - explicit VulkanBuffer(VkBuffer buffer_, VkDeviceSize size_, -@@ -143,6 +153,7 @@ struct VulkanMemory { - VkDeviceSize size; - VkDeviceSize offset; - }; -+#endif - - struct CpuMemory { - CpuMemory() = default; -@@ -228,10 +239,15 @@ bool IsValid(const TensorObjectDef& def); - // @return the number of elements in a tensor object. - uint32_t NumElements(const TensorObjectDef& def); - -+#ifdef TFLITE_GPU_LIB_FIX - using TensorObject = - absl::variant; -- -+#else -+using TensorObject = -+ absl::variant; -+#endif - // @return true if object is set and corresponding values are defined. - bool IsValid(const TensorObjectDef& def, const TensorObject& object); - -diff --git a/tensorflow/lite/delegates/gpu/cl/api.h b/tensorflow/lite/delegates/gpu/cl/api.h -index 65671117..c339f3f0 100644 ---- a/tensorflow/lite/delegates/gpu/cl/api.h -+++ b/tensorflow/lite/delegates/gpu/cl/api.h -@@ -20,7 +20,9 @@ limitations under the License. - #define EGL_NO_PROTOTYPES - #endif - -+#ifdef TFLITE_GPU_LIB_FIX - #include -+#endif - - #include - #include -@@ -115,9 +117,10 @@ struct InferenceEnvironmentOptions { - // It is the error to set egl_display, egl_context AND context at the same - // time. If egl_display and egl_context are set, they will be used to create - // GL-aware CL context. -+#ifdef TFLITE_GPU_LIB_FIX - EGLDisplay egl_display = EGL_NO_DISPLAY; - EGLContext egl_context = EGL_NO_CONTEXT; -- -+#endif //TFLITE_GPU_LIB_FIX - // Should contain data returned from - // InferenceEnvironment::GetSerializedBinaryCache method. - // Invalid or incompatible data will be discarded. Compiled binary may become -@@ -125,7 +128,11 @@ struct InferenceEnvironmentOptions { - absl::Span serialized_binary_cache; - - bool IsGlAware() const { -+#ifdef TFLITE_GPU_LIB_FIX - return egl_context != EGL_NO_CONTEXT && egl_display != EGL_NO_DISPLAY; -+#else //TFLITE_GPU_LIB_FIX -+ return false; -+#endif //TFLITE_GPU_LIB_FIX - } - }; - -diff --git a/tensorflow/lite/delegates/gpu/cl/arguments.h b/tensorflow/lite/delegates/gpu/cl/arguments.h -index a5435c4f..e088355b 100644 ---- a/tensorflow/lite/delegates/gpu/cl/arguments.h -+++ b/tensorflow/lite/delegates/gpu/cl/arguments.h -@@ -23,7 +23,9 @@ limitations under the License. - #include "tensorflow/lite/delegates/gpu/cl/cl_device.h" - #include "tensorflow/lite/delegates/gpu/cl/gpu_object.h" - #include "tensorflow/lite/delegates/gpu/cl/opencl_wrapper.h" -+#ifdef TFLITE_GPU_LIB_FIX - #include "tensorflow/lite/delegates/gpu/cl/serialization_generated.h" -+#endif - #include "tensorflow/lite/delegates/gpu/cl/util.h" - #include "tensorflow/lite/delegates/gpu/common/access_type.h" - #include "tensorflow/lite/delegates/gpu/common/status.h" -@@ -78,11 +80,12 @@ class Arguments : public ArgumentsBinder { - ~Arguments() override = default; - - private: -+#ifdef TFLITE_GPU_LIB_FIX - friend flatbuffers::Offset Encode( - const Arguments& args, flatbuffers::FlatBufferBuilder* builder); - friend absl::Status Decode(CLContext* context, const data::Arguments* fb_args, - Arguments* args); -- -+#endif - void AddBuffer(const std::string& name, const GPUBufferDescriptor& desc); - void AddImage2D(const std::string& name, const GPUImage2DDescriptor& desc); - void AddImage2DArray(const std::string& name, -diff --git a/tensorflow/lite/delegates/gpu/cl/gpu_object.h b/tensorflow/lite/delegates/gpu/cl/gpu_object.h -index abd77a44..ac1b7f00 100644 ---- a/tensorflow/lite/delegates/gpu/cl/gpu_object.h -+++ b/tensorflow/lite/delegates/gpu/cl/gpu_object.h -@@ -23,7 +23,9 @@ limitations under the License. - - #include "tensorflow/lite/delegates/gpu/cl/cl_context.h" - #include "tensorflow/lite/delegates/gpu/cl/opencl_wrapper.h" -+#ifdef TFLITE_GPU_LIB_FIX - #include "tensorflow/lite/delegates/gpu/cl/serialization_generated.h" -+#endif - #include "tensorflow/lite/delegates/gpu/common/access_type.h" - #include "tensorflow/lite/delegates/gpu/common/data_type.h" - #include "tensorflow/lite/delegates/gpu/common/status.h" -@@ -165,10 +167,12 @@ class GPUObjectDescriptor { - AccessType GetAccess() const { return access_type_; } - - protected: -+#ifdef TFLITE_GPU_LIB_FIX - friend flatbuffers::Offset Encode( - const GPUObjectDescriptor& desc, flatbuffers::FlatBufferBuilder* builder); - friend void Decode(const data::GPUObjectDescriptor* fb_obj, - GPUObjectDescriptor* obj); -+#endif - mutable std::map state_vars_; - AccessType access_type_; - }; -diff --git a/tensorflow/lite/delegates/gpu/cl/inference_context.cc b/tensorflow/lite/delegates/gpu/cl/inference_context.cc -index ca0c0319..f3cbc863 100644 ---- a/tensorflow/lite/delegates/gpu/cl/inference_context.cc -+++ b/tensorflow/lite/delegates/gpu/cl/inference_context.cc -@@ -151,6 +151,7 @@ CLNode& CLNode::operator=(CLNode&& node) { - return *this; - } - -+#ifdef TFLITE_GPU_LIB_FIX - absl::Status InferenceContext::InitFromGraph( - const CreateInferenceInfo& create_info, const GraphFloat32& graph, - Environment* env, std::vector* serialized_model) { -@@ -239,6 +240,7 @@ absl::Status InferenceContext::RestoreDeserialized( - } - return absl::OkStatus(); - } -+#endif - - absl::Status InferenceContext::InitFromGraphWithTransforms( - const CreateInferenceInfo& create_info, GraphFloat32* graph, -diff --git a/tensorflow/lite/delegates/gpu/cl/inference_context.h b/tensorflow/lite/delegates/gpu/cl/inference_context.h -index ec8055eb..871af9dd 100644 ---- a/tensorflow/lite/delegates/gpu/cl/inference_context.h -+++ b/tensorflow/lite/delegates/gpu/cl/inference_context.h -@@ -31,7 +31,9 @@ limitations under the License. - #include "tensorflow/lite/delegates/gpu/cl/model_hints.h" - #include "tensorflow/lite/delegates/gpu/cl/opencl_wrapper.h" - #include "tensorflow/lite/delegates/gpu/cl/precision.h" -+#ifdef TFLITE_GPU_LIB_FIX - #include "tensorflow/lite/delegates/gpu/cl/serialization_generated.h" -+#endif - #include "tensorflow/lite/delegates/gpu/cl/tensor_type.h" - #include "tensorflow/lite/delegates/gpu/common/model.h" - #include "tensorflow/lite/delegates/gpu/common/status.h" -@@ -100,12 +102,14 @@ class InferenceContext { - private: - enum TensorMemoryType { STRONG_SHAPE = 0, BUFFER = 1, VARIABLE = 2 }; - -+#ifdef TFLITE_GPU_LIB_FIX - friend flatbuffers::Offset Encode( - const InferenceContext& inference, - flatbuffers::FlatBufferBuilder* builder); - friend absl::Status Decode(CLContext* context, - const data::InferenceContext* fb_inference, - InferenceContext* inference); -+#endif - - void CopyInAndOutIds(const GraphFloat32& graph); - absl::Status ConvertOperations(const DeviceInfo& device_info, -diff --git a/tensorflow/lite/delegates/gpu/cl/kernels/gpu_operation.h b/tensorflow/lite/delegates/gpu/cl/kernels/gpu_operation.h -index 57d8690c..8178e2de 100644 ---- a/tensorflow/lite/delegates/gpu/cl/kernels/gpu_operation.h -+++ b/tensorflow/lite/delegates/gpu/cl/kernels/gpu_operation.h -@@ -30,7 +30,9 @@ limitations under the License. - #include "tensorflow/lite/delegates/gpu/cl/kernels/tuning_parameters.h" - #include "tensorflow/lite/delegates/gpu/cl/precision.h" - #include "tensorflow/lite/delegates/gpu/cl/program_cache.h" -+#ifdef TFLITE_GPU_LIB_FIX - #include "tensorflow/lite/delegates/gpu/cl/serialization_generated.h" -+#endif - #include "tensorflow/lite/delegates/gpu/cl/tensor.h" - #include "tensorflow/lite/delegates/gpu/cl/tensor_type.h" - #include "tensorflow/lite/delegates/gpu/common/data_type.h" -@@ -169,11 +171,12 @@ class GPUOperation { - bool check_src_channels_size_ = false; - - protected: -+#ifdef TFLITE_GPU_LIB_FIX - friend flatbuffers::Offset Encode( - const GPUOperation& op, flatbuffers::FlatBufferBuilder* builder); - friend absl::Status Decode(CLContext* context, - const data::GPUOperation* fb_op, GPUOperation* op); -- -+#endif - virtual absl::Status BindArguments(ArgumentsBinder* args) { - return absl::OkStatus(); - } -diff --git a/tensorflow/lite/delegates/gpu/cl/program_cache.cc b/tensorflow/lite/delegates/gpu/cl/program_cache.cc -index 285aa06d..f636a909 100644 ---- a/tensorflow/lite/delegates/gpu/cl/program_cache.cc -+++ b/tensorflow/lite/delegates/gpu/cl/program_cache.cc -@@ -18,9 +18,13 @@ limitations under the License. - #include - #include - -+#ifdef TFLITE_GPU_LIB_FIX - #include "flatbuffers/flatbuffers.h" // from @flatbuffers -+#endif - #include "tensorflow/lite/delegates/gpu/cl/cl_program.h" -+#ifdef TFLITE_GPU_LIB_FIX - #include "tensorflow/lite/delegates/gpu/cl/compiled_program_cache_generated.h" -+#endif - #include "tensorflow/lite/delegates/gpu/cl/util.h" - #include "tensorflow/lite/delegates/gpu/common/status.h" - #include -@@ -82,6 +86,7 @@ absl::Status ProgramCache::GetOrCreateCLKernel(const std::string& code, - return GetOrCreateCLKernel(code, function_name, {}, context, device, result); - } - -+#ifdef TFLITE_GPU_LIB_FIX - absl::Status ProgramCache::AddSerializedCache( - const CLContext& context, const CLDevice& device, - absl::Span serialized_cache) { -@@ -143,6 +148,7 @@ absl::Status ProgramCache::GetSerializedCache( - builder.GetSize()); - return absl::OkStatus(); - } -+#endif - - } // namespace cl - } // namespace gpu -diff --git a/tensorflow/lite/delegates/gpu/common/types.h b/tensorflow/lite/delegates/gpu/common/types.h -index 4ddb46f3..2b692f0b 100644 ---- a/tensorflow/lite/delegates/gpu/common/types.h -+++ b/tensorflow/lite/delegates/gpu/common/types.h -@@ -34,9 +34,9 @@ class alignas(2) half { - HalfBits bits; - - half() = default; -- -+#ifdef TFLITE_GPU_LIB_FIX - half(const half& f) : bits(f.bits) {} -- -+#endif - explicit half(float other) { bits = fp16_ieee_from_fp32_value(other); } - - void operator=(float f) { *this = half(f); } diff --git a/infra/cmake/packages/TensorFlowGpuSourceConfig.cmake b/infra/cmake/packages/TensorFlowGpuSourceConfig.cmake deleted file mode 100644 index 369816a..0000000 --- a/infra/cmake/packages/TensorFlowGpuSourceConfig.cmake +++ /dev/null @@ -1,75 +0,0 @@ -# -# Download Tensorflow 2.4.1, use gpu delegate codes only -# - -function(_TensorFlowGpuSource_Import) - SET(PATCH_FILE_CHECK "20211014") - SET(DATE_STAMP_PATH "${NNAS_EXTERNALS_DIR}/TENSORFLOW_GPU.stamp") - - set(PATCH_DONE FALSE) - if(EXISTS ${DATE_STAMP_PATH}) - file(STRINGS ${DATE_STAMP_PATH} OBTAINED_CONTENT) - if(${OBTAINED_CONTENT} STREQUAL "${PATCH_FILE_CHECK}") - set(PATCH_DONE "TRUE") - endif() - endif() - - if(${PATCH_DONE} STREQUAL "TRUE") - message(STATUS "Skip downloading TensorFlowGpuSource") - set(TENSORFLOWGPU_SOURCE_DIR "${NNAS_EXTERNALS_DIR}/TENSORFLOW_GPU" PARENT_SCOPE) - set(TensorFlowGpuSource_DIR "${TensorFlowGpuSource_DIR}" PARENT_SCOPE) - set(TensorFlowGpuSource_FOUND TRUE PARENT_SCOPE) - return() - else(${PATCH_DONE} STREQUAL "TRUE") - # PATCH_DONE FALSE - message(STATUS "TensorFlowGpuSource patch not found!") - endif(${PATCH_DONE} STREQUAL "TRUE") - - # Download TFLite Source Code - nnas_include(ExternalSourceTools) - nnas_include(OptionTools) - envoption(EXTERNAL_DOWNLOAD_SERVER "https://github.com") - envoption(TENSORFLOW_2_4_1_URL ${EXTERNAL_DOWNLOAD_SERVER}/tensorflow/tensorflow/archive/v2.4.1.tar.gz) - ExternalSource_Download(TFLITE_GPU_DELEGATE DIRNAME TENSORFLOW-2.4.1 ${TENSORFLOW_2_4_1_URL}) - - # Patch for non used codes on onert backend/gpu_cl - # ToDo: Do it more simpler - set(TENSORFLOWGPU_SOURCE_DIR "${NNAS_EXTERNALS_DIR}/TENSORFLOW_GPU") - - # remove & copy gpu delegate source codes only - if(EXISTS ${TENSORFLOWGPU_SOURCE_DIR}) - file(REMOVE_RECURSE "${TENSORFLOWGPU_SOURCE_DIR}") - endif() - - file(MAKE_DIRECTORY "${TENSORFLOWGPU_SOURCE_DIR}") - execute_process( - WORKING_DIRECTORY "${TFLITE_GPU_DELEGATE_SOURCE_DIR}" - COMMAND bash -c "cp -r --parents ./tensorflow/lite/delegates/gpu ../TENSORFLOW_GPU" - ) - - # Create Stamp - set(_remove_path "${TENSORFLOWGPU_SOURCE_DIR}.stamp") - if(EXISTS ${_remove_path}) - file(REMOVE ${_remove_path}) - endif() - execute_process( - WORKING_DIRECTORY "${NNAS_EXTERNALS_DIR}/TENSORFLOW_GPU" - COMMAND bash -c "patch -p1 < ${CMAKE_CURRENT_LIST_DIR}/TensorFlowGpuSource/patch_for_gpu_cl_build.patch" - ) - file(WRITE ${DATE_STAMP_PATH} "${PATCH_FILE_CHECK}") - set(TENSORFLOWGPU_SOURCE_DIR "${TENSORFLOWGPU_SOURCE_DIR}" PARENT_SCOPE) - set(TensorFlowGpuSource_DIR "${TensorFlowGpuSource_DIR}" PARENT_SCOPE) - set(TensorFlowGpuSource_FOUND TRUE PARENT_SCOPE) - - execute_process( - WORKING_DIRECTORY "${NNAS_EXTERNALS_DIR}" - COMMAND bash -c "rm -rf ${TFLITE_GPU_DELEGATE_SOURCE_DIR}.stamp" - COMMAND bash -c "rm -rf ${TFLITE_GPU_DELEGATE_SOURCE_DIR}" - ) -endfunction(_TensorFlowGpuSource_Import) - -if(NOT TensorFlowGpuSource_FOUND) - _TensorFlowGpuSource_Import() -else() - set(TensorFlowGpuSource_FOUND FALSE PARENT_SCOPE) -endif(NOT TensorFlowGpuSource_FOUND) diff --git a/infra/cmake/packages/TensorFlowLiteGpu/CMakeLists.txt b/infra/cmake/packages/TensorFlowLiteGpu/CMakeLists.txt deleted file mode 100644 index c69e0bb..0000000 --- a/infra/cmake/packages/TensorFlowLiteGpu/CMakeLists.txt +++ /dev/null @@ -1,72 +0,0 @@ -# -# Tensorflow Lite GPU delegate library 2.4.1 -# - -set(LIB_TENSORFLOW_GPU_DELEGATE "TensorFlowGpu") - -#TENSORFLOWGPU_SOURCE_DIR -set(REF_TENSORFLOW_SRC_BASE ${TENSORFLOWGPU_SOURCE_DIR}) -set(REF_TENSORFLOW_LITE_SRC_BASE ${REF_TENSORFLOW_SRC_BASE}/tensorflow/lite) -set(REF_TENSORFLOW_LITE_GPU_DELEGATE_SRC_BASE "${REF_TENSORFLOW_LITE_SRC_BASE}/delegates/gpu") - -set(SRC_BASE "${REF_TENSORFLOW_LITE_GPU_DELEGATE_SRC_BASE}") -file(GLOB GPU_CL_SRC_LIST "${SRC_BASE}/cl/*.cc" - "${SRC_BASE}/cl/kernels/*.cc" - "${SRC_BASE}/cl/kernels/special/*.cc" - "${SRC_BASE}/cl/kernels/selectors/*.cc" - "${SRC_BASE}/cl/selectors/*.cc" - "${SRC_BASE}/common/*.cc" -# Available, but not needed yet -# "${SRC_BASE}/common/default/*.cc" -# "${SRC_BASE}/common/memory_managements/*.cc" -# "${SRC_BASE}/common/transformations/*.cc" - ) - -file(GLOB GPU_CL_HDRS_GLOB "${SRC_BASE}/cl/*.h" - "${SRC_BASE}/cl/kernels/*.h" - "${SRC_BASE}/cl/kernels/special/*.h" - "${SRC_BASE}/cl/kernels/selectors/*.h" - "${SRC_BASE}/cl/selectors/*.h" - "${SRC_BASE}/common/*.h" - "${SRC_BASE}/common/default/*.h" - "${SRC_BASE}/common/memory_managements/*.h" - "${SRC_BASE}/common/transformations/*.h" - ) -list(APPEND GPU_CL_SRC_LIST "${_GPU_CL_HDRS_GLOB}") - -file(GLOB REMOVE_TEST_SRCS "${SRC_BASE}/cl/*_test*.cc" - "${SRC_BASE}/cl/testing/*.cc" - "${SRC_BASE}/cl/kernels/*_test*.cc" - "${SRC_BASE}/common/*_test*.cc" - "${SRC_BASE}/common/transformations/*_test*.cc" - ) -# Not available -file(GLOB REMOVE_SRCS "${SRC_BASE}/cl/*gl*.cc" - "${SRC_BASE}/cl/gpu_api_delegate.cc" - "${SRC_BASE}/cl/serialization.cc" - "${SRC_BASE}/common/lstm_parser.cc" - "${SRC_BASE}/common/model_builder.cc" - "${SRC_BASE}/common/model_builder_helper.cc" - "${SRC_BASE}/common/object_reader.cc" - "${SRC_BASE}/common/quantization_util.cc" - "${SRC_BASE}/common/memory_management/*_test.cc" - ) - -list(REMOVE_ITEM GPU_CL_SRC_LIST ${REMOVE_TEST_SRCS}) -list(REMOVE_ITEM GPU_CL_SRC_LIST ${REMOVE_SRCS}) -list(APPEND TFLITE_GPU_SRCS ${GPU_CL_SRC_LIST}) - -add_library(${LIB_TENSORFLOW_GPU_DELEGATE} STATIC ${TFLITE_GPU_SRCS}) -target_include_directories(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "${Fp16Source_DIR}/include") -target_include_directories(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "${TENSORFLOWGPU_SOURCE_DIR}") -target_link_libraries(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE abseil farmhash fp16) - -add_library(tflitegpu_ignore_warnings INTERFACE) -target_compile_options(tflitegpu_ignore_warnings INTERFACE -Wno-unused-parameter -Wno-sign-compare) -target_link_libraries(${LIB_TENSORFLOW_GPU_DELEGATE} INTERFACE tflitegpu_ignore_warnings) - -# GL codes are not used on gpu_cl -target_compile_options(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "-DCL_DELEGATE_NO_GL") - -# Applying PIC first, currently used on gpu_cl only -set_target_properties(${LIB_TENSORFLOW_GPU_DELEGATE} PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/infra/command/format b/infra/command/format index 993a6ad..461da6f 100644 --- a/infra/command/format +++ b/infra/command/format @@ -157,6 +157,10 @@ function check_python_files() { FILES_TO_CHECK_PYTHON=(`echo "$FILES_TO_CHECK" | tr ' ' '\n' | egrep '\.py$'`) # Exceptional case: one-cmds don't have '.py' extension: ignore non-python source (cmake, etc) and ignore shell script: one-prepare-venv FILES_TO_CHECK_PYTHON+=(`echo "$FILES_TO_CHECK" | tr ' ' '\n' | egrep '^compiler/one-cmds/[^(\./)]*$' | egrep -v '^compiler/one-cmds/one-prepare-venv$'`) + # Exceptional case: onecc-docker don't have '.py' extension. + FILES_TO_CHECK_PYTHON+=(`echo "$FILES_TO_CHECK" | tr ' ' '\n' | egrep '^compiler/onecc-docker/onecc-docker$'`) + # Exceptional case: visq don't have '.py' extension. + FILES_TO_CHECK_PYTHON+=(`echo "$FILES_TO_CHECK" | tr ' ' '\n' | egrep '^compiler/visq/visq$'`) for s in ${DIRECTORIES_NOT_TO_BE_TESTED[@]}; do skip=${s#'.'/}/ diff --git a/infra/command/gen-coverage-report b/infra/command/gen-coverage-report index df6377d..c841dc0 100644 --- a/infra/command/gen-coverage-report +++ b/infra/command/gen-coverage-report @@ -66,13 +66,10 @@ done "${LCOV_PATH}" -e "${RAW_COVERAGE_INFO_PATH}" -o "${EXTRACTED_COVERAGE_INFO_PATH}" \ "${CANDIDATES[@]}" - -opencl_files=($(find ./runtime/onert/backend/gpu_cl/open_cl/ \( -name "*.cc" -o -name "*.h" \) -exec realpath {} \; )) - # Exclude test files from coverage report # Exclude flatbuffer generated files from coverage report "${LCOV_PATH}" -r "${EXTRACTED_COVERAGE_INFO_PATH}" -o "${EXCLUDED_COVERAGE_INFO_PATH}" \ - '*.test.cpp' '*.test.cc' '*/test/*' '*/tests/*' '*_schema_generated.h' "${opencl_files[@]}" + '*.test.cpp' '*.test.cc' '*/test/*' '*/tests/*' '*_schema_generated.h' # Final coverage data cp -v ${EXCLUDED_COVERAGE_INFO_PATH} ${COVERAGE_INFO_PATH} diff --git a/infra/debian/compiler/changelog b/infra/debian/compiler/changelog index ddca70a..ba5eea9 100644 --- a/infra/debian/compiler/changelog +++ b/infra/debian/compiler/changelog @@ -1,3 +1,16 @@ +one (1.22.0) bionic; urgency=medium + + * Introduce new optimization options: `unroll_unidirseqlstm`, `forward_transpose_op`, `fold_fully_connected`, `fuse_prelu` + * Support more Ops for fake quantization: `Depth2Space`, `Space2Depth`, `Pack`, `Unpack`, `Abs` + * Support more Ops for quantization: `Abs`, `ReduceProd` + * Introduce _visq_ tool for quantization error visualization + * Introduce _Environment_ section into configuration file + * Improve speed of `convert_nchw_to_nhwc` option + * Support `Add`, `Mul` of index-type (int32, int64) tensors in _one-quantize_ + * Support ubuntu 20.04 + + -- seongwoo Fri, 24 Mar 2023 13:58:16 +0900 + one (1.21.0) bionic; urgency=medium * Support unrolling of LSTM and RNN Ops in `one-import-onnx` tool diff --git a/infra/debian/compiler/one-compiler-dev.install b/infra/debian/compiler/one-compiler-dev.install index b7a681d..47f53ad 100644 --- a/infra/debian/compiler/one-compiler-dev.install +++ b/infra/debian/compiler/one-compiler-dev.install @@ -1,6 +1,7 @@ # {FILES_TO_INSTALL} {DEST_DIR} # bin usr/bin/circledump usr/share/one/bin/ +usr/bin/circle-opselector usr/share/one/bin/ usr/bin/circle-tensordump usr/share/one/bin/ usr/bin/tflchef usr/share/one/bin/ usr/bin/tflchef-file usr/share/one/bin/ diff --git a/infra/debian/compiler/one-compiler.install b/infra/debian/compiler/one-compiler.install index 65e46d1..641bbe6 100644 --- a/infra/debian/compiler/one-compiler.install +++ b/infra/debian/compiler/one-compiler.install @@ -2,12 +2,14 @@ # bin usr/bin/circle2circle usr/share/one/bin/ usr/bin/circle-eval-diff usr/share/one/bin/ +usr/bin/circle-interpreter usr/share/one/bin/ usr/bin/circle-operator usr/share/one/bin/ usr/bin/circle-partitioner usr/share/one/bin/ usr/bin/circle-quantizer usr/share/one/bin/ +usr/bin/dalgona usr/share/one/bin/ usr/bin/generate_bcq_metadata.py usr/share/one/bin/ usr/bin/generate_bcq_output_arrays.py usr/share/one/bin/ -usr/bin/model2nnpkg.sh usr/share/one/bin/ +usr/bin/model2nnpkg usr/share/one/bin/ usr/bin/onecc usr/share/one/bin/ usr/bin/onecc.template.cfg usr/share/one/bin/ usr/bin/one-build usr/share/one/bin/ @@ -32,14 +34,24 @@ usr/bin/onelib/CfgRunner.py usr/share/one/bin/onelib/ usr/bin/onelib/OptionBuilder.py usr/share/one/bin/onelib/ usr/bin/onelib/TopologicalSortHelper.py usr/share/one/bin/onelib/ usr/bin/onelib/WorkflowRunner.py usr/share/one/bin/onelib/ +usr/bin/onelib/Command.py usr/share/one/bin/onelib/ +usr/bin/onelib/utils.py usr/share/one/bin/onelib/ +usr/bin/onelib/export_constant.py usr/share/one/bin/onelib/ usr/bin/onnx_legalizer.py usr/share/one/bin/ usr/bin/rawdata2hdf5 usr/share/one/bin/ usr/bin/record-minmax usr/share/one/bin/ usr/bin/tf2nnpkg usr/share/one/bin/ usr/bin/tf2tfliteV2.py usr/share/one/bin/ usr/bin/tflite2circle usr/share/one/bin/ -usr/bin/utils.py usr/share/one/bin/ +usr/bin/visq usr/share/one/bin/ +usr/bin/visqlib/DumpFakeQuantFM.py usr/share/one/bin/visqlib/ +usr/bin/visqlib/DumpFP32FM.py usr/share/one/bin/visqlib/ +usr/bin/visqlib/Palette.py usr/share/one/bin/visqlib/ +usr/bin/visqlib/QErrorComputer.py usr/share/one/bin/visqlib/ +usr/bin/visqlib/Util.py usr/share/one/bin/visqlib/ # lib usr/lib/* usr/share/one/lib/ # doc usr/doc/* usr/share/one/doc/ +# optimization +usr/optimization/* usr/share/one/optimization/ diff --git a/infra/debian/compiler/rules b/infra/debian/compiler/rules index 145634d..447fb21 100755 --- a/infra/debian/compiler/rules +++ b/infra/debian/compiler/rules @@ -1,7 +1,7 @@ #!/usr/bin/make -f export DH_VERBOSE = 1 export NNAS_BUILD_PREFIX = build -export PRESET = 20220323 +export PRESET = 20221125 export _DESTDIR = debian/tmp/usr %: @@ -14,9 +14,6 @@ override_dh_auto_install: cmake --build "$(NNAS_BUILD_PREFIX)/nncc" -- install override_dh_install: - install -t "$(_DESTDIR)/bin" -D "tools/nnpackage_tool/model2nnpkg/model2nnpkg.sh" install -T -m 755 -D "infra/packaging/res/tf2nnpkg.${PRESET}" "$(_DESTDIR)/bin/tf2nnpkg" dh_install -override_dh_builddeb: - dh_builddeb --destdir=$(NNAS_BUILD_PREFIX) diff --git a/infra/debian/runtime/rules b/infra/debian/runtime/rules index 97170ee..a228196 100755 --- a/infra/debian/runtime/rules +++ b/infra/debian/runtime/rules @@ -3,7 +3,7 @@ DEBVER := $(shell dpkg-parsechangelog -SVersion) export DH_VERBOSE = 1 export _DESTDIR = debian/tmp/ export BUILD_TYPE=release -export OPTIONS=-DBUILD_LOGGING=0 -DBUILD_TFLITE_COMPARATOR_TEST_TOOL=0 -DBUILD_NNPACKAGE_RUN=0 -DBUILD_TFLITE_RUN=0 -DBUILD_NNAPI_TEST=0 -DBUILD_RUNTIME_NNAPI_TEST=0 -DBUILD_TFLITE_BENCHMARK_MODEL=0 -DBUILD_TFLITE_VANILLA_RUN=0 -DBUILD_TENSORFLOW_LITE_2_8_0=0 -DBUILD_TENSORFLOW_LITE=0 +export OPTIONS=-DBUILD_LOGGING=0 -DBUILD_TFLITE_COMPARATOR_TEST_TOOL=0 -DBUILD_ONERT_RUN=0 -DBUILD_TFLITE_RUN=0 -DBUILD_RUNTIME_NNAPI_TEST=0 -DBUILD_TFLITE_VANILLA_RUN=0 -DBUILD_TENSORFLOW_LITE_2_8_0=0 -DBUILD_TENSORFLOW_LITE=0 export DEBIAN_BUILD=1 export INSTALL_PATH=debian/tmp/usr/ %: diff --git a/infra/docker/bionic/Dockerfile b/infra/docker/bionic/Dockerfile index f7ffc73..383fddc 100644 --- a/infra/docker/bionic/Dockerfile +++ b/infra/docker/bionic/Dockerfile @@ -41,12 +41,12 @@ RUN apt-get update && apt-get -qqy install libprotobuf-dev protobuf-compiler # Additonal tools RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive \ - apt-get -qqy install doxygen graphviz wget zip unzip clang-format-8 python3 python3-pip python3-venv hdf5-tools pylint curl -RUN apt-get update && apt-get -qqy install python3.8 python3.8-venv + apt-get -qqy install doxygen graphviz wget zip unzip clang-format-8 python3 python3-pip python3-venv python3-dev hdf5-tools pylint curl +RUN apt-get update && apt-get -qqy install python3.8 python3.8-venv python3.8-dev RUN python3 -m pip install --upgrade pip -RUN python3 -m pip install yapf==0.22.0 numpy +RUN python3 -m pip install yapf==0.22.0 numpy flatbuffers RUN python3.8 -m pip install --upgrade pip -RUN python3.8 -m pip install numpy +RUN python3.8 -m pip install numpy flatbuffers # Install google test (source) RUN apt-get update && apt-get -qqy install libgtest-dev @@ -131,5 +131,15 @@ ENV LC_ALL "C.UTF-8" # setup adb server EXPOSE 5037 +# Setup user to match host user, and give superuser permissions +ARG USER_ID=1000 +ARG GROUP_ID=${USER_ID} +RUN addgroup --gid ${GROUP_ID} ubuntu && adduser --disabled-password --gecos '' --uid ${USER_ID} --gid ${GROUP_ID} ubuntu && usermod -aG sudo ubuntu +RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +RUN echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + # Clean archives (to reduce image size) RUN apt-get clean -y + +# Set user to the one we just created +USER ${USER_ID} diff --git a/infra/docker/focal/Dockerfile b/infra/docker/focal/Dockerfile index 1cdeffb..2848579 100644 --- a/infra/docker/focal/Dockerfile +++ b/infra/docker/focal/Dockerfile @@ -34,9 +34,9 @@ RUN apt-get update && apt-get -qqy install libprotobuf-dev protobuf-compiler # Additonal tools RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive \ - apt-get -qqy install doxygen graphviz wget zip unzip clang-format-8 python3 python3-pip python3-venv hdf5-tools pylint curl + apt-get -qqy install doxygen graphviz wget zip unzip clang-format-8 python3 python3-pip python3-venv python3-dev hdf5-tools pylint curl RUN python3 -m pip install --upgrade pip -RUN python3 -m pip install yapf==0.22.0 numpy +RUN python3 -m pip install yapf==0.22.0 numpy flatbuffers # Install google test (source) RUN apt-get update && apt-get -qqy install libgtest-dev @@ -48,5 +48,15 @@ RUN wget http://download.tizen.org/sdk/tizenstudio/official/binary/sdb_4.2.19_ub RUN unzip -d tmp sdb.zip && rm sdb.zip RUN cp tmp/data/tools/sdb /usr/bin/. && rm -rf tmp/* +# Setup user to match host user, and give superuser permissions +ARG USER_ID=1000 +ARG GROUP_ID=${USER_ID} +RUN addgroup --gid ${GROUP_ID} ubuntu && adduser --disabled-password --gecos '' --uid ${USER_ID} --gid ${GROUP_ID} ubuntu && usermod -aG sudo ubuntu +RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +RUN echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + # Clean archives (to reduce image size) RUN apt-get clean -y + +# Set user to the one we just created +USER ${USER_ID} diff --git a/infra/docker/jammy/Dockerfile b/infra/docker/jammy/Dockerfile new file mode 100644 index 0000000..aa500b0 --- /dev/null +++ b/infra/docker/jammy/Dockerfile @@ -0,0 +1,60 @@ +# Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM ubuntu:jammy + +ARG UBUNTU_MIRROR + +# Install 'add-apt-repository' +RUN apt-get update && apt-get -qqy install software-properties-common + +# Build tool +RUN apt-get update && apt-get -qqy install build-essential cmake scons git lcov g++-arm-linux-gnueabihf g++-aarch64-linux-gnu + +# Debian build tool +RUN apt-get update && apt-get -qqy install fakeroot devscripts debhelper python3-all dh-python + +# Install extra dependencies (Caffe, nnkit) +RUN apt-get update && apt-get -qqy install libboost-all-dev libgflags-dev libgoogle-glog-dev libatlas-base-dev libhdf5-dev + +# Install protocol buffer +RUN apt-get update && apt-get -qqy install libprotobuf-dev protobuf-compiler + +# Additonal tools +# TODO install clang-format (No official clang-format-8 package for ubuntu jammy) +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive \ + apt-get -qqy install doxygen graphviz wget zip unzip python3 python3-pip python3-venv python3-dev hdf5-tools pylint curl +RUN python3 -m pip install --upgrade pip +RUN python3 -m pip install yapf==0.22.0 numpy flatbuffers + +# Install google test (source) +RUN apt-get update && apt-get -qqy install libgtest-dev + +# TODO: Install gbs & sdb +# gbs & sdb are not support ubuntu jammy yet + +# Setup user to match host user, and give superuser permissions +ARG USER_ID=1000 +ARG GROUP_ID=${USER_ID} +RUN apt-get update && apt-get -qqy install sudo +RUN addgroup --gid ${GROUP_ID} ubuntu && adduser --disabled-password --gecos '' --uid ${USER_ID} --gid ${GROUP_ID} ubuntu && usermod -aG sudo ubuntu +RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +RUN echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + +# Clean archives (to reduce image size) +RUN apt-get clean -y + +# Set user to the one we just created +USER ${USER_ID} diff --git a/infra/doxygen/Doxyfile b/infra/doxygen/Doxyfile index 0dc6fdf..af2adfc 100644 --- a/infra/doxygen/Doxyfile +++ b/infra/doxygen/Doxyfile @@ -32,7 +32,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = nnas +PROJECT_NAME = "ONE - On-device Neural Engine" # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version @@ -252,7 +252,7 @@ TCL_SUBST = # members will be omitted, etc. # The default value is: NO. -OPTIMIZE_OUTPUT_FOR_C = NO +OPTIMIZE_OUTPUT_FOR_C = YES # Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or # Python sources only. Doxygen will then generate output that is more tailored @@ -623,13 +623,13 @@ STRICT_PROTO_MATCHING = NO # list. This list is created by putting \todo commands in the documentation. # The default value is: YES. -GENERATE_TODOLIST = YES +GENERATE_TODOLIST = NO # The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test # list. This list is created by putting \test commands in the documentation. # The default value is: YES. -GENERATE_TESTLIST = YES +GENERATE_TESTLIST = NO # The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug # list. This list is created by putting \bug commands in the documentation. @@ -642,7 +642,7 @@ GENERATE_BUGLIST = YES # the documentation. # The default value is: YES. -GENERATE_DEPRECATEDLIST= YES +GENERATE_DEPRECATEDLIST= NO # The ENABLED_SECTIONS tag can be used to enable conditional documentation # sections, marked by \if ... \endif and \cond @@ -790,7 +790,14 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = +INPUT = README.md \ + docs/howto/ \ + docs/overview/ \ + docs/runtime/ \ + compute/ \ + compiler/ \ + onert-micro/ \ + runtime/ # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses @@ -873,23 +880,14 @@ RECURSIVE = YES # Note that relative paths are relative to the directory from which doxygen is # run. -EXCLUDE = Product/ \ - build/ \ - doxygen/ \ - report/ \ - externals/ \ - packaging/ \ - runtimes/contrib/ \ - runtimes/pure_arm_compute/ \ - tests/ \ - tools/ +EXCLUDE = # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded # from the input. # The default value is: NO. -EXCLUDE_SYMLINKS = NO +EXCLUDE_SYMLINKS = YES # If the value of the INPUT tag contains directories, you can use the # EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude @@ -898,7 +896,17 @@ EXCLUDE_SYMLINKS = NO # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories for example use the pattern */test/* -EXCLUDE_PATTERNS = +EXCLUDE_PATTERNS = *.test.* \ + */test/* \ + */tests/* \ + */unittest/* \ + *_generated.* \ + */3rdparty/* \ + */contrib/* \ + */compiler/*/*.md \ + */compute/*/*.md \ + */runtime/*/*.md + # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names # (namespaces, classes, functions, etc.) that should be excluded from the @@ -991,7 +999,7 @@ FILTER_SOURCE_PATTERNS = # (index.html). This can be useful if you have a project on for instance GitHub # and want to reuse the introduction page also for the doxygen output. -USE_MDFILE_AS_MAINPAGE = docs/nnfw/roadmap.md +USE_MDFILE_AS_MAINPAGE = README.md #--------------------------------------------------------------------------- # Configuration options related to source browsing @@ -1010,7 +1018,7 @@ SOURCE_BROWSER = YES # classes and enums directly into the documentation. # The default value is: NO. -INLINE_SOURCES = NO +INLINE_SOURCES = YES # Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any # special comment blocks from generated source code fragments. Normal C, C++ and @@ -1023,13 +1031,13 @@ STRIP_CODE_COMMENTS = YES # function all documented functions referencing it will be listed. # The default value is: NO. -REFERENCED_BY_RELATION = NO +REFERENCED_BY_RELATION = YES # If the REFERENCES_RELATION tag is set to YES then for each documented function # all documented entities called/used by that function will be listed. # The default value is: NO. -REFERENCES_RELATION = NO +REFERENCES_RELATION = YES # If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set # to YES then the hyperlinks from functions in REFERENCES_RELATION and @@ -2265,7 +2273,7 @@ DOT_FONTPATH = # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. -CLASS_GRAPH = YES +CLASS_GRAPH = NO # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # graph for each documented class showing the direct and indirect implementation @@ -2310,7 +2318,7 @@ UML_LIMIT_NUM_FIELDS = 10 # The default value is: NO. # This tag requires that the tag HAVE_DOT is set to YES. -TEMPLATE_RELATIONS = NO +TEMPLATE_RELATIONS = YES # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to # YES then doxygen will generate a graph for each documented file showing the @@ -2319,7 +2327,7 @@ TEMPLATE_RELATIONS = NO # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. -INCLUDE_GRAPH = YES +INCLUDE_GRAPH = NO # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are # set to YES then doxygen will generate a graph for each documented file showing @@ -2328,7 +2336,7 @@ INCLUDE_GRAPH = YES # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. -INCLUDED_BY_GRAPH = YES +INCLUDED_BY_GRAPH = NO # If the CALL_GRAPH tag is set to YES then doxygen will generate a call # dependency graph for every global function or class method. @@ -2340,7 +2348,7 @@ INCLUDED_BY_GRAPH = YES # The default value is: NO. # This tag requires that the tag HAVE_DOT is set to YES. -CALL_GRAPH = YES +CALL_GRAPH = NO # If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller # dependency graph for every global function or class method. @@ -2352,7 +2360,7 @@ CALL_GRAPH = YES # The default value is: NO. # This tag requires that the tag HAVE_DOT is set to YES. -CALLER_GRAPH = YES +CALLER_GRAPH = NO # If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical # hierarchy of all classes instead of a textual one. @@ -2401,7 +2409,7 @@ INTERACTIVE_SVG = NO # found. If left blank, it is assumed the dot tool can be found in the path. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_PATH = /usr/local/bin/dot +DOT_PATH = # The DOTFILE_DIRS tag can be used to specify one or more directories that # contain dot files that are included in the documentation (see the \dotfile @@ -2450,7 +2458,7 @@ PLANTUML_INCLUDE_PATH = # Minimum value: 0, maximum value: 10000, default value: 50. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_GRAPH_MAX_NODES = 50 +DOT_GRAPH_MAX_NODES = 500 # The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs # generated by dot. A depth value of 3 means that only nodes reachable from the diff --git a/infra/nncc/CMakeLists.txt b/infra/nncc/CMakeLists.txt index 768d797..bd53c33 100644 --- a/infra/nncc/CMakeLists.txt +++ b/infra/nncc/CMakeLists.txt @@ -1,7 +1,4 @@ -# The libboost 1.74 uses IN_LIST operator, which requires the policy CMP0057, in a CMake file. -# This policy requires ``cmake_minimum_required(VERSION 3.3)``. -# Run "cmake --help-policy CMP0057" for policy details. -cmake_minimum_required(VERSION 3.3) +cmake_minimum_required(VERSION 3.10) project(nncc) @@ -14,11 +11,6 @@ set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib:$ORIGIN/") set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) -# This feature works with CMake 3.5.2 or later. However, using previous versions does not produce -# an error. We are still officially using CMake 3.1.0, but put this code for the sake of semantic -# support in various development tools. -# Todo: Someday, CMake needs to be updated to 3.7.2 or later to take advantage of improvements -# such as `cmake-server`. set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(NNAS_PROJECT_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../.." CACHE diff --git a/infra/nncc/Makefile.arm32 b/infra/nncc/Makefile.arm32 index 22d96e7..97631fc 100644 --- a/infra/nncc/Makefile.arm32 +++ b/infra/nncc/Makefile.arm32 @@ -4,6 +4,7 @@ # BUILD_TYPE?=Debug +BUILD_JOBS?=1 CURRENT_DIR=$(shell pwd) BUILDFOLDER=build @@ -27,6 +28,7 @@ ARM32_BUILD_ITEMS+=;hermes;hermes-std ARM32_BUILD_ITEMS+=;loco;locop;logo-core;logo ARM32_BUILD_ITEMS+=;safemain;mio-circle04;mio-tflite280 ARM32_BUILD_ITEMS+=;dio-hdf5 +ARM32_BUILD_ITEMS+=;luci-compute ARM32_BUILD_ITEMS+=;foder;circle-verify;souschef;arser;vconone ARM32_BUILD_ITEMS+=;luci ARM32_BUILD_ITEMS+=;luci-interpreter @@ -43,6 +45,7 @@ ARM32_HOST_ITEMS+=;oops ARM32_HOST_ITEMS+=;hermes;hermes-std ARM32_HOST_ITEMS+=;loco;locop;logo-core;logo ARM32_HOST_ITEMS+=;safemain;mio-circle04;mio-tflite280 +ARM32_HOST_ITEMS+=;luci-compute ARM32_HOST_ITEMS+=;foder;circle-verify;souschef;arser;vconone ARM32_HOST_ITEMS+=;luci ARM32_HOST_ITEMS+=;luci-interpreter @@ -54,12 +57,12 @@ ARM32_HOST_ITEMS+=;common-artifacts ARM32_HOST_ITEMS+=;luci-eval-driver;luci-value-test -_SPACE_:= -_SPACE_+= +_EMPTY_:= +_SPACE_:=$(_EMPTY_) $(_EMPTY_) ARM32_BUILD_WHITELIST=$(subst $(_SPACE_),,$(ARM32_BUILD_ITEMS)) ARM32_HOST_WHITELIST=$(subst $(_SPACE_),,$(ARM32_HOST_ITEMS)) -NNCC_CFG_OPTION+= -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DENABLE_COVERAGE=OFF +NNCC_CFG_OPTION+= -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DENABLE_COVERAGE=OFF -DEXTERNALS_BUILD_THREADS=$(BUILD_JOBS) NNCC_CFG_STRICT= -DENABLE_STRICT_BUILD=ON @@ -105,19 +108,22 @@ int_configure_arm32: $(NNCC_ARM32_DEBUG) $(NNCC_CFG_STRICT) \ -DCMAKE_TOOLCHAIN_FILE=$(ARM32_TOOLCHAIN_FILE) \ -DCMAKE_INSTALL_PREFIX="$(ARM32_INSTALL_FOLDER)" \ + -DBUILD_ARM32_NEON=ON \ -DENABLE_TEST=ON +# TODO remove BUILD_ARM32_NEON=ON as default is ON, after a while. +# explictly added to prevent using cached 'BUILD_ARM32_NEON=OFF' # # builds # int_build_arm32_host: - NNCC_WORKSPACE=$(BUILD_ARM32_HOST) ./nncc build -j1 + NNCC_WORKSPACE=$(BUILD_ARM32_HOST) ./nncc build -j$(BUILD_JOBS) int_build_arm32: ROOTFS_DIR=$(ROOTFS_ARM) TARGET_ARCH=armv7l \ BUILD_HOST_EXEC=$(CURRENT_DIR)/$(BUILD_ARM32_HOST) \ - NNCC_WORKSPACE=$(BUILD_ARM32_FOLDER) ./nncc build -j1 + NNCC_WORKSPACE=$(BUILD_ARM32_FOLDER) ./nncc build -j$(BUILD_JOBS) # # host test; run test in host to generate random input and expected outputs diff --git a/infra/nncc/cmake/buildtool/config/config_armv7l-linux.cmake b/infra/nncc/cmake/buildtool/config/config_armv7l-linux.cmake index c800f33..87704db 100644 --- a/infra/nncc/cmake/buildtool/config/config_armv7l-linux.cmake +++ b/infra/nncc/cmake/buildtool/config/config_armv7l-linux.cmake @@ -9,15 +9,16 @@ include("${CMAKE_CURRENT_LIST_DIR}/config_linux.cmake") # addition for arm-linux set(FLAGS_COMMON ${FLAGS_COMMON} - "-mcpu=cortex-a7" + "-march=armv7-a" + "-mtune=cortex-a8" "-mfloat-abi=hard" - "-ftree-vectorize" "-mfp16-format=ieee" ) if(BUILD_ARM32_NEON) set(FLAGS_COMMON ${FLAGS_COMMON} - "-mfpu=neon-vfpv4" + "-mfpu=vfpv3-d16" + "-ftree-vectorize" ) else(BUILD_ARM32_NEON) message(STATUS "ARMv7l: NEON is disabled") diff --git a/infra/nncc/config/docker.configuration b/infra/nncc/config/docker.configuration index fb32957..2765c36 100644 --- a/infra/nncc/config/docker.configuration +++ b/infra/nncc/config/docker.configuration @@ -11,7 +11,7 @@ DOCKER_PATH="$NNCC_PROJECT_PATH" export GIT_SSL_NO_VERIFY=1 -DOCKER_VOLUMES=" -v $HOST_PATH:$DOCKER_PATH" +DOCKER_VOLUMES+=" -v $HOST_PATH:$DOCKER_PATH" DOCKER_ENV_VARS+=" -e http_proxy" DOCKER_ENV_VARS+=" -e no_proxy" diff --git a/infra/nnfw/CMakeLists.txt b/infra/nnfw/CMakeLists.txt index 2a27eee..bb55a8f 100644 --- a/infra/nnfw/CMakeLists.txt +++ b/infra/nnfw/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5.1) +cmake_minimum_required(VERSION 3.10) project(nnfw) @@ -64,11 +64,6 @@ endif() set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_EXTENSIONS OFF) -# This feature works with CMake 3.5.2 or later. However, using previous versions does not produce -# an error. We are still officially using CMake 3.5.1, but put this code for the sake of semantic -# support in various development tools. -# Todo: Someday, CMake needs to be updated to 3.7.2 or later to take advantage of improvements -# such as `cmake-server`. set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # identify platform: HOST_PLATFORM, TARGET_PLATFORM and related diff --git a/infra/nnfw/cmake/CfgOptionFlags.cmake b/infra/nnfw/cmake/CfgOptionFlags.cmake index 440f185..b2f4da5 100644 --- a/infra/nnfw/cmake/CfgOptionFlags.cmake +++ b/infra/nnfw/cmake/CfgOptionFlags.cmake @@ -19,9 +19,7 @@ option(BUILD_RUNTIME_NNAPI_TEST "Build Runtime NN API Generated Test" ON) option(BUILD_RUNTIME_NNFW_API_TEST "Build Runtime NNFW API Tests" ON) option(BUILD_TFLITE_RUN "Build tflite-run" ON) option(BUILD_TFLITE_VANILLA_RUN "Build tflite-vanilla-run" OFF) -option(BUILD_TFLITE_BENCHMARK_MODEL "Build tflite benchmark model" OFF) -option(BUILD_NNAPI_TEST "Build nnapi_test" ON) -option(BUILD_NNPACKAGE_RUN "Build nnpackge_run" ON) +option(BUILD_ONERT_RUN "Build onert_run" ON) option(BUILD_TFLITE_LOADER "Build TensorFlow Lite loader" ON) option(BUILD_CIRCLE_LOADER "Build circle loader" ON) option(BUILD_TRIX_LOADER "Build trix loader" ON) @@ -31,12 +29,11 @@ option(GENERATE_RUNTIME_NNAPI_TESTS "Generate NNAPI operation gtest" ON) option(ENVVAR_ONERT_CONFIG "Use environment variable for onert configuration" ON) option(INSTALL_TEST_SCRIPTS "Install test scripts" ON) option(BUILD_GPU_CL "Build gpu_cl backend" OFF) -option(BUILD_NPUD "Build NPU daemon" ON) -option(ENVVAR_NPUD_CONFIG "Use environment variable for npud configuration" ON) +option(BUILD_NPUD "Build NPU daemon" OFF) +option(ENVVAR_NPUD_CONFIG "Use environment variable for npud configuration" OFF) # # Default build configuration for contrib # -option(BUILD_ANDROID_TFLITE "Enable android support for TensorFlow Lite" OFF) option(BUILD_ANDROID_BENCHMARK_APP "Enable Android Benchmark App" OFF) option(BUILD_BENCHMARK_ACL "Build ARM Compute Library Benchmarks" OFF) option(BUILD_DETECTION_APP "Build detection example app" OFF) @@ -57,7 +54,6 @@ option(BUILD_TFLITE_ACCURACY "Build tflite accuracy tool" OFF) # Default external libraries source download and build configuration # option(DOWNLOAD_TENSORFLOW "Download Tensorflow source" ON) -option(DOWNLOAD_TENSORFLOW_GPU "Download Tensorflow GPU delegate source" OFF) option(DOWNLOAD_ABSEIL "Download Abseil source" ON) option(DOWNLOAD_OPENCL_HEADERS "Download Opencl_headers source" OFF) option(DOWNLOAD_EIGEN "Download Eigen source" ON) @@ -74,7 +70,6 @@ option(DOWNLOAD_OOURAFFT "Download Ooura FFT source" ON) option(DOWNLOAD_GTEST "Download Google Test source and build Google Test" ON) option(BUILD_BOOST "Build boost source" OFF) option(BUILD_TENSORFLOW_LITE "Build TensorFlow Lite from the downloaded source" ON) -option(BUILD_TENSORFLOW_LITE_2_8_0 "Build TensorFlow Lite 2.8.0 from the downloaded source" OFF) option(BUILD_TENSORFLOW_LITE_GPU "Build TensorFlow Lite GPU delegate from the downloaded source" OFF) option(BUILD_ARMCOMPUTE "Build ARM Compute from the downloaded source" ON) option(DEBUG_ARMCOMPUTE "Build ARM Compute as debug type" OFF) diff --git a/infra/nnfw/cmake/options/options_aarch64-android.cmake b/infra/nnfw/cmake/options/options_aarch64-android.cmake index e95ccca..242dcd3 100644 --- a/infra/nnfw/cmake/options/options_aarch64-android.cmake +++ b/infra/nnfw/cmake/options/options_aarch64-android.cmake @@ -1,14 +1,16 @@ # aarch64 android cmake options # -# NOTE BUILD_ANDROID_TFLITE(JNI lib) is disabled due to BuiltinOpResolver issue. -# tensorflow-lite does not build BuiltinOpResolver but JNI lib need it -# Related Issue : #1403 -option(BUILD_ANDROID_TFLITE "Enable android support for TensorFlow Lite" OFF) option(BUILD_ANDROID_BENCHMARK_APP "Enable Android Benchmark App" ON) option(DOWNLOAD_NEON2SSE "Download NEON2SSE library source" OFF) # Need boost library option(DOWNLOAD_BOOST "Download boost source" ON) option(BUILD_BOOST "Build boost source" ON) option(BUILD_LOGGING "Build logging runtime" OFF) -# Do not support npud -option(BUILD_NPUD "Build NPU daemon" OFF) + +option(DOWNLOAD_OPENGL_HEADERS "Download Opengl_headers source" ON) +option(DOWNLOAD_EGL_HEADERS "Download Egl_headers source" ON) +option(DOWNLOAD_VULKAN "Download vulkan source" ON) +option(DOWNLOAD_OPENCL_HEADERS "Download Opencl_headers source" ON) +option(DOWNLOAD_PYBIND11 "Download Pybind11 source" ON) +option(BUILD_GPU_CL "Build gpu_cl backend" ON) +option(BUILD_TENSORFLOW_LITE_GPU "Build TensorFlow Lite GPU delegate from the downloaded source" ON) diff --git a/infra/nnfw/cmake/options/options_aarch64-tizen.cmake b/infra/nnfw/cmake/options/options_aarch64-tizen.cmake index ed6e35e..daaa4c4 100644 --- a/infra/nnfw/cmake/options/options_aarch64-tizen.cmake +++ b/infra/nnfw/cmake/options/options_aarch64-tizen.cmake @@ -3,9 +3,14 @@ # option(BUILD_ARMCOMPUTE "Build ARM Compute from the downloaded source" OFF) option(BUILD_TENSORFLOW_LITE "Build TensorFlow Lite from the downloaded source" OFF) +option(DOWNLOAD_ARMCOMPUTE "Build ARM Compute from the downloaded source" OFF) option(DOWNLOAD_NEON2SSE "Download NEON2SSE library source" OFF) option(DOWNLOAD_GTEST "Download Google Test source and build Google Test" OFF) option(BUILD_LOGGING "Build logging runtime" OFF) option(GENERATE_RUNTIME_NNAPI_TESTS "Generate NNAPI operation gtest" OFF) option(ENVVAR_ONERT_CONFIG "Use environment variable for onert configuration" OFF) + +option(BUILD_NPUD "Build NPU daemon" ON) +# Do not allow to use CONFIG option on Tizen +option(ENVVAR_NPUD_CONFIG "Use environment variable for npud configuration" OFF) diff --git a/infra/nnfw/cmake/options/options_armv7hl-tizen.cmake b/infra/nnfw/cmake/options/options_armv7hl-tizen.cmake index aa2d2f8..e28801f 100644 --- a/infra/nnfw/cmake/options/options_armv7hl-tizen.cmake +++ b/infra/nnfw/cmake/options/options_armv7hl-tizen.cmake @@ -3,6 +3,7 @@ # option(BUILD_ARMCOMPUTE "Build ARM Compute from the downloaded source" OFF) option(BUILD_TENSORFLOW_LITE "Build TensorFlow Lite from the downloaded source" OFF) +option(DOWNLOAD_ARMCOMPUTE "Build ARM Compute from the downloaded source" OFF) option(DOWNLOAD_NEON2SSE "Download NEON2SSE library source" OFF) option(DOWNLOAD_GTEST "Download Google Test source and build Google Test" OFF) @@ -11,6 +12,13 @@ option(GENERATE_RUNTIME_NNAPI_TESTS "Generate NNAPI operation gtest" OFF) option(ENVVAR_ONERT_CONFIG "Use environment variable for onert configuration" OFF) option(DOWNLOAD_OPENCL_HEADERS "Download Opencl_headers source" ON) -option(DOWNLOAD_TENSORFLOW_GPU "Download Tensorflow GPU delegate source" ON) +option(DOWNLOAD_OPENGL_HEADERS "Download Opengl_headers source" ON) +option(DOWNLOAD_EGL_HEADERS "Download Egl_headers source" ON) +option(DOWNLOAD_VULKAN "Download vulkan source" ON) + option(BUILD_GPU_CL "Build gpu_cl backend" ON) option(BUILD_TENSORFLOW_LITE_GPU "Build TensorFlow Lite GPU delegate from the downloaded source" ON) + +option(BUILD_NPUD "Build NPU daemon" ON) +# Do not allow to use CONFIG option on Tizen +option(ENVVAR_NPUD_CONFIG "Use environment variable for npud configuration" OFF) diff --git a/infra/nnfw/cmake/options/options_armv7l-linux.cmake b/infra/nnfw/cmake/options/options_armv7l-linux.cmake index 325e7cc..c73a2be 100644 --- a/infra/nnfw/cmake/options/options_armv7l-linux.cmake +++ b/infra/nnfw/cmake/options/options_armv7l-linux.cmake @@ -4,7 +4,9 @@ option(DOWNLOAD_NEON2SSE "Download NEON2SSE library source" OFF) option(BUILD_OPENCL_TOOL "Build OpenCL tool" ON) +option(DOWNLOAD_OPENGL_HEADERS "Download Opengl_headers source" ON) +option(DOWNLOAD_EGL_HEADERS "Download Egl_headers source" ON) +option(DOWNLOAD_VULKAN "Download vulkan source" ON) option(DOWNLOAD_OPENCL_HEADERS "Download Opencl_headers source" ON) -option(DOWNLOAD_TENSORFLOW_GPU "Download Tensorflow GPU delegate source" ON) option(BUILD_GPU_CL "Build gpu_cl backend" ON) option(BUILD_TENSORFLOW_LITE_GPU "Build TensorFlow Lite GPU delegate from the downloaded source" ON) diff --git a/infra/nnfw/cmake/options/options_armv7l-tizen.cmake b/infra/nnfw/cmake/options/options_armv7l-tizen.cmake index 9b487d9..c5f4a2c 100644 --- a/infra/nnfw/cmake/options/options_armv7l-tizen.cmake +++ b/infra/nnfw/cmake/options/options_armv7l-tizen.cmake @@ -3,15 +3,22 @@ # option(BUILD_ARMCOMPUTE "Build ARM Compute from the downloaded source" OFF) option(BUILD_TENSORFLOW_LITE "Build TensorFlow Lite from the downloaded source" OFF) +option(DOWNLOAD_ARMCOMPUTE "Build ARM Compute from the downloaded source" OFF) option(DOWNLOAD_NEON2SSE "Download NEON2SSE library source" OFF) option(DOWNLOAD_GTEST "Download Google Test source and build Google Test" OFF) option(BUILD_LOGGING "Build logging runtime" OFF) option(GENERATE_RUNTIME_NNAPI_TESTS "Generate NNAPI operation gtest" OFF) option(ENVVAR_ONERT_CONFIG "Use environment variable for onert configuration" OFF) -option(ENVVAR_NPUD_CONFIG "Use environment variable for npud configuration" OFF) option(DOWNLOAD_OPENCL_HEADERS "Download Opencl_headers source" ON) -option(DOWNLOAD_TENSORFLOW_GPU "Download Tensorflow GPU delegate source" ON) +option(DOWNLOAD_OPENGL_HEADERS "Download Opengl_headers source" ON) +option(DOWNLOAD_EGL_HEADERS "Download Egl_headers source" ON) +option(DOWNLOAD_VULKAN "Download vulkan source" ON) + option(BUILD_GPU_CL "Build gpu_cl backend" ON) option(BUILD_TENSORFLOW_LITE_GPU "Build TensorFlow Lite GPU delegate from the downloaded source" ON) + +option(BUILD_NPUD "Build NPU daemon" ON) +# Do not allow to use CONFIG option on Tizen +option(ENVVAR_NPUD_CONFIG "Use environment variable for npud configuration" OFF) diff --git a/infra/nnfw/cmake/options/options_x86_64-tizen.cmake b/infra/nnfw/cmake/options/options_x86_64-tizen.cmake index eea3722..24b4d95 100644 --- a/infra/nnfw/cmake/options/options_x86_64-tizen.cmake +++ b/infra/nnfw/cmake/options/options_x86_64-tizen.cmake @@ -12,3 +12,7 @@ option(ENVVAR_ONERT_CONFIG "Use environment variable for onert configuration" OF option(BUILD_XNNPACK "Build XNNPACK" OFF) option(DOWNLOAD_OPENCL_HEADERS "Download opencl headers" OFF) + +option(BUILD_NPUD "Build NPU daemon" ON) +# Do not allow to use CONFIG option on Tizen +option(ENVVAR_NPUD_CONFIG "Use environment variable for npud configuration" OFF) diff --git a/infra/nnfw/cmake/packages/ARMComputeConfig.cmake b/infra/nnfw/cmake/packages/ARMComputeConfig.cmake index f6a4efd..acc244a 100644 --- a/infra/nnfw/cmake/packages/ARMComputeConfig.cmake +++ b/infra/nnfw/cmake/packages/ARMComputeConfig.cmake @@ -11,14 +11,25 @@ function(_ARMCompute_Import) message(STATUS "Search acl in ${ARMCompute_LIB_SEARCH_PATHS}") - if(NOT INCLUDE_DIR) + # ARMCompute v21.02 moves some headers into "src/". + # And we cannot build armcompute-ex library without these headers. + # So we need to download and use source code if our build root doesn't have headers in "src/" (tizen's devel package includes these headers). + # TODO Don't use headers in "src/" + find_path(HEADER_SRC_DIR NAMES src/core/CL/ICLKernel.h PATHS ${ARMCompute_INCLUDE_SEARCH_PATHS}) + if(NOT INCLUDE_DIR OR NOT HEADER_SRC_DIR) nnas_find_package(ARMComputeSource QUIET) if (NOT ARMComputeSource_FOUND) set(ARMCompute_FOUND FALSE PARENT_SCOPE) return() endif() - set(INCLUDE_DIR ${ARMComputeSource_DIR} ${ARMComputeSource_DIR}/include) - endif(NOT INCLUDE_DIR) + + # Clean if INCLUDE_DIR is NOT_FOUND + if(NOT INCLUDE_DIR) + unset(INCLUDE_DIR) + endif(NOT INCLUDE_DIR) + + list(APPEND INCLUDE_DIR ${ARMComputeSource_DIR} ${ARMComputeSource_DIR}/include) + endif(NOT INCLUDE_DIR OR NOT HEADER_SRC_DIR) if(NOT CORE_LIBRARY) set(ARMCompute_FOUND FALSE PARENT_SCOPE) diff --git a/infra/nnfw/cmake/packages/EigenConfig.cmake b/infra/nnfw/cmake/packages/EigenConfig.cmake index e71830a..1537882 100644 --- a/infra/nnfw/cmake/packages/EigenConfig.cmake +++ b/infra/nnfw/cmake/packages/EigenConfig.cmake @@ -1,5 +1,5 @@ function(_Eigen_import) - nnas_find_package(TensorFlowEigenSource EXACT 2.3.0 QUIET) + nnas_find_package(TensorFlowEigenSource EXACT 2.8.0 QUIET) if(NOT TensorFlowEigenSource_FOUND) set(Eigen_FOUND FALSE PARENT_SCOPE) diff --git a/infra/nnfw/cmake/packages/GObject2.0Config.cmake b/infra/nnfw/cmake/packages/GObject2.0Config.cmake new file mode 100644 index 0000000..f1bfb3a --- /dev/null +++ b/infra/nnfw/cmake/packages/GObject2.0Config.cmake @@ -0,0 +1,30 @@ +function(_GOBJECT_2_0_import) + nnfw_find_package(GLib2.0 REQUIRED) + + find_library(GOBJECT_LIBRARIES + NAMES gobject-2.0) + + # The gobject-2.0 requires glib-2.0 and access the header file based on + # the glib-2.0 include directory. + set(GOBJECT_INCLUDE_DIRS ${GLIB2.0_INCLUDE_DIRS}) + + set(GOBJECT_FOUND TRUE) + + if(NOT GOBJECT_LIBRARIES) + set(GOBJECT_FOUND FALSE) + endif(NOT GOBJECT_LIBRARIES) + + if(NOT GOBJECT_INCLUDE_DIRS) + set(GOBJECT_FOUND FALSE) + endif(NOT GOBJECT_INCLUDE_DIRS) + + if(NOT GOBJECT_FOUND) + message(STATUS "Failed to find gobject-2.0") + endif(NOT GOBJECT_FOUND) + + set(GOBJECT2.0_FOUND ${GOBJECT_FOUND} PARENT_SCOPE) + set(GOBJECT2.0_INCLUDE_DIRS ${GOBJECT_INCLUDE_DIRS} PARENT_SCOPE) + set(GOBJECT2.0_LIBRARIES ${GOBJECT_LIBRARIES} PARENT_SCOPE) +endfunction(_GOBJECT_2_0_import) + +_GOBJECT_2_0_import() diff --git a/infra/nnfw/cmake/packages/GTestConfig.cmake b/infra/nnfw/cmake/packages/GTestConfig.cmake index ab2b39e..d0f7b18 100644 --- a/infra/nnfw/cmake/packages/GTestConfig.cmake +++ b/infra/nnfw/cmake/packages/GTestConfig.cmake @@ -44,7 +44,7 @@ if(${GTEST_FOUND} AND TARGET Threads::Threads) add_library(gmock INTERFACE) target_include_directories(gmock INTERFACE ${GMOCK_INCLUDE_DIR}) target_link_libraries(gmock INTERFACE ${GMOCK_LIBRARIES} Threads::Threads) - endif(GMOCK_LIBRARIES) + endif(GMOCK_LIBRARIES AND GMOCK_INCLUDE_DIR) endif(NOT TARGET gmock) if(NOT TARGET gmock_main) diff --git a/infra/nnfw/cmake/packages/Gio2.0Config.cmake b/infra/nnfw/cmake/packages/Gio2.0Config.cmake new file mode 100644 index 0000000..26d3607 --- /dev/null +++ b/infra/nnfw/cmake/packages/Gio2.0Config.cmake @@ -0,0 +1,32 @@ +function(_GIO_2_0_import) + nnfw_find_package(GLib2.0 REQUIRED) + nnfw_find_package(GObject2.0 REQUIRED) + + find_library(GIO_LIBRARIES + NAMES gio-2.0) + + # The gio-2.0 requires glib-2.0 and access the header file based on + # the glib-2.0 include directory. + set(GIO_INCLUDE_DIRS ${GLIB2.0_INCLUDE_DIRS} ${GOBJECT2.0_INCLUDE_DIRS}) + set(GIO_LIBRARIES ${GIO_LIBRARIES} ${GOBJECT2.0_LIBRARIES}) + + set(GIO_FOUND TRUE) + + if(NOT GIO_LIBRARIES) + set(GIO_FOUND FALSE) + endif(NOT GIO_LIBRARIES) + + if(NOT GIO_INCLUDE_DIRS) + set(GIO_FOUND FALSE) + endif(NOT GIO_INCLUDE_DIRS) + + if(NOT GIO_FOUND) + message(STATUS "Failed to find gio-2.0") + endif(NOT GIO_FOUND) + + set(GIO2.0_FOUND ${GIO_FOUND} PARENT_SCOPE) + set(GIO2.0_INCLUDE_DIRS ${GIO_INCLUDE_DIRS} PARENT_SCOPE) + set(GIO2.0_LIBRARIES ${GIO_LIBRARIES} PARENT_SCOPE) +endfunction(_GIO_2_0_import) + +_GIO_2_0_import() diff --git a/infra/nnfw/cmake/packages/Giounix2.0Config.cmake b/infra/nnfw/cmake/packages/Giounix2.0Config.cmake new file mode 100644 index 0000000..05f1874 --- /dev/null +++ b/infra/nnfw/cmake/packages/Giounix2.0Config.cmake @@ -0,0 +1,30 @@ +function(_GIO_UNIX_2_0_import) + nnfw_find_package(Gio2.0 REQUIRED) + + find_path(GIO_UNIX_INCLUDE_DIR + NAMES gio/gunixfdlist.h + PATH_SUFFIXES gio-unix-2.0) + + # The gio-unix-2.0 requires gio-2.0 and link the gio-2.0 library. + set(GIO_UNIX_LIBRARIES ${GIO2.0_LIBRARIES}) + + set(GIO_UNIX_FOUND TRUE) + + if(NOT GIO_UNIX_LIBRARIES) + set(GIO_UNIX_FOUND FALSE) + endif(NOT GIO_UNIX_LIBRARIES) + + if(NOT GIO_UNIX_INCLUDE_DIR) + set(GIO_UNIX_FOUND FALSE) + endif(NOT GIO_UNIX_INCLUDE_DIR) + + if(NOT GIO_UNIX_FOUND) + message(STATUS "Failed to find gio-unix-2.0") + endif(NOT GIO_UNIX_FOUND) + + set(GIO_UNIX_2.0_FOUND ${GIO_UNIX_FOUND} PARENT_SCOPE) + set(GIO_UNIX_2.0_INCLUDE_DIRS ${GIO_UNIX_INCLUDE_DIR} PARENT_SCOPE) + set(GIO_UNIX_2.0_LIBRARIES ${GIO_UNIX_LIBRARIES} PARENT_SCOPE) +endfunction(_GIO_UNIX_2_0_import) + +_GIO_UNIX_2_0_import() diff --git a/infra/nnfw/cmake/packages/TensorFlowEigen-1.13.1/TensorFlowEigenConfig.cmake b/infra/nnfw/cmake/packages/TensorFlowEigen-1.13.1/TensorFlowEigenConfig.cmake deleted file mode 100644 index 253b290..0000000 --- a/infra/nnfw/cmake/packages/TensorFlowEigen-1.13.1/TensorFlowEigenConfig.cmake +++ /dev/null @@ -1,19 +0,0 @@ -function(_Eigen_import) - nnas_find_package(EigenSource QUIET) - - if(NOT EigenSource_FOUND) - set(TensorFlowEigen_1_13_1_FOUND FALSE PARENT_SCOPE) - return() - endif(NOT EigenSource_FOUND) - - if(NOT TARGET eigen-tf-1.13.1) - add_library(eigen-tf-1.13.1 INTERFACE) - target_include_directories(eigen-tf-1.13.1 SYSTEM INTERFACE "${EigenSource_DIR}") - # Add EIGEN_MPL2_ONLY to remove license issue posibility - target_compile_definitions(eigen-tf-1.13.1 INTERFACE EIGEN_MPL2_ONLY) - endif(NOT TARGET eigen-tf-1.13.1) - - set(TensorFlowEigen_1_13_1_FOUND TRUE PARENT_SCOPE) -endfunction(_Eigen_import) - -_Eigen_import() diff --git a/infra/nnfw/cmake/packages/TensorFlowEigen-1.13.1/TensorFlowEigenConfigVersion.cmake b/infra/nnfw/cmake/packages/TensorFlowEigen-1.13.1/TensorFlowEigenConfigVersion.cmake deleted file mode 100644 index ed79ecd..0000000 --- a/infra/nnfw/cmake/packages/TensorFlowEigen-1.13.1/TensorFlowEigenConfigVersion.cmake +++ /dev/null @@ -1,9 +0,0 @@ -set(PACKAGE_VERSION "1.13.1") -set(PACKAGE_VERSION_EXACT FALSE) -set(PACKAGE_VERSION_COMPATIBLE FALSE) -set(PACKAGE_VERSION_UNSUITABLE TRUE) - -if(PACKAGE_FIND_VERSION VERSION_EQUAL PACKAGE_VERSION) - set(PACKAGE_VERSION_EXACT TRUE) - set(PACKAGE_VERSION_UNSUITABLE FALSE) -endif(PACKAGE_FIND_VERSION VERSION_EQUAL PACKAGE_VERSION) diff --git a/infra/nnfw/cmake/packages/TensorFlowGpuConfig.cmake b/infra/nnfw/cmake/packages/TensorFlowGpuConfig.cmake new file mode 100644 index 0000000..5d20dd3 --- /dev/null +++ b/infra/nnfw/cmake/packages/TensorFlowGpuConfig.cmake @@ -0,0 +1,51 @@ +# TensorFlowGpuConfig.cmake +macro(return_unless VAR) +if(NOT ${VAR}) + message("TensorFlowGpu: ${VAR} NOT TRUE") + set(TensorFlowGpu_FOUND FALSE PARENT_SCOPE) + return() +endif(NOT ${VAR}) +endmacro(return_unless) + +function(_Build_TfliteGpuDelagate_) + nnas_find_package(TensorFlowSource EXACT 2.8.0 QUIET) + return_unless(TensorFlowSource_FOUND) + + nnas_find_package(TensorFlowGEMMLowpSource EXACT 2.8.0 QUIET) + return_unless(TensorFlowGEMMLowpSource_FOUND) + + nnas_find_package(TensorFlowEigenSource EXACT 2.8.0 QUIET) + return_unless(TensorFlowEigenSource_FOUND) + + nnas_find_package(AbseilSource REQUIRED) + return_unless(AbseilSource_FOUND) + + nnas_find_package(Farmhash REQUIRED) + return_unless(Farmhash_FOUND) + + nnas_find_package(Fp16Source REQUIRED) + return_unless(Fp16Source_FOUND) + + nnas_find_package(VulkanSource QUIET) + return_unless(VulkanSource_FOUND) + + nnas_find_package(Opengl_HeadersSource QUIET) + return_unless(Opengl_HeadersSource_FOUND) + + nnas_find_package(Egl_HeadersSource QUIET) + return_unless(Egl_HeadersSource_FOUND) + + if(NOT TARGET TensorFlowGpu) + nnas_include(ExternalProjectTools) + add_extdirectory("${CMAKE_CURRENT_LIST_DIR}/TensorFlowLiteGpu" TensorFlowLiteGpu) + endif() + set(TensorFlowSource_DIR ${TensorFlowSource_DIR} PARENT_SCOPE) + set(TensorFlowGpu_DIR ${TensorFlowGpu_DIR} PARENT_SCOPE) +endfunction(_Build_TfliteGpuDelagate_) + +if(BUILD_TENSORFLOW_LITE_GPU) + _Build_TfliteGpuDelagate_() + set(TensorFlowGpu_FOUND TRUE PARENT_SCOPE) +else(BUILD_TENSORFLOW_LITE_GPU) + set(TensorFlowGpu_FOUND FALSE PARENT_SCOPE) +endif(BUILD_TENSORFLOW_LITE_GPU) diff --git a/infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLite/CMakeLists.txt b/infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLite/CMakeLists.txt deleted file mode 100644 index f872b88..0000000 --- a/infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLite/CMakeLists.txt +++ /dev/null @@ -1,64 +0,0 @@ -set(TENSORFLOW_LITE_BASE ${TensorFlowSource_DIR}/tensorflow/lite) - -# -# Tensorflow Lite library -# -file(GLOB TFLITE_CORE_SRCS "${TENSORFLOW_LITE_BASE}/*.c" "${TENSORFLOW_LITE_BASE}/*.cc" "${TENSORFLOW_LITE_BASE}/core/*.cc") -file(GLOB TFLITE_CORE_TESTS "${TENSORFLOW_LITE_BASE}/*test*.cc") -list(REMOVE_ITEM TFLITE_CORE_SRCS ${TFLITE_CORE_TESTS}) - -file(GLOB_RECURSE TFLITE_KERNEL_SRCS "${TENSORFLOW_LITE_BASE}/kernels/*.cc") -file(GLOB_RECURSE TFLITE_KERNEL_TESTS "${TENSORFLOW_LITE_BASE}/kernels/*test*.cc") -list(REMOVE_ITEM TFLITE_KERNEL_SRCS ${TFLITE_KERNEL_TESTS}) - -file(GLOB TFLITE_LIB_SRCS "${TENSORFLOW_LITE_BASE}/c/*.c" "${TENSORFLOW_LITE_BASE}/c/*.cc") -file(GLOB TFLITE_LIB_TESTS "${TENSORFLOW_LITE_BASE}/c/*test*.cc") -list(REMOVE_ITEM TFLITE_LIB_SRCS ${TFLITE_LIB_TESTS}) - -file(GLOB TFLITE_API_SRCS "${TENSORFLOW_LITE_BASE}/core/api/*.c" "${TENSORFLOW_LITE_BASE}/core/api/*.cc") -file(GLOB TFLITE_API_TESTS "${TENSORFLOW_LITE_BASE}/core/api/*test*.cc") -list(REMOVE_ITEM TFLITE_API_SRCS ${TFLITE_API_TESTS}) - -file(GLOB TFLITE_PROFILING_SRCS "${TENSORFLOW_LITE_BASE}/profiling/*.cc") -file(GLOB TFLITE_PROFILING_TESTS "${TENSORFLOW_LITE_BASE}/profiling/*test*.cc") -list(REMOVE_ITEM TFLITE_PROFILING_SRCS ${TFLITE_PROFILING_TESTS}) - -# We will use our own summarizer -list(REMOVE_ITEM TFLITE_PROFILING_SRCS "${TENSORFLOW_LITE_BASE}/profiling/profile_summarizer.cc") -list(APPEND TFLITE_SRCS ${TFLITE_CORE_SRCS}) -list(APPEND TFLITE_SRCS ${TFLITE_KERNEL_SRCS}) -list(APPEND TFLITE_SRCS ${TFLITE_LIB_SRCS}) -list(APPEND TFLITE_SRCS ${TFLITE_API_SRCS}) -list(APPEND TFLITE_SRCS ${TFLITE_PROFILING_SRCS}) - -list(APPEND TFLITE_SRCS "${FarmhashSource_DIR}/src/farmhash.cc") - -# externals for spectrogram -list(APPEND TFLITE_SRCS "${OouraFFTSource_DIR}/fftsg.c") -list(APPEND TFLITE_SRCS "${OouraFFTSource_DIR}/fftsg2d.c") - -list(APPEND TFLITE_INCLUDES "${TensorFlowSource_DIR}") -list(APPEND TFLITE_INCLUDES "${AbseilSource_DIR}") -list(APPEND TFLITE_INCLUDES "${GEMMLowpSource_DIR}") -list(APPEND TFLITE_INCLUDES "${FarmhashSource_DIR}/src") - -if(NEON2SSESource_FOUND) - list(APPEND TFLITE_INCLUDES "${NEON2SSESource_DIR}") -endif(NEON2SSESource_FOUND) - -add_library(tensorflow-lite STATIC ${TFLITE_SRCS}) -target_include_directories(tensorflow-lite SYSTEM PUBLIC ${TFLITE_INCLUDES}) -target_compile_definitions(tensorflow-lite PUBLIC "GEMMLOWP_ALLOW_SLOW_SCALAR_FALLBACK") -set_property(TARGET tensorflow-lite PROPERTY POSITION_INDEPENDENT_CODE ON) -target_link_libraries(tensorflow-lite eigen-tf-1.13.1 flatbuffers::flatbuffers ${LIB_PTHREAD} dl) - -# Define TF_LITE_DISABLE_X86_NEON for debug build -# If we upgrade NEON2SSE version, we can remove below line -if(NEON2SSESource_FOUND) - target_compile_definitions(tensorflow-lite PRIVATE $<$:TF_LITE_DISABLE_X86_NEON>) -endif(NEON2SSESource_FOUND) - -if(ANDROID) - target_link_libraries(tensorflow-lite log) - target_include_directories(tensorflow-lite PUBLIC "${NDK_DIR}/..") -endif() diff --git a/infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLiteConfig.cmake b/infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLiteConfig.cmake deleted file mode 100644 index b03d593..0000000 --- a/infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLiteConfig.cmake +++ /dev/null @@ -1,79 +0,0 @@ -# NOTE This line prevents multiple definitions of tensorflow-lite target -if(TARGET tensorflow-lite) - set(TensorFlowLite_FOUND TRUE) - return() -endif(TARGET tensorflow-lite) - -if(BUILD_TENSORFLOW_LITE) - macro(return_unless VAR) - if(NOT ${VAR}) - set(TensorFlowLite_FOUND PARENT_SCOPE) - return() - endif(NOT ${VAR}) - endmacro(return_unless) - - # Required packages - nnas_find_package(Abseil QUIET) - return_unless(Abseil_FOUND) - nnfw_find_package(TensorFlowEigen EXACT 1.13.1 QUIET) - return_unless(TensorFlowEigen_1_13_1_FOUND) - nnas_find_package(FarmhashSource QUIET) - return_unless(FarmhashSource_FOUND) - nnfw_find_package(FlatBuffers QUIET) - return_unless(FlatBuffers_FOUND) - nnas_find_package(GEMMLowpSource QUIET) - return_unless(GEMMLowpSource_FOUND) - nnas_find_package(TensorFlowSource EXACT 1.13.1 QUIET) - return_unless(TensorFlowSource_FOUND) - nnas_find_package(OouraFFTSource QUIET) - return_unless(OouraFFTSource_FOUND) - - # Optional packages - nnas_find_package(NEON2SSESource QUIET) - - nnas_include(ExternalProjectTools) - add_extdirectory("${CMAKE_CURRENT_LIST_DIR}/TensorFlowLite" tflite) - - set(TensorFlowLite_FOUND TRUE) - return() -endif(BUILD_TENSORFLOW_LITE) - -# Use pre-built TensorFlow Lite -find_path(TFLITE_INCLUDE_DIR NAMES tensorflow/lite/interpreter.h) -find_library(TFLITE_LIB NAMES tensorflow-lite) - -if(NOT TFLITE_INCLUDE_DIR) - # Tizen install TensorFlow Lite 1.13.1 headers in /usr/include/tensorflow1 - find_path(TFLITE_INCLUDE_DIR NAMES tensorflow/lite/interpreter.h PATHS "/usr/include/tensorflow1") - if(NOT TFLITE_INCLUDE_DIR) - set(TensorFlowLite_FOUND FALSE) - return() - endif(NOT TFLITE_INCLUDE_DIR) -endif(NOT TFLITE_INCLUDE_DIR) - -if(NOT TFLITE_LIB) - set(TensorFlowLite_FOUND FALSE) - return() -endif(NOT TFLITE_LIB) - -message(STATUS "Found TensorFlow Lite: TRUE (include: ${TFLITE_INCLUDE_DIR}, lib: ${TFLITE_LIB}") - -# TODO Use IMPORTED target -add_library(tensorflow-lite INTERFACE) -target_include_directories(tensorflow-lite SYSTEM INTERFACE ${TFLITE_INCLUDE_DIR}) -target_link_libraries(tensorflow-lite INTERFACE ${TFLITE_LIB}) -find_package(Flatbuffers) -if(Flatbuffers_FOUND) - target_link_libraries(tensorflow-lite INTERFACE flatbuffers::flatbuffers) -endif(Flatbuffers_FOUND) - -# Prefer -pthread to -lpthread -set(THREADS_PREFER_PTHREAD_FLAG TRUE) -set(CMAKE_THREAD_PREFER_PTHREAD TRUE) -find_package(Threads QUIET) - -if(Threads_FOUND) - target_link_libraries(tensorflow-lite INTERFACE ${CMAKE_THREAD_LIBS_INIT}) -endif(Threads_FOUND) - -set(TensorFlowLite_FOUND TRUE) diff --git a/infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLiteConfigVersion.cmake b/infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLiteConfigVersion.cmake deleted file mode 100644 index ed79ecd..0000000 --- a/infra/nnfw/cmake/packages/TensorFlowLite-1.13.1/TensorFlowLiteConfigVersion.cmake +++ /dev/null @@ -1,9 +0,0 @@ -set(PACKAGE_VERSION "1.13.1") -set(PACKAGE_VERSION_EXACT FALSE) -set(PACKAGE_VERSION_COMPATIBLE FALSE) -set(PACKAGE_VERSION_UNSUITABLE TRUE) - -if(PACKAGE_FIND_VERSION VERSION_EQUAL PACKAGE_VERSION) - set(PACKAGE_VERSION_EXACT TRUE) - set(PACKAGE_VERSION_UNSUITABLE FALSE) -endif(PACKAGE_FIND_VERSION VERSION_EQUAL PACKAGE_VERSION) diff --git a/infra/nnfw/cmake/packages/TensorFlowLite-2.8.0/TensorFlowLite/CMakeLists.txt b/infra/nnfw/cmake/packages/TensorFlowLite-2.8.0/TensorFlowLite/CMakeLists.txt index d7e1d06..cbc10d2 100644 --- a/infra/nnfw/cmake/packages/TensorFlowLite-2.8.0/TensorFlowLite/CMakeLists.txt +++ b/infra/nnfw/cmake/packages/TensorFlowLite-2.8.0/TensorFlowLite/CMakeLists.txt @@ -1,111 +1,175 @@ -# Reference: https://github.com/tensorflow/tensorflow/blob/v2.3.0/tensorflow/lite/tools/make/Makefile +# Reference: https://github.com/tensorflow/tensorflow/blob/v2.8.0/tensorflow/lite/CMakeLists.txt # -# Tensorflow Lite library 2.3.0 +# Tensorflow Lite library 2.8.0 # -set(TENSORFLOW_LITE_BASE ${TensorFlowSource_DIR}/tensorflow/lite) - -file(GLOB TFLITE_CORE_SRCS "${TENSORFLOW_LITE_BASE}/*.c" - "${TENSORFLOW_LITE_BASE}/*.cc" - "${TENSORFLOW_LITE_BASE}/core/*.cc") - -file(GLOB_RECURSE TFLITE_KERNEL_SRCS "${TENSORFLOW_LITE_BASE}/kernels/*.cc") - -file(GLOB TFLITE_LIB_SRCS "${TENSORFLOW_LITE_BASE}/c/*.c" "${TENSORFLOW_LITE_BASE}/c/*.cc") - -file(GLOB TFLITE_API_SRCS "${TENSORFLOW_LITE_BASE}/core/api/*.c" - "${TENSORFLOW_LITE_BASE}/core/api/*.cc") - -list(APPEND TFLITE_PROFILING_SRCS "${TENSORFLOW_LITE_BASE}/profiling/memory_info.cc") -list(APPEND TFLITE_PROFILING_SRCS "${TENSORFLOW_LITE_BASE}/profiling/time.cc") -list(APPEND TFLITE_PROFILING_SRCS "${TENSORFLOW_LITE_BASE}/profiling/platform_profiler.cc") - -file(GLOB TFLITE_EXPERIMENTAL_SRCS "${TENSORFLOW_LITE_BASE}/experimental/resource/*.cc") - -file(GLOB TFLITE_SCHEMA_UTIL_SRCS "${TENSORFLOW_LITE_BASE}/schema/*.cc") - -# Moved to kerenls/internal/utils -#file(GLOB TFLITE_SPARSITY_SRCS "${TENSORFLOW_LITE_BASE}/tools/optimize/sparsity/*.cc") - -list(APPEND TFLITE_SRCS ${TFLITE_CORE_SRCS}) -list(APPEND TFLITE_SRCS ${TFLITE_KERNEL_SRCS}) -list(APPEND TFLITE_SRCS ${TFLITE_LIB_SRCS}) -list(APPEND TFLITE_SRCS ${TFLITE_API_SRCS}) -list(APPEND TFLITE_SRCS ${TFLITE_PROFILING_SRCS}) -list(APPEND TFLITE_SRCS ${TFLITE_EXPERIMENTAL_SRCS}) -#list(APPEND TFLITE_SRCS ${TFLITE_SPARSITY_SRCS}) -list(APPEND TFLITE_SRCS ${TFLITE_SCHEMA_UTIL_SRCS}) - -# externals -list(APPEND TFLITE_SRCS "${OouraFFTSource_DIR}/fftsg.c") -list(APPEND TFLITE_SRCS "${OouraFFTSource_DIR}/fftsg2d.c") - -# Build with mmap? true -# caution: v2.3.0's Makefile has wrong code on this part. This is fixed on master branch. -set(BUILD_WITH_MMAP TRUE) -if(${BUILD_WITH_MMAP}) - list(REMOVE_ITEM TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/mmap_allocation_disabled.cc") -else() - list(REMOVE_ITEM TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/mmap_allocation.cc") +set(TFLITE_SOURCE_DIR ${TensorFlowSource_DIR}/tensorflow/lite) + +# Generate TensorFlow Lite FlatBuffer code. +# We used to have an actual compilation logic with flatc but decided to use +# schema_generated.h since flatc doesn't work with cross compilation. +set(TFLITE_FLATBUFFERS_SCHEMA_DIR "${TFLITE_SOURCE_DIR}/schema") + +macro(populate_source_vars SOURCE_DIR SOURCES_VAR) + cmake_parse_arguments(ARGS "RECURSE" "" "FILTER" ${ARGN}) + if(ARGS_RECURSE) + set(GLOB_OP GLOB_RECURSE) + else() + set(GLOB_OP GLOB) + endif() + set(DEFAULT_FILE_FILTER ".*(_test|test_util)\\.(c|cc|h)$") + file(${GLOB_OP} FOUND_SOURCES "${SOURCE_DIR}/*.*") + list(FILTER FOUND_SOURCES INCLUDE REGEX ".*\\.(c|cc|h)$") + list(FILTER FOUND_SOURCES EXCLUDE REGEX "${DEFAULT_FILE_FILTER}") + foreach(FILE_FILTER ${ARGS_FILTER}) + list(FILTER FOUND_SOURCES EXCLUDE REGEX "${FILE_FILTER}") + endforeach() + list(APPEND ${SOURCES_VAR} ${FOUND_SOURCES}) +endmacro() +# Simplifies inclusion of non-test sources and headers from a directory +# relative to TFLITE_SOURCE_DIR. See populate_source_vars() for the +# description of arguments including and following SOURCES_VAR. +macro(populate_tflite_source_vars RELATIVE_DIR SOURCES_VAR) + populate_source_vars( + "${TFLITE_SOURCE_DIR}/${RELATIVE_DIR}" ${SOURCES_VAR} ${ARGN} + ) +endmacro() + +# Build a list of source files to compile into the TF Lite library. +populate_tflite_source_vars("." TFLITE_SRCS) + +# This particular file is excluded because the more explicit approach to enable +# XNNPACK delegate is preferred to the weak-symbol one. +list(FILTER TFLITE_SRCS EXCLUDE REGEX ".*tflite_with_xnnpack\\.cc$") + +# Exclude Flex related files. +list(FILTER TFLITE_SRCS EXCLUDE REGEX ".*with_selected_ops\\.cc$") + +# Use MMAP +list(FILTER TFLITE_SRCS EXCLUDE REGEX ".*mmap_allocation_disabled\\.cc$") + +if(NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "Android") + list(FILTER TFLITE_SRCS EXCLUDE REGEX ".*minimal_logging_android\\.cc$") endif() - -# Build with nnapi? true -# caution: this nnapi delegate comes from tflite, not ours. -set(BUILD_WITH_NNAPI TRUE) -if(${BUILD_WITH_NNAPI}) - list(APPEND TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/delegates/nnapi/nnapi_delegate.cc") - list(APPEND TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/delegates/nnapi/quant_lstm_sup.cc") - list(APPEND TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/delegates/utils.cc") - list(APPEND TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/delegates/serialization.cc") - list(APPEND TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/nnapi/nnapi_implementation.cc") - list(APPEND TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/nnapi/nnapi_util.cc") -else() - list(APPEND TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/delegates/nnapi/nnapi_delegate_disabled.cc") - list(APPEND TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/nnapi/nnapi_implementation_disabled.cc") +if(NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "iOS") + list(FILTER TFLITE_SRCS EXCLUDE REGEX ".*minimal_logging_ios\\.cc$") endif() -# ios: we don't support ios -list(REMOVE_ITEM TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/minimal_logging_ios.cc") - -# android -if(NOT ANDROID) - list(REMOVE_ITEM TFLITE_SRCS "${TENSORFLOW_LITE_BASE}/minimal_logging_android.cc") +populate_tflite_source_vars("core" TFLITE_CORE_SRCS) +populate_tflite_source_vars("core/api" TFLITE_CORE_API_SRCS) +populate_tflite_source_vars("c" TFLITE_C_SRCS) +populate_tflite_source_vars("delegates" TFLITE_DELEGATES_SRCS) + +# Enable NNAPI +populate_tflite_source_vars("delegates/nnapi" +TFLITE_DELEGATES_NNAPI_SRCS +FILTER "(_test_list|_disabled)\\.(cc|h)$" +) +populate_tflite_source_vars( +"nnapi" TFLITE_NNAPI_SRCS FILTER "(_disabled)\\.(cc|h)$" +) + +# Disable XNNPack + +# Enable experimental support for resource (need for build success) +populate_tflite_source_vars("experimental/resource" +TFLITE_EXPERIMENTAL_RESOURCE_SRCS +) + +# Enable Ruy +populate_tflite_source_vars("experimental/ruy" + TFLITE_EXPERIMENTAL_RUY_SRCS + FILTER + ".*(test(_fast|_slow|_special_specs))\\.(cc|h)$" + ".*(benchmark|tune_tool|example)\\.(cc|h)$" +) +populate_tflite_source_vars("experimental/ruy/profiler" + TFLITE_EXPERIMENTAL_RUY_PROFILER_SRCS + FILTER ".*(test|test_instrumented_library)\\.(cc|h)$" +) +list(APPEND TFLITE_TARGET_PUBLIC_OPTIONS "-DTFLITE_WITH_RUY") + +populate_tflite_source_vars("kernels" + TFLITE_KERNEL_SRCS + FILTER "(.*_test_util_internal|test_.*|.*_ops_wrapper)\\.(cc|h)" +) +populate_tflite_source_vars("kernels/internal" TFLITE_KERNEL_INTERNAL_SRCS) +populate_tflite_source_vars("kernels/internal/optimized" + TFLITE_KERNEL_INTERNAL_OPT_SRCS +) +populate_tflite_source_vars("kernels/internal/optimized/integer_ops" + TFLITE_KERNEL_INTERNAL_OPT_INTEGER_OPS_SRCS +) +populate_tflite_source_vars("kernels/internal/optimized/sparse_ops" + TFLITE_KERNEL_INTERNAL_OPT_SPARSE_OPS_SRCS +) +populate_tflite_source_vars("kernels/internal/reference" + TFLITE_KERNEL_INTERNAL_REF_SRCS +) +populate_tflite_source_vars("kernels/internal/reference/integer_ops" + TFLITE_KERNEL_INTERNAL_REF_INTEGER_OPS_SRCS +) +populate_tflite_source_vars("kernels/internal/reference/sparse_ops" + TFLITE_KERNEL_INTERNAL_REF_SPARSE_OPS_SRCS +) +set(TFLITE_PROFILER_SRCS ${TFLITE_SOURCE_DIR}/profiling/platform_profiler.cc) +if(CMAKE_SYSTEM_NAME MATCHES "Android") + list(APPEND TFLITE_PROFILER_SRCS + ${TFLITE_SOURCE_DIR}/profiling/atrace_profiler.cc + ) endif() -# exclude some source files -file(GLOB_RECURSE TFLITE_EXCLS "${TENSORFLOW_LITE_BASE}/*test*.cc" - "${TENSORFLOW_LITE_BASE}/*benchmark*.cc" - "${TENSORFLOW_LITE_BASE}/*example*.cc" - "${TENSORFLOW_LITE_BASE}/*tool*.cc") -list(REMOVE_ITEM TFLITE_SRCS ${TFLITE_EXCLS}) - -# exclude some kernels (requires python3-dev package) -# TODO Enable these kernels by installing package on build system -file(GLOB_RECURSE TFLITE_KERNEL_EXCLS "${TENSORFLOW_LITE_BASE}/kernels/variable_ops_wrapper.cc" - "${TENSORFLOW_LITE_BASE}/kernels/gradient/*.cc" - "${TENSORFLOW_LITE_BASE}/kernels/perception/*.cc") -list(REMOVE_ITEM TFLITE_SRCS ${TFLITE_KERNEL_EXCLS}) - -# exclude kernel shim -file(GLOB_RECURSE TFLITE_SHIM_EXCLS "${TENSORFLOW_LITE_BASE}/kernels/shim/*.cc") -list(REMOVE_ITEM TFLITE_SRCS ${TFLITE_SHIM_EXCLS}) +# Common include directories +set(TFLITE_INCLUDE_DIRS + "${TENSORFLOW_SOURCE_DIR}" + "${TFLITE_FLATBUFFERS_SCHEMA_DIR}" +) # include headers -list(APPEND TFLITE_INCLUDES "${TensorFlowSource_DIR}") -list(APPEND TFLITE_INCLUDES "${TensorFlowGEMMLowpSource_DIR}") -list(APPEND TFLITE_INCLUDES "${Fp16Source_DIR}/include") -#list(APPEND TFLITE_INCLUDES "${Pybind11Source_DIR}/include") +list(APPEND TFLITE_INCLUDE_DIRS "${TensorFlowSource_DIR}") +list(APPEND TFLITE_INCLUDE_DIRS "${TensorFlowGEMMLowpSource_DIR}") +list(APPEND TFLITE_INCLUDE_DIRS "${Fp16Source_DIR}/include") +#list(APPEND TFLITE_INCLUDE_DIRS "${Pybind11Source_DIR}/include") +list(APPEND TFLITE_INCLUDE_DIRS "${CpuInfoSource_DIR}") if(NEON2SSESource_FOUND) - list(APPEND TFLITE_INCLUDES "${NEON2SSESource_DIR}") + list(APPEND TFLITE_INCLUDE_DIRS "${NEON2SSESource_DIR}") endif(NEON2SSESource_FOUND) -add_library(tensorflow-lite-2.8.0 STATIC ${TFLITE_SRCS}) -target_include_directories(tensorflow-lite-2.8.0 SYSTEM PUBLIC ${TFLITE_INCLUDES}) -target_include_directories(tensorflow-lite-2.8.0 PRIVATE ${CpuInfoSource_DIR}) -target_compile_definitions(tensorflow-lite-2.8.0 PUBLIC "GEMMLOWP_ALLOW_SLOW_SCALAR_FALLBACK -DTFLITE_WITH_RUY -DTFLITE_WITH_RUY_GEMV -DRUY_HAVE_CPUINFO") +# TFLite library +add_library(tensorflow-lite-2.8.0 STATIC + ${TFLITE_CORE_API_SRCS} + ${TFLITE_CORE_SRCS} + ${TFLITE_C_SRCS} + ${TFLITE_DELEGATES_NNAPI_SRCS} + ${TFLITE_DELEGATES_SRCS} + ${TFLITE_EXPERIMENTAL_RESOURCE_SRCS} + ${TFLITE_EXPERIMENTAL_RUY_PROFILER_SRCS} + ${TFLITE_EXPERIMENTAL_RUY_SRCS} + ${TFLITE_KERNEL_INTERNAL_OPT_INTEGER_OPS_SRCS} + ${TFLITE_KERNEL_INTERNAL_OPT_SPARSE_OPS_SRCS} + ${TFLITE_KERNEL_INTERNAL_OPT_SRCS} + ${TFLITE_KERNEL_INTERNAL_REF_INTEGER_OPS_SRCS} + ${TFLITE_KERNEL_INTERNAL_REF_SPARSE_OPS_SRCS} + ${TFLITE_KERNEL_INTERNAL_REF_SRCS} + ${TFLITE_KERNEL_INTERNAL_SRCS} + ${TFLITE_KERNEL_SRCS} + ${TFLITE_NNAPI_SRCS} + ${TFLITE_SRCS} + ${TFLITE_PROFILER_SRCS} + ${TFLITE_SOURCE_DIR}/kernels/internal/utils/sparsity_format_converter.cc + ${TFLITE_SOURCE_DIR}/schema/schema_utils.cc + ${OouraFFTSource_DIR}/fftsg.c + ${OouraFFTSource_DIR}/fftsg2d.c +) +target_include_directories(tensorflow-lite-2.8.0 + SYSTEM PUBLIC + ${TFLITE_INCLUDE_DIRS} +) + +target_compile_definitions(tensorflow-lite-2.8.0 PUBLIC "GEMMLOWP_ALLOW_SLOW_SCALAR_FALLBACK -DTFLITE_WITH_RUY -DTFLITE_WITH_RUY_GEMV -DRUY_HAVE_CPUINFO -DNNAPI_VERBOSE_VALIDATION") set_property(TARGET tensorflow-lite-2.8.0 PROPERTY POSITION_INDEPENDENT_CODE ON) target_link_libraries(tensorflow-lite-2.8.0 eigen flatbuffers::flatbuffers ruy abseil farmhash ${LIB_PTHREAD} dl) -if(NOT ANDROID AND ${BUILD_WITH_NNAPI}) +if(NOT ANDROID) target_link_libraries(tensorflow-lite-2.8.0 rt) endif() @@ -117,5 +181,5 @@ endif(NEON2SSESource_FOUND) if(ANDROID) target_link_libraries(tensorflow-lite-2.8.0 log) - target_include_directories(tensorflow-lite-2.8.0 PUBLIC "${NDK_DIR}/..") + #target_include_directories(tensorflow-lite-2.8.0 PUBLIC "${NDK_DIR}/..") endif() diff --git a/infra/nnfw/cmake/packages/TensorFlowLite-2.8.0/TensorFlowLiteConfig.cmake b/infra/nnfw/cmake/packages/TensorFlowLite-2.8.0/TensorFlowLiteConfig.cmake index 1c80618..60f7f54 100644 --- a/infra/nnfw/cmake/packages/TensorFlowLite-2.8.0/TensorFlowLiteConfig.cmake +++ b/infra/nnfw/cmake/packages/TensorFlowLite-2.8.0/TensorFlowLiteConfig.cmake @@ -1,8 +1,14 @@ -if(BUILD_TENSORFLOW_LITE_2_8_0) +# NOTE This line prevents multiple definitions of tensorflow-lite target +if(TARGET tensorflow-lite-2.8.0) + set(TensorFlowLite_FOUND TRUE) + return() +endif(TARGET tensorflow-lite-2.8.0) + +if(BUILD_TENSORFLOW_LITE) macro(return_unless VAR) if(NOT ${VAR}) message("TFLite 2.8: ${VAR} NOT TRUE") - set(TensorFlowLite_2_8_0_FOUND FALSE PARENT_SCOPE) + set(TensorFlowLite_FOUND FALSE) return() endif(NOT ${VAR}) endmacro(return_unless) @@ -13,9 +19,9 @@ if(BUILD_TENSORFLOW_LITE_2_8_0) nnas_find_package(TensorFlowSource EXACT 2.8.0 QUIET) return_unless(TensorFlowSource_FOUND) - # Below urls come from https://github.com/tensorflow/tensorflow/blob/v2.3.0/tensorflow/tensorflow/workspace.bzl - nnas_find_package(AbseilSource QUIET) - return_unless(AbseilSource_FOUND) + # Below urls come from https://github.com/tensorflow/tensorflow/blob/v2.8.0/tensorflow/workspace2.bzl + nnas_find_package(Abseil QUIET) + return_unless(Abseil_FOUND) nnfw_find_package(Eigen QUIET) return_unless(Eigen_FOUND) nnas_find_package(Farmhash QUIET) @@ -45,6 +51,46 @@ if(BUILD_TENSORFLOW_LITE_2_8_0) nnas_include(ExternalProjectTools) add_extdirectory("${CMAKE_CURRENT_LIST_DIR}/TensorFlowLite" tflite-2.8.0) - set(TensorFlowLite_2_8_0_FOUND TRUE) + set(TensorFlowLite_FOUND TRUE) return() endif() + +# Use pre-built TensorFlow Lite +find_path(TFLITE_INCLUDE_DIR NAMES tensorflow/lite/c/c_api.h) +find_library(TFLITE_LIB NAMES tensorflow2-lite) + +if(NOT TFLITE_INCLUDE_DIR) + # Tizen install TensorFlow Lite 2.8 headers in /usr/include/tensorflow2 + find_path(TFLITE_INCLUDE_DIR NAMES tensorflow/lite/c/c_api.h PATHS "/usr/include/tensorflow2") + if(NOT TFLITE_INCLUDE_DIR) + set(TensorFlowLite_FOUND FALSE) + return() + endif(NOT TFLITE_INCLUDE_DIR) +endif(NOT TFLITE_INCLUDE_DIR) + +if(NOT TFLITE_LIB) + set(TensorFlowLite_FOUND FALSE) + return() +endif(NOT TFLITE_LIB) + +message(STATUS "Found TensorFlow Lite: TRUE (include: ${TFLITE_INCLUDE_DIR}, lib: ${TFLITE_LIB}") + +# TODO Use IMPORTED target +add_library(tensorflow-lite-2.8.0 INTERFACE) +target_include_directories(tensorflow-lite-2.8.0 SYSTEM INTERFACE ${TFLITE_INCLUDE_DIR}) +target_link_libraries(tensorflow-lite-2.8.0 INTERFACE ${TFLITE_LIB}) +find_package(Flatbuffers) +if(Flatbuffers_FOUND) + target_link_libraries(tensorflow-lite-2.8.0 INTERFACE flatbuffers::flatbuffers) +endif(Flatbuffers_FOUND) + +# Prefer -pthread to -lpthread +set(THREADS_PREFER_PTHREAD_FLAG TRUE) +set(CMAKE_THREAD_PREFER_PTHREAD TRUE) +find_package(Threads QUIET) + +if(Threads_FOUND) + target_link_libraries(tensorflow-lite-2.8.0 INTERFACE ${CMAKE_THREAD_LIBS_INIT}) +endif(Threads_FOUND) + +set(TensorFlowLite_FOUND TRUE) diff --git a/infra/nnfw/cmake/packages/TensorFlowLiteGpu/CMakeLists.txt b/infra/nnfw/cmake/packages/TensorFlowLiteGpu/CMakeLists.txt new file mode 100644 index 0000000..73264d1 --- /dev/null +++ b/infra/nnfw/cmake/packages/TensorFlowLiteGpu/CMakeLists.txt @@ -0,0 +1,73 @@ +# +# Tensorflow Lite GPU delegate library 2.8.0 +# + +set(LIB_TENSORFLOW_GPU_DELEGATE "TensorFlowGpu") + +#TENSORFLOWGPU_SOURCE_DIR +set(TENSORFLOWSOURCE_DIR ${TensorFlowSource_DIR}) +set(TENSORFLOW_LITE_BASE ${TENSORFLOWSOURCE_DIR}/tensorflow/lite) +set(REF_TENSORFLOW_LITE_GPU_DELEGATE_SRC_BASE "${TENSORFLOW_LITE_BASE}/delegates/gpu") + +set(SRC_BASE "${REF_TENSORFLOW_LITE_GPU_DELEGATE_SRC_BASE}") +file(GLOB GPU_CL_SRC_LIST "${SRC_BASE}/cl/*.cc" + "${SRC_BASE}/cl/kernels/*.cc" + "${SRC_BASE}/common/*.cc" + "${SRC_BASE}/common/selectors/*.cc" + "${SRC_BASE}/common/selectors/default/*.cc" + "${SRC_BASE}/common/task/*.cc" + "${SRC_BASE}/common/tasks/*.cc" + "${SRC_BASE}/common/tasks/special/*.cc" + "${SRC_BASE}/common/memory_management/*.cc" + "${SRC_BASE}/common/transformations/*.cc" + ) + +file(GLOB REMOVE_TEST_SRCS "${SRC_BASE}/cl/*_test*.cc" + "${SRC_BASE}/cl/testing/*.cc" + "${SRC_BASE}/cl/kernels/*_test*.cc" + "${SRC_BASE}/common/*_test*.cc" + "${SRC_BASE}/common/tasks/*_test*.cc" + "${SRC_BASE}/common/transformations/*_test*.cc" + ) +# Not available +file(GLOB REMOVE_SRCS "${SRC_BASE}/cl/*gl*.cc" + "${SRC_BASE}/cl/gpu_api_delegate.cc" + "${SRC_BASE}/cl/serialization.cc" + "${SRC_BASE}/common/lstm_parser.cc" + "${SRC_BASE}/common/model_builder.cc" + "${SRC_BASE}/common/model_builder_helper.cc" + "${SRC_BASE}/common/object_reader.cc" + "${SRC_BASE}/common/quantization_util.cc" + "${SRC_BASE}/common/memory_management/*_test.cc" + ) + +list(APPEND GPU_CL_SRC_LIST "${TENSORFLOW_LITE_BASE}/experimental/acceleration/compatibility/android_info.cc") + +list(REMOVE_ITEM GPU_CL_SRC_LIST ${REMOVE_TEST_SRCS}) +list(REMOVE_ITEM GPU_CL_SRC_LIST ${REMOVE_SRCS}) +list(APPEND TFLITE_GPU_SRCS ${GPU_CL_SRC_LIST}) + +add_library(${LIB_TENSORFLOW_GPU_DELEGATE} STATIC ${TFLITE_GPU_SRCS}) + +target_include_directories(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "${Opencl_Headers_DIR}") +target_include_directories(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "${Fp16Source_DIR}/include") +target_include_directories(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "${TensorFlowSource_DIR}") +target_include_directories(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "${TensorFlowGEMMLowpSource_DIR}") +target_include_directories(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "${TensorFlowEigenSource_DIR}") +target_include_directories(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "${VulkanSource_DIR}/include") +target_include_directories(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "${Opengl_HeadersSource_DIR}/api") +target_include_directories(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "${Egl_HeadersSource_DIR}/api") + +target_link_libraries(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE abseil farmhash fp16 flatbuffers) + +# GL codes are not used on gpu_cl +target_compile_options(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "-DCL_DELEGATE_NO_GL") +target_compile_options(${LIB_TENSORFLOW_GPU_DELEGATE} PRIVATE "-DTFLITE_GPU_BINARY_RELEASE" "-DEGL_NO_X11") + +# deprecated-copy warning on header (gcc 9.4.0) +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 9.4) + target_compile_options(${LIB_TENSORFLOW_GPU_DELEGATE} PUBLIC "-Wno-deprecated-copy") +endif() + +# Applying PIC first, currently used on gpu_cl only +set_target_properties(${LIB_TENSORFLOW_GPU_DELEGATE} PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/infra/nnfw/command/prepare-model b/infra/nnfw/command/prepare-model new file mode 100644 index 0000000..35600e1 --- /dev/null +++ b/infra/nnfw/command/prepare-model @@ -0,0 +1,18 @@ +#!/bin/bash + +import "build.configuration" + +# This command is used to download test materials on host environment +# by using test command on host + +# Common variables +DRIVER_PATH=$NNFW_PROJECT_PATH/tests/scripts +CACHE_PATH=${CACHE_PATH:-$WORKSPACE_PATH/out/test/cache} + +COMMAND_FILE=$DRIVER_PATH/command/prepare-model +if [[ ! -f $COMMAND_FILE ]]; then + echo "ERROR: '$COMMAND' is not supported" + exit 255 +fi + +source $COMMAND_FILE $@ diff --git a/infra/nnfw/config/gbs.conf b/infra/nnfw/config/gbs.conf index 2b5994a..23278bb 100644 --- a/infra/nnfw/config/gbs.conf +++ b/infra/nnfw/config/gbs.conf @@ -3,11 +3,10 @@ profile = profile.tizen [profile.tizen] -repos = repo.tizen_base,repo.tizen_mobile -buildroot = /home/GBS-ROOT/ +repos = repo.base, repo.unified -[repo.tizen_mobile] -url = http://download.tizen.org/snapshots/tizen/unified/latest/repos/standard/packages/ +[repo.unified] +url = http://download.tizen.org/snapshots/TIZEN/Tizen-7.0/Tizen-7.0-Unified/latest/repos/standard/packages/ -[repo.tizen_base] -url = http://download.tizen.org/snapshots/tizen/base/latest/repos/standard/packages/ +[repo.base] +url = http://download.tizen.org/snapshots/TIZEN/Tizen-7.0/Tizen-7.0-Base/latest/repos/standard/packages/ diff --git a/infra/onert-micro/CMakeLists.txt b/infra/onert-micro/CMakeLists.txt new file mode 100644 index 0000000..21533c1 --- /dev/null +++ b/infra/onert-micro/CMakeLists.txt @@ -0,0 +1,61 @@ +cmake_minimum_required(VERSION 3.15) + +project(onert-micro) + +enable_testing() + +set(CMAKE_CXX_STANDARD 14) + +set(CMAKE_SKIP_BUILD_RPATH FALSE) +set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) +set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib:$ORIGIN/") +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if (NOT DEFINED TARGET_ARCH) + set(TARGET_ARCH "armv7em") +endif() + +if (NOT DEFINED TARGET_CPU) + set(TARGET_CPU "cortex-m7") +endif() + +if (NOT DEFINED TARGET_OS) + set(TARGET_OS "generic") +endif() + +include(utils.cmake) + +nnas_find_package(GTest QUIET) + +option(ENABLE_TEST "Build Tests using Google Test" ${GTest_FOUND}) + +if(${ENABLE_TEST} AND NOT ${GTest_FOUND}) + message(FATAL_ERROR "Google Test is required to enable test") +endif(${ENABLE_TEST} AND NOT ${GTest_FOUND}) + +option(ENABLE_COVERAGE "Build for coverage test" OFF) +if(${ENABLE_COVERAGE} AND NOT ${ENABLE_TEST}) + message(FATAL_ERROR "Test should be enabled to measure test coverage") +endif(${ENABLE_COVERAGE} AND NOT ${ENABLE_TEST}) + +if(${ENABLE_TEST}) + include(CTest) +endif(${ENABLE_TEST}) + +### +### Target +### +add_library(onert_micro_common INTERFACE) +if(ENABLE_STRICT_BUILD) + target_compile_options(onert_micro_common INTERFACE -Werror -Wall -Wextra -Wno-reorder) +endif(ENABLE_STRICT_BUILD) + +add_library(onert_micro_coverage INTERFACE) +if(ENABLE_COVERAGE) + target_compile_options(onert_micro_coverage INTERFACE -g -O0 -fprofile-arcs -ftest-coverage) + target_link_libraries(onert_micro_coverage INTERFACE gcov) +endif(ENABLE_COVERAGE) + +add_subdirectory("${NNAS_PROJECT_SOURCE_DIR}/onert-micro" "${CMAKE_BINARY_DIR}/onert-micro") diff --git a/infra/onert-micro/cmake/ApplyCompileFlags.cmake b/infra/onert-micro/cmake/ApplyCompileFlags.cmake new file mode 100644 index 0000000..fb99fbd --- /dev/null +++ b/infra/onert-micro/cmake/ApplyCompileFlags.cmake @@ -0,0 +1,35 @@ +# +# Platform independent compile flag setting +# +# flags for build type: debug, release +set(CMAKE_C_FLAGS_DEBUG "-O0 -g -DDEBUG") +set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g -DDEBUG") +set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG") +set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG") + +# +# Platform specific compile flag setting +# +if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/buildtool/config/config_${TARGET_PLATFORM}.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/buildtool/config/config_${TARGET_PLATFORM}.cmake") +endif() + +# +# Apply compile flags +# note: this should be placed after cmake/buildtool/config/config_xxx.cmake files +# +# add common flags +foreach(FLAG ${FLAGS_COMMON}) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${FLAG}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${FLAG}") +endforeach() + +# add c flags +foreach(FLAG ${FLAGS_CONLY}) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${FLAG}") +endforeach() + +# add cxx flags +foreach(FLAG ${FLAGS_CXXONLY}) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${FLAG}") +endforeach() diff --git a/infra/onert-micro/cmake/CfgOptionFlags.cmake b/infra/onert-micro/cmake/CfgOptionFlags.cmake new file mode 100644 index 0000000..ffbc7b2 --- /dev/null +++ b/infra/onert-micro/cmake/CfgOptionFlags.cmake @@ -0,0 +1,18 @@ +# Platform specific configuration +# note: this should be placed before default setting for option setting priority +# (platform specific setting have higher priority) +# +include("${NNAS_PROJECT_SOURCE_DIR}/infra/onert-micro/cmake/options/options_${TARGET_PLATFORM}.cmake") + +### +### Configuration +### +option(DOWNLOAD_RUY "Download ruy source" ON) +option(DOWNLOAD_EIGEN "Download Eigen source" ON) +option(DOWNLOAD_GEMMLOWP "Download GEMM low precesion library source" ON) +option(DOWNLOAD_FLATBUFFERS "Download FlatBuffers source" ON) +option(BUILD_FLATBUFFERS "Locally build Flatbuffers from the downloaded source" ON) +option(DOWNLOAD_TENSORFLOW "Download TensorFlow source" ON) + +option(DOWNLOAD_GTEST "Download Google Test source" ON) +option(BUILD_GTEST "Build Google Test from the downloaded source" ON) diff --git a/infra/onert-micro/cmake/buildtool/config/arm-none-eabi-gcc.cmake b/infra/onert-micro/cmake/buildtool/config/arm-none-eabi-gcc.cmake new file mode 100644 index 0000000..544be03 --- /dev/null +++ b/infra/onert-micro/cmake/buildtool/config/arm-none-eabi-gcc.cmake @@ -0,0 +1,66 @@ +set(CMAKE_SYSTEM_NAME Generic) + +set(CMAKE_SYSTEM_PROCESSOR "${CPU_ARCH}") +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +set(CMAKE_C_COMPILER "${C_COMPILER}") +set(CMAKE_CXX_COMPILER "${CXX_COMPILER}") +set(CMAKE_ASM_COMPILER "${ASM_COMPILER}") +set(CMAKE_OBJCOPY "${OBJCOPY}") + +set(TARGET_CPU "cortex-m4" CACHE STRING "Target CPU") + +# Convert TARGET_CPU=Cortex-M33+nofp+nodsp into +# - CMAKE_SYSTEM_PROCESSOR=cortex-m33 +# - TARGET_CPU_FEATURES=no-fp;no-dsp +string(REPLACE "+" ";" TARGET_CPU_FEATURES ${TARGET_CPU}) +list(POP_FRONT TARGET_CPU_FEATURES CMAKE_SYSTEM_PROCESSOR) +string(TOLOWER ${CMAKE_SYSTEM_PROCESSOR} CMAKE_SYSTEM_PROCESSOR) + +set(CMAKE_EXECUTABLE_SUFFIX ".elf") +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +# Select C/C++ version +set(CMAKE_C_STANDARD 99) +set(CMAKE_CXX_STANDARD 14) + +# Compile options +add_compile_options( + -mcpu=${TARGET_CPU} + -mthumb + "$<$:-gdwarf-3>" + "$<$:-funwind-tables;-frtti;-fexceptions>") + +# Compile definescd +add_compile_definitions( + "$<$>:NDEBUG>") + +# Link options +add_link_options( + -mcpu=${TARGET_CPU} + -mthumb + --specs=nosys.specs) + +# Set floating point unit +if("${TARGET_CPU}" MATCHES "\\+fp") + set(FLOAT hard) +elseif("${TARGET_CPU}" MATCHES "\\+nofp") + set(FLOAT soft) +elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "cortex-m33" OR + "${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "cortex-m55") + set(FLOAT hard) +else() + set(FLOAT soft) +endif() + +if (FLOAT) + add_compile_options(-mfloat-abi=${FLOAT}) + add_link_options(-mfloat-abi=${FLOAT}) +endif() + +# Compilation warnings +add_compile_options( + -Wno-all +) diff --git a/infra/onert-micro/cmake/buildtool/config/config_linux.cmake b/infra/onert-micro/cmake/buildtool/config/config_linux.cmake new file mode 100644 index 0000000..d7b17cf --- /dev/null +++ b/infra/onert-micro/cmake/buildtool/config/config_linux.cmake @@ -0,0 +1,11 @@ +# +# linux common compile options +# + +# Disable annoying ABI compatibility warning. +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 7.0) + list(APPEND FLAGS_CXXONLY "-Wno-psabi") +endif() + +# lib pthread as a variable (pthread must be disabled on android) +set(LIB_PTHREAD pthread) diff --git a/infra/onert-micro/cmake/buildtool/config/config_x86_64-linux.cmake b/infra/onert-micro/cmake/buildtool/config/config_x86_64-linux.cmake new file mode 100644 index 0000000..528e483 --- /dev/null +++ b/infra/onert-micro/cmake/buildtool/config/config_x86_64-linux.cmake @@ -0,0 +1,12 @@ +# +# x86_64 linux compile options +# +message(STATUS "Building for x86-64 Linux") + +# include linux common +include("cmake/buildtool/config/config_linux.cmake") + +# SIMD for x86 +set(FLAGS_COMMON ${FLAGS_COMMON} + "-msse4" + ) diff --git a/infra/onert-micro/cmake/options/options_armv7-r-generic.cmake b/infra/onert-micro/cmake/options/options_armv7-r-generic.cmake new file mode 100644 index 0000000..d671b73 --- /dev/null +++ b/infra/onert-micro/cmake/options/options_armv7-r-generic.cmake @@ -0,0 +1,3 @@ +# +# armv7em generic cmake options +# diff --git a/infra/onert-micro/cmake/options/options_armv7em-generic.cmake b/infra/onert-micro/cmake/options/options_armv7em-generic.cmake new file mode 100644 index 0000000..d671b73 --- /dev/null +++ b/infra/onert-micro/cmake/options/options_armv7em-generic.cmake @@ -0,0 +1,3 @@ +# +# armv7em generic cmake options +# diff --git a/infra/onert-micro/cmake/options/options_armv8-m-generic.cmake b/infra/onert-micro/cmake/options/options_armv8-m-generic.cmake new file mode 100644 index 0000000..cbd70de --- /dev/null +++ b/infra/onert-micro/cmake/options/options_armv8-m-generic.cmake @@ -0,0 +1,3 @@ +# +# armv8-m generic cmake options +# diff --git a/infra/onert-micro/cmake/options/options_x86_64-linux.cmake b/infra/onert-micro/cmake/options/options_x86_64-linux.cmake new file mode 100644 index 0000000..0fb72f1 --- /dev/null +++ b/infra/onert-micro/cmake/options/options_x86_64-linux.cmake @@ -0,0 +1,3 @@ +# +# x86_64 linux cmake options +# diff --git a/infra/onert-micro/utils.cmake b/infra/onert-micro/utils.cmake new file mode 100644 index 0000000..4c78e2c --- /dev/null +++ b/infra/onert-micro/utils.cmake @@ -0,0 +1,53 @@ +set(NNAS_PROJECT_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../.." CACHE + INTERNAL "Where to find nnas top-level source directory" + ) + +set(NNAS_EXTERNALS_DIR + "${NNAS_PROJECT_SOURCE_DIR}/externals" CACHE + INTERNAL "Where to download external dependencies" + ) +set(ONERT_MICRO_OVERLAY_DIR "${CMAKE_BINARY_DIR}/overlay" CACHE + INTERNAL "Where locally built external dependencies are installed") + +# Share package build script with runtime +set(EXT_OVERLAY_DIR ${ONERT_MICRO_OVERLAY_DIR}) + +# This allows find_package to access configurations installed inside overlay +list(APPEND CMAKE_PREFIX_PATH "${EXT_OVERLAY_DIR}") + +macro(nnas_include PREFIX) + include("${NNAS_PROJECT_SOURCE_DIR}/infra/cmake/modules/${PREFIX}.cmake") +endmacro(nnas_include) + +macro(nnas_find_package PREFIX) + find_package(${PREFIX} + CONFIG NO_DEFAULT_PATH + PATHS ${NNAS_PROJECT_SOURCE_DIR}/infra/cmake/packages + ${ARGN}) +endmacro(nnas_find_package) + +macro(nnas_find_package_folder PREFIX FIND_FOLDER) + find_package(${PREFIX} + CONFIG NO_DEFAULT_PATH + PATHS ${NNAS_PROJECT_SOURCE_DIR}/infra/cmake/packages ${FIND_FOLDER} + ${ARGN}) +endmacro(nnas_find_package_folder) + +### +### CMake configuration +### +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Type of build" FORCE) +endif(NOT CMAKE_BUILD_TYPE) +message(STATUS "Use '${CMAKE_BUILD_TYPE}' configuration") + +# identify platform: HOST_PLATFORM, TARGET_PLATFORM and related +# note: this should be placed before flags and options setting +nnas_include(IdentifyPlatform) + +# Configuration flags +include("${NNAS_PROJECT_SOURCE_DIR}/infra/onert-micro/cmake/CfgOptionFlags.cmake") + +# apply compilation flags +# NOTE this should be after all option +include("${NNAS_PROJECT_SOURCE_DIR}/infra/onert-micro/cmake/ApplyCompileFlags.cmake") diff --git a/infra/packaging/build b/infra/packaging/build index 5d6bdd9..0c1370f 100644 --- a/infra/packaging/build +++ b/infra/packaging/build @@ -8,7 +8,7 @@ if [[ -z "${NNAS_PROJECT_PATH}" ]]; then fi # The default preset -PRESET="20220323" +PRESET="20221125" # Test is enabled by default DISABLE_TEST=false diff --git a/infra/packaging/preset/20220323 b/infra/packaging/preset/20220323 index 0eac106..69251d0 100644 --- a/infra/packaging/preset/20220323 +++ b/infra/packaging/preset/20220323 @@ -35,6 +35,8 @@ function preset_configure() REQUIRED_UNITS+=("circle-partitioner" "circle-operator") REQUIRED_UNITS+=("one-cmds") REQUIRED_UNITS+=("bcq-tools") + REQUIRED_UNITS+=("dalgona") + REQUIRED_UNITS+=("visq") # Dependent modules needed for build REQUIRED_UNITS+=("circlechef") diff --git a/infra/packaging/preset/20220323_windows b/infra/packaging/preset/20220323_windows index 14917b3..c5a3f0e 100644 --- a/infra/packaging/preset/20220323_windows +++ b/infra/packaging/preset/20220323_windows @@ -30,6 +30,8 @@ function preset_configure() REQUIRED_UNITS+=("circle-partitioner" "circle-operator") REQUIRED_UNITS+=("one-cmds") REQUIRED_UNITS+=("bcq-tools") + REQUIRED_UNITS+=("dalgona") + REQUIRED_UNITS+=("visq") # Dependent modules needed for build REQUIRED_UNITS+=("circlechef") diff --git a/infra/packaging/preset/20221125 b/infra/packaging/preset/20221125 new file mode 100644 index 0000000..d798087 --- /dev/null +++ b/infra/packaging/preset/20221125 @@ -0,0 +1,66 @@ +#!/bin/bash + +# NOTE purpose of this file is static analysis only +# new official preset will be added when new programs are ready + +PRESET="20221125" + +function preset_configure() +{ + REQUIRED_UNITS=() + # Common Libraries + REQUIRED_UNITS+=("angkor" "cwrap" "pepper-str" "pepper-strcast" "pp") + REQUIRED_UNITS+=("oops" "pepper-assert" "pepper-csv2vec" "foder" "crew") + REQUIRED_UNITS+=("souschef") + REQUIRED_UNITS+=("safemain") + REQUIRED_UNITS+=("arser") + REQUIRED_UNITS+=("vconone") + # Hermes Logging Framework + REQUIRED_UNITS+=("hermes" "hermes-std") + # loco IR and related utilities + REQUIRED_UNITS+=("loco" "locop" "locomotiv" "logo-core" "logo") + # Flatbuffer I/O + REQUIRED_UNITS+=("mio-tflite280" "mio-circle04") + # Data I/O + REQUIRED_UNITS+=("dio-hdf5") + # Compute + REQUIRED_UNITS+=("luci-compute") + # Circle compiler library (.circle -> .circle) + REQUIRED_UNITS+=("luci") + # Python interface for circle schema + REQUIRED_UNITS+=("pics") + # Tools + REQUIRED_UNITS+=("tflite2circle" "circle2circle" "tflchef") + REQUIRED_UNITS+=("circle-tensordump" "circledump") + REQUIRED_UNITS+=("tf2tfliteV2" "luci-interpreter") + REQUIRED_UNITS+=("luci-eval-driver") + REQUIRED_UNITS+=("record-minmax" "circle-quantizer" "rawdata2hdf5") + REQUIRED_UNITS+=("circle-eval-diff" "circle-interpreter") + REQUIRED_UNITS+=("circle-partitioner" "circle-operator") + REQUIRED_UNITS+=("one-cmds") + REQUIRED_UNITS+=("bcq-tools") + REQUIRED_UNITS+=("dalgona") + REQUIRED_UNITS+=("visq") + REQUIRED_UNITS+=("circle-opselector") + + # Dependent modules needed for build + REQUIRED_UNITS+=("circlechef") + REQUIRED_UNITS+=("circle-verify") + + NPROC=${NPROC:-$(cat /proc/cpuinfo | grep -c processor)} + + # TODO Use "nncc configure" and "nncc build" + cmake \ + -DCMAKE_INSTALL_PREFIX="${NNCC_INSTALL_PREFIX}" \ + -DCMAKE_BUILD_TYPE=release \ + -DBUILD_WHITELIST=$(join_by ";" "${REQUIRED_UNITS[@]}") \ + -DEXTERNALS_BUILD_THREADS=$((NPROC/2)) \ + ${EXTRA_OPTIONS[@]} \ + "${NNAS_PROJECT_PATH}/infra/nncc" +} + +function preset_install() +{ + # Install tf2nnpkg + install -T -m 755 -D "${SCRIPT_PATH}/res/tf2nnpkg.${PRESET}" "${NNAS_INSTALL_PREFIX}/bin/tf2nnpkg" +} diff --git a/infra/packaging/preset/20221125_windows b/infra/packaging/preset/20221125_windows new file mode 100644 index 0000000..75c6426 --- /dev/null +++ b/infra/packaging/preset/20221125_windows @@ -0,0 +1,80 @@ +#!/bin/bash + +PRESET="20221125" + +function preset_configure() +{ + REQUIRED_UNITS=() + # Common Libraries + REQUIRED_UNITS+=("angkor" "cwrap" "pepper-str" "pepper-strcast" "pp") + REQUIRED_UNITS+=("oops" "pepper-assert" "pepper-csv2vec" "foder" "crew") + REQUIRED_UNITS+=("souschef") + REQUIRED_UNITS+=("safemain") + REQUIRED_UNITS+=("arser") + REQUIRED_UNITS+=("vconone") + # Hermes Logging Framework + REQUIRED_UNITS+=("hermes" "hermes-std") + # loco IR and related utilities + REQUIRED_UNITS+=("loco" "locop" "locomotiv" "logo-core" "logo") + # Flatbuffer I/O + REQUIRED_UNITS+=("mio-tflite280" "mio-circle04") + # Data I/O + REQUIRED_UNITS+=("dio-hdf5") + # Compute + REQUIRED_UNITS+=("luci-compute") + # Circle compiler library (.circle -> .circle) + REQUIRED_UNITS+=("luci") + # Python interface for circle schema + REQUIRED_UNITS+=("pics") + # Tools + REQUIRED_UNITS+=("tflite2circle" "circle2circle" "tflchef") + REQUIRED_UNITS+=("circle-tensordump" "circledump") + REQUIRED_UNITS+=("tf2tfliteV2" "luci-interpreter") + REQUIRED_UNITS+=("luci-eval-driver") + REQUIRED_UNITS+=("record-minmax" "circle-quantizer" "rawdata2hdf5") + REQUIRED_UNITS+=("circle-eval-diff" "circle-interpreter") + REQUIRED_UNITS+=("circle-partitioner" "circle-operator") + REQUIRED_UNITS+=("one-cmds") + REQUIRED_UNITS+=("bcq-tools") + REQUIRED_UNITS+=("dalgona") + REQUIRED_UNITS+=("visq") + + # Dependent modules needed for build + REQUIRED_UNITS+=("circlechef") + REQUIRED_UNITS+=("circle-verify") + + NPROC=$(cat /proc/cpuinfo | grep -c processor) + + # TODO Use "nncc configure" and "nncc build" + cmake \ + -G "MSYS Makefiles" \ + -DUSE_PROTOBUF_LEGACY_IMPORT=ON \ + -DCMAKE_EXE_LINKER_FLAGS="-Wl,--allow-multiple-definition" \ + -DCMAKE_SHARED_LINKER_FLAGS="-Wl,--allow-multiple-definition" \ + -DENABLE_TEST=OFF \ + -DDOWNLOAD_GTEST=OFF \ + -DBUILD_GTEST=OFF \ + -DCMAKE_C_COMPILER=gcc \ + -DCMAKE_CXX_COMPILER=g++ \ + -DCMAKE_INSTALL_PREFIX="${NNCC_INSTALL_PREFIX}" \ + -DCMAKE_BUILD_TYPE=release \ + -DBUILD_WHITELIST=$(join_by ";" "${REQUIRED_UNITS[@]}") \ + -DEXTERNALS_BUILD_THREADS=$((NPROC/2)) \ + ${EXTRA_OPTIONS[@]} \ + "${NNAS_PROJECT_PATH}/infra/nncc" +} + +function preset_install() +{ + # Install libraries to bin/ for Windows release + mv ${NNCC_INSTALL_PREFIX}/lib/*.dll ${NNCC_INSTALL_PREFIX}/bin + rm -rf ${NNCC_INSTALL_PREFIX}/lib + + # Install tf2nnpkg + install -T -m 755 -D "${SCRIPT_PATH}/res/tf2nnpkg.${PRESET}" "${NNAS_INSTALL_PREFIX}/bin/tf2nnpkg" + + # Though you have to install tensorflow to run 'tf2tfliteV2', + # tensorflow can't be installed in mingw. First, You can install tensorflow + # from Window native CMD(run as administrator) with python virtual environment. + # And, you must copy it to "${NNAS_INSTALL_PREFIX}/bin/venv" +} diff --git a/infra/packaging/res/tf2nnpkg.20221125 b/infra/packaging/res/tf2nnpkg.20221125 new file mode 100644 index 0000000..a7446e6 --- /dev/null +++ b/infra/packaging/res/tf2nnpkg.20221125 @@ -0,0 +1,109 @@ +#!/bin/bash + +set -e + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +command_exists() { + if [ "$#" -le 0 ]; then + return 1 + fi + command -v "$@" > /dev/null 2>&1 +} + +usage() +{ + echo "Convert TensorFlow model to nnpackage." + echo "Usage: tf2nnpkg" + echo " --info " + echo " --graphdef " + echo " -o " + echo " --v2 (optional) Use TF 2.x interface" + exit 255 +} + +TF_INTERFACE="--v1" + +# Parse command-line arguments +# +while [ "$#" -ne 0 ]; do + CUR="$1" + + case $CUR in + '--help') + usage + ;; + '--info') + export INFO_FILE="$2" + shift 2 + ;; + '--graphdef') + export GRAPHDEF_FILE="$2" + shift 2 + ;; + '-o') + export OUTPUT_DIR="$2" + shift 2 + ;; + '--v2') + TF_INTERFACE="--v2" + shift + ;; + *) + echo "${CUR}" + shift + ;; + esac +done + +if [ -z ${GRAPHDEF_FILE} ] || [ ! -e ${GRAPHDEF_FILE} ]; then + echo "pb is not found. Please check --graphdef is correct." + exit 2 +fi + +if [ -z ${INFO_FILE} ] || [ ! -e ${INFO_FILE} ]; then + echo "info is not found. Please check --info is correct." + exit 2 +fi + +if [ -z ${OUTPUT_DIR} ]; then + echo "output directory is not specifed. Please check -o is correct.." + exit 2 +fi + +FILE_BASE=$(basename ${GRAPHDEF_FILE}) +MODEL_NAME="${FILE_BASE%.*}" +TMPDIR=$(mktemp -d) +trap "{ rm -rf $TMPDIR; }" EXIT + +# activate python virtual environment +VIRTUALENV_LINUX="${ROOT}/bin/venv/bin/activate" +VIRTUALENV_WINDOWS="${ROOT}/bin/venv/Scripts/activate" + +if [ -e ${VIRTUALENV_LINUX} ]; then + source ${VIRTUALENV_LINUX} +elif [ -e ${VIRTUALENV_WINDOWS} ]; then + source ${VIRTUALENV_WINDOWS} +fi + +# parse inputs, outputs from info file +INPUT=$(awk -F, '/^input/ { print $2 }' ${INFO_FILE} | cut -d: -f1 | tr -d ' ' | paste -d, -s) +OUTPUT=$(awk -F, '/^output/ { print $2 }' ${INFO_FILE} | cut -d: -f1 | tr -d ' ' | paste -d, -s) + +INPUT_SHAPES=$(grep ^input ${INFO_FILE} | cut -d "[" -f2 | cut -d "]" -f1 | tr -d ' ' | xargs | tr ' ' ':') + +ONE_IMPORT_BCQ_SCRIPT="${ROOT}/bin/one-import-bcq ${TF_INTERFACE} " +ONE_IMPORT_BCQ_SCRIPT+="-i ${GRAPHDEF_FILE} " +ONE_IMPORT_BCQ_SCRIPT+="-o ${TMPDIR}/${MODEL_NAME}.tmp.circle " +ONE_IMPORT_BCQ_SCRIPT+="-I ${INPUT} " +ONE_IMPORT_BCQ_SCRIPT+="-O ${OUTPUT} " +if [ ! -z ${INPUT_SHAPES} ]; then + ONE_IMPORT_BCQ_SCRIPT+="-s ${INPUT_SHAPES} " +fi + +${ONE_IMPORT_BCQ_SCRIPT} + +# optimize +"${ROOT}/bin/circle2circle" --resolve_customop_add "${TMPDIR}/${MODEL_NAME}.tmp.circle" "${TMPDIR}/${MODEL_NAME}.circle" + +"${ROOT}/bin/model2nnpkg" -o "${OUTPUT_DIR}" -m "${TMPDIR}/${MODEL_NAME}.circle" diff --git a/infra/scripts/build_android_runtime_release.sh b/infra/scripts/build_android_runtime_release.sh deleted file mode 100755 index 99858eb..0000000 --- a/infra/scripts/build_android_runtime_release.sh +++ /dev/null @@ -1,15 +0,0 @@ -[[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo "Please don't source ${BASH_SOURCE[0]}, execute it" && return - -CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_PATH="$CURRENT_PATH/../../" - -# prepare ndk -if [ ! -n "$NDK_DIR" ]; then - export NDK_DIR=$ROOT_PATH/tools/cross/ndk/r20/ndk - echo "It will use default external path" -fi - -export TARGET_OS=android -export CROSS_BUILD=1 -export BUILD_TYPE=release -make -f Makefile.template install diff --git a/infra/scripts/common.sh b/infra/scripts/common.sh index 4a1385d..0beaf67 100755 --- a/infra/scripts/common.sh +++ b/infra/scripts/common.sh @@ -50,10 +50,10 @@ function TFLiteModelVerification() export BACKENDS=$1 if [[ "$2" == "" ]]; then - $INSTALL_PATH/test/onert-test verify-tflite --api=loader \ + $INSTALL_PATH/test/onert-test verify-tflite \ --reportdir=$ROOT_PATH/$3 else - $INSTALL_PATH/test/onert-test verify-tflite --api=loader \ + $INSTALL_PATH/test/onert-test verify-tflite \ --list=$2 \ --reportdir=$ROOT_PATH/$3 fi @@ -74,7 +74,7 @@ function NNAPIGTest() # Backup original nnapi_gtest.skip # TODO Pass skiplist to test-driver.sh - SKIPLIST_FILE="${INSTALL_PATH}/unittest/nnapi_gtest.skip" + SKIPLIST_FILE="${INSTALL_PATH}/nnapi-gtest/nnapi_gtest.skip" BACKUP_FILE="${SKIPLIST_FILE}.backup" if [[ "$2" != "" ]]; then cp ${SKIPLIST_FILE} ${BACKUP_FILE} @@ -84,7 +84,7 @@ function NNAPIGTest() export BACKENDS=$1 $INSTALL_PATH/test/onert-test unittest \ --reportdir=$ROOT_PATH/$3 \ - --unittestdir=$INSTALL_PATH/unittest + --unittestdir=$INSTALL_PATH/nnapi-gtest unset BACKENDS # TODO Pass skiplist to test-driver.sh @@ -129,27 +129,3 @@ function NNPackageTest() popd > /dev/null } - -# $1: (required) backend -# $2: (required) test list file relative path from nnfw root directory -# pass empty string if there is no skiplist -# $3: (required) relative path to report from nnfw root directory -function NNAPIFrontendTest() -{ - [[ $# -ne 3 ]] && echo "NNAPIFrontendTest: Invalid function argument setting" && exit 1 - - pushd ${ROOT_PATH} > /dev/null - - export BACKENDS=$1 - if [[ "$2" == "" ]]; then - $INSTALL_PATH/test/onert-test verify-tflite --api=nnapi \ - --reportdir=$ROOT_PATH/$3 - else - $INSTALL_PATH/test/onert-test verify-tflite --api=nnapi \ - --list=$2 \ - --reportdir=$ROOT_PATH/$3 - fi - unset BACKENDS - - popd > /dev/null -} diff --git a/infra/scripts/compiler_modules.sh b/infra/scripts/compiler_modules.sh index 51cba92..55e48f4 100644 --- a/infra/scripts/compiler_modules.sh +++ b/infra/scripts/compiler_modules.sh @@ -12,6 +12,7 @@ DEBUG_BUILD_ITEMS+=";hermes;hermes-std" DEBUG_BUILD_ITEMS+=";loco;locop;locomotiv;logo-core;logo" DEBUG_BUILD_ITEMS+=";foder;crew;souschef;arser;vconone" DEBUG_BUILD_ITEMS+=";safemain;mio-circle04;mio-tflite280;dio-hdf5" +DEBUG_BUILD_ITEMS+=";luci-compute" DEBUG_BUILD_ITEMS+=";tflite2circle" DEBUG_BUILD_ITEMS+=";luci" DEBUG_BUILD_ITEMS+=";luci-interpreter" @@ -20,14 +21,17 @@ DEBUG_BUILD_ITEMS+=";circle2circle;record-minmax;circle-quantizer" DEBUG_BUILD_ITEMS+=";circle-eval-diff" DEBUG_BUILD_ITEMS+=";circle-partitioner;circle-part-driver;circle-operator" DEBUG_BUILD_ITEMS+=";circle-verify" -DEBUG_BUILD_ITEMS+=";circle-tensordump" +DEBUG_BUILD_ITEMS+=";circle-tensordump;circle-opselector" DEBUG_BUILD_ITEMS+=";tflchef;circlechef" DEBUG_BUILD_ITEMS+=";common-artifacts" DEBUG_BUILD_ITEMS+=";circle2circle-dredd-recipe-test" DEBUG_BUILD_ITEMS+=";record-minmax-conversion-test" DEBUG_BUILD_ITEMS+=";tf2tfliteV2;tf2tfliteV2-conversion-test" DEBUG_BUILD_ITEMS+=";tflite2circle-conversion-test" -DEBUG_BUILD_ITEMS+=";pota-quantization-value-test" +DEBUG_BUILD_ITEMS+=";pota-quantization-value-test;pics" DEBUG_BUILD_ITEMS+=";circle-part-value-test" DEBUG_BUILD_ITEMS+=";circle-quantizer-dredd-recipe-test" DEBUG_BUILD_ITEMS+=";circle-operator-test" +DEBUG_BUILD_ITEMS+=";circle-interpreter;circle-interpreter-test" +DEBUG_BUILD_ITEMS+=";dalgona;dalgona-test" +DEBUG_BUILD_ITEMS+=";visq" diff --git a/infra/scripts/docker_build_cross_aarch64_runtime.sh b/infra/scripts/docker_build_cross_aarch64_runtime.sh deleted file mode 100755 index f73894f..0000000 --- a/infra/scripts/docker_build_cross_aarch64_runtime.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -[[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo "Please don't source ${BASH_SOURCE[0]}, execute it" && return - -CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_PATH="$CURRENT_PATH/../../" - -# prepare rootfs -if [ -z "$ROOTFS_DIR" ] || [ ! -d $ROOTFS_DIR ]; then - echo "It will use default rootfs path" -else - DOCKER_VOLUMES+=" -v $ROOTFS_DIR:/opt/rootfs" - DOCKER_ENV_VARS+=" -e ROOTFS_DIR=/opt/rootfs" -fi - -# mount volume (or directory) for externals -if [ -n "$EXTERNAL_VOLUME" ]; then - DOCKER_VOLUMES+=" -v $EXTERNAL_VOLUME:/externals" - DOCKER_ENV_VARS+=" -e EXTERNAL_VOLUME=/externals" -else - echo "It will use default external path" -fi - -# docker image name -# - for bionic, use DOCKER_IMAGE_NAME="nnfw/one-devtools:bionic" -# - for focal, use DOCKER_IMAGE_NAME="nnfw/one-devtools:focal" -if [[ -z $DOCKER_IMAGE_NAME ]]; then - echo "It will use default docker image name" -fi - -# Mirror server setting -if [[ -z $EXTERNAL_DOWNLOAD_SERVER ]]; then - echo "It will not use mirror server" -fi - -DOCKER_ENV_VARS+=" -e TARGET_ARCH=aarch64" -DOCKER_ENV_VARS+=" -e CROSS_BUILD=1" - -set -e - -pushd $ROOT_PATH > /dev/null - -# TODO use command instead of makefile -export DOCKER_ENV_VARS -export DOCKER_VOLUMES -CMD="cp -nv Makefile.template Makefile && \ - make all install build_test_suite" -./nnfw docker-run bash -c "$CMD" - -popd > /dev/null diff --git a/infra/scripts/docker_build_cross_arm_runtime.sh b/infra/scripts/docker_build_cross_arm_runtime.sh deleted file mode 100755 index 17d75de..0000000 --- a/infra/scripts/docker_build_cross_arm_runtime.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -[[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo "Please don't source ${BASH_SOURCE[0]}, execute it" && return - -CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_PATH="$CURRENT_PATH/../../" - -# prepare rootfs -if [ -z "$ROOTFS_DIR" ] || [ ! -d $ROOTFS_DIR ]; then - echo "It will use default rootfs path" -else - DOCKER_VOLUMES+=" -v $ROOTFS_DIR:/opt/rootfs" - DOCKER_ENV_VARS+=" -e ROOTFS_DIR=/opt/rootfs" -fi - -# mount volume (or directory) for externals -if [ -n "$EXTERNAL_VOLUME" ]; then - DOCKER_VOLUMES+=" -v $EXTERNAL_VOLUME:/externals" - DOCKER_ENV_VARS+=" -e EXTERNAL_VOLUME=/externals" -else - echo "It will use default external path" -fi - -# docker image name -# - for bionic, use DOCKER_IMAGE_NAME="nnfw/one-devtools:bionic" -# - for focal, use DOCKER_IMAGE_NAME="nnfw/one-devtools:focal" -if [[ -z $DOCKER_IMAGE_NAME ]]; then - echo "It will use default docker image name" -fi - -# Mirror server setting -if [[ -z $EXTERNAL_DOWNLOAD_SERVER ]]; then - echo "It will not use mirror server" -fi - -DOCKER_ENV_VARS+=" -e TARGET_ARCH=armv7l" -DOCKER_ENV_VARS+=" -e CROSS_BUILD=1" - -set -e - -pushd $ROOT_PATH > /dev/null - -# TODO use command instead of makefile -export DOCKER_ENV_VARS -export DOCKER_VOLUMES -CMD="cp -nv Makefile.template Makefile && \ - make all install build_test_suite" -./nnfw docker-run bash -c "$CMD" - -popd > /dev/null diff --git a/infra/scripts/docker_build_cross_arm_runtime_release.sh b/infra/scripts/docker_build_cross_arm_runtime_release.sh deleted file mode 100755 index 377bc3e..0000000 --- a/infra/scripts/docker_build_cross_arm_runtime_release.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -[[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo "Please don't source ${BASH_SOURCE[0]}, execute it" && return - -CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_PATH="$CURRENT_PATH/../../" - -# prepare rootfs -if [ -z "$ROOTFS_DIR" ] || [ ! -d $ROOTFS_DIR ]; then - echo "It will use default rootfs path" -else - DOCKER_VOLUMES+=" -v $ROOTFS_DIR:/opt/rootfs" - DOCKER_ENV_VARS+=" -e ROOTFS_DIR=/opt/rootfs" -fi - -# mount volume (or directory) for externals -if [ -n "$EXTERNAL_VOLUME" ]; then - DOCKER_VOLUMES+=" -v $EXTERNAL_VOLUME:/externals" - DOCKER_ENV_VARS+=" -e EXTERNAL_VOLUME=/externals" -else - echo "It will use default external path" -fi - -# docker image name -# - for bionic, use DOCKER_IMAGE_NAME="nnfw/one-devtools:bionic" -# - for focal, use DOCKER_IMAGE_NAME="nnfw/one-devtools:focal" -if [[ -z $DOCKER_IMAGE_NAME ]]; then - echo "It will use default docker image name" -fi - -# Mirror server setting -if [[ -z $EXTERNAL_DOWNLOAD_SERVER ]]; then - echo "It will not use mirror server" -fi - -DOCKER_ENV_VARS+=" -e TARGET_ARCH=armv7l" -DOCKER_ENV_VARS+=" -e CROSS_BUILD=1" -DOCKER_ENV_VARS+=" -e BUILD_TYPE=release" - -set -e - -pushd $ROOT_PATH > /dev/null - -# TODO use command instead of makefile -export DOCKER_ENV_VARS -export DOCKER_VOLUMES -CMD="cp -nv Makefile.template Makefile && \ - make all install build_test_suite" -./nnfw docker-run bash -c "$CMD" - -popd > /dev/null diff --git a/infra/scripts/docker_build_cross_coverage.sh b/infra/scripts/docker_build_cross_coverage.sh deleted file mode 100755 index 454bf27..0000000 --- a/infra/scripts/docker_build_cross_coverage.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -[[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo "Please don't source ${BASH_SOURCE[0]}, execute it" && return - -CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_PATH="$CURRENT_PATH/../../" - -# prepare rootfs -if [ -z "$ROOTFS_DIR" ] || [ ! -d $ROOTFS_DIR ]; then - echo "It will use default rootfs path" -else - DOCKER_VOLUMES+=" -v $ROOTFS_DIR:/opt/rootfs" - DOCKER_ENV_VARS+=" -e ROOTFS_DIR=/opt/rootfs" -fi - -# mount volume (or directory) for externals -if [ -n "$EXTERNAL_VOLUME" ]; then - DOCKER_VOLUMES+=" -v $EXTERNAL_VOLUME:/externals" - DOCKER_ENV_VARS+=" -e EXTERNAL_VOLUME=/externals" -else - echo "It will use default external path" -fi - -# docker image name -# - for bionic, use DOCKER_IMAGE_NAME="nnfw/one-devtools:bionic" -# - for focal, use DOCKER_IMAGE_NAME="nnfw/one-devtools:focal" -if [[ -z $DOCKER_IMAGE_NAME ]]; then - echo "It will use default docker image name" -fi - -# Mirror server setting -if [[ -z $EXTERNAL_DOWNLOAD_SERVER ]]; then - echo "It will not use mirror server" -fi - -NNAS_WORKSPACE=${NNAS_WORKSPACE:-build} -if [[ -z "${ARCHIVE_PATH}" ]]; then - ARCHIVE_PATH=${NNAS_WORKSPACE}/archive -fi - -DOCKER_ENV_VARS+=" -e TARGET_ARCH=armv7l" -DOCKER_ENV_VARS+=" -e CROSS_BUILD=1" -DOCKER_ENV_VARS+=" -e COVERAGE_BUILD=1" - -set -e - -pushd $ROOT_PATH > /dev/null - -# TODO use command instead of makefile -export DOCKER_ENV_VARS -export DOCKER_VOLUMES -CMD="cp -nv Makefile.template Makefile && \ - make all install build_coverage_suite" -./nnfw docker-run bash -c "$CMD" - -mkdir -p ${ARCHIVE_PATH} -# TODO change workspace usage in makefile -mv Product/out/coverage-suite.tar.gz ${ARCHIVE_PATH}/ - -popd > /dev/null diff --git a/infra/scripts/docker_build_test_x64.sh b/infra/scripts/docker_build_test_x64.sh deleted file mode 100755 index b3428e0..0000000 --- a/infra/scripts/docker_build_test_x64.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash - -[[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo "Please don't source ${BASH_SOURCE[0]}, execute it" && return - -CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_PATH="$CURRENT_PATH/../../" - -# mount volume (or directory) for externals -if [ -n "$EXTERNAL_VOLUME" ]; then - DOCKER_VOLUMES+=" -v $EXTERNAL_VOLUME:/externals" - DOCKER_ENV_VARS+=" -e EXTERNAL_VOLUME=/externals" -else - echo "It will use default external path" -fi - -# docker image name -# - for bionic, use DOCKER_IMAGE_NAME="nnfw/one-devtools:bionic" -# - for focal, use DOCKER_IMAGE_NAME="nnfw/one-devtools:focal" -if [[ -z $DOCKER_IMAGE_NAME ]]; then - echo "It will use default docker image name" -fi - -# Mirror server setting -if [[ -z $EXTERNAL_DOWNLOAD_SERVER ]]; then - echo "It will not use mirror server" -fi - -set -e - -pushd $ROOT_PATH > /dev/null - -export DOCKER_ENV_VARS -export DOCKER_VOLUMES -export BUILD_OPTIONS - -CMD="export OPTIONS='$BUILD_OPTIONS' && \ - export BUILD_TYPE=Release && \ - cp -nv Makefile.template Makefile && \ - make all install build_test_suite" -./nnfw docker-run bash -c "$CMD" - -# Model download server setting -if [[ -z $MODELFILE_SERVER ]]; then - echo "Need model file server setting" - exit 1 -fi - -export DOCKER_ENV_VARS=" -e MODELFILE_SERVER=$MODELFILE_SERVER" -./nnfw docker-run-user ./infra/scripts/test_ubuntu_runtime.sh --backend cpu -./nnfw docker-run-user ./infra/scripts/test_ubuntu_runtime.sh --interp - -popd > /dev/null diff --git a/infra/scripts/docker_build_tizen_cross.sh b/infra/scripts/docker_build_tizen_cross.sh deleted file mode 100755 index 42e79a7..0000000 --- a/infra/scripts/docker_build_tizen_cross.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash - -[[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo "Please don't source ${BASH_SOURCE[0]}, execute it" && return - -CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_PATH="$CURRENT_PATH/../../" - -# prepare rootfs -if [ -z "$ROOTFS_DIR" ] || [ ! -d $ROOTFS_DIR ]; then - echo "It will use default rootfs path" -else - DOCKER_VOLUMES+=" -v $ROOTFS_DIR:/opt/rootfs" - DOCKER_ENV_VARS+=" -e ROOTFS_DIR=/opt/rootfs" -fi - -# mount volume (or directory) for externals -if [ -n "$EXTERNAL_VOLUME" ]; then - DOCKER_VOLUMES+=" -v $EXTERNAL_VOLUME:/externals" - DOCKER_ENV_VARS+=" -e EXTERNAL_VOLUME=/externals" -else - echo "It will use default external path" -fi - -# docker image name -# - for bionic, use DOCKER_IMAGE_NAME="nnfw/one-devtools:bionic" -# - for focal, use DOCKER_IMAGE_NAME="nnfw/one-devtools:focal" -if [[ -z $DOCKER_IMAGE_NAME ]]; then - echo "It will use default docker image name" -fi - -DOCKER_ENV_VARS+=" -e TARGET_ARCH=armv7l" -DOCKER_ENV_VARS+=" -e CROSS_BUILD=1" -DOCKER_ENV_VARS+=" -e TARGET_OS=tizen" -DOCKER_ENV_VARS+=" -e BUILD_TYPE=release" - -# Mirror server setting -if [[ -z $EXTERNAL_DOWNLOAD_SERVER ]]; then - echo "It will not use mirror server" -fi - -set -e - -pushd $ROOT_PATH > /dev/null - -export DOCKER_ENV_VARS -export DOCKER_VOLUMES -CMD="export OPTIONS+=' -DGENERATE_RUNTIME_NNAPI_TESTS=ON' && \ - cp -nv Makefile.template Makefile && \ - make all install build_test_suite" -./nnfw docker-run bash -c "$CMD" - -popd > /dev/null diff --git a/infra/scripts/docker_build_tizen_gbs.sh b/infra/scripts/docker_build_tizen_gbs.sh deleted file mode 100755 index 2d508f4..0000000 --- a/infra/scripts/docker_build_tizen_gbs.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -[[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo "Please don't source ${BASH_SOURCE[0]}, execute it" && return - -CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_PATH="$CURRENT_PATH/../../" - -GBS_RPM_DIR=$ROOT_PATH/Product/out/rpm -mkdir -p $GBS_RPM_DIR -DOCKER_VOLUMES=" -v $GBS_RPM_DIR:/opt/rpm" - -if [[ -z $DOCKER_IMAGE_NAME ]]; then - echo "It will use default docker image name for tizen gbs build" - DOCKER_IMAGE_NAME="nnfw_docker_tizen" -fi - -DOCKER_ENV_VARS=" --privileged" - -set -e - -pushd $ROOT_PATH > /dev/null - -CMD="gbs -c $ROOT_PATH/infra/nnfw/config/gbs.conf build \ - -A armv7l --profile=profile.tizen --clean --include-all --define '$GBS_DEFINE' && \ - cp -rf /home/GBS-ROOT/local/repos/tizen/armv7l/RPMS/*.rpm /opt/rpm/" - -export DOCKER_ENV_VARS -export DOCKER_VOLUMES -./nnfw docker-run bash -c "$CMD" - -popd > /dev/null diff --git a/infra/scripts/docker_collect_nnpkg_resources.sh b/infra/scripts/docker_collect_nnpkg_resources.sh index afdd3b9..9fb7def 100755 --- a/infra/scripts/docker_collect_nnpkg_resources.sh +++ b/infra/scripts/docker_collect_nnpkg_resources.sh @@ -68,6 +68,8 @@ REQUIRED_UNITS+=("oops" "safemain" "foder" "crew" "arser" "vconone") REQUIRED_UNITS+=("hermes" "hermes-std") # loco IR and related utilities REQUIRED_UNITS+=("loco" "locop" "locomotiv" "logo-core" "logo") +# Compute +REQUIRED_UNITS+=("luci-compute") # Circle compiler library (.circle -> .circle) REQUIRED_UNITS+=("luci") # Flatbuffer I/O diff --git a/infra/scripts/docker_coverage_report.sh b/infra/scripts/docker_coverage_report.sh deleted file mode 100755 index 2c3ee30..0000000 --- a/infra/scripts/docker_coverage_report.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -# coverage test data: ${ARCHIVE_PATH}/coverage-data.tar.gz - -[[ "${BASH_SOURCE[0]}" != "${0}" ]] && echo "Please don't source ${BASH_SOURCE[0]}, execute it" && return - -CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_PATH="$CURRENT_PATH/../../" - -# docker image name -# - for bionic, use DOCKER_IMAGE_NAME="nnfw/one-devtools:bionic" -# - for focal, use DOCKER_IMAGE_NAME="nnfw/one-devtools:focal" -if [[ -z $DOCKER_IMAGE_NAME ]]; then - echo "It will use default docker image name" -fi - -NNAS_WORKSPACE=${NNAS_WORKSPACE:-build} -if [[ -z "${ARCHIVE_PATH}" ]]; then - ARCHIVE_PATH=${NNAS_WORKSPACE}/archive -fi - -set -e - -pushd $ROOT_PATH > /dev/null - -tar -zxf ${ARCHIVE_PATH}/coverage-data.tar.gz - -CMD="GCOV_PATH=arm-linux-gnueabihf-gcov NNAS_WORKSPACE=Product ./nnas gen-coverage-report runtime compute && - tar -zcf coverage/coverage_report.tar.gz coverage/html && - python runtime/3rdparty/lcov-to-cobertura-xml/lcov_cobertura.py coverage/coverage.info -o coverage/nnfw_coverage.xml" - -./nnfw docker-run-user bash -c "$CMD" - -popd > /dev/null diff --git a/infra/scripts/test_arm_nnpkg.sh b/infra/scripts/test_arm_nnpkg.sh index d00eb73..74fae6b 100755 --- a/infra/scripts/test_arm_nnpkg.sh +++ b/infra/scripts/test_arm_nnpkg.sh @@ -10,7 +10,4 @@ do NNPackageTest ${BACKEND} "Product/out/test/list/nnpkg_test_list.armv7l-linux.${BACKEND}" done -# Interpreter test -export DISABLE_COMPILE=1 -NNPackageTest "interp" "Product/out/test/list/nnpkg_test_list.noarch.interp" unset DISABLE_COMPILE diff --git a/infra/scripts/test_coverage.sh b/infra/scripts/test_coverage.sh index 6cb4bb7..97043ce 100755 --- a/infra/scripts/test_coverage.sh +++ b/infra/scripts/test_coverage.sh @@ -35,9 +35,6 @@ export TRACE_FILEPATH=trace.json TFLiteModelVerification "acl_cl" "Product/out/test/list/tflite_comparator.armv7l.acl_cl.list" "report/acl_cl/trace" unset TRACE_FILEPATH -# Interpreter -./infra/scripts/test_ubuntu_runtime.sh --interp - # nnpackage test suite if [[ -e ${ARCHIVE_PATH}/nnpkg-test-suite.tar.gz ]]; then tar -zxf ${ARCHIVE_PATH}/nnpkg-test-suite.tar.gz -C ./ diff --git a/infra/scripts/test_ubuntu_npud.sh b/infra/scripts/test_ubuntu_npud.sh new file mode 100755 index 0000000..3b33042 --- /dev/null +++ b/infra/scripts/test_ubuntu_npud.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +set -eo pipefail + +CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_PATH="$(cd ${CURRENT_PATH}/../../ && pwd)" + +# Install path on CI +INSTALL_PATH="$ROOT_PATH/Product/out" +MODEL_PATH="${INSTALL_PATH}/npud-gtest/models" + +# Install dbus configuration file +DBUS_CONF="${INSTALL_PATH}/share/org.tizen.npud.conf" +mkdir -p /usr/share/dbus-1/system.d/ +cp ${DBUS_CONF} /usr/share/dbus-1/system.d/ + +service dbus restart + +function TestPrepared() +{ + if [[ -z "${MODELFILE}" ]]; then + echo "Model file is not set. Try to use default setting." + exit 1 + fi + + mkdir -p ${MODEL_PATH} + if [[ "${MODELFILE: -7}" == ".tar.gz" ]]; then + curl -o model.tar.gz -kLsSO ${MODELFILE} + tar -zxf model.tar.gz -C ${MODEL_PATH} + else + echo "The file format is not supported." + echo "Supported format: tar.gz" + exit 1 + fi +} + +function TestCleanUp() +{ + rm -rf ${MODEL_PATH} +} + +function NpudTest() +{ + pushd ${ROOT_PATH} > /dev/null + + $INSTALL_PATH/npud-gtest/npud_gtest + EXITCODE=$? + if [ ${EXITCODE} -ne 0 ]; then + exit ${EXITCODE} + fi + + popd > /dev/null +} + +TestPrepared + +DEVICE_MODULE_PATH=${INSTALL_PATH}/lib GTEST_MODEL_PATH=${MODEL_PATH} NpudTest + +TestCleanUp diff --git a/infra/scripts/test_ubuntu_runtime.sh b/infra/scripts/test_ubuntu_runtime.sh index 17bdf6e..9a98e5b 100755 --- a/infra/scripts/test_ubuntu_runtime.sh +++ b/infra/scripts/test_ubuntu_runtime.sh @@ -7,10 +7,8 @@ source "$(dirname "${BASH_SOURCE[0]}")/common.sh" BACKEND="cpu" TEST_OS="linux" TEST_PLATFORM="$TEST_ARCH-$TEST_OS" -TFLITE_LOADER="1" LINEAR_ONLY="0" RUN_INTERP="0" -NNAPI_FRONTEND="0" function Usage() { @@ -18,7 +16,6 @@ function Usage() echo "" echo "Options:" echo " --backend Runtime backend to test (default: ${BACKEND})" - echo " --nnapi-frontend NNAPI Frontend test" echo " --linear-only Use Linear executor only" } @@ -38,24 +35,10 @@ do BACKEND=$(echo ${1#*=} | tr '[:upper:]' '[:lower:]') shift ;; - --tflite-loader) - TFLITE_LOADER="1" - NNAPI_FRONTEND="1" # For CI test - echo "[INFO] \"--tflite-loader\" argument is deprecated" - shift - ;; - --nnapi-frontend) - NNAPI_FRONTEND="1" - shift - ;; --linear-only) LINEAR_ONLY="1" shift ;; - --interp) - RUN_INTERP="1" - shift; - ;; *) # Ignore shift @@ -65,16 +48,9 @@ done CheckTestPrepared -if [ $RUN_INTERP = "1" ]; then - TEST_PLATFORM="noarch" - TEST_ARCH="noarch" - BACKEND="interp" - echo "[[ Interpreter test ]]" -else - echo "[[ ${TEST_PLATFORM}: ${BACKEND} backend test ]]" -fi +echo "[[ ${TEST_PLATFORM}: ${BACKEND} backend test ]]" -UNITTEST_SKIPLIST="Product/out/unittest/nnapi_gtest.skip.${TEST_PLATFORM}.${BACKEND}" +UNITTEST_SKIPLIST="Product/out/nnapi-gtest/nnapi_gtest.skip.${TEST_PLATFORM}.${BACKEND}" TFLITE_TESTLIST="Product/out/test/list/tflite_comparator.${TEST_ARCH}.${BACKEND}.list" REPORT_BASE="report/${BACKEND}" EXECUTORS=("Linear" "Dataflow" "Parallel") @@ -82,34 +58,16 @@ EXECUTORS=("Linear" "Dataflow" "Parallel") if [ $LINEAR_ONLY = "1" ]; then EXECUTORS=("Linear") fi -if [ $RUN_INTERP = "1" ]; then - EXECUTORS=("Interpreter") -fi for EXECUTOR in "${EXECUTORS[@]}"; do echo "[EXECUTOR]: ${EXECUTOR}" REPORT_PATH="${REPORT_BASE}/${EXECUTOR}" - if [ $EXECUTOR = "Interpreter" ]; then - export DISABLE_COMPILE=1 - BACKEND="" - else - export EXECUTOR="${EXECUTOR}" - fi + export EXECUTOR="${EXECUTOR}" NNAPIGTest "${BACKEND}" "${UNITTEST_SKIPLIST}" "${REPORT_PATH}" TFLiteModelVerification "${BACKEND}" "${TFLITE_TESTLIST}" "${REPORT_PATH}" - if [ $EXECUTOR = "Interpreter" ]; then - unset DISABLE_COMPILE - else - unset EXECUTOR - fi + unset EXECUTOR done - -# TODO Support more backends -NNAPI_FRONTEND_TESTLIST="Product/out/test/list/nnapi_test.${TEST_ARCH}.list" -if [[ $NNAPI_FRONTEND = "1" ]]; then - NNAPIFrontendTest "${BACKEND}" "${NNAPI_FRONTEND_TESTLIST}" "${REPORT_BASE}/nnapi/${EXECUTOR}" -fi diff --git a/infra/scripts/test_ubuntu_runtime_mixed.sh b/infra/scripts/test_ubuntu_runtime_mixed.sh index 2510d9c..a6fd2a4 100755 --- a/infra/scripts/test_ubuntu_runtime_mixed.sh +++ b/infra/scripts/test_ubuntu_runtime_mixed.sh @@ -17,7 +17,7 @@ pushd ${ROOT_PATH} > /dev/null echo "" echo "==== Run standalone unittest begin ====" echo "" -Product/out/test/onert-test unittest --unittestdir=Product/out/unittest_standalone +Product/out/test/onert-test unittest --unittestdir=Product/out/unittest echo "" echo "==== Run standalone unittest end ====" echo "" @@ -33,7 +33,7 @@ BACKENDS=(acl_cl acl_neon cpu) # Get the intersect of framework test list files TESTLIST_PREFIX="Product/out/test/list/tflite_comparator.${TEST_ARCH}" -SKIPLIST_PREFIX="Product/out/unittest/nnapi_gtest.skip.${TEST_ARCH}-${TEST_OS}" +SKIPLIST_PREFIX="Product/out/nnapi-gtest/nnapi_gtest.skip.${TEST_ARCH}-${TEST_OS}" sort $TESTLIST_PREFIX.${BACKENDS[0]}.list > $TESTLIST_PREFIX.intersect.list sort $SKIPLIST_PREFIX.${BACKENDS[0]} > $SKIPLIST_PREFIX.union for BACKEND in "${BACKENDS[@]:1}"; do @@ -59,5 +59,5 @@ export OP_BACKEND_Pool2D="acl_cl" export OP_BACKEND_FullyConnected="acl_neon" export ACL_LAYOUT="NCHW" export RUY_THREADS=4 -NNAPIGTest "acl_cl;acl_neon;cpu" "Product/out/unittest/nnapi_gtest.skip.${TEST_ARCH}-${TEST_OS}.union" "report/mixed" +NNAPIGTest "acl_cl;acl_neon;cpu" "Product/out/nnapi-gtest/nnapi_gtest.skip.${TEST_ARCH}-${TEST_OS}.union" "report/mixed" TFLiteModelVerification "acl_cl;acl_neon;cpu" "${TESTLIST_PREFIX}.intersect.list" "report/mixed" diff --git a/infra/scripts/tizen_xu4_test.sh b/infra/scripts/tizen_xu4_test.sh index 37576ac..5610756 100755 --- a/infra/scripts/tizen_xu4_test.sh +++ b/infra/scripts/tizen_xu4_test.sh @@ -25,15 +25,18 @@ function install_model() { # download tflite model files pushd $HOST_HOME - tests/scripts/models/run_test.sh --download=on --run=off + TEMP_PATH=$(mktemp -d) + CACHE_PATH=$TEMP_PATH/cache + mkdir -p $CACHE_PATH + ./nnfw prepare-model --cachedir=$CACHE_PATH # TODO Since this command removes model file(.zip), # We must always download the file unlike model file(.tflite). # Because caching applies only to tflite file. - find tests -name "*.zip" -exec rm {} \; - tar -zcf cache.tar.gz -C tests/scripts/models cache - $SDB_CMD push cache.tar.gz $TEST_ROOT/. - rm -rf cache.tar.gz - $SDB_CMD shell tar -zxf $TEST_ROOT/cache.tar.gz -C $TEST_ROOT/Product/out/test/models + find $CACHE_PATH -name "*.zip" -exec rm {} \; + tar -zcf $TEMP_PATH/cache.tar.gz -C $TEMP_PATH cache + $SDB_CMD push $TEMP_PATH/cache.tar.gz $TEST_ROOT/ + rm -rf $TEMP_PATH + $SDB_CMD shell tar -zxf $TEST_ROOT/cache.tar.gz -C $TEST_ROOT/Product/out/test popd } @@ -153,7 +156,6 @@ if [ -z "${GCOV_DIR}" ]; then ${SDB_CMD} shell /bin/bash -c "IGNORE_MD5=1 TEST_ARCH=armv7l ${TEST_ROOT}/infra/scripts/test_ubuntu_runtime.sh --backend acl_neon" ${SDB_CMD} shell /bin/bash -c "IGNORE_MD5=1 TEST_ARCH=armv7l ${TEST_ROOT}/infra/scripts/test_ubuntu_runtime.sh --backend cpu" ${SDB_CMD} shell /bin/bash -c "IGNORE_MD5=1 TEST_ARCH=armv7l ${TEST_ROOT}/infra/scripts/test_ubuntu_runtime_mixed.sh" - ${SDB_CMD} shell /bin/bash -c "IGNORE_MD5=1 TEST_ARCH=armv7l ${TEST_ROOT}/infra/scripts/test_ubuntu_runtime.sh --interp" else mkdir -p ${GCOV_DIR} rm -rf ${GCOV_DIR}/* @@ -169,7 +171,6 @@ else ${SDB_CMD} shell /bin/bash -c "GCOV_PREFIX_STRIP=${GCOV_PREFIX_STRIP} IGNORE_MD5=1 TEST_ARCH=armv7l ${TEST_ROOT}/infra/scripts/test_ubuntu_runtime.sh --backend acl_neon" ${SDB_CMD} shell /bin/bash -c "GCOV_PREFIX_STRIP=${GCOV_PREFIX_STRIP} IGNORE_MD5=1 TEST_ARCH=armv7l ${TEST_ROOT}/infra/scripts/test_ubuntu_runtime.sh --backend cpu" ${SDB_CMD} shell /bin/bash -c "GCOV_PREFIX_STRIP=${GCOV_PREFIX_STRIP} IGNORE_MD5=1 TEST_ARCH=armv7l ${TEST_ROOT}/infra/scripts/test_ubuntu_runtime_mixed.sh" - ${SDB_CMD} shell /bin/bash -c "GCOV_PREFIX_STRIP=${GCOV_PREFIX_STRIP} IGNORE_MD5=1 TEST_ARCH=armv7l ${TEST_ROOT}/infra/scripts/test_ubuntu_runtime.sh --interp" # More test to check coverage ${SDB_CMD} shell "rm -rf ${GCOV_DATA_PATH} && mkdir -p ${GCOV_DATA_PATH}" diff --git a/onert-micro/CMakeLists.txt b/onert-micro/CMakeLists.txt new file mode 100644 index 0000000..d5ea95d --- /dev/null +++ b/onert-micro/CMakeLists.txt @@ -0,0 +1,217 @@ +set(ARM_C_COMPILER "arm-none-eabi-gcc") +set(ARM_ASM_COMPILER "arm-none-eabi-gcc") +set(ARM_CXX_COMPILER "arm-none-eabi-g++") +set(ARM_OBJCOPY "arm-none-eabi-objcopy") + +find_program(ARM_C_COMPILER_PATH ${ARM_C_COMPILER}) + +if (NOT ARM_C_COMPILER_PATH) + message(STATUS "Build luci-micro: FALSE(ARM compiler is NOT FOUND)") + return() +endif () + +nnas_find_package(FlatBuffers EXACT 2.0 QUIET) + +if (NOT FlatBuffers_FOUND) + message(STATUS "Build luci-micro: FALSE(FlatBuffers 2.0 NOT FOUND)") + return() +endif (NOT FlatBuffers_FOUND) + +message(STATUS "Build luci-micro: TRUE") + +set(SCHEMA_FILE "${NNAS_PROJECT_SOURCE_DIR}/res/CircleSchema/0.4/circle_schema.fbs") + +# NOTE Copy circle_schema.fbs as schema.fbs to generate "schema_generated.fbs" instead of "circle_schema_generated.fbs" +add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/schema.fbs" + COMMAND ${CMAKE_COMMAND} -E copy "${SCHEMA_FILE}" schema.fbs + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + DEPENDS "${SCHEMA_FILE}" + ) + +FlatBuffers_Target(luci_micro_circle_schema + OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/gen/circle-generated/circle" + INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/gen" + SCHEMA_DIR "${CMAKE_CURRENT_BINARY_DIR}" + SCHEMA_FILES "schema.fbs" + ) + +set(LUCI_INTERPRETER_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/luci-interpreter/include") +add_subdirectory(luci-interpreter/src/core/reader) + +# Choosing Kernel: reference mcu, optimized cmsisnn, optimized linux +if (NOT KERNELS) + message(STATUS "KERNEL variable is not defined, default reference mcu kernels will be used") + set(LUCI_INTERPRETER_PAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/luci-interpreter/pal/mcu") +elseif("${KERNELS}" STREQUAL "mcu") + message(STATUS "ONERT_MICRO will use reference mcu kernels") + set(LUCI_INTERPRETER_PAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/luci-interpreter/pal/mcu") +elseif("${KERNELS}" STREQUAL "cmsisnn") + message(STATUS "ONERT_MICRO will use optimized cmsisnn kernels") + set(LUCI_INTERPRETER_PAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/luci-interpreter/pal/cmsisnn") +elseif("${KERNELS}" STREQUAL "linux") + message(STATUS "ONERT_MICRO will use optimized linux kernels") + set(LUCI_INTERPRETER_PAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/luci-interpreter/pal/linux") +else() + message(STATUS "Build onert-micro: FAILED (Non-existent kernel variable. Choose one of the following options: mcu, cmsisnn, linux)") + return() +endif() + +if (USE_STATIC_ALLOC) + # TODO: enable it + message(STATUS "FAILED ONERT-MICRO is not support Static Memory Manager now") + return() +else() + message(STATUS "USE_STATIC_ALLOC variable is not defined, default dynamic memory manager will be used") +endif() + +set(CMAKE_ARM_OPTIONS + -DLUCI_INTERPRETER_STATIC=ON + -DLUCI_STATIC=ON + -DBUILD_CMSIS_NN_FUNCTIONS=ON + -DTARGET_CPU=${TARGET_CPU} + -DTARGET_ARCH=${TARGET_ARCH} + "-DEXT_OVERLAY_DIR=${CMAKE_CURRENT_BINARY_DIR}/../../overlay" + "-DFlatbuffers_DIR=${CMAKE_CURRENT_BINARY_DIR}/../../overlay/lib/cmake/flatbuffers" + "-DCMAKE_TOOLCHAIN_FILE=${NNAS_PROJECT_SOURCE_DIR}/infra/onert-micro/cmake/buildtool/config/arm-none-eabi-gcc.cmake" + "-DLUCI_INTERPRETER_PAL_DIR=${LUCI_INTERPRETER_PAL_DIR}" + "-DNNAS_PROJECT_SOURCE_DIR=${NNAS_PROJECT_SOURCE_DIR}" + "-DNNAS_EXTERNALS_DIR=${NNAS_EXTERNALS_DIR}" + -DC_COMPILER=${ARM_C_COMPILER} + -DCXX_COMPILER=${ARM_CXX_COMPILER} + -DASM_COMPILER=${ARM_ASM_COMPILER} + -DOBJCOPY=${ARM_OBJCOPY} + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DENABLE_TEST=OFF + -DBUILD_GTEST=OFF + "-DNNAS_ROOT=${NNAS_PROJECT_SOURCE_DIR}" + -DENABLE_STRICT_BUILD=OFF + "-DGENERATED_INCLUDE_DIR=${CMAKE_CURRENT_BINARY_DIR}/gen" + ) + +if (GENERATE_KERNELS_LIST_FROM) + set(GENERATED_KERNELS_LIST_PATH "${LUCI_INTERPRETER_PAL_DIR}/GeneratedKernelsToBuild.lst") + list(APPEND CMAKE_ARM_OPTIONS "-DLUCI_INTERPRETER_KERNELS_BUILD_LIST=${GENERATED_KERNELS_LIST_PATH}") +endif () + +if (DIS_QUANT) + message(STATUS "ONERT-MICRO will not use part for QUANTIZED models") + add_definitions(-DDIS_QUANT) + list(APPEND CMAKE_ARM_OPTIONS "-DDIS_QUANT=ON") +endif() + +if (DIS_FLOAT) + message(STATUS "ONERT-MICRO will not use part for FLOAT models") + add_definitions(-DDIS_FLOAT) + list(APPEND CMAKE_ARM_OPTIONS "-DDIS_FLOAT=ON") +endif() + +set(MICRO_ARM_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/standalone_arm") +file(MAKE_DIRECTORY "${MICRO_ARM_BUILD_DIR}") + +set(MICRO_ARM_BUILD_DEPENDENCY "${MICRO_ARM_BUILD_DIR}/CMakeCache.txt") + +add_custom_command( + OUTPUT "${MICRO_ARM_BUILD_DEPENDENCY}" + COMMAND "${CMAKE_COMMAND}" "${CMAKE_CURRENT_SOURCE_DIR}/standalone" ${CMAKE_ARM_OPTIONS} + WORKING_DIRECTORY "${MICRO_ARM_BUILD_DIR}" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/standalone/CMakeLists.txt" + VERBATIM +) + +add_custom_target(luci_interpreter_micro_arm_cmake DEPENDS "${MICRO_ARM_BUILD_DEPENDENCY}") + +# Generate KernelsToBuild list from circle model +if (GENERATE_KERNELS_LIST_FROM) + add_executable(generator_kernels_list_exec helpers/GenerateKernelsListHelper.cpp) + + target_link_libraries(generator_kernels_list_exec luci_micro_circle_reader) + target_link_libraries(generator_kernels_list_exec luci_micro_circle_schema) + + target_include_directories(generator_kernels_list_exec PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/luci-interpreter/include") + + add_custom_target( + generate_kernels_list ALL + COMMAND generator_kernels_list_exec ${GENERATE_KERNELS_LIST_FROM} ${GENERATED_KERNELS_LIST_PATH} + COMMENT "Generating KernelsToBuild list" + ) + add_dependencies(generate_kernels_list luci_micro_circle_reader) + add_dependencies(luci_interpreter_micro_arm_cmake generate_kernels_list) + +endif () + +# To remove GENERATE_KERNELS_LIST_FROM and KERNELS variable from cmake cache +unset(GENERATE_KERNELS_LIST_FROM CACHE) +unset(KERNELS CACHE) +unset(USE_STATIC_KERNEL CACHE) +unset(DIS_QUANT CACHE) +unset(DIS_FLOAT CACHE) + +set(MICRO_ARM_BINARY "${MICRO_ARM_BUILD_DIR}/luci-interpreter/src/libluci_interpreter_micro.a") + +add_custom_command( + OUTPUT "${MICRO_ARM_BINARY}" + COMMAND "${CMAKE_MAKE_PROGRAM}" luci_interpreter_micro -j ${CPU_COUNT} + WORKING_DIRECTORY "${MICRO_ARM_BUILD_DIR}" + DEPENDS luci_interpreter_micro_arm_cmake luci_micro_circle_schema + VERBATIM +) + +add_custom_target(luci_interpreter_micro_arm DEPENDS "${MICRO_ARM_BINARY}") + +add_subdirectory(eval-driver) + +if (NOT DEFINED BUILD_TEST) + return() +endif () + +#MBED OS QEMU build +nnas_find_package(MbedOSSource EXACT 6.15 QUIET) + +if (NOT MbedOSSource_FOUND) + message(STATUS "Skipping luci-micro: MbedOSSource not found") + return() +endif () + +set(MBED_OS_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/tests/mbed-os") +file(MAKE_DIRECTORY "${MBED_OS_BUILD_DIR}") + +set(MBED_OS_BUILD_DEPENDENCY "${MBED_OS_BUILD_DIR}/CMakeCache.txt") + +set(ONERTMICRO_SRC_DIR "${NNAS_PROJECT_SOURCE_DIR}/onert-micro") + +add_custom_command( + OUTPUT "${MBED_OS_BUILD_DEPENDENCY}" + COMMAND "${CMAKE_COMMAND}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/mbed-os" + -DMICRO_ARM_BUILD_DIR=${MICRO_ARM_BUILD_DIR} + -DMbedOSSource_DIR=${MbedOSSource_DIR} + -DFlatBuffersSource_DIR=${FlatBuffersSource_DIR} + -DONERTMICRO_SRC_DIR=${ONERTMICRO_SRC_DIR} + WORKING_DIRECTORY "${MBED_OS_BUILD_DIR}" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/mbed-os/CMakeLists.txt" + VERBATIM +) + +add_custom_target(mbed_os_cmake DEPENDS "${MBED_OS_BUILD_DEPENDENCY}") + +set(MBED_OS_BINARY "${MBED_OS_BUILD_DIR}/libmbed_os.a") + +add_custom_command( + OUTPUT "${MBED_OS_BINARY}" + COMMAND "${CMAKE_MAKE_PROGRAM}" mbed_os -j ${CPU_COUNT} + WORKING_DIRECTORY "${MBED_OS_BUILD_DIR}" + DEPENDS mbed_os_cmake + VERBATIM +) + +add_custom_target(mbed_os_arm DEPENDS "${MBED_OS_BINARY}") + +set(BUILD_TEST_BINARY "${MBED_OS_BUILD_DIR}/build_test.bin") + +add_custom_command( + OUTPUT "${BUILD_TEST_BINARY}" + COMMAND "${CMAKE_MAKE_PROGRAM}" build_test -j ${CPU_COUNT} + WORKING_DIRECTORY "${MBED_OS_BUILD_DIR}" + DEPENDS mbed_os_arm "${CMAKE_CURRENT_SOURCE_DIR}/tests/mbed-os/main.cpp" ${MICRO_ARM_BINARY} + VERBATIM +) +add_custom_target(onert_micro_build_test_arm DEPENDS "${BUILD_TEST_BINARY}") diff --git a/compiler/luci-micro/README.md b/onert-micro/README.md similarity index 100% rename from compiler/luci-micro/README.md rename to onert-micro/README.md diff --git a/onert-micro/eval-driver/CMakeLists.txt b/onert-micro/eval-driver/CMakeLists.txt new file mode 100644 index 0000000..2a1b73a --- /dev/null +++ b/onert-micro/eval-driver/CMakeLists.txt @@ -0,0 +1,13 @@ +set(SRCS_EVAL_TESTER Driver.cpp) + +add_executable(onert_micro_eval_driver ${SRCS_EVAL_TESTER}) + +# This variable is needed to separate standalone interpreter libraries from the libraries used in driver +set(READER_SUFFIX "_driver") + +add_subdirectory(${NNAS_PROJECT_SOURCE_DIR}/onert-micro/luci-interpreter ${CMAKE_CURRENT_BINARY_DIR}/luci-interpreter) + +target_include_directories(onert_micro_eval_driver PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/luci-interpreter/include") +target_link_libraries(onert_micro_eval_driver PUBLIC luci_interpreter_micro) + +install(TARGETS onert_micro_eval_driver DESTINATION bin) diff --git a/onert-micro/eval-driver/Driver.cpp b/onert-micro/eval-driver/Driver.cpp new file mode 100644 index 0000000..7ec4219 --- /dev/null +++ b/onert-micro/eval-driver/Driver.cpp @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + +using DataBuffer = std::vector; + +void readDataFromFile(const std::string &filename, char *data, size_t data_size) +{ + std::ifstream fs(filename, std::ifstream::binary); + if (fs.fail()) + throw std::runtime_error("Cannot open file \"" + filename + "\".\n"); + if (fs.read(data, data_size).fail()) + throw std::runtime_error("Failed to read data from file \"" + filename + "\".\n"); +} + +void writeDataToFile(const std::string &filename, const char *data, size_t data_size) +{ + std::ofstream fs(filename, std::ofstream::binary); + if (fs.fail()) + throw std::runtime_error("Cannot open file \"" + filename + "\".\n"); + if (fs.write(data, data_size).fail()) + { + throw std::runtime_error("Failed to write data to file \"" + filename + "\".\n"); + } +} + +} // namespace + +/* + * @brief EvalDriver main + * + * Driver for testing luci-inerpreter + * + */ +int entry(int argc, char **argv) +{ + if (argc != 5) + { + std::cerr + << "Usage: " << argv[0] + << " \n"; + return EXIT_FAILURE; + } + + const char *filename = argv[1]; + const int32_t num_inputs = atoi(argv[2]); + const char *input_prefix = argv[3]; + const char *output_file = argv[4]; + + std::ifstream file(filename, std::ios::binary | std::ios::in); + if (!file.good()) + { + std::string errmsg = "Failed to open file"; + throw std::runtime_error(errmsg.c_str()); + } + + file.seekg(0, std::ios::end); + auto fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + // reserve capacity + DataBuffer model_data(fileSize); + + // read the data + file.read(model_data.data(), fileSize); + if (file.fail()) + { + std::string errmsg = "Failed to read file"; + throw std::runtime_error(errmsg.c_str()); + } + + // Create interpreter. + luci_interpreter::Interpreter interpreter(model_data.data()); + + // Set input. + // Data for n'th input is read from ${input_prefix}n + // (ex: Add.circle.input0, Add.circle.input1 ..) + int num_inference = 1; + for (int j = 0; j < num_inference; ++j) + { + for (int32_t i = 0; i < num_inputs; i++) + { + auto input_data = reinterpret_cast(interpreter.allocateInputTensor(i)); + readDataFromFile(std::string(input_prefix) + std::to_string(i), input_data, + interpreter.getInputDataSizeByIndex(i)); + } + + // Do inference. + interpreter.interpret(); + } + + // Get output. + int num_outputs = 1; + for (int i = 0; i < num_outputs; i++) + { + auto data = interpreter.readOutputTensor(i); + + // Output data is written in ${output_file} + // (ex: Add.circle.output0) + writeDataToFile(std::string(output_file) + std::to_string(i), reinterpret_cast(data), + interpreter.getOutputDataSizeByIndex(i)); + } + return EXIT_SUCCESS; +} + +int entry(int argc, char **argv); + +#ifdef NDEBUG +int main(int argc, char **argv) +{ + try + { + return entry(argc, argv); + } + catch (const std::exception &e) + { + std::cerr << "ERROR: " << e.what() << std::endl; + } + + return 255; +} +#else // NDEBUG +int main(int argc, char **argv) +{ + // NOTE main does not catch internal exceptions for debug build to make it easy to + // check the stacktrace with a debugger + return entry(argc, argv); +} +#endif // !NDEBUG diff --git a/onert-micro/helpers/GenerateKernelsListHelper.cpp b/onert-micro/helpers/GenerateKernelsListHelper.cpp new file mode 100644 index 0000000..1420020 --- /dev/null +++ b/onert-micro/helpers/GenerateKernelsListHelper.cpp @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci_interpreter/core/reader/CircleMicroReader.h" + +#include + +#include +#include +#include + +std::string get_register_kernel_str(const circle::BuiltinOperator builtin_operator) +{ + switch (builtin_operator) + { + case circle::BuiltinOperator_ADD: + return "REGISTER_KERNEL(ADD, Add)"; + case circle::BuiltinOperator_ARG_MAX: + return "REGISTER_KERNEL(ARG_MAX, ArgMax)"; + case circle::BuiltinOperator_AVERAGE_POOL_2D: + return "REGISTER_KERNEL(AVERAGE_POOL_2D, AveragePool2D)"; + case circle::BuiltinOperator_BATCH_TO_SPACE_ND: + return "REGISTER_KERNEL(BATCH_TO_SPACE_ND, BatchToSpaceND)"; + case circle::BuiltinOperator_CAST: + return "REGISTER_KERNEL(CAST, Cast)"; + case circle::BuiltinOperator_CONCATENATION: + return "REGISTER_KERNEL(CONCATENATION, Concatenation)"; + case circle::BuiltinOperator_CONV_2D: + return "REGISTER_KERNEL(CONV_2D, Conv2D)"; + case circle::BuiltinOperator_DEPTH_TO_SPACE: + return "REGISTER_KERNEL(DEPTH_TO_SPACE, DepthToSpace)"; + case circle::BuiltinOperator_DEPTHWISE_CONV_2D: + return "REGISTER_KERNEL(DEPTHWISE_CONV_2D, DepthwiseConv2D)"; + case circle::BuiltinOperator_DEQUANTIZE: + return "REGISTER_KERNEL(DEQUANTIZE, Dequantize)"; + case circle::BuiltinOperator_DIV: + return "REGISTER_KERNEL(DIV, Div)"; + case circle::BuiltinOperator_ELU: + return "REGISTER_KERNEL(ELU, Elu)"; + case circle::BuiltinOperator_EXP: + return "REGISTER_KERNEL(EXP, Exp)"; + case circle::BuiltinOperator_EXPAND_DIMS: + return "REGISTER_KERNEL(EXPAND_DIMS, ExpandDims)"; + case circle::BuiltinOperator_FILL: + return "REGISTER_KERNEL(FILL, Fill)"; + case circle::BuiltinOperator_FLOOR: + return "REGISTER_KERNEL(FLOOR, Floor)"; + case circle::BuiltinOperator_FLOOR_DIV: + return "REGISTER_KERNEL(FLOOR_DIV, FloorDiv)"; + case circle::BuiltinOperator_EQUAL: + return "REGISTER_KERNEL(EQUAL, Equal)"; + case circle::BuiltinOperator_FULLY_CONNECTED: + return "REGISTER_KERNEL(FULLY_CONNECTED, FullyConnected)"; + case circle::BuiltinOperator_GREATER: + return "REGISTER_KERNEL(GREATER, Greater)"; + case circle::BuiltinOperator_GREATER_EQUAL: + return "REGISTER_KERNEL(GREATER_EQUAL, GreaterEqual)"; + case circle::BuiltinOperator_INSTANCE_NORM: + return "REGISTER_KERNEL(INSTANCE_NORM, InstanceNorm)"; + case circle::BuiltinOperator_L2_NORMALIZATION: + return "REGISTER_KERNEL(L2_NORMALIZATION, L2Normalize)"; + case circle::BuiltinOperator_L2_POOL_2D: + return "REGISTER_KERNEL(L2_POOL_2D, L2Pool2D)"; + case circle::BuiltinOperator_LEAKY_RELU: + return "REGISTER_KERNEL(LEAKY_RELU, LeakyRelu)"; + case circle::BuiltinOperator_LESS: + return "REGISTER_KERNEL(LESS, Less)"; + case circle::BuiltinOperator_LESS_EQUAL: + return "REGISTER_KERNEL(LESS_EQUAL, LessEqual)"; + case circle::BuiltinOperator_LOGICAL_AND: + return "REGISTER_KERNEL(LOGICAL_AND, LogicalAnd)"; + case circle::BuiltinOperator_LOGICAL_NOT: + return "REGISTER_KERNEL(LOGICAL_NOT, LogicalNot)"; + case circle::BuiltinOperator_LOGICAL_OR: + return "REGISTER_KERNEL(LOGICAL_OR, LogicalOr)"; + case circle::BuiltinOperator_LOGISTIC: + return "REGISTER_KERNEL(LOGISTIC, Logistic)"; + case circle::BuiltinOperator_MAXIMUM: + return "REGISTER_KERNEL(MAXIMUM, Maximum)"; + case circle::BuiltinOperator_MAX_POOL_2D: + return "REGISTER_KERNEL(MAX_POOL_2D, MaxPool2D)"; + case circle::BuiltinOperator_MINIMUM: + return "REGISTER_KERNEL(MINIMUM, Minimum)"; + case circle::BuiltinOperator_MIRROR_PAD: + return "REGISTER_KERNEL(MIRROR_PAD, MirrorPad)"; + case circle::BuiltinOperator_MUL: + return "REGISTER_KERNEL(MUL, Mul)"; + case circle::BuiltinOperator_NEG: + return "REGISTER_KERNEL(NEG, Neg)"; + case circle::BuiltinOperator_NOT_EQUAL: + return "REGISTER_KERNEL(NOT_EQUAL, NotEqual)"; + case circle::BuiltinOperator_PAD: + return "REGISTER_KERNEL(PAD, Pad)"; + case circle::BuiltinOperator_PADV2: + return "REGISTER_KERNEL(PADV2, PadV2)"; + case circle::BuiltinOperator_PRELU: + return "REGISTER_KERNEL(PRELU, PRelu)"; + case circle::BuiltinOperator_QUANTIZE: + return "REGISTER_KERNEL(QUANTIZE, Quantize)"; + case circle::BuiltinOperator_RESHAPE: + return "REGISTER_KERNEL(RESHAPE, Reshape)"; + case circle::BuiltinOperator_RESIZE_BILINEAR: + return "REGISTER_KERNEL(RESIZE_BILINEAR, ResizeBilinear)"; + case circle::BuiltinOperator_RESIZE_NEAREST_NEIGHBOR: + return "REGISTER_KERNEL(RESIZE_NEAREST_NEIGHBOR, ResizeNearestNeighbor)"; + case circle::BuiltinOperator_RSQRT: + return "REGISTER_KERNEL(RSQRT, Rsqrt)"; + case circle::BuiltinOperator_SHAPE: + return "REGISTER_KERNEL(SHAPE, Shape)"; + case circle::BuiltinOperator_SOFTMAX: + return "REGISTER_KERNEL(SOFTMAX, Softmax)"; + case circle::BuiltinOperator_SPACE_TO_BATCH_ND: + return "REGISTER_KERNEL(SPACE_TO_BATCH_ND, SpaceToBatchND)"; + case circle::BuiltinOperator_SPACE_TO_DEPTH: + return "REGISTER_KERNEL(SPACE_TO_DEPTH, SpaceToDepth)"; + case circle::BuiltinOperator_STRIDED_SLICE: + return "REGISTER_KERNEL(STRIDED_SLICE, StridedSlice)"; + case circle::BuiltinOperator_SQRT: + return "REGISTER_KERNEL(SQRT, Sqrt)"; + case circle::BuiltinOperator_SQUARE: + return "REGISTER_KERNEL(SQUARE, Square)"; + case circle::BuiltinOperator_SQUARED_DIFFERENCE: + return "REGISTER_KERNEL(SQUARED_DIFFERENCE, SquaredDifference)"; + case circle::BuiltinOperator_SQUEEZE: + return "REGISTER_KERNEL(SQUEEZE, Squeeze)"; + case circle::BuiltinOperator_SUB: + return "REGISTER_KERNEL(SUB, Sub)"; + case circle::BuiltinOperator_SVDF: + return "REGISTER_KERNEL(SVDF, SVDF)"; + case circle::BuiltinOperator_TANH: + return "REGISTER_KERNEL(TANH, Tanh)"; + case circle::BuiltinOperator_TRANSPOSE: + return "REGISTER_KERNEL(TRANSPOSE, Transpose)"; + case circle::BuiltinOperator_TRANSPOSE_CONV: + return "REGISTER_KERNEL(TRANSPOSE_CONV, TransposeConv)"; + case circle::BuiltinOperator_UNIDIRECTIONAL_SEQUENCE_LSTM: + return "REGISTER_KERNEL(UNIDIRECTIONAL_SEQUENCE_LSTM, UnidirectionalSequenceLSTM)"; + default: + assert(false && "Not supported kernel"); + } +} + +std::vector loadFile(const std::string &path) +{ + std::ifstream file(path, std::ios::binary | std::ios::in); + if (!file.good()) + { + assert(false && "Failed to open file"); + } + + file.unsetf(std::ios::skipws); + + file.seekg(0, std::ios::end); + auto fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + // reserve capacity + std::vector data(fileSize); + + // read the data + file.read(data.data(), fileSize); + if (file.fail()) + { + assert(false && "Failed to read file"); + } + + return data; +} + +// Parse model and write to std::ofstream &os models operations +void run(std::ofstream &os, const circle::Model *model) +{ + luci_interpreter::CircleReader reader; + reader.parse(model); + const uint32_t subgraph_size = reader.num_subgraph(); + + // Set to avoid duplication in generated list + std::set operations_set; + + for (uint32_t g = 0; g < subgraph_size; g++) + { + reader.select_subgraph(g); + auto ops = reader.operators(); + for (uint32_t i = 0; i < ops.size(); ++i) + { + const auto op = ops.at(i); + auto op_builtin_operator = reader.builtin_code(op); + + auto result = operations_set.insert(op_builtin_operator); + if (result.second) + { + os << get_register_kernel_str(op_builtin_operator) << std::endl; + } + } + } +} + +int main(int argc, char **argv) +{ + if (argc != 3) + { + assert(false && "Should be 2 arguments: circle model path, and path for generated model\n"); + } + + std::string model_file(argv[1]); + std::string generated_file_path(argv[2]); + + std::vector model_data = loadFile(model_file); + const circle::Model *circle_model = circle::GetModel(model_data.data()); + + if (circle_model == nullptr) + { + std::cerr << "ERROR: Failed to load circle '" << model_file << "'" << std::endl; + return 255; + } + + // Open or create file + std::ofstream out; + out.open(generated_file_path); + + if (out.is_open()) + run(out, circle_model); + else + std::cout << "SMTH GOES WRONG WHILE OPEN FILE" << std::endl; + return 0; +} diff --git a/onert-micro/luci-interpreter/CMakeLists.txt b/onert-micro/luci-interpreter/CMakeLists.txt new file mode 100644 index 0000000..b07ded2 --- /dev/null +++ b/onert-micro/luci-interpreter/CMakeLists.txt @@ -0,0 +1,29 @@ +set(LUCI_INTERPRETER_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include") +set(LUCI_INTERPRETER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src") +if (NOT LUCI_INTERPRETER_PAL_DIR) + set(LUCI_INTERPRETER_PAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/pal/mcu") +endif() + +if (NOT LUCI_INTERPRETER_KERNELS_BUILD_LIST) + set(KERNEL_REGISTER_FILE "${LUCI_INTERPRETER_PAL_DIR}/KernelsToBuild.lst") +else() + set(KERNEL_REGISTER_FILE ${LUCI_INTERPRETER_KERNELS_BUILD_LIST}) +endif() + +if (NOT DEFINED CUSTOM_LUCI_INTERPRETER_SUFFIX) + set(LUCI_INTERPRETER_SUFFIX "") +else() + set(LUCI_INTERPRETER_SUFFIX ${CUSTOM_LUCI_INTERPRETER_SUFFIX}) +endif() + +if (DIS_QUANT) + add_definitions(-DDIS_QUANT) +endif() + +if (DIS_FLOAT) + add_definitions(-DDIS_FLOAT) +endif() + +add_compile_options(-fno-exceptions) +add_compile_options(-Os) +add_subdirectory(src) diff --git a/compiler/luci-micro/luci-interpreter/README.md b/onert-micro/luci-interpreter/README.md similarity index 100% rename from compiler/luci-micro/luci-interpreter/README.md rename to onert-micro/luci-interpreter/README.md diff --git a/onert-micro/luci-interpreter/include/luci_interpreter/Interpreter.h b/onert-micro/luci-interpreter/include/luci_interpreter/Interpreter.h new file mode 100644 index 0000000..483c789 --- /dev/null +++ b/onert-micro/luci-interpreter/include/luci_interpreter/Interpreter.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_INTERPRETER_H +#define LUCI_INTERPRETER_INTERPRETER_H + +#include "luci_interpreter/core/Tensor.h" + +#ifdef USE_STATIC_ALLOC +#include "luci_interpreter/InterpreterConfigure.h" +#include "memory_managers/StaticMemoryManager.h" +#else +#include "memory_managers/SimpleMemoryManager.h" +#endif // USE_STATIC_ALLOC + +#include "loader/ModuleLoader.h" +#include + +namespace luci_interpreter +{ + +class Interpreter +{ +public: + // Construct default interpreter with dynamic allocations and with input allocations + explicit Interpreter(const char *model_data_raw); + +#ifdef USE_STATIC_ALLOC + // Construct interpreter with configurations + explicit Interpreter(const char *model_data_raw, const InterpreterConfigure &configuration); +#endif // USE_STATIC_ALLOC + + ~Interpreter(); + + void allocateAndWriteInputTensor(int32_t input_tensor_index, const void *data, size_t data_size); + uint8_t *allocateInputTensor(int32_t input_tensor_index); + + uint8_t *readOutputTensor(int32_t output_tensor_index); + + int32_t getInputDataSizeByIndex(int32_t input_tensor_index); + int32_t getOutputDataSizeByIndex(int32_t output_tensor_index); + + void interpret(); + +private: + // _default_memory_manager should be before _runtime_module due to + // the order of deletion in the destructor + MemoryManager _memory_manager{}; + RuntimeModule _runtime_module{}; +}; + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_INTERPRETER_H diff --git a/onert-micro/luci-interpreter/include/luci_interpreter/InterpreterConfigure.h b/onert-micro/luci-interpreter/include/luci_interpreter/InterpreterConfigure.h new file mode 100644 index 0000000..5619c93 --- /dev/null +++ b/onert-micro/luci-interpreter/include/luci_interpreter/InterpreterConfigure.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ONERT_MICRO_INTERPRETER_CONFIGURE_H +#define ONERT_MICRO_INTERPRETER_CONFIGURE_H + +namespace luci_interpreter +{ +#ifdef USE_STATIC_ALLOC + +enum MemoryManagerType +{ + STATIC, + DYNAMIC +}; + +class InterpreterConfigure +{ +public: + void setAllocateInputValue(bool allocate_input) { _allocate_input = allocate_input; } + bool getAllocateInputValue() const { return _allocate_input; } + + InterpreterConfigure &setMemoryManager(MemoryManagerType mm_type) + { + switch (mm_type) + { + case MemoryManagerType::STATIC: + _use_static_manager = true; + break; + case MemoryManagerType::DYNAMIC: + _use_static_manager = false; + break; + default: + assert(false); + } + return *this; + } + + // TODO: remove this method + InterpreterConfigure &configStaticMemoryManager(uint32_t input_buf_size, uint32_t temp_buf_size, + uint32_t output_buf_size) + { + assert(_use_static_manager); + _input_buf_size = input_buf_size; + _temp_buf_size = temp_buf_size; + _output_buf_size = output_buf_size; + return *this; + } + + bool isStaticManager() const { return _use_static_manager; } + +private: + bool _use_static_manager = false; + bool _allocate_input = true; + +public: + // TODO: remove it and read these values from circle file + uint32_t _input_buf_size = 0; + uint32_t _temp_buf_size = 0; + uint32_t _output_buf_size = 0; +}; + +#endif + +} // namespace luci_interpreter + +#endif // ONERT_MICRO_INTERPRETER_CONFIGURE_H diff --git a/onert-micro/luci-interpreter/include/luci_interpreter/core/DataType.h b/onert-micro/luci-interpreter/include/luci_interpreter/core/DataType.h new file mode 100644 index 0000000..e34d224 --- /dev/null +++ b/onert-micro/luci-interpreter/include/luci_interpreter/core/DataType.h @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_CORE_DATATYPE_H +#define LUCI_INTERPRETER_CORE_DATATYPE_H + +#include +#include +#include +#include + +namespace luci_interpreter +{ +// TODO check can we remove it +/** + * @brief "scalar" value type + */ +enum class DataType +{ + Unknown, // Unknown type (serves as a default value) + + U8, // 8-bit unsigned integer + U16, // 16-bit unsigned integer + U32, // 32-bit unsigned integer + U64, // 64-bit unsigned integer + + S8, // 8-bit signed integer + S16, // 16-bit signed integer + S32, // 32-bit signed integer + S64, // 64-bit signed integer + + FLOAT16, // IEEE 16-bit floating-point + FLOAT32, // IEEE 32-bit floating-point + FLOAT64, // IEEE 64-bit floating-point + + // WARNING the size of Bool may vary for NN frameworks + // TODO we need to find a way to resolve this issue + BOOL, // Boolean +}; + +/** + * @brief C++ scalar type corresponding to each DataType + */ +template struct DataTypeImpl +{ + // using Type = ... +}; + +// TODO Support other enum values +template <> struct DataTypeImpl +{ + // Use C++ int8_t type for 8bit integer + using Type = int8_t; +}; + +template <> struct DataTypeImpl +{ + // Use C++ uint8_t type for unsigned 8bit integer + using Type = uint8_t; +}; + +template <> struct DataTypeImpl +{ + // Use C++ int16_t type for 16bit integer + using Type = int16_t; +}; + +template <> struct DataTypeImpl +{ + // Use C++ uint16_t type for unsigned 16bit integer + using Type = uint16_t; +}; + +template <> struct DataTypeImpl +{ + // Use C++ int32_t type for 32bit integer + using Type = int32_t; +}; + +template <> struct DataTypeImpl +{ + // Use C++ uint32_t type for unsigned 32bit integer + using Type = uint32_t; +}; + +template <> struct DataTypeImpl +{ + // Use C++ int64_t type for 64bit integer + using Type = int64_t; +}; + +template <> struct DataTypeImpl +{ + // Use C++ uint64_t type for unsigned 64bit integer + using Type = uint64_t; +}; + +template <> struct DataTypeImpl +{ + // float16 type with 16bit value, encoded with help of FP16 library + // https://github.com/Maratyszcza/FP16/ + using Type = uint16_t; +}; + +template <> struct DataTypeImpl +{ + // Use C++ float type for IEEE 32-bit floating-point numbers + using Type = float; +}; + +template <> struct DataTypeImpl +{ + // Use C++ double type for IEEE 64-bit floating-point numbers + using Type = double; +}; + +// NOTE DataTypeImpl for BOOL is subject to change +template <> struct DataTypeImpl +{ + // Use C++ uint8_t type for bool + using Type = uint8_t; +}; + +/** + * @brief Returns the size of the data type. + * @note If you need the size at compile time, use `sizeof(typename DataTypeImpl
::Type)`. + */ +inline uint32_t size(DataType data_type) +{ + switch (data_type) + { + case DataType::S8: + return sizeof(DataTypeImpl::Type); + case DataType::U8: + return sizeof(DataTypeImpl::Type); + case DataType::S16: + return sizeof(DataTypeImpl::Type); + case DataType::U16: + return sizeof(DataTypeImpl::Type); + case DataType::S32: + return sizeof(DataTypeImpl::Type); + case DataType::U32: + return sizeof(DataTypeImpl::Type); + case DataType::S64: + return sizeof(DataTypeImpl::Type); + case DataType::U64: + return sizeof(DataTypeImpl::Type); + case DataType::FLOAT16: + return sizeof(DataTypeImpl::Type); + case DataType::FLOAT32: + return sizeof(DataTypeImpl::Type); + case DataType::FLOAT64: + return sizeof(DataTypeImpl::Type); + case DataType::BOOL: + return sizeof(DataTypeImpl::Type); + default: + // TODO Support remaining data types. + assert(false); + return UINT32_MAX; // Avoid compiler warning. + } +} + +inline size_t getDataTypeSize(DataType data_type) { return size(data_type); } + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_CORE_DATATYPE_H diff --git a/onert-micro/luci-interpreter/include/luci_interpreter/core/ParamsType.h b/onert-micro/luci-interpreter/include/luci_interpreter/core/ParamsType.h new file mode 100644 index 0000000..af0687f --- /dev/null +++ b/onert-micro/luci-interpreter/include/luci_interpreter/core/ParamsType.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __LUCI_MICRO_INTERPRETER_PARAMS_TYPE_H__ +#define __LUCI_MICRO_INTERPRETER_PARAMS_TYPE_H__ + +#include +#include +#include + +namespace luci_interpreter +{ +// TODO check can we remove it +enum class FusedActFunc +{ + UNDEFINED, // This is not defined by TFLite or Circle. This was added to + // prevent programming error. + NONE, + RELU, + RELU_N1_TO_1, + RELU6, + TANH, + SIGN_BIT +}; + +enum class Padding +{ + UNDEFINED, // This is not defined by TFLite. This was added to prevent programming error. + + SAME, + VALID, +}; + +enum class MirrorPadMode +{ + UNDEFINED, // This is not defined by Circle. This was added to prevent programming error. + + REFLECT, + SYMMETRIC, +}; + +} // namespace luci_interpreter + +#endif // __LUCI_MICRO_INTERPRETER_PARAMS_TYPE_H__ diff --git a/onert-micro/luci-interpreter/include/luci_interpreter/core/Tensor.h b/onert-micro/luci-interpreter/include/luci_interpreter/core/Tensor.h new file mode 100644 index 0000000..318ec0e --- /dev/null +++ b/onert-micro/luci-interpreter/include/luci_interpreter/core/Tensor.h @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_CORE_TENSOR_H +#define LUCI_INTERPRETER_CORE_TENSOR_H + +#include "luci_interpreter/core/DataType.h" +#include "luci_interpreter/core/reader/CircleMicroReader.h" + +#include +#include +#include +#include +#include +#include + +namespace luci_interpreter +{ + +class Tensor +{ +public: +#ifndef DIS_QUANT + static float scale(const circle::Tensor *circle_tensor) + { + const auto *quant_params = circle_tensor->quantization(); + if (quant_params == nullptr) + { + assert(false && "There is no quantization params"); + return 0; + } + + return *quant_params->scale()->cbegin(); + } + + static int32_t zero_point(const circle::Tensor *circle_tensor) + { + const auto *quant_params = circle_tensor->quantization(); + if (quant_params == nullptr) + { + assert(false && "There is no quantization params"); + return 0; + } + + return *quant_params->zero_point()->cbegin(); + } + + static const std::vector scales(const circle::Tensor *circle_tensor) + { + const auto *quant_params = circle_tensor->quantization(); + if (quant_params == nullptr) + { + assert(false && "There is no quantization params"); + return {}; + } + assert(quant_params->scale() != nullptr); + std::vector scales(quant_params->scale()->cbegin(), quant_params->scale()->cend()); + + return scales; + } + + static const std::vector zero_points(const circle::Tensor *circle_tensor) + { + const auto *quant_params = circle_tensor->quantization(); + if (quant_params == nullptr) + { + assert(false && "There is no quantization params"); + return {}; + } + assert(quant_params->zero_point() != nullptr); + std::vector zero_points(quant_params->zero_point()->cbegin(), + quant_params->zero_point()->cend()); + + return zero_points; + } + + static int32_t quantized_dimension(const circle::Tensor *circle_tensor) + { + const auto *quant_params = circle_tensor->quantization(); + if (quant_params == nullptr) + { + assert(false && "There is no quantization params"); + return 0; + } + return quant_params->quantized_dimension(); + } +#endif + + static DataType element_type(const circle::Tensor *circle_tensor) + { + return luci_datatype(circle_tensor->type()); + } + + static int num_dims(const circle::Tensor *circle_tensor) + { + // TODO check removing of wrap + auto const &const_dims = wrap(circle_tensor->shape()); + return const_dims.size(); + } + + static int32_t dim(const circle::Tensor *circle_tensor, int i) + { + // TODO check removing of wrap + assert(i >= 0); + auto const &const_dims = wrap(circle_tensor->shape()); + assert(i < const_dims.size()); + + return const_dims[i]; + } + + static int32_t num_elements(const circle::Tensor *circle_tensor) + { + int32_t result = 1; + auto const &const_dims = wrap(circle_tensor->shape()); + for (const int32_t dim : const_dims) + { + result *= dim; + } + return result; + } +}; + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_CORE_TENSOR_H diff --git a/onert-micro/luci-interpreter/include/luci_interpreter/core/reader/CircleMicroReader.h b/onert-micro/luci-interpreter/include/luci_interpreter/core/reader/CircleMicroReader.h new file mode 100644 index 0000000..3967e40 --- /dev/null +++ b/onert-micro/luci-interpreter/include/luci_interpreter/core/reader/CircleMicroReader.h @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __LUCI_MICRO_INTERPRETER_MICRO_READER_H__ +#define __LUCI_MICRO_INTERPRETER_MICRO_READER_H__ + +#include "luci_interpreter/core/ParamsType.h" +#include "luci_interpreter/core/DataType.h" + +#include + +#include +#include +#include + +namespace luci_interpreter +{ + +#ifdef USE_STATIC_ALLOC +namespace +{ + +using ExecutionPlanTable = std::map>; + +template uint32_t read_u32(const VECTORTYPE &buffer, uint32_t idx) +{ + static_assert(std::is_same::value, "Types mismatch!"); + + uint32_t val = 0; + val += (buffer.at(idx + 0) << 0 * 8); + val += (buffer.at(idx + 1) << 1 * 8); + val += (buffer.at(idx + 2) << 2 * 8); + val += (buffer.at(idx + 3) << 3 * 8); + return val; +} + +} // namespace + +namespace read_metadata +{ + +template +ExecutionPlanTable decode_execution_plan(const VECTORTYPE &execution_plan_data) +{ + static_assert(std::is_same::value, "Types mismatch!"); + + ExecutionPlanTable execution_plan_table; + uint32_t idx = 0; + + if (execution_plan_data.size() < 4) + assert(false && "Op table decode error : invalid entry number"); + + uint32_t entry_number = read_u32(execution_plan_data, idx); + idx += sizeof(uint32_t); + + while (idx < execution_plan_data.size()) + { + if (idx + 2 * sizeof(uint32_t) > execution_plan_data.size()) + assert(false && "Op table decode error : invalid entry item"); + + uint32_t id = read_u32(execution_plan_data, idx); + idx += sizeof(uint32_t); + + uint32_t size = read_u32(execution_plan_data, idx); + + if (size == 0) + assert(false && "Op table decode error : empty execution plan entry"); + + idx += sizeof(uint32_t); + + if (idx + sizeof(uint32_t) * size > execution_plan_data.size()) + assert(false && "Source table decode error : invalid entry data"); + + std::vector execution_plan_vector; + uint32_t position = read_u32(execution_plan_data, idx); + idx += sizeof(uint32_t); + + for (uint32_t j = 1; j < size; ++j) + { + uint32_t execution_plan_inform = read_u32(execution_plan_data, idx); + idx += sizeof(uint32_t); + + execution_plan_vector.push_back(execution_plan_inform); + } + + if (!execution_plan_table.insert({position, execution_plan_vector}).second) + assert(false && "Op table decode error : duplicated origin ID"); + } + + if (idx != execution_plan_data.size()) + assert(false && "Op table decode error : data size invalid"); + + if (execution_plan_table.size() != entry_number) + assert(false && "Op table decode error : entry number invalid"); + + return execution_plan_table; +} + +} // namespace read_metadata +#endif + +DataType luci_datatype(circle::TensorType type); +FusedActFunc luci_actfunc(circle::ActivationFunctionType type); +Padding luci_padding(circle::Padding padding); +MirrorPadMode luci_mirrorpad_mode(circle::MirrorPadMode mode); + +/** + * @brief Wrapper to use flatbuffers::Vector pointer as std::vector entity + */ +template class VectorWrapper +{ +public: + explicit VectorWrapper(const flatbuffers::Vector *ptr); + + const T *data() const; + uint32_t size() const; + + using iterator = typename flatbuffers::Vector::const_iterator; + iterator begin() const; + iterator end() const; + + using value_type = typename flatbuffers::Vector::return_type; + value_type at(uint32_t i) const; + value_type operator[](uint32_t i) const; + + bool null() const; + bool empty() const; + +private: + const flatbuffers::Vector *_vector; +}; + +template VectorWrapper wrap(const flatbuffers::Vector *vec) +{ + return VectorWrapper(vec); +} + +/** + * @brief Loads Circle file and provides helpers to access attributes + */ +class CircleReader +{ +public: + using CircleBuffers = VectorWrapper>; + using CircleTensors = VectorWrapper>; + using CircleOperators = VectorWrapper>; + using CircleOperatorCodes = VectorWrapper>; + using CircleMetadataSet = VectorWrapper>; + +public: + CircleReader() = default; + +public: // direct API + CircleOperatorCodes opcodes() const { return wrap(_model->operator_codes()); } + CircleBuffers buffers() const { return wrap(_model->buffers()); } + CircleTensors tensors() const { return wrap(_current_subgraph->tensors()); } + CircleOperators operators() const { return wrap(_current_subgraph->operators()); } + VectorWrapper inputs() const { return wrap(_current_subgraph->inputs()); } + VectorWrapper outputs() const { return wrap(_current_subgraph->outputs()); } + circle::DataFormat data_format() const { return _current_subgraph->data_format(); } + CircleMetadataSet metadata() const { return wrap(_model->metadata()); } + + uint32_t num_subgraph() const { return wrap(_model->subgraphs()).size(); } + circle::BuiltinOperator builtin_code(const circle::Operator *op) const; + +public: + bool parse(const circle::Model *model); + bool select_subgraph(uint32_t subgraph); + +private: + const circle::Model *_model{nullptr}; + const circle::SubGraph *_current_subgraph{nullptr}; +}; + +} // namespace luci_interpreter + +#endif // __LUCI_MICRO_INTERPRETER_MICRO_READER_H__ diff --git a/onert-micro/luci-interpreter/include/luci_interpreter/core/reader/CircleMicroReaderHelper.h b/onert-micro/luci-interpreter/include/luci_interpreter/core/reader/CircleMicroReaderHelper.h new file mode 100644 index 0000000..c1122cf --- /dev/null +++ b/onert-micro/luci-interpreter/include/luci_interpreter/core/reader/CircleMicroReaderHelper.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __LUCI_MICRO_CIRCLE_MICRO_READER_HELPER_H__ +#define __LUCI_MICRO_CIRCLE_MICRO_READER_HELPER_H__ + +#include + +#include + +namespace circle +{ + +::circle::BuiltinOperator builtin_code_neutral(const ::circle::OperatorCode *opcode); +bool is_valid(const ::circle::OperatorCode *opcode); +bool is_custom(const ::circle::OperatorCode *opcode); +const char *tensor_type(const ::circle::Tensor *tensor); + +template std::vector as_index_vector(const flatbuffers::Vector *flat_array) +{ + if (flat_array == nullptr) + { + assert(false && "flat array is nullptr"); + } + + std::vector ret(flat_array->Length()); + for (uint32_t i = 0; i < flat_array->Length(); i++) + { + ret[i] = flat_array->Get(i); + } + return ret; +} + +} // namespace circle + +#endif // __LUCI_MICRO_CIRCLE_MICRO_READER_HELPER_H__ diff --git a/onert-micro/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst b/onert-micro/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst new file mode 100644 index 0000000..353e117 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/cmsisnn/KernelsToBuild.lst @@ -0,0 +1,61 @@ +REGISTER_KERNEL(ADD, Add) +REGISTER_KERNEL(ARG_MAX, ArgMax) +REGISTER_KERNEL(AVERAGE_POOL_2D, AveragePool2D) +REGISTER_KERNEL(BATCH_TO_SPACE_ND, BatchToSpaceND) +REGISTER_KERNEL(CAST, Cast) +REGISTER_KERNEL(CONCATENATION, Concatenation) +REGISTER_KERNEL(CONV_2D, Conv2D) +REGISTER_KERNEL(DEPTH_TO_SPACE, DepthToSpace) +REGISTER_KERNEL(DEPTHWISE_CONV_2D, DepthwiseConv2D) +REGISTER_KERNEL(DEQUANTIZE, Dequantize) +REGISTER_KERNEL(DIV, Div) +REGISTER_KERNEL(ELU, Elu) +REGISTER_KERNEL(EXP, Exp) +REGISTER_KERNEL(EXPAND_DIMS, ExpandDims) +REGISTER_KERNEL(FILL, Fill) +REGISTER_KERNEL(FLOOR, Floor) +REGISTER_KERNEL(FLOOR_DIV, FloorDiv) +REGISTER_KERNEL(EQUAL, Equal) +REGISTER_KERNEL(FULLY_CONNECTED, FullyConnected) +REGISTER_KERNEL(GREATER, Greater) +REGISTER_KERNEL(GREATER_EQUAL, GreaterEqual) +REGISTER_KERNEL(INSTANCE_NORM, InstanceNorm) +REGISTER_KERNEL(L2_NORMALIZATION, L2Normalize) +REGISTER_KERNEL(L2_POOL_2D, L2Pool2D) +REGISTER_KERNEL(LEAKY_RELU, LeakyRelu) +REGISTER_KERNEL(LESS, Less) +REGISTER_KERNEL(LESS_EQUAL, LessEqual) +REGISTER_KERNEL(LOGICAL_AND, LogicalAnd) +REGISTER_KERNEL(LOGICAL_NOT, LogicalNot) +REGISTER_KERNEL(LOGICAL_OR, LogicalOr) +REGISTER_KERNEL(LOGISTIC, Logistic) +REGISTER_KERNEL(MAXIMUM, Maximum) +REGISTER_KERNEL(MAX_POOL_2D, MaxPool2D) +REGISTER_KERNEL(MINIMUM, Minimum) +REGISTER_KERNEL(MIRROR_PAD, MirrorPad) +REGISTER_KERNEL(MUL, Mul) +REGISTER_KERNEL(NEG, Neg) +REGISTER_KERNEL(NOT_EQUAL, NotEqual) +REGISTER_KERNEL(PAD, Pad) +REGISTER_KERNEL(PADV2, PadV2) +REGISTER_KERNEL(PRELU, PRelu) +REGISTER_KERNEL(QUANTIZE, Quantize) +REGISTER_KERNEL(RESHAPE, Reshape) +REGISTER_KERNEL(RESIZE_BILINEAR, ResizeBilinear) +REGISTER_KERNEL(RESIZE_NEAREST_NEIGHBOR, ResizeNearestNeighbor) +REGISTER_KERNEL(RSQRT, Rsqrt) +REGISTER_KERNEL(SHAPE, Shape) +REGISTER_KERNEL(SOFTMAX, Softmax) +REGISTER_KERNEL(SPACE_TO_BATCH_ND, SpaceToBatchND) +REGISTER_KERNEL(SPACE_TO_DEPTH, SpaceToDepth) +REGISTER_KERNEL(STRIDED_SLICE, StridedSlice) +REGISTER_KERNEL(SQRT, Sqrt) +REGISTER_KERNEL(SQUARE, Square) +REGISTER_KERNEL(SQUARED_DIFFERENCE, SquaredDifference) +REGISTER_KERNEL(SQUEEZE, Squeeze) +REGISTER_KERNEL(SUB, Sub) +REGISTER_KERNEL(SVDF, SVDF) +REGISTER_KERNEL(TANH, Tanh) +REGISTER_KERNEL(TRANSPOSE, Transpose) +REGISTER_KERNEL(TRANSPOSE_CONV, TransposeConv) +REGISTER_KERNEL(UNIDIRECTIONAL_SEQUENCE_LSTM, UnidirectionalSequenceLSTM) diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALArgMax.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALArgMax.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALArgMax.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALArgMax.h diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALAveragePool2d.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALAveragePool2d.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALAveragePool2d.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALAveragePool2d.h diff --git a/onert-micro/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h new file mode 100644 index 0000000..f8a4a80 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/cmsisnn/PALBatchToSpaceND.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H +#define LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H + +#include + +namespace luci_interpreter_pal +{ +template +static inline void +BatchToSpaceND(const tflite::RuntimeShape &unextended_input1_shape, const T *input1_data, + const tflite::RuntimeShape &unextended_input2_shape, const int32 *block_shape_data, + const tflite::RuntimeShape &unextended_input3_shape, const int32 *crops_data, + const tflite::RuntimeShape &unextended_output_shape, T *output_data) +{ + tflite::reference_ops::BatchToSpaceND( + unextended_input1_shape, input1_data, unextended_input2_shape, block_shape_data, + unextended_input3_shape, crops_data, unextended_output_shape, output_data); +} +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H diff --git a/onert-micro/luci-interpreter/pal/cmsisnn/PALConv2d.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALConv2d.h new file mode 100644 index 0000000..bd47a88 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/cmsisnn/PALConv2d.h @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_CONV2D_H +#define LUCI_INTERPRETER_PAL_CONV2D_H + +#include +#include +#include +#include + +namespace luci_interpreter_pal +{ +static inline void Conv(const tflite::ConvParams ¶ms, const tflite::RuntimeShape &input_shape, + const float *input_data, const tflite::RuntimeShape &filter_shape, + const float *filter_data, const tflite::RuntimeShape &bias_shape, + const float *bias_data, const tflite::RuntimeShape &output_shape, + float *output_data, const tflite::RuntimeShape &scratchpad_shape, + float *scratchpad_data) +{ + (void)scratchpad_shape; + (void)scratchpad_data; + tflite::reference_ops::Conv(params, input_shape, input_data, filter_shape, filter_data, + bias_shape, bias_data, output_shape, output_data, + tflite::RuntimeShape(), nullptr); +} + +static inline void Conv(const tflite::ConvParams ¶ms, const tflite::RuntimeShape &input_shape, + const uint8 *input_data, const tflite::RuntimeShape &filter_shape, + const uint8 *filter_data, const tflite::RuntimeShape &bias_shape, + const int32 *bias_data, const tflite::RuntimeShape &output_shape, + uint8 *output_data, const tflite::RuntimeShape &scratchpad_shape, + uint8 *scratchpad_data) +{ + (void)scratchpad_shape; + (void)scratchpad_data; + tflite::reference_ops::Conv(params, input_shape, input_data, filter_shape, filter_data, + bias_shape, bias_data, output_shape, output_data, scratchpad_shape, + scratchpad_data, nullptr); +} + +static inline void ConvPerChannel(const tflite::ConvParams ¶ms, const int32_t *mult, + const int32_t *shifts, const tflite::RuntimeShape &input_shape, + const int8 *input_data, const tflite::RuntimeShape &filter_shape, + const int8 *filter_data, const tflite::RuntimeShape &bias_shape, + const int32 *bias_data, const tflite::RuntimeShape &output_shape, + int8 *output_data, const tflite::RuntimeShape &scratchpad_shape, + int8 *scratchpad_data) +{ + if (scratchpad_data) + { + cmsis_nn_conv_params conv_params; + conv_params.dilation.h = params.dilation_height_factor; + conv_params.dilation.w = params.dilation_width_factor; + + assert(conv_params.dilation.h == 1); + assert(conv_params.dilation.w == 1); + + conv_params.input_offset = params.input_offset; + conv_params.output_offset = params.output_offset; + conv_params.stride.h = params.stride_height; + conv_params.stride.w = params.stride_width; + conv_params.padding.h = params.padding_values.height; + conv_params.padding.w = params.padding_values.width; + conv_params.activation.min = params.quantized_activation_min; + conv_params.activation.max = params.quantized_activation_max; + + cmsis_nn_per_channel_quant_params quant_params; + quant_params.multiplier = const_cast(mult); + quant_params.shift = const_cast(shifts); + + assert(conv_params.activation.min <= conv_params.activation.max); + assert(input_shape.DimensionsCount() == 4); + assert(filter_shape.DimensionsCount() == 4); + assert(output_shape.DimensionsCount() == 4); + const int batch_size = tflite::MatchingDim(input_shape, 0, output_shape, 0); + const int input_depth = tflite::MatchingDim(input_shape, 3, filter_shape, 3); + const int output_depth = tflite::MatchingDim(filter_shape, 0, output_shape, 3); + if (bias_data) + { + assert(bias_shape.FlatSize() == output_depth); + } + + cmsis_nn_dims input_dims; + input_dims.n = batch_size; + input_dims.h = input_shape.Dims(1); + input_dims.w = input_shape.Dims(2); + input_dims.c = input_depth; + + cmsis_nn_dims filter_dims; + filter_dims.n = output_depth; + filter_dims.h = filter_shape.Dims(1); + filter_dims.w = filter_shape.Dims(2); + filter_dims.c = input_depth; + + cmsis_nn_dims bias_dims; + bias_dims.n = 1; + bias_dims.h = 1; + bias_dims.w = 1; + bias_dims.c = output_depth; + + cmsis_nn_dims output_dims; + output_dims.n = batch_size; + output_dims.h = output_shape.Dims(1); + output_dims.w = output_shape.Dims(2); + output_dims.c = output_depth; + + cmsis_nn_context ctx; + ctx.buf = scratchpad_data; + ctx.size = scratchpad_shape.Dims(0); + + auto res = arm_convolve_wrapper_s8(&ctx, &conv_params, &quant_params, &input_dims, input_data, + &filter_dims, filter_data, &bias_dims, bias_data, + &output_dims, output_data); + assert(res == ARM_MATH_SUCCESS); + } + else + { + tflite::reference_integer_ops::ConvPerChannel(params, mult, shifts, input_shape, input_data, + filter_shape, filter_data, bias_shape, bias_data, + output_shape, output_data); + } +} + +static inline void SetupScratchpadTensor(luci_interpreter::Tensor *scratchpad, + const luci_interpreter::DataType &input_data_type, + const tflite::ConvParams ¶ms, + const tflite::RuntimeShape &input_shape, + const tflite::RuntimeShape &filter_shape, + const tflite::RuntimeShape &output_shape) +{ + cmsis_nn_conv_params conv_params; + conv_params.dilation.h = params.dilation_height_factor; + conv_params.dilation.w = params.dilation_width_factor; + + if (input_data_type == luci_interpreter::DataType::S8 && conv_params.dilation.h == 1 && + conv_params.dilation.w == 1) + { + const int32_t batches = tflite::MatchingDim(input_shape, 0, output_shape, 0); + const int32_t input_depth = tflite::MatchingDim(input_shape, 3, filter_shape, 3); + const int32_t output_depth = tflite::MatchingDim(filter_shape, 0, output_shape, 3); + const int32_t filter_height = filter_shape.Dims(1); + const int32_t filter_width = filter_shape.Dims(2); + const int32_t output_height = output_shape.Dims(1); + const int32_t output_width = output_shape.Dims(2); + + conv_params.input_offset = params.input_offset; + conv_params.output_offset = params.output_offset; + conv_params.stride.h = params.stride_height; + conv_params.stride.w = params.stride_width; + conv_params.padding.h = params.padding_values.height; + conv_params.padding.w = params.padding_values.width; + + cmsis_nn_dims input_dims; + input_dims.n = batches; + input_dims.h = input_shape.Dims(1); + input_dims.w = input_shape.Dims(2); + input_dims.c = input_depth; + + cmsis_nn_dims filter_dims; + filter_dims.n = output_depth; + filter_dims.h = filter_height; + filter_dims.w = filter_width; + filter_dims.c = input_depth; + + cmsis_nn_dims output_dims; + output_dims.n = batches; + output_dims.h = output_height; + output_dims.w = output_width; + output_dims.c = output_depth; + + const int32_t buf_size = arm_convolve_wrapper_s8_get_buffer_size(&conv_params, &input_dims, + &filter_dims, &output_dims); + + luci_interpreter::Shape scratchpad_shape{buf_size}; + scratchpad->resize(scratchpad_shape); + } + else + { + scratchpad->set_allocatable(false); + } +} + +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_CONV2D_H diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALDepthToSpace.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALDepthToSpace.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALDepthToSpace.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALDepthToSpace.h diff --git a/onert-micro/luci-interpreter/pal/cmsisnn/PALDepthwiseConv2d.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALDepthwiseConv2d.h new file mode 100644 index 0000000..46878f4 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/cmsisnn/PALDepthwiseConv2d.h @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_DEPTHWISECONV2D_H +#define LUCI_INTERPRETER_PAL_DEPTHWISECONV2D_H + +#include +#include +#include +#include + +namespace luci_interpreter_pal +{ +template +static inline void +DepthwiseConvPerChannel(const tflite::DepthwiseParams ¶ms, const int32_t *output_multiplier, + const int32_t *output_shift, const tflite::RuntimeShape &input_shape, + const T *input_data, const tflite::RuntimeShape &filter_shape, + const T *filter_data, const tflite::RuntimeShape &bias_shape, + const int32_t *bias_data, const tflite::RuntimeShape &output_shape, + T *output_data, const tflite::RuntimeShape &scratchpad_shape, + T *scratchpad_data) +{ + { + // MARK: At this moment this operation is not supported + assert(false && "DepthwiseConvPerChannel NYI"); + (void)params; + (void)output_multiplier; + (void)output_shift; + (void)input_shape; + (void)output_data; + (void)input_data; + (void)filter_shape; + (void)filter_data; + (void)bias_shape; + (void)bias_data; + (void)output_shape; + (void)output_data; + (void)scratchpad_shape; + (void)scratchpad_data; + } +} + +template <> +inline void DepthwiseConvPerChannel( + const tflite::DepthwiseParams ¶ms, const int32_t *output_multiplier, + const int32_t *output_shift, const tflite::RuntimeShape &input_shape, const int8_t *input_data, + const tflite::RuntimeShape &filter_shape, const int8_t *filter_data, + const tflite::RuntimeShape &bias_shape, const int32_t *bias_data, + const tflite::RuntimeShape &output_shape, int8_t *output_data, + const tflite::RuntimeShape &scratchpad_shape, int8_t *scratchpad_data) +{ + if (scratchpad_data) + { + cmsis_nn_dw_conv_params dw_conv_params; + dw_conv_params.dilation.h = params.dilation_height_factor; + dw_conv_params.dilation.w = params.dilation_width_factor; + assert(dw_conv_params.dilation.h == 1); + assert(dw_conv_params.dilation.w == 1); + + dw_conv_params.input_offset = params.input_offset; + dw_conv_params.output_offset = params.output_offset; + dw_conv_params.stride.h = params.stride_height; + dw_conv_params.stride.w = params.stride_width; + dw_conv_params.padding.h = params.padding_values.height; + dw_conv_params.padding.w = params.padding_values.width; + + dw_conv_params.activation.min = params.quantized_activation_min; + dw_conv_params.activation.max = params.quantized_activation_max; + dw_conv_params.ch_mult = params.depth_multiplier; + + cmsis_nn_per_channel_quant_params quant_params; + int32_t output_multiplier = params.output_multiplier; + int32_t output_shift = params.output_shift; + + quant_params.multiplier = &output_multiplier; + quant_params.shift = &output_shift; + + assert(dw_conv_params.activation.min <= dw_conv_params.activation.max); + const int batch_size = tflite::MatchingDim(input_shape, 0, output_shape, 0); + const int output_depth = tflite::MatchingDim(filter_shape, 3, output_shape, 3); + if (bias_data) + { + assert(bias_shape.FlatSize() == output_depth); + } + + cmsis_nn_dims input_dims; + input_dims.n = batch_size; + input_dims.h = input_shape.Dims(1); + input_dims.w = input_shape.Dims(2); + input_dims.c = input_shape.Dims(3); + + cmsis_nn_dims filter_dims; + filter_dims.n = filter_shape.Dims(0); + filter_dims.h = filter_shape.Dims(1); + filter_dims.w = filter_shape.Dims(2); + filter_dims.c = output_depth; + + cmsis_nn_dims bias_dims; + bias_dims.n = 1; + bias_dims.h = 1; + bias_dims.w = 1; + bias_dims.c = output_depth; + + cmsis_nn_dims output_dims; + output_dims.n = batch_size; + output_dims.h = output_shape.Dims(1); + output_dims.w = output_shape.Dims(2); + output_dims.c = output_depth; + + cmsis_nn_context ctx; + ctx.buf = scratchpad_data; + ctx.size = scratchpad_shape.Dims(0); + + auto res = arm_depthwise_conv_wrapper_s8(&ctx, &dw_conv_params, &quant_params, &input_dims, + input_data, &filter_dims, filter_data, &bias_dims, + bias_data, &output_dims, output_data); + assert(res == ARM_MATH_SUCCESS); + } + else + { + tflite::reference_integer_ops::DepthwiseConvPerChannel( + params, output_multiplier, output_shift, input_shape, input_data, filter_shape, filter_data, + bias_shape, bias_data, output_shape, output_data); + } +} + +static inline void SetupScratchpadTensor(luci_interpreter::Tensor *scratchpad, + const tflite::DepthwiseParams ¶ms, + const luci_interpreter::DataType &input_data_type, + const tflite::RuntimeShape &input_shape, + const tflite::RuntimeShape &filter_shape, + const tflite::RuntimeShape &output_shape) +{ + cmsis_nn_dw_conv_params dw_conv_params; + dw_conv_params.dilation.h = params.dilation_height_factor; + dw_conv_params.dilation.w = params.dilation_width_factor; + + if (input_data_type == luci_interpreter::DataType::S8 && dw_conv_params.dilation.h == 1 && + dw_conv_params.dilation.w == 1) + { + const int batch_size = tflite::MatchingDim(input_shape, 0, output_shape, 0); + const int output_depth = tflite::MatchingDim(filter_shape, 3, output_shape, 3); + + cmsis_nn_dims input_dims; + input_dims.n = batch_size; + input_dims.h = input_shape.Dims(1); + input_dims.w = input_shape.Dims(2); + input_dims.c = input_shape.Dims(3); + + cmsis_nn_dims filter_dims; + filter_dims.n = filter_shape.Dims(0); + filter_dims.h = filter_shape.Dims(1); + filter_dims.w = filter_shape.Dims(2); + filter_dims.c = output_depth; + + cmsis_nn_dims output_dims; + output_dims.n = batch_size; + output_dims.h = output_shape.Dims(1); + output_dims.w = output_shape.Dims(2); + output_dims.c = output_depth; + + const int32_t buf_size = arm_depthwise_conv_wrapper_s8_get_buffer_size( + &dw_conv_params, &input_dims, &filter_dims, &output_dims); + + auto data_type_size = static_cast(luci_interpreter::getDataTypeSize(input_data_type)); + + luci_interpreter::Shape scratchpad_shape{buf_size * data_type_size}; + scratchpad->resize(scratchpad_shape); + } + else + { + scratchpad->set_allocatable(false); + } +} + +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_DEPTHWISECONV2D_H diff --git a/onert-micro/luci-interpreter/pal/cmsisnn/PALDequantize.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALDequantize.h new file mode 100644 index 0000000..efa6b16 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/cmsisnn/PALDequantize.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_DEQUANTIZE_H +#define LUCI_INTERPRETER_PAL_DEQUANTIZE_H + +#include "tensorflow/lite/kernels/internal/reference/integer_ops/dequantize.h" +#include "PALreference_ops.h" + +namespace luci_interpreter_pal +{ + +template +static inline void Dequantize(tflite::DequantizationParams ¶ms, + const tflite::RuntimeShape &input_shape, const T *input_data, + const tflite::RuntimeShape &output_shape, float *output_data) +{ + tflite::reference_integer_ops::Dequantize(params, input_shape, input_data, output_shape, + output_data); +} + +static inline void Dequantize(tflite::DequantizationParams ¶ms, + const tflite::RuntimeShape &input_shape, const uint8_t *input_data, + const tflite::RuntimeShape &output_shape, float *output_data) +{ + tflite::reference_ops::Dequantize(params, input_shape, input_data, output_shape, output_data); +} + +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_DEQUANTIZE_H diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALElu.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALElu.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALElu.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALElu.h diff --git a/onert-micro/luci-interpreter/pal/cmsisnn/PALFill.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALFill.h new file mode 100644 index 0000000..1448b0c --- /dev/null +++ b/onert-micro/luci-interpreter/pal/cmsisnn/PALFill.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_FILL_H +#define LUCI_INTERPRETER_PAL_FILL_H + +#include "PALreference_ops.h" + +#endif // LUCI_INTERPRETER_PAL_FILL_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALFullyConnected.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALFullyConnected.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALFullyConnected.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALFullyConnected.h diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALL2Normalize.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALL2Normalize.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALL2Normalize.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALL2Normalize.h diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALL2Pool2D.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALL2Pool2D.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALL2Pool2D.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALL2Pool2D.h diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALLeakyRelu.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALLeakyRelu.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALLeakyRelu.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALLeakyRelu.h diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALMul.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALMul.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALMul.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALMul.h diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALNeg.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALNeg.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALNeg.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALNeg.h diff --git a/onert-micro/luci-interpreter/pal/cmsisnn/PALQuantize.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALQuantize.h new file mode 100644 index 0000000..effb85d --- /dev/null +++ b/onert-micro/luci-interpreter/pal/cmsisnn/PALQuantize.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_QUANTIZE_H +#define LUCI_INTERPRETER_PAL_QUANTIZE_H + +#include "PALreference_ops.h" + +namespace luci_interpreter_pal +{ +template +static inline void Quantize(tflite::QuantizationParams ¶ms, + const tflite::RuntimeShape &input_shape, const float *input_data, + const tflite::RuntimeShape &output_shape, T *output_data) +{ + tflite::reference_ops::AffineQuantize(params, input_shape, input_data, output_shape, output_data); +} + +template +static inline void Requantize(const Input *input_data, int32_t size, + int32_t effective_scale_multiplier, int32_t effective_scale_shift, + int32_t input_zero_point, int32_t output_zero_point, + Output *output_data) +{ + tflite::reference_ops::Requantize(input_data, size, effective_scale_multiplier, + effective_scale_shift, input_zero_point, output_zero_point, + output_data); +} +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_QUANTIZE_H diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALResizeBilinear.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALResizeBilinear.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALResizeBilinear.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALResizeBilinear.h diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALResizeNearestNeighbor.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALResizeNearestNeighbor.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALResizeNearestNeighbor.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALResizeNearestNeighbor.h diff --git a/onert-micro/luci-interpreter/pal/cmsisnn/PALSVDF.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALSVDF.h new file mode 100644 index 0000000..c1f096c --- /dev/null +++ b/onert-micro/luci-interpreter/pal/cmsisnn/PALSVDF.h @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2020 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_SVDF_H +#define LUCI_INTERPRETER_PAL_SVDF_H + +#include +#include + +namespace luci_interpreter_pal +{ +static inline void +IntegerSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, + const int8_t *input_data, const tflite::RuntimeShape &weight_feature_shape, + const int8_t *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, + const int16_t *weight_time_data, const tflite::RuntimeShape &bias_shape, + const int32_t *bias_data, int16_t *activation_state_data, + const tflite::RuntimeShape &output_shape, int8_t *output_data, int32_t *scratchpad_data, + int32_t *output_temp_data, int32_t scale_1_a, int scale_1_b, int32_t scale_2_a, + int scale_2_b, int32_t input_zp, int32_t output_zp) +{ + const int32_t rank = params.rank; + const int32_t batch_size = input_shape.Dims(0); + const int32_t num_filters = weight_feature_shape.Dims(0); + const int32_t memory_size = weight_time_shape.Dims(1); + + cmsis_nn_dims input_dims; + input_dims.n = input_shape.Dims(0); + input_dims.h = input_shape.Dims(1); + + cmsis_nn_dims weights_feature_dims; + weights_feature_dims.n = weight_feature_shape.Dims(0); + weights_feature_dims.h = weight_feature_shape.Dims(1); + + cmsis_nn_dims weights_time_dims; + weights_time_dims.n = weight_time_shape.Dims(0); + weights_time_dims.h = weight_time_shape.Dims(1); + + cmsis_nn_dims bias_dims; + bias_dims.n = bias_shape.Dims(0); + + cmsis_nn_dims state_dims; + state_dims.n = batch_size; + state_dims.h = memory_size * num_filters; + + cmsis_nn_dims output_dims; + output_dims.n = output_shape.Dims(0); + output_dims.h = output_shape.Dims(1); + + cmsis_nn_svdf_params svdf_params; + svdf_params.rank = params.rank; + svdf_params.input_offset = input_zp; + svdf_params.output_offset = output_zp; + + svdf_params.input_activation.min = INT16_MIN; + svdf_params.input_activation.max = INT16_MAX; + + svdf_params.output_activation.min = INT8_MIN; + svdf_params.output_activation.max = INT8_MAX; + + cmsis_nn_per_tensor_quant_params in_quant_params; + in_quant_params.multiplier = scale_1_a; + in_quant_params.shift = scale_1_b; + + cmsis_nn_per_tensor_quant_params out_quant_params; + out_quant_params.multiplier = scale_2_a; + out_quant_params.shift = scale_2_b; + + cmsis_nn_context scratch_ctx; + scratch_ctx.buf = scratchpad_data; + + cmsis_nn_context scratch_output_ctx; + scratch_output_ctx.buf = output_temp_data; + + arm_svdf_s8(&scratch_ctx, &scratch_output_ctx, &svdf_params, &in_quant_params, &out_quant_params, + &input_dims, input_data, &state_dims, activation_state_data, &weights_feature_dims, + weight_feature_data, &weights_time_dims, weight_time_data, &bias_dims, bias_data, + &output_dims, output_data); +} +static inline void +FloatSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, + const float *input_data, const tflite::RuntimeShape &weight_feature_shape, + const float *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, + const float *weight_time_data, const tflite::RuntimeShape &bias_shape, + const float *bias_data, float *scratchpad_data, float *activation_state_data, + const tflite::RuntimeShape &output_shape, float *output_data) +{ + const int32_t rank = params.rank; + const int32_t batch_size = input_shape.Dims(0); + const int32_t input_size = input_shape.Dims(1); + const int32_t num_filters = weight_feature_shape.Dims(0); + const int32_t num_units = num_filters / rank; + const int32_t memory_size = weight_time_shape.Dims(1); + + // Left shift the activation_state. + { + float *new_state_start = activation_state_data; + const float *old_state_start = activation_state_data + 1; + const float *old_state_end = activation_state_data + batch_size * num_filters * memory_size; + while (old_state_start != old_state_end) + { + *new_state_start++ = *old_state_start++; + } + } + + // Note: no need to clear the latest activation, matmul is not accumulative. + + // Compute conv1d(inputs, weights_feature). + // The activation_state's rightmost column is used to save current cycle + // activation. This is achieved by starting at state_ptr[memory_size - 1] and + // having the stride equal to memory_size. + + // Perform batched matrix vector multiply operation: + { + const float *matrix = weight_feature_data; + const float *vector = input_data; + float *result = &activation_state_data[memory_size - 1]; + float *result_in_batch = result; + for (int i = 0; i < batch_size; ++i) + { + const float *matrix_ptr = matrix; + for (int j = 0; j < num_filters; ++j) + { + float dot_prod = 0.0f; + const float *vector_in_batch = vector + i * input_size; + for (int k = 0; k < input_size; ++k) + { + dot_prod += *matrix_ptr++ * *vector_in_batch++; + } + *result_in_batch = dot_prod; + result_in_batch += memory_size; + } + } + } + + tflite::reference_ops::ApplyTimeWeightsBiasAndActivation( + batch_size, memory_size, num_filters, num_units, rank, weight_time_data, bias_data, + params.activation, activation_state_data, scratchpad_data, output_data); +} + +static inline void SetupScratchpadTensor( + const luci_interpreter::DataType &input_data_type, + const luci_interpreter::DataType &weight_feature_data_type, + luci_interpreter::Tensor *scratchpad_1, luci_interpreter::Tensor *scratchpad_2, + luci_interpreter::Tensor *scratchpad_3, luci_interpreter::Tensor *scratchpad_4, + luci_interpreter::Tensor *scratchpad_5, luci_interpreter::Tensor *scratchpad_6, + const luci_interpreter::Shape input_shape, const luci_interpreter::Shape weight_time_shape, + const int32_t batch_size, const int32_t num_filters, const int32_t num_units) +{ + if (input_data_type == luci_interpreter::DataType::FLOAT32 && + (weight_feature_data_type == luci_interpreter::DataType::S8 || + weight_feature_data_type == luci_interpreter::DataType::U8)) + { + (void)input_shape; + (void)weight_time_shape; + (void)scratchpad_3; + (void)scratchpad_4; + (void)scratchpad_5; + (void)scratchpad_6; + + assert(false && "Hybrid type is not supported for cmsisnn"); + } + + // Resize scratchpad_1 tensor + scratchpad_1->resize({batch_size, num_filters}); + + if (input_data_type == luci_interpreter::DataType::S8) + { + // Resize scratchpad_2 for full_integer op + scratchpad_2->resize({batch_size, num_units}); + } +} + +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_SVDF_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSoftmax.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALSoftmax.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSoftmax.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALSoftmax.h diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALSpaceToBatchND.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALSpaceToBatchND.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALSpaceToBatchND.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALSpaceToBatchND.h diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALSpaceToDepth.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALSpaceToDepth.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALSpaceToDepth.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALSpaceToDepth.h diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALSub.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALSub.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALSub.h rename to onert-micro/luci-interpreter/pal/cmsisnn/PALSub.h diff --git a/onert-micro/luci-interpreter/pal/cmsisnn/PALUnidirectionalSequenceLSTM.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALUnidirectionalSequenceLSTM.h new file mode 100644 index 0000000..1a86e74 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/cmsisnn/PALUnidirectionalSequenceLSTM.h @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_UNIDIRECTIONAL_SEQUENCE_LSTM_H +#define LUCI_INTERPRETER_PAL_UNIDIRECTIONAL_SEQUENCE_LSTM_H + +#include "arm_nnfunctions.h" +#include "core/KernelParams.h" +#include "tensorflow/lite/kernels/internal/reference/integer_ops/logistic.h" +#include "tensorflow/lite/kernels/internal/reference/integer_ops/tanh.h" +#include "fixedpoint/fixedpoint.h" + +namespace luci_interpreter_pal +{ +namespace lstm +{ + +inline cmsis_nn_lstm_params +convert_lstm_params(const luci_interpreter::IntegerLSTMParams ¶ms_in, bool time_major, + int32_t output_zeropoint, const int32_t *input_gate_bias, + const int32_t *forget_gate_bias, const int32_t *cell_gate_bias, + const int32_t *output_gate_bias, int16_t *input_layer_norm_coefficients, + int16_t *forget_layer_norm_coefficients, int16_t *cell_layer_norm_coefficients, + int16_t *output_layer_norm_coefficients) +{ + cmsis_nn_lstm_params params_out; + + params_out.time_major = time_major; + + // Multipliers and shifts for weights + params_out.input_to_input_scaling.multiplier = params_in.effective_input_to_input_scale_a; + params_out.input_to_input_scaling.shift = params_in.effective_input_to_input_scale_b; + params_out.recurrent_to_input_scaling.multiplier = params_in.effective_recurrent_to_input_scale_a; + params_out.recurrent_to_input_scaling.shift = params_in.effective_recurrent_to_input_scale_b; + params_out.cell_to_input_scaling.multiplier = params_in.effective_cell_to_input_scale_a; + params_out.cell_to_input_scaling.shift = params_in.effective_cell_to_input_scale_b; + params_out.input_to_forget_scaling.multiplier = params_in.effective_input_to_forget_scale_a; + params_out.input_to_forget_scaling.shift = params_in.effective_input_to_forget_scale_b; + params_out.recurrent_to_forget_scaling.multiplier = + params_in.effective_recurrent_to_forget_scale_a; + params_out.recurrent_to_forget_scaling.shift = params_in.effective_recurrent_to_forget_scale_b; + params_out.cell_to_forget_scaling.multiplier = params_in.effective_cell_to_forget_scale_a; + params_out.cell_to_forget_scaling.shift = params_in.effective_cell_to_forget_scale_b; + params_out.input_to_cell_scaling.multiplier = params_in.effective_input_to_cell_scale_a; + params_out.input_to_cell_scaling.shift = params_in.effective_input_to_cell_scale_b; + params_out.recurrent_to_cell_scaling.multiplier = params_in.effective_recurrent_to_cell_scale_a; + params_out.recurrent_to_cell_scaling.shift = params_in.effective_recurrent_to_cell_scale_b; + params_out.input_to_output_scaling.multiplier = params_in.effective_input_to_output_scale_a; + params_out.input_to_output_scaling.shift = params_in.effective_input_to_output_scale_b; + + params_out.recurrent_to_output_scaling.multiplier = + params_in.effective_recurrent_to_output_scale_a; + params_out.recurrent_to_output_scaling.shift = params_in.effective_recurrent_to_output_scale_b; + params_out.cell_to_output_scaling.multiplier = params_in.effective_cell_to_output_scale_a; + params_out.cell_to_output_scaling.shift = params_in.effective_cell_to_output_scale_b; + params_out.projection_scaling.multiplier = params_in.effective_proj_scale_a; + params_out.projection_scaling.shift = params_in.effective_proj_scale_b; + + params_out.layer_norm_input_scaling.multiplier = params_in.layer_norm_input_scale_a; + params_out.layer_norm_input_scaling.shift = params_in.layer_norm_input_scale_b; + params_out.layer_norm_forget_scaling.multiplier = params_in.layer_norm_forget_scale_a; + params_out.layer_norm_forget_scaling.shift = params_in.layer_norm_forget_scale_b; + params_out.layer_norm_cell_scaling.multiplier = params_in.layer_norm_cell_scale_a; + params_out.layer_norm_cell_scaling.shift = params_in.layer_norm_cell_scale_b; + params_out.layer_norm_output_scaling.multiplier = params_in.layer_norm_output_scale_a; + params_out.layer_norm_output_scaling.shift = params_in.layer_norm_output_scale_b; + + params_out.clip.cell = params_in.quantized_cell_clip; + params_out.clip.projection = params_in.quantized_proj_clip; + + params_out.cell_state_shift = params_in.cell_scale; + + params_out.hidden_offset = params_in.hidden_zp; + params_out.output_state_offset = output_zeropoint; + + params_out.guard.input_variance = params_in.input_variance_guard; + params_out.guard.forget_variance = params_in.forget_variance_guard; + params_out.guard.cell_variance = params_in.cell_variance_guard; + params_out.guard.output_variance = params_in.output_variance_guard; + + params_out.i2f_effective_bias = params_in.input_to_forget_effective_bias.data(); + params_out.r2f_effective_bias = params_in.recurrent_to_forget_effective_bias.data(); + params_out.i2c_effective_bias = params_in.input_to_cell_effective_bias.data(); + params_out.r2c_effective_bias = params_in.recurrent_to_cell_effective_bias.data(); + params_out.i2o_effective_bias = params_in.input_to_output_effective_bias.data(); + params_out.r2o_effective_bias = params_in.recurrent_to_output_effective_bias.data(); + params_out.i2i_effective_bias = params_in.input_to_input_effective_bias.data(); + params_out.r2i_effective_bias = params_in.recurrent_to_input_effective_bias.data(); + params_out.projection_effective_bias = params_in.projection_effective_bias.data(); + + params_out.hidden_scaling.multiplier = params_in.effective_hidden_scale_a; + params_out.hidden_scaling.shift = params_in.effective_hidden_scale_b; + + params_out.input_gate_bias = input_gate_bias; + params_out.forget_gate_bias = forget_gate_bias; + params_out.cell_gate_bias = cell_gate_bias; + params_out.output_gate_bias = output_gate_bias; + + params_out.layer_norm.input_weight = input_layer_norm_coefficients; + params_out.layer_norm.forget_weight = forget_layer_norm_coefficients; + params_out.layer_norm.cell_weight = cell_layer_norm_coefficients; + params_out.layer_norm.output_weight = output_layer_norm_coefficients; + + params_out.activation.min = std::numeric_limits::min(); + params_out.activation.max = std::numeric_limits::max(); + + return params_out; +} + +} // namespace lstm + +void eval_integer_8x8_16_lstm( + const luci_interpreter::Tensor *input, const luci_interpreter::Tensor *input_to_input_weights, + const luci_interpreter::Tensor *input_to_forget_weights, + const luci_interpreter::Tensor *input_to_cell_weights, + const luci_interpreter::Tensor *input_to_output_weights, + const luci_interpreter::Tensor *recurrent_to_input_weights, + const luci_interpreter::Tensor *recurrent_to_forget_weights, + const luci_interpreter::Tensor *recurrent_to_cell_weights, + const luci_interpreter::Tensor *recurrent_to_output_weights, + const luci_interpreter::Tensor *cell_to_input_weights, + const luci_interpreter::Tensor *cell_to_forget_weights, + const luci_interpreter::Tensor *cell_to_output_weights, + const luci_interpreter::Tensor *input_layer_norm_coefficients, + const luci_interpreter::Tensor *forget_layer_norm_coefficients, + const luci_interpreter::Tensor *cell_layer_norm_coefficients, + const luci_interpreter::Tensor *output_layer_norm_coefficients, + const luci_interpreter::Tensor *input_gate_bias, const luci_interpreter::Tensor *forget_gate_bias, + const luci_interpreter::Tensor *cell_gate_bias, const luci_interpreter::Tensor *output_gate_bias, + const luci_interpreter::Tensor *projection_weights, + const luci_interpreter::Tensor *projection_bias, + const luci_interpreter::UnidirectionalSequenceLSTMParams ¶ms, bool forward_sequence, + bool time_major, const luci_interpreter::IntegerLSTMParams &integer_lstm_param, + int32_t output_state_zp, luci_interpreter::Tensor *output_state, + luci_interpreter::Tensor *cell_state, luci_interpreter::Tensor *output, int16_t *scratch0, + int16_t *scratch1, int16_t *scratch2, int16_t *scratch3, int8_t *scratch4, int32_t *scratch5) +{ + // CMSIS-NN does not support these configurations currently. + // Please use MCU kernels instead + const bool use_layer_norm = (forget_layer_norm_coefficients != nullptr); + const bool use_peephole = (cell_to_output_weights != nullptr); + const bool use_projection = (projection_weights != nullptr); + const bool use_cifg = (input_to_input_weights == nullptr); + const bool unsupported_config = use_layer_norm || use_peephole || use_projection || use_cifg; + + if (unsupported_config) + { + assert(false && "CMSIS-NN does not support these configurations currently"); + return; + } + + const auto input_shape = input->shape(); + LUCI_INTERPRETER_CHECK(input_shape.num_dims() >= 2 && input_shape.num_dims() <= 3); + + cmsis_nn_lstm_context scratch_buffers; + scratch_buffers.input_gate = scratch0; + scratch_buffers.forget_gate = scratch1; + scratch_buffers.cell_gate = scratch2; + scratch_buffers.output_gate = scratch3; + scratch_buffers.scratch = scratch4; + + cmsis_nn_lstm_params cmsis_lstm_params = lstm::convert_lstm_params( + integer_lstm_param, time_major, output_state_zp, + luci_interpreter::kernels::getTensorData(input_gate_bias), + luci_interpreter::kernels::getTensorData(forget_gate_bias), + luci_interpreter::kernels::getTensorData(cell_gate_bias), + luci_interpreter::kernels::getTensorData(output_gate_bias), + const_cast( + luci_interpreter::kernels::getTensorData(input_layer_norm_coefficients)), + const_cast( + luci_interpreter::kernels::getTensorData(forget_layer_norm_coefficients)), + const_cast( + luci_interpreter::kernels::getTensorData(cell_layer_norm_coefficients)), + const_cast( + luci_interpreter::kernels::getTensorData(output_layer_norm_coefficients))); + + const int n_input = input_shape.dim(input_shape.num_dims() - 1); + int max_time, n_batch; + if (input_shape.num_dims() == 2) + { + max_time = 1; + n_batch = input_shape.dim(0); + } + else + { + max_time = (time_major) ? input_shape.dim(0) : input_shape.dim(1); + n_batch = (time_major) ? input_shape.dim(1) : input_shape.dim(0); + } + + // n_cell and n_output will be the same size when there is no projection. + const int n_cell = input_to_output_weights->shape().dim(0); + const int n_output = recurrent_to_output_weights->shape().dim(1); + + cmsis_nn_lstm_dims lstm_dims; + lstm_dims.num_inputs = n_input; + lstm_dims.num_outputs = n_output; + lstm_dims.num_batches = n_batch; + lstm_dims.max_time = max_time; + + arm_lstm_unidirectional_s16_s8( + &scratch_buffers, const_cast(luci_interpreter::kernels::getTensorData(input)), + &lstm_dims, + const_cast(luci_interpreter::kernels::getTensorData(input_to_input_weights)), + const_cast(luci_interpreter::kernels::getTensorData(input_to_forget_weights)), + const_cast(luci_interpreter::kernels::getTensorData(input_to_cell_weights)), + const_cast(luci_interpreter::kernels::getTensorData(input_to_output_weights)), + const_cast( + luci_interpreter::kernels::getTensorData(recurrent_to_input_weights)), + const_cast( + luci_interpreter::kernels::getTensorData(recurrent_to_forget_weights)), + const_cast( + luci_interpreter::kernels::getTensorData(recurrent_to_cell_weights)), + const_cast( + luci_interpreter::kernels::getTensorData(recurrent_to_output_weights)), + const_cast(luci_interpreter::kernels::getTensorData(cell_to_input_weights)), + const_cast( + luci_interpreter::kernels::getTensorData(cell_to_forget_weights)), + const_cast( + luci_interpreter::kernels::getTensorData(cell_to_output_weights)), + const_cast(luci_interpreter::kernels::getTensorData(projection_weights)), + &cmsis_lstm_params, + const_cast(luci_interpreter::kernels::getTensorData(output_state)), + const_cast(luci_interpreter::kernels::getTensorData(cell_state)), + const_cast(luci_interpreter::kernels::getTensorData(output))); +} + +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_UNIDIRECTIONAL_SEQUENCE_LSTM_H diff --git a/onert-micro/luci-interpreter/pal/cmsisnn/PALreference_ops.h b/onert-micro/luci-interpreter/pal/cmsisnn/PALreference_ops.h new file mode 100644 index 0000000..62c7209 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/cmsisnn/PALreference_ops.h @@ -0,0 +1,1556 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_REFERENCE_OPS_H +#define LUCI_INTERPRETER_PAL_REFERENCE_OPS_H + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "third_party/eigen3/Eigen/Core" +#include "fixedpoint/fixedpoint.h" +#include "ruy/profiler/instrumentation.h" // from @ruy +#include "tensorflow/lite/c/common.h" +#include "tensorflow/lite/kernels/internal/common.h" +#include "tensorflow/lite/kernels/internal/quantization_util.h" +#include "tensorflow/lite/kernels/internal/reference/add.h" +#include "tensorflow/lite/kernels/internal/reference/add_n.h" +#include "tensorflow/lite/kernels/internal/reference/arg_min_max.h" +#include "tensorflow/lite/kernels/internal/reference/batch_matmul.h" +#include "tensorflow/lite/kernels/internal/reference/batch_to_space_nd.h" +#include "tensorflow/lite/kernels/internal/reference/binary_function.h" +#include "tensorflow/lite/kernels/internal/reference/cast.h" +#include "tensorflow/lite/kernels/internal/reference/ceil.h" +#include "tensorflow/lite/kernels/internal/reference/comparisons.h" +#include "tensorflow/lite/kernels/internal/reference/concatenation.h" +#include "tensorflow/lite/kernels/internal/reference/conv.h" +#include "tensorflow/lite/kernels/internal/reference/depth_to_space.h" +#include "tensorflow/lite/kernels/internal/reference/dequantize.h" +#include "tensorflow/lite/kernels/internal/reference/div.h" +#include "tensorflow/lite/kernels/internal/reference/elu.h" +#include "tensorflow/lite/kernels/internal/reference/exp.h" +#include "tensorflow/lite/kernels/internal/reference/fill.h" +#include "tensorflow/lite/kernels/internal/reference/floor.h" +#include "tensorflow/lite/kernels/internal/reference/floor_div.h" +#include "tensorflow/lite/kernels/internal/reference/floor_mod.h" +#include "tensorflow/lite/kernels/internal/reference/fully_connected.h" +#include "tensorflow/lite/kernels/internal/reference/gather.h" +#include "tensorflow/lite/kernels/internal/reference/hard_swish.h" +#include "tensorflow/lite/kernels/internal/reference/l2normalization.h" +#include "tensorflow/lite/kernels/internal/reference/leaky_relu.h" +#include "tensorflow/lite/kernels/internal/reference/log_softmax.h" +#include "tensorflow/lite/kernels/internal/reference/logistic.h" +#include "tensorflow/lite/kernels/internal/reference/maximum_minimum.h" +#include "tensorflow/lite/kernels/internal/reference/mul.h" +#include "tensorflow/lite/kernels/internal/reference/neg.h" +#include "tensorflow/lite/kernels/internal/reference/pad.h" +#include "tensorflow/lite/kernels/internal/reference/pooling.h" +#include "tensorflow/lite/kernels/internal/reference/prelu.h" +#include "tensorflow/lite/kernels/internal/reference/process_broadcast_shapes.h" +#include "tensorflow/lite/kernels/internal/reference/quantize.h" +#include "tensorflow/lite/kernels/internal/reference/reduce.h" +#include "tensorflow/lite/kernels/internal/reference/requantize.h" +#include "tensorflow/lite/kernels/internal/reference/resize_bilinear.h" +#include "tensorflow/lite/kernels/internal/reference/resize_nearest_neighbor.h" +#include "tensorflow/lite/kernels/internal/reference/round.h" +#include "tensorflow/lite/kernels/internal/reference/softmax.h" +#include "tensorflow/lite/kernels/internal/reference/space_to_batch_nd.h" +#include "tensorflow/lite/kernels/internal/reference/space_to_depth.h" +#include "tensorflow/lite/kernels/internal/reference/strided_slice.h" +#include "tensorflow/lite/kernels/internal/reference/string_comparisons.h" +#include "tensorflow/lite/kernels/internal/reference/sub.h" +#include "tensorflow/lite/kernels/internal/reference/tanh.h" +#include "tensorflow/lite/kernels/internal/reference/transpose.h" +#include "tensorflow/lite/kernels/internal/reference/transpose_conv.h" +#include "tensorflow/lite/kernels/internal/strided_slice_logic.h" +#include "tensorflow/lite/kernels/internal/tensor.h" +#include "tensorflow/lite/kernels/internal/types.h" +namespace tflite +{ + +namespace reference_ops +{ + +template +inline void Relu(const RuntimeShape &input_shape, const T *input_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int flat_size = MatchingFlatSize(input_shape, output_shape); + for (int i = 0; i < flat_size; ++i) + { + const T val = input_data[i]; + const T lower = 0; + const T clamped = val < lower ? lower : val; + output_data[i] = clamped; + } +} + +template +inline void Relu1(const RuntimeShape &input_shape, const T *input_data, + const RuntimeShape &output_shape, T *output_data) +{ + ruy::profiler::ScopeLabel label("Relu1 (not fused)"); + const int flat_size = MatchingFlatSize(input_shape, output_shape); + for (int i = 0; i < flat_size; ++i) + { + const T val = input_data[i]; + const T upper = 1; + const T lower = -1; + const T clamped = val > upper ? upper : val < lower ? lower : val; + output_data[i] = clamped; + } +} + +inline void Relu6(const RuntimeShape &input_shape, const float *input_data, + const RuntimeShape &output_shape, float *output_data) +{ + ruy::profiler::ScopeLabel label("Relu6 (not fused)"); + const int flat_size = MatchingFlatSize(input_shape, output_shape); + for (int i = 0; i < flat_size; ++i) + { + const float val = input_data[i]; + const float upper = 6; + const float lower = 0; + const float clamped = val > upper ? upper : val < lower ? lower : val; + output_data[i] = clamped; + } +} + +template +inline void ReluX(const tflite::ReluParams ¶ms, const RuntimeShape &input_shape, + const T *input_data, const RuntimeShape &output_shape, T *output_data) +{ + ruy::profiler::ScopeLabel label("Quantized ReluX (not fused)"); + const int flat_size = MatchingFlatSize(input_shape, output_shape); + for (int i = 0; i < flat_size; ++i) + { + const int32 val = static_cast(input_data[i]); + int32 clamped = params.output_offset + MultiplyByQuantizedMultiplier(val - params.input_offset, + params.output_multiplier, + params.output_shift); + clamped = std::max(params.quantized_activation_min, clamped); + clamped = std::min(params.quantized_activation_max, clamped); + output_data[i] = static_cast(clamped); + } +} + +template +inline void ReluX(const tflite::ActivationParams ¶ms, const RuntimeShape &input_shape, + const T *input_data, const RuntimeShape &output_shape, T *output_data) +{ + ruy::profiler::ScopeLabel label("Quantized ReluX (not fused)"); + const int flat_size = MatchingFlatSize(input_shape, output_shape); + const T max_value = params.quantized_activation_max; + const T min_value = params.quantized_activation_min; + for (int i = 0; i < flat_size; ++i) + { + const T val = input_data[i]; + const T clamped = val > max_value ? max_value : val < min_value ? min_value : val; + output_data[i] = clamped; + } +} + +// TODO(jiawen): We can implement BroadcastMul on buffers of arbitrary +// dimensionality if the runtime code does a single loop over one dimension +// that handles broadcasting as the base case. The code generator would then +// generate max(D1, D2) nested for loops. +inline void BroadcastMulFivefold(const ArithmeticParams &unswitched_params, + const RuntimeShape &unswitched_input1_shape, + const uint8 *unswitched_input1_data, + const RuntimeShape &unswitched_input2_shape, + const uint8 *unswitched_input2_data, + const RuntimeShape &output_shape, uint8 *output_data) +{ + ArithmeticParams switched_params = unswitched_params; + switched_params.input1_offset = unswitched_params.input2_offset; + switched_params.input2_offset = unswitched_params.input1_offset; + + const bool use_unswitched = unswitched_params.broadcast_category == + tflite::BroadcastableOpCategory::kFirstInputBroadcastsFast; + + const ArithmeticParams ¶ms = use_unswitched ? unswitched_params : switched_params; + const uint8 *input1_data = use_unswitched ? unswitched_input1_data : unswitched_input2_data; + const uint8 *input2_data = use_unswitched ? unswitched_input2_data : unswitched_input1_data; + + // Fivefold nested loops. The second input resets its position for each + // iteration of the second loop. The first input resets its position at the + // beginning of the fourth loop. The innermost loop is an elementwise Mul of + // sections of the arrays. + uint8 *output_data_ptr = output_data; + const uint8 *input1_data_ptr = input1_data; + const uint8 *input2_data_reset = input2_data; + int y0 = params.broadcast_shape[0]; + int y1 = params.broadcast_shape[1]; + int y2 = params.broadcast_shape[2]; + int y3 = params.broadcast_shape[3]; + int y4 = params.broadcast_shape[4]; + for (int i0 = 0; i0 < y0; ++i0) + { + const uint8 *input2_data_ptr; + for (int i1 = 0; i1 < y1; ++i1) + { + input2_data_ptr = input2_data_reset; + for (int i2 = 0; i2 < y2; ++i2) + { + for (int i3 = 0; i3 < y3; ++i3) + { + MulElementwise(y4, params, input1_data_ptr, input2_data_ptr, output_data_ptr); + input2_data_ptr += y4; + output_data_ptr += y4; + } + input1_data_ptr += y4; + } + } + input2_data_reset = input2_data_ptr; + } +} + +inline void Mul(const ArithmeticParams ¶ms, const RuntimeShape &input1_shape, + const int16 *input1_data, const RuntimeShape &input2_shape, + const int16 *input2_data, const RuntimeShape &output_shape, int16 *output_data) +{ + ruy::profiler::ScopeLabel label("Mul/Int16"); + + const int flat_size = MatchingElementsSize(input1_shape, input2_shape, output_shape); + + for (int i = 0; i < flat_size; i++) + { + // F0 uses 0 integer bits, range [-1, 1]. + using F0 = gemmlowp::FixedPoint; + + F0 unclamped_result = F0::FromRaw(input1_data[i]) * F0::FromRaw(input2_data[i]); + output_data[i] = unclamped_result.raw(); + } +} + +inline void Mul(const ArithmeticParams ¶ms, const RuntimeShape &input1_shape, + const int16 *input1_data, const RuntimeShape &input2_shape, + const int16 *input2_data, const RuntimeShape &output_shape, uint8 *output_data) +{ + ruy::profiler::ScopeLabel label("Mul/Int16Uint8"); + int32 output_offset = params.output_offset; + int32 output_activation_min = params.quantized_activation_min; + int32 output_activation_max = params.quantized_activation_max; + TFLITE_DCHECK_LE(output_activation_min, output_activation_max); + + const int flat_size = MatchingElementsSize(input1_shape, input2_shape, output_shape); + + for (int i = 0; i < flat_size; i++) + { + // F0 uses 0 integer bits, range [-1, 1]. + using F0 = gemmlowp::FixedPoint; + + F0 unclamped_result = F0::FromRaw(input1_data[i]) * F0::FromRaw(input2_data[i]); + int16 rescaled_result = gemmlowp::RoundingDivideByPOT(unclamped_result.raw(), 8); + int16 clamped_result = std::min(output_activation_max - output_offset, rescaled_result); + clamped_result = std::max(output_activation_min - output_offset, clamped_result); + output_data[i] = output_offset + clamped_result; + } +} + +inline void Sub16(const ArithmeticParams ¶ms, const RuntimeShape &input1_shape, + const int16_t *input1_data, const RuntimeShape &input2_shape, + const int16_t *input2_data, const RuntimeShape &output_shape, + int16_t *output_data) +{ + ruy::profiler::ScopeLabel label("Sub/Int16"); + const int input1_shift = params.input1_shift; + const int flat_size = MatchingElementsSize(input1_shape, input2_shape, output_shape); + const int16 output_activation_min = params.quantized_activation_min; + const int16 output_activation_max = params.quantized_activation_max; + + TFLITE_DCHECK(input1_shift == 0 || params.input2_shift == 0); + TFLITE_DCHECK_LE(input1_shift, 0); + TFLITE_DCHECK_LE(params.input2_shift, 0); + const int16 *not_shift_input = input1_shift == 0 ? input1_data : input2_data; + const int16 *shift_input = input1_shift == 0 ? input2_data : input1_data; + const int input_right_shift = input1_shift == 0 ? -params.input2_shift : -input1_shift; + + if (input1_shift == 0) + { + // F0 uses 0 integer bits, range [-1, 1]. + using F0 = gemmlowp::FixedPoint; + for (int i = 0; i < flat_size; ++i) + { + F0 input_ready_scaled = F0::FromRaw(not_shift_input[i]); + F0 scaled_input = + F0::FromRaw(gemmlowp::RoundingDivideByPOT(shift_input[i], input_right_shift)); + F0 result = SaturatingSub(input_ready_scaled, scaled_input); + const int16 raw_output = result.raw(); + const int16 clamped_output = + std::min(output_activation_max, std::max(output_activation_min, raw_output)); + output_data[i] = clamped_output; + } + } + else + { + // F0 uses 0 integer bits, range [-1, 1]. + using F0 = gemmlowp::FixedPoint; + for (int i = 0; i < flat_size; ++i) + { + F0 input_ready_scaled = F0::FromRaw(not_shift_input[i]); + F0 scaled_input = + F0::FromRaw(gemmlowp::RoundingDivideByPOT(shift_input[i], input_right_shift)); + F0 result = SaturatingSub(scaled_input, input_ready_scaled); + const int16 raw_output = result.raw(); + const int16 clamped_output = + std::min(output_activation_max, std::max(output_activation_min, raw_output)); + output_data[i] = clamped_output; + } + } +} + +template +void Pack(const PackParams ¶ms, const RuntimeShape *const *input_shapes, + const Scalar *const *input_data, const RuntimeShape &output_shape, Scalar *output_data) +{ + ruy::profiler::ScopeLabel label("Pack"); + const int dimensions = output_shape.DimensionsCount(); + int axis = params.axis; + int inputs_count = params.inputs_count; + + int outer_size = 1; + for (int i = 0; i < axis; i++) + { + outer_size *= output_shape.Dims(i); + } + int copy_size = 1; + for (int i = params.axis + 1; i < dimensions; i++) + { + copy_size *= output_shape.Dims(i); + } + TFLITE_DCHECK_EQ((**input_shapes).FlatSize(), copy_size * outer_size); + + for (int i = 0; i < inputs_count; ++i) + { + for (int k = 0; k < outer_size; k++) + { + const Scalar *input_ptr = input_data[i] + copy_size * k; + int loc = k * inputs_count * copy_size + i * copy_size; + memcpy(output_data + loc, input_ptr, copy_size * sizeof(Scalar)); + } + } +} + +template +void Unpack(const UnpackParams ¶ms, const RuntimeShape &input_shape, const Scalar *input_data, + const RuntimeShape &output_shape, Scalar *const *output_datas) +{ + ruy::profiler::ScopeLabel label("Unpack"); + const int dimensions = input_shape.DimensionsCount(); + const int outputs_count = params.num_split; + + int outer_size = 1; + int axis = params.axis; + if (axis < 0) + { + axis += dimensions; + } + TFLITE_DCHECK_GE(axis, 0); + TFLITE_DCHECK_LT(axis, dimensions); + for (int i = 0; i < axis; ++i) + { + outer_size *= input_shape.Dims(i); + } + int copy_size = 1; + for (int i = axis + 1; i < dimensions; ++i) + { + copy_size *= input_shape.Dims(i); + } + TFLITE_DCHECK_EQ(output_shape.FlatSize(), copy_size * outer_size); + + for (int i = 0; i < outputs_count; ++i) + { + for (int k = 0; k < outer_size; k++) + { + Scalar *output_ptr = output_datas[i] + copy_size * k; + int loc = k * outputs_count * copy_size + i * copy_size; + memcpy(output_ptr, input_data + loc, copy_size * sizeof(Scalar)); + } + } +} + +template +void PackWithScaling(const PackParams ¶ms, const RuntimeShape *const *input_shapes, + const uint8 *const *input_data, const RuntimeShape &output_shape, + uint8 *output_data) +{ + ruy::profiler::ScopeLabel label("PackWithScaling"); + const int dimensions = output_shape.DimensionsCount(); + int axis = params.axis; + const int32 *input_zeropoint = params.input_zeropoint; + const float *input_scale = params.input_scale; + int inputs_count = params.inputs_count; + const int32 output_zeropoint = params.output_zeropoint; + const float output_scale = params.output_scale; + + int outer_size = 1; + for (int i = 0; i < axis; i++) + { + outer_size *= output_shape.Dims(i); + } + int copy_size = 1; + for (int i = axis + 1; i < dimensions; i++) + { + copy_size *= output_shape.Dims(i); + } + TFLITE_DCHECK_EQ((**input_shapes).FlatSize(), copy_size * outer_size); + + Scalar *output_ptr = output_data; + const float inverse_output_scale = 1.f / output_scale; + for (int k = 0; k < outer_size; k++) + { + for (int i = 0; i < inputs_count; ++i) + { + if (input_zeropoint[i] == output_zeropoint && input_scale[i] == output_scale) + { + memcpy(output_ptr, input_data[i] + k * copy_size, copy_size * sizeof(Scalar)); + } + else + { + assert(false); + const float scale = input_scale[i] * inverse_output_scale; + const float bias = -input_zeropoint[i] * scale; + auto input_ptr = input_data[i]; + for (int j = 0; j < copy_size; ++j) + { + const int value = + static_cast(std::round(input_ptr[j] * scale + bias)) + output_zeropoint; + output_ptr[j] = static_cast(std::max(std::min(255, value), 0)); + } + } + output_ptr += copy_size; + } + } +} + +template +void DepthConcatenation(const ConcatenationParams ¶ms, const RuntimeShape *const *input_shapes, + const Scalar *const *input_data, const RuntimeShape &output_shape, + Scalar *output_data) +{ + ruy::profiler::ScopeLabel label("DepthConcatenation"); + auto params_copy = params; + params_copy.axis = 3; + Concatenation(params_copy, input_shapes, input_data, output_shape, output_data); +} + +inline void LstmCell(const LstmCellParams ¶ms, const RuntimeShape &unextended_input_shape, + const float *input_data, const RuntimeShape &unextended_prev_activ_shape, + const float *prev_activ_data, const RuntimeShape &weights_shape, + const float *weights_data, const RuntimeShape &unextended_bias_shape, + const float *bias_data, const RuntimeShape &unextended_prev_state_shape, + const float *prev_state_data, + const RuntimeShape &unextended_output_state_shape, float *output_state_data, + const RuntimeShape &unextended_output_activ_shape, float *output_activ_data, + const RuntimeShape &unextended_concat_temp_shape, float *concat_temp_data, + const RuntimeShape &unextended_activ_temp_shape, float *activ_temp_data) +{ + TFLITE_DCHECK_LE(unextended_input_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_prev_activ_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_bias_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_prev_state_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_output_state_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_output_activ_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_concat_temp_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_activ_temp_shape.DimensionsCount(), 4); + const RuntimeShape input_shape = RuntimeShape::ExtendedShape(4, unextended_input_shape); + const RuntimeShape prev_activ_shape = RuntimeShape::ExtendedShape(4, unextended_prev_activ_shape); + const RuntimeShape bias_shape = RuntimeShape::ExtendedShape(4, unextended_bias_shape); + const RuntimeShape prev_state_shape = RuntimeShape::ExtendedShape(4, unextended_prev_state_shape); + const RuntimeShape output_state_shape = + RuntimeShape::ExtendedShape(4, unextended_output_state_shape); + const RuntimeShape output_activ_shape = + RuntimeShape::ExtendedShape(4, unextended_output_activ_shape); + const RuntimeShape concat_temp_shape = + RuntimeShape::ExtendedShape(4, unextended_concat_temp_shape); + const RuntimeShape activ_temp_shape = RuntimeShape::ExtendedShape(4, unextended_activ_temp_shape); + TFLITE_DCHECK_GE(weights_shape.DimensionsCount(), 2); + + const int weights_dim_count = weights_shape.DimensionsCount(); + const int batches = MatchingDim(input_shape, 0, prev_activ_shape, 0, prev_state_shape, 0, + output_state_shape, 0, output_activ_shape, 0); + const int height = MatchingDim(input_shape, 1, prev_activ_shape, 1, prev_state_shape, 1, + output_state_shape, 1, output_activ_shape, 1); + const int width = MatchingDim(input_shape, 2, prev_activ_shape, 2, prev_state_shape, 2, + output_state_shape, 2, output_activ_shape, 2); + const int input_depth = input_shape.Dims(3); + const int prev_activ_depth = prev_activ_shape.Dims(3); + const int total_input_depth = prev_activ_depth + input_depth; + TFLITE_DCHECK_EQ(weights_shape.Dims(weights_dim_count - 1), total_input_depth); + TFLITE_DCHECK_EQ(FlatSizeSkipDim(bias_shape, 3), 1); + const int intern_activ_depth = MatchingDim(weights_shape, weights_dim_count - 2, bias_shape, 3); + TFLITE_DCHECK_EQ(weights_shape.FlatSize(), intern_activ_depth * total_input_depth); + TFLITE_DCHECK_EQ(intern_activ_depth % 4, 0); + const int output_depth = MatchingDim(prev_state_shape, 3, prev_activ_shape, 3, output_state_shape, + 3, output_activ_shape, 3); + TFLITE_DCHECK_EQ(output_depth, intern_activ_depth / 4); + + // Concatenate prev_activ and input data together + std::vector concat_input_arrays_data; + std::vector concat_input_arrays_shapes; + concat_input_arrays_data.push_back(input_data); + concat_input_arrays_data.push_back(prev_activ_data); + concat_input_arrays_shapes.push_back(&input_shape); + concat_input_arrays_shapes.push_back(&prev_activ_shape); + tflite::ConcatenationParams concat_params; + concat_params.axis = 3; + concat_params.inputs_count = concat_input_arrays_data.size(); + Concatenation(concat_params, &(concat_input_arrays_shapes[0]), &(concat_input_arrays_data[0]), + concat_temp_shape, concat_temp_data); + + // Fully connected + tflite::FullyConnectedParams fc_params; + fc_params.float_activation_min = std::numeric_limits::lowest(); + fc_params.float_activation_max = std::numeric_limits::max(); + FullyConnected(fc_params, concat_temp_shape, concat_temp_data, weights_shape, weights_data, + bias_shape, bias_data, activ_temp_shape, activ_temp_data); + + // Memory state update (the LSTM "guts") + for (int b = 0; b < batches; ++b) + { + for (int w = 0; w < width; ++w) + { + for (int h = 0; h < height; ++h) + { + for (int c = 0; c < output_depth; ++c) + { + const float input_gate = + 1.f / + (1.f + + std::exp(-activ_temp_data[Offset(activ_temp_shape, b, h, w, 0 * output_depth + c)])); + const float new_input = + std::tanh(activ_temp_data[Offset(activ_temp_shape, b, h, w, 1 * output_depth + c)]); + const float forget_gate = + 1.f / + (1.f + + std::exp(-activ_temp_data[Offset(activ_temp_shape, b, h, w, 2 * output_depth + c)])); + const float output_gate = + 1.f / + (1.f + + std::exp(-activ_temp_data[Offset(activ_temp_shape, b, h, w, 3 * output_depth + c)])); + const float new_state = + input_gate * new_input + + forget_gate * prev_state_data[Offset(prev_state_shape, b, h, w, c)]; + output_state_data[Offset(output_state_shape, b, h, w, c)] = new_state; + output_activ_data[Offset(output_activ_shape, b, h, w, c)] = + output_gate * std::tanh(new_state); + } + } + } + } +} + +// Quantized LSTM cell implementation. +// The quantization of the input, output arrays is as follows: +// - The input activations are quantized as uint8 on the interval +// [-1, 127/128]. +// The rationale for that is that is the natural interval for output +// activations (see next point) and these need to be concatenated together. +// We could accommodate different ranges by re-scaling, but we empirically +// found that setting the input activations range to be [-1, 127/128] in the +// first place, removing the need for re-scaling, greatly improves accuracy. +// - The output activations are quantized as uint8 on the interval +// [-1, 127/128]. +// The rationale for that is that the definition of a LSTM cell makes them +// intrinsically constrained in [-1, 1]; tweaking that to [-1, 127/128] +// makes for simpler, more accurate fixed-point arithmetic. +// - The output-at-previous-timestep state array is obviously quantized as +// the output activations. +// - The internal LSTM memory (not the output-at-previous-timestep, the other +// internal state array) is int16-quantized and may use any power-of-two, +// symmetric range i.e. [-2^N, 2^N * 32767/32768] for any N, which we call +// StateIntegerBits below, see the below discussion of that template +// parameter ("The StateIntegerBits template parameter"). +// - The output of the internal fully-connected node is int16-quantized +// on the interval [-8, 8 * 32767/32768], the rationale for which is +// explained just below ("Why [-8, 8] for fully-connected output?"). +// +// +// === The StateIntegerBits template parameter === +// +// The StateIntegerBits template parameter controls the fixed-point format used +// to represent the internal memory of the LSTM cell (not the +// output-at-previous-timestep, the other internal state array). It's currently +// a template parameter so that the model can control that. The most typical +// value for StateIntegerBits is 4. Other plausible values are anywhere between +// 3 and 5. We might eventually standardize on a single supported value, e.g. 4, +// and drop that template parameter. The reason why it can't be a runtime +// parameter is that this controls the fixed-point format used, i.e. we need to +// generate actually different code based on it. In particular, we generate code +// for a fixed-point tanh() implementation for that format, which internally +// uses a fixed-point exp() implementation, which internally uses a +// barrel-shifter with a number of steps that depends on StateIntegerBits. +// Another consequence of that is that a higher value of StateIntegerBits +// results in a more expensive implementation (more barrel shifter steps +// needed). +// +// +// === Why [-8, 8] for fully-connected output? === +// +// This array is only fed to Logistic and Tanh functions, for which +// the quantized implementation will want to use fixed-point arithmetic, +// requiring a power-of-two representation interval. Thus, we should right +// away quantize this array to a power-of-two interval; otherwise, +// implementation will need to rescale that, losing any benefit that a tighter +// representation interval might otherwise yield, while introducing some +// numerical error and computational overhead. +// +// Now, Logistic and Tanh +// are nearly constant (nearly equal to their horizontal asymptotes) +// outside of a small bounded interval around 0: +// +// Logistic(4) = 1 - 1.8e-2 Tanh(4) = 1 - 6.7e-4 +// Logistic(8) = 1 - 3.4e-4 Tanh(8) = 1 - 2.3e-7 +// Logistic(16) = 1 - 1.1e-7 Tanh(16) = 1 - 2.5e-14 +// +// From this, we see that clamping to [-4, 4] would be too inaccurate +// (the error of 1.8e-2 on Logistic would be felt even in 8bit precision) +// while clamping to [-16, 16] would make no difference even in float32. +// However, for a fixed-point implementation in 16-bit integers, using 5 +// integer bits to represent the [-16, 16] range would leave only 11 +// fractional bits, giving an increment of 2^-11 = 4.9e-4 between consecutive +// representable values. Notice that is higher than the +// worst-case clamping error with clamping to [-8, 8]: 3.4e-4 for Logistic. +// Using [-8, 8] thus seems like the better compromise overall, enjoying +// an increment of 2.4e-4 between representable values and a worst-case +// clamping error of 3.4e-4, both better than the increment of 4.9e-4 with +// [-16, 16]. +// +// Moreover, all other things being equal, it is nice to choose the narrower +// representation range, as that makes the implementation of fixed-point +// math functions a little cheaper (each integer bit requires an additional +// barrel-shifter atep in the implementation of exp(-x)). That is further +// reason to prefer [-8, 8] over [-16, 16]. The choice of [-16, 16] would make +// sense for 32-bit float or 32-bit fixed-point quantization, but we are +// aiming for 16-bit fixed-point quantization of these internal nodes here. +// +template +inline void +LstmCell(const LstmCellParams ¶ms, const RuntimeShape &unextended_input_shape, + const uint8 *input_data_uint8, const RuntimeShape &unextended_prev_activ_shape, + const uint8 *prev_activ_data_uint8, const RuntimeShape &weights_shape, + const uint8 *weights_data_uint8, const RuntimeShape &unextended_bias_shape, + const int32 *bias_data_int32, const RuntimeShape &unextended_prev_state_shape, + const int16 *prev_state_data_int16, const RuntimeShape &unextended_output_state_shape, + int16 *output_state_data_int16, const RuntimeShape &unextended_output_activ_shape, + uint8 *output_activ_data_uint8, const RuntimeShape &unextended_concat_temp_shape, + uint8 *concat_temp_data_uint8, const RuntimeShape &unextended_activ_temp_shape, + int16 *activ_temp_data_int16, void *gemmlowp_context) +{ + (void)gemmlowp_context; // only used in optimized code. + int32 weights_zero_point = params.weights_zero_point; + int32 accum_multiplier = params.accum_multiplier; + int accum_shift = params.accum_shift; + TFLITE_DCHECK_LE(unextended_input_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_prev_activ_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_bias_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_prev_state_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_output_state_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_output_activ_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_concat_temp_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_activ_temp_shape.DimensionsCount(), 4); + const RuntimeShape input_shape = RuntimeShape::ExtendedShape(4, unextended_input_shape); + const RuntimeShape prev_activ_shape = RuntimeShape::ExtendedShape(4, unextended_prev_activ_shape); + const RuntimeShape bias_shape = RuntimeShape::ExtendedShape(4, unextended_bias_shape); + const RuntimeShape prev_state_shape = RuntimeShape::ExtendedShape(4, unextended_prev_state_shape); + const RuntimeShape output_state_shape = + RuntimeShape::ExtendedShape(4, unextended_output_state_shape); + const RuntimeShape output_activ_shape = + RuntimeShape::ExtendedShape(4, unextended_output_activ_shape); + const RuntimeShape concat_temp_shape = + RuntimeShape::ExtendedShape(4, unextended_concat_temp_shape); + const RuntimeShape activ_temp_shape = RuntimeShape::ExtendedShape(4, unextended_activ_temp_shape); + TFLITE_DCHECK_GE(weights_shape.DimensionsCount(), 2); + + // Gather dimensions information, and perform consistency checks. + const int weights_dim_count = weights_shape.DimensionsCount(); + const int outer_size = MatchingFlatSizeSkipDim(input_shape, 3, prev_activ_shape, prev_state_shape, + output_state_shape, output_activ_shape); + const int input_depth = input_shape.Dims(3); + const int prev_activ_depth = prev_activ_shape.Dims(3); + const int total_input_depth = prev_activ_depth + input_depth; + TFLITE_DCHECK_EQ(weights_shape.Dims(weights_dim_count - 1), total_input_depth); + const int intern_activ_depth = MatchingDim(weights_shape, weights_dim_count - 2, bias_shape, 3); + TFLITE_DCHECK_EQ(weights_shape.FlatSize(), intern_activ_depth * total_input_depth); + TFLITE_DCHECK_EQ(FlatSizeSkipDim(bias_shape, 3), 1); + TFLITE_DCHECK_EQ(intern_activ_depth % 4, 0); + const int output_depth = MatchingDim(prev_state_shape, 3, prev_activ_shape, 3, output_state_shape, + 3, output_activ_shape, 3); + TFLITE_DCHECK_EQ(output_depth, intern_activ_depth / 4); + const int fc_batches = FlatSizeSkipDim(activ_temp_shape, 3); + const int fc_output_depth = + MatchingDim(weights_shape, weights_dim_count - 2, activ_temp_shape, 3); + const int fc_accum_depth = total_input_depth; + TFLITE_DCHECK_EQ(fc_output_depth, 4 * output_depth); + + // Depth-concatenate prev_activ and input data together. + uint8 const *concat_input_arrays_data[2] = {input_data_uint8, prev_activ_data_uint8}; + const RuntimeShape *concat_input_arrays_shapes[2] = {&input_shape, &prev_activ_shape}; + tflite::ConcatenationParams concat_params; + concat_params.axis = 3; + concat_params.inputs_count = 2; + Concatenation(concat_params, concat_input_arrays_shapes, concat_input_arrays_data, + concat_temp_shape, concat_temp_data_uint8); + + // Implementation of the fully connected node inside the LSTM cell. + // The operands are 8-bit integers, the accumulators are internally 32bit + // integers, and the output is 16-bit fixed-point with 3 integer bits so + // the output range is [-2^3, 2^3] == [-8, 8]. The rationale for that + // is explained in the function comment above. + for (int b = 0; b < fc_batches; ++b) + { + for (int out_c = 0; out_c < fc_output_depth; ++out_c) + { + // Internal accumulation. + // Initialize accumulator with the bias-value. + int32 accum = bias_data_int32[out_c]; + // Accumulation loop. + for (int d = 0; d < fc_accum_depth; ++d) + { + int16 input_val = concat_temp_data_uint8[b * fc_accum_depth + d] - 128; + int16 weights_val = weights_data_uint8[out_c * fc_accum_depth + d] - weights_zero_point; + accum += input_val * weights_val; + } + // Down-scale the final int32 accumulator to the scale used by our + // (16-bit, using 3 integer bits) fixed-point format. The quantized + // multiplier and shift here have been pre-computed offline + // (e.g. by toco). + accum = MultiplyByQuantizedMultiplier(accum, accum_multiplier, accum_shift); + // Saturate, cast to int16, and store to the temporary activations array. + accum = std::max(-32768, std::min(32767, static_cast(accum))); + activ_temp_data_int16[out_c + fc_output_depth * b] = accum; + } + } + + // Rest of the LSTM cell: tanh and logistic math functions, and some adds + // and muls, all done in 16-bit fixed-point. + for (int b = 0; b < outer_size; ++b) + { + for (int c = 0; c < output_depth; ++c) + { + // Define the fixed-point data types that we will use here. All use + // int16 as the underlying integer type i.e. all are 16-bit fixed-point. + // They only differ by the number of integral vs. fractional bits, + // determining the range of values that they can represent. + // + // F0 uses 0 integer bits, range [-1, 1]. + // This is the return type of math functions such as tanh, logistic, + // whose range is in [-1, 1]. + using F0 = gemmlowp::FixedPoint; + // F3 uses 3 integer bits, range [-8, 8]. + // This is the range of the previous fully-connected node's output, + // which is our input here. + using F3 = gemmlowp::FixedPoint; + // FS uses StateIntegerBits integer bits, range [-2^StateIntegerBits, + // 2^StateIntegerBits]. It's used to represent the internal state, whose + // number of integer bits is currently dictated by the model. See comment + // on the StateIntegerBits template parameter above. + using FS = gemmlowp::FixedPoint; + // Implementation of input gate, using fixed-point logistic function. + F3 input_gate_input = + F3::FromRaw(activ_temp_data_int16[b * fc_output_depth + 0 * output_depth + c]); + F0 input_gate_output = gemmlowp::logistic(input_gate_input); + // Implementation of input modulation gate, using fixed-point tanh + // function. + F3 input_modulation_gate_input = + F3::FromRaw(activ_temp_data_int16[b * fc_output_depth + 1 * output_depth + c]); + F0 input_modulation_gate_output = gemmlowp::tanh(input_modulation_gate_input); + // Implementation of forget gate, using fixed-point logistic function. + F3 forget_gate_input = + F3::FromRaw(activ_temp_data_int16[b * fc_output_depth + 2 * output_depth + c]); + F0 forget_gate_output = gemmlowp::logistic(forget_gate_input); + // Implementation of output gate, using fixed-point logistic function. + F3 output_gate_input = + F3::FromRaw(activ_temp_data_int16[b * fc_output_depth + 3 * output_depth + c]); + F0 output_gate_output = gemmlowp::logistic(output_gate_input); + // Implementation of internal multiplication nodes, still in fixed-point. + F0 input_times_input_modulation = input_gate_output * input_modulation_gate_output; + FS prev_state = FS::FromRaw(prev_state_data_int16[b * output_depth + c]); + FS prev_state_times_forget_state = forget_gate_output * prev_state; + // Implementation of internal addition node, saturating. + FS new_state = + gemmlowp::SaturatingAdd(gemmlowp::Rescale(input_times_input_modulation), + prev_state_times_forget_state); + // Implementation of last internal Tanh node, still in fixed-point. + // Since a Tanh fixed-point implementation is specialized for a given + // number or integer bits, and each specialization can have a substantial + // code size, and we already used above a Tanh on an input with 3 integer + // bits, and per the table in the above function comment there is no + // significant accuracy to be lost by clamping to [-8, +8] for a + // 3-integer-bits representation, let us just do that. This helps people + // porting this to targets where code footprint must be minimized. + F3 new_state_f3 = gemmlowp::Rescale<3>(new_state); + F0 output_activ_int16 = output_gate_output * gemmlowp::tanh(new_state_f3); + // Store the new internal state back to memory, as 16-bit integers. + // Note: here we store the original value with StateIntegerBits, not + // the rescaled 3-integer-bits value fed to tanh. + output_state_data_int16[b * output_depth + c] = new_state.raw(); + // Down-scale the output activations to 8-bit integers, saturating, + // and store back to memory. + int16 rescaled_output_activ = gemmlowp::RoundingDivideByPOT(output_activ_int16.raw(), 8); + int16 clamped_output_activ = + std::max(-128, std::min(127, rescaled_output_activ)); + output_activ_data_uint8[b * output_depth + c] = 128 + clamped_output_activ; + } + } +} + +template +void Split(const SplitParams ¶ms, const RuntimeShape &input_shape, const Scalar *input_data, + const RuntimeShape *const *output_shapes, Scalar *const *output_data) +{ + ruy::profiler::ScopeLabel label("Split"); + const int split_dimensions = input_shape.DimensionsCount(); + int axis = params.axis < 0 ? params.axis + split_dimensions : params.axis; + int outputs_count = params.num_split; + TFLITE_DCHECK_LT(axis, split_dimensions); + + int64_t split_size = 0; + for (int i = 0; i < outputs_count; i++) + { + TFLITE_DCHECK_EQ(output_shapes[i]->DimensionsCount(), split_dimensions); + for (int j = 0; j < split_dimensions; j++) + { + if (j != axis) + { + MatchingDim(*output_shapes[i], j, input_shape, j); + } + } + split_size += output_shapes[i]->Dims(axis); + } + TFLITE_DCHECK_EQ(split_size, input_shape.Dims(axis)); + int64_t outer_size = 1; + for (int i = 0; i < axis; ++i) + { + outer_size *= input_shape.Dims(i); + } + // For all output arrays, + // FlatSize() = outer_size * Dims(axis) * base_inner_size; + int64_t base_inner_size = 1; + for (int i = axis + 1; i < split_dimensions; ++i) + { + base_inner_size *= input_shape.Dims(i); + } + + const Scalar *input_ptr = input_data; + for (int k = 0; k < outer_size; k++) + { + for (int i = 0; i < outputs_count; ++i) + { + const int copy_size = output_shapes[i]->Dims(axis) * base_inner_size; + memcpy(output_data[i] + k * copy_size, input_ptr, copy_size * sizeof(Scalar)); + input_ptr += copy_size; + } + } +} + +inline int NodeOffset(int b, int h, int w, int height, int width) +{ + return (b * height + h) * width + w; +} + +inline void LocalResponseNormalization(const tflite::LocalResponseNormalizationParams &op_params, + const RuntimeShape &input_shape, const float *input_data, + const RuntimeShape &output_shape, float *output_data) +{ + const int trailing_dim = input_shape.DimensionsCount() - 1; + const int outer_size = MatchingFlatSizeSkipDim(input_shape, trailing_dim, output_shape); + const int depth = MatchingDim(input_shape, trailing_dim, output_shape, trailing_dim); + + for (int i = 0; i < outer_size; ++i) + { + for (int c = 0; c < depth; ++c) + { + const int begin_input_c = std::max(0, static_cast(c - op_params.range)); + const int end_input_c = std::min(depth, static_cast(c + op_params.range)); + float accum = 0.f; + for (int input_c = begin_input_c; input_c < end_input_c; ++input_c) + { + const float input_val = input_data[i * depth + input_c]; + accum += input_val * input_val; + } + const float multiplier = std::pow(op_params.bias + op_params.alpha * accum, -op_params.beta); + output_data[i * depth + c] = input_data[i * depth + c] * multiplier; + } + } +} + +inline void Dequantize(const RuntimeShape &input_shape, const Eigen::half *input_data, + const RuntimeShape &output_shape, float *output_data) +{ + const int flat_size = MatchingFlatSize(input_shape, output_shape); + for (int i = 0; i < flat_size; i++) + { + output_data[i] = static_cast(input_data[i]); + } +} + +inline void FakeQuant(const tflite::FakeQuantParams &op_params, const RuntimeShape &input_shape, + const float *input_data, const RuntimeShape &output_shape, float *output_data) +{ + ruy::profiler::ScopeLabel label("FakeQuant"); + float rmin = op_params.minmax.min; + float rmax = op_params.minmax.max; + int num_bits = op_params.num_bits; + // 0 should always be a representable value. Let's assume that the initial + // min,max range contains 0. + TFLITE_DCHECK_LE(rmin, 0.0f); + TFLITE_DCHECK_GE(rmax, 0.0f); + TFLITE_DCHECK_LT(rmin, rmax); + + // Code matches tensorflow's FakeQuantWithMinMaxArgsFunctor. + int quant_min = 0; + int quant_max = (1 << num_bits) - 1; + float nudged_min, nudged_max, nudged_scale; + NudgeQuantizationRange(rmin, rmax, quant_min, quant_max, &nudged_min, &nudged_max, &nudged_scale); + const int flat_size = MatchingFlatSize(input_shape, output_shape); + FakeQuantizeArray(nudged_scale, nudged_min, nudged_max, input_data, output_data, flat_size); +} + +// Common subroutine for both `GatherNd` and `GatherNdString`. +struct GatherNdHelperResult +{ + int n_slices; + int slice_size; + int indices_nd; + std::vector dims_to_count; +}; + +// Returns common values being used on both `GatherNd` and `GatherNdString`. +inline GatherNdHelperResult GatherNdHelper(const RuntimeShape ¶ms_shape, + const RuntimeShape &indices_shape) +{ + GatherNdHelperResult ret; + ret.n_slices = 1; + ret.slice_size = 1; + const int indices_dims = indices_shape.DimensionsCount(); + ret.indices_nd = indices_shape.Dims(indices_dims - 1); + const int params_dims = params_shape.DimensionsCount(); + for (int i = 0; i < indices_dims - 1; ++i) + { + ret.n_slices *= indices_shape.Dims(i); + } + for (int i = ret.indices_nd; i < params_dims; ++i) + { + ret.slice_size *= params_shape.Dims(i); + } + + int remain_flat_size = params_shape.FlatSize(); + ret.dims_to_count = std::vector(ret.indices_nd, 0); + for (int i = 0; i < ret.indices_nd; ++i) + { + ret.dims_to_count[i] = remain_flat_size / params_shape.Dims(i); + remain_flat_size = ret.dims_to_count[i]; + } + + return ret; +} + +template +inline void GatherNd(const RuntimeShape ¶ms_shape, const ParamsT *params_data, + const RuntimeShape &indices_shape, const IndicesT *indices_data, + const RuntimeShape &output_shape, ParamsT *output_data) +{ + ruy::profiler::ScopeLabel label("GatherNd"); + + const GatherNdHelperResult res = GatherNdHelper(params_shape, indices_shape); + for (int i = 0; i < res.n_slices; ++i) + { + int from_pos = 0; + for (int j = 0; j < res.indices_nd; ++j) + { + from_pos += indices_data[i * res.indices_nd + j] * res.dims_to_count[j]; + } + std::memcpy(output_data + i * res.slice_size, params_data + from_pos, + sizeof(ParamsT) * res.slice_size); + } +} + +#ifndef TF_LITE_STATIC_MEMORY +template +inline void GatherNdString(const RuntimeShape ¶ms_shape, const TfLiteTensor *params_data, + const RuntimeShape &indices_shape, const IndicesT *indices_data, + const RuntimeShape &output_shape, TfLiteTensor *output_data) +{ + ruy::profiler::ScopeLabel label("GatherNdString"); + + const GatherNdHelperResult res = GatherNdHelper(params_shape, indices_shape); + DynamicBuffer buffer; + for (int i = 0; i < res.n_slices; ++i) + { + int from_pos = 0; + for (int j = 0; j < res.indices_nd; ++j) + { + from_pos += indices_data[i * res.indices_nd + j] * res.dims_to_count[j]; + } + for (int j = 0; j < res.slice_size; ++j) + { + buffer.AddString(GetString(params_data, from_pos + j)); + } + } + buffer.WriteToTensor(output_data, /*new_shape=*/nullptr); +} +#endif + +template +inline void ScatterNd(const RuntimeShape &indices_shape, const IndicesT *indices_data, + const RuntimeShape &updates_shape, const UpdatesT *updates_data, + const RuntimeShape &output_shape, UpdatesT *output_data) +{ + ruy::profiler::ScopeLabel label("ScatterNd"); + + int n_slices = 1; + int slice_size = 1; + const int outer_dims = indices_shape.DimensionsCount() - 1; + const int indices_nd = indices_shape.Dims(outer_dims); + const int updates_dims = updates_shape.DimensionsCount(); + for (int i = 0; i < outer_dims; ++i) + { + n_slices *= indices_shape.Dims(i); + } + for (int i = outer_dims; i < updates_dims; ++i) + { + slice_size *= updates_shape.Dims(i); + } + + int output_flat_size = output_shape.FlatSize(); + int remain_flat_size = output_flat_size; + std::vector dims_to_count(indices_nd, 0); + for (int i = 0; i < indices_nd; ++i) + { + dims_to_count[i] = remain_flat_size / output_shape.Dims(i); + remain_flat_size = dims_to_count[i]; + } + + memset(output_data, 0, sizeof(UpdatesT) * output_flat_size); + for (int i = 0; i < n_slices; ++i) + { + int to_pos = 0; + for (int j = 0; j < indices_nd; ++j) + { + IndicesT idx = indices_data[i * indices_nd + j]; + TFLITE_DCHECK(0 <= idx && idx < output_shape.Dims(j)); + to_pos += idx * dims_to_count[j]; + } + for (int j = 0; j < slice_size; j++) + { + output_data[to_pos + j] += updates_data[i * slice_size + j]; + } + } +} + +template +inline void Slice(const tflite::SliceParams &op_params, const RuntimeShape &input_shape, + const RuntimeShape &output_shape, SequentialTensorWriter *writer) +{ + const RuntimeShape ext_shape = RuntimeShape::ExtendedShape(5, input_shape); + TFLITE_DCHECK_LE(op_params.begin_count, 5); + TFLITE_DCHECK_LE(op_params.size_count, 5); + const int begin_count = op_params.begin_count; + const int size_count = op_params.size_count; + // We front-pad the begin and size vectors. + std::array start; + std::array stop; + for (int i = 0; i < 5; ++i) + { + int padded_i = 5 - i; + start[i] = begin_count < padded_i ? 0 : op_params.begin[begin_count - padded_i]; + stop[i] = (size_count < padded_i || op_params.size[size_count - padded_i] == -1) + ? ext_shape.Dims(i) + : start[i] + op_params.size[size_count - padded_i]; + } + + for (int i0 = start[0]; i0 < stop[0]; ++i0) + { + for (int i1 = start[1]; i1 < stop[1]; ++i1) + { + for (int i2 = start[2]; i2 < stop[2]; ++i2) + { + for (int i3 = start[3]; i3 < stop[3]; ++i3) + { + for (int i4 = start[4]; i4 < stop[4]; ++i4) + { + writer->Write(Offset(ext_shape, i0, i1, i2, i3, i4)); + } + } + } + } + } +} + +template +inline void Slice(const tflite::SliceParams &op_params, const RuntimeShape &input_shape, + const T *input_data, const RuntimeShape &output_shape, T *output_data) +{ + SequentialTensorWriter writer(input_data, output_data); + return Slice(op_params, input_shape, output_shape, &writer); +} + +template +inline void Slice(const tflite::SliceParams &op_params, const RuntimeShape &input_shape, + const TfLiteTensor *input, const RuntimeShape &output_shape, TfLiteTensor *output) +{ + SequentialTensorWriter writer(input, output); + return Slice(op_params, input_shape, output_shape, &writer); +} + +template +void Minimum(const RuntimeShape &input1_shape, const T *input1_data, const T *input2_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int flat_size = MatchingFlatSize(input1_shape, output_shape); + + auto min_value = input2_data[0]; + for (int i = 0; i < flat_size; i++) + { + output_data[i] = input1_data[i] > min_value ? min_value : input1_data[i]; + } +} + +// Convenience version that allows, for example, generated-code calls to be +// the same as other binary ops. +template +inline void Minimum(const RuntimeShape &input1_shape, const T *input1_data, const RuntimeShape &, + const T *input2_data, const RuntimeShape &output_shape, T *output_data) +{ + // Drop shape of second input: not needed. + Minimum(input1_shape, input1_data, input2_data, output_shape, output_data); +} + +template +void Maximum(const RuntimeShape &input1_shape, const T *input1_data, const T *input2_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int flat_size = MatchingFlatSize(input1_shape, output_shape); + + auto max_value = input2_data[0]; + for (int i = 0; i < flat_size; i++) + { + output_data[i] = input1_data[i] < max_value ? max_value : input1_data[i]; + } +} + +// Convenience version that allows, for example, generated-code calls to be +// the same as other binary ops. +template +inline void Maximum(const RuntimeShape &input1_shape, const T *input1_data, const RuntimeShape &, + const T *input2_data, const RuntimeShape &output_shape, T *output_data) +{ + // Drop shape of second input: not needed. + Maximum(input1_shape, input1_data, input2_data, output_shape, output_data); +} + +template +void ArgMax(const RuntimeShape &input1_shape, const T1 *input1_data, const T3 *input2_data, + const RuntimeShape &output_shape, T2 *output_data) +{ + ArgMinMax(input1_shape, input1_data, input2_data, output_shape, output_data, std::greater()); +} + +// Convenience version that allows, for example, generated-code calls to be +// the same as other binary ops. +template +inline void ArgMax(const RuntimeShape &input1_shape, const T1 *input1_data, + const RuntimeShape &input2_shape, const T3 *input2_data, + const RuntimeShape &output_shape, T2 *output_data) +{ + // Drop shape of second input: not needed. + ArgMax(input1_shape, input1_data, input2_data, output_shape, output_data); +} + +template +void Select(const RuntimeShape &input_condition_shape, const D *input_condition_data, + const RuntimeShape &input_x_shape, const T *input_x_data, + const RuntimeShape &input_y_shape, const T *input_y_data, + const RuntimeShape &output_shape, T *output_data) +{ + int64_t flatsize; + // Allow select operator executions on mixed scalar tensors and one element + // tensors. + if (input_condition_shape.FlatSize() == 1 && input_x_shape.FlatSize() == 1 && + input_y_shape.FlatSize() == 1 && output_shape.FlatSize() == 1) + { + flatsize = 1; + } + else + { + flatsize = MatchingFlatSize(input_condition_shape, input_x_shape, input_y_shape, output_shape); + } + for (int64_t i = 0; i < flatsize; ++i) + { + output_data[i] = input_condition_data[i] ? input_x_data[i] : input_y_data[i]; + } +} + +template +void RankOneSelect(const RuntimeShape &input_condition_shape, const D *input_condition_data, + const RuntimeShape &input_x_shape, const T *input_x_data, + const RuntimeShape &input_y_shape, const T *input_y_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int64_t outer_size = input_condition_shape.FlatSize(); + int64_t inner_size; + if (input_condition_shape.DimensionsCount() == 0) + { + inner_size = MatchingFlatSize(input_x_shape, input_y_shape, output_shape); + } + else + { + TFLITE_DCHECK_EQ(MatchingDim(input_x_shape, 0, input_y_shape, 0, output_shape, 0), outer_size); + inner_size = MatchingFlatSizeSkipDim(input_x_shape, 0, input_y_shape, output_shape); + } + + int64_t offset = 0; + for (int64_t i = 0; i < outer_size; i++) + { + const T *input_data = input_condition_data[i] ? input_x_data : input_y_data; + memcpy(output_data + offset, input_data + offset, inner_size * sizeof(T)); + offset += inner_size; + } +} + +template +void BroadcastSelect4DSlow(const RuntimeShape &input_condition_shape, const D *input_condition_data, + const RuntimeShape &input_x_shape, const T *input_x_data, + const RuntimeShape &input_y_shape, const T *input_y_data, + const RuntimeShape &output_shape, T *output_data) +{ + TFLITE_DCHECK_LE(input_condition_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(input_x_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(input_y_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(output_shape.DimensionsCount(), 4); + + const RuntimeShape extended_output_shape = RuntimeShape::ExtendedShape(4, output_shape); + + NdArrayDesc<4> desc_condition; + NdArrayDesc<4> desc_x; + NdArrayDesc<4> desc_y; + NdArrayDescsForElementwiseBroadcast(input_condition_shape, input_x_shape, input_y_shape, + &desc_condition, &desc_x, &desc_y); + + // In Tensorflow, the dimensions are canonically named (batch_number, row, + // col, channel), with extents (batches, height, width, depth), with the + // trailing dimension changing most rapidly (channels has the smallest + // stride, typically 1 element). + // + // In generated C code, we store arrays with the dimensions reversed. The + // first dimension has smallest stride. + // + // We name our variables by their Tensorflow convention, but generate C code + // nesting loops such that the innermost loop has the smallest stride for + // the best cache behavior. + for (int b = 0; b < extended_output_shape.Dims(0); ++b) + { + for (int y = 0; y < extended_output_shape.Dims(1); ++y) + { + for (int x = 0; x < extended_output_shape.Dims(2); ++x) + { + for (int c = 0; c < extended_output_shape.Dims(3); ++c) + { + const int condition_index = SubscriptToIndex(desc_condition, b, y, x, c); + const int x_index = SubscriptToIndex(desc_x, b, y, x, c); + const int y_index = SubscriptToIndex(desc_y, b, y, x, c); + output_data[Offset(extended_output_shape, b, y, x, c)] = + input_condition_data[condition_index] ? input_x_data[x_index] : input_y_data[y_index]; + } + } + } + } +} + +template +void SelectTrueCoords(const RuntimeShape &input_condition_shape, const D *input_condition_data, + T *output_data) +{ + const size_t size = input_condition_shape.FlatSize(); + if (size == 0) + { + // Dimension is zero, in which case we don't need to output. + return; + } + const size_t cond_rank = input_condition_shape.DimensionsCount(); + + std::vector dims_to_count(cond_rank, 0); + int cur_flat_size = size; + for (int i = 0; i < cond_rank; ++i) + { + dims_to_count[i] = cur_flat_size / input_condition_shape.Dims(i); + cur_flat_size = dims_to_count[i]; + } + + int output_index = 0; + for (int i = 0; i < size; ++i) + { + if (input_condition_data[i]) + { + // Insert the coordinate of the current item (row major) into output. + int flat_index = i; + for (int j = 0; j < cond_rank; ++j) + { + int coord_j = flat_index / dims_to_count[j]; + output_data[output_index * cond_rank + j] = coord_j; + flat_index %= dims_to_count[j]; + } + output_index++; + } + } +} + +// For easy implementation, the indices is always a vector of size-4 vectors. +template +inline void SparseToDense(const std::vector> &indices, const T *values, + T default_value, bool value_is_scalar, + const RuntimeShape &unextended_output_shape, T *output_data) +{ + TFLITE_DCHECK_LE(unextended_output_shape.DimensionsCount(), 4); + const RuntimeShape output_shape = RuntimeShape::ExtendedShape(4, unextended_output_shape); + const int value_count = indices.size(); + + // First fill the output_data with default value. + const int num_elements = output_shape.FlatSize(); + for (int i = 0; i < num_elements; ++i) + { + output_data[i] = default_value; + } + + // Special handle for value is scalar case to avoid checking the boolean + // condition within the loop every time. + if (value_is_scalar) + { + for (int i = 0; i < value_count; ++i) + { + const std::vector &index = indices[i]; + TFLITE_DCHECK_EQ(index.size(), 4); + const T value = *values; // just use the first value. + output_data[Offset(output_shape, index[0], index[1], index[2], index[3])] = value; + } + return; + } + + // Go through the values and indices to fill the sparse values. + for (int i = 0; i < value_count; ++i) + { + const std::vector &index = indices[i]; + TFLITE_DCHECK_EQ(index.size(), 4); + const T value = values[i]; + output_data[Offset(output_shape, index[0], index[1], index[2], index[3])] = value; + } +} + +template +inline void Pow(const RuntimeShape &input1_shape, const T *input1_data, + const RuntimeShape &input2_shape, const T *input2_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int flat_size = MatchingFlatSize(input1_shape, input2_shape, output_shape); + for (int i = 0; i < flat_size; ++i) + { + output_data[i] = std::pow(input1_data[i], input2_data[i]); + } +} + +template +inline void BroadcastPow4DSlow(const RuntimeShape &unextended_input1_shape, const T *input1_data, + const RuntimeShape &unextended_input2_shape, const T *input2_data, + const RuntimeShape &unextended_output_shape, T *output_data) +{ + TFLITE_DCHECK_LE(unextended_input1_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_input2_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_output_shape.DimensionsCount(), 4); + const RuntimeShape output_shape = RuntimeShape::ExtendedShape(4, unextended_output_shape); + + NdArrayDesc<4> desc1; + NdArrayDesc<4> desc2; + NdArrayDescsForElementwiseBroadcast(unextended_input1_shape, unextended_input2_shape, &desc1, + &desc2); + + for (int b = 0; b < output_shape.Dims(0); ++b) + { + for (int y = 0; y < output_shape.Dims(1); ++y) + { + for (int x = 0; x < output_shape.Dims(2); ++x) + { + for (int c = 0; c < output_shape.Dims(3); ++c) + { + auto out_idx = Offset(output_shape, b, y, x, c); + auto in1_idx = SubscriptToIndex(desc1, b, y, x, c); + auto in2_idx = SubscriptToIndex(desc2, b, y, x, c); + auto in1_val = input1_data[in1_idx]; + auto in2_val = input2_data[in2_idx]; + output_data[out_idx] = std::pow(in1_val, in2_val); + } + } + } + } +} + +template +void Reverse(int axis, const RuntimeShape &input_shape, const Scalar *input_data, + const RuntimeShape &output_shape, Scalar *output_data) +{ + ruy::profiler::ScopeLabel label("Reverse"); + + int outer_size = 1; + for (int i = 0; i < axis; ++i) + { + outer_size *= input_shape.Dims(i); + } + + int copy_size = 1; + for (int i = axis + 1; i < input_shape.DimensionsCount(); ++i) + { + copy_size *= input_shape.Dims(i); + } + + const int dims_at_axis = input_shape.Dims(axis); + for (int i = 0; i < outer_size; ++i) + { + for (int j = 0; j < dims_at_axis; ++j) + { + const int start_pos = (i * dims_at_axis + j) * copy_size; + Scalar *output_ptr = output_data + start_pos; + int loc = (i * dims_at_axis + dims_at_axis - j - 1) * copy_size; + memcpy(output_ptr, input_data + loc, copy_size * sizeof(Scalar)); + } + } +} + +template +void ReverseSequence(const TS *seq_lengths, const int seq_dim, const int batch_dim, + const RuntimeShape &input_shape, const Scalar *input_data, + const RuntimeShape &output_shape, Scalar *output_data) +{ + ruy::profiler::ScopeLabel label("ReverseSequence"); + + int outer_size = 1; + int outer_dim = std::min(batch_dim, seq_dim); + int medium_dim = std::max(batch_dim, seq_dim); + for (int i = 0; i < outer_dim; ++i) + { + outer_size *= input_shape.Dims(i); + } + + int medium_size = 1; + for (int i = outer_dim + 1; i < medium_dim; ++i) + { + medium_size *= input_shape.Dims(i); + } + + int copy_size = 1; + for (int i = medium_dim + 1; i < input_shape.DimensionsCount(); ++i) + { + copy_size *= input_shape.Dims(i); + } + + const int dims_at_outer_dim = input_shape.Dims(outer_dim); + const int dims_at_medium_dim = input_shape.Dims(medium_dim); + + Scalar *output_ptr; + if (batch_dim > seq_dim) + { + for (int i = 0; i < outer_size; ++i) + { + for (int j = 0; j < dims_at_outer_dim; ++j) + { + const int in_pos_base = (i * dims_at_outer_dim + j) * medium_size; + for (int p = 0; p < medium_size; ++p) + { + for (int q = 0; q < dims_at_medium_dim; ++q) + { + const int in_pos = ((in_pos_base + p) * dims_at_medium_dim + q) * copy_size; + const Scalar *in_ptr = input_data + in_pos; + int sl = seq_lengths[q] - 1; + if (j > sl) + { + output_ptr = output_data + in_pos; + } + else + { + const int out_pos_base = (i * dims_at_outer_dim + sl - j) * medium_size; + const int out_pos = ((out_pos_base + p) * dims_at_medium_dim + q) * copy_size; + output_ptr = output_data + out_pos; + } + memcpy(output_ptr, in_ptr, copy_size * sizeof(Scalar)); + } + } + } + } + } + else if (batch_dim < seq_dim) + { + for (int i = 0; i < outer_size; ++i) + { + for (int j = 0; j < dims_at_outer_dim; ++j) + { + const int in_pos_base = (i * dims_at_outer_dim + j) * medium_size; + int sl = seq_lengths[j] - 1; + const int out_pos_base = (i * dims_at_outer_dim + j) * medium_size; + for (int p = 0; p < medium_size; ++p) + { + for (int q = 0; q < dims_at_medium_dim; ++q) + { + const int in_pos = ((in_pos_base + p) * dims_at_medium_dim + q) * copy_size; + const Scalar *in_ptr = input_data + in_pos; + if (q > sl) + { + output_ptr = output_data + in_pos; + } + else + { + const int out_pos = ((out_pos_base + p) * dims_at_medium_dim + sl - q) * copy_size; + output_ptr = output_data + out_pos; + } + memcpy(output_ptr, in_ptr, copy_size * sizeof(Scalar)); + } + } + } + } + } +} + +template +inline void SegmentSum(const RuntimeShape &input_shape, const T *input_data, + const RuntimeShape &segment_ids_shape, const int32_t *segment_ids_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int segment_flat_size = MatchingFlatSizeSkipDim(input_shape, 0, output_shape); + + memset(output_data, 0, sizeof(T) * output_shape.FlatSize()); + + for (int i = 0; i < input_shape.Dims(0); i++) + { + int output_index = segment_ids_data[i]; + for (int j = 0; j < segment_flat_size; ++j) + { + output_data[output_index * segment_flat_size + j] += input_data[i * segment_flat_size + j]; + } + } +} + +} // namespace reference_ops +} // namespace tflite + +#endif // LUCI_INTERPRETER_PAL_REFERENCE_OPS_H diff --git a/onert-micro/luci-interpreter/pal/cmsisnn/pal.cmake b/onert-micro/luci-interpreter/pal/cmsisnn/pal.cmake new file mode 100644 index 0000000..5110474 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/cmsisnn/pal.cmake @@ -0,0 +1,83 @@ +macro(initialize_pal) + nnas_find_package(TensorFlowSource EXACT 2.6.0 REQUIRED) + nnas_find_package(TensorFlowGEMMLowpSource EXACT 2.6.0 REQUIRED) + nnas_find_package(TensorFlowEigenSource EXACT 2.6.0 REQUIRED) + nnas_find_package(TensorFlowRuySource EXACT 2.6.0 REQUIRED) + nnas_find_package(CMSIS-NN EXACT 4.0.0 REQUIRED) + + if (NOT TensorFlowSource_FOUND) + message(STATUS "Skipping luci-interpreter: TensorFlow not found") + return() + endif () + + if (NOT TensorFlowGEMMLowpSource_FOUND) + message(STATUS "Skipping luci-interpreter: gemmlowp not found") + return() + endif () + + if (NOT TensorFlowEigenSource_FOUND) + message(STATUS "Skipping luci-interpreter: Eigen not found") + return() + endif () + + if (NOT TensorFlowRuySource_FOUND) + message(STATUS "Skipping luci-interpreter: Ruy not found") + return() + endif () + + if (NOT CMSISSource_FOUND) + message(STATUS "Skipping luci-interpreter: CMSISSource not found") + return() + endif () + + if (NOT CMSIS_NNSource_FOUND) + message(STATUS "Skipping luci-interpreter: CMSIS-NN not found") + return() + endif () + + set(PAL_INITIALIZED TRUE) +endmacro() + +macro(add_pal_to_target TGT) + target_include_directories(${TGT} PRIVATE "${PAL}") + target_include_directories(${TGT} PRIVATE + "${TensorFlowRuySource_DIR}" + "${TensorFlowGEMMLowpSource_DIR}" + "${TensorFlowEigenSource_DIR}" + "${TensorFlowSource_DIR}") + target_include_directories(${TGT} PRIVATE ${LUCI_INTERPRETER_PAL_DIR}) + + file(GLOB_RECURSE PAL_SOURCES "${CMSIS_NNSource_DIR}/Source/ActivationFunctions/*.c" + "${CMSIS_NNSource_DIR}/Source/BasicMathFunctions/*.c" + "${CMSIS_NNSource_DIR}/Source/ConcatenationFunctions/*.c" + "${CMSIS_NNSource_DIR}/Source/ConvolutionFunctions/*.c" + "${CMSIS_NNSource_DIR}/Source/FullyConnectedFunctions/*.c" + "${CMSIS_NNSource_DIR}/Source/LSTMFunctions/*.c" + "${CMSIS_NNSource_DIR}/Source/NNSupportFunctions/*.c" + "${CMSIS_NNSource_DIR}/Source/PoolingFunctions/*.c" + "${CMSIS_NNSource_DIR}/Source/ReshapeFunctions/*.c" + "${CMSIS_NNSource_DIR}/Source/SoftmaxFunctions/*.c") + + list(APPEND PAL_SOURCES ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/quantization_util.cc + ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/tensor_utils.cc + ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/reference/portable_tensor_utils.cc) + add_library(luci_interpreter_cmsisnn_pal STATIC ${PAL_SOURCES}) + set_property(TARGET luci_interpreter_cmsisnn_pal PROPERTY POSITION_INDEPENDENT_CODE ON) + target_include_directories(luci_interpreter_cmsisnn_pal PRIVATE + "${TensorFlowRuySource_DIR}" + "${TensorFlowGEMMLowpSource_DIR}" + "${TensorFlowEigenSource_DIR}" + "${TensorFlowSource_DIR}" + "${CMSIS_NNSource_DIR}" + ) + + set(CMSIS_PATH ${CMSISSource_DIR} CACHE INTERNAL "CMSIS_PATH") + add_subdirectory(${CMSIS_NNSource_DIR} ${CMAKE_CURRENT_BINARY_DIR}/CMSISNN) + + target_include_directories(luci_interpreter_cmsisnn_pal PUBLIC + "${CMSISSource_DIR}/CMSIS/DSP/Include" + "${CMSISSource_DIR}/CMSIS/Core/Include" + "${CMSIS_NNSource_DIR}/Include") + + target_link_libraries(${TGT} PRIVATE luci_interpreter_cmsisnn_pal) +endmacro() diff --git a/onert-micro/luci-interpreter/pal/linux/KernelsToBuild.lst b/onert-micro/luci-interpreter/pal/linux/KernelsToBuild.lst new file mode 100644 index 0000000..1f4ccb3 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/linux/KernelsToBuild.lst @@ -0,0 +1,77 @@ +REGISTER_KERNEL(ADD, Add) +REGISTER_KERNEL(ARG_MAX, ArgMax) +REGISTER_KERNEL(AVERAGE_POOL_2D, AveragePool2D) +REGISTER_KERNEL(BATCH_MATMUL, BatchMatMul) +REGISTER_KERNEL(BATCH_TO_SPACE_ND, BatchToSpaceND) +REGISTER_KERNEL(CAST, Cast) +REGISTER_KERNEL(CONCATENATION, Concatenation) +REGISTER_KERNEL(CONV_2D, Conv2D) +REGISTER_KERNEL(DEPTH_TO_SPACE, DepthToSpace) +REGISTER_KERNEL(DEPTHWISE_CONV_2D, DepthwiseConv2D) +REGISTER_KERNEL(DEQUANTIZE, Dequantize) +REGISTER_KERNEL(DIV, Div) +REGISTER_KERNEL(ELU, Elu) +REGISTER_KERNEL(EXP, Exp) +REGISTER_KERNEL(EXPAND_DIMS, ExpandDims) +REGISTER_KERNEL(FILL, Fill) +REGISTER_KERNEL(FLOOR, Floor) +REGISTER_KERNEL(FLOOR_DIV, FloorDiv) +REGISTER_KERNEL(EQUAL, Equal) +REGISTER_KERNEL(FULLY_CONNECTED, FullyConnected) +REGISTER_KERNEL(GATHER, Gather) +REGISTER_KERNEL(GREATER, Greater) +REGISTER_KERNEL(GREATER_EQUAL, GreaterEqual) +REGISTER_KERNEL(IF, If) +REGISTER_KERNEL(INSTANCE_NORM, InstanceNorm) +REGISTER_KERNEL(L2_NORMALIZATION, L2Normalize) +REGISTER_KERNEL(L2_POOL_2D, L2Pool2D) +REGISTER_KERNEL(LEAKY_RELU, LeakyRelu) +REGISTER_KERNEL(LESS, Less) +REGISTER_KERNEL(LESS_EQUAL, LessEqual) +REGISTER_KERNEL(LOCAL_RESPONSE_NORMALIZATION, LocalResponseNormalization) +REGISTER_KERNEL(LOGICAL_AND, LogicalAnd) +REGISTER_KERNEL(LOGICAL_NOT, LogicalNot) +REGISTER_KERNEL(LOGICAL_OR, LogicalOr) +REGISTER_KERNEL(LOGISTIC, Logistic) +REGISTER_KERNEL(LOG_SOFTMAX, LogSoftmax) +REGISTER_KERNEL(MAXIMUM, Maximum) +REGISTER_KERNEL(MAX_POOL_2D, MaxPool2D) +REGISTER_KERNEL(MEAN, Mean) +REGISTER_KERNEL(MINIMUM, Minimum) +REGISTER_KERNEL(MIRROR_PAD, MirrorPad) +REGISTER_KERNEL(MUL, Mul) +REGISTER_KERNEL(NEG, Neg) +REGISTER_KERNEL(NOT_EQUAL, NotEqual) +REGISTER_KERNEL(ONE_HOT, OneHot) +REGISTER_KERNEL(PACK, Pack) +REGISTER_KERNEL(PAD, Pad) +REGISTER_KERNEL(PADV2, PadV2) +REGISTER_KERNEL(POW, Pow) +REGISTER_KERNEL(PRELU, PRelu) +REGISTER_KERNEL(QUANTIZE, Quantize) +REGISTER_KERNEL(RELU, Relu) +REGISTER_KERNEL(RELU6, Relu6) +REGISTER_KERNEL(RESHAPE, Reshape) +REGISTER_KERNEL(RESIZE_BILINEAR, ResizeBilinear) +REGISTER_KERNEL(RESIZE_NEAREST_NEIGHBOR, ResizeNearestNeighbor) +REGISTER_KERNEL(REVERSE_V2, ReverseV2) +REGISTER_KERNEL(RSQRT, Rsqrt) +REGISTER_KERNEL(SHAPE, Shape) +REGISTER_KERNEL(SLICE, Slice) +REGISTER_KERNEL(SOFTMAX, Softmax) +REGISTER_KERNEL(SPACE_TO_BATCH_ND, SpaceToBatchND) +REGISTER_KERNEL(SPACE_TO_DEPTH, SpaceToDepth) +REGISTER_KERNEL(SPLIT, Split) +REGISTER_KERNEL(SPLIT_V, SplitV) +REGISTER_KERNEL(STRIDED_SLICE, StridedSlice) +REGISTER_KERNEL(SQRT, Sqrt) +REGISTER_KERNEL(SQUARE, Square) +REGISTER_KERNEL(SQUARED_DIFFERENCE, SquaredDifference) +REGISTER_KERNEL(SQUEEZE, Squeeze) +REGISTER_KERNEL(SUB, Sub) +REGISTER_KERNEL(SVDF, SVDF) +REGISTER_KERNEL(TANH, Tanh) +REGISTER_KERNEL(TRANSPOSE, Transpose) +REGISTER_KERNEL(TRANSPOSE_CONV, TransposeConv) +REGISTER_KERNEL(UNPACK, Unpack) +REGISTER_KERNEL(WHILE, While) diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALArgMax.h b/onert-micro/luci-interpreter/pal/linux/PALArgMax.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALArgMax.h rename to onert-micro/luci-interpreter/pal/linux/PALArgMax.h diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALAveragePool2d.h b/onert-micro/luci-interpreter/pal/linux/PALAveragePool2d.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALAveragePool2d.h rename to onert-micro/luci-interpreter/pal/linux/PALAveragePool2d.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALBatchMatMul.h b/onert-micro/luci-interpreter/pal/linux/PALBatchMatMul.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALBatchMatMul.h rename to onert-micro/luci-interpreter/pal/linux/PALBatchMatMul.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALBatchToSpaceND.h b/onert-micro/luci-interpreter/pal/linux/PALBatchToSpaceND.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALBatchToSpaceND.h rename to onert-micro/luci-interpreter/pal/linux/PALBatchToSpaceND.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALConv2d.h b/onert-micro/luci-interpreter/pal/linux/PALConv2d.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALConv2d.h rename to onert-micro/luci-interpreter/pal/linux/PALConv2d.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALDepthToSpace.h b/onert-micro/luci-interpreter/pal/linux/PALDepthToSpace.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALDepthToSpace.h rename to onert-micro/luci-interpreter/pal/linux/PALDepthToSpace.h diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALDepthwiseConv2d.h b/onert-micro/luci-interpreter/pal/linux/PALDepthwiseConv2d.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALDepthwiseConv2d.h rename to onert-micro/luci-interpreter/pal/linux/PALDepthwiseConv2d.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALDequantize.h b/onert-micro/luci-interpreter/pal/linux/PALDequantize.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALDequantize.h rename to onert-micro/luci-interpreter/pal/linux/PALDequantize.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALElu.h b/onert-micro/luci-interpreter/pal/linux/PALElu.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALElu.h rename to onert-micro/luci-interpreter/pal/linux/PALElu.h diff --git a/onert-micro/luci-interpreter/pal/linux/PALFill.h b/onert-micro/luci-interpreter/pal/linux/PALFill.h new file mode 100644 index 0000000..4ef1af2 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/linux/PALFill.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_FILL_H +#define LUCI_INTERPRETER_PAL_FILL_H + +#include "tensorflow/lite/kernels/internal/reference/reference_ops.h" + +#endif // LUCI_INTERPRETER_PAL_FILL_H diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALFullyConnected.h b/onert-micro/luci-interpreter/pal/linux/PALFullyConnected.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALFullyConnected.h rename to onert-micro/luci-interpreter/pal/linux/PALFullyConnected.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALGather.h b/onert-micro/luci-interpreter/pal/linux/PALGather.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALGather.h rename to onert-micro/luci-interpreter/pal/linux/PALGather.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALL2Normalize.h b/onert-micro/luci-interpreter/pal/linux/PALL2Normalize.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALL2Normalize.h rename to onert-micro/luci-interpreter/pal/linux/PALL2Normalize.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALL2Pool2D.h b/onert-micro/luci-interpreter/pal/linux/PALL2Pool2D.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALL2Pool2D.h rename to onert-micro/luci-interpreter/pal/linux/PALL2Pool2D.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALLeakyRelu.h b/onert-micro/luci-interpreter/pal/linux/PALLeakyRelu.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALLeakyRelu.h rename to onert-micro/luci-interpreter/pal/linux/PALLeakyRelu.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALLocalResponseNormalization.h b/onert-micro/luci-interpreter/pal/linux/PALLocalResponseNormalization.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALLocalResponseNormalization.h rename to onert-micro/luci-interpreter/pal/linux/PALLocalResponseNormalization.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALLogSoftmax.h b/onert-micro/luci-interpreter/pal/linux/PALLogSoftmax.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALLogSoftmax.h rename to onert-micro/luci-interpreter/pal/linux/PALLogSoftmax.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALMul.h b/onert-micro/luci-interpreter/pal/linux/PALMul.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALMul.h rename to onert-micro/luci-interpreter/pal/linux/PALMul.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALNeg.h b/onert-micro/luci-interpreter/pal/linux/PALNeg.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALNeg.h rename to onert-micro/luci-interpreter/pal/linux/PALNeg.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALQuantize.h b/onert-micro/luci-interpreter/pal/linux/PALQuantize.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALQuantize.h rename to onert-micro/luci-interpreter/pal/linux/PALQuantize.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALRelu.h b/onert-micro/luci-interpreter/pal/linux/PALRelu.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALRelu.h rename to onert-micro/luci-interpreter/pal/linux/PALRelu.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALRelu6.h b/onert-micro/luci-interpreter/pal/linux/PALRelu6.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALRelu6.h rename to onert-micro/luci-interpreter/pal/linux/PALRelu6.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALResizeBilinear.h b/onert-micro/luci-interpreter/pal/linux/PALResizeBilinear.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALResizeBilinear.h rename to onert-micro/luci-interpreter/pal/linux/PALResizeBilinear.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALResizeNearestNeighbor.h b/onert-micro/luci-interpreter/pal/linux/PALResizeNearestNeighbor.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALResizeNearestNeighbor.h rename to onert-micro/luci-interpreter/pal/linux/PALResizeNearestNeighbor.h diff --git a/onert-micro/luci-interpreter/pal/linux/PALSVDF.h b/onert-micro/luci-interpreter/pal/linux/PALSVDF.h new file mode 100644 index 0000000..101d045 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/linux/PALSVDF.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_SVDF_H +#define LUCI_INTERPRETER_PAL_SVDF_H + +#include + +namespace luci_interpreter_pal +{ +static inline void +IntegerSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, + const int8_t *input_data, const tflite::RuntimeShape &weight_feature_shape, + const int8_t *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, + const int16_t *weight_time_data, const tflite::RuntimeShape &bias_shape, + const int32_t *bias_data, int16_t *activation_state_data, + const tflite::RuntimeShape &output_shape, int8_t *output_data, int32_t *scratchpad_data, + int32_t *output_temp_data, int32_t scale_1_a, int scale_1_b, int32_t scale_2_a, + int scale_2_b, int32_t input_zp, int32_t output_zp) +{ + tflite::reference_ops::EvalIntegerSVDF(¶ms, input_shape, input_data, weight_feature_shape, + weight_feature_data, weight_time_shape, weight_time_data, + bias_shape, bias_data, activation_state_data, output_shape, + output_data, scratchpad_data, output_temp_data, scale_1_a, + scale_1_b, scale_2_a, scale_2_b, input_zp, output_zp); +} +static inline void +FloatSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, + const float *input_data, const tflite::RuntimeShape &weight_feature_shape, + const float *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, + const float *weight_time_data, const tflite::RuntimeShape &bias_shape, + const float *bias_data, float *scratchpad_data, float *activation_state_data, + const tflite::RuntimeShape &output_shape, float *output_data) +{ + tflite::reference_ops::EvalFloatSVDF(¶ms, input_shape, input_data, weight_feature_shape, + weight_feature_data, weight_time_shape, weight_time_data, + bias_shape, bias_data, scratchpad_data, + activation_state_data, output_shape, output_data); +} + +static inline void SetupScratchpadTensor( + const luci_interpreter::DataType &input_data_type, + const luci_interpreter::DataType &weight_feature_data_type, + luci_interpreter::Tensor *scratchpad_1, luci_interpreter::Tensor *scratchpad_2, + luci_interpreter::Tensor *scratchpad_3, luci_interpreter::Tensor *scratchpad_4, + luci_interpreter::Tensor *scratchpad_5, luci_interpreter::Tensor *scratchpad_6, + const luci_interpreter::Shape input_shape, const luci_interpreter::Shape weight_time_shape, + const int32_t batch_size, const int32_t num_filters, const int32_t num_units) +{ + + if (input_data_type == luci_interpreter::DataType::FLOAT32 && + (weight_feature_data_type == luci_interpreter::DataType::S8 || + weight_feature_data_type == luci_interpreter::DataType::U8)) + { + (void)input_shape; + (void)weight_time_shape; + (void)scratchpad_3; + (void)scratchpad_4; + (void)scratchpad_5; + (void)scratchpad_6; + + assert(false && "Hybrid type is not currently supported for linux platform"); + } + + // Resize scratchpad_1 tensor + scratchpad_1->resize({batch_size, num_filters}); + + if (input_data_type == luci_interpreter::DataType::S8) + { + // Resize scratchpad_2 for full_integer op + scratchpad_2->resize({batch_size, num_units}); + } +} + +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_SVDF_H diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALSlice.h b/onert-micro/luci-interpreter/pal/linux/PALSlice.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALSlice.h rename to onert-micro/luci-interpreter/pal/linux/PALSlice.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALSoftmax.h b/onert-micro/luci-interpreter/pal/linux/PALSoftmax.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALSoftmax.h rename to onert-micro/luci-interpreter/pal/linux/PALSoftmax.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALSpaceToBatchND.h b/onert-micro/luci-interpreter/pal/linux/PALSpaceToBatchND.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALSpaceToBatchND.h rename to onert-micro/luci-interpreter/pal/linux/PALSpaceToBatchND.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALSpaceToDepth.h b/onert-micro/luci-interpreter/pal/linux/PALSpaceToDepth.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALSpaceToDepth.h rename to onert-micro/luci-interpreter/pal/linux/PALSpaceToDepth.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALSplit.h b/onert-micro/luci-interpreter/pal/linux/PALSplit.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALSplit.h rename to onert-micro/luci-interpreter/pal/linux/PALSplit.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALSub.h b/onert-micro/luci-interpreter/pal/linux/PALSub.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALSub.h rename to onert-micro/luci-interpreter/pal/linux/PALSub.h diff --git a/onert-micro/luci-interpreter/pal/linux/pal.cmake b/onert-micro/luci-interpreter/pal/linux/pal.cmake new file mode 100644 index 0000000..c373b96 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/linux/pal.cmake @@ -0,0 +1,82 @@ +macro(initialize_pal) + nnas_find_package(TensorFlowSource EXACT 2.6.0 QUIET) + nnas_find_package(TensorFlowGEMMLowpSource EXACT 2.6.0 QUIET) + nnas_find_package(TensorFlowEigenSource EXACT 2.6.0 QUIET) + nnas_find_package(TensorFlowRuySource EXACT 2.6.0 QUIET) + + if (NOT TensorFlowSource_FOUND) + message(STATUS "Skipping luci-interpreter: TensorFlow not found") + return() + endif () + + if (NOT TensorFlowGEMMLowpSource_FOUND) + message(STATUS "Skipping luci-interpreter: gemmlowp not found") + return() + endif () + + if (NOT TensorFlowEigenSource_FOUND) + message(STATUS "Skipping luci-interpreter: Eigen not found") + return() + endif () + + if (NOT TensorFlowRuySource_FOUND) + message(STATUS "Skipping luci-interpreter: Ruy not found") + return() + endif () + + find_package(Threads REQUIRED) + + set(PAL_INITIALIZED TRUE) +endmacro() + +macro(add_pal_to_target TGT) + target_include_directories(${TGT} PRIVATE "${PAL}") + target_include_directories(${TGT} SYSTEM PRIVATE + "${TensorFlowRuySource_DIR}" + "${TensorFlowGEMMLowpSource_DIR}" + "${TensorFlowEigenSource_DIR}" + "${TensorFlowSource_DIR}") + target_include_directories(${TGT} PRIVATE ${LUCI_INTERPRETER_PAL_DIR}) + + # TODO put it back, I changed my mind. + # instead add sources with visitors in this library + set(PAL_SOURCES ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/tensor_utils.cc + ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/reference/portable_tensor_utils.cc + ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/quantization_util.cc) + + if(BUILD_ARM32_NEON) + # NOTE may need to revise this list for version upgrade + set(PAL_SOURCES ${PAL_SOURCES} + ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/optimized/neon_tensor_utils.cc + ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/optimized/cpu_check.cc + ${TensorFlowRuySource_DIR}/ruy/allocator.cc + ${TensorFlowRuySource_DIR}/ruy/block_map.cc + ${TensorFlowRuySource_DIR}/ruy/blocking_counter.cc + ${TensorFlowRuySource_DIR}/ruy/context_get_ctx.cc + ${TensorFlowRuySource_DIR}/ruy/cpuinfo.cc + ${TensorFlowRuySource_DIR}/ruy/ctx.cc + ${TensorFlowRuySource_DIR}/ruy/denormal.cc + ${TensorFlowRuySource_DIR}/ruy/frontend.cc + ${TensorFlowRuySource_DIR}/ruy/pack_arm.cc + ${TensorFlowRuySource_DIR}/ruy/prepacked_cache.cc + ${TensorFlowRuySource_DIR}/ruy/prepare_packed_matrices.cc + ${TensorFlowRuySource_DIR}/ruy/system_aligned_alloc.cc + ${TensorFlowRuySource_DIR}/ruy/thread_pool.cc + ${TensorFlowRuySource_DIR}/ruy/trmul.cc + ${TensorFlowRuySource_DIR}/ruy/tune.cc + ${TensorFlowRuySource_DIR}/ruy/wait.cc + ${TensorFlowRuySource_DIR}/ruy/kernel_arm32.cc + ) + endif(BUILD_ARM32_NEON) + + add_library(luci_interpreter_linux_pal_micro STATIC ${PAL_SOURCES}) + set_target_properties(luci_interpreter_linux_pal_micro PROPERTIES POSITION_INDEPENDENT_CODE ON) + target_include_directories(luci_interpreter_linux_pal_micro SYSTEM PRIVATE + "${TensorFlowRuySource_DIR}" + "${TensorFlowGEMMLowpSource_DIR}" + "${TensorFlowEigenSource_DIR}" + "${TensorFlowSource_DIR}" + ) + + target_link_libraries(${TGT} PRIVATE Threads::Threads luci_interpreter_linux_pal_micro) +endmacro() diff --git a/onert-micro/luci-interpreter/pal/mcu/KernelsToBuild.lst b/onert-micro/luci-interpreter/pal/mcu/KernelsToBuild.lst new file mode 100644 index 0000000..9203973 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/KernelsToBuild.lst @@ -0,0 +1,9 @@ +REGISTER_KERNEL(FULLY_CONNECTED, FullyConnected) +REGISTER_KERNEL(CONV_2D, Conv2D) +REGISTER_KERNEL(LOGISTIC, Logistic) +REGISTER_KERNEL(EXPAND_DIMS, ExpandDims) +REGISTER_KERNEL(RESHAPE, Reshape) +REGISTER_KERNEL(MAX_POOL_2D, MaxPool2D) +REGISTER_KERNEL(CONCATENATION, Concatenation) +REGISTER_KERNEL(SOFTMAX, Softmax) +REGISTER_KERNEL(UNIDIRECTIONAL_SEQUENCE_LSTM, UnidirectionalSequenceLSTM) diff --git a/onert-micro/luci-interpreter/pal/mcu/PALApplyActivationToVector.h b/onert-micro/luci-interpreter/pal/mcu/PALApplyActivationToVector.h new file mode 100644 index 0000000..55076c2 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALApplyActivationToVector.h @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_APPLY_ACTIVATION_TO_VECTOR_H +#define LUCI_INTERPRETER_PAL_APPLY_ACTIVATION_TO_VECTOR_H + +#include +#include +#include +#include + +#include "tensorflow/lite/c/builtin_op_data.h" + +namespace luci_interpreter_pal +{ + +// Dynamic (non-fused) activation functor. perhaps it is worth having +// template instantiation? +// TODO(aselle): Make this more efficient by pulling the switch to conv_eval +// using template inlining. +class ActivationFunctor +{ +public: + explicit ActivationFunctor(TfLiteFusedActivation act) : act_(act) {} + + float operator()(float a) const + { + switch (act_) + { + case kTfLiteActNone: + return a; + case kTfLiteActRelu: + return a < 0.f ? 0.f : a; + case kTfLiteActRelu6: + return std::max(0.f, std::min(a, 6.f)); + case kTfLiteActTanh: + return std::tanh(a); + case kTfLiteActSigmoid: + return 1.0f / (1.0f + std::exp(-a)); + default: + assert(false && "Activation functor is not supported"); + } + } + +private: + TfLiteFusedActivation act_; +}; + +inline void ApplyActivationToVector(const float *vector, int v_size, + TfLiteFusedActivation activation, float *result) +{ + auto activation_func = ActivationFunctor(activation); + for (int v = 0; v < v_size; v++) + { + *result++ = (activation_func)(*vector++); + } +} + +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_APPLY_ACTIVATION_TO_VECTOR_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALArgMax.h b/onert-micro/luci-interpreter/pal/mcu/PALArgMax.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALArgMax.h rename to onert-micro/luci-interpreter/pal/mcu/PALArgMax.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALAveragePool2d.h b/onert-micro/luci-interpreter/pal/mcu/PALAveragePool2d.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALAveragePool2d.h rename to onert-micro/luci-interpreter/pal/mcu/PALAveragePool2d.h diff --git a/onert-micro/luci-interpreter/pal/mcu/PALBatchToSpaceND.h b/onert-micro/luci-interpreter/pal/mcu/PALBatchToSpaceND.h new file mode 100644 index 0000000..acfd8b7 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALBatchToSpaceND.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H +#define LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H + +#include + +namespace luci_interpreter_pal +{ +template +static inline void +BatchToSpaceND(const tflite::RuntimeShape &unextended_input1_shape, const T *input1_data, + const tflite::RuntimeShape &unextended_input2_shape, const int32_t *block_shape_data, + const tflite::RuntimeShape &unextended_input3_shape, const int32_t *crops_data, + const tflite::RuntimeShape &unextended_output_shape, T *output_data) +{ + tflite::reference_ops::BatchToSpaceND( + unextended_input1_shape, input1_data, unextended_input2_shape, block_shape_data, + unextended_input3_shape, crops_data, unextended_output_shape, output_data); +} +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_BATCHTOSPACEND_H diff --git a/onert-micro/luci-interpreter/pal/mcu/PALConv2d.h b/onert-micro/luci-interpreter/pal/mcu/PALConv2d.h new file mode 100644 index 0000000..31fd7f2 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALConv2d.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_CONV2D_H +#define LUCI_INTERPRETER_PAL_CONV2D_H + +#include +#include + +namespace luci_interpreter_pal +{ +static inline void Conv(const tflite::ConvParams ¶ms, const tflite::RuntimeShape &input_shape, + const float *input_data, const tflite::RuntimeShape &filter_shape, + const float *filter_data, const tflite::RuntimeShape &bias_shape, + const float *bias_data, const tflite::RuntimeShape &output_shape, + float *output_data, const tflite::RuntimeShape &scratchpad_shape, + float *scratchpad_data) +{ + (void)scratchpad_shape; + (void)scratchpad_data; + tflite::reference_ops::Conv(params, input_shape, input_data, filter_shape, filter_data, + bias_shape, bias_data, output_shape, output_data, + tflite::RuntimeShape(), nullptr); +} + +static inline void Conv(const tflite::ConvParams ¶ms, const tflite::RuntimeShape &input_shape, + const uint8_t *input_data, const tflite::RuntimeShape &filter_shape, + const uint8_t *filter_data, const tflite::RuntimeShape &bias_shape, + const int32_t *bias_data, const tflite::RuntimeShape &output_shape, + uint8_t *output_data, const tflite::RuntimeShape &scratchpad_shape, + uint8_t *scratchpad_data) +{ + (void)scratchpad_shape; + (void)scratchpad_data; + tflite::reference_ops::Conv(params, input_shape, input_data, filter_shape, filter_data, + bias_shape, bias_data, output_shape, output_data, scratchpad_shape, + scratchpad_data, nullptr); +} + +static inline void +ConvPerChannel(const tflite::ConvParams ¶ms, const int32_t *mult, const int32_t *shifts, + const tflite::RuntimeShape &input_shape, const int8_t *input_data, + const tflite::RuntimeShape &filter_shape, const int8_t *filter_data, + const tflite::RuntimeShape &bias_shape, const int32_t *bias_data, + const tflite::RuntimeShape &output_shape, int8_t *output_data, + const tflite::RuntimeShape &scratchpad_shape, int8_t *scratchpad_data) +{ + (void)scratchpad_shape; + (void)scratchpad_data; + tflite::reference_integer_ops::ConvPerChannel(params, mult, shifts, input_shape, input_data, + filter_shape, filter_data, bias_shape, bias_data, + output_shape, output_data); +} + +static inline void SetupScratchpadTensor(luci_interpreter::Tensor *scratchpad, + const luci_interpreter::DataType &input_data_type, + const tflite::ConvParams ¶ms, + const tflite::RuntimeShape &input_shape, + const tflite::RuntimeShape &filter_shape, + const tflite::RuntimeShape &output_shape) +{ + (void)input_data_type; + (void)params; + (void)input_shape; + (void)filter_shape; + (void)output_shape; + (void)scratchpad; +} + +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_CONV2D_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALDepthToSpace.h b/onert-micro/luci-interpreter/pal/mcu/PALDepthToSpace.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALDepthToSpace.h rename to onert-micro/luci-interpreter/pal/mcu/PALDepthToSpace.h diff --git a/compiler/luci-micro/luci-interpreter/pal/linux/PALDepthwiseConv2d.h b/onert-micro/luci-interpreter/pal/mcu/PALDepthwiseConv2d.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/linux/PALDepthwiseConv2d.h rename to onert-micro/luci-interpreter/pal/mcu/PALDepthwiseConv2d.h diff --git a/onert-micro/luci-interpreter/pal/mcu/PALDequantize.h b/onert-micro/luci-interpreter/pal/mcu/PALDequantize.h new file mode 100644 index 0000000..efa6b16 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALDequantize.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_DEQUANTIZE_H +#define LUCI_INTERPRETER_PAL_DEQUANTIZE_H + +#include "tensorflow/lite/kernels/internal/reference/integer_ops/dequantize.h" +#include "PALreference_ops.h" + +namespace luci_interpreter_pal +{ + +template +static inline void Dequantize(tflite::DequantizationParams ¶ms, + const tflite::RuntimeShape &input_shape, const T *input_data, + const tflite::RuntimeShape &output_shape, float *output_data) +{ + tflite::reference_integer_ops::Dequantize(params, input_shape, input_data, output_shape, + output_data); +} + +static inline void Dequantize(tflite::DequantizationParams ¶ms, + const tflite::RuntimeShape &input_shape, const uint8_t *input_data, + const tflite::RuntimeShape &output_shape, float *output_data) +{ + tflite::reference_ops::Dequantize(params, input_shape, input_data, output_shape, output_data); +} + +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_DEQUANTIZE_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALElu.h b/onert-micro/luci-interpreter/pal/mcu/PALElu.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALElu.h rename to onert-micro/luci-interpreter/pal/mcu/PALElu.h diff --git a/onert-micro/luci-interpreter/pal/mcu/PALFill.h b/onert-micro/luci-interpreter/pal/mcu/PALFill.h new file mode 100644 index 0000000..1448b0c --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALFill.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_FILL_H +#define LUCI_INTERPRETER_PAL_FILL_H + +#include "PALreference_ops.h" + +#endif // LUCI_INTERPRETER_PAL_FILL_H diff --git a/compiler/luci-micro/luci-interpreter/pal/mcu/PALFullyConnected.h b/onert-micro/luci-interpreter/pal/mcu/PALFullyConnected.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/mcu/PALFullyConnected.h rename to onert-micro/luci-interpreter/pal/mcu/PALFullyConnected.h diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALL2Normalize.h b/onert-micro/luci-interpreter/pal/mcu/PALL2Normalize.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALL2Normalize.h rename to onert-micro/luci-interpreter/pal/mcu/PALL2Normalize.h diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALL2Pool2D.h b/onert-micro/luci-interpreter/pal/mcu/PALL2Pool2D.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALL2Pool2D.h rename to onert-micro/luci-interpreter/pal/mcu/PALL2Pool2D.h diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALLeakyRelu.h b/onert-micro/luci-interpreter/pal/mcu/PALLeakyRelu.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALLeakyRelu.h rename to onert-micro/luci-interpreter/pal/mcu/PALLeakyRelu.h diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALMul.h b/onert-micro/luci-interpreter/pal/mcu/PALMul.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALMul.h rename to onert-micro/luci-interpreter/pal/mcu/PALMul.h diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALNeg.h b/onert-micro/luci-interpreter/pal/mcu/PALNeg.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALNeg.h rename to onert-micro/luci-interpreter/pal/mcu/PALNeg.h diff --git a/onert-micro/luci-interpreter/pal/mcu/PALQuantize.h b/onert-micro/luci-interpreter/pal/mcu/PALQuantize.h new file mode 100644 index 0000000..effb85d --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALQuantize.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_QUANTIZE_H +#define LUCI_INTERPRETER_PAL_QUANTIZE_H + +#include "PALreference_ops.h" + +namespace luci_interpreter_pal +{ +template +static inline void Quantize(tflite::QuantizationParams ¶ms, + const tflite::RuntimeShape &input_shape, const float *input_data, + const tflite::RuntimeShape &output_shape, T *output_data) +{ + tflite::reference_ops::AffineQuantize(params, input_shape, input_data, output_shape, output_data); +} + +template +static inline void Requantize(const Input *input_data, int32_t size, + int32_t effective_scale_multiplier, int32_t effective_scale_shift, + int32_t input_zero_point, int32_t output_zero_point, + Output *output_data) +{ + tflite::reference_ops::Requantize(input_data, size, effective_scale_multiplier, + effective_scale_shift, input_zero_point, output_zero_point, + output_data); +} +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_QUANTIZE_H diff --git a/onert-micro/luci-interpreter/pal/mcu/PALResizeBilinear.h b/onert-micro/luci-interpreter/pal/mcu/PALResizeBilinear.h new file mode 100644 index 0000000..4db7fa8 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALResizeBilinear.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_RESIZEBILINEAR_H +#define LUCI_INTERPRETER_PAL_RESIZEBILINEAR_H + +#include + +namespace luci_interpreter_pal +{ +template +static inline void +ResizeBilinear(const tflite::ResizeBilinearParams &op_params, + const tflite::RuntimeShape &unextended_input_shape, const T *input_data, + const tflite::RuntimeShape &output_size_shape, const int32_t *output_size_data, + const tflite::RuntimeShape &unextended_output_shape, T *output_data) +{ + tflite::reference_ops::ResizeBilinear(op_params, unextended_input_shape, input_data, + output_size_shape, output_size_data, + unextended_output_shape, output_data); +} +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_RESIZEBILINEAR_H diff --git a/onert-micro/luci-interpreter/pal/mcu/PALResizeNearestNeighbor.h b/onert-micro/luci-interpreter/pal/mcu/PALResizeNearestNeighbor.h new file mode 100644 index 0000000..06c597d --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALResizeNearestNeighbor.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_RESIZENEARESTNEIGHBOR_H +#define LUCI_INTERPRETER_PAL_RESIZENEARESTNEIGHBOR_H + +#include + +namespace luci_interpreter_pal +{ +template +static inline void +ResizeNearestNeighbor(const tflite::ResizeNearestNeighborParams &op_params, + const tflite::RuntimeShape &unextended_input_shape, const T *input_data, + const tflite::RuntimeShape &output_size_shape, + const int32_t *output_size_data, + const tflite::RuntimeShape &unextended_output_shape, T *output_data) +{ + tflite::reference_ops::ResizeNearestNeighbor(op_params, unextended_input_shape, input_data, + output_size_shape, output_size_data, + unextended_output_shape, output_data); +} +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_RESIZENEARESTNEIGHBOR_H diff --git a/onert-micro/luci-interpreter/pal/mcu/PALSVDF.h b/onert-micro/luci-interpreter/pal/mcu/PALSVDF.h new file mode 100644 index 0000000..d39a2f1 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALSVDF.h @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2020 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_SVDF_H +#define LUCI_INTERPRETER_PAL_SVDF_H + +#include + +namespace luci_interpreter_pal +{ +static inline void +IntegerSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, + const int8_t *input_data, const tflite::RuntimeShape &weight_feature_shape, + const int8_t *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, + const int16_t *weight_time_data, const tflite::RuntimeShape &bias_shape, + const int32_t *bias_data, int16_t *activation_state_data, + const tflite::RuntimeShape &output_shape, int8_t *output_data, int32_t *scratchpad_data, + int32_t *output_temp_data, int32_t scale_1_a, int scale_1_b, int32_t scale_2_a, + int scale_2_b, int32_t input_zp, int32_t output_zp) +{ + const int n_rank = params.rank; + const int n_batch = input_shape.Dims(0); + const int n_input = input_shape.Dims(1); + const int n_filter = weight_feature_shape.Dims(0); + const int n_unit = n_filter / n_rank; + const int n_memory = weight_time_shape.Dims(1); + + // Left shift the activation_state. + { + int16_t *new_state_start = activation_state_data; + const int16_t *old_state_start = activation_state_data + 1; + const int16_t *old_state_end = activation_state_data + n_batch * n_filter * n_memory; + while (old_state_start != old_state_end) + { + *new_state_start++ = *old_state_start++; + } + } + + // Note: no need to clear the latest activation, matmul is not accumulative. + + // Feature matmul. + { + const int32_t output_max = std::numeric_limits::max(); + const int32_t output_min = std::numeric_limits::min(); + int16_t *result_in_batch = activation_state_data + (n_memory - 1); + for (int b = 0; b < n_batch; b++) + { + const int8_t *matrix_ptr = weight_feature_data; + for (int r = 0; r < n_filter; r++) + { + int32_t dot_prod = 0; + const int8_t *vector_in_batch = input_data + b * n_input; + for (int c = 0; c < n_input; c++) + { + dot_prod += *matrix_ptr++ * (*vector_in_batch++ - input_zp); + } + dot_prod = tflite::MultiplyByQuantizedMultiplier(dot_prod, scale_1_a, scale_1_b); + dot_prod = std::min(std::max(output_min, dot_prod), output_max); + // This assumes state is symmetrically quantized. Otherwise last bit of + // state should be initialized to its zero point and accumulate the + // dot_prod. + // Equivalent as the following: + // result_in_batch = zero point, which happens to be zero. + // result_in_batch += dot_prod_56. + *result_in_batch = dot_prod; + result_in_batch += n_memory; + } + } + } + + // Time. + { + for (int b = 0; b < n_batch; ++b) + { + int32_t *scratch_ptr_batch = scratchpad_data + b * n_filter; + + // Perform batched vector dot product: + const int16_t *vector1_ptr = weight_time_data; + const int16_t *vector2_ptr = activation_state_data + b * n_memory * n_filter; + + for (int i = 0; i < n_filter; i++) + { + *scratch_ptr_batch = 0; + for (int j = 0; j < n_memory; j++) + { + *scratch_ptr_batch += *vector1_ptr++ * *vector2_ptr++; + } + scratch_ptr_batch++; + } + } + } + + // Reduce, add bias, rescale, activation. + { + // Add bias. + if (bias_data) + { + // Vector batch assign: + for (int i = 0; i < n_batch; ++i) + { + int32_t *output_ptr = output_temp_data + i * n_unit; + const int32_t *bias_ptr = bias_data; + for (int j = 0; j < n_unit; ++j) + { + *output_ptr++ = *bias_ptr++; + } + } + } + else + { + int32_t *output_ptr = output_temp_data; + for (int i = 0; i < n_batch * n_unit; ++i) + { + *output_ptr++ = 0; + } + } + + // Reduce. + for (int b = 0; b < n_batch; ++b) + { + int32_t *output_temp_ptr = output_temp_data + b * n_unit; + int32_t *scratch_ptr_batch = scratchpad_data + b * n_filter; + + // Reduction sum vector + for (int i = 0; i < n_unit; ++i) + { + for (int j = 0; j < n_rank; ++j) + { + output_temp_ptr[i] += *scratch_ptr_batch++; + } + } + } + + // Rescale. + const int32_t output_max = std::numeric_limits::max(); + const int32_t output_min = std::numeric_limits::min(); + for (int i = 0; i < n_batch * n_unit; ++i) + { + int32_t x1 = output_temp_data[i]; + int32_t x2 = tflite::MultiplyByQuantizedMultiplier(x1, scale_2_a, scale_2_b); + int32_t x3 = x2 + output_zp; + int32_t x4 = std::min(std::max(output_min, x3), output_max); + output_data[i] = static_cast(x4); + } + } +} +static inline void +FloatSVDF(const TfLiteSVDFParams ¶ms, const tflite::RuntimeShape &input_shape, + const float *input_data, const tflite::RuntimeShape &weight_feature_shape, + const float *weight_feature_data, const tflite::RuntimeShape &weight_time_shape, + const float *weight_time_data, const tflite::RuntimeShape &bias_shape, + const float *bias_data, float *scratchpad_data, float *activation_state_data, + const tflite::RuntimeShape &output_shape, float *output_data) +{ + const int32_t rank = params.rank; + const int32_t batch_size = input_shape.Dims(0); + const int32_t input_size = input_shape.Dims(1); + const int32_t num_filters = weight_feature_shape.Dims(0); + const int32_t num_units = num_filters / rank; + const int32_t memory_size = weight_time_shape.Dims(1); + + // Left shift the activation_state. + { + float *new_state_start = activation_state_data; + const float *old_state_start = activation_state_data + 1; + const float *old_state_end = activation_state_data + batch_size * num_filters * memory_size; + while (old_state_start != old_state_end) + { + *new_state_start++ = *old_state_start++; + } + } + + // Note: no need to clear the latest activation, matmul is not accumulative. + + // Compute conv1d(inputs, weights_feature). + // The activation_state's rightmost column is used to save current cycle + // activation. This is achieved by starting at state_ptr[memory_size - 1] and + // having the stride equal to memory_size. + + // Perform batched matrix vector multiply operation: + { + const float *matrix = weight_feature_data; + const float *vector = input_data; + float *result = &activation_state_data[memory_size - 1]; + float *result_in_batch = result; + for (int i = 0; i < batch_size; ++i) + { + const float *matrix_ptr = matrix; + for (int j = 0; j < num_filters; ++j) + { + float dot_prod = 0.0f; + const float *vector_in_batch = vector + i * input_size; + for (int k = 0; k < input_size; ++k) + { + dot_prod += *matrix_ptr++ * *vector_in_batch++; + } + *result_in_batch = dot_prod; + result_in_batch += memory_size; + } + } + } + + tflite::reference_ops::ApplyTimeWeightsBiasAndActivation( + batch_size, memory_size, num_filters, num_units, rank, weight_time_data, bias_data, + params.activation, activation_state_data, scratchpad_data, output_data); +} + +static inline void SetupScratchpadTensor( + const luci_interpreter::DataType &input_data_type, + const luci_interpreter::DataType &weight_feature_data_type, + luci_interpreter::Tensor *scratchpad_1, luci_interpreter::Tensor *scratchpad_2, + luci_interpreter::Tensor *scratchpad_3, luci_interpreter::Tensor *scratchpad_4, + luci_interpreter::Tensor *scratchpad_5, luci_interpreter::Tensor *scratchpad_6, + const luci_interpreter::Shape input_shape, const luci_interpreter::Shape weight_time_shape, + const int32_t batch_size, const int32_t num_filters, const int32_t num_units) +{ + + if (input_data_type == luci_interpreter::DataType::FLOAT32 && + (weight_feature_data_type == luci_interpreter::DataType::S8 || + weight_feature_data_type == luci_interpreter::DataType::U8)) + { + (void)input_shape; + (void)weight_time_shape; + (void)scratchpad_3; + (void)scratchpad_4; + (void)scratchpad_5; + (void)scratchpad_6; + + assert(false && "Hybrid type is not currently supported for mcu platform"); + } + + // Resize scratchpad_1 tensor + scratchpad_1->resize({batch_size, num_filters}); + + if (input_data_type == luci_interpreter::DataType::S8) + { + // Resize scratchpad_2 for full_integer op + scratchpad_2->resize({batch_size, num_units}); + } +} + +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_SVDF_H diff --git a/onert-micro/luci-interpreter/pal/mcu/PALSoftmax.h b/onert-micro/luci-interpreter/pal/mcu/PALSoftmax.h new file mode 100644 index 0000000..1c4c64d --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALSoftmax.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_SOFTMAX_H +#define LUCI_INTERPRETER_PAL_SOFTMAX_H + +#include + +namespace luci_interpreter_pal +{ +static inline void PopulateSoftmaxLookupTable(tflite::SoftmaxParams *data, float input_scale, + float beta) +{ + // Do nothing for mcu + (void)data; + (void)input_scale; + (void)beta; +} + +static inline void InitializeParams(tflite::SoftmaxParams *params, float input_scale, float beta) +{ + // TODO Impl it + assert(false && "Softmax NYI"); + (void)params; + (void)input_scale; + (void)beta; +} + +template +static inline void Softmax(const tflite::SoftmaxParams ¶ms, + const tflite::RuntimeShape &input_shape, const T *input_data, + const tflite::RuntimeShape &output_shape, T *output_data) +{ + // TODO Impl it + // MARK: At this moment this operation doesn't support on mcu + assert(false && "Softmax NYI"); + (void)params; + (void)input_shape; + (void)input_data; + (void)output_shape; + (void)output_data; +} +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_SOFTMAX_H diff --git a/onert-micro/luci-interpreter/pal/mcu/PALSpaceToBatchND.h b/onert-micro/luci-interpreter/pal/mcu/PALSpaceToBatchND.h new file mode 100644 index 0000000..9f7c54e --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALSpaceToBatchND.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_SPACETOBATCHND_H +#define LUCI_INTERPRETER_PAL_SPACETOBATCHND_H + +#include + +namespace luci_interpreter_pal +{ +template +static inline void +SpaceToBatchND(const tflite::SpaceToBatchParams ¶ms, + const tflite::RuntimeShape &unextended_input1_shape, const T *input1_data, + const tflite::RuntimeShape &unextended_input2_shape, const int32_t *block_shape_data, + const tflite::RuntimeShape &unextended_input3_shape, const int32_t *paddings_data, + const tflite::RuntimeShape &unextended_output_shape, T *output_data) +{ + tflite::reference_ops::SpaceToBatchND( + params, unextended_input1_shape, input1_data, unextended_input2_shape, block_shape_data, + unextended_input3_shape, paddings_data, unextended_output_shape, output_data); +} +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_SPACETOBATCHND_H diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSpaceToDepth.h b/onert-micro/luci-interpreter/pal/mcu/PALSpaceToDepth.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSpaceToDepth.h rename to onert-micro/luci-interpreter/pal/mcu/PALSpaceToDepth.h diff --git a/compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSub.h b/onert-micro/luci-interpreter/pal/mcu/PALSub.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/pal/cmsisnn/PALSub.h rename to onert-micro/luci-interpreter/pal/mcu/PALSub.h diff --git a/onert-micro/luci-interpreter/pal/mcu/PALUnidirectionalSequenceLSTM.h b/onert-micro/luci-interpreter/pal/mcu/PALUnidirectionalSequenceLSTM.h new file mode 100644 index 0000000..287f303 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALUnidirectionalSequenceLSTM.h @@ -0,0 +1,678 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_UNIDIRECTIONAL_SEQUENCE_LSTM_H +#define LUCI_INTERPRETER_PAL_UNIDIRECTIONAL_SEQUENCE_LSTM_H + +#include "kernels/UnidirectionalSequenceLSTM.h" +#include "tensorflow/lite/kernels/internal/reference/fully_connected.h" +#include "tensorflow/lite/kernels/internal/reference/integer_ops/logistic.h" +#include "tensorflow/lite/kernels/internal/reference/logistic.h" +#include "tensorflow/lite/kernels/internal/reference/integer_ops/tanh.h" +#include "tensorflow/lite/kernels/internal/reference/tanh.h" + +namespace luci_interpreter_pal +{ +namespace lstm_internal +{ +namespace +{ +// Possible fused activation functions. +typedef enum +{ + kTfLiteActNone = 0, + kTfLiteActRelu, + kTfLiteActReluN1To1, // min(max(-1, x), 1) + kTfLiteActRelu6, // min(max(0, x), 6) + kTfLiteActTanh, + kTfLiteActSignBit, + kTfLiteActSigmoid, +} TfLiteFusedActivation; + +} // namespace + +template +inline T activationFunctionWithMinMax(T x, T output_activation_min, T output_activation_max) +{ + using std::max; + using std::min; + return min(max(x, output_activation_min), output_activation_max); +} + +template +inline void mul(const luci_interpreter::lstm::ArithmeticParams *params, + const tflite::RuntimeShape &input1_shape, const T *input1_data, + const tflite::RuntimeShape &input2_shape, const T *input2_data, + const tflite::RuntimeShape &output_shape, T *output_data) +{ + T output_activation_min = params->quantized_activation_min; + T output_activation_max = params->quantized_activation_max; + + const int flat_size = input1_shape.FlatSize(); + for (int i = 0; i < flat_size; ++i) + { + output_data[i] = activationFunctionWithMinMax(input1_data[i] * input2_data[i], + output_activation_min, output_activation_max); + } +} + +#ifndef DIS_QUANT +inline int32_t multiplyByQuantizedMultiplier(int32_t x, int32_t quantized_multiplier, int shift) +{ + using gemmlowp::RoundingDivideByPOT; + using gemmlowp::SaturatingRoundingDoublingHighMul; + int left_shift = shift > 0 ? shift : 0; + int right_shift = shift > 0 ? 0 : -shift; + return RoundingDivideByPOT( + SaturatingRoundingDoublingHighMul(x * (1 << left_shift), quantized_multiplier), right_shift); +} + +template +void fullyConnectedInteger(const tflite::FullyConnectedParams ¶ms, + const tflite::RuntimeShape &input_shape, const InputType *input_data, + const tflite::RuntimeShape &filter_shape, const WeightType *filter_data, + const tflite::RuntimeShape &bias_shape, const BiasType *bias_data, + const tflite::RuntimeShape &output_shape, OutputType *output_data) +{ + const int32_t input_offset = params.input_offset; + const int32_t filter_offset = params.weights_offset; + const int32_t output_offset = params.output_offset; + const int32_t output_multiplier = params.output_multiplier; + const int output_shift = params.output_shift; + const int32_t output_activation_min = params.quantized_activation_min; + const int32_t output_activation_max = params.quantized_activation_max; + TFLITE_DCHECK_GE(filter_shape.DimensionsCount(), 2); + TFLITE_DCHECK_GE(output_shape.DimensionsCount(), 1); + + TFLITE_DCHECK_LE(output_activation_min, output_activation_max); + const int filter_dim_count = filter_shape.DimensionsCount(); + const int output_dim_count = output_shape.DimensionsCount(); + const int batches = FlatSizeSkipDim(output_shape, output_dim_count - 1); + const int output_depth = output_shape.Dims(output_dim_count - 1); + TFLITE_DCHECK_LE(output_depth, filter_shape.Dims(filter_dim_count - 2)); + const int accum_depth = filter_shape.Dims(filter_dim_count - 1); + for (int b = 0; b < batches; ++b) + { + for (int out_c = 0; out_c < output_depth; ++out_c) + { + BiasType acc = 0; + for (int d = 0; d < accum_depth; ++d) + { + int32_t input_val = input_data[b * accum_depth + d]; + int32_t filter_val = filter_data[out_c * accum_depth + d]; + acc += (filter_val + filter_offset) * (input_val + input_offset); + } + if (bias_data) + { + acc += bias_data[out_c]; + } + int32_t acc_scaled = multiplyByQuantizedMultiplier(acc, output_multiplier, output_shift); + acc_scaled += output_offset; + acc_scaled = std::max(acc_scaled, output_activation_min); + acc_scaled = std::min(acc_scaled, output_activation_max); + output_data[out_c + output_depth * b] = static_cast(acc_scaled); + } + } +} + +void fullyConnected(const tflite::FullyConnectedParams ¶ms, + const tflite::RuntimeShape &input_shape, const int8_t *input_data, + const tflite::RuntimeShape &filter_shape, const int8_t *filter_data, + const tflite::RuntimeShape &bias_shape, const int32_t *bias_data, + const tflite::RuntimeShape &output_shape, int16_t *output_data) +{ + return fullyConnectedInteger(params, input_shape, input_data, filter_shape, filter_data, + bias_shape, bias_data, output_shape, output_data); +} + +void fullyConnected(const tflite::FullyConnectedParams ¶ms, + const tflite::RuntimeShape &input_shape, const int16_t *input_data, + const tflite::RuntimeShape &filter_shape, const int8_t *filter_data, + const tflite::RuntimeShape &bias_shape, const int64_t *bias_data, + const tflite::RuntimeShape &output_shape, int16_t *output_data) +{ + return fullyConnectedInteger(params, input_shape, input_data, filter_shape, filter_data, + bias_shape, bias_data, output_shape, output_data); +} + +template +void mulElementwise(int size, const luci_interpreter::lstm::ArithmeticParams *params, + const InputType *input1_data, const InputType *input2_data, + OutputType *output_data) +{ + for (int i = 0; i < size; ++i) + { + const int32_t input1_val = params->input1_offset + input1_data[i]; + const int32_t input2_val = params->input2_offset + input2_data[i]; + const int32_t unclamped_result = + params->output_offset + multiplyByQuantizedMultiplier(input1_val * input2_val, + params->output_multiplier, + params->output_shift); + const int32_t clamped_output = + std::min(params->quantized_activation_max, + std::max(params->quantized_activation_min, unclamped_result)); + output_data[i] = static_cast(clamped_output); + } +} + +// Input and output have the same shape in LSTM +void mul(const tflite::RuntimeShape &shape, const luci_interpreter::lstm::ArithmeticParams *params, + const int16_t *input1_data, const int16_t *input2_data, int8_t *output_data) +{ + return mulElementwise(shape.FlatSize(), params, input1_data, input2_data, + output_data); +} + +// Input and output have the same shape in LSTM +void mul(const tflite::RuntimeShape &shape, const luci_interpreter::lstm::ArithmeticParams *params, + const int16_t *input1_data, const int16_t *input2_data, int16_t *output_data) +{ + return mulElementwise(shape.FlatSize(), params, input1_data, input2_data, output_data); +} + +void addElementWise(const int16_t *input_1, const int16_t *input_2, int n_batch, int n_input, + int16_t *output) +{ + for (int batch = 0; batch < n_batch; ++batch) + { + for (int i = 0; i < n_input; ++i) + { + const int index = batch * n_input + i; + int32_t sum = input_1[index] + input_2[index]; + const int32_t sum_clamped = + std::min(static_cast(std::numeric_limits::max()), + std::max(static_cast(std::numeric_limits::min()), sum)); + output[index] = static_cast(sum_clamped); + } + } +} + +void tanh(int32_t cell_state_scale_power, const tflite::RuntimeShape &input_data_shape, + int16_t *input_data, const tflite::RuntimeShape &output_data_shape, int16_t *output_data) +{ + int32_t tanh_input_left_shift = (15 + cell_state_scale_power) - 3; + int32_t input_multiplier = 0; + if (tanh_input_left_shift < 0) /* handling negative shift value */ + { + tanh_input_left_shift = -tanh_input_left_shift; + input_multiplier = 3; + } + tflite::reference_integer_ops::Tanh(input_multiplier, tanh_input_left_shift, input_data_shape, + input_data, output_data_shape, output_data); +} + +void sigmoid(const tflite::RuntimeShape &data_shape, int16_t *data) +{ + tflite::reference_integer_ops::Logistic(0 /*data->input_multiplier*/, + 0 /*data->input_left_shift */, + data_shape.FlatSize() /*NumElements(input->dims)*/, + data /* tflite::micro::GetTensorData(input) */, + data /*tflite::micro::GetTensorData(output) */); +} + +void clipping(const int v_size, const luci_interpreter::lstm::CellStateInfo *cell_state_info, + int16_t *vector) +{ + for (int i = 0; i < v_size; i++) + { + vector[i] = std::max(std::min(cell_state_info->quantized_cell_clip, vector[i]), + static_cast(-cell_state_info->quantized_cell_clip)); + } +} +#endif // DIS_QUANT + +#ifndef DIS_FLOAT +void fullyConnected(const tflite::FullyConnectedParams ¶ms, + const tflite::RuntimeShape &input_shape, const float *input_data, + const tflite::RuntimeShape &filter_shape, const float *filter_data, + const tflite::RuntimeShape &bias_shape, const float *bias_data, + const tflite::RuntimeShape &output_shape, float *output_data) +{ + return tflite::reference_ops::FullyConnected(params, input_shape, input_data, filter_shape, + filter_data, bias_shape, bias_data, output_shape, + output_data); +} + +// Input and output have the same shape in LSTM +void mul(const tflite::RuntimeShape &shape, const luci_interpreter::lstm::ArithmeticParams *params, + const float *input1_data, const float *input2_data, float *output_data) +{ + return mul(params, shape, input1_data, shape, input2_data, shape, output_data); +} + +void addElementWise(const float *input_1, const float *input_2, int n_batch, int n_input, + float *output) +{ + for (int batch = 0; batch < n_batch; ++batch) + { + for (int i = 0; i < n_input; ++i) + { + const int index = batch * n_input + i; + output[index] = input_1[index] + input_2[index]; + } + } +} + +void tanh(int32_t cell_state_scale_power, const tflite::RuntimeShape &input_data_shape, + float *input_data, const tflite::RuntimeShape &output_data_shape, float *output_data) +{ + tflite::reference_ops::Tanh(input_data_shape, input_data, output_data_shape, output_data); +} + +void sigmoid(const tflite::RuntimeShape &data_shape, float *data) +{ + tflite::reference_ops::Logistic(data_shape, data, data_shape, data); +} + +void clipping(const int v_size, const luci_interpreter::lstm::CellStateInfo *cell_state_info, + float *vector) +{ + for (int i = 0; i < v_size; i++) + { + vector[i] = + std::max(std::min(cell_state_info->cell_clip, vector[i]), -cell_state_info->cell_clip); + } +} +#endif // DIS_FLOAT + +// Size information about the LSTM kernel, which is deduced from tensors stored +// in the flat buffer file. +struct LstmSizeInfo +{ + bool time_major; + int32_t batch_size; + int32_t time_steps; + int32_t input_dimension; + int32_t state_dimension; +}; + +class LstmStepManager +{ +public: + LstmStepManager() = delete; + // Does not take any ownership, and all pointers must refer to valid objects + // that outlive the one constructed. + explicit LstmStepManager(const LstmSizeInfo &size_info) : size_info_(size_info) {} + + void updateTime() + { + current_time_ += 1; + // default as one batch per inference + int input_step = size_info_.input_dimension; + int output_step = size_info_.state_dimension; + // time major: batch inference + if (size_info_.time_major) + { + input_step = input_step * size_info_.batch_size; + output_step = output_step * size_info_.batch_size; + } + + input_offset_ += input_step; + output_offset_ += output_step; + } + + void updateBatch() + { + current_batch_ += 1; + TFLITE_DCHECK_LE(current_batch_, size_info_.batch_size); + // batch inference for time major: no action needed + if (size_info_.time_major) + { + return; + } + // otherwise: singe batch inference, go to the next batch + hidden_state_offset_ += size_info_.state_dimension; + cell_state_offset_ += size_info_.state_dimension; + } + + void resetTime() { current_time_ = 0; } + + tflite::RuntimeShape inputShape() const + { + int batch_size = 1; + if (size_info_.time_major) + { + batch_size = size_info_.batch_size; + } + const int dims[2] = {batch_size, size_info_.input_dimension}; + const int32_t *dims_data = reinterpret_cast(dims); + return tflite::RuntimeShape(2, dims_data); + } + + tflite::RuntimeShape stateShape() const + { + int batch_size = 1; + if (size_info_.time_major) + { + batch_size = size_info_.batch_size; + } + const int dims[2] = {batch_size, size_info_.state_dimension}; + const int32_t *dims_data = reinterpret_cast(dims); + return tflite::RuntimeShape(2, dims_data); + } + + int inputOffset() const { return input_offset_; } + + int outputOffset() const { return output_offset_; } + + int hiddenStateOffset() const { return hidden_state_offset_; } + + int cellStateOffset() const { return cell_state_offset_; } + +private: + int32_t current_time_ = 0; + int32_t current_batch_ = 0; + int32_t input_offset_ = 0; + int32_t output_offset_ = 0; + int32_t hidden_state_offset_ = 0; + int32_t cell_state_offset_ = 0; + + const LstmSizeInfo &size_info_; +}; + +// Calculates a single LSTM gate. +// Implements the following formula: +// gate = activate(FC(input) + FC(recurrent)) +// Activation is sigmoid except for the "cell" gate (configurable, usually tanh) +template +void calculateLstmGate(const LstmStepManager *step_info, + const luci_interpreter::lstm::GateParameters *gate_params, + // Input FC + ActivationType *input_data, const circle::Tensor *input_weight, + const circle::Tensor *input_bias, + // Recurrent FC + ActivationType *recurrent_data, const circle::Tensor *recurrent_weight, + const circle::Tensor *recurrent_bias, + // Output + CellType *gate_output, + // Scratch arrays + CellType *fc_output_buffer, const TfLiteFusedActivation activation, + luci_interpreter::BaseRuntimeGraph *runtime_graph) +{ + // Input FC + const auto gate_output_shape = step_info->stateShape(); + { + tflite::FullyConnectedParams op_params{}; + op_params.input_offset = gate_params->input_fc_params.input_offset; + op_params.weights_offset = gate_params->input_fc_params.weights_offset; + op_params.output_offset = gate_params->input_fc_params.output_offset; + op_params.output_multiplier = gate_params->input_fc_params.output_multiplier; + op_params.output_shift = gate_params->input_fc_params.output_shift; + op_params.quantized_activation_min = gate_params->input_fc_params.quantized_activation_min; + op_params.quantized_activation_max = gate_params->input_fc_params.quantized_activation_max; + op_params.float_activation_max = gate_params->input_fc_params.float_activation_max; + op_params.float_activation_min = gate_params->input_fc_params.float_activation_min; + + fullyConnected(op_params, step_info->inputShape(), input_data + step_info->inputOffset(), + luci_interpreter::kernels::getTensorShape(input_weight), + luci_interpreter::kernels::getTensorData( + runtime_graph->getConstDataByTensor(input_weight)), + luci_interpreter::kernels::getTensorShape(input_bias), + luci_interpreter::kernels::getTensorData( + runtime_graph->getConstDataByTensor(input_bias)), + gate_output_shape, gate_output); + } + + // Recurrent FC + { + tflite::FullyConnectedParams op_params{}; + op_params.input_offset = gate_params->recurrent_fc_params.input_offset; + op_params.weights_offset = gate_params->recurrent_fc_params.weights_offset; + op_params.output_offset = gate_params->recurrent_fc_params.output_offset; + op_params.output_multiplier = gate_params->recurrent_fc_params.output_multiplier; + op_params.output_shift = gate_params->recurrent_fc_params.output_shift; + op_params.quantized_activation_min = gate_params->recurrent_fc_params.quantized_activation_min; + op_params.quantized_activation_max = gate_params->recurrent_fc_params.quantized_activation_max; + op_params.float_activation_max = gate_params->recurrent_fc_params.float_activation_max; + op_params.float_activation_min = gate_params->recurrent_fc_params.float_activation_min; + + fullyConnected(op_params, step_info->stateShape(), + recurrent_data + step_info->hiddenStateOffset(), + luci_interpreter::kernels::getTensorShape(recurrent_weight), + luci_interpreter::kernels::getTensorData( + runtime_graph->getConstDataByTensor(recurrent_weight)), + luci_interpreter::kernels::getTensorShape(recurrent_bias), + luci_interpreter::kernels::getTensorData( + runtime_graph->getConstDataByTensor(recurrent_bias)), + gate_output_shape, fc_output_buffer); + + addElementWise(gate_output, fc_output_buffer, /*n_batch=*/gate_output_shape.DimsData()[0], + /*n_state=*/gate_output_shape.DimsData()[1], gate_output); + + switch (activation) + { + case TfLiteFusedActivation::kTfLiteActSigmoid: + sigmoid(gate_output_shape, gate_output); + break; + case TfLiteFusedActivation::kTfLiteActTanh: + { + // Set the scale power to -12 to avoid shift + tanh(/*cell_state_scale_power=*/-12, gate_output_shape, gate_output, gate_output_shape, + gate_output); + } + break; + default: + // Only Sigmoid or Tanh is used. + assert(false && "Only Sigmoid or Tanh is used"); + } + } +} + +// Update the hidden state of the LSTM kernel using the following formula: +// updated_hidden_state = Tanh(updated_cell_state) * output_gate_output, * means +// element wise multiplication +template +void updateLstmHidden(const LstmStepManager *step_info, CellType *cell_state_data_base, + ActivationType *hidden_state_data, const CellType *output_gate_output, + const luci_interpreter::lstm::ArithmeticParams *mul_params, + int32_t cell_state_scale_power, CellType *buffer) +{ + auto cell_state_shape = step_info->stateShape(); + CellType *cell_state_data = cell_state_data_base + step_info->cellStateOffset(); + // Tanh(cell_state) + tanh(cell_state_scale_power, cell_state_shape, cell_state_data, cell_state_shape, buffer); + // Update the hidden state + mul(cell_state_shape, mul_params, buffer, output_gate_output, + hidden_state_data + step_info->hiddenStateOffset()); +} + +// Update the cell state using the output from the forget gate, input gate, and +// cell gate Formula: updated_cell_state = forget_gate_output*cell_state + +// input_gate_output * cell_gate_output, where * denotes element wise +// multiplication +template +void updateLstmCell(const LstmStepManager *step_info, CellType *cell_state_data, + // Gate outputs + CellType *forget_gate_output, const CellType *input_gate_output, + const CellType *cell_gate_output, + // Mul parameters + const luci_interpreter::lstm::ArithmeticParams &forget_cell_mul_params, + const luci_interpreter::lstm::ArithmeticParams &input_mul_params, + const luci_interpreter::lstm::CellStateInfo *cell_state_info, CellType *buffer) +{ + auto cell_state_shape = step_info->stateShape(); + // Forget Gate x Cell State + mul(cell_state_shape, &forget_cell_mul_params, forget_gate_output, + cell_state_data + step_info->cellStateOffset(), + cell_state_data + step_info->cellStateOffset()); + // Input Gate x Cell Gate + mul(cell_state_shape, &input_mul_params, input_gate_output, cell_gate_output, buffer); + + // Update the cell state + addElementWise(cell_state_data + step_info->cellStateOffset(), buffer, + /*n_batch=*/cell_state_shape.DimsData()[0], + /*n_state=*/cell_state_shape.DimsData()[1], + cell_state_data + step_info->cellStateOffset()); + + if (cell_state_info->cell_clip > 0) + { + clipping(cell_state_shape.FlatSize(), cell_state_info, + cell_state_data + step_info->cellStateOffset()); + } +} + +template +void lstmStep(luci_interpreter::lstm::LSTMStruct *lstm_struct, + luci_interpreter::lstm::LSTMParameters *lstm_params, LstmStepManager *step_info, + luci_interpreter::lstm::CellStateInfo *cell_state_info, + ActivationType *output_state_data, CellType *cell_state_data, CellType *scratch0, + CellType *scratch1, CellType *scratch2, CellType *scratch3, + luci_interpreter::BaseRuntimeGraph *runtime_graph) +{ + /*Step1: Calculate gate outputs to prepare cell state update*/ + CellType *gate_internal_buffer = scratch3; + CellType *forget_gate_output = scratch0; + + auto input_data = luci_interpreter::kernels::getTensorData( + runtime_graph->getDataByTensor(lstm_struct->input())); + + calculateLstmGate( + step_info, &lstm_params->forget_gate_parameters, + // Input FC + input_data, lstm_struct->input_to_forget_weights(), lstm_struct->forget_gate_bias(), + // Recurrent FC + output_state_data, lstm_struct->recurrent_to_forget_weights(), nullptr, + // Output + forget_gate_output, gate_internal_buffer, TfLiteFusedActivation::kTfLiteActSigmoid, + runtime_graph); + + // Input Gate calculation; + CellType *input_gate_output = scratch1; + calculateLstmGate( + step_info, &lstm_params->input_gate_parameters, + // Input FC + input_data, lstm_struct->input_to_input_weights(), lstm_struct->input_gate_bias(), + // Recurrent FC + output_state_data, lstm_struct->recurrent_to_input_weights(), + /*recurrent_bias*/ nullptr, + // Output + input_gate_output, + // Scratch arrays + gate_internal_buffer, TfLiteFusedActivation::kTfLiteActSigmoid, runtime_graph); + + // Cell Gate calculation + CellType *cell_gate_output = scratch2; + calculateLstmGate( + step_info, &lstm_params->cell_gate_parameters, + // Input FC + input_data, lstm_struct->input_to_cell_weights(), lstm_struct->cell_gate_bias(), + // Recurrent FC + output_state_data, lstm_struct->recurrent_to_cell_weights(), + /*recurrent_bias*/ nullptr, + // Output + cell_gate_output, + // Scratch arrays + gate_internal_buffer, TfLiteFusedActivation::kTfLiteActTanh, runtime_graph); + + /*Step2: update the cell state */ + { + // const InterGateParameters& inter_gate_params = op_data.inter_gate_parameters; + CellType *updated_input_buffer = scratch1; // reuse buffer + + updateLstmCell( + step_info, cell_state_data, forget_gate_output, input_gate_output, cell_gate_output, + lstm_params->inter_gate_parameters.forget_cell_mul_params, + lstm_params->inter_gate_parameters.input_mul_params, cell_state_info, updated_input_buffer); + } + + { + /*Step3: update the hidden state */ + CellType *output_gate_output = scratch1; // reuse buffer + calculateLstmGate( + step_info, &lstm_params->output_gate_parameters, + // Input FC + input_data, lstm_struct->input_to_output_weights(), lstm_struct->output_gate_bias(), + // Recurrent FC + output_state_data, lstm_struct->recurrent_to_output_weights(), nullptr, + // Output + output_gate_output, + // Scratch arrays + gate_internal_buffer, TfLiteFusedActivation::kTfLiteActSigmoid, runtime_graph); + CellType *tanh_activated_cell_buffer = scratch0; // reuse buffer + updateLstmHidden( + step_info, cell_state_data, output_state_data, output_gate_output, + &lstm_params->inter_gate_parameters.output_mul_params, + cell_state_info->cell_state_scale_power, tanh_activated_cell_buffer); + + ActivationType *output_ptr = luci_interpreter::kernels::getTensorData( + runtime_graph->getDataByTensor(lstm_struct->output())); + std::memcpy(output_ptr + step_info->outputOffset(), + output_state_data + step_info->hiddenStateOffset(), + step_info->stateShape().FlatSize() * sizeof(ActivationType)); + } +} + +} // namespace lstm_internal + +// Evaluate the LSTM kernel with (potential) multi-steps and multi-batch input +template +void evalLSTM(luci_interpreter::lstm::LSTMStruct *lstm_struct, + luci_interpreter::lstm::LSTMParameters *lstm_params, + luci_interpreter::lstm::CellStateInfo *cell_state_info, + ActivationType *output_state_data, CellType *cell_state_data, CellType *scratch0, + CellType *scratch1, CellType *scratch2, CellType *scratch3, + luci_interpreter::BaseRuntimeGraph *runtime_graph) +{ + lstm_internal::LstmSizeInfo size_info; + + size_info.time_major = lstm_struct->options->time_major(); + size_info.batch_size = size_info.time_major + ? luci_interpreter::Tensor::dim(lstm_struct->input(), 1) + : luci_interpreter::Tensor::dim(lstm_struct->input(), 0); + size_info.time_steps = size_info.time_major + ? luci_interpreter::Tensor::dim(lstm_struct->input(), 0) + : luci_interpreter::Tensor::dim(lstm_struct->input(), 1); + size_info.input_dimension = luci_interpreter::Tensor::dim(lstm_struct->input(), 2); + size_info.state_dimension = luci_interpreter::Tensor::dim(lstm_struct->output_state(), 1); + + lstm_internal::LstmStepManager step_info(size_info); + + // time is the first dimention, enable batch computation + if (size_info.time_major) + { + for (int t = 0; t < size_info.time_steps; t++) + { + lstm_internal::lstmStep( + lstm_struct, lstm_params, &step_info, cell_state_info, output_state_data, cell_state_data, + scratch0, scratch1, scratch2, scratch3, runtime_graph); + // prepare for the next time step + step_info.updateTime(); + } + } + else + { + // batch first, unable to size the input data. single batch inference + for (int b = 0; b < size_info.batch_size; b++) + { + for (int t = 0; t < size_info.time_steps; t++) + { + lstm_internal::lstmStep( + lstm_struct, lstm_params, &step_info, cell_state_info, output_state_data, cell_state_data, + scratch0, scratch1, scratch2, scratch3, runtime_graph); + // prepare for the next time step + step_info.updateTime(); + } + // prepare for the next batch + step_info.updateBatch(); + step_info.resetTime(); + } + } +} + +} // namespace luci_interpreter_pal + +#endif // LUCI_INTERPRETER_PAL_UNIDIRECTIONAL_SEQUENCE_LSTM_H diff --git a/onert-micro/luci-interpreter/pal/mcu/PALreference_ops.h b/onert-micro/luci-interpreter/pal/mcu/PALreference_ops.h new file mode 100644 index 0000000..62c7209 --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/PALreference_ops.h @@ -0,0 +1,1556 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_PAL_REFERENCE_OPS_H +#define LUCI_INTERPRETER_PAL_REFERENCE_OPS_H + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "third_party/eigen3/Eigen/Core" +#include "fixedpoint/fixedpoint.h" +#include "ruy/profiler/instrumentation.h" // from @ruy +#include "tensorflow/lite/c/common.h" +#include "tensorflow/lite/kernels/internal/common.h" +#include "tensorflow/lite/kernels/internal/quantization_util.h" +#include "tensorflow/lite/kernels/internal/reference/add.h" +#include "tensorflow/lite/kernels/internal/reference/add_n.h" +#include "tensorflow/lite/kernels/internal/reference/arg_min_max.h" +#include "tensorflow/lite/kernels/internal/reference/batch_matmul.h" +#include "tensorflow/lite/kernels/internal/reference/batch_to_space_nd.h" +#include "tensorflow/lite/kernels/internal/reference/binary_function.h" +#include "tensorflow/lite/kernels/internal/reference/cast.h" +#include "tensorflow/lite/kernels/internal/reference/ceil.h" +#include "tensorflow/lite/kernels/internal/reference/comparisons.h" +#include "tensorflow/lite/kernels/internal/reference/concatenation.h" +#include "tensorflow/lite/kernels/internal/reference/conv.h" +#include "tensorflow/lite/kernels/internal/reference/depth_to_space.h" +#include "tensorflow/lite/kernels/internal/reference/dequantize.h" +#include "tensorflow/lite/kernels/internal/reference/div.h" +#include "tensorflow/lite/kernels/internal/reference/elu.h" +#include "tensorflow/lite/kernels/internal/reference/exp.h" +#include "tensorflow/lite/kernels/internal/reference/fill.h" +#include "tensorflow/lite/kernels/internal/reference/floor.h" +#include "tensorflow/lite/kernels/internal/reference/floor_div.h" +#include "tensorflow/lite/kernels/internal/reference/floor_mod.h" +#include "tensorflow/lite/kernels/internal/reference/fully_connected.h" +#include "tensorflow/lite/kernels/internal/reference/gather.h" +#include "tensorflow/lite/kernels/internal/reference/hard_swish.h" +#include "tensorflow/lite/kernels/internal/reference/l2normalization.h" +#include "tensorflow/lite/kernels/internal/reference/leaky_relu.h" +#include "tensorflow/lite/kernels/internal/reference/log_softmax.h" +#include "tensorflow/lite/kernels/internal/reference/logistic.h" +#include "tensorflow/lite/kernels/internal/reference/maximum_minimum.h" +#include "tensorflow/lite/kernels/internal/reference/mul.h" +#include "tensorflow/lite/kernels/internal/reference/neg.h" +#include "tensorflow/lite/kernels/internal/reference/pad.h" +#include "tensorflow/lite/kernels/internal/reference/pooling.h" +#include "tensorflow/lite/kernels/internal/reference/prelu.h" +#include "tensorflow/lite/kernels/internal/reference/process_broadcast_shapes.h" +#include "tensorflow/lite/kernels/internal/reference/quantize.h" +#include "tensorflow/lite/kernels/internal/reference/reduce.h" +#include "tensorflow/lite/kernels/internal/reference/requantize.h" +#include "tensorflow/lite/kernels/internal/reference/resize_bilinear.h" +#include "tensorflow/lite/kernels/internal/reference/resize_nearest_neighbor.h" +#include "tensorflow/lite/kernels/internal/reference/round.h" +#include "tensorflow/lite/kernels/internal/reference/softmax.h" +#include "tensorflow/lite/kernels/internal/reference/space_to_batch_nd.h" +#include "tensorflow/lite/kernels/internal/reference/space_to_depth.h" +#include "tensorflow/lite/kernels/internal/reference/strided_slice.h" +#include "tensorflow/lite/kernels/internal/reference/string_comparisons.h" +#include "tensorflow/lite/kernels/internal/reference/sub.h" +#include "tensorflow/lite/kernels/internal/reference/tanh.h" +#include "tensorflow/lite/kernels/internal/reference/transpose.h" +#include "tensorflow/lite/kernels/internal/reference/transpose_conv.h" +#include "tensorflow/lite/kernels/internal/strided_slice_logic.h" +#include "tensorflow/lite/kernels/internal/tensor.h" +#include "tensorflow/lite/kernels/internal/types.h" +namespace tflite +{ + +namespace reference_ops +{ + +template +inline void Relu(const RuntimeShape &input_shape, const T *input_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int flat_size = MatchingFlatSize(input_shape, output_shape); + for (int i = 0; i < flat_size; ++i) + { + const T val = input_data[i]; + const T lower = 0; + const T clamped = val < lower ? lower : val; + output_data[i] = clamped; + } +} + +template +inline void Relu1(const RuntimeShape &input_shape, const T *input_data, + const RuntimeShape &output_shape, T *output_data) +{ + ruy::profiler::ScopeLabel label("Relu1 (not fused)"); + const int flat_size = MatchingFlatSize(input_shape, output_shape); + for (int i = 0; i < flat_size; ++i) + { + const T val = input_data[i]; + const T upper = 1; + const T lower = -1; + const T clamped = val > upper ? upper : val < lower ? lower : val; + output_data[i] = clamped; + } +} + +inline void Relu6(const RuntimeShape &input_shape, const float *input_data, + const RuntimeShape &output_shape, float *output_data) +{ + ruy::profiler::ScopeLabel label("Relu6 (not fused)"); + const int flat_size = MatchingFlatSize(input_shape, output_shape); + for (int i = 0; i < flat_size; ++i) + { + const float val = input_data[i]; + const float upper = 6; + const float lower = 0; + const float clamped = val > upper ? upper : val < lower ? lower : val; + output_data[i] = clamped; + } +} + +template +inline void ReluX(const tflite::ReluParams ¶ms, const RuntimeShape &input_shape, + const T *input_data, const RuntimeShape &output_shape, T *output_data) +{ + ruy::profiler::ScopeLabel label("Quantized ReluX (not fused)"); + const int flat_size = MatchingFlatSize(input_shape, output_shape); + for (int i = 0; i < flat_size; ++i) + { + const int32 val = static_cast(input_data[i]); + int32 clamped = params.output_offset + MultiplyByQuantizedMultiplier(val - params.input_offset, + params.output_multiplier, + params.output_shift); + clamped = std::max(params.quantized_activation_min, clamped); + clamped = std::min(params.quantized_activation_max, clamped); + output_data[i] = static_cast(clamped); + } +} + +template +inline void ReluX(const tflite::ActivationParams ¶ms, const RuntimeShape &input_shape, + const T *input_data, const RuntimeShape &output_shape, T *output_data) +{ + ruy::profiler::ScopeLabel label("Quantized ReluX (not fused)"); + const int flat_size = MatchingFlatSize(input_shape, output_shape); + const T max_value = params.quantized_activation_max; + const T min_value = params.quantized_activation_min; + for (int i = 0; i < flat_size; ++i) + { + const T val = input_data[i]; + const T clamped = val > max_value ? max_value : val < min_value ? min_value : val; + output_data[i] = clamped; + } +} + +// TODO(jiawen): We can implement BroadcastMul on buffers of arbitrary +// dimensionality if the runtime code does a single loop over one dimension +// that handles broadcasting as the base case. The code generator would then +// generate max(D1, D2) nested for loops. +inline void BroadcastMulFivefold(const ArithmeticParams &unswitched_params, + const RuntimeShape &unswitched_input1_shape, + const uint8 *unswitched_input1_data, + const RuntimeShape &unswitched_input2_shape, + const uint8 *unswitched_input2_data, + const RuntimeShape &output_shape, uint8 *output_data) +{ + ArithmeticParams switched_params = unswitched_params; + switched_params.input1_offset = unswitched_params.input2_offset; + switched_params.input2_offset = unswitched_params.input1_offset; + + const bool use_unswitched = unswitched_params.broadcast_category == + tflite::BroadcastableOpCategory::kFirstInputBroadcastsFast; + + const ArithmeticParams ¶ms = use_unswitched ? unswitched_params : switched_params; + const uint8 *input1_data = use_unswitched ? unswitched_input1_data : unswitched_input2_data; + const uint8 *input2_data = use_unswitched ? unswitched_input2_data : unswitched_input1_data; + + // Fivefold nested loops. The second input resets its position for each + // iteration of the second loop. The first input resets its position at the + // beginning of the fourth loop. The innermost loop is an elementwise Mul of + // sections of the arrays. + uint8 *output_data_ptr = output_data; + const uint8 *input1_data_ptr = input1_data; + const uint8 *input2_data_reset = input2_data; + int y0 = params.broadcast_shape[0]; + int y1 = params.broadcast_shape[1]; + int y2 = params.broadcast_shape[2]; + int y3 = params.broadcast_shape[3]; + int y4 = params.broadcast_shape[4]; + for (int i0 = 0; i0 < y0; ++i0) + { + const uint8 *input2_data_ptr; + for (int i1 = 0; i1 < y1; ++i1) + { + input2_data_ptr = input2_data_reset; + for (int i2 = 0; i2 < y2; ++i2) + { + for (int i3 = 0; i3 < y3; ++i3) + { + MulElementwise(y4, params, input1_data_ptr, input2_data_ptr, output_data_ptr); + input2_data_ptr += y4; + output_data_ptr += y4; + } + input1_data_ptr += y4; + } + } + input2_data_reset = input2_data_ptr; + } +} + +inline void Mul(const ArithmeticParams ¶ms, const RuntimeShape &input1_shape, + const int16 *input1_data, const RuntimeShape &input2_shape, + const int16 *input2_data, const RuntimeShape &output_shape, int16 *output_data) +{ + ruy::profiler::ScopeLabel label("Mul/Int16"); + + const int flat_size = MatchingElementsSize(input1_shape, input2_shape, output_shape); + + for (int i = 0; i < flat_size; i++) + { + // F0 uses 0 integer bits, range [-1, 1]. + using F0 = gemmlowp::FixedPoint; + + F0 unclamped_result = F0::FromRaw(input1_data[i]) * F0::FromRaw(input2_data[i]); + output_data[i] = unclamped_result.raw(); + } +} + +inline void Mul(const ArithmeticParams ¶ms, const RuntimeShape &input1_shape, + const int16 *input1_data, const RuntimeShape &input2_shape, + const int16 *input2_data, const RuntimeShape &output_shape, uint8 *output_data) +{ + ruy::profiler::ScopeLabel label("Mul/Int16Uint8"); + int32 output_offset = params.output_offset; + int32 output_activation_min = params.quantized_activation_min; + int32 output_activation_max = params.quantized_activation_max; + TFLITE_DCHECK_LE(output_activation_min, output_activation_max); + + const int flat_size = MatchingElementsSize(input1_shape, input2_shape, output_shape); + + for (int i = 0; i < flat_size; i++) + { + // F0 uses 0 integer bits, range [-1, 1]. + using F0 = gemmlowp::FixedPoint; + + F0 unclamped_result = F0::FromRaw(input1_data[i]) * F0::FromRaw(input2_data[i]); + int16 rescaled_result = gemmlowp::RoundingDivideByPOT(unclamped_result.raw(), 8); + int16 clamped_result = std::min(output_activation_max - output_offset, rescaled_result); + clamped_result = std::max(output_activation_min - output_offset, clamped_result); + output_data[i] = output_offset + clamped_result; + } +} + +inline void Sub16(const ArithmeticParams ¶ms, const RuntimeShape &input1_shape, + const int16_t *input1_data, const RuntimeShape &input2_shape, + const int16_t *input2_data, const RuntimeShape &output_shape, + int16_t *output_data) +{ + ruy::profiler::ScopeLabel label("Sub/Int16"); + const int input1_shift = params.input1_shift; + const int flat_size = MatchingElementsSize(input1_shape, input2_shape, output_shape); + const int16 output_activation_min = params.quantized_activation_min; + const int16 output_activation_max = params.quantized_activation_max; + + TFLITE_DCHECK(input1_shift == 0 || params.input2_shift == 0); + TFLITE_DCHECK_LE(input1_shift, 0); + TFLITE_DCHECK_LE(params.input2_shift, 0); + const int16 *not_shift_input = input1_shift == 0 ? input1_data : input2_data; + const int16 *shift_input = input1_shift == 0 ? input2_data : input1_data; + const int input_right_shift = input1_shift == 0 ? -params.input2_shift : -input1_shift; + + if (input1_shift == 0) + { + // F0 uses 0 integer bits, range [-1, 1]. + using F0 = gemmlowp::FixedPoint; + for (int i = 0; i < flat_size; ++i) + { + F0 input_ready_scaled = F0::FromRaw(not_shift_input[i]); + F0 scaled_input = + F0::FromRaw(gemmlowp::RoundingDivideByPOT(shift_input[i], input_right_shift)); + F0 result = SaturatingSub(input_ready_scaled, scaled_input); + const int16 raw_output = result.raw(); + const int16 clamped_output = + std::min(output_activation_max, std::max(output_activation_min, raw_output)); + output_data[i] = clamped_output; + } + } + else + { + // F0 uses 0 integer bits, range [-1, 1]. + using F0 = gemmlowp::FixedPoint; + for (int i = 0; i < flat_size; ++i) + { + F0 input_ready_scaled = F0::FromRaw(not_shift_input[i]); + F0 scaled_input = + F0::FromRaw(gemmlowp::RoundingDivideByPOT(shift_input[i], input_right_shift)); + F0 result = SaturatingSub(scaled_input, input_ready_scaled); + const int16 raw_output = result.raw(); + const int16 clamped_output = + std::min(output_activation_max, std::max(output_activation_min, raw_output)); + output_data[i] = clamped_output; + } + } +} + +template +void Pack(const PackParams ¶ms, const RuntimeShape *const *input_shapes, + const Scalar *const *input_data, const RuntimeShape &output_shape, Scalar *output_data) +{ + ruy::profiler::ScopeLabel label("Pack"); + const int dimensions = output_shape.DimensionsCount(); + int axis = params.axis; + int inputs_count = params.inputs_count; + + int outer_size = 1; + for (int i = 0; i < axis; i++) + { + outer_size *= output_shape.Dims(i); + } + int copy_size = 1; + for (int i = params.axis + 1; i < dimensions; i++) + { + copy_size *= output_shape.Dims(i); + } + TFLITE_DCHECK_EQ((**input_shapes).FlatSize(), copy_size * outer_size); + + for (int i = 0; i < inputs_count; ++i) + { + for (int k = 0; k < outer_size; k++) + { + const Scalar *input_ptr = input_data[i] + copy_size * k; + int loc = k * inputs_count * copy_size + i * copy_size; + memcpy(output_data + loc, input_ptr, copy_size * sizeof(Scalar)); + } + } +} + +template +void Unpack(const UnpackParams ¶ms, const RuntimeShape &input_shape, const Scalar *input_data, + const RuntimeShape &output_shape, Scalar *const *output_datas) +{ + ruy::profiler::ScopeLabel label("Unpack"); + const int dimensions = input_shape.DimensionsCount(); + const int outputs_count = params.num_split; + + int outer_size = 1; + int axis = params.axis; + if (axis < 0) + { + axis += dimensions; + } + TFLITE_DCHECK_GE(axis, 0); + TFLITE_DCHECK_LT(axis, dimensions); + for (int i = 0; i < axis; ++i) + { + outer_size *= input_shape.Dims(i); + } + int copy_size = 1; + for (int i = axis + 1; i < dimensions; ++i) + { + copy_size *= input_shape.Dims(i); + } + TFLITE_DCHECK_EQ(output_shape.FlatSize(), copy_size * outer_size); + + for (int i = 0; i < outputs_count; ++i) + { + for (int k = 0; k < outer_size; k++) + { + Scalar *output_ptr = output_datas[i] + copy_size * k; + int loc = k * outputs_count * copy_size + i * copy_size; + memcpy(output_ptr, input_data + loc, copy_size * sizeof(Scalar)); + } + } +} + +template +void PackWithScaling(const PackParams ¶ms, const RuntimeShape *const *input_shapes, + const uint8 *const *input_data, const RuntimeShape &output_shape, + uint8 *output_data) +{ + ruy::profiler::ScopeLabel label("PackWithScaling"); + const int dimensions = output_shape.DimensionsCount(); + int axis = params.axis; + const int32 *input_zeropoint = params.input_zeropoint; + const float *input_scale = params.input_scale; + int inputs_count = params.inputs_count; + const int32 output_zeropoint = params.output_zeropoint; + const float output_scale = params.output_scale; + + int outer_size = 1; + for (int i = 0; i < axis; i++) + { + outer_size *= output_shape.Dims(i); + } + int copy_size = 1; + for (int i = axis + 1; i < dimensions; i++) + { + copy_size *= output_shape.Dims(i); + } + TFLITE_DCHECK_EQ((**input_shapes).FlatSize(), copy_size * outer_size); + + Scalar *output_ptr = output_data; + const float inverse_output_scale = 1.f / output_scale; + for (int k = 0; k < outer_size; k++) + { + for (int i = 0; i < inputs_count; ++i) + { + if (input_zeropoint[i] == output_zeropoint && input_scale[i] == output_scale) + { + memcpy(output_ptr, input_data[i] + k * copy_size, copy_size * sizeof(Scalar)); + } + else + { + assert(false); + const float scale = input_scale[i] * inverse_output_scale; + const float bias = -input_zeropoint[i] * scale; + auto input_ptr = input_data[i]; + for (int j = 0; j < copy_size; ++j) + { + const int value = + static_cast(std::round(input_ptr[j] * scale + bias)) + output_zeropoint; + output_ptr[j] = static_cast(std::max(std::min(255, value), 0)); + } + } + output_ptr += copy_size; + } + } +} + +template +void DepthConcatenation(const ConcatenationParams ¶ms, const RuntimeShape *const *input_shapes, + const Scalar *const *input_data, const RuntimeShape &output_shape, + Scalar *output_data) +{ + ruy::profiler::ScopeLabel label("DepthConcatenation"); + auto params_copy = params; + params_copy.axis = 3; + Concatenation(params_copy, input_shapes, input_data, output_shape, output_data); +} + +inline void LstmCell(const LstmCellParams ¶ms, const RuntimeShape &unextended_input_shape, + const float *input_data, const RuntimeShape &unextended_prev_activ_shape, + const float *prev_activ_data, const RuntimeShape &weights_shape, + const float *weights_data, const RuntimeShape &unextended_bias_shape, + const float *bias_data, const RuntimeShape &unextended_prev_state_shape, + const float *prev_state_data, + const RuntimeShape &unextended_output_state_shape, float *output_state_data, + const RuntimeShape &unextended_output_activ_shape, float *output_activ_data, + const RuntimeShape &unextended_concat_temp_shape, float *concat_temp_data, + const RuntimeShape &unextended_activ_temp_shape, float *activ_temp_data) +{ + TFLITE_DCHECK_LE(unextended_input_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_prev_activ_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_bias_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_prev_state_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_output_state_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_output_activ_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_concat_temp_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_activ_temp_shape.DimensionsCount(), 4); + const RuntimeShape input_shape = RuntimeShape::ExtendedShape(4, unextended_input_shape); + const RuntimeShape prev_activ_shape = RuntimeShape::ExtendedShape(4, unextended_prev_activ_shape); + const RuntimeShape bias_shape = RuntimeShape::ExtendedShape(4, unextended_bias_shape); + const RuntimeShape prev_state_shape = RuntimeShape::ExtendedShape(4, unextended_prev_state_shape); + const RuntimeShape output_state_shape = + RuntimeShape::ExtendedShape(4, unextended_output_state_shape); + const RuntimeShape output_activ_shape = + RuntimeShape::ExtendedShape(4, unextended_output_activ_shape); + const RuntimeShape concat_temp_shape = + RuntimeShape::ExtendedShape(4, unextended_concat_temp_shape); + const RuntimeShape activ_temp_shape = RuntimeShape::ExtendedShape(4, unextended_activ_temp_shape); + TFLITE_DCHECK_GE(weights_shape.DimensionsCount(), 2); + + const int weights_dim_count = weights_shape.DimensionsCount(); + const int batches = MatchingDim(input_shape, 0, prev_activ_shape, 0, prev_state_shape, 0, + output_state_shape, 0, output_activ_shape, 0); + const int height = MatchingDim(input_shape, 1, prev_activ_shape, 1, prev_state_shape, 1, + output_state_shape, 1, output_activ_shape, 1); + const int width = MatchingDim(input_shape, 2, prev_activ_shape, 2, prev_state_shape, 2, + output_state_shape, 2, output_activ_shape, 2); + const int input_depth = input_shape.Dims(3); + const int prev_activ_depth = prev_activ_shape.Dims(3); + const int total_input_depth = prev_activ_depth + input_depth; + TFLITE_DCHECK_EQ(weights_shape.Dims(weights_dim_count - 1), total_input_depth); + TFLITE_DCHECK_EQ(FlatSizeSkipDim(bias_shape, 3), 1); + const int intern_activ_depth = MatchingDim(weights_shape, weights_dim_count - 2, bias_shape, 3); + TFLITE_DCHECK_EQ(weights_shape.FlatSize(), intern_activ_depth * total_input_depth); + TFLITE_DCHECK_EQ(intern_activ_depth % 4, 0); + const int output_depth = MatchingDim(prev_state_shape, 3, prev_activ_shape, 3, output_state_shape, + 3, output_activ_shape, 3); + TFLITE_DCHECK_EQ(output_depth, intern_activ_depth / 4); + + // Concatenate prev_activ and input data together + std::vector concat_input_arrays_data; + std::vector concat_input_arrays_shapes; + concat_input_arrays_data.push_back(input_data); + concat_input_arrays_data.push_back(prev_activ_data); + concat_input_arrays_shapes.push_back(&input_shape); + concat_input_arrays_shapes.push_back(&prev_activ_shape); + tflite::ConcatenationParams concat_params; + concat_params.axis = 3; + concat_params.inputs_count = concat_input_arrays_data.size(); + Concatenation(concat_params, &(concat_input_arrays_shapes[0]), &(concat_input_arrays_data[0]), + concat_temp_shape, concat_temp_data); + + // Fully connected + tflite::FullyConnectedParams fc_params; + fc_params.float_activation_min = std::numeric_limits::lowest(); + fc_params.float_activation_max = std::numeric_limits::max(); + FullyConnected(fc_params, concat_temp_shape, concat_temp_data, weights_shape, weights_data, + bias_shape, bias_data, activ_temp_shape, activ_temp_data); + + // Memory state update (the LSTM "guts") + for (int b = 0; b < batches; ++b) + { + for (int w = 0; w < width; ++w) + { + for (int h = 0; h < height; ++h) + { + for (int c = 0; c < output_depth; ++c) + { + const float input_gate = + 1.f / + (1.f + + std::exp(-activ_temp_data[Offset(activ_temp_shape, b, h, w, 0 * output_depth + c)])); + const float new_input = + std::tanh(activ_temp_data[Offset(activ_temp_shape, b, h, w, 1 * output_depth + c)]); + const float forget_gate = + 1.f / + (1.f + + std::exp(-activ_temp_data[Offset(activ_temp_shape, b, h, w, 2 * output_depth + c)])); + const float output_gate = + 1.f / + (1.f + + std::exp(-activ_temp_data[Offset(activ_temp_shape, b, h, w, 3 * output_depth + c)])); + const float new_state = + input_gate * new_input + + forget_gate * prev_state_data[Offset(prev_state_shape, b, h, w, c)]; + output_state_data[Offset(output_state_shape, b, h, w, c)] = new_state; + output_activ_data[Offset(output_activ_shape, b, h, w, c)] = + output_gate * std::tanh(new_state); + } + } + } + } +} + +// Quantized LSTM cell implementation. +// The quantization of the input, output arrays is as follows: +// - The input activations are quantized as uint8 on the interval +// [-1, 127/128]. +// The rationale for that is that is the natural interval for output +// activations (see next point) and these need to be concatenated together. +// We could accommodate different ranges by re-scaling, but we empirically +// found that setting the input activations range to be [-1, 127/128] in the +// first place, removing the need for re-scaling, greatly improves accuracy. +// - The output activations are quantized as uint8 on the interval +// [-1, 127/128]. +// The rationale for that is that the definition of a LSTM cell makes them +// intrinsically constrained in [-1, 1]; tweaking that to [-1, 127/128] +// makes for simpler, more accurate fixed-point arithmetic. +// - The output-at-previous-timestep state array is obviously quantized as +// the output activations. +// - The internal LSTM memory (not the output-at-previous-timestep, the other +// internal state array) is int16-quantized and may use any power-of-two, +// symmetric range i.e. [-2^N, 2^N * 32767/32768] for any N, which we call +// StateIntegerBits below, see the below discussion of that template +// parameter ("The StateIntegerBits template parameter"). +// - The output of the internal fully-connected node is int16-quantized +// on the interval [-8, 8 * 32767/32768], the rationale for which is +// explained just below ("Why [-8, 8] for fully-connected output?"). +// +// +// === The StateIntegerBits template parameter === +// +// The StateIntegerBits template parameter controls the fixed-point format used +// to represent the internal memory of the LSTM cell (not the +// output-at-previous-timestep, the other internal state array). It's currently +// a template parameter so that the model can control that. The most typical +// value for StateIntegerBits is 4. Other plausible values are anywhere between +// 3 and 5. We might eventually standardize on a single supported value, e.g. 4, +// and drop that template parameter. The reason why it can't be a runtime +// parameter is that this controls the fixed-point format used, i.e. we need to +// generate actually different code based on it. In particular, we generate code +// for a fixed-point tanh() implementation for that format, which internally +// uses a fixed-point exp() implementation, which internally uses a +// barrel-shifter with a number of steps that depends on StateIntegerBits. +// Another consequence of that is that a higher value of StateIntegerBits +// results in a more expensive implementation (more barrel shifter steps +// needed). +// +// +// === Why [-8, 8] for fully-connected output? === +// +// This array is only fed to Logistic and Tanh functions, for which +// the quantized implementation will want to use fixed-point arithmetic, +// requiring a power-of-two representation interval. Thus, we should right +// away quantize this array to a power-of-two interval; otherwise, +// implementation will need to rescale that, losing any benefit that a tighter +// representation interval might otherwise yield, while introducing some +// numerical error and computational overhead. +// +// Now, Logistic and Tanh +// are nearly constant (nearly equal to their horizontal asymptotes) +// outside of a small bounded interval around 0: +// +// Logistic(4) = 1 - 1.8e-2 Tanh(4) = 1 - 6.7e-4 +// Logistic(8) = 1 - 3.4e-4 Tanh(8) = 1 - 2.3e-7 +// Logistic(16) = 1 - 1.1e-7 Tanh(16) = 1 - 2.5e-14 +// +// From this, we see that clamping to [-4, 4] would be too inaccurate +// (the error of 1.8e-2 on Logistic would be felt even in 8bit precision) +// while clamping to [-16, 16] would make no difference even in float32. +// However, for a fixed-point implementation in 16-bit integers, using 5 +// integer bits to represent the [-16, 16] range would leave only 11 +// fractional bits, giving an increment of 2^-11 = 4.9e-4 between consecutive +// representable values. Notice that is higher than the +// worst-case clamping error with clamping to [-8, 8]: 3.4e-4 for Logistic. +// Using [-8, 8] thus seems like the better compromise overall, enjoying +// an increment of 2.4e-4 between representable values and a worst-case +// clamping error of 3.4e-4, both better than the increment of 4.9e-4 with +// [-16, 16]. +// +// Moreover, all other things being equal, it is nice to choose the narrower +// representation range, as that makes the implementation of fixed-point +// math functions a little cheaper (each integer bit requires an additional +// barrel-shifter atep in the implementation of exp(-x)). That is further +// reason to prefer [-8, 8] over [-16, 16]. The choice of [-16, 16] would make +// sense for 32-bit float or 32-bit fixed-point quantization, but we are +// aiming for 16-bit fixed-point quantization of these internal nodes here. +// +template +inline void +LstmCell(const LstmCellParams ¶ms, const RuntimeShape &unextended_input_shape, + const uint8 *input_data_uint8, const RuntimeShape &unextended_prev_activ_shape, + const uint8 *prev_activ_data_uint8, const RuntimeShape &weights_shape, + const uint8 *weights_data_uint8, const RuntimeShape &unextended_bias_shape, + const int32 *bias_data_int32, const RuntimeShape &unextended_prev_state_shape, + const int16 *prev_state_data_int16, const RuntimeShape &unextended_output_state_shape, + int16 *output_state_data_int16, const RuntimeShape &unextended_output_activ_shape, + uint8 *output_activ_data_uint8, const RuntimeShape &unextended_concat_temp_shape, + uint8 *concat_temp_data_uint8, const RuntimeShape &unextended_activ_temp_shape, + int16 *activ_temp_data_int16, void *gemmlowp_context) +{ + (void)gemmlowp_context; // only used in optimized code. + int32 weights_zero_point = params.weights_zero_point; + int32 accum_multiplier = params.accum_multiplier; + int accum_shift = params.accum_shift; + TFLITE_DCHECK_LE(unextended_input_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_prev_activ_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_bias_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_prev_state_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_output_state_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_output_activ_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_concat_temp_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_activ_temp_shape.DimensionsCount(), 4); + const RuntimeShape input_shape = RuntimeShape::ExtendedShape(4, unextended_input_shape); + const RuntimeShape prev_activ_shape = RuntimeShape::ExtendedShape(4, unextended_prev_activ_shape); + const RuntimeShape bias_shape = RuntimeShape::ExtendedShape(4, unextended_bias_shape); + const RuntimeShape prev_state_shape = RuntimeShape::ExtendedShape(4, unextended_prev_state_shape); + const RuntimeShape output_state_shape = + RuntimeShape::ExtendedShape(4, unextended_output_state_shape); + const RuntimeShape output_activ_shape = + RuntimeShape::ExtendedShape(4, unextended_output_activ_shape); + const RuntimeShape concat_temp_shape = + RuntimeShape::ExtendedShape(4, unextended_concat_temp_shape); + const RuntimeShape activ_temp_shape = RuntimeShape::ExtendedShape(4, unextended_activ_temp_shape); + TFLITE_DCHECK_GE(weights_shape.DimensionsCount(), 2); + + // Gather dimensions information, and perform consistency checks. + const int weights_dim_count = weights_shape.DimensionsCount(); + const int outer_size = MatchingFlatSizeSkipDim(input_shape, 3, prev_activ_shape, prev_state_shape, + output_state_shape, output_activ_shape); + const int input_depth = input_shape.Dims(3); + const int prev_activ_depth = prev_activ_shape.Dims(3); + const int total_input_depth = prev_activ_depth + input_depth; + TFLITE_DCHECK_EQ(weights_shape.Dims(weights_dim_count - 1), total_input_depth); + const int intern_activ_depth = MatchingDim(weights_shape, weights_dim_count - 2, bias_shape, 3); + TFLITE_DCHECK_EQ(weights_shape.FlatSize(), intern_activ_depth * total_input_depth); + TFLITE_DCHECK_EQ(FlatSizeSkipDim(bias_shape, 3), 1); + TFLITE_DCHECK_EQ(intern_activ_depth % 4, 0); + const int output_depth = MatchingDim(prev_state_shape, 3, prev_activ_shape, 3, output_state_shape, + 3, output_activ_shape, 3); + TFLITE_DCHECK_EQ(output_depth, intern_activ_depth / 4); + const int fc_batches = FlatSizeSkipDim(activ_temp_shape, 3); + const int fc_output_depth = + MatchingDim(weights_shape, weights_dim_count - 2, activ_temp_shape, 3); + const int fc_accum_depth = total_input_depth; + TFLITE_DCHECK_EQ(fc_output_depth, 4 * output_depth); + + // Depth-concatenate prev_activ and input data together. + uint8 const *concat_input_arrays_data[2] = {input_data_uint8, prev_activ_data_uint8}; + const RuntimeShape *concat_input_arrays_shapes[2] = {&input_shape, &prev_activ_shape}; + tflite::ConcatenationParams concat_params; + concat_params.axis = 3; + concat_params.inputs_count = 2; + Concatenation(concat_params, concat_input_arrays_shapes, concat_input_arrays_data, + concat_temp_shape, concat_temp_data_uint8); + + // Implementation of the fully connected node inside the LSTM cell. + // The operands are 8-bit integers, the accumulators are internally 32bit + // integers, and the output is 16-bit fixed-point with 3 integer bits so + // the output range is [-2^3, 2^3] == [-8, 8]. The rationale for that + // is explained in the function comment above. + for (int b = 0; b < fc_batches; ++b) + { + for (int out_c = 0; out_c < fc_output_depth; ++out_c) + { + // Internal accumulation. + // Initialize accumulator with the bias-value. + int32 accum = bias_data_int32[out_c]; + // Accumulation loop. + for (int d = 0; d < fc_accum_depth; ++d) + { + int16 input_val = concat_temp_data_uint8[b * fc_accum_depth + d] - 128; + int16 weights_val = weights_data_uint8[out_c * fc_accum_depth + d] - weights_zero_point; + accum += input_val * weights_val; + } + // Down-scale the final int32 accumulator to the scale used by our + // (16-bit, using 3 integer bits) fixed-point format. The quantized + // multiplier and shift here have been pre-computed offline + // (e.g. by toco). + accum = MultiplyByQuantizedMultiplier(accum, accum_multiplier, accum_shift); + // Saturate, cast to int16, and store to the temporary activations array. + accum = std::max(-32768, std::min(32767, static_cast(accum))); + activ_temp_data_int16[out_c + fc_output_depth * b] = accum; + } + } + + // Rest of the LSTM cell: tanh and logistic math functions, and some adds + // and muls, all done in 16-bit fixed-point. + for (int b = 0; b < outer_size; ++b) + { + for (int c = 0; c < output_depth; ++c) + { + // Define the fixed-point data types that we will use here. All use + // int16 as the underlying integer type i.e. all are 16-bit fixed-point. + // They only differ by the number of integral vs. fractional bits, + // determining the range of values that they can represent. + // + // F0 uses 0 integer bits, range [-1, 1]. + // This is the return type of math functions such as tanh, logistic, + // whose range is in [-1, 1]. + using F0 = gemmlowp::FixedPoint; + // F3 uses 3 integer bits, range [-8, 8]. + // This is the range of the previous fully-connected node's output, + // which is our input here. + using F3 = gemmlowp::FixedPoint; + // FS uses StateIntegerBits integer bits, range [-2^StateIntegerBits, + // 2^StateIntegerBits]. It's used to represent the internal state, whose + // number of integer bits is currently dictated by the model. See comment + // on the StateIntegerBits template parameter above. + using FS = gemmlowp::FixedPoint; + // Implementation of input gate, using fixed-point logistic function. + F3 input_gate_input = + F3::FromRaw(activ_temp_data_int16[b * fc_output_depth + 0 * output_depth + c]); + F0 input_gate_output = gemmlowp::logistic(input_gate_input); + // Implementation of input modulation gate, using fixed-point tanh + // function. + F3 input_modulation_gate_input = + F3::FromRaw(activ_temp_data_int16[b * fc_output_depth + 1 * output_depth + c]); + F0 input_modulation_gate_output = gemmlowp::tanh(input_modulation_gate_input); + // Implementation of forget gate, using fixed-point logistic function. + F3 forget_gate_input = + F3::FromRaw(activ_temp_data_int16[b * fc_output_depth + 2 * output_depth + c]); + F0 forget_gate_output = gemmlowp::logistic(forget_gate_input); + // Implementation of output gate, using fixed-point logistic function. + F3 output_gate_input = + F3::FromRaw(activ_temp_data_int16[b * fc_output_depth + 3 * output_depth + c]); + F0 output_gate_output = gemmlowp::logistic(output_gate_input); + // Implementation of internal multiplication nodes, still in fixed-point. + F0 input_times_input_modulation = input_gate_output * input_modulation_gate_output; + FS prev_state = FS::FromRaw(prev_state_data_int16[b * output_depth + c]); + FS prev_state_times_forget_state = forget_gate_output * prev_state; + // Implementation of internal addition node, saturating. + FS new_state = + gemmlowp::SaturatingAdd(gemmlowp::Rescale(input_times_input_modulation), + prev_state_times_forget_state); + // Implementation of last internal Tanh node, still in fixed-point. + // Since a Tanh fixed-point implementation is specialized for a given + // number or integer bits, and each specialization can have a substantial + // code size, and we already used above a Tanh on an input with 3 integer + // bits, and per the table in the above function comment there is no + // significant accuracy to be lost by clamping to [-8, +8] for a + // 3-integer-bits representation, let us just do that. This helps people + // porting this to targets where code footprint must be minimized. + F3 new_state_f3 = gemmlowp::Rescale<3>(new_state); + F0 output_activ_int16 = output_gate_output * gemmlowp::tanh(new_state_f3); + // Store the new internal state back to memory, as 16-bit integers. + // Note: here we store the original value with StateIntegerBits, not + // the rescaled 3-integer-bits value fed to tanh. + output_state_data_int16[b * output_depth + c] = new_state.raw(); + // Down-scale the output activations to 8-bit integers, saturating, + // and store back to memory. + int16 rescaled_output_activ = gemmlowp::RoundingDivideByPOT(output_activ_int16.raw(), 8); + int16 clamped_output_activ = + std::max(-128, std::min(127, rescaled_output_activ)); + output_activ_data_uint8[b * output_depth + c] = 128 + clamped_output_activ; + } + } +} + +template +void Split(const SplitParams ¶ms, const RuntimeShape &input_shape, const Scalar *input_data, + const RuntimeShape *const *output_shapes, Scalar *const *output_data) +{ + ruy::profiler::ScopeLabel label("Split"); + const int split_dimensions = input_shape.DimensionsCount(); + int axis = params.axis < 0 ? params.axis + split_dimensions : params.axis; + int outputs_count = params.num_split; + TFLITE_DCHECK_LT(axis, split_dimensions); + + int64_t split_size = 0; + for (int i = 0; i < outputs_count; i++) + { + TFLITE_DCHECK_EQ(output_shapes[i]->DimensionsCount(), split_dimensions); + for (int j = 0; j < split_dimensions; j++) + { + if (j != axis) + { + MatchingDim(*output_shapes[i], j, input_shape, j); + } + } + split_size += output_shapes[i]->Dims(axis); + } + TFLITE_DCHECK_EQ(split_size, input_shape.Dims(axis)); + int64_t outer_size = 1; + for (int i = 0; i < axis; ++i) + { + outer_size *= input_shape.Dims(i); + } + // For all output arrays, + // FlatSize() = outer_size * Dims(axis) * base_inner_size; + int64_t base_inner_size = 1; + for (int i = axis + 1; i < split_dimensions; ++i) + { + base_inner_size *= input_shape.Dims(i); + } + + const Scalar *input_ptr = input_data; + for (int k = 0; k < outer_size; k++) + { + for (int i = 0; i < outputs_count; ++i) + { + const int copy_size = output_shapes[i]->Dims(axis) * base_inner_size; + memcpy(output_data[i] + k * copy_size, input_ptr, copy_size * sizeof(Scalar)); + input_ptr += copy_size; + } + } +} + +inline int NodeOffset(int b, int h, int w, int height, int width) +{ + return (b * height + h) * width + w; +} + +inline void LocalResponseNormalization(const tflite::LocalResponseNormalizationParams &op_params, + const RuntimeShape &input_shape, const float *input_data, + const RuntimeShape &output_shape, float *output_data) +{ + const int trailing_dim = input_shape.DimensionsCount() - 1; + const int outer_size = MatchingFlatSizeSkipDim(input_shape, trailing_dim, output_shape); + const int depth = MatchingDim(input_shape, trailing_dim, output_shape, trailing_dim); + + for (int i = 0; i < outer_size; ++i) + { + for (int c = 0; c < depth; ++c) + { + const int begin_input_c = std::max(0, static_cast(c - op_params.range)); + const int end_input_c = std::min(depth, static_cast(c + op_params.range)); + float accum = 0.f; + for (int input_c = begin_input_c; input_c < end_input_c; ++input_c) + { + const float input_val = input_data[i * depth + input_c]; + accum += input_val * input_val; + } + const float multiplier = std::pow(op_params.bias + op_params.alpha * accum, -op_params.beta); + output_data[i * depth + c] = input_data[i * depth + c] * multiplier; + } + } +} + +inline void Dequantize(const RuntimeShape &input_shape, const Eigen::half *input_data, + const RuntimeShape &output_shape, float *output_data) +{ + const int flat_size = MatchingFlatSize(input_shape, output_shape); + for (int i = 0; i < flat_size; i++) + { + output_data[i] = static_cast(input_data[i]); + } +} + +inline void FakeQuant(const tflite::FakeQuantParams &op_params, const RuntimeShape &input_shape, + const float *input_data, const RuntimeShape &output_shape, float *output_data) +{ + ruy::profiler::ScopeLabel label("FakeQuant"); + float rmin = op_params.minmax.min; + float rmax = op_params.minmax.max; + int num_bits = op_params.num_bits; + // 0 should always be a representable value. Let's assume that the initial + // min,max range contains 0. + TFLITE_DCHECK_LE(rmin, 0.0f); + TFLITE_DCHECK_GE(rmax, 0.0f); + TFLITE_DCHECK_LT(rmin, rmax); + + // Code matches tensorflow's FakeQuantWithMinMaxArgsFunctor. + int quant_min = 0; + int quant_max = (1 << num_bits) - 1; + float nudged_min, nudged_max, nudged_scale; + NudgeQuantizationRange(rmin, rmax, quant_min, quant_max, &nudged_min, &nudged_max, &nudged_scale); + const int flat_size = MatchingFlatSize(input_shape, output_shape); + FakeQuantizeArray(nudged_scale, nudged_min, nudged_max, input_data, output_data, flat_size); +} + +// Common subroutine for both `GatherNd` and `GatherNdString`. +struct GatherNdHelperResult +{ + int n_slices; + int slice_size; + int indices_nd; + std::vector dims_to_count; +}; + +// Returns common values being used on both `GatherNd` and `GatherNdString`. +inline GatherNdHelperResult GatherNdHelper(const RuntimeShape ¶ms_shape, + const RuntimeShape &indices_shape) +{ + GatherNdHelperResult ret; + ret.n_slices = 1; + ret.slice_size = 1; + const int indices_dims = indices_shape.DimensionsCount(); + ret.indices_nd = indices_shape.Dims(indices_dims - 1); + const int params_dims = params_shape.DimensionsCount(); + for (int i = 0; i < indices_dims - 1; ++i) + { + ret.n_slices *= indices_shape.Dims(i); + } + for (int i = ret.indices_nd; i < params_dims; ++i) + { + ret.slice_size *= params_shape.Dims(i); + } + + int remain_flat_size = params_shape.FlatSize(); + ret.dims_to_count = std::vector(ret.indices_nd, 0); + for (int i = 0; i < ret.indices_nd; ++i) + { + ret.dims_to_count[i] = remain_flat_size / params_shape.Dims(i); + remain_flat_size = ret.dims_to_count[i]; + } + + return ret; +} + +template +inline void GatherNd(const RuntimeShape ¶ms_shape, const ParamsT *params_data, + const RuntimeShape &indices_shape, const IndicesT *indices_data, + const RuntimeShape &output_shape, ParamsT *output_data) +{ + ruy::profiler::ScopeLabel label("GatherNd"); + + const GatherNdHelperResult res = GatherNdHelper(params_shape, indices_shape); + for (int i = 0; i < res.n_slices; ++i) + { + int from_pos = 0; + for (int j = 0; j < res.indices_nd; ++j) + { + from_pos += indices_data[i * res.indices_nd + j] * res.dims_to_count[j]; + } + std::memcpy(output_data + i * res.slice_size, params_data + from_pos, + sizeof(ParamsT) * res.slice_size); + } +} + +#ifndef TF_LITE_STATIC_MEMORY +template +inline void GatherNdString(const RuntimeShape ¶ms_shape, const TfLiteTensor *params_data, + const RuntimeShape &indices_shape, const IndicesT *indices_data, + const RuntimeShape &output_shape, TfLiteTensor *output_data) +{ + ruy::profiler::ScopeLabel label("GatherNdString"); + + const GatherNdHelperResult res = GatherNdHelper(params_shape, indices_shape); + DynamicBuffer buffer; + for (int i = 0; i < res.n_slices; ++i) + { + int from_pos = 0; + for (int j = 0; j < res.indices_nd; ++j) + { + from_pos += indices_data[i * res.indices_nd + j] * res.dims_to_count[j]; + } + for (int j = 0; j < res.slice_size; ++j) + { + buffer.AddString(GetString(params_data, from_pos + j)); + } + } + buffer.WriteToTensor(output_data, /*new_shape=*/nullptr); +} +#endif + +template +inline void ScatterNd(const RuntimeShape &indices_shape, const IndicesT *indices_data, + const RuntimeShape &updates_shape, const UpdatesT *updates_data, + const RuntimeShape &output_shape, UpdatesT *output_data) +{ + ruy::profiler::ScopeLabel label("ScatterNd"); + + int n_slices = 1; + int slice_size = 1; + const int outer_dims = indices_shape.DimensionsCount() - 1; + const int indices_nd = indices_shape.Dims(outer_dims); + const int updates_dims = updates_shape.DimensionsCount(); + for (int i = 0; i < outer_dims; ++i) + { + n_slices *= indices_shape.Dims(i); + } + for (int i = outer_dims; i < updates_dims; ++i) + { + slice_size *= updates_shape.Dims(i); + } + + int output_flat_size = output_shape.FlatSize(); + int remain_flat_size = output_flat_size; + std::vector dims_to_count(indices_nd, 0); + for (int i = 0; i < indices_nd; ++i) + { + dims_to_count[i] = remain_flat_size / output_shape.Dims(i); + remain_flat_size = dims_to_count[i]; + } + + memset(output_data, 0, sizeof(UpdatesT) * output_flat_size); + for (int i = 0; i < n_slices; ++i) + { + int to_pos = 0; + for (int j = 0; j < indices_nd; ++j) + { + IndicesT idx = indices_data[i * indices_nd + j]; + TFLITE_DCHECK(0 <= idx && idx < output_shape.Dims(j)); + to_pos += idx * dims_to_count[j]; + } + for (int j = 0; j < slice_size; j++) + { + output_data[to_pos + j] += updates_data[i * slice_size + j]; + } + } +} + +template +inline void Slice(const tflite::SliceParams &op_params, const RuntimeShape &input_shape, + const RuntimeShape &output_shape, SequentialTensorWriter *writer) +{ + const RuntimeShape ext_shape = RuntimeShape::ExtendedShape(5, input_shape); + TFLITE_DCHECK_LE(op_params.begin_count, 5); + TFLITE_DCHECK_LE(op_params.size_count, 5); + const int begin_count = op_params.begin_count; + const int size_count = op_params.size_count; + // We front-pad the begin and size vectors. + std::array start; + std::array stop; + for (int i = 0; i < 5; ++i) + { + int padded_i = 5 - i; + start[i] = begin_count < padded_i ? 0 : op_params.begin[begin_count - padded_i]; + stop[i] = (size_count < padded_i || op_params.size[size_count - padded_i] == -1) + ? ext_shape.Dims(i) + : start[i] + op_params.size[size_count - padded_i]; + } + + for (int i0 = start[0]; i0 < stop[0]; ++i0) + { + for (int i1 = start[1]; i1 < stop[1]; ++i1) + { + for (int i2 = start[2]; i2 < stop[2]; ++i2) + { + for (int i3 = start[3]; i3 < stop[3]; ++i3) + { + for (int i4 = start[4]; i4 < stop[4]; ++i4) + { + writer->Write(Offset(ext_shape, i0, i1, i2, i3, i4)); + } + } + } + } + } +} + +template +inline void Slice(const tflite::SliceParams &op_params, const RuntimeShape &input_shape, + const T *input_data, const RuntimeShape &output_shape, T *output_data) +{ + SequentialTensorWriter writer(input_data, output_data); + return Slice(op_params, input_shape, output_shape, &writer); +} + +template +inline void Slice(const tflite::SliceParams &op_params, const RuntimeShape &input_shape, + const TfLiteTensor *input, const RuntimeShape &output_shape, TfLiteTensor *output) +{ + SequentialTensorWriter writer(input, output); + return Slice(op_params, input_shape, output_shape, &writer); +} + +template +void Minimum(const RuntimeShape &input1_shape, const T *input1_data, const T *input2_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int flat_size = MatchingFlatSize(input1_shape, output_shape); + + auto min_value = input2_data[0]; + for (int i = 0; i < flat_size; i++) + { + output_data[i] = input1_data[i] > min_value ? min_value : input1_data[i]; + } +} + +// Convenience version that allows, for example, generated-code calls to be +// the same as other binary ops. +template +inline void Minimum(const RuntimeShape &input1_shape, const T *input1_data, const RuntimeShape &, + const T *input2_data, const RuntimeShape &output_shape, T *output_data) +{ + // Drop shape of second input: not needed. + Minimum(input1_shape, input1_data, input2_data, output_shape, output_data); +} + +template +void Maximum(const RuntimeShape &input1_shape, const T *input1_data, const T *input2_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int flat_size = MatchingFlatSize(input1_shape, output_shape); + + auto max_value = input2_data[0]; + for (int i = 0; i < flat_size; i++) + { + output_data[i] = input1_data[i] < max_value ? max_value : input1_data[i]; + } +} + +// Convenience version that allows, for example, generated-code calls to be +// the same as other binary ops. +template +inline void Maximum(const RuntimeShape &input1_shape, const T *input1_data, const RuntimeShape &, + const T *input2_data, const RuntimeShape &output_shape, T *output_data) +{ + // Drop shape of second input: not needed. + Maximum(input1_shape, input1_data, input2_data, output_shape, output_data); +} + +template +void ArgMax(const RuntimeShape &input1_shape, const T1 *input1_data, const T3 *input2_data, + const RuntimeShape &output_shape, T2 *output_data) +{ + ArgMinMax(input1_shape, input1_data, input2_data, output_shape, output_data, std::greater()); +} + +// Convenience version that allows, for example, generated-code calls to be +// the same as other binary ops. +template +inline void ArgMax(const RuntimeShape &input1_shape, const T1 *input1_data, + const RuntimeShape &input2_shape, const T3 *input2_data, + const RuntimeShape &output_shape, T2 *output_data) +{ + // Drop shape of second input: not needed. + ArgMax(input1_shape, input1_data, input2_data, output_shape, output_data); +} + +template +void Select(const RuntimeShape &input_condition_shape, const D *input_condition_data, + const RuntimeShape &input_x_shape, const T *input_x_data, + const RuntimeShape &input_y_shape, const T *input_y_data, + const RuntimeShape &output_shape, T *output_data) +{ + int64_t flatsize; + // Allow select operator executions on mixed scalar tensors and one element + // tensors. + if (input_condition_shape.FlatSize() == 1 && input_x_shape.FlatSize() == 1 && + input_y_shape.FlatSize() == 1 && output_shape.FlatSize() == 1) + { + flatsize = 1; + } + else + { + flatsize = MatchingFlatSize(input_condition_shape, input_x_shape, input_y_shape, output_shape); + } + for (int64_t i = 0; i < flatsize; ++i) + { + output_data[i] = input_condition_data[i] ? input_x_data[i] : input_y_data[i]; + } +} + +template +void RankOneSelect(const RuntimeShape &input_condition_shape, const D *input_condition_data, + const RuntimeShape &input_x_shape, const T *input_x_data, + const RuntimeShape &input_y_shape, const T *input_y_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int64_t outer_size = input_condition_shape.FlatSize(); + int64_t inner_size; + if (input_condition_shape.DimensionsCount() == 0) + { + inner_size = MatchingFlatSize(input_x_shape, input_y_shape, output_shape); + } + else + { + TFLITE_DCHECK_EQ(MatchingDim(input_x_shape, 0, input_y_shape, 0, output_shape, 0), outer_size); + inner_size = MatchingFlatSizeSkipDim(input_x_shape, 0, input_y_shape, output_shape); + } + + int64_t offset = 0; + for (int64_t i = 0; i < outer_size; i++) + { + const T *input_data = input_condition_data[i] ? input_x_data : input_y_data; + memcpy(output_data + offset, input_data + offset, inner_size * sizeof(T)); + offset += inner_size; + } +} + +template +void BroadcastSelect4DSlow(const RuntimeShape &input_condition_shape, const D *input_condition_data, + const RuntimeShape &input_x_shape, const T *input_x_data, + const RuntimeShape &input_y_shape, const T *input_y_data, + const RuntimeShape &output_shape, T *output_data) +{ + TFLITE_DCHECK_LE(input_condition_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(input_x_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(input_y_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(output_shape.DimensionsCount(), 4); + + const RuntimeShape extended_output_shape = RuntimeShape::ExtendedShape(4, output_shape); + + NdArrayDesc<4> desc_condition; + NdArrayDesc<4> desc_x; + NdArrayDesc<4> desc_y; + NdArrayDescsForElementwiseBroadcast(input_condition_shape, input_x_shape, input_y_shape, + &desc_condition, &desc_x, &desc_y); + + // In Tensorflow, the dimensions are canonically named (batch_number, row, + // col, channel), with extents (batches, height, width, depth), with the + // trailing dimension changing most rapidly (channels has the smallest + // stride, typically 1 element). + // + // In generated C code, we store arrays with the dimensions reversed. The + // first dimension has smallest stride. + // + // We name our variables by their Tensorflow convention, but generate C code + // nesting loops such that the innermost loop has the smallest stride for + // the best cache behavior. + for (int b = 0; b < extended_output_shape.Dims(0); ++b) + { + for (int y = 0; y < extended_output_shape.Dims(1); ++y) + { + for (int x = 0; x < extended_output_shape.Dims(2); ++x) + { + for (int c = 0; c < extended_output_shape.Dims(3); ++c) + { + const int condition_index = SubscriptToIndex(desc_condition, b, y, x, c); + const int x_index = SubscriptToIndex(desc_x, b, y, x, c); + const int y_index = SubscriptToIndex(desc_y, b, y, x, c); + output_data[Offset(extended_output_shape, b, y, x, c)] = + input_condition_data[condition_index] ? input_x_data[x_index] : input_y_data[y_index]; + } + } + } + } +} + +template +void SelectTrueCoords(const RuntimeShape &input_condition_shape, const D *input_condition_data, + T *output_data) +{ + const size_t size = input_condition_shape.FlatSize(); + if (size == 0) + { + // Dimension is zero, in which case we don't need to output. + return; + } + const size_t cond_rank = input_condition_shape.DimensionsCount(); + + std::vector dims_to_count(cond_rank, 0); + int cur_flat_size = size; + for (int i = 0; i < cond_rank; ++i) + { + dims_to_count[i] = cur_flat_size / input_condition_shape.Dims(i); + cur_flat_size = dims_to_count[i]; + } + + int output_index = 0; + for (int i = 0; i < size; ++i) + { + if (input_condition_data[i]) + { + // Insert the coordinate of the current item (row major) into output. + int flat_index = i; + for (int j = 0; j < cond_rank; ++j) + { + int coord_j = flat_index / dims_to_count[j]; + output_data[output_index * cond_rank + j] = coord_j; + flat_index %= dims_to_count[j]; + } + output_index++; + } + } +} + +// For easy implementation, the indices is always a vector of size-4 vectors. +template +inline void SparseToDense(const std::vector> &indices, const T *values, + T default_value, bool value_is_scalar, + const RuntimeShape &unextended_output_shape, T *output_data) +{ + TFLITE_DCHECK_LE(unextended_output_shape.DimensionsCount(), 4); + const RuntimeShape output_shape = RuntimeShape::ExtendedShape(4, unextended_output_shape); + const int value_count = indices.size(); + + // First fill the output_data with default value. + const int num_elements = output_shape.FlatSize(); + for (int i = 0; i < num_elements; ++i) + { + output_data[i] = default_value; + } + + // Special handle for value is scalar case to avoid checking the boolean + // condition within the loop every time. + if (value_is_scalar) + { + for (int i = 0; i < value_count; ++i) + { + const std::vector &index = indices[i]; + TFLITE_DCHECK_EQ(index.size(), 4); + const T value = *values; // just use the first value. + output_data[Offset(output_shape, index[0], index[1], index[2], index[3])] = value; + } + return; + } + + // Go through the values and indices to fill the sparse values. + for (int i = 0; i < value_count; ++i) + { + const std::vector &index = indices[i]; + TFLITE_DCHECK_EQ(index.size(), 4); + const T value = values[i]; + output_data[Offset(output_shape, index[0], index[1], index[2], index[3])] = value; + } +} + +template +inline void Pow(const RuntimeShape &input1_shape, const T *input1_data, + const RuntimeShape &input2_shape, const T *input2_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int flat_size = MatchingFlatSize(input1_shape, input2_shape, output_shape); + for (int i = 0; i < flat_size; ++i) + { + output_data[i] = std::pow(input1_data[i], input2_data[i]); + } +} + +template +inline void BroadcastPow4DSlow(const RuntimeShape &unextended_input1_shape, const T *input1_data, + const RuntimeShape &unextended_input2_shape, const T *input2_data, + const RuntimeShape &unextended_output_shape, T *output_data) +{ + TFLITE_DCHECK_LE(unextended_input1_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_input2_shape.DimensionsCount(), 4); + TFLITE_DCHECK_LE(unextended_output_shape.DimensionsCount(), 4); + const RuntimeShape output_shape = RuntimeShape::ExtendedShape(4, unextended_output_shape); + + NdArrayDesc<4> desc1; + NdArrayDesc<4> desc2; + NdArrayDescsForElementwiseBroadcast(unextended_input1_shape, unextended_input2_shape, &desc1, + &desc2); + + for (int b = 0; b < output_shape.Dims(0); ++b) + { + for (int y = 0; y < output_shape.Dims(1); ++y) + { + for (int x = 0; x < output_shape.Dims(2); ++x) + { + for (int c = 0; c < output_shape.Dims(3); ++c) + { + auto out_idx = Offset(output_shape, b, y, x, c); + auto in1_idx = SubscriptToIndex(desc1, b, y, x, c); + auto in2_idx = SubscriptToIndex(desc2, b, y, x, c); + auto in1_val = input1_data[in1_idx]; + auto in2_val = input2_data[in2_idx]; + output_data[out_idx] = std::pow(in1_val, in2_val); + } + } + } + } +} + +template +void Reverse(int axis, const RuntimeShape &input_shape, const Scalar *input_data, + const RuntimeShape &output_shape, Scalar *output_data) +{ + ruy::profiler::ScopeLabel label("Reverse"); + + int outer_size = 1; + for (int i = 0; i < axis; ++i) + { + outer_size *= input_shape.Dims(i); + } + + int copy_size = 1; + for (int i = axis + 1; i < input_shape.DimensionsCount(); ++i) + { + copy_size *= input_shape.Dims(i); + } + + const int dims_at_axis = input_shape.Dims(axis); + for (int i = 0; i < outer_size; ++i) + { + for (int j = 0; j < dims_at_axis; ++j) + { + const int start_pos = (i * dims_at_axis + j) * copy_size; + Scalar *output_ptr = output_data + start_pos; + int loc = (i * dims_at_axis + dims_at_axis - j - 1) * copy_size; + memcpy(output_ptr, input_data + loc, copy_size * sizeof(Scalar)); + } + } +} + +template +void ReverseSequence(const TS *seq_lengths, const int seq_dim, const int batch_dim, + const RuntimeShape &input_shape, const Scalar *input_data, + const RuntimeShape &output_shape, Scalar *output_data) +{ + ruy::profiler::ScopeLabel label("ReverseSequence"); + + int outer_size = 1; + int outer_dim = std::min(batch_dim, seq_dim); + int medium_dim = std::max(batch_dim, seq_dim); + for (int i = 0; i < outer_dim; ++i) + { + outer_size *= input_shape.Dims(i); + } + + int medium_size = 1; + for (int i = outer_dim + 1; i < medium_dim; ++i) + { + medium_size *= input_shape.Dims(i); + } + + int copy_size = 1; + for (int i = medium_dim + 1; i < input_shape.DimensionsCount(); ++i) + { + copy_size *= input_shape.Dims(i); + } + + const int dims_at_outer_dim = input_shape.Dims(outer_dim); + const int dims_at_medium_dim = input_shape.Dims(medium_dim); + + Scalar *output_ptr; + if (batch_dim > seq_dim) + { + for (int i = 0; i < outer_size; ++i) + { + for (int j = 0; j < dims_at_outer_dim; ++j) + { + const int in_pos_base = (i * dims_at_outer_dim + j) * medium_size; + for (int p = 0; p < medium_size; ++p) + { + for (int q = 0; q < dims_at_medium_dim; ++q) + { + const int in_pos = ((in_pos_base + p) * dims_at_medium_dim + q) * copy_size; + const Scalar *in_ptr = input_data + in_pos; + int sl = seq_lengths[q] - 1; + if (j > sl) + { + output_ptr = output_data + in_pos; + } + else + { + const int out_pos_base = (i * dims_at_outer_dim + sl - j) * medium_size; + const int out_pos = ((out_pos_base + p) * dims_at_medium_dim + q) * copy_size; + output_ptr = output_data + out_pos; + } + memcpy(output_ptr, in_ptr, copy_size * sizeof(Scalar)); + } + } + } + } + } + else if (batch_dim < seq_dim) + { + for (int i = 0; i < outer_size; ++i) + { + for (int j = 0; j < dims_at_outer_dim; ++j) + { + const int in_pos_base = (i * dims_at_outer_dim + j) * medium_size; + int sl = seq_lengths[j] - 1; + const int out_pos_base = (i * dims_at_outer_dim + j) * medium_size; + for (int p = 0; p < medium_size; ++p) + { + for (int q = 0; q < dims_at_medium_dim; ++q) + { + const int in_pos = ((in_pos_base + p) * dims_at_medium_dim + q) * copy_size; + const Scalar *in_ptr = input_data + in_pos; + if (q > sl) + { + output_ptr = output_data + in_pos; + } + else + { + const int out_pos = ((out_pos_base + p) * dims_at_medium_dim + sl - q) * copy_size; + output_ptr = output_data + out_pos; + } + memcpy(output_ptr, in_ptr, copy_size * sizeof(Scalar)); + } + } + } + } + } +} + +template +inline void SegmentSum(const RuntimeShape &input_shape, const T *input_data, + const RuntimeShape &segment_ids_shape, const int32_t *segment_ids_data, + const RuntimeShape &output_shape, T *output_data) +{ + const int segment_flat_size = MatchingFlatSizeSkipDim(input_shape, 0, output_shape); + + memset(output_data, 0, sizeof(T) * output_shape.FlatSize()); + + for (int i = 0; i < input_shape.Dims(0); i++) + { + int output_index = segment_ids_data[i]; + for (int j = 0; j < segment_flat_size; ++j) + { + output_data[output_index * segment_flat_size + j] += input_data[i * segment_flat_size + j]; + } + } +} + +} // namespace reference_ops +} // namespace tflite + +#endif // LUCI_INTERPRETER_PAL_REFERENCE_OPS_H diff --git a/onert-micro/luci-interpreter/pal/mcu/pal.cmake b/onert-micro/luci-interpreter/pal/mcu/pal.cmake new file mode 100644 index 0000000..24e469f --- /dev/null +++ b/onert-micro/luci-interpreter/pal/mcu/pal.cmake @@ -0,0 +1,56 @@ +macro(initialize_pal) + nnas_find_package(TensorFlowSource EXACT 2.8.0 REQUIRED) + nnas_find_package(TensorFlowGEMMLowpSource EXACT 2.8.0 REQUIRED) + nnas_find_package(TensorFlowEigenSource EXACT 2.8.0 REQUIRED) + nnas_find_package(TensorFlowRuySource EXACT 2.8.0 REQUIRED) + + if (NOT TensorFlowSource_FOUND) + message(STATUS "Skipping luci-interpreter: TensorFlow not found") + return() + endif () + + if (NOT TensorFlowGEMMLowpSource_FOUND) + message(STATUS "Skipping luci-interpreter: gemmlowp not found") + return() + endif () + + if (NOT TensorFlowEigenSource_FOUND) + message(STATUS "Skipping luci-interpreter: Eigen not found") + return() + endif () + + if (NOT TensorFlowRuySource_FOUND) + message(STATUS "Skipping luci-interpreter: Ruy not found") + return() + endif () + #find_package(Threads REQUIRED) + + set(PAL_INITIALIZED TRUE) +endmacro() + +macro(add_pal_to_target TGT) + target_include_directories(${TGT} PRIVATE "${PAL}") + target_include_directories(${TGT} PRIVATE + "${TensorFlowRuySource_DIR}" + "${TensorFlowGEMMLowpSource_DIR}" + "${TensorFlowEigenSource_DIR}" + "${TensorFlowSource_DIR}") + target_include_directories(${TGT} PRIVATE ${LUCI_INTERPRETER_PAL_DIR}) + + # TODO put it back, I changed my mind. + # instead add sources with visitors in this library + set(PAL_SOURCES ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/quantization_util.cc + ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/tensor_utils.cc + ${TensorFlowSource_DIR}/tensorflow/lite/kernels/internal/reference/portable_tensor_utils.cc) + add_library(luci_interpreter_mcu_pal STATIC ${PAL_SOURCES}) + set_target_properties(luci_interpreter_mcu_pal PROPERTIES POSITION_INDEPENDENT_CODE ON) + target_include_directories(luci_interpreter_mcu_pal PRIVATE + "${TensorFlowRuySource_DIR}" + "${TensorFlowGEMMLowpSource_DIR}" + "${TensorFlowEigenSource_DIR}" + "${TensorFlowSource_DIR}" + ) + + target_link_libraries(${TGT} PRIVATE luci_interpreter_mcu_pal) + #target_link_libraries(${TGT} PRIVATE Threads::Threads luci_interpreter_mcu_pal) +endmacro() diff --git a/tests/tools/tflite_benchmark_model/.FORMATDENY b/onert-micro/luci-interpreter/requires.cmake similarity index 100% rename from tests/tools/tflite_benchmark_model/.FORMATDENY rename to onert-micro/luci-interpreter/requires.cmake diff --git a/onert-micro/luci-interpreter/src/CMakeLists.txt b/onert-micro/luci-interpreter/src/CMakeLists.txt new file mode 100644 index 0000000..75d6836 --- /dev/null +++ b/onert-micro/luci-interpreter/src/CMakeLists.txt @@ -0,0 +1,45 @@ +include("${LUCI_INTERPRETER_PAL_DIR}/pal.cmake") + +initialize_pal() + +if (NOT PAL_INITIALIZED) + message("PAL Failed to initialize, skip luci-interpreter") + return() +endif() + +message(STATUS "LUCI INTERPRETER BEGIN") + +set(LUCI_INTERPRETER_BINARY "luci_interpreter_micro${LUCI_INTERPRETER_SUFFIX}") +set(LUCI_INTERPRETER_MEMORY_MANAGER "luci_interpreter_micro_memory_manager${LUCI_INTERPRETER_SUFFIX}") +set(LUCI_INTERPRETER_CORE "luci_interpreter_core_micro${LUCI_INTERPRETER_SUFFIX}") +set(LUCI_INTERPRETER_KERNELS "luci_interpreter_kernels_micro${LUCI_INTERPRETER_SUFFIX}") +set(LUCI_INTERPRETER_LOADER "luci_interpreter_loader_micro${LUCI_INTERPRETER_SUFFIX}") +set(LUCI_INTERPRETER_IMPORT "luci_interpreter_import_micro${LUCI_INTERPRETER_SUFFIX}") + +add_subdirectory(memory_managers) +message(STATUS "LUCI INTERPRETER MEMORY MANAGER") +add_subdirectory(core) +message(STATUS "LUCI INTERPRETER CORE") +add_subdirectory(kernels) +message(STATUS "LUCI INTERPRETER KERNELS") +add_subdirectory(loader) +message(STATUS "LUCI INTERPRETER LOADER") + +target_link_libraries(${LUCI_INTERPRETER_CORE} PUBLIC ${LUCI_INTERPRETER_KERNELS}) +target_include_directories(${LUCI_INTERPRETER_CORE} PUBLIC ${LUCI_INTERPRETER_KERNELS}) + +message(STATUS "LUCI INTERPTER INITALIZED") + +set(SOURCES + "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/Interpreter.h" Interpreter.cpp) + +add_library(${LUCI_INTERPRETER_BINARY} STATIC ${SOURCES}) + +target_include_directories(${LUCI_INTERPRETER_BINARY} PUBLIC "${LUCI_INTERPRETER_INCLUDE_DIR}") +target_include_directories(${LUCI_INTERPRETER_BINARY} PRIVATE "${LUCI_INTERPRETER_SOURCE_DIR}") +target_link_libraries(${LUCI_INTERPRETER_BINARY} + PUBLIC ${LUCI_INTERPRETER_MEMORY_MANAGER} ${LUCI_INTERPRETER_LOADER} ${LUCI_INTERPRETER_CORE}) + +install(TARGETS ${LUCI_INTERPRETER_BINARY} DESTINATION lib) +install(DIRECTORY include/ DESTINATION include + FILES_MATCHING PATTERN "*.h") diff --git a/onert-micro/luci-interpreter/src/Interpreter.cpp b/onert-micro/luci-interpreter/src/Interpreter.cpp new file mode 100644 index 0000000..dccef82 --- /dev/null +++ b/onert-micro/luci-interpreter/src/Interpreter.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci_interpreter/Interpreter.h" + +#include "loader/ModuleLoader.h" + +namespace luci_interpreter +{ + +#ifdef USE_STATIC_ALLOC +// Construct static interpreter with configurations +Interpreter::Interpreter(const char *model_data_raw, const InterpreterConfigure &configuration) +{ + _runtime_module = std::make_unique(); + + _memory_manager = StaticMemoryManager(configuration._input_buf_size, configuration._temp_buf_size, + configuration._output_buf_size) + + // Note: + // configuration._input_buf_size, configuration._temp_buf_size, configuration._output_buf_size + // will be removed and will be read from circle file + if (configuration.isStaticManager()) + { + _memory_manager = std::make_unique( + configuration._input_buf_size, configuration._temp_buf_size, configuration._output_buf_size); + } + else { _memory_manager = std::make_unique(); } + + _memory_manager->is_allocate_input(configuration.getAllocateInputValue()); + + ModuleLoader loader(); + ModuleLoader::load(_runtime_module.get(), _memory_manager.get(), + /* is_static_allocations */ configuration.isStaticManager(), model_data_raw); + + ModuleLoader loader(_runtime_module.get(), _memory_manager.get()); + loader.load(configuration.isStaticManager(), model_data_raw); +} +#else + +// Construct default interpreter with dynamic allocations and with input allocations +Interpreter::Interpreter(const char *model_data_raw) +{ + ModuleLoader::load(&_runtime_module, &_memory_manager, model_data_raw); +} + +#endif // USE_STATIC_ALLOC + +Interpreter::~Interpreter() = default; + +void Interpreter::interpret() { _runtime_module.execute(); } + +int32_t Interpreter::getInputDataSizeByIndex(int32_t input_tensor_index) +{ + auto *runtime_graph = _runtime_module.getMainGraph(); + + return runtime_graph->getInputDataSizeByIndex(input_tensor_index); +} + +int32_t Interpreter::getOutputDataSizeByIndex(int32_t output_tensor_index) +{ + auto *runtime_graph = _runtime_module.getMainGraph(); + + return runtime_graph->getOutputDataSizeByIndex(output_tensor_index); +} + +void Interpreter::allocateAndWriteInputTensor(int32_t input_tensor_index, const void *data, + size_t data_size) +{ + assert(data_size > 0); + assert(data != nullptr); + assert(input_tensor_index >= 0); + auto *runtime_graph = _runtime_module.getMainGraph(); + auto tensor_data = runtime_graph->configureGraphInput(input_tensor_index); + + std::memcpy(tensor_data, data, data_size); +} + +uint8_t *Interpreter::allocateInputTensor(int32_t input_tensor_index) +{ + assert(input_tensor_index >= 0); + + auto *runtime_graph = _runtime_module.getMainGraph(); + + return runtime_graph->configureGraphInput(input_tensor_index); +} + +uint8_t *Interpreter::readOutputTensor(int32_t output_tensor_index) +{ + auto *runtime_graph = _runtime_module.getMainGraph(); + + return runtime_graph->getOutputDataByIndex(output_tensor_index); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/core/CMakeLists.txt b/onert-micro/luci-interpreter/src/core/CMakeLists.txt new file mode 100644 index 0000000..48d92aa --- /dev/null +++ b/onert-micro/luci-interpreter/src/core/CMakeLists.txt @@ -0,0 +1,20 @@ +set(SOURCES + "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/core/DataType.h" + "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/core/Tensor.h" + RuntimeGraph.h + RuntimeGraph.cpp + RuntimeModule.h) + +add_library(${LUCI_INTERPRETER_CORE} STATIC ${SOURCES}) +if (NOT NNCC_LIBRARY_NO_PIC) + set_target_properties(${LUCI_INTERPRETER_CORE} PROPERTIES POSITION_INDEPENDENT_CODE ON) +endif(NOT NNCC_LIBRARY_NO_PIC) + +add_subdirectory(reader) + +target_link_libraries(${LUCI_INTERPRETER_CORE} PUBLIC "luci_micro_circle_reader${READER_SUFFIX}") +target_link_libraries(${LUCI_INTERPRETER_CORE} PUBLIC luci_micro_circle_schema) +target_link_libraries(${LUCI_INTERPRETER_CORE} PUBLIC ${LUCI_INTERPRETER_MEMORY_MANAGER}) + +target_include_directories(${LUCI_INTERPRETER_CORE} PUBLIC "${LUCI_INTERPRETER_INCLUDE_DIR}") +target_include_directories(${LUCI_INTERPRETER_CORE} PUBLIC "${LUCI_INTERPRETER_SOURCE_DIR}") diff --git a/onert-micro/luci-interpreter/src/core/RuntimeGraph.cpp b/onert-micro/luci-interpreter/src/core/RuntimeGraph.cpp new file mode 100644 index 0000000..41447f9 --- /dev/null +++ b/onert-micro/luci-interpreter/src/core/RuntimeGraph.cpp @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "core/RuntimeGraph.h" +#include "kernels/KernelBuilder.h" + +#include +#include + +namespace luci_interpreter +{ + +// IBaseRuntimeGraph +RuntimeGraph::RuntimeGraph(SimpleMemoryManager *memory_manager, CircleReader *circle_reader) + : _memory_manager(memory_manager), + _tensor_to_data(std::unordered_map{}), + _reader(circle_reader), _inplace_op_indexes(std::unordered_set{}) +{ +} + +RuntimeGraph::~RuntimeGraph() +{ + for (auto &idx_to_tensor : _tensor_to_data) + { + auto *data = idx_to_tensor.second; + + _memory_manager->release_memory(data); + } +} + +// TODO: modify this +void RuntimeGraph::buildAllocDeallocPlan() +{ + invalidate(); + using Lifetime = std::pair; + std::map lifetimes; + const size_t num_kernels = _reader->operators().size(); + + for (const auto input_ind : _reader->inputs()) + { + const auto raw_tensor = _reader->tensors()[input_ind]; + + assert(lifetimes.count(raw_tensor) == 0); + lifetimes[raw_tensor] = Lifetime(-1, 0); + } + + for (int32_t index = 0; index < num_kernels; ++index) + { + const auto kernel = _reader->operators().at(index); + assert(kernel != nullptr); + + for (int32_t j = 0; j < kernel->inputs()->size(); ++j) + { + const auto input_index = kernel->inputs()->operator[](j); + + if (input_index == -1) + continue; + + const auto raw_tensor = _reader->tensors()[input_index]; + + // Pass constant tensors + auto const &buffer = wrap(_reader->buffers()[raw_tensor->buffer()]->data()); + if (not buffer.empty()) + { + // unknown shape tensor and scalar tensor + continue; + } + + if (lifetimes.count(raw_tensor) > 0) + { + if (_inplace_op_indexes.find(index) != _inplace_op_indexes.end()) + lifetimes.at(raw_tensor).second = -1; + else + lifetimes.at(raw_tensor).second = index; + } + } + + for (int32_t j = 0; j < kernel->outputs()->size(); ++j) + { + const auto output_index = kernel->outputs()->operator[](j); + const auto raw_tensor = _reader->tensors()[output_index]; + + assert(lifetimes.count(raw_tensor) == 0); + if (_inplace_op_indexes.find(index) != _inplace_op_indexes.end()) + lifetimes[raw_tensor] = Lifetime(-1, index); + else + lifetimes[raw_tensor] = Lifetime(index, index); + } + } + + for (const auto output_ind : _reader->outputs()) + { + const auto raw_tensor = _reader->tensors()[output_ind]; + + if (lifetimes.count(raw_tensor) > 0) + lifetimes.at(raw_tensor).second = num_kernels; + } + + _alloc_plan.assign(num_kernels, std::vector()); + _dealloc_plan.assign(num_kernels + 1, std::vector()); + for (const auto &item : lifetimes) + { + if (item.second.first != -1) + _alloc_plan[item.second.first].push_back(item.first); + if (item.second.second != -1) + _dealloc_plan[item.second.second].push_back(item.first); + } + _is_valid = true; +} + +void RuntimeGraph::allocate(size_t kernel_index) +{ + assert(_is_valid && kernel_index < _alloc_plan.size()); + for (const circle::Tensor *tensor : _alloc_plan[kernel_index]) + { + if (_tensor_to_data.find(tensor) != _tensor_to_data.end()) + { + auto *data = _tensor_to_data.at(tensor); + _memory_manager->release_memory(data); + } + auto *data = _memory_manager->allocate_memory(tensor); + _tensor_to_data[tensor] = data; + } +} + +void RuntimeGraph::deallocate(size_t kernel_index) +{ + assert(_is_valid && kernel_index < _dealloc_plan.size()); + for (const circle::Tensor *tensor : _dealloc_plan[kernel_index]) + { + const auto it = _tensor_to_data.find(tensor); + assert(it != _tensor_to_data.end()); + + auto *data = _tensor_to_data.at(tensor); + _memory_manager->release_memory(data); + + _tensor_to_data.erase(it); + } +} + +void RuntimeGraph::resetOutputTensorsData() +{ + for (int i = 0; i < _reader->outputs().size(); ++i) + { + const auto tensor_index = _reader->outputs()[i]; + assert(tensor_index != -1); + const auto tensor = _reader->tensors()[tensor_index]; + assert(tensor != nullptr); + + auto tensor_it = _tensor_to_data.find(tensor); + if (tensor_it != _tensor_to_data.end()) + { + auto *data = _tensor_to_data.at(tensor); + _memory_manager->release_memory(data); + _tensor_to_data.erase(tensor_it); + } + } +} + +uint8_t *RuntimeGraph::configureGraphInput(int32_t input_index) +{ + resetOutputTensorsData(); + + const auto tensor_index = _reader->inputs()[input_index]; + assert(tensor_index != -1); + const auto tensor = _reader->tensors()[tensor_index]; + assert(tensor != nullptr); + + if (_tensor_to_data.find(tensor) != _tensor_to_data.end()) + { + auto *data = _tensor_to_data.at(tensor); + _memory_manager->release_memory(data); + } + + auto *data = _memory_manager->allocate_memory(tensor); + _tensor_to_data[tensor] = data; + + return data; +} + +// To save data +// TODO maybe remove it +void RuntimeGraph::configureGraphInput(int32_t input_index, uint8_t *data) +{ + resetOutputTensorsData(); + + const auto tensor_index = _reader->inputs()[input_index]; + assert(tensor_index != -1); + const auto tensor = _reader->tensors()[tensor_index]; + assert(tensor != nullptr); + + if (_tensor_to_data.find(tensor) != _tensor_to_data.end()) + { + auto *data_prev = _tensor_to_data.at(tensor); + _memory_manager->release_memory(data_prev); + } + _tensor_to_data[tensor] = data; +} + +int32_t RuntimeGraph::getInputDataSizeByIndex(int32_t input_index) +{ + const auto tensor_index = _reader->inputs()[input_index]; + assert(tensor_index != -1); + const auto tensor = _reader->tensors()[tensor_index]; + assert(tensor != nullptr); + + return Tensor::num_elements(tensor) * size(Tensor::element_type(tensor)); +} + +int32_t RuntimeGraph::getOutputDataSizeByIndex(int32_t output_index) +{ + const auto tensor_index = _reader->outputs()[output_index]; + assert(tensor_index != -1); + const auto tensor = _reader->tensors()[tensor_index]; + assert(tensor != nullptr); + + return Tensor::num_elements(tensor) * size(Tensor::element_type(tensor)); +} + +uint8_t *RuntimeGraph::getOutputDataByIndex(int32_t output_index) +{ + const auto tensor_index = _reader->outputs()[output_index]; + assert(tensor_index != -1); + const auto tensor = _reader->tensors()[tensor_index]; + assert(tensor != nullptr); + + assert(_tensor_to_data.find(tensor) != _tensor_to_data.end()); + + return _tensor_to_data[tensor]; +} + +uint8_t *RuntimeGraph::getDataByTensor(const circle::Tensor *raw_tensor) +{ + if (raw_tensor == nullptr) + return nullptr; + + if (_tensor_to_data.find(raw_tensor) == _tensor_to_data.end()) + { + return nullptr; + } + + return _tensor_to_data.at(raw_tensor); +} + +void RuntimeGraph::makeInplaceOperation(const circle::Tensor *src_tensor, + const circle::Tensor *dst_tensor) +{ + if (src_tensor == nullptr or dst_tensor == nullptr) + return; + + auto src_it = _tensor_to_data.find(src_tensor); + + assert(src_it != _tensor_to_data.end() && "Failed makeInplaceOperation"); + + auto *data = _tensor_to_data[src_tensor]; + + _tensor_to_data.erase(src_it); + + assert(_tensor_to_data.find(dst_tensor) == _tensor_to_data.end() && + "Failed makeInplaceOperation"); + _tensor_to_data[dst_tensor] = data; +} + +uint8_t *RuntimeGraph::getConstDataByTensor(const circle::Tensor *raw_tensor) +{ + if (raw_tensor == nullptr) + return nullptr; + + auto const &buffer = wrap(_reader->buffers()[raw_tensor->buffer()]->data()); + + return const_cast(buffer.data()); +} + +const circle::Tensor *RuntimeGraph::getCircleTensorByIndex(int32_t index) +{ + if (index < 0) + return nullptr; + + const auto raw_tensor = _reader->tensors()[index]; + + return raw_tensor; +} + +void RuntimeGraph::configure() +{ + KernelConfigureRegistry kernel_configure; + + for (uint32_t i = 0; i < _reader->operators().size(); ++i) + { + const auto op = _reader->operators().at(i); + assert(op != nullptr); + + const auto opcode = _reader->builtin_code(op); + + kernel_configure.configure_kernel(op, opcode, this); + } + + if (not _is_valid) + buildAllocDeallocPlan(); + + _is_valid = true; +} + +void RuntimeGraph::execute() +{ + if (not _is_valid) + configure(); + + KernelExecuteRegistry kernel_executor; + + for (uint32_t i = 0; i < _reader->operators().size(); ++i) + { + const auto op = _reader->operators().at(i); + assert(op != nullptr); + + const auto opcode = _reader->builtin_code(op); + + allocate(i); + + bool is_inplace = false; + + if (_inplace_op_indexes.find(i) != _inplace_op_indexes.end()) + is_inplace = true; + + kernel_executor.execute_kernel(op, opcode, this, is_inplace); + + deallocate(i); + } +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/core/RuntimeGraph.h b/onert-micro/luci-interpreter/src/core/RuntimeGraph.h new file mode 100644 index 0000000..b64e8dc --- /dev/null +++ b/onert-micro/luci-interpreter/src/core/RuntimeGraph.h @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_CORE_RUNTIMEGRAPH_H +#define LUCI_INTERPRETER_CORE_RUNTIMEGRAPH_H + +#include "luci_interpreter/core/Tensor.h" +#ifdef USE_STATIC_ALLOC +#include "memory_managers/StaticMemoryManager.h" +#else +#include "memory_managers/SimpleMemoryManager.h" +#endif // USE_STATIC_ALLOC + +#include "luci_interpreter/core/reader/CircleMicroReader.h" + +#include +#include +#include +#include + +namespace luci_interpreter +{ + +class RuntimeModule; + +#ifdef USE_STATIC_ALLOC +// TODO: Enable it +#if 0 +class StaticRuntimeGraph final : public IBaseRuntimeGraph +{ +public: + explicit StaticRuntimeGraph(IMemoryManager *memory_manager, CircleReader *circle_reader); + ~StaticRuntimeGraph() final; + + void configureGraphInputs() final; + void execute() final; + void configure() final; + + void configure_kernels() final; +}; +#endif +#else + +class RuntimeGraph +{ +public: + explicit RuntimeGraph(SimpleMemoryManager *memory_manager, CircleReader *circle_reader); + ~RuntimeGraph(); + + Tensor *addTensor(const circle::Tensor *raw_tensor, std::unique_ptr &&tensor); + + const circle::Tensor *getCircleTensorByIndex(int32_t index); + + void makeInplaceOperation(const circle::Tensor *src_tensor, const circle::Tensor *dst_tensor); + + uint8_t *getDataByTensor(const circle::Tensor *raw_tensor); + uint8_t *getConstDataByTensor(const circle::Tensor *raw_tensor); + + uint8_t *configureGraphInput(int32_t input_index); + void configureGraphInput(int32_t input_index, uint8_t *data); + + int32_t getInputDataSizeByIndex(int32_t input_index); + int32_t getOutputDataSizeByIndex(int32_t output_index); + + uint8_t *getOutputDataByIndex(int32_t output_index); + + void addInplaceOpIndex(uint32_t index) { _inplace_op_indexes.insert(index); } + + void execute(); + void configure(); + + void invalidate() { _is_valid = false; } + bool isValid() const { return _is_valid; } + +private: + void buildAllocDeallocPlan(); + void allocate(size_t kernel_index); + void deallocate(size_t kernel_index); + + void resetOutputTensorsData(); + +private: + SimpleMemoryManager *_memory_manager; + CircleReader *_reader; + + std::unordered_map _tensor_to_data; + std::unordered_set _inplace_op_indexes; + + bool _is_valid = false; + + // Tensors that are not used anymore after given op + std::vector> _alloc_plan; + std::vector> _dealloc_plan; +}; + +#endif // USE_STATIC_ALLOC + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_CORE_RUNTIMEGRAPH_H diff --git a/onert-micro/luci-interpreter/src/core/RuntimeModule.h b/onert-micro/luci-interpreter/src/core/RuntimeModule.h new file mode 100644 index 0000000..5dbe5a9 --- /dev/null +++ b/onert-micro/luci-interpreter/src/core/RuntimeModule.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_CORE_RUNTIMEMODULE_H +#define LUCI_INTERPRETER_CORE_RUNTIMEMODULE_H + +#include "core/RuntimeGraph.h" +#include "luci_interpreter/core/reader/CircleMicroReader.h" + +#include +#include + +namespace luci_interpreter +{ + +#ifdef USE_STATIC_ALLOC +using BaseRuntimeGraph = StaticRuntimeGraph; +using MemoryManager = StaticMemoryManager; +#else +using BaseRuntimeGraph = RuntimeGraph; +using MemoryManager = SimpleMemoryManager; +#endif // USE_STATIC_ALLOC + +class RuntimeModule +{ +public: + RuntimeModule() = default; + + void addGraph(MemoryManager *memory_manager) + { + _graphs.emplace_back(memory_manager, &_circle_reader); + } + + BaseRuntimeGraph *getRuntimeGraphAt(uint32_t pos) { return &_graphs.at(pos); } + + void execute() { getMainGraph()->execute(); } + + CircleReader &getCircleReader() { return _circle_reader; } + + BaseRuntimeGraph *getMainGraph() const { return const_cast(&_graphs[0]); } + +private: + std::vector _graphs; + + CircleReader _circle_reader; +}; + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_CORE_RUNTIMEMODULE_H diff --git a/onert-micro/luci-interpreter/src/core/reader/CMakeLists.txt b/onert-micro/luci-interpreter/src/core/reader/CMakeLists.txt new file mode 100644 index 0000000..0de1731 --- /dev/null +++ b/onert-micro/luci-interpreter/src/core/reader/CMakeLists.txt @@ -0,0 +1,12 @@ +set(MICRO_READER_SOURCE + "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/core/reader/CircleMicroReader.h" + "${LUCI_INTERPRETER_INCLUDE_DIR}/luci_interpreter/core/reader/CircleMicroReaderHelper.h" + "CircleMicroReader.cpp" + "CircleMicroReaderHelper.cpp" + ) + +add_library("luci_micro_circle_reader${READER_SUFFIX}" STATIC ${MICRO_READER_SOURCE}) +target_link_libraries("luci_micro_circle_reader${READER_SUFFIX}" PUBLIC luci_micro_circle_schema) + +target_include_directories("luci_micro_circle_reader${READER_SUFFIX}" PUBLIC "${GENERATED_INCLUDE_DIR}") +target_include_directories("luci_micro_circle_reader${READER_SUFFIX}" PUBLIC "${LUCI_INTERPRETER_INCLUDE_DIR}") diff --git a/onert-micro/luci-interpreter/src/core/reader/CircleMicroReader.cpp b/onert-micro/luci-interpreter/src/core/reader/CircleMicroReader.cpp new file mode 100644 index 0000000..332e464 --- /dev/null +++ b/onert-micro/luci-interpreter/src/core/reader/CircleMicroReader.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci_interpreter/core/reader/CircleMicroReader.h" +#include "luci_interpreter/core/reader/CircleMicroReaderHelper.h" + +#include + +namespace luci_interpreter +{ + +// TODO check can we remove it +DataType luci_datatype(const circle::TensorType type) +{ + switch (type) + { + case circle::TensorType_FLOAT32: + return DataType::FLOAT32; + case circle::TensorType_FLOAT16: + return DataType::FLOAT16; + case circle::TensorType_INT32: + return DataType::S32; + case circle::TensorType_UINT8: + return DataType::U8; + case circle::TensorType_INT64: + return DataType::S64; + case circle::TensorType_BOOL: + return DataType::BOOL; + case circle::TensorType_INT16: + return DataType::S16; + case circle::TensorType_COMPLEX64: + break; + case circle::TensorType_INT8: + return DataType::S8; + default: + break; + } + assert(false); + return DataType::Unknown; +} + +FusedActFunc luci_actfunc(const circle::ActivationFunctionType type) +{ + switch (type) + { + case circle::ActivationFunctionType::ActivationFunctionType_NONE: + return FusedActFunc::NONE; + case circle::ActivationFunctionType::ActivationFunctionType_RELU: + return FusedActFunc::RELU; + case circle::ActivationFunctionType::ActivationFunctionType_RELU_N1_TO_1: + return FusedActFunc::RELU_N1_TO_1; + case circle::ActivationFunctionType::ActivationFunctionType_RELU6: + return FusedActFunc::RELU6; + case circle::ActivationFunctionType::ActivationFunctionType_TANH: + return FusedActFunc::TANH; + case circle::ActivationFunctionType::ActivationFunctionType_SIGN_BIT: + return FusedActFunc::SIGN_BIT; + default: + break; + } + assert(false); + return FusedActFunc::UNDEFINED; +} + +Padding luci_padding(const circle::Padding padding) +{ + switch (padding) + { + case circle::Padding::Padding_SAME: + return Padding::SAME; + case circle::Padding::Padding_VALID: + return Padding::VALID; + } + assert(false); + return Padding::UNDEFINED; +} + +MirrorPadMode luci_mirrorpad_mode(const circle::MirrorPadMode mode) +{ + switch (mode) + { + case circle::MirrorPadMode::MirrorPadMode_REFLECT: + return MirrorPadMode::REFLECT; + case circle::MirrorPadMode::MirrorPadMode_SYMMETRIC: + return MirrorPadMode::SYMMETRIC; + } + assert(false); + return MirrorPadMode::UNDEFINED; +} + +circle::BuiltinOperator CircleReader::builtin_code(const circle::Operator *op) const +{ + assert(op != nullptr); + + const auto op_codes = opcodes(); + uint32_t index = op->opcode_index(); + assert(index < op_codes.size()); + const auto opcode = op_codes[index]; + assert(opcode != nullptr); + + return circle::builtin_code_neutral(opcode); +} + +bool CircleReader::parse(const circle::Model *model) +{ + assert(model != nullptr); + + // for direct pointer access + _model = model; + + return true; +} + +bool CircleReader::select_subgraph(uint32_t sgindex) +{ + if (num_subgraph() <= sgindex) + { + assert(false); + return false; + } + + // for direct pointer access + auto subgraphs = _model->subgraphs(); + assert(subgraphs != nullptr); + + _current_subgraph = subgraphs->Get(sgindex); + assert(_current_subgraph != nullptr); + + return true; +} + +template +VectorWrapper::VectorWrapper(const flatbuffers::Vector *ptr) : _vector(ptr) +{ + // Do nothing +} + +template uint32_t VectorWrapper::size() const +{ + return null() ? 0 : _vector->size(); +} + +template const T *VectorWrapper::data() const +{ + return null() ? nullptr : _vector->data(); +} + +template typename VectorWrapper::iterator VectorWrapper::begin() const +{ + return null() ? iterator(nullptr, 0) : _vector->begin(); +} + +template typename VectorWrapper::iterator VectorWrapper::end() const +{ + return null() ? begin() : _vector->end(); +} + +template typename VectorWrapper::value_type VectorWrapper::at(uint32_t i) const +{ + if (i >= size()) + { + // TODO find better error message + assert(false && "Access to prohibited vector element"); + } + + return _vector->Get(i); +} + +template +typename VectorWrapper::value_type VectorWrapper::operator[](uint32_t i) const +{ + return at(i); +} + +template bool VectorWrapper::null() const { return _vector == nullptr; } +template bool VectorWrapper::empty() const { return size() == 0; } + +#define REGISTER_WRAPPER(T) template class VectorWrapper +REGISTER_WRAPPER(flatbuffers::Offset); +REGISTER_WRAPPER(flatbuffers::Offset); +REGISTER_WRAPPER(flatbuffers::Offset); +REGISTER_WRAPPER(flatbuffers::Offset); +REGISTER_WRAPPER(flatbuffers::Offset); +REGISTER_WRAPPER(flatbuffers::Offset); +REGISTER_WRAPPER(int32_t); +REGISTER_WRAPPER(uint8_t); +#undef REGISTER_WRAPPER + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/core/reader/CircleMicroReaderHelper.cpp b/onert-micro/luci-interpreter/src/core/reader/CircleMicroReaderHelper.cpp new file mode 100644 index 0000000..8cc0058 --- /dev/null +++ b/onert-micro/luci-interpreter/src/core/reader/CircleMicroReaderHelper.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "luci_interpreter/core/reader/CircleMicroReaderHelper.h" + +namespace circle +{ + +::circle::BuiltinOperator builtin_code_neutral(const ::circle::OperatorCode *opcode) +{ + assert(opcode != nullptr); + if (opcode->deprecated_builtin_code() == 127) + { + assert(opcode->builtin_code() >= 127); + return opcode->builtin_code(); + } + // There was no 255(-1) value in v0.3 + assert(opcode->deprecated_builtin_code() != -1); + return static_cast<::circle::BuiltinOperator>(opcode->deprecated_builtin_code()); +} + +bool is_valid(const ::circle::OperatorCode *opcode) +{ + ::circle::BuiltinOperator code = opcode->builtin_code(); + return (::circle::BuiltinOperator_MIN <= code && code <= ::circle::BuiltinOperator_MAX); +} + +bool is_custom(const ::circle::OperatorCode *opcode) +{ + ::circle::BuiltinOperator code = opcode->builtin_code(); + return (code == ::circle::BuiltinOperator_CUSTOM); +} + +const char *tensor_type(const ::circle::Tensor *tensor) +{ + return ::circle::EnumNameTensorType(tensor->type()); +} + +} // namespace circle diff --git a/onert-micro/luci-interpreter/src/kernels/Add.cpp b/onert-micro/luci-interpreter/src/kernels/Add.cpp new file mode 100644 index 0000000..07b6c20 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Add.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Add.h" + +#include "kernels/BinaryOpCommon.h" +#include "kernels/Utils.h" + +#include +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +Add::Add(const Tensor *input1, const Tensor *input2, Tensor *output, const AddParams ¶ms) + : KernelWithParams({input1, input2}, {output}, params) +{ +} + +void Add::configure() +{ + LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); + LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()); + if (input1()->element_type() == DataType::S16) + { + LUCI_INTERPRETER_CHECK(input1()->zero_points().size() == 1 && + input2()->zero_points().size() == 1); + LUCI_INTERPRETER_CHECK(input1()->zero_point() == 0 && input2()->zero_point() == 0 && + output()->zero_point() == 0); + } + + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); +} + +void Add::execute() const +{ + switch (input1()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::S64: + evalInteger(); + break; + case DataType::S32: + evalInteger(); + break; + case DataType::U8: + evalQuantized(); + break; + case DataType::S16: + evalQuantizedS16(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Add::evalFloat() const +{ + tflite::ArithmeticParams params{}; + fillArithmeticActivationRange(params, _params.activation); + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + tflite::reference_ops::BroadcastAdd4DSlow( + params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), + getTensorData(input2()), getTensorShape(output()), getTensorData(output())); + } + else + { + tflite::reference_ops::Add(params, getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +template void Add::evalInteger() const +{ + tflite::ArithmeticParams params{}; + fillArithmeticActivationRange(params, _params.activation); + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + tflite::reference_ops::BroadcastAdd4DSlow( + params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), + getTensorData(input2()), getTensorShape(output()), getTensorData(output())); + } + else + { + tflite::reference_ops::Add(params, getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +void Add::evalQuantized() const +{ + const auto input1_scale = static_cast(input1()->scale()); + const auto input2_scale = static_cast(input2()->scale()); + const auto output_scale = static_cast(output()->scale()); + + const int left_shift = 20; + const double twice_max_input_scale = 2 * std::max(input1_scale, input2_scale); + const double real_input1_multiplier = input1_scale / twice_max_input_scale; + const double real_input2_multiplier = input2_scale / twice_max_input_scale; + const double real_output_multiplier = twice_max_input_scale / ((1 << left_shift) * output_scale); + + int32_t input1_multiplier{}, input2_multiplier{}, output_multiplier{}; + int input1_shift{}, input2_shift{}, output_shift{}; + quantizeMultiplierSmallerThanOneExp(real_input1_multiplier, &input1_multiplier, &input1_shift); + quantizeMultiplierSmallerThanOneExp(real_input2_multiplier, &input2_multiplier, &input2_shift); + quantizeMultiplierSmallerThanOneExp(real_output_multiplier, &output_multiplier, &output_shift); + + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + + tflite::ArithmeticParams params{}; + params.left_shift = left_shift; + // The kernel expects inputs' zero points to be negated. + params.input1_offset = -input1()->zero_point(); // Note the '-'. + params.input1_multiplier = input1_multiplier; + params.input1_shift = input1_shift; + params.input2_offset = -input2()->zero_point(); // Note the '-'. + params.input2_multiplier = input2_multiplier; + params.input2_shift = input2_shift; + params.output_offset = output()->zero_point(); + params.output_multiplier = output_multiplier; + params.output_shift = output_shift; + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + tflite::reference_ops::BroadcastAdd4DSlow( + params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), + getTensorData(input2()), getTensorShape(output()), getTensorData(output())); + } + else + { + tflite::reference_ops::Add(params, getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +void Add::evalQuantizedS16() const +{ + const auto input1_scale = static_cast(input1()->scale()); + const auto input2_scale = static_cast(input2()->scale()); + const auto output_scale = static_cast(output()->scale()); + + constexpr int left_shift = 12; + const double twice_max_input_scale = 2 * std::max(input1_scale, input2_scale); + const double real_input1_multiplier = input1_scale / twice_max_input_scale; + const double real_input2_multiplier = input2_scale / twice_max_input_scale; + const double real_output_multiplier = twice_max_input_scale / ((1 << left_shift) * output_scale); + + int32_t input1_multiplier{}, input2_multiplier{}, output_multiplier{}; + int input1_shift{}, input2_shift{}, output_shift{}; + quantizeMultiplierSmallerThanOneExp(real_input1_multiplier, &input1_multiplier, &input1_shift); + quantizeMultiplierSmallerThanOneExp(real_input2_multiplier, &input2_multiplier, &input2_shift); + quantizeMultiplierSmallerThanOneExp(real_output_multiplier, &output_multiplier, &output_shift); + + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + + auto fn = [input1_multiplier, input1_shift, // + input2_multiplier, input2_shift, // + output_multiplier, output_shift, // + activation_min, activation_max](int16_t input1_val, int16_t input2_val) { + const int32_t shifted_input1_val = static_cast(input1_val) << left_shift; + const int32_t shifted_input2_val = static_cast(input2_val) << left_shift; + const int32_t scaled_input1_val = tflite::MultiplyByQuantizedMultiplierSmallerThanOneExp( + shifted_input1_val, input1_multiplier, input1_shift); + const int32_t scaled_input2_val = tflite::MultiplyByQuantizedMultiplierSmallerThanOneExp( + shifted_input2_val, input2_multiplier, input2_shift); + const int32_t raw_sum = scaled_input1_val + scaled_input2_val; + const int32_t raw_output = tflite::MultiplyByQuantizedMultiplierSmallerThanOneExp( + raw_sum, output_multiplier, output_shift); + const int32_t clamped_output = std::min(activation_max, std::max(activation_min, raw_output)); + return static_cast(clamped_output); + }; + + BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output()), fn); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Add.h b/onert-micro/luci-interpreter/src/kernels/Add.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Add.h rename to onert-micro/luci-interpreter/src/kernels/Add.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Add.test.cpp b/onert-micro/luci-interpreter/src/kernels/Add.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Add.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Add.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/ArgMax.cpp b/onert-micro/luci-interpreter/src/kernels/ArgMax.cpp new file mode 100644 index 0000000..f51476d --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/ArgMax.cpp @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/ArgMax.h" +#include "kernels/Utils.h" +#include "PALArgMax.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +ArgMax::ArgMax(const Tensor *input, const Tensor *axis, Tensor *output, const ArgMaxParams ¶ms) + : KernelWithParams({input, axis}, {output}, params) +{ +} + +void ArgMax::configure() +{ + assert(axis()->element_type() == DataType::S32 || axis()->element_type() == DataType::S64); + assert(input()->shape().num_dims() >= 1); + const Shape &input_shape = input()->shape(); + const int num_dims = input_shape.num_dims(); + Shape output_shape(num_dims - 1); + + // If axis value is negative, then update by adding input_shape's num_dims. + // If updated value also negative, then assert. + assert(axis()->shape().num_elements() == 1); + int axis_value = getTensorData(axis())[0]; + if (axis_value < 0) + axis_value = axis_value + num_dims; + assert(axis_value >= 0); + + int j = 0; + for (int i = 0; i < num_dims; i++) + { + if (i == axis_value) + continue; + output_shape.dim(j++) = input_shape.dim(i); + } + + assert(output()->element_type() == _params.output_type); + + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void ArgMax::execute() const +{ + +#define TF_LITE_ARG_MAX(data_type, axis_type, output_type) \ + luci_interpreter_pal::ArgMinMax(getTensorShape(input()), getTensorData(input()), \ + getTensorData(axis()), getTensorShape(output()), \ + getTensorData(output()), std::greater()) + if (axis()->element_type() == DataType::S32) + { + switch (_params.output_type) + { + case DataType::S32: + switch (input()->element_type()) + { + case DataType::FLOAT32: + TF_LITE_ARG_MAX(float, int32_t, int32_t); + break; + case DataType::U8: + TF_LITE_ARG_MAX(uint8_t, int32_t, int32_t); + break; + default: + assert(false && "Unsupported input type."); + } + break; + case DataType::S64: + switch (input()->element_type()) + { + case DataType::FLOAT32: + TF_LITE_ARG_MAX(float, int32_t, int64_t); + break; + case DataType::U8: + TF_LITE_ARG_MAX(uint8_t, int32_t, int64_t); + break; + default: + assert(false && "Unsupported input type."); + } + break; + default: + assert(false && "Unsupported output type."); + } + } + else + { + switch (_params.output_type) + { + case DataType::S32: + switch (input()->element_type()) + { + case DataType::FLOAT32: + TF_LITE_ARG_MAX(float, int64_t, int32_t); + break; + case DataType::U8: + TF_LITE_ARG_MAX(uint8_t, int64_t, int32_t); + break; + default: + assert(false && "Unsupported input type."); + } + break; + case DataType::S64: + switch (input()->element_type()) + { + case DataType::FLOAT32: + TF_LITE_ARG_MAX(float, int64_t, int64_t); + break; + case DataType::U8: + TF_LITE_ARG_MAX(uint8_t, int64_t, int64_t); + break; + default: + assert(false && "Unsupported input type."); + } + break; + default: + assert(false && "Unsupported output type."); + } + } +#undef TF_LITE_ARG_MAX +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ArgMax.h b/onert-micro/luci-interpreter/src/kernels/ArgMax.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/ArgMax.h rename to onert-micro/luci-interpreter/src/kernels/ArgMax.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ArgMax.test.cpp b/onert-micro/luci-interpreter/src/kernels/ArgMax.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/ArgMax.test.cpp rename to onert-micro/luci-interpreter/src/kernels/ArgMax.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/AveragePool2D.cpp b/onert-micro/luci-interpreter/src/kernels/AveragePool2D.cpp new file mode 100644 index 0000000..498334c --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/AveragePool2D.cpp @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/AveragePool2D.h" + +#include "kernels/Utils.h" + +#include "PALAveragePool2d.h" + +namespace luci_interpreter +{ + +namespace kernels +{ + +AveragePool2D::AveragePool2D(const Tensor *input, Tensor *output, Tensor *scratchpad, + const Pool2DParams ¶ms) + : KernelWithParams({input}, {output, scratchpad}, params) +{ +} + +void AveragePool2D::configure() +{ + if (input()->element_type() != output()->element_type()) + { + assert(false && "Input Tensor and Output Tensor Type must be same"); + } + if (input()->shape().num_dims() != 4) + { + assert(false && "Input Tensor Shape must be 4-D"); + } + const Shape &input_shape = input()->shape(); + + const int32_t batches = input_shape.dim(0); + const int32_t input_height = input_shape.dim(1); + const int32_t input_width = input_shape.dim(2); + const int32_t depth = input_shape.dim(3); + + const int32_t output_height = + computeOutputSize(_params.padding, input_height, _params.filter_height, _params.stride_height); + const int32_t output_width = + computeOutputSize(_params.padding, input_width, _params.filter_width, _params.stride_width); + + _padding_height = + computePadding(_params.stride_height, 1, input_height, _params.filter_height, output_height); + _padding_width = + computePadding(_params.stride_width, 1, input_width, _params.filter_width, output_width); + if (input()->element_type() == DataType::U8) + { + LUCI_INTERPRETER_CHECK(std::abs(output()->scale() - input()->scale()) <= 1.0e-6); + LUCI_INTERPRETER_CHECK(output()->zero_point() == input()->zero_point()); + } + else if (input()->element_type() == DataType::S16) + { + LUCI_INTERPRETER_CHECK(std::abs(output()->scale() - input()->scale()) <= 1.0e-6); + LUCI_INTERPRETER_CHECK(input()->zero_point() == 0 && output()->zero_point() == 0); + } + else if (input()->element_type() == DataType::S8) + { + LUCI_INTERPRETER_CHECK(std::abs(output()->scale() - input()->scale()) <= 1.0e-6); + LUCI_INTERPRETER_CHECK(output()->zero_point() == input()->zero_point()); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize({batches, output_height, output_width, depth}); + + auto scratchpad = getOutputTensors()[1]; + luci_interpreter_pal::SetupScratchpadTensor(scratchpad, input()->element_type(), + getTensorShape(input()), getTensorShape(output())); +} + +void AveragePool2D::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::U8: + evalQuantized(); + break; + case DataType::S16: + evalSInt16(); + break; + case DataType::S8: + evalSInt8(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void AveragePool2D::evalFloat() const +{ + float activation_min{}; + float activation_max{}; + calculateActivationRange(_params.activation, &activation_min, &activation_max); + + tflite::PoolParams params{}; + params.padding_values.height = _padding_height; + params.padding_values.width = _padding_width; + params.stride_height = _params.stride_height; + params.stride_width = _params.stride_width; + params.filter_height = _params.filter_height; + params.filter_width = _params.filter_width; + params.float_activation_min = activation_min; + params.float_activation_max = activation_max; + + tflite::reference_ops::AveragePool(params, getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); +} + +void AveragePool2D::evalQuantized() const +{ + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + + tflite::PoolParams params{}; + params.padding_values.height = _padding_height; + params.padding_values.width = _padding_width; + params.stride_height = _params.stride_height; + params.stride_width = _params.stride_width; + params.filter_height = _params.filter_height; + params.filter_width = _params.filter_width; + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + tflite::reference_ops::AveragePool(params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); +} + +void AveragePool2D::evalSInt8() const +{ + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + tflite::PoolParams params{}; + params.padding_values.height = _padding_height; + params.padding_values.width = _padding_width; + params.stride_height = _params.stride_height; + params.stride_width = _params.stride_width; + params.filter_height = _params.filter_height; + params.filter_width = _params.filter_width; + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + auto scratchpad = getOutputTensors()[1]; + int8_t *scratchpad_data = nullptr; + if (scratchpad->is_allocatable()) + scratchpad_data = scratchpad->data(); + + luci_interpreter_pal::AveragePool( + params, getTensorShape(input()), getTensorData(input()), getTensorShape(output()), + getTensorData(output()), getTensorShape(scratchpad), scratchpad_data); +} + +void AveragePool2D::evalSInt16() const +{ + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + + tflite::PoolParams params{}; + params.padding_values.height = _padding_height; + params.padding_values.width = _padding_width; + params.stride_height = _params.stride_height; + params.stride_width = _params.stride_width; + params.filter_height = _params.filter_height; + params.filter_width = _params.filter_width; + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + tflite::reference_integer_ops::AveragePool( + params, getTensorShape(input()), getTensorData(input()), // + getTensorShape(output()), getTensorData(output())); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/AveragePool2D.h b/onert-micro/luci-interpreter/src/kernels/AveragePool2D.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/AveragePool2D.h rename to onert-micro/luci-interpreter/src/kernels/AveragePool2D.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/AveragePool2D.test.cpp b/onert-micro/luci-interpreter/src/kernels/AveragePool2D.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/AveragePool2D.test.cpp rename to onert-micro/luci-interpreter/src/kernels/AveragePool2D.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/BatchMatMul.cpp b/onert-micro/luci-interpreter/src/kernels/BatchMatMul.cpp new file mode 100644 index 0000000..065e444 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/BatchMatMul.cpp @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2020 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/BatchMatMul.h" +#include "kernels/Utils.h" + +#include "PALBatchMatMul.h" + +#include + +namespace +{ + +tflite::RuntimeShape SwapRowColumnDims(const tflite::RuntimeShape &shape) +{ + tflite::RuntimeShape swapped_shape(shape); + const int32_t dims = shape.DimensionsCount(); + swapped_shape.SetDim(dims - 2, shape.Dims(dims - 1)); + swapped_shape.SetDim(dims - 1, shape.Dims(dims - 2)); + return swapped_shape; +} + +} // namespace + +namespace luci_interpreter +{ +namespace kernels +{ + +BatchMatMul::BatchMatMul(const Tensor *x, const Tensor *y, Tensor *output, Tensor *x_tmp, + Tensor *y_tmp, const BatchMatMulParams ¶ms) + : KernelWithParams({x, y}, {output, x_tmp, y_tmp}, params) +{ +} + +void BatchMatMul::configure() +{ + auto lhs = x(); + auto rhs = y(); + auto adj_x = params().adj_x; + auto adj_y = params().adj_y; + + // TODO Support non-float types + if (lhs->element_type() != DataType::FLOAT32 || rhs->element_type() != DataType::FLOAT32) + assert(false && "Unsupported type."); + + LUCI_INTERPRETER_CHECK(lhs->element_type() == rhs->element_type()); + + auto lhs_rank = lhs->shape().num_dims(); + auto rhs_rank = rhs->shape().num_dims(); + LUCI_INTERPRETER_CHECK(lhs_rank >= 2 && lhs_rank <= 4); + LUCI_INTERPRETER_CHECK(rhs_rank >= 2 && rhs_rank <= 4); + + auto lhs_scratchpad = temp_lhs(); + auto rhs_scratchpad = temp_rhs(); + luci_interpreter_pal::SetupScratchpadTensor(lhs_scratchpad, rhs_scratchpad, getTensorShape(lhs), + getTensorShape(rhs)); + + auto output_rank = std::max(lhs_rank, rhs_rank); + + auto extended_lhs_shape = tflite::RuntimeShape::ExtendedShape(output_rank, getTensorShape(lhs)); + auto extended_rhs_shape = tflite::RuntimeShape::ExtendedShape(output_rank, getTensorShape(rhs)); + + // Ensure any batch dimensions obey broacasting rules. + for (int i = 0; i < output_rank - 2; ++i) + { + const int lhs_dim = extended_lhs_shape.Dims(i); + const int rhs_dim = extended_rhs_shape.Dims(i); + if (lhs_dim != rhs_dim) + { + if (lhs_dim != 1) + { + LUCI_INTERPRETER_CHECK(rhs_dim == 1); + } + } + } + + // Ensure other dimensions work for matrix multiplication. + int accum_dim_lhs = + adj_x ? extended_lhs_shape.Dims(output_rank - 2) : extended_lhs_shape.Dims(output_rank - 1); + int accum_dim_rhs = + adj_y ? extended_rhs_shape.Dims(output_rank - 1) : extended_rhs_shape.Dims(output_rank - 2); + LUCI_INTERPRETER_CHECK(accum_dim_lhs == accum_dim_rhs); + + Shape output_shape(output_rank); + // Fill in any broadcast dimensions. + for (int i = 0; i < output_rank - 2; ++i) + { + const int lhs_dim = extended_lhs_shape.Dims(i); + const int rhs_dim = extended_rhs_shape.Dims(i); + int broadcast_dim = lhs_dim; + if ((lhs_dim != rhs_dim) && (lhs_dim == 1)) + { + broadcast_dim = rhs_dim; + } + output_shape.dim(i) = broadcast_dim; + } + // Fill in the matmul dimensions. + int lhs_rows_index = adj_x ? output_rank - 1 : output_rank - 2; + int rhs_cols_index = adj_y ? output_rank - 2 : output_rank - 1; + + output_shape.dim(output_rank - 2) = extended_lhs_shape.Dims(lhs_rows_index); + output_shape.dim(output_rank - 1) = extended_rhs_shape.Dims(rhs_cols_index); + + output()->resize(output_shape); +} + +void TransposeRowsColumns(const Tensor *tensor_in, Tensor *tensor_out) +{ + tflite::RuntimeShape transposed_shape(getTensorShape(tensor_in)); + tflite::RuntimeShape shape(getTensorShape(tensor_in)); + tflite::TransposeParams params; + int rank = shape.DimensionsCount(); + params.perm_count = rank; + for (int i = 0; i < rank - 2; ++i) + { + params.perm[i] = i; + } + // Transpose the last two dimensions. + params.perm[rank - 2] = rank - 1; + params.perm[rank - 1] = rank - 2; + transposed_shape.SetDim(rank - 1, shape.Dims(rank - 2)); + transposed_shape.SetDim(rank - 2, shape.Dims(rank - 1)); + switch (tensor_in->element_type()) + { + case DataType::FLOAT32: + tflite::reference_ops::Transpose(params, shape, getTensorData(tensor_in), + transposed_shape, getTensorData(tensor_out)); + break; + default: + assert(false && "Only suppport fp32 BatchMatMul for now."); + } +} + +void BatchMatMul::execute() const +{ + auto lhs = x(); + auto rhs = y(); + + bool adj_x = params().adj_x; + bool adj_y = params().adj_y; + + auto orig_lhs_shape = getTensorShape(lhs); + auto orig_rhs_shape = getTensorShape(rhs); + + auto rhs_tensor = adj_y ? rhs : temp_rhs(); + auto lhs_tensor = adj_x ? temp_lhs() : lhs; + if (not adj_y) + { + TransposeRowsColumns(rhs, temp_rhs()); + } + if (adj_x) + { + TransposeRowsColumns(lhs, temp_lhs()); + } + tflite::RuntimeShape rhs_shape = adj_y ? orig_rhs_shape : SwapRowColumnDims(orig_rhs_shape); + tflite::RuntimeShape lhs_shape = adj_x ? orig_lhs_shape : SwapRowColumnDims(orig_lhs_shape); + + switch (x()->element_type()) + { + case DataType::FLOAT32: + luci_interpreter_pal::BatchMatMul(rhs_shape, getTensorData(rhs_tensor), lhs_shape, + getTensorData(lhs_tensor), getTensorShape(output()), + getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/BatchMatMul.h b/onert-micro/luci-interpreter/src/kernels/BatchMatMul.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/BatchMatMul.h rename to onert-micro/luci-interpreter/src/kernels/BatchMatMul.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/BatchMatMul.test.cpp b/onert-micro/luci-interpreter/src/kernels/BatchMatMul.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/BatchMatMul.test.cpp rename to onert-micro/luci-interpreter/src/kernels/BatchMatMul.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/BatchToSpaceND.cpp b/onert-micro/luci-interpreter/src/kernels/BatchToSpaceND.cpp new file mode 100644 index 0000000..9ebe289 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/BatchToSpaceND.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/BatchToSpaceND.h" +#include "kernels/Utils.h" + +#include "PALBatchToSpaceND.h" + +namespace luci_interpreter +{ + +namespace kernels +{ + +namespace +{ +const int kInputMinDimensionNum = 3; +const int kInputMaxDimensionNum = 4; +} // namespace + +BatchToSpaceND::BatchToSpaceND(const Tensor *input, const Tensor *block_shape, const Tensor *crops, + Tensor *output) + : Kernel({input, block_shape, crops}, {output}) +{ +} + +void BatchToSpaceND::configure() +{ + + const auto *block_shape_data = block_shape()->data(); + const auto *crops_data = crops()->data(); + LUCI_INTERPRETER_CHECK(input()->shape().num_dims() >= kInputMinDimensionNum); + LUCI_INTERPRETER_CHECK(input()->shape().num_dims() <= kInputMaxDimensionNum); + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + + int spatial_dims_num = input()->shape().num_dims() - 2; + + LUCI_INTERPRETER_CHECK(block_shape()->shape().num_dims() == 1); + LUCI_INTERPRETER_CHECK(block_shape()->shape().dim(0) == spatial_dims_num); + + LUCI_INTERPRETER_CHECK(crops()->shape().num_dims() == 2); + LUCI_INTERPRETER_CHECK(crops()->shape().dim(0) == spatial_dims_num); + LUCI_INTERPRETER_CHECK(crops()->shape().dim(1) == 2); + for (int i = 0; i < spatial_dims_num * 2; ++i) + { + LUCI_INTERPRETER_CHECK(crops_data[i] >= 0); + } + + Shape output_shape = Shape(input()->shape().num_dims()); + int output_batch_size = input()->shape().dim(0); + for (int i = 0; i < spatial_dims_num; ++i) + { + LUCI_INTERPRETER_CHECK(output_batch_size % block_shape_data[i] == 0); + output_batch_size = output_batch_size / block_shape_data[i]; + output_shape.dim(i + 1) = + input()->shape().dim(i + 1) * block_shape_data[i] - crops_data[i * 2] - crops_data[i * 2 + 1]; + } + + output_shape.dim(0) = output_batch_size; + output_shape.dim(input()->shape().num_dims() - 1) = + input()->shape().dim(input()->shape().num_dims() - 1); + + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void BatchToSpaceND::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + luci_interpreter_pal::BatchToSpaceND( + getTensorShape(input()), getTensorData(input()), getTensorShape(block_shape()), + getTensorData(block_shape()), getTensorShape(crops()), + getTensorData(crops()), getTensorShape(output()), getTensorData(output())); + break; + case DataType::U8: + luci_interpreter_pal::BatchToSpaceND( + getTensorShape(input()), getTensorData(input()), getTensorShape(block_shape()), + getTensorData(block_shape()), getTensorShape(crops()), + getTensorData(crops()), getTensorShape(output()), + getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/BatchToSpaceND.h b/onert-micro/luci-interpreter/src/kernels/BatchToSpaceND.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/BatchToSpaceND.h rename to onert-micro/luci-interpreter/src/kernels/BatchToSpaceND.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/BatchToSpaceND.test.cpp b/onert-micro/luci-interpreter/src/kernels/BatchToSpaceND.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/BatchToSpaceND.test.cpp rename to onert-micro/luci-interpreter/src/kernels/BatchToSpaceND.test.cpp diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/BinaryOpCommon.h b/onert-micro/luci-interpreter/src/kernels/BinaryOpCommon.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/BinaryOpCommon.h rename to onert-micro/luci-interpreter/src/kernels/BinaryOpCommon.h diff --git a/onert-micro/luci-interpreter/src/kernels/Builders.h b/onert-micro/luci-interpreter/src/kernels/Builders.h new file mode 100644 index 0000000..b209b2e --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Builders.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_KERNELS_NODES_BUILDERS_H +#define LUCI_INTERPRETER_KERNELS_NODES_BUILDERS_H + +#include "KernelBuilder.h" +#include "luci_interpreter/core/reader/CircleMicroReader.h" +#include "core/RuntimeGraph.h" + +namespace luci_interpreter +{ + +#define REGISTER_KERNEL(builtin_operator, name) \ + void configure_kernel_Circle##name(const circle::Operator *cur_op, \ + BaseRuntimeGraph *runtime_graph); \ + \ + void execute_kernel_Circle##name(const circle::Operator *cur_op, \ + BaseRuntimeGraph *runtime_graph, bool is_inplace); + +#if USE_GENERATED_LIST +#include "GeneratedKernelsToBuild.lst" +#else +#include "KernelsToBuild.lst" +#endif + +#undef REGISTER_KERNEL + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_KERNELS_NODES_BUILDERS_H diff --git a/onert-micro/luci-interpreter/src/kernels/CMakeLists.txt b/onert-micro/luci-interpreter/src/kernels/CMakeLists.txt new file mode 100644 index 0000000..8e5c02c --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/CMakeLists.txt @@ -0,0 +1,40 @@ +set(SOURCES + BinaryOpCommon.h + Utils.h + Utils.cpp + Builders.h + KernelBuilder.h + KernelBuilder.cpp) + +macro(REGISTER_KERNEL OPERATOR, NODE) + list(APPEND SOURCES "${NODE}.cpp") +endmacro(REGISTER_KERNEL) + +include(${KERNEL_REGISTER_FILE}) + +add_library(${LUCI_INTERPRETER_KERNELS} STATIC ${SOURCES}) +if (NOT NNCC_LIBRARY_NO_PIC) + set_target_properties(${LUCI_INTERPRETER_KERNELS} PROPERTIES POSITION_INDEPENDENT_CODE ON) +endif(NOT NNCC_LIBRARY_NO_PIC) +target_include_directories(${LUCI_INTERPRETER_KERNELS} PUBLIC ${LUCI_INTERPRETER_SOURCE_DIR}) + +target_link_libraries(${LUCI_INTERPRETER_KERNELS} PUBLIC ${LUCI_INTERPRETER_CORE}) + +add_pal_to_target(${LUCI_INTERPRETER_KERNELS}) + +if(NOT ENABLE_TEST) + return() +endif(NOT ENABLE_TEST) + +nnas_find_package(GTest REQUIRED) + +macro(REGISTER_KERNEL OPERATOR, NODE) + list(APPEND TEST_SOURCES "${NODE}.test.cpp") +endmacro(REGISTER_KERNEL) + +include(${KERNEL_REGISTER_FILE}) + +list(APPEND TEST_SOURCES TestUtils.h TestUtils.cpp) + +GTest_AddTest(${LUCI_INTERPRETER_KERNELS}_test ${TEST_SOURCES}) +target_link_libraries(${LUCI_INTERPRETER_KERNELS}_test ${LUCI_INTERPRETER_KERNELS}) diff --git a/onert-micro/luci-interpreter/src/kernels/Cast.cpp b/onert-micro/luci-interpreter/src/kernels/Cast.cpp new file mode 100644 index 0000000..82b187a --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Cast.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Cast.h" +#include "kernels/Utils.h" + +namespace +{ + +using namespace luci_interpreter; +using namespace luci_interpreter::kernels; + +template +void cast_data(const InT *in_data, OutT *out_data, uint32_t elements_count) +{ + std::transform(in_data, in_data + elements_count, out_data, + [](InT a) { return static_cast(a); }); +} + +template void cast_from_pointer_to_tensor(const InT *in_data, Tensor *out_tensor) +{ + auto const out_type = out_tensor->element_type(); + auto const elements_count = out_tensor->shape().num_elements(); + + switch (out_type) + { + case DataType::U8: + cast_data(in_data, getTensorData(out_tensor), elements_count); + break; + case DataType::U16: + cast_data(in_data, getTensorData(out_tensor), elements_count); + break; + case DataType::U32: + cast_data(in_data, getTensorData(out_tensor), elements_count); + break; + case DataType::U64: + cast_data(in_data, getTensorData(out_tensor), elements_count); + break; + case DataType::S8: + cast_data(in_data, getTensorData(out_tensor), elements_count); + break; + case DataType::S16: + cast_data(in_data, getTensorData(out_tensor), elements_count); + break; + case DataType::S32: + cast_data(in_data, getTensorData(out_tensor), elements_count); + break; + case DataType::S64: + cast_data(in_data, getTensorData(out_tensor), elements_count); + break; + case DataType::FLOAT32: + cast_data(in_data, getTensorData(out_tensor), elements_count); + break; + case DataType::BOOL: + cast_data(in_data, getTensorData(out_tensor), elements_count); + break; + default: + assert(false && "Unsupported output type."); + } +} + +void cast_from_tensor_to_tensor(const Tensor *in_tensor, Tensor *out_tensor) +{ + auto in_type = in_tensor->element_type(); + + switch (in_type) + { + case DataType::U8: + cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); + break; + case DataType::U16: + cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); + break; + case DataType::U32: + cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); + break; + case DataType::U64: + cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); + break; + case DataType::S8: + cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); + break; + case DataType::S16: + cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); + break; + case DataType::S32: + cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); + break; + case DataType::S64: + cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); + break; + case DataType::FLOAT32: + cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); + break; + case DataType::BOOL: + cast_from_pointer_to_tensor(getTensorData(in_tensor), out_tensor); + break; + default: + assert(false && "Unsupported input type."); + } +} + +} // namespace + +namespace luci_interpreter +{ +namespace kernels +{ + +Cast::Cast(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Cast::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() != DataType::Unknown); + LUCI_INTERPRETER_CHECK(output()->element_type() != DataType::Unknown); + + const Shape &shape = input()->shape(); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(shape); +} + +void Cast::execute() const +{ + assert(input()->shape().num_elements() == output()->shape().num_elements()); + + cast_from_tensor_to_tensor(input(), output()); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Cast.h b/onert-micro/luci-interpreter/src/kernels/Cast.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Cast.h rename to onert-micro/luci-interpreter/src/kernels/Cast.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Cast.test.cpp b/onert-micro/luci-interpreter/src/kernels/Cast.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Cast.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Cast.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Concatenation.cpp b/onert-micro/luci-interpreter/src/kernels/Concatenation.cpp new file mode 100644 index 0000000..a9eff09 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Concatenation.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace +{ + +template +void evalGeneric(const circle::Operator *cur_op, BaseRuntimeGraph *runtime_graph, bool) +{ + const auto output_index = cur_op->outputs()->operator[](0); + + assert(output_index != -1); + + auto output = runtime_graph->getCircleTensorByIndex(output_index); + + const auto *options = cur_op->builtin_options_as_ConcatenationOptions(); + + int axis = options->axis(); + if (axis < 0) + axis += Tensor::num_dims(output); + + const auto input_sizes = cur_op->inputs()->size(); + + std::vector all_input_data; + std::vector all_shape; + std::vector all_shape_ptr; + + all_input_data.reserve(input_sizes); + all_shape.reserve(input_sizes); + all_shape_ptr.reserve(input_sizes); + + for (int32_t i = 0; i < input_sizes; ++i) + { + auto input_index = cur_op->inputs()->operator[](i); + const auto *tensor = runtime_graph->getCircleTensorByIndex(input_index); + + auto *data = reinterpret_cast(runtime_graph->getDataByTensor(tensor)); + + all_input_data.push_back(data); + all_shape.push_back(kernels::getTensorShape(tensor)); + } + + for (tflite::RuntimeShape &shape : all_shape) + { + all_shape_ptr.push_back(&shape); + } + + auto *output_data = reinterpret_cast(runtime_graph->getDataByTensor(output)); + + // kernels::VectorOfTensors inputs(_inputs); + tflite::ConcatenationParams params{}; + params.axis = axis; + params.inputs_count = input_sizes; + tflite::reference_ops::Concatenation(params, all_shape_ptr.data(), all_input_data.data(), + kernels::getTensorShape(output), output_data); +} + +} // namespace + +void configure_kernel_CircleConcatenation(const circle::Operator *cur_op, + BaseRuntimeGraph *runtime_graph) +{ + const int num_inputs = cur_op->inputs()->size(); + LUCI_INTERPRETER_CHECK(num_inputs > 0); + + auto input_index = cur_op->inputs()->operator[](0); + auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(output_index != -1); + + const auto *t0 = runtime_graph->getCircleTensorByIndex(input_index); + const auto *output = runtime_graph->getCircleTensorByIndex(output_index); + + const auto *params = cur_op->builtin_options_as_ConcatenationOptions(); + + // TODO: Support concat with fused activation function + LUCI_INTERPRETER_CHECK(luci_actfunc(params->fused_activation_function()) == FusedActFunc::NONE); + + int axis = params->axis(); + if (axis < 0) + axis += Tensor::num_dims(t0); + LUCI_INTERPRETER_CHECK(axis >= 0 && axis < Tensor::num_dims(t0)); + + int32_t sum_axis = Tensor::dim(t0, axis); + for (int i = 1; i < num_inputs; ++i) + { + input_index = cur_op->inputs()->operator[](i); + const auto *tensor = runtime_graph->getCircleTensorByIndex(input_index); + LUCI_INTERPRETER_CHECK(Tensor::element_type(tensor) == Tensor::element_type(t0)); + LUCI_INTERPRETER_CHECK(Tensor::num_dims(tensor) == Tensor::num_dims(t0)); + for (int d = 0; d < Tensor::num_dims(t0); ++d) + { + if (d == axis) + { + sum_axis += Tensor::dim(tensor, axis); + } + else + { + LUCI_INTERPRETER_CHECK(Tensor::dim(tensor, d) == Tensor::dim(t0, d)); + } + } + } + +#ifndef DIS_QUANT + // If input tensors are INT8 type then quantization parameters of all input tensors and the output + // should be the same + for (int i = 1; i < num_inputs; ++i) + { + input_index = cur_op->inputs()->operator[](i); + const auto *tensor = runtime_graph->getCircleTensorByIndex(input_index); + if (Tensor::element_type(tensor) == DataType::S8) + { + LUCI_INTERPRETER_CHECK(Tensor::quantized_dimension(tensor) == + Tensor::quantized_dimension(output)); + + LUCI_INTERPRETER_CHECK(Tensor::zero_points(tensor).size() == Tensor::scales(tensor).size()); + LUCI_INTERPRETER_CHECK(Tensor::zero_points(tensor) == Tensor::zero_points(output)); + LUCI_INTERPRETER_CHECK(Tensor::scales(tensor) == Tensor::scales(output)); + } + } +#endif // DIS_QUANT +} + +void execute_kernel_CircleConcatenation(const circle::Operator *cur_op, + BaseRuntimeGraph *runtime_graph, bool is_inplace) +{ + int num_inputs = cur_op->inputs()->size(); + LUCI_INTERPRETER_CHECK(num_inputs > 0); + + const auto input_index = cur_op->inputs()->operator[](0); + assert(input_index != -1); + const auto *t0 = runtime_graph->getCircleTensorByIndex(input_index); + + switch (Tensor::element_type(t0)) + { +#ifndef DIS_FLOAT + case DataType::FLOAT32: + evalGeneric(cur_op, runtime_graph, is_inplace); + break; +#endif // DIS_FLOAT +#ifndef DIS_QUANT + case DataType::S8: + evalGeneric(cur_op, runtime_graph, is_inplace); + break; + case DataType::S32: + evalGeneric(cur_op, runtime_graph, is_inplace); + break; + case DataType::S64: + evalGeneric(cur_op, runtime_graph, is_inplace); + break; +#endif + default: + assert(false && "Unsupported type."); + } +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/Concatenation.test.cpp b/onert-micro/luci-interpreter/src/kernels/Concatenation.test.cpp new file mode 100644 index 0000000..441ecfe --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Concatenation.test.cpp @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// TODO enable it +#if 0 +#include "kernels/Concatenation.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +class ConcatenationTest : public ::testing::Test +{ +protected: + void SetUp() override { _memory_manager = std::make_unique(); } + + std::unique_ptr _memory_manager; +}; + +TEST_F(ConcatenationTest, Float) +{ + std::vector input1_data{1, 2, 3, 4, 5, 6}; + std::vector input2_data{7, 8, 9, 10, 11, 12}; + Tensor input1_tensor = + makeInputTensor({2, 3}, input1_data, _memory_manager.get()); + Tensor input2_tensor = + makeInputTensor({2, 3}, input2_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + ConcatenationParams params{}; + + // Try different 'axis' and expect different results. + { + params.axis = 0; + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + kernel.configure(); + for (auto t : kernel.getOutputTensors()) + { + _memory_manager->allocate_memory(*t); + } + kernel.execute(); + + EXPECT_THAT(extractTensorData(output_tensor), + FloatArrayNear({1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12})); + } + { + params.axis = -2; // Same as '0'. + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorData(output_tensor), + FloatArrayNear({1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12})); + } + { + params.axis = 1; + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorData(output_tensor), + FloatArrayNear({1, 2, 3, 7, 8, 9, 4, 5, 6, 10, 11, 12})); + } + { + params.axis = -1; // Same as '1'. + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorData(output_tensor), + FloatArrayNear({1, 2, 3, 7, 8, 9, 4, 5, 6, 10, 11, 12})); + } +} + +TEST_F(ConcatenationTest, Input_Number_Check_NEG) +{ + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + ConcatenationParams params{}; + + params.axis = -1; + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({}, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(ConcatenationTest, Invalid_Axis_NEG) +{ + std::vector input1_data{1, 2, 3, 4, 5, 6}; + std::vector input2_data{7, 8, 9, 10, 11, 12}; + Tensor input1_tensor = + makeInputTensor({2, 3}, input1_data, _memory_manager.get()); + Tensor input2_tensor = + makeInputTensor({2, 3}, input2_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + ConcatenationParams params{}; + + params.axis = -3; + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(ConcatenationTest, Mismatching_Input_Type_NEG) +{ + std::vector input1_data{1, 2, 3, 4, 5, 6}; + std::vector input2_data{7, 8, 9, 10, 11, 12}; + Tensor input1_tensor = + makeInputTensor({2, 3}, input1_data, _memory_manager.get()); + Tensor input2_tensor = makeInputTensor({2, 3}, input2_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + ConcatenationParams params{}; + + params.axis = -1; + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(ConcatenationTest, Mismatching_Input_Dimension_Num_NEG) +{ + std::vector input1_data{1, 2, 3, 4, 5, 6}; + std::vector input2_data{7, 8, 9, 10, 11, 12}; + Tensor input1_tensor = + makeInputTensor({2, 3}, input1_data, _memory_manager.get()); + Tensor input2_tensor = + makeInputTensor({1, 2, 3}, input2_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + ConcatenationParams params{}; + + params.axis = -1; + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(ConcatenationTest, Mismatching_Input_Dimension_NEG) +{ + std::vector input1_data{1, 2, 3, 4, 5, 6}; + std::vector input2_data{7, 8, 9, 10, 11, 12, 13, 14, 15}; + Tensor input1_tensor = + makeInputTensor({2, 3}, input1_data, _memory_manager.get()); + Tensor input2_tensor = + makeInputTensor({3, 3}, input2_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + ConcatenationParams params{}; + + params.axis = -1; + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(ConcatenationTest, Int8_Mismatching_Input_Type_NEG) +{ + std::vector input1_data{1, 2, 3, 4}; + std::vector input2_data{5, 6, 7, 8}; + Tensor input1_tensor = makeInputTensor({2, 2}, input1_data, _memory_manager.get()); + Tensor input2_tensor = makeInputTensor({2, 2}, input2_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::S8); + ConcatenationParams params{}; + + params.axis = -1; + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(ConcatenationTest, Int8_Mismatching_Input_Output_Quant_Params_NEG) +{ + std::vector input1_data{1, 2, 3, 4, 5, 6}; + std::vector input2_data{7, 8, 9, 10, 11, 12}; + int quantized_dimension = 3; + std::vector scales{0.1, 0.2, 0.3}; + std::vector zero_points{1, -1, 1}; + + Tensor input1_tensor = makeInputTensor( + {1, 1, 2, 3}, scales, zero_points, quantized_dimension, input1_data, _memory_manager.get()); + Tensor input2_tensor = makeInputTensor( + {1, 1, 2, 3}, scales, zero_points, quantized_dimension, input2_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::S8, scales.at(0), zero_points.at(0)); + ConcatenationParams params{}; + + params.axis = -1; + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(ConcatenationTest, Int8_Mismatching_Zero_Point_NEG) +{ + std::vector input1_data{1, 2, 3, 4}; + std::vector input2_data{5, 6, 7, 8}; + float scale = 0.1; + int32_t zero_point_1 = 1; + int32_t zero_point_2 = -1; + + Tensor input1_tensor = + makeInputTensor({2, 2}, scale, zero_point_1, input1_data, _memory_manager.get()); + Tensor input2_tensor = + makeInputTensor({2, 2}, scale, zero_point_2, input2_data, _memory_manager.get()); + + Tensor output_tensor = makeOutputTensor(DataType::S8, scale, zero_point_1); + ConcatenationParams params{}; + + params.axis = -1; + params.activation = luci::FusedActFunc::NONE; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +// TODO: Remove this test when concat w/ fused_activation is supported +TEST_F(ConcatenationTest, With_Fused_Activation_NEG) +{ + std::vector input1_data{1, 2, 3, 4, 5, 6}; + std::vector input2_data{7, 8, 9, 10, 11, 12}; + Tensor input1_tensor = + makeInputTensor({2, 3}, input1_data, _memory_manager.get()); + Tensor input2_tensor = + makeInputTensor({2, 3}, input2_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + ConcatenationParams params{}; + + params.axis = 1; + params.activation = luci::FusedActFunc::RELU; + + Concatenation kernel({&input1_tensor, &input2_tensor}, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter +#endif diff --git a/onert-micro/luci-interpreter/src/kernels/Conv2D.cpp b/onert-micro/luci-interpreter/src/kernels/Conv2D.cpp new file mode 100644 index 0000000..7045249 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Conv2D.cpp @@ -0,0 +1,524 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" +#include "kernels/Utils.h" + +#include "PALConv2d.h" + +namespace luci_interpreter +{ + +namespace +{ + +int32_t compute_padding_h(const circle::Tensor *input, const circle::Tensor *filter, + const circle::Conv2DOptions *options) +{ + const int32_t input_height = Tensor::dim(input, 1); + const int32_t filter_height = Tensor::dim(filter, 1); + const int32_t output_height = + kernels::computeOutputSize(luci_padding(options->padding()), input_height, filter_height, + options->stride_h(), options->dilation_h_factor()); + + const auto padding_height = kernels::computePadding( + options->stride_h(), options->dilation_h_factor(), input_height, filter_height, output_height); + return padding_height; +} + +int32_t compute_padding_w(const circle::Tensor *input, const circle::Tensor *filter, + const circle::Conv2DOptions *options) +{ + const int32_t input_width = Tensor::dim(input, 2); + const int32_t filter_width = Tensor::dim(filter, 2); + const int32_t output_width = + kernels::computeOutputSize(luci_padding(options->padding()), input_width, filter_width, + options->stride_w(), options->dilation_w_factor()); + + const auto padding_width = kernels::computePadding( + options->stride_w(), options->dilation_w_factor(), input_width, filter_width, output_width); + + return padding_width; +} + +#ifndef DIS_FLOAT + +void evalFloat(const circle::Tensor *input, const circle::Tensor *filter, + const circle::Tensor *bias, const circle::Tensor *output, + const circle::Conv2DOptions *options, BaseRuntimeGraph *runtime_graph) +{ + float activation_min{}; + float activation_max{}; + kernels::calculateActivationRange(luci_actfunc(options->fused_activation_function()), + &activation_min, &activation_max); + + tflite::ConvParams params{}; + params.padding_values.height = compute_padding_h(input, filter, options); + params.padding_values.width = compute_padding_w(input, filter, options); + params.stride_height = options->stride_h(); + params.stride_width = options->stride_w(); + params.dilation_height_factor = options->dilation_h_factor(); + params.dilation_width_factor = options->dilation_w_factor(); + params.float_activation_min = activation_min; + params.float_activation_max = activation_max; + + auto *input_data = runtime_graph->getDataByTensor(input); + auto *output_data = runtime_graph->getDataByTensor(output); + + auto *filter_data = runtime_graph->getConstDataByTensor(filter); + auto *bias_data = runtime_graph->getConstDataByTensor(bias); + + luci_interpreter_pal::Conv( + params, kernels::getTensorShape(input), kernels::getTensorData(input_data), + kernels::getTensorShape(filter), kernels::getTensorData(filter_data), + kernels::getTensorShape(bias), kernels::getTensorData(bias_data), + kernels::getTensorShape(output), kernels::getTensorData(output_data), + kernels::getTensorShape(nullptr), nullptr); +} + +#endif // DIS_FLOAT + +#ifndef DIS_QUANT + +void evalQuantized(const circle::Tensor *input, const circle::Tensor *filter, + const circle::Tensor *bias, const circle::Tensor *output, + const circle::Conv2DOptions *options, BaseRuntimeGraph *runtime_graph) +{ + const auto input_scale = static_cast(Tensor::scale(input)); + const auto filter_scale = static_cast(Tensor::scale(filter)); + const auto output_scale = static_cast(Tensor::scale(output)); + + const double real_multiplier = input_scale * filter_scale / output_scale; + int32_t output_multiplier{}; + int output_shift{}; + kernels::quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); + + int32_t activation_min{}; + int32_t activation_max{}; + kernels::calculateActivationRangeQuantized(luci_actfunc(options->fused_activation_function()), + output, &activation_min, &activation_max); + + tflite::ConvParams params{}; + params.padding_values.height = compute_padding_h(input, filter, options); + params.padding_values.width = compute_padding_w(input, filter, options); + params.stride_height = options->stride_h(); + params.stride_width = options->stride_w(); + params.dilation_height_factor = options->dilation_h_factor(); + params.dilation_width_factor = options->dilation_w_factor(); + // The kernel expects input and filter zero points to be negated. + params.input_offset = -Tensor::zero_point(input); // Note the '-'. + params.weights_offset = -Tensor::zero_point(filter); // Note the '-'. + params.output_offset = Tensor::zero_point(output); + params.output_multiplier = output_multiplier; + params.output_shift = output_shift; + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + auto *input_data = runtime_graph->getDataByTensor(input); + auto *output_data = runtime_graph->getDataByTensor(output); + + auto *filter_data = runtime_graph->getConstDataByTensor(filter); + auto *bias_data = runtime_graph->getConstDataByTensor(bias); + + luci_interpreter_pal::Conv( + params, kernels::getTensorShape(input), kernels::getTensorData(input_data), + kernels::getTensorShape(filter), kernels::getTensorData(filter_data), + kernels::getTensorShape(bias), kernels::getTensorData(bias_data), + kernels::getTensorShape(output), kernels::getTensorData(output_data), + kernels::getTensorShape(nullptr), nullptr); +} + +void evalQuantizedPerChannel(const circle::Tensor *input, const circle::Tensor *filter, + const circle::Tensor *bias, const circle::Tensor *output, + const circle::Conv2DOptions *options, BaseRuntimeGraph *runtime_graph) +{ + auto *raw_input_data = runtime_graph->getDataByTensor(input); + auto *raw_output_data = runtime_graph->getDataByTensor(output); + + auto *raw_filter_data = runtime_graph->getConstDataByTensor(filter); + auto *raw_bias_data = runtime_graph->getConstDataByTensor(bias); + + const auto *input_data = kernels::getTensorData(raw_input_data); + const auto *filter_data = kernels::getTensorData(raw_filter_data); + const auto *bias_data = kernels::getTensorData(raw_bias_data); + auto *output_data = kernels::getTensorData(raw_output_data); + + const int32_t batches = Tensor::dim(input, 0); + const int32_t input_height = Tensor::dim(input, 1); + const int32_t input_width = Tensor::dim(input, 2); + const int32_t input_depth = Tensor::dim(input, 3); + const int32_t output_depth = Tensor::dim(filter, 0); + const int32_t filter_height = Tensor::dim(filter, 1); + const int32_t filter_width = Tensor::dim(filter, 2); + const int32_t output_height = Tensor::dim(output, 1); + const int32_t output_width = Tensor::dim(output, 2); + + const int32_t stride_height = options->stride_h(); + const int32_t stride_width = options->stride_w(); + const int32_t dilation_height_factor = options->dilation_h_factor(); + const int32_t dilation_width_factor = options->dilation_w_factor(); + + int32_t activation_min{}; + int32_t activation_max{}; + kernels::calculateActivationRangeQuantized(luci_actfunc(options->fused_activation_function()), + output, &activation_min, &activation_max); + + const std::vector effective_output_scale = kernels::getQuantizedConvolutionMultiplers( + Tensor::scale(input), Tensor::scales(filter), Tensor::scale(output)); + + const std::vector multipliers_raw = + kernels::quantizeMultipliers(effective_output_scale); + kernels::BroadcastableWrapper quant_multipliers( + multipliers_raw); + + for (int32_t batch = 0; batch < batches; ++batch) + { + for (int32_t out_y = 0; out_y < output_height; ++out_y) + { + for (int32_t out_x = 0; out_x < output_width; ++out_x) + { + for (int32_t out_c = 0; out_c < output_depth; ++out_c) + { + const int32_t in_y_origin = + out_y * stride_height - compute_padding_h(input, filter, options); + const int32_t in_x_origin = + out_x * stride_width - compute_padding_w(input, filter, options); + int32_t acc = 0; + for (int32_t filter_y = 0; filter_y < filter_height; ++filter_y) + { + for (int32_t filter_x = 0; filter_x < filter_width; ++filter_x) + { + const int32_t in_y = in_y_origin + dilation_height_factor * filter_y; + const int32_t in_x = in_x_origin + dilation_width_factor * filter_x; + if ((in_y >= 0 && in_y < input_height) && (in_x >= 0 && in_x < input_width)) + { + for (int32_t in_c = 0; in_c < input_depth; ++in_c) + { + const uint8_t input_val = + input_data[kernels::calcOffset(input, batch, in_y, in_x, in_c)]; + const uint8_t filter_val = + filter_data[kernels::calcOffset(filter, out_c, filter_y, filter_x, in_c)]; + acc += static_cast(input_val - Tensor::zero_point(input)) * + static_cast(filter_val - Tensor::zero_points(filter)[out_c]); + } + } + } + } + if (bias_data) + { + acc += bias_data[out_c]; + } + + int32_t scaled_acc = tflite::MultiplyByQuantizedMultiplier( + acc, quant_multipliers[out_c].multiplier, quant_multipliers[out_c].shift); + + scaled_acc += Tensor::zero_point(output); + scaled_acc = std::max(scaled_acc, activation_min); + scaled_acc = std::min(scaled_acc, activation_max); + output_data[kernels::calcOffset(output, batch, out_y, out_x, out_c)] = scaled_acc; + } + } + } + } +} + +void evalQuantizedS8PerChannel(const circle::Tensor *input, const circle::Tensor *filter, + const circle::Tensor *bias, const circle::Tensor *output, + const circle::Conv2DOptions *options, + BaseRuntimeGraph *runtime_graph) +{ + int32_t activation_min{}; + int32_t activation_max{}; + kernels::calculateActivationRangeQuantized(luci_actfunc(options->fused_activation_function()), + output, &activation_min, &activation_max); + + tflite::ConvParams params{}; + params.padding_values.height = compute_padding_h(input, filter, options); + params.padding_values.width = compute_padding_w(input, filter, options); + params.stride_height = options->stride_h(); + params.stride_width = options->stride_w(); + params.dilation_height_factor = options->dilation_h_factor(); + params.dilation_width_factor = options->dilation_w_factor(); + // The kernel expects filter zero points to be negated. + params.input_offset = -Tensor::zero_point(input); // Note the '-'. + params.weights_offset = 0; // Unused in tflite code + params.output_offset = Tensor::zero_point(output); + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + const std::vector effective_output_scales = kernels::getQuantizedConvolutionMultiplers( + Tensor::scale(input), Tensor::scales(filter), Tensor::scale(output)); + + std::vector quant_multipliers = + kernels::quantizeMultipliers(effective_output_scales); + + std::vector shifts; + std::transform(quant_multipliers.begin(), quant_multipliers.end(), std::back_inserter(shifts), + [](kernels::ChannelQuantMultipliers cm) { return cm.shift; }); + std::vector multipliers; + std::transform(quant_multipliers.begin(), quant_multipliers.end(), + std::back_inserter(multipliers), + [](kernels::ChannelQuantMultipliers cm) { return cm.multiplier; }); + + auto *input_data = runtime_graph->getDataByTensor(input); + auto *output_data = runtime_graph->getDataByTensor(output); + + auto *filter_data = runtime_graph->getConstDataByTensor(filter); + auto *bias_data = runtime_graph->getConstDataByTensor(bias); + + luci_interpreter_pal::ConvPerChannel( + params, multipliers.data(), shifts.data(), kernels::getTensorShape(input), + kernels::getTensorData(input_data), kernels::getTensorShape(filter), + kernels::getTensorData(filter_data), kernels::getTensorShape(bias), + kernels::getTensorData(bias_data), kernels::getTensorShape(output), + kernels::getTensorData(output_data), kernels::getTensorShape(nullptr), nullptr); +} + +void evalQuantizedS16(const circle::Tensor *input, const circle::Tensor *filter, + const circle::Tensor *bias, const circle::Tensor *output, + const circle::Conv2DOptions *options, BaseRuntimeGraph *runtime_graph) +{ + auto *raw_input_data = runtime_graph->getDataByTensor(input); + auto *raw_output_data = runtime_graph->getDataByTensor(output); + + auto *raw_filter_data = runtime_graph->getConstDataByTensor(filter); + auto *raw_bias_data = runtime_graph->getConstDataByTensor(bias); + + const auto *input_data = kernels::getTensorData(raw_input_data); + const auto *filter_data = kernels::getTensorData(raw_filter_data); + const auto *bias_data = kernels::getTensorData(raw_bias_data); + auto *output_data = kernels::getTensorData(raw_output_data); + + const int32_t batches = Tensor::dim(input, 0); + const int32_t input_height = Tensor::dim(input, 1); + const int32_t input_width = Tensor::dim(input, 2); + const int32_t input_depth = Tensor::dim(input, 3); + const int32_t output_depth = Tensor::dim(filter, 0); + const int32_t filter_height = Tensor::dim(filter, 1); + const int32_t filter_width = Tensor::dim(filter, 2); + const int32_t output_height = Tensor::dim(output, 1); + const int32_t output_width = Tensor::dim(output, 2); + + const int32_t stride_height = options->stride_h(); + const int32_t stride_width = options->stride_w(); + const int32_t dilation_height_factor = options->dilation_h_factor(); + const int32_t dilation_width_factor = options->dilation_w_factor(); + + int32_t activation_min{}; + int32_t activation_max{}; + kernels::calculateActivationRangeQuantized(luci_actfunc(options->fused_activation_function()), + output, &activation_min, &activation_max); + + const std::vector effective_output_scale = kernels::getQuantizedConvolutionMultiplers( + Tensor::scale(input), Tensor::scales(filter), Tensor::scale(output)); + + const std::vector multipliers_raw = + kernels::quantizeMultipliers(effective_output_scale); + kernels::BroadcastableWrapper multipliers(multipliers_raw); + + for (int32_t batch = 0; batch < batches; ++batch) + { + for (int32_t out_y = 0; out_y < output_height; ++out_y) + { + for (int32_t out_x = 0; out_x < output_width; ++out_x) + { + for (int32_t out_c = 0; out_c < output_depth; ++out_c) + { + const int32_t in_y_origin = + out_y * stride_height - compute_padding_h(input, filter, options); + const int32_t in_x_origin = + out_x * stride_width - compute_padding_w(input, filter, options); + int64_t acc = 0; + for (int32_t filter_y = 0; filter_y < filter_height; ++filter_y) + { + for (int32_t filter_x = 0; filter_x < filter_width; ++filter_x) + { + const int32_t in_y = in_y_origin + dilation_height_factor * filter_y; + const int32_t in_x = in_x_origin + dilation_width_factor * filter_x; + if ((in_y >= 0 && in_y < input_height) && (in_x >= 0 && in_x < input_width)) + { + for (int32_t in_c = 0; in_c < input_depth; ++in_c) + { + const int16_t input_val = + input_data[kernels::calcOffset(input, batch, in_y, in_x, in_c)]; + const int16_t filter_val = + filter_data[kernels::calcOffset(filter, out_c, filter_y, filter_x, in_c)]; + acc += static_cast(input_val) * static_cast(filter_val); + } + } + } + } + if (bias_data) + { + acc += bias_data[out_c]; + } + + int32_t scaled_acc = tflite::MultiplyByQuantizedMultiplier( + acc, multipliers[out_c].multiplier, multipliers[out_c].shift); + + scaled_acc = std::max(scaled_acc, activation_min); + scaled_acc = std::min(scaled_acc, activation_max); + + output_data[kernels::calcOffset(output, batch, out_y, out_x, out_c)] = scaled_acc; + } + } + } + } +} +#endif // DIS_QUANT + +} // namespace + +void configure_kernel_CircleConv2D(const circle::Operator *cur_op, BaseRuntimeGraph *runtime_graph) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto filter_index = cur_op->inputs()->operator[](1); + const auto bias_index = cur_op->inputs()->operator[](2); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(filter_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + const auto filter = runtime_graph->getCircleTensorByIndex(filter_index); + const auto bias = runtime_graph->getCircleTensorByIndex(bias_index); + const auto output = runtime_graph->getCircleTensorByIndex(output_index); + + assert(input != nullptr); + assert(filter != nullptr); + + auto filter_data = runtime_graph->getConstDataByTensor(filter); + + assert(filter_data != nullptr); + + const auto *options = cur_op->builtin_options_as_Conv2DOptions(); + + if (Tensor::element_type(input) == DataType::FLOAT32 && + Tensor::element_type(filter) == DataType::FLOAT32) + { + LUCI_INTERPRETER_CHECK(bias == nullptr || Tensor::element_type(bias) == DataType::FLOAT32); + } +#ifndef DIS_QUANT + else if (Tensor::element_type(input) == DataType::U8 && + Tensor::element_type(filter) == DataType::U8) + { + LUCI_INTERPRETER_CHECK(bias == nullptr || Tensor::element_type(bias) == DataType::S32); + } + else if (Tensor::element_type(input) == DataType::S8 && + Tensor::element_type(filter) == DataType::S8) + { + LUCI_INTERPRETER_CHECK(bias == nullptr || Tensor::element_type(bias) == DataType::S32); + LUCI_INTERPRETER_CHECK(Tensor::num_dims(filter) == 4); + LUCI_INTERPRETER_CHECK(Tensor::scales(filter).size() == + static_cast(Tensor::dim(filter, 0))); + for (auto zerop : Tensor::zero_points(filter)) + { + LUCI_INTERPRETER_CHECK(zerop == 0); + } + } + else if (Tensor::element_type(input) == DataType::S16 && + Tensor::element_type(filter) == DataType::S16) + { + LUCI_INTERPRETER_CHECK(bias == nullptr || Tensor::element_type(bias) == DataType::S64); + } +#endif // DIS_QUANT + else + { + assert(false && "Unsupported type."); + } + LUCI_INTERPRETER_CHECK(Tensor::element_type(output) == Tensor::element_type(input)); + LUCI_INTERPRETER_CHECK(Tensor::num_dims(input) == 4 && Tensor::num_dims(filter) == 4); + + const int32_t output_depth = Tensor::dim(filter, 0); + LUCI_INTERPRETER_CHECK(Tensor::dim(filter, 3) == Tensor::dim(input, 3)); + + LUCI_INTERPRETER_CHECK(bias == nullptr || + (Tensor::num_dims(bias) == 1 && Tensor::dim(bias, 0) == output_depth)); + + switch (options->fused_activation_function()) + { + case circle::ActivationFunctionType_NONE: + case circle::ActivationFunctionType_RELU: + case circle::ActivationFunctionType_RELU6: + case circle::ActivationFunctionType_RELU_N1_TO_1: + break; + default: + assert(false && "Unsupported fused activation"); + } +} + +void execute_kernel_CircleConv2D(const circle::Operator *cur_op, BaseRuntimeGraph *runtime_graph, + bool) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto weight_index = cur_op->inputs()->operator[](1); + const auto bias_index = cur_op->inputs()->operator[](2); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(weight_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + const auto weights = runtime_graph->getCircleTensorByIndex(weight_index); + const auto bias = runtime_graph->getCircleTensorByIndex(bias_index); + const auto output = runtime_graph->getCircleTensorByIndex(output_index); + + assert(input != nullptr); + assert(weights != nullptr); + assert(output != nullptr); + + const auto *options = cur_op->builtin_options_as_Conv2DOptions(); + + switch (Tensor::element_type(input)) + { +#ifndef DIS_FLOAT + case DataType::FLOAT32: + if (Tensor::element_type(weights) == DataType::FLOAT32) + { + evalFloat(input, weights, bias, output, options, runtime_graph); + break; + } +#endif // DIS_FLOAT +#ifndef DIS_QUANT + case DataType::U8: + if (Tensor::scales(weights).size() == 1) + { + evalQuantized(input, weights, bias, output, options, runtime_graph); + } + else if (Tensor::scales(weights).size() > 1) + { + LUCI_INTERPRETER_CHECK(Tensor::num_dims(weights) == 4); + LUCI_INTERPRETER_CHECK(Tensor::scales(weights).size() == + static_cast(Tensor::dim(weights, 0))); + evalQuantizedPerChannel(input, weights, bias, output, options, runtime_graph); + } + break; + case DataType::S8: + evalQuantizedS8PerChannel(input, weights, bias, output, options, runtime_graph); + break; + case DataType::S16: + evalQuantizedS16(input, weights, bias, output, options, runtime_graph); + break; +#endif // DIS_QUANT + default: + assert(false && "Unsupported type."); + } +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/Conv2D.test.cpp b/onert-micro/luci-interpreter/src/kernels/Conv2D.test.cpp new file mode 100644 index 0000000..b23f9d4 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Conv2D.test.cpp @@ -0,0 +1,710 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO enable it +#if 0 +#include "kernels/Conv2D.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +class Conv2DTest : public ::testing::Test +{ +protected: + void SetUp() override { _memory_manager = std::make_unique(); } + + std::unique_ptr _memory_manager; +}; + +TEST_F(Conv2DTest, Float) +{ + Shape input_shape{1, 4, 3, 2}; + Shape filter_shape{2, 2, 2, 2}; + Shape bias_shape{2}; + std::vector input_data{ + 1, 2, 3, 4, 5, 6, // row = 0 + 7, 8, 9, 10, 11, 12, // row = 1 + 13, 14, 15, 16, 17, 18, // row = 2 + 19, 20, 21, 22, 23, 24, // row = 3 + }; + std::vector filter_data{ + 1, 2, -3, -4, // out = 0, row = 0 + -5, 6, -7, 8, // out = 1, row = 0 + 4, -2, 3, -1, // out = 0, row = 1 + -8, -6, 7, 5, // out = 1, row = 1 + }; + std::vector bias_data{1, 2}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor filter_tensor = + makeInputTensor(filter_shape, filter_data, _memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, bias_data, _memory_manager.get()); + Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 2; + params.stride_width = 1; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::RELU; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + kernel.configure(); + _memory_manager->allocate_memory(im2col); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + std::vector ref_output_data{ + 11, 16, 7, 20, // row = 0 + 0, 40, 0, 44, // row = 1 + }; + std::vector ref_output_shape{1, 2, 2, 2}; + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(Conv2DTest, FloatPointwise) +{ + Shape input_shape{1, 2, 2, 2}; + Shape filter_shape{2, 1, 1, 2}; + Shape bias_shape{2}; + std::vector input_data{ + 1, 2, // row = 0, col = 0 + 3, 4, // row = 0, col = 1 + 5, 6, // row = 1, col = 0 + 7, 8, // row = 1, col = 1 + }; + std::vector filter_data{ + -1, 2, // out = 0 + -3, 4, // out = 1 + }; + std::vector bias_data{1, 2}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor filter_tensor = + makeInputTensor(filter_shape, filter_data, _memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, bias_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 1; + params.stride_width = 1; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::RELU; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + kernel.configure(); + _memory_manager->allocate_memory(im2col); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + std::vector ref_output_data{ + 4, 7, 6, 9, // row = 0 + 8, 11, 10, 13, // row = 1 + }; + std::vector ref_output_shape{1, 2, 2, 2}; + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(Conv2DTest, FloatCheck) +{ + Shape input_shape{2, 2, 4, 1}; + Shape filter_shape{3, 2, 2, 1}; + Shape bias_shape{3}; + std::vector input_data{ + // First batch + 1, 1, 1, 1, // row = 1 + 2, 2, 2, 2, // row = 2 + // Second batch + 1, 2, 3, 4, // row = 1 + 1, 2, 3, 4, // row = 2 + }; + std::vector filter_data{ + 1, 2, 3, 4, // first 2x2 filter + -1, 1, -1, 1, // second 2x2 filter + -1, -1, 1, 1, // third 2x2 filter + }; + std::vector bias_data{1, 2, 3}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor filter_tensor = + makeInputTensor(filter_shape, filter_data, _memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, bias_data, _memory_manager.get()); + Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 2; + params.stride_width = 2; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::NONE; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(im2col); + kernel.execute(); + + std::vector ref_output_data{ + 18, 2, 5, // first batch, left + 18, 2, 5, // first batch, right + 17, 4, 3, // second batch, left + 37, 4, 3, // second batch, right + }; + std::vector ref_output_shape{2, 1, 2, 3}; + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(Conv2DTest, Uint8) +{ + std::vector input_data{ + // First batch + 1, 1, 1, 1, // row = 1 + 2, 2, 2, 2, // row = 2 + // Second batch + 1, 2, 3, 4, // row = 1 + 1, 2, 3, 4, // row = 2 + }; + std::vector filter_data{ + 1, 2, 3, 4, // first 2x2 filter + -1, 1, -1, 1, // second 2x2 filter + -1, -1, 1, 1, // third 2x2 filter + }; + std::vector bias_data{1, 2, 3}; + + std::pair input_quant_param = quantizationParams(-63.5, 64); + std::pair output_quant_param = quantizationParams(-127, 128); + + Tensor input_tensor = + makeInputTensor({2, 2, 4, 1}, input_quant_param.first, input_quant_param.second, + input_data, _memory_manager.get()); + Tensor filter_tensor = + makeInputTensor({3, 2, 2, 1}, input_quant_param.first, input_quant_param.second, + filter_data, _memory_manager.get()); + Tensor bias_tensor = makeInputTensor( + {3}, input_quant_param.first * input_quant_param.first, 0, bias_data, _memory_manager.get()); + Tensor im2col(DataType::U8, Shape({}), {}, ""); + Tensor output_tensor = + makeOutputTensor(DataType::U8, output_quant_param.first, output_quant_param.second); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 2; + params.stride_width = 2; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::NONE; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(im2col); + kernel.execute(); + + std::vector ref_output_data{ + 18, 2, 5, // first batch, left + 18, 2, 5, // first batch, right + 17, 4, 3, // second batch, left + 37, 4, 3, // second batch, right + }; + std::vector ref_output_shape{2, 1, 2, 3}; + EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(Conv2DTest, Uint8_CWQ) +{ + const int output_channels = 3; + std::vector input_data{ + // First batch + 1, 1, 1, 1, // row = 1 + 2, 2, 2, 2, // row = 2 + // Second batch + 1, 2, 3, 4, // row = 1 + 1, 2, 3, 4, // row = 2 + }; + std::vector filter_data{ + 1, 2, 3, 4, // first 2x2 filter + -1, 1, -1, 1, // second 2x2 filter + -1, -1, 1, 1, // third 2x2 filter + }; + std::vector bias_data{1, 2, 3}; + Shape filter_shape{output_channels, 2, 2, 1}; + + std::pair input_quant_param = quantizationParams(0, 4); + std::pair output_quant_param = quantizationParams(-127, 128); + + std::vector> filter_quant_params; + filter_quant_params.push_back(quantizationParams(0, 4)); + filter_quant_params.push_back(quantizationParams(-1, 1)); + filter_quant_params.push_back(quantizationParams(-1, 1)); + + std::vector filter_scales; + std::vector filter_zerops; + for (auto iter : filter_quant_params) + { + filter_scales.push_back(iter.first); + filter_zerops.push_back(iter.second); + } + + std::vector bias_scales; + for (int i = 0; i < output_channels; ++i) + bias_scales.push_back(filter_quant_params[i].first * input_quant_param.first); + std::vector zerop(output_channels, 0); + + Tensor input_tensor = + makeInputTensor({2, 2, 4, 1}, input_quant_param.first, input_quant_param.second, + input_data, _memory_manager.get()); + Tensor filter_tensor = makeInputTensor(filter_shape, filter_scales, filter_zerops, + 0, filter_data, _memory_manager.get()); + Tensor bias_tensor = makeInputTensor({output_channels}, bias_scales, zerop, 0, + bias_data, _memory_manager.get()); + Tensor im2col(DataType::U8, Shape({}), {}, ""); + Tensor output_tensor = + makeOutputTensor(DataType::U8, output_quant_param.first, output_quant_param.second); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 2; + params.stride_width = 2; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::NONE; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(im2col); + kernel.execute(); + + std::vector ref_output_data{ + 18, 2, 5, // first batch, left + 18, 2, 5, // first batch, right + 17, 4, 3, // second batch, left + 37, 4, 3, // second batch, right + }; + std::vector ref_output_shape{2, 1, 2, 3}; + EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(Conv2DTest, SInt8_CWQ) +{ + const int output_channels = 3; + std::vector input_data{ + // First batch + 1, 1, 1, 1, // row = 1 + 2, 2, 2, 2, // row = 2 + // Second batch + 1, 2, 3, 4, // row = 1 + 1, 2, 3, 4, // row = 2 + }; + std::vector filter_data{ + 1, 2, 3, 4, // first 2x2 filter + -1, 1, -1, 1, // second 2x2 filter + -1, -1, 1, 1, // third 2x2 filter + }; + std::vector bias_data{1, 2, 3}; + Shape filter_shape{output_channels, 2, 2, 1}; + + std::pair input_quant_param = quantizationParams(0, 4); + std::pair output_quant_param = quantizationParams(-127, 128); + + std::vector> filter_quant_params; + filter_quant_params.push_back(std::pair(0.5, 0)); + filter_quant_params.push_back(std::pair(0.25, 0)); + filter_quant_params.push_back(std::pair(0.125, 0)); + + std::vector filter_scales; + std::vector filter_zerops; + for (auto iter : filter_quant_params) + { + filter_scales.push_back(iter.first); + filter_zerops.push_back(iter.second); + } + + std::vector bias_scales; + for (int i = 0; i < output_channels; ++i) + bias_scales.push_back(filter_quant_params[i].first * input_quant_param.first); + std::vector zerop(output_channels, 0); + + Tensor input_tensor = + makeInputTensor({2, 2, 4, 1}, input_quant_param.first, input_quant_param.second, + input_data, _memory_manager.get()); + Tensor filter_tensor = makeInputTensor(filter_shape, filter_scales, filter_zerops, + 0, filter_data, _memory_manager.get()); + Tensor bias_tensor = makeInputTensor({output_channels}, bias_scales, zerop, 0, + bias_data, _memory_manager.get()); + Tensor im2col(DataType::S8, Shape({}), {}, ""); + Tensor output_tensor = + makeOutputTensor(DataType::S8, output_quant_param.first, output_quant_param.second); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 2; + params.stride_width = 2; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::NONE; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(im2col); + kernel.execute(); + + std::vector ref_output_data{ + 18, 2, 5, // first batch, left + 18, 2, 5, // first batch, right + 17, 4, 3, // second batch, left + 37, 4, 3, // second batch, right + }; + std::vector ref_output_shape{2, 1, 2, 3}; + EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(Conv2DTest, SInt16) +{ + Shape input_shape{1, 4, 3, 2}; + Shape filter_shape{2, 2, 2, 2}; + Shape bias_shape{2}; + std::vector ref_output_shape{1, 2, 2, 2}; + + std::vector input_data{ + 1, 2, 3, 4, 5, 6, // row = 0 + 7, 8, 9, 10, 11, 12, // row = 1 + 13, 14, 15, 16, 17, 18, // row = 2 + 19, 20, 21, 22, 23, 24, // row = 3 + }; + std::vector filter_data{ + 1, 2, -3, -4, // out = 0, row = 0 + -5, 6, -7, 8, // out = 1, row = 0 + 4, -2, 3, -1, // out = 0, row = 1 + -8, -6, 7, 5, // out = 1, row = 1 + }; + std::vector bias_data{1, 2}; + std::vector ref_output_data{ + 11, 16, 7, 20, // row = 0 + 0, 40, 0, 44, // row = 1 + }; + + Tensor input_tensor = + makeInputTensor(input_shape, 0.25, 0, input_data, _memory_manager.get()); + Tensor filter_tensor = + makeInputTensor(filter_shape, 0.2, 0, filter_data, _memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, 0.25 * 0.2, 0, bias_data, _memory_manager.get()); + Tensor im2col(DataType::S16, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::S16, 0.5, 0); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 2; + params.stride_width = 1; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::RELU; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(im2col); + kernel.execute(); + + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); + EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); +} + +TEST_F(Conv2DTest, SInt16_CWQ_weights) +{ + Shape input_shape{1, 2, 2, 2}; // Batch x H x W x C + Shape filter_shape{3, 1, 1, 2}; // Out channels x H x W x In Channels + Shape bias_shape{3}; + std::vector ref_output_shape{1, 2, 2, 3}; + + std::vector input_data{ + 1, 2, // row = 0, col 0 + 3, 4, // row = 0, col 1 + 5, 6, // row = 1, col 0 + 7, 8, // row = 1, col 1 + }; + std::vector filter_data{ + 4, -3, // out = 0 + 1, -3, // out = 1 + 5, -3, // out = 2 + }; + std::vector bias_data{1, 10, 5}; + std::vector ref_output_data{ + 0, 5, 4, // row 0, col 0 + 1, 1, 8, // row 0, col 1 + 3, 0, 12, // row 1, col 0 + 5, 0, 16, // row 1, col 1 + }; + + float input_scale = 0.25f; + float output_scale = 0.05f; + std::vector filter_scales = {0.25f, 0.2f, 0.1f}; + std::vector bias_scales; + for (int i = 0; i < filter_scales.size(); ++i) + bias_scales.push_back(filter_scales[i] * input_scale); + std::vector zerop = {0, 0, 0}; + + Tensor input_tensor = + makeInputTensor(input_shape, input_scale, 0, input_data, _memory_manager.get()); + Tensor filter_tensor = makeInputTensor(filter_shape, filter_scales, zerop, 0, + filter_data, _memory_manager.get()); + Tensor bias_tensor = makeInputTensor(bias_shape, bias_scales, zerop, 0, bias_data, + _memory_manager.get()); + Tensor im2col(DataType::S16, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::S16, output_scale, 0); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 1; + params.stride_width = 1; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::RELU; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(im2col); + kernel.execute(); + + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); + EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); +} + +TEST_F(Conv2DTest, Unsupported_Type_Configure_NEG) +{ + Shape input_shape{1, 4, 3, 2}; + Shape filter_shape{2, 2, 2, 2}; + Shape bias_shape{2}; + std::vector input_data{ + 1, 2, 3, 4, 5, 6, // row = 0 + 7, 8, 9, 10, 11, 12, // row = 1 + 13, 14, 15, 16, 17, 18, // row = 2 + 19, 20, 21, 22, 23, 24, // row = 3 + }; + std::vector filter_data{ + 1, 2, -3, -4, // out = 0, row = 0 + -5, 6, -7, 8, // out = 1, row = 0 + 4, -2, 3, -1, // out = 0, row = 1 + -8, -6, 7, 5, // out = 1, row = 1 + }; + std::vector bias_data{1, 2}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor filter_tensor = + makeInputTensor(filter_shape, filter_data, _memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, bias_data, _memory_manager.get()); + Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 2; + params.stride_width = 1; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::RELU; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(Conv2DTest, Invalid_Bias_Type_NEG) +{ + Shape input_shape{1, 4, 3, 2}; + Shape filter_shape{2, 2, 2, 2}; + Shape bias_shape{2}; + std::vector input_data{ + 1, 2, 3, 4, 5, 6, // row = 0 + 7, 8, 9, 10, 11, 12, // row = 1 + 13, 14, 15, 16, 17, 18, // row = 2 + 19, 20, 21, 22, 23, 24, // row = 3 + }; + std::vector filter_data{ + 1, 2, -3, -4, // out = 0, row = 0 + -5, 6, -7, 8, // out = 1, row = 0 + 4, -2, 3, -1, // out = 0, row = 1 + -8, -6, 7, 5, // out = 1, row = 1 + }; + std::vector bias_data{1, 2}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor filter_tensor = + makeInputTensor(filter_shape, filter_data, _memory_manager.get()); + Tensor bias_tensor = makeInputTensor(bias_shape, bias_data, _memory_manager.get()); + Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 2; + params.stride_width = 1; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::RELU; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(Conv2DTest, Invalid_Bias_Data_NEG) +{ + Shape input_shape{1, 4, 3, 2}; + Shape filter_shape{2, 2, 2, 2}; + Shape bias_shape{3}; + std::vector input_data{ + 1, 2, 3, 4, 5, 6, // row = 0 + 7, 8, 9, 10, 11, 12, // row = 1 + 13, 14, 15, 16, 17, 18, // row = 2 + 19, 20, 21, 22, 23, 24, // row = 3 + }; + std::vector filter_data{ + 1, 2, -3, -4, // out = 0, row = 0 + -5, 6, -7, 8, // out = 1, row = 0 + 4, -2, 3, -1, // out = 0, row = 1 + -8, -6, 7, 5, // out = 1, row = 1 + }; + std::vector bias_data{1, 2, 3}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor filter_tensor = + makeInputTensor(filter_shape, filter_data, _memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, bias_data, _memory_manager.get()); + Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 2; + params.stride_width = 1; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::RELU; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(Conv2DTest, Invalid_Input_Shape_NEG) +{ + Shape input_shape{1, 4, 6, 1}; + Shape filter_shape{2, 2, 2, 2}; + Shape bias_shape{2}; + std::vector input_data{ + 1, 2, 3, 4, 5, 6, // row = 0 + 7, 8, 9, 10, 11, 12, // row = 1 + 13, 14, 15, 16, 17, 18, // row = 2 + 19, 20, 21, 22, 23, 24, // row = 3 + }; + std::vector filter_data{ + 1, 2, -3, -4, // out = 0, row = 0 + -5, 6, -7, 8, // out = 1, row = 0 + 4, -2, 3, -1, // out = 0, row = 1 + -8, -6, 7, 5, // out = 1, row = 1 + }; + std::vector bias_data{1, 2}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor filter_tensor = + makeInputTensor(filter_shape, filter_data, _memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, bias_data, _memory_manager.get()); + Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 2; + params.stride_width = 1; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::RELU; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(Conv2DTest, Invalid_fused_act_tanh_NEG) +{ + Shape input_shape{1, 4, 3, 2}; + Shape filter_shape{2, 2, 2, 2}; + Shape bias_shape{2}; + std::vector input_data{ + 1, 2, 3, 4, 5, 6, // row = 0 + 7, 8, 9, 10, 11, 12, // row = 1 + 13, 14, 15, 16, 17, 18, // row = 2 + 19, 20, 21, 22, 23, 24, // row = 3 + }; + std::vector filter_data{ + 1, 2, -3, -4, // out = 0, row = 0 + -5, 6, -7, 8, // out = 1, row = 0 + 4, -2, 3, -1, // out = 0, row = 1 + -8, -6, 7, 5, // out = 1, row = 1 + }; + std::vector bias_data{1, 2}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor filter_tensor = + makeInputTensor(filter_shape, filter_data, _memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, bias_data, _memory_manager.get()); + Tensor im2col(DataType::FLOAT32, Shape({}), {}, ""); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Conv2DParams params{}; + params.padding = Padding::VALID; + params.stride_height = 2; + params.stride_width = 1; + params.dilation_height_factor = 1; + params.dilation_width_factor = 1; + params.activation = Activation::TANH; + + Conv2D kernel(&input_tensor, &filter_tensor, &bias_tensor, &output_tensor, &im2col, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter +#endif diff --git a/onert-micro/luci-interpreter/src/kernels/DepthToSpace.cpp b/onert-micro/luci-interpreter/src/kernels/DepthToSpace.cpp new file mode 100644 index 0000000..937958b --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/DepthToSpace.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "DepthToSpace.h" +#include "Utils.h" +#include "PALDepthToSpace.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +DepthToSpace::DepthToSpace(const Tensor *input, Tensor *output, const DepthToSpaceParams ¶ms) + : KernelWithParams({input}, {output}, params) +{ +} + +void DepthToSpace::configure() +{ + LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::FLOAT32 || + output()->element_type() == DataType::U8) + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()) + const int block_size = params().block_size; + const int32_t input_height = input()->shape().dim(1); + const int32_t input_width = input()->shape().dim(2); + const int32_t input_channels = input()->shape().dim(3); + int32_t output_height = input_height * block_size; + int32_t output_width = input_width * block_size; + int32_t output_channels = input_channels / block_size / block_size; + + LUCI_INTERPRETER_CHECK(input_height == output_height / block_size); + LUCI_INTERPRETER_CHECK(input_width == output_width / block_size); + LUCI_INTERPRETER_CHECK(input_channels == output_channels * block_size * block_size); + + Shape output_shape(4); + output_shape.dim(0) = input()->shape().dim(0); + output_shape.dim(1) = output_height; + output_shape.dim(2) = output_width; + output_shape.dim(3) = output_channels; + + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void DepthToSpace::execute() const +{ + tflite::DepthToSpaceParams op_params; + op_params.block_size = params().block_size; + switch (input()->element_type()) + { + case DataType::FLOAT32: + luci_interpreter_pal::DepthToSpace(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + case DataType::U8: + luci_interpreter_pal::DepthToSpace(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + default: + assert(false && "Unsupported Type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/DepthToSpace.h b/onert-micro/luci-interpreter/src/kernels/DepthToSpace.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/DepthToSpace.h rename to onert-micro/luci-interpreter/src/kernels/DepthToSpace.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/DepthToSpace.test.cpp b/onert-micro/luci-interpreter/src/kernels/DepthToSpace.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/DepthToSpace.test.cpp rename to onert-micro/luci-interpreter/src/kernels/DepthToSpace.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/DepthwiseConv2D.cpp b/onert-micro/luci-interpreter/src/kernels/DepthwiseConv2D.cpp new file mode 100644 index 0000000..201eaf3 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/DepthwiseConv2D.cpp @@ -0,0 +1,450 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/DepthwiseConv2D.h" + +#include "kernels/Utils.h" + +#include "PALDepthwiseConv2d.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +DepthwiseConv2D::DepthwiseConv2D(const Tensor *input, const Tensor *filter, const Tensor *bias, + Tensor *output, Tensor *scratchpad, + const DepthwiseConv2DParams ¶ms) + : KernelWithParams({input, filter, bias}, {output, scratchpad}, params) +{ +} + +void DepthwiseConv2D::configure() +{ + // TensorFlow Lite (as of v2.2.0) supports the following combinations of types: + // | input filter bias output | + // ----+---------------------------+ + // (1) | float float float float | + // (2) | float int8 float float | hybrid + // (3) | uint8 uint8 int32 uint8 | quantized + // (4) | int8 int8 int32 int8 | quantized per channel + // (5) | int16 int8 int64 int16 | quantized per channel 16x8 + // + // We only support (1), (3) and (4) for now, and additionally the following: + // | input filter bias output | + // ----+---------------------------+ + // (5) | int16 int16 int64 int16 | + // + if (input()->element_type() == DataType::FLOAT32 && filter()->element_type() == DataType::FLOAT32) + { + LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::FLOAT32); + } + else if (input()->element_type() == DataType::U8 && filter()->element_type() == DataType::U8) + { + LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::S32); + } + else if (input()->element_type() == DataType::S8 && filter()->element_type() == DataType::S8) + { + LUCI_INTERPRETER_CHECK(filter()->shape().num_dims() == 4); + LUCI_INTERPRETER_CHECK(static_cast(filter()->shape().dim(3)) == + filter()->scales().size()); + for (auto zerop : filter()->zero_points()) + { + LUCI_INTERPRETER_CHECK(zerop == 0); + } + LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::S32); + } + else if (input()->element_type() == DataType::S16 && filter()->element_type() == DataType::S16) + { + LUCI_INTERPRETER_CHECK(bias() == nullptr || bias()->element_type() == DataType::S64); + } + else + { + assert(false && "Unsupported type."); + } + LUCI_INTERPRETER_CHECK(output()->element_type() == input()->element_type()); + + const Shape &input_shape = input()->shape(); + const Shape &filter_shape = filter()->shape(); + LUCI_INTERPRETER_CHECK(input_shape.num_dims() == 4 && filter_shape.num_dims() == 4); + + const int32_t batches = input_shape.dim(0); + const int32_t input_height = input_shape.dim(1); + const int32_t input_width = input_shape.dim(2); + // Filter format: [1, H, W, O]. + LUCI_INTERPRETER_CHECK(filter_shape.dim(0) == 1); + const int32_t filter_height = filter_shape.dim(1); + const int32_t filter_width = filter_shape.dim(2); + const int32_t channels_out = filter_shape.dim(3); + + LUCI_INTERPRETER_CHECK(bias() == nullptr || (bias()->shape().num_dims() == 1 && + bias()->shape().dim(0) == channels_out)); + + const int32_t output_height = + computeOutputSize(_params.padding, input_height, filter_height, _params.stride_height, + _params.dilation_height_factor); + const int32_t output_width = + computeOutputSize(_params.padding, input_width, filter_width, _params.stride_width, + _params.dilation_width_factor); + + _padding_height = computePadding(_params.stride_height, _params.dilation_height_factor, + input_height, filter_height, output_height); + _padding_width = computePadding(_params.stride_width, _params.dilation_width_factor, input_width, + filter_width, output_width); + + // TODO: enable it only if kernel with dynamic shapes + output()->resize({batches, output_height, output_width, channels_out}); + + tflite::DepthwiseParams params{}; + + params.dilation_height_factor = _params.dilation_height_factor; + params.dilation_width_factor = _params.dilation_width_factor; + + auto scratchpad = getOutputTensors()[1]; + luci_interpreter_pal::SetupScratchpadTensor(scratchpad, params, input()->element_type(), + getTensorShape(input()), getTensorShape(filter()), + getTensorShape(output())); +} + +void DepthwiseConv2D::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + if (filter()->element_type() == DataType::FLOAT32) + { + evalFloat(); + break; + } + assert(false && "Unsupported type."); + case DataType::U8: + if (filter()->scales().size() == 1) + { + evalQuantized(); + } + else if (filter()->scales().size() > 1) + { + LUCI_INTERPRETER_CHECK(filter()->shape().num_dims() == 4); + LUCI_INTERPRETER_CHECK(filter()->scales().size() == + static_cast(filter()->shape().dim(3))); + evalQuantizedPerChannel(); + } + break; + case DataType::S8: + evalQuantizedS8PerChannel(); + break; + case DataType::S16: + evalQuantizedS16(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void DepthwiseConv2D::evalFloat() const +{ + float activation_min{}; + float activation_max{}; + calculateActivationRange(_params.activation, &activation_min, &activation_max); + + tflite::DepthwiseParams params{}; + params.padding_values.height = _padding_height; + params.padding_values.width = _padding_width; + params.stride_height = _params.stride_height; + params.stride_width = _params.stride_width; + params.dilation_height_factor = _params.dilation_height_factor; + params.dilation_width_factor = _params.dilation_width_factor; + params.depth_multiplier = _params.depth_multiplier; + params.float_activation_min = activation_min; + params.float_activation_max = activation_max; + + tflite::reference_ops::DepthwiseConv( + params, getTensorShape(input()), getTensorData(input()), getTensorShape(filter()), + getTensorData(filter()), getTensorShape(bias()), getTensorData(bias()), + getTensorShape(output()), getTensorData(output())); +} + +void DepthwiseConv2D::evalQuantizedPerChannel() const +{ + const auto *input_data = getTensorData(input()); + const auto *filter_data = getTensorData(filter()); + const auto *bias_data = getTensorData(bias()); + auto *output_data = getTensorData(output()); + + const Shape &input_shape = input()->shape(); + const Shape &filter_shape = filter()->shape(); + const Shape &output_shape = output()->shape(); + + const int32_t batches = input_shape.dim(0); + const int32_t input_height = input_shape.dim(1); + const int32_t input_width = input_shape.dim(2); + const int32_t input_depth = input_shape.dim(3); + const int32_t filter_height = filter_shape.dim(1); + const int32_t filter_width = filter_shape.dim(2); + const int32_t output_height = output_shape.dim(1); + const int32_t output_width = output_shape.dim(2); + + const int32_t stride_height = _params.stride_height; + const int32_t stride_width = _params.stride_width; + const int32_t dilation_height_factor = _params.dilation_height_factor; + const int32_t dilation_width_factor = _params.dilation_width_factor; + const int32_t depth_multiplier = _params.depth_multiplier; + + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + + const std::vector effective_output_scales = + getQuantizedConvolutionMultiplers(input()->scale(), filter()->scales(), output()->scale()); + + std::vector quant_multipliers_raw = + quantizeMultipliers(effective_output_scales); + BroadcastableWrapper quant_multipliers(quant_multipliers_raw); + + for (int batch = 0; batch < batches; ++batch) + { + for (int out_y = 0; out_y < output_height; ++out_y) + { + for (int out_x = 0; out_x < output_width; ++out_x) + { + for (int in_channel = 0; in_channel < input_depth; ++in_channel) + { + for (int m = 0; m < depth_multiplier; ++m) + { + const int output_channel = m + in_channel * depth_multiplier; + const int in_x_origin = (out_x * stride_width) - _padding_width; + const int in_y_origin = (out_y * stride_height) - _padding_height; + int32_t acc = 0; + for (int filter_y = 0; filter_y < filter_height; ++filter_y) + { + for (int filter_x = 0; filter_x < filter_width; ++filter_x) + { + const int in_x = in_x_origin + dilation_width_factor * filter_x; + const int in_y = in_y_origin + dilation_height_factor * filter_y; + // Zero padding by omitting the areas outside the image. + const bool is_point_inside_image = + (in_x >= 0) && (in_x < input_width) && (in_y >= 0) && (in_y < input_height); + if (is_point_inside_image) + { + int32_t input_val = + input_data[calcOffset(input_shape, batch, in_y, in_x, in_channel)]; + int32_t filter_val = + filter_data[calcOffset(filter_shape, 0, filter_y, filter_x, output_channel)]; + acc += (filter_val - filter()->zero_points()[output_channel]) * + (input_val - input()->zero_point()); + } + } + } + if (bias_data) + { + acc += bias_data[output_channel]; + } + int32_t output_multiplier = quant_multipliers[output_channel].multiplier; + int output_shift = quant_multipliers[output_channel].shift; + int32_t scaled_acc = + tflite::MultiplyByQuantizedMultiplier(acc, output_multiplier, output_shift); + scaled_acc += output()->zero_point(); + scaled_acc = std::max(scaled_acc, activation_min); + scaled_acc = std::min(scaled_acc, activation_max); + output_data[calcOffset(output_shape, batch, out_y, out_x, output_channel)] = + static_cast(scaled_acc); + } + } + } + } + } +} + +void DepthwiseConv2D::evalQuantized() const +{ + const auto input_scale = static_cast(input()->scale()); + const auto filter_scale = static_cast(filter()->scale()); + const auto output_scale = static_cast(output()->scale()); + + const double real_multiplier = input_scale * filter_scale / output_scale; + int32_t output_multiplier{}; + int output_shift{}; + quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); + + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + + tflite::DepthwiseParams params{}; + params.padding_values.height = _padding_height; + params.padding_values.width = _padding_width; + params.stride_height = _params.stride_height; + params.stride_width = _params.stride_width; + params.dilation_height_factor = _params.dilation_height_factor; + params.dilation_width_factor = _params.dilation_width_factor; + params.depth_multiplier = _params.depth_multiplier; + // The kernel expects input and filter zero points to be negated. + params.input_offset = -input()->zero_point(); // Note the '-'. + params.weights_offset = -filter()->zero_point(); // Note the '-'. + params.output_offset = output()->zero_point(); + params.output_multiplier = output_multiplier; + params.output_shift = output_shift; + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + tflite::reference_ops::DepthwiseConv( + params, getTensorShape(input()), getTensorData(input()), getTensorShape(filter()), + getTensorData(filter()), getTensorShape(bias()), getTensorData(bias()), + getTensorShape(output()), getTensorData(output())); +} + +void DepthwiseConv2D::evalQuantizedS8PerChannel() const +{ + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + + tflite::DepthwiseParams params{}; + + params.padding_type = tflite::PaddingType::kSame; + params.padding_values.height = _padding_height; + params.padding_values.width = _padding_width; + params.stride_height = _params.stride_height; + params.stride_width = _params.stride_width; + params.dilation_height_factor = _params.dilation_height_factor; + params.dilation_width_factor = _params.dilation_width_factor; + params.depth_multiplier = _params.depth_multiplier; + // The kernel expects input and filter zero points to be negated. + params.input_offset = -input()->zero_point(); // Note the '-'. + params.weights_offset = 0; + params.output_offset = output()->zero_point(); + params.output_multiplier = 1; // unused in tflite code + params.output_shift = 0; // unused in tflite code + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + const std::vector effective_output_scales = + getQuantizedConvolutionMultiplers(input()->scale(), filter()->scales(), output()->scale()); + + std::vector quant_multipliers = + quantizeMultipliers(effective_output_scales); + + std::vector shifts; + std::transform(quant_multipliers.begin(), quant_multipliers.end(), std::back_inserter(shifts), + [](ChannelQuantMultipliers cm) { return cm.shift; }); + std::vector multipliers; + std::transform(quant_multipliers.begin(), quant_multipliers.end(), + std::back_inserter(multipliers), + [](ChannelQuantMultipliers cm) { return cm.multiplier; }); + + auto scratchpad = getOutputTensors()[1]; + int8_t *scratchpad_data = nullptr; + if (scratchpad->is_allocatable()) + scratchpad_data = scratchpad->data(); + + luci_interpreter_pal::DepthwiseConvPerChannel( + params, multipliers.data(), shifts.data(), getTensorShape(input()), + getTensorData(input()), getTensorShape(filter()), getTensorData(filter()), + getTensorShape(bias()), getTensorData(bias()), getTensorShape(output()), + getTensorData(output()), getTensorShape(scratchpad), scratchpad_data); +} + +void DepthwiseConv2D::evalQuantizedS16() const +{ + const auto *input_data = getTensorData(input()); + const auto *filter_data = getTensorData(filter()); + const auto *bias_data = getTensorData(bias()); + auto *output_data = getTensorData(output()); + + const Shape &input_shape = input()->shape(); + const Shape &filter_shape = filter()->shape(); + const Shape &output_shape = output()->shape(); + + const int32_t batches = input_shape.dim(0); + const int32_t input_height = input_shape.dim(1); + const int32_t input_width = input_shape.dim(2); + const int32_t input_depth = input_shape.dim(3); + const int32_t filter_height = filter_shape.dim(1); + const int32_t filter_width = filter_shape.dim(2); + const int32_t output_height = output_shape.dim(1); + const int32_t output_width = output_shape.dim(2); + + const int32_t stride_height = _params.stride_height; + const int32_t stride_width = _params.stride_width; + const int32_t dilation_height_factor = _params.dilation_height_factor; + const int32_t dilation_width_factor = _params.dilation_width_factor; + const int32_t depth_multiplier = _params.depth_multiplier; + + const std::vector effective_output_scales = + getQuantizedConvolutionMultiplers(input()->scale(), filter()->scales(), output()->scale()); + + std::vector quant_multipliers_raw = + quantizeMultipliers(effective_output_scales); + + BroadcastableWrapper quant_multipliers(quant_multipliers_raw); + + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + + for (int32_t batch = 0; batch < batches; ++batch) + { + for (int32_t out_y = 0; out_y < output_height; ++out_y) + { + for (int32_t out_x = 0; out_x < output_width; ++out_x) + { + for (int32_t in_c = 0; in_c < input_depth; ++in_c) + { + for (int32_t m = 0; m < depth_multiplier; ++m) + { + const int32_t out_c = m + in_c * depth_multiplier; + const int32_t in_y_origin = out_y * stride_height - _padding_height; + const int32_t in_x_origin = out_x * stride_width - _padding_width; + int64_t acc = 0; + for (int32_t filter_y = 0; filter_y < filter_height; ++filter_y) + { + for (int32_t filter_x = 0; filter_x < filter_width; ++filter_x) + { + const int32_t in_y = in_y_origin + dilation_height_factor * filter_y; + const int32_t in_x = in_x_origin + dilation_width_factor * filter_x; + if ((in_y >= 0 && in_y < input_height) && (in_x >= 0 && in_x < input_width)) + { + const int16_t input_val = + input_data[calcOffset(input_shape, batch, in_y, in_x, in_c)]; + const int16_t filter_val = + filter_data[calcOffset(filter_shape, 0, filter_y, filter_x, out_c)]; + acc += static_cast(input_val) * static_cast(filter_val); + } + } + } + if (bias_data != nullptr) + { + acc += bias_data[out_c]; + } + + int32_t output_multiplier = quant_multipliers[out_c].multiplier; + int output_shift = quant_multipliers[out_c].shift; + int32_t scaled_acc = + tflite::MultiplyByQuantizedMultiplier(acc, output_multiplier, output_shift); + + scaled_acc = std::max(scaled_acc, activation_min); + scaled_acc = std::min(scaled_acc, activation_max); + + output_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] = scaled_acc; + } + } + } + } + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/DepthwiseConv2D.h b/onert-micro/luci-interpreter/src/kernels/DepthwiseConv2D.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/DepthwiseConv2D.h rename to onert-micro/luci-interpreter/src/kernels/DepthwiseConv2D.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/DepthwiseConv2D.test.cpp b/onert-micro/luci-interpreter/src/kernels/DepthwiseConv2D.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/DepthwiseConv2D.test.cpp rename to onert-micro/luci-interpreter/src/kernels/DepthwiseConv2D.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Dequantize.cpp b/onert-micro/luci-interpreter/src/kernels/Dequantize.cpp new file mode 100644 index 0000000..a097342 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Dequantize.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Dequantize.h" +#include "kernels/Utils.h" +#include "PALDequantize.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +Dequantize::Dequantize(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Dequantize::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == DataType::S8 || + input()->element_type() == DataType::U8 || + input()->element_type() == DataType::S16); + + LUCI_INTERPRETER_CHECK(input()->scales().size() == 1); + + if (input()->element_type() == DataType::S16) + LUCI_INTERPRETER_CHECK(input()->zero_point() == 0); + + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::FLOAT32); + + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Dequantize::execute() const +{ + tflite::DequantizationParams op_params; + op_params.zero_point = input()->zero_point(); + op_params.scale = input()->scale(); + + switch (input()->element_type()) + { + case DataType::U8: + { + luci_interpreter_pal::Dequantize(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + } + case DataType::S8: + { + luci_interpreter_pal::Dequantize(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + } + case DataType::S16: + { + luci_interpreter_pal::Dequantize(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + } + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Dequantize.h b/onert-micro/luci-interpreter/src/kernels/Dequantize.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Dequantize.h rename to onert-micro/luci-interpreter/src/kernels/Dequantize.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Dequantize.test.cpp b/onert-micro/luci-interpreter/src/kernels/Dequantize.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Dequantize.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Dequantize.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Div.cpp b/onert-micro/luci-interpreter/src/kernels/Div.cpp new file mode 100644 index 0000000..efa90f8 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Div.cpp @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Div.h" + +#include "kernels/Utils.h" + +#include +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +Div::Div(const Tensor *input1, const Tensor *input2, Tensor *output, const DivParams ¶ms) + : KernelWithParams({input1, input2}, {output}, params) +{ +} + +void Div::configure() +{ + LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); + LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()); + + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); +} + +void Div::execute() const +{ + switch (input1()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::S64: + evalInteger(); + break; + case DataType::S32: + evalInteger(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Div::evalFloat() const +{ + tflite::ArithmeticParams params{}; + fillArithmeticActivationRange(params, _params.activation); + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + tflite::reference_ops::BroadcastDivSlow( + params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), + getTensorData(input2()), getTensorShape(output()), getTensorData(output())); + } + else + { + tflite::reference_ops::Div(params, getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +template void Div::evalInteger() const +{ + tflite::ArithmeticParams params{}; + fillArithmeticActivationRange(params, _params.activation); + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + tflite::reference_ops::BroadcastDivSlow( + params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), + getTensorData(input2()), getTensorShape(output()), getTensorData(output())); + } + else + { + tflite::reference_ops::Div(params, getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +void Div::evalQuantized() const +{ + const auto input1_scale = static_cast(input1()->scale()); + const auto input2_scale = static_cast(input2()->scale()); + const auto output_scale = static_cast(output()->scale()); + + const double real_output_multiplier = input1_scale / (input2_scale * output_scale); + + int32_t output_multiplier{}; + int output_shift{}; + + quantizeMultiplier(real_output_multiplier, &output_multiplier, &output_shift); + + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + + tflite::ArithmeticParams params{}; + + params.input1_offset = -input1()->zero_point(); // Note the '-'. + params.input2_offset = -input2()->zero_point(); // Note the '-'. + params.output_offset = output()->zero_point(); + params.output_multiplier = output_multiplier; + params.output_shift = output_shift; + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + tflite::reference_ops::BroadcastDivSlow( + params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), + getTensorData(input2()), getTensorShape(output()), getTensorData(output())); + } + else + { + tflite::reference_ops::Div(params, getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Div.h b/onert-micro/luci-interpreter/src/kernels/Div.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Div.h rename to onert-micro/luci-interpreter/src/kernels/Div.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Div.test.cpp b/onert-micro/luci-interpreter/src/kernels/Div.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Div.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Div.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Elu.cpp b/onert-micro/luci-interpreter/src/kernels/Elu.cpp new file mode 100644 index 0000000..0f0df02 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Elu.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Elu.h" +#include "kernels/Utils.h" + +#include "PALElu.h" + +namespace luci_interpreter +{ + +namespace kernels +{ + +Elu::Elu(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Elu::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Elu::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + luci_interpreter_pal::Elu(getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Elu.h b/onert-micro/luci-interpreter/src/kernels/Elu.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Elu.h rename to onert-micro/luci-interpreter/src/kernels/Elu.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Elu.test.cpp b/onert-micro/luci-interpreter/src/kernels/Elu.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Elu.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Elu.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Equal.cpp b/onert-micro/luci-interpreter/src/kernels/Equal.cpp new file mode 100644 index 0000000..b7cf8e5 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Equal.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Equal.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +Equal::Equal(const Tensor *x, const Tensor *y, Tensor *output) : Kernel({x, y}, {output}) {} + +void Equal::configure() +{ + LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); + + if (x()->element_type() == DataType::U8) + { + quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); + quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); +} + +void Equal::execute() const +{ + switch (x()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::S64: + evalInteger(); + break; + case DataType::S32: + evalInteger(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Equal::evalFloat() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowEqual(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::Equal(op_params, getTensorShape(x()), x_data, getTensorShape(y()), + y_data, getTensorShape(output()), output_data); + } +} + +template void Equal::evalInteger() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowEqualNoScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::EqualNoScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } +} + +void Equal::evalQuantized() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.left_shift = 8; + op_params.input1_offset = -x()->zero_point(); // Note the '-' + op_params.input1_shift = _x_shift; + op_params.input1_multiplier = _x_multiplier; + op_params.input2_offset = -y()->zero_point(); // Note the '-' + op_params.input2_shift = _y_shift; + op_params.input2_multiplier = _y_multiplier; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowEqualWithScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::EqualWithScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Equal.h b/onert-micro/luci-interpreter/src/kernels/Equal.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Equal.h rename to onert-micro/luci-interpreter/src/kernels/Equal.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Equal.test.cpp b/onert-micro/luci-interpreter/src/kernels/Equal.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Equal.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Equal.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Exp.cpp b/onert-micro/luci-interpreter/src/kernels/Exp.cpp new file mode 100644 index 0000000..ceb54e7 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Exp.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2018 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Exp.h" + +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +Exp::Exp(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Exp::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Exp::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Exp::evalFloat() const +{ + const int size = tflite::MatchingFlatSize(getTensorShape(input()), getTensorShape(output())); + tflite::reference_ops::Exp(getTensorData(input()), size, getTensorData(output())); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Exp.h b/onert-micro/luci-interpreter/src/kernels/Exp.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Exp.h rename to onert-micro/luci-interpreter/src/kernels/Exp.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Exp.test.cpp b/onert-micro/luci-interpreter/src/kernels/Exp.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Exp.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Exp.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/ExpandDims.cpp b/onert-micro/luci-interpreter/src/kernels/ExpandDims.cpp new file mode 100644 index 0000000..6c7e20b --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/ExpandDims.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" +#include "kernels/Utils.h" + +namespace luci_interpreter +{ + +void configure_kernel_CircleExpandDims(const circle::Operator *cur_op, + BaseRuntimeGraph *runtime_graph) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto axis_index = cur_op->inputs()->operator[](1); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(axis_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + const auto axis = runtime_graph->getCircleTensorByIndex(axis_index); + auto output = runtime_graph->getCircleTensorByIndex(output_index); + + assert(input != nullptr); + assert(axis != nullptr); + assert(output != nullptr); + + auto axis_data = runtime_graph->getConstDataByTensor(axis); + + int32_t axis_value; + + switch (Tensor::element_type(axis)) + { + case DataType::S32: + axis_value = *reinterpret_cast(axis_data); + break; + case DataType::S64: + axis_value = static_cast(*reinterpret_cast(axis_data)); + break; + default: + assert(false && "Unsupported type."); + } + + if (axis_value < 0) + { + axis_value += Tensor::num_dims(input) + 1; + } + + LUCI_INTERPRETER_CHECK(axis_value <= Tensor::num_dims(input) and axis_value >= 0); +} + +void execute_kernel_CircleExpandDims(const circle::Operator *cur_op, + BaseRuntimeGraph *runtime_graph, bool is_inplace) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + const auto output = runtime_graph->getCircleTensorByIndex(output_index); + + if (is_inplace) + { + runtime_graph->makeInplaceOperation(input, output); + return; + } + + // Just copy input to output + const auto input_data = runtime_graph->getDataByTensor(input); + auto output_data = runtime_graph->getDataByTensor(output); + + assert(input_data != nullptr); + assert(output_data != nullptr); + + const size_t element_size = getDataTypeSize(Tensor::element_type(input)); + const int32_t num_elements = Tensor::num_elements(input); + std::memcpy(output_data, input_data, num_elements * element_size); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/ExpandDims.test.cpp b/onert-micro/luci-interpreter/src/kernels/ExpandDims.test.cpp new file mode 100644 index 0000000..1ebbcd8 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/ExpandDims.test.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO enable it +#if 0 +#include "kernels/ExpandDims.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +class ExpandDimsTest : public ::testing::Test +{ +protected: + void SetUp() override { _memory_manager = std::make_unique(); } + + std::unique_ptr _memory_manager; +}; + +TEST_F(ExpandDimsTest, PositiveAxis) +{ + std::vector input_data{-1, 1, -2, 2}; + std::initializer_list input_shape = {2, 2}; + + std::initializer_list axis_value = {0}; + + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor axis_tensor = makeInputTensor({1}, axis_value, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::S32); + + ExpandDims kernel(&input_tensor, &axis_tensor, &output_tensor); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorData(output_tensor), ::testing::ElementsAreArray(input_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray({1, 2, 2})); +} + +TEST_F(ExpandDimsTest, NegAxis) +{ + std::vector input_data{-1, 1, -2, 2}; + std::initializer_list input_shape = {2, 2}; + + std::initializer_list axis_value = {-1}; + + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor axis_tensor = makeInputTensor({1}, axis_value, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::S32); + + ExpandDims kernel(&input_tensor, &axis_tensor, &output_tensor); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorData(output_tensor), ::testing::ElementsAreArray(input_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray({2, 2, 1})); +} + +TEST_F(ExpandDimsTest, InvalidAxisType_NEG) +{ + std::vector input_data{-1, 1, -2, 2}; + std::initializer_list input_shape = {2, 2}; + + std::initializer_list axis_value = {1.0}; + + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor axis_tensor = makeInputTensor({1}, axis_value, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::S32); + + ExpandDims kernel(&input_tensor, &axis_tensor, &output_tensor); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(ExpandDimsTest, InvalidAxisValue_NEG) +{ + std::vector input_data{-1, 1, -2, 2}; + std::initializer_list input_shape = {2, 2}; + + std::initializer_list axis_value = {3}; + + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor axis_tensor = makeInputTensor({1}, axis_value, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::S32); + + ExpandDims kernel(&input_tensor, &axis_tensor, &output_tensor); + EXPECT_ANY_THROW(kernel.configure()); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter +#endif diff --git a/onert-micro/luci-interpreter/src/kernels/Fill.cpp b/onert-micro/luci-interpreter/src/kernels/Fill.cpp new file mode 100644 index 0000000..addab74 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Fill.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Fill.h" +#include "kernels/Utils.h" +#include "PALFill.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +Fill::Fill(const Tensor *dims, const Tensor *value, Tensor *output) + : Kernel({dims, value}, {output}) +{ +} + +template void Fill::configureShape() +{ + const auto dims_data = getTensorData(dims()); + Shape output_shape(dims()->shape().dim(0)); + + for (int i = 0; i < output_shape.num_dims(); ++i) + { + T data = dims_data[i]; + if (data < 0) + assert(false && "Fill dimensions must be >= 0"); + + output_shape.dim(i) = data; + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void Fill::configure() +{ + const auto dims_shape = dims()->shape(); + const auto value_shape = value()->shape(); + + // Make sure the 1st input tensor is 1-D + LUCI_INTERPRETER_CHECK(dims_shape.num_dims() == 1); + + // Make sure the 1st input tensor is int32 or int64 + LUCI_INTERPRETER_CHECK(dims()->element_type() == DataType::S32 or + dims()->element_type() == DataType::S64); + + // Make sure the 2nd input tensor is a scalar + LUCI_INTERPRETER_CHECK(value_shape.num_dims() == 0) + + // Check zero point and scale for S16 and S8 + if (value()->element_type() == DataType::S16 or value()->element_type() == DataType::S8) + { + LUCI_INTERPRETER_CHECK(value()->scale() == output()->scale()); + LUCI_INTERPRETER_CHECK(value()->zero_point() == output()->zero_point()); + + if (value()->element_type() == DataType::S16) + LUCI_INTERPRETER_CHECK(value()->zero_point() == 0); + } + // Resize output + switch (dims()->element_type()) + { + case DataType::S32: + configureShape(); + break; + case DataType::S64: + configureShape(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Fill::execute() const +{ + switch (output()->element_type()) + { + case DataType::S8: + tflite::reference_ops::Fill(getTensorShape(value()), getTensorData(value()), + getTensorShape(output()), getTensorData(output())); + break; + case DataType::S16: + tflite::reference_ops::Fill(getTensorShape(value()), getTensorData(value()), + getTensorShape(output()), getTensorData(output())); + break; + case DataType::S32: + tflite::reference_ops::Fill(getTensorShape(value()), getTensorData(value()), + getTensorShape(output()), getTensorData(output())); + break; + case DataType::S64: + tflite::reference_ops::Fill(getTensorShape(value()), getTensorData(value()), + getTensorShape(output()), getTensorData(output())); + break; + case DataType::FLOAT32: + tflite::reference_ops::Fill(getTensorShape(value()), getTensorData(value()), + getTensorShape(output()), getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Fill.h b/onert-micro/luci-interpreter/src/kernels/Fill.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Fill.h rename to onert-micro/luci-interpreter/src/kernels/Fill.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Fill.test.cpp b/onert-micro/luci-interpreter/src/kernels/Fill.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Fill.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Fill.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Floor.cpp b/onert-micro/luci-interpreter/src/kernels/Floor.cpp new file mode 100644 index 0000000..f7871b5 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Floor.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Floor.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +Floor::Floor(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Floor::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Floor::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + + default: + assert(false && "Unsupported type."); + } +} + +void Floor::evalFloat() const +{ + tflite::reference_ops::Floor(getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Floor.h b/onert-micro/luci-interpreter/src/kernels/Floor.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Floor.h rename to onert-micro/luci-interpreter/src/kernels/Floor.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Floor.test.cpp b/onert-micro/luci-interpreter/src/kernels/Floor.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Floor.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Floor.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/FloorDiv.cpp b/onert-micro/luci-interpreter/src/kernels/FloorDiv.cpp new file mode 100644 index 0000000..6a8631a --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/FloorDiv.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/FloorDiv.h" +#include "kernels/Utils.h" + +#include +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +FloorDiv::FloorDiv(const Tensor *input, const Tensor *alpha, Tensor *output) + : Kernel({input, alpha}, {output}) +{ +} + +void FloorDiv::configure() +{ + LUCI_INTERPRETER_CHECK(x()->element_type() == output()->element_type()); + LUCI_INTERPRETER_CHECK(y()->element_type() == output()->element_type()); + + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); +} + +void FloorDiv::execute() const +{ + switch (x()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void FloorDiv::evalFloat() const +{ + auto FloorDivFunc = [](float x, float y) -> float { + return std::floor(static_cast(x) / static_cast(y)); + }; + + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + + // Check the denominator + for (int i = 0; i < getTensorShape(y()).FlatSize(); ++i) + { + LUCI_INTERPRETER_CHECK(y_data[i] != 0); + } + + if (x()->shape() != y()->shape()) + { + tflite::reference_ops::BroadcastBinaryFunction4DSlow( + getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), + getTensorData(output()), FloorDivFunc); + } + else + { + tflite::reference_ops::BinaryFunction( + getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), + getTensorData(output()), FloorDivFunc); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/FloorDiv.h b/onert-micro/luci-interpreter/src/kernels/FloorDiv.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/FloorDiv.h rename to onert-micro/luci-interpreter/src/kernels/FloorDiv.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/FloorDiv.test.cpp b/onert-micro/luci-interpreter/src/kernels/FloorDiv.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/FloorDiv.test.cpp rename to onert-micro/luci-interpreter/src/kernels/FloorDiv.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/FullyConnected.cpp b/onert-micro/luci-interpreter/src/kernels/FullyConnected.cpp new file mode 100644 index 0000000..1f695af --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/FullyConnected.cpp @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" +#include "kernels/Utils.h" + +#include "PALFullyConnected.h" + +namespace luci_interpreter +{ + +namespace +{ +void evalFloat(const circle::Tensor *input, const circle::Tensor *weights, + const circle::Tensor *bias, const circle::Tensor *output, + const circle::FullyConnectedOptions *options, BaseRuntimeGraph *runtime_graph) +{ + float activation_min{}; + float activation_max{}; + kernels::calculateActivationRange(luci_actfunc(options->fused_activation_function()), + &activation_min, &activation_max); + + tflite::FullyConnectedParams params{}; + params.float_activation_min = activation_min; + params.float_activation_max = activation_max; + params.weights_format = tflite::FullyConnectedWeightsFormat::kDefault; + + auto *input_data = runtime_graph->getDataByTensor(input); + auto *output_data = runtime_graph->getDataByTensor(output); + + auto *weights_data = runtime_graph->getConstDataByTensor(weights); + auto *bias_data = runtime_graph->getConstDataByTensor(bias); + + assert(input_data != nullptr); + assert(weights_data != nullptr); + assert(output_data != nullptr); + + tflite::reference_ops::FullyConnected( + params, kernels::getTensorShape(input), kernels::getTensorData(input_data), + kernels::getTensorShape(weights), kernels::getTensorData(weights_data), + kernels::getTensorShape(bias), kernels::getTensorData(bias_data), + kernels::getTensorShape(output), kernels::getTensorData(output_data)); +} + +#ifndef DIS_QUANT +void evalQuantized(const circle::Tensor *input, const circle::Tensor *weights, + const circle::Tensor *bias, const circle::Tensor *output, + const circle::FullyConnectedOptions *options, BaseRuntimeGraph *runtime_graph) +{ + double real_multiplier = 0.0; + int output_shift; + int32_t output_activation_min; + int32_t output_activation_max; + int32_t output_multiplier; + real_multiplier = kernels::getQuantizedConvolutionMultipler( + Tensor::scale(input), Tensor::scale(weights), Tensor::scale(output)); + kernels::quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); + kernels::calculateActivationRangeQuantized(luci_actfunc(options->fused_activation_function()), + output, &output_activation_min, + &output_activation_max); + + int32_t input_offset = -Tensor::zero_point(input); + int32_t filter_offset = -Tensor::zero_point(weights); + int32_t output_offset = Tensor::zero_point(output); + + tflite::FullyConnectedParams op_params{}; + op_params.input_offset = input_offset; + op_params.weights_offset = filter_offset; + op_params.output_offset = output_offset; + op_params.output_multiplier = output_multiplier; + op_params.output_shift = output_shift; + op_params.quantized_activation_min = output_activation_min; + op_params.quantized_activation_max = output_activation_max; + op_params.lhs_cacheable = false; + op_params.rhs_cacheable = false; + + auto *input_data = runtime_graph->getDataByTensor(input); + auto *output_data = runtime_graph->getDataByTensor(output); + + auto *weights_data = runtime_graph->getConstDataByTensor(weights); + auto *bias_data = runtime_graph->getConstDataByTensor(bias); + + assert(input_data != nullptr); + assert(weights_data != nullptr); + assert(output_data != nullptr); + + tflite::reference_ops::FullyConnected( + op_params, kernels::getTensorShape(input), kernels::getTensorData(input_data), + kernels::getTensorShape(weights), kernels::getTensorData(weights_data), + kernels::getTensorShape(bias), kernels::getTensorData(bias_data), + kernels::getTensorShape(output), kernels::getTensorData(output_data)); +} + +void evalQuantizedS8(const circle::Tensor *input, const circle::Tensor *weights, + const circle::Tensor *bias, const circle::Tensor *output, + const circle::FullyConnectedOptions *options, BaseRuntimeGraph *runtime_graph) +{ + double real_multiplier = 0.0; + int output_shift; + int32_t output_activation_min; + int32_t output_activation_max; + int32_t output_multiplier; + real_multiplier = kernels::getQuantizedConvolutionMultipler( + Tensor::scale(input), Tensor::scale(weights), Tensor::scale(output)); + kernels::quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); + kernels::calculateActivationRangeQuantized(luci_actfunc(options->fused_activation_function()), + output, &output_activation_min, + &output_activation_max); + + int32_t input_offset = -Tensor::zero_point(input); + int32_t filter_offset = -Tensor::zero_point(weights); + int32_t output_offset = Tensor::zero_point(output); + + tflite::FullyConnectedParams op_params{}; + op_params.input_offset = input_offset; + op_params.weights_offset = filter_offset; + op_params.output_offset = output_offset; + op_params.output_multiplier = output_multiplier; + op_params.output_shift = output_shift; + op_params.quantized_activation_min = output_activation_min; + op_params.quantized_activation_max = output_activation_max; + op_params.lhs_cacheable = false; + op_params.rhs_cacheable = false; + + auto *input_data = runtime_graph->getDataByTensor(input); + auto *output_data = runtime_graph->getDataByTensor(output); + + auto *weights_data = runtime_graph->getConstDataByTensor(weights); + auto *bias_data = runtime_graph->getConstDataByTensor(bias); + + assert(input_data != nullptr); + assert(weights_data != nullptr); + assert(output_data != nullptr); + + luci_interpreter_pal::FullyConnected( + op_params, kernels::getTensorShape(input), kernels::getTensorData(input_data), + kernels::getTensorShape(weights), kernels::getTensorData(weights_data), + kernels::getTensorShape(bias), kernels::getTensorData(bias_data), + kernels::getTensorShape(output), kernels::getTensorData(output_data)); +} +#endif + +} // namespace + +// TODO think how remove unused param +void configure_kernel_CircleFullyConnected(const circle::Operator *cur_op, + BaseRuntimeGraph *runtime_graph) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto weight_index = cur_op->inputs()->operator[](1); + const auto bias_index = cur_op->inputs()->operator[](2); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(weight_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + const auto weights = runtime_graph->getCircleTensorByIndex(weight_index); + const auto bias = runtime_graph->getCircleTensorByIndex(bias_index); + const auto output = runtime_graph->getCircleTensorByIndex(output_index); + + assert(input != nullptr); + assert(weights != nullptr); + assert(output != nullptr); + +#ifndef DIS_FLOAT + if (Tensor::element_type(weights) == DataType::FLOAT32) + { + LUCI_INTERPRETER_CHECK(Tensor::element_type(input) == DataType::FLOAT32); + LUCI_INTERPRETER_CHECK(Tensor::element_type(output) == DataType::FLOAT32); + LUCI_INTERPRETER_CHECK(!bias || Tensor::element_type(bias) == DataType::FLOAT32) + } +#endif // DIS_FLOAT +#ifndef DIS_QUANT + else if (Tensor::element_type(weights) == DataType::U8) + { + LUCI_INTERPRETER_CHECK(Tensor::element_type(input) == DataType::U8); + LUCI_INTERPRETER_CHECK(Tensor::element_type(output) == DataType::U8); + LUCI_INTERPRETER_CHECK(!bias || Tensor::element_type(bias) == DataType::S32) + } + else if (Tensor::element_type(weights) == DataType::S8) + { + LUCI_INTERPRETER_CHECK(Tensor::element_type(input) == DataType::S8); + LUCI_INTERPRETER_CHECK(Tensor::element_type(output) == DataType::S8); + LUCI_INTERPRETER_CHECK(!bias || Tensor::element_type(bias) == DataType::S32) + } +#endif // DIS_QUANT + else + { + assert(false && "Unsupported type."); + } + + LUCI_INTERPRETER_CHECK(Tensor::num_dims(weights) == 2); + LUCI_INTERPRETER_CHECK(bias == nullptr || Tensor::num_elements(bias) == Tensor::dim(weights, 0)); + LUCI_INTERPRETER_CHECK(Tensor::num_elements(input) % Tensor::dim(weights, 1) == 0); + + if (bias) + LUCI_INTERPRETER_CHECK(Tensor::num_elements(bias) == Tensor::dim(weights, 0)); + + const auto *options = cur_op->builtin_options_as_FullyConnectedOptions(); + + // TODO: handle with it + assert(options->keep_num_dims() == false); +} + +// TODO think how remove unused param +void execute_kernel_CircleFullyConnected(const circle::Operator *cur_op, + BaseRuntimeGraph *runtime_graph, bool) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto weight_index = cur_op->inputs()->operator[](1); + const auto bias_index = cur_op->inputs()->operator[](2); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(weight_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + const auto weights = runtime_graph->getCircleTensorByIndex(weight_index); + const auto bias = runtime_graph->getCircleTensorByIndex(bias_index); + const auto output = runtime_graph->getCircleTensorByIndex(output_index); + + assert(input != nullptr); + assert(weights != nullptr); + assert(output != nullptr); + + const auto *options = cur_op->builtin_options_as_FullyConnectedOptions(); + + switch (Tensor::element_type(input)) + { +#ifndef DIS_QUANT + case DataType::U8: + evalQuantized(input, weights, bias, output, options, runtime_graph); + break; + case DataType::S8: + evalQuantizedS8(input, weights, bias, output, options, runtime_graph); + break; +#endif // DIS_QUANT +#ifndef DIS_FLOAT + case DataType::FLOAT32: + evalFloat(input, weights, bias, output, options, runtime_graph); + break; +#endif // DIS_FLOAT + default: + assert(false && "Unsupported type."); + } +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/FullyConnected.test.cpp b/onert-micro/luci-interpreter/src/kernels/FullyConnected.test.cpp new file mode 100644 index 0000000..43fa994 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/FullyConnected.test.cpp @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// TODO enable it +#if 0 +#include "kernels/FullyConnected.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +template +void Check(std::initializer_list input_shape, std::initializer_list weights_shape, + std::initializer_list bias_shape, std::initializer_list output_shape, + std::initializer_list input_data, std::initializer_list weights_data, + std::initializer_list bias_data, std::initializer_list output_data) +{ + std::unique_ptr memory_manager = std::make_unique(); + Tensor input_tensor = + makeInputTensor(input_shape, input_data, memory_manager.get()); + Tensor weights_tensor = + makeInputTensor(weights_shape, weights_data, memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, bias_data, memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + FullyConnectedParams params{}; + params.activation = Activation::RELU; + + FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); + kernel.configure(); + memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(output_data)); +} + +template <> +void Check(std::initializer_list input_shape, + std::initializer_list weights_shape, + std::initializer_list bias_shape, + std::initializer_list output_shape, + std::initializer_list input_data, + std::initializer_list weights_data, + std::initializer_list bias_data, std::initializer_list output_data) +{ + std::unique_ptr memory_manager = std::make_unique(); + const float quantized_tolerance = getTolerance(-127, 128, 255); + std::pair input_quant_param = quantizationParams(-63.5, 64); + std::pair output_quant_param = quantizationParams(-127, 128); + Tensor input_tensor = + makeInputTensor(input_shape, input_quant_param.first, input_quant_param.second, + input_data, memory_manager.get()); + Tensor weights_tensor = + makeInputTensor(weights_shape, input_quant_param.first, input_quant_param.second, + weights_data, memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, input_quant_param.first * input_quant_param.first, 0, + bias_data, memory_manager.get()); + Tensor output_tensor = + makeOutputTensor(DataType::S8, output_quant_param.first, output_quant_param.second); + + FullyConnectedParams params{}; + params.activation = Activation::RELU; + + FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); + kernel.configure(); + memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); + EXPECT_THAT(dequantizeTensorData(output_tensor), + FloatArrayNear(output_data, quantized_tolerance)); +} + +template <> +void Check( + std::initializer_list input_shape, std::initializer_list weights_shape, + std::initializer_list bias_shape, std::initializer_list output_shape, + std::initializer_list input_data, std::initializer_list weights_data, + std::initializer_list bias_data, std::initializer_list output_data) +{ + std::unique_ptr memory_manager = std::make_unique(); + const float quantized_tolerance = getTolerance(-127, 128, 255); + std::pair input_quant_param = quantizationParams(-63.5, 64); + std::pair output_quant_param = quantizationParams(-127, 128); + Tensor input_tensor = + makeInputTensor(input_shape, input_quant_param.first, input_quant_param.second, + input_data, memory_manager.get()); + Tensor weights_tensor = + makeInputTensor(weights_shape, input_quant_param.first, input_quant_param.second, + weights_data, memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, input_quant_param.first * input_quant_param.first, 0, + bias_data, memory_manager.get()); + Tensor output_tensor = + makeOutputTensor(DataType::U8, output_quant_param.first, output_quant_param.second); + + FullyConnectedParams params{}; + params.activation = Activation::RELU; + + FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); + kernel.configure(); + memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); + EXPECT_THAT(dequantizeTensorData(output_tensor), + FloatArrayNear(output_data, quantized_tolerance)); +} + +template class FullyConnectedTest : public ::testing::Test +{ +}; + +using DataTypes = ::testing::Types; +TYPED_TEST_SUITE(FullyConnectedTest, DataTypes); + +TYPED_TEST(FullyConnectedTest, Simple) +{ + Check({3, 2, 2, 1}, {3, 6}, {3}, {2, 3}, + { + -3, -5, 5, 4, 9, -2, // batch = 0 + -3, -2, -4, 9, -8, 1, // batch = 1 + }, + { + -3, -7, 4, -4, -6, 4, // unit = 0 + 3, 5, 2, 3, -3, -8, // unit = 1 + -3, 7, 4, 9, 0, -5, // unit = 2 + }, + {-1, -5, -8}, + { + 0, 0, 32, // batch = 0 + 22, 11, 47, // batch = 1 + }); +} + +TEST(FullyConnectedTest, InvalidBiasType_NEG) +{ + Shape input_shape{3, 2, 2, 1}; + std::vector input_data{ + -3, -5, 5, 4, 9, -2, // batch = 0 + -3, -2, -4, 9, -8, 1, // batch = 1 + }; + Shape weights_shape{3, 6}; + std::vector weights_data{ + -3, -7, 4, -4, -6, 4, // unit = 0 + 3, 5, 2, 3, -3, -8, // unit = 1 + -3, 7, 4, 9, 0, -5, // unit = 2 + }; + Shape bias_shape{3}; + std::vector bias_data{-1, -5, -8}; + + std::unique_ptr memory_manager = std::make_unique(); + + Tensor input_tensor = + makeInputTensor(input_shape, input_data, memory_manager.get()); + Tensor weights_tensor = + makeInputTensor(weights_shape, weights_data, memory_manager.get()); + Tensor bias_tensor = makeInputTensor(bias_shape, bias_data, memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + FullyConnectedParams params{}; + params.activation = Activation::RELU; + + FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST(FullyConnectedTest, InvalidWeightShapeDim_NEG) +{ + Shape input_shape{3, 2, 2, 1}; + std::vector input_data{ + -3, -5, 5, 4, 9, -2, // batch = 0 + -3, -2, -4, 9, -8, 1, // batch = 1 + }; + Shape weights_shape{1, 3, 6}; + std::vector weights_data{ + -3, -7, 4, -4, -6, 4, // unit = 0 + 3, 5, 2, 3, -3, -8, // unit = 1 + -3, 7, 4, 9, 0, -5, // unit = 2 + }; + Shape bias_shape{3}; + std::vector bias_data{-1, -5, -8}; + + std::unique_ptr memory_manager = std::make_unique(); + + Tensor input_tensor = + makeInputTensor(input_shape, input_data, memory_manager.get()); + Tensor weights_tensor = + makeInputTensor(weights_shape, weights_data, memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, bias_data, memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + FullyConnectedParams params{}; + params.activation = Activation::RELU; + + FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST(FullyConnectedTest, BiasElementNumWeightDimMismatch_NEG) +{ + Shape input_shape{3, 2, 2, 1}; + std::vector input_data{ + -3, -5, 5, 4, 9, -2, // batch = 0 + -3, -2, -4, 9, -8, 1, // batch = 1 + }; + Shape weights_shape{6, 3}; + std::vector weights_data{ + -3, -7, 4, // unit = 0 + -4, -6, 4, // unit = 1 + 3, 5, 2, // unit = 2 + 3, -3, -8, // unit = 3 + -3, 7, 4, // unit = 4 + 9, 0, -5, // unit = 5 + }; + Shape bias_shape{3}; + std::vector bias_data{-1, -5, -8}; + + std::unique_ptr memory_manager = std::make_unique(); + + Tensor input_tensor = + makeInputTensor(input_shape, input_data, memory_manager.get()); + Tensor weights_tensor = + makeInputTensor(weights_shape, weights_data, memory_manager.get()); + Tensor bias_tensor = + makeInputTensor(bias_shape, bias_data, memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + FullyConnectedParams params{}; + params.activation = Activation::RELU; + + FullyConnected kernel(&input_tensor, &weights_tensor, &bias_tensor, &output_tensor, params); + EXPECT_ANY_THROW(kernel.configure()); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter +#endif diff --git a/onert-micro/luci-interpreter/src/kernels/Gather.cpp b/onert-micro/luci-interpreter/src/kernels/Gather.cpp new file mode 100644 index 0000000..d26b718 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Gather.cpp @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2021 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Gather.h" +#include "kernels/Utils.h" +#include "PALGather.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +Gather::Gather(const Tensor *params, const Tensor *indices, Tensor *output, + const GatherParams &gparams) + : KernelWithParams({params, indices}, {output}, gparams) +{ +} + +void Gather::configure() +{ + if (params()->element_type() == DataType::FLOAT32) + { + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::FLOAT32); + } + else + { + assert(false && "Unsupported type."); + } + + LUCI_INTERPRETER_CHECK(indices()->element_type() == DataType::S32 || + indices()->element_type() == DataType::S64); + + // refer tensorflow/lite/kernels/gather.cc + + const Shape ¶ms_shape = params()->shape(); + const Shape &indices_shape = indices()->shape(); + + int axis = _params.axis; + if (axis < 0) + { + axis += params_shape.num_dims(); + } + LUCI_INTERPRETER_CHECK(0 <= axis && axis < params_shape.num_dims()); + + int batch_dims = _params.batch_dims; + // batch_dims should be in range: [-rank(indices), rank(indices)]. + // Negative batch_dims is added with rank of positions. + if (batch_dims < 0) + { + batch_dims += indices_shape.num_dims(); + } + LUCI_INTERPRETER_CHECK(batch_dims <= axis); + LUCI_INTERPRETER_CHECK(0 <= batch_dims && batch_dims < params_shape.num_dims()); + LUCI_INTERPRETER_CHECK(batch_dims <= indices_shape.num_dims()); + for (int i = 0; i < batch_dims; ++i) + { + LUCI_INTERPRETER_CHECK(params_shape.dim(i) == indices_shape.dim(i)); + } + + const int num_dimensions = params_shape.num_dims() + indices_shape.num_dims() - 1 - batch_dims; + + Shape output_shape(num_dimensions); + int output_index = 0; + for (int i = 0; i < axis; ++i) + { + output_shape.dim(output_index++) = params_shape.dim(i); + } + for (int i = batch_dims; i < indices_shape.num_dims(); ++i) + { + output_shape.dim(output_index++) = indices_shape.dim(i); + } + for (int i = axis + 1; i < params_shape.num_dims(); ++i) + { + output_shape.dim(output_index++) = params_shape.dim(i); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void Gather::execute() const +{ + switch (params()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Gather::evalFloat() const +{ + assert(indices()->element_type() == DataType::S32 || indices()->element_type() == DataType::S64); + + const auto params_data = getTensorData(params()); + auto output_data = getTensorData(output()); + + tflite::GatherParams tparams; + tparams.axis = _params.axis; + tparams.batch_dims = _params.batch_dims; + + if (indices()->element_type() == DataType::S32) + { + const auto indices_data = getTensorData(indices()); + + luci_interpreter_pal::Gather(tparams, getTensorShape(params()), params_data, + getTensorShape(indices()), indices_data, + getTensorShape(output()), output_data); + } + else + { + const auto indices_data = getTensorData(indices()); + + luci_interpreter_pal::Gather(tparams, getTensorShape(params()), params_data, + getTensorShape(indices()), indices_data, + getTensorShape(output()), output_data); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Gather.h b/onert-micro/luci-interpreter/src/kernels/Gather.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Gather.h rename to onert-micro/luci-interpreter/src/kernels/Gather.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Gather.test.cpp b/onert-micro/luci-interpreter/src/kernels/Gather.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Gather.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Gather.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Greater.cpp b/onert-micro/luci-interpreter/src/kernels/Greater.cpp new file mode 100644 index 0000000..9d77780 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Greater.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Greater.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +Greater::Greater(const Tensor *x, const Tensor *y, Tensor *output) : Kernel({x, y}, {output}) {} + +void Greater::configure() +{ + LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); + + if (x()->element_type() == DataType::U8) + { + quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); + quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); +} + +void Greater::execute() const +{ + switch (x()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::S64: + evalInteger(); + break; + case DataType::S32: + evalInteger(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Greater::evalFloat() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowGreater(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::Greater(op_params, getTensorShape(x()), x_data, getTensorShape(y()), + y_data, getTensorShape(output()), output_data); + } +} + +template void Greater::evalInteger() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowGreaterNoScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::GreaterNoScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } +} + +void Greater::evalQuantized() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.left_shift = 8; + op_params.input1_offset = -x()->zero_point(); // Note the '-' + op_params.input1_shift = _x_shift; + op_params.input1_multiplier = _x_multiplier; + op_params.input2_offset = -y()->zero_point(); // Note the '-' + op_params.input2_shift = _y_shift; + op_params.input2_multiplier = _y_multiplier; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowGreaterWithScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::GreaterWithScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Greater.h b/onert-micro/luci-interpreter/src/kernels/Greater.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Greater.h rename to onert-micro/luci-interpreter/src/kernels/Greater.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Greater.test.cpp b/onert-micro/luci-interpreter/src/kernels/Greater.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Greater.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Greater.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/GreaterEqual.cpp b/onert-micro/luci-interpreter/src/kernels/GreaterEqual.cpp new file mode 100644 index 0000000..8f37160 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/GreaterEqual.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/GreaterEqual.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +GreaterEqual::GreaterEqual(const Tensor *x, const Tensor *y, Tensor *output) + : Kernel({x, y}, {output}) +{ +} + +void GreaterEqual::configure() +{ + LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); + + if (x()->element_type() == DataType::U8) + { + quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); + quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); +} + +void GreaterEqual::execute() const +{ + switch (x()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::S64: + evalInteger(); + break; + case DataType::S32: + evalInteger(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void GreaterEqual::evalFloat() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowGreaterEqual(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::GreaterEqual(op_params, getTensorShape(x()), x_data, getTensorShape(y()), + y_data, getTensorShape(output()), output_data); + } +} + +template void GreaterEqual::evalInteger() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowGreaterEqualNoScaling( + op_params, getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } + else + { + tflite::reference_ops::GreaterEqualNoScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } +} + +void GreaterEqual::evalQuantized() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.left_shift = 8; + op_params.input1_offset = -x()->zero_point(); // Note the '-' + op_params.input1_shift = _x_shift; + op_params.input1_multiplier = _x_multiplier; + op_params.input2_offset = -y()->zero_point(); // Note the '-' + op_params.input2_shift = _y_shift; + op_params.input2_multiplier = _y_multiplier; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowGreaterEqualWithScaling( + op_params, getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } + else + { + tflite::reference_ops::GreaterEqualWithScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/GreaterEqual.h b/onert-micro/luci-interpreter/src/kernels/GreaterEqual.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/GreaterEqual.h rename to onert-micro/luci-interpreter/src/kernels/GreaterEqual.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/GreaterEqual.test.cpp b/onert-micro/luci-interpreter/src/kernels/GreaterEqual.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/GreaterEqual.test.cpp rename to onert-micro/luci-interpreter/src/kernels/GreaterEqual.test.cpp diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/If.cpp b/onert-micro/luci-interpreter/src/kernels/If.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/If.cpp rename to onert-micro/luci-interpreter/src/kernels/If.cpp diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/If.h b/onert-micro/luci-interpreter/src/kernels/If.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/If.h rename to onert-micro/luci-interpreter/src/kernels/If.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/If.test.cpp b/onert-micro/luci-interpreter/src/kernels/If.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/If.test.cpp rename to onert-micro/luci-interpreter/src/kernels/If.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/InstanceNorm.cpp b/onert-micro/luci-interpreter/src/kernels/InstanceNorm.cpp new file mode 100644 index 0000000..577dc64 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/InstanceNorm.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/InstanceNorm.h" + +#include "kernels/Utils.h" + +#include +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +InstanceNorm::InstanceNorm(const Tensor *input, const Tensor *gamma, const Tensor *beta, + Tensor *output, const InstanceNormParams ¶ms) + : KernelWithParams({input, gamma, beta}, {output}, params) +{ +} + +void InstanceNorm::configure() +{ + LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + LUCI_INTERPRETER_CHECK(gamma()->element_type() == input()->element_type()); + LUCI_INTERPRETER_CHECK(gamma()->shape().num_dims() == 1); + LUCI_INTERPRETER_CHECK(gamma()->shape().dim(0) == input()->shape().dim(3) || + gamma()->shape().dim(0) == 1); + LUCI_INTERPRETER_CHECK(beta()->element_type() == input()->element_type()); + LUCI_INTERPRETER_CHECK(beta()->shape().num_dims() == 1); + LUCI_INTERPRETER_CHECK(beta()->shape().dim(0) == input()->shape().dim(3) || + beta()->shape().dim(0) == 1); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void InstanceNorm::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void InstanceNorm::evalFloat() const +{ + float activation_min, activation_max; + calculateActivationRange(params().activation, &activation_min, &activation_max); + auto input_shape = getTensorShape(input()); + auto output_shape = getTensorShape(output()); + const int32_t batches = tflite::MatchingDim(input_shape, 0, output_shape, 0); + const int32_t heights = tflite::MatchingDim(input_shape, 1, output_shape, 1); + const int32_t widths = tflite::MatchingDim(input_shape, 2, output_shape, 2); + const int32_t channels = tflite::MatchingDim(input_shape, 3, output_shape, 3); + const float *input_data = getTensorData(input()); + const float *gamma_data = getTensorData(gamma()); + auto gamma_shape = getTensorShape(gamma()); + bool single_gamma = gamma_shape.DimensionsCount() == 1 && gamma_shape.Dims(0) == 1; + const float *beta_data = getTensorData(beta()); + auto beta_shape = getTensorShape(beta()); + bool single_beta = beta_shape.DimensionsCount() == 1 && beta_shape.Dims(0) == 1; + float *output_data = getTensorData(output()); + for (int32_t batch = 0; batch < batches; batch++) + { + for (int32_t channel = 0; channel < channels; channel++) + { + double sum = 0.0f; + double square_sum = 0.0f; + int32_t size = heights * widths; + for (int32_t height = 0; height < heights; height++) + { + for (int32_t width = 0; width < widths; width++) + { + double input_val = input_data[tflite::Offset(input_shape, batch, height, width, channel)]; + sum += input_val; + square_sum += (input_val * input_val); + } + } + double mean = sum / size; + double var = square_sum / size - mean * mean; + + double gamma = single_gamma ? gamma_data[0] : gamma_data[channel]; + double beta = single_beta ? beta_data[0] : beta_data[channel]; + double a = gamma / (std::sqrt(var + params().epsilon)); + double b = -mean * a + beta; + + for (int32_t height = 0; height < heights; height++) + { + for (int32_t width = 0; width < widths; width++) + { + double input_value = + input_data[tflite::Offset(output_shape, batch, height, width, channel)]; + double output_value = input_value * a + b; + output_data[tflite::Offset(output_shape, batch, height, width, channel)] = + tflite::ActivationFunctionWithMinMax((float)output_value, activation_min, + activation_max); + } + } + } + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/InstanceNorm.h b/onert-micro/luci-interpreter/src/kernels/InstanceNorm.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/InstanceNorm.h rename to onert-micro/luci-interpreter/src/kernels/InstanceNorm.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/InstanceNorm.test.cpp b/onert-micro/luci-interpreter/src/kernels/InstanceNorm.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/InstanceNorm.test.cpp rename to onert-micro/luci-interpreter/src/kernels/InstanceNorm.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/KernelBuilder.cpp b/onert-micro/luci-interpreter/src/kernels/KernelBuilder.cpp new file mode 100644 index 0000000..fc9c09c --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/KernelBuilder.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "KernelBuilder.h" +#include "Builders.h" + +namespace luci_interpreter +{ + +KernelConfigureRegistry::KernelConfigureRegistry() +{ +#define REGISTER_KERNEL(builtin_operator, name) \ + register_kernel_configure(circle::BuiltinOperator::BuiltinOperator_##builtin_operator, \ + configure_kernel_Circle##name); + +#if USE_GENERATED_LIST +#include "GeneratedKernelsToBuild.lst" +#else +#include "KernelsToBuild.lst" +#endif + +#undef REGISTER_KERNEL +} + +void KernelConfigureRegistry::configure_kernel(const circle::Operator *cur_op, + circle::BuiltinOperator opcode, + BaseRuntimeGraph *runtime_graph) +{ + auto specific_configure_func = get_kernel_configure_func(opcode); + if (specific_configure_func == nullptr) + assert(false && "Unsupported operator"); + + specific_configure_func(cur_op, runtime_graph); +} + +KernelExecuteRegistry::KernelExecuteRegistry() +{ +#define REGISTER_KERNEL(builtin_operator, name) \ + register_kernel_execute(circle::BuiltinOperator::BuiltinOperator_##builtin_operator, \ + execute_kernel_Circle##name); + +#if USE_GENERATED_LIST +#include "GeneratedKernelsToBuild.lst" +#else +#include "KernelsToBuild.lst" +#endif + +#undef REGISTER_KERNEL +} + +void KernelExecuteRegistry::execute_kernel(const circle::Operator *cur_op, + circle::BuiltinOperator opcode, + BaseRuntimeGraph *runtime_graph, bool is_inplace) +{ + auto specific_execute_func = get_kernel_execute_func(opcode); + if (specific_execute_func == nullptr) + assert(false && "Unsupported operator"); + + specific_execute_func(cur_op, runtime_graph, is_inplace); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/KernelBuilder.h b/onert-micro/luci-interpreter/src/kernels/KernelBuilder.h new file mode 100644 index 0000000..c73cf05 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/KernelBuilder.h @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_KERNEL_KERNELBUILDER_H +#define LUCI_INTERPRETER_KERNEL_KERNELBUILDER_H + +#include "core/RuntimeGraph.h" +#include "luci_interpreter/core/reader/CircleMicroReader.h" + +#include +#include + +namespace luci_interpreter +{ +namespace +{ +#ifdef USE_STATIC_ALLOC +using BaseRuntimeGraph = StaticRuntimeGraph; +#else +using BaseRuntimeGraph = RuntimeGraph; +#endif +} // namespace + +class KernelConfigureRegistry +{ +public: + using KernelConfigureFunc = void(const circle::Operator *, BaseRuntimeGraph *); + + KernelConfigureRegistry(); + + void configure_kernel(const circle::Operator *cur_op, circle::BuiltinOperator opcode, + BaseRuntimeGraph *runtime_graph); + +private: + std::unordered_map _operator_configure; + +private: + KernelConfigureFunc *get_kernel_configure_func(circle::BuiltinOperator opcode) const + { + return _operator_configure.at(size_t(opcode)); + } + + void register_kernel_configure(circle::BuiltinOperator id, KernelConfigureFunc *func) + { + _operator_configure[size_t(id)] = func; + } +}; + +class KernelExecuteRegistry +{ +public: + using KernelExecuteFunc = void(const circle::Operator *, BaseRuntimeGraph *, bool); + + KernelExecuteRegistry(); + + void execute_kernel(const circle::Operator *cur_op, circle::BuiltinOperator opcode, + BaseRuntimeGraph *runtime_graph, bool is_inplace); + +private: + std::unordered_map _operator_execute; + +private: + KernelExecuteFunc *get_kernel_execute_func(circle::BuiltinOperator opcode) const + { + return _operator_execute.at(size_t(opcode)); + } + + void register_kernel_execute(circle::BuiltinOperator id, KernelExecuteFunc *func) + { + _operator_execute[size_t(id)] = func; + } +}; + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_KERNEL_KERNELBUILDER_H diff --git a/onert-micro/luci-interpreter/src/kernels/L2Normalize.cpp b/onert-micro/luci-interpreter/src/kernels/L2Normalize.cpp new file mode 100644 index 0000000..97c9db8 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/L2Normalize.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/L2Normalize.h" +#include "kernels/Utils.h" + +#include "PALL2Normalize.h" + +namespace luci_interpreter +{ + +namespace kernels +{ + +L2Normalize::L2Normalize(const Tensor *input, Tensor *output, const L2NormParams ¶ms) + : KernelWithParams({input}, {output}, params) +{ +} + +void L2Normalize::configure() +{ + LUCI_INTERPRETER_CHECK(input()->shape().num_dims() <= 4); + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::FLOAT32 || + output()->element_type() == DataType::U8); + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + if (output()->element_type() == DataType::U8) + { + LUCI_INTERPRETER_CHECK(output()->scale() == (1. / 128.)); + LUCI_INTERPRETER_CHECK(output()->zero_point() == 128); + } + LUCI_INTERPRETER_CHECK(params().activation == Activation::NONE); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void L2Normalize::execute() const +{ + switch (output()->element_type()) + { + case DataType::FLOAT32: + eval(0); + break; + case DataType::U8: + eval(input()->zero_point()); + break; + default: + assert(false && "Unsupported type."); + } +} + +template void L2Normalize::eval(int32_t zero_point) const +{ + tflite::L2NormalizationParams op_params{}; + op_params.input_zero_point = zero_point; + luci_interpreter_pal::L2Normalization(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/L2Normalize.h b/onert-micro/luci-interpreter/src/kernels/L2Normalize.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/L2Normalize.h rename to onert-micro/luci-interpreter/src/kernels/L2Normalize.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/L2Normalize.test.cpp b/onert-micro/luci-interpreter/src/kernels/L2Normalize.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/L2Normalize.test.cpp rename to onert-micro/luci-interpreter/src/kernels/L2Normalize.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/L2Pool2D.cpp b/onert-micro/luci-interpreter/src/kernels/L2Pool2D.cpp new file mode 100644 index 0000000..e465c22 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/L2Pool2D.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/L2Pool2D.h" + +#include "kernels/Utils.h" + +#include "PALL2Pool2D.h" + +namespace luci_interpreter +{ + +namespace kernels +{ + +L2Pool2D::L2Pool2D(const Tensor *input, Tensor *output, const Pool2DParams ¶ms) + : KernelWithParams({input}, {output}, params) +{ +} + +void L2Pool2D::configure() +{ + LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + + int batches = input()->shape().dim(0); + int height = input()->shape().dim(1); + int width = input()->shape().dim(2); + int channels_out = input()->shape().dim(3); + + // Matching GetWindowedOutputSize in TensorFlow. + auto padding = params().padding; + int out_width, out_height; + out_width = computeOutputSize(padding, width, params().filter_width, params().stride_width, 1); + out_height = + computeOutputSize(padding, height, params().filter_height, params().stride_height, 1); + _padding_width = + computePadding(params().stride_width, 1, width, params().filter_width, out_width); + _padding_height = + computePadding(params().stride_height, 1, height, params().filter_height, out_height); + + LUCI_INTERPRETER_CHECK(input()->element_type() == DataType::FLOAT32); + // TODO: enable it only if kernel with dynamic shapes + output()->resize({batches, out_height, out_width, channels_out}); +} + +void L2Pool2D::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + float activation_min, activation_max; + calculateActivationRange(params().activation, &activation_min, &activation_max); + tflite::PoolParams op_params; + op_params.stride_height = params().stride_height; + op_params.stride_width = params().stride_width; + op_params.filter_height = params().filter_height; + op_params.filter_width = params().filter_width; + op_params.padding_values.height = _padding_height; + op_params.padding_values.width = _padding_width; + op_params.float_activation_min = activation_min; + op_params.float_activation_max = activation_max; + luci_interpreter_pal::L2Pool(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/L2Pool2D.h b/onert-micro/luci-interpreter/src/kernels/L2Pool2D.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/L2Pool2D.h rename to onert-micro/luci-interpreter/src/kernels/L2Pool2D.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/L2Pool2D.test.cpp b/onert-micro/luci-interpreter/src/kernels/L2Pool2D.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/L2Pool2D.test.cpp rename to onert-micro/luci-interpreter/src/kernels/L2Pool2D.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/LeakyRelu.cpp b/onert-micro/luci-interpreter/src/kernels/LeakyRelu.cpp new file mode 100644 index 0000000..d13fef2 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/LeakyRelu.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/LeakyRelu.h" + +#include "kernels/Utils.h" + +#include + +#include "PALLeakyRelu.h" + +namespace luci_interpreter +{ + +namespace kernels +{ + +LeakyRelu::LeakyRelu(const Tensor *input, Tensor *output, const LeakyReluParams ¶ms) + : KernelWithParams({input}, {output}, params) +{ +} + +void LeakyRelu::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + if (input()->element_type() == DataType::U8) + { + double alpha_multiplier = input()->scale() * params().alpha / output()->scale(); + quantizeMultiplier(alpha_multiplier, &_output_multiplier_alpha, &_output_shift_alpha); + double identity_multiplier = input()->scale() / output()->scale(); + quantizeMultiplier(identity_multiplier, &_output_multiplier_identity, &_output_shift_identity); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void LeakyRelu::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void LeakyRelu::evalFloat() const +{ + tflite::LeakyReluParams op_params{}; + op_params.alpha = params().alpha; + luci_interpreter_pal::LeakyRelu(op_params, getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); +} + +void LeakyRelu::evalQuantized() const +{ + tflite::LeakyReluParams op_params{}; + op_params.input_offset = input()->zero_point(); + op_params.output_offset = output()->zero_point(); + op_params.output_multiplier_alpha = _output_multiplier_alpha; + op_params.output_shift_alpha = _output_shift_alpha; + op_params.output_multiplier_identity = _output_multiplier_identity; + op_params.output_shift_identity = _output_shift_identity; + + tflite::reference_ops::QuantizeLeakyRelu( + op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(output()), + getTensorData(output())); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LeakyRelu.h b/onert-micro/luci-interpreter/src/kernels/LeakyRelu.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LeakyRelu.h rename to onert-micro/luci-interpreter/src/kernels/LeakyRelu.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LeakyRelu.test.cpp b/onert-micro/luci-interpreter/src/kernels/LeakyRelu.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LeakyRelu.test.cpp rename to onert-micro/luci-interpreter/src/kernels/LeakyRelu.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Less.cpp b/onert-micro/luci-interpreter/src/kernels/Less.cpp new file mode 100644 index 0000000..1d7bbd6 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Less.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Less.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +Less::Less(const Tensor *x, const Tensor *y, Tensor *output) : Kernel({x, y}, {output}) {} + +void Less::configure() +{ + LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); + + if (x()->element_type() == DataType::U8) + { + quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); + quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); +} + +void Less::execute() const +{ + switch (x()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::S64: + evalInteger(); + break; + case DataType::S32: + evalInteger(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Less::evalFloat() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowLess(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::Less(op_params, getTensorShape(x()), x_data, getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } +} + +template void Less::evalInteger() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowLessNoScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::LessNoScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } +} + +void Less::evalQuantized() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.left_shift = 8; + op_params.input1_offset = -x()->zero_point(); // Note the '-' + op_params.input1_shift = _x_shift; + op_params.input1_multiplier = _x_multiplier; + op_params.input2_offset = -y()->zero_point(); // Note the '-' + op_params.input2_shift = _y_shift; + op_params.input2_multiplier = _y_multiplier; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowLessWithScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::LessWithScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Less.h b/onert-micro/luci-interpreter/src/kernels/Less.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Less.h rename to onert-micro/luci-interpreter/src/kernels/Less.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Less.test.cpp b/onert-micro/luci-interpreter/src/kernels/Less.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Less.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Less.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/LessEqual.cpp b/onert-micro/luci-interpreter/src/kernels/LessEqual.cpp new file mode 100644 index 0000000..d750893 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/LessEqual.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/LessEqual.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +LessEqual::LessEqual(const Tensor *x, const Tensor *y, Tensor *output) : Kernel({x, y}, {output}) {} + +void LessEqual::configure() +{ + LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); + + if (x()->element_type() == DataType::U8) + { + quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); + quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); +} + +void LessEqual::execute() const +{ + switch (x()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::S64: + evalInteger(); + break; + case DataType::S32: + evalInteger(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void LessEqual::evalFloat() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowLessEqual(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::LessEqual(op_params, getTensorShape(x()), x_data, getTensorShape(y()), + y_data, getTensorShape(output()), output_data); + } +} + +template void LessEqual::evalInteger() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowLessEqualNoScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::LessEqualNoScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } +} + +void LessEqual::evalQuantized() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.left_shift = 8; + op_params.input1_offset = -x()->zero_point(); // Note the '-' + op_params.input1_shift = _x_shift; + op_params.input1_multiplier = _x_multiplier; + op_params.input2_offset = -y()->zero_point(); // Note the '-' + op_params.input2_shift = _y_shift; + op_params.input2_multiplier = _y_multiplier; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowLessEqualWithScaling( + op_params, getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } + else + { + tflite::reference_ops::LessEqualWithScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LessEqual.h b/onert-micro/luci-interpreter/src/kernels/LessEqual.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LessEqual.h rename to onert-micro/luci-interpreter/src/kernels/LessEqual.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LessEqual.test.cpp b/onert-micro/luci-interpreter/src/kernels/LessEqual.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LessEqual.test.cpp rename to onert-micro/luci-interpreter/src/kernels/LessEqual.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/LocalResponseNormalization.cpp b/onert-micro/luci-interpreter/src/kernels/LocalResponseNormalization.cpp new file mode 100644 index 0000000..bf08db0 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/LocalResponseNormalization.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/LocalResponseNormalization.h" + +#include "kernels/Utils.h" + +#include "PALLocalResponseNormalization.h" + +namespace luci_interpreter +{ + +namespace kernels +{ + +LocalResponseNormalization::LocalResponseNormalization( + const Tensor *input, Tensor *output, const LocalResponseNormalizationParams ¶ms) + : KernelWithParams({input}, {output}, params) +{ +} + +void LocalResponseNormalization::configure() +{ + LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::FLOAT32); + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void LocalResponseNormalization::execute() const +{ + switch (output()->element_type()) + { + case DataType::FLOAT32: + tflite::LocalResponseNormalizationParams op_params; + op_params.range = params().radius; + op_params.bias = params().bias; + op_params.alpha = params().alpha; + op_params.beta = params().beta; + luci_interpreter_pal::LocalResponseNormalization( + op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LocalResponseNormalization.h b/onert-micro/luci-interpreter/src/kernels/LocalResponseNormalization.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LocalResponseNormalization.h rename to onert-micro/luci-interpreter/src/kernels/LocalResponseNormalization.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LocalResponseNormalization.test.cpp b/onert-micro/luci-interpreter/src/kernels/LocalResponseNormalization.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LocalResponseNormalization.test.cpp rename to onert-micro/luci-interpreter/src/kernels/LocalResponseNormalization.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/LogSoftmax.cpp b/onert-micro/luci-interpreter/src/kernels/LogSoftmax.cpp new file mode 100644 index 0000000..b467cb0 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/LogSoftmax.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/LogSoftmax.h" + +#include "kernels/Utils.h" + +#include + +#include "PALLogSoftmax.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +LogSoftmax::LogSoftmax(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void LogSoftmax::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + if (input()->element_type() == DataType::U8) + { + LUCI_INTERPRETER_CHECK(output()->scale() == 16. / 256); + LUCI_INTERPRETER_CHECK(output()->zero_point() == 255); + + tflite::SoftmaxParams params{}; + + params.table = _table; + params.beta = 1.0; + luci_interpreter_pal::PopulateSoftmaxLookupTable(¶ms, input()->scale(), params.beta); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void LogSoftmax::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void LogSoftmax::evalFloat() const +{ + tflite::SoftmaxParams params{}; + tflite::reference_ops::LogSoftmax(params, getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); +} + +void LogSoftmax::evalQuantized() const +{ + const auto input_shape = getTensorShape(input()); + const auto output_shape = getTensorShape(output()); + const auto input_scale = input()->scale(); + uint8_t *output_data = getTensorData(output()); + const uint8_t *input_data = getTensorData(input()); + const float beta = 1.0; + + tflite::SoftmaxParams params{}; + + params.table = const_cast(_table); + params.zero_point = output()->zero_point(); + params.scale = output()->scale(); + + luci_interpreter_pal::InitializeParams(¶ms, input_scale, beta); + luci_interpreter_pal::LogSoftmax(params, input_scale, input_shape, input_data, output_shape, + output_data); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogSoftmax.h b/onert-micro/luci-interpreter/src/kernels/LogSoftmax.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LogSoftmax.h rename to onert-micro/luci-interpreter/src/kernels/LogSoftmax.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogSoftmax.test.cpp b/onert-micro/luci-interpreter/src/kernels/LogSoftmax.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LogSoftmax.test.cpp rename to onert-micro/luci-interpreter/src/kernels/LogSoftmax.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/LogicalAnd.cpp b/onert-micro/luci-interpreter/src/kernels/LogicalAnd.cpp new file mode 100644 index 0000000..65e0b50 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/LogicalAnd.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2018 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/LogicalAnd.h" + +#include "kernels/Utils.h" + +#include "kernels/BinaryOpCommon.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +LogicalAnd::LogicalAnd(const Tensor *input1, const Tensor *input2, Tensor *output) + : Kernel({input1, input2}, {output}) +{ +} + +void LogicalAnd::configure() +{ + LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); + LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); +} + +void LogicalAnd::execute() const +{ + switch (input1()->element_type()) + { + case DataType::BOOL: + evalLogicalAnd(); + break; + default: + assert(false && "Unsupported type."); + } +} + +inline void LogicalAnd::evalLogicalAnd() const +{ + BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output()), + [](bool x, bool y) { return x && y; }); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalAnd.h b/onert-micro/luci-interpreter/src/kernels/LogicalAnd.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LogicalAnd.h rename to onert-micro/luci-interpreter/src/kernels/LogicalAnd.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalAnd.test.cpp b/onert-micro/luci-interpreter/src/kernels/LogicalAnd.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LogicalAnd.test.cpp rename to onert-micro/luci-interpreter/src/kernels/LogicalAnd.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/LogicalNot.cpp b/onert-micro/luci-interpreter/src/kernels/LogicalNot.cpp new file mode 100644 index 0000000..4ba4499 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/LogicalNot.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/LogicalNot.h" + +#include "kernels/Utils.h" + +#include "kernels/BinaryOpCommon.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +LogicalNot::LogicalNot(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void LogicalNot::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void LogicalNot::execute() const +{ + switch (input()->element_type()) + { + case DataType::BOOL: + evalLogicalNot(); + break; + default: + assert(false && "Unsupported type."); + } +} + +inline void LogicalNot::evalLogicalNot() const +{ + const int size = tflite::MatchingFlatSize(getTensorShape(input()), getTensorShape(output())); + bool *output_data = getTensorData(output()); + const bool *input_data = getTensorData(input()); + for (int i = 0; i < size; ++i) + { + output_data[i] = !input_data[i]; + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalNot.h b/onert-micro/luci-interpreter/src/kernels/LogicalNot.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LogicalNot.h rename to onert-micro/luci-interpreter/src/kernels/LogicalNot.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalNot.test.cpp b/onert-micro/luci-interpreter/src/kernels/LogicalNot.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LogicalNot.test.cpp rename to onert-micro/luci-interpreter/src/kernels/LogicalNot.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/LogicalOr.cpp b/onert-micro/luci-interpreter/src/kernels/LogicalOr.cpp new file mode 100644 index 0000000..ecb8ee4 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/LogicalOr.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/LogicalOr.h" + +#include "kernels/Utils.h" +#include "kernels/BinaryOpCommon.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +LogicalOr::LogicalOr(const Tensor *input1, const Tensor *input2, Tensor *output) + : Kernel({input1, input2}, {output}) +{ +} + +void LogicalOr::configure() +{ + LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); + LUCI_INTERPRETER_CHECK(input1()->element_type() == DataType::BOOL); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); +} + +void LogicalOr::execute() const +{ + BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output()), + [](bool x, bool y) { return x || y; }); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalOr.h b/onert-micro/luci-interpreter/src/kernels/LogicalOr.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LogicalOr.h rename to onert-micro/luci-interpreter/src/kernels/LogicalOr.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/LogicalOr.test.cpp b/onert-micro/luci-interpreter/src/kernels/LogicalOr.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/LogicalOr.test.cpp rename to onert-micro/luci-interpreter/src/kernels/LogicalOr.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Logistic.cpp b/onert-micro/luci-interpreter/src/kernels/Logistic.cpp new file mode 100644 index 0000000..631c060 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Logistic.cpp @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ +namespace +{ + +#ifndef DIS_FLOAT +void evalFloat(const circle::Tensor *input, const circle::Tensor *output, bool is_inplace, + BaseRuntimeGraph *runtime_graph) +{ + const float *input_data = reinterpret_cast(runtime_graph->getDataByTensor(input)); + float *output_data = reinterpret_cast(runtime_graph->getDataByTensor(output)); + + if (is_inplace) + { + output_data = const_cast(input_data); + } + + assert(input_data != nullptr); + assert(output_data != nullptr); + + tflite::reference_ops::Logistic(kernels::getTensorShape(input), input_data, + kernels::getTensorShape(output), output_data); + if (is_inplace) + { + runtime_graph->makeInplaceOperation(input, output); + } +} +#endif // DIS_FLOAT + +#ifndef DIS_QUANT +void evalQuantized(const circle::Tensor *input, const circle::Tensor *output, bool is_inplace, + BaseRuntimeGraph *runtime_graph) +{ + const int8_t *input_data = + reinterpret_cast(runtime_graph->getDataByTensor(input)); + int8_t *output_data = reinterpret_cast(runtime_graph->getDataByTensor(output)); + if (is_inplace) + output_data = const_cast(input_data); + + tflite::reference_ops::Logistic(kernels::getTensorShape(input), input_data, Tensor::scale(input), + Tensor::zero_point(input), kernels::getTensorShape(output), + output_data, Tensor::scale(output), Tensor::zero_point(output)); + if (is_inplace) + { + runtime_graph->makeInplaceOperation(input, output); + } +} +#endif // DIS_QUANT + +} // namespace + +void configure_kernel_CircleLogistic(const circle::Operator *cur_op, + BaseRuntimeGraph *runtime_graph) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + auto output = runtime_graph->getCircleTensorByIndex(output_index); + + assert(input != nullptr); + assert(output != nullptr); + + LUCI_INTERPRETER_CHECK(Tensor::element_type(input) == Tensor::element_type(output)); + +#ifndef DIS_QUANT + if (Tensor::element_type(input) == DataType::U8) + { + LUCI_INTERPRETER_CHECK(Tensor::scale(output) == 1. / 256); + } +#endif // DIS_QUANT +} + +void execute_kernel_CircleLogistic(const circle::Operator *cur_op, BaseRuntimeGraph *runtime_graph, + bool is_inplace) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + auto output = runtime_graph->getCircleTensorByIndex(output_index); + + assert(input != nullptr); + assert(output != nullptr); + + switch (Tensor::element_type(input)) + { +#ifndef DIS_FLOAT + case DataType::FLOAT32: + evalFloat(input, output, is_inplace, runtime_graph); + break; +#endif // DIS_FLOAT +#ifndef DIS_QUANT + case DataType::S8: + evalQuantized(input, output, is_inplace, runtime_graph); + break; +#endif // DIS_QUANT + default: + assert(false && "Unsupported type."); + } +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/Logistic.test.cpp b/onert-micro/luci-interpreter/src/kernels/Logistic.test.cpp new file mode 100644 index 0000000..951dd3d --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Logistic.test.cpp @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// TODO enable it +#if 0 +#include "kernels/Logistic.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +template +void Check(std::initializer_list input_shape, std::initializer_list output_shape, + std::initializer_list input_data, std::initializer_list output_data) +{ + std::unique_ptr memory_manager = std::make_unique(); + + Tensor input_tensor = + makeInputTensor()>(input_shape, input_data, memory_manager.get()); + Tensor output_tensor = makeOutputTensor(getElementType()); + + Logistic kernel(&input_tensor, &output_tensor); + kernel.configure(); + memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); +} + +template <> +void Check(std::initializer_list input_shape, + std::initializer_list output_shape, + std::initializer_list input_data, + std::initializer_list output_data) +{ + std::unique_ptr memory_manager = std::make_unique(); + + std::pair input_quant_param = + quantizationParams(std::min(input_data), std::max(input_data)); + Tensor input_tensor = + makeInputTensor(input_shape, input_quant_param.first, input_quant_param.second, + input_data, memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::U8, 1. / 256, 0); + + Logistic kernel(&input_tensor, &output_tensor); + kernel.configure(); + memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(dequantizeTensorData(output_tensor), + FloatArrayNear(output_data, output_tensor.scale() * 2)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); +} + +template class LogisticTest : public ::testing::Test +{ +}; + +using DataTypes = ::testing::Types; +TYPED_TEST_SUITE(LogisticTest, DataTypes); + +TYPED_TEST(LogisticTest, Simple) +{ + Check( + {89}, {89}, + {-10.0000000000, -9.7727272727, -9.5454545455, -9.3181818182, -9.0909090909, -8.8636363636, + -8.6363636364, -8.4090909091, -8.1818181818, -7.9545454545, -7.7272727273, -7.5000000000, + -7.2727272727, -7.0454545455, -6.8181818182, -6.5909090909, -6.3636363636, -6.1363636364, + -5.9090909091, -5.6818181818, -5.4545454545, -5.2272727273, -5.0000000000, -4.7727272727, + -4.5454545455, -4.3181818182, -4.0909090909, -3.8636363636, -3.6363636364, -3.4090909091, + -3.1818181818, -2.9545454545, -2.7272727273, -2.5000000000, -2.2727272727, -2.0454545455, + -1.8181818182, -1.5909090909, -1.3636363636, -1.1363636364, -0.9090909091, -0.6818181818, + -0.4545454545, -0.2272727273, 0.0000000000, 0.2272727273, 0.4545454545, 0.6818181818, + 0.9090909091, 1.1363636364, 1.3636363636, 1.5909090909, 1.8181818182, 2.0454545455, + 2.2727272727, 2.5000000000, 2.7272727273, 2.9545454545, 3.1818181818, 3.4090909091, + 3.6363636364, 3.8636363636, 4.0909090909, 4.3181818182, 4.5454545455, 4.7727272727, + 5.0000000000, 5.2272727273, 5.4545454545, 5.6818181818, 5.9090909091, 6.1363636364, + 6.3636363636, 6.5909090909, 6.8181818182, 7.0454545455, 7.2727272727, 7.5000000000, + 7.7272727273, 7.9545454545, 8.1818181818, 8.4090909091, 8.6363636364, 8.8636363636, + 9.0909090909, 9.3181818182, 9.5454545455, 9.7727272727, 10.0000000000}, + {0.0000453979, 0.0000569815, 0.0000715205, 0.0000897689, 0.0001126729, 0.0001414198, + 0.0001774998, 0.0002227827, 0.0002796147, 0.0003509396, 0.0004404502, 0.0005527786, + 0.0006937345, 0.0008706021, 0.0010925128, 0.0013709094, 0.0017201256, 0.0021581065, + 0.0027073042, 0.0033957870, 0.0042586071, 0.0053394826, 0.0066928509, 0.0083863576, + 0.0105038445, 0.0131488902, 0.0164489307, 0.0205599431, 0.0256715863, 0.0320125562, + 0.0398556989, 0.0495221198, 0.0613831074, 0.0758581800, 0.0934070047, 0.1145124805, + 0.1396521834, 0.1692560327, 0.2036499335, 0.2429886272, 0.2871859014, 0.3358556241, + 0.3882805886, 0.4434251301, 0.5000000000, 0.5565748699, 0.6117194114, 0.6641443759, + 0.7128140986, 0.7570113728, 0.7963500665, 0.8307439673, 0.8603478166, 0.8854875195, + 0.9065929953, 0.9241418200, 0.9386168926, 0.9504778802, 0.9601443011, 0.9679874438, + 0.9743284137, 0.9794400569, 0.9835510693, 0.9868511098, 0.9894961555, 0.9916136424, + 0.9933071491, 0.9946605174, 0.9957413929, 0.9966042130, 0.9972926958, 0.9978418935, + 0.9982798744, 0.9986290906, 0.9989074872, 0.9991293979, 0.9993062655, 0.9994472214, + 0.9995595498, 0.9996490604, 0.9997203853, 0.9997772173, 0.9998225002, 0.9998585802, + 0.9998873271, 0.9999102311, 0.9999284795, 0.9999430185, 0.9999546021}); +} + +TEST(LogisticTest, IvalidInputOutputType_NEG) +{ + std::unique_ptr memory_manager = std::make_unique(); + + Shape input_shape = {1}; + std::vector input_data{10}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::U8, 1. / 256, 0); + + Logistic kernel(&input_tensor, &output_tensor); + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST(LogisticTest, IvalidQuantParam_NEG) +{ + std::unique_ptr memory_manager = std::make_unique(); + Shape input_shape = {2}; + std::vector input_data{-10, 10}; + std::pair input_quant_param = quantizationParams(-10, 10); + Tensor input_tensor = + makeInputTensor(input_shape, input_quant_param.first, input_quant_param.second, + input_data, memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::U8, 1. / 255, 0); + + Logistic kernel(&input_tensor, &output_tensor); + EXPECT_ANY_THROW(kernel.configure()); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter +#endif diff --git a/onert-micro/luci-interpreter/src/kernels/MaxPool2D.cpp b/onert-micro/luci-interpreter/src/kernels/MaxPool2D.cpp new file mode 100644 index 0000000..865e4f8 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/MaxPool2D.cpp @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Utils.h" + +#include +#include + +namespace luci_interpreter +{ + +namespace +{ + +#ifndef DIS_FLOAT + +void evalFloat(const circle::Tensor *input, const circle::Tensor *output, + const circle::Pool2DOptions *options, BaseRuntimeGraph *runtime_graph) +{ + const int32_t input_height = Tensor::dim(input, 1); + const int32_t input_width = Tensor::dim(input, 2); + + const int32_t output_height = kernels::computeOutputSize( + luci_padding(options->padding()), input_height, options->filter_height(), options->stride_h()); + const int32_t output_width = kernels::computeOutputSize( + luci_padding(options->padding()), input_width, options->filter_width(), options->stride_w()); + + const auto padding_height = kernels::computePadding(options->stride_h(), 1, input_height, + options->filter_height(), output_height); + const auto padding_width = kernels::computePadding(options->stride_w(), 1, input_width, + options->filter_width(), output_width); + + const auto *input_data = runtime_graph->getDataByTensor(input); + auto *output_data = runtime_graph->getDataByTensor(output); + + float activation_min{}; + float activation_max{}; + kernels::calculateActivationRange(luci_actfunc(options->fused_activation_function()), + &activation_min, &activation_max); + tflite::PoolParams params{}; + params.padding_values.height = padding_height; + params.padding_values.width = padding_width; + params.stride_height = options->stride_h(); + params.stride_width = options->stride_w(); + params.filter_height = options->filter_height(); + params.filter_width = options->filter_width(); + params.float_activation_min = activation_min; + params.float_activation_max = activation_max; + + tflite::reference_ops::MaxPool( + params, kernels::getTensorShape(input), kernels::getTensorData(input_data), + kernels::getTensorShape(output), kernels::getTensorData(output_data)); +} + +#endif // DIS_FLOAT + +#ifndef DIS_QUANT +void evalQuantized(const circle::Tensor *input, const circle::Tensor *output, + const circle::Pool2DOptions *options, BaseRuntimeGraph *runtime_graph) +{ + int32_t activation_min{}; + int32_t activation_max{}; + kernels::calculateActivationRangeQuantized(luci_actfunc(options->fused_activation_function()), + output, &activation_min, &activation_max); + + // Compute padding + const int32_t input_height = Tensor::dim(input, 1); + const int32_t input_width = Tensor::dim(input, 2); + + const int32_t output_height = kernels::computeOutputSize( + luci_padding(options->padding()), input_height, options->filter_height(), options->stride_h()); + const int32_t output_width = kernels::computeOutputSize( + luci_padding(options->padding()), input_width, options->filter_width(), options->stride_w()); + + const auto padding_height = kernels::computePadding(options->stride_h(), 1, input_height, + options->filter_height(), output_height); + const auto padding_width = kernels::computePadding(options->stride_w(), 1, input_width, + options->filter_width(), output_width); + + tflite::PoolParams params{}; + params.padding_values.height = padding_height; + params.padding_values.width = padding_width; + params.stride_height = options->stride_h(); + params.stride_width = options->stride_w(); + params.filter_height = options->filter_height(); + params.filter_width = options->filter_width(); + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + const auto *input_data = runtime_graph->getDataByTensor(input); + auto *output_data = runtime_graph->getDataByTensor(output); + + tflite::reference_ops::MaxPool( + params, kernels::getTensorShape(input), kernels::getTensorData(input_data), + kernels::getTensorShape(output), kernels::getTensorData(output_data)); +} + +void evalSInt16(const circle::Tensor *input, const circle::Tensor *output, + const circle::Pool2DOptions *options, BaseRuntimeGraph *runtime_graph) +{ + int32_t activation_min{}; + int32_t activation_max{}; + kernels::calculateActivationRangeQuantized(luci_actfunc(options->fused_activation_function()), + output, &activation_min, &activation_max); + + // Compute padding + const int32_t input_height = Tensor::dim(input, 1); + const int32_t input_width = Tensor::dim(input, 2); + + const int32_t output_height = kernels::computeOutputSize( + luci_padding(options->padding()), input_height, options->filter_height(), options->stride_h()); + const int32_t output_width = kernels::computeOutputSize( + luci_padding(options->padding()), input_width, options->filter_width(), options->stride_w()); + + const auto padding_height = kernels::computePadding(options->stride_h(), 1, input_height, + options->filter_height(), output_height); + const auto padding_width = kernels::computePadding(options->stride_w(), 1, input_width, + options->filter_width(), output_width); + + tflite::PoolParams params{}; + params.padding_values.height = padding_height; + params.padding_values.width = padding_width; + params.stride_height = options->stride_h(); + params.stride_width = options->stride_w(); + params.filter_height = options->filter_height(); + params.filter_width = options->filter_width(); + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + const auto *input_data = runtime_graph->getDataByTensor(input); + auto *output_data = runtime_graph->getDataByTensor(output); + + tflite::reference_integer_ops::MaxPool( + params, kernels::getTensorShape(input), kernels::getTensorData(input_data), + kernels::getTensorShape(output), kernels::getTensorData(output_data)); +} + +#endif // DIS_QUANT + +} // namespace + +void configure_kernel_CircleMaxPool2D(const circle::Operator *cur_op, + BaseRuntimeGraph *runtime_graph) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + const auto output = runtime_graph->getCircleTensorByIndex(output_index); + + LUCI_INTERPRETER_CHECK(Tensor::element_type(input) == Tensor::element_type(output)); + assert(Tensor::num_dims(input) == 4); + +#ifndef DIS_QUANT + if (Tensor::element_type(input) == DataType::U8) + { + LUCI_INTERPRETER_CHECK(std::abs(Tensor::scale(output) - Tensor::scale(input)) <= 1.0e-6); + LUCI_INTERPRETER_CHECK(Tensor::zero_point(output) == Tensor::zero_point(input)); + } + else if (Tensor::element_type(input) == DataType::S16) + { + LUCI_INTERPRETER_CHECK(std::abs(Tensor::scale(output) - Tensor::scale(input)) <= 1.0e-6); + LUCI_INTERPRETER_CHECK(Tensor::zero_point(input) == 0 && Tensor::zero_point(output) == 0); + } +#endif // DIS_QUANT +} + +void execute_kernel_CircleMaxPool2D(const circle::Operator *cur_op, BaseRuntimeGraph *runtime_graph, + bool) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + auto output = runtime_graph->getCircleTensorByIndex(output_index); + + const auto *options = cur_op->builtin_options_as_Pool2DOptions(); + + switch (Tensor::element_type(input)) + { +#ifndef DIS_FLOAT + case DataType::FLOAT32: + evalFloat(input, output, options, runtime_graph); + break; +#endif // DIS_FLOAT +#ifndef DIS_QUANT + case DataType::U8: + evalQuantized(input, output, options, runtime_graph); + break; + case DataType::S16: + evalSInt16(input, output, options, runtime_graph); + break; +#endif // DIS_QUANT + default: + assert(false && "Unsupported type."); + } +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/MaxPool2D.test.cpp b/onert-micro/luci-interpreter/src/kernels/MaxPool2D.test.cpp new file mode 100644 index 0000000..d59c5f0 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/MaxPool2D.test.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// TODO enable it +#if 0 +#include "kernels/MaxPool2D.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +class MaxPool2DTest : public ::testing::Test +{ +protected: + void SetUp() override { _memory_manager = std::make_unique(); } + + std::unique_ptr _memory_manager; +}; + +TEST_F(MaxPool2DTest, Float) +{ + Shape input_shape{1, 3, 5, 1}; + std::vector input_data{ + 1, -1, 0, -2, 2, // + -7, -6, -5, -4, -3, // + 5, 4, 3, 6, 7, // + }; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Pool2DParams params{}; + params.padding = Padding::VALID; + params.filter_height = 2; + params.filter_width = 3; + params.stride_height = 1; + params.stride_width = 2; + params.activation = Activation::RELU6; + + MaxPool2D kernel(&input_tensor, &output_tensor, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + std::vector ref_output_data{ + 1, 2, // + 5, 6, // + }; + std::initializer_list ref_output_shape{1, 2, 2, 1}; + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(MaxPool2DTest, Uint8) +{ + std::pair quant_param = quantizationParams(-15.9375, 15.9375); + std::vector input_data{ + 0, -6, 12, 4, // + -3, -2, 10, 7, // + }; + Tensor input_tensor = makeInputTensor( + {1, 2, 4, 1}, quant_param.first, quant_param.second, input_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::U8, quant_param.first, quant_param.second); + + Pool2DParams params{}; + params.padding = Padding::VALID; + params.filter_height = 2; + params.filter_width = 2; + params.stride_height = 2; + params.stride_width = 2; + params.activation = Activation::RELU6; + + MaxPool2D kernel(&input_tensor, &output_tensor, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + std::vector ref_output_data{0.0, 6.0}; + std::initializer_list ref_output_shape{1, 1, 2, 1}; + EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(MaxPool2DTest, SInt16) +{ + Shape input_shape{1, 3, 5, 1}; + std::vector ref_output_shape{1, 2, 2, 1}; + std::vector input_data{ + 1, -1, 0, -2, 2, // + -7, -6, -5, -4, -3, // + 5, 4, 3, 6, 7, // + }; + std::vector ref_output_data{ + 1, 2, // + 5, 6, // + }; + + Tensor input_tensor = + makeInputTensor(input_shape, 0.2, 0, input_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::S16, 0.2, 0); + + Pool2DParams params{}; + params.padding = Padding::VALID; + params.filter_height = 2; + params.filter_width = 3; + params.stride_height = 1; + params.stride_width = 2; + params.activation = Activation::RELU6; + + MaxPool2D kernel(&input_tensor, &output_tensor, params); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); + EXPECT_THAT(dequantizeTensorData(output_tensor), FloatArrayNear(ref_output_data)); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter +#ednif diff --git a/onert-micro/luci-interpreter/src/kernels/Maximum.cpp b/onert-micro/luci-interpreter/src/kernels/Maximum.cpp new file mode 100644 index 0000000..1a7ee4c --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Maximum.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2018 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Maximum.h" + +#include "kernels/Utils.h" + +#include "kernels/BinaryOpCommon.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +Maximum::Maximum(const Tensor *input1, const Tensor *input2, Tensor *output) + : Kernel({input1, input2}, {output}) +{ +} + +void Maximum::configure() +{ + LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()) + LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()) + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); +} + +void Maximum::execute() const +{ + switch (input1()->element_type()) + { + case DataType::FLOAT32: + evalMaximum(); + break; + case DataType::U8: + evalMaximum(); + break; + default: + assert(false && "Unsupported type."); + } +} + +template inline void Maximum::evalMaximum() const +{ + BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output()), + [](T x, T y) { return std::max(x, y); }); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Maximum.h b/onert-micro/luci-interpreter/src/kernels/Maximum.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Maximum.h rename to onert-micro/luci-interpreter/src/kernels/Maximum.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Maximum.test.cpp b/onert-micro/luci-interpreter/src/kernels/Maximum.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Maximum.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Maximum.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Mean.cpp b/onert-micro/luci-interpreter/src/kernels/Mean.cpp new file mode 100644 index 0000000..4128aa6 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Mean.cpp @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Mean.h" + +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +static void resolveAxes(const int32_t *axes_data, int num_axes, tflite::MeanParams *params) +{ + params->axis_count = num_axes; + for (int i = 0; i < num_axes; ++i) + { + params->axis[i] = static_cast(axes_data[i]); + } + for (int i = num_axes; i < 4; ++i) + { + params->axis[i] = 1; + } +} + +// Returns the number of axes that will be reduced. Removes duplicates. +static int getAxisReductionCount(const int32_t *axes_data, int num_axes, int input_num_dims) +{ + int reduction_count = num_axes; + for (int i = 0; i < num_axes; ++i) + { + int current = axes_data[i] >= 0 ? axes_data[i] : axes_data[i] + input_num_dims; + assert(current >= 0 && current < input_num_dims); + for (int j = 0; j < i; j++) + { + int previous = axes_data[j] >= 0 ? axes_data[j] : axes_data[j] + input_num_dims; + // This checks for duplicate axis + if (current == previous) + { + --reduction_count; + break; + } + } + } + return reduction_count; +} + +static Shape getOutputShape(const Shape &input_shape, const int32_t *axes_data, int num_axes, + bool keep_dims) +{ + int input_num_dims = input_shape.num_dims(); + if (input_num_dims == 0) + { + return Shape(0); + } + + if (keep_dims) + { + Shape output_shape(input_num_dims); + for (int idx = 0; idx < input_num_dims; ++idx) + { + bool is_axis = false; + for (int axis_idx = 0; axis_idx < num_axes; ++axis_idx) + { + if (axes_data[axis_idx] == idx || axes_data[axis_idx] + input_num_dims == idx) + { + is_axis = true; + break; + } + } + if (is_axis) + { + output_shape.dim(idx) = 1; + } + else + { + output_shape.dim(idx) = input_shape.dim(idx); + } + } + return output_shape; + } + else + { + int num_reduce_axes = getAxisReductionCount(axes_data, num_axes, input_num_dims); + Shape output_shape(input_num_dims - num_reduce_axes); + int num_skip_axes = 0; + for (int idx = 0; idx < input_num_dims; ++idx) + { + bool is_axis = false; + for (int axis_idx = 0; axis_idx < num_axes; ++axis_idx) + { + if (axes_data[axis_idx] == idx || axes_data[axis_idx] + input_num_dims == idx) + { + ++num_skip_axes; + is_axis = true; + break; + } + } + if (!is_axis) + { + output_shape.dim(idx - num_skip_axes) = input_shape.dim(idx); + } + } + return output_shape; + } +} + +Mean::Mean(const Tensor *input, const Tensor *axes, Tensor *output, Tensor *temp_index, + Tensor *resolved_axes, Tensor *temp_sum, const ReducerParams ¶ms) + : KernelWithParams({input, axes}, {output, temp_index, resolved_axes, temp_sum}, + params) +{ +} + +void Mean::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + LUCI_INTERPRETER_CHECK(axes()->element_type() == DataType::S32); + if (input()->element_type() == DataType::S16) + { + LUCI_INTERPRETER_CHECK(input()->zero_point() == 0 && output()->zero_point() == 0); + } + + const Shape &input_shape = input()->shape(); + int input_num_dims = input_shape.num_dims(); + + const auto *axes_data = getTensorData(axes()); + int num_axes = axes()->shape().num_elements(); + assert(num_axes <= 4); + // TODO: enable it only if kernel with dynamic shapes + Shape output_shape = getOutputShape(input_shape, axes_data, num_axes, _params.keep_dims); + output()->resize(output_shape); + + tflite::MeanParams params{}; + resolveAxes(axes_data, num_axes, ¶ms); + _need_temporaries = !( + _params.keep_dims && input_num_dims == 4 && params.axis_count == 2 && + ((params.axis[0] == 1 && params.axis[1] == 2) || (params.axis[0] == 2 && params.axis[1] == 1))); + if (_need_temporaries) + { + auto temp_index = getOutputTensors()[1]; + auto resolved_axes = getOutputTensors()[2]; + auto temp_sum = getOutputTensors()[3]; + + temp_index->resize(Shape(input_num_dims)); + resolved_axes->resize(Shape(num_axes)); + temp_sum->resize(output()->shape()); + } + else + { + auto temp_index = getOutputTensors()[1]; + auto resolved_axes = getOutputTensors()[2]; + auto temp_sum = getOutputTensors()[3]; + + temp_index->set_allocatable(false); + resolved_axes->set_allocatable(false); + temp_sum->set_allocatable(false); + } +} + +void Mean::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::U8: + evalQuantized(); + break; + case DataType::S16: + evalQuantizedS16(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Mean::evalFloat() const +{ + const Shape &input_shape = input()->shape(); + int input_num_dims = input_shape.num_dims(); + const auto *axes_data = getTensorData(axes()); + int num_axes = axes()->shape().num_elements(); + + tflite::MeanParams params{}; + resolveAxes(axes_data, num_axes, ¶ms); + + auto temp_index = getOutputTensors()[1]; + auto resolved_axes = getOutputTensors()[2]; + auto temp_sum = getOutputTensors()[3]; + + // Defer to specialized implementation for 4D Mean across axes 1 & 2. + if (_params.keep_dims && input_num_dims == 4 && params.axis_count == 2 && + ((params.axis[0] == 1 && params.axis[1] == 2) || + (params.axis[0] == 2 && params.axis[1] == 1))) + { + tflite::reference_ops::Mean(params, getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); + } + else + { + tflite::reference_ops::Mean(getTensorData(input()), getTensorShape(input()).DimsData(), + input()->shape().num_dims(), getTensorData(output()), + getTensorShape(output()).DimsData(), output()->shape().num_dims(), + axes_data, num_axes, _params.keep_dims, + getTensorData(temp_index), getTensorData(resolved_axes), + getTensorData(temp_sum)); + } +} + +void Mean::evalQuantized() const +{ + const Shape &input_shape = input()->shape(); + int input_num_dims = input_shape.num_dims(); + const auto *axes_data = getTensorData(axes()); + int num_axes = axes()->shape().num_elements(); + + tflite::MeanParams params{}; + resolveAxes(axes_data, num_axes, ¶ms); + + auto temp_index = getOutputTensors()[1]; + auto resolved_axes = getOutputTensors()[2]; + auto temp_sum = getOutputTensors()[3]; + + // Defer to specialized implementation for 4D Mean across axes 1 & 2. + if (_params.keep_dims && input_num_dims == 4 && params.axis_count == 2 && + ((params.axis[0] == 1 && params.axis[1] == 2) || + (params.axis[0] == 2 && params.axis[1] == 1))) + { + tflite::reference_ops::Mean(params, getTensorShape(input()), getTensorData(input()), + input()->zero_point(), input()->scale(), getTensorShape(output()), + getTensorData(output()), output()->zero_point(), + output()->scale()); + } + else if (input()->zero_point() == output()->zero_point() && input()->scale() == output()->scale()) + { + tflite::reference_ops::Mean(getTensorData(input()), getTensorShape(input()).DimsData(), + input()->shape().num_dims(), getTensorData(output()), + getTensorShape(output()).DimsData(), output()->shape().num_dims(), + axes_data, num_axes, _params.keep_dims, + getTensorData(temp_index), getTensorData(resolved_axes), + getTensorData(temp_sum)); + } + else + { + tflite::reference_ops::QuantizedMeanOrSum<>( + getTensorData(input()), input()->zero_point(), input()->scale(), + getTensorShape(input()).DimsData(), input()->shape().num_dims(), + getTensorData(output()), output()->zero_point(), output()->scale(), + getTensorShape(output()).DimsData(), output()->shape().num_dims(), axes_data, num_axes, + _params.keep_dims, getTensorData(temp_index), getTensorData(resolved_axes), + getTensorData(temp_sum), + /*compute_sum=*/false); + } +} + +void Mean::evalQuantizedS16() const +{ + const auto *input_data = getTensorData(input()); + auto *output_data = getTensorData(output()); + + const Shape &input_shape = input()->shape(); + const Shape &output_shape = output()->shape(); + + const auto *axes_data = getTensorData(axes()); + const int num_axes = axes()->shape().num_elements(); + + constexpr int32_t output_min = -std::numeric_limits::max(); + constexpr int32_t output_max = std::numeric_limits::max(); + + // Defer to specialized implementation for 4D Mean across axes 1 & 2. + if (_params.keep_dims && input_shape.num_dims() == 4 && num_axes == 2 && + ((axes_data[0] == 1 && axes_data[1] == 2) || (axes_data[0] == 2 && axes_data[1] == 1))) + { + const int32_t batches = input_shape.dim(0); + const int32_t input_height = input_shape.dim(1); + const int32_t input_width = input_shape.dim(2); + const int32_t depth = input_shape.dim(3); + assert(output_shape.num_dims() == 4); + assert(output_shape.dim(0) == batches); + assert(output_shape.dim(1) == 1); + assert(output_shape.dim(2) == 1); + assert(output_shape.dim(3) == depth); + + const double real_multiplier = + static_cast(input()->scale()) / static_cast(output()->scale()); + + int32_t output_multiplier{}; + int output_shift{}; + quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); + + const int32_t num_elements_in_axes = input_height * input_width; + + for (int32_t batch = 0; batch < batches; ++batch) + { + for (int32_t c = 0; c < depth; ++c) + { + int32_t acc = 0; + for (int32_t in_y = 0; in_y < input_height; ++in_y) + { + for (int32_t in_x = 0; in_x < input_width; ++in_x) + { + acc += input_data[calcOffset(input_shape, batch, in_y, in_x, c)]; + } + } + int32_t scaled_acc = + tflite::MultiplyByQuantizedMultiplier(acc, output_multiplier, output_shift); + // Divide by the number of elements rounding to the nearest integer. + scaled_acc = scaled_acc > 0 + ? (scaled_acc + num_elements_in_axes / 2) / num_elements_in_axes + : (scaled_acc - num_elements_in_axes / 2) / num_elements_in_axes; + + scaled_acc = std::max(scaled_acc, output_min); + scaled_acc = std::min(scaled_acc, output_max); + + output_data[calcOffset(output_shape, batch, 0, 0, c)] = scaled_acc; + } + } + } + else + { + assert(false && "Unsupported configuration."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Mean.h b/onert-micro/luci-interpreter/src/kernels/Mean.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Mean.h rename to onert-micro/luci-interpreter/src/kernels/Mean.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Mean.test.cpp b/onert-micro/luci-interpreter/src/kernels/Mean.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Mean.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Mean.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Minimum.cpp b/onert-micro/luci-interpreter/src/kernels/Minimum.cpp new file mode 100644 index 0000000..f74e6c0 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Minimum.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2018 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Minimum.h" + +#include "kernels/Utils.h" + +#include "kernels/BinaryOpCommon.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +Minimum::Minimum(const Tensor *input1, const Tensor *input2, Tensor *output) + : Kernel({input1, input2}, {output}) +{ +} + +void Minimum::configure() +{ + LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()) + LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()) + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); +} + +void Minimum::execute() const +{ + switch (input1()->element_type()) + { + case DataType::FLOAT32: + evalMinimum(); + break; + case DataType::U8: + evalMinimum(); + break; + default: + assert(false && "Unsupported type."); + } +} + +template inline void Minimum::evalMinimum() const +{ + BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output()), + [](T x, T y) { return std::min(x, y); }); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Minimum.h b/onert-micro/luci-interpreter/src/kernels/Minimum.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Minimum.h rename to onert-micro/luci-interpreter/src/kernels/Minimum.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Minimum.test.cpp b/onert-micro/luci-interpreter/src/kernels/Minimum.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Minimum.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Minimum.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/MirrorPad.cpp b/onert-micro/luci-interpreter/src/kernels/MirrorPad.cpp new file mode 100644 index 0000000..d9e60b0 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/MirrorPad.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/MirrorPad.h" + +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +MirrorPad::MirrorPad(const Tensor *input, const Tensor *paddings, Tensor *output, + const MirrorPadParams ¶ms) + : KernelWithParams({input, paddings}, {output}, params) +{ +} + +void MirrorPad::configure() +{ + const Shape &input_shape = input()->shape(); + const int num_dims = input_shape.num_dims(); + + if (num_dims > 4) + assert(false && "Unsupported number of dimensions."); + + assert(output()->element_type() == input()->element_type()); + assert(paddings()->element_type() == DataType::S32); + // Paddings shape should be [N, 2]. + assert(paddings()->shape().num_dims() == 2); + assert(paddings()->shape().dim(0) == num_dims); + assert(paddings()->shape().dim(1) == 2); + + Shape output_shape(num_dims); + const auto *paddings_data = getTensorData(paddings()); + for (int i = 0; i < num_dims; ++i) + { + const int32_t padding_before = paddings_data[i * 2]; + const int32_t padding_after = paddings_data[i * 2 + 1]; + assert(padding_before >= 0 && padding_after >= 0); + output_shape.dim(i) = input_shape.dim(i) + padding_before + padding_after; + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +template +inline void MirrorPadImpl(const Tensor &input, const Tensor &paddings, MirrorPadMode mode, + Tensor &output); + +void MirrorPad::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + { + MirrorPadImpl(*input(), *paddings(), params().mode, *output()); + break; + } + case DataType::U8: + { + assert(output()->zero_point() >= std::numeric_limits::min()); + assert(output()->zero_point() <= std::numeric_limits::max()); + + MirrorPadImpl(*input(), *paddings(), params().mode, *output()); + break; + } + default: + assert(false && "Unsupported type."); + } +} + +template +inline void MirrorPadImpl(const Tensor &input, const Tensor &paddings, MirrorPadMode mode, + Tensor &output) +{ + auto const input_dims = input.shape().num_dims(); + auto const input_data = input.data(); + auto const paddings_data = paddings.data(); + auto const output_data = output.data(); + + auto const input_b = input_dims > 3 ? input.shape().dim(input_dims - 4) : 1; + auto const input_h = input_dims > 2 ? input.shape().dim(input_dims - 3) : 1; + auto const input_w = input_dims > 1 ? input.shape().dim(input_dims - 2) : 1; + auto const input_d = input.shape().dim(input_dims - 1); + + auto const input_h_offset = input_d * input_w; + auto const input_b_offset = input_h_offset * input_h; + + auto const output_b = input_dims > 3 ? output.shape().dim(input_dims - 4) : 1; + auto const output_h = input_dims > 2 ? output.shape().dim(input_dims - 3) : 1; + auto const output_w = input_dims > 1 ? output.shape().dim(input_dims - 2) : 1; + auto const output_d = output.shape().dim(input_dims - 1); + + auto const left_b_pad = paddings_data[2 * (input_dims - 4)]; + auto const left_h_pad = paddings_data[2 * (input_dims - 3)]; + auto const left_w_pad = paddings_data[2 * (input_dims - 2)]; + auto const left_d_pad = paddings_data[2 * (input_dims - 1)]; + + auto const right_b_pad = paddings_data[2 * (input_dims - 4) + 1]; + auto const right_h_pad = paddings_data[2 * (input_dims - 3) + 1]; + auto const right_w_pad = paddings_data[2 * (input_dims - 2) + 1]; + auto const right_d_pad = paddings_data[2 * (input_dims - 1) + 1]; + + const auto positive_mod = [](auto a, auto b) { return (a % b + b) % b; }; + const auto offset_index = [input_d, input_h_offset, input_b_offset](auto d, auto w, auto h, + auto b) { + return d + w * input_d + h * input_h_offset + b * input_b_offset; + }; + + const auto symmetric_dim = [&positive_mod](auto i, auto left_pad, auto input) { + bool reflected = (((i < left_pad ? i + 1 - input : i) - left_pad) / input & 1) == 1; + return positive_mod(reflected ? input + left_pad - i - 1 : i - left_pad, input); + }; + + const T *in_ptr = input_data; + T *out_ptr = output_data; + + for (int32_t b = 0; b < output_b; ++b) + { + for (int32_t h = 0; h < output_h; ++h) + { + for (int32_t w = 0; w < output_w; ++w) + { + for (int32_t d = 0; d < output_d; ++d) + { + if (b < left_b_pad || b >= output_b - right_b_pad || // + h < left_h_pad || h >= output_h - right_h_pad || // + w < left_w_pad || w >= output_w - right_w_pad || // + d < left_d_pad || d >= output_d - right_d_pad) + { + if (mode == MirrorPadMode::REFLECT) + { + *out_ptr++ = input_data[offset_index( + positive_mod(d - left_d_pad, input_d), positive_mod(w - left_w_pad, input_w), + positive_mod(h - left_h_pad, input_h), positive_mod(b - left_b_pad, input_b))]; + } + else + { + *out_ptr++ = input_data[offset_index( + symmetric_dim(d, left_d_pad, input_d), symmetric_dim(w, left_w_pad, input_w), + symmetric_dim(h, left_h_pad, input_h), symmetric_dim(b, left_b_pad, input_b))]; + } + } + else + { + *out_ptr++ = *in_ptr++; + } + } + } + } + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/MirrorPad.h b/onert-micro/luci-interpreter/src/kernels/MirrorPad.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/MirrorPad.h rename to onert-micro/luci-interpreter/src/kernels/MirrorPad.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/MirrorPad.test.cpp b/onert-micro/luci-interpreter/src/kernels/MirrorPad.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/MirrorPad.test.cpp rename to onert-micro/luci-interpreter/src/kernels/MirrorPad.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Mul.cpp b/onert-micro/luci-interpreter/src/kernels/Mul.cpp new file mode 100644 index 0000000..bac9fe3 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Mul.cpp @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Mul.h" + +#include "kernels/BinaryOpCommon.h" +#include "kernels/Utils.h" + +#include "PALMul.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +Mul::Mul(const Tensor *input1, const Tensor *input2, Tensor *output, const MulParams ¶ms) + : KernelWithParams({input1, input2}, {output}, params) +{ +} + +void Mul::configure() +{ + LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); + LUCI_INTERPRETER_CHECK(output()->element_type() == input1()->element_type()); + if (input1()->element_type() == DataType::S16) + { + LUCI_INTERPRETER_CHECK(input1()->zero_points().size() == 1 && + input2()->zero_points().size() == 1) + LUCI_INTERPRETER_CHECK(input1()->zero_point() == 0 && input2()->zero_point() == 0 && + output()->zero_point() == 0); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); +} + +void Mul::execute() const +{ + switch (input1()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::S64: + evalInteger(); + break; + case DataType::S32: + evalInteger(); + break; + case DataType::S16: + evalQuantizedS16(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Mul::evalFloat() const +{ + tflite::ArithmeticParams params{}; + fillArithmeticActivationRange(params, _params.activation); + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + luci_interpreter_pal::BroadcastMul4DSlow( + params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), + getTensorData(input2()), getTensorShape(output()), getTensorData(output())); + } + else + { + luci_interpreter_pal::Mul(params, getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +template void Mul::evalInteger() const +{ + tflite::ArithmeticParams params{}; + fillArithmeticActivationRange(params, _params.activation); + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + luci_interpreter_pal::BroadcastMul4DSlow( + params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), + getTensorData(input2()), getTensorShape(output()), getTensorData(output())); + } + else + { + luci_interpreter_pal::Mul(params, getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +void Mul::evalQuantizedS16() const +{ + const auto input1_scale = static_cast(input1()->scale()); + const auto input2_scale = static_cast(input2()->scale()); + const auto output_scale = static_cast(output()->scale()); + + const double real_multiplier = input1_scale * input2_scale / output_scale; + + int32_t output_multiplier; + int output_shift; + quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); + + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + + auto fn = [output_multiplier, output_shift, activation_min, activation_max](int16_t input1_val, + int16_t input2_val) { + int32_t output = static_cast(input1_val) * static_cast(input2_val); + output = tflite::MultiplyByQuantizedMultiplier(output, output_multiplier, output_shift); + output = std::max(output, activation_min); + output = std::min(output, activation_max); + return static_cast(output); + }; + + BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output()), fn); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Mul.h b/onert-micro/luci-interpreter/src/kernels/Mul.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Mul.h rename to onert-micro/luci-interpreter/src/kernels/Mul.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Mul.test.cpp b/onert-micro/luci-interpreter/src/kernels/Mul.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Mul.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Mul.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Neg.cpp b/onert-micro/luci-interpreter/src/kernels/Neg.cpp new file mode 100644 index 0000000..961148a --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Neg.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Neg.h" +#include "kernels/Utils.h" + +#include "PALNeg.h" + +namespace luci_interpreter +{ + +namespace kernels +{ + +Neg::Neg(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Neg::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Neg::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Neg::evalFloat() const +{ + luci_interpreter_pal::Negate(getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Neg.h b/onert-micro/luci-interpreter/src/kernels/Neg.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Neg.h rename to onert-micro/luci-interpreter/src/kernels/Neg.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Neg.test.cpp b/onert-micro/luci-interpreter/src/kernels/Neg.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Neg.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Neg.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/NotEqual.cpp b/onert-micro/luci-interpreter/src/kernels/NotEqual.cpp new file mode 100644 index 0000000..8c3983e --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/NotEqual.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/NotEqual.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +NotEqual::NotEqual(const Tensor *x, const Tensor *y, Tensor *output) : Kernel({x, y}, {output}) {} + +void NotEqual::configure() +{ + LUCI_INTERPRETER_CHECK(x()->element_type() == y()->element_type()); + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::BOOL); + + if (x()->element_type() == DataType::U8) + { + quantizeMultiplierSmallerThanOneExp(x()->scale(), &_x_multiplier, &_x_shift); + quantizeMultiplierSmallerThanOneExp(y()->scale(), &_y_multiplier, &_y_shift); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(x()->shape(), y()->shape())); +} + +void NotEqual::execute() const +{ + switch (x()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::S64: + evalInteger(); + break; + case DataType::S32: + evalInteger(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void NotEqual::evalFloat() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowNotEqual(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::NotEqual(op_params, getTensorShape(x()), x_data, getTensorShape(y()), + y_data, getTensorShape(output()), output_data); + } +} + +template void NotEqual::evalInteger() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowNotEqualNoScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } + else + { + tflite::reference_ops::NotEqualNoScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } +} + +void NotEqual::evalQuantized() const +{ + const auto x_data = getTensorData(x()); + const auto y_data = getTensorData(y()); + auto output_data = getTensorData(output()); + + tflite::ComparisonParams op_params; + op_params.left_shift = 8; + op_params.input1_offset = -x()->zero_point(); // Note the '-' + op_params.input1_shift = _x_shift; + op_params.input1_multiplier = _x_multiplier; + op_params.input2_offset = -y()->zero_point(); // Note the '-' + op_params.input2_shift = _y_shift; + op_params.input2_multiplier = _y_multiplier; + op_params.is_broadcast = x()->shape() != y()->shape(); + + if (op_params.is_broadcast) + { + tflite::reference_ops::Broadcast4DSlowNotEqualWithScaling( + op_params, getTensorShape(x()), x_data, getTensorShape(y()), y_data, getTensorShape(output()), + output_data); + } + else + { + tflite::reference_ops::NotEqualWithScaling(op_params, getTensorShape(x()), x_data, + getTensorShape(y()), y_data, + getTensorShape(output()), output_data); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/NotEqual.h b/onert-micro/luci-interpreter/src/kernels/NotEqual.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/NotEqual.h rename to onert-micro/luci-interpreter/src/kernels/NotEqual.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/NotEqual.test.cpp b/onert-micro/luci-interpreter/src/kernels/NotEqual.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/NotEqual.test.cpp rename to onert-micro/luci-interpreter/src/kernels/NotEqual.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/OneHot.cpp b/onert-micro/luci-interpreter/src/kernels/OneHot.cpp new file mode 100644 index 0000000..fa99895 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/OneHot.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/OneHot.h" +#include "kernels/Utils.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +namespace +{ + +template +void OneHotComputeImpl(const Tensor *indices_tensor, const Tensor *on_value_tensor, + const Tensor *off_value_tensor, int32_t depth, int32_t axis, + Tensor *output_tensor) +{ + // define input shape and correct axis + auto const &input_shape = indices_tensor->shape(); + axis = axis == -1 ? input_shape.num_dims() : axis; + + // TODO support other integer input types + auto const *indices = getTensorData(indices_tensor); + auto const on_value = getTensorData(on_value_tensor)[0]; + auto const off_value = getTensorData(off_value_tensor)[0]; + auto *output = getTensorData(output_tensor); + + // prefix_dim_size == # of elements before the axis + // depth == # of elements per axis + // suffix_dim_size == # of elements after the axis + auto prefix_dim_size = 1; + for (int32_t i = 0; i < axis; ++i) + { + prefix_dim_size *= input_shape.dim(i); + } + assert(prefix_dim_size > 0); + auto const suffix_dim_size = input_shape.num_elements() / prefix_dim_size; + + // View the indices as a matrix of size: + // prefix_dim_size x suffix_dim_size + // View the output as a matrix of size: + // prefix_dim_size x depth x suffix_dim_size + // Then the output is: + // output(i, j, k) == (indices(i, k) == j) ? on : off + for (int32_t i = 0; i < prefix_dim_size; ++i) + for (int32_t j = 0; j < depth; ++j) + for (int32_t k = 0; k < suffix_dim_size; ++k, ++output) + *output = indices[i * suffix_dim_size + k] == j ? on_value : off_value; +} + +} // namespace + +OneHot::OneHot(const Tensor *indices, const Tensor *depth, const Tensor *on_value, + const Tensor *off_value, Tensor *output, const OneHotParams ¶ms) + : KernelWithParams({indices, depth, on_value, off_value}, {output}, params) +{ + // Do nothing +} + +void OneHot::configure() +{ + // check types + LUCI_INTERPRETER_CHECK(indices()->element_type() == DataType::S32); + LUCI_INTERPRETER_CHECK(depth()->element_type() == DataType::S32); + LUCI_INTERPRETER_CHECK(on_value()->element_type() == off_value()->element_type()); + LUCI_INTERPRETER_CHECK(output()->element_type() == on_value()->element_type()); + + // check shape dependent parameters + LUCI_INTERPRETER_CHECK(on_value()->shape().num_elements() == 1); + LUCI_INTERPRETER_CHECK(off_value()->shape().num_elements() == 1); + LUCI_INTERPRETER_CHECK(depth()->shape().num_elements() == 1); + LUCI_INTERPRETER_CHECK(params().axis >= -1 && params().axis <= indices()->shape().num_dims()); + + // define parameters that affect the output shape + auto const depth_value = getTensorData(depth())[0]; + auto const &input_shape = indices()->shape(); + auto const input_dims = input_shape.num_dims(); + auto const axis = params().axis == -1 ? input_dims : params().axis; + + // define output shape + Shape output_shape(input_shape.num_dims() + 1); + { + for (int32_t d = 0; d < axis; ++d) + output_shape.dim(d) = input_shape.dim(d); + + output_shape.dim(axis) = depth_value; + + for (int32_t d = axis + 1; d < output_shape.num_dims(); ++d) + output_shape.dim(d) = input_shape.dim(d - 1); + } + // TODO: enable it only if kernel with dynamic shapes + // reshape output + output()->resize(output_shape); +} + +void OneHot::execute() const +{ + auto const depth_value = getTensorData(depth())[0]; + auto const axis = params().axis; + + switch (output()->element_type()) + { + case DataType::FLOAT32: + OneHotComputeImpl(indices(), on_value(), off_value(), depth_value, axis, output()); + break; + case DataType::U8: + OneHotComputeImpl(indices(), on_value(), off_value(), depth_value, axis, output()); + break; + case DataType::S16: + OneHotComputeImpl(indices(), on_value(), off_value(), depth_value, axis, output()); + break; + default: + // TODO Support other data types + assert(false && "Not supported, yet!"); + break; + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/OneHot.h b/onert-micro/luci-interpreter/src/kernels/OneHot.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/OneHot.h rename to onert-micro/luci-interpreter/src/kernels/OneHot.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/OneHot.test.cpp b/onert-micro/luci-interpreter/src/kernels/OneHot.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/OneHot.test.cpp rename to onert-micro/luci-interpreter/src/kernels/OneHot.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/PRelu.cpp b/onert-micro/luci-interpreter/src/kernels/PRelu.cpp new file mode 100644 index 0000000..3d64215 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/PRelu.cpp @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/PRelu.h" + +#include "kernels/BinaryOpCommon.h" +#include "kernels/Utils.h" + +#include +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +PRelu::PRelu(const Tensor *input, const Tensor *alpha, Tensor *output) + : Kernel({input, alpha}, {output}) +{ +} + +PRelu::~PRelu() +{ + // Destructor declared to delete vector of alpha quantized data properly +} + +void PRelu::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + LUCI_INTERPRETER_CHECK(alpha()->element_type() == output()->element_type()); + LUCI_INTERPRETER_CHECK(input()->scales().size() <= 1); + LUCI_INTERPRETER_CHECK(output()->scales().size() <= 1); + + if (input()->element_type() == DataType::U8) + { + LUCI_INTERPRETER_CHECK(alpha()->scales().size() <= 1); // remove when CWQ kernel arrives + _alpha_multipliers.resize(1); + double alpha_multiplier = input()->scale() * alpha()->scale() / output()->scale(); + quantizeMultiplier(alpha_multiplier, &_alpha_multipliers[0].multiplier, + &_alpha_multipliers[0].shift); + double identity_multiplier = input()->scale() / output()->scale(); + quantizeMultiplier(identity_multiplier, &_output_multiplier_identity, &_output_shift_identity); + } + else if (input()->element_type() == DataType::S16) + { + // Common check for correctness of quant params + LUCI_INTERPRETER_CHECK(input()->zero_point() == 0 && output()->zero_point() == 0); + for (size_t channel = 0; channel < alpha()->zero_points().size(); ++channel) + { + LUCI_INTERPRETER_CHECK(alpha()->zero_points()[channel] == 0); + } + // PRelu specific checks for CWQ + LUCI_INTERPRETER_CHECK(alpha()->quantized_dimension() == alpha()->shape().num_dims() - 1); + LUCI_INTERPRETER_CHECK(static_cast(alpha()->scales().size()) == + alpha()->shape().dim(alpha()->quantized_dimension())); + LUCI_INTERPRETER_CHECK(alpha()->shape().num_elements() == + input()->shape().dim(input()->shape().num_dims() - 1)); + + // all dimension of alpha except last one should be size 1 + for (int dim = 0; dim < alpha()->shape().num_dims() - 1; ++dim) + { + LUCI_INTERPRETER_CHECK(alpha()->shape().dim(dim) == 1); + } + + std::vector real_multipliers = + getQuantizedConvolutionMultiplers(input()->scale(), alpha()->scales(), output()->scale()); + + _alpha_multipliers = quantizeMultipliers(real_multipliers); + + double identity_multiplier = input()->scale() / output()->scale(); + quantizeMultiplier(identity_multiplier, &_output_multiplier_identity, &_output_shift_identity); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(input()->shape(), alpha()->shape())); +} + +void PRelu::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::U8: + evalQuantized(); + break; + case DataType::S16: + evalQuantizedS16(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void PRelu::evalFloat() const +{ + const auto input_data = getTensorData(input()); + const auto alpha_data = getTensorData(alpha()); + const auto size = getTensorShape(input()).FlatSize(); + auto output_data = getTensorData(output()); + + auto PReluFunc = [](float input, float alpha) { return input >= 0.0 ? input : input * alpha; }; + + if (input()->shape() != alpha()->shape()) + { + tflite::reference_ops::BroadcastBinaryFunction4DSlow( + getTensorShape(input()), getTensorData(input()), getTensorShape(alpha()), + getTensorData(alpha()), getTensorShape(output()), getTensorData(output()), + PReluFunc); + } + else + { + for (auto i = decltype(size){0}; i < size; ++i) + { + if (input_data[i] >= 0) + output_data[i] = input_data[i]; + else + output_data[i] = input_data[i] * alpha_data[i]; + } + } +} + +void PRelu::evalQuantized() const +{ + tflite::PreluParams op_params{}; + + op_params.input_offset = -input()->zero_point(); // Note the '-'. + op_params.alpha_offset = -alpha()->zero_point(); // Note the '-'. + op_params.output_offset = output()->zero_point(); + op_params.output_shift_1 = _output_shift_identity; + op_params.output_multiplier_1 = _output_multiplier_identity; + op_params.output_shift_2 = _alpha_multipliers[0].shift; + op_params.output_multiplier_2 = _alpha_multipliers[0].multiplier; + + if (input()->shape() != alpha()->shape()) + { + tflite::reference_ops::BroadcastPrelu4DSlow( + op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(alpha()), + getTensorData(alpha()), getTensorShape(output()), getTensorData(output())); + } + else + { + tflite::reference_ops::Prelu( + op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(alpha()), + getTensorData(alpha()), getTensorShape(output()), getTensorData(output())); + } +} + +static inline int16_t evalElemS16PRelu(int16_t input_val, int16_t alpha_val, + const ChannelQuantMultipliers &identity_mult, + const ChannelQuantMultipliers &alpha_mult) +{ + constexpr int32_t quantized_min = std::numeric_limits::min(); + constexpr int32_t quantized_max = std::numeric_limits::max(); + + const int32_t output_val = + input_val >= 0 + ? tflite::MultiplyByQuantizedMultiplier(static_cast(input_val), + identity_mult.multiplier, identity_mult.shift) + : tflite::MultiplyByQuantizedMultiplier(static_cast(input_val * alpha_val), + alpha_mult.multiplier, alpha_mult.shift); + const int32_t clamped_output = std::min(quantized_max, std::max(quantized_min, output_val)); + return clamped_output; +} + +void PRelu::evalQuantizedS16() const +{ + // Note that this kernel assumes alpha is CWQ + tflite::RuntimeShape input_shape = getTensorShape(input()); + const int16_t *input_data = input()->data(); + const int16_t *alpha_data = alpha()->data(); + int16_t *output_data = output()->data(); + + const ChannelQuantMultipliers pos_mult{_output_shift_identity, _output_multiplier_identity}; + + const int last_dim = input()->shape().num_dims() - 1; + + int32_t outer_dims_size = 1; + for (int i = 0; i < last_dim; ++i) + outer_dims_size *= input_shape.Dims(i); + int32_t quant_dim_size = input_shape.Dims(last_dim); + + for (int32_t outer_dims = 0; outer_dims < outer_dims_size; ++outer_dims) + for (int32_t quant_channel = 0; quant_channel < quant_dim_size; ++quant_channel) + { + const ChannelQuantMultipliers &neg_mult = _alpha_multipliers[quant_channel]; + size_t offset = static_cast(outer_dims) * static_cast(quant_dim_size); + offset += quant_channel; + + output_data[offset] = + evalElemS16PRelu(input_data[offset], alpha_data[quant_channel], pos_mult, neg_mult); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/PRelu.h b/onert-micro/luci-interpreter/src/kernels/PRelu.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/PRelu.h rename to onert-micro/luci-interpreter/src/kernels/PRelu.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/PRelu.test.cpp b/onert-micro/luci-interpreter/src/kernels/PRelu.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/PRelu.test.cpp rename to onert-micro/luci-interpreter/src/kernels/PRelu.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Pack.cpp b/onert-micro/luci-interpreter/src/kernels/Pack.cpp new file mode 100644 index 0000000..62228f1 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Pack.cpp @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Pack.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +Pack::Pack(std::vector inputs, Tensor *output, const PackParams ¶ms) + : KernelWithParams(std::move(inputs), {output}, params) +{ +} + +void Pack::configure() +{ + LUCI_INTERPRETER_CHECK(_inputs.size() == static_cast(params().values_count)); + const Tensor *t0 = _inputs[0]; + const int dimension_size = t0->shape().num_dims() + 1; + int axis = params().axis; + if (axis < 0) + { + axis += dimension_size; + } + LUCI_INTERPRETER_CHECK(axis >= 0 && axis <= t0->shape().num_dims()); + + if (t0->element_type() != DataType::S32 && t0->element_type() != DataType::FLOAT32 && + t0->element_type() != DataType::U8 && t0->element_type() != DataType::S8 && + t0->element_type() != DataType::S16 && t0->element_type() != DataType::S64) + { + assert(false && "Unsupported type."); + } + + for (uint32_t i = 1; i < _inputs.size(); ++i) + { + const Tensor *tensor = _inputs[i]; + LUCI_INTERPRETER_CHECK(tensor->element_type() == t0->element_type()); + LUCI_INTERPRETER_CHECK(tensor->shape().num_dims() == t0->shape().num_dims()); + for (int d = 0; d < t0->shape().num_dims(); ++d) + { + LUCI_INTERPRETER_CHECK(tensor->shape().dim(d) == t0->shape().dim(d)); + } + } + + Shape output_shape(dimension_size); + int i = 0; + for (int index = 0; index < dimension_size; ++index) + { + if (index == axis) + { + output_shape.dim(index) = params().values_count; + } + else + { + output_shape.dim(index) = t0->shape().dim(i++); + } + } + + if (t0->element_type() == DataType::U8 || t0->element_type() == DataType::S8 || + t0->element_type() == DataType::S16) + { + LUCI_INTERPRETER_CHECK(output()->zero_point() == t0->zero_point()); + LUCI_INTERPRETER_CHECK(output()->scale() == t0->scale()); + // Guarantee input/output quantization params match as we do not support + // packing quantized tensors. + for (int i = 0; i < params().values_count; i++) + { + LUCI_INTERPRETER_CHECK(_inputs[i]->zero_point() == t0->zero_point()); + LUCI_INTERPRETER_CHECK(_inputs[i]->scale() == t0->scale()); + } + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void Pack::execute() const +{ + switch (_inputs[0]->element_type()) + { + case DataType::FLOAT32: + evalGeneric(); + break; + case DataType::U8: + evalGeneric(); + break; + case DataType::S8: + evalGeneric(); + break; + case DataType::S16: + evalGeneric(); + break; + case DataType::S32: + evalGeneric(); + break; + case DataType::S64: + evalGeneric(); + break; + default: + assert(false && "Unsupported type."); + } +} + +template void Pack::evalGeneric() const +{ + const Tensor *t0 = _inputs[0]; + const int dimension_size = t0->shape().num_dims() + 1; + int axis = params().axis; + if (axis < 0) + { + axis += dimension_size; + } + + VectorOfTensors inputs(_inputs); + tflite::PackParams params{}; + params.axis = axis; + params.inputs_count = _inputs.size(); + tflite::reference_ops::Pack(params, inputs.shapes(), inputs.data(), getTensorShape(output()), + getTensorData(output())); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Pack.h b/onert-micro/luci-interpreter/src/kernels/Pack.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Pack.h rename to onert-micro/luci-interpreter/src/kernels/Pack.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Pack.test.cpp b/onert-micro/luci-interpreter/src/kernels/Pack.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Pack.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Pack.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Pad.cpp b/onert-micro/luci-interpreter/src/kernels/Pad.cpp new file mode 100644 index 0000000..6b45265 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Pad.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Pad.h" + +#include "kernels/Utils.h" + +#include + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +Pad::Pad(const Tensor *input, const Tensor *paddings, Tensor *output) + : Kernel({input, paddings}, {output}) +{ +} + +void Pad::configure() +{ + const Shape &input_shape = input()->shape(); + const int num_dims = input_shape.num_dims(); + + if (num_dims > 4) + assert(false && "Unsupported number of dimensions."); + + assert(output()->element_type() == input()->element_type()); + assert(paddings()->element_type() == DataType::S32); + // Paddings shape should be [N, 2]. + assert(paddings()->shape().num_dims() == 2); + assert(paddings()->shape().dim(0) == num_dims); + assert(paddings()->shape().dim(1) == 2); + + Shape output_shape(num_dims); + const auto *paddings_data = getTensorData(paddings()); + for (int i = 0; i < num_dims; ++i) + { + const int32_t padding_before = paddings_data[i * 2]; + const int32_t padding_after = paddings_data[i * 2 + 1]; + assert(padding_before >= 0 && padding_after >= 0); + output_shape.dim(i) = input_shape.dim(i) + padding_before + padding_after; + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void Pad::execute() const +{ + const int num_dims = input()->shape().num_dims(); + + tflite::PadParams params{}; + params.left_padding_count = num_dims; + params.right_padding_count = num_dims; + + const auto *paddings_data = getTensorData(paddings()); + for (int i = num_dims - 1; i >= 0; --i) + { + params.left_padding[i] = paddings_data[i * 2]; + params.right_padding[i] = paddings_data[i * 2 + 1]; + } + + switch (input()->element_type()) + { + case DataType::FLOAT32: + { + const float pad_value = 0.0f; + tflite::reference_ops::Pad(params, getTensorShape(input()), getTensorData(input()), + &pad_value, getTensorShape(output()), + getTensorData(output())); + break; + } + case DataType::U8: + { + assert(output()->zero_point() >= std::numeric_limits::min()); + assert(output()->zero_point() <= std::numeric_limits::max()); + const auto pad_value = static_cast(output()->zero_point()); + tflite::reference_ops::Pad(params, getTensorShape(input()), getTensorData(input()), + &pad_value, getTensorShape(output()), + getTensorData(output())); + break; + } + case DataType::S8: + { + assert(output()->zero_point() >= std::numeric_limits::min()); + assert(output()->zero_point() <= std::numeric_limits::max()); + const auto pad_value = static_cast(output()->zero_point()); + tflite::reference_ops::Pad(params, getTensorShape(input()), getTensorData(input()), + &pad_value, getTensorShape(output()), + getTensorData(output())); + break; + } + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Pad.h b/onert-micro/luci-interpreter/src/kernels/Pad.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Pad.h rename to onert-micro/luci-interpreter/src/kernels/Pad.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Pad.test.cpp b/onert-micro/luci-interpreter/src/kernels/Pad.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Pad.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Pad.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/PadV2.cpp b/onert-micro/luci-interpreter/src/kernels/PadV2.cpp new file mode 100644 index 0000000..c141e3d --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/PadV2.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/PadV2.h" + +#include "kernels/Utils.h" + +#include + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +PadV2::PadV2(const Tensor *input, const Tensor *paddings, const Tensor *constant_values, + Tensor *output) + : Kernel({input, paddings, constant_values}, {output}) +{ +} + +void PadV2::configure() +{ + const Shape &input_shape = input()->shape(); + const int num_dims = input_shape.num_dims(); + + if (num_dims > 4) + assert(false && "Unsupported number of dimensions."); + + assert(output()->element_type() == input()->element_type()); + assert(paddings()->element_type() == DataType::S32); + assert(constant_values()->element_type() == output()->element_type()); + // Paddings shape should be [N, 2]. + assert(paddings()->shape().num_dims() == 2); + assert(paddings()->shape().dim(0) == num_dims); + assert(paddings()->shape().dim(1) == 2); + // Constant values elements number should be 1. + assert(constant_values()->shape().num_elements() == 1); + + Shape output_shape(num_dims); + const auto *paddings_data = getTensorData(paddings()); + for (int i = 0; i < num_dims; ++i) + { + const int32_t padding_before = paddings_data[i * 2]; + const int32_t padding_after = paddings_data[i * 2 + 1]; + assert(padding_before >= 0 && padding_after >= 0); + output_shape.dim(i) = input_shape.dim(i) + padding_before + padding_after; + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void PadV2::execute() const +{ + const int num_dims = input()->shape().num_dims(); + + tflite::PadParams params{}; + params.left_padding_count = num_dims; + params.right_padding_count = num_dims; + + const auto *paddings_data = getTensorData(paddings()); + for (int i = num_dims - 1; i >= 0; --i) + { + params.left_padding[i] = paddings_data[i * 2]; + params.right_padding[i] = paddings_data[i * 2 + 1]; + } + + switch (input()->element_type()) + { + case DataType::FLOAT32: + { + const auto pad_value = getTensorData(constant_values())[0]; + tflite::reference_ops::Pad(params, getTensorShape(input()), getTensorData(input()), + &pad_value, getTensorShape(output()), + getTensorData(output())); + break; + } + case DataType::U8: + { + assert(output()->zero_point() >= std::numeric_limits::min()); + assert(output()->zero_point() <= std::numeric_limits::max()); + const auto pad_value = getTensorData(constant_values())[0]; + tflite::reference_ops::Pad(params, getTensorShape(input()), getTensorData(input()), + &pad_value, getTensorShape(output()), + getTensorData(output())); + break; + } + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/PadV2.h b/onert-micro/luci-interpreter/src/kernels/PadV2.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/PadV2.h rename to onert-micro/luci-interpreter/src/kernels/PadV2.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/PadV2.test.cpp b/onert-micro/luci-interpreter/src/kernels/PadV2.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/PadV2.test.cpp rename to onert-micro/luci-interpreter/src/kernels/PadV2.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Pow.cpp b/onert-micro/luci-interpreter/src/kernels/Pow.cpp new file mode 100644 index 0000000..6345bf3 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Pow.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Pow.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +Pow::Pow(const Tensor *input1, const Tensor *input2, Tensor *output) + : Kernel({input1, input2}, {output}) +{ +} + +void Pow::configure() +{ + LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()); + LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); +} + +void Pow::execute() const +{ + switch (input1()->element_type()) + { + case DataType::FLOAT32: + eval(); + break; + case DataType::S32: + eval(); + break; + default: + assert(false && "Unsupported type."); + } +} + +template void Pow::eval() const +{ + tflite::ArithmeticParams params{}; + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + tflite::reference_ops::BroadcastPow4DSlow(getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } + else + { + tflite::reference_ops::Pow(getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Pow.h b/onert-micro/luci-interpreter/src/kernels/Pow.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Pow.h rename to onert-micro/luci-interpreter/src/kernels/Pow.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Pow.test.cpp b/onert-micro/luci-interpreter/src/kernels/Pow.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Pow.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Pow.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Quantize.cpp b/onert-micro/luci-interpreter/src/kernels/Quantize.cpp new file mode 100644 index 0000000..9f622d0 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Quantize.cpp @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Quantize.h" +#include "kernels/Utils.h" +#include "PALQuantize.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +namespace +{ + +template void call_requantize(const Tensor *input, Tensor *output) +{ + int32_t multiplier; + int shift; + + const double effective_output_scale = input->scale() / output->scale(); + quantizeMultiplier(effective_output_scale, &multiplier, &shift); + + const auto input_shape = getTensorShape(input); + const auto output_shape = getTensorShape(output); + const auto size = tflite::MatchingFlatSize(input_shape, output_shape); + + const auto input_data = getTensorData(input); + + switch (output->element_type()) + { + case DataType::S8: + luci_interpreter_pal::Requantize(input_data, size, multiplier, shift, input->zero_point(), + output->zero_point(), getTensorData(output)); + break; + case DataType::U8: + luci_interpreter_pal::Requantize(input_data, size, multiplier, shift, input->zero_point(), + output->zero_point(), getTensorData(output)); + break; + case DataType::S16: + luci_interpreter_pal::Requantize(input_data, size, multiplier, shift, input->zero_point(), + output->zero_point(), getTensorData(output)); + break; + default: + assert(false && "Unsupported quantized type, yet!"); + } +} + +} // namespace + +Quantize::Quantize(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Quantize::configure() +{ + + if (input()->element_type() == DataType::S16) + LUCI_INTERPRETER_CHECK(input()->zero_point() == 0); + + switch (input()->element_type()) + { + case DataType::FLOAT32: + { + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::U8 || + output()->element_type() == DataType::S8 || + output()->element_type() == DataType::S16); + break; + } + case DataType::S16: + case DataType::S8: + case DataType::U8: + { + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::S8 || + output()->element_type() == DataType::U8 || + output()->element_type() == DataType::S16); + if (output()->element_type() == DataType::S16) + { + LUCI_INTERPRETER_CHECK(output()->zero_point() == 0); + } + break; + } + default: + assert(false && "Unsupported type"); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Quantize::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + { + tflite::QuantizationParams op_params; + op_params.zero_point = output()->zero_point(); + op_params.scale = output()->scale(); + const auto input_data = getTensorData(input()); + + switch (output()->element_type()) + { + case DataType::S8: + { + luci_interpreter_pal::Quantize(op_params, getTensorShape(input()), input_data, + getTensorShape(output()), getTensorData(output())); + break; + } + case DataType::U8: + { + luci_interpreter_pal::Quantize(op_params, getTensorShape(input()), input_data, + getTensorShape(output()), + getTensorData(output())); + break; + } + case DataType::S16: + { + luci_interpreter_pal::Quantize(op_params, getTensorShape(input()), input_data, + getTensorShape(output()), + getTensorData(output())); + break; + } + default: + assert(false && "Unsupported type."); + } + break; + } + case DataType::S16: + { + call_requantize(input(), output()); + break; + } + case DataType::S8: + { + call_requantize(input(), output()); + break; + } + case DataType::U8: + { + call_requantize(input(), output()); + break; + } + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Quantize.h b/onert-micro/luci-interpreter/src/kernels/Quantize.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Quantize.h rename to onert-micro/luci-interpreter/src/kernels/Quantize.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Quantize.test.cpp b/onert-micro/luci-interpreter/src/kernels/Quantize.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Quantize.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Quantize.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Relu.cpp b/onert-micro/luci-interpreter/src/kernels/Relu.cpp new file mode 100644 index 0000000..9cd1556 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Relu.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Relu.h" +#include "kernels/Utils.h" + +#include "PALRelu.h" + +namespace luci_interpreter +{ + +namespace kernels +{ + +Relu::Relu(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Relu::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + if (input()->element_type() == DataType::S16) + { + LUCI_INTERPRETER_CHECK(input()->zero_point() == 0 && output()->zero_point() == 0); + } + + if (input()->element_type() == DataType::U8 || input()->element_type() == DataType::S16) + { + double multiplier = input()->scale() / output()->scale(); + quantizeMultiplier(multiplier, &_output_multiplier, &_output_shift); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Relu::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::U8: + evalQuantized(); + break; + case DataType::S16: + evalQuantizedS16(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Relu::evalFloat() const +{ + const auto input_data = getTensorData(input()); + const auto input_shape = getTensorShape(input()); + auto output_data = getTensorData(output()); + auto output_shape = getTensorShape(output()); + + luci_interpreter_pal::Relu(input_shape, input_data, output_shape, output_data); +} + +void Relu::evalQuantized() const +{ + tflite::ReluParams params; + params.input_offset = input()->zero_point(); + params.output_offset = output()->zero_point(); + params.output_multiplier = _output_multiplier; + params.output_shift = _output_shift; + + params.quantized_activation_min = + std::max(static_cast(std::numeric_limits::min()), params.output_offset); + params.quantized_activation_max = static_cast(std::numeric_limits::max()); + + luci_interpreter_pal::ReluX(params, getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); +} + +void Relu::evalQuantizedS16() const +{ + const auto *input_data = getTensorData(input()); + auto *output_data = getTensorData(output()); + + constexpr int32_t output_min = 0; + constexpr int32_t output_max = std::numeric_limits::max(); + + const int32_t num_elements = input()->shape().num_elements(); + + for (int32_t i = 0; i < num_elements; ++i) + { + const int32_t input_val = input_data[i]; + int32_t output_val = + tflite::MultiplyByQuantizedMultiplier(input_val, _output_multiplier, _output_shift); + output_val = std::max(output_val, output_min); + output_val = std::min(output_val, output_max); + output_data[i] = static_cast(output_val); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Relu.h b/onert-micro/luci-interpreter/src/kernels/Relu.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Relu.h rename to onert-micro/luci-interpreter/src/kernels/Relu.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Relu.test.cpp b/onert-micro/luci-interpreter/src/kernels/Relu.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Relu.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Relu.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Relu6.cpp b/onert-micro/luci-interpreter/src/kernels/Relu6.cpp new file mode 100644 index 0000000..c2072e3 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Relu6.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Relu6.h" +#include "kernels/Utils.h" + +#include "PALRelu6.h" + +namespace luci_interpreter +{ + +namespace kernels +{ + +Relu6::Relu6(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Relu6::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + + if (input()->element_type() == DataType::U8) + { + double multiplier = input()->scale() / output()->scale(); + quantizeMultiplier(multiplier, &_output_multiplier, &_output_shift); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Relu6::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Relu6::evalFloat() const +{ + const auto input_data = getTensorData(input()); + const auto input_shape = getTensorShape(input()); + auto output_data = getTensorData(output()); + auto output_shape = getTensorShape(output()); + + luci_interpreter_pal::Relu6(input_shape, input_data, output_shape, output_data); +} + +void Relu6::evalQuantized() const +{ + tflite::ReluParams params; + params.input_offset = input()->zero_point(); + params.output_offset = output()->zero_point(); + params.output_multiplier = _output_multiplier; + params.output_shift = _output_shift; + + params.quantized_activation_min = + std::max(static_cast(std::numeric_limits::min()), params.output_offset); + params.quantized_activation_max = + std::min(static_cast(std::numeric_limits::max()), + params.output_offset + static_cast(roundf(6.f / output()->scale()))); + + luci_interpreter_pal::ReluX(params, getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Relu6.h b/onert-micro/luci-interpreter/src/kernels/Relu6.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Relu6.h rename to onert-micro/luci-interpreter/src/kernels/Relu6.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Relu6.test.cpp b/onert-micro/luci-interpreter/src/kernels/Relu6.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Relu6.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Relu6.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Reshape.cpp b/onert-micro/luci-interpreter/src/kernels/Reshape.cpp new file mode 100644 index 0000000..ba47df4 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Reshape.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include +#include + +namespace luci_interpreter +{ + +void configure_kernel_CircleReshape(const circle::Operator *cur_op, BaseRuntimeGraph *runtime_graph) +{ + // Do nothing +} + +void execute_kernel_CircleReshape(const circle::Operator *cur_op, BaseRuntimeGraph *runtime_graph, + bool is_inplace) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + const auto output = runtime_graph->getCircleTensorByIndex(output_index); + + if (is_inplace) + { + runtime_graph->makeInplaceOperation(input, output); + return; + } + + const auto input_data = (runtime_graph->getDataByTensor(input)); + auto output_data = (runtime_graph->getDataByTensor(output)); + + assert(input_data != nullptr); + assert(output_data != nullptr); + + const size_t element_size = getDataTypeSize(Tensor::element_type(input)); + const int32_t num_elements = Tensor::num_elements(input); + std::memcpy(output_data, input_data, num_elements * element_size); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/Reshape.test.cpp b/onert-micro/luci-interpreter/src/kernels/Reshape.test.cpp new file mode 100644 index 0000000..745f6ca --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Reshape.test.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#if 0 +#include "kernels/Reshape.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +class ReshapeTest : public ::testing::Test +{ +protected: + void SetUp() override { _memory_manager = std::make_unique(); } + + std::unique_ptr _memory_manager; +}; + +// TODO Test types other than FLOAT32. + +TEST_F(ReshapeTest, Regular) +{ + Shape input_shape{1, 2, 2, 3}; + std::vector input_data{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + Shape shape_shape{2}; + std::vector shape_data{3, 4}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor shape_tensor = + makeInputTensor(shape_shape, shape_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Reshape kernel(&input_tensor, &shape_tensor, &output_tensor); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(input_data)); +} + +TEST_F(ReshapeTest, UnknownDimension) +{ + Shape input_shape{2, 1, 2, 3}; + std::vector input_data{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + Shape shape_shape{3}; + std::vector shape_data{2, -1, 2}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + Tensor shape_tensor = + makeInputTensor(shape_shape, shape_data, _memory_manager.get()); + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Reshape kernel(&input_tensor, &shape_tensor, &output_tensor); + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(input_data)); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter +#endif diff --git a/onert-micro/luci-interpreter/src/kernels/ResizeBilinear.cpp b/onert-micro/luci-interpreter/src/kernels/ResizeBilinear.cpp new file mode 100644 index 0000000..8d07ea7 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/ResizeBilinear.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/ResizeBilinear.h" + +#include "kernels/Utils.h" + +#include "PALResizeBilinear.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +ResizeBilinear::ResizeBilinear(const Tensor *input, const Tensor *size, Tensor *output, + const ResizeBilinearParams ¶ms) + : KernelWithParams({input, size}, {output}, params) +{ +} + +void ResizeBilinear::configure() +{ + LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); + LUCI_INTERPRETER_CHECK(size()->shape().num_dims() == 1); + LUCI_INTERPRETER_CHECK(size()->element_type() == DataType::S32); + if (params().half_pixel_centers && params().align_corners) + assert(false && "If half_pixel_centers is True, align_corners must be False."); + LUCI_INTERPRETER_CHECK(size()->shape().dim(0) == 2); + Shape output_shape(4); + output_shape.dim(0) = input()->shape().dim(0); + output_shape.dim(1) = getTensorData(size())[0]; + output_shape.dim(2) = getTensorData(size())[1]; + output_shape.dim(3) = input()->shape().dim(3); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void ResizeBilinear::execute() const +{ + tflite::ResizeBilinearParams op_params{}; + op_params.align_corners = params().align_corners; + op_params.half_pixel_centers = params().half_pixel_centers; + switch (output()->element_type()) + { + case DataType::FLOAT32: + luci_interpreter_pal::ResizeBilinear( + op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(size()), + getTensorData(size()), getTensorShape(output()), getTensorData(output())); + break; + case DataType::U8: + luci_interpreter_pal::ResizeBilinear( + op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(size()), + getTensorData(size()), getTensorShape(output()), getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ResizeBilinear.h b/onert-micro/luci-interpreter/src/kernels/ResizeBilinear.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/ResizeBilinear.h rename to onert-micro/luci-interpreter/src/kernels/ResizeBilinear.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ResizeBilinear.test.cpp b/onert-micro/luci-interpreter/src/kernels/ResizeBilinear.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/ResizeBilinear.test.cpp rename to onert-micro/luci-interpreter/src/kernels/ResizeBilinear.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.cpp b/onert-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.cpp new file mode 100644 index 0000000..57ca4d5 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/ResizeNearestNeighbor.h" + +#include "kernels/Utils.h" + +#include +#include "PALResizeNearestNeighbor.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +ResizeNearestNeighbor::ResizeNearestNeighbor(const Tensor *input, const Tensor *size, + Tensor *output, + const ResizeNearestNeighborParams ¶ms) + : KernelWithParams({input, size}, {output}, params) +{ +} + +void ResizeNearestNeighbor::configure() +{ + LUCI_INTERPRETER_CHECK(input()->shape().num_dims() == 4); + LUCI_INTERPRETER_CHECK(size()->shape().num_dims() == 1); + LUCI_INTERPRETER_CHECK(size()->element_type() == DataType::S32); + LUCI_INTERPRETER_CHECK(size()->shape().dim(0) == 2); + Shape output_shape(4); + output_shape.dim(0) = input()->shape().dim(0); + output_shape.dim(1) = getTensorData(size())[0]; + output_shape.dim(2) = getTensorData(size())[1]; + output_shape.dim(3) = input()->shape().dim(3); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void ResizeNearestNeighbor::execute() const +{ + tflite::ResizeNearestNeighborParams op_params{}; + op_params.align_corners = params().align_corners; + op_params.half_pixel_centers = params().half_pixel_centers; + switch (output()->element_type()) + { + case DataType::FLOAT32: + tflite::reference_ops::ResizeNearestNeighbor( + op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(size()), + getTensorData(size()), getTensorShape(output()), getTensorData(output())); + break; + case DataType::U8: + luci_interpreter_pal::ResizeNearestNeighbor( + op_params, getTensorShape(input()), getTensorData(input()), getTensorShape(size()), + getTensorData(size()), getTensorShape(output()), getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.h b/onert-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.h rename to onert-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.test.cpp b/onert-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.test.cpp rename to onert-micro/luci-interpreter/src/kernels/ResizeNearestNeighbor.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/ReverseV2.cpp b/onert-micro/luci-interpreter/src/kernels/ReverseV2.cpp new file mode 100644 index 0000000..76eadbd --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/ReverseV2.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/ReverseV2.h" +#include "kernels/Utils.h" +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +ReverseV2::ReverseV2(const Tensor *input, const Tensor *axes, Tensor *output) + : Kernel({input, axes}, {output}) +{ +} + +void ReverseV2::configure() +{ + assert(axes()->shape().num_dims() == 1); + assert(input()->shape().num_dims() >= axes()->shape().num_elements()); + if (input()->element_type() != DataType::S32 && input()->element_type() != DataType::FLOAT32 && + input()->element_type() != DataType::U8 && input()->element_type() != DataType::S16 && + input()->element_type() != DataType::S64) + { + assert(false && "Unsupported input type."); + } + if (axes()->element_type() != DataType::S32) + { + assert(false && "Unsupported axes type."); + } + if (axes()->shape().num_elements() > 1) + { + assert(false && "Current implementation does not support more than 1 axis."); + } + int axis_value = getTensorData(axes())[0]; + if (axis_value < 0 || axis_value >= input()->shape().num_dims()) + { + assert(false && "Invalid axes value"); + } + assert(input()->element_type() == output()->element_type()); + + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void ReverseV2::execute() const +{ + int axis_value = getTensorData(axes())[0]; + switch (output()->element_type()) + { + case DataType::FLOAT32: + tflite::reference_ops::Reverse(axis_value, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + case DataType::U8: + tflite::reference_ops::Reverse( + axis_value, getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); + break; + default: + assert(false && "Unsupported output type"); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ReverseV2.h b/onert-micro/luci-interpreter/src/kernels/ReverseV2.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/ReverseV2.h rename to onert-micro/luci-interpreter/src/kernels/ReverseV2.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/ReverseV2.test.cpp b/onert-micro/luci-interpreter/src/kernels/ReverseV2.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/ReverseV2.test.cpp rename to onert-micro/luci-interpreter/src/kernels/ReverseV2.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Rsqrt.cpp b/onert-micro/luci-interpreter/src/kernels/Rsqrt.cpp new file mode 100644 index 0000000..c45c3e4 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Rsqrt.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Rsqrt.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +Rsqrt::Rsqrt(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Rsqrt::configure() +{ + if (input()->element_type() != output()->element_type()) + { + assert(false && "Input/output tensor data type mismatch."); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Rsqrt::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + + default: + assert(false && "Unsupported type."); + } +} + +void Rsqrt::evalFloat() const +{ + auto in = getTensorData(input()); + auto out = getTensorData(output()); + auto size = getTensorShape(input()).FlatSize(); + for (auto i = in; i != in + size; ++i) + { + *out = 1.f / std::sqrt(*i); + ++out; + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Rsqrt.h b/onert-micro/luci-interpreter/src/kernels/Rsqrt.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Rsqrt.h rename to onert-micro/luci-interpreter/src/kernels/Rsqrt.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Rsqrt.test.cpp b/onert-micro/luci-interpreter/src/kernels/Rsqrt.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Rsqrt.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Rsqrt.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/SVDF.cpp b/onert-micro/luci-interpreter/src/kernels/SVDF.cpp new file mode 100644 index 0000000..a0ff302 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/SVDF.cpp @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/SVDF.h" +#include "kernels/Utils.h" +#include "PALSVDF.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +namespace +{ +TfLiteFusedActivation get_tflite_activation(Activation activation) +{ + switch (activation) + { + case FusedActFunc::RELU: + return kTfLiteActRelu; + case FusedActFunc::RELU6: + return kTfLiteActRelu6; + case FusedActFunc::RELU_N1_TO_1: + return kTfLiteActReluN1To1; + case FusedActFunc::TANH: + return kTfLiteActTanh; + case FusedActFunc::SIGN_BIT: + return kTfLiteActSignBit; + case FusedActFunc::NONE: + return kTfLiteActNone; + default: + assert(false && "Unsupported activation type"); + } +} +} // namespace + +SVDF::SVDF(const Tensor *input, const Tensor *weight_feature, const Tensor *weight_time, + const Tensor *bias, const Tensor *input_activation_state, Tensor *output, + Tensor *scratchpad_activation_state, Tensor *scratchpad_1, Tensor *scratchpad_2, + Tensor *scratchpad_3, Tensor *scratchpad_4, Tensor *scratchpad_5, Tensor *scratchpad_6, + const SVDFParams ¶ms) + : KernelWithParams({input, weight_feature, weight_time, bias, input_activation_state}, + {output, scratchpad_activation_state, scratchpad_1, scratchpad_2, + scratchpad_3, scratchpad_4, scratchpad_5, scratchpad_6}, + params) +{ + // Do nothing +} + +void SVDF::configure() +{ + const Shape &input_shape = input()->shape(); + const Shape &weight_features_shape = weight_feature()->shape(); + const Shape &weight_time_shape = weight_time()->shape(); + + // Validate Input Tensor: + LUCI_INTERPRETER_CHECK(input()->element_type() == DataType::FLOAT32 || + input()->element_type() == DataType::S8); + LUCI_INTERPRETER_CHECK(input_shape.num_dims() == 2); + + // Validate inputs and output types + if (input()->element_type() == DataType::S8) + { + LUCI_INTERPRETER_CHECK(weight_feature()->element_type() == DataType::S8); + LUCI_INTERPRETER_CHECK(weight_time()->element_type() == DataType::S16 || + weight_time()->element_type() == DataType::S8); + if (bias()) + LUCI_INTERPRETER_CHECK(bias()->element_type() == DataType::S32); + + LUCI_INTERPRETER_CHECK(input_activation_state()->element_type() == DataType::S16 || + input_activation_state()->element_type() == DataType::S8); + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::S8); + + // Note: now tflite support only ReLU activation for integer SVDF + LUCI_INTERPRETER_CHECK(params().activation == FusedActFunc::RELU); + } + else if (weight_feature()->element_type() == DataType::FLOAT32) + { + LUCI_INTERPRETER_CHECK(weight_feature()->element_type() == DataType::FLOAT32); + LUCI_INTERPRETER_CHECK(weight_time()->element_type() == DataType::FLOAT32); + LUCI_INTERPRETER_CHECK(input_activation_state()->element_type() == DataType::FLOAT32); + if (bias()) + LUCI_INTERPRETER_CHECK(bias()->element_type() == DataType::FLOAT32); + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::FLOAT32); + } + else if ((weight_feature()->element_type() == DataType::U8 || + weight_feature()->element_type() == DataType::S8) && + input()->element_type() == DataType::FLOAT32) + { + // TODO:: support hybrid SVDF op + assert(false && "Hybrid type is not currently supported"); + } + else + { + assert(false && "Unsupported type."); + } + + // Check all the parameters of tensor match within themselves and match the + // input configuration. + const int rank = params().svdf_rank; + const int batch_size = input_shape.dim(0); + const int num_filters = weight_features_shape.dim(0); + LUCI_INTERPRETER_CHECK(rank != 0); + LUCI_INTERPRETER_CHECK(num_filters % rank == 0); + + const int num_units = num_filters / rank; + const int memory_size = weight_time_shape.dim(1); + + // Validate Weight_Feature Input Tensor: + LUCI_INTERPRETER_CHECK(weight_features_shape.num_dims() == 2); + LUCI_INTERPRETER_CHECK(weight_features_shape.dim(1) == input_shape.dim(1)); + + // Validate Weight_Time Input Tensor: + LUCI_INTERPRETER_CHECK(weight_time_shape.num_dims() == 2); + LUCI_INTERPRETER_CHECK(weight_time_shape.dim(0) == num_filters); + + // Validate Bias + if (bias()) + LUCI_INTERPRETER_CHECK(bias()->shape().dim(0) == num_units); + + // Validate Input Activation State + LUCI_INTERPRETER_CHECK(input_activation_state()->shape().num_dims() == 2); + LUCI_INTERPRETER_CHECK(input_activation_state()->shape().dim(0) == batch_size); + LUCI_INTERPRETER_CHECK(input_activation_state()->shape().dim(1) == memory_size * num_filters); + + // Resize scratchpad_state to input_activation_state + auto scratchpad_activation_state = getOutputTensors()[1]; + scratchpad_activation_state->resize({batch_size, memory_size * num_filters}); + + // TODO: enable it only if kernel with dynamic shapes + // Resize output tensor + output()->resize({batch_size, num_units}); + + luci_interpreter_pal::SetupScratchpadTensor( + input()->element_type(), weight_feature()->element_type(), getOutputTensors()[2], + getOutputTensors()[3], getOutputTensors()[4], getOutputTensors()[5], getOutputTensors()[6], + getOutputTensors()[7], input_shape, weight_time_shape, batch_size, num_filters, num_units); +} + +void SVDF::execute() const +{ + switch (weight_feature()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::S8: + { + if (input()->element_type() == DataType::S8) + evalInteger(); + else + // TODO:: support hybrid SVDF op + assert(false && "Hybrid type is not currently supported"); + break; + } + default: + assert(false && "Unsupported type"); + } +} + +void SVDF::evalInteger() const +{ + const auto effective_scale_1 = static_cast(input()->scale() * weight_feature()->scale() / + input_activation_state()->scale()); + const auto effective_scale_2 = static_cast(input_activation_state()->scale() * + weight_time()->scale() / output()->scale()); + + int32_t effective_scale_1_a; + int effective_scale_1_b; + int32_t effective_scale_2_a; + int effective_scale_2_b; + + tflite::QuantizeMultiplier(effective_scale_1, &effective_scale_1_a, &effective_scale_1_b); + tflite::QuantizeMultiplier(effective_scale_2, &effective_scale_2_a, &effective_scale_2_b); + + TfLiteSVDFParams params_svdf{}; + params_svdf.asymmetric_quantize_inputs = params().asymmetric_quantize_inputs; + params_svdf.rank = params().svdf_rank; + params_svdf.activation = get_tflite_activation(params().activation); + + auto scratchpad_activation_state = getOutputTensors()[1]; + // Note: it is expected that activation_state input variable tensor reset to zero, + // also expected that this variable tensor doesn't have buffer + auto scratchpad_data = getTensorData(scratchpad_activation_state); + std::fill_n(scratchpad_data, scratchpad_activation_state->shape().num_elements(), 0); + + auto scratchpad = getOutputTensors()[2]; + auto output_temp = getOutputTensors()[3]; + + int32_t input_zp = input()->zero_point(); + int32_t output_zp = output()->zero_point(); + luci_interpreter_pal::IntegerSVDF( + params_svdf, getTensorShape(input()), getTensorData(input()), + getTensorShape(weight_feature()), getTensorData(weight_feature()), + getTensorShape(weight_time()), getTensorData(weight_time()), getTensorShape(bias()), + getTensorData(bias()), scratchpad_data, getTensorShape(output()), + getTensorData(output()), getTensorData(scratchpad), + getTensorData(output_temp), effective_scale_1_a, effective_scale_1_b, + effective_scale_2_a, effective_scale_2_b, input_zp, output_zp); +} + +void SVDF::evalFloat() const +{ + TfLiteSVDFParams params_svdf{}; + params_svdf.asymmetric_quantize_inputs = params().asymmetric_quantize_inputs; + params_svdf.rank = params().svdf_rank; + params_svdf.activation = get_tflite_activation(params().activation); + + auto scratchpad_activation_state = getOutputTensors()[1]; + // Note: it is expected that activation_state input variable tensor reset to zero, + // also expected that this variable tensor doesn't have buffer + auto scratchpad_data = getTensorData(scratchpad_activation_state); + std::fill_n(scratchpad_data, scratchpad_activation_state->shape().num_elements(), 0); + + auto scratchpad_1 = getOutputTensors()[2]; + + luci_interpreter_pal::FloatSVDF( + params_svdf, getTensorShape(input()), getTensorData(input()), + getTensorShape(weight_feature()), getTensorData(weight_feature()), + getTensorShape(weight_time()), getTensorData(weight_time()), getTensorShape(bias()), + getTensorData(bias()), getTensorData(scratchpad_1), scratchpad_data, + getTensorShape(output()), getTensorData(output())); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SVDF.h b/onert-micro/luci-interpreter/src/kernels/SVDF.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/SVDF.h rename to onert-micro/luci-interpreter/src/kernels/SVDF.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SVDF.test.cpp b/onert-micro/luci-interpreter/src/kernels/SVDF.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/SVDF.test.cpp rename to onert-micro/luci-interpreter/src/kernels/SVDF.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Shape.cpp b/onert-micro/luci-interpreter/src/kernels/Shape.cpp new file mode 100644 index 0000000..87c12d2 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Shape.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Shape.h" +#include "kernels/Utils.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +ShapeKernel::ShapeKernel(const Tensor *input, Tensor *output, const ShapeParams ¶ms) + : KernelWithParams({input}, {output}, params) +{ +} + +void ShapeKernel::configure() +{ + LUCI_INTERPRETER_CHECK(output()->element_type() == DataType::S32 or + output()->element_type() == DataType::S64); + const auto input_shape = input()->shape(); + + Shape output_shape(1); + output_shape.dim(0) = input_shape.num_dims(); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void ShapeKernel::execute() const +{ + switch (params().out_type) + { + case DataType::S32: + evalInt(); + break; + case DataType::S64: + evalInt(); + break; + default: + assert(false && "Unsupported type."); + } +} + +template void ShapeKernel::evalInt() const +{ + const auto input_shape = input()->shape(); + + auto output_data = getTensorData(output()); + + for (int i = 0; i < input_shape.num_dims(); ++i) + { + output_data[i] = input_shape.dim(i); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Shape.h b/onert-micro/luci-interpreter/src/kernels/Shape.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Shape.h rename to onert-micro/luci-interpreter/src/kernels/Shape.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Shape.test.cpp b/onert-micro/luci-interpreter/src/kernels/Shape.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Shape.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Shape.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Slice.cpp b/onert-micro/luci-interpreter/src/kernels/Slice.cpp new file mode 100644 index 0000000..cb41e26 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Slice.cpp @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Slice.h" +#include "Utils.h" +#include "PALSlice.h" + +#include +#include + +namespace luci_interpreter +{ + +namespace kernels +{ +const int max_dim = 4; + +Slice::Slice(const Tensor *input, const Tensor *begin, const Tensor *size, Tensor *output) + : Kernel({input, begin, size}, {output}) +{ +} + +template +Shape calculateOutputShape(const Tensor *input, const Tensor *begin, const Tensor *size) +{ + Shape output_shape = Shape(input->shape().num_dims()); + for (int idx = 0; idx < input->shape().num_dims(); idx++) + { + T size_value = getTensorData(size)[idx]; + if (size_value < 0) + { + if (size_value != -1) + { + assert(false && "Invalid size."); + } + size_value = input->shape().dim(idx) - getTensorData(begin)[idx]; + } + else + { + if (input->shape().dim(idx) < getTensorData(begin)[idx] + size_value) + { + assert(false && "Invalid begin and size."); + } + } + output_shape.dim(idx) = static_cast(size_value); + } + return output_shape; +} + +template +void getBeginAndSizeVectors(int dimensions, const Tensor *begin, const Tensor *size, + std::vector *begins, std::vector *sizes) +{ + for (int idx = dimensions - 1; idx >= 0; --idx) + { + begins->push_back(getTensorData(begin)[idx]); + sizes->push_back(getTensorData(size)[idx]); + } +} + +void Slice::configure() +{ + assert(input()->element_type() == output()->element_type()); + assert(begin()->element_type() == DataType::S32 || begin()->element_type() == DataType::S64); + assert(size()->element_type() == DataType::S32 || size()->element_type() == DataType::S64); + assert(begin()->shape().num_dims() == 1); + assert(size()->shape().num_dims() == 1); + assert(input()->shape().num_dims() <= max_dim); + // TODO: enable it only if kernel with dynamic shapes + if (begin()->element_type() == DataType::S32) + { + output()->resize(calculateOutputShape(input(), begin(), size())); + } + else if (begin()->element_type() == DataType::S64) + { + output()->resize(calculateOutputShape(input(), begin(), size())); + } + else + { + assert(false && "Unsupported type."); + } +} + +void Slice::execute() const +{ + std::vector begins; + begins.reserve(max_dim); + std::vector sizes; + sizes.reserve(max_dim); + if (begin()->element_type() == DataType::S32) + { + getBeginAndSizeVectors(input()->shape().num_dims(), begin(), size(), &begins, &sizes); + } + else if (begin()->element_type() == DataType::S64) + { + getBeginAndSizeVectors(input()->shape().num_dims(), begin(), size(), &begins, &sizes); + } + else + { + assert(false && "Unsupported begin type."); + } + for (int i = input()->shape().num_dims(); i < max_dim; ++i) + { + begins.push_back(0); + sizes.push_back(1); + } + + assert(begins.size() == 4); + assert(sizes.size() == 4); + tflite::SliceParams op_params{}; + op_params.begin_count = 4; + op_params.size_count = 4; + for (int i = 0; i < 4; i++) + { + op_params.begin[i] = begins[3 - i]; + op_params.size[i] = sizes[3 - i]; + } + switch (input()->element_type()) + { + case DataType::FLOAT32: + luci_interpreter_pal::Slice(op_params, getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); + break; + case DataType::U8: + luci_interpreter_pal::Slice(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + case DataType::S8: + luci_interpreter_pal::Slice(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + default: + assert(false && "Unsupported input type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Slice.h b/onert-micro/luci-interpreter/src/kernels/Slice.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Slice.h rename to onert-micro/luci-interpreter/src/kernels/Slice.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Slice.test.cpp b/onert-micro/luci-interpreter/src/kernels/Slice.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Slice.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Slice.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Softmax.cpp b/onert-micro/luci-interpreter/src/kernels/Softmax.cpp new file mode 100644 index 0000000..651040b --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Softmax.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" +#include "kernels/Utils.h" + +#include +#include "PALSoftmax.h" + +namespace luci_interpreter +{ + +namespace +{ + +#ifndef DIS_FLOAT +void evalFloat(const circle::Tensor *input, const circle::Tensor *output, + const circle::SoftmaxOptions *options, BaseRuntimeGraph *runtime_graph) +{ + const auto *input_data = runtime_graph->getDataByTensor(input); + auto *output_data = runtime_graph->getDataByTensor(output); + + tflite::SoftmaxParams op_params{}; + op_params.beta = options->beta(); + + tflite::reference_ops::Softmax( + op_params, kernels::getTensorShape(input), kernels::getTensorData(input_data), + kernels::getTensorShape(output), kernels::getTensorData(output_data)); +} +#endif // DIS_FLOAT + +#ifndef DIS_QUANT +template +void evalQuantized(const circle::Tensor *input, const circle::Tensor *output, + const circle::SoftmaxOptions *options, BaseRuntimeGraph *runtime_graph) +{ + // TODO: Enable it + assert(false && "Not impl yet"); + + const auto *input_data = runtime_graph->getDataByTensor(input); + auto *output_data = runtime_graph->getDataByTensor(output); + + tflite::SoftmaxParams op_params{}; + + luci_interpreter_pal::InitializeParams(&op_params, Tensor::scale(input), options->beta()); + luci_interpreter_pal::Softmax( + op_params, kernels::getTensorShape(input), kernels::getTensorData(input_data), + kernels::getTensorShape(output), kernels::getTensorData(output_data)); +} +#endif + +} // namespace + +void configure_kernel_CircleSoftmax(const circle::Operator *cur_op, BaseRuntimeGraph *runtime_graph) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + auto output = runtime_graph->getCircleTensorByIndex(output_index); + + assert(input != nullptr); + assert(output != nullptr); + + LUCI_INTERPRETER_CHECK(Tensor::element_type(input) == Tensor::element_type(output)); + LUCI_INTERPRETER_CHECK(Tensor::num_dims(input) >= 1); + +#ifndef DIS_QUANT + if (Tensor::element_type(input) == DataType::U8 || Tensor::element_type(input) == DataType::S8) + { + LUCI_INTERPRETER_CHECK(Tensor::element_type(input) == DataType::S8 || + Tensor::zero_point(output) == 0); + LUCI_INTERPRETER_CHECK(Tensor::element_type(input) == DataType::U8 || + Tensor::zero_point(output) == std::numeric_limits::min()); + } +#endif +} + +void execute_kernel_CircleSoftmax(const circle::Operator *cur_op, BaseRuntimeGraph *runtime_graph, + bool) +{ + const auto input_index = cur_op->inputs()->operator[](0); + const auto output_index = cur_op->outputs()->operator[](0); + + assert(input_index != -1); + assert(output_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + auto output = runtime_graph->getCircleTensorByIndex(output_index); + + assert(input != nullptr); + assert(output != nullptr); + + const auto *options = cur_op->builtin_options_as_SoftmaxOptions(); + + switch (Tensor::element_type(input)) + { +#ifndef DIS_FLOAT + case DataType::FLOAT32: + evalFloat(input, output, options, runtime_graph); + break; +#endif // DIS_FLOAT +#ifndef DIS_QUANT + case DataType::S8: + evalQuantized(input, output, options, runtime_graph); + break; + case DataType::U8: + evalQuantized(input, output, options, runtime_graph); + break; +#endif // DIS_QUANT + default: + assert(false && "Unsupported type."); + } +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/Softmax.test.cpp b/onert-micro/luci-interpreter/src/kernels/Softmax.test.cpp new file mode 100644 index 0000000..f026e89 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Softmax.test.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// TODO enable it +#if 0 +#include "kernels/Softmax.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +template constexpr loco::DataType toLocoDataType(); + +template <> constexpr loco::DataType toLocoDataType() { return loco::DataType::FLOAT32; } + +template <> constexpr loco::DataType toLocoDataType() { return loco::DataType::U8; } + +template <> constexpr loco::DataType toLocoDataType() { return loco::DataType::S8; } + +template ::value, bool> = true> +void Check(std::initializer_list input_shape, std::initializer_list output_shape, + std::initializer_list input_data, std::initializer_list output_data) +{ + std::unique_ptr memory_manager = std::make_unique(); + + Tensor input_tensor = + makeInputTensor()>(input_shape, input_data, memory_manager.get()); + Tensor output_tensor = makeOutputTensor(toLocoDataType()); + + SoftmaxParams params{}; + params.beta = 0.1; + + Softmax kernel(&input_tensor, &output_tensor, params); + kernel.configure(); + memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(output_data)); + EXPECT_THAT(extractTensorShape(output_tensor), output_shape); +} + +template ::value, bool> = true> +void Check(std::initializer_list input_shape, std::initializer_list output_shape, + std::initializer_list input_data, std::initializer_list output_data) +{ + std::unique_ptr memory_manager = std::make_unique(); + + std::pair input_quant_param = + quantizationParams(std::min(std::min(input_data), 0.f), + std::max(std::max(input_data), 0.f)); + std::pair output_quant_param = + quantizationParams(std::min(std::min(output_data), 0.f), + std::max(std::max(output_data), 0.f)); + Tensor input_tensor = makeInputTensor()>(input_shape, input_quant_param.first, + input_quant_param.second, input_data, + memory_manager.get()); + Tensor output_tensor = + makeOutputTensor(toLocoDataType(), output_quant_param.first, output_quant_param.second); + + SoftmaxParams params{}; + params.beta = 0.1; + + Softmax kernel(&input_tensor, &output_tensor, params); + kernel.configure(); + memory_manager->allocate_memory(output_tensor); + kernel.execute(); + + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(output_shape)); + EXPECT_THAT(dequantizeTensorData(output_tensor), + FloatArrayNear(output_data, output_tensor.scale())); +} + +template class SoftmaxTest : public ::testing::Test +{ +}; + +using DataTypes = ::testing::Types; +TYPED_TEST_SUITE(SoftmaxTest, DataTypes); + +TYPED_TEST(SoftmaxTest, Simple) +{ + Check({2, 1, 2, 3}, {2, 1, 2, 3}, + { + 5, -9, 8, // + -7, 2, -4, // + 1, -2, 9, // + 3, -6, -1, // + }, + { + 0.38514, 0.09497, 0.51989, // + 0.20792, 0.51141, 0.28067, // + 0.25212, 0.18678, 0.56110, // + 0.48149, 0.19576, 0.32275, // + }); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter +#endif diff --git a/onert-micro/luci-interpreter/src/kernels/SpaceToBatchND.cpp b/onert-micro/luci-interpreter/src/kernels/SpaceToBatchND.cpp new file mode 100644 index 0000000..0a5b447 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/SpaceToBatchND.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/SpaceToBatchND.h" +#include "kernels/Utils.h" + +#include "PALSpaceToBatchND.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +const int kInputMinDimensionNum = 3; +const int kInputMaxDimensionNum = 4; + +} // namespace + +SpaceToBatchND::SpaceToBatchND(const Tensor *input, const Tensor *block_shape, + const Tensor *paddings, Tensor *output) + : Kernel({input, block_shape, paddings}, {output}) +{ +} + +void SpaceToBatchND::configure() +{ + const auto *block_shape_data = block_shape()->data(); + const auto *paddings_data = paddings()->data(); + LUCI_INTERPRETER_CHECK(input()->shape().num_dims() >= kInputMinDimensionNum); + LUCI_INTERPRETER_CHECK(input()->shape().num_dims() <= kInputMaxDimensionNum); + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + + int spatial_dims_num = input()->shape().num_dims() - 2; + + LUCI_INTERPRETER_CHECK(block_shape()->shape().num_dims() == 1); + LUCI_INTERPRETER_CHECK(block_shape()->shape().dim(0) == spatial_dims_num); + + LUCI_INTERPRETER_CHECK(paddings()->shape().num_dims() == 2); + LUCI_INTERPRETER_CHECK(paddings()->shape().dim(0) == spatial_dims_num); + LUCI_INTERPRETER_CHECK(paddings()->shape().dim(1) == 2); + + Shape output_shape = Shape(input()->shape().num_dims()); + int output_batch_size = input()->shape().dim(0); + for (int i = 0; i < spatial_dims_num; ++i) + { + int final_dim_size = + (input()->shape().dim(i + 1) + paddings_data[i * 2] + paddings_data[i * 2 + 1]); + LUCI_INTERPRETER_CHECK(final_dim_size % block_shape_data[i] == 0); + output_shape.dim(i + 1) = final_dim_size / block_shape_data[i]; + output_batch_size = output_batch_size * block_shape_data[i]; + } + output_shape.dim(0) = output_batch_size; + output_shape.dim(input()->shape().num_dims() - 1) = + input()->shape().dim(input()->shape().num_dims() - 1); + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void SpaceToBatchND::execute() const +{ + switch (input()->element_type()) + { + tflite::SpaceToBatchParams op_params; + case DataType::FLOAT32: + op_params.output_offset = 0; + luci_interpreter_pal::SpaceToBatchND( + op_params, getTensorShape(input()), getTensorData(input()), + getTensorShape(block_shape()), getTensorData(block_shape()), + getTensorShape(paddings()), getTensorData(paddings()), getTensorShape(output()), + getTensorData(output())); + break; + case DataType::U8: + op_params.output_offset = output()->zero_point(); + luci_interpreter_pal::SpaceToBatchND( + op_params, getTensorShape(input()), getTensorData(input()), + getTensorShape(block_shape()), getTensorData(block_shape()), + getTensorShape(paddings()), getTensorData(paddings()), getTensorShape(output()), + getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SpaceToBatchND.h b/onert-micro/luci-interpreter/src/kernels/SpaceToBatchND.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/SpaceToBatchND.h rename to onert-micro/luci-interpreter/src/kernels/SpaceToBatchND.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SpaceToBatchND.test.cpp b/onert-micro/luci-interpreter/src/kernels/SpaceToBatchND.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/SpaceToBatchND.test.cpp rename to onert-micro/luci-interpreter/src/kernels/SpaceToBatchND.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/SpaceToDepth.cpp b/onert-micro/luci-interpreter/src/kernels/SpaceToDepth.cpp new file mode 100644 index 0000000..06cc5fa --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/SpaceToDepth.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SpaceToDepth.h" +#include "Utils.h" +#include "PALSpaceToDepth.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +SpaceToDepth::SpaceToDepth(const Tensor *input, Tensor *output, const SpaceToDepthParams ¶ms) + : KernelWithParams({input}, {output}, params) +{ +} + +void SpaceToDepth::configure() +{ + assert(input()->shape().num_dims() == 4); + assert(output()->element_type() == DataType::FLOAT32 || + output()->element_type() == DataType::U8 || output()->element_type() == DataType::S8 || + output()->element_type() == DataType::S32 || output()->element_type() == DataType::S64); + assert(input()->element_type() == output()->element_type()); + + const int block_size = params().block_size; + const int32_t input_height = input()->shape().dim(1); + const int32_t input_width = input()->shape().dim(2); + int32_t output_height = input_height / block_size; + int32_t output_width = input_width / block_size; + + assert(input_height == output_height * block_size); + assert(input_width == output_width * block_size); + + Shape output_shape(4); + output_shape.dim(0) = input()->shape().dim(0); + output_shape.dim(1) = output_height; + output_shape.dim(2) = output_width; + output_shape.dim(3) = input()->shape().dim(3) * block_size * block_size; + // TODO: enable it only if kernel with dynamic shapes + output()->resize(output_shape); +} + +void SpaceToDepth::execute() const +{ + tflite::SpaceToDepthParams op_params{}; + op_params.block_size = params().block_size; + switch (input()->element_type()) + { + case DataType::FLOAT32: + luci_interpreter_pal::SpaceToDepth(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + case DataType::U8: + luci_interpreter_pal::SpaceToDepth(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SpaceToDepth.h b/onert-micro/luci-interpreter/src/kernels/SpaceToDepth.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/SpaceToDepth.h rename to onert-micro/luci-interpreter/src/kernels/SpaceToDepth.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SpaceToDepth.test.cpp b/onert-micro/luci-interpreter/src/kernels/SpaceToDepth.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/SpaceToDepth.test.cpp rename to onert-micro/luci-interpreter/src/kernels/SpaceToDepth.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Split.cpp b/onert-micro/luci-interpreter/src/kernels/Split.cpp new file mode 100644 index 0000000..87ec028 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Split.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Split.h" + +#include "Utils.h" + +#include "PALSplit.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +Split::Split(const Tensor *axis, const Tensor *input, std::vector outputs) + : Kernel({axis, input}, std::move(outputs)) +{ +} + +void Split::configure() +{ + assert(axis()->shape().num_elements() == 1); + _axis_value = getTensorData(axis())[0]; + if (_axis_value < 0) + _axis_value += input()->shape().num_dims(); + assert(_axis_value >= 0 && _axis_value < input()->shape().num_dims()); + + const int32_t input_size = input()->shape().dim(_axis_value); + assert(input_size % _outputs.size() == 0); + const int32_t slice_size = input_size / _outputs.size(); + // TODO: enable it only if kernel with dynamic shapes + Shape output_shape = input()->shape(); + output_shape.dim(_axis_value) = slice_size; + for (Tensor *output : _outputs) + { + output->resize(output_shape); + } +} + +void Split::execute() const +{ + tflite::SplitParams params{}; + params.num_split = _outputs.size(); + params.axis = _axis_value; + +#define TF_LITE_SPLIT(scalar) \ + { \ + VectorOfTensors all_outputs(_outputs); \ + luci_interpreter_pal::Split(params, getTensorShape(input()), getTensorData(input()), \ + all_outputs.shapes(), all_outputs.data()); \ + } + + switch (input()->element_type()) + { + case DataType::FLOAT32: + TF_LITE_SPLIT(float); + break; + case DataType::U8: + TF_LITE_SPLIT(uint8_t); + break; + default: + assert(false && "Unsupported type."); + } +#undef TF_LITE_SPLIT +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Split.h b/onert-micro/luci-interpreter/src/kernels/Split.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Split.h rename to onert-micro/luci-interpreter/src/kernels/Split.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Split.test.cpp b/onert-micro/luci-interpreter/src/kernels/Split.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Split.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Split.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/SplitV.cpp b/onert-micro/luci-interpreter/src/kernels/SplitV.cpp new file mode 100644 index 0000000..0e3a541 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/SplitV.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SplitV.h" + +#include "Utils.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +SplitV::SplitV(const Tensor *input, const Tensor *size_splits, const Tensor *axis, + std::vector outputs) + : Kernel({input, size_splits, axis}, std::move(outputs)) +{ +} + +void SplitV::configure() +{ + assert(axis()->shape().num_elements() == 1); + _axis_value = getTensorData(axis())[0]; + if (_axis_value < 0) + _axis_value += input()->shape().num_dims(); + assert(_axis_value >= 0 && _axis_value < input()->shape().num_dims()); + + auto num_split = static_cast(_outputs.size()); + auto sizes_data = getTensorData(size_splits()); + + assert(size_splits()->shape().num_dims() == 1); + + int32_t sum = 0; + const auto num_dims_size_spits = size_splits()->shape().dim(0); + int32_t count_neg_dim = 0; + + for (int32_t i = 0; i < num_dims_size_spits - 1; ++i) + { + if (sizes_data[i] != -1) + { + sum += sizes_data[i]; + } + else + { + count_neg_dim++; + } + } + assert(count_neg_dim < 2); + assert(size_splits()->shape().num_elements() == num_split); + + // TODO: enable it only if kernel with dynamic shapes + auto output_shape = input()->shape(); + for (int32_t i = 0; i < num_split; ++i) + { + if (sizes_data[i] == -1) + { + output_shape.dim(_axis_value) = input()->shape().dim(_axis_value) - sum; + } + else + { + output_shape.dim(_axis_value) = sizes_data[i]; + } + _outputs[i]->resize(output_shape); + } +} + +void SplitV::execute() const +{ + tflite::SplitParams params{}; + params.num_split = _outputs.size(); + params.axis = _axis_value; + +#define TF_LITE_SPLIT(scalar) \ + { \ + VectorOfTensors all_outputs(_outputs); \ + tflite::optimized_ops::Split(params, getTensorShape(input()), getTensorData(input()), \ + all_outputs.shapes(), all_outputs.data()); \ + } + + switch (input()->element_type()) + { + case DataType::FLOAT32: + TF_LITE_SPLIT(float); + break; + case DataType::U8: + TF_LITE_SPLIT(uint8_t); + break; + case DataType::S16: + TF_LITE_SPLIT(int16_t); + break; + default: + assert(false && "Unsupported type."); + } +#undef TF_LITE_SPLIT +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SplitV.h b/onert-micro/luci-interpreter/src/kernels/SplitV.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/SplitV.h rename to onert-micro/luci-interpreter/src/kernels/SplitV.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SplitV.test.cpp b/onert-micro/luci-interpreter/src/kernels/SplitV.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/SplitV.test.cpp rename to onert-micro/luci-interpreter/src/kernels/SplitV.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Sqrt.cpp b/onert-micro/luci-interpreter/src/kernels/Sqrt.cpp new file mode 100644 index 0000000..eed50df --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Sqrt.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Sqrt.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +Sqrt::Sqrt(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Sqrt::configure() +{ + if (input()->element_type() != output()->element_type()) + { + assert(false && "Input/output tensor data type mismatch."); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Sqrt::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + + default: + assert(false && "Unsupported type."); + } +} + +void Sqrt::evalFloat() const +{ + auto in = getTensorData(input()); + auto out = getTensorData(output()); + auto size = getTensorShape(input()).FlatSize(); + for (auto i = in; i != in + size; ++i) + { + *out = std::sqrt(*i); + ++out; + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Sqrt.h b/onert-micro/luci-interpreter/src/kernels/Sqrt.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Sqrt.h rename to onert-micro/luci-interpreter/src/kernels/Sqrt.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Sqrt.test.cpp b/onert-micro/luci-interpreter/src/kernels/Sqrt.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Sqrt.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Sqrt.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Square.cpp b/onert-micro/luci-interpreter/src/kernels/Square.cpp new file mode 100644 index 0000000..6386b91 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Square.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Square.h" +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +Square::Square(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Square::configure() +{ + if (input()->element_type() != output()->element_type()) + { + assert(false && "Input/output tensor data type mismatch."); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Square::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + + default: + assert(false && "Unsupported type."); + } +} + +void Square::evalFloat() const +{ + auto in = getTensorData(input()); + auto out = getTensorData(output()); + auto size = getTensorShape(input()).FlatSize(); + for (auto i = in; i != in + size; ++i) + { + *out = (*i) * (*i); + ++out; + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Square.h b/onert-micro/luci-interpreter/src/kernels/Square.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Square.h rename to onert-micro/luci-interpreter/src/kernels/Square.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Square.test.cpp b/onert-micro/luci-interpreter/src/kernels/Square.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Square.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Square.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/SquaredDifference.cpp b/onert-micro/luci-interpreter/src/kernels/SquaredDifference.cpp new file mode 100644 index 0000000..27f395a --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/SquaredDifference.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2018 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/SquaredDifference.h" + +#include "kernels/Utils.h" + +#include "kernels/BinaryOpCommon.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +SquaredDifference::SquaredDifference(const Tensor *input1, const Tensor *input2, Tensor *output) + : Kernel({input1, input2}, {output}) +{ +} + +void SquaredDifference::configure() +{ + LUCI_INTERPRETER_CHECK(input1()->element_type() == input2()->element_type()) + LUCI_INTERPRETER_CHECK(input1()->element_type() == output()->element_type()) + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); +} + +void SquaredDifference::execute() const +{ + switch (input1()->element_type()) + { + case DataType::FLOAT32: + evalSquaredDifference(); + break; + default: + assert(false && "Unsupported type."); + } +} + +template inline void SquaredDifference::evalSquaredDifference() const +{ + BinaryOpBroadcastSlow(getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output()), [](T x, T y) { + const T difference = x - y; + return difference * difference; + }); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SquaredDifference.h b/onert-micro/luci-interpreter/src/kernels/SquaredDifference.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/SquaredDifference.h rename to onert-micro/luci-interpreter/src/kernels/SquaredDifference.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/SquaredDifference.test.cpp b/onert-micro/luci-interpreter/src/kernels/SquaredDifference.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/SquaredDifference.test.cpp rename to onert-micro/luci-interpreter/src/kernels/SquaredDifference.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Squeeze.cpp b/onert-micro/luci-interpreter/src/kernels/Squeeze.cpp new file mode 100644 index 0000000..9736dce --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Squeeze.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2018 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Squeeze.h" + +#include "kernels/Utils.h" + +namespace luci_interpreter +{ +namespace kernels +{ + +Squeeze::Squeeze(const Tensor *input, Tensor *output, const SqueezeParams ¶ms) + : KernelWithParams({input}, {output}, params) +{ +} + +void Squeeze::configure() +{ + int input_num_dims = input()->shape().num_dims(); + int num_squeeze_dims = params().squeeze_dims.size(); + assert(input_num_dims <= 8); + bool should_squeeze[8] = {false}; + int num_squeezed_dims = 0; + if (num_squeeze_dims == 0) + { + for (int idx = 0; idx < input_num_dims; ++idx) + { + if (input()->shape().dim(idx) == 1) + { + should_squeeze[idx] = true; + ++num_squeezed_dims; + } + } + } + else + { + for (int idx = 0; idx < num_squeeze_dims; ++idx) + { + int current = params().squeeze_dims[idx] < 0 ? params().squeeze_dims[idx] + input_num_dims + : params().squeeze_dims[idx]; + assert(current >= 0 && current < input_num_dims && input()->shape().dim(current) == 1); + if (!should_squeeze[current]) + ++num_squeezed_dims; + should_squeeze[current] = true; + } + } + // TODO: enable it only if kernel with dynamic shapes + Shape output_shape(input_num_dims - num_squeezed_dims); + for (int in_idx = 0, out_idx = 0; in_idx < input_num_dims; ++in_idx) + { + if (!should_squeeze[in_idx]) + { + output_shape.dim(out_idx++) = input()->shape().dim(in_idx); + } + } + output()->resize(output_shape); +} + +void Squeeze::execute() const +{ + assert(input()->shape().num_elements() == output()->shape().num_elements()); + + const auto *input_data = input()->data(); + auto *output_data = output()->data(); + std::memcpy(output_data, input_data, + getDataTypeSize(input()->element_type()) * input()->shape().num_elements()); +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Squeeze.h b/onert-micro/luci-interpreter/src/kernels/Squeeze.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Squeeze.h rename to onert-micro/luci-interpreter/src/kernels/Squeeze.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Squeeze.test.cpp b/onert-micro/luci-interpreter/src/kernels/Squeeze.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Squeeze.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Squeeze.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/StridedSlice.cpp b/onert-micro/luci-interpreter/src/kernels/StridedSlice.cpp new file mode 100644 index 0000000..654fd3c --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/StridedSlice.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/StridedSlice.h" + +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +StridedSlice::StridedSlice(const Tensor *input, const Tensor *begin, const Tensor *end, + const Tensor *strides, Tensor *output, const StridedSliceParams ¶ms) + : KernelWithParams({input, begin, end, strides}, {output}, params) +{ +} + +void StridedSlice::configure() +{ + assert(begin()->shape().num_dims() == 1); + assert(end()->shape().num_dims() == 1); + assert(strides()->shape().num_dims() == 1); + assert(input()->element_type() == output()->element_type()); + assert(begin()->element_type() == DataType::S32); + assert(end()->element_type() == DataType::S32); + assert(strides()->element_type() == DataType::S32); + assert(input()->shape().num_dims() <= 4); + if (params().ellipsis_mask != 0) + { + assert(false && "ellipsis_mask is not implemented yet."); + } + if (params().new_axis_mask != 0) + { + assert(false && "new_axis_mask is not implemented yet."); + } + if (input()->element_type() == DataType::U8) + { + assert(input()->scale() == output()->scale()); + assert(input()->zero_point() == output()->zero_point()); + } + tflite::StridedSliceParams op_params{}; + op_params.start_indices_count = input()->shape().num_dims(); + op_params.stop_indices_count = input()->shape().num_dims(); + op_params.strides_count = input()->shape().num_dims(); + + for (int i = 0; i < input()->shape().num_dims(); i++) + { + op_params.start_indices[i] = getTensorData(begin())[i]; + op_params.stop_indices[i] = getTensorData(end())[i]; + op_params.strides[i] = getTensorData(strides())[i]; + } + op_params.begin_mask = params().begin_mask; + op_params.ellipsis_mask = 0; + op_params.end_mask = params().end_mask; + op_params.new_axis_mask = 0; + op_params.shrink_axis_mask = params().shrink_axis_mask; + std::vector output_shape_vector; + for (int i = 0; i < input()->shape().num_dims(); i++) + { + int idx = input()->shape().num_dims() - i - 1; + int32_t stride = getTensorData(strides())[idx]; + assert(stride != 0); + int32_t begin = ::tflite::strided_slice::StartForAxis(op_params, getTensorShape(input()), idx); + int32_t end = + ::tflite::strided_slice::StopForAxis(op_params, getTensorShape(input()), idx, begin); + + const bool shrink_axis = params().shrink_axis_mask & (1 << idx); + if (shrink_axis) + { + end = begin + 1; + } + + int32_t dim_shape = std::ceil((end - begin) / static_cast(stride)); + dim_shape = dim_shape < 0 ? 0 : dim_shape; + if (!shrink_axis) + { + output_shape_vector.push_back(dim_shape); + } + } + // TODO: enable it only if kernel with dynamic shapes + Shape output_shape = Shape(output_shape_vector.size()); + for (size_t i = 0; i < output_shape_vector.size(); i++) + { + output_shape.dim(i) = output_shape_vector[output_shape_vector.size() - i - 1]; + } + output()->resize(output_shape); +} + +void StridedSlice::execute() const +{ + tflite::StridedSliceParams op_params{}; + op_params.start_indices_count = input()->shape().num_dims(); + op_params.stop_indices_count = input()->shape().num_dims(); + op_params.strides_count = input()->shape().num_dims(); + + for (int i = 0; i < input()->shape().num_dims(); i++) + { + op_params.start_indices[i] = getTensorData(begin())[i]; + op_params.stop_indices[i] = getTensorData(end())[i]; + op_params.strides[i] = getTensorData(strides())[i]; + } + op_params.begin_mask = params().begin_mask; + op_params.ellipsis_mask = 0; + op_params.end_mask = params().end_mask; + op_params.new_axis_mask = 0; + op_params.shrink_axis_mask = params().shrink_axis_mask; + + switch (input()->element_type()) + { + case DataType::FLOAT32: + tflite::reference_ops::StridedSlice(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + case DataType::U8: + tflite::reference_ops::StridedSlice(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + case DataType::S32: + tflite::reference_ops::StridedSlice(op_params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/StridedSlice.h b/onert-micro/luci-interpreter/src/kernels/StridedSlice.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/StridedSlice.h rename to onert-micro/luci-interpreter/src/kernels/StridedSlice.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/StridedSlice.test.cpp b/onert-micro/luci-interpreter/src/kernels/StridedSlice.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/StridedSlice.test.cpp rename to onert-micro/luci-interpreter/src/kernels/StridedSlice.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Sub.cpp b/onert-micro/luci-interpreter/src/kernels/Sub.cpp new file mode 100644 index 0000000..7b02c1e --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Sub.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Sub.h" +#include "kernels/Utils.h" + +#include "PALSub.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +Sub::Sub(const Tensor *input1, const Tensor *input2, Tensor *output, const SubParams ¶ms) + : KernelWithParams({input1, input2}, {output}, params) +{ +} + +void Sub::configure() +{ + LUCI_INTERPRETER_CHECK(!(input1()->element_type() != input2()->element_type())) + LUCI_INTERPRETER_CHECK(!(input1()->element_type() != output()->element_type())) + // TODO: enable it only if kernel with dynamic shapes + output()->resize(calculateShapeForBroadcast(input1()->shape(), input2()->shape())); +} + +void Sub::execute() const +{ + switch (input1()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::S64: + evalInteger(); + break; + case DataType::S32: + evalInteger(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Sub::evalFloat() const +{ + tflite::ArithmeticParams params{}; + fillArithmeticActivationRange(params, _params.activation); + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + tflite::reference_ops::BroadcastSubSlow( + params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), + getTensorData(input2()), getTensorShape(output()), getTensorData(output())); + } + else + { + luci_interpreter_pal::Sub(params, getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +template void Sub::evalInteger() const +{ + tflite::ArithmeticParams params{}; + fillArithmeticActivationRange(params, _params.activation); + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + tflite::reference_ops::BroadcastSubSlow( + params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), + getTensorData(input2()), getTensorShape(output()), getTensorData(output())); + } + else + { + tflite::reference_ops::Sub(params, getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +void Sub::evalQuantized() const +{ + const auto input1_scale = static_cast(input1()->scale()); + const auto input2_scale = static_cast(input2()->scale()); + const auto output_scale = static_cast(output()->scale()); + + const int left_shift = 20; + const double twice_max_input_scale = 2 * std::max(input1_scale, input2_scale); + const double real_input1_multiplier = input1_scale / twice_max_input_scale; + const double real_input2_multiplier = input2_scale / twice_max_input_scale; + const double real_output_multiplier = twice_max_input_scale / ((1 << left_shift) * output_scale); + + int32_t input1_multiplier{}, input2_multiplier{}, output_multiplier{}; + int input1_shift{}, input2_shift{}, output_shift{}; + quantizeMultiplierSmallerThanOneExp(real_input1_multiplier, &input1_multiplier, &input1_shift); + quantizeMultiplierSmallerThanOneExp(real_input2_multiplier, &input2_multiplier, &input2_shift); + quantizeMultiplierSmallerThanOneExp(real_output_multiplier, &output_multiplier, &output_shift); + + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(_params.activation, output(), &activation_min, &activation_max); + + tflite::ArithmeticParams params{}; + params.left_shift = left_shift; + // The kernel expects inputs' zero points to be negated. + params.input1_offset = -input1()->zero_point(); // Note the '-'. + params.input1_multiplier = input1_multiplier; + params.input1_shift = input1_shift; + params.input2_offset = -input2()->zero_point(); // Note the '-'. + params.input2_multiplier = input2_multiplier; + params.input2_shift = input2_shift; + params.output_offset = output()->zero_point(); + params.output_multiplier = output_multiplier; + params.output_shift = output_shift; + params.quantized_activation_min = activation_min; + params.quantized_activation_max = activation_max; + + const bool need_broadcast = tflite::reference_ops::ProcessBroadcastShapes( + getTensorShape(input1()), getTensorShape(input2()), ¶ms); + + if (need_broadcast) + { + tflite::reference_ops::BroadcastSubSlow( + params, getTensorShape(input1()), getTensorData(input1()), getTensorShape(input2()), + getTensorData(input2()), getTensorShape(output()), getTensorData(output())); + } + else + { + tflite::reference_ops::Sub(params, getTensorShape(input1()), getTensorData(input1()), + getTensorShape(input2()), getTensorData(input2()), + getTensorShape(output()), getTensorData(output())); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Sub.h b/onert-micro/luci-interpreter/src/kernels/Sub.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Sub.h rename to onert-micro/luci-interpreter/src/kernels/Sub.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Sub.test.cpp b/onert-micro/luci-interpreter/src/kernels/Sub.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Sub.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Sub.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Tanh.cpp b/onert-micro/luci-interpreter/src/kernels/Tanh.cpp new file mode 100644 index 0000000..069320e --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Tanh.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Tanh.h" + +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +Tanh::Tanh(const Tensor *input, Tensor *output) : Kernel({input}, {output}) {} + +void Tanh::configure() +{ + LUCI_INTERPRETER_CHECK(input()->element_type() == output()->element_type()); + if (input()->element_type() == DataType::U8) + { + populateLookupTable(); + } + // TODO: enable it only if kernel with dynamic shapes + output()->resize(input()->shape()); +} + +void Tanh::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::U8: + evalQuantized(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void Tanh::evalFloat() const +{ + tflite::reference_ops::Tanh(getTensorShape(input()), getTensorData(input()), + getTensorShape(output()), getTensorData(output())); +} + +void Tanh::evalQuantized() const +{ + const int size = tflite::MatchingFlatSize(getTensorShape(input()), getTensorShape(output())); + uint8_t *output_data = getTensorData(output()); + const uint8_t *input_data = getTensorData(input()); + for (int i = 0; i < size; ++i) + { + output_data[i] = getTableValue(input_data[i]); + } +} + +void Tanh::populateLookupTable() +{ + const auto input_scale = static_cast(input()->scale()); + const auto input_zero_point = static_cast(input()->zero_point()); + const auto output_scale = static_cast(output()->scale()); + const auto output_zero_point = static_cast(output()->zero_point()); + const float inverse_scale = 1 / output_scale; + int32_t maxval = std::numeric_limits::max(); + int32_t minval = std::numeric_limits::min(); + for (int32_t val = minval; val <= maxval; ++val) + { + const float dequantized = input_scale * (val - input_zero_point); + const float transformed = std::tanh(dequantized); + const float rescaled = std::round(transformed * inverse_scale); + const int32_t quantized = static_cast(rescaled + output_zero_point); + setTableValue(static_cast(std::max(std::min(maxval, quantized), minval)), + static_cast(val)); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Tanh.h b/onert-micro/luci-interpreter/src/kernels/Tanh.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Tanh.h rename to onert-micro/luci-interpreter/src/kernels/Tanh.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Tanh.test.cpp b/onert-micro/luci-interpreter/src/kernels/Tanh.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Tanh.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Tanh.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/TestUtils.cpp b/onert-micro/luci-interpreter/src/kernels/TestUtils.cpp new file mode 100644 index 0000000..78caa37 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/TestUtils.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/TestUtils.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace testing +{ + +using ::testing::FloatNear; +using ::testing::Matcher; + +Tensor makeOutputTensor(DataType element_type) { return Tensor(element_type, {}, {}, ""); } + +Tensor makeOutputTensor(DataType element_type, float scale, int32_t zero_point) +{ + return Tensor(element_type, {}, {{scale}, {zero_point}}, ""); +} + +std::vector dequantizeTensorData(const Tensor &tensor) +{ + if (tensor.element_type() == DataType::U8) + { + std::vector data = extractTensorData(tensor); + return dequantize(data.data(), data.size(), tensor.scale(), tensor.zero_point()); + } + if (tensor.element_type() == DataType::S8) + { + std::vector data = extractTensorData(tensor); + return dequantize(data.data(), data.size(), tensor.scale(), tensor.zero_point()); + } + else if (tensor.element_type() == DataType::S16) + { + // S16 quantization is symmetric, so zero point should be zero. + for (auto zp : tensor.zero_points()) + { + (void)zp; + assert(zp == 0); + } + + std::vector data = extractTensorData(tensor); + if (tensor.scales().size() == 1) + { + return dequantize(data.data(), data.size(), tensor.scale(), 0); + } + + // quantize_dimension breaks shape into two parts: + // inner dimensions that contains continuous data with one quantization type + // outer dimensions that contains other dimensions + const Shape shape = tensor.shape(); + const int32_t quantized_dimension = tensor.quantized_dimension(); + assert(quantized_dimension < shape.num_dims()); + size_t outer_dims_size = 1; + int32_t quant_dim_size = shape.dim(quantized_dimension); + size_t inner_dims_size = 1; + assert(quant_dim_size == tensor.scales().size()); + + for (int i = 0; i < quantized_dimension; ++i) + outer_dims_size *= shape.dim(i); + for (int i = quantized_dimension + 1; i < shape.num_dims(); ++i) + inner_dims_size *= shape.dim(i); + + assert(shape.num_elements() == outer_dims_size * quant_dim_size * inner_dims_size); + + std::vector dequantized_data; + dequantized_data.reserve(shape.num_elements()); + for (size_t outer_it = 0; outer_it < outer_dims_size; ++outer_it) + for (int32_t channel = 0; channel < quant_dim_size; ++channel) + { + float scale = tensor.scales()[channel]; + size_t offset = inner_dims_size * (quant_dim_size * outer_it + channel); + std::vector part_dequantized_data = + dequantize(data.data() + offset, inner_dims_size, scale, 0); + dequantized_data.insert(dequantized_data.end(), part_dequantized_data.begin(), + part_dequantized_data.end()); + } + return dequantized_data; + } + else + { + assert(false && "Unsupported type."); + } +} + +Matcher> FloatArrayNear(const std::vector &values, float max_abs_error) +{ + std::vector> matchers; + matchers.reserve(values.size()); + for (const float v : values) + { + matchers.emplace_back(FloatNear(v, max_abs_error)); + } + return ElementsAreArray(matchers); +} + +std::vector extractTensorShape(const Tensor &tensor) +{ + std::vector result; + int dims = tensor.shape().num_dims(); + for (int i = 0; i < dims; i++) + { + result.push_back(tensor.shape().dim(i)); + } + return result; +} + +} // namespace testing +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/TestUtils.h b/onert-micro/luci-interpreter/src/kernels/TestUtils.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/TestUtils.h rename to onert-micro/luci-interpreter/src/kernels/TestUtils.h diff --git a/onert-micro/luci-interpreter/src/kernels/Transpose.cpp b/onert-micro/luci-interpreter/src/kernels/Transpose.cpp new file mode 100644 index 0000000..c7396ae --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Transpose.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Transpose.h" + +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +Transpose::Transpose(const Tensor *input, const Tensor *perm, Tensor *output) + : Kernel({input, perm}, {output}) +{ +} + +void Transpose::configure() +{ + // Transpose op only supports 1D-4D input arrays. + int dims = input()->shape().num_dims(); + const int32_t *perm_data = getTensorData(perm()); + + assert(input()->shape().num_dims() <= 4); + assert(input()->element_type() == output()->element_type()); + + assert(perm()->shape().num_dims() == 1); + assert(perm()->shape().dim(0) == dims); + + Shape output_shape(dims); + for (int i = 0; i < dims; i++) + { + assert(perm_data[i] < dims && perm_data[i] >= 0); + output_shape.dim(i) = input()->shape().dim(perm_data[i]); + } + // TODO: enable it only if kernel with dynamic shapes + + output()->resize(output_shape); +} + +void Transpose::execute() const +{ + tflite::TransposeParams params{}; + const int32_t *perm_data = getTensorData(perm()); + const int32_t size = perm()->shape().dim(0); + params.perm_count = size; + for (int i = 0; i < size; i++) + params.perm[i] = perm_data[i]; + switch (input()->element_type()) + { + case DataType::FLOAT32: + tflite::reference_ops::Transpose(params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + case DataType::U8: + tflite::reference_ops::Transpose(params, getTensorShape(input()), + getTensorData(input()), getTensorShape(output()), + getTensorData(output())); + break; + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Transpose.h b/onert-micro/luci-interpreter/src/kernels/Transpose.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Transpose.h rename to onert-micro/luci-interpreter/src/kernels/Transpose.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Transpose.test.cpp b/onert-micro/luci-interpreter/src/kernels/Transpose.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Transpose.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Transpose.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/TransposeConv.cpp b/onert-micro/luci-interpreter/src/kernels/TransposeConv.cpp new file mode 100644 index 0000000..f8483ea --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/TransposeConv.cpp @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/TransposeConv.h" + +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +TransposeConv::TransposeConv(const Tensor *output_shape, const Tensor *filter, const Tensor *input, + const Tensor *bias, Tensor *output, Tensor *scratch_tensor, + const TransposeConvParams ¶ms) + : KernelWithParams({output_shape, filter, input, bias}, + {output, scratch_tensor}, params) +{ +} + +TransposeConv::~TransposeConv() +{ + // Define destructor here, to delete vector of qunatized multipliers properly +} + +void TransposeConv::configure() +{ + assert(output_shape()->shape().num_dims() == 1); + assert(input()->shape().num_dims() == 4); + assert(filter()->shape().num_dims() == 4); + assert(input()->element_type() == DataType::FLOAT32 || input()->element_type() == DataType::U8 || + input()->element_type() == DataType::S16); + assert(input()->element_type() == output()->element_type()); + assert(input()->shape().dim(3) == filter()->shape().dim(3)); + + const int num_dims = output_shape()->shape().dim(0); + Shape out_shape(num_dims); + const auto *shape_data = getTensorData(output_shape()); + for (int i = 0; i < num_dims; i++) + out_shape.dim(i) = shape_data[i]; + // TODO: enable it only if kernel with dynamic shapes + output()->resize(out_shape); + + const int32_t filter_height = filter()->shape().dim(1); + const int32_t filter_width = filter()->shape().dim(2); + const int32_t output_height = out_shape.dim(1); + const int32_t output_width = out_shape.dim(2); + + const int32_t unused_output_height = + computeOutputSize(params().padding, output_height, filter_height, params().stride_height, 1); + const int32_t unused_output_width = + computeOutputSize(params().padding, output_width, filter_width, params().stride_width, 1); + + _padding_height = + computePadding(params().stride_height, 1, output_height, filter_height, unused_output_height); + _padding_width = + computePadding(params().stride_width, 1, output_width, filter_width, unused_output_width); + + if (input()->element_type() == DataType::U8 || input()->element_type() == DataType::S16) + { + auto scratch_tensor = getOutputTensors()[1]; + scratch_tensor->resize(output()->shape()); + const std::vector real_multipliers = + getQuantizedConvolutionMultiplers(input()->scale(), filter()->scales(), output()->scale()); + + _quant_multipliers = quantizeMultipliers(real_multipliers); + } + else + { + auto scratch_tensor = getOutputTensors()[1]; + scratch_tensor->set_allocatable(false); + } +} + +void TransposeConv::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + evalFloat(); + break; + case DataType::U8: + if (filter()->scales().size() == 1) + { + evalQuantized(); + } + else if (filter()->scales().size() > 1) + { + LUCI_INTERPRETER_CHECK(filter()->shape().num_dims() == 4); + LUCI_INTERPRETER_CHECK(filter()->scales().size() == + static_cast(filter()->shape().dim(0))); + evalQuantizedPerChannel(); + } + break; + case DataType::S16: + evalQuantizedS16(); + break; + default: + assert(false && "Unsupported type."); + } +} + +void TransposeConv::evalFloat() const +{ + tflite::ConvParams op_params{}; + op_params.padding_type = tflite::PaddingType::kSame; + op_params.padding_values.height = _padding_height; + op_params.padding_values.width = _padding_width; + op_params.stride_height = params().stride_height; + op_params.stride_width = params().stride_width; + tflite::reference_ops::TransposeConv(op_params, // + getTensorShape(input()), getTensorData(input()), // + getTensorShape(filter()), getTensorData(filter()), // + getTensorShape(bias()), getTensorData(bias()), // + getTensorShape(output()), getTensorData(output()), // + tflite::RuntimeShape(), nullptr); +} + +void TransposeConv::evalQuantized() const +{ + tflite::ConvParams op_params{}; + op_params.padding_type = tflite::PaddingType::kSame; + op_params.padding_values.height = _padding_height; + op_params.padding_values.width = _padding_width; + op_params.stride_height = params().stride_height; + op_params.stride_width = params().stride_width; + // The kernel expects input and filter zero points to be negated. + op_params.input_offset = -input()->zero_point(); // Note the '-'. + op_params.weights_offset = -filter()->zero_point(); // Note the '-'. + op_params.output_offset = output()->zero_point(); + op_params.output_multiplier = _quant_multipliers[0].multiplier; + op_params.output_shift = _quant_multipliers[0].shift; + op_params.quantized_activation_min = std::numeric_limits::min(); + op_params.quantized_activation_max = std::numeric_limits::max(); + + auto scratch_tensor = getOutputTensors()[1]; + + tflite::reference_ops::TransposeConv( + op_params, // + getTensorShape(input()), getTensorData(input()), // + getTensorShape(filter()), getTensorData(filter()), // + getTensorShape(bias()), getTensorData(bias()), // + getTensorShape(output()), getTensorData(output()), // + tflite::RuntimeShape(), nullptr, // + getTensorData(scratch_tensor)); +} + +void TransposeConv::evalQuantizedPerChannel() const +{ + const auto *input_data = getTensorData(input()); + const auto *filter_data = getTensorData(filter()); + const auto *bias_data = getTensorData(bias()); + auto *output_data = getTensorData(output()); + + auto scratch_tensor = getOutputTensors()[1]; + auto *scratch_data = getTensorData(scratch_tensor); + + const Shape &input_shape = input()->shape(); + const Shape &filter_shape = filter()->shape(); + const Shape &output_shape = output()->shape(); + + const int32_t batches = input_shape.dim(0); + const int32_t input_height = input_shape.dim(1); + const int32_t input_width = input_shape.dim(2); + const int32_t input_depth = input_shape.dim(3); + const int32_t output_depth = filter_shape.dim(0); + const int32_t filter_height = filter_shape.dim(1); + const int32_t filter_width = filter_shape.dim(2); + const int32_t output_height = output_shape.dim(1); + const int32_t output_width = output_shape.dim(2); + + const int32_t stride_height = _params.stride_height; + const int32_t stride_width = _params.stride_width; + + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(Activation::NONE, output(), &activation_min, &activation_max); + + std::memset(scratch_data, 0, scratch_tensor->shape().num_elements() * sizeof(int32_t)); + + BroadcastableWrapper output_multipliers(_quant_multipliers); + for (int32_t batch = 0; batch < batches; ++batch) + { + for (int32_t in_y = 0; in_y < input_height; ++in_y) + { + for (int32_t in_x = 0; in_x < input_width; ++in_x) + { + for (int32_t in_c = 0; in_c < input_depth; ++in_c) + { + const int32_t out_y_origin = in_y * stride_height - _padding_height; + const int32_t out_x_origin = in_x * stride_width - _padding_width; + for (int32_t filter_y = 0; filter_y < filter_height; ++filter_y) + { + for (int32_t filter_x = 0; filter_x < filter_width; ++filter_x) + { + const int32_t out_x = out_x_origin + filter_x; + const int32_t out_y = out_y_origin + filter_y; + if ((out_y >= 0 && out_y < output_height) && (out_x >= 0 && out_x < output_width)) + { + for (int32_t out_c = 0; out_c < output_depth; ++out_c) + { + const uint8_t input_val = + input_data[calcOffset(input_shape, batch, in_y, in_x, in_c)]; + const uint8_t filter_val = + filter_data[calcOffset(filter_shape, out_c, filter_y, filter_x, in_c)]; + scratch_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] += + static_cast(input_val - input()->zero_point()) * + static_cast(filter_val - filter()->zero_points()[out_c]); + } + } + } + } + } + } + } + for (int32_t out_y = 0; out_y < output_height; ++out_y) + { + for (int32_t out_x = 0; out_x < output_width; ++out_x) + { + for (int32_t out_c = 0; out_c < output_depth; ++out_c) + { + int32_t acc = scratch_data[calcOffset(output_shape, batch, out_y, out_x, out_c)]; + if (bias_data) + { + acc += bias_data[out_c]; + } + + int32_t scaled_acc = tflite::MultiplyByQuantizedMultiplier( + acc, output_multipliers[out_c].multiplier, output_multipliers[out_c].shift); + + scaled_acc += output()->zero_point(); + scaled_acc = std::max(scaled_acc, activation_min); + scaled_acc = std::min(scaled_acc, activation_max); + + output_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] = scaled_acc; + } + } + } + } +} + +void TransposeConv::evalQuantizedS16() const +{ + const auto *input_data = getTensorData(input()); + const auto *filter_data = getTensorData(filter()); + const auto *bias_data = getTensorData(bias()); + auto *output_data = getTensorData(output()); + + auto scratch_tensor = getOutputTensors()[1]; + auto *scratch_data = getTensorData(scratch_tensor); + + const Shape &input_shape = input()->shape(); + const Shape &filter_shape = filter()->shape(); + const Shape &output_shape = output()->shape(); + + const int32_t batches = input_shape.dim(0); + const int32_t input_height = input_shape.dim(1); + const int32_t input_width = input_shape.dim(2); + const int32_t input_depth = input_shape.dim(3); + const int32_t output_depth = filter_shape.dim(0); + const int32_t filter_height = filter_shape.dim(1); + const int32_t filter_width = filter_shape.dim(2); + const int32_t output_height = output_shape.dim(1); + const int32_t output_width = output_shape.dim(2); + + const int32_t stride_height = _params.stride_height; + const int32_t stride_width = _params.stride_width; + + int32_t activation_min{}; + int32_t activation_max{}; + calculateActivationRangeQuantized(Activation::NONE, output(), &activation_min, &activation_max); + + std::memset(scratch_data, 0, scratch_tensor->shape().num_elements() * sizeof(int64_t)); + + BroadcastableWrapper output_multipliers(_quant_multipliers); + for (int32_t batch = 0; batch < batches; ++batch) + { + for (int32_t in_y = 0; in_y < input_height; ++in_y) + { + for (int32_t in_x = 0; in_x < input_width; ++in_x) + { + for (int32_t in_c = 0; in_c < input_depth; ++in_c) + { + const int32_t out_y_origin = in_y * stride_height - _padding_height; + const int32_t out_x_origin = in_x * stride_width - _padding_width; + for (int32_t filter_y = 0; filter_y < filter_height; ++filter_y) + { + for (int32_t filter_x = 0; filter_x < filter_width; ++filter_x) + { + const int32_t out_x = out_x_origin + filter_x; + const int32_t out_y = out_y_origin + filter_y; + if ((out_y >= 0 && out_y < output_height) && (out_x >= 0 && out_x < output_width)) + { + for (int32_t out_c = 0; out_c < output_depth; ++out_c) + { + const int16_t input_val = + input_data[calcOffset(input_shape, batch, in_y, in_x, in_c)]; + const int16_t filter_val = + filter_data[calcOffset(filter_shape, out_c, filter_y, filter_x, in_c)]; + scratch_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] += + static_cast(input_val) * static_cast(filter_val); + } + } + } + } + } + } + } + for (int32_t out_y = 0; out_y < output_height; ++out_y) + { + for (int32_t out_x = 0; out_x < output_width; ++out_x) + { + for (int32_t out_c = 0; out_c < output_depth; ++out_c) + { + int64_t acc = scratch_data[calcOffset(output_shape, batch, out_y, out_x, out_c)]; + if (bias_data) + { + acc += bias_data[out_c]; + } + int32_t scaled_acc = tflite::MultiplyByQuantizedMultiplier( + acc, output_multipliers[out_c].multiplier, output_multipliers[out_c].shift); + + scaled_acc = std::max(scaled_acc, activation_min); + scaled_acc = std::min(scaled_acc, activation_max); + + output_data[calcOffset(output_shape, batch, out_y, out_x, out_c)] = scaled_acc; + } + } + } + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/TransposeConv.h b/onert-micro/luci-interpreter/src/kernels/TransposeConv.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/TransposeConv.h rename to onert-micro/luci-interpreter/src/kernels/TransposeConv.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/TransposeConv.test.cpp b/onert-micro/luci-interpreter/src/kernels/TransposeConv.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/TransposeConv.test.cpp rename to onert-micro/luci-interpreter/src/kernels/TransposeConv.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.cpp b/onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.cpp new file mode 100644 index 0000000..57cf9f2 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.cpp @@ -0,0 +1,441 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" +#include "kernels/Utils.h" + +#include "PALUnidirectionalSequenceLSTM.h" +#include "PALApplyActivationToVector.h" + +namespace luci_interpreter +{ +namespace +{ + +#ifndef DIS_QUANT + +bool checkedLog2(const float x, int *log2_result) +{ + // Using TfLiteRound instead of std::round and std::log instead of + // std::log2 to work around these functions being missing in a toolchain + // used in some TensorFlow tests as of May 2018. + const float x_log2 = std::log(x) * (1.0f / std::log(2.0f)); + const float x_log2_rounded = std::round(x_log2); + const float x_log2_fracpart = x_log2 - x_log2_rounded; + + *log2_result = static_cast(x_log2_rounded); + return std::abs(x_log2_fracpart) < 1e-3f; +} + +// Create parameters for element wise multiplication that happens in a) cell +// state update ; b) hidden state update +// Note that all the output of gates are symmetrically quantized so only scales +// are required for input. However, during the hidden state update phase, the +// output is the updated hidden state, which is asymmetrically quantized. Thus +// output may require zero point +lstm::ArithmeticParams createInterGateParams(const float input1_scale, const float input2_scale, + const float output_scale, const DataType output_type, + const int output_zp) +{ + lstm::ArithmeticParams op_params; + if (output_type == DataType::S16) + { + op_params.quantized_activation_min = std::numeric_limits::min(); + op_params.quantized_activation_max = std::numeric_limits::max(); + } + else if (output_type == DataType::S8) + { + op_params.quantized_activation_min = std::numeric_limits::min(); + op_params.quantized_activation_max = std::numeric_limits::max(); + } + + op_params.input1_offset = 0; // symmetric + op_params.input2_offset = 0; // symmetric + op_params.output_offset = output_zp; + + const double input_product_scale = + static_cast(input1_scale) * static_cast(input2_scale); + double effective_scale = input_product_scale / static_cast(output_scale); + auto output_shift = static_cast(op_params.output_shift); + kernels::quantizeMultiplier(effective_scale, &op_params.output_multiplier, &output_shift); + op_params.output_shift = output_shift; + return op_params; +} + +void createGateParams(const circle::Tensor *input, const circle::Tensor *input_weight, + const circle::Tensor *input_bias, const circle::Tensor *hidden_state, + const circle::Tensor *hidden_state_weight, + const float nonlinear_activation_input_scale, const DataType cell_type, + lstm::GateParameters *gate_params) +{ + // Input CalculateOpDataFullyConnected + { + lstm::FullyConnectedParams input_gate_params; + double real_multiplier = 0.0; + int output_shift; + int32_t output_activation_min; + int32_t output_activation_max; + int32_t output_multiplier; + real_multiplier = kernels::getQuantizedConvolutionMultipler( + Tensor::scale(input), Tensor::scale(input_weight), nonlinear_activation_input_scale); + kernels::quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); + kernels::calculateActivationRangeQuantized(FusedActFunc::NONE, 0, + nonlinear_activation_input_scale, cell_type, + &output_activation_min, &output_activation_max); + + input_gate_params.output_shift = output_shift; + input_gate_params.output_multiplier = output_multiplier; + input_gate_params.quantized_activation_max = output_activation_max; + input_gate_params.quantized_activation_min = output_activation_min; + input_gate_params.input_offset = -Tensor::zero_point(input); + input_gate_params.weights_offset = -Tensor::zero_point(input_weight); + input_gate_params.output_offset = 0; + + gate_params->input_fc_params = input_gate_params; + } + + // Recurrent CalculateOpDataFullyConnected + { + lstm::FullyConnectedParams recurrent_gate_params; + double real_multiplier = 0.0; + int output_shift; + int32_t output_activation_min; + int32_t output_activation_max; + int32_t output_multiplier; + real_multiplier = kernels::getQuantizedConvolutionMultipler(Tensor::scale(hidden_state), + Tensor::scale(hidden_state_weight), + nonlinear_activation_input_scale); + kernels::quantizeMultiplier(real_multiplier, &output_multiplier, &output_shift); + kernels::calculateActivationRangeQuantized(FusedActFunc::NONE, 0, + nonlinear_activation_input_scale, cell_type, + &output_activation_min, &output_activation_max); + + recurrent_gate_params.output_shift = output_shift; + recurrent_gate_params.output_multiplier = output_multiplier; + recurrent_gate_params.quantized_activation_max = output_activation_max; + recurrent_gate_params.quantized_activation_min = output_activation_min; + recurrent_gate_params.input_offset = -Tensor::zero_point(hidden_state); + recurrent_gate_params.weights_offset = -Tensor::zero_point(hidden_state_weight); + recurrent_gate_params.output_offset = 0; + + gate_params->recurrent_fc_params = recurrent_gate_params; + } +} + +void prepareGateParamsInteger(lstm::LSTMStruct *lstm_struct, + lstm::LSTMParameters *quant_lstm_params) +{ + float nonlinear_input_scale = 0.00024414062; // 2^-12 Q3.12 -> Q0.15 + + createGateParams(lstm_struct->input(), lstm_struct->input_to_forget_weights(), + lstm_struct->forget_gate_bias(), lstm_struct->output_state(), + lstm_struct->recurrent_to_forget_weights(), nonlinear_input_scale, DataType::S16, + &quant_lstm_params->forget_gate_parameters); + + createGateParams(lstm_struct->input(), lstm_struct->input_to_input_weights(), + lstm_struct->input_gate_bias(), lstm_struct->output_state(), + lstm_struct->recurrent_to_input_weights(), nonlinear_input_scale, DataType::S16, + &quant_lstm_params->input_gate_parameters); + + // lstm::GateParameters cell_gate_parameters; + createGateParams(lstm_struct->input(), lstm_struct->input_to_cell_weights(), + lstm_struct->cell_gate_bias(), lstm_struct->output_state(), + lstm_struct->recurrent_to_cell_weights(), nonlinear_input_scale, DataType::S16, + &quant_lstm_params->cell_gate_parameters); + + // lstm::GateParameters output_gate_parameters; + createGateParams(lstm_struct->input(), lstm_struct->input_to_output_weights(), + lstm_struct->output_gate_bias(), lstm_struct->output_state(), + lstm_struct->recurrent_to_output_weights(), nonlinear_input_scale, DataType::S16, + &quant_lstm_params->output_gate_parameters); + + // Inter gate multiplication parameters + float nonlinear_output_scale = 0.00003051757; // 2^-15 Q3.12 -> Q0.15 + float cell_state_scale = + Tensor::scale(lstm_struct->cell_state()); // lstm_tensors.CellStateTensor()->params.scale; + // forget gate output (nonlinear output) x cell state -> cell state + quant_lstm_params->inter_gate_parameters.forget_cell_mul_params = createInterGateParams( + nonlinear_output_scale, cell_state_scale, cell_state_scale, DataType::S16, 0); + + // input gate output x cell gate output -> cell state + quant_lstm_params->inter_gate_parameters.input_mul_params = createInterGateParams( + nonlinear_output_scale, nonlinear_output_scale, cell_state_scale, DataType::S16, 0); + + // tanh output x output gate output -> hidden state (potentially asymmetric) + quant_lstm_params->inter_gate_parameters.output_mul_params = createInterGateParams( + nonlinear_output_scale, nonlinear_output_scale, Tensor::scale(lstm_struct->output_state()), + Tensor::element_type(lstm_struct->output_state()), + Tensor::zero_point(lstm_struct->output_state())); +} + +// Create the additional information about the cell state, which include: +// cell_state_scale_power: used in integer nonlinear function (e.g., tanh) +// quantized_cell_clip: quantized cell clip range +lstm::CellStateInfo createLstmCellStateInfo(const float cell_state_scale, const float cell_clip) +{ + lstm::CellStateInfo cell_state_info; + // cell_state_scale_power: 2^-cell_state_scale_power = cell state scale + int buffer; + checkedLog2(cell_state_scale, &buffer); + cell_state_info.cell_state_scale_power = buffer; + // Cell state specifics + cell_state_info.cell_clip = cell_clip; + cell_state_info.quantized_cell_clip = static_cast(std::min( + std::max(static_cast(cell_clip) / static_cast(cell_state_scale), -32768.0), + 32767.0)); + return cell_state_info; +} + +void evalInt8(const circle::Operator *cur_op, BaseRuntimeGraph *runtime_graph, bool) +{ + lstm::LSTMStruct lstm_struct(cur_op, runtime_graph); + + lstm::LSTMParameters quant_lstm_params; + prepareGateParamsInteger(&lstm_struct, &quant_lstm_params); + + lstm::CellStateInfo cell_state_info = createLstmCellStateInfo( + luci_interpreter::Tensor::scale(lstm_struct.cell_state()), lstm_struct.options->cell_clip()); + + const bool time_major = lstm_struct.options->time_major(); + const auto batch_size = + time_major ? Tensor::dim(lstm_struct.input(), 1) : Tensor::dim(lstm_struct.input(), 0); + const auto state_dimension = Tensor::dim(lstm_struct.output_state(), 1); + const auto cell_state_type_size = getDataTypeSize(Tensor::element_type(lstm_struct.cell_state())); + + auto scratch_0_data = + std::make_unique(batch_size * state_dimension * cell_state_type_size); + auto scratch_1_data = + std::make_unique(batch_size * state_dimension * cell_state_type_size); + auto scratch_2_data = + std::make_unique(batch_size * state_dimension * cell_state_type_size); + auto scratch_3_data = + std::make_unique(batch_size * state_dimension * cell_state_type_size); + + // Create and fill with 0 output state tensor + auto output_state_data = + std::make_unique(Tensor::num_elements(lstm_struct.output_state())); + std::fill_n(output_state_data.get(), Tensor::num_elements(lstm_struct.output_state()), 0); + + // Create and fill with 0 cell state tensor + auto cell_state_data = + std::make_unique(Tensor::num_elements(lstm_struct.cell_state())); + std::fill_n(cell_state_data.get(), Tensor::num_elements(lstm_struct.cell_state()), 0); + + luci_interpreter_pal::evalLSTM( + &lstm_struct, &quant_lstm_params, &cell_state_info, output_state_data.get(), + cell_state_data.get(), kernels::getTensorData(scratch_0_data.get()), + kernels::getTensorData(scratch_1_data.get()), + kernels::getTensorData(scratch_2_data.get()), + kernels::getTensorData(scratch_3_data.get()), runtime_graph); +} + +#endif // DIS_QUANT + +#ifndef DIS_FLOAT +lstm::FullyConnectedParams createFcParamsFloat() +{ + lstm::FullyConnectedParams op_params; + kernels::calculateActivationRange(FusedActFunc::NONE, &op_params.float_activation_min, + &op_params.float_activation_max); + return op_params; +} + +lstm::GateParameters createGateParamsFloat() +{ + lstm::GateParameters gate_params; + + gate_params.input_fc_params = createFcParamsFloat(); + gate_params.recurrent_fc_params = createFcParamsFloat(); + + return gate_params; +} + +lstm::CellStateInfo createLstmCellStateInfoFloat(const float cell_clip) +{ + lstm::CellStateInfo cell_state_info; + cell_state_info.cell_clip = cell_clip; + cell_state_info.cell_state_scale_power = 0; // no quantization + cell_state_info.quantized_cell_clip = 0; // no quantization + return cell_state_info; +} + +void prepareGateParamsFloat(lstm::LSTMParameters *float_lstm_params) +{ + // Gate Parameters + float_lstm_params->forget_gate_parameters = createGateParamsFloat(); + float_lstm_params->input_gate_parameters = createGateParamsFloat(); + float_lstm_params->cell_gate_parameters = createGateParamsFloat(); + float_lstm_params->output_gate_parameters = createGateParamsFloat(); + + // Inter gate multiplication parameters + lstm::ArithmeticParams op_params; + kernels::calculateActivationRange(FusedActFunc::NONE, &op_params.float_activation_min, + &op_params.float_activation_max); + float_lstm_params->inter_gate_parameters.forget_cell_mul_params = op_params; + float_lstm_params->inter_gate_parameters.input_mul_params = op_params; + float_lstm_params->inter_gate_parameters.output_mul_params = op_params; +} + +void evalFloat(const circle::Operator *cur_op, BaseRuntimeGraph *runtime_graph, bool) +{ + lstm::LSTMStruct lstm_struct(cur_op, runtime_graph); + + lstm::CellStateInfo cell_state_info = + createLstmCellStateInfoFloat(lstm_struct.options->cell_clip()); + + lstm::LSTMParameters lstm_params; + prepareGateParamsFloat(&lstm_params); + + const bool time_major = lstm_struct.options->time_major(); + const auto batch_size = + time_major ? Tensor::dim(lstm_struct.input(), 1) : Tensor::dim(lstm_struct.input(), 0); + const auto state_dimension = Tensor::dim(lstm_struct.output_state(), 1); + const auto cell_state_type_size = getDataTypeSize(Tensor::element_type(lstm_struct.cell_state())); + + auto scratch_0_data = + std::make_unique(batch_size * state_dimension * cell_state_type_size); + auto scratch_1_data = + std::make_unique(batch_size * state_dimension * cell_state_type_size); + auto scratch_2_data = + std::make_unique(batch_size * state_dimension * cell_state_type_size); + auto scratch_3_data = + std::make_unique(batch_size * state_dimension * cell_state_type_size); + + // Create and fill with 0 output state tensor + auto output_state_data = + std::make_unique(Tensor::num_elements(lstm_struct.output_state())); + std::fill_n(output_state_data.get(), Tensor::num_elements(lstm_struct.output_state()), 0); + + // Create and fill with 0 cell state tensor + auto cell_state_data = std::make_unique(Tensor::num_elements(lstm_struct.cell_state())); + std::fill_n(cell_state_data.get(), Tensor::num_elements(lstm_struct.cell_state()), 0); + + luci_interpreter_pal::evalLSTM( + &lstm_struct, &lstm_params, &cell_state_info, output_state_data.get(), cell_state_data.get(), + kernels::getTensorData(scratch_0_data.get()), + kernels::getTensorData(scratch_1_data.get()), + kernels::getTensorData(scratch_2_data.get()), + kernels::getTensorData(scratch_3_data.get()), runtime_graph); +} +#endif // DIS_FLOAT + +void validateWeightTensorSize(const circle::Tensor *weight_tensor, int dim1_size, int dim2_size) +{ + LUCI_INTERPRETER_CHECK(Tensor::num_dims(weight_tensor) == 2); + LUCI_INTERPRETER_CHECK(Tensor::dim(weight_tensor, 0) == dim1_size); + LUCI_INTERPRETER_CHECK(Tensor::dim(weight_tensor, 1) == dim2_size); +} + +void validateTensorsSize(lstm::LSTMStruct *lstm_struct, const bool time_major) +{ + const auto batch_size = + time_major ? Tensor::dim(lstm_struct->input(), 1) : Tensor::dim(lstm_struct->input(), 0); + + const auto input_dimension = Tensor::dim(lstm_struct->input(), 2); + const auto state_dimension = Tensor::dim(lstm_struct->output_state(), 1); + + // Input FC weights + for (int32_t i = 1; i < 5; i++) + { + validateWeightTensorSize(lstm_struct->get_internal_tensor(i), state_dimension, input_dimension); + } + + // Recurrent FC weights + for (int32_t i = 5; i < 9; i++) + { + validateWeightTensorSize(lstm_struct->get_internal_tensor(i), state_dimension, state_dimension); + } + + // Biases + for (int32_t i = 12; i < 16; i++) + { + LUCI_INTERPRETER_CHECK(Tensor::num_dims(lstm_struct->get_internal_tensor(i)) == 1); + LUCI_INTERPRETER_CHECK(Tensor::dim(lstm_struct->get_internal_tensor(i), 0) == state_dimension); + } + + // Check the shape of input state tensors. + // These tensor may be 1D or 2D. It's fine as long as the total size is + // correct. + LUCI_INTERPRETER_CHECK(Tensor::num_elements(lstm_struct->output_state()) == + batch_size * state_dimension); + LUCI_INTERPRETER_CHECK(Tensor::num_elements(lstm_struct->cell_state()) == + batch_size * state_dimension); + + // Check the shape of output tensor against that of input tensor + LUCI_INTERPRETER_CHECK(Tensor::num_dims(lstm_struct->output()) == 3); + LUCI_INTERPRETER_CHECK(Tensor::dim(lstm_struct->input(), 0) == + Tensor::dim(lstm_struct->output(), 0)); + LUCI_INTERPRETER_CHECK(Tensor::dim(lstm_struct->input(), 1) == + Tensor::dim(lstm_struct->output(), 1)); + LUCI_INTERPRETER_CHECK(Tensor::dim(lstm_struct->output(), 2) == state_dimension); +} + +} // namespace + +void configure_kernel_CircleUnidirectionalSequenceLSTM(const circle::Operator *cur_op, + BaseRuntimeGraph *runtime_graph) +{ + lstm::LSTMStruct lstm_struct(cur_op, runtime_graph); + + LUCI_INTERPRETER_CHECK(Tensor::element_type(lstm_struct.input()) == DataType::FLOAT32 or + Tensor::element_type(lstm_struct.input()) == DataType::S8); + + lstm_struct.validateTensorTypes(); + + const bool time_major = lstm_struct.options->time_major(); + + validateTensorsSize(&lstm_struct, time_major); + + // No peephole + for (int32_t i = 9; i < 12; ++i) + LUCI_INTERPRETER_CHECK(lstm_struct.get_internal_tensor(i) == nullptr); + + // No projection + for (int32_t i = 16; i < 18; ++i) + LUCI_INTERPRETER_CHECK(lstm_struct.get_internal_tensor(i) == nullptr); + + // No internal layer norm + for (int32_t i = 20; i < 24; ++i) + LUCI_INTERPRETER_CHECK(lstm_struct.get_internal_tensor(i) == nullptr); +} + +void execute_kernel_CircleUnidirectionalSequenceLSTM(const circle::Operator *cur_op, + BaseRuntimeGraph *runtime_graph, bool in_place) +{ + const auto input_index = cur_op->inputs()->operator[](0); + assert(input_index != -1); + + const auto input = runtime_graph->getCircleTensorByIndex(input_index); + + switch (Tensor::element_type(input)) + { +#ifndef DIS_FLOAT + case DataType::FLOAT32: + evalFloat(cur_op, runtime_graph, in_place); + break; +#endif // DIS_FLOAT +#ifndef DIS_QUANT + case DataType::S8: + evalInt8(cur_op, runtime_graph, in_place); + break; +#endif // DIS_QUANT + default: + assert(false && "Unsupported type."); + } +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.h b/onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.h new file mode 100644 index 0000000..4a24978 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.h @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_KERNELS_UNIDIRECTIONAL_SEQUENCE_LSTM_H +#define LUCI_INTERPRETER_KERNELS_UNIDIRECTIONAL_SEQUENCE_LSTM_H + +#include "Utils.h" + +namespace luci_interpreter +{ +namespace lstm +{ + +struct LSTMStruct +{ + LSTMStruct() = delete; + LSTMStruct(const LSTMStruct &) = delete; + + explicit LSTMStruct(const circle::Operator *cur_op, + luci_interpreter::BaseRuntimeGraph *runtime_graph) + { + const auto input_index = cur_op->inputs()->operator[](0); + const auto input_to_input_weights_index = cur_op->inputs()->operator[](1); + const auto input_to_forget_weights_index = cur_op->inputs()->operator[](2); + const auto input_to_cell_weights_index = cur_op->inputs()->operator[](3); + const auto input_to_output_weights_index = cur_op->inputs()->operator[](4); + assert(input_index != -1); + // input_to_input_weights_index - optional + assert(input_to_forget_weights_index != -1); + assert(input_to_cell_weights_index != -1); + assert(input_to_output_weights_index != -1); + internal_tensors[0] = runtime_graph->getCircleTensorByIndex(input_index); + internal_tensors[1] = runtime_graph->getCircleTensorByIndex(input_to_input_weights_index); + internal_tensors[2] = runtime_graph->getCircleTensorByIndex(input_to_forget_weights_index); + internal_tensors[3] = runtime_graph->getCircleTensorByIndex(input_to_cell_weights_index); + internal_tensors[4] = runtime_graph->getCircleTensorByIndex(input_to_output_weights_index); + + const auto recurrent_to_input_weights_index = cur_op->inputs()->operator[](5); + const auto recurrent_to_forget_weights_index = cur_op->inputs()->operator[](6); + const auto recurrent_to_cell_weights_index = cur_op->inputs()->operator[](7); + const auto recurrent_to_output_weights_index = cur_op->inputs()->operator[](8); + // recurrent_to_input_weights_index - optional + assert(recurrent_to_forget_weights_index != -1); + assert(recurrent_to_cell_weights_index != -1); + assert(recurrent_to_output_weights_index != -1); + internal_tensors[5] = runtime_graph->getCircleTensorByIndex(recurrent_to_input_weights_index); + internal_tensors[6] = runtime_graph->getCircleTensorByIndex(recurrent_to_forget_weights_index); + internal_tensors[7] = runtime_graph->getCircleTensorByIndex(recurrent_to_cell_weights_index); + internal_tensors[8] = runtime_graph->getCircleTensorByIndex(recurrent_to_output_weights_index); + + const auto cell_to_input_weights_index = cur_op->inputs()->operator[](9); + const auto cell_to_forget_weights_index = cur_op->inputs()->operator[](10); + const auto cell_to_output_weights_index = cur_op->inputs()->operator[](11); + // optional cell_to_input_weights_index + // optional cell_to_forget_weights_index + // optional cell_to_output_weights_index + internal_tensors[9] = runtime_graph->getCircleTensorByIndex(cell_to_input_weights_index); + internal_tensors[10] = runtime_graph->getCircleTensorByIndex(cell_to_forget_weights_index); + internal_tensors[11] = runtime_graph->getCircleTensorByIndex(cell_to_output_weights_index); + + const auto input_gate_bias_index = cur_op->inputs()->operator[](12); + const auto forget_gate_bias_index = cur_op->inputs()->operator[](13); + const auto cell_gate_bias_index = cur_op->inputs()->operator[](14); + const auto output_gate_bias_index = cur_op->inputs()->operator[](15); + // optional input_gate_bias_index + assert(forget_gate_bias_index != -1); + assert(cell_gate_bias_index != -1); + assert(output_gate_bias_index != -1); + internal_tensors[12] = runtime_graph->getCircleTensorByIndex(input_gate_bias_index); + internal_tensors[13] = runtime_graph->getCircleTensorByIndex(forget_gate_bias_index); + internal_tensors[14] = runtime_graph->getCircleTensorByIndex(cell_gate_bias_index); + internal_tensors[15] = runtime_graph->getCircleTensorByIndex(output_gate_bias_index); + + const auto projection_weights_index = cur_op->inputs()->operator[](16); + const auto projection_bias_index = cur_op->inputs()->operator[](17); + // optional projection_weights_index + // optional projection_bias_index + internal_tensors[16] = runtime_graph->getCircleTensorByIndex(projection_weights_index); + internal_tensors[17] = runtime_graph->getCircleTensorByIndex(projection_bias_index); + + const auto output_state_index = cur_op->inputs()->operator[](18); + const auto cell_state_index = cur_op->inputs()->operator[](19); + assert(output_state_index != -1); + assert(cell_state_index != -1); + internal_tensors[18] = runtime_graph->getCircleTensorByIndex(output_state_index); + internal_tensors[19] = runtime_graph->getCircleTensorByIndex(cell_state_index); + + const auto input_layer_norm_coefficients_index = cur_op->inputs()->operator[](20); + const auto forget_layer_norm_coefficients_index = cur_op->inputs()->operator[](21); + const auto cell_layer_norm_coefficients_index = cur_op->inputs()->operator[](22); + const auto output_layer_norm_coefficients_index = cur_op->inputs()->operator[](23); + // optional input_layer_norm_coefficients_index + // optional forget_layer_norm_coefficients_index + // optional cell_layer_norm_coefficients_index + // optional output_layer_norm_coefficients_index + internal_tensors[20] = + runtime_graph->getCircleTensorByIndex(input_layer_norm_coefficients_index); + internal_tensors[21] = + runtime_graph->getCircleTensorByIndex(forget_layer_norm_coefficients_index); + internal_tensors[22] = + runtime_graph->getCircleTensorByIndex(cell_layer_norm_coefficients_index); + internal_tensors[23] = + runtime_graph->getCircleTensorByIndex(output_layer_norm_coefficients_index); + + const auto output_index = cur_op->outputs()->operator[](0); + assert(output_index != -1); + output_internal = runtime_graph->getCircleTensorByIndex(output_index); + + options = cur_op->builtin_options_as_UnidirectionalSequenceLSTMOptions(); + } + + void validateTensorTypes() + { + LUCI_INTERPRETER_CHECK(Tensor::element_type(input()) == Tensor::element_type(output_state())); + LUCI_INTERPRETER_CHECK(Tensor::element_type(output()) == Tensor::element_type(input())); + + for (int32_t i = 1; i < 9; ++i) + { + LUCI_INTERPRETER_CHECK(internal_tensors[i] == nullptr or + Tensor::element_type(input_to_forget_weights()) == + Tensor::element_type(internal_tensors[i])); + } + + for (int32_t i = 12; i < 16; ++i) + { + LUCI_INTERPRETER_CHECK(internal_tensors[i] == nullptr or + Tensor::element_type(forget_gate_bias()) == + Tensor::element_type(internal_tensors[i])); + } + } + + const circle::Tensor *input() { return internal_tensors[0]; }; + + const circle::Tensor *input_to_input_weights() { return internal_tensors[1]; }; + const circle::Tensor *input_to_forget_weights() { return internal_tensors[2]; }; + const circle::Tensor *input_to_cell_weights() { return internal_tensors[3]; }; + const circle::Tensor *input_to_output_weights() { return internal_tensors[4]; }; + + const circle::Tensor *recurrent_to_input_weights() { return internal_tensors[5]; }; + const circle::Tensor *recurrent_to_forget_weights() { return internal_tensors[6]; }; + const circle::Tensor *recurrent_to_cell_weights() { return internal_tensors[7]; }; + const circle::Tensor *recurrent_to_output_weights() { return internal_tensors[8]; }; + + const circle::Tensor *cell_to_input_weights() { return internal_tensors[9]; }; + const circle::Tensor *cell_to_forget_weights() { return internal_tensors[10]; }; + const circle::Tensor *cell_to_output_weights() { return internal_tensors[11]; }; + + const circle::Tensor *input_gate_bias() { return internal_tensors[12]; }; + const circle::Tensor *forget_gate_bias() { return internal_tensors[13]; }; + const circle::Tensor *cell_gate_bias() { return internal_tensors[14]; }; + const circle::Tensor *output_gate_bias() { return internal_tensors[15]; }; + + const circle::Tensor *projection_weights() { return internal_tensors[16]; }; + const circle::Tensor *projection_bias() { return internal_tensors[17]; }; + + const circle::Tensor *output_state() { return internal_tensors[18]; }; + const circle::Tensor *cell_state() { return internal_tensors[19]; }; + + const circle::Tensor *input_layer_norm_coefficients() { return internal_tensors[20]; }; + const circle::Tensor *forget_layer_norm_coefficients() { return internal_tensors[21]; }; + const circle::Tensor *cell_layer_norm_coefficients() { return internal_tensors[22]; }; + const circle::Tensor *output_layer_norm_coefficients() { return internal_tensors[23]; }; + const circle::Tensor *output() { return output_internal; }; + + const circle::UnidirectionalSequenceLSTMOptions *options; + + const circle::Tensor *get_internal_tensor(int i) { return internal_tensors[i]; } + +private: + const circle::Tensor *output_internal; + const circle::Tensor *internal_tensors[24]; +}; + +struct FullyConnectedParams +{ + int32_t input_offset; + int32_t weights_offset; + int32_t output_offset; + int32_t output_multiplier; + int32_t output_shift; + int32_t quantized_activation_min; + int32_t quantized_activation_max; + int32_t float_activation_min; + int32_t float_activation_max; +}; + +struct GateParameters +{ + FullyConnectedParams input_fc_params; + FullyConnectedParams recurrent_fc_params; +}; + +struct ArithmeticParams +{ + int32_t input1_offset; + int32_t input2_offset; + int32_t quantized_activation_min; + int32_t quantized_activation_max; + int32_t output_offset; + int32_t output_multiplier; + int32_t output_shift; + int32_t float_activation_min; + int32_t float_activation_max; +}; + +struct InterGateParameters +{ + ArithmeticParams forget_cell_mul_params; + ArithmeticParams input_mul_params; + ArithmeticParams output_mul_params; +}; + +struct CellStateInfo +{ + float cell_clip; + // clipping range for cell state only 16 bits cell is supported (could be + // generalized through templatation) + int16_t quantized_cell_clip; + // 2^-cell_state_scale_power = cell state scale, required by integer tanh + // computation + int32_t cell_state_scale_power; +}; + +struct LSTMParameters +{ + GateParameters forget_gate_parameters; + GateParameters input_gate_parameters; + GateParameters cell_gate_parameters; + GateParameters output_gate_parameters; + InterGateParameters inter_gate_parameters; +}; + +} // namespace lstm +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_KERNELS_UNIDIRECTIONAL_SEQUENCE_LSTM_H diff --git a/onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.test.cpp b/onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.test.cpp new file mode 100644 index 0000000..df059cf --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/UnidirectionalSequenceLSTM.test.cpp @@ -0,0 +1,565 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/UnidirectionalSequenceLSTM.h" +#include "kernels/TestUtils.h" +#include "luci_interpreter/TestMemoryManager.h" + +namespace luci_interpreter +{ +namespace kernels +{ +namespace +{ + +using namespace testing; + +class UnidirectionalSequenceLSTMTest : public ::testing::Test +{ +protected: + void SetUp() override { _memory_manager = std::make_unique(); } + + std::unique_ptr _memory_manager; +}; + +// NOTE from NoCifgNoPeepholeNoProjectionNoClippingUnidirectionalLstmTest +TEST_F(UnidirectionalSequenceLSTMTest, FloatTest) +{ + const int32_t n_batch = 1; + const int32_t n_input = 2; + const int32_t n_cell = 4; + const int32_t n_output = 4; + const int32_t sequence_length = 3; + + std::vector input_to_input_weights = {-0.45018822, -0.02338299, -0.0870589, -0.34550029, + 0.04266912, -0.15680569, -0.34856534, 0.43890524}; + + std::vector input_to_cell_weights = {-0.50013041, 0.1370284, 0.11810488, 0.2013163, + -0.20583314, 0.44344562, 0.22077113, -0.29909778}; + + std::vector input_to_forget_weights = {0.09701663, 0.20334584, -0.50592935, -0.31343272, + -0.40032279, 0.44781327, 0.01387155, -0.35593212}; + + std::vector input_to_output_weights = {-0.25065863, -0.28290087, 0.04613829, 0.40525138, + 0.44272184, 0.03897077, -0.1556896, 0.19487578}; + + std::vector input_gate_bias = {0., 0., 0., 0.}; + std::vector forget_gate_bias = {1., 1., 1., 1.}; + std::vector cell_gate_bias = {0., 0., 0., 0.}; + std::vector output_gate_bias = {0., 0., 0., 0.}; + + std::vector recurrent_to_input_weights = { + -0.0063535, -0.2042388, 0.31454784, -0.35746509, 0.28902304, 0.08183324, + -0.16555229, 0.02286911, -0.13566875, 0.03034258, 0.48091322, -0.12528998, + 0.24077177, -0.51332325, -0.33502164, 0.10629296}; + + std::vector recurrent_to_forget_weights = { + -0.48684245, -0.06655136, 0.42224967, 0.2112639, 0.27654213, 0.20864892, + -0.07646349, 0.45877004, 0.00141793, -0.14609534, 0.36447752, 0.09196436, + 0.28053468, 0.01560611, -0.20127171, -0.01140004}; + + std::vector recurrent_to_cell_weights = { + -0.3407414, 0.24443203, -0.2078532, 0.26320225, 0.05695659, -0.00123841, + -0.4744786, -0.35869038, -0.06418842, -0.13502428, -0.501764, 0.22830659, + -0.46367589, 0.26016325, -0.03894562, -0.16368064}; + + std::vector recurrent_to_output_weights = { + 0.43385774, -0.17194885, 0.2718237, 0.09215671, 0.24107647, -0.39835793, + 0.18212086, 0.01301402, 0.48572797, -0.50656658, 0.20047462, -0.20607421, + -0.51818722, -0.15390486, 0.0468148, 0.39922136}; + + Shape input_to_input_weights_shape{n_cell, n_input}; + Shape input_to_cell_weights_shape{n_cell, n_input}; + Shape input_to_forget_weights_shape{n_cell, n_input}; + Shape input_to_output_weights_shape{n_cell, n_input}; + + Shape input_gate_bias_shape{n_cell}; + Shape forget_gate_bias_shape{n_cell}; + Shape cell_gate_bias_shape{n_cell}; + Shape output_gate_bias_shape{n_cell}; + + Shape recurrent_to_input_weights_shape{n_cell, n_output}; + Shape recurrent_to_cell_weights_shape{n_cell, n_output}; + Shape recurrent_to_forget_weights_shape{n_cell, n_output}; + Shape recurrent_to_output_weights_shape{n_cell, n_output}; + + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + Tensor input_to_cell_weights_tensor = makeInputTensor( + input_to_cell_weights_shape, input_to_cell_weights, _memory_manager.get()); + Tensor input_to_forget_weights_tensor = makeInputTensor( + input_to_forget_weights_shape, input_to_forget_weights, _memory_manager.get()); + Tensor input_to_output_weights_tensor = makeInputTensor( + input_to_output_weights_shape, input_to_output_weights, _memory_manager.get()); + + Tensor input_gate_bias_tensor = makeInputTensor( + input_gate_bias_shape, input_gate_bias, _memory_manager.get()); + Tensor forget_gate_bias_tensor = makeInputTensor( + forget_gate_bias_shape, forget_gate_bias, _memory_manager.get()); + Tensor cell_gate_bias_tensor = + makeInputTensor(cell_gate_bias_shape, cell_gate_bias, _memory_manager.get()); + Tensor output_gate_bias_tensor = makeInputTensor( + output_gate_bias_shape, output_gate_bias, _memory_manager.get()); + + Tensor recurrent_to_input_weights_tensor = makeInputTensor( + recurrent_to_input_weights_shape, recurrent_to_input_weights, _memory_manager.get()); + Tensor recurrent_to_cell_weights_tensor = makeInputTensor( + recurrent_to_cell_weights_shape, recurrent_to_cell_weights, _memory_manager.get()); + Tensor recurrent_to_forget_weights_tensor = makeInputTensor( + recurrent_to_forget_weights_shape, recurrent_to_forget_weights, _memory_manager.get()); + Tensor recurrent_to_output_weights_tensor = makeInputTensor( + recurrent_to_output_weights_shape, recurrent_to_output_weights, _memory_manager.get()); + + std::vector input_data{2., 3., 3., 4., 1., 1.}; + Shape input_shape{sequence_length, n_batch, n_input}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + Shape output_state_shape{n_batch, n_output}; + Tensor output_state_tensor = makeOutputTensor(DataType::FLOAT32); + output_state_tensor.resize(output_state_shape); + + Shape cell_state_shape{n_batch, n_cell}; + Tensor cell_state_tensor = makeOutputTensor(DataType::FLOAT32); + cell_state_tensor.resize(cell_state_shape); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_2(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_3(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 0.0; + params.proj_clip = 0.0; + params.time_major = true; + params.asymmetric_quantize_inputs = false; + + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_forget_weights_tensor, + &input_to_cell_weights_tensor, &input_to_output_weights_tensor, + &recurrent_to_input_weights_tensor, &recurrent_to_forget_weights_tensor, + &recurrent_to_cell_weights_tensor, &recurrent_to_output_weights_tensor, nullptr, nullptr, + nullptr, &input_gate_bias_tensor, &forget_gate_bias_tensor, &cell_gate_bias_tensor, + &output_gate_bias_tensor, nullptr, nullptr, &output_state_tensor, &cell_state_tensor, nullptr, + nullptr, nullptr, nullptr, &output_tensor, &scratchpad_1, &scratchpad_2, &scratchpad_3, params); + + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(output_state_tensor); + _memory_manager->allocate_memory(cell_state_tensor); + _memory_manager->allocate_memory(scratchpad_1); + _memory_manager->allocate_memory(scratchpad_2); + _memory_manager->allocate_memory(scratchpad_3); + kernel.execute(); + + std::vector ref_output_data{-0.02973187, 0.1229473, 0.20885126, -0.15358765, + -0.03716109, 0.12507336, 0.41193449, -0.20860538, + -0.15053082, 0.09120187, 0.24278517, -0.12222792}; + + std::vector ref_output_shape{sequence_length, n_batch, n_output}; + const float tolerance = 1e-5; + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data, tolerance)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(UnidirectionalSequenceLSTMTest, FloatTest_batch) +{ + const int32_t n_batch = 1; + const int32_t n_input = 2; + const int32_t n_cell = 4; + const int32_t n_output = 4; + const int32_t sequence_length = 3; + + std::vector input_to_input_weights = {-0.45018822, -0.02338299, -0.0870589, -0.34550029, + 0.04266912, -0.15680569, -0.34856534, 0.43890524}; + + std::vector input_to_cell_weights = {-0.50013041, 0.1370284, 0.11810488, 0.2013163, + -0.20583314, 0.44344562, 0.22077113, -0.29909778}; + + std::vector input_to_forget_weights = {0.09701663, 0.20334584, -0.50592935, -0.31343272, + -0.40032279, 0.44781327, 0.01387155, -0.35593212}; + + std::vector input_to_output_weights = {-0.25065863, -0.28290087, 0.04613829, 0.40525138, + 0.44272184, 0.03897077, -0.1556896, 0.19487578}; + + std::vector input_gate_bias = {0., 0., 0., 0.}; + std::vector forget_gate_bias = {1., 1., 1., 1.}; + std::vector cell_gate_bias = {0., 0., 0., 0.}; + std::vector output_gate_bias = {0., 0., 0., 0.}; + + std::vector recurrent_to_input_weights = { + -0.0063535, -0.2042388, 0.31454784, -0.35746509, 0.28902304, 0.08183324, + -0.16555229, 0.02286911, -0.13566875, 0.03034258, 0.48091322, -0.12528998, + 0.24077177, -0.51332325, -0.33502164, 0.10629296}; + + std::vector recurrent_to_forget_weights = { + -0.48684245, -0.06655136, 0.42224967, 0.2112639, 0.27654213, 0.20864892, + -0.07646349, 0.45877004, 0.00141793, -0.14609534, 0.36447752, 0.09196436, + 0.28053468, 0.01560611, -0.20127171, -0.01140004}; + + std::vector recurrent_to_cell_weights = { + -0.3407414, 0.24443203, -0.2078532, 0.26320225, 0.05695659, -0.00123841, + -0.4744786, -0.35869038, -0.06418842, -0.13502428, -0.501764, 0.22830659, + -0.46367589, 0.26016325, -0.03894562, -0.16368064}; + + std::vector recurrent_to_output_weights = { + 0.43385774, -0.17194885, 0.2718237, 0.09215671, 0.24107647, -0.39835793, + 0.18212086, 0.01301402, 0.48572797, -0.50656658, 0.20047462, -0.20607421, + -0.51818722, -0.15390486, 0.0468148, 0.39922136}; + + Shape input_to_input_weights_shape{n_cell, n_input}; + Shape input_to_cell_weights_shape{n_cell, n_input}; + Shape input_to_forget_weights_shape{n_cell, n_input}; + Shape input_to_output_weights_shape{n_cell, n_input}; + + Shape input_gate_bias_shape{n_cell}; + Shape forget_gate_bias_shape{n_cell}; + Shape cell_gate_bias_shape{n_cell}; + Shape output_gate_bias_shape{n_cell}; + + Shape recurrent_to_input_weights_shape{n_cell, n_output}; + Shape recurrent_to_cell_weights_shape{n_cell, n_output}; + Shape recurrent_to_forget_weights_shape{n_cell, n_output}; + Shape recurrent_to_output_weights_shape{n_cell, n_output}; + + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + Tensor input_to_cell_weights_tensor = makeInputTensor( + input_to_cell_weights_shape, input_to_cell_weights, _memory_manager.get()); + Tensor input_to_forget_weights_tensor = makeInputTensor( + input_to_forget_weights_shape, input_to_forget_weights, _memory_manager.get()); + Tensor input_to_output_weights_tensor = makeInputTensor( + input_to_output_weights_shape, input_to_output_weights, _memory_manager.get()); + + Tensor input_gate_bias_tensor = makeInputTensor( + input_gate_bias_shape, input_gate_bias, _memory_manager.get()); + Tensor forget_gate_bias_tensor = makeInputTensor( + forget_gate_bias_shape, forget_gate_bias, _memory_manager.get()); + Tensor cell_gate_bias_tensor = + makeInputTensor(cell_gate_bias_shape, cell_gate_bias, _memory_manager.get()); + Tensor output_gate_bias_tensor = makeInputTensor( + output_gate_bias_shape, output_gate_bias, _memory_manager.get()); + + Tensor recurrent_to_input_weights_tensor = makeInputTensor( + recurrent_to_input_weights_shape, recurrent_to_input_weights, _memory_manager.get()); + Tensor recurrent_to_cell_weights_tensor = makeInputTensor( + recurrent_to_cell_weights_shape, recurrent_to_cell_weights, _memory_manager.get()); + Tensor recurrent_to_forget_weights_tensor = makeInputTensor( + recurrent_to_forget_weights_shape, recurrent_to_forget_weights, _memory_manager.get()); + Tensor recurrent_to_output_weights_tensor = makeInputTensor( + recurrent_to_output_weights_shape, recurrent_to_output_weights, _memory_manager.get()); + + std::vector input_data{2., 3., 3., 4., 1., 1.}; + Shape input_shape{n_batch, sequence_length, n_input}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + Shape output_state_shape{n_batch, n_output}; + Tensor output_state_tensor = makeOutputTensor(DataType::FLOAT32); + output_state_tensor.resize(output_state_shape); + + Shape cell_state_shape{n_batch, n_cell}; + Tensor cell_state_tensor = makeOutputTensor(DataType::FLOAT32); + cell_state_tensor.resize(cell_state_shape); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 0.0; + params.proj_clip = 0.0; + params.time_major = false; + params.asymmetric_quantize_inputs = false; + + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_forget_weights_tensor, + &input_to_cell_weights_tensor, &input_to_output_weights_tensor, + &recurrent_to_input_weights_tensor, &recurrent_to_forget_weights_tensor, + &recurrent_to_cell_weights_tensor, &recurrent_to_output_weights_tensor, nullptr, nullptr, + nullptr, &input_gate_bias_tensor, &forget_gate_bias_tensor, &cell_gate_bias_tensor, + &output_gate_bias_tensor, nullptr, nullptr, &output_state_tensor, &cell_state_tensor, nullptr, + nullptr, nullptr, nullptr, &output_tensor, &output_state_tensor, &cell_state_tensor, + &scratchpad_1, params); + + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(output_state_tensor); + _memory_manager->allocate_memory(cell_state_tensor); + _memory_manager->allocate_memory(scratchpad_1); + kernel.execute(); + + std::vector ref_output_data{-0.02973187, 0.1229473, 0.20885126, -0.15358765, + -0.03716109, 0.12507336, 0.41193449, -0.20860538, + -0.15053082, 0.09120187, 0.24278517, -0.12222792}; + + std::vector ref_output_shape{n_batch, sequence_length, n_output}; + const float tolerance = 1e-5; + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data, tolerance)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(UnidirectionalSequenceLSTMTest, FloatTest_simple) +{ + const int32_t n_batch = 1; + const int32_t n_input = 1; + const int32_t n_cell = 1; + const int32_t n_output = 1; + const int32_t sequence_length = 1; + + std::vector input_to_input_weights = {0.329067}; + std::vector input_to_forget_weights = {0.308059}; + std::vector input_to_cell_weights = {0.152916}; + std::vector input_to_output_weights = {-0.476033}; + + std::vector input_gate_bias = {0.}; + std::vector forget_gate_bias = {1.}; + std::vector cell_gate_bias = {0.}; + std::vector output_gate_bias = {0.}; + + std::vector recurrent_to_input_weights = {0.207806}; + std::vector recurrent_to_forget_weights = {0.028718}; + std::vector recurrent_to_cell_weights = {-0.182756}; + std::vector recurrent_to_output_weights = {-0.960517}; + + Shape input_to_input_weights_shape{n_cell, n_input}; + Shape input_to_cell_weights_shape{n_cell, n_input}; + Shape input_to_forget_weights_shape{n_cell, n_input}; + Shape input_to_output_weights_shape{n_cell, n_input}; + + Shape input_gate_bias_shape{n_cell}; + Shape forget_gate_bias_shape{n_cell}; + Shape cell_gate_bias_shape{n_cell}; + Shape output_gate_bias_shape{n_cell}; + + Shape recurrent_to_input_weights_shape{n_cell, n_output}; + Shape recurrent_to_cell_weights_shape{n_cell, n_output}; + Shape recurrent_to_forget_weights_shape{n_cell, n_output}; + Shape recurrent_to_output_weights_shape{n_cell, n_output}; + + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + Tensor input_to_cell_weights_tensor = makeInputTensor( + input_to_cell_weights_shape, input_to_cell_weights, _memory_manager.get()); + Tensor input_to_forget_weights_tensor = makeInputTensor( + input_to_forget_weights_shape, input_to_forget_weights, _memory_manager.get()); + Tensor input_to_output_weights_tensor = makeInputTensor( + input_to_output_weights_shape, input_to_output_weights, _memory_manager.get()); + + Tensor input_gate_bias_tensor = makeInputTensor( + input_gate_bias_shape, input_gate_bias, _memory_manager.get()); + Tensor forget_gate_bias_tensor = makeInputTensor( + forget_gate_bias_shape, forget_gate_bias, _memory_manager.get()); + Tensor cell_gate_bias_tensor = + makeInputTensor(cell_gate_bias_shape, cell_gate_bias, _memory_manager.get()); + Tensor output_gate_bias_tensor = makeInputTensor( + output_gate_bias_shape, output_gate_bias, _memory_manager.get()); + + Tensor recurrent_to_input_weights_tensor = makeInputTensor( + recurrent_to_input_weights_shape, recurrent_to_input_weights, _memory_manager.get()); + Tensor recurrent_to_cell_weights_tensor = makeInputTensor( + recurrent_to_cell_weights_shape, recurrent_to_cell_weights, _memory_manager.get()); + Tensor recurrent_to_forget_weights_tensor = makeInputTensor( + recurrent_to_forget_weights_shape, recurrent_to_forget_weights, _memory_manager.get()); + Tensor recurrent_to_output_weights_tensor = makeInputTensor( + recurrent_to_output_weights_shape, recurrent_to_output_weights, _memory_manager.get()); + + std::vector input_data{0.03653763}; + Shape input_shape{n_batch, sequence_length, n_input}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + Shape output_state_shape{n_batch, n_output}; + Tensor output_state_tensor = makeOutputTensor(DataType::FLOAT32); + output_state_tensor.resize(output_state_shape); + + Shape cell_state_shape{n_batch, n_cell}; + Tensor cell_state_tensor = makeOutputTensor(DataType::FLOAT32); + cell_state_tensor.resize(cell_state_shape); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 10.0; + params.proj_clip = 0.0; + params.time_major = false; + params.asymmetric_quantize_inputs = false; + + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_forget_weights_tensor, + &input_to_cell_weights_tensor, &input_to_output_weights_tensor, + &recurrent_to_input_weights_tensor, &recurrent_to_forget_weights_tensor, + &recurrent_to_cell_weights_tensor, &recurrent_to_output_weights_tensor, nullptr, nullptr, + nullptr, &input_gate_bias_tensor, &forget_gate_bias_tensor, &cell_gate_bias_tensor, + &output_gate_bias_tensor, nullptr, nullptr, &output_state_tensor, &cell_state_tensor, nullptr, + nullptr, nullptr, nullptr, &output_tensor, &output_state_tensor, &cell_state_tensor, + &scratchpad_1, params); + + kernel.configure(); + _memory_manager->allocate_memory(output_tensor); + _memory_manager->allocate_memory(output_state_tensor); + _memory_manager->allocate_memory(cell_state_tensor); + _memory_manager->allocate_memory(scratchpad_1); + kernel.execute(); + + std::vector ref_output_data{0.00139296}; + std::vector ref_output_shape{n_batch, sequence_length, n_output}; + const float tolerance = 1e-5; + auto aa = extractTensorData(output_tensor); + EXPECT_THAT(extractTensorData(output_tensor), FloatArrayNear(ref_output_data, tolerance)); + EXPECT_THAT(extractTensorShape(output_tensor), ::testing::ElementsAreArray(ref_output_shape)); +} + +TEST_F(UnidirectionalSequenceLSTMTest, Unsupported_Type_Configure_NEG) +{ + const int32_t n_batch = 1; + const int32_t n_input = 2; + const int32_t n_cell = 4; + const int32_t n_output = 4; + const int32_t sequence_length = 3; + + std::vector input_data{2, 3, 3, 4, 1, 1}; // int8 is not support as of now + Shape input_shape{sequence_length, n_batch, n_input}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + std::vector input_to_input_weights = {-0.45018822, -0.02338299, -0.0870589, -0.34550029, + 0.04266912, -0.15680569, -0.34856534, 0.43890524}; + Shape input_to_input_weights_shape{n_cell, n_input}; + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_2(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_3(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 0.0; + params.proj_clip = 0.0; + params.time_major = true; + params.asymmetric_quantize_inputs = false; + + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + nullptr, nullptr, nullptr, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, nullptr, + nullptr, &output_tensor, &scratchpad_1, &scratchpad_2, &scratchpad_3, params); + + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(UnidirectionalSequenceLSTMTest, Invalid_Input_Shape_NEG) +{ + const int32_t n_batch = 1; + const int32_t n_input = 2; + const int32_t n_cell = 4; + const int32_t n_output = 4; + const int32_t sequence_length = 3; + + std::vector input_data{2., 3., 3., 4., 1., 1.}; + Shape input_shape{sequence_length, n_input}; // this is wrong + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + std::vector input_to_input_weights = {-0.45018822, -0.02338299, -0.0870589, -0.34550029, + 0.04266912, -0.15680569, -0.34856534, 0.43890524}; + Shape input_to_input_weights_shape{n_cell, n_input}; + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_2(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_3(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 0.0; + params.proj_clip = 0.0; + params.time_major = true; + params.asymmetric_quantize_inputs = false; + + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + nullptr, nullptr, nullptr, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, nullptr, + nullptr, &output_tensor, &scratchpad_1, &scratchpad_2, &scratchpad_3, params); + + EXPECT_ANY_THROW(kernel.configure()); +} + +TEST_F(UnidirectionalSequenceLSTMTest, Invalid_Input_Shape_2_NEG) +{ + const int32_t n_batch = 1; + const int32_t n_input = 2; + const int32_t n_cell = 4; + const int32_t n_output = 4; + const int32_t sequence_length = 3; + + std::vector input_data{2., 3., 3., 4., 1., 1.}; + Shape input_shape{sequence_length, n_batch, n_input}; + Tensor input_tensor = + makeInputTensor(input_shape, input_data, _memory_manager.get()); + + std::vector input_to_input_weights = {-0.45018822, -0.02338299, -0.0870589, -0.34550029, + 0.04266912, -0.15680569, -0.34856534, 0.43890524}; + Shape input_to_input_weights_shape{n_cell, n_input}; + Tensor input_to_input_weights_tensor = makeInputTensor( + input_to_input_weights_shape, input_to_input_weights, _memory_manager.get()); + + Tensor output_tensor = makeOutputTensor(DataType::FLOAT32); + Tensor scratchpad_1(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_2(DataType::FLOAT32, Shape({}), {}, ""); + Tensor scratchpad_3(DataType::FLOAT32, Shape({}), {}, ""); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = Activation::TANH; + params.cell_clip = 0.0; + params.proj_clip = 0.0; + params.time_major = true; + params.asymmetric_quantize_inputs = false; + + // NOTE provide wrong shaped inputs + UnidirectionalSequenceLSTM kernel( + &input_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + nullptr, nullptr, nullptr, &input_to_input_weights_tensor, &input_to_input_weights_tensor, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, + &input_to_input_weights_tensor, &input_to_input_weights_tensor, nullptr, nullptr, nullptr, + nullptr, &output_tensor, &scratchpad_1, &scratchpad_2, &scratchpad_3, params); + + EXPECT_ANY_THROW(kernel.configure()); +} + +} // namespace +} // namespace kernels +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/Unpack.cpp b/onert-micro/luci-interpreter/src/kernels/Unpack.cpp new file mode 100644 index 0000000..80f4d1f --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Unpack.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Unpack.h" + +#include "kernels/Utils.h" + +#include + +namespace luci_interpreter +{ + +namespace kernels +{ + +Unpack::Unpack(const Tensor *input, std::vector outputs, const UnpackParams ¶ms) + : KernelWithParams({input}, std::move(outputs), params) +{ +} + +void Unpack::configure() +{ + const Shape &input_shape = input()->shape(); + + int axis = _params.axis; + if (axis < 0) + axis += input()->shape().num_dims(); + assert(axis >= 0 && axis < input_shape.num_dims()); + + Shape output_shape(input_shape.num_dims() - 1); + int out_index = 0; + for (int in_index = 0; in_index < input_shape.num_dims(); ++in_index) + { + if (in_index != axis) + output_shape.dim(out_index++) = input_shape.dim(in_index); + } + + // TODO: enable it only if kernel with dynamic shapes + for (Tensor *output : _outputs) + { + assert(output->element_type() == input()->element_type()); + output->resize(output_shape); + } +} + +template void Unpack::executeImpl() const +{ + tflite::UnpackParams params{}; + params.axis = _params.axis; + params.num_split = _outputs.size(); + VectorOfTensors all_outputs(_outputs); + tflite::reference_ops::Unpack(params, getTensorShape(input()), getTensorData(input()), + **all_outputs.shapes(), all_outputs.data()); +} + +void Unpack::execute() const +{ + switch (input()->element_type()) + { + case DataType::FLOAT32: + return executeImpl(); + case DataType::U8: + return executeImpl(); + default: + assert(false && "Unsupported type."); + } +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Unpack.h b/onert-micro/luci-interpreter/src/kernels/Unpack.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Unpack.h rename to onert-micro/luci-interpreter/src/kernels/Unpack.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/Unpack.test.cpp b/onert-micro/luci-interpreter/src/kernels/Unpack.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/Unpack.test.cpp rename to onert-micro/luci-interpreter/src/kernels/Unpack.test.cpp diff --git a/onert-micro/luci-interpreter/src/kernels/Utils.cpp b/onert-micro/luci-interpreter/src/kernels/Utils.cpp new file mode 100644 index 0000000..0810b82 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Utils.cpp @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernels/Utils.h" + +#include +#include +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +template +void calculateActivationRange(Activation activation, T *activation_min, T *activation_max) +{ + switch (activation) + { + case Activation::NONE: + *activation_min = std::numeric_limits::lowest(); + *activation_max = std::numeric_limits::max(); + break; + case Activation::RELU: + *activation_min = 0; + *activation_max = std::numeric_limits::max(); + break; + case Activation::RELU_N1_TO_1: + *activation_min = -1; + *activation_max = 1; + break; + case Activation::RELU6: + *activation_min = 0; + *activation_max = 6; + break; + default: + assert(false && "Unsupported activation."); + } +} + +void matrixScalarMultiplyAccumulate(const int8_t *matrix, int32_t scalar, int32_t n_row, + int32_t n_col, int32_t *output) +{ + for (int i = 0; i < n_row; ++i) + { + int32_t row_sum = 0; + for (int j = 0; j < n_col; ++j) + { + row_sum += *matrix++; + } + output[i] += row_sum * scalar; + } +} + +template void calculateActivationRange(Activation activation, float *activation_min, + float *activation_max); +template void calculateActivationRange(Activation activation, int32_t *activation_min, + int32_t *activation_max); +template void calculateActivationRange(Activation activation, int64_t *activation_min, + int64_t *activation_max); + +#ifndef DIS_QUANT + +static void calculateActivationRangeQuantizedImpl(Activation activation, int32_t qmin, int32_t qmax, + int32_t zero_point, float scale, + int32_t *activation_min, int32_t *activation_max) +{ + auto quantize = [scale, zero_point](float x) { + return zero_point + static_cast(std::round(x / scale)); + }; + + switch (activation) + { + case Activation::NONE: + case Activation::TANH: + *activation_min = qmin; + *activation_max = qmax; + break; + case Activation::RELU: + *activation_min = std::max(qmin, quantize(0.0f)); + *activation_max = qmax; + break; + case Activation::RELU_N1_TO_1: + *activation_min = std::max(qmin, quantize(-1.0f)); + *activation_max = std::min(qmax, quantize(1.0f)); + break; + case Activation::RELU6: + *activation_min = std::max(qmin, quantize(0.0f)); + *activation_max = std::min(qmax, quantize(6.0f)); + break; + default: + assert(false && "Unsupported activation."); + } +} + +static void calculateActivationRangeQuantizedImpl(Activation activation, int32_t qmin, int32_t qmax, + const circle::Tensor *output, + int32_t *activation_min, int32_t *activation_max) +{ + const float scale = Tensor::scale(output); + const int32_t zero_point = Tensor::zero_point(output); + + calculateActivationRangeQuantizedImpl(activation, qmin, qmax, zero_point, zero_point, + activation_min, activation_max); +} + +void calculateActivationRangeQuantized(Activation activation, int32_t output_zero_point, + float output_scale, DataType data_type, + int32_t *activation_min, int32_t *activation_max) +{ + int32_t qmin{}; + int32_t qmax{}; + switch (data_type) + { + case DataType::U8: + qmin = 0; + qmax = std::numeric_limits::max(); + break; + case DataType::S8: + qmin = -std::numeric_limits::max(); + qmax = std::numeric_limits::max(); + break; + case DataType::S16: + // For now, assume that signed int16 type implies signed symmetric quantization. + assert(output_zero_point == 0); + qmin = -std::numeric_limits::max(); + qmax = std::numeric_limits::max(); + break; + default: + assert(false && "Unsupported type."); + } + + calculateActivationRangeQuantizedImpl(activation, qmin, qmax, output_zero_point, output_scale, + activation_min, activation_max); +} + +void calculateActivationRangeQuantized(Activation activation, const circle::Tensor *output, + int32_t *activation_min, int32_t *activation_max) +{ + assert(Tensor::zero_points(output).size() == 1); + const float scale = Tensor::scale(output); + const int32_t zero_point = Tensor::zero_point(output); + calculateActivationRangeQuantized(activation, zero_point, scale, Tensor::element_type(output), + activation_min, activation_max); +} + +void quantizeMultiplier(double double_multiplier, int32_t *quantized_multiplier, int *shift) +{ + if (double_multiplier == 0.0) + { + *quantized_multiplier = 0; + *shift = 0; + return; + } + + const double q = std::frexp(double_multiplier, shift); + auto q_fixed = static_cast(std::round(q * (int64_t(1) << 31))); + + if (q_fixed == (int64_t(1) << 31)) + { + q_fixed /= 2; + ++*shift; + } + assert(q_fixed <= std::numeric_limits::max()); + // A shift amount smaller than -31 would cause all bits to be shifted out + // and thus all results would be zero. We implement that instead with + // q_fixed==0, so as to avoid hitting issues with right-shift + // operations with shift amounts greater than 31. Note that this happens + // roughly when abs(double_multiplier) < 2^-31 and the present handling means + // that we're effectively flushing tiny double_multiplier's to zero. + // We could conceivably handle values in the range (roughly) [32, 63] + // as 'denormals' i.e. (shift==0, q_fixed < 2^30). In that point of view + // the present handling is just doing 'flush denormals to zero'. We could + // reconsider and actually generate nonzero denormals if a need arises. + if (*shift < -31) + { + *shift = 0; + q_fixed = 0; + } + *quantized_multiplier = static_cast(q_fixed); +} + +void quantizeMultiplierSmallerThanOneExp(double double_multiplier, int32_t *quantized_multiplier, + int *left_shift) +{ + assert(double_multiplier < 1.0); + assert(double_multiplier > 0.0); + int shift; + quantizeMultiplier(double_multiplier, quantized_multiplier, &shift); + assert(shift <= 0); + *left_shift = shift; +} +#endif + +tflite::RuntimeShape calculateShapeForBroadcast(const circle::Tensor *input1, + const circle::Tensor *input2) +{ + const int num_input1_dims = Tensor::num_dims(input1); + const int num_input2_dims = Tensor::num_dims(input2); + const int num_out_dims = std::max(num_input1_dims, num_input2_dims); + tflite::RuntimeShape output_shape(num_out_dims); + + for (int i = 0; i < num_out_dims; ++i) + { + const int32_t input1_dim = + i < num_input1_dims ? Tensor::dim(input1, num_input1_dims - i - 1) : 1; + const int32_t input2_dim = + i < num_input2_dims ? Tensor::dim(input2, num_input2_dims - i - 1) : 1; + + bool need_broadcast = input1_dim != input2_dim; + bool can_broadcast = input1_dim == 1 || input2_dim == 1; + LUCI_INTERPRETER_CHECK(!need_broadcast || can_broadcast); + + output_shape.SetDim(num_out_dims - i - 1, std::max(input1_dim, input2_dim)); + } + + return output_shape; +} + +} // namespace kernels +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/kernels/Utils.h b/onert-micro/luci-interpreter/src/kernels/Utils.h new file mode 100644 index 0000000..8d3cd69 --- /dev/null +++ b/onert-micro/luci-interpreter/src/kernels/Utils.h @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_KERNELS_UTILS_H +#define LUCI_INTERPRETER_KERNELS_UTILS_H + +#include "luci_interpreter/core/Tensor.h" + +#include +#include +#include + +namespace luci_interpreter +{ +namespace kernels +{ + +using Activation = luci_interpreter::FusedActFunc; + +#define LUCI_INTERPRETER_CHECK(cond) \ + if (!(cond)) \ + { \ + assert(false && "LUCI_INTERPRETER_CHECK fails"); \ + } + +inline int32_t computePadding(int32_t stride, int32_t dilation_rate, int32_t in_size, + int32_t filter_size, int32_t out_size) +{ + const int32_t effective_filter_size = (filter_size - 1) * dilation_rate + 1; + const int32_t padding = ((out_size - 1) * stride + effective_filter_size - in_size) / 2; + return padding > 0 ? padding : 0; +} + +inline int32_t computePaddingWithOffset(int32_t stride, int32_t dilation_rate, int32_t in_size, + int32_t filter_size, int32_t out_size, int32_t *offset) +{ + int32_t effective_filter_size = (filter_size - 1) * dilation_rate + 1; + int32_t total_padding = ((out_size - 1) * stride + effective_filter_size - in_size); + total_padding = total_padding > 0 ? total_padding : 0; + *offset = total_padding % 2; + return total_padding / 2; +} + +inline int32_t computeOutputSize(Padding padding, int32_t image_size, int32_t filter_size, + int32_t stride, int32_t dilation_rate = 1) +{ + const int32_t effective_filter_size = (filter_size - 1) * dilation_rate + 1; + switch (padding) + { + case Padding::SAME: + return (image_size + stride - 1) / stride; + case Padding::VALID: + return (image_size + stride - effective_filter_size) / stride; + default: + assert(false); + return 0; + } +} + +inline int32_t calcOffset(const circle::Tensor *tensor, int32_t d0, int32_t d1, int32_t d2, + int32_t d3) +{ + + return ((d0 * Tensor::dim(tensor, 1) + d1) * Tensor::dim(tensor, 2) + d2) * + Tensor::dim(tensor, 3) + + d3; +} + +template +void calculateActivationRange(Activation activation, T *activation_min, T *activation_max); + +tflite::RuntimeShape calculateShapeForBroadcast(const circle::Tensor *input1, + const circle::Tensor *input2); + +// Helper wrapper to hide broadcast logic +template class BroadcastableWrapper +{ +public: + BroadcastableWrapper(const std::vector &v) : _v(v), _stride(v.size() == 1 ? 0 : 1) {} + + T operator[](int idx) { return _v[idx * _stride]; } + +private: + const std::vector &_v; + int _stride; +}; + +inline tflite::RuntimeShape getTensorShape(const circle::Tensor *tensor) +{ + if (tensor == nullptr) + return tflite::RuntimeShape(); + + tflite::RuntimeShape runtime_shape(Tensor::num_dims(tensor)); + for (int i = 0; i < Tensor::num_dims(tensor); ++i) + { + runtime_shape.SetDim(i, Tensor::dim(tensor, i)); + } + return runtime_shape; +} + +template const T *getTensorData(const uint8_t *tensor_data) +{ + return tensor_data != nullptr ? reinterpret_cast(tensor_data) : nullptr; +} + +template T *getTensorData(uint8_t *tensor_data) +{ + return tensor_data != nullptr ? reinterpret_cast(tensor_data) : nullptr; +} + +// A list of tensors in a format that can be used by kernels like split and +// concatenation. +template class VectorOfTensors +{ +public: + using ElementT = typename std::conditional::type; + using TensorT = typename std::conditional::type; + + // Build with the tensors in 'tensor_list'. + explicit VectorOfTensors(const std::vector &tensor_list) + { + const int num_tensors = tensor_list.size(); + + all_data_.reserve(num_tensors); + all_shape_.reserve(num_tensors); + all_shape_ptr_.reserve(num_tensors); + + for (TensorT *tensor : tensor_list) + { + all_data_.push_back(getTensorData(tensor)); + all_shape_.push_back(getTensorShape(tensor)); + } + + // Taking the pointer from inside a std::vector is only OK if the vector is + // never modified, so we populate all_shape in the previous loop and then we + // are free to grab iterators here. + for (tflite::RuntimeShape &shape : all_shape_) + { + all_shape_ptr_.push_back(&shape); + } + } + // Return a pointer to the data pointers of all tensors in the list. For + // example: + // float* const* f = v.data(); + // f[0][1] is the second element of the first tensor. + ElementT *const *data() const { return all_data_.data(); } + + // Return a pointer the shape pointers of all tensors in the list. For + // example: + // const RuntimeShape* const* d = v.dims(); + // dims[1] are the dimensions of the second tensor in the list. + const tflite::RuntimeShape *const *shapes() const { return all_shape_ptr_.data(); } + +private: + std::vector all_data_; + std::vector all_shape_; + std::vector all_shape_ptr_; +}; + +#ifndef DIS_QUANT +void calculateActivationRangeQuantized(Activation activation, const circle::Tensor *output, + int32_t *activation_min, int32_t *activation_max); +void calculateActivationRangeQuantized(Activation activation, int32_t output_zero_point, + float output_scale, DataType data_type, + int32_t *activation_min, int32_t *activation_max); + +template constexpr bool one_of_types() { return false; } + +// Checks if T is equal to one of {U,Other} types +template constexpr bool one_of_types() +{ + return std::is_same::value || one_of_types(); +} + +void matrixScalarMultiplyAccumulate(const int8_t *matrix, int32_t scalar, int32_t n_row, + int32_t n_col, int32_t *output); + +/** + * Fills activation min and max parameters depending on given data type and activation + * + * T is a template parameter, so after optimization this code left with only required if case + * + * @tparam T data type of arithmetic operation output tensor + * @param params tflite params to fill + * @param activation luci_interpreter::Activation of arithmetic operation + */ +template +void fillArithmeticActivationRange(tflite::ArithmeticParams &p, Activation act) +{ + static_assert(one_of_types(), "Unsupported dtype"); + + if (std::is_same::value) + calculateActivationRange(act, &p.float_activation_min, &p.float_activation_max); + if (std::is_same::value) + calculateActivationRange(act, &p.quantized_activation_min, &p.quantized_activation_max); + else + calculateActivationRange(act, &p.int64_activation_min, &p.int64_activation_max); +} + +// Decompose a double multiplier into a Q0.31 int32 representation of its +// significand, and shift representation of its exponent. +// +// Handles an arbitrary positive multiplier. The 'shift' output-value is +// basically the 'floating-point exponent' of the multiplier: +// Negative for a right-shift (when the multiplier is <1), positive for a +// left-shift (when the multiplier is >1) +void quantizeMultiplier(double double_multiplier, int32_t *quantized_multiplier, int *shift); + +// Decompose a double multiplier into a Q0.31 int32 representation of its +// significand, and shift representation of NEGATIVE its exponent --- +// this is intended as a RIGHT-shift. +// +// Restricted to the case where the multiplier < 1 (and non-negative). +void quantizeMultiplierSmallerThanOneExp(double double_multiplier, int32_t *quantized_multiplier, + int *left_shift); + +inline double getQuantizedConvolutionMultipler(float input_scale, float filter_scale, + float output_scale) +{ + const double input_product_scale = static_cast(input_scale * filter_scale); + LUCI_INTERPRETER_CHECK(input_product_scale >= 0); + return input_product_scale / static_cast(output_scale); +} + +// TODO rename getQuantizedConvolutionMultiplers to something more general +// it is used for non conv operators too +inline std::vector getQuantizedConvolutionMultiplers(float input_scale, + const std::vector &filter_scale, + float output_scale) +{ + std::vector effective_output_scales; + size_t n = filter_scale.size(); + effective_output_scales.reserve(n); + for (size_t i = 0; i < n; ++i) + { + effective_output_scales.push_back( + getQuantizedConvolutionMultipler(input_scale, filter_scale[i], output_scale)); + } + return effective_output_scales; +} + +struct ChannelQuantMultipliers +{ + int shift; + int32_t multiplier; + ChannelQuantMultipliers() = default; +}; + +inline std::vector +quantizeMultipliers(const std::vector &effective_scale) +{ + size_t n = effective_scale.size(); + std::vector params(n); + for (size_t i = 0; i < n; ++i) + { + quantizeMultiplier(effective_scale[i], ¶ms[i].multiplier, ¶ms[i].shift); + } + return params; +} + +// A list of quantized tensors in a format that can be used by kernels like +// split and concatenation. +template class VectorOfQuantizedTensors : public VectorOfTensors +{ +public: + using typename VectorOfTensors::TensorT; + + // Build with the tensors in 'tensor_list'. + explicit VectorOfQuantizedTensors(const std::vector &tensor_list) + : VectorOfTensors(tensor_list) + { + for (TensorT *tensor : tensor_list) + { + zero_point_.push_back(tensor->zero_point()); + scale_.push_back(tensor->scale()); + } + } + + const float *scale() const { return scale_.data(); } + const int32_t *zero_point() const { return zero_point_.data(); } + +private: + std::vector zero_point_; + std::vector scale_; +}; +#endif // DIS_QUANT + +} // namespace kernels +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_KERNELS_UTILS_H diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/While.cpp b/onert-micro/luci-interpreter/src/kernels/While.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/While.cpp rename to onert-micro/luci-interpreter/src/kernels/While.cpp diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/While.h b/onert-micro/luci-interpreter/src/kernels/While.h similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/While.h rename to onert-micro/luci-interpreter/src/kernels/While.h diff --git a/compiler/luci-micro/luci-interpreter/src/kernels/While.test.cpp b/onert-micro/luci-interpreter/src/kernels/While.test.cpp similarity index 100% rename from compiler/luci-micro/luci-interpreter/src/kernels/While.test.cpp rename to onert-micro/luci-interpreter/src/kernels/While.test.cpp diff --git a/onert-micro/luci-interpreter/src/loader/CMakeLists.txt b/onert-micro/luci-interpreter/src/loader/CMakeLists.txt new file mode 100644 index 0000000..0ef63e2 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SOURCES + GraphLoader.h + GraphLoader.cpp + ModuleLoader.h + ModuleLoader.cpp) + +add_library(${LUCI_INTERPRETER_LOADER} STATIC ${SOURCES}) +if (NOT NNCC_LIBRARY_NO_PIC) + set_target_properties(${LUCI_INTERPRETER_LOADER} PROPERTIES POSITION_INDEPENDENT_CODE ON) +endif(NOT NNCC_LIBRARY_NO_PIC) +target_include_directories(${LUCI_INTERPRETER_LOADER} PUBLIC "${LUCI_INTERPRETER_SOURCE_DIR}") + +target_link_libraries(${LUCI_INTERPRETER_LOADER} + PUBLIC ${LUCI_INTERPRETER_MEMORY_MANAGER} ${LUCI_INTERPRETER_CORE} + PRIVATE ${LUCI_INTERPRETER_KERNELS}) diff --git a/onert-micro/luci-interpreter/src/loader/GraphLoader.cpp b/onert-micro/luci-interpreter/src/loader/GraphLoader.cpp new file mode 100644 index 0000000..cb9dd7f --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/GraphLoader.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "loader/GraphLoader.h" + +namespace luci_interpreter +{ +namespace +{ + +// TODO: add more operations +bool isCouldBeEmplaceOperation(circle::BuiltinOperator op) +{ + switch (op) + { + case circle::BuiltinOperator_LOGISTIC: + case circle::BuiltinOperator_RESHAPE: + case circle::BuiltinOperator_EXPAND_DIMS: + return true; + default: + return false; + } +} + +bool isCouldBeEmplaceTensor(CircleReader *reader, const int32_t tensor_index) +{ + uint32_t usage_count = 0; + for (uint32_t i = 0; i < reader->operators().size(); ++i) + { + const auto op = reader->operators().at(i); + assert(op != nullptr); + + for (int32_t j = 0; j < op->inputs()->size(); ++j) + { + const auto input_index = op->inputs()->operator[](j); + if (input_index == tensor_index) + usage_count++; + + if (usage_count > 1) + return false; + } + } + return true; +} + +} // namespace + +void GraphLoader::checkInplaceOps(CircleReader *reader, RuntimeGraph *runtime_graph) +{ + for (uint32_t i = 0; i < reader->operators().size(); ++i) + { + const auto *op = reader->operators().at(i); + assert(op != nullptr); + + bool is_graph_input = false; + for (int32_t j = 0; j < op->inputs()->size(); ++j) + { + const auto input_index = op->inputs()->operator[](j); + if (input_index == -1) + continue; + + const auto &inputs_indexes = reader->inputs(); + + is_graph_input = (std::find(inputs_indexes.begin(), inputs_indexes.end(), input_index) != + inputs_indexes.end()) or + is_graph_input; + + if (not is_graph_input and isCouldBeEmplaceOperation(reader->builtin_code(op)) and + op->outputs()->size() == 1 and isCouldBeEmplaceTensor(reader, input_index)) + { + runtime_graph->addInplaceOpIndex(i); + } + } + } +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/GraphLoader.h b/onert-micro/luci-interpreter/src/loader/GraphLoader.h new file mode 100644 index 0000000..3265f11 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/GraphLoader.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_LOADER_GRAPHLOADER_H +#define LUCI_INTERPRETER_LOADER_GRAPHLOADER_H + +#include "core/RuntimeGraph.h" +#include "luci_interpreter/core/reader/CircleMicroReader.h" + +#include + +namespace luci_interpreter +{ + +class GraphLoader +{ +public: + static void checkInplaceOps(CircleReader *reader, RuntimeGraph *runtime_graph); +}; + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_LOADER_GRAPHLOADER_H diff --git a/onert-micro/luci-interpreter/src/loader/ModuleLoader.cpp b/onert-micro/luci-interpreter/src/loader/ModuleLoader.cpp new file mode 100644 index 0000000..0bb38c6 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/ModuleLoader.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModuleLoader.h" + +#include "GraphLoader.h" + +namespace luci_interpreter +{ + +void ModuleLoader::load(RuntimeModule *runtime_module, SimpleMemoryManager *memory_manager, + const char *model_data_raw) +{ + const circle::Model *model = circle::GetModel(model_data_raw); + + CircleReader &reader = runtime_module->getCircleReader(); + if (!reader.parse(model)) + assert(false && "Error during parse"); + + for (size_t i = 0; i < reader.num_subgraph(); ++i) + { + runtime_module->addGraph(memory_manager); + } + +#ifndef USE_STATIC_ALLOC + for (size_t i = 0; i < reader.num_subgraph(); ++i) + { + if (!reader.select_subgraph(i)) + assert(false && "Error during select subgraph"); + auto *runtime_graph = runtime_module->getRuntimeGraphAt(i); + // For Dynamic memory manager we can use inplace optimization + GraphLoader::checkInplaceOps(&reader, runtime_graph); + } +#endif // USE_STATIC_ALLOC + + // For Dynamic Memory manager we build memory allocate/deallocate plan and then configure kernels. + // For Static Memory manager we only configure kernels. + for (size_t i = 0; i < reader.num_subgraph(); ++i) + { + auto *runtime_graph = runtime_module->getRuntimeGraphAt(i); +#ifdef USE_STATIC_ALLOC + runtime_graph->configure_kernels(); +#else + runtime_graph->configure(); +#endif // USE_STATIC_ALLOC + } +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/ModuleLoader.h b/onert-micro/luci-interpreter/src/loader/ModuleLoader.h new file mode 100644 index 0000000..c35c72d --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/ModuleLoader.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LUCI_INTERPRETER_LOADER_MODULELOADER_H +#define LUCI_INTERPRETER_LOADER_MODULELOADER_H + +#include "core/RuntimeModule.h" +#include "luci_interpreter/core/reader/CircleMicroReader.h" + +#include + +namespace luci_interpreter +{ + +class ModuleLoader +{ +public: + static void load(RuntimeModule *runtime_module, MemoryManager *memory_manager, + const char *model_data_raw); +}; + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_LOADER_MODULELOADER_H diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Add.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Add.cpp new file mode 100644 index 0000000..2a2140b --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Add.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Add.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleAdd(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input1 = inputs.at(0); + const Tensor *input2 = inputs.at(1); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsAddOptions(); + + AddParams params{}; + params.activation = luci_actfunc(options->fused_activation_function); + + return std::make_unique(input1, input2, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/ArgMax.cpp b/onert-micro/luci-interpreter/src/loader/nodes/ArgMax.cpp new file mode 100644 index 0000000..0da72da --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/ArgMax.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/ArgMax.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleArgMax(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input = inputs.at(0); + const Tensor *axis = inputs.at(1); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsArgMaxOptions(); + + ArgMaxParams params{}; + params.output_type = static_cast(options->output_type); + + return std::make_unique(input, axis, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/AveragePool2D.cpp b/onert-micro/luci-interpreter/src/loader/nodes/AveragePool2D.cpp new file mode 100644 index 0000000..9a5af1c --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/AveragePool2D.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/AveragePool2D.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleAveragePool2D(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsPool2DOptions(); + + Pool2DParams params{}; + params.padding = luci_padding(options->padding); + params.filter_height = options->filter_height; + params.filter_width = options->filter_width; + params.stride_height = options->stride_h; + params.stride_width = options->stride_w; + params.activation = luci_actfunc(options->fused_activation_function); + + // It is unknown what data will be stored in scratchpad tensor, + // using UINT8 as a most general option + auto scratchpad = std::make_unique(DataType::U8, Shape({}), nullptr); + scratchpad->set_data_buffer(nullptr); + // TODO move tensors offset initialization to one place + // TODO handle with static manager + Tensor *tmp = builder.get_runtime_graph()->addTensor(std::move(scratchpad)); + + return std::make_unique(input, output, tmp, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/BatchMatMul.cpp b/onert-micro/luci-interpreter/src/loader/nodes/BatchMatMul.cpp new file mode 100644 index 0000000..7799331 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/BatchMatMul.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/BatchMatMul.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleBatchMatMul(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *lhs = inputs.at(0); + const Tensor *rhs = inputs.at(1); + Tensor *output = outputs.at(0); + + auto lhs_scratchpad = std::make_unique(lhs->element_type(), Shape({}), nullptr); + lhs_scratchpad->set_data_buffer(nullptr); + auto rhs_scratchpad = std::make_unique(rhs->element_type(), Shape({}), nullptr); + rhs_scratchpad->set_data_buffer(nullptr); + // TODO move tensors offset initialization to one place + // TODO handle with StaticManager + Tensor *lhs_tmp = builder.get_runtime_graph()->addTensor(std::move(lhs_scratchpad)); + Tensor *rhs_tmp = builder.get_runtime_graph()->addTensor(std::move(rhs_scratchpad)); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsBatchMatMulOptions(); + + BatchMatMulParams params; + params.adj_x = options->adjoint_lhs; + params.adj_y = options->adjoint_rhs; + + return std::make_unique(lhs, rhs, output, lhs_tmp, rhs_tmp, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/BatchToSpaceND.cpp b/onert-micro/luci-interpreter/src/loader/nodes/BatchToSpaceND.cpp new file mode 100644 index 0000000..424844a --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/BatchToSpaceND.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/BatchToSpaceND.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleBatchToSpaceND(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 3); + + const Tensor *input = inputs.at(0); + const Tensor *block_shape = inputs.at(1); + const Tensor *crops = inputs.at(2); + Tensor *output = outputs.at(0); + + return std::make_unique(input, block_shape, crops, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Cast.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Cast.cpp new file mode 100644 index 0000000..441dacb --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Cast.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Cast.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleCast(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Concatenation.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Concatenation.cpp new file mode 100644 index 0000000..e2b847a --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Concatenation.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Concatenation.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleConcatenation(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + std::vector input_tensors(inputs.size()); + for (uint32_t i = 0; i < inputs.size(); ++i) + { + input_tensors[i] = inputs.at(i); + } + Tensor *output = outputs.at(0); + ; + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsConcatenationOptions(); + + ConcatenationParams params{}; + params.axis = options->axis; + params.activation = luci_actfunc(options->fused_activation_function); + + return std::make_unique(std::move(input_tensors), output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Conv2D.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Conv2D.cpp new file mode 100644 index 0000000..1750e8a --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Conv2D.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Conv2D.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleConv2D(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 3); + + const Tensor *input = inputs.at(0); + const Tensor *filter = inputs.at(1); + const Tensor *bias = inputs.at(2); + Tensor *output = outputs.at(0); + + // It is unknown what data will be stored in scratchpad tensor, + // using UINT8 as a most general option + auto scratchpad = std::make_unique(DataType::U8, Shape({}), nullptr); + scratchpad->set_data_buffer(nullptr); + // TODO move tensors offset initialization to one place + // TODO handle with StaticManager + Tensor *tmp = builder.get_runtime_graph()->addTensor(std::move(scratchpad)); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsConv2DOptions(); + + Conv2DParams params{}; + params.padding = luci_padding(options->padding); + params.stride_height = options->stride_h; + params.stride_width = options->stride_w; + params.dilation_height_factor = options->dilation_h_factor; + params.dilation_width_factor = options->dilation_w_factor; + params.activation = luci_actfunc(options->fused_activation_function); + + return std::make_unique(input, filter, bias, output, tmp, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/DepthToSpace.cpp b/onert-micro/luci-interpreter/src/loader/nodes/DepthToSpace.cpp new file mode 100644 index 0000000..ebab0cc --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/DepthToSpace.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/DepthToSpace.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleDepthToSpace(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsDepthToSpaceOptions(); + + DepthToSpaceParams params{}; + params.block_size = options->block_size; + + return std::make_unique(input, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/DepthwiseConv2D.cpp b/onert-micro/luci-interpreter/src/loader/nodes/DepthwiseConv2D.cpp new file mode 100644 index 0000000..cebad55 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/DepthwiseConv2D.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/DepthwiseConv2D.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleDepthwiseConv2D(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 3); + + const Tensor *input = inputs.at(0); + const Tensor *filter = inputs.at(1); + const Tensor *bias = inputs.at(2); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsDepthwiseConv2DOptions(); + + DepthwiseConv2DParams params{}; + params.padding = luci_padding(options->padding); + params.depth_multiplier = options->depth_multiplier; + params.stride_height = options->stride_h; + params.stride_width = options->stride_w; + params.dilation_height_factor = options->dilation_h_factor; + params.dilation_width_factor = options->dilation_w_factor; + params.activation = luci_actfunc(options->fused_activation_function); + + // It is unknown what data will be stored in scratchpad tensor, + // using UINT8 as a most general option + auto scratchpad = std::make_unique(DataType::U8, Shape({}), nullptr); + scratchpad->set_data_buffer(nullptr); + // TODO move tensors offset initialization to one place + // TODO handle with StaticManager + Tensor *tmp = builder.get_runtime_graph()->addTensor(std::move(scratchpad)); + + return std::make_unique(input, filter, bias, output, tmp, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Dequantize.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Dequantize.cpp new file mode 100644 index 0000000..06b7518 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Dequantize.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Dequantize.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleDequantize(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Div.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Div.cpp new file mode 100644 index 0000000..3331df9 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Div.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Div.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleDiv(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + const Tensor *input1 = inputs.at(0); + const Tensor *input2 = inputs.at(1); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsDivOptions(); + + DivParams params{}; + params.activation = luci_actfunc(options->fused_activation_function); + + return std::make_unique(input1, input2, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Elu.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Elu.cpp new file mode 100644 index 0000000..2d89328 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Elu.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Elu.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleElu(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Equal.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Equal.cpp new file mode 100644 index 0000000..eee5009 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Equal.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Equal.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleEqual(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) + +{ + assert(inputs.size() == 2); + + const Tensor *x = inputs.at(0); + const Tensor *y = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(x, y, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Exp.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Exp.cpp new file mode 100644 index 0000000..401b6d0 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Exp.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Exp.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleExp(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/ExpandDims.cpp b/onert-micro/luci-interpreter/src/loader/nodes/ExpandDims.cpp new file mode 100644 index 0000000..f4745b3 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/ExpandDims.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/ExpandDims.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleExpandDims(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + const Tensor *input = inputs.at(0); + const Tensor *axis = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(input, axis, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Fill.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Fill.cpp new file mode 100644 index 0000000..cd5a336 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Fill.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Fill.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleFill(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + const Tensor *dims = inputs.at(0); + const Tensor *value = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(dims, value, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Floor.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Floor.cpp new file mode 100644 index 0000000..59164ce --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Floor.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Floor.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleFloor(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/FloorDiv.cpp b/onert-micro/luci-interpreter/src/loader/nodes/FloorDiv.cpp new file mode 100644 index 0000000..7caf123 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/FloorDiv.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/FloorDiv.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleFloorDiv(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + const Tensor *x = inputs.at(0); + const Tensor *y = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(x, y, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/FullyConnected.cpp b/onert-micro/luci-interpreter/src/loader/nodes/FullyConnected.cpp new file mode 100644 index 0000000..cb35c0f --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/FullyConnected.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/FullyConnected.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleFullyConnected(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 3); + + const Tensor *input = inputs.at(0); + const Tensor *weights = inputs.at(1); + const Tensor *bias = inputs.at(2); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsFullyConnectedOptions(); + + FullyConnectedParams params{}; + params.activation = luci_actfunc(options->fused_activation_function); + params.keep_num_dims = options->keep_num_dims; + + return std::make_unique(input, weights, bias, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Gather.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Gather.cpp new file mode 100644 index 0000000..02cb7e6 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Gather.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Gather.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleGather(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + const Tensor *params = inputs.at(0); + const Tensor *indices = inputs.at(1); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsGatherOptions(); + + GatherParams gparams{}; + gparams.axis = options->axis; + // TODO support batch_dims + gparams.batch_dims = 0; + + return std::make_unique(params, indices, output, gparams); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Greater.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Greater.cpp new file mode 100644 index 0000000..160911e --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Greater.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Greater.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleGreater(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + const Tensor *x = inputs.at(0); + const Tensor *y = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(x, y, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/GreaterEqual.cpp b/onert-micro/luci-interpreter/src/loader/nodes/GreaterEqual.cpp new file mode 100644 index 0000000..4c19682 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/GreaterEqual.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/GreaterEqual.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleGreaterEqual(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + const Tensor *x = inputs.at(0); + const Tensor *y = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(x, y, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/If.cpp b/onert-micro/luci-interpreter/src/loader/nodes/If.cpp new file mode 100644 index 0000000..1c6f12c --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/If.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/If.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleIf(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + // TODO: support IF operation + assert(false && "Not supported now"); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/InstanceNorm.cpp b/onert-micro/luci-interpreter/src/loader/nodes/InstanceNorm.cpp new file mode 100644 index 0000000..1d7f639 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/InstanceNorm.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/InstanceNorm.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleInstanceNorm(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 3); + + const Tensor *input = inputs.at(0); + const Tensor *gamma = inputs.at(1); + const Tensor *beta = inputs.at(2); + + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsInstanceNormOptions(); + + InstanceNormParams params{}; + params.epsilon = options->epsilon; + params.activation = luci_actfunc(options->fused_activation_function); + + return std::make_unique(input, gamma, beta, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/L2Normalize.cpp b/onert-micro/luci-interpreter/src/loader/nodes/L2Normalize.cpp new file mode 100644 index 0000000..6435f09 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/L2Normalize.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/L2Normalize.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleL2Normalize(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsL2NormOptions(); + + L2NormParams params{}; + params.activation = luci_actfunc(options->fused_activation_function); + + return std::make_unique(input, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/L2Pool2D.cpp b/onert-micro/luci-interpreter/src/loader/nodes/L2Pool2D.cpp new file mode 100644 index 0000000..d297525 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/L2Pool2D.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/L2Pool2D.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleL2Pool2D(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsPool2DOptions(); + + Pool2DParams params{}; + params.padding = luci_padding(options->padding); + params.filter_height = options->filter_height; + params.filter_width = options->filter_width; + params.stride_height = options->stride_h; + params.stride_width = options->stride_w; + params.activation = luci_actfunc(options->fused_activation_function); + + return std::make_unique(input, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/LeakyRelu.cpp b/onert-micro/luci-interpreter/src/loader/nodes/LeakyRelu.cpp new file mode 100644 index 0000000..2cb27e2 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/LeakyRelu.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/LeakyRelu.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleLeakyRelu(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsLeakyReluOptions(); + + LeakyReluParams params{}; + params.alpha = options->alpha; + + return std::make_unique(input, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Less.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Less.cpp new file mode 100644 index 0000000..9de9dbe --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Less.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Less.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleLess(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *x = inputs.at(0); + const Tensor *y = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(x, y, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/LessEqual.cpp b/onert-micro/luci-interpreter/src/loader/nodes/LessEqual.cpp new file mode 100644 index 0000000..ca4f26c --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/LessEqual.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/LessEqual.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleLessEqual(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *x = inputs.at(0); + const Tensor *y = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(x, y, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/LocalResponseNormalization.cpp b/onert-micro/luci-interpreter/src/loader/nodes/LocalResponseNormalization.cpp new file mode 100644 index 0000000..71ed634 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/LocalResponseNormalization.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/LocalResponseNormalization.h" + +namespace luci_interpreter +{ + +std::unique_ptr +build_kernel_CircleLocalResponseNormalization(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsLocalResponseNormalizationOptions(); + + LocalResponseNormalizationParams params{}; + params.radius = options->radius; + params.bias = options->bias; + params.alpha = options->alpha; + params.beta = options->beta; + + return std::make_unique(input, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/LogSoftmax.cpp b/onert-micro/luci-interpreter/src/loader/nodes/LogSoftmax.cpp new file mode 100644 index 0000000..03e40e9 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/LogSoftmax.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/LogSoftmax.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleLogSoftmax(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/LogicalAnd.cpp b/onert-micro/luci-interpreter/src/loader/nodes/LogicalAnd.cpp new file mode 100644 index 0000000..32677a7 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/LogicalAnd.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/LogicalAnd.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleLogicalAnd(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input1 = inputs.at(0); + const Tensor *input2 = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(input1, input2, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/LogicalNot.cpp b/onert-micro/luci-interpreter/src/loader/nodes/LogicalNot.cpp new file mode 100644 index 0000000..43ec1e4 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/LogicalNot.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/LogicalNot.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleLogicalNot(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/LogicalOr.cpp b/onert-micro/luci-interpreter/src/loader/nodes/LogicalOr.cpp new file mode 100644 index 0000000..7ce29a8 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/LogicalOr.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/LogicalOr.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleLogicalOr(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input1 = inputs.at(0); + const Tensor *input2 = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(input1, input2, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Logistic.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Logistic.cpp new file mode 100644 index 0000000..113b5ea --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Logistic.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Logistic.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleLogistic(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/MaxPool2D.cpp b/onert-micro/luci-interpreter/src/loader/nodes/MaxPool2D.cpp new file mode 100644 index 0000000..40fff4d --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/MaxPool2D.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/MaxPool2D.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleMaxPool2D(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsPool2DOptions(); + + Pool2DParams params{}; + params.padding = luci_padding(options->padding); + params.filter_height = options->filter_height; + params.filter_width = options->filter_width; + params.stride_height = options->stride_h; + params.stride_width = options->stride_w; + params.activation = luci_actfunc(options->fused_activation_function); + + return std::make_unique(input, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Maximum.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Maximum.cpp new file mode 100644 index 0000000..1a7930a --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Maximum.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Maximum.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleMaximum(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input1 = inputs.at(0); + const Tensor *input2 = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(input1, input2, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Mean.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Mean.cpp new file mode 100644 index 0000000..6ef48bc --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Mean.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Mean.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleMean(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input = inputs.at(0); + const Tensor *axis = inputs.at(1); + Tensor *output = outputs.at(0); + + auto temp_index_unique = std::make_unique(DataType::S32, Shape({}), nullptr); + temp_index_unique->set_data_buffer(nullptr); + Tensor *temp_index = builder.get_runtime_graph()->addTensor(std::move(temp_index_unique)); + + auto resolved_axes_unique = std::make_unique(DataType::S32, Shape({}), nullptr); + resolved_axes_unique->set_data_buffer(nullptr); + Tensor *resolved_axes = builder.get_runtime_graph()->addTensor(std::move(resolved_axes_unique)); + + auto temp_sum_unique = std::make_unique(input->element_type(), Shape({}), nullptr); + temp_sum_unique->set_data_buffer(nullptr); + Tensor *temp_sum = builder.get_runtime_graph()->addTensor(std::move(temp_sum_unique)); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsReducerOptions(); + + ReducerParams params{}; + params.keep_dims = options->keep_dims; + + return std::make_unique(input, axis, output, temp_index, resolved_axes, temp_sum, + params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Minimum.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Minimum.cpp new file mode 100644 index 0000000..232ebf0 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Minimum.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Minimum.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleMinimum(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input1 = inputs.at(0); + const Tensor *input2 = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(input1, input2, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/MirrorPad.cpp b/onert-micro/luci-interpreter/src/loader/nodes/MirrorPad.cpp new file mode 100644 index 0000000..d96ad25 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/MirrorPad.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/MirrorPad.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleMirrorPad(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input = inputs.at(0); + const Tensor *paddings = inputs.at(1); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsMirrorPadOptions(); + + MirrorPadParams params{}; + params.mode = luci_mirrorpad_mode(options->mode); + + return std::make_unique(input, paddings, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Mul.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Mul.cpp new file mode 100644 index 0000000..283f042 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Mul.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Mul.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleMul(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input1 = inputs.at(0); + const Tensor *input2 = inputs.at(1); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsMulOptions(); + + MulParams params{}; + params.activation = luci_actfunc(options->fused_activation_function); + + return std::make_unique(input1, input2, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Neg.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Neg.cpp new file mode 100644 index 0000000..a898f41 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Neg.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Neg.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleNeg(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/NotEqual.cpp b/onert-micro/luci-interpreter/src/loader/nodes/NotEqual.cpp new file mode 100644 index 0000000..1c84931 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/NotEqual.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/NotEqual.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleNotEqual(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *x = inputs.at(0); + const Tensor *y = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(x, y, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/OneHot.cpp b/onert-micro/luci-interpreter/src/loader/nodes/OneHot.cpp new file mode 100644 index 0000000..a1c8a5b --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/OneHot.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/OneHot.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleOneHot(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 4); + + const Tensor *indices = inputs.at(0); + const Tensor *depth = inputs.at(1); + const Tensor *on_value = inputs.at(2); + const Tensor *off_value = inputs.at(3); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsOneHotOptions(); + + OneHotParams params{}; + params.axis = options->axis; + + return std::make_unique(indices, depth, on_value, off_value, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/PRelu.cpp b/onert-micro/luci-interpreter/src/loader/nodes/PRelu.cpp new file mode 100644 index 0000000..6adc549 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/PRelu.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/PRelu.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CirclePRelu(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input = inputs.at(0); + const Tensor *alpha = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(input, alpha, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Pack.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Pack.cpp new file mode 100644 index 0000000..a92196b --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Pack.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Pack.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CirclePack(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + std::vector input_tensors(inputs.size()); + for (uint32_t i = 0; i < inputs.size(); ++i) + { + input_tensors[i] = inputs.at(i); + } + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsPackOptions(); + + PackParams params{}; + params.axis = options->axis; + params.values_count = options->values_count; + + return std::make_unique(std::move(input_tensors), output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Pad.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Pad.cpp new file mode 100644 index 0000000..26aa7e7 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Pad.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Pad.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CirclePad(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input = inputs.at(0); + const Tensor *paddings = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(input, paddings, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/PadV2.cpp b/onert-micro/luci-interpreter/src/loader/nodes/PadV2.cpp new file mode 100644 index 0000000..829c47f --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/PadV2.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/PadV2.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CirclePadV2(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 3); + + const Tensor *input = inputs.at(0); + const Tensor *paddings = inputs.at(1); + const Tensor *constant_values = inputs.at(2); + Tensor *output = outputs.at(0); + + return std::make_unique(input, paddings, constant_values, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Pow.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Pow.cpp new file mode 100644 index 0000000..005c281 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Pow.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Pow.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CirclePow(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input1 = inputs.at(0); + const Tensor *input2 = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(input1, input2, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Quantize.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Quantize.cpp new file mode 100644 index 0000000..4fa7e66 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Quantize.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Quantize.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleQuantize(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Relu.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Relu.cpp new file mode 100644 index 0000000..27fcb6b --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Relu.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Relu.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleRelu(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Relu6.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Relu6.cpp new file mode 100644 index 0000000..68dba2c --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Relu6.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Relu6.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleRelu6(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Reshape.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Reshape.cpp new file mode 100644 index 0000000..45ebf0a --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Reshape.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Reshape.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleReshape(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input = inputs.at(0); + const Tensor *shape = inputs.at(1); + Tensor *output = outputs.at(0); + + // NOTE 'newShape' attribute is ignored. + return std::make_unique(input, shape, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/ResizeBilinear.cpp b/onert-micro/luci-interpreter/src/loader/nodes/ResizeBilinear.cpp new file mode 100644 index 0000000..bd82048 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/ResizeBilinear.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/ResizeBilinear.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleResizeBilinear(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input = inputs.at(0); + const Tensor *size = inputs.at(1); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsResizeBilinearOptions(); + + ResizeBilinearParams params{}; + params.align_corners = options->align_corners; + params.half_pixel_centers = options->half_pixel_centers; + + return std::make_unique(input, size, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/ResizeNearestNeighbor.cpp b/onert-micro/luci-interpreter/src/loader/nodes/ResizeNearestNeighbor.cpp new file mode 100644 index 0000000..bc59e87 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/ResizeNearestNeighbor.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/ResizeNearestNeighbor.h" + +namespace luci_interpreter +{ + +std::unique_ptr +build_kernel_CircleResizeNearestNeighbor(std::vector &&inputs, + std::vector &&outputs, const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input = inputs.at(0); + const Tensor *size = inputs.at(1); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsResizeNearestNeighborOptions(); + + ResizeNearestNeighborParams params{}; + params.align_corners = options->align_corners; + // TODO update half_pixel_centers after CircleResizeNearestNeighbor updated + // Current CircleResizeNearestNeighbor don't have half_pixel_centers. + // default value on current is false. + // it need to be updated when CircleResizeNearestNeighbor updated. + params.half_pixel_centers = false; + + return std::make_unique(input, size, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/ReverseV2.cpp b/onert-micro/luci-interpreter/src/loader/nodes/ReverseV2.cpp new file mode 100644 index 0000000..0b23ee0 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/ReverseV2.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/ReverseV2.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleReverseV2(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input = inputs.at(0); + const Tensor *axis = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(input, axis, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Rsqrt.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Rsqrt.cpp new file mode 100644 index 0000000..87ca438 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Rsqrt.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Rsqrt.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleRsqrt(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/SVDF.cpp b/onert-micro/luci-interpreter/src/loader/nodes/SVDF.cpp new file mode 100644 index 0000000..8a77459 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/SVDF.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/SVDF.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSVDF(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 5); + + const Tensor *input = inputs.at(0); + const Tensor *feature = inputs.at(1); + const Tensor *time = inputs.at(2); + const Tensor *bias = inputs.at(3); + const Tensor *input_activation_state = inputs.at(4); + Tensor *output = outputs.at(0); + + auto scratchpad_tensor = + std::make_unique(input_activation_state->element_type(), Shape({}), nullptr); + scratchpad_tensor->set_data_buffer(nullptr); + Tensor *tmp = builder.get_runtime_graph()->addTensor(std::move(scratchpad_tensor)); + + DataType data_type = input->element_type() == DataType::S8 ? DataType::S32 : DataType::FLOAT32; + + scratchpad_tensor = std::make_unique(data_type, Shape({}), nullptr); + scratchpad_tensor->set_data_buffer(nullptr); + Tensor *tmp_1 = builder.get_runtime_graph()->addTensor(std::move(scratchpad_tensor)); + + if (data_type == DataType::FLOAT32 && + (feature->element_type() == DataType::S8 || feature->element_type() == DataType::U8)) + { + data_type = feature->element_type(); + } + + scratchpad_tensor = std::make_unique(data_type, Shape({}), nullptr); + scratchpad_tensor->set_data_buffer(nullptr); + Tensor *tmp_2 = builder.get_runtime_graph()->addTensor(std::move(scratchpad_tensor)); + + data_type = DataType::FLOAT32; + + scratchpad_tensor = std::make_unique(data_type, Shape({}), nullptr); + scratchpad_tensor->set_data_buffer(nullptr); + Tensor *tmp_3 = builder.get_runtime_graph()->addTensor(std::move(scratchpad_tensor)); + + scratchpad_tensor = std::make_unique(data_type, Shape({}), nullptr); + scratchpad_tensor->set_data_buffer(nullptr); + Tensor *tmp_4 = builder.get_runtime_graph()->addTensor(std::move(scratchpad_tensor)); + + scratchpad_tensor = std::make_unique(data_type, Shape({}), nullptr); + scratchpad_tensor->set_data_buffer(nullptr); + Tensor *tmp_5 = builder.get_runtime_graph()->addTensor(std::move(scratchpad_tensor)); + + scratchpad_tensor = std::make_unique(data_type, Shape({}), nullptr); + scratchpad_tensor->set_data_buffer(nullptr); + Tensor *tmp_6 = builder.get_runtime_graph()->addTensor(std::move(scratchpad_tensor)); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsSVDFOptions(); + + SVDFParams params{}; + params.activation = luci_actfunc(options->fused_activation_function); + params.svdf_rank = options->rank; + params.asymmetric_quantize_inputs = options->asymmetric_quantize_inputs; + + return std::make_unique(input, feature, time, bias, input_activation_state, output, + tmp, tmp_1, tmp_2, tmp_3, tmp_4, tmp_5, tmp_6, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Shape.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Shape.cpp new file mode 100644 index 0000000..69a727f --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Shape.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Shape.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleShape(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsShapeOptions(); + + ShapeParams shape_params{}; + shape_params.out_type = luci_datatype(options->out_type); + + return std::make_unique(input, output, shape_params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Slice.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Slice.cpp new file mode 100644 index 0000000..e28742b --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Slice.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Slice.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSlice(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 3); + + const Tensor *input = inputs.at(0); + const Tensor *begin = inputs.at(1); + const Tensor *size = inputs.at(2); + Tensor *output = outputs.at(0); + + return std::make_unique(input, begin, size, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Softmax.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Softmax.cpp new file mode 100644 index 0000000..1957a76 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Softmax.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Softmax.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSoftmax(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsSoftmaxOptions(); + + SoftmaxParams params{}; + params.beta = options->beta; + + return std::make_unique(input, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/SpaceToBatchND.cpp b/onert-micro/luci-interpreter/src/loader/nodes/SpaceToBatchND.cpp new file mode 100644 index 0000000..c5553c6 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/SpaceToBatchND.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/SpaceToBatchND.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSpaceToBatchND(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 3); + + const Tensor *input = inputs.at(0); + const Tensor *block_shape = inputs.at(1); + const Tensor *paddings = inputs.at(2); + Tensor *output = outputs.at(0); + + return std::make_unique(input, block_shape, paddings, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/SpaceToDepth.cpp b/onert-micro/luci-interpreter/src/loader/nodes/SpaceToDepth.cpp new file mode 100644 index 0000000..156976a --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/SpaceToDepth.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/SpaceToDepth.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSpaceToDepth(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsSpaceToDepthOptions(); + + SpaceToDepthParams params{}; + params.block_size = options->block_size; + + return std::make_unique(input, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Split.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Split.cpp new file mode 100644 index 0000000..ecec1cb --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Split.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Split.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSplit(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *axis = inputs.at(0); + const Tensor *input = inputs.at(1); + std::vector output_tensors(outputs.size()); + + for (uint32_t i = 0; i < outputs.size(); ++i) + { + output_tensors[i] = outputs.at(i).first; + } + + // NOTE 'num_splits' attribute is ignored. + return std::make_unique(axis, input, std::move(output_tensors)); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/SplitV.cpp b/onert-micro/luci-interpreter/src/loader/nodes/SplitV.cpp new file mode 100644 index 0000000..a4c0ae2 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/SplitV.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/SplitV.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSplitV(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 3); + + const Tensor *input = inputs.at(0); + const Tensor *sizes_data = inputs.at(1); + const Tensor *axis = inputs.at(2); + std::vector output_tensors(outputs.size()); + + for (uint32_t i = 0; i < outputs.size(); ++i) + { + output_tensors[i] = outputs.at(i).first; + } + + // NOTE 'num_splits' attribute is ignored. + return std::make_unique(input, sizes_data, axis, std::move(output_tensors)); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Sqrt.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Sqrt.cpp new file mode 100644 index 0000000..3eaf234 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Sqrt.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Sqrt.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSqrt(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Square.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Square.cpp new file mode 100644 index 0000000..1afc6cc --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Square.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Square.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSquare(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/SquaredDifference.cpp b/onert-micro/luci-interpreter/src/loader/nodes/SquaredDifference.cpp new file mode 100644 index 0000000..0a5ba78 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/SquaredDifference.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/SquaredDifference.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSquaredDifference(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input1 = inputs.at(0); + const Tensor *input2 = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(input1, input2, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Squeeze.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Squeeze.cpp new file mode 100644 index 0000000..4f0c265 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Squeeze.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Squeeze.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSqueeze(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsSqueezeOptions(); + + SqueezeParams params{}; + params.squeeze_dims = options->squeeze_dims; + + return std::make_unique(input, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/StridedSlice.cpp b/onert-micro/luci-interpreter/src/loader/nodes/StridedSlice.cpp new file mode 100644 index 0000000..c0a53fc --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/StridedSlice.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/StridedSlice.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleStridedSlice(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 4); + + const Tensor *input = inputs.at(0); + const Tensor *begin = inputs.at(1); + const Tensor *end = inputs.at(2); + const Tensor *strides = inputs.at(3); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsStridedSliceOptions(); + + StridedSliceParams params{}; + params.begin_mask = options->begin_mask; + params.ellipsis_mask = options->ellipsis_mask; + params.end_mask = options->end_mask; + params.new_axis_mask = options->new_axis_mask; + params.shrink_axis_mask = options->shrink_axis_mask; + + return std::make_unique(input, begin, end, strides, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Sub.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Sub.cpp new file mode 100644 index 0000000..79c773b --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Sub.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Sub.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleSub(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input1 = inputs.at(0); + const Tensor *input2 = inputs.at(1); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsSubOptions(); + + SubParams params{}; + params.activation = luci_actfunc(options->fused_activation_function); + + return std::make_unique(input1, input2, output, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Tanh.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Tanh.cpp new file mode 100644 index 0000000..f4aff4c --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Tanh.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Tanh.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleTanh(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + const Tensor *input = inputs.at(0); + Tensor *output = outputs.at(0); + + return std::make_unique(input, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Transpose.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Transpose.cpp new file mode 100644 index 0000000..83b466d --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Transpose.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Transpose.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleTranspose(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 2); + + const Tensor *input = inputs.at(0); + const Tensor *perm = inputs.at(1); + Tensor *output = outputs.at(0); + + return std::make_unique(input, perm, output); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/TransposeConv.cpp b/onert-micro/luci-interpreter/src/loader/nodes/TransposeConv.cpp new file mode 100644 index 0000000..06ee63e --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/TransposeConv.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/TransposeConv.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleTransposeConv(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, + KernelBuilder &builder) +{ + assert(inputs.size() == 4); + + const Tensor *input_sizes = inputs.at(0); + const Tensor *filter = inputs.at(1); + const Tensor *out_backprop = inputs.at(2); + const Tensor *bias = inputs.at(3); + Tensor *output = outputs.at(0); + + DataType scratch_data_type = + input_sizes->element_type() == DataType::S16 ? DataType::S64 : DataType::S32; + + auto scratch_tensor = std::make_unique(scratch_data_type, Shape({}), nullptr); + scratch_tensor->set_data_buffer(nullptr); + Tensor *tmp = builder.get_runtime_graph()->addTensor(std::move(scratch_tensor)); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsTransposeConvOptions(); + + TransposeConvParams params{}; + params.padding = luci_padding(options->padding); + params.stride_height = options->stride_h; + params.stride_width = options->stride_w; + + return std::make_unique(input_sizes, filter, out_backprop, bias, output, + tmp, params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/UnidirectionalSequenceLSTM.cpp b/onert-micro/luci-interpreter/src/loader/nodes/UnidirectionalSequenceLSTM.cpp new file mode 100644 index 0000000..b66e53f --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/UnidirectionalSequenceLSTM.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 Samsung Electronics Co., Ltd. All Rights Reserved + * Copyright 2017 The TensorFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/UnidirectionalSequenceLSTM.h" + +namespace luci_interpreter +{ + +std::unique_ptr +build_kernel_CircleUnidirectionalSequenceLSTM(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 24); + const Tensor *input = inputs.at(0); + const Tensor *input_to_input_weights = inputs.at(1); + const Tensor *input_to_forget_weights = inputs.at(2); + const Tensor *input_to_cell_weights = inputs.at(3); + const Tensor *input_to_output_weights = inputs.at(4); + + const Tensor *recurrent_to_input_weights = inputs.at(5); + const Tensor *recurrent_to_forget_weights = inputs.at(6); + const Tensor *recurrent_to_cell_weights = inputs.at(7); + const Tensor *recurrent_to_output_weights = inputs.at(8); + + const Tensor *cell_to_input_weights = inputs.at(9); + const Tensor *cell_to_forget_weights = inputs.at(10); + const Tensor *cell_to_output_weights = inputs.at(11); + + const Tensor *input_gate_bias = inputs.at(12); + const Tensor *forget_gate_bias = inputs.at(13); + const Tensor *cell_gate_bias = inputs.at(14); + const Tensor *output_gate_bias = inputs.at(15); + + const Tensor *projection_weights = inputs.at(16); + const Tensor *projection_bias = inputs.at(17); + + Tensor *output_state = const_cast(inputs.at(18)); + Tensor *cell_state = const_cast(inputs.at(19)); + + const Tensor *input_layer_norm_coefficients = inputs.at(20); + const Tensor *forget_layer_norm_coefficients = inputs.at(21); + const Tensor *cell_layer_norm_coefficients = inputs.at(22); + const Tensor *output_layer_norm_coefficients = inputs.at(23); + Tensor *output = outputs.at(0); + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsUnidirectionalSequenceLSTMOptions(); + + UnidirectionalSequenceLSTMParams params{}; + params.activation = luci_actfunc(options->fused_activation_function); + params.cell_clip = options->cell_clip; + params.proj_clip = options->proj_clip; + params.time_major = options->time_major; + params.asymmetric_quantize_inputs = options->asymmetric_quantize_inputs; + + // scratch pad tensor + const bool is_integer = input->element_type() == DataType::S8; + bool use_layer_norm = (forget_layer_norm_coefficients != nullptr); + + if (is_integer) + { + if (not use_layer_norm) + { + params.intermediate_affine_quant = + builder.get_runtime_graph()->getIntermediateAffineQuantizations(); + + // For integer LSTM need 4 16-bit buffer with size n_batch * n_cell + // and 1 8-bit buffer with size n_batch * n_cell + auto tmp_1 = std::make_unique(DataType::S16, Shape({}), nullptr); + tmp_1->set_data_buffer(nullptr); + outputs.push_back(builder.get_runtime_graph()->addTensor(std::move(tmp_1))); + + auto tmp_2 = std::make_unique(DataType::S16, Shape({}), nullptr); + tmp_2->set_data_buffer(nullptr); + outputs.push_back(builder.get_runtime_graph()->addTensor(std::move(tmp_2))); + + auto tmp_3 = std::make_unique(DataType::S16, Shape({}), nullptr); + tmp_3->set_data_buffer(nullptr); + outputs.push_back(builder.get_runtime_graph()->addTensor(std::move(tmp_3))); + + auto tmp_4 = std::make_unique(DataType::S16, Shape({}), nullptr); + tmp_4->set_data_buffer(nullptr); + outputs.push_back(builder.get_runtime_graph()->addTensor(std::move(tmp_4))); + + auto tmp_5 = std::make_unique( + DataType::S8, Shape({}), + builder.get_runtime_graph()->getIntermediateAffineQuantizations()[0]); + tmp_5->set_data_buffer(nullptr); + outputs.push_back(builder.get_runtime_graph()->addTensor(std::move(tmp_5))); + } + else + { + // TODO: support float + assert(false && "Not supported now"); + } + } + else + { + // NOTE provide more scratch pads if support hybrid or integer + auto sp_output_state = + std::make_unique(output_state->element_type(), Shape({}), nullptr); + sp_output_state->set_data_buffer(nullptr); + outputs.push_back(builder.get_runtime_graph()->addTensor(std::move(sp_output_state))); + + auto sp_cell_state = std::make_unique(cell_state->element_type(), Shape({}), nullptr); + sp_cell_state->set_data_buffer(nullptr); + outputs.push_back(builder.get_runtime_graph()->addTensor(std::move(sp_cell_state))); + + auto sp_3 = std::make_unique(input->element_type(), Shape({}), nullptr); + sp_3->set_data_buffer(nullptr); + outputs.push_back(builder.get_runtime_graph()->addTensor(std::move(sp_3))); + } + + outputs.push_back(output_state); + outputs.push_back(cell_state); + + return std::make_unique( + input, input_to_input_weights, input_to_forget_weights, input_to_cell_weights, + input_to_output_weights, recurrent_to_input_weights, recurrent_to_forget_weights, + recurrent_to_cell_weights, recurrent_to_output_weights, cell_to_input_weights, + cell_to_forget_weights, cell_to_output_weights, input_gate_bias, forget_gate_bias, + cell_gate_bias, output_gate_bias, projection_weights, projection_bias, + input_layer_norm_coefficients, forget_layer_norm_coefficients, cell_layer_norm_coefficients, + output_layer_norm_coefficients, std::move(outputs), params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/Unpack.cpp b/onert-micro/luci-interpreter/src/loader/nodes/Unpack.cpp new file mode 100644 index 0000000..7f067f4 --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/Unpack.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/Unpack.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleUnpack(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + assert(inputs.size() == 1); + + const Tensor *input = inputs.at(0); + std::vector output_tensors(outputs.size()); + + for (uint32_t i = 0; i < outputs.size(); ++i) + { + output_tensors[i] = outputs.at(i); + } + + circle::OperatorT oper_t; + builder.get_circle_reader()->operators()[op_index]->UnPackTo(&oper_t); + const auto *options = oper_t.builtin_options.AsUnpackOptions(); + + UnpackParams params{}; + params.axis = options->axis; + + // NOTE 'num' attribute is ignored. + return std::make_unique(input, std::move(output_tensors), params); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/loader/nodes/While.cpp b/onert-micro/luci-interpreter/src/loader/nodes/While.cpp new file mode 100644 index 0000000..b1f719e --- /dev/null +++ b/onert-micro/luci-interpreter/src/loader/nodes/While.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Builders.h" + +#include "kernels/While.h" + +namespace luci_interpreter +{ + +std::unique_ptr build_kernel_CircleWhile(std::vector &&inputs, + std::vector &&outputs, + const uint32_t op_index, KernelBuilder &builder) +{ + // TODO: support IF operation + assert(false && "Not supported now"); +} + +} // namespace luci_interpreter diff --git a/onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.cpp b/onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.cpp new file mode 100644 index 0000000..ca1c92b --- /dev/null +++ b/onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if 0 + +#include "BuddyMemoryManager.h" + +namespace luci_interpreter +{ + +BuddyMemoryManager::BuddyMemoryManager(uint8_t *memory_start, int32_t memSize) +{ + int32_t p = lowerLog2(memSize); + + // We assume that the requested size of memory does not exceed 4 GB + assert(p < 32); + memSize = 1 << p; + + _start_block = reinterpret_cast(memory_start); + _start_block->size = memSize - sizeof(Block); + _start_block->is_free = true; + _start_block->self = _start_block; + _num_blocks = 0; + _size = _start_block->size; + + for (auto &_free_block : _free_blocks) + _free_block = nullptr; + + addToBlocks(_start_block, p); +} + +void BuddyMemoryManager::allocate_memory(luci_interpreter::Tensor &tensor) +{ + const size_t element_size = getDataTypeSize(tensor.element_type()); + const int32_t num_elements = tensor.shape().num_elements(); + auto size = num_elements * element_size; + auto footprint = size + sizeof(Block); + auto l = (footprint & (footprint - 1)) == 0 + ? lowerLog2(footprint) + : lowerLog2(footprint) + 1; // check footprint is pow_of_2 + + while (l < 32 && !_free_blocks[l]) + l++; + + assert(l < 32); + + Block *tmp; + tmp = _free_blocks[l]; + removeFromBlocks(tmp, l); + + while ((tmp->size + sizeof(Block)) / 2 >= size + sizeof(Block)) + { + divideBlock(tmp, l); + l--; + } + + tmp->is_free = false; + tmp->self = tmp; + _num_blocks++; + + auto *data = (uint8_t *)(tmp + 1); + tensor.set_data_buffer(data); +} + +void BuddyMemoryManager::release_memory(luci_interpreter::Tensor &tensor) +{ + auto data = tensor.data(); + auto *tmp = (Block *)((uint8_t *)data - sizeof(Block)); + + assert(tmp->self == tmp); + + tmp->is_free = true; + addToBlocks(tmp, lowerLog2(tmp->size + sizeof(Block))); + + while (tmp) + if (tmp->size == _size) + break; + else + tmp = mergeBlock(tmp); + + _num_blocks--; + tensor.set_data_buffer(nullptr); +} + +} // namespace luci_interpreter + +#endif diff --git a/onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.h b/onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.h new file mode 100644 index 0000000..ea56e97 --- /dev/null +++ b/onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.h @@ -0,0 +1,148 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if 0 + +#include "MemoryManager.h" + +#ifndef LUCI_INTERPRETER_BUDDY_MEMORY_MANAGER_H +#define LUCI_INTERPRETER_BUDDY_MEMORY_MANAGER_H + +namespace luci_interpreter +{ + +class BuddyMemoryManager : public IMemoryManager +{ +public: + BuddyMemoryManager(uint8_t *memory_start, int32_t memSize); + + void allocate_memory(luci_interpreter::Tensor &tensor) final; + void release_memory(luci_interpreter::Tensor &tensor) final; + +private: + struct Block + { + Block *next_free; + bool is_free; + uint32_t size; + // debug field + Block *self; + }; + + Block *_start_block; + int32_t _num_blocks; + uint32_t _size; + Block *_free_blocks[32]{}; + + static int32_t lowerLog2(uint32_t val) + { + int32_t i = 0; + while (val >>= 1) + i++; + + return i; + } + + void addToBlocks(Block *block, int32_t l) + { + if (!block) + return; + + block->next_free = _free_blocks[l]; + _free_blocks[l] = block; + } + + void removeFromBlocks(const Block *block, int32_t l) + { + if (!block) + return; + + Block *tmp = _free_blocks[l]; + + if (block == tmp) + { + _free_blocks[l] = block->next_free; + return; + } + + while (tmp) + { + if (tmp->next_free == block) + { + tmp->next_free = block->next_free; + return; + } + + tmp = tmp->next_free; + } + } + + void divideBlock(Block *block, int32_t l) + { + int32_t size = ((block->size + sizeof(Block)) / 2) - sizeof(Block); + + removeFromBlocks(block, l); + + // there is no need to add to the free_blocks list here + block->is_free = true; + block->size = size; + block->self = block; + + Block *buddy; + buddy = (Block *)((uint8_t *)block + sizeof(Block) + size); + buddy->is_free = true; + buddy->size = size; + buddy->self = buddy; + + addToBlocks(buddy, l - 1); + } + + Block *mergeBlock(Block *block) + { + Block *buddy; + + const int32_t l = lowerLog2(block->size + sizeof(Block)); + + const int64_t address = ((uint8_t *)block - (uint8_t *)_start_block); + buddy = (Block *)((address ^ (1 << l)) + (uint8_t *)_start_block); + + if (!buddy->is_free || buddy->size != block->size) + return nullptr; + + if (block > buddy) + { + Block *x = block; + block = buddy; + buddy = x; + } + + removeFromBlocks(block, l); + removeFromBlocks(buddy, l); + + block->size = block->size * 2 + sizeof(Block); + block->is_free = true; + block->self = block; + + addToBlocks(block, l + 1); + + return block; + } +}; + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_BUDDY_MEMORY_MANAGER_H + +#endif diff --git a/onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.test.cpp b/onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.test.cpp new file mode 100644 index 0000000..996b36d --- /dev/null +++ b/onert-micro/luci-interpreter/src/memory_managers/BuddyMemoryManager.test.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if 0 + +#include "BuddyMemoryManager.h" +#include + +namespace luci_interpreter +{ +namespace +{ + +using namespace testing; + +TEST(BuddyMemoryManager, basic) +{ + auto mem_pool = std::make_unique(200); + auto buddy_memory_manager = std::make_unique(mem_pool.get(), 130); + Tensor first_tensor(DataType::U8, Shape({8}), AffineQuantization{}, "first_tensor"); + + buddy_memory_manager->allocate_memory(first_tensor); + + uint8_t data_1[] = {1, 2, 3, 4, 5, 6, 7, 8}; + + first_tensor.writeData(data_1, 8); + uint8_t array_1[8]; + first_tensor.readData(array_1, 8); + for (int i = 0; i < 8; i++) + { + EXPECT_EQ(data_1[i], array_1[i]); + } + + Tensor second_tensor(DataType::U8, Shape({2, 5}), AffineQuantization{}, "second_tensor"); + buddy_memory_manager->allocate_memory(second_tensor); + + uint8_t data_2[2][5] = {{11, 22, 33, 44, 55}, {12, 23, 34, 45, 56}}; + second_tensor.writeData(data_2, 10); + + uint8_t array_2[2][5]; + second_tensor.readData(array_2, 10); + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 5; j++) + { + EXPECT_EQ(data_2[i][j], array_2[i][j]); + } + } + + buddy_memory_manager->release_memory(first_tensor); + EXPECT_EQ(first_tensor.data(), nullptr); + + buddy_memory_manager->release_memory(second_tensor); + EXPECT_EQ(second_tensor.data(), nullptr); +} + +} // namespace +} // namespace luci_interpreter + +#endif diff --git a/onert-micro/luci-interpreter/src/memory_managers/CMakeLists.txt b/onert-micro/luci-interpreter/src/memory_managers/CMakeLists.txt new file mode 100644 index 0000000..e783d8d --- /dev/null +++ b/onert-micro/luci-interpreter/src/memory_managers/CMakeLists.txt @@ -0,0 +1,21 @@ +set(SOURCES + "SimpleMemoryManager.h" SimpleMemoryManager.cpp + "TestMemoryManager.h" TestMemoryManager.cpp + "BuddyMemoryManager.h" BuddyMemoryManager.cpp + "StaticMemoryManager.h" StaticMemoryManager.cpp) + +add_library(${LUCI_INTERPRETER_MEMORY_MANAGER} STATIC ${SOURCES}) +target_include_directories(${LUCI_INTERPRETER_MEMORY_MANAGER} PUBLIC "${LUCI_INTERPRETER_INCLUDE_DIR}") +target_link_libraries(${LUCI_INTERPRETER_MEMORY_MANAGER} PUBLIC "luci_micro_circle_reader${READER_SUFFIX}") +target_link_libraries(${LUCI_INTERPRETER_MEMORY_MANAGER} PUBLIC luci_micro_circle_schema) + +if(NOT ENABLE_TEST) + return() +endif(NOT ENABLE_TEST) + +set(TEST_SOURCES BuddyMemoryManager.test.cpp) + +nnas_find_package(GTest REQUIRED) + +GTest_AddTest(buddy_manager_test_micro ${TEST_SOURCES}) +target_link_libraries(buddy_manager_test_micro ${LUCI_INTERPRETER_BINARY}) diff --git a/onert-micro/luci-interpreter/src/memory_managers/SimpleMemoryManager.cpp b/onert-micro/luci-interpreter/src/memory_managers/SimpleMemoryManager.cpp new file mode 100644 index 0000000..e8caa7c --- /dev/null +++ b/onert-micro/luci-interpreter/src/memory_managers/SimpleMemoryManager.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef USE_STATIC_ALLOC + +#include "SimpleMemoryManager.h" + +namespace luci_interpreter +{ + +uint8_t *SimpleMemoryManager::allocate_memory(const circle::Tensor *tensor) +{ + const auto element_size = getDataTypeSize(Tensor::element_type(tensor)); + const auto num_elements = Tensor::num_elements(tensor); + + assert(element_size * num_elements > 0); + + return new uint8_t[num_elements * element_size]; +} + +void SimpleMemoryManager::release_memory(uint8_t *data) +{ + if (data == nullptr) + return; + + delete[] data; +} + +} // namespace luci_interpreter + +#endif // USE_STATIC_ALLOC diff --git a/onert-micro/luci-interpreter/src/memory_managers/SimpleMemoryManager.h b/onert-micro/luci-interpreter/src/memory_managers/SimpleMemoryManager.h new file mode 100644 index 0000000..0817fb2 --- /dev/null +++ b/onert-micro/luci-interpreter/src/memory_managers/SimpleMemoryManager.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef USE_STATIC_ALLOC +#ifndef LUCI_INTERPRETER_SIMPLE_MEMORY_MANAGER_H +#define LUCI_INTERPRETER_SIMPLE_MEMORY_MANAGER_H + +#include "luci_interpreter/core/DataType.h" +#include "luci_interpreter/core/Tensor.h" + +#include + +namespace luci_interpreter +{ + +class SimpleMemoryManager +{ +public: + uint8_t *allocate_memory(const circle::Tensor *tensor); + void release_memory(uint8_t *data); +}; + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_SIMPLE_MEMORY_MANAGER_H +#endif // USE_STATIC_ALLOC diff --git a/onert-micro/luci-interpreter/src/memory_managers/StaticMemoryManager.cpp b/onert-micro/luci-interpreter/src/memory_managers/StaticMemoryManager.cpp new file mode 100644 index 0000000..08d0850 --- /dev/null +++ b/onert-micro/luci-interpreter/src/memory_managers/StaticMemoryManager.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifdef USE_STATIC_ALLOC + +#include "StaticMemoryManager.h" + +namespace luci_interpreter +{ + +uint8_t *StaticMemoryManager::allocate_memory(int32_t offset) +{ + assert(_buffer_ptr != nullptr); + return _buffer_ptr + offset; +} + +uint8_t *StaticMemoryManager::allocate_memory_for_input(int32_t offset) +{ + assert(_input_buffer_ptr != nullptr); + return _input_buffer_ptr + offset; +} + +uint8_t *StaticMemoryManager::allocate_memory_for_output(int32_t offset) +{ + assert(_output_buffer_ptr != nullptr); + return _output_buffer_ptr + offset; +} + +void StaticMemoryManager::allocate_input_buf() +{ + assert(_input_req_size > 0); + if (_input_buffer_ptr == nullptr) + _input_buffer_ptr = new uint8_t[_input_req_size]; +} + +void StaticMemoryManager::allocate_output_buf() +{ + assert(_output_req_size > 0); + if (_output_buffer_ptr == nullptr) + _output_buffer_ptr = new uint8_t[_output_req_size]; +} + +void StaticMemoryManager::allocate_computing_buf() +{ + assert(_buffer_req_size > 0); + if (_buffer_ptr == nullptr) + _buffer_ptr = new uint8_t[_buffer_req_size]; +} + +void StaticMemoryManager::release_computing_buf() +{ + delete[] _buffer_ptr; + _buffer_ptr = nullptr; +} + +void StaticMemoryManager::release_input_buf() +{ + delete[] _input_buffer_ptr; + _input_buffer_ptr = nullptr; +} + +void StaticMemoryManager::release_output_buf() +{ + delete[] _output_buffer_ptr; + _output_buffer_ptr = nullptr; +} + +} // namespace luci_interpreter + +#endif // USE_STATIC_ALLOC diff --git a/onert-micro/luci-interpreter/src/memory_managers/StaticMemoryManager.h b/onert-micro/luci-interpreter/src/memory_managers/StaticMemoryManager.h new file mode 100644 index 0000000..2971e38 --- /dev/null +++ b/onert-micro/luci-interpreter/src/memory_managers/StaticMemoryManager.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifdef USE_STATIC_ALLOC + +#ifndef LUCI_INTERPRETER_STATIC_MEMORY_MANAGER_H +#define LUCI_INTERPRETER_STATIC_MEMORY_MANAGER_H + +#include "luci_interpreter/core/DataType.h" +#include "luci_interpreter/core/Tensor.h" + +#include + +namespace luci_interpreter +{ + +// Used for allocations in static buffer, using offsets defined in luci model. +class StaticMemoryManager +{ +public: + StaticMemoryManager() = delete; + + // To initialize static memory manager with precalculating required buffers size for input, + // output and for intermediate computations buffers. + // Using Static Memory Manager with common buffer for input, output, and for intermediate + // computations + // TODO remove this *_req_size to read it from circle file + explicit StaticMemoryManager(int32_t input_req_size, int32_t buffer_req_size, + int32_t output_req_size) + : _input_buffer_ptr(nullptr), _buffer_ptr(nullptr), _output_buffer_ptr(nullptr), + _input_req_size(input_req_size), _buffer_req_size(buffer_req_size), + _output_req_size(output_req_size) + { /* Do nothing */ + } + + // To set a pointer for tensor in _buffer_ptr with right offset + uint8_t *allocate_memory(int32_t offset); + // To set a pointer for tensor in input_buffer with right offset + uint8_t *allocate_memory_for_input(int32_t offset); + // To set a pointer for tensor in output_buffer with right offset + uint8_t *allocate_memory_for_output(int32_t offset); + + // Methods to set data pointer for tensor + // To allocate input memory buffer with _input_req_size * size_type bytes. Result pointer - + // _input_buffer_ptr + void allocate_input_buf(); + // To allocate input memory buffer with _output_req_size * size_type bytes. Result pointer - + // _output_buffer_ptr + void allocate_output_buf(); + // To allocate intermediate computing memory buffer with _buffer_req_size * size_type bytes. + // Result pointer - _buffer_ptr + void allocate_computing_buf(); + + // To delete memory for intermediate computing buffer + void release_computing_buf(); + // To delete memory for input buffer + void release_input_buf(); + // To delete memory for output buffer + void release_output_buf(); + +private: + // Stores a pointer to the beginning of the allocated memory buffer. + uint8_t *_buffer_ptr; + uint8_t *_input_buffer_ptr; + uint8_t *_output_buffer_ptr; + + // TODO remove this fields to read it from circle file + int32_t _input_req_size{}; + int32_t _buffer_req_size{}; + int32_t _output_req_size{}; +}; + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_STATIC_MEMORY_MANAGER_H + +#endif // USE_STATIC_ALLOC diff --git a/onert-micro/luci-interpreter/src/memory_managers/TestMemoryManager.cpp b/onert-micro/luci-interpreter/src/memory_managers/TestMemoryManager.cpp new file mode 100644 index 0000000..803e038 --- /dev/null +++ b/onert-micro/luci-interpreter/src/memory_managers/TestMemoryManager.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO Enable it + +#if 0 + +#include "TestMemoryManager.h" + +namespace luci_interpreter +{ + +void TestMemoryManager::allocate_memory(luci_interpreter::Tensor &tensor) +{ + if (!tensor.is_allocatable()) + { + return; + } + if (tensor.is_data_allocated()) + { + release_memory(tensor); + } + const auto element_size = getDataTypeSize(tensor.element_type()); + const auto num_elements = tensor.shape().num_elements(); + + auto *data = new uint8_t[num_elements * element_size]; + allocations.push_back(data); + tensor.set_data_buffer(data); +} + +void TestMemoryManager::release_memory(luci_interpreter::Tensor &tensor) +{ + tensor.set_data_buffer(nullptr); +} + +} // namespace luci_interpreter + +#endif diff --git a/onert-micro/luci-interpreter/src/memory_managers/TestMemoryManager.h b/onert-micro/luci-interpreter/src/memory_managers/TestMemoryManager.h new file mode 100644 index 0000000..25ee38d --- /dev/null +++ b/onert-micro/luci-interpreter/src/memory_managers/TestMemoryManager.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO Enable it + +#if 0 + +#ifndef LUCI_INTERPRETER_TEST_MEMORY_MANAGER_H +#define LUCI_INTERPRETER_TEST_MEMORY_MANAGER_H + +#include "MemoryManager.h" + +namespace luci_interpreter +{ +// Memory Manager for using in kernels tests. This eliminates the need to manually delete the +// allocated memory in tests. This mem_manager remembers all its allocations and in destructor +// delete all allocations. +class TestMemoryManager : public IMemoryManager +{ +public: + void allocate_memory(luci_interpreter::Tensor &tensor) final; + void release_memory(luci_interpreter::Tensor &tensor) final; + + ~TestMemoryManager() override + { + for (auto allocation : allocations) + { + delete[] allocation; + } + } + +private: + std::vector allocations; +}; + +} // namespace luci_interpreter + +#endif // LUCI_INTERPRETER_TEST_MEMORY_MANAGER_H + +#endif diff --git a/onert-micro/requires.cmake b/onert-micro/requires.cmake new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/onert-micro/requires.cmake @@ -0,0 +1 @@ + diff --git a/onert-micro/standalone/CMakeLists.txt b/onert-micro/standalone/CMakeLists.txt new file mode 100644 index 0000000..2dd7ef1 --- /dev/null +++ b/onert-micro/standalone/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.15) +project(luci_interpreter_micro_standalone) + +include(${NNAS_ROOT}/infra/onert-micro/utils.cmake) + +nnas_find_package(FlatBuffersSource EXACT 2.0 QUIET) +include_directories(${FlatBuffersSource_DIR}/include) + +# TODO: fix luci/plan for new luci-micro without luci/IR +add_subdirectory(${NNAS_PROJECT_SOURCE_DIR}/onert-micro/luci-interpreter ${CMAKE_CURRENT_BINARY_DIR}/luci-interpreter) diff --git a/onert-micro/tests/mbed-os/CMakeLists.txt b/onert-micro/tests/mbed-os/CMakeLists.txt new file mode 100644 index 0000000..8785ddf --- /dev/null +++ b/onert-micro/tests/mbed-os/CMakeLists.txt @@ -0,0 +1,194 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 3.9) +SET(CMAKE_SYSTEM_NAME Generic) +SET(CMAKE_CROSSCOMPILING TRUE) + +# force compiler settings +SET(CMAKE_C_COMPILER_WORKS TRUE) +SET(CMAKE_CXX_COMPILER_WORKS TRUE) + +# force cmake compilers +SET(CMAKE_ASM_COMPILER "arm-none-eabi-gcc") +SET(CMAKE_C_COMPILER "arm-none-eabi-gcc") +SET(CMAKE_CXX_COMPILER "arm-none-eabi-g++") +SET(ELF2BIN "arm-none-eabi-objcopy") + + +# if the environment does not specify build type, set to Debug +IF (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug" + CACHE STRING "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." + FORCE) +ENDIF () + +# here starts the project +PROJECT(mbed-os-example-onert-micro C CXX ASM) + +# uncomment below to have a verbose build process +#SET(CMAKE_VERBOSE_MAKEFILE ON) + +SET(LD_SYS_LIBS "-Wl,--start-group -lstdc++ -lsupc++ -lm -lc -lgcc -lnosys -Wl,--end-group") + + +SET(CMAKE_C_FLAGS "-g3 -std=gnu11 -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fmessage-length=0 -fexceptions -ffunction-sections -fdata-sections") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -funsigned-char -MMD -fomit-frame-pointer -Og -DMBED_DEBUG") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMBED_TRAP_ERRORS_ENABLED=1 -DMBED_MINIMAL_PRINTF -mcpu=cortex-m7 -mthumb") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mfpu=fpv5-d16 -mfloat-abi=softfp -DMBED_ROM_START=0x8000000") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMBED_ROM_SIZE=0x200000 -DMBED_RAM_START=0x20000000 -DMBED_RAM_SIZE=0x20000") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMBED_RAM1_START=0x24000000 -DMBED_RAM1_SIZE=0x80000") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -include ${CMAKE_CURRENT_SOURCE_DIR}/mbed_config.h") + +SET(CMAKE_CXX_FLAGS "-g3 -std=gnu++14 -frtti -Wvla -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fmessage-length=0 -fexceptions -ffunction-sections -fdata-sections") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -funsigned-char -MMD -fomit-frame-pointer -Og -DMBED_DEBUG") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMBED_TRAP_ERRORS_ENABLED=1 -DMBED_MINIMAL_PRINTF -mcpu=cortex-m7") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mthumb -mfpu=fpv5-d16 -mfloat-abi=softfp -DMBED_ROM_START=0x8000000") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMBED_ROM_SIZE=0x200000 -DMBED_RAM_START=0x20000000 -DMBED_RAM_SIZE=0x20000") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMBED_RAM1_START=0x24000000 -DMBED_RAM1_SIZE=0x80000") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -include ${CMAKE_CURRENT_SOURCE_DIR}/mbed_config.h") + +SET(CMAKE_ASM_FLAGS "-g3 -x assembler-with-cpp -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers") +SET(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -fmessage-length=0 -fexceptions -ffunction-sections -fdata-sections") +SET(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -funsigned-char -MMD -fomit-frame-pointer -Og -DMBED_DEBUG") +SET(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -DMBED_TRAP_ERRORS_ENABLED=1 -DMBED_MINIMAL_PRINTF -mcpu=cortex-m7") +SET(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -mthumb -mfpu=fpv5-d16 -mfloat-abi=softfp ") +SET(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -include ${CMAKE_CURRENT_SOURCE_DIR}/mbed_config.h") + +SET(CMAKE_CXX_LINK_FLAGS "-Wl,--gc-sections -Wl,--wrap,main -Wl,--wrap,_malloc_r -Wl,--wrap,_free_r") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -Wl,--wrap,_realloc_r -Wl,--wrap,__memalign_r -Wl,--wrap,__calloc_r") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -Wl,--wrap,exit -Wl,--wrap,atexit -Wl,-n -Wl,--wrap,printf") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -Wl,--wrap,sprintf -Wl,--wrap,snprintf -Wl,--wrap,vprintf") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -Wl,--wrap,vsprintf -Wl,--wrap,vsnprintf -Wl,--wrap,fprintf") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -Wl,--wrap,vfprintf -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -mfloat-abi=softfp -Wall -Wextra -Wno-unused-parameter") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -Wno-missing-field-initializers -fmessage-length=0 -fexceptions") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -ffunction-sections -fdata-sections -funsigned-char -MMD") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -fomit-frame-pointer -Og -DMBED_DEBUG -DMBED_TRAP_ERRORS_ENABLED=1") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -DMBED_MINIMAL_PRINTF -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -mfloat-abi=softfp -DMBED_ROM_START=0x8000000") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -DMBED_ROM_SIZE=0x200000 -DMBED_RAM_START=0x20000400") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -DMBED_RAM_SIZE=0x1FC00 -DMBED_RAM1_START=0x24000000") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -DMBED_RAM1_SIZE=0x80000 -DMBED_BOOT_STACK_SIZE=1024 -DXIP_ENABLE=0") +SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} ${LD_SYS_LIBS} -T ${CMAKE_BINARY_DIR}/build_test_pp.link_script.ld") + +ADD_DEFINITIONS( + -DARM_MATH_CM7 + -DCOMPONENT_FLASHIAP=1 + -DDEVICE_ANALOGIN=1 + -DDEVICE_ANALOGOUT=1 + -DDEVICE_CAN=1 + -DDEVICE_CRC=1 + -DDEVICE_EMAC=1 + -DDEVICE_FLASH=1 + -DDEVICE_I2C=1 + -DDEVICE_I2CSLAVE=1 + -DDEVICE_I2C_ASYNCH=1 + -DDEVICE_INTERRUPTIN=1 + -DDEVICE_LPTICKER=1 + -DDEVICE_MPU=1 + -DDEVICE_PORTIN=1 + -DDEVICE_PORTINOUT=1 + -DDEVICE_PORTOUT=1 + -DDEVICE_PWMOUT=1 + -DDEVICE_RESET_REASON=1 + -DDEVICE_RTC=1 + -DDEVICE_SERIAL=1 + -DDEVICE_SERIAL_FC=1 + -DDEVICE_SLEEP=1 + -DDEVICE_SPI=1 + -DDEVICE_SPISLAVE=1 + -DDEVICE_SPI_ASYNCH=1 + -DDEVICE_STDIO_MESSAGES=1 + -DDEVICE_TRNG=1 + -DDEVICE_USBDEVICE=1 + -DDEVICE_USTICKER=1 + -DDEVICE_WATCHDOG=1 + -DEXTRA_IDLE_STACK_REQUIRED + -DMBED_BUILD_TIMESTAMP=1640167847.81 + -DMBED_TICKLESS + -DSTM32H743xx + -DTARGET_CORTEX + -DTARGET_CORTEX_M + -DTARGET_FF_ARDUINO_UNO + -DTARGET_LIKE_CORTEX_M7 + -DTARGET_LIKE_MBED + -DTARGET_M7 + -DTARGET_MCU_STM32 + -DTARGET_MCU_STM32H7 + -DTARGET_MCU_STM32H743xI + -DTARGET_NAME=NUCLEO_H743ZI2 + -DTARGET_NUCLEO_H743ZI2 + -DTARGET_RELEASE + -DTARGET_RTOS_M4_M7 + -DTARGET_STM + -DTARGET_STM32H7 + -DTARGET_STM32H743xI + -DTOOLCHAIN_GCC + -DTOOLCHAIN_GCC_ARM + -DTRANSACTION_QUEUE_SIZE_SPI=2 + -DUSE_FULL_LL_DRIVER + -DUSE_HAL_DRIVER + -D__CMSIS_RTOS + -D__CORTEX_M7 + -D__FPU_PRESENT=1 + -D__MBED_CMSIS_RTOS_CM + -D__MBED__=1 + -DMBED_MEM_TRACING_ENABLED=0 +) + +include(mbed-sources.cmake) + +set_sources_mbed(${MbedOSSource_DIR}) +list(APPEND SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/startup_stm32h743xx.S") + + +add_library(mbed_os STATIC ${SOURCES}) + +target_include_directories_mbed(mbed_os ${MbedOSSource_DIR}) + + +SET_TARGET_PROPERTIES(mbed_os PROPERTIES ENABLE_EXPORTS 1) +# add syslibs dependencies to create the correct linker order +TARGET_LINK_LIBRARIES(mbed_os -lstdc++ -lsupc++ -lm -lc -lgcc -lnosys) + +add_executable(build_test main.cpp) + +target_link_libraries(build_test mbed_os) +target_include_directories_mbed(build_test ${MbedOSSource_DIR}) + +target_link_libraries(mbed_os "${MICRO_ARM_BUILD_DIR}/luci-interpreter/src/core/reader/libluci_micro_circle_reader.a") +target_link_libraries(mbed_os "${MICRO_ARM_BUILD_DIR}/luci-interpreter/src/core/libluci_interpreter_core_micro.a") +target_link_libraries(mbed_os "${MICRO_ARM_BUILD_DIR}/luci-interpreter/src/kernels/libluci_interpreter_kernels_micro.a") +target_link_libraries(mbed_os "${MICRO_ARM_BUILD_DIR}/luci-interpreter/src/kernels/libluci_interpreter_mcu_pal.a") +target_link_libraries(mbed_os "${MICRO_ARM_BUILD_DIR}/luci-interpreter/src/loader/libluci_interpreter_loader_micro.a") +target_link_libraries(mbed_os "${MICRO_ARM_BUILD_DIR}/luci-interpreter/src/libluci_interpreter_micro.a") + +target_include_directories(build_test PRIVATE + ${ONERTMICRO_SRC_DIR}/luci-interpreter/include + ${CMAKE_CURRENT_SOURCE_DIR} + ${FlatBuffersSource_DIR}/include + ) + +add_custom_command(TARGET build_test PRE_LINK + COMMAND "arm-none-eabi-cpp" -E -P -Wl,--gc-sections -Wl,--wrap,main -Wl,--wrap,_malloc_r + -Wl,--wrap,_free_r -Wl,--wrap,_realloc_r -Wl,--wrap,_memalign_r -Wl,--wrap,_calloc_r + -Wl,--wrap,exit -Wl,--wrap,atexit -Wl,-n -Wl,--wrap,printf -Wl,--wrap,sprintf + -Wl,--wrap,snprintf -Wl,--wrap,vprintf -Wl,--wrap,vsprintf -Wl,--wrap,vsnprintf + -Wl,--wrap,fprintf -Wl,--wrap,vfprintf -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=softfp + -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -fmessage-length=0 + -fexceptions -ffunction-sections -fdata-sections -funsigned-char -MMD -fomit-frame-pointer + -Og -DMBED_DEBUG -DMBED_TRAP_ERRORS_ENABLED=1 -DMBED_MINIMAL_PRINTF -mcpu=cortex-m7 -mthumb + -mfpu=fpv5-d16 -mfloat-abi=softfp -DMBED_ROM_START=0x8000000 -DMBED_ROM_SIZE=0x200000 + -DMBED_RAM_START=0x20000400 -DMBED_RAM_SIZE=0x1FC00 -DMBED_RAM1_START=0x24000000 -DMBED_RAM1_SIZE=0x80000 + -DMBED_BOOT_STACK_SIZE=1024 -DXIP_ENABLE=0 + ${MbedOSSource_DIR}/targets/TARGET_STM/TARGET_STM32H7/TARGET_STM32H743xI/TOOLCHAIN_GCC_ARM/STM32H743xI.ld + -o ${CMAKE_CURRENT_BINARY_DIR}/build_test_pp.link_script.ld + + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/build_test_pp.link_script.ld" + ) + +add_custom_command(TARGET build_test POST_BUILD + COMMAND ${ELF2BIN} -O binary $ $.bin + COMMAND ${CMAKE_COMMAND} -E echo "-- built: $.bin" + ) diff --git a/onert-micro/tests/mbed-os/main.cpp b/onert-micro/tests/mbed-os/main.cpp new file mode 100644 index 0000000..cdbe14b --- /dev/null +++ b/onert-micro/tests/mbed-os/main.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// base app for qemu +static volatile unsigned int *const UART_DR = (unsigned int *)0x40011004; + +void uart_print(const char *s) +{ + while (*s != '\0') + { + *UART_DR = *s; + s++; + } +} + +int main() { uart_print("Hello, World!\n"); } diff --git a/onert-micro/tests/mbed-os/mbed-sources.cmake b/onert-micro/tests/mbed-os/mbed-sources.cmake new file mode 100644 index 0000000..25f9e31 --- /dev/null +++ b/onert-micro/tests/mbed-os/mbed-sources.cmake @@ -0,0 +1,1589 @@ +macro(set_sources_mbed) + set(SOURCES + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/Include/cmsis_os2.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/Include/os_tick.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Config/RTX_Config.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Config/RTX_Config.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Include/rtx_def.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Include/rtx_evr.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Include/rtx_os.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Include1/cmsis_os.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Library/cmsis_os1.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/TOOLCHAIN_GCC/TARGET_RTOS_M4_M7/irq_cm4f.S + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_core_c.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_core_ca.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_core_cm.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_delay.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_evflags.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_evr.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_kernel.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_lib.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_lib.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_memory.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_mempool.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_msgqueue.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_mutex.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_semaphore.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_system.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_thread.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_timer.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/Source/os_systick.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/RTOS2/Source/os_tick_ptim.c + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/cachel1_armv7.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/cmsis_armcc.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/cmsis_armclang.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/cmsis_armclang_ltm.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/cmsis_compiler.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/cmsis_gcc.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/cmsis_iccarm.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/cmsis_version.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_armv81mml.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_armv8mbl.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_armv8mml.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_cm0.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_cm0plus.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_cm1.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_cm23.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_cm3.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_cm33.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_cm35p.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_cm4.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_cm55.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_cm7.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_sc000.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/core_sc300.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/mpu_armv7.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/mpu_armv8.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/pmu_armv8.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include/tz_context.h + ${ARGV0}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Source/mbed_tz_context.c + ${ARGV0}/cmsis/device/RTE/include/RTE_Components.h + ${ARGV0}/cmsis/device/mbed_cmsis_conf.h + ${ARGV0}/cmsis/device/rtos/TOOLCHAIN_GCC_ARM/mbed_boot_gcc_arm.c + ${ARGV0}/cmsis/device/rtos/include/mbed_boot.h + ${ARGV0}/cmsis/device/rtos/include/mbed_rtx_conf.h + ${ARGV0}/cmsis/device/rtos/include/mbed_rtx_storage.h + ${ARGV0}/cmsis/device/rtos/source/mbed_boot.c + ${ARGV0}/cmsis/device/rtos/source/mbed_rtos_rtx.c + ${ARGV0}/cmsis/device/rtos/source/mbed_rtx_handlers.c + ${ARGV0}/cmsis/device/rtos/source/mbed_rtx_idle.cpp + ${ARGV0}/connectivity/cellular/include/cellular/framework/API/ATHandler.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/API/CellularContext.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/API/CellularDevice.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/API/CellularInformation.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/API/CellularNetwork.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/API/CellularSMS.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/AT/AT_CellularContext.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/AT/AT_CellularDevice.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/AT/AT_CellularInformation.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/AT/AT_CellularNetwork.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/AT/AT_CellularSMS.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/AT/AT_CellularStack.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/AT/AT_ControlPlane_netif.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/common/APN_db.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/common/CellularCommon.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/common/CellularList.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/common/CellularLog.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/common/CellularUtil.h + ${ARGV0}/connectivity/cellular/include/cellular/framework/device/CellularStateMachine.h + ${ARGV0}/connectivity/cellular/source/framework/AT/AT_CellularContext.cpp + ${ARGV0}/connectivity/cellular/source/framework/AT/AT_CellularDevice.cpp + ${ARGV0}/connectivity/cellular/source/framework/AT/AT_CellularInformation.cpp + ${ARGV0}/connectivity/cellular/source/framework/AT/AT_CellularNetwork.cpp + ${ARGV0}/connectivity/cellular/source/framework/AT/AT_CellularSMS.cpp + ${ARGV0}/connectivity/cellular/source/framework/AT/AT_CellularStack.cpp + ${ARGV0}/connectivity/cellular/source/framework/AT/AT_ControlPlane_netif.cpp + ${ARGV0}/connectivity/cellular/source/framework/common/APN_db.cpp + ${ARGV0}/connectivity/cellular/source/framework/common/CellularLog.cpp + ${ARGV0}/connectivity/cellular/source/framework/common/CellularUtil.cpp + ${ARGV0}/connectivity/cellular/source/framework/device/ATHandler.cpp + ${ARGV0}/connectivity/cellular/source/framework/device/CellularContext.cpp + ${ARGV0}/connectivity/cellular/source/framework/device/CellularDevice.cpp + ${ARGV0}/connectivity/cellular/source/framework/device/CellularStateMachine.cpp + ${ARGV0}/connectivity/drivers/802.15.4_RF/atmel-rf-driver/atmel-rf-driver/NanostackRfPhyAtmel.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/atmel-rf-driver/source/AT86RF215Reg.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/atmel-rf-driver/source/AT86RFReg.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/atmel-rf-driver/source/NanostackRfPhyAT86RF215.cpp + ${ARGV0}/connectivity/drivers/802.15.4_RF/atmel-rf-driver/source/NanostackRfPhyAtmel.cpp + ${ARGV0}/connectivity/drivers/802.15.4_RF/atmel-rf-driver/source/at24mac.cpp + ${ARGV0}/connectivity/drivers/802.15.4_RF/atmel-rf-driver/source/at24mac.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/atmel-rf-driver/source/rfbits.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/mcr20a-rf-driver/mcr20a-rf-driver/NanostackRfPhyMcr20a.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/mcr20a-rf-driver/source/MCR20Drv.c + ${ARGV0}/connectivity/drivers/802.15.4_RF/mcr20a-rf-driver/source/MCR20Drv.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/mcr20a-rf-driver/source/MCR20Overwrites.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/mcr20a-rf-driver/source/MCR20Reg.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/mcr20a-rf-driver/source/NanostackRfPhyMcr20a.cpp + ${ARGV0}/connectivity/drivers/802.15.4_RF/mcr20a-rf-driver/source/XcvrSpi.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/stm-s2lp-rf-driver/source/NanostackRfPhys2lp.cpp + ${ARGV0}/connectivity/drivers/802.15.4_RF/stm-s2lp-rf-driver/source/at24mac_s2lp.cpp + ${ARGV0}/connectivity/drivers/802.15.4_RF/stm-s2lp-rf-driver/source/at24mac_s2lp.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/stm-s2lp-rf-driver/source/rf_configuration.c + ${ARGV0}/connectivity/drivers/802.15.4_RF/stm-s2lp-rf-driver/source/rf_configuration.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/stm-s2lp-rf-driver/source/s2lpReg.h + ${ARGV0}/connectivity/drivers/802.15.4_RF/stm-s2lp-rf-driver/stm-s2lp-rf-driver/NanostackRfPhys2lp.h + ${ARGV0}/connectivity/drivers/cellular/Altair/ALT1250/PPP/ALT1250_PPP.cpp + ${ARGV0}/connectivity/drivers/cellular/Altair/ALT1250/PPP/ALT1250_PPP.h + ${ARGV0}/connectivity/drivers/cellular/Altair/ALT1250/PPP/ALT1250_PPP_CellularContext.cpp + ${ARGV0}/connectivity/drivers/cellular/Altair/ALT1250/PPP/ALT1250_PPP_CellularContext.h + ${ARGV0}/connectivity/drivers/cellular/Altair/ALT1250/PPP/ALT1250_PPP_CellularNetwork.cpp + ${ARGV0}/connectivity/drivers/cellular/Altair/ALT1250/PPP/ALT1250_PPP_CellularNetwork.h + ${ARGV0}/connectivity/drivers/cellular/GEMALTO/CINTERION/GEMALTO_CINTERION.cpp + ${ARGV0}/connectivity/drivers/cellular/GEMALTO/CINTERION/GEMALTO_CINTERION.h + ${ARGV0}/connectivity/drivers/cellular/GEMALTO/CINTERION/GEMALTO_CINTERION_CellularContext.cpp + ${ARGV0}/connectivity/drivers/cellular/GEMALTO/CINTERION/GEMALTO_CINTERION_CellularContext.h + ${ARGV0}/connectivity/drivers/cellular/GEMALTO/CINTERION/GEMALTO_CINTERION_CellularInformation.cpp + ${ARGV0}/connectivity/drivers/cellular/GEMALTO/CINTERION/GEMALTO_CINTERION_CellularInformation.h + ${ARGV0}/connectivity/drivers/cellular/GEMALTO/CINTERION/GEMALTO_CINTERION_CellularStack.cpp + ${ARGV0}/connectivity/drivers/cellular/GEMALTO/CINTERION/GEMALTO_CINTERION_CellularStack.h + ${ARGV0}/connectivity/drivers/cellular/GENERIC/GENERIC_AT3GPP/GENERIC_AT3GPP.cpp + ${ARGV0}/connectivity/drivers/cellular/GENERIC/GENERIC_AT3GPP/GENERIC_AT3GPP.h + ${ARGV0}/connectivity/drivers/cellular/MultiTech/DragonflyNano/PPP/SARA4_PPP.cpp + ${ARGV0}/connectivity/drivers/cellular/MultiTech/DragonflyNano/PPP/SARA4_PPP.h + ${ARGV0}/connectivity/drivers/cellular/MultiTech/DragonflyNano/PPP/SARA4_PPP_CellularNetwork.cpp + ${ARGV0}/connectivity/drivers/cellular/MultiTech/DragonflyNano/PPP/SARA4_PPP_CellularNetwork.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BC95/QUECTEL_BC95.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BC95/QUECTEL_BC95.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BC95/QUECTEL_BC95_CellularContext.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BC95/QUECTEL_BC95_CellularContext.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BC95/QUECTEL_BC95_CellularInformation.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BC95/QUECTEL_BC95_CellularInformation.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BC95/QUECTEL_BC95_CellularNetwork.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BC95/QUECTEL_BC95_CellularNetwork.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BC95/QUECTEL_BC95_CellularStack.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BC95/QUECTEL_BC95_CellularStack.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96_CellularContext.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96_CellularContext.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96_CellularInformation.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96_CellularInformation.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96_CellularNetwork.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96_CellularNetwork.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96_CellularStack.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96_CellularStack.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96_ControlPlane_netif.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/BG96/QUECTEL_BG96_ControlPlane_netif.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/EC2X/QUECTEL_EC2X.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/EC2X/QUECTEL_EC2X.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/M26/QUECTEL_M26.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/M26/QUECTEL_M26.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/M26/QUECTEL_M26_CellularContext.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/M26/QUECTEL_M26_CellularContext.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/M26/QUECTEL_M26_CellularInformation.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/M26/QUECTEL_M26_CellularInformation.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/M26/QUECTEL_M26_CellularStack.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/M26/QUECTEL_M26_CellularStack.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/UG96/QUECTEL_UG96.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/UG96/QUECTEL_UG96.h + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/UG96/QUECTEL_UG96_CellularContext.cpp + ${ARGV0}/connectivity/drivers/cellular/QUECTEL/UG96/QUECTEL_UG96_CellularContext.h + ${ARGV0}/connectivity/drivers/cellular/RiotMicro/AT/RM1000_AT.cpp + ${ARGV0}/connectivity/drivers/cellular/RiotMicro/AT/RM1000_AT.h + ${ARGV0}/connectivity/drivers/cellular/RiotMicro/AT/RM1000_AT_CellularContext.cpp + ${ARGV0}/connectivity/drivers/cellular/RiotMicro/AT/RM1000_AT_CellularContext.h + ${ARGV0}/connectivity/drivers/cellular/RiotMicro/AT/RM1000_AT_CellularNetwork.cpp + ${ARGV0}/connectivity/drivers/cellular/RiotMicro/AT/RM1000_AT_CellularNetwork.h + ${ARGV0}/connectivity/drivers/cellular/RiotMicro/AT/RM1000_AT_CellularStack.cpp + ${ARGV0}/connectivity/drivers/cellular/RiotMicro/AT/RM1000_AT_CellularStack.h + ${ARGV0}/connectivity/drivers/cellular/TELIT/HE910/TELIT_HE910.cpp + ${ARGV0}/connectivity/drivers/cellular/TELIT/HE910/TELIT_HE910.h + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME310/TELIT_ME310.cpp + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME310/TELIT_ME310.h + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME310/TELIT_ME310_CellularContext.cpp + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME310/TELIT_ME310_CellularContext.h + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME310/TELIT_ME310_CellularNetwork.cpp + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME310/TELIT_ME310_CellularNetwork.h + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME310/TELIT_ME310_CellularStack.cpp + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME310/TELIT_ME310_CellularStack.h + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME910/TELIT_ME910.cpp + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME910/TELIT_ME910.h + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME910/TELIT_ME910_CellularContext.cpp + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME910/TELIT_ME910_CellularContext.h + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME910/TELIT_ME910_CellularNetwork.cpp + ${ARGV0}/connectivity/drivers/cellular/TELIT/ME910/TELIT_ME910_CellularNetwork.h + ${ARGV0}/connectivity/drivers/cellular/UBLOX/AT/UBLOX_AT.cpp + ${ARGV0}/connectivity/drivers/cellular/UBLOX/AT/UBLOX_AT.h + ${ARGV0}/connectivity/drivers/cellular/UBLOX/AT/UBLOX_AT_CellularContext.cpp + ${ARGV0}/connectivity/drivers/cellular/UBLOX/AT/UBLOX_AT_CellularContext.h + ${ARGV0}/connectivity/drivers/cellular/UBLOX/AT/UBLOX_AT_CellularNetwork.cpp + ${ARGV0}/connectivity/drivers/cellular/UBLOX/AT/UBLOX_AT_CellularNetwork.h + ${ARGV0}/connectivity/drivers/cellular/UBLOX/AT/UBLOX_AT_CellularStack.cpp + ${ARGV0}/connectivity/drivers/cellular/UBLOX/AT/UBLOX_AT_CellularStack.h + ${ARGV0}/connectivity/drivers/cellular/UBLOX/N2XX/UBLOX_N2XX.cpp + ${ARGV0}/connectivity/drivers/cellular/UBLOX/N2XX/UBLOX_N2XX.h + ${ARGV0}/connectivity/drivers/cellular/UBLOX/N2XX/UBLOX_N2XX_CellularContext.cpp + ${ARGV0}/connectivity/drivers/cellular/UBLOX/N2XX/UBLOX_N2XX_CellularContext.h + ${ARGV0}/connectivity/drivers/cellular/UBLOX/N2XX/UBLOX_N2XX_CellularNetwork.cpp + ${ARGV0}/connectivity/drivers/cellular/UBLOX/N2XX/UBLOX_N2XX_CellularNetwork.h + ${ARGV0}/connectivity/drivers/cellular/UBLOX/N2XX/UBLOX_N2XX_CellularSMS.cpp + ${ARGV0}/connectivity/drivers/cellular/UBLOX/N2XX/UBLOX_N2XX_CellularSMS.h + ${ARGV0}/connectivity/drivers/cellular/UBLOX/N2XX/UBLOX_N2XX_CellularStack.cpp + ${ARGV0}/connectivity/drivers/cellular/UBLOX/N2XX/UBLOX_N2XX_CellularStack.h + ${ARGV0}/connectivity/drivers/cellular/UBLOX/PPP/UBLOX_PPP.cpp + ${ARGV0}/connectivity/drivers/cellular/UBLOX/PPP/UBLOX_PPP.h + ${ARGV0}/connectivity/drivers/emac/TARGET_STM/TARGET_STM32H7/TARGET_NUCLEO_H743ZI2/stm32h7_eth_init.c + ${ARGV0}/connectivity/drivers/emac/TARGET_STM/TARGET_STM32H7/lan8742/lan8742.c + ${ARGV0}/connectivity/drivers/emac/TARGET_STM/TARGET_STM32H7/lan8742/lan8742.h + ${ARGV0}/connectivity/drivers/emac/TARGET_STM/TARGET_STM32H7/stm32xx_emac_config.h + ${ARGV0}/connectivity/drivers/emac/TARGET_STM/stm32xx_emac.cpp + ${ARGV0}/connectivity/drivers/emac/TARGET_STM/stm32xx_emac.h + ${ARGV0}/connectivity/drivers/emac/TARGET_STM/stm32xx_eth_irq_callback.cpp + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/aes_alt.cpp + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/aes_alt.h + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/aes_alt_stm32l4.c + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/aes_alt_stm32l4.h + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/ccm_alt.cpp + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/ccm_alt.h + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/cryp_stm32.c + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/cryp_stm32.h + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/gcm_alt.cpp + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/gcm_alt.h + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/hash_stm32.c + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/hash_stm32.h + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/md5_alt.cpp + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/md5_alt.h + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/sha1_alt.cpp + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/sha1_alt.h + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/sha256_alt.cpp + ${ARGV0}/connectivity/drivers/mbedtls/TARGET_STM/sha256_alt.h + ${ARGV0}/connectivity/drivers/nfc/PN512/include/nfc/controllers/PN512Driver.h + ${ARGV0}/connectivity/drivers/nfc/PN512/include/nfc/controllers/PN512SPITransportDriver.h + ${ARGV0}/connectivity/drivers/nfc/PN512/include/nfc/controllers/PN512TransportDriver.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/PN512Driver.cpp + ${ARGV0}/connectivity/drivers/nfc/PN512/source/PN512SPITransportDriver.cpp + ${ARGV0}/connectivity/drivers/nfc/PN512/source/PN512TransportDriver.cpp + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512.c + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_callback.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_cmd.c + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_cmd.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_hw.c + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_hw.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_internal.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_irq.c + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_irq.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_poll.c + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_poll.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_registers.c + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_registers.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_rf.c + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_rf.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_timer.c + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_timer.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_transceive.c + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_transceive.h + ${ARGV0}/connectivity/drivers/nfc/PN512/source/transceiver/pn512_types.h + ${ARGV0}/connectivity/drivers/wifi/esp8266-driver/ESP8266/ESP8266.cpp + ${ARGV0}/connectivity/drivers/wifi/esp8266-driver/ESP8266/ESP8266.h + ${ARGV0}/connectivity/drivers/wifi/esp8266-driver/ESP8266Interface.cpp + ${ARGV0}/connectivity/drivers/wifi/esp8266-driver/ESP8266Interface.h + ${ARGV0}/connectivity/libraries/mbed-coap/mbed-coap/sn_coap_header.h + ${ARGV0}/connectivity/libraries/mbed-coap/mbed-coap/sn_coap_protocol.h + ${ARGV0}/connectivity/libraries/mbed-coap/mbed-coap/sn_config.h + ${ARGV0}/connectivity/libraries/mbed-coap/source/include/sn_coap_header_internal.h + ${ARGV0}/connectivity/libraries/mbed-coap/source/include/sn_coap_protocol_internal.h + ${ARGV0}/connectivity/libraries/mbed-coap/source/sn_coap_builder.c + ${ARGV0}/connectivity/libraries/mbed-coap/source/sn_coap_header_check.c + ${ARGV0}/connectivity/libraries/mbed-coap/source/sn_coap_parser.c + ${ARGV0}/connectivity/libraries/mbed-coap/source/sn_coap_protocol.c + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/common_functions.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/ip4string.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/ip6string.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/ip_fsc.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/ns_list.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/ns_nvm_helper.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/ns_types.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/nsdynmemLIB.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/nsdynmem_tracker.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/nsdynmem_tracker_lib.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/platform/arm_hal_interrupt.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/platform/arm_hal_nvm.h + ${ARGV0}/connectivity/libraries/nanostack-libservice/source/IPv6_fcf_lib/ip_fsc.c + ${ARGV0}/connectivity/libraries/nanostack-libservice/source/libBits/common_functions.c + ${ARGV0}/connectivity/libraries/nanostack-libservice/source/libList/ns_list.c + ${ARGV0}/connectivity/libraries/nanostack-libservice/source/libip4string/ip4tos.c + ${ARGV0}/connectivity/libraries/nanostack-libservice/source/libip4string/stoip4.c + ${ARGV0}/connectivity/libraries/nanostack-libservice/source/libip6string/ip6tos.c + ${ARGV0}/connectivity/libraries/nanostack-libservice/source/libip6string/stoip6.c + ${ARGV0}/connectivity/libraries/nanostack-libservice/source/nsdynmemLIB/nsdynmemLIB.c + ${ARGV0}/connectivity/libraries/nanostack-libservice/source/nsdynmemtracker/nsdynmem_tracker_lib.c + ${ARGV0}/connectivity/libraries/nanostack-libservice/source/nvmHelper/ns_nvm_helper.c + ${ARGV0}/connectivity/libraries/ppp/include/polarssl/arc4.h + ${ARGV0}/connectivity/libraries/ppp/include/polarssl/des.h + ${ARGV0}/connectivity/libraries/ppp/include/polarssl/md4.h + ${ARGV0}/connectivity/libraries/ppp/include/polarssl/md5.h + ${ARGV0}/connectivity/libraries/ppp/include/polarssl/sha1.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/ccp.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/chap-md5.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/chap-new.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/chap_ms.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/eap.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/ecp.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/eui64.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/fsm.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/ipcp.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/ipv6cp.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/lcp.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/magic.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/mppe.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/ppp.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/ppp_impl.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/ppp_opts.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/ppp_service.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/ppp_service_if.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/pppapi.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/pppcrypt.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/pppdebug.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/pppoe.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/pppol2tp.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/pppos.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/upap.h + ${ARGV0}/connectivity/libraries/ppp/include/ppp/vj.h + ${ARGV0}/connectivity/libraries/ppp/source/auth.c + ${ARGV0}/connectivity/libraries/ppp/source/ccp.c + ${ARGV0}/connectivity/libraries/ppp/source/chap-md5.c + ${ARGV0}/connectivity/libraries/ppp/source/chap-new.c + ${ARGV0}/connectivity/libraries/ppp/source/chap_ms.c + ${ARGV0}/connectivity/libraries/ppp/source/demand.c + ${ARGV0}/connectivity/libraries/ppp/source/eap.c + ${ARGV0}/connectivity/libraries/ppp/source/eui64.c + ${ARGV0}/connectivity/libraries/ppp/source/fsm.c + ${ARGV0}/connectivity/libraries/ppp/source/ipcp.c + ${ARGV0}/connectivity/libraries/ppp/source/ipv6cp.c + ${ARGV0}/connectivity/libraries/ppp/source/lcp.c + ${ARGV0}/connectivity/libraries/ppp/source/magic.c + ${ARGV0}/connectivity/libraries/ppp/source/mppe.c + ${ARGV0}/connectivity/libraries/ppp/source/multilink.c + ${ARGV0}/connectivity/libraries/ppp/source/polarssl/ppp_arc4.c + ${ARGV0}/connectivity/libraries/ppp/source/polarssl/ppp_des.c + ${ARGV0}/connectivity/libraries/ppp/source/polarssl/ppp_md4.c + ${ARGV0}/connectivity/libraries/ppp/source/polarssl/ppp_md5.c + ${ARGV0}/connectivity/libraries/ppp/source/polarssl/ppp_sha1.c + ${ARGV0}/connectivity/libraries/ppp/source/ppp.c + ${ARGV0}/connectivity/libraries/ppp/source/ppp_ecp.c + ${ARGV0}/connectivity/libraries/ppp/source/ppp_service.cpp + ${ARGV0}/connectivity/libraries/ppp/source/ppp_service_if.cpp + ${ARGV0}/connectivity/libraries/ppp/source/pppapi.c + ${ARGV0}/connectivity/libraries/ppp/source/pppcrypt.c + ${ARGV0}/connectivity/libraries/ppp/source/pppoe.c + ${ARGV0}/connectivity/libraries/ppp/source/pppol2tp.c + ${ARGV0}/connectivity/libraries/ppp/source/pppos.cpp + ${ARGV0}/connectivity/libraries/ppp/source/upap.c + ${ARGV0}/connectivity/libraries/ppp/source/utils.c + ${ARGV0}/connectivity/libraries/ppp/source/vj.c + ${ARGV0}/connectivity/lorawan/include/lorawan/LoRaRadio.h + ${ARGV0}/connectivity/lorawan/include/lorawan/LoRaWANBase.h + ${ARGV0}/connectivity/lorawan/include/lorawan/LoRaWANInterface.h + ${ARGV0}/connectivity/lorawan/include/lorawan/LoRaWANStack.h + ${ARGV0}/connectivity/lorawan/include/lorawan/lorawan_types.h + ${ARGV0}/connectivity/lorawan/lorastack/mac/LoRaMac.cpp + ${ARGV0}/connectivity/lorawan/lorastack/mac/LoRaMac.h + ${ARGV0}/connectivity/lorawan/lorastack/mac/LoRaMacChannelPlan.cpp + ${ARGV0}/connectivity/lorawan/lorastack/mac/LoRaMacChannelPlan.h + ${ARGV0}/connectivity/lorawan/lorastack/mac/LoRaMacCommand.cpp + ${ARGV0}/connectivity/lorawan/lorastack/mac/LoRaMacCommand.h + ${ARGV0}/connectivity/lorawan/lorastack/mac/LoRaMacCrypto.cpp + ${ARGV0}/connectivity/lorawan/lorastack/mac/LoRaMacCrypto.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHY.cpp + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHY.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYAS923.cpp + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYAS923.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYAU915.cpp + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYAU915.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYCN470.cpp + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYCN470.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYCN779.cpp + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYCN779.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYEU433.cpp + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYEU433.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYEU868.cpp + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYEU868.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYIN865.cpp + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYIN865.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYKR920.cpp + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYKR920.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYUS915.cpp + ${ARGV0}/connectivity/lorawan/lorastack/phy/LoRaPHYUS915.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/lora_phy_ds.h + ${ARGV0}/connectivity/lorawan/lorastack/phy/loraphy_target.h + ${ARGV0}/connectivity/lorawan/source/LoRaWANInterface.cpp + ${ARGV0}/connectivity/lorawan/source/LoRaWANStack.cpp + ${ARGV0}/connectivity/lorawan/system/LoRaWANTimer.cpp + ${ARGV0}/connectivity/lorawan/system/LoRaWANTimer.h + ${ARGV0}/connectivity/lorawan/system/lorawan_data_structures.h + ${ARGV0}/connectivity/nfc/include/nfc/NFC.h + ${ARGV0}/connectivity/nfc/include/nfc/NFCController.h + ${ARGV0}/connectivity/nfc/include/nfc/NFCControllerDriver.h + ${ARGV0}/connectivity/nfc/include/nfc/NFCDefinitions.h + ${ARGV0}/connectivity/nfc/include/nfc/NFCEEPROM.h + ${ARGV0}/connectivity/nfc/include/nfc/NFCEEPROMDriver.h + ${ARGV0}/connectivity/nfc/include/nfc/NFCNDEFCapable.h + ${ARGV0}/connectivity/nfc/include/nfc/NFCRemoteEndpoint.h + ${ARGV0}/connectivity/nfc/include/nfc/NFCRemoteInitiator.h + ${ARGV0}/connectivity/nfc/include/nfc/NFCTarget.h + ${ARGV0}/connectivity/nfc/include/nfc/Type4RemoteInitiator.h + ${ARGV0}/connectivity/nfc/include/nfc/ndef/MessageBuilder.h + ${ARGV0}/connectivity/nfc/include/nfc/ndef/MessageParser.h + ${ARGV0}/connectivity/nfc/include/nfc/ndef/Record.h + ${ARGV0}/connectivity/nfc/include/nfc/ndef/RecordParser.h + ${ARGV0}/connectivity/nfc/include/nfc/ndef/common/Mime.h + ${ARGV0}/connectivity/nfc/include/nfc/ndef/common/SimpleMessageParser.h + ${ARGV0}/connectivity/nfc/include/nfc/ndef/common/Text.h + ${ARGV0}/connectivity/nfc/include/nfc/ndef/common/URI.h + ${ARGV0}/connectivity/nfc/include/nfc/ndef/common/util.h + ${ARGV0}/connectivity/nfc/libraries/acore/acore/ac_buffer.h + ${ARGV0}/connectivity/nfc/libraries/acore/acore/ac_buffer_builder.h + ${ARGV0}/connectivity/nfc/libraries/acore/acore/ac_buffer_reader.h + ${ARGV0}/connectivity/nfc/libraries/acore/acore/ac_debug.h + ${ARGV0}/connectivity/nfc/libraries/acore/acore/ac_macros.h + ${ARGV0}/connectivity/nfc/libraries/acore/acore/ac_stream.h + ${ARGV0}/connectivity/nfc/libraries/acore/source/ac_buffer.c + ${ARGV0}/connectivity/nfc/libraries/acore/source/ac_buffer_builder.c + ${ARGV0}/connectivity/nfc/libraries/acore/source/ac_buffer_reader.c + ${ARGV0}/connectivity/nfc/libraries/acore/source/ac_stream.c + ${ARGV0}/connectivity/nfc/libraries/stack/ndef/ndef.c + ${ARGV0}/connectivity/nfc/libraries/stack/ndef/ndef.h + ${ARGV0}/connectivity/nfc/libraries/stack/nfc_common.h + ${ARGV0}/connectivity/nfc/libraries/stack/nfc_errors.h + ${ARGV0}/connectivity/nfc/libraries/stack/platform/nfc_debug.h + ${ARGV0}/connectivity/nfc/libraries/stack/platform/nfc_scheduler.c + ${ARGV0}/connectivity/nfc/libraries/stack/platform/nfc_scheduler.h + ${ARGV0}/connectivity/nfc/libraries/stack/platform/nfc_transport.c + ${ARGV0}/connectivity/nfc/libraries/stack/platform/nfc_transport.h + ${ARGV0}/connectivity/nfc/libraries/stack/tech/iso7816/iso7816.c + ${ARGV0}/connectivity/nfc/libraries/stack/tech/iso7816/iso7816.h + ${ARGV0}/connectivity/nfc/libraries/stack/tech/iso7816/iso7816_app.c + ${ARGV0}/connectivity/nfc/libraries/stack/tech/iso7816/iso7816_app.h + ${ARGV0}/connectivity/nfc/libraries/stack/tech/iso7816/iso7816_defs.h + ${ARGV0}/connectivity/nfc/libraries/stack/tech/isodep/isodep.h + ${ARGV0}/connectivity/nfc/libraries/stack/tech/isodep/isodep_target.c + ${ARGV0}/connectivity/nfc/libraries/stack/tech/isodep/isodep_target.h + ${ARGV0}/connectivity/nfc/libraries/stack/tech/type4/type4_target.c + ${ARGV0}/connectivity/nfc/libraries/stack/tech/type4/type4_target.h + ${ARGV0}/connectivity/nfc/libraries/stack/transceiver/protocols.h + ${ARGV0}/connectivity/nfc/libraries/stack/transceiver/transceiver.c + ${ARGV0}/connectivity/nfc/libraries/stack/transceiver/transceiver.h + ${ARGV0}/connectivity/nfc/libraries/stack/transceiver/transceiver_internal.h + ${ARGV0}/connectivity/nfc/source/NFCController.cpp + ${ARGV0}/connectivity/nfc/source/NFCControllerDriver.cpp + ${ARGV0}/connectivity/nfc/source/NFCEEPROM.cpp + ${ARGV0}/connectivity/nfc/source/NFCEEPROMDriver.cpp + ${ARGV0}/connectivity/nfc/source/NFCNDEFCapable.cpp + ${ARGV0}/connectivity/nfc/source/NFCRemoteEndpoint.cpp + ${ARGV0}/connectivity/nfc/source/NFCRemoteInitiator.cpp + ${ARGV0}/connectivity/nfc/source/NFCTarget.cpp + ${ARGV0}/connectivity/nfc/source/Type4RemoteInitiator.cpp + ${ARGV0}/connectivity/nfc/source/ndef/MessageBuilder.cpp + ${ARGV0}/connectivity/nfc/source/ndef/MessageParser.cpp + ${ARGV0}/connectivity/nfc/source/ndef/RecordParser.cpp + ${ARGV0}/connectivity/nfc/source/ndef/common/Mime.cpp + ${ARGV0}/connectivity/nfc/source/ndef/common/SimpleMessageParser.cpp + ${ARGV0}/connectivity/nfc/source/ndef/common/Text.cpp + ${ARGV0}/connectivity/nfc/source/ndef/common/URI.cpp + ${ARGV0}/connectivity/nfc/source/ndef/common/util.cpp + ${ARGV0}/drivers/device_key/include/device_key/DeviceKey.h + ${ARGV0}/drivers/device_key/source/DeviceKey.cpp + ${ARGV0}/drivers/include/drivers/AnalogIn.h + ${ARGV0}/drivers/include/drivers/AnalogOut.h + ${ARGV0}/drivers/include/drivers/BufferedSerial.h + ${ARGV0}/drivers/include/drivers/BusIn.h + ${ARGV0}/drivers/include/drivers/BusInOut.h + ${ARGV0}/drivers/include/drivers/BusOut.h + ${ARGV0}/drivers/include/drivers/CAN.h + ${ARGV0}/drivers/include/drivers/DigitalIn.h + ${ARGV0}/drivers/include/drivers/DigitalInOut.h + ${ARGV0}/drivers/include/drivers/DigitalOut.h + ${ARGV0}/drivers/include/drivers/FlashIAP.h + ${ARGV0}/drivers/include/drivers/HighResClock.h + ${ARGV0}/drivers/include/drivers/I2C.h + ${ARGV0}/drivers/include/drivers/I2CSlave.h + ${ARGV0}/drivers/include/drivers/InterruptIn.h + ${ARGV0}/drivers/include/drivers/LowPowerClock.h + ${ARGV0}/drivers/include/drivers/LowPowerTicker.h + ${ARGV0}/drivers/include/drivers/LowPowerTimeout.h + ${ARGV0}/drivers/include/drivers/LowPowerTimer.h + ${ARGV0}/drivers/include/drivers/MbedCRC.h + ${ARGV0}/drivers/include/drivers/OSPI.h + ${ARGV0}/drivers/include/drivers/PortIn.h + ${ARGV0}/drivers/include/drivers/PortInOut.h + ${ARGV0}/drivers/include/drivers/PortOut.h + ${ARGV0}/drivers/include/drivers/PwmOut.h + ${ARGV0}/drivers/include/drivers/QSPI.h + ${ARGV0}/drivers/include/drivers/RawCAN.h + ${ARGV0}/drivers/include/drivers/RealTimeClock.h + ${ARGV0}/drivers/include/drivers/ResetReason.h + ${ARGV0}/drivers/include/drivers/SPI.h + ${ARGV0}/drivers/include/drivers/SPISlave.h + ${ARGV0}/drivers/include/drivers/SerialBase.h + ${ARGV0}/drivers/include/drivers/SerialWireOutput.h + ${ARGV0}/drivers/include/drivers/Ticker.h + ${ARGV0}/drivers/include/drivers/TickerDataClock.h + ${ARGV0}/drivers/include/drivers/Timeout.h + ${ARGV0}/drivers/include/drivers/Timer.h + ${ARGV0}/drivers/include/drivers/TimerEvent.h + ${ARGV0}/drivers/include/drivers/UnbufferedSerial.h + ${ARGV0}/drivers/include/drivers/Watchdog.h + ${ARGV0}/drivers/include/drivers/interfaces/InterfaceCAN.h + ${ARGV0}/drivers/include/drivers/interfaces/InterfaceDigitalIn.h + ${ARGV0}/drivers/include/drivers/interfaces/InterfaceDigitalInOut.h + ${ARGV0}/drivers/include/drivers/interfaces/InterfaceDigitalOut.h + ${ARGV0}/drivers/source/AnalogIn.cpp + ${ARGV0}/drivers/source/AnalogOut.cpp + ${ARGV0}/drivers/source/BufferedSerial.cpp + ${ARGV0}/drivers/source/BusIn.cpp + ${ARGV0}/drivers/source/BusInOut.cpp + ${ARGV0}/drivers/source/BusOut.cpp + ${ARGV0}/drivers/source/CAN.cpp + ${ARGV0}/drivers/source/DigitalIn.cpp + ${ARGV0}/drivers/source/DigitalInOut.cpp + ${ARGV0}/drivers/source/DigitalOut.cpp + ${ARGV0}/drivers/source/FlashIAP.cpp + ${ARGV0}/drivers/source/I2C.cpp + ${ARGV0}/drivers/source/I2CSlave.cpp + ${ARGV0}/drivers/source/InterruptIn.cpp + ${ARGV0}/drivers/source/MbedCRC.cpp + ${ARGV0}/drivers/source/OSPI.cpp + ${ARGV0}/drivers/source/PortIn.cpp + ${ARGV0}/drivers/source/PortInOut.cpp + ${ARGV0}/drivers/source/PortOut.cpp + ${ARGV0}/drivers/source/PwmOut.cpp + ${ARGV0}/drivers/source/QSPI.cpp + ${ARGV0}/drivers/source/ResetReason.cpp + ${ARGV0}/drivers/source/SPI.cpp + ${ARGV0}/drivers/source/SPISlave.cpp + ${ARGV0}/drivers/source/SerialBase.cpp + ${ARGV0}/drivers/source/SerialWireOutput.cpp + ${ARGV0}/drivers/source/Ticker.cpp + ${ARGV0}/drivers/source/Timeout.cpp + ${ARGV0}/drivers/source/Timer.cpp + ${ARGV0}/drivers/source/TimerEvent.cpp + ${ARGV0}/drivers/source/UnbufferedSerial.cpp + ${ARGV0}/drivers/source/Watchdog.cpp + ${ARGV0}/drivers/usb/include/usb/USBAudio.h + ${ARGV0}/drivers/usb/include/usb/USBCDC.h + ${ARGV0}/drivers/usb/include/usb/USBCDC_ECM.h + ${ARGV0}/drivers/usb/include/usb/USBHID.h + ${ARGV0}/drivers/usb/include/usb/USBKeyboard.h + ${ARGV0}/drivers/usb/include/usb/USBMIDI.h + ${ARGV0}/drivers/usb/include/usb/USBMSD.h + ${ARGV0}/drivers/usb/include/usb/USBMouse.h + ${ARGV0}/drivers/usb/include/usb/USBMouseKeyboard.h + ${ARGV0}/drivers/usb/include/usb/USBSerial.h + ${ARGV0}/drivers/usb/include/usb/internal/AsyncOp.h + ${ARGV0}/drivers/usb/include/usb/internal/ByteBuffer.h + ${ARGV0}/drivers/usb/include/usb/internal/EndpointResolver.h + ${ARGV0}/drivers/usb/include/usb/internal/LinkEntry.h + ${ARGV0}/drivers/usb/include/usb/internal/LinkedList.h + ${ARGV0}/drivers/usb/include/usb/internal/LinkedListBase.h + ${ARGV0}/drivers/usb/include/usb/internal/MIDIMessage.h + ${ARGV0}/drivers/usb/include/usb/internal/OperationList.h + ${ARGV0}/drivers/usb/include/usb/internal/OperationListBase.h + ${ARGV0}/drivers/usb/include/usb/internal/PolledQueue.h + ${ARGV0}/drivers/usb/include/usb/internal/Task.h + ${ARGV0}/drivers/usb/include/usb/internal/TaskBase.h + ${ARGV0}/drivers/usb/include/usb/internal/TaskQueue.h + ${ARGV0}/drivers/usb/include/usb/internal/USBAudio_Types.h + ${ARGV0}/drivers/usb/include/usb/internal/USBDescriptor.h + ${ARGV0}/drivers/usb/include/usb/internal/USBDevice.h + ${ARGV0}/drivers/usb/include/usb/internal/USBDevice_Types.h + ${ARGV0}/drivers/usb/include/usb/internal/USBHID_Types.h + ${ARGV0}/drivers/usb/source/AsyncOp.cpp + ${ARGV0}/drivers/usb/source/ByteBuffer.cpp + ${ARGV0}/drivers/usb/source/EndpointResolver.cpp + ${ARGV0}/drivers/usb/source/LinkedListBase.cpp + ${ARGV0}/drivers/usb/source/OperationListBase.cpp + ${ARGV0}/drivers/usb/source/PolledQueue.cpp + ${ARGV0}/drivers/usb/source/TaskBase.cpp + ${ARGV0}/drivers/usb/source/USBAudio.cpp + ${ARGV0}/drivers/usb/source/USBCDC.cpp + ${ARGV0}/drivers/usb/source/USBCDC_ECM.cpp + ${ARGV0}/drivers/usb/source/USBDevice.cpp + ${ARGV0}/drivers/usb/source/USBHID.cpp + ${ARGV0}/drivers/usb/source/USBKeyboard.cpp + ${ARGV0}/drivers/usb/source/USBMIDI.cpp + ${ARGV0}/drivers/usb/source/USBMSD.cpp + ${ARGV0}/drivers/usb/source/USBMouse.cpp + ${ARGV0}/drivers/usb/source/USBMouseKeyboard.cpp + ${ARGV0}/drivers/usb/source/USBSerial.cpp + ${ARGV0}/events/include/events/Event.h + ${ARGV0}/events/include/events/EventQueue.h + ${ARGV0}/events/include/events/UserAllocatedEvent.h + ${ARGV0}/events/include/events/equeue.h + ${ARGV0}/events/include/events/internal/equeue_platform.h + ${ARGV0}/events/include/events/mbed_events.h + ${ARGV0}/events/include/events/mbed_shared_queues.h + ${ARGV0}/events/source/EventQueue.cpp + ${ARGV0}/events/source/equeue.c + ${ARGV0}/events/source/equeue_mbed.cpp + ${ARGV0}/events/source/equeue_posix.c + ${ARGV0}/events/source/mbed_shared_queues.cpp + ${ARGV0}/features/frameworks/greentea-client/greentea-client/greentea_metrics.h + ${ARGV0}/features/frameworks/greentea-client/greentea-client/test_env.h + ${ARGV0}/features/frameworks/greentea-client/source/greentea_metrics.cpp + ${ARGV0}/features/frameworks/greentea-client/source/greentea_test_env.cpp + ${ARGV0}/features/frameworks/mbed-client-cli/mbed-client-cli/ns_cmdline.h + ${ARGV0}/features/frameworks/mbed-client-cli/source/ns_cmdline.c + ${ARGV0}/features/frameworks/mbed-greentea-io/mbed_io.cpp + ${ARGV0}/features/frameworks/unity/source/unity.c + ${ARGV0}/features/frameworks/unity/unity/unity.h + ${ARGV0}/features/frameworks/unity/unity/unity_config.h + ${ARGV0}/features/frameworks/unity/unity/unity_internals.h + ${ARGV0}/features/frameworks/utest/mbed-utest-shim.cpp + ${ARGV0}/features/frameworks/utest/source/unity_handler.cpp + ${ARGV0}/features/frameworks/utest/source/utest_case.cpp + ${ARGV0}/features/frameworks/utest/source/utest_default_handlers.cpp + ${ARGV0}/features/frameworks/utest/source/utest_greentea_handlers.cpp + ${ARGV0}/features/frameworks/utest/source/utest_harness.cpp + ${ARGV0}/features/frameworks/utest/source/utest_print.cpp + ${ARGV0}/features/frameworks/utest/source/utest_shim.cpp + ${ARGV0}/features/frameworks/utest/source/utest_stack_trace.cpp + ${ARGV0}/features/frameworks/utest/source/utest_types.cpp + ${ARGV0}/features/frameworks/utest/utest/unity_handler.h + ${ARGV0}/features/frameworks/utest/utest/utest.h + ${ARGV0}/features/frameworks/utest/utest/utest_case.h + ${ARGV0}/features/frameworks/utest/utest/utest_default_handlers.h + ${ARGV0}/features/frameworks/utest/utest/utest_harness.h + ${ARGV0}/features/frameworks/utest/utest/utest_print.h + ${ARGV0}/features/frameworks/utest/utest/utest_scheduler.h + ${ARGV0}/features/frameworks/utest/utest/utest_shim.h + ${ARGV0}/features/frameworks/utest/utest/utest_specification.h + ${ARGV0}/features/frameworks/utest/utest/utest_stack_trace.h + ${ARGV0}/features/frameworks/utest/utest/utest_types.h + ${ARGV0}/hal/include/hal/LowPowerTickerWrapper.h + ${ARGV0}/hal/include/hal/PinNameAliases.h + ${ARGV0}/hal/include/hal/analogin_api.h + ${ARGV0}/hal/include/hal/analogout_api.h + ${ARGV0}/hal/include/hal/buffer.h + ${ARGV0}/hal/include/hal/can_api.h + ${ARGV0}/hal/include/hal/can_helper.h + ${ARGV0}/hal/include/hal/crc_api.h + ${ARGV0}/hal/include/hal/critical_section_api.h + ${ARGV0}/hal/include/hal/dma_api.h + ${ARGV0}/hal/include/hal/flash_api.h + ${ARGV0}/hal/include/hal/gpio_api.h + ${ARGV0}/hal/include/hal/gpio_irq_api.h + ${ARGV0}/hal/include/hal/i2c_api.h + ${ARGV0}/hal/include/hal/itm_api.h + ${ARGV0}/hal/include/hal/lp_ticker_api.h + ${ARGV0}/hal/include/hal/mbed_lp_ticker_wrapper.h + ${ARGV0}/hal/include/hal/mpu_api.h + ${ARGV0}/hal/include/hal/ospi_api.h + ${ARGV0}/hal/include/hal/pinmap.h + ${ARGV0}/hal/include/hal/port_api.h + ${ARGV0}/hal/include/hal/pwmout_api.h + ${ARGV0}/hal/include/hal/qspi_api.h + ${ARGV0}/hal/include/hal/reset_reason_api.h + ${ARGV0}/hal/include/hal/rtc_api.h + ${ARGV0}/hal/include/hal/serial_api.h + ${ARGV0}/hal/include/hal/sleep_api.h + ${ARGV0}/hal/include/hal/spi_api.h + ${ARGV0}/hal/include/hal/static_pinmap.h + ${ARGV0}/hal/include/hal/ticker_api.h + ${ARGV0}/hal/include/hal/trng_api.h + ${ARGV0}/hal/include/hal/us_ticker_api.h + ${ARGV0}/hal/include/hal/watchdog_api.h + ${ARGV0}/hal/source/LowPowerTickerWrapper.cpp + ${ARGV0}/hal/source/mbed_compat.c + ${ARGV0}/hal/source/mbed_critical_section_api.c + ${ARGV0}/hal/source/mbed_flash_api.c + ${ARGV0}/hal/source/mbed_gpio.c + ${ARGV0}/hal/source/mbed_gpio_irq.c + ${ARGV0}/hal/source/mbed_itm_api.c + ${ARGV0}/hal/source/mbed_lp_ticker_api.c + ${ARGV0}/hal/source/mbed_lp_ticker_wrapper.cpp + ${ARGV0}/hal/source/mbed_pinmap_common.c + ${ARGV0}/hal/source/mbed_pinmap_default.cpp + ${ARGV0}/hal/source/mbed_ticker_api.c + ${ARGV0}/hal/source/mbed_us_ticker_api.c + ${ARGV0}/hal/source/mpu/mbed_mpu_v7m.c + ${ARGV0}/hal/source/mpu/mbed_mpu_v8m.c + ${ARGV0}/hal/source/static_pinmap.cpp + ${ARGV0}/hal/usb/include/usb/USBPhy.h + ${ARGV0}/hal/usb/include/usb/USBPhyEvents.h + ${ARGV0}/hal/usb/include/usb/USBPhyTypes.h + ${ARGV0}/hal/usb/include/usb/usb_phy_api.h + ${ARGV0}/hal/usb/source/mbed_usb_phy.cpp + ${ARGV0}/mbed.h + ${ARGV0}/platform/cxxsupport/mstd_algorithm + ${ARGV0}/platform/cxxsupport/mstd_atomic + ${ARGV0}/platform/cxxsupport/mstd_cstddef + ${ARGV0}/platform/cxxsupport/mstd_functional + ${ARGV0}/platform/cxxsupport/mstd_iterator + ${ARGV0}/platform/cxxsupport/mstd_memory + ${ARGV0}/platform/cxxsupport/mstd_mutex + ${ARGV0}/platform/cxxsupport/mstd_mutex.cpp + ${ARGV0}/platform/cxxsupport/mstd_new + ${ARGV0}/platform/cxxsupport/mstd_span + ${ARGV0}/platform/cxxsupport/mstd_tuple + ${ARGV0}/platform/cxxsupport/mstd_type_traits + ${ARGV0}/platform/cxxsupport/mstd_utility + ${ARGV0}/platform/include/platform/ATCmdParser.h + ${ARGV0}/platform/include/platform/CThunk.h + ${ARGV0}/platform/include/platform/Callback.h + ${ARGV0}/platform/include/platform/CircularBuffer.h + ${ARGV0}/platform/include/platform/CriticalSectionLock.h + ${ARGV0}/platform/include/platform/DeepSleepLock.h + ${ARGV0}/platform/include/platform/DirHandle.h + ${ARGV0}/platform/include/platform/FileBase.h + ${ARGV0}/platform/include/platform/FileHandle.h + ${ARGV0}/platform/include/platform/FileLike.h + ${ARGV0}/platform/include/platform/FilePath.h + ${ARGV0}/platform/include/platform/FileSystemHandle.h + ${ARGV0}/platform/include/platform/FileSystemLike.h + ${ARGV0}/platform/include/platform/LocalFileSystem.h + ${ARGV0}/platform/include/platform/NonCopyable.h + ${ARGV0}/platform/include/platform/PlatformMutex.h + ${ARGV0}/platform/include/platform/ScopedLock.h + ${ARGV0}/platform/include/platform/ScopedRamExecutionLock.h + ${ARGV0}/platform/include/platform/ScopedRomWriteLock.h + ${ARGV0}/platform/include/platform/SharedPtr.h + ${ARGV0}/platform/include/platform/SingletonPtr.h + ${ARGV0}/platform/include/platform/Span.h + ${ARGV0}/platform/include/platform/Stream.h + ${ARGV0}/platform/include/platform/Transaction.h + ${ARGV0}/platform/include/platform/internal/CThunkBase.h + ${ARGV0}/platform/include/platform/internal/SysTimer.h + ${ARGV0}/platform/include/platform/internal/mbed_atomic_impl.h + ${ARGV0}/platform/include/platform/internal/mbed_error_hist.h + ${ARGV0}/platform/include/platform/internal/mbed_fault_handler.h + ${ARGV0}/platform/include/platform/internal/mbed_os_timer.h + ${ARGV0}/platform/include/platform/mbed_application.h + ${ARGV0}/platform/include/platform/mbed_assert.h + ${ARGV0}/platform/include/platform/mbed_atomic.h + ${ARGV0}/platform/include/platform/mbed_chrono.h + ${ARGV0}/platform/include/platform/mbed_critical.h + ${ARGV0}/platform/include/platform/mbed_debug.h + ${ARGV0}/platform/include/platform/mbed_enum_flags.h + ${ARGV0}/platform/include/platform/mbed_error.h + ${ARGV0}/platform/include/platform/mbed_interface.h + ${ARGV0}/platform/include/platform/mbed_mem_trace.h + ${ARGV0}/platform/include/platform/mbed_mktime.h + ${ARGV0}/platform/include/platform/mbed_mpu_mgmt.h + ${ARGV0}/platform/include/platform/mbed_poll.h + ${ARGV0}/platform/include/platform/mbed_power_mgmt.h + ${ARGV0}/platform/include/platform/mbed_preprocessor.h + ${ARGV0}/platform/include/platform/mbed_retarget.h + ${ARGV0}/platform/include/platform/mbed_rtc_time.h + ${ARGV0}/platform/include/platform/mbed_semihost_api.h + ${ARGV0}/platform/include/platform/mbed_stats.h + ${ARGV0}/platform/include/platform/mbed_thread.h + ${ARGV0}/platform/include/platform/mbed_toolchain.h + ${ARGV0}/platform/include/platform/mbed_version.h + ${ARGV0}/platform/include/platform/mbed_wait_api.h + ${ARGV0}/platform/include/platform/platform.h + ${ARGV0}/platform/mbed-trace/include/mbed-trace/mbed_trace.h + ${ARGV0}/platform/mbed-trace/include/mbed-trace/ns_trace.h + ${ARGV0}/platform/mbed-trace/source/mbed_trace.c + ${ARGV0}/platform/randlib/include/mbed-client-randlib/platform/arm_hal_random.h + ${ARGV0}/platform/randlib/include/mbed-client-randlib/randLIB.h + ${ARGV0}/platform/randlib/source/randLIB.c + ${ARGV0}/platform/source/ATCmdParser.cpp + ${ARGV0}/platform/source/CThunkBase.cpp + ${ARGV0}/platform/source/CriticalSectionLock.cpp + ${ARGV0}/platform/source/DeepSleepLock.cpp + ${ARGV0}/platform/source/FileBase.cpp + ${ARGV0}/platform/source/FileHandle.cpp + ${ARGV0}/platform/source/FilePath.cpp + ${ARGV0}/platform/source/FileSystemHandle.cpp + ${ARGV0}/platform/source/LocalFileSystem.cpp + ${ARGV0}/platform/source/Stream.cpp + ${ARGV0}/platform/source/SysTimer.cpp + ${ARGV0}/platform/source/TARGET_CORTEX_M/TOOLCHAIN_GCC/except.S + ${ARGV0}/platform/source/TARGET_CORTEX_M/mbed_fault_handler.c + ${ARGV0}/platform/source/mbed_alloc_wrappers.cpp + ${ARGV0}/platform/source/mbed_application.c + ${ARGV0}/platform/source/mbed_assert.c + ${ARGV0}/platform/source/mbed_atomic_impl.c + ${ARGV0}/platform/source/mbed_board.c + ${ARGV0}/platform/source/mbed_crash_data_offsets.h + ${ARGV0}/platform/source/mbed_critical.c + ${ARGV0}/platform/source/mbed_error.c + ${ARGV0}/platform/source/mbed_error_hist.c + ${ARGV0}/platform/source/mbed_interface.c + ${ARGV0}/platform/source/mbed_mem_trace.cpp + ${ARGV0}/platform/source/mbed_mktime.c + ${ARGV0}/platform/source/mbed_mpu_mgmt.c + ${ARGV0}/platform/source/mbed_os_timer.cpp + ${ARGV0}/platform/source/mbed_poll.cpp + ${ARGV0}/platform/source/mbed_power_mgmt.c + ${ARGV0}/platform/source/mbed_retarget.cpp + ${ARGV0}/platform/source/mbed_rtc_time.cpp + ${ARGV0}/platform/source/mbed_sdk_boot.c + ${ARGV0}/platform/source/mbed_semihost_api.c + ${ARGV0}/platform/source/mbed_stats.c + ${ARGV0}/platform/source/mbed_thread.cpp + ${ARGV0}/platform/source/mbed_wait_api_no_rtos.c + ${ARGV0}/platform/source/minimal-printf/mbed_printf_armlink_overrides.c + ${ARGV0}/platform/source/minimal-printf/mbed_printf_implementation.c + ${ARGV0}/platform/source/minimal-printf/mbed_printf_implementation.h + ${ARGV0}/platform/source/minimal-printf/mbed_printf_wrapper.c + ${ARGV0}/platform/source/newlib_nano_malloc_workaround.c + ${ARGV0}/rtos/include/rtos/ConditionVariable.h + ${ARGV0}/rtos/include/rtos/EventFlags.h + ${ARGV0}/rtos/include/rtos/Kernel.h + ${ARGV0}/rtos/include/rtos/Mail.h + ${ARGV0}/rtos/include/rtos/MemoryPool.h + ${ARGV0}/rtos/include/rtos/Mutex.h + ${ARGV0}/rtos/include/rtos/Queue.h + ${ARGV0}/rtos/include/rtos/Semaphore.h + ${ARGV0}/rtos/include/rtos/ThisThread.h + ${ARGV0}/rtos/include/rtos/Thread.h + ${ARGV0}/rtos/include/rtos/internal/mbed_rtos1_types.h + ${ARGV0}/rtos/include/rtos/internal/mbed_rtos_storage.h + ${ARGV0}/rtos/include/rtos/mbed_rtos_types.h + ${ARGV0}/rtos/include/rtos/rtos.h + ${ARGV0}/rtos/source/ConditionVariable.cpp + ${ARGV0}/rtos/source/EventFlags.cpp + ${ARGV0}/rtos/source/Kernel.cpp + ${ARGV0}/rtos/source/Mutex.cpp + ${ARGV0}/rtos/source/Semaphore.cpp + ${ARGV0}/rtos/source/ThisThread.cpp + ${ARGV0}/rtos/source/Thread.cpp + ${ARGV0}/rtos/source/rtos_handlers.h + ${ARGV0}/rtos/source/rtos_idle.h + ${ARGV0}/storage/blockdevice/COMPONENT_FLASHIAP/include/FlashIAP/FlashIAPBlockDevice.h + ${ARGV0}/storage/blockdevice/COMPONENT_FLASHIAP/source/FlashIAPBlockDevice.cpp + ${ARGV0}/storage/blockdevice/include/blockdevice/BlockDevice.h + ${ARGV0}/storage/blockdevice/include/blockdevice/BufferedBlockDevice.h + ${ARGV0}/storage/blockdevice/include/blockdevice/ChainingBlockDevice.h + ${ARGV0}/storage/blockdevice/include/blockdevice/ExhaustibleBlockDevice.h + ${ARGV0}/storage/blockdevice/include/blockdevice/FlashSimBlockDevice.h + ${ARGV0}/storage/blockdevice/include/blockdevice/HeapBlockDevice.h + ${ARGV0}/storage/blockdevice/include/blockdevice/MBRBlockDevice.h + ${ARGV0}/storage/blockdevice/include/blockdevice/ObservingBlockDevice.h + ${ARGV0}/storage/blockdevice/include/blockdevice/ProfilingBlockDevice.h + ${ARGV0}/storage/blockdevice/include/blockdevice/ReadOnlyBlockDevice.h + ${ARGV0}/storage/blockdevice/include/blockdevice/SlicingBlockDevice.h + ${ARGV0}/storage/blockdevice/include/blockdevice/internal/SFDP.h + ${ARGV0}/storage/blockdevice/source/BufferedBlockDevice.cpp + ${ARGV0}/storage/blockdevice/source/ChainingBlockDevice.cpp + ${ARGV0}/storage/blockdevice/source/ExhaustibleBlockDevice.cpp + ${ARGV0}/storage/blockdevice/source/FlashSimBlockDevice.cpp + ${ARGV0}/storage/blockdevice/source/HeapBlockDevice.cpp + ${ARGV0}/storage/blockdevice/source/MBRBlockDevice.cpp + ${ARGV0}/storage/blockdevice/source/ObservingBlockDevice.cpp + ${ARGV0}/storage/blockdevice/source/ProfilingBlockDevice.cpp + ${ARGV0}/storage/blockdevice/source/ReadOnlyBlockDevice.cpp + ${ARGV0}/storage/blockdevice/source/SFDP.cpp + ${ARGV0}/storage/blockdevice/source/SlicingBlockDevice.cpp + ${ARGV0}/storage/filesystem/fat/ChaN/diskio.h + ${ARGV0}/storage/filesystem/fat/ChaN/ff.cpp + ${ARGV0}/storage/filesystem/fat/ChaN/ff.h + ${ARGV0}/storage/filesystem/fat/ChaN/ffconf.h + ${ARGV0}/storage/filesystem/fat/ChaN/ffunicode.cpp + ${ARGV0}/storage/filesystem/fat/ChaN/integer.h + ${ARGV0}/storage/filesystem/fat/include/fat/FATFileSystem.h + ${ARGV0}/storage/filesystem/fat/source/FATFileSystem.cpp + ${ARGV0}/storage/filesystem/include/filesystem/Dir.h + ${ARGV0}/storage/filesystem/include/filesystem/File.h + ${ARGV0}/storage/filesystem/include/filesystem/FileSystem.h + ${ARGV0}/storage/filesystem/include/filesystem/mbed_filesystem.h + ${ARGV0}/storage/filesystem/littlefs/include/littlefs/LittleFileSystem.h + ${ARGV0}/storage/filesystem/littlefs/littlefs/lfs.c + ${ARGV0}/storage/filesystem/littlefs/littlefs/lfs.h + ${ARGV0}/storage/filesystem/littlefs/littlefs/lfs_util.c + ${ARGV0}/storage/filesystem/littlefs/littlefs/lfs_util.h + ${ARGV0}/storage/filesystem/littlefs/source/LittleFileSystem.cpp + ${ARGV0}/storage/filesystem/littlefsv2/include/littlefsv2/LittleFileSystem2.h + ${ARGV0}/storage/filesystem/littlefsv2/littlefs/lfs2.c + ${ARGV0}/storage/filesystem/littlefsv2/littlefs/lfs2.h + ${ARGV0}/storage/filesystem/littlefsv2/littlefs/lfs2_util.c + ${ARGV0}/storage/filesystem/littlefsv2/littlefs/lfs2_util.h + ${ARGV0}/storage/filesystem/littlefsv2/source/LittleFileSystem2.cpp + ${ARGV0}/storage/filesystem/source/Dir.cpp + ${ARGV0}/storage/filesystem/source/File.cpp + ${ARGV0}/storage/filesystem/source/FileSystem.cpp + ${ARGV0}/storage/kvstore/direct_access_devicekey/include/direct_access_devicekey/DirectAccessDevicekey.h + ${ARGV0}/storage/kvstore/direct_access_devicekey/source/DirectAccessDevicekey.cpp + ${ARGV0}/storage/kvstore/filesystemstore/include/filesystemstore/FileSystemStore.h + ${ARGV0}/storage/kvstore/filesystemstore/source/FileSystemStore.cpp + ${ARGV0}/storage/kvstore/include/kvstore/KVStore.h + ${ARGV0}/storage/kvstore/kv_config/include/kv_config/kv_config.h + ${ARGV0}/storage/kvstore/kv_config/source/kv_config.cpp + ${ARGV0}/storage/kvstore/kvstore_global_api/include/kvstore_global_api/KVMap.h + ${ARGV0}/storage/kvstore/kvstore_global_api/include/kvstore_global_api/kvstore_global_api.h + ${ARGV0}/storage/kvstore/kvstore_global_api/source/KVMap.cpp + ${ARGV0}/storage/kvstore/kvstore_global_api/source/kvstore_global_api.cpp + ${ARGV0}/storage/kvstore/securestore/include/securestore/SecureStore.h + ${ARGV0}/storage/kvstore/securestore/source/SecureStore.cpp + ${ARGV0}/storage/kvstore/tdbstore/include/tdbstore/TDBStore.h + ${ARGV0}/storage/kvstore/tdbstore/source/TDBStore.cpp + ${ARGV0}/storage/platform/source/PlatformStorage.cpp + ${ARGV0}/targets/TARGET_STM/PeripheralPins.h + ${ARGV0}/targets/TARGET_STM/PinNamesTypes.h + ${ARGV0}/targets/TARGET_STM/PortNames.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/PeripheralNames.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h723xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h725xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h730xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h730xxq.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h733xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h735xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h742xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h743xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h745xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h747xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h750xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h753xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h755xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h757xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h7a3xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h7a3xxq.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h7b0xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h7b0xxq.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h7b3xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h7b3xxq.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/stm32h7xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS/system_stm32h7xx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/Legacy/stm32_hal_legacy.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_adc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_adc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_adc_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_adc_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_cec.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_cec.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_comp.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_comp.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_cordic.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_cordic.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_cortex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_cortex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_crc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_crc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_crc_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_crc_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_cryp.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_cryp.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_cryp_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_cryp_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dac.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dac.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dac_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dac_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dcmi.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dcmi.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_def.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dfsdm.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dfsdm.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dfsdm_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dfsdm_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dma.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dma.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dma2d.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dma2d.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dma_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dma_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dsi.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dsi.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dts.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_dts.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_eth.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_eth.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_eth_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_eth_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_exti.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_exti.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_fdcan.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_fdcan.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_flash.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_flash.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_flash_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_flash_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_fmac.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_fmac.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_gfxmmu.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_gfxmmu.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_gpio.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_gpio.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_gpio_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_hash.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_hash.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_hash_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_hash_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_hcd.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_hcd.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_hrtim.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_hrtim.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_hsem.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_hsem.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_i2c.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_i2c.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_i2c_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_i2c_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_i2s.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_i2s.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_i2s_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_i2s_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_irda.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_irda.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_irda_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_iwdg.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_iwdg.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_jpeg.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_jpeg.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_lptim.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_lptim.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_ltdc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_ltdc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_ltdc_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_ltdc_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_mdios.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_mdios.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_mdma.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_mdma.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_mmc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_mmc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_mmc_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_mmc_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_nand.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_nand.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_nor.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_nor.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_opamp.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_opamp.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_opamp_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_opamp_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_ospi.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_ospi.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_otfdec.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_otfdec.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_pcd.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_pcd.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_pcd_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_pcd_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_pssi.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_pssi.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_pwr.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_pwr.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_pwr_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_pwr_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_qspi.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_qspi.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_ramecc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_ramecc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rcc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rcc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rcc_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rcc_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rng.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rng.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rng_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rng_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rtc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rtc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rtc_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_rtc_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sai.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sai.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sai_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sai_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sd.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sd.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sd_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sd_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sdram.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sdram.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_smartcard.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_smartcard.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_smartcard_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_smartcard_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_smbus.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_smbus.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_spdifrx.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_spdifrx.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_spi.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_spi.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_spi_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_spi_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sram.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_sram.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_swpmi.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_swpmi.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_tim.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_tim.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_tim_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_tim_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_uart.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_uart.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_uart_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_uart_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_usart.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_usart.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_usart_ex.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_usart_ex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_wwdg.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_hal_wwdg.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_adc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_adc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_bdma.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_bdma.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_bus.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_comp.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_comp.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_cordic.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_cordic.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_cortex.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_crc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_crc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_crs.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_crs.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_dac.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_dac.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_delayblock.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_delayblock.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_dma.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_dma.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_dma2d.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_dma2d.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_dmamux.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_exti.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_exti.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_fmac.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_fmac.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_fmc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_fmc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_gpio.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_gpio.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_hrtim.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_hrtim.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_hsem.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_i2c.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_i2c.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_iwdg.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_lptim.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_lptim.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_lpuart.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_lpuart.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_mdma.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_mdma.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_opamp.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_opamp.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_pwr.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_pwr.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_rcc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_rcc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_rng.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_rng.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_rtc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_rtc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_sdmmc.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_sdmmc.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_spi.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_spi.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_swpmi.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_swpmi.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_system.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_tim.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_tim.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_usart.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_usart.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_usb.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_usb.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_utils.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_utils.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/stm32h7xx_ll_wwdg.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/stm32h7xx_hal_conf.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/system_stm32h7xx_dualcore_boot_cm4_cm7.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/system_stm32h7xx_singlecore.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/TARGET_STM32H743xI/TARGET_NUCLEO_H743ZI2/PeripheralPins.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/TARGET_STM32H743xI/TARGET_NUCLEO_H743ZI2/PinNames.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/TARGET_STM32H743xI/TARGET_NUCLEO_H743ZI2/system_clock.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/TARGET_STM32H743xI/cmsis_nvic.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/analogin_device.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/analogout_device.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/cmsis.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/flash_api.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/gpio_irq_device.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/gpio_irq_device.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/i2c_device.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/i2c_device.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/objects.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/pin_device.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/pwmout_device.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/pwmout_device.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/serial_device.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/spi_api.c + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/spi_device.h + ${ARGV0}/targets/TARGET_STM/TARGET_STM32H7/us_ticker_data.h + ${ARGV0}/targets/TARGET_STM/USBPhyHw.h + ${ARGV0}/targets/TARGET_STM/USBPhy_STM32.cpp + ${ARGV0}/targets/TARGET_STM/analogin_api.c + ${ARGV0}/targets/TARGET_STM/analogout_api.c + ${ARGV0}/targets/TARGET_STM/can_api.c + ${ARGV0}/targets/TARGET_STM/device.h + ${ARGV0}/targets/TARGET_STM/gpio_api.c + ${ARGV0}/targets/TARGET_STM/gpio_irq_api.c + ${ARGV0}/targets/TARGET_STM/gpio_object.h + ${ARGV0}/targets/TARGET_STM/hal_tick_overrides.c + ${ARGV0}/targets/TARGET_STM/i2c_api.c + ${ARGV0}/targets/TARGET_STM/lp_ticker.c + ${ARGV0}/targets/TARGET_STM/lp_ticker_defines.h + ${ARGV0}/targets/TARGET_STM/mbed_crc_api.c + ${ARGV0}/targets/TARGET_STM/mbed_overrides.c + ${ARGV0}/targets/TARGET_STM/mbed_rtx.h + ${ARGV0}/targets/TARGET_STM/nvic_addr.h + ${ARGV0}/targets/TARGET_STM/ospi_api.c + ${ARGV0}/targets/TARGET_STM/pinmap.c + ${ARGV0}/targets/TARGET_STM/port_api.c + ${ARGV0}/targets/TARGET_STM/pwmout_api.c + ${ARGV0}/targets/TARGET_STM/qspi_api.c + ${ARGV0}/targets/TARGET_STM/reset_reason.c + ${ARGV0}/targets/TARGET_STM/rtc_api.c + ${ARGV0}/targets/TARGET_STM/rtc_api_hal.h + ${ARGV0}/targets/TARGET_STM/serial_api.c + ${ARGV0}/targets/TARGET_STM/serial_api_hal.h + ${ARGV0}/targets/TARGET_STM/sleep.c + ${ARGV0}/targets/TARGET_STM/stm32_assert.h + ${ARGV0}/targets/TARGET_STM/stm_spi_api.c + ${ARGV0}/targets/TARGET_STM/trng_api.c + ${ARGV0}/targets/TARGET_STM/us_ticker.c + ${ARGV0}/targets/TARGET_STM/us_ticker_defines.h + ${ARGV0}/targets/TARGET_STM/watchdog_api.c + mbed_config.h + ) +endmacro() + +macro(target_include_directories_mbed) + target_include_directories(${ARGV0} PRIVATE + ${ARGV1}/targets/TARGET_STM/TARGET_STM32H7/TARGET_STM32H743xI/TARGET_NUCLEO_H743ZI2 + ${ARGV1}/targets/TARGET_STM/TARGET_STM32H7/TARGET_STM32H743xI + ${ARGV1}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver/Legacy + ${ARGV1}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/STM32H7xx_HAL_Driver + ${ARGV1}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW/CMSIS + ${ARGV1}/targets/TARGET_STM/TARGET_STM32H7/STM32Cube_FW + ${ARGV1}/targets/TARGET_STM/TARGET_STM32H7 + ${ARGV1}/targets/TARGET_STM + ${ARGV1}/storage/kvstore/tdbstore/include/tdbstore + ${ARGV1}/storage/kvstore/tdbstore/include + ${ARGV1}/storage/kvstore/tdbstore + ${ARGV1}/storage/kvstore/securestore/include/securestore + ${ARGV1}/storage/kvstore/securestore/include + ${ARGV1}/storage/kvstore/securestore + ${ARGV1}/storage/kvstore/kvstore_global_api/include/kvstore_global_api + ${ARGV1}/storage/kvstore/kvstore_global_api/include + ${ARGV1}/storage/kvstore/kvstore_global_api + ${ARGV1}/storage/kvstore/kv_config/include/kv_config + ${ARGV1}/storage/kvstore/kv_config/include + ${ARGV1}/storage/kvstore/kv_config + ${ARGV1}/storage/kvstore/include/kvstore + ${ARGV1}/storage/kvstore/include + ${ARGV1}/storage/kvstore/filesystemstore/include/filesystemstore + ${ARGV1}/storage/kvstore/filesystemstore/include + ${ARGV1}/storage/kvstore/filesystemstore + ${ARGV1}/storage/kvstore/direct_access_devicekey/include/direct_access_devicekey + ${ARGV1}/storage/kvstore/direct_access_devicekey/include + ${ARGV1}/storage/kvstore/direct_access_devicekey + ${ARGV1}/storage/kvstore + ${ARGV1}/storage/filesystem/littlefsv2/littlefs + ${ARGV1}/storage/filesystem/littlefsv2/include/littlefsv2 + ${ARGV1}/storage/filesystem/littlefsv2/include + ${ARGV1}/storage/filesystem/littlefsv2 + ${ARGV1}/storage/filesystem/littlefs/littlefs + ${ARGV1}/storage/filesystem/littlefs/include/littlefs + ${ARGV1}/storage/filesystem/littlefs/include + ${ARGV1}/storage/filesystem/littlefs + ${ARGV1}/storage/filesystem/include/filesystem + ${ARGV1}/storage/filesystem/include + ${ARGV1}/storage/filesystem/fat/include/fat + ${ARGV1}/storage/filesystem/fat/include + ${ARGV1}/storage/filesystem/fat/ChaN + ${ARGV1}/storage/filesystem/fat + ${ARGV1}/storage/filesystem + ${ARGV1}/storage/blockdevice/include/blockdevice/internal + ${ARGV1}/storage/blockdevice/include/blockdevice + ${ARGV1}/storage/blockdevice/include + ${ARGV1}/storage/blockdevice/COMPONENT_FLASHIAP/include/FlashIAP + ${ARGV1}/storage/blockdevice/COMPONENT_FLASHIAP/include + ${ARGV1}/storage/blockdevice/COMPONENT_FLASHIAP + ${ARGV1}/storage/blockdevice + ${ARGV1}/storage + ${ARGV1}/rtos/source + ${ARGV1}/rtos/include/rtos/internal + ${ARGV1}/rtos/include/rtos + ${ARGV1}/rtos/include + ${ARGV1}/rtos + ${ARGV1}/platform/source/minimal-printf + ${ARGV1}/platform/source + ${ARGV1}/platform/randlib/include/mbed-client-randlib/platform + ${ARGV1}/platform/randlib/include/mbed-client-randlib + ${ARGV1}/platform/randlib/include + ${ARGV1}/platform/randlib + ${ARGV1}/platform/mbed-trace/include/mbed-trace + ${ARGV1}/platform/mbed-trace/include + ${ARGV1}/platform/mbed-trace + ${ARGV1}/platform/include/platform/internal + ${ARGV1}/platform/include/platform + ${ARGV1}/platform/include + ${ARGV1}/platform/cxxsupport + ${ARGV1}/platform + ${ARGV1}/hal/usb/include/usb + ${ARGV1}/hal/usb/include + ${ARGV1}/hal/usb + ${ARGV1}/hal/include/hal + ${ARGV1}/hal/include + ${ARGV1}/hal + ${ARGV1}/features/frameworks/utest/utest + ${ARGV1}/features/frameworks/utest + ${ARGV1}/features/frameworks/unity/unity + ${ARGV1}/features/frameworks/unity + ${ARGV1}/features/frameworks/mbed-client-cli/mbed-client-cli + ${ARGV1}/features/frameworks/mbed-client-cli + ${ARGV1}/features/frameworks/greentea-client/greentea-client + ${ARGV1}/features/frameworks/greentea-client + ${ARGV1}/features/frameworks + ${ARGV1}/features + ${ARGV1}/events/include/events/internal + ${ARGV1}/events/include/events + ${ARGV1}/events/include + ${ARGV1}/events + ${ARGV1}/drivers/usb/include/usb/internal + ${ARGV1}/drivers/usb/include/usb + ${ARGV1}/drivers/usb/include + ${ARGV1}/drivers/usb + ${ARGV1}/drivers/include/drivers/interfaces + ${ARGV1}/drivers/include/drivers + ${ARGV1}/drivers/include + ${ARGV1}/drivers/device_key/include/device_key + ${ARGV1}/drivers/device_key/include + ${ARGV1}/drivers/device_key + ${ARGV1}/drivers + ${ARGV1}/connectivity/nfc/libraries/stack/transceiver + ${ARGV1}/connectivity/nfc/libraries/stack/tech/type4 + ${ARGV1}/connectivity/nfc/libraries/stack/tech/isodep + ${ARGV1}/connectivity/nfc/libraries/stack/tech/iso7816 + ${ARGV1}/connectivity/nfc/libraries/stack/tech + ${ARGV1}/connectivity/nfc/libraries/stack/platform + ${ARGV1}/connectivity/nfc/libraries/stack/ndef + ${ARGV1}/connectivity/nfc/libraries/stack + ${ARGV1}/connectivity/nfc/libraries/acore/acore + ${ARGV1}/connectivity/nfc/libraries/acore + ${ARGV1}/connectivity/nfc/libraries + ${ARGV1}/connectivity/nfc/include/nfc/ndef/common + ${ARGV1}/connectivity/nfc/include/nfc/ndef + ${ARGV1}/connectivity/nfc/include/nfc + ${ARGV1}/connectivity/nfc/include + ${ARGV1}/connectivity/nfc + ${ARGV1}/connectivity/netsocket/include/netsocket + ${ARGV1}/connectivity/netsocket/include + ${ARGV1}/connectivity/netsocket + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/libNET/src + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/libNET + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/libDHCPv6 + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/ipv6_stack + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/configs/base + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/configs + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/whiteboard + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/utils + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/random_early_detection + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/pan_blacklist + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/nist_aes_kw + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/nd_proxy + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mle_service + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns/fnet/fnet_stack/stack + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns/fnet/fnet_stack/services/serial + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns/fnet/fnet_stack/services/poll + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns/fnet/fnet_stack/services/mdns + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns/fnet/fnet_stack/services/dns + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns/fnet/fnet_stack/services + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns/fnet/fnet_stack/port/cpu + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns/fnet/fnet_stack/port/compiler + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns/fnet/fnet_stack/port + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns/fnet/fnet_stack + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns/fnet + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mdns + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/mac_neighbor_table + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/load_balance + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/ieee_802_11 + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/hmac + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/fnv_hash + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/fhss + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/etx + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/blacklist + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/Trickle + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs/Neighbor_cache + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Service_Libs + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/protocols/tls_sec_prot + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/protocols/radius_sec_prot + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/protocols/msg_sec_prot + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/protocols/key_sec_prot + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/protocols/gkh_sec_prot + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/protocols/fwh_sec_prot + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/protocols/eap_tls_sec_prot + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/protocols + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/kmp + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/eapol + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/TLS + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/PANA + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security/Common + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Security + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/RPL + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/NWK_INTERFACE/Include + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/NWK_INTERFACE + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/MPL + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/MLE + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/MAC/virtual_rf + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/MAC/IEEE802_15_4 + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/MAC + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/DHCPv6_client + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/DHCPv6_Server + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Core/include + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Core + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/Common_Protocols + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/BorderRouter + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/6LoWPAN/ws + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/6LoWPAN/Thread + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/6LoWPAN/NVM + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/6LoWPAN/ND + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/6LoWPAN/Mesh + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/6LoWPAN/MAC + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/6LoWPAN/IPHC_Decode + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/6LoWPAN/Fragmentation + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/6LoWPAN/Bootstraps + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source/6LoWPAN + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/source + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/nanostack/platform + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack/nanostack + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack-eventloop/source + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack-eventloop/nanostack-event-loop/platform + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack-eventloop/nanostack-event-loop + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack-eventloop + ${ARGV1}/connectivity/nanostack/sal-stack-nanostack + ${ARGV1}/connectivity/nanostack/nanostack-hal-mbed-cmsis-rtos + ${ARGV1}/connectivity/nanostack/mbed-mesh-api/source/include + ${ARGV1}/connectivity/nanostack/mbed-mesh-api/source + ${ARGV1}/connectivity/nanostack/mbed-mesh-api/mbed-mesh-api + ${ARGV1}/connectivity/nanostack/mbed-mesh-api + ${ARGV1}/connectivity/nanostack/include/nanostack-interface + ${ARGV1}/connectivity/nanostack/include + ${ARGV1}/connectivity/nanostack/coap-service/source/include + ${ARGV1}/connectivity/nanostack/coap-service/source + ${ARGV1}/connectivity/nanostack/coap-service/coap-service + ${ARGV1}/connectivity/nanostack/coap-service + ${ARGV1}/connectivity/nanostack + ${ARGV1}/connectivity/mbedtls/source + ${ARGV1}/connectivity/mbedtls/platform/inc + ${ARGV1}/connectivity/mbedtls/platform + ${ARGV1}/connectivity/mbedtls/include/mbedtls + ${ARGV1}/connectivity/mbedtls/include + ${ARGV1}/connectivity/mbedtls + ${ARGV1}/connectivity/lwipstack/lwip/src/include/netif + ${ARGV1}/connectivity/lwipstack/lwip/src/include/lwip/prot + ${ARGV1}/connectivity/lwipstack/lwip/src/include/lwip/priv + ${ARGV1}/connectivity/lwipstack/lwip/src/include/lwip + ${ARGV1}/connectivity/lwipstack/lwip/src/include/compat/posix/sys + ${ARGV1}/connectivity/lwipstack/lwip/src/include/compat/posix/net + ${ARGV1}/connectivity/lwipstack/lwip/src/include/compat/posix/arpa + ${ARGV1}/connectivity/lwipstack/lwip/src/include/compat/posix + ${ARGV1}/connectivity/lwipstack/lwip/src/include/compat + ${ARGV1}/connectivity/lwipstack/lwip/src/include + ${ARGV1}/connectivity/lwipstack/lwip/src + ${ARGV1}/connectivity/lwipstack/lwip-sys/arch + ${ARGV1}/connectivity/lwipstack/lwip-sys + ${ARGV1}/connectivity/lwipstack/lwip + ${ARGV1}/connectivity/lwipstack/include/lwipstack + ${ARGV1}/connectivity/lwipstack/include + ${ARGV1}/connectivity/lwipstack + ${ARGV1}/connectivity/lorawan/system + ${ARGV1}/connectivity/lorawan/lorastack/phy + ${ARGV1}/connectivity/lorawan/lorastack/mac + ${ARGV1}/connectivity/lorawan/lorastack + ${ARGV1}/connectivity/lorawan/include/lorawan + ${ARGV1}/connectivity/lorawan/include + ${ARGV1}/connectivity/lorawan + ${ARGV1}/connectivity/libraries/ppp/include/ppp + ${ARGV1}/connectivity/libraries/ppp/include/polarssl + ${ARGV1}/connectivity/libraries/ppp/include + ${ARGV1}/connectivity/libraries/ppp + ${ARGV1}/connectivity/libraries/nanostack-libservice/mbed-client-libservice/platform + ${ARGV1}/connectivity/libraries/nanostack-libservice/mbed-client-libservice + ${ARGV1}/connectivity/libraries/nanostack-libservice + ${ARGV1}/connectivity/libraries/mbed-coap/source/include + ${ARGV1}/connectivity/libraries/mbed-coap/source + ${ARGV1}/connectivity/libraries/mbed-coap/mbed-coap + ${ARGV1}/connectivity/libraries/mbed-coap + ${ARGV1}/connectivity/libraries + ${ARGV1}/connectivity/drivers/wifi/esp8266-driver/ESP8266 + ${ARGV1}/connectivity/drivers/wifi/esp8266-driver + ${ARGV1}/connectivity/drivers/wifi + ${ARGV1}/connectivity/drivers/nfc/PN512/source/transceiver + ${ARGV1}/connectivity/drivers/nfc/PN512/source + ${ARGV1}/connectivity/drivers/nfc/PN512/include/nfc/controllers + ${ARGV1}/connectivity/drivers/nfc/PN512/include/nfc + ${ARGV1}/connectivity/drivers/nfc/PN512/include + ${ARGV1}/connectivity/drivers/nfc/PN512 + ${ARGV1}/connectivity/drivers/nfc + ${ARGV1}/connectivity/drivers/mbedtls/TARGET_STM + ${ARGV1}/connectivity/drivers/emac/TARGET_STM/TARGET_STM32H7/lan8742 + ${ARGV1}/connectivity/drivers/emac/TARGET_STM/TARGET_STM32H7 + ${ARGV1}/connectivity/drivers/emac/TARGET_STM + ${ARGV1}/connectivity/drivers/cellular/UBLOX/PPP + ${ARGV1}/connectivity/drivers/cellular/UBLOX/N2XX + ${ARGV1}/connectivity/drivers/cellular/UBLOX/AT + ${ARGV1}/connectivity/drivers/cellular/UBLOX + ${ARGV1}/connectivity/drivers/cellular/TELIT/ME910 + ${ARGV1}/connectivity/drivers/cellular/TELIT/ME310 + ${ARGV1}/connectivity/drivers/cellular/TELIT/HE910 + ${ARGV1}/connectivity/drivers/cellular/TELIT + ${ARGV1}/connectivity/drivers/cellular/RiotMicro/AT + ${ARGV1}/connectivity/drivers/cellular/RiotMicro + ${ARGV1}/connectivity/drivers/cellular/QUECTEL/UG96 + ${ARGV1}/connectivity/drivers/cellular/QUECTEL/M26 + ${ARGV1}/connectivity/drivers/cellular/QUECTEL/EC2X + ${ARGV1}/connectivity/drivers/cellular/QUECTEL/BG96 + ${ARGV1}/connectivity/drivers/cellular/QUECTEL/BC95 + ${ARGV1}/connectivity/drivers/cellular/QUECTEL + ${ARGV1}/connectivity/drivers/cellular/MultiTech/DragonflyNano/PPP + ${ARGV1}/connectivity/drivers/cellular/MultiTech/DragonflyNano + ${ARGV1}/connectivity/drivers/cellular/MultiTech + ${ARGV1}/connectivity/drivers/cellular/GENERIC/GENERIC_AT3GPP + ${ARGV1}/connectivity/drivers/cellular/GENERIC + ${ARGV1}/connectivity/drivers/cellular/GEMALTO/CINTERION + ${ARGV1}/connectivity/drivers/cellular/GEMALTO + ${ARGV1}/connectivity/drivers/cellular/Altair/ALT1250/PPP + ${ARGV1}/connectivity/drivers/cellular/Altair/ALT1250 + ${ARGV1}/connectivity/drivers/cellular/Altair + ${ARGV1}/connectivity/drivers/cellular + ${ARGV1}/connectivity/drivers/802.15.4_RF/stm-s2lp-rf-driver/stm-s2lp-rf-driver + ${ARGV1}/connectivity/drivers/802.15.4_RF/stm-s2lp-rf-driver/source + ${ARGV1}/connectivity/drivers/802.15.4_RF/stm-s2lp-rf-driver + ${ARGV1}/connectivity/drivers/802.15.4_RF/mcr20a-rf-driver/source + ${ARGV1}/connectivity/drivers/802.15.4_RF/mcr20a-rf-driver/mcr20a-rf-driver + ${ARGV1}/connectivity/drivers/802.15.4_RF/mcr20a-rf-driver + ${ARGV1}/connectivity/drivers/802.15.4_RF/atmel-rf-driver/source + ${ARGV1}/connectivity/drivers/802.15.4_RF/atmel-rf-driver/atmel-rf-driver + ${ARGV1}/connectivity/drivers/802.15.4_RF/atmel-rf-driver + ${ARGV1}/connectivity/drivers/802.15.4_RF + ${ARGV1}/connectivity/drivers + ${ARGV1}/connectivity/cellular/include/cellular/framework/device + ${ARGV1}/connectivity/cellular/include/cellular/framework/common + ${ARGV1}/connectivity/cellular/include/cellular/framework/AT + ${ARGV1}/connectivity/cellular/include/cellular/framework/API + ${ARGV1}/connectivity/cellular/include/cellular/framework + ${ARGV1}/connectivity/cellular/include/cellular + ${ARGV1}/connectivity/cellular/include + ${ARGV1}/connectivity/cellular + ${ARGV1}/connectivity + ${ARGV1}/cmsis/device/rtos/include + ${ARGV1}/cmsis/device/rtos + ${ARGV1}/cmsis/device/RTE/include + ${ARGV1}/cmsis/device/RTE + ${ARGV1}/cmsis/device + ${ARGV1}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M/Include + ${ARGV1}/cmsis/CMSIS_5/CMSIS/TARGET_CORTEX_M + ${ARGV1}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source + ${ARGV1}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Include1 + ${ARGV1}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Include + ${ARGV1}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Config + ${ARGV1}/cmsis/CMSIS_5/CMSIS/RTOS2/RTX + ${ARGV1}/cmsis/CMSIS_5/CMSIS/RTOS2/Include + ${ARGV1}/cmsis/CMSIS_5/CMSIS/RTOS2 + ${ARGV1}/cmsis/CMSIS_5/CMSIS + ${ARGV1}/cmsis/CMSIS_5 + ${ARGV1}/cmsis + ${ARGV1} + ) + +endmacro() diff --git a/onert-micro/tests/mbed-os/mbed_config.h b/onert-micro/tests/mbed-os/mbed_config.h new file mode 100644 index 0000000..5649f46 --- /dev/null +++ b/onert-micro/tests/mbed-os/mbed_config.h @@ -0,0 +1,488 @@ +/* + * mbed SDK + * Copyright (c) 2017 ARM Limited + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Automatically generated configuration file. +// DO NOT EDIT, content will be overwritten. + +#ifndef __MBED_CONFIG_DATA__ +#define __MBED_CONFIG_DATA__ + +// Configuration parameters +#define CLOCK_SOURCE USE_PLL_HSE_EXTC | USE_PLL_HSI // set by target:MCU_STM32H7 +#define HSE_VALUE 8000000 // set by target:NUCLEO_H743ZI2 +#define LPTICKER_DELAY_TICKS 0 // set by target:MCU_STM32H7 +#define MBED_CONF_ALT1250_PPP_BAUDRATE 115200 // set by library:ALT1250_PPP +#define MBED_CONF_ALT1250_PPP_PROVIDE_DEFAULT 0 // set by library:ALT1250_PPP +#define MBED_CONF_ATMEL_RF_ASSUME_SPACED_SPI 1 // set by library:atmel-rf[STM] +#define MBED_CONF_ATMEL_RF_FULL_SPI_SPEED 7500000 // set by library:atmel-rf +#define MBED_CONF_ATMEL_RF_FULL_SPI_SPEED_BYTE_SPACING 250 // set by library:atmel-rf +#define MBED_CONF_ATMEL_RF_IRQ_THREAD_STACK_SIZE 1024 // set by library:atmel-rf +#define MBED_CONF_ATMEL_RF_LOW_SPI_SPEED 3750000 // set by library:atmel-rf +#define MBED_CONF_ATMEL_RF_PROVIDE_DEFAULT 0 // set by library:atmel-rf +#define MBED_CONF_ATMEL_RF_USE_SPI_SPACING_API 0 // set by library:atmel-rf +#define MBED_CONF_CELLULAR_AT_HANDLER_BUFFER_SIZE 32 // set by library:cellular +#define MBED_CONF_CELLULAR_CONTROL_PLANE_OPT 0 // set by library:cellular +#define MBED_CONF_CELLULAR_DEBUG_AT 0 // set by library:cellular +#define MBED_CONF_CELLULAR_MAX_CP_DATA_RECV_LEN 1358 // set by library:cellular +#define MBED_CONF_CELLULAR_PRESENT 1 // set by library:cellular +#define MBED_CONF_CELLULAR_RANDOM_MAX_START_DELAY 0 // set by library:cellular +#define MBED_CONF_CELLULAR_USE_APN_LOOKUP 0 // set by library:cellular +#define MBED_CONF_CELLULAR_USE_SMS 0 // set by library:cellular +#define MBED_CONF_DRIVERS_OSPI_CSN OSPI_FLASH1_CSN // set by library:drivers +#define MBED_CONF_DRIVERS_OSPI_DQS OSPI_FLASH1_DQS // set by library:drivers +#define MBED_CONF_DRIVERS_OSPI_IO0 OSPI_FLASH1_IO0 // set by library:drivers +#define MBED_CONF_DRIVERS_OSPI_IO1 OSPI_FLASH1_IO1 // set by library:drivers +#define MBED_CONF_DRIVERS_OSPI_IO2 OSPI_FLASH1_IO2 // set by library:drivers +#define MBED_CONF_DRIVERS_OSPI_IO3 OSPI_FLASH1_IO3 // set by library:drivers +#define MBED_CONF_DRIVERS_OSPI_IO4 OSPI_FLASH1_IO4 // set by library:drivers +#define MBED_CONF_DRIVERS_OSPI_IO5 OSPI_FLASH1_IO5 // set by library:drivers +#define MBED_CONF_DRIVERS_OSPI_IO6 OSPI_FLASH1_IO6 // set by library:drivers +#define MBED_CONF_DRIVERS_OSPI_IO7 OSPI_FLASH1_IO7 // set by library:drivers +#define MBED_CONF_DRIVERS_OSPI_SCK OSPI_FLASH1_SCK // set by library:drivers +#define MBED_CONF_DRIVERS_QSPI_CSN QSPI_FLASH1_CSN // set by library:drivers +#define MBED_CONF_DRIVERS_QSPI_IO0 QSPI_FLASH1_IO0 // set by library:drivers +#define MBED_CONF_DRIVERS_QSPI_IO1 QSPI_FLASH1_IO1 // set by library:drivers +#define MBED_CONF_DRIVERS_QSPI_IO2 QSPI_FLASH1_IO2 // set by library:drivers +#define MBED_CONF_DRIVERS_QSPI_IO3 QSPI_FLASH1_IO3 // set by library:drivers +#define MBED_CONF_DRIVERS_QSPI_SCK QSPI_FLASH1_SCK // set by library:drivers +#define MBED_CONF_DRIVERS_UART_SERIAL_RXBUF_SIZE 256 // set by library:drivers +#define MBED_CONF_DRIVERS_UART_SERIAL_TXBUF_SIZE 256 // set by library:drivers +#define MBED_CONF_ESP8266_BUILT_IN_DNS 0 // set by library:esp8266 +#define MBED_CONF_ESP8266_DEBUG 0 // set by library:esp8266 +#define MBED_CONF_ESP8266_POWER_OFF_TIME_MS 3 // set by library:esp8266 +#define MBED_CONF_ESP8266_POWER_ON_POLARITY 0 // set by library:esp8266 +#define MBED_CONF_ESP8266_POWER_ON_TIME_MS 3 // set by library:esp8266 +#define MBED_CONF_ESP8266_PROVIDE_DEFAULT 0 // set by library:esp8266 +#define MBED_CONF_ESP8266_SERIAL_BAUDRATE 115200 // set by library:esp8266 +#define MBED_CONF_ESP8266_SNTP_ENABLE 0 // set by library:esp8266 +#define MBED_CONF_ESP8266_SNTP_SERVER0 "" // set by library:esp8266 +#define MBED_CONF_ESP8266_SNTP_SERVER1 "" // set by library:esp8266 +#define MBED_CONF_ESP8266_SNTP_SERVER2 "" // set by library:esp8266 +#define MBED_CONF_ESP8266_SNTP_TIMEZONE 0 // set by library:esp8266 +#define MBED_CONF_ESP8266_SOCKET_BUFSIZE 8192 // set by library:esp8266 +#define MBED_CONF_EVENTS_PRESENT 1 // set by library:events +#define MBED_CONF_EVENTS_SHARED_DISPATCH_FROM_APPLICATION 0 // set by library:events +#define MBED_CONF_EVENTS_SHARED_EVENTSIZE 768 // set by library:events +#define MBED_CONF_EVENTS_SHARED_HIGHPRIO_EVENTSIZE 256 // set by library:events +#define MBED_CONF_EVENTS_SHARED_HIGHPRIO_STACKSIZE 1024 // set by library:events +#define MBED_CONF_EVENTS_SHARED_STACKSIZE 2048 // set by library:events +#define MBED_CONF_EVENTS_USE_LOWPOWER_TIMER_TICKER 0 // set by library:events +#define MBED_CONF_FAT_CHAN_FFS_DBG 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_CODE_PAGE 437 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_FS_EXFAT 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_FS_HEAPBUF 1 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_FS_LOCK 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_FS_MINIMIZE 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_FS_NOFSINFO 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_FS_NORTC 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_FS_READONLY 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_FS_REENTRANT 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_FS_RPATH 1 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_FS_TIMEOUT 1000 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_FS_TINY 1 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_LFN_BUF 255 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_LFN_UNICODE 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_MAX_LFN 255 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_MAX_SS 4096 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_MIN_SS 512 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_MULTI_PARTITION 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_NORTC_MDAY 1 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_NORTC_MON 1 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_NORTC_YEAR 2017 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_PRINT_FLOAT 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_PRINT_LLI 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_SFN_BUF 12 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_STRF_ENCODE 3 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_STR_VOLUME_ID 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_SYNC_T HANDLE // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_USE_CHMOD 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_USE_EXPAND 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_USE_FASTSEEK 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_USE_FIND 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_USE_FORWARD 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_USE_LABEL 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_USE_LFN 3 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_USE_MKFS 1 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_USE_STRFUNC 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_USE_TRIM 1 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_VOLUMES 4 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FF_VOLUME_STRS \ + "RAM", "NAND", "CF", "SD", "SD2", "USB", "USB2", "USB3" // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FLUSH_ON_NEW_CLUSTER 0 // set by library:fat_chan +#define MBED_CONF_FAT_CHAN_FLUSH_ON_NEW_SECTOR 1 // set by library:fat_chan +#define MBED_CONF_FILESYSTEM_PRESENT 1 // set by library:filesystem +#define MBED_CONF_FLASHIAP_BLOCK_DEVICE_BASE_ADDRESS \ + 0xFFFFFFFF // set by library:flashiap-block-device +#define MBED_CONF_FLASHIAP_BLOCK_DEVICE_SIZE 0 // set by library:flashiap-block-device +#define MBED_CONF_GEMALTO_CINTERION_BAUDRATE 115200 // set by library:GEMALTO_CINTERION +#define MBED_CONF_GEMALTO_CINTERION_PROVIDE_DEFAULT 0 // set by library:GEMALTO_CINTERION +#define MBED_CONF_GENERIC_AT3GPP_BAUDRATE 115200 // set by library:GENERIC_AT3GPP +#define MBED_CONF_GENERIC_AT3GPP_PROVIDE_DEFAULT 0 // set by library:GENERIC_AT3GPP +#define MBED_CONF_LORA_ADR_ON 1 // set by library:lora +#define MBED_CONF_LORA_APPLICATION_EUI \ + { \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ + } // set by library:lora +#define MBED_CONF_LORA_APPLICATION_KEY \ + { \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ + } // set by library:lora +#define MBED_CONF_LORA_APPSKEY \ + { \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ + } // set by library:lora +#define MBED_CONF_LORA_APP_PORT 15 // set by library:lora +#define MBED_CONF_LORA_AUTOMATIC_UPLINK_MESSAGE 1 // set by library:lora +#define MBED_CONF_LORA_DEVICE_ADDRESS 0x00000000 // set by library:lora +#define MBED_CONF_LORA_DEVICE_EUI \ + { \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ + } // set by library:lora +#define MBED_CONF_LORA_DOWNLINK_PREAMBLE_LENGTH 5 // set by library:lora +#define MBED_CONF_LORA_DUTY_CYCLE_ON 1 // set by library:lora +#define MBED_CONF_LORA_DUTY_CYCLE_ON_JOIN 1 // set by library:lora +#define MBED_CONF_LORA_FSB_MASK \ + { \ + 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x00FF \ + } // set by library:lora +#define MBED_CONF_LORA_FSB_MASK_CHINA \ + { \ + 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF \ + } // set by library:lora +#define MBED_CONF_LORA_LBT_ON 0 // set by library:lora +#define MBED_CONF_LORA_MAX_SYS_RX_ERROR 5 // set by library:lora +#define MBED_CONF_LORA_NB_TRIALS 12 // set by library:lora +#define MBED_CONF_LORA_NWKSKEY \ + { \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ + } // set by library:lora +#define MBED_CONF_LORA_OVER_THE_AIR_ACTIVATION 1 // set by library:lora +#define MBED_CONF_LORA_PHY EU868 // set by library:lora +#define MBED_CONF_LORA_PUBLIC_NETWORK 1 // set by library:lora +#define MBED_CONF_LORA_TX_MAX_SIZE 64 // set by library:lora +#define MBED_CONF_LORA_UPLINK_PREAMBLE_LENGTH 8 // set by library:lora +#define MBED_CONF_LORA_WAKEUP_TIME 5 // set by library:lora +#define MBED_CONF_LWIP_ADDR_TIMEOUT 5 // set by library:lwip +#define MBED_CONF_LWIP_ADDR_TIMEOUT_MODE 1 // set by library:lwip +#define MBED_CONF_LWIP_DEBUG_ENABLED 0 // set by library:lwip +#define MBED_CONF_LWIP_DEFAULT_THREAD_STACKSIZE 512 // set by library:lwip +#define MBED_CONF_LWIP_DHCP_TIMEOUT 60 // set by library:lwip +#define MBED_CONF_LWIP_ENABLE_PPP_TRACE 0 // set by library:lwip +#define MBED_CONF_LWIP_ETHERNET_ENABLED 1 // set by library:lwip +#define MBED_CONF_LWIP_IPV4_ENABLED 1 // set by library:lwip +#define MBED_CONF_LWIP_IPV6_ENABLED 0 // set by library:lwip +#define MBED_CONF_LWIP_IP_VER_PREF 4 // set by library:lwip +#define MBED_CONF_LWIP_L3IP_ENABLED 0 // set by library:lwip +#define MBED_CONF_LWIP_MBOX_SIZE 8 // set by library:lwip +#define MBED_CONF_LWIP_MEMP_NUM_TCPIP_MSG_INPKT 8 // set by library:lwip +#define MBED_CONF_LWIP_MEMP_NUM_TCP_SEG 16 // set by library:lwip +#define MBED_CONF_LWIP_MEM_SIZE 2310 // set by library:lwip[STM] +#define MBED_CONF_LWIP_ND6_QUEUEING 0 // set by library:lwip +#define MBED_CONF_LWIP_ND6_RDNSS_MAX_DNS_SERVERS 0 // set by library:lwip +#define MBED_CONF_LWIP_NUM_NETBUF 8 // set by library:lwip +#define MBED_CONF_LWIP_NUM_PBUF 8 // set by library:lwip +#define MBED_CONF_LWIP_PBUF_POOL_SIZE 5 // set by library:lwip +#define MBED_CONF_LWIP_PPP_ENABLED 0 // set by library:lwip +#define MBED_CONF_LWIP_PPP_IPV4_ENABLED 0 // set by library:lwip +#define MBED_CONF_LWIP_PPP_IPV6_ENABLED 0 // set by library:lwip +#define MBED_CONF_LWIP_PPP_THREAD_STACKSIZE 768 // set by library:lwip +#define MBED_CONF_LWIP_PRESENT 1 // set by library:lwip +#define MBED_CONF_LWIP_RAW_SOCKET_ENABLED 0 // set by library:lwip +#define MBED_CONF_LWIP_SOCKET_MAX 4 // set by library:lwip +#define MBED_CONF_LWIP_TCPIP_THREAD_PRIORITY osPriorityNormal // set by library:lwip +#define MBED_CONF_LWIP_TCPIP_THREAD_STACKSIZE 1200 // set by library:lwip +#define MBED_CONF_LWIP_TCP_CLOSE_TIMEOUT 1000 // set by library:lwip +#define MBED_CONF_LWIP_TCP_ENABLED 1 // set by library:lwip +#define MBED_CONF_LWIP_TCP_MAXRTX 6 // set by library:lwip +#define MBED_CONF_LWIP_TCP_MSS 536 // set by library:lwip +#define MBED_CONF_LWIP_TCP_SERVER_MAX 4 // set by library:lwip +#define MBED_CONF_LWIP_TCP_SND_BUF (2 * TCP_MSS) // set by library:lwip +#define MBED_CONF_LWIP_TCP_SOCKET_MAX 4 // set by library:lwip +#define MBED_CONF_LWIP_TCP_SYNMAXRTX 6 // set by library:lwip +#define MBED_CONF_LWIP_TCP_WND (4 * TCP_MSS) // set by library:lwip +#define MBED_CONF_LWIP_UDP_SOCKET_MAX 4 // set by library:lwip +#define MBED_CONF_LWIP_USE_MBED_TRACE 0 // set by library:lwip +#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_CHANNEL 0 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_CHANNEL_MASK 0x7fff800 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_CHANNEL_PAGE 0 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_DEVICE_TYPE \ + NET_6LOWPAN_ROUTER // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_PANID_FILTER 0xffff // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_PSK_KEY \ + { \ + 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf \ + } // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_PSK_KEY_ID 1 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_SECURITY_MODE NONE // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_6LOWPAN_ND_SEC_LEVEL 5 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_HEAP_SIZE 32500 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_HEAP_STAT_INFO NULL // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_MAC_NEIGH_TABLE_SIZE 32 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_RADIUS_RETRY_COUNT 3 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_RADIUS_RETRY_IMAX 30 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_RADIUS_RETRY_IMIN 20 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_SYSTEM_TIME_UPDATE_FROM_NANOSTACK 1 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_CHANNEL 22 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_CHANNEL_MASK 0x7fff800 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_CHANNEL_PAGE 0 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_COMMISSIONING_DATASET_TIMESTAMP \ + 0x10000 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_EXTENDED_PANID \ + { \ + 0xf1, 0xb5, 0xa1, 0xb2, 0xc4, 0xd5, 0xa1, 0xbd \ + } // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_ML_PREFIX \ + { \ + 0xfd, 0x0, 0x0d, 0xb8, 0x0, 0x0, 0x0, 0x0 \ + } // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_NETWORK_NAME \ + "Thread Network" // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_PANID 0x0700 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_CONFIG_PSKC \ + { \ + 0xc8, 0xa6, 0x2e, 0xae, 0xf3, 0x68, 0xf3, 0x46, 0xa9, 0x9e, 0x57, 0x85, 0x98, 0x9d, 0x1c, 0xd0 \ + } // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_DEVICE_TYPE \ + MESH_DEVICE_TYPE_THREAD_ROUTER // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_MASTER_KEY \ + { \ + 0x10, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff \ + } // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_PSKD "ABCDEFGH" // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_SECURITY_POLICY 255 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_THREAD_USE_STATIC_LINK_CONFIG 1 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_USE_MALLOC_FOR_HEAP 0 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_BC_CHANNEL_FUNCTION 255 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_BC_DWELL_INTERVAL 0 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_BC_FIXED_CHANNEL 65535 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_BC_INTERVAL 0 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_CHANNEL_PLAN_ID 255 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_DEVICE_TYPE \ + MESH_DEVICE_TYPE_WISUN_ROUTER // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_NETWORK_NAME "Wi-SUN Network" // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_OPERATING_CLASS 255 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_OPERATING_MODE 255 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_PHY_MODE_ID 255 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_REGULATORY_DOMAIN 3 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_UC_CHANNEL_FUNCTION 255 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_UC_DWELL_INTERVAL 255 // set by library:mbed-mesh-api +#define MBED_CONF_MBED_MESH_API_WISUN_UC_FIXED_CHANNEL 65535 // set by library:mbed-mesh-api +#define MBED_CONF_MCR20A_PROVIDE_DEFAULT 0 // set by library:mcr20a +#define MBED_CONF_NANOSTACK_CONFIGURATION nanostack_full // set by library:nanostack +#define MBED_CONF_NANOSTACK_HAL_CRITICAL_SECTION_USABLE_FROM_INTERRUPT \ + 0 // set by library:nanostack-hal +#define MBED_CONF_NANOSTACK_HAL_EVENT_LOOP_DISPATCH_FROM_APPLICATION \ + 0 // set by library:nanostack-hal +#define MBED_CONF_NANOSTACK_HAL_EVENT_LOOP_THREAD_STACK_SIZE 6144 // set by library:nanostack-hal +#define MBED_CONF_NANOSTACK_HAL_EVENT_LOOP_USE_MBED_EVENTS 0 // set by library:nanostack-hal +#define MBED_CONF_NANOSTACK_HAL_KVSTORE_PATH "/kv/" // set by library:nanostack-hal +#define MBED_CONF_NANOSTACK_HAL_USE_KVSTORE 0 // set by library:nanostack-hal +#define MBED_CONF_NANOSTACK_LIBSERVICE_NSDYNMEM_TRACKER_ENABLED \ + 0 // set by library:nanostack-libservice +#define MBED_CONF_NANOSTACK_LIBSERVICE_PRESENT 1 // set by library:nanostack-libservice +#define MBED_CONF_NSAPI_ADD_EVENT_LISTENER_RETURN_CHANGE 0 // set by library:nsapi +#define MBED_CONF_NSAPI_DEFAULT_MESH_TYPE THREAD // set by library:nsapi +#define MBED_CONF_NSAPI_DEFAULT_STACK LWIP // set by library:nsapi +#define MBED_CONF_NSAPI_DEFAULT_WIFI_SECURITY NONE // set by library:nsapi +#define MBED_CONF_NSAPI_DNS_ADDRESSES_LIMIT 10 // set by library:nsapi +#define MBED_CONF_NSAPI_DNS_CACHE_SIZE 3 // set by library:nsapi +#define MBED_CONF_NSAPI_DNS_RESPONSE_WAIT_TIME 10000 // set by library:nsapi +#define MBED_CONF_NSAPI_DNS_RETRIES 1 // set by library:nsapi +#define MBED_CONF_NSAPI_DNS_TOTAL_ATTEMPTS 10 // set by library:nsapi +#define MBED_CONF_NSAPI_PRESENT 1 // set by library:nsapi +#define MBED_CONF_NSAPI_SOCKET_STATS_ENABLED 0 // set by library:nsapi +#define MBED_CONF_NSAPI_SOCKET_STATS_MAX_COUNT 10 // set by library:nsapi +#define MBED_CONF_PLATFORM_CALLBACK_COMPARABLE 1 // set by library:platform +#define MBED_CONF_PLATFORM_CALLBACK_NONTRIVIAL 0 // set by library:platform +#define MBED_CONF_PLATFORM_CRASH_CAPTURE_ENABLED 0 // set by library:platform +#define MBED_CONF_PLATFORM_CTHUNK_COUNT_MAX 8 // set by library:platform +#define MBED_CONF_PLATFORM_DEEPSLEEP_STATS_VERBOSE 0 // set by library:platform[STM] +#define MBED_CONF_PLATFORM_DEFAULT_SERIAL_BAUD_RATE 9600 // set by library:platform +#define MBED_CONF_PLATFORM_ERROR_ALL_THREADS_INFO 0 // set by library:platform +#define MBED_CONF_PLATFORM_ERROR_FILENAME_CAPTURE_ENABLED 0 // set by library:platform +#define MBED_CONF_PLATFORM_ERROR_HIST_ENABLED 0 // set by library:platform +#define MBED_CONF_PLATFORM_ERROR_HIST_SIZE 4 // set by library:platform +#define MBED_CONF_PLATFORM_ERROR_REBOOT_MAX 1 // set by library:platform +#define MBED_CONF_PLATFORM_FATAL_ERROR_AUTO_REBOOT_ENABLED 0 // set by library:platform +#define MBED_CONF_PLATFORM_MAX_ERROR_FILENAME_LEN 16 // set by library:platform +#define MBED_CONF_PLATFORM_MINIMAL_PRINTF_ENABLE_64_BIT 1 // set by library:platform +#define MBED_CONF_PLATFORM_MINIMAL_PRINTF_ENABLE_FLOATING_POINT 0 // set by library:platform +#define MBED_CONF_PLATFORM_MINIMAL_PRINTF_SET_FLOATING_POINT_MAX_DECIMALS \ + 6 // set by library:platform +#define MBED_CONF_PLATFORM_POLL_USE_LOWPOWER_TIMER 0 // set by library:platform +#define MBED_CONF_PLATFORM_STDIO_BAUD_RATE 9600 // set by library:platform +#define MBED_CONF_PLATFORM_STDIO_BUFFERED_SERIAL 0 // set by library:platform +#define MBED_CONF_PLATFORM_STDIO_CONVERT_NEWLINES 1 // set by library:platform +#define MBED_CONF_PLATFORM_STDIO_CONVERT_TTY_NEWLINES 1 // set by library:platform +#define MBED_CONF_PLATFORM_STDIO_FLUSH_AT_EXIT 1 // set by library:platform +#define MBED_CONF_PLATFORM_STDIO_MINIMAL_CONSOLE_ONLY 0 // set by library:platform +#define MBED_CONF_PLATFORM_USE_MPU 1 // set by library:platform +#define MBED_CONF_PPP_ENABLED 0 // set by library:ppp +#define MBED_CONF_PPP_ENABLE_TRACE 0 // set by library:ppp +#define MBED_CONF_PPP_IPV4_ENABLED 1 // set by library:ppp +#define MBED_CONF_PPP_IPV6_ENABLED 0 // set by library:ppp +#define MBED_CONF_PPP_MBED_EVENT_QUEUE 0 // set by library:ppp +#define MBED_CONF_PPP_THREAD_STACKSIZE 816 // set by library:ppp +#define MBED_CONF_QUECTEL_BC95_BAUDRATE 9600 // set by library:QUECTEL_BC95 +#define MBED_CONF_QUECTEL_BC95_PROVIDE_DEFAULT 0 // set by library:QUECTEL_BC95 +#define MBED_CONF_QUECTEL_BG96_BAUDRATE 115200 // set by library:QUECTEL_BG96 +#define MBED_CONF_QUECTEL_BG96_PROVIDE_DEFAULT 0 // set by library:QUECTEL_BG96 +#define MBED_CONF_QUECTEL_EC2X_BAUDRATE 115200 // set by library:QUECTEL_EC2X +#define MBED_CONF_QUECTEL_EC2X_PROVIDE_DEFAULT 0 // set by library:QUECTEL_EC2X +#define MBED_CONF_QUECTEL_EC2X_START_TIMEOUT 15000 // set by library:QUECTEL_EC2X +#define MBED_CONF_QUECTEL_M26_BAUDRATE 115200 // set by library:QUECTEL_M26 +#define MBED_CONF_QUECTEL_M26_PROVIDE_DEFAULT 0 // set by library:QUECTEL_M26 +#define MBED_CONF_QUECTEL_UG96_BAUDRATE 115200 // set by library:QUECTEL_UG96 +#define MBED_CONF_QUECTEL_UG96_PROVIDE_DEFAULT 0 // set by library:QUECTEL_UG96 +#define MBED_CONF_RM1000_AT_BAUDRATE 230400 // set by library:RM1000_AT +#define MBED_CONF_RM1000_AT_PROVIDE_DEFAULT 0 // set by library:RM1000_AT +#define MBED_CONF_RTOS_API_PRESENT 1 // set by library:rtos-api +#define MBED_CONF_RTOS_ENABLE_ALL_RTX_EVENTS 0 // set by library:rtos +#define MBED_CONF_RTOS_EVFLAGS_NUM 0 // set by library:rtos +#define MBED_CONF_RTOS_IDLE_THREAD_STACK_SIZE 512 // set by library:rtos +#define MBED_CONF_RTOS_IDLE_THREAD_STACK_SIZE_DEBUG_EXTRA 128 // set by library:rtos[STM] +#define MBED_CONF_RTOS_IDLE_THREAD_STACK_SIZE_TICKLESS_EXTRA 256 // set by library:rtos +#define MBED_CONF_RTOS_MAIN_THREAD_STACK_SIZE 4096 // set by library:rtos +#define MBED_CONF_RTOS_MSGQUEUE_DATA_SIZE 0 // set by library:rtos +#define MBED_CONF_RTOS_MSGQUEUE_NUM 0 // set by library:rtos +#define MBED_CONF_RTOS_MUTEX_NUM 0 // set by library:rtos +#define MBED_CONF_RTOS_PRESENT 1 // set by library:rtos +#define MBED_CONF_RTOS_SEMAPHORE_NUM 0 // set by library:rtos +#define MBED_CONF_RTOS_THREAD_NUM 0 // set by library:rtos +#define MBED_CONF_RTOS_THREAD_STACK_SIZE 4096 // set by library:rtos +#define MBED_CONF_RTOS_THREAD_USER_STACK_SIZE 0 // set by library:rtos +#define MBED_CONF_RTOS_TIMER_NUM 0 // set by library:rtos +#define MBED_CONF_RTOS_TIMER_THREAD_STACK_SIZE 768 // set by library:rtos +#define MBED_CONF_S2LP_PROVIDE_DEFAULT 0 // set by library:s2lp +#define MBED_CONF_SARA4_PPP_BAUDRATE 115200 // set by library:SARA4_PPP +#define MBED_CONF_SARA4_PPP_PROVIDE_DEFAULT 0 // set by library:SARA4_PPP +#define MBED_CONF_STM32_EMAC_ETH_PHY_ADDRESS 0 // set by library:stm32-emac +#define MBED_CONF_STM32_EMAC_ETH_PHY_AUTONEGOTIATION \ + ETH_AUTONEGOTIATION_ENABLE // set by library:stm32-emac +#define MBED_CONF_STM32_EMAC_ETH_PHY_DUPLEXMODE ETH_MODE_FULLDUPLEX // set by library:stm32-emac +#define MBED_CONF_STM32_EMAC_ETH_PHY_DUPLEX_STATUS 0x0010 // set by library:stm32-emac +#define MBED_CONF_STM32_EMAC_ETH_PHY_MEDIA_INTERFACE \ + ETH_MEDIA_INTERFACE_RMII // set by library:stm32-emac +#define MBED_CONF_STM32_EMAC_ETH_PHY_RESET_DELAY 500 // set by library:stm32-emac +#define MBED_CONF_STM32_EMAC_ETH_PHY_SPEED ETH_SPEED_100M // set by library:stm32-emac +#define MBED_CONF_STM32_EMAC_ETH_PHY_SPEED_STATUS 0x0004 // set by library:stm32-emac +#define MBED_CONF_STM32_EMAC_ETH_PHY_STATUS_REGISTER 31 // set by library:stm32-emac +#define MBED_CONF_STM32_EMAC_ETH_RXBUFNB 4 // set by library:stm32-emac +#define MBED_CONF_STM32_EMAC_ETH_TXBUFNB 10 // set by library:stm32-emac[STM32H7] +#define MBED_CONF_STM32_EMAC_THREAD_STACKSIZE 1024 // set by library:stm32-emac +#define MBED_CONF_STORAGE_DEFAULT_KV kv // set by library:storage +#define MBED_CONF_STORAGE_FILESYSTEM_BLOCKDEVICE default // set by library:storage_filesystem +#define MBED_CONF_STORAGE_FILESYSTEM_EXTERNAL_BASE_ADDRESS 0 // set by library:storage_filesystem +#define MBED_CONF_STORAGE_FILESYSTEM_EXTERNAL_SIZE 0 // set by library:storage_filesystem +#define MBED_CONF_STORAGE_FILESYSTEM_FILESYSTEM default // set by library:storage_filesystem +#define MBED_CONF_STORAGE_FILESYSTEM_FOLDER_PATH kvstore // set by library:storage_filesystem +#define MBED_CONF_STORAGE_FILESYSTEM_INTERNAL_BASE_ADDRESS 0 // set by library:storage_filesystem +#define MBED_CONF_STORAGE_FILESYSTEM_MOUNT_POINT kv // set by library:storage_filesystem +#define MBED_CONF_STORAGE_FILESYSTEM_NO_RBP_BLOCKDEVICE \ + default // set by library:storage_filesystem_no_rbp +#define MBED_CONF_STORAGE_FILESYSTEM_NO_RBP_EXTERNAL_BASE_ADDRESS \ + 0 // set by library:storage_filesystem_no_rbp +#define MBED_CONF_STORAGE_FILESYSTEM_NO_RBP_EXTERNAL_SIZE \ + 0 // set by library:storage_filesystem_no_rbp +#define MBED_CONF_STORAGE_FILESYSTEM_NO_RBP_FILESYSTEM \ + default // set by library:storage_filesystem_no_rbp +#define MBED_CONF_STORAGE_FILESYSTEM_NO_RBP_FOLDER_PATH \ + kvstore // set by library:storage_filesystem_no_rbp +#define MBED_CONF_STORAGE_FILESYSTEM_NO_RBP_MOUNT_POINT \ + kv // set by library:storage_filesystem_no_rbp +#define MBED_CONF_STORAGE_FILESYSTEM_RBP_INTERNAL_SIZE 0 // set by library:storage_filesystem +#define MBED_CONF_STORAGE_STORAGE_TYPE TDB_INTERNAL // set by library:storage[NUCLEO_H743ZI2] +#define MBED_CONF_STORAGE_TDB_EXTERNAL_BLOCKDEVICE default // set by library:storage_tdb_external +#define MBED_CONF_STORAGE_TDB_EXTERNAL_EXTERNAL_BASE_ADDRESS \ + 0 // set by library:storage_tdb_external +#define MBED_CONF_STORAGE_TDB_EXTERNAL_EXTERNAL_SIZE 0 // set by library:storage_tdb_external +#define MBED_CONF_STORAGE_TDB_EXTERNAL_INTERNAL_BASE_ADDRESS \ + 0 // set by library:storage_tdb_external +#define MBED_CONF_STORAGE_TDB_EXTERNAL_NO_RBP_BLOCKDEVICE \ + default // set by library:storage_tdb_external_no_rbp +#define MBED_CONF_STORAGE_TDB_EXTERNAL_NO_RBP_EXTERNAL_BASE_ADDRESS \ + 0 // set by library:storage_tdb_external_no_rbp +#define MBED_CONF_STORAGE_TDB_EXTERNAL_NO_RBP_EXTERNAL_SIZE \ + 0 // set by library:storage_tdb_external_no_rbp +#define MBED_CONF_STORAGE_TDB_EXTERNAL_RBP_INTERNAL_SIZE 0 // set by library:storage_tdb_external +#define MBED_CONF_STORAGE_TDB_INTERNAL_INTERNAL_BASE_ADDRESS \ + 0 // set by library:storage_tdb_internal +#define MBED_CONF_STORAGE_TDB_INTERNAL_INTERNAL_SIZE 0 // set by library:storage_tdb_internal +#define MBED_CONF_TARGET_BOOT_STACK_SIZE 0x400 // set by library:rtos[*] +#define MBED_CONF_TARGET_CONSOLE_UART 1 // set by target:Target +#define MBED_CONF_TARGET_CUSTOM_TICKERS 1 // set by target:Target +#define MBED_CONF_TARGET_DEEP_SLEEP_LATENCY 4 // set by target:MCU_STM32 +#define MBED_CONF_TARGET_DEFAULT_ADC_VREF NAN // set by target:Target +#define MBED_CONF_TARGET_GPIO_RESET_AT_INIT 0 // set by target:MCU_STM32 +#define MBED_CONF_TARGET_I2C_TIMING_VALUE_ALGO 0 // set by target:MCU_STM32H7 +#define MBED_CONF_TARGET_INIT_US_TICKER_AT_BOOT 1 // set by target:MCU_STM32 +#define MBED_CONF_TARGET_INTERNAL_FLASH_UNIFORM_SECTORS 1 // set by target:Target +#define MBED_CONF_TARGET_LPTICKER_LPTIM 1 // set by target:MCU_STM32H7 +#define MBED_CONF_TARGET_LPTICKER_LPTIM_CLOCK 1 // set by target:MCU_STM32 +#define MBED_CONF_TARGET_LPUART_CLOCK_SOURCE \ + USE_LPUART_CLK_LSE | USE_LPUART_CLK_PCLK1 | USE_LPUART_CLK_PCLK3 // set by target:MCU_STM32 +#define MBED_CONF_TARGET_LSE_AVAILABLE 1 // set by target:MCU_STM32 +#define MBED_CONF_TARGET_LSE_DRIVE_LOAD_LEVEL RCC_LSEDRIVE_LOW // set by target:MCU_STM32H7 +#define MBED_CONF_TARGET_MPU_ROM_END 0x0fffffff // set by target:Target +#define MBED_CONF_TARGET_NETWORK_DEFAULT_INTERFACE_TYPE ETHERNET // set by target:NUCLEO_H743ZI2 +#define MBED_CONF_TARGET_RTC_CLOCK_SOURCE USE_RTC_CLK_LSE_OR_LSI // set by target:MCU_STM32 +#define MBED_CONF_TARGET_SYSTEM_POWER_SUPPLY PWR_LDO_SUPPLY // set by target:MCU_STM32H743xI +#define MBED_CONF_TARGET_TICKLESS_FROM_US_TICKER 0 // set by target:Target +#define MBED_CONF_TARGET_XIP_ENABLE 0 // set by target:Target +#define MBED_CONF_TELIT_HE910_BAUDRATE 115200 // set by library:TELIT_HE910 +#define MBED_CONF_TELIT_HE910_PROVIDE_DEFAULT 0 // set by library:TELIT_HE910 +#define MBED_CONF_TELIT_ME310_BAUDRATE 115200 // set by library:TELIT_ME310 +#define MBED_CONF_TELIT_ME310_PROVIDE_DEFAULT 0 // set by library:TELIT_ME310 +#define MBED_CONF_TELIT_ME910_BAUDRATE 115200 // set by library:TELIT_ME910 +#define MBED_CONF_TELIT_ME910_PROVIDE_DEFAULT 0 // set by library:TELIT_ME910 +#define MBED_CONF_UBLOX_AT_BAUDRATE 115200 // set by library:UBLOX_AT +#define MBED_CONF_UBLOX_AT_PROVIDE_DEFAULT 0 // set by library:UBLOX_AT +#define MBED_CONF_UBLOX_N2XX_BAUDRATE 9600 // set by library:UBLOX_N2XX +#define MBED_CONF_UBLOX_N2XX_PROVIDE_DEFAULT 0 // set by library:UBLOX_N2XX +#define MBED_CONF_UBLOX_PPP_BAUDRATE 115200 // set by library:UBLOX_PPP +#define MBED_CONF_UBLOX_PPP_PROVIDE_DEFAULT 0 // set by library:UBLOX_PPP +#define MBED_CRC_TABLE_SIZE 16 // set by library:drivers +#define MBED_LFS2_BLOCK_CYCLES 1024 // set by library:littlefs2 +#define MBED_LFS2_BLOCK_SIZE 512 // set by library:littlefs2 +#define MBED_LFS2_CACHE_SIZE 64 // set by library:littlefs2 +#define MBED_LFS2_ENABLE_INFO 0 // set by library:littlefs2 +#define MBED_LFS2_INTRINSICS 1 // set by library:littlefs2 +#define MBED_LFS2_LOOKAHEAD_SIZE 64 // set by library:littlefs2 +#define MBED_LFS_BLOCK_SIZE 512 // set by library:littlefs +#define MBED_LFS_ENABLE_INFO 0 // set by library:littlefs +#define MBED_LFS_INTRINSICS 1 // set by library:littlefs +#define MBED_LFS_LOOKAHEAD 512 // set by library:littlefs +#define MBED_LFS_PROG_SIZE 64 // set by library:littlefs +#define MBED_LFS_READ_SIZE 64 // set by library:littlefs +#define MBED_STACK_DUMP_ENABLED 0 // set by library:platform +#define MBED_TRACE_COLOR_THEME 0 // set by library:mbed-trace +#define MEM_ALLOC malloc // set by library:mbed-trace +#define MEM_FREE free // set by library:mbed-trace +#define PPP_DEBUG 0 // set by library:ppp +#define STM32_D11_SPI_ETHERNET_PIN PB_5 // set by target:NUCLEO_H743ZI2 +// Macros +#define MBEDTLS_CIPHER_MODE_CTR // defined by library:SecureStore +#define NSAPI_PPP_AVAILABLE \ + (MBED_CONF_PPP_ENABLED || MBED_CONF_LWIP_PPP_ENABLED) // defined by library:ppp +#define NSDYNMEM_TRACKER_ENABLED \ + MBED_CONF_NANOSTACK_LIBSERVICE_NSDYNMEM_TRACKER_ENABLED // defined by library:nanostack-libservice +#define NS_USE_EXTERNAL_MBED_TLS // defined by library:nanostack +#define UNITY_INCLUDE_CONFIG_H // defined by library:utest +#define _RTE_ // defined by library:rtos + +#endif diff --git a/onert-micro/tests/mbed-os/startup_stm32h743xx.S b/onert-micro/tests/mbed-os/startup_stm32h743xx.S new file mode 100644 index 0000000..b978ae1 --- /dev/null +++ b/onert-micro/tests/mbed-os/startup_stm32h743xx.S @@ -0,0 +1,675 @@ +.syntax unified +.cpu cortex-m7 +.fpu softvfp +.thumb + +.global g_pfnVectors +.global Default_Handler + +.word _sidata +.word _sdata +.word _edata +.word _sbss +.word _ebss +.section .text.Reset_Handler +.weak Reset_Handler +.type Reset_Handler, %function + +Reset_Handler: + ldr sp, =_estack + bl main + +CopyDataInit: + ldr r3, =_sidata + ldr r3, [r3, r1] + str r3, [r0, r1] + adds r1, r1, #4 + +LoopCopyDataInit: + ldr r0, =_sdata + ldr r3, =_edata + adds r2, r0, r1 + cmp r2, r3 + bcc CopyDataInit + ldr r2, =_sbss + b LoopFillZerobss + +FillZerobss: + movs r3, #0 + str r3, [r2], #4 + +LoopFillZerobss: + ldr r3, = _ebss + cmp r2, r3 + bcc FillZerobss + + bl _start + bx lr + +.size Reset_Handler, .-Reset_Handler + +.section .text.Default_Handler,"ax",%progbits + +Default_Handler: +Infinite_Loop: + b Infinite_Loop + .size Default_Handler, .-Default_Handler + .section .isr_vector,"a",%progbits + .type g_pfnVectors, %object + .size g_pfnVectors, .-g_pfnVectors + + +g_pfnVectors: + .word _estack + .word Reset_Handler + + .word NMI_Handler + .word HardFault_Handler + .word MemManage_Handler + .word BusFault_Handler + .word UsageFault_Handler + .word 0 + .word 0 + .word 0 + .word 0 + .word SVC_Handler + .word DebugMon_Handler + .word 0 + .word PendSV_Handler + .word SysTick_Handler + + /* External Interrupts */ + .word WWDG_IRQHandler /* Window WatchDog */ + .word PVD_AVD_IRQHandler /* PVD/AVD through EXTI Line detection */ + .word TAMP_STAMP_IRQHandler /* Tamper and TimeStamps through the EXTI line */ + .word RTC_WKUP_IRQHandler /* RTC Wakeup through the EXTI line */ + .word FLASH_IRQHandler /* FLASH */ + .word RCC_IRQHandler /* RCC */ + .word EXTI0_IRQHandler /* EXTI Line0 */ + .word EXTI1_IRQHandler /* EXTI Line1 */ + .word EXTI2_IRQHandler /* EXTI Line2 */ + .word EXTI3_IRQHandler /* EXTI Line3 */ + .word EXTI4_IRQHandler /* EXTI Line4 */ + .word DMA1_Stream0_IRQHandler /* DMA1 Stream 0 */ + .word DMA1_Stream1_IRQHandler /* DMA1 Stream 1 */ + .word DMA1_Stream2_IRQHandler /* DMA1 Stream 2 */ + .word DMA1_Stream3_IRQHandler /* DMA1 Stream 3 */ + .word DMA1_Stream4_IRQHandler /* DMA1 Stream 4 */ + .word DMA1_Stream5_IRQHandler /* DMA1 Stream 5 */ + .word DMA1_Stream6_IRQHandler /* DMA1 Stream 6 */ + .word ADC_IRQHandler /* ADC1, ADC2 and ADC3s */ + .word FDCAN1_IT0_IRQHandler /* FDCAN1 interrupt line 0 */ + .word FDCAN2_IT0_IRQHandler /* FDCAN2 interrupt line 0 */ + .word FDCAN1_IT1_IRQHandler /* FDCAN1 interrupt line 1 */ + .word FDCAN2_IT1_IRQHandler /* FDCAN2 interrupt line 1 */ + .word EXTI9_5_IRQHandler /* External Line[9:5]s */ + .word TIM1_BRK_IRQHandler /* TIM1 Break interrupt */ + .word TIM1_UP_IRQHandler /* TIM1 Update interrupt */ + .word TIM1_TRG_COM_IRQHandler /* TIM1 Trigger and Commutation interrupt */ + .word TIM1_CC_IRQHandler /* TIM1 Capture Compare */ + .word TIM2_IRQHandler /* TIM2 */ + .word TIM3_IRQHandler /* TIM3 */ + .word TIM4_IRQHandler /* TIM4 */ + .word I2C1_EV_IRQHandler /* I2C1 Event */ + .word I2C1_ER_IRQHandler /* I2C1 Error */ + .word I2C2_EV_IRQHandler /* I2C2 Event */ + .word I2C2_ER_IRQHandler /* I2C2 Error */ + .word SPI1_IRQHandler /* SPI1 */ + .word SPI2_IRQHandler /* SPI2 */ + .word USART1_IRQHandler /* USART1 */ + .word USART2_IRQHandler /* USART2 */ + .word USART3_IRQHandler /* USART3 */ + .word EXTI15_10_IRQHandler /* External Line[15:10]s */ + .word RTC_Alarm_IRQHandler /* RTC Alarm (A and B) through EXTI Line */ + .word 0 /* Reserved */ + .word TIM8_BRK_TIM12_IRQHandler /* TIM8 Break and TIM12 */ + .word TIM8_UP_TIM13_IRQHandler /* TIM8 Update and TIM13 */ + .word TIM8_TRG_COM_TIM14_IRQHandler /* TIM8 Trigger and Commutation and TIM14 */ + .word TIM8_CC_IRQHandler /* TIM8 Capture Compare */ + .word DMA1_Stream7_IRQHandler /* DMA1 Stream7 */ + .word FMC_IRQHandler /* FMC */ + .word SDMMC1_IRQHandler /* SDMMC1 */ + .word TIM5_IRQHandler /* TIM5 */ + .word SPI3_IRQHandler /* SPI3 */ + .word UART4_IRQHandler /* UART4 */ + .word UART5_IRQHandler /* UART5 */ + .word TIM6_DAC_IRQHandler /* TIM6 and DAC1&2 underrun errors */ + .word TIM7_IRQHandler /* TIM7 */ + .word DMA2_Stream0_IRQHandler /* DMA2 Stream 0 */ + .word DMA2_Stream1_IRQHandler /* DMA2 Stream 1 */ + .word DMA2_Stream2_IRQHandler /* DMA2 Stream 2 */ + .word DMA2_Stream3_IRQHandler /* DMA2 Stream 3 */ + .word DMA2_Stream4_IRQHandler /* DMA2 Stream 4 */ + .word ETH_IRQHandler /* Ethernet */ + .word ETH_WKUP_IRQHandler /* Ethernet Wakeup through EXTI line */ + .word FDCAN_CAL_IRQHandler /* FDCAN calibration unit interrupt*/ + .word 0 /* Reserved */ + .word 0 /* Reserved */ + .word 0 /* Reserved */ + .word 0 /* Reserved */ + .word DMA2_Stream5_IRQHandler /* DMA2 Stream 5 */ + .word DMA2_Stream6_IRQHandler /* DMA2 Stream 6 */ + .word DMA2_Stream7_IRQHandler /* DMA2 Stream 7 */ + .word USART6_IRQHandler /* USART6 */ + .word I2C3_EV_IRQHandler /* I2C3 event */ + .word I2C3_ER_IRQHandler /* I2C3 error */ + .word OTG_HS_EP1_OUT_IRQHandler /* USB OTG HS End Point 1 Out */ + .word OTG_HS_EP1_IN_IRQHandler /* USB OTG HS End Point 1 In */ + .word OTG_HS_WKUP_IRQHandler /* USB OTG HS Wakeup through EXTI */ + .word OTG_HS_IRQHandler /* USB OTG HS */ + .word DCMI_IRQHandler /* DCMI */ + .word 0 /* Reserved */ + .word RNG_IRQHandler /* Rng */ + .word FPU_IRQHandler /* FPU */ + .word UART7_IRQHandler /* UART7 */ + .word UART8_IRQHandler /* UART8 */ + .word SPI4_IRQHandler /* SPI4 */ + .word SPI5_IRQHandler /* SPI5 */ + .word SPI6_IRQHandler /* SPI6 */ + .word SAI1_IRQHandler /* SAI1 */ + .word LTDC_IRQHandler /* LTDC */ + .word LTDC_ER_IRQHandler /* LTDC error */ + .word DMA2D_IRQHandler /* DMA2D */ + .word SAI2_IRQHandler /* SAI2 */ + .word QUADSPI_IRQHandler /* QUADSPI */ + .word LPTIM1_IRQHandler /* LPTIM1 */ + .word CEC_IRQHandler /* HDMI_CEC */ + .word I2C4_EV_IRQHandler /* I2C4 Event */ + .word I2C4_ER_IRQHandler /* I2C4 Error */ + .word SPDIF_RX_IRQHandler /* SPDIF_RX */ + .word OTG_FS_EP1_OUT_IRQHandler /* USB OTG FS End Point 1 Out */ + .word OTG_FS_EP1_IN_IRQHandler /* USB OTG FS End Point 1 In */ + .word OTG_FS_WKUP_IRQHandler /* USB OTG FS Wakeup through EXTI */ + .word OTG_FS_IRQHandler /* USB OTG FS */ + .word DMAMUX1_OVR_IRQHandler /* DMAMUX1 Overrun interrupt */ + .word HRTIM1_Master_IRQHandler /* HRTIM Master Timer global Interrupt */ + .word HRTIM1_TIMA_IRQHandler /* HRTIM Timer A global Interrupt */ + .word HRTIM1_TIMB_IRQHandler /* HRTIM Timer B global Interrupt */ + .word HRTIM1_TIMC_IRQHandler /* HRTIM Timer C global Interrupt */ + .word HRTIM1_TIMD_IRQHandler /* HRTIM Timer D global Interrupt */ + .word HRTIM1_TIME_IRQHandler /* HRTIM Timer E global Interrupt */ + .word HRTIM1_FLT_IRQHandler /* HRTIM Fault global Interrupt */ + .word DFSDM1_FLT0_IRQHandler /* DFSDM Filter0 Interrupt */ + .word DFSDM1_FLT1_IRQHandler /* DFSDM Filter1 Interrupt */ + .word DFSDM1_FLT2_IRQHandler /* DFSDM Filter2 Interrupt */ + .word DFSDM1_FLT3_IRQHandler /* DFSDM Filter3 Interrupt */ + .word SAI3_IRQHandler /* SAI3 global Interrupt */ + .word SWPMI1_IRQHandler /* Serial Wire Interface 1 global interrupt */ + .word TIM15_IRQHandler /* TIM15 global Interrupt */ + .word TIM16_IRQHandler /* TIM16 global Interrupt */ + .word TIM17_IRQHandler /* TIM17 global Interrupt */ + .word MDIOS_WKUP_IRQHandler /* MDIOS Wakeup Interrupt */ + .word MDIOS_IRQHandler /* MDIOS global Interrupt */ + .word JPEG_IRQHandler /* JPEG global Interrupt */ + .word MDMA_IRQHandler /* MDMA global Interrupt */ + .word 0 /* Reserved */ + .word SDMMC2_IRQHandler /* SDMMC2 global Interrupt */ + .word HSEM1_IRQHandler /* HSEM1 global Interrupt */ + .word 0 /* Reserved */ + .word ADC3_IRQHandler /* ADC3 global Interrupt */ + .word DMAMUX2_OVR_IRQHandler /* DMAMUX Overrun interrupt */ + .word BDMA_Channel0_IRQHandler /* BDMA Channel 0 global Interrupt */ + .word BDMA_Channel1_IRQHandler /* BDMA Channel 1 global Interrupt */ + .word BDMA_Channel2_IRQHandler /* BDMA Channel 2 global Interrupt */ + .word BDMA_Channel3_IRQHandler /* BDMA Channel 3 global Interrupt */ + .word BDMA_Channel4_IRQHandler /* BDMA Channel 4 global Interrupt */ + .word BDMA_Channel5_IRQHandler /* BDMA Channel 5 global Interrupt */ + .word BDMA_Channel6_IRQHandler /* BDMA Channel 6 global Interrupt */ + .word BDMA_Channel7_IRQHandler /* BDMA Channel 7 global Interrupt */ + .word COMP1_IRQHandler /* COMP1 global Interrupt */ + .word LPTIM2_IRQHandler /* LP TIM2 global interrupt */ + .word LPTIM3_IRQHandler /* LP TIM3 global interrupt */ + .word LPTIM4_IRQHandler /* LP TIM4 global interrupt */ + .word LPTIM5_IRQHandler /* LP TIM5 global interrupt */ + .word LPUART1_IRQHandler /* LP UART1 interrupt */ + .word 0 /* Reserved */ + .word CRS_IRQHandler /* Clock Recovery Global Interrupt */ + .word ECC_IRQHandler /* ECC diagnostic Global Interrupt */ + .word SAI4_IRQHandler /* SAI4 global interrupt */ + .word 0 /* Reserved */ + .word 0 /* Reserved */ + .word WAKEUP_PIN_IRQHandler /* Interrupt for all 6 wake-up pins */ + + .weak NMI_Handler + .thumb_set NMI_Handler,Default_Handler + + .weak HardFault_Handler + .thumb_set HardFault_Handler,Default_Handler + + .weak MemManage_Handler + .thumb_set MemManage_Handler,Default_Handler + + .weak BusFault_Handler + .thumb_set BusFault_Handler,Default_Handler + + .weak UsageFault_Handler + .thumb_set UsageFault_Handler,Default_Handler + + .weak SVC_Handler + .thumb_set SVC_Handler,Default_Handler + + .weak DebugMon_Handler + .thumb_set DebugMon_Handler,Default_Handler + + .weak PendSV_Handler + .thumb_set PendSV_Handler,Default_Handler + + .weak SysTick_Handler + .thumb_set SysTick_Handler,Default_Handler + + .weak WWDG_IRQHandler + .thumb_set WWDG_IRQHandler,Default_Handler + + .weak PVD_AVD_IRQHandler + .thumb_set PVD_AVD_IRQHandler,Default_Handler + + .weak TAMP_STAMP_IRQHandler + .thumb_set TAMP_STAMP_IRQHandler,Default_Handler + + .weak RTC_WKUP_IRQHandler + .thumb_set RTC_WKUP_IRQHandler,Default_Handler + + .weak FLASH_IRQHandler + .thumb_set FLASH_IRQHandler,Default_Handler + + .weak RCC_IRQHandler + .thumb_set RCC_IRQHandler,Default_Handler + + .weak EXTI0_IRQHandler + .thumb_set EXTI0_IRQHandler,Default_Handler + + .weak EXTI1_IRQHandler + .thumb_set EXTI1_IRQHandler,Default_Handler + + .weak EXTI2_IRQHandler + .thumb_set EXTI2_IRQHandler,Default_Handler + + .weak EXTI3_IRQHandler + .thumb_set EXTI3_IRQHandler,Default_Handler + + .weak EXTI4_IRQHandler + .thumb_set EXTI4_IRQHandler,Default_Handler + + .weak DMA1_Stream0_IRQHandler + .thumb_set DMA1_Stream0_IRQHandler,Default_Handler + + .weak DMA1_Stream1_IRQHandler + .thumb_set DMA1_Stream1_IRQHandler,Default_Handler + + .weak DMA1_Stream2_IRQHandler + .thumb_set DMA1_Stream2_IRQHandler,Default_Handler + + .weak DMA1_Stream3_IRQHandler + .thumb_set DMA1_Stream3_IRQHandler,Default_Handler + + .weak DMA1_Stream4_IRQHandler + .thumb_set DMA1_Stream4_IRQHandler,Default_Handler + + .weak DMA1_Stream5_IRQHandler + .thumb_set DMA1_Stream5_IRQHandler,Default_Handler + + .weak DMA1_Stream6_IRQHandler + .thumb_set DMA1_Stream6_IRQHandler,Default_Handler + + .weak ADC_IRQHandler + .thumb_set ADC_IRQHandler,Default_Handler + + .weak FDCAN1_IT0_IRQHandler + .thumb_set FDCAN1_IT0_IRQHandler,Default_Handler + + .weak FDCAN2_IT0_IRQHandler + .thumb_set FDCAN2_IT0_IRQHandler,Default_Handler + + .weak FDCAN1_IT1_IRQHandler + .thumb_set FDCAN1_IT1_IRQHandler,Default_Handler + + .weak FDCAN2_IT1_IRQHandler + .thumb_set FDCAN2_IT1_IRQHandler,Default_Handler + + .weak EXTI9_5_IRQHandler + .thumb_set EXTI9_5_IRQHandler,Default_Handler + + .weak TIM1_BRK_IRQHandler + .thumb_set TIM1_BRK_IRQHandler,Default_Handler + + .weak TIM1_UP_IRQHandler + .thumb_set TIM1_UP_IRQHandler,Default_Handler + + .weak TIM1_TRG_COM_IRQHandler + .thumb_set TIM1_TRG_COM_IRQHandler,Default_Handler + + .weak TIM1_CC_IRQHandler + .thumb_set TIM1_CC_IRQHandler,Default_Handler + + .weak TIM2_IRQHandler + .thumb_set TIM2_IRQHandler,Default_Handler + + .weak TIM3_IRQHandler + .thumb_set TIM3_IRQHandler,Default_Handler + + .weak TIM4_IRQHandler + .thumb_set TIM4_IRQHandler,Default_Handler + + .weak I2C1_EV_IRQHandler + .thumb_set I2C1_EV_IRQHandler,Default_Handler + + .weak I2C1_ER_IRQHandler + .thumb_set I2C1_ER_IRQHandler,Default_Handler + + .weak I2C2_EV_IRQHandler + .thumb_set I2C2_EV_IRQHandler,Default_Handler + + .weak I2C2_ER_IRQHandler + .thumb_set I2C2_ER_IRQHandler,Default_Handler + + .weak SPI1_IRQHandler + .thumb_set SPI1_IRQHandler,Default_Handler + + .weak SPI2_IRQHandler + .thumb_set SPI2_IRQHandler,Default_Handler + + .weak USART1_IRQHandler + .thumb_set USART1_IRQHandler,Default_Handler + + .weak USART2_IRQHandler + .thumb_set USART2_IRQHandler,Default_Handler + + .weak USART3_IRQHandler + .thumb_set USART3_IRQHandler,Default_Handler + + .weak EXTI15_10_IRQHandler + .thumb_set EXTI15_10_IRQHandler,Default_Handler + + .weak RTC_Alarm_IRQHandler + .thumb_set RTC_Alarm_IRQHandler,Default_Handler + + .weak TIM8_BRK_TIM12_IRQHandler + .thumb_set TIM8_BRK_TIM12_IRQHandler,Default_Handler + + .weak TIM8_UP_TIM13_IRQHandler + .thumb_set TIM8_UP_TIM13_IRQHandler,Default_Handler + + .weak TIM8_TRG_COM_TIM14_IRQHandler + .thumb_set TIM8_TRG_COM_TIM14_IRQHandler,Default_Handler + + .weak TIM8_CC_IRQHandler + .thumb_set TIM8_CC_IRQHandler,Default_Handler + + .weak DMA1_Stream7_IRQHandler + .thumb_set DMA1_Stream7_IRQHandler,Default_Handler + + .weak FMC_IRQHandler + .thumb_set FMC_IRQHandler,Default_Handler + + .weak SDMMC1_IRQHandler + .thumb_set SDMMC1_IRQHandler,Default_Handler + + .weak TIM5_IRQHandler + .thumb_set TIM5_IRQHandler,Default_Handler + + .weak SPI3_IRQHandler + .thumb_set SPI3_IRQHandler,Default_Handler + + .weak UART4_IRQHandler + .thumb_set UART4_IRQHandler,Default_Handler + + .weak UART5_IRQHandler + .thumb_set UART5_IRQHandler,Default_Handler + + .weak TIM6_DAC_IRQHandler + .thumb_set TIM6_DAC_IRQHandler,Default_Handler + + .weak TIM7_IRQHandler + .thumb_set TIM7_IRQHandler,Default_Handler + + .weak DMA2_Stream0_IRQHandler + .thumb_set DMA2_Stream0_IRQHandler,Default_Handler + + .weak DMA2_Stream1_IRQHandler + .thumb_set DMA2_Stream1_IRQHandler,Default_Handler + + .weak DMA2_Stream2_IRQHandler + .thumb_set DMA2_Stream2_IRQHandler,Default_Handler + + .weak DMA2_Stream3_IRQHandler + .thumb_set DMA2_Stream3_IRQHandler,Default_Handler + + .weak DMA2_Stream4_IRQHandler + .thumb_set DMA2_Stream4_IRQHandler,Default_Handler + + .weak ETH_IRQHandler + .thumb_set ETH_IRQHandler,Default_Handler + + .weak ETH_WKUP_IRQHandler + .thumb_set ETH_WKUP_IRQHandler,Default_Handler + + .weak FDCAN_CAL_IRQHandler + .thumb_set FDCAN_CAL_IRQHandler,Default_Handler + + .weak DMA2_Stream5_IRQHandler + .thumb_set DMA2_Stream5_IRQHandler,Default_Handler + + .weak DMA2_Stream6_IRQHandler + .thumb_set DMA2_Stream6_IRQHandler,Default_Handler + + .weak DMA2_Stream7_IRQHandler + .thumb_set DMA2_Stream7_IRQHandler,Default_Handler + + .weak USART6_IRQHandler + .thumb_set USART6_IRQHandler,Default_Handler + + .weak I2C3_EV_IRQHandler + .thumb_set I2C3_EV_IRQHandler,Default_Handler + + .weak I2C3_ER_IRQHandler + .thumb_set I2C3_ER_IRQHandler,Default_Handler + + .weak OTG_HS_EP1_OUT_IRQHandler + .thumb_set OTG_HS_EP1_OUT_IRQHandler,Default_Handler + + .weak OTG_HS_EP1_IN_IRQHandler + .thumb_set OTG_HS_EP1_IN_IRQHandler,Default_Handler + + .weak OTG_HS_WKUP_IRQHandler + .thumb_set OTG_HS_WKUP_IRQHandler,Default_Handler + + .weak OTG_HS_IRQHandler + .thumb_set OTG_HS_IRQHandler,Default_Handler + + .weak DCMI_IRQHandler + .thumb_set DCMI_IRQHandler,Default_Handler + + .weak RNG_IRQHandler + .thumb_set RNG_IRQHandler,Default_Handler + + .weak FPU_IRQHandler + .thumb_set FPU_IRQHandler,Default_Handler + + .weak UART7_IRQHandler + .thumb_set UART7_IRQHandler,Default_Handler + + .weak UART8_IRQHandler + .thumb_set UART8_IRQHandler,Default_Handler + + .weak SPI4_IRQHandler + .thumb_set SPI4_IRQHandler,Default_Handler + + .weak SPI5_IRQHandler + .thumb_set SPI5_IRQHandler,Default_Handler + + .weak SPI6_IRQHandler + .thumb_set SPI6_IRQHandler,Default_Handler + + .weak SAI1_IRQHandler + .thumb_set SAI1_IRQHandler,Default_Handler + + .weak LTDC_IRQHandler + .thumb_set LTDC_IRQHandler,Default_Handler + + .weak LTDC_ER_IRQHandler + .thumb_set LTDC_ER_IRQHandler,Default_Handler + + .weak DMA2D_IRQHandler + .thumb_set DMA2D_IRQHandler,Default_Handler + + .weak SAI2_IRQHandler + .thumb_set SAI2_IRQHandler,Default_Handler + + .weak QUADSPI_IRQHandler + .thumb_set QUADSPI_IRQHandler,Default_Handler + + .weak LPTIM1_IRQHandler + .thumb_set LPTIM1_IRQHandler,Default_Handler + + .weak CEC_IRQHandler + .thumb_set CEC_IRQHandler,Default_Handler + + .weak I2C4_EV_IRQHandler + .thumb_set I2C4_EV_IRQHandler,Default_Handler + + .weak I2C4_ER_IRQHandler + .thumb_set I2C4_ER_IRQHandler,Default_Handler + + .weak SPDIF_RX_IRQHandler + .thumb_set SPDIF_RX_IRQHandler,Default_Handler + + .weak OTG_FS_EP1_OUT_IRQHandler + .thumb_set OTG_FS_EP1_OUT_IRQHandler,Default_Handler + + .weak OTG_FS_EP1_IN_IRQHandler + .thumb_set OTG_FS_EP1_IN_IRQHandler,Default_Handler + + .weak OTG_FS_WKUP_IRQHandler + .thumb_set OTG_FS_WKUP_IRQHandler,Default_Handler + + .weak OTG_FS_IRQHandler + .thumb_set OTG_FS_IRQHandler,Default_Handler + + .weak DMAMUX1_OVR_IRQHandler + .thumb_set DMAMUX1_OVR_IRQHandler,Default_Handler + + .weak HRTIM1_Master_IRQHandler + .thumb_set HRTIM1_Master_IRQHandler,Default_Handler + + .weak HRTIM1_TIMA_IRQHandler + .thumb_set HRTIM1_TIMA_IRQHandler,Default_Handler + + .weak HRTIM1_TIMB_IRQHandler + .thumb_set HRTIM1_TIMB_IRQHandler,Default_Handler + + .weak HRTIM1_TIMC_IRQHandler + .thumb_set HRTIM1_TIMC_IRQHandler,Default_Handler + + .weak HRTIM1_TIMD_IRQHandler + .thumb_set HRTIM1_TIMD_IRQHandler,Default_Handler + + .weak HRTIM1_TIME_IRQHandler + .thumb_set HRTIM1_TIME_IRQHandler,Default_Handler + + .weak HRTIM1_FLT_IRQHandler + .thumb_set HRTIM1_FLT_IRQHandler,Default_Handler + + .weak DFSDM1_FLT0_IRQHandler + .thumb_set DFSDM1_FLT0_IRQHandler,Default_Handler + + .weak DFSDM1_FLT1_IRQHandler + .thumb_set DFSDM1_FLT1_IRQHandler,Default_Handler + + .weak DFSDM1_FLT2_IRQHandler + .thumb_set DFSDM1_FLT2_IRQHandler,Default_Handler + + .weak DFSDM1_FLT3_IRQHandler + .thumb_set DFSDM1_FLT3_IRQHandler,Default_Handler + + .weak SAI3_IRQHandler + .thumb_set SAI3_IRQHandler,Default_Handler + + .weak SWPMI1_IRQHandler + .thumb_set SWPMI1_IRQHandler,Default_Handler + + .weak TIM15_IRQHandler + .thumb_set TIM15_IRQHandler,Default_Handler + + .weak TIM16_IRQHandler + .thumb_set TIM16_IRQHandler,Default_Handler + + .weak TIM17_IRQHandler + .thumb_set TIM17_IRQHandler,Default_Handler + + .weak MDIOS_WKUP_IRQHandler + .thumb_set MDIOS_WKUP_IRQHandler,Default_Handler + + .weak MDIOS_IRQHandler + .thumb_set MDIOS_IRQHandler,Default_Handler + + .weak JPEG_IRQHandler + .thumb_set JPEG_IRQHandler,Default_Handler + + .weak MDMA_IRQHandler + .thumb_set MDMA_IRQHandler,Default_Handler + + .weak SDMMC2_IRQHandler + .thumb_set SDMMC2_IRQHandler,Default_Handler + + .weak HSEM1_IRQHandler + .thumb_set HSEM1_IRQHandler,Default_Handler + + .weak ADC3_IRQHandler + .thumb_set ADC3_IRQHandler,Default_Handler + + .weak DMAMUX2_OVR_IRQHandler + .thumb_set DMAMUX2_OVR_IRQHandler,Default_Handler + + .weak BDMA_Channel0_IRQHandler + .thumb_set BDMA_Channel0_IRQHandler,Default_Handler + + .weak BDMA_Channel1_IRQHandler + .thumb_set BDMA_Channel1_IRQHandler,Default_Handler + + .weak BDMA_Channel2_IRQHandler + .thumb_set BDMA_Channel2_IRQHandler,Default_Handler + + .weak BDMA_Channel3_IRQHandler + .thumb_set BDMA_Channel3_IRQHandler,Default_Handler + + .weak BDMA_Channel4_IRQHandler + .thumb_set BDMA_Channel4_IRQHandler,Default_Handler + + .weak BDMA_Channel5_IRQHandler + .thumb_set BDMA_Channel5_IRQHandler,Default_Handler + + .weak BDMA_Channel6_IRQHandler + .thumb_set BDMA_Channel6_IRQHandler,Default_Handler + + .weak BDMA_Channel7_IRQHandler + .thumb_set BDMA_Channel7_IRQHandler,Default_Handler + + .weak COMP1_IRQHandler + .thumb_set COMP1_IRQHandler,Default_Handler + + .weak LPTIM2_IRQHandler + .thumb_set LPTIM2_IRQHandler,Default_Handler + + .weak LPTIM3_IRQHandler + .thumb_set LPTIM3_IRQHandler,Default_Handler + + .weak LPTIM4_IRQHandler + .thumb_set LPTIM4_IRQHandler,Default_Handler + + .weak LPTIM5_IRQHandler + .thumb_set LPTIM5_IRQHandler,Default_Handler + + .weak LPUART1_IRQHandler + .thumb_set LPUART1_IRQHandler,Default_Handler + + .weak CRS_IRQHandler + .thumb_set CRS_IRQHandler,Default_Handler + + .weak ECC_IRQHandler + .thumb_set ECC_IRQHandler,Default_Handler + + .weak SAI4_IRQHandler + .thumb_set SAI4_IRQHandler,Default_Handler + + .weak WAKEUP_PIN_IRQHandler + .thumb_set WAKEUP_PIN_IRQHandler,Default_Handler diff --git a/packaging/EGL_HEADERS.tar.gz b/packaging/EGL_HEADERS.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..80222056cbed4da718975fc27f400bb342fe4190 GIT binary patch literal 74033 zcmV)8K*qlxiwFP!000001MEC&Q{zaI{dN5-YUz$0%o^htrh%D>aN7bL<5$?GfxWm> z$WqzXv@Ln{fM#~@zh7on=`95=0A4bW52JP%`>=Gm z{q1vYYrm)7|A#(H&)c|XM@I|OTl%xIZm*AiTROI7yO)mbZ;q79-r<-pOxJTBk2FVL zn)m#u**HD@T&-44d97KWozzeG$w~eAv|iUwE6rL{ubpc3N^@Q-|6%?JeR_Xh?@^-l zfAphh;2J+UIo`Vd;lcG^gH>OxqMfMLs*RHm?D$796o3AoumAY`Ya8>2=)eo+ztY6^ zYVD-@KL2m>r04&)tMRMrz_aGRe$r?_w_1bt|N8Oq`~1JflQ;k0md<9Sd2`!YmzS?V zW4HfTYgEA4C+EKj{onn+w|G99bIag!rmC@7SC?wxBea?p&%P)e9TW}@Sliw_I_6^O zv3hCm~aF-e_(5~A6llszdoBLA?o*Qr6FwC28J0c~#3 z*liFa(fyt4o6OCzt*J8Zwt5rR8Y^rx9)9h1 zln&c#O;~rbS7JBa>DBOh%5GZYaceNWW5Wy98r-qpx`PhMU%4HPmC1w+$E@2Q^}0%@ z#JYoa@4C|+T(U0!Iv7q_uiNiV0q=B3R3)~q0&o|IrLT((QGpcO`bwoemJ~ z1>kM5QENQywy%4wF&kZvN5hE%OmqPApgXu210kia45m0PpbgDT`3k?;~m<4)?i@J$iRY-Mj#*t+aw|hB4IEE8{0KK8~)Th_L3kv z;AI88z=~d@g6qO4n7T9_c8awJ%|$YR7uR*SbB0LhX`X0xUisW?=r!Zym}|$i&nL6f zdEKb-*-1k?X+TuoY@XJn5L$-40SVwpAEhA@YZUDw(sg|x00TFjP`>2|3Ty^q;g)Xu zjVdEAFPVQ|L%kJCc6T*$VUrfAvE#?(~~>10R9b_Ap0iO z2tg*WOCeZyy;*VGTDbEQ>~+3raDV{Jb0lJ+FvNTU0@xcKaJ!3mM@%kk8Y=@wys~xL zt)TZK$dXp?ERLKYZ(~NB zy6OTMEpLo@xG;kGkJt$(KyyP?W9*}BWQc6z+Gw@mU zSmJTb1%fj6nX#Gar6Bwq5Z{t8xex$QZKfbX8r9|&8flP_MlJ*Z)N9*l)N5O4q(MR& zxex%*Y;2>^Y;2*C1_^28LIA+VbCM7t4Od%pX8G$GXc?@H8LAczq_dx)(@L=p?why+2I7ZlcLOjrfbjz$A_J%-X zY3fTdkMJ4?v_Lh4|6sdu1PZWMAr6V##cdNjYXYSGMRRF`ia5hPMC|GkCNzuoHwZCy z+C@nYO#W~%oTwMzi-F%4{G5)j6{ch*I0xUHA9;=sTer6IBSRcRHMt&*z@t>VgDI#Y z@Lgr*JkUxxd=aR){stoeZk!8QJeV-DN_h*ui{7v`4e`iL2Y%4hLBgod!vMRtO(R2}5LsZ{_Mf#HLHHoBMYZ&j-nC->_|5?dxb7ceE~Fs$Rdv#a7^;1x zP6n-!sutN_{=)toybb4b+JZ=_SojDuKKz5E_ zl^EHXjl*?raItb74o+RAqA6?Rnx$HdTK=h!*L zLfUUV$B8ZGRY_(hT3iu_iZU7$ z%HE!P^GzW7U7dfF@WIEDp=Lbk^m_7^;@OYs?zGcZzbfNNcQ}wcXS$L7`U|U`o}N~! zy(0VPKVwGia&X;7phV-OJ8dhxJht=@&L{TB>?mLR6P6OQ zyD*C>5d#k*bCD5p{LQdw@(kkO6y$b!szPEfkY|EK z1o=j4>-tv4JQ(J}^t4q>loQ{=P_-joBxH*5LEiJUOjD3qnI?k@tWN_$q?qlEnvG(J zdu78S%i-v0>8Bubq)*#Wf=~{%XA+7a^9bQyh8{~aW-=A8H~p1hUauZE7%PKEkH>I9 zlAj!X%ND^kCDlnWWq|!=v;V|+D^HHMQjoJIM|;UF?!bmV2aj&1>EGVBx#uX1@T{Z= zMW&q+{g`vvcR@=ur%1jarB`in&>QHIo6wdpyNN{rxnupDt1J24{dE5*IlBZs4K*y4 zlh47Q-`@`bvV#Yi%pp3Saqoe1i}lb9QOqO$z*1U|>v*{3vO^4Ys0U?SWU^T#twcV| zEm2(Z6PkseFe-OF6ND&oz-}3LGQ_&saLu7Cqi16t358B6%U*^mlGJ?%{dG(?g|0Yl zQLu?N8(FC(EmJx>|D-9)q~LQVCLcm3+MiD$=%2L7Nh*2MzjGkD^S^T-8HgtalBEz0 zM65F0@p2cV)W8d}$cR0$uEIxH?DRsB2MxQ)kX67V_$emb$rVlHE=77$p5X>3bC_g; zmzW~lnW$$Xv8`Fr`N6fN8Nd53rj;k+JQN9+*_Ic$Dp23W@eLw_2sBOs{A(;evDX-Z z!m@ER=P@{G<(Tqi`@Fm;%YL3bq^HG114aWTde3Ydh!=qsJgVHz4vZ$4UqmBQ+msL& zM&iNIHZH@R24z)2Hh`z&&kgp73mFN-WkxGorii&orvg%YU<(c?l@k-c<PZwTU8lc_cL2j@Pbvo zpInG1MLp+SLM{w^`<2cmg2R<1y!?YZcI*@!bE2lf9l%DDE>hysKgly`-N3XiDo)$f#zzm`O9$FQ(6O( zg?cd@w-t5L{Tsq(MEJj34#Lh037;}q@rTU*tIHJMpC9z3>wo!T<#E?5>#s=Tsrz5` zYBN>;+c<7C-s^wg;t_^2D(0Ni$cv%=sB?SR6UC{AT?5Zb=O%Z~SZkx{ zOMX}@SHzJY7?TOr9x>D@vEwZ;!XlH5Wmn<|$t5dl1_wxOa+BMC{c9rl+JpY6p5^6U zwvm7Lu>D^x&Hub7z5e^!H;%{KuSw&H^lWGQjNw|_(eEG%zMHQ?K{_f~*dNmvX%;wQs+EyJJF1kGhv4AC!*vlKN+g!ju zg|Twko#4UTFAOPsU#|+6%3wH}0tEgQG4W|%;4ZSmFDYD~_9YCw3yLVAF~D6c6tvaq z3|gX5L0dSgvw$?wAa;v|KM~TPcO-%kj%4rx&`@Rx6Kji8dKUxyMS2zjGg|=1kfA48 zE=-jPiiSokNkIMDhG+uQ7vr?h^f3@oS{lB+(^5!3w z)EnOat5%yQ+53Nu>ihox7Eg5lFE;DR`+unynfre=n6{m6!@=`H1{pDoo_DW@YFfn( zy8UiT^6o%*BW>4h7esYu`;>amz375x7rU;uB=5DzJ0>r`+QRZLw#a)H@@@;33FB=R zO#j92xiEPFkuDeN@moycMVAs2%msIs@bbvBJ^~U{bMCkcg45U+rL#dHE0WHs1j152 zO*njEg>by98%xK9P)4i-7CPf46H#b(NH|54D9-`~xsa&#!>Xg=In9d9zr{iKmD^DO zJ7S}Y0pPm09?&JW(Rc{1PYMenN|ywpnq}(5G>6B_h%-V9Yh-CPiXnt+P!4ii&0rEr z)|w?i1rJ**(sDYF;y(^(Mx$Ff1DLRT-Z5R^{}GS8O0wz3v<&+}w4fM7>&X1R);3d- zc_m4JPY0?^8vj5bha$0lXz&fNgsFIt|~eFvcU!W1Jpn2Pc>9-d|fngpym z9E%o1ZLgN=Xavw)gm;+`3TG1c98V-WFoUrdsD7|eA<7sIE_-)uth9&yzB1@cME2wn zER!hnr?FvuX{zoFm&v_}7KVmZicT{CEVvz~&F6T>cV+_vGt zDedk>x2;a^Mi};l8FH#5}&H{C+%@}G*t#vArvnK+09F$M;;%E4m)`OxlP(P*<#07T{so} zP9E1Y6E;#*&t%QY8*1E5PsQaBtc94&V9)hosXnY!LmEJQo>&bL5Nv?zhFD`yFq?!C z>cpP8UewDk{lrX5WQ*BaHH3YBCd&vhvsr48TZNl^p2=)f*&kO8*$RV@thC~&&VyDI zjzuq|T

I&eX0OGtZJi4vB*#^p5#UaD9+RdWUSTCyF}!+i;*H7p=H1`Q*w?b=$Ih zQ@wW1ly8~erEN{^8n@MIdWXo@gj{B`cR_b@y`%fH*pF!$N?7m?rU(9D`<~b%?>U&G zM6nsXJ-S_)Gc@U`R^KyM2}>V3N4v*8l9piiaBt+=f95D#qft-E*!ed#pQ?J*>i%|3 zOhlsk#vO~Vj?6Vga0zK;O~;G%<)sSqOj%xlyX%@-LYn_p+~Pe8?l=%{?gfE5)jK#` zw-=WA2RHU&MHtc%uBBb%xx8@(5hcXfk8zhdRvD2u%53a$p6wqEl4U}WobJ&WStgD- zr@BUQ$A-cKtp61EUxFAzV^O^-UpSiZ_t^^b$c$UIzgS|50LF{o7NHObVuBT4K+o{< z7+rW0Ne*#emTfdw#Ln~?B!l5MBy8fkgg1?`YDFd&OeeTr9F)0~u%2eEx!+2`?Pe@) za>3KFF&_|I_A8cxYqnH&2*H7|g2*knaa?BXW{Ig1m>zMK01UHw?J#yf7lIfVjO))K zpaM{H9wCaCF9r(W;T35i#aLt?L}_%NI=q~ID8B;`P(l>>wM{`utw1cN49|~`Sew9f z{Md@T%!|55As~!~j-ZLzKhZNEnemmqst4nA5IR6ffbPjFc=%8p9JXe@Nc+;}cR1|^KaO=!u8{PeJ+^RXtvMg^K4ID=-h)m_84CIk zKgQQ!)#_R0_^e)Ihn15`g?%a{6StY89`KOjHVXb%Zl}SmLPA`Hl}pAoI^b`IANa8y z`X&O1n+}kv#4^aBMWvKytbCAo#esJZ+f~b6Yo5MT|3}_xrR)}}m?e4!x$_Wx%MhQV z152LUEs0Tyx;S+1o9>ZJz^cDpja40@VXO>J2qyj_c142hI*1KF*9QLRpaWj9IA{-^ z%J+KZw4DH$+zr|hRI^AZ$O8Y8Hw1_E>366&zo)0exh?9bN>%je(y}gx_KDa7lPF33BW}_*Ci& zR#ovluv#&N2E@}LQ5W1(!Fg2i zyA7&l7!C&?(ao8)9lgWkVUqeTh+K|rYi=&&*AH@7NW4z3lHmZeT&*09mpa@?s^Zs6 zfcZc6zP`VW+(!8Ry?lykwpZJ!-z51h+s!^FOCwudSyJ^9C++q08%twbjjT~MQXKc? zyFUa-&X62(h9k>PvS;6QV@m`Gf&d7DAlPViy6tks9W?4qx7-<=)*1Nz7jPp~fO4aK zULIgSFz0UE9sv&ab!?ESHjEq283xy6{kz4#=D|ca{A8;n7kVI%9k=0@yM#XXy!^i2 z>I|B#CRONtyY`F;#r?O(b^x_Tzk5!50H_l^Co}`lG6YqG10*>ieQjXDMfs0}EPj8T z%LJOs6Foq^X#l95Y5{7E@>y4JUn^RGR@-etM7Gm{EG$A{_ZP-70#Z!_>9}3DM<%fc zhZ$u!IvjL+9b($fK5`6&QyD|ErGpZNRXCMbfkunWXVh<#K}!I&G#ab!Y5z>tAexMk z21In}vfQcSqC+S=ZNxeiAt_xkcXe@T!V&pAnu2TJN?S<&Q~0A^-&)0H0JHKv4(K?RRRhJn8^SI)FNq15U2M2yme70esKoij4q= zT7Vwm(uJN&M-l)vNEcnQ(0A%R$Zrs#5a6u=K$LX}plkpjMy#pj(y0NUPTUnOKt%%p zsTrr`$}d`g6VVw4VbwXpi06WsNHRTGf4S9={b4~boFTiZo^U=@&r|65lwF3?b^fQm z+xdt+NPS|Bw?$N)T zPKBkYrA|KWFWxZbC&D-*39jm9=Ni5lgk+1Gn3nh}VU15K3QGXtzA`TEj5qpAcDd?8 zQrO4LUOC|~Yj#$-(ty7&%k^F^Ug!#MM?!*pwwdk$3tr?n|~ zbeFJ!f(g?ZKg)nxj!0UL5ZcXz?7b$zJe+x&HBtIjSr2i~7sbPLQH{6N6o78$>{M~D z_fOuY0I-N#1cCr%-RLEv4TGLTitqet2^pgU%+AmaKG`bC1mX=;-Og#hMjQi8tR1!t zIubRg4Wz=xcsch6#j2T5wM~*t*vIudF0|0i)u|Dn;l6hp31?>?_8q!$B`C=lRmoyh zrOZ*445KQV0RG&n)x>ZW2dJixs$@B;;quCdq`AzYNe#{8tMpGH{?{I~TV3LAymtp5 zBDX}@SnHt3`c_t<4($8-7(*S}_a#6b+4uG580y%*F9AvyNj5TEH;wu^;}evKCDq{z z>TQbAR7zL7{sHLJ%BJj1He04x2>d_`%-0Y)+}__3eH;F9io1v%q`Efj27@zPd=Xp= zyp2k~$2I+QNz01iKkM`?vk8nwiSuFaTt#qN?@Hp`lk>#Z5lf{i$q3UHEJM-<@I-@N z-0>^LC{ZDWH_F`3dsj8PAlTKSi3p0%ObN|2HqgQ{$aVgX)If)R5)3~L!sYy0=j4cA zDuc!NDvxO$q-H1VMnZ8Q`?hccm^6;M5F2Ps7z7DrSI2 z|DxV3L%c)&ssB*NlDVT%BmsBFqFYl7J*WspgXdaQD%`; zN+qczc&ZsPgt2HQLK#Uo?v@MhYT~oa{D3-t ziHXs+Qe@_$--8!ZSu|n5!U=JH09n;_GNVVWBXt_^%py~kOrnKCBH5up(p3FSyG0Rq zC>2Bl`;#Qa$zF8S&b&FixjR_SAfq%vs{FO8ew25Hy%eZlscNenWYM87)s!ZyjFkL%s{9=GN9nHxGIXQ7k17+%> zd)WMXr0VAt4s`y10&6O5H&=INy2zI%n&5t^&TlJJkJD%ZjH&e8dk+&rxS1x_z(m(& z`C|iaWhZ!5)rCS5SW{{HQYe_|B43(l`cNnscy(w7=vL4LmHhNV^(bu+K!R=nbAK74 zVbq=}O5ef|avc-Gt7QFT9v?fwlNr*jpEb*k#5P$3kbR4OGYtd*s?=N-7H&btt z>(EW={1Hc8vS#TtGhEDlFKK}3$Jz{k{N@)iS1(adoHV1q|B~y}AH+bP9hMLSP6RqN zvNm=57qFQu_uOXX3Tk%0WbcV>3Y0y<~$gzc1Gt zkySys6Ej?^Q8mN8HN#cwT{bAj2Lw6tQJSQpgJOmDw?kUgR2&l~CpBc$pG5d4wcA!ck!B zz1c%x7oZG|0{b@0cVEe#W*MUO&}gIijg0(Aq=1>J0|Fu41Tg@Z2VpG-ZI)fF;f!7OQL-my58<; z_Ta3+Qj*Aj-45CIKdFbTSjA`Qi&?z9eLXUOA&EC(GM#W2D}8tqTM=Jbou94)rm8tQ zFoxq}56Y;28l6eg#d#2hm^18?f3K4PX+t|9Xd_j|tc@o=ap$peNiY{9Gb#GoYz^>c zEZKKr^-lR#ohyW744rK66r@m{p_%at07^fQ5L567MYH!hYn@h}ZcW32y4{0O^pU+P znh+}De8$&*Jx6nCjFm4_n+KMC1R z&Xw3%L8%_V0AQgb>U5br5hy6N14Mvmy%ghr1-1T*01(A9WdQ5+8DaMU_|~%b;Fm_$ znty;ja?Iv$;exHlVAw-q)bAoZ*3OSRVX&}VKW9Y7p5dm zbJILZrm3<(C3^p4e_yie40o1Q=h|VDqD8YN9GV@G<+E#IYqmQ`MtX|PZ8I)rWLrI& z;77>4Zlbj!M8BJWu{I^XXxeDgjCJj_ecpRBmWmS&puqlMSwAZ*?!rZJO`W zy{z}nX|77T4NcF=d!k$wGEkZzA6}AS(!3R;9bJ(^qcoYgted;fH<0H7b*S{$K$P#5 z{D=2vgLb(CMbUJ3Iq=qp?#7ymR7cpoH(uzo8KQ4wu`yk>edHzEM-?|%{W56U1jh_e z8OPIz2$kId=?Go{6xpZwKB4)#Hl}{ACc)KU2-PCB-NavO?y|F2*#Q848#5raLJb~? z`-@Y83SO1r72};vswMQ;{F6`lDI#*nhh6eF*C*N%eUM~Z{1{rx+Zm}1l5+MqQgRcK zDAOav(@z_%(@ME=?!t39U4|t^#uKDFA-mF(9F=q9+pe)lqG|vs!V1hiU=1TCu8T08 za%kkvB)O~(6D#@8O7Us(SuNVDX`}Qe9Lvh2S1dT~ET^xO2>@KC=keg8ad1QFEgb^s zmaY~@F%t*Bey9bP4x6MaVP+qZc0y|((4J(WU4yqLQKEJE=(bsG7s+OB=gmesOcHoK z_wIalADZ5-(+Xa)nIdnP@xkV;SMHp-Jwe>4ys7z`DY(tl2GNiP-f|H@g_F4--WrHD zv`*08BEADpVD9&zBwwxLcFzTQBlN3Rud}-`4Ff6#;HU>sU?E(1cQgG8scWcP#PkJO zrsOYf^5lE6u;=gg$VO? zwKWH**lq@ispBJUbqF>xmF%p}t!8^ehI;}~@+nsl9hHRDdtPF=joO-RN7)AATNT@? zabdn>n2X{jSv_&psISH*AvFd~WPOyYd+a*4Jdd8S$Ur}aibpbn@xgK~7s6NO34L4$ zf_ToA;BFR#<3%thw2Y@W-Q^7481zS}&Um_BtkUmF-91hjY{+c-UluhwZ+R)yYJg3j z*k3fTM@@(*_W8R`-;h1OtfzU`T3;;9!?WJinHm#Mk3a2p?#)N{-rT2Kg+Jul^?Ntq^1yVOm$>dWFLLcLC!hTKFjYh@_k zXl$hri&t?N#k4%4NH7wsxrQYH)T)wORya7!0Eli`NgFHKS8cY;ojA^pRkXF*Z&MtI zqGqwG%N6z>QAl<cMqc z$!U=+F!cXuSEDVA2v&l;ON*AGv?)ol&@?6RMBaP^H?*{K5r3U1gi~pCs(6D&zcDTx zs8xS7IFnQn<_<=s(IC7f)^$LiK}%hyvH=vztEH+~`>uOlCcm(kaS$CQ046TiTlHiZ z4?Z>vusYhKFOzh>CELgM^$Mp;;c%(EuL2YbMhFB*9%U#b=m+q3$amnUor75bkKgI$ zLECP%!V|2F4&r@O{Rw)Y@C1b?+H`o7(Q6`3uhVAM>Zgmh0%47+FUoSOnLG<9%~6;w zq@Ggxpz{D$Grqy}7As zWb54uydKjnLx8i3w93MBqLv%Tz^_scCgzGr+F%jTaJ2`Cv2@>k|MDhX1eIw~4sVthJE*a{lIm6sqW(IR1o^sQ2QOUS(Polel{9#u1R zSXy;;As7JNS%`25aA>%lE!ruK zqG`z)jlNRrl62}9<<7@J9Gi}wwtr%W}KjBJRsa>#k?;cKO5O z&1qz)x^O&zqTV2O5zXpKO1e^)~s^JR62og|jZJMRmdcxvt9KHrER({pNK1=)vrdlw%| zOWhJ>^L8CCrr{tc;%G0l+Dx+!#fdie*d?&jo6H_f-G=P;pBUqT=tQDJ8uDy{`Gc0TQU=9w!&H)^ z6w?x=8H0uDOWCQ|^U!Q3CyDm9BT(e!NIes_=_=Q7W?mRBZs)=B=GJ~p0+8*%+U0Jy zciw6B&(5R3+}muyGIW|f#Beyjxyl$3wn-g8w*qgn#5M>GkIp3O5L*&yCsATk3z8K% zI?U#@)Pat4dQvvJ^WcC2&Y^;N@@z)eNFkSy*8ec5UX3WeOVUXifR3cMX#hHseT;C_WETgGLF@EBLoOoH37bNJM?XPEtnipsQ3Di`g62jFzu_4YdV1A{kJ9n*_R|28YIJar+Cx%}4h~Yy*+r{buhpT#b}L$X z4^r(}1aV%EAyN&RwvSYcrtKreq!smjq}nuXA1Owyr0*les+IJ8l=FpVF3ON9fKRl^ z;UV3Q9zqK=Uqy%^BtC$k0{bv;pa`kq!^)56){LH>f15}!f@)5(K8h6rL! zVW&Iqba`E)j7f`|tit6yBwX-ur4jLrtThFKGHjvQ+M3_RHnd6KNw~Fgzahus4|K^h zI>croqiiZjmcRi1JeU}W)wWH%?TLjw;|D*~rSd@WZ5cq61cs23Z9mrb;C?Uz==O|h zL(s7YGqpaC{d<=6m*9!oAn_VkqgQ`lcP|I{pGvE*cyfi3E)Oe#RYzN}T+EgW{Q-*b zo$dgog`7#g&Rkh_{wmR7&l?!)EbkBP{XE1a^(y{JbmF&;LPlK+*&!-&zH z*qG_T1DL7_K+`b-C`IZn%kB7*wQi$Uv@TM2=JsgcNNdWXl9u#@=1XR-^M)3bk!+;$ z5gI%CS*{D6lwjc1JBXKQ36M9Qsq!wAwkJ zD1(~AzC2XrI{=Jj$(kEnWI!06YJQ%GpvO!O>YMG{Xh~u-^XS{o!u0FCv?sS!_EZ8R z9!2D81}c+4fdlDhm8)ANJFoi*3CVj9E%czAMz3IhEN1|w*Jj-LjZPl+ub+z5cn|n( zSf6TMOgkpg1x;~Zbw}s%SDK^qN~c#zcwvz%rj()q01QxQArRm&*#mvEtCc);MF90; zheC16SPe(t(zp7f?{>ZIVo@OZ@Ehql(E{LhDv>RK0B_9$NI5H?jlcL4w#y$}PXaG_ z>XB?>bGb>}4waO;Ia&aEbBmwpD1~$M5RF#37ekzC7@(nD%LV>msCL^WeJfvJm5H1n4=H5vTJ&Sg%T+Q|P$&}21% zrX@;N*Hm?<88v2wr{9}iG1g{@V)RFs2%{oRB~16|v#vqbqt9yCB>CvG7HcYP>Z8x< zmuZhaC)VqIyGNg;1GeO&&j}LJ2QWH8*b9pq;B5~1;*Ga`ZaLtKwMVCvFM4L{Yt_r`lg*E{%*-F=&`i2cLLe$$y^lEIG zqmH`t=n>t@r>;)g#~yS$o;5#)M7uDV++0+d%&VE5+G%3d04lxm$x&fY8r=B{4<2?% zPLrvn>TUAxFUlVxF>ch)&dGn>_iiIjFWWc6@ppgS4%}Z>;B-Q1rY>#N-HUe5E$*Kj zWe+>qRK0Z^HHgbj(rc@o4-xGL)-WGqn4%@jnb1$k7Dnjjz!K(E=;zQDM(F3r0CRZ` zbyBgG$lwFBdH*~K&xPA7SIfOJSxw3f z$RLrHp+hJzog%Xu(b5X7&ASw74IlX~MY=)#X5OVR8rLRSA;3wBEFBDrYI-Bt-ev$4Re&UyL;(0d1t7^KQCb`Ii+azk3d?07 zIR+5`6GCMLdYdlZQc5dw=`b-Kr$saRvFmltBCVWac9^Uqg60Y-$JH?t~nSpI;Y zu*GwH33txrkIwPpx%?R%f{gq>y!yAC>cR@~&`{%mm6jvq@o}Hjjt_E1FU6?kdEGi_ zX=K0~%A+;C+9rkL2=+BmrdveSN!AGnq^_~F!i0bb-)RpSXc}oWS%S2ZSySI$006iy zl4#8VbUhG&vJL_P)cQOCP&Rad0P0OGb&M*1>KA9xP5aq=9L&cH`3j@1fuS4$mSU^- zKC_lQz}WV%cuhipmIt(P#wEy@*ffA zsn05Fgb9vm+N4%Q#soiV8@q11422kwT%`0c1(3q9toH~)4Of~-QipfpcFS=3sCjHo z{z`M8$_Qc#1I-1?Ua0Py0Qy~5YRv(%dH^*j6)7kJ6iomM_Na&C^=JkF2{TnUIfmq+ zurLO)Q*OdT?hMNC7L*EvRHF zx20|E+z)VwY|)oUGzyuJB;(x)f6mj^ zx4_0Eu0owq`-#nr@Ii2W9r|ks7cqIFltS;Alw&PdtMV~FT{%gzWu8&cwKA%e`REMn z=ISmf>*BYjL%Ormi{uJ}%Y|A@B8f7cB;kGE6w-uNCKB03p#kNggtdl{sP4DSsI{ch z$`GUZ__I=wJMpbxA0s~qZVsXzFR50-f_?BttP>= z(gRx^8no6#vey|~@8IwVjq4>(Hz6uZGp3`MHXp@DOi1y2n33Ywn3AG)PIw{t>v?cD z&;*wZZPZ9SJpbsz>uCUx+_0DSkJZc_6XFDs?+2yP=P{I;)*caPzE_CD_dKB-l-nY$ zHN+JXIjTCO8p70VG~u*2Nj$ic)-Wr=K=v;Ue4h{q$C4cCVw;LxOEa;S;5E>K>Pbqa zuM_mKgMFrRGCyIST{Qy20C?s1B!HR}pdliA7-Yy{F!`J{9Nx?{EM6GG?Y0{4T}CWQ zh#dgGZ%P0WN)6T+x`9_HIfxprNg0HWEz6NN@IR|5Owu>%;QQ6`zm*rDxjbE{(z89h3(@?^a zNt1Qpl<#uBNkb!k%!h5hH!tJ#eJ%EBkM!WY;T50qRKgZIrP69`rU#EJ8YyB@p88nJC z3&I5~-^9K{0pKM8pWdh%rEQc*@H93wbsH|PbWeRHzgBnKt!@vFnWG>we>j)>DEp_b z^+R$57ePfa*-F3Y7=MX=i>p)^3THp^Z8^e|?(&Md@fH%ZDv<_3Ql~^SsefO3qxlm2 z1O2S3-z(Glr+&HG>4RIs=2i97iY1N6C6H`OBeO@?mr04b%Nns|!I z&jX(QJis7q#CM=tsUODxXuEQ?nwX=KQa?@t=n%HNm;4q0I85o`eYxJiw+9&v0vzSb z?M765i0rChs;28wsd*)T%Q80l=O^(A$zVyZfa2|?H|WmF zBrd<$I}DVva^+9`ddEe^<|eF8k40wn=*T6??#oL@$8EUf?xW}!YpGaA-ag{z$@}Er zsXttp%=VK>TudvV)A=FtB?A}4BM+51#mtg-<>B9#$?X-pMznSpp)x+|0)&d|k)?~4=0($(t)PA$hx7A>Bi-z>Ff)T)w@m4efSbJ=; zaU>E*5xVTzE?O0&o6j6PX@aw?VT52h5iTomM!O8HC2f@i=uCk~5x03jyc4#oGD(@( z#^7RfTHS7RwIlKX&@obP%$X2p5gA!8flX>5?iN#B>sWoqa-S}8SrLNIP>O< zaiVg+u~izK?Q*AA$9G|3*Ql7dooYs%N$NZps>h**HmPJ1Eri2RamI@WYEe`Z;w&P= zhj8KFrP4^7q>kgMs?!dv#atHZj~t@87AzKpX#7HwHXh;K1@E6g7dt1Qi)0fd%+> z9!!I!NtV@S`njpqYb}f;pLbf#R-a2WmZ#7h-k5NwP&-+MwsNXy7sj zQ4e?yO9n0*0S>grN86s-68RnpZ@3@Ptyrt#;h`3wUv$6A1r8Nf6KteK+& zcxxDg5kP-AwP*%7HH^Uskn?pl@%67XV`g8|FUxqkJu2XQEUU$eqTgs1UGyisKhK?b zS{Ma%Ip>khBMHPkx|HLZL9RsTI;vTVuYehr3hUBoDeIBHpyR(_CNp>ZCHE}ajem8j z9yxXCYpwAPdE6wJk8j4N?F;?V;6LG7+3M8K>dnCiDaL$slnL-r1~@hYT$KL^H+MeN zMGx)hM8CXXVh-to%uYGlOCi8p{Q_nNkPTfECc5BcL?H+=^qTIaK70fO6{MQ|aJZ6w=Z;Dr;9bo>hR)VfXOfeu z=rFwlNBf5+xxGLO0@F1fr$IM?x|yR>mUwr9QXcPKK4G9M94z5Xa4_}A?>-@M6b_I` zI4*?djCH?YFs&QeWU?e49VF!flaV25^MY80u9Urr*^Ea?5Lop4I$s0igxs0P^FPcy>ho?*w8^p9$Z-S7PEDMCX3@U=>GHbaATm|FT`l$BI z=*^dDfn%`3VoGVDML90Z^U9z0Imm7L4FQ=uVb_Oaw@62B;=Kh1Msz z^u`N)*ETYfNn1B6K4$1Z6qHPCXFk#@1-9}^hq`EM7fD#Rlk2*1L5H)-f$mO0Uvl6G@gRdbgsXaCoiKXgA9q6VP7zT1yoBMxX z63NEobcR1C3IXy_(;m;EX7-7snR9ZbFBPAhm^o86uEX*TH1Ql6qe@S&{%5zzZjH0t zbPZv~R;i_35K5u2tWs!3Z9%H;`k%R(o>+E8T%{I?Rr8xuaI4f_HU?MY>4?|))@JM& z+6Z(~6*{dqtE|j8JAb)sqA|gCIazJ80pitpI`mr?#GyY5p}Ha$-KCD?av)n_1wVs6&%< zrgq1L(|JP;(i&Y!gST=@jKL+f1&k&#qxl$;*++kR^$fk>sT%8q-jGS}VX|Inn)*1KRE^ zgdT_xJ0C6@4p=9swn8k_!!AYPJHzFC4h021`*W!6;RM$R%iC>p#KGJNhh!(U2mx{# zl8zREbLV~Xu{|YZ+kqE-;=i3CSsAZ<=NfL_lBEnl9p`qjnEkl7cYlAs%l7rV!Te@# z&bkDJV`Jna`0d$v;S7VjJMvHWyj=LvIrQFo$A0O0c;)=oA!Lg!MuZJNI>6|i{ga*I zfm0~_xPSO#sp#zNAMfuwzkNlm;B+`yj(q1QrsTZ#)$Z-jN;!6#aKY`oxGKtDMK_RP z?wzw}MhJaC6OvRQ+KyIOYEj_~BLw{T%6S8G^&>16@cVl@H6aGg#-x=$`q9~aBYowQ zfiL3EtNyjOoGcuW%PDQahD(|>gQ<^wjA%5@o9NpOroo-J7~T&47h1b<6F4*foM@pq z7{n#-LYTZTkaHS@&xHWb5{vE=POz{WbE`BDi&yzBDxN^HGVT#c5+^Wqb`v;h$u3Q2 z?EbRmn+mNkqkOaZ1`WjpF{2ePT=atWJeZO7eK{Qhr`rJ;o@}yMLH#EpO-P) zQWyV+1B0Txg@>oe%N4lLU;a$Q-D$u00YeZUXm&VI7k4{>TSjz4s`%M z-HQbb;3x^u?{uJW3JYGS0LKXcw#zd0aFPJHDEBJoW-h%=0I+Zhty_@jtu^N5XD0@`+6j@@`4)h=hkSP94#9~k>zbF2<9w3q2 z4gmG4nItuluH$mPy3MnC(^UbK{9sy|CRO8<=UrP+uBY75bIBEs=!mLs?-aIV=uY`k z3mDTU3l>B}nqV)D3a+Gu*UF8qo;C!K+MeNaajAosjlsDKS%InK%c|Nach1}{*(On0 z(E-R4i1ou$8Khj=f&71lT|*uMl#KvHSKUT80dQ&rxU5%u=Sh-e&13RM>=W5XK(T-% zKvf6uzTWMZ8wqJg05=5ye2)Zxda&Fy3k3owj{p+@)IKP>UVwdkP?% zSV)@CPFXw+Wd)#Ww&7GVlzbLig)XHBrO+Fz)W*u)X#tcJQz*c1^ayYaYSydUtkfHZ zao`w~1e?Cm1!4foD+JMzhoU*c7-YlO>y(>aviyR}f$QY^GWo9raA*XeW>$nifFmOS zOrq2Q1voYXoSo{}S~LPA2aY`)C;xP&W6y(P-}7=ORm{$JG^TG?mLVWf7jiM!eJN z=*MUq1~Q=rtmSI8L);qHfmk*k%2B|bJA3(vUGwxCt8ebSzXfyQ&+#;v+jnzO{>5dJ zm#t+mi_!-$wW}*Ro^j`~A9>U->!ksH3%1@g`3DShzG&~K8afx(~c zDTpkGJ3};tRlpWw+Q^cYDiiCOzo7hJm;Kk`)Yo@B%Q$2r9 zL7fm04QvQnE){hEh7&jhD5>Ws2J9T*P$?0@*jdz`X&^vN3qap})snQY1xNu-+i+r! z0BQme2N;C27OMg-xJSSu>%>P(T6A;R4LFoRXFCF0YG;|k7JhSt<`p$U{kx= z(^yTS1OU80%aJG%_W*`QM^brT*MqJ#2hfs)Yo{LIt$d=7$wh;5w+-dHJ5Zf10iY&( z@Pr{EPa9y`qY*3V6}pn~i0U13xv!>SF}?4!xukYq*?5 z%>(^AqgDr5+iB??Wl3^`t7NO4aof0)=a8eca_pTBGjS(=Wfu%uD03IX;fJm!0Dw@o zVJ<0_3^%tVopEliAtQ=KR3t$yZ)ks-ej+9jFh7N~@@n zO@xZC)Fy^>Zexx%_K>vqxS|ww%MeQ+l16uy=JsiPJ_SH=`!qhE3ZQWkD_H=-U|^OSERBOD1E@Zh#=+77#Djn_6xEUnqMVs!^S~gR2Z^Uq=?k{{WK(g4 zO2n$wgwYFvr>;D8d)oTIhC{|uRD3xNfWqFlZ!>#f1Kb<~V6xvov4s z#Oy=F`;VAQ2ZT$O$!T#iiJTc=yVMxYHRGd>sB=4Gd5|KIhX#Q>G}XC_S6Z?_%og*3 z@C;9`3V52mEruBWptZTQqOqF5sj;~cK)q=Is3mH+ApjE@G9$Hm!^KT22T(VJ5&)~Y z+UnJ7#4Du5;Ck>L;@L+m2yv?9-M2IlB7KEZ*&S}Qx^CQOlS0b?`pq7r53i^2QGqz4 z^zSFOKMAmlHlgjb-hcL@%^1z0T^sRA6S1_MTApip!Lz&xv~e_RgI$U%8APmg$^=vU1EgKoArmLBoa zcwU2K+4*R)~+~ zMdcZxMMRQOvf70aiN1SGAAB0}R=a8=k(WrZe3X2wtgkkkQUrB%QOU?0u5RkwtO%h{ zeK*s-#mF+t=|0w|WzsE`!d;6ci@T(XuALI|S^ZO(FWcEOQzhH17@s*^v(4<0Xw=qW zs04vejl{EFawj&gVx4l*lsox*6_b=X=c}k#b|~xcS6ECAMkv;QoBKDrvxoKe0Wpr_ z!$W=jx58nucyz4Rf7?Gk`j40TZ_n`a!*BMM;e79EJl&f;EN+9Tvon76!>b?iRB`*k zs)*aG@!c$#FPw3(9faF+e>=p4=I!tye1#Rmhu^k-d+!^Q-Z}KM-B~c(+<=cZwu#1vyWwI4YbUlOnyNG; zpOAJN|1;g#ig4}H&NiHl-ME>Su)tdxl3ksJx2?E2-(b)iNPOfY zg%LLL3-G>9CXhHZH!yqCU_m{d-3B1p_}QY_-HfdASL4Ze@xa>ylQ>K}2*dFe88{tA z^max_DeOS4{}6=5;f%!3x$HUd@A&x+{}F!2FF>uhG>gBNC4OVm4Jt<)7EQ&kMX+k% zhw(6Xor*WzSVWRf=5MlqYW*-{6p`sCo=!54VSvU0m1~a+40=s-Z_F5D`Wc5@E{QxW zs03XRaSJ~|Eltvm|7A8A5648Jb`x9>f2TK6LW+USA_Ba5Lly>c67*zCkk+Q?} z9k3=I+Pe62!PW#g;Y{=iN<>X{<(<=;FMa&r2Zwfr#tRjmO?G7LsaYFSOS?-u5Uf8m z0UF-DLzx!7LG$ks^8+i={L-6G3CA{HHv=3?G_q~7bz5-b8Cj87>ZpmqEv2*Z+WEbN zKly`Hk{u$Yd&~K~^Lpd;)~onyPVjHuscjY7e-La4P&^{42_iyy7?CQEmo9fTk1T>Q z2O|@j%0)hhc{s^oRBlE+uEf!pFC347kGy+{mk~?~KRMt^JY^rC3=<7J^+LZsy$;CQ zd_{hBh7&KO#$r5$J*Ee%g5zBgR}M9bR?&5?$v%`A8-idH;y!yf;GyopzI+dqm#pJ^ zU@fD{uUpi*0jI}vG6do^+y=|Zh*}SZ4J;##-H)8n5}gWGxR#l4f{0n$&08-7gL>ur zQ(iIFxq2WZa^{c`u&Tpb{||viJ{cSB8-SIL2^!wDH-V(K2MWdf7VrUg(NT_E7-~>s zBy52AR4G|)Hn-42gI7lRsP`fgi18WIYnoJ&!d-Mc+2CgLG2jlOVT1eXO#OS*B)q5q zJO15Qbh7vqM)t4cGg%GM;)g&ETC~LcZPQxIjM;!a<}|l@bOR3L4hEZ{Ke?97E&RZT zyV2CX6ND}LHX6D1TVyiTf~ZS@lhEdo-GNg^73>!5VXqo#H+$MlBhg5CxSAt+P z#!Ji|K|vZukF$r&ohKaU-Zx@n{D??cjUmtt5lNzPh;&`Nkp&P3_|Ey~zkz2i`OAW3 zQzVW0KyaUX$3aTRL;5GHbxv0b_Qy{E@iS~DxE*sgHqesL40LB>$^Trkzb6y%lcNJ? zi?NkcxjI*$gq1jUaYcgppN z4~t+JOq_pW$ArUke@d1Z2u6g&2x9nRgZQ?iV7}q}n_~)Q!(arsBpmEhj*77KiY!(U z5ZHy!1P1^0CjNB62_%a(VuD~bF5VOWvmevMcGMKnXMd-|Y2icjP_R$E^Aj!lB$Tgp1!ml0ptmxMbtx++@f zJ}j0$v1RmU0!t?2r^wy;nQpCMpRp4;8Tcn-`li2ov+Ha?s%z(TfRhC=CDM$j3c^xZ zilO-v3hWDtps-uq+D-K9aG(%P@^Z>x!M6d~3)%fc1?4_9_CqqeHvt*;PV_687U+%V zSdyUMyWjg3kkNF;h1wr^u|erev#pAIv|60_3$UMjW23!=nAft|1~t%;yFh{)Wzm zM%|KjZ1y&it=RlaL&A?X#4U>$nACL;@n$!ik)8YI#(-GT;?`dZe@*G}_5N#RB?jpc zytejydEl-}XrlmQvh-)u{m^4NBDJHAE)J%`=>Elj*knBlL1XsG zZr04R-Q7M5CX1l)R1r`zf(@d_!pvx!kwMxtBYe;hQ7}iei!ilaOcc*Gxe@PhI(J3> zHOU7_-m(5T)c}Qi0SN8q>RE4wRolze6=H8RuC3Tukav-2{4|?vYzBlYB!W-Q9%I3g z;L*(p=!EfH>jpAo3I&5ED#FJVnR?Qe2<}uIGpY_~19R?mZRK%_W8}2*AvWM#nH?a- zpX>ttn?TgN1ar1*8q?{wX)0(M-6HWfh*~oYfhR*D0sBJSEVIZ2;rM7dWRZ#9Em@|b z*0A={gbQclO>dT%`9OTl891Ea+{0pwD6bw~{SZH%K7i!4Kwm;6E_O>@0={!tn?UW9Z~VJU)Y8=y@IW0SE~U2lH1j z#00!!e#>(LJ|-RhVJoUes{U z=p5{p$W{c7D{r?MGteueA^9uc*(eY$6n75)`Y-a|gNP-&EM|trMR=WQ2hQE#%z*A8 zJU+RU8a$mKR${#1n+-UgCe}Q>Hoa@Y9S!Hvbr%P+6%!KE7?T5ruN|!iJFqc?4S@rBJwSuF*Q=I2Q1U-y=gy#Wjyn2ywC@ceuEaZ%d9;%#&?U3IFYr_RwZH% z)5ha^>N=s@pK#l6S|sTEd2HjtH<;#KO7gfSGVgTPs%gP(x3NKVRAOon8KHM*FEuSB zV`yxZlwmW`l7*}}zgVx|F?eg-aX*qHsk=H3%KQAf(z zI=CAtvM?T0WnzfbBTH|=yrOH}Y&Run|9p7;oW|yK>f=Aj7b!#XP4S&KoCo1nVB4;k z40fY?cb8?$TG7Dz@}g~A~BK z`|{|zu=eywh9SWLX2K`NLQ@P`dUE)1cNI*a527rIv9ONlkt8ZZ$WlcR9Yao*5D-x* zmk_Y=w4G=I1UyVJxL+SGVFMF_Yf&jT8X$c%YdwrMbwubeu&eLGq$9io5F0k_+hT8+ z3NH6C%iT%bhf&s&owm7emN0ai>Id3zvf{2)UL3Y>)nU`JC0aJ#k$Kg$Q0*B_-O_V7 zvi_Nkm^saAM{S-}E3hWUhTJlMYoPy{YIVl_IQm52l!=bel;sZT zT2lcM5#uN3wp5IiPZGNq6ej-jj*C~K85JtA8GfQ~H%qjef%~btjETUikl-YxHsOkO z;Z2D*83N3B=uCo}@o*ddxq+Ju7^#PT%HZNC(4iAmqVgaZw^LJCfji%s9RG=2QCHXzbmj5Xd*AAy`Nk;3QK8 z-i%EM}qQWJs#Ql2&;a|v-fS;&P1Ee-@%PEYL}_ib&&u6sb9grb;M9SLsYk3m zY4$4j{M(^}$`t#?UgXB+aZ{Pyf=~**yG1_G zG~-q+t6gOC-idd?l%N~=s22ORN@Oddhy5mhEzrS#RYF@a0sL1X%>t`|$BCU1BvZm> z?2;$E1hhKBcRbcajc}y;9yh%v%L_^uea>*ebPL~$ z-7A7Al1a8^QFl{Xxne;fGTaDm1Xg+rdfq^m_$dX#dp@u*jtML1!33^*xaS88&h-F( zAn-<+39&ZH1T}MJF=yMYaj;A8-PBu~TT&Z#QxV^2{BeVkO?~Ef3| zwTXyGr!$GD*2{VdVrMO;AQ3`Murbli#+x^9oLae8Za63cV38w3zDwo$M`tV zl>WHk{GR@{CD;s5gV^<1Z*g zc-4s`^32)OT_@mLi3gy<2fNpkWq7-(Y$+pch??_H^z9?TjoR~1^zGR&5?@B#<>UN~ zIM5h-@@q1j6wdF0hwH0U7#F%&XqiVyUan0sxCBS{BGz9os1Fn(dU$t@^d0O^fey!A<# zfGTvY+y7^IAbNrm-UvQ2C)}LkgmWjI&RXQEQ+N8uq_9ckSYEH&D3k5=-E1LlKCr6| z+K)c!vw=Gf*w>Du%Y+#;~lU3sJMlp}+#9y_uMOW@a!3$`J2SY&hY z3?pJ&l~o1>=Z?w9k9h2;e7hSb;SpV_>_i8NiI|WfN~3m{8m57*FSxHBhM;i2rqTmh zE8{8b!(;O@mbMH)@*O;eSoMdK5H^TgQ9%Z>|B4QO&?cJ>H0Z971yO!t9RE44T;Pf6 zI*a>&pNaB4rch5IiUDveq|Ma7pO8`DejPn^8cBiA_-7YbJf2RW0@)-Oe!{vc@>&OzdK!%A@rL41kO?+?MW$H(h8x*fXIEBN zipb#>lBp?C&mA6+Q&JfC9$wa1h){atDZSepD?eNf zp0R@}f}ObA(-C!c0>SH=d26!nTJtJ4RD4v7>M{0FB&%wghVDh$7W7u8R~u^?ev ziZEm}qVtm^4D&C>a46uhNg{h%Z-(Z3Mi7zJQVa|JASPYx#+8AfF;uo1FX({-z4?Jr z{qZFFBxX2rX~HsUfn3=$ThNniKA>ne#8w}U(k}z$)xrHASA>Uxg;qNW^a$0heFHvkonXb_f z#4o-_Wu@up#Ekhg=Q>WU0UN%}bL`2&j&;_EITDauONK+VZ{aRvIzgNlA5De>SJX~a zt$A=KARz*X1pG{9bmT`N7FKd9#6kq9pj^#)wE+PKi{C&m-VMDvz(S$1>`jTxO#JC4 zHRXzF=Z4kUj3UQS(sCo^wonXnL&FF7ZtD;KGu`+@^d(z9{=#K1 zZoSPVO+fHhB~-%wa3xB>LX{h0c#_3q@<#|&x6K40NKIvjJF41c^ddF&@pn$;_rFJ@ zNt9W+oyskMm>&^^rqsBeGOg4{kd46rjKkdDC40xsjsMxbh4~%a!oAXfp!XKH zyRZ)dH2R?7mNsb5vMC9cs5j#rsGu|-PzK#7nCFoH&|9y3L#E0=vdn2ZMg|6Fyigvn z^4j^Q6#z{iO>b(TR1^&e9_+xaoyi!h5n*;NXz?~Jltz38#Rc9n2nf%^QKWz@8%b@y zA={9-57+hM8kM`lTYvazK)11*f7u{yHn60_=N+PtA=Yx(*$Hoh`Qlw+AO3>+s~dma zf(kLj-fnIz7uP!{8(X_T=xj4-gP_+D7;%ekC&3MNSfJg~ER%Qt<&AASq{nW~W~=MM z!PK_cWrf&xf7GUb;ny0A81ra@9X@QNjY!`J%rtIglk=495q1UP?%c;4LYo`E?f9R^ zU@U$MJoxR|n3lXC|8&pGg&!SIXig+q@SWdoBw4PatP)}q=wJjfIvkG)A3IM=m3mRU zy8-dhuLAP@9BNEM5^y-2kFR(x30aE8Y?2+kP$$4+dJLJiEAMK82|K(#6(HlMEtmqH zykqf0bUW^jCi{H$i5E5r_|1ZNJR5s&qM+F*&Rivv_oJ09aNfW!2igiA8$D@B0aolg zo^$@Fvy=oqE7d1<5f$vSqVGLVSdA}2ipb)m!hF~;_%5T8a}=%6@r!N3RX%nR6?P#ZmkFncF_~X!gLKa`^W_6yhz*JcJKh3@KI{S({^TK19dH-!q&^VOgr?SEJRU1e3z)auN|H0xl~z)U%l@K%h?Cr=tQ*#e4WWC*xXfutH5I+ch#fAz2|{{|q5 z!Db&IRX;=4|3-7=FYZukiz4_gWycsziToFY3O5!Q@DL8PgxwqlgAKF?1(LFtar_5foH!EBqc`0MH@}m)9H_E@ZlP zGttEc0L6R{g<5xc+obHn%s^@jsw~ z3D&#hu~9ydXASDfQ2btAS%1$!^K0sACXzp= zlWHY5(wy}KA-Pvs0}X7I%a~Cf-FA=VWUa-;QC6FP<+fZs%B6h^#la5ThT72Yxhp4${$JBgGEh z;Cut_K?r3GwjocHxD1%HHx|weVW2Lp*38Ge8ehL@iAOFI-18k-3+(j)WlTKmfD1O&_%qGtTVSwBd2CCt(@}v zF|+{hnC7ietIn(-tv$Njc9s)?0kU5Orzrd(R#;W>)O`XEO>CA*c=(4ObAoo`(v#TL zhMu-_ygCQdume8O4H0@;Uzivae=@=XcT0X(%cJE&W#VH=hFsKS@0x1G3+gP<13vtH ztSz=Fjy=MRNl9iFsUnag3D9=>O+Xq3k1#QYX_K7Z7O_d0@MxWTN1^|+|5u55X@0P_ zVyWyAM`#OweI?Y$b9Pwy5n{^NWqwSgk+12)ZBY-lC;Sfohl>WkJt&sU!}HZBGp7fC z3E4SlFazRO{KWD899>w0g6N65=+X5xULoa^|3LRhs8Q@{nM12pX)(k_+Y=~gnf3?= z2$L2?bjvM)LA>9FX@pvMesL`U&g)D^7>KpiVxW}*qj$3+1TaG}{bIE=erP0KUgYq+ zwZ1D2fG}XGeh-BSe(yNXKp%~5@3x=W6FVOLV& z1A1(PrPreCnnJx#ELfse7>xJbG!>Fp(w2(8rur+-KzX1~H;%=2MA1aJt$%){@TA-<~_OBKHrJzCz)h|Sr&^S%&LsoP4g zZ*QRx^2>L4$kX(diEFn6P90atCp-OcD=NVu9EW#NK4a~3_MkWOGje85Pb-NCph#}L=Sobp#u1XB?s!K)xq>6s3^8G@E_b+#U>F`+(#p! z@DnlQcqfz1>m&$1EoUOW>uC}NZ6#wNlZIGm>NH=Db)|KbwOCq;iDny9pw(+bxH$74 z-EEj?9i|Qb`EU8_c$L+D9W9{xule$5_?oYMhOd3GQr({VL8I|e29U|cg16K#J-N6C&~onMy1 z2Asf+2&LfN?odAe6EOb^vHlA%{)-r9(F_7t=Tr7X+b_vq(}TvdV(S;uEl}Idqd6 zEo=g%%9Xc_*A?N1R({`9AM4mwUoVV+k|80A!uZ?=Ap{vR73kD#Y?A?Y8ax*+-_3n~ zx|ka5jv2FoZU*TcvNt~Q>i7Cl#KirDwEQdHaJ>$s;6uVKpoLzbh9uPrt`DLfzz!?E zQbC{Po$~MgP8=NSs0kZC>hXlz?B0X7*o_4LTB%%5U0}ue%~TVvAhBXlgRVL~%(73- zLdK+rHBp0%au@{!G9d_wR~!?vfE5qORMzm02;ot5!Y&o~;TD6@s}z2q`N_%@3&Mc! zfFM&JMIEjKk0T5l{WV2S8A#+uz|_S{p5h^r^{AcP+eH^Bag#^)b|Z1=f&$^=Fe>sX zT1>tHQ6oMa6gfmb=20?StBF~F_J(xXin6(Ups(x+@BM|;EnWmOS_uP}w~(F=2#8n( zcy4ZE6H9Cd-Hzb0h()C~?_p=;+>P-7MLb=JO4Gu%6?Xe2c{fPn6}@iHOYDN1z$wIQ zdw+25;Cd9?J81nPop`V}8W~;j`?^2!F%WJ~>B|R<5}=*moH6+MlBaylE@0si5u2O% ziW*Y6M`OCGth1$((Z=6WWe-!)$6LGvtHB5?6hvcssUyAu6kLnEB_m2ELEs~k!fIl; ztPMZdloIe4+WN2T^o8BlqZoFjJ5&+Zlw}scw&NEn;m@sY=C^Fid|pME(LqT_GyRl` za}E*XU`ZL<)e0GP1~pP<+7KoLi3V4-l0=E2@7!vm{|ckA;kNMhC4K6v=?%;f zuTxE^xuvlW377DkD1=+WTaR72j7P7ZcSd<~-r*rfJWMid`4jVxBxt7}`@r{&Z6+Vu zZO0FE!k;kntV)DL#47V1ZT@lUrMtuVmJ1Qmdc~k2uhkX~uG)MA_R1ZRs$WE8s?3WB zkLM(&9xi8VXP%SF-?cU`BoT>Ev6iUgOD-vJH`{JSftHB{MY;XGXIfQ;W73$v6$%&9 zGqnK=mJG++@fLVnnhZM?7*lfsn+ObmM~6Cb`31?GC}t54qX1BYqq}X0URN{cB zMqCW9F?zr?@s&&d-diU3qS{3sa4`0^MHj2PlNlR3}~lm~}ubI5ioV)<@Ic9=eZVT2l}dzT1?1O68g# zVPUk7VlDn6?++Db^n?mh&c@EqIxuiU@{No8CW=N2!{RN{N977tvGjxTR~;0~lgT$Ow%*9Y z8VbW=4bn$dkt<{A2R2-FU}SyE92AQ(i7SBq4j%b|^^)Cub7+u|QJnA!lo5#7F`TrT z@I}i*oEDAYC#8BrQ(GP1hq(2zUJDeSfM_iUz)<}_#VRC?Ldp7qMi-!T; zUwVgk0StI~P#FxnCaTN6<<%_UX@DS@_}x1S{y6K9EdN^$J_$@8WrPF;EOeJ>MC*>}4#pBeR5A-N zvK!1QBR~+fk0~WG9FOzehCsS7PiOu@%`c<%e&WcVe4GW$WL-pPFkZ$?UNR~kQ)CDS z@%WV?8=dU?ytqsWzfTmiOq|bM6%xX-Rd@}u+3GpV=?)A?0>%n!KhbH?Y=q!dX|B_xuLE$7Og1ov^2*Y`r*9i@nR*fDHwus_2Mp0d0v9yn=sScK#rWpdma_hu0$ z&9(P5u{n$HG(-;wvwE5dL;?Y@V_*SNLDmd?Cv+)7l9WU)M!T`ZCM(kziu9UJ!c>)T z$mew`301dNZ@N{bV&-+66(@%8e`18tv>ZWH)@XASd&A+f>`iP@qMoFLDB&c++MpBTNIu@v&Z|J$iA zoO|{roHGkTTJ@G@T+P5;0$6?& z9n-7@1+^4{p2pQcSP7CLOZdUN8i2xlHwnH@H5AxLe-$ImV2Hyt8Vf!(#J4xkj3Kti zCWnDOZf;l?xu*vLlQy%H2&VDL0R-l`fv{iJ)W8p2)geq0*Gr>Eml)&6=bdS4s&RQ| zXrv{cS;GjEXk6lTUW10PmDmA)=s^+zo?y^BP!R&Spv(&^;-lcWTxl>}g1^Ab6^5aQ z5Hh6oCh)CmA5W2kC0lVdFP8m4$PE|1H-aJ1`zIb>TasOh4u;{PCtBc7tdb*j?I6pA zJ{c;6?TK%$U;lqV!-)CaW8w>q##qxCY9P-2sTKlnZ1R&-kP38KA`RKy+necZrubvM zO#K)M%lK|K84t&c$paUfFyY`(vNWX-6MZLacMNH?e)-K#Qj=4?6!FO>$xVr#Yl1)G zsPwjQ`Glcx1fK?D*n(3NvNmK+#;<(|K2TECF z8YeA0wT@u|re_)@y#g;O5f{ApZRF?zf(I3~ZYI7yS!|oD2dqqF^-?$Ruw2n17UnE8 z&YDf0cB-VC#(+q|4y7ap2^|J;o5L+2{l`3C3T^o4isjk zcb93oJlGe|qw7`ZOf;DV$-EOXqD9{-fTkhI+_hvaS!+X9on0Lh$xMTl7BjnLt{b+lk~@E$!mRv}4k}=YEElA{s<=yWk+^z(P>R!t521ulA73FCh z`MDat35Y0=_DoMDfhW7Z!j>Gj6*|hX z;fiX@+l6{XVV5k)LJspjjXmK6`}T2SBf-8xurA4T1%hE$kR*7RD|~9Cd8p8=&kJ0E z>I%o+uxVUdz)AzU_~iJE%kFM9FKr?4q(-8k!e_FwZ)i8|ms* zn*nCV3e*-Puf~BSYfX=~I#{hBvf6vm>UBMd;$4E>mGQ>K4z#Ab65P!oYB%?9{4cXG zszpLzL0+Tk3cu6UASXakt!y|E*d0=AaxEW@V@res=fnb5EVnJem&IDvDF~#R*Aj+~ zQoA|@iCX4b&q~O+fO)_;7mtbx48&(-f~a zF%PkbIa*Q^hzOHi)L7U?;a%6n1`Qh~Yp7|ePAharUY3ec5TRXU+$JG|tSDFhTo($Y zoW5YCF@V9pi+rpfLN)fhOIptHaKwPf$2$yg*__>gU^R&OJpARabh?s9GZg;6|C=Zf z?0n(;XoKtJhIq)c0V2Hd|MVv*WB%6drlJxFUPK3TdPIDorSD7;IDNY)_A}NMQWk&G z<6o^_bXv(ha1@OV8oW>U^XaA$v&?qcUW^Vh`Vjxhz``EKrPH?w7b)-{3;E`VW`n3TN#gmuVR&9Qvs*lkZ8UGm_D8FkR}LDCt+aObc>U$omN$|rx_C*O)1)R zB4WX{feYgC;cBl0UhdXI@d z{X3SbS!t$9cpgLYGB}?ZI$a87w5QPSxzwG4KVGw|m}7br9}t{N;b?T8HEml42d*2i zN1YAaq;C(B#gxA&G@@tf4y~yaYhYsi1CTa{FnFTLhEb-qp4>Kuwts}GLoA3cIm1$c zOVzQItG?4j`{@38xTj50--Lw8sSuIJi}hPHd0oN=8R3i^3DW<9!p9+ZA&;A|PtM>5 zb&{}oV&V{ZS$yA4(1FM6j z#}w}xPq4uOlX{>siQX{770c!(eY`Tt2#|8_BrMYDMzVUMR-fACZWjciS3YxUZn@X* zxLqeljPZ?<&wMS0;&YthMRc&8zI4@vCya^Zgf$v}y_-B{WcSPs*?{x}T35As1`LF8x0 zdEE+5Ff3wpmW6nSVF?q{O`?-2o?$*-@U0|srNls$;$Hj^z00XQg|F07qXjA&u7 zrQ5b`+qSLKwr#sl+qO>Iwr$(CZBO5OCo?yb%+pM=Qv2o4zu$Its%m{}@Z>z9r{`W|XZ(crjLxuKcIDJncWbB-C|^iP)^=xnX6;S^rC>-Z6F^{173maSpl zbFs4tVTuS3o%ABcLMn6I1x7Eu-`0?Uv;V8C25OO#@H_O&y{y+ODQJ#*kC@ktPW!Ih zchg`IuXo%u?qlm~(8N+>-Z~Xew#=WTI*xwsy=!zgObY{RO-Jc;=OQjQ$Mc;AFs#eI zB(<~Rx1&mTcA9<1?^pV6ekU}9L_j?PUq^pUfYEuQrs2W}X|-*LvU$`zw$-wiBe5>c z>!nPyX^oO4JMJUhnm_zkFQs?#x(1q6VslY^bj8cBikE1I&?D^sDkA_5Jn<`BxQp%T zz-XH6UJ`#0Y|jG(*rOyHS=Y5xRcYr~iik*SV|!BS>u?d_&8AvTF^n)J*U__n4Bwg# z*$}pwiyx%TRIAqw@CFY`FWCJon&~o|l`Scc_EY4DKW|g~1l=i~m1phtqfW>?Y0k-e z+)xJ7f*_zqZcs##B6H5~Me`4Z%?VIB7N>D-Y~lDMDC%UY*mCB09N{X_tQVDt%Yo)| zilAKZf@%Bgy>t z!BnjHOiwqS9V=HMwz}d#UDaUvHDjAh2T;Biihp+tYP1H4t=zV1)J-FqDPJDYS_`@VUNe}1{T2bc%+Ef4xwL5(~Z zSz)~w4F8?F5b6bk@SXN^nq-FOQ0*4c@lm52eY3j?ZMkhzfQQ_{+rA#7>xNUm;aPKg z3NWt0{FnW9e0{V>b9*;hf#bcHh^kXUyk;?ujQtx&Q~0YB_g!8&f3YKk1Ftg=P!Rnt zqm2KFAY-){CO1di<~Du-_vCxI1d-2scFbVL6`EdkAd^E;kN+L71-z-`-6SH?s3sVc8-(UR2+#1&mP4Vy0F zcbMMn+p0V0{7NwQ9v_o4-j)H3ULM2qFp#7BOOPMOj&bDf6iI9T9nd?dFGWAlw$5|o zCBQcUw(9dauc$NncHy4k6--)dfdD2Y&17RQT3;8g!D=`o^c}KGS`aaB95}rcV`2Vy ztb0L$7*(&3FSIN-fhF2~i}8S3qwf@X&p|zw-ruD7F_73I6zfkbqZhkAcPXt~kpMOI zrPL*FU)UHT90R^iS50O=&t?13?k0jko$xzI-7kWFNp}Oo27iAOjIQz0KeUQ*I%++-y>V!Sr@d1&3&_?3LN0*k zYjhF4kW9wsbKLZe3MZGMx7e|3iv8!|^j4GC^Kv(3phV{-f#np|w}CcnP`=Gx=!ddf z{JvK7B8mcg68N9T@UaW4@X_|#k!&P3(2i=>g!_xW*rr(a^nNNH`8z(OG6RaI^q!%q z%JaK5;v0>S@#lb1Bm<} z0N9ZQG_SLuqVzXEl_(@HjW=lsLkmT*@fC|_{C+K1l_;O0owA}9Ev|ZVAv(o_@o@uA zYme-kL2FzfKnPr5rR`_if-pn?M%RE^`J;Ao@nYJqDKD~}>e_01t)x=tsSJo$Av#0O zh}pm^0m>YwbH)v_ycyqgvL$AsycQi#z(po{x9y zw`ovYvUa|v-(Y{DdPwQ~xG3lfQ`9+GxDQ%x&SnGO2O4F*zu1>9aH7fwovl^>jYvY)Q`PZ-5=O+^r;SQ5@fZghylQxc^&a56VXmz$4Y;QyhK z5XYcCy+?q7VayW!vdJdYlswy37%{{76yg1~WRsmtIAPRY3XIuR7;q1TV1r0(4w z-S4JZ^t8VOzaQZbry@8&70+_4Hqx3W^fVmupmrXkA%G6)J_n#z|Cb)|H35we$Mj{V zD((Fx(Qv=?c2Lm&Ne5U~9db}~8?(X-aoc}l_`&0D>UvO#w#y-_v@)=y``hJpJTqFS zlyG60+vDz>k)c|2-)$d11T?%1J*J{nEuT%)I|&nH8d-!cCZEIY`WQy;tJ>6C3OX^fHv|l>ga_jaq5p2n zecxaf;`S!LQe6Q?QR6iyecw0|CgNi7?H~PM+*;W7HfdwS`XGh4x3Yeo#`o|3%SY(; zZr8@yvX<;VF#HLiqNg8Jj9GC1e4Yq#p4PtXE!;FwOZLl~UY@W_-Gd&Brl9wZRg|*z z=gP=W`$x^(@Nx>37;ySu4*!JYmNZB57u4^G=VB^1PTBiHX!jrLMv`;zlLP=gQ>CGm zRJ9k6)A`+L!L?uK7UpuOjIYlg%tu0~5#%N(g%MGvVET;5@yT8~SPQF(14z8)Ja(Px zBwU95CIb|fFMnvE>8kXD(B$O=Rb~eown*3^_67JP30{b3?7x zC-3i=X=@0V{MonGMwAw;7W2gNcGUFo0ggxpM|+gt2s18bk!d}nAO7W_2o2$eNTvfT zP>Yh~yAmWqT_$`ac0XiZR%%%x9DgV}QOz17UhbY84k7Xdd2}$Fbze|AwbSAR!71EJ z+0K+)YnDI?9*BNU;;480M%Ifmx^%=noM(A-f4lgC%Y}|WflgL}#M@zJ&TK412Cpcj zk}h{WC89J6g@2)iWRY8@In713(qQyv4dWBjAO!*BP}^3tVkf*Rf2+25PiCd|%G%my zq@Bb@)a4!JN>Fv>cc+BwQED8{CyC1f8%sI-`C^oxvYh0>1$pvKwcIn@VEU;doiF1q zUs=9DdDrLKx6kDKMa~(h;2e!#-G4N{UB^IU@2x4Ca7vY>UbIPl;JWZaWTwD|Vn^;C z{30?22`aT*z>1EA;)6OjZuiz2nt*X*k^H_S_Zz{1ftwS5H8|ysygx zaAnAYD8lEddRhvaQ5^*Y5FBp3!;i?f6YB`g4f*Vye{=#g_KOT}s_Q3~%1YU@5zZV& zAA9N8orBqNeRw11xcqoM_%h-v?oy+B9Qpl`6Dk6eq9^Vw{fk#XqiXSO|^1KL-E{b!~sGr|=8pK=4Vo@@g& zea2V*+D|Hrm)_DPxyoft@yZ9i=U4v9kN9J|R(yaO%M2Y3n%edf6h{5h@2PObs`9ps z&M_BTm!4~n8MmG>lZc|AXmz{`qkggY01dF1e|jU8K;jZ91)qf=F@JM*9ClAdGx73d z>7@HN9Ms8LD5DLqZ(hD${C-|6tIdLVr7J!sCkZu{yL)6pN@mGq%lW=;)%F#OtCqFVk+w8`hVr%7)AA$w6sq9MOCS}dNZ+1<3L9{%APare5^b-u9JMXeUfm~dKHabE*HN>v7!?qrTGN8u=;vhqJBR9 z;_L#Mfqo6YnQrim+3LW(E7yXir{<_SAO5@i5V_%Cx_y?SP4)&ckZO%Z#Jt#qTM?{4_CHb5uGA@{4O z=YR+MBd8FB&wa~y=C!2nkO_j)8>));X0KXQB+o#jit!ptml$;ydwy`t!0X$Xn#{5UeW&grS)fFvUSwNrEaX5>N0{j!1j=9Uykg(Hmd|#U+Yf$U31xx|D#*G13RUkhAn}>yjREhlG)$_5}Uo z#H-7HY7R`>Z|fgo5aYnsxtdNjuFOxwc8RV@W$2)7Ep3G1u9@k+nk0ayjfs7rX9FbA?f{@bqX8;z-=* zC$F}nr{$XO_AAtA;RF7)pz}w83N7|$fK!{Dus!GJ;D-P77+-F9Wo-oN!B3gXGyUh} zm2rxnjIgQ=AZVe&kw~@5&B8=(jWTi%q}%A z1k-!;ferAweXfZt;_F4@djYHOIdNNIzi-T)exJY@g?A@-sV1*PQd)lKGr`E+#2GwV zh%QPL`e8S0Oq5Gwi);mV-$z*Q#oPX1C-kV(4r+$huCK%{A;>eYAuAg=u%Obut6kI@ zQVbseqg_^i)+MzXp#7T3yuu22h@4iU+lob{-Wq4tzj|{*xpxYjkTr`~2c1O zQl-@x=!mhkKwkQ~p1scFfO5chsxS6*eL?86YeIrP`45h~68U{^YuAuWp7+|8qK~zjgb5m77#YRBO>IYu%!gL7uJ@ za<)^jTlRW#1l~q8wlPNJp;ZGnffQ$a^ZA7%-quKu)x1qKRt^c~gQiDmenlB^Ec(ol zL1W{PVn1@@8WJ6F_PUOQyWSZa1}2IGMmp9wPWSj`zP4*zFYR;$x_%9awoPd!%T|pd zDcI=LtCobT{!*>I_ntT5tP~|1!$_g9m5acl$OFYHHjH35e~huTR%to7<-Yc<8KAGf z-u{-xxdr-@R6!dO0IfdYbHnzkR&TG9#1FF`<#%HHb(!U+z{W-_bjsFV zxmH%UX419JZB}KBXk;jb5_(BJUINQ^US7TQ;n|{RyE|JOpE(a97p>8du~@m~&|!PR zn@)BwU9m%G8r)1h<~UEQ*N9Em!ci1(EKO#Oql<=8%mGA5s-fhW?bslVf?ypod)m7S zLHgj6{+Zg$!_r(RM8(3(#0KhtiZx!pXwBM|n<6JrH&N>LNXx#zw-sIT3YAbEICu)< zSp~Qn;u|`7y+$c~&sL$6-u#CohNEjsdVga%yzLCZ60^Cm_e^sPXyp{y`p9$$)wxo6 zNj)sm@ddpuy24zeU}sb>pip*x^LN~svryvqm z90wdt(xcrB7l{*CuI@`z}EDjo2Nik}aqgDv$&l5QV z+T{}LH<-2IfkfIM>RxT3cPO^NcTp!F;+cKsEvp^{{EK+UNH1 z*!9L-caj@fP@MTu>LJdAxHlWgdDT4?&r#@Ol$^9^yN@)yG-1464)>&V{fxU#H~W74 z!mL|BcE55E8)Wk5{TC*PK`TyfS7ZXvvrJD6#W4Wo_}e!mi(z&|MI_D^z9I1W$d4WCQO|#rqjvr^dxoBm8LydS5j$w zgB3}oH1B5y6y<}hE}HnnD501X_P!zwTAAQ(hJ#E>`cCm&7(?S|# zK8Fs#&0K@scN<3jG%b(j;XO@F!%sTZ_{mQ#su%4KGdI`0BP{BeKrm+KZGa?1qGRs@ABnNfkE zC5n^E5}G|)39uWRtdJo&{jZGT&k81OP7&hl-}cvu4m{S#yn#u?M4Pvc z+-#zob7Ro*-iCMUr7xPwDG+?FXKer~iLdEOKc@XgVVPQf2$^*lCF@B(otz*8`!IIA z^>4xM$)Q|4lLmgmZa90-)I<(A{9iEmXtCmQrtShgqd2}t zY!E^@&vB=ja9Ku0<2-*MfJpo15KD+@Y_n54!x&k- z!ya}CgYo#1;E?R98tO!-ebWd~2zI@xu=!~u7rARe zcIn<}(_iAj$o{aY_!&Q_R3=fWonh408Q{#6riJNNuO)a8P`O6eC7Gl6g8v@Bb z!;y1)`F6a4pJ~hKIvy$WI1Xl2gvX>I&U3(}ElW(yM(bnr+!YY>5hA%~u(OKo0jMRR zYKE_h`buqiYkrX+hxE*&?L9W=y61DBUXjj*ThQhXu znUi)E8wZsu->+}~gaLMaN6N*9rN`}BGj$taz+<&KhyL-DMZDNG0|KIwH!O3pd8AWF z5I=#3Youa`XsXs@2xm|{aE$Fjw(+2^PWEsens?YbGkS8mZkzWY-w=#b@3-rrQtR@% zZed5ye>uV{ee74w{DSOTf6xCSEpGX3wpaU@@biYlPj8X&|4U~7{UWUD?m~)ce&2kZF{UK}Sq+H-6KY2}ZWciGPe4S(^Q-|%~n z$Qkt5d(Tkn=grBhNBw&b3w;0w=XVkZGfQ4pb_KV{C`|ZH@K@Kq;J-n;7Y(M&91b^3 zFr}hEEm}zr9#FUjz=c>dD6HA7i2<{Y9+;ax)~*R>xAmRwx?K4PZ@Q8ns>rYYStyLDqMl(-)blb~khlZB~j zDQL}<6|g686xVVXj)TgFPbeJ)PzMN)ys385op?|cCo>l)f2p_Ad&Xm#GdcBVmiK=D z+P)9I4eq~g`sbv-M$Fzk#^!(3_fNOp`TtQcKHbJ=o&Ohke*`aSlm3u9+MX=zwZBWu z{7~ZH<0d72<_st~{E+^faQgbjjsKuXl^yx{)|P(@XT+O~eeAQ$WW{;Aha#px=!HtA zJ?!NJV)*R*$HZbU*30YV<)wrDVCd@J(hXztiV=bYAVTTNepZbiL4cLM%)YZlZ!=IE zQ0SYZFt&5QEmHU!Dpk!d(0q^xfHiyjb()N%K?aY28vNNE0VKd1!B-vGw#yULGRlGa zM4H2Ko3qAWLF~lGf<0~L5;Yv)CO+QTA(xxjZg2134{L26`Xt%!S_lw``?S1vi!T}3 z5m>8-VANdljex}Qc^@Gg^{xgxfBfCR8W&q|5j%^=IevlD@F-{jQ6D>mcsr4)LCC< zjL^{m)*W<;6Td&nc4JQ;e9LIh57_fpP}XNU?+P?cr=SK&L(ooV{^GDv$s8J&JVg$nSdjideP`kqZH+FGh%#``GMGa zW})A}gZ{6IsmbcT?o8HT^yCE_LG{nbje}*P_J(edkJ&8*OhEyTvYb!MzQi2RP9 z3O7-$xO+6YlB)a$0jo1Wph=3_&#qZV&Y17N^LTP5se8h`v+bEf?EJDl!_M9TMIPSh zijwP~$WX{EXV?=!5=YR9A!|vGKK1qivj{l=?=Yo&2*||v*IO72T8hEudzZnpBsLDv zACWez{+f2SMmtpTV29wJXuSnJ&N;+Lq~45SQ>k3WKag4SU57QT~w@0>=z zQ_Afy3RX+BgAEh1z)}>z*8Zf?jw546GS>uy6qbmDpayd^d=Yq??Ji$fB>R&c9mLpJ zOCsTq2sja3aBMI~Nq+QBPS<<+(7VJ~b(9AZcndBQ7QFU^OAe!MYajqNZ1@AP)vSEQ z4BduTA`LtN@lR0vyf7XRpsqMsu|c%K27<%o0!Z?Q9Z%`E;*A3XaYK-(9gP048Cn6eNxn-$Md}dh+TA0iX-#(hX3Zxt^z;9 z>U@|!=p%l@NCCa;hMx`4r@>=sK$s6Gb?i}x^k;evUiaSF!@%aeLDxt82BKWW^P5#~ z%QK(LEYQM!;f5qLV8_c3hegKS44t;2{{14mHIn>uI2xkwf5DEA&k%x)wf!Fe#p_I4 zi4Q1$aDE2|>>$3};@p1i=+C{u0VDI{knf;(&~Bjg4FFu(ZL}W3PD5b@9xqQD0~FSh zRlX15TX_CRh`qwC%*46``n^rQa{w(*@zE0<>%g;*@ zKkx1Q!A+U&&!`LFDa&3)9YpapPAl(RB}N#sN;BKFa8Uwe8MM zgD!>6M-&Q=v$)Iy{2x_Gu%a<($IYgvbClO!es@jaf*DiL`ASFpFCVsQ0%dcXJMVTU`;G-K>S zW-m;p$u!YY5EWNxZ6V=^rwH2&Y}jncus3e?_-PXKK8NhlAE^pg?myp9vKnpdSzF)V z2lDVkw!L}!5K`GisNvO#(c{WENqL_``nF4}^I z(?795an+?+L@V&0--WfUxlS479%(#mSA9iDEn_8suwW)?uVlB z{mC=BU>)R6{#9=78p{z>%M{wMbPrqgL3_BhQmcn>)@kXP8vmNp_Wz6(sL?2%-VpTs z(}-30jhR_1^oS}1EaD?V!4b<)mPk6~OY;+2cb$_25!K(_1DA1+u6I z)h?AMCa$k)fnu<&=LrTRPk0mtAocwoJeI(q}6Zxjm&ZkP0(v=Mr>HCD|qjy&lb zr0M%0X#gHcJbf7sD2T5%#-Y)23I5V#0sV0{55a(KPAAT4w)}^f`?bEjOC3*+0n5=H z*&_W<5w1mpf)DNpSpg`%UAJxuY3tzX4q)kDEhlMYm(vAe_aJ(PaZNTo-7C3@`{bm^ znNKptA@`UYD$!Gfvs9KB>2x~Tc_OyzHjHs#kC4@0U7QXS{&bL++8Rjx%=1{rYEjp` z1MYoWglAP(+b{a*>7|>j7)QG1fBVqI+KV>#7U-t zR-4b8W3Y_(=&BuC4mBsc*Rc_!iaW!X~RsgrRHUt!bca0_h-q^Z{bez=_uPKF6HX2$Z`)2?Cw{9%cLfWC=?%OKy*$Cteg zU>?lkUIn7T8HLaTC{}Vff#M;M$`hTUD&&p-!GZ;>o%C_0Uc)$Sfa3b5pak@Ai5(p? z<<&-jPf_h%@7h9bH8rfPRRjn!a!scL`r6+TT$Z4LF)lA#<@~6T5gp!-wMYe)+W9+A zKB_KHPU>YC%`~!eEYGiz3RD9i&e= zimp>u$JKqLt&OYuNSm2d_K`NyE*!$El*%2$r{*o4!mpxG+XuzgxpWKzCoOdh4<<8y zOXaxE?+f35OB9u^;liY*uHgYz{8VLzE?m`F(YmtklC(go?uN8=n$n)Mb&BGNw6*1* zD{5OCl{;!n8^uTIToJW5>B6MtpMU|#SaiX8H3n(>Psy90^Tr20HIMd9}UxxlGAc{bnCazT8>Dr9Qu!sobOpbt1w6rAZi> z+GxlIKEK%-r1BHx7>^$lAZZKcYkRCjKeU`_-mz~W*s;)V5)J-QC{qDoDkvsoMM>#6v zosNj3>R!kO9Yw7&9)InsOLt$97$ib2a+H$C7!RuG{GdcBkzB)+8(JTnmZ=3CM%_%Au_US{o+LUsb<==& zs-@&mcgi>|kXoHG-eKM}aE4Ci*i4p*E|GC3mxR*g8@p?YINgg-)q&9xK_E~bi8K{k z9+*sdp#dVs9kB3Ms`55zd7sF(X!&nM=H5b5=$NDYp(W#3O!RYK!rWU5hK^lhOcbt( z5MrN*LAs2Ney}0a3HJhXSmgAoMp;q-Ggj!+PJAkrYKlf%V^l@f&%oVgYR8!7OxmBB zA9(nfSh$$QhoAjD>916?x3{yO__gB~xa}^w%j+I8E@qfrftRfd-9Zz!tpBm<81QXFLxFP;sSd%H?S=tS`xHG|KNQ6KXvILSjKipxv+hkl zZNmygxcg}p$4QFyeT_Fhlo3rW9T*PHb}Tuv85T#PK2*t>nlU{mwgC|bo~!(qR|Xl@ z$sur(eJoj_4t$pd!w~iOCleGm@`;S`5C%~GQ5r%$mQvQipI{WX@m3z1`=`u|Q8FNc`~CVC~#=E5@goM;VUs4wwz0E$M1Du?wT_TEwkJG<2TwDK!@W0Tk^G;I+iS?(TU}pUQ~U7z z2A=+SsPRh>D$1fHRp6<|x@#}D;nPFOiRr>J9#}K<3EP&_409|Vq_Gi-M#Idq!O@aeSwNH>AKTF=O!=@B# zh~^rett}z!F1x8HtSVN4Xg%>GhfaLWYQe6HvBT72tg7QOSv|}4!rqMMR6=d`Gx<{s z8wu8jyzI`XnQ(~C!5v4cFgl|5q+~A1Rp>S$n_J)(U#mr1f0XlW5rI84&B^-QwKBhYSqoRp z1_C{6)}-bxeD6CWY5Oyc>sa4830Z>hy|EON8M=&y>hK65YZ=xuuA%XVEU$UmkmwFk zMjcqw9=I<;7>}^t86$&(c2ut>KA5ULp>5-vcLBO0*mL61+WscR`#^k-=Je&= zj{Xc(_+5M%xNn5IKNr}WVQfx?wdzKb9}ERLN%Q^8m4Uqkk=DBJJ<0Jyg$BvFbN`!C zuO)sb{4j0x697X;ug*8kB%8>s!3A^ce%SSLBFxv<#K@N_^x}`Ci}PCNw{p`=R!?ot ze?ejBbJgPMFl`BK!IZwveI1L2haPL*&EkOeW&r$2;)9#OFm#w2m=e7m%ds63Wa7+smAbG9X{-g+?J0RN$~6Xq z`i7Z7(W6js@~2a#h;%r4#SNA zWV9$Vhf`)b5A;Y6Qa2baJP_%hCm7~S(1X71?*`end>M6S<$y(lh+qnc^es>`3%_`^ zKlBn3yAVdBhcl*`KX4IJ@&QXh29=IYYlq$r0VJYMh>6>6-{s~;w}d!T5B3fjMeFj zo|5Lo1S}vW7atU~!$bBbesV%Co5m5$J(6&ti#~dKf7S~5hD-*ZMVif&OJ01YMI}9O zM-4M~K=>Zp}DSmZbgj=l*mpsrrbCoVXnq}8fcQwA91Ant);g6w!D_x);n zQ!hlHAunNLw(Vs&GK*;CYqb)_@;BZ&ivu4WJxCMU%RG=UFI;{Fxzhg`S7TD3$*HfY zx5qvlKECV&ga{4L4M2c8tS6vIi;`XI3(s(Z@|LRj!EvN%&?zpUWd6oE9GiqlX*GPK zzOG04ux2yXLlw>k0=(FC`ty*>mWS;7ii z??~DWZg@0>%fZauVjNJG{c(tN+Ma!t3Dc>DL@kpF9sLa%!WNoGajnL^mFdP^w_B zcN1-^BemWx@RHBew3AvoHdQ53&e{q`>R}oNXvU|-A&hWQPYxjN{gj=gs;a0 zk*(!-eZ!U7qQDEcA75APMaZpi!YbbhPX2m1Lv9ZVNF%Nt!dh-kj~UrY&p!FRG5-SdUlrP%S9Tys8UN9#p^5UnQy9g-W#n?&g~{EA|nIcvn5EolsqkM^JD* z{ZdRwF=V|z5KN4*Hk&E5y^`EB1#_0o?h}S+fJ0+k(2wNv&glOZ#TVW8C*iT#l{sI# z=B(2pJuqk#@l&yK$+AWxyvMEPNJm8cu=Dmfq%}gjpUH9gRltmG2m+V;XdDNmQKgD~rt}tR zXC=h7dDHgBjuM2Cvr!1iq>p4ydF#c6iO5QZg;*>w^q;jlVIN2;2^7rzIx#_pX>WY_ z9ML~x6$Z>aB{C}T=|Rp$s=q<1+9nmYE%Rk9UYM>Dmx9v`lJ|cvdDe{|1R=<#d#(nh z(%Hqbu2f6RqM4+IB=e0N6gChW647Dtu$bzEh1k}yV2o!hV5{+=FEv%*sp*f8y?_l( zBD28hW!n}s;Yv5-hd=wfktj{E0Jt~67!4>dOl05U(Wv*udfWki+;LvQL!xftssFY6J)}8M*R_Sow{e#R<<(LYm%?N13o9C=93689CJ8z%^ zYpsk*kK1Sp7HqVqyqnRw!oKSMNM4jBKtgL66%P z59zOVNH+Qo&}ig+7AT! zM)IX(^-?rI=FALUS(q8kch&|&7$(E4;h1PODcbfC32H3MmK3d;gq1+qc8}PTa1sxv z5tlB4lZ{h7;g5x3CJL?wG#&0BNIxGB`X>thbQG-c=S0B2xkmXxtj_{m_FYcr^gj%%txDYEj*@aTGJYu z;GzumUL-6fh5$8J#7;l)t}dsS055M6tq?h11gvJvxj)Q`D^YLPJp4J!Do})dH;@5W znMOV$ygrD3b+gOg|Ex1DpA09q-f|G`)cwhHhd+qxJI{p^UI!T9EqWe9=`sT3GPO)k zcK&@TJ2LGxlj(&_pWSH`-+AzO#+$L80NGP4291@{t>!wL_&gBt!aB zmHi`)-MB2V*${>jz=y9LRxO04o8YPV%z0`);4%%FFvNS4Ry`2r=dUH zh@zfYU*?oM0Z6W)$7rNUD)K&!XF@mH;`tAzpN5!1c)V8!ASL#5B~(38zLv_7(}m9) zWyu7X*h0t`PMx;Zzp{xy-pKqK&bb8vl?Y8#lSVT>P!pLKglElF+3#}G#BUHN zQc2trli+_8W3WMkPAs0ZY2xz;7TmDTeT1MJ_p^2(a@Vn4^XyT`PqNvy;HE*R@q;7HTSslLR~I(`K!9Mc)s zQd=$RtSXvUT-K8T)XIiSZA}G{7C}?kS3;`PWQsG8IZ9wCcGO&jsA)6W$G0t1-LlM{ zuUal^4feKM7hJY0-nPf#hz@raU@p)L(pnfzVG4RGD8wZ-Mgy)}9$m?UO*?F(Q3Zqsgd#7bO`_1C5+bU8|z|6?YQ zVh6_f(aU(M^@(tt9T@WqPmB3_#!5KJ4vguYx6X9)9pNa`KgxIM*5BR0&P4(@{5frj zmWexWbQ)dsG{Qe|&A-igmZe#1S9OaV?)^lyYI!P5!4$A0-ZWTmeHdviRHkf!LbP}% zL|X|0B=aG+?IL~?+x&(Jl2Y5_D+}t$iRzi~C?YFZ9vG-rgP(_sBq@n{tt2d22r5Em zHy^K+hl7y686`<6-Z(%yk|u+rIn)&Mw^vuFBIg9pjjK6*Sn*PtJmmCby=#PPj~nC2 zEGW;CTP62Zxjh-;Q*njXj~Ay-c&ba4QjZ8Nds{o?WH}g^PS$KrCC0*{ZyH%*IfxwE zXzkEeZ?1!sZr;iYEikr^z+D75X{A@bAFrWO%e3`71Yq3!n6oS6>PzO=Yu@DgpC5IT>mNMOvf%GcqbL7iv9%%{4xtgu`7NCd&R6Iat{xnC<* zz?9HJ9ToXCi7Qf1w@@?fG_CWxr5K#blgWK})_1l&r8IzM-YAvX*Csckw2ddXz1V_R zkj^bOSAs6nn&)YV>5w&gd+4;CBmrtk1=_H#t%XGmG1yh7DIDMxzfhlK_tQqYg4^DT zih}P7Sr1$l>COwuj6lk4>9(!Rm}WKxIA54eITCN6I~Fw+>@S@?vk<+ zsqx(tvH*S&?%1vasHV8qYJw9766y4OvaPc9&&|+h9PDK7+XPYH^gy$w1(dG&8nXAe z5|nKOKj&RXQL_AK&d=Z?@?v@4$ESWuuLKZsF=D0QHDJSt_H%aGRE|WO@c?P9gnPh0yxxPkgklOQK zmfI)&v2q&2odVM?Q}DQoeoT@rD64tku-bK6Uo?pMGwH{~g(HeEbQ$Q6fCj)$a+g|!e!n^LW7w$^>?mags<>1C3>o1K zm<&{RYsY5odB};qjo>_7>&Euz5B6^KbKnI%D-28F|7i~BS$Of}llS|q1hnafU6d%BR%3(*YwN4914;wI0bUw7@um#ahaxzsilw?ad z|2**5vU2Wt#Tt0NDLzDo4;lzN@=oCv`u2WG)F$N)NJHKEJs#S2u@?q-`|t3rXEIkVnY=Ze*GCQe(8`H)Z^epl+4uoD}{rK+%1&2e~+xjDBR zLP}ySUkB{$(}64N)`}{6D5~h8-Hk-$@Ke&x($R^<;;Z($BJtvV%z}8?v+H~JzCSVz zpgZ?{@}KeO)|-p=WwyMUjE6Xo!dshhs06Gy4)Nk<;Y8erG>sdvKL-B9X^$s?Y9CKZ z2Nsm8U9L#^TM>82GISaNlz9DzIt@n$rQ_6P>Qoz<5zAGl8j{nrD$2iWf_%T|F}nIPqw6!ofcC~^CL4t ztSO*RLQgtUxf)PQgXppFx>Acz(2MFBuh5HpC9efl_&JxG6xn0p6TEnuUQ}MB_LA2o zRz#avF$c#Kqqv-phrZJpUtOst{pjf6V9VKYw9zytocY5vd#TUsw1|}$vN>(SEZSVo zrd4!Fo59)D2Tb$`bn&!qj&NQ@7ghWC{$t+)R2NkvkQM>CiY}^F|Jb((16FiVHHbgj z4ObUZLv|e=9$LioD!Pyw!XJw<-FPWq@gp_n*X5OX#0@f3qW`ei;27vOK6|L%w4!O0(PLb7+cQ4G`ZVY;tY zl}ql%FSxLwWjq5z5TrdylMQWph=z4fHovE(q6MCk0Z%rh=?NR+eAKHpLy(M76jb?8 zC}ajyQb5)9FCX3`Bi?QWbG09jA(cSeUXHcBoSmkZ>COP9I-?`a3h{{+F?)FSvEW9o zS>vzhC#G_mS|c4nt6ET2T(SYzS+a%QHFht^MfN7BT9XZ)-fqI9FV&^*ECPp^xd7i2 zxw&273ncz$e-g~VEN;WgD4|bZZKBM@w^F<-0^z`rVrr;eb>`lb_+@NAg^h{S?h*0W zyYwC=-gLCbUca39Vd(Hj+QZ!#QSa7&z_(Jag2k;fBs+dW3E8X>((cD2-ys8bq(@ia zRcX@cXxq6B?g{b2B ztv9~|@^_uuaIu%Yp;Li+F)bVt2a=XyDpUoDP%06NAY<=bxRb6E1ab|_BsX}qpo zHk32y%f{epJRJ@0DUpK_eIOxq>5{A`S0G-2HuV~tV&6C{%~!|*m8+Dp>uDbc{lWW8 z&=Uh_Hso79J5Fta058AXRq`TW-G9s3aKEH&I^a^h)T?a6BX~x6{ zfwfa8r()C03-Hd$-MH>VL38hw{U|R6V@(X$NMh{<#ZfjXnu|ncQX+-n1X+^;(>I@blWc-Y*#s3=o~UBZi8`<$X4Q!* z=9;L(Jf!5EsNxC}b(BgFud$@C#Ev(gZ^-}CG^|WAY|ufU54_hCTFSYO~?Mk`6U?MZadi0Y0rbd`9meG?6^=$%{wV7?=qFD zqS>d<1+$7?l%5nu73GZUTKxli>mvz@;0f=k2^3VOVHK#@9RwB&sTZV+1-VKgmmdQqCtuI~8Emdb@IbByE|>m}ev)AI z*(c^)RsPhkLtSom{ZOa(NnFV*I8?X!$K5W3ntB`d%Q!I8atmOCn@ zMhc6?gk)Jf`-y;(WZ42Gp7gSQkrKgO19;}+lL#$OtN6}JCTvMA6W^)FF9c=eIv3?U z?mzX*)lT0CZJ5zgF};h#u?m|HykVC)w#3m&rPHfqgB_}1G%lA3ca-Rk+QC+~0LV`d z!5;3i#>ZN(W`mz-z}xk<3-u(d5WLkP0O7L%maCBWt7T4lmIR$90Hyk@S>Tm~ITLVX z0Vj(r2*8GF7$zxBcR5Qs-@MXkb-PfKt!Go9p(v+bOABuFv}Jbvk)oV|&9@2J#U+Yz z?ll7>PGPTAX*Du|OB!IeS;e|lnZP=4u+f5g+gXr0XOQu|)57{`Sr~M_U^MDlRwa+( zfzAtzT0Cs{Y+_Wejq%W8Ur)gvDwkp54*>fdpB!#h4W;uPEfm%tXYXa;y z>mM?qYbI!7IWxyAKR$)8oKe1AxV>_<+$#@Gp}?;EhN#XR{sO zRXDQRm#BTOI0W(HKvBG_Q0^E<&?5v_g5Xo=KhUkz+qXxh)L`1NWV48a*#9u7UX;OY z&-C<;3mRDB=+;{OP9@v3K9;ksSf}V+@55@E313QqXV?5QaU7)JptUeFQ5+gk6s(4E zlmc%%jN=p>mcuwPq9|Go<82DO?J&wII4p-DSuaS3rO$WLn_pN|H4}yGwyr|! zd^JJBd8b7NBfKmFb+CzlZimk`;EP_G$%RdFaq7K3T%A1HFJuDBPslOA%{ww#Z$|>Jb(1BububfoAHD?ZH4_K;}K96D4uFkqG-sIuE6^iD0DTE1r*u0M2e@H2XjWfKbe!nD;$PBU=W z45gBR!)7d1qkyvd`!<7dvyj+~rj~`I_?YWJMe%-OQM{k{e*D>+E{xShK6=x?EvL?( zA=8{0w!*>N)G~TS`SKye8|le0-nFzXms6}1WQtcDxOfk$=&owa3&*8H$(Y~FmV=o; zzs|Je@@=TIc7M=zJ2mR&XG(>96AEG4PgST87;PUXp^7DYD51>3aJz`kQ(^`^~pI}qL z{G^PXL`lwc0>DgrrxvO!6%qwH; zrXjr-y#Vb;nb!SgJ?l8URMg8+As>=N@S7b6q#!YkS-9m(ep0)Ylf~FueZ2lnI>mCr zSPTj`aB}FOgq>%n*_0ABKiOG*J|u5tB$`lh zmMv0}$#f`a(takQ(`-aowj&dfoDkZ%fYSc3$xfb;8RW2373DBBS*#={i=9bj3{QLL z>99wlvgnpm#snt_-@BasyUXJJn7va9WZR;?CTri;o)6Wu?^|ysuZ?7U}z7zetWW;Cg>b^1rQl4+w4beGL20xn!x7|i*s-H*gFUSuISOtDPe3e=^tG;xRO z1NR9;nHVIS)a#Thzd+Slw*xhF6K~HLgy2wk&9QWtzSTKYPH2^PHV7{p^xmM2gL8Ny zcAUPj97?lBFL7H$gO23;X1(&sQDIO*h2F=P9 zWPwPwz1M1I0?7AlO zu0bR{>~K5-h3>`yg)CRIa{f!lx?2Ynxy zt(+555_3XImNl<|W+=ol@*#g_FL|m6u0tyCAp(Qy5gu(J9FB7<^ z0p^}fCU8ju%yW4%fe$pmap{v(&_i9nu;OMy9_b*p*Om=pcA6e)lv6jbVEKS-N*br& zoMuw@HVJ&uB?yRniK(NR2+Dc{+8X0Y=uBat-gPN8bq9XK5BkeuZ5uM%zEVtYl^^lw4i? zXoSBgll>&T_+^1aS4q0u?NtT`AF_)Jmt0*nX#_vC0@u}&X5iURyOmsBC20gdvI5st zlV;#Y7T~&y(g=QH)w`~$Gy}KkT~{NCfoE5xF1fm@MhITmx1wHGh={}6kgqE$&Eak6 z*AGvW^Rb^0Hv9#mAfz@f7Nk^WiYFq!~RLQ5P* zJ8)!=!)OdZ9W8MfEy9UC9Y(WoVoyhkZ8)*TVKfk^qLxUEW&-up5=ok|upUmD#UKi2 ziN$O;iaD^Dtq1C^B^I3t>9sA4$HBhNc;EvL*-sf96m*6Ifh}G$I4I~$20kzPKKliR zgM#tpiGz*>w9ZU$Xq%DiOazCw8obUvwD)bMte`Us2x>#P&K@AJ71cURfP!Yzwax+{ zu%>Ioknph&AT=j)jti-eHXg`N4CA3qy)x*vesP-?+1Us3L&Gw-#iA;|H7tXdY=>Jc z$$Np-wiE~A`>_WlDe^-fm17iRKW4y$EZIWN6p+5{+ z58FG%Eh#8kzi_*~@FE{E|4<{Zd^^-V`t{t%Q(%412)rHg4 zs#~sN1h_k>!pqDi3;UuT=b!ok0NvB zhf*3v26a9-mGy8%brxUGJve;KQP)vn|0p~5o9J*wy2T1K2V(V`N{g7t_r~`uSc&Fy ze?A{aclM47N5xbNbfnzbqp4_ooxwaydiawi1adY=bb-I))-37{9vo>sAJRRsyltp# zN}LXsc94lm>;J%chwJ7=CGYUWH?39Bq&OTM%JaCQIEa;m4vqzJ!1te0MWMS0roOY; z?l=Xrx6b>B$63F?q-Q)pv&r*4I4)?wqgoD#Clf_cgTnn_KoRlKj)f;83r}n`yTXT`u+D>pkx*$_ZA41ZB|M~M*~r51BXGAxp;+U0Jyciw6B&l0YLflF@-^7vNcl9=ppV4dvn3vu5%%P{mO zlX#{-k3EO*=5~4I%$DSfx&Qa2A1*8=TqR;fGa%+69Guz}6#Jt$-8uK>-ed&thmYpt zPw1Ljj72jJm4L)J@QJnd#P>pqOYO?%jN%AFqOJIF-Nc*0 z2q><*@pQC{FJjOMpaS5*v%(+(*Yv$MGMT$hFK}-BDVYR9w-sUYRb|4;^*VG5F43ZrWD_a3o z{3|KoNljRX$seW{#%l7zV%v`RIL5*L;o&ZLn2}JNHFnMRmG&vRGH~yO#Y{=B=Jk|7 z7Lt%~jF1IZ)Pj{W;YvxkvQ2+-RZ*HB+pd4OD7T~f7cmOWla5_@!=csu5!DZ70kmo4 zUyr9_v@n7ovTqZUERe^VC?ZAd!6Nwir`h6R=Ko9}PJ@70x#>@P^b_YNK=`wNGdU%? zNObzAJ^X-tkAMm?Vvp|xWH2HHlQ$d3D6n}<;a-Bobr9Y2ztkZ^Bp2mX`mCaYaWs1{ z5wFG4rR5V8EFu3i2PR^K6fE?0vS;LFsyp~mZm}t&jI1CztMfJ~6cJx9z44+e4dgpuY1+uHCtgmK>5cDHv$!B11eXjZAAm0Y#6O=reDwqv^Fnc&f zk0Si8{iG(@dw=%Z<1csKtVcdm8SBk&{3qaRg}yni;VY&tM_)fXXB8($^iJ~P(F_nn z0E3}Ax+nJLR-Y!^2PDm0G*8ve1vY__xZjzJyE;>}wlHEz&7Tha6)fy}#84YrO-T;TONM={nDMEb=W(Sh$!kha!KeA*92C@yM)BpSGvt^2A$Q z2lKn{TRA>M_6ZN?qk0ThR)eqL-L5i6U+2K!+nJ_s&04WJx*ShO!TqmDkM40mzbZYF z=IB?VM=MX$v+L1<%*9>GR-QJ1{SPEvRsC={AI}!S{Oj!Qsk5b_M8XP5VuMlC;Hz=% zIeGM*2og0zUz0K4o*|^1Y0rbvau{tlzgqNcEt?{fG=%v1_aM&NqiSW!zS=RtquHAt z>9mU}eG5}$oS(0gZL&UQehn*?A@W}>ZRGnF(F~Rz9enRh6_)hetVOhHMoq+<^<>jz ztiIp(7EyJL>1Z@@NU|gO4irY4`Ba6_SEUw>-MmgGZtBe z?8jQwK7Hrw&-oKfr2AzS%oo**@@c>J{Q0qKSV`$;aFLh!q}QX>Z+N|l7NYUUe>9rl z`a1L%Ml{bcn}1sR^M~($N`vKmXmfuTuj1Qm!75HOd>!a>PcCpg+iuAz6{m%$?u2}@`?rl*|xHrWgP>Si7WrI~ox z_pM{zABRWWN~a-un$?#l&91TvYmWS94Bcv3t|OI*Z~5dj2Uqjz(>KJ}XFaeV3<&!! zmf@n}g^OO$o(D63URzFwa9zC(g#`V%#@k~&p9jM-@n8MWW`CA+j!4^KTBu}oNwG^Z zrlVl#(ceI7^fz;W2wZuX4!>F`8!H^bJnsCEuC5Fr2Va#)4n7xfLk&Xrh03{D8aeG3lt z*KytADAm`WAvIPOhsyr%&QCiC7v1F*4by*}3wlpj2t8jVvn2fL-W`R%ACDHdkHm57 zk8f_DbCqk){gAA1_)-bD=U*|yac!a@KANhaTa2F~FqWQbaW@OX@gkTL_Kv4F-Q^67 zkU#o0a5^c#PZN2}!{A>jUzfOh4j10t%;t>7vGA+mKl#pkw3;P`HlF#cuSuNJ8tKQc z|0>kUm`{35$N0R=MaD)#_bazs3qH=T$>Z?Xz}ut{TgkBHJUxXST;xIcb&a7W6h;II z6ZcczWO@SG#V)_2OSP8b^{of`)8_*(gy#(d{<@Cz7#}{5hyIE&2!u^W4o3L-w-7sc z3eIYCECpV67x3KjH^s|m-f0NT$emZFPq(66$hYPsIr>Iq=~}Y4GEhAkZ|TDDH6w># zQDG5}azi)C7I-Z5Y47T4`}`)RB2C}dH4D$e(617BeGzTe=bd-DysicFJMVe(gR@qH zYf92%4fvYSz&enD>S<@_g5RM1R_2IuV+|9Xc{f;#YxS$fT6F3ZaHsFf!kWJs1AM8+i-wS6GHWcOXz>&>jg-i?uxxiDjJ{ z@fs}5=+}`aICOtecaA9H#y6wy%db^0|680MW3cR*oI>gfeiLEKuj0CWUnP<)Ir*Kt zZIlOJ%}RW86Jvax<7-Y2s|`_Zo;BS6!?Q$Pp06|?9iMz2`aS>4*Lp7DDW~Dti*a#w z-pZE_pFBaaVE zyfGtu0`Vf_t9oD`I{wW#nEskpRc3eR{_xRN#ioN)EKf5pFA*voqHp2?YBW2)mhjZ~ z9a^G0*smC1>W*)w-sHP`NyS1*?}BG75Fa!0t5i(-1{m;S^sOwzvs-)|eT5jB{^zxQUe z5ZKpv2I^aR%U0mR<#=&RL%LsS9e(;kn%`+iS2suBXTbgc!W_{9k!I_c?{I!nnYz5| zSkBF3r};T1sQ5BN-`)(>A^U@P0^daH*f>GVlk=eG&+o=lZ}G%g7f(QWe>(XtD{(&w zE5hhB;w!mQE5CH|EpI0;z43xR+V_lUG|=f;CJfhoN^D8cO^0V z4g@}pgVz(!{3h?3ADZCI`>xM{pN)k^8LULbaJxQ#MYuuCl${;6@QR`e5p(}~FdjX6 zr?R{o5Q8=!KWn`y9hb^Hj1AaM2Go3(?< zc$oXO)u(3c45@lXrhv{|dJhwVg1LE@#+`_E43BXRE-bq^A(9i7)HS(6DvOtST5Lkv*;=ViMoe zhx#l-A;PVGE&}dn;=BF|1fYk{FG*sEU8 zR`W-P;CIUHx`PLiaERrM{Od71=}gSxuCueluv`;gVmbF6Z#Kr_(yv*aX#ziZ?{>Nb z*`P4kCm%2GuKfAC*M;5v*8)-SDmKwQ#X|raBOod_+UMm#xBh2WEK{v(d0MUx%9V=S z?OL^LG+Gs_hP_^=e%kL@^<%$&@)q1zxD1WqR zdQt9G&TSet%f0%0chIhXxG1-C09@9a)z+mAK)c*2U${NDW7F7{8;!neCqDgJ&Fxq< zrL#P!l;6AM%o${j>(%Ur9hdXWZJyPenRHa$cJJJZ;wq6gLT@L#VW*rqRYYvvCTMr2 zqSeZcZf2Kf9oKDIjpE#`pPgqiyix9)x!vBNO=mQ-x%=L2WHq`df8h8msSqKE%D9b8 zW{D7&^=dCuDVeMg+fa2o<g5Izlx)gdwg#58v^18arKQc`vv^w1?4DXvo0c87oH^`{TeVW8Zuz3! za5E*6He;4CyLF>=X*ru+Vn8bOh8?Yat2LcXjg^%4I^|}UEH<#H;`;EuO#Yh(irTE) zU@$-VfzD3zG`C|wHVN;ES!rcjPv+-kHM6p3)KSpt0`phnhfFHBK@dWy=6ZZ%(K?tt zuq;fA#uy`G$J4gW+cf0Ha>1;juuy(ZAbOT%4OI2XJ|%dt9uI48jUS%^hH0R?`Wyfj za{+Ca^SM7=u)SO!kUT@kgWjFbgSl19_BMObHB7Ww%zU^;zh? zco)&mRPe0)wJU2;RMz5(At}4b72Cqjr=2Zl5{PyCi*omuJaG=2nvi4#)t^$Y3qZ{ zBxyib>5UP*BB7RQWV!I+t}l}mX+?-#Lc0)L9B9$R7S zV2JC$fL+>fSU=%wJ6m{B{v+Xor74Vn>dpK>Y>wKUR;^Ct0KE5{pfFir?|StbS%6{T z9S{$_+;h)tPBN0vnXY21>&7%RDTbUh;}ItxvxR&nz%SZ_*3){U-uw6nJXQC!pXq-q zFvRR%mOIsmhV0Web-r4s{cg`PLm^r-OBt3KKhkOgbS-PcU>;m8!^IS$kxxhZYGRsi zSm=5UAkBUnLQ;x^$!r@Y158#cW{sZo@*aGm^K!GAJMqw^p}0;D#aC z4byFEd{4s`3Np*=4~eWA)14B(X>8?KK*MCHFqdlF_=|xznG9z0U^wvTA&hTrDbT7JEWhmQKrbbV9%UI{wt`_GT+`_C)SAx_xf$_uk*4I`oUfq$~Z=wqjb zmBaENh%Ft7*10^u*~D912lG4au(c=g48Z!Fp9K^dSk9aa8Q7TRk!zY+L7@p*gQD&2 zj`KI-&^vQ~;(hT$$D2AnefoDIw>@I zf_d!V3%$kA!VwsKnP_k|39bf1c!ue<6E0`)1h+q;0S0Ac+6a_>%%<0!5@Mj4&Ao_P z%Mq|0+Z3}DxF157+SP}oDmJw^?6j-j1*>2flPx&ei}%PB)6mglk%_*-I`DC$n}y1t z#x!F@JWqH^0l1!%kC<<@@aDt?UE93ZAiJKQabi}0i5+Vh33$)zm1CV@f9}}{qHzqo zryK>NUkHkVxxqtnLRi(|MQsmf@Fm29L8H>BHq_42C@x6g+eOS8V37!$d==A zqQQ*5X}?C#NxbaQrr4iqD0~ngno-DKmeTK9E?Jqy>+u(#j0&#&tZn~T|G+MRJ_Yfe z3$j(5O~%79J$}b3ckAD2DF@Yc;waTC^&Z|l?=;Iu$l3=s!!YgOZaG_}tY+j{ zAat8u0txC1_ntOG_TI<)VEOku64T&YLZ;HuvpGbb&c6c&xRy09=cE z9+tz!3NpDMGFt0hi7Jkjrg^LaNiV6c0vYB6qKAe?O4l{(qxs$XmKw66v01?^N!y7e zU>9UpPt1L-`jiq5{rYo^!mZmyHP7+7h!1A|^!|Ena|KrJvc{*Y{-k`zO~Q+5c?#3= z)K)@DDH~iZiu=X=!DKx3z4dL<?mXD3Lo|g9W^JP)vey4Fs9uy|&u+cDp+Lm*tF`)_O2v9*X5A3Z zHQjxKzi0tSb68gdZr9-eY!%qTsuULK?35@ip9E}HOrk&>oPwa<0WHZE<#n%!)G(cacTVZZO288Dy^!=rN`)T=@FJ^e{j*N)@yaQ zGibCPi{ZS!qUC%n4TVS3P<%8E#YfXndNd6sD;f;D+&k}dd0h+Ucitjz7!5%$xNbe2 zHp_Rfp2TCc;kn{svT_DbF>E}u`aJgU*}>x4#*DQJ-q+pB0sg1b>g(6QM~@&_E@sQc zBjess4nM}*LX-{2j4;Wm4xmkXwm(y7^?U6;q>_DfwOhP&uy(6sZOb8Ywn}dTT%PWv z@_hC-;>=$Ne783c5gf4J9!%(Km0;icz;gx+GbS(r=&fu zc6k6Qt!C4$^jc37u3=nxn$v@Jz3pOBt^WKV4HC@46P-HD#$Wu&;2Pb9!Sy8Y))(ub zn@r3aZSop?_UFqQSeiQXC=88O`DxZi`NHE`Z_p@LBki*l%r_)g#35ajd{Qpb!?3l9 zBC#_)s0)^IShYUd@9FD(>nHxQ{E^tpC$a;a`Y(mT`YHcVu#U4xFx?8)@qgL@57&1J z6cb<39{*&*JmrAr!NMPsA=2EU>G}+Pe7O`;aGq!f64TF;Prozz>1$-be0;iaKq#jE z#yYP;ORpnRNAgL=!qR`c7ww)~+&?)Q-1!S{_< z+$-}E(NAM1?!6gH5)KFVY?Mn=tm$W)n`=}6}>GUs72}FxUg{_lw0k+#eWruY+farU5`OlELSXHiBh1B^R zaCG;7+HU6oV!7@)ASmDAxBNiw`>XWyafPl3khK*Q3H-PMg#>7YzREdS7>nS+pqTGy zQ3frp3JU+XqWCxJ7xkWfv_S>OL)=wWD95RE0yf9_bOJW-Np-BhD}tS@23F8^_b9u6 zUT61H=q01|#lYxA6ZVQAIy-o%mn-@rMqQGij~+Li#L)oYI`5c3^o^BbHw;$`0?CFs z+m~|zl=VG4ii`D&v*^OkY(5U=xcD^$3g&h~yLNU;&tv3egY-;BUf^1zeAdkiq+c`0HWqyRB9Cdxd>!@ODh#H<0ILK) z)rEf|>29Z^x08<^%gDc4-mFYHehc-8-7mC=i8JzNP|bci9Q&ce3nVWBhb(Qd%%QRn zIjE4n<+j6MKKF-#W`JiaE@|-<^yP6ZEN4R%k3q+0QMVEMcWXFlOLvlkJl_MRD7+vf2 zB8I9HM>Zc68B+tFqqcVPtDut?#P=h+U4btJ3GMEj!X9EP=VF#&sf|qe~OWdb#qxZ#Jktzi&nGMB}&|;d4kQ}(ZT900poiS zgp0WJd+2zRFrWqC{gHF^5N$8uU=J$tIY5%$(CW6UhF6aAZF!>?dGbKf!n!MLj&rD5 z^zM#*H`4Pv)?m^rJWnBRmitee-Q+V|_^=fO83>-|Sq+kf5T1nb+9!P0$Tg{e{}YWM z@et4&=@+U2+RvTzbkI|ey7YvjF0C?Z_7Krik6V7E=m`d|dCq7BZfKr0dJLBMd84(^ z@+d6vb4P2TMT_Op60=_MkY2ma5${Rg!KA&Sl-D0H0j-CT>#atoyh8PVk)LZ$0?3rl zN0U7XhM(4+vxZR%9vjFJW4r7?V&@8gjTYH{tpL|*_lW2<_|Z%b$P28_&)_U!SZi<` z509YcLAkOjY;LIX;Dr3F+9!jy=1n`Y0wB9?!#co^6G`88!lE3OU}WONESa28SUVvf zOsi8rt6Og&qmBqGh;md1W|t}x{0y&oJOaUKqwhWjo*rgC0zsTSoexQ)bqR$L+?q8+ zzunG)NXDRuL1Y|$le$-*6L?ZLify@FUHoeS0y>wT-#GAiL%+y zqAHU9XkY*Ln*l%)BnKq;p|pKx-MwZ_05bz%FqjztGgq_bBCkZ0e|%3Kls8B9)V$PX=}#`TmAEKPs_t%kR<;FZ_&dME2mKerk$$uI zczN8P496da`c2s|uwXM~8jJ>@%f2spS)AWfjSkXF7$v}kgHQT;5$s>NH+TNSPR`>5 zKL0+4?L0cfH(RVYqopoZ!HO7Po}#WRlCoZo2ijM;E=K3amq(bg1T&nMMp>$r=oSj1 zYHdI+_a`ORd!j85PlllZsx%**UQDzP{}L_F2A{<+^*{B8C#6J*4CnmhxDhy*NR5w$ z{zzgV#Bxl@TjWi&N;0w4R$4ZdYsIT8MF!o7n{4F}No|7dn0X?04S&Y-&4g4fne*>1 z2XZ;HB-4P|RhQYyVoU@Y>d)DX)`Bsz9Lx07Fq-1wDE`uwBQC8-{G8s6n`Y$x(H|~B z4~^8)T!tNwBX<6V*Aqt$`N~4ay$SdZFn)s zxeZP#ij0~MwqwD*G2hs0-`!M_0v|SRF_@-?d1)`*?GLSxk{^a2&Zj4XvyT&PFT7~U zQnsI+f3DIjT80(cMk;I-;fg8Jw>Wh$X^UWvIb|IT!REMg)5qsSx|7+dfC}10_f5VL z)8yH=x$Edc5WU5tT`?98iYS%$IDV-6-+|{F8_O~8@pk>nV9&jvc(t%uzD2DG`4@!= z+s5eKUZE2k-#j;aG4RvJ=|z8pFXNS0(`wUxXWDg>8fOMR$D>h;-qXRjk1@X+9%c;8 zElvx>NiZVPk+4!rP=%^A69_S?%YLs4fC^1O9u1ExQHz}%AN9{r_p93c7vSisjgKxz z!q$!=--P1mdHS00k{7Vm4 z+P+v(3{lt_7XoOZV;cgXQ)G0c_vaIV7iS4)ONzn1McGr`X!|N}}x< z6g1lLv-96`U1h_Lgtw!f@Z)AcwN27%T-ht8y|)eG!&f;=FeID4)c$2UJi`opgX3xA zq<@<3FK%u8+W4jR{^bOZJ_e(~IpArU?70g|2zMpVyjeTVQGIj{Lya0N-^<4Y>AB5# zL`7ElZnrRxVIs}J`26G()sr%(OTNu({VKyC!?((>w46&xybN+HXZd|QZ^j^LBv07Mbu;W+ms^QevJTmRz?CAU$ zk0kW^oo+<4hh9tZSb7^HhIwKblQqj;B2i596t7Gl^enClHmFcAgX4_B7W$lGuJT&) z96=Qt@({Jn57yfCeB5F{D*Mxo8>|-gs##<7MP1ofSX~%J^#aH|9EtXJCNvl^R#c=S>X9Abjse^3(K9}ST`do*?nPuze#(5tgSYj0xE!)DK>!ubl1{`o;*sS)5!`o~I@EU(!J zlmKl5KZ!89fT^nq@*RksAB5Ww{JVk;p_EFW+{gxKx|gFs+$ds%lxrz&6De9Yw5sr? z%XHP``>u<@8MAcx&Dm0O%`caT_S@|;9#UU#+?5+NF45V29(0tj`=FOw6zVJ@Ci2kp)c}H#VR*mi$jgW3W zMq3=48@Itn;E7ia!!xit`zOQy(LY28wH$qx7cDR+^ZC*FNx4=a*my$OQmsJf_e&y* zM0TlGk-*lPetD+iyQAZ9VhwlYJQ#MI?L~24-mtxx8=jP(fDe@2(-60M?)+<$OF2dQ z7MzfRziyHGS{Hj8c^V!+ z?bO|89jd%;A^UVVbsDj3qIKvmQ2{c=Tfd=%dOOXjmP((c7*J~ZV-`A_%KfyR_ zU&{6=9XmnWD9MCQC@~~uG=*d$fHLFG@&M>H2xA|VTYMidRRg{D=w?#*1KlPJhc;P> zPzl=RKoluKw?!gQ!ZrjCdh@6Jg4wp@58Ww5j1Rd!U_%&s?F$x(*3AzI4D=RG=O?4# z_KxN!V4$Cg`}^WPOvB_SldUp*+wMWrH&sO!!eEun_W>_e0<*4N&-NS4HU>NOX@B&E zZP6Huo^9Lw3a94jJ5(EtV5h=VJkz0e4t5FQ@#q|5*nb|3Cqj2B599ZP0S!Y(+D5}V zw)`W0MfSw@eC^8w6aX$$1K;whf#J8is*}On*P`M*fS6eK98xtZ#`fQ>DlPBK^@Z!$ z^M|HW$0K8lwLd+Y^j{tBP4}C3cSn%X4o11R>G-0 zJX}uNH$0~wvmP2xDLXu6TYs!XY7U5K2mhQ5kA?z3xwfDn=eMmrfHPPyQ{Tmh91Y&1 z{s3CjBuEoEfPxz`6Auk)h zmlj666wB}PiB4C;M(j4DW}EQp{#?y++Nged3jd7?H?`&qc5T< z=g@4+y30$XJq{O`NOs5GD%QLMrf|w6#vg<0uzNMCB_6c&{K9CKJVEL0Cf=EBzyGF{4wxEsX${1EaPUBp&*t# zFkW2?l0JCvTG-+W%&g7ByViGG`yt)G4EjL#xYNG&%{iX;HH@PRzr(`={CDr=i>~}H z{`TOH@Amdz9vpPLFM9_szU#sthrRENZo_ntzrgd&jbVIu?OE>ES3PCDqVoUH-^jY# z*uFJbfpTpb?yA-L-ss=D_QKd$3!CLWcU*VvwM4}u7bN0;{bO+U>8H`)csSz8wed*g zpGL3S4Xcd;$1?uc*fsum@@E8j?ktR3YlEz9Ep}%QyBK61&zuc!ZOy$1nYF;Q?EbZ7 zc=qZFV63gpu4rh?-StBf0c+gBC{pkr?LGLvZm+#wERAd50d5|V;a__{bvk!Hz9awly~W>vm*Zz&udDi~n`JdYNH;xjayS8xFypFf{+F7`O4|dS~4r~3Jy*2|Q zq4`%c|Kl$q|Bud(F|`Ui4ZA#=v~L#8;3$y)-GlvZO8y_bd~xt7|G&kr52E+R#gY|DG>-p4q-=k3Uw2Qyep-3f?r>qPe0ynIo1cvS+MHioyS;W7 zl?R!VTHnKm2^TAd33?sIL@}-dkSV)hP%kjYh=HdZn12}7?$Ta?>c`e7>W7g@B!e|R zz%sW0V_dvr*uG(}=1#B>;V@(SFSwamn;m26m{%Tvv6l>bTmcAM2Q$Z>?=TS%LowE- ze;sP1G9{=(LC@zV3ca@IBD-I?HdhFe$J#ZX0?BxZ5(na`5qJQUNmu|m3v3XP0h311 zJU0Cdv_%ny%jx8#MV;S4DU7Nxj*=Vr>;@+Qkg6uA^V+Re+|}@!r^r#oW3hY2KbQf^ z2Z~##d+ZAvGGVWB;QX`gSZMd+f5(J(0C0y!(Dv{B);|e4zR&ihM)*i=E2OmG1t72 zPJ{j9;<`M;oNyBI+SG_7avu?6aaeOK67b1+8Gku_KkSE>L)Q3MVRH)_SeBNTSz0#s zEt=OL$6OS)zLiIpz|_gXn!j>b;&_6vS4}J*2D|`2k4Z;R@VXHSICzqV;pxXr>@rH0 zb}JA9usbRoBrzDP5A&lvtGXNP!P2N(k{@juHvJk_HUF z7HsCo&n|$(ZId8{5ccIr$)ZtGL_}1iNU9?LWr_$iB?hTSkwk0)bw{bEr5ljc)D^=C z1oY5kqWMvD5F)sEqCRzK1S&dGVcjS?sz}_JjuZkF9f~b+uZpmZkt;VsoTnw;QC1=D zRS~W-7sSxjVoco3^5&*Oe)MV!)f5gTEqqh*u0gY-)PY`YnJOdD5$x2`>{Zd76@r~! znZFFDb9NhDDL;EvvOuA@VVtYGx0L_idMbsZsUY}y1Sk|I$w)^(5MA}k^kBlA#1 z)m4xs@FWTB@dv+v6`H7Vv`mViDmwun=@cT%5xgk%WKkz}cJ+R>-^BKik5xIL$qrJ) z)pcqlF{n8!n~REaQPx$y2{c8_o(WArkX6@vtw5z*1UG@>+qbrLH@#!8QYf0L>JCsIUJ6V6$pVl27D z5bsMVs;a5!S*i$hx#WqOxgL@iRfIG}Tn1S#X&Yd)YQG0 zk;YL>jAHTDb!8MT-9#nz(tTN1ENd^_f(`0rU7=jKK}j#*6q>`!x(cxbzF3~}$`l!m ziBpb3eXJhk{-UU;=cJH;D-T$r%yty|dX9-0Hi=ysK#8KHiZo>iI=QHH2~!K#msNyo zE@gQ#7CFle{B__f*H2w_R{~$Ogclo1uD-0R=El&K>YPLGWzHOxM7popp~(6d;I2vQwX zylP0YtB)$g;icFB_9<=obv1W#>UcE5pm`KJUAa?hvmp=PU)O%DLZecWwyRd1fZNWjW8&#Z|f$suSETO~K*Nl~Z7r#!ohf?#41=7m+>%I{X$5#)nA z8kJhpi`*{K%^s4lDw~4Bb1$JPbp|`VaTo0UGEHIKL&bcQYIzJQy(40F0YA7Vy%y@~ z?O8NRiV~ji>CDGB;#2iAly2S4A_rftnNPM`|4pM$-h6X4%j;5I8$gXtef}+OZI4ec zCWBu0)nT6Ix7V#}8mQ3~=z&otO!BQ^y<6dF6gs3x87fJNYTgZ}F%=NTl8uqnRIwN` z)G+WxV?gn zoH|`o_XPGTJs3@n7lBEWG`0tlRMp+d2s{O9AsJMje6U{k%pwP$`u?IEx1FS_?tw%C zL#{oEcXzmqM@!lB&6_p3n^4bCE18lM!a@o)t@FJ~&s0@ZYTM@QwOcZv18i2Nqi}TW zReH!OzpPW>Dp(Xn7i{(_J#Wn}hT?lR%rh$F%f^CX#S6$JMRh%A87xIg6`JcLCH3CC z%HY*o5eg3sB}0!zUEv;sFS`Dzaz0dgyh_~s%)JS#+VDV|@;o2Z^Wch{p-S&n)!Ph> zSnVg_&xaQq5E-Oss`P|4=^v3%q@;vL`BEoGT333fDm~LF zkjaTeBP9Mt%n}a2fQZEnsZUmVWtyP?fRpGI$tp-fstDT8C!HKLDw2u<{FCHWk-jl` zMJRLpvT?Y5lCDZ;771@`j6nBz&bo5c164C>ivm>>q*Z!asygdtf&Mj~4a`Z4q|$Rz z`rW`hG)2BQ&d_DH+*ooXN{;p?K-v2?A!w1pF#ap%cRd|67WO&%<;Q>B>%Q23nU4Q| z0RKG3fBgo(?{N|AjznzHR%^n;u0~&sjU_xH7<6>yoA%0!Vzc%yhAd#}AmZP!*-ncG zQN^df$!MfIMy$cFVd;AeLi`P%gn2v;D+WdtmuA53jf1}qeHKdA@m!3kx-d+P)H*Zg zUl}$a)iPkQF)Ht@nSoEH4R_hXze6Af$mhbS`5>J-#v2Uk%6z6g3~cMh^f0Q-42YUv z^BAqRClAw|?Ks8^aV;H@(g1^Ew_B~@ibrV$2s_arMkL*t7--vOV{Bm*T8ux-#EdZx zW+DkfD@N32A!h+O!)mX10B?zqYXG=%eW1l!8T|N(=U2G0{p(=1)0zc7i<;}fs3Fnj zt!X>#!KUfA!r__UdWqrjZdV9IQp&RzL@wc!5X ziQvM}p;oxTz&FpcuU33s26DXPOcdWA40CPc@Uj#PGV*wlAO>8O5*-*&*-EV0ooJMB zZsILVL_hZUszN@oR(N3zn9MR4sI;eg1@Hirv_x4hiA0wc_L8k^9N5(YjHR$2(6+{QF83tu%bhEJ0H{m^vc8htAaWwE$=$eZm{sI4jnGT6x7^t`zV%+$m zadAb4y#i2JAH#HDr1=GdW!>Yo5YIRtvp+>t2kvq=o`w7Qb7Ntx@siDI4m^lmlN12x zP~L>@7*`lso2_&hC_X&9VI3gK3uZhi3RnyrIA+xlP&I@o>nd~%JLg$zlg*@O`PkuQ z7(W}CMTi!Lfh7qMI1=<4i*V{;ZGVsXK`=HfG7wfBL_9EJJkV;fu7_VeMnnif2seqD z+n@~Hh3qqj83*xcIxP5&wP*notT!&o48Vpm5kwVBpY8d`VnQIZgY^dsx<9?;3+YeqI-y>U z5ZcC<%(&iw>c_MQw);ekK!lD*c1=kBXHsU1kf~r*{pBxziPMg~gTEq1+c9KTQEUEB zxD19F1aK1n#XOg8@f?@!Q0#e(87S(L@$Tt~LqL4DVAWYXJ3M^Cjoas8sYC`4;K&$Q zaw+dH;UX=%Xb4bMk``dZcE)hYIQQDTwb*uH&jZKD!_|cwuI3$snT!r!laa>c&9AK+ zGwJ~Ej_9qVS)qXU)7yTmy@0;*hEZ^1K5(s)+yBN8)1U~o#o`+dsTTtqhdgy{Icprp zPu2#6QJ7MN@4RBWL9)Vq6<3C}K`F3A5oWH}4wGPP#r}GVyK9)B$q>pTjGFgH@jT$$1vkKBT6VA5`>6%Y$&A??J*kK2jQA(y*i_GfSVY;7>DQoUPo~*gjm#`h4F^-?Jlr~Vd z-hlFCV<_O6tYzRe#~Wb5ITP#H0RWYd8l6xH*)f>r?X8~r$eH2-Dy-zz*uAmXIhRBy zyA8>8B4L366~HRMI~yA(?sD>B_Z3bZCgEXf8f^RzEs=zWSt3A#EoWEjkB^2!)G+W` z3JfR*5!1p!2ua?;n-my)J|0gSBQF^z_A0nHM4rrDhe;0Cbzsy?oX@8aXRg;L@P2OStW9*j5NJ;nC-o^kX9df}llY{zUT?1M0*zQITijU;YS zwV|*h@}{t~DIHm4bXae^&g&j_Vd6$>i;XC*K3qtAN#U1pk>LtwHs#7fHuDY4WaJwk z&o0?o+G#!gD@@DN$Ub_SSh!C?rLop=o+aEt?^jn$1pamIn47O1Y~IGyY|nRENW#Sf zNQ;%R-+mFA;f8~%v*qKKB}yF|EMSnW5#aU1+GbFJG4Uo%2itU>C&}W}a3%a{ED8AM zmPnE58FwHzEj$>(hDVHwqaS}{V-IYzVGChtUj-Nf8R3UxfnsQ<6?g$#gIDu8YkKwL zk1*h{!HbM49#7QbuB=cqjkj0e|Ft>)%CcQCjx+xX|H~M?wZLh#Vd*k)gpk9&eP-r? zXY6%h2c6l#iJ!+O$Wi}l_fFwY_WwmvwP76j{=b74-NQ`&w{G{*|MyLPZ+{2#-TIA( z%F}%6+diy;pN*$s@+Z_=LizRdO-p$7tf>oD=Cdd5b_f0+EDJCyI|2kVM?0WsvHzO- z?ixAj$@4d@-@R+K-mc#nleFV2@+&3{hi%saxTYOnnw!Vx&{T5lcuo$=N* zt~b{5=O^h5pf51%=aY9~%M=lBJLWs%8GnttV7)W?!4+zny_Y*i4;JZjMraQBRD#hk zv2qd*R*LMo8`zIv)DSmz1jtYIA#@!vKM&CUTgQJB{r%f3{|%=b1+z3Dxp0^RDG7>w zss=4|4W&WG-!pLesN@1i2N|rX4DihN2sj7&Ol+x9>c)_Nusb+={6&f}7>DY{^N5HJ zauDa$QRG>=z~#MTOsx6!3RPX>%=Lx)>GQSiV}-TMwgeYSsbM=3_oY2rtP>OiLjy#Q zh;b)jn#+@S2qsCD@%^4~WbL&NkU~BrCMfTqTY}sXHw{FZqnPUxT|akFUL>6$@}lJB zVmU@K3$Z5I9OaR{5L!iiy-0tx9wb)l0kvAAKIQ<)tjV0^$bl8E$uyGv1f}~Fr8;1G z2{1hZOgN#7q!dq$0j?ituOT+{y0@30Yp*6}bp2t`<2< zQkz(m?U9-JJeh04UmTqzY?$%NQ=f%Lf{a+-e;3j7wgZ3hy(s?7L^v-A4Jm|%<`r|P zqM0uCwM5T_i4J`ZozuLhL!YN_1XTdLCtOeAiW9f|*#RWW{0Pbn&(Olw6N&^OX_G=% zs1G)C@GKc9H20XLCm+CwS7VE)FK+1Yj(rMq#}%%-wjMBK`tKM7^Wm~S`P|EHEkfzXIR^ltL*VBpOoM{?V+{V}UCg;Er!;4; zw=S2V-1q(Sp0rdHw>`5tD%n0ll!$Imypo+i=jt%ZDO$O6AAC2 z7#@)@GKQQgByqeb#v!MHjDZs=DC22=JKHn>H<3@WY2f<}6P?XdNsjknF^Wv8$rw+f zh=l)2lLw<3#OM@@UsmM`Kr8FAFn$k$Z$1y1?(8x3< zJCo3kmNz{b!AnJN@-o$leq_pM3<`eA%|RDoNR`nT6nvFUG#7zYGJj9OMC#me@o2~2 zY0wJco|geF<`}tz2Cv{tb#gNfJXJu$R>*jD+Oin7R6xU4uy{^JG7dcXorYb=-9(L8nLDfE6#2u%aQ!rG$9%aVm6bNWWFWJXoE;& ze^;cZl4denfkvibj^-qt%C68o}U7Q6f zxpSv$(AX3rMV;s=LLwK?uocoGKYddKKV3-UPzVkEj7;I0Rn?)lwV$l$Pr$H-(f=<4o z7ztH_MyC)J`ZkFB2h#m zP>77eq(72clP;zaDTGE|P9LHpP(r)kG&Be~V~?bsh_5tQ4P8NCj%eeHwv=p_53KlVvj6Npz@T zz+^&mB%IW75sg4cj8KVfq$x-j(}*n-v9@*dswi>ht@D=}YC|e{N<8&a0S#P3RY>MV$?z?F*VtTj z0LGbB=wmsBsK`x@k|CEDRil^!n@F-k)2)y_+PP6ARasSyMy%nX$mSoB#9pF|Mxo;i zEwlpiYSCyFGDRz0i0q@wRG`r*L`_aYka*6d0vfnNsOTm5kd#BJ3XM@iqR`npGOI}{ zr%~z4my~}gT|%FyE4>tXc{dVH+Ngv^pmU!_okm0DnT;yZNHq>$qLdji3igQxs5*Gf z%Yl(LoWn92L!tUmn9m}OkFH5$SBL|Rloj#a2ciOvOv8durlN>vi>yK;EM-whi6$tE zLKM@8O87L%^btufT&4<*w1kJVARR;!eNJV??U@oT&)ifGX@|0+s)7m)i9;rIh*>LB ztiU9TnZmFz6_Pk4t`%mhD5z4%D!uFsu{)Elqrj<U;j z*HzGS}d~ObKa)kF2P`uHlBvWJHiIKFCE142o^C zOoD@??L^g5;8vLpCDRsU@#`Cqstf3vNeQGYz=~Q5wA!YG;$TxzL4i%f{XymvkQ`O% zVg;tcyX)zE0U3=53lzXLq);*yfFx+6?+Vx&A|@5%KXb)HZLF%UnO6zYuOUmN77BV4 zawR`1zhpGaMG6!p1WPK~esVoag$hiC%J_nS{*jfPoxX2h3l~SsKs0V1A8JvN;eW>c-}e)rAv$$C#N5 z7J1LxVPGtO-hRFl#@!Tg*hE~s%CE~s05lN>W{`epZhT}g3=uyp3XK8d^FhSf8i)YK znV`kuS$s$_8!}fxF5%mqiGXt$k0hLnCbF*_NU;PwLp6CTbQV;FA$bFi6YZk*#I zw`E&HzqZwlac1ZKvHRivr27|cm85QW`vpuGa`r4eIl-1nRVAt3Qv2kr+CI8IIjLW@ zaz&$9%D;O0rvT6H_BQ@4?rs;7f1~GBacgI*w7pf{Dr~(f6w8Hj`4!pz2E5EYp$Mo? z$g5!N4lH-f{3USr_5YhbC$sxcJ@1xHT;G`3))Udd4c>tbUA+Ir0_^t8{_m8xwqB9K z6VYG!{D<#<&!>(#Vqz~ti0fDr{7XoV*i;0}&t#Z$%YE0enG@_~pl@ioJ!&baHS&w= z-%#HTojw`3J{bjpC-(CBArD4j&oEs(Z@NyOs{iMDA-DQ@E=0&gerIb(BDwt7e&4)k zH0u|&=5gcVbdOxOPjc^MU#)p@T!-fdpC8_kOr0P5Du%Wk{gYNtA&$EJbL@1p5eFE2mTKQ^zrN7s$B!m2i(O10u-_-h$-pZaYErVv9tPV zwf6C_7>}9fIF}PaKWBO#F@N}>a2G+ILvI`StpD1+ssfm};hvn_(@v>`09(&Yv@8LZ z5&cSnyDJE8OMoXP_~QzKI}%_8LC>NAsFjUEnj?4bcGTaUawY@9Kj6dArvY~z)ql)- z;ZT0MViu!xdCwp#BsS`WXK!_Q)7M7n~*zd zQ=dCzOnrwtLt#i3`GBxHE&^!}E`4|CQ=6P{iwUw0E49%%Zyn*ZJH@h50DEEju5br| zQFCqEbquiiLl%fm!}eT1s1Cr^b$(TZWd#UE zwTkq~^W9I(4E6}RCut9iTk{TEq4SpcJy-BxvPW!iS2ndW8H$LL`shu95y(HNGj-z7 zE6*JWN-WFpCM$p>76i#?@TrDhR@?6nU(b8b*ZYeaJHqt27l{4WIPUenG6A7)k(_7^ zNX{ib1WBQngINRyyJNARAH(XXKEoGzIc^p-H<{r|(SS|57R(o&XgVFkg!OehJ?_MK zOp^Pcn87amk_ngyx{BXD-DO$Ch_OH@W8UN<`;jx1Xudk|Tij<+t zntg&OwW#{cCRr&aU}y+pMg*!a!r5Jc6TZ**ZbfGI53q2jkx@IJoKl2wC*K;-mrE3!*xJV)>!^p-BA_(^txbPI4jx(x@k z0V}S{D#TZ?t8<7o4(xjfj(@b4vgW{=LT8#c!{~Gtu8fI0m}?O3m{!R#iMh|x+~IPaE}M&z6r`Q-3!?8cT(hg>SvUO1ici1|z_f6@-b6HG#J zPl13yE$WxSvQilQGy@|!(^do}A6KUr4h8@c5w$pE3jQP;Q_uIy@^^{Z$b+AZyt>_nsZ11s?P=OS!#LL$VubZa|pp?a516+H14d~ z#+V{>9A+{hfPL@~9&`H4F*!H}3TX>QI)VfvrnG_#D7Pd!WS#`vW-tR8bF9US7fn5v zo4{BHxzYXi&S_4XYio<8d5yfMx4@JOG;I(381RS_!_5Slg1^u}9pHszr6=dAJ8PqtWF{np@+^*kX~frT!%$I?5Ce%^Kb&Yf24NbK(USthYQm|WzsU< zrvX*dga(K?g&X>?%$?4?@Tf%#2K3#)0XN~WF%g!NZiEIx`}4dRroahAIvIk8hJ_XZ zguS}W7RphgGs*+3$9!^Ux!}1f0I>V3-nGV>Mr|NXjTV<#p@BuXgp`##Ra*zCNJ!Pdgl#xdKGDUL0OYV_m zTQpFvW@m|iO^jUz@mJSi{>o>r{)gtqh0siD>C@0yUH>Z-ws(uO^*?NSssBC0^BMFb zi|UcxBiW(r4lR~Xxc}@X_6HMB_D4a0Z0s`?hhwA#eF?iDYk`;VFBRMkn~Yt*F97H_ zO(DBUvMC}Y3lhhv#s83&E&ID1IdX00fdxVI3rdU2xV5;+93e+lJqj5?u4EKcm(Ttg zav!@sOS_QuMjpad4z~?iMf{Qk%k|l<2374e3|z~l{ls|T=gRR{XgHNjrOYHaxIINl z0)0VrnJ<6!#_MNJ|6#>$V9lRU{qNKN;?CCW`OnVQR_R6mpW{jC|C}mCnG}{QK{z5c zno<381`dBh>VpA?xY`LwE2+%nENL|cSdu|gp!DaHNa`;LorIe2us_L!RoMlba!EH0 zqww2qKeX6TsHWdn4vWI8kjmrrMeh8RWY zsE3(HjtM)*?l2C=F@p6;CoA$8+Z)NO>oA#v?dR19@;Q~6ec8`Vn(s}+8=Fr1<5%xGSYwZ%i9!I!C-_k8VJjhxbI5sAdbQ20+ za&P0k2NOJUeWcTdoGs~ebRsk716uVuIcwDF7p*#JTtpv@;7)_~!eL@J^&}oN0ezG; zox?(hC5IUr5qDDvB6XT3rN{|*hi8jP=j=aZTTgul;49AmchLU7od19OXW{-Q)aof@ z;qm*wTiDuu+5hKw?%n_C=Ni^=s1pdo6px~DPyAQfF72lGzqGYg*nNrrp5w{C&5*aG z=6VyK4@UtWNtcOAVo1X=4N|o%BD(|@)|h|G`UVWaQD@4@_DKld%a=t|m2gEpo8*HE zAPIzyWE}yrx+}Y}k0>T?NNhSGjvElHTFN3P#)yR7naso5u`GaEoH{1cMcF9<kt9cMm^}-fv6Q1K<0(= zSQ_Fb7=ZO>V$m^yjF%3527Q5xOpkqz#p_Lir;KCjGepzpBJg=H3{qM*Ds9x=ntmUR_l$+KqaPG_T}UnT9-aBFz(0z4%CeZd@F1 z62@h%7(zN9X8>$+Turr!5U8^h3z)d$ci}OULo^>3@yO7HTmaACiN|~!0w35_SwTlM z7PomIgMBeSFC>)DWI}<1TN4p5dq4HfsJWH}ksA&-WQ~rSGZ1Io`R7bio81NbnGB{b z0`@Q!3uaU>CnH^x2xQG9rbS@^7ML>$8zu1ZC=bfE8i>8FPjpgomWO zzkEoG|G_SOHsE)Q!1Hh4k}A=;72z>z6dl+Bm=Ar&juhzHgg>oBcY}#M^ek0 zkN~q894?E-iTe|HbfvV0Vw8{P8K4<45-@56Hq)j7C_z7B$%xce5JyWiwZ?QJhy>gu zR7^3zMn{P8JY|M72tI+745!yewGb1qDe>Ui2*gV+(KH zUU?~Yi=}R1eFFn`Rp5EP@cmCekMRFO9}7I1#y$SOyj?2I`2SLQXZywfKgaWb|MR~G zKi%0@q(q0=qEW~aNJd>eB0S87!65f8`%@)zaH$e((so@-e2BrZDnyRifQDAkR(E6$ z)g_870mKe`)BaX~k+G>T?D+N#=GA>rN(;}o^7_mxUn&%~vK7#!19csTXv&A#zpd+Q zJoUQ(d<&W=JmA{s#2rcIHsh5-~di?Gr_pV&79hZt#P~zh+fFdzQVNpko z?TN=Ky4ZJ+PqZnZs^=Xu!G~auPh1Ovgo<7wa9s!a`Cc{Z%5{T_$j4U_#^@lQ?$Shr zV0O=%V0#)m)uVuww4V6TGIsqNy|tCwEo1v2zob)*#E_tB0k`O?D%-`H zphC769ORR23hEbi`ST2IMWuX@PksLui>=Sr7caM5IIrKss}(EOQtig-rPOpDvY3C- zGBzOEP8zW*1`5S68*8YGQ-@gPBol?yLwOy&Qx`;R4LKW zfsEe*fde#R30T7cr|f!Ds7yYuuX$Po&6IDeT$}drB08)m)iw-Q+jO14$J#S&+6ZEz zp^~iuG`VA-hRE>66%ZvF=zS4}u393flq=mqZmAWjTBZ(#5Eq~FfY^Y#YjRm0RYln; zLq_Je=;I!VENz+lJO$Yc)(!>JKdiKYnxm992u-h|t`h#fs zBWs_KddW;u@9>ZWD{c{MQ}zd#A}B4|18j&;KIilk;Ly0It=w7ADg$(UL8Qn0xbceN zwn`{$Lw{icOUlSiJz1ZAKQ^v&BhFkd1@ z_;#yIYH&VefdFgv*Rkkoyj>#YDIoZ_1PlvMi$GWV|9j)VILrL{7~{V4|KiTf{qIs? z2fAP4zh`(};=h;p?^)u%RR{HXu#N{Jw87a=Dsc-+o{-i@L?^m4r^`Tz-!kcEmE_1X z{!MA6vFHRtxKGduD^KnE!k7}wrzpmUyhxBJtGD=O03Pu7dR=gv+bz^_WunqSn9x zHNfhl9LODGmNhlab(~R`ZzehI_Iev+Pp<0vMm%h~7Wlwy4iRvFrd2Kt^zdoNzv0RF zf5vmK|4&ujzmCRg|G%@lv%NiA|10C~f4%tsZ~wgb{}=!NEdF0*tG>Dtwnf(Z@)%R7 ze0PNJy5rmIHf%?LQwJ(Chs0galL4cc=)ghC?0hg%&zpoQs^5HNpA!fHX|PrxQb>Na4h+al#p9iXbgixQ>*NuXt9n1{u+TrM|&3Ia`>W-2iShpSV)I z=rZvdh+0~xQ&7=0$RqAR+OVHlV@{tB77Qcbe^0)f|6zwUb(uE}x>^{dyd)$dBKd%8 z&$I`=1g)vM_`E<+b$T;D>zOPe#w0ZpElNdxjB=T|j3*v&1^SCG8YTuNo9;)TUMb#f4*j`<Z+-i~ytVEj2 zrm%D^ouT@O(smZhBB1&n15|PSI^fdqHw~E}PMu_Lm4nJobTdaj*G{C1uw99ag5oy==^V7VsfB78$|E z&|YN+lX&LLlvZS^A<6Yyq8IVIE*hhu8usN9f46vz@rfm8mXql&>Ak`~i;IOdX|XVH zhg`&zKg{+fl_rcw+#F%_5@NGPxB2cZ4$gYgC5CT-iCnN*8d%oeJXh?? z+TS_;KptA|8?5;GL6pYo1zl)E^{y{=A+YuT+56k|Hg05L6rI1zSJ7tP*CJ(#mYl@Nl5Ax;vYcqfk-aTBnYCkY zPK#_w9aC%$o0P2O(R*X($GN0)3kUT9pzzVnrYt+jZ1b6kDK@$Q6bgl^LVdjHsKdDS zefLz*Kn%NB?tO!a#Yli*#ttbOM-^u3kOE!5VB}Sw`D#AXOzFF0CX6emL5U5xKfJS) zE#iIh&V27dceiPB6?q%?{>w~tTDk3cDC^OQ!`4*FA1I*)pS&Xrvgwyfl$H85f91I{d~oEW3Y?c1JHdLiRth8p-DYx?AM z0JN^Rvjs<&6@y}ubF)I}X@CO7%P!LCu#^Ddx#El(3T`8246$WfN~qDqU|(|O#5i&B z);cV3kF~44>o(3qoliv;Z->t-FF5l+2)_4J%3dJz}Uh zZXXrJsME#Y0A2bQN@Nmge>^JE8NI;m>6gkEhJ~V#4By}v_?OBzN+1KL3;Lz<1>4@t zp|*W_T1?$J&@YuQ%Y{$&Rp*6uO-+s|6thwQlo zV%>@8U$fGO_wWS|2=SJ3`{6zP&1YJ?#dM(d^W%qy)jNh+mE3v1GI9?)Z|$QHNBj`1{|o%eGmOWe<;q zbUnsN56Oo9A?tv+XoO;o7V6(s{p)*z4Lk$P{!{=r6-lvizUR) zW4tJ$=(NYvZgb7rV(Lf7QI|>gdyLA&aC@nxr^G5FgE>!_&r^DaZ34BkBj2(9& zBDUDyeey5>wa6@R9sa6Xcs8vnV^&WK`3PCTK|^ZmEfyw!8{-|&rCb5h{cbNUkNCiWVtD5?Se8@}}F8N5Vj0yIRBB(d}( z0dSRVa?wBs4xPiuyu;!H(*r4R;KQ%lVb1)Ll{$ro$$Yke2DAd$Znv>H-v6wxKYjYD zlRSKweA`L><-F#9vmN60 z>Uh~JC16+FeIK9;=762&Xp>j}JfyzW*SmMao8S-veB7h$SN0;mo8)g4R5G)*(Mqi_ zpDV5ETThCmHK_i}kt2SOi6A5;?QTGYx*M33lspV+X#SKA7n4R6Im9&;{W`ts4wHJF zx>qvr`9o0FH2If8HkmDJs7!G&T!KF+Sv8r3M`G8T5g%3&WpI(K@Q! zzG1g(oKxjcfxLL~50wDjsFK7xUQxY<&kAc=14{*X!*|=K>?{s)EWEQ;TIlbfH_L(C9&b?~nP0hP=dx zZ(^(;%mJO7pVcTs6zXw_J~uIaw~nJC&p1z~l4k%Ub8~+8ZXVJuwD>iTA}Xp!|IXy- zu&+M}K?Y;~vyfzrqWBbw(o(HF79p>|o{er1XW8tu7*eJzjHxW9LrOag^`s$@BwlD^ z`A^{~0ZGw@jxU0T%q5pyqhG&4jh8b7YHeTdnb|ZSpS(JJ`mMWh@aGR9LVUFsbvZoF z-zgcdJvdFLtBG;OStv!(TY#lieJSuLx#L>af5>JghhAGUt4>n^WtS7U_5ddUkJRJR zol#eh;I2WLIg;szr9|L*w)Q(LQ3V!wo3DGwSg`B5Axd2HUahjp)fYjj#PW!lsg`uE zlWiBrfTz{CGY?=8OiwQI&~UYAv%SJ*24C8S71?%P$KEDmIC22<1|w+1EO|_wt(vV5 zpC!(6j}|CuUa9m|VS;PWXdA6&a zHoUI)UPl3{QO}~={5#J-Kz=q|%b361wB(`O?9qT;{Jrrk&=(dGo@yk>4qRK7C%WA0 zc)qabW&{VL`CZ8E%ln_4+|*qE6vXDO{WGOrJ8O@YjfGSPtpwSjlD<6Mjr`8%i+gWI zc{rfoh4dbL^~p&OCi`cky{B-K=Bz*7t=+)-ljG#V^^YKb2`TYfi|OGgzNSlA5#u1> zvr2e~kcOwu*cJ1C38q-rTLlkvD95zAL#upE`)`z$dIJ>2p0%GziV*SX zXsUv_`9#Idu0vIb^a@k2ZUu=C@t!JBCD6`rahi>t^ago}`8L4U-~tmt@aHDfjb?Tz zE$hoS!z;AtTT7hipH^>KYA*9@PiAR3ywNmR7+z`)M>w49Ft$L;YHb*-*(ZiTe_OA#f9=RgCdttCQGtQb}HXL8nuKYWrly74G zfXJ*IS!KSO3e|;8^MGR4c8{UyFB*8xUjMQq5=EeU?Q|BI9T*{j^`E9;BF95d|Am(s zAC8g*WAm>ZEZjQB;v2YmP(uiB9!N@t(RmW|BHv09Z`F&=-9r=m+uZaSi0z-`f+3Qy z!!y4{NHc1NV_XE*IC#I}+Tr6^;mkmmHMvz?R5Y=|RlU2sVGamDk%O!FEFwUV>w$*s z4MZd2qd^)m=TtEEr$Sz{6Ye;mFH9cxv7vqC+Vh$Vc2jG$K3Q zpRbX9ihG7nM|qLk2Tger%pZpeW9GevWEgw(+IJG9)GGS$_1!=4J%l8hKZEg@>7?RX z;)YGfc+SjG^{u>vs3V=qLEo9jZok6uHn_N8OMqJ3z)bK*zO4Au(p~)S;-(1?Pnt@~ zYrwUt$9)~zyZ-eC=quN{-iYWlwOYBW(nNwe90P)X8xYxEF$Jg52pEf-xhDBKR7B6ST4Zo6De}o$OvLTZI^+FHnV|5i z4vX@zpw7`6lVi<;(q&#;@C6D224h(Uv%ZNuu%XwEow4hg>qGNXg+1J&pfVE@WxNa1 zr%+lIk;ShIDeo`~J0-sHv#Z?GVMYFnJNu?G5^2p7_ zOrS-+97x%cm$p_MV@aMdAD9=2qFgZsv3&-qK#w4AB4sk^G|Lx6(@z} z;6(kycrKy3WJLHn4i{=LEzU)>f~&ETV3GrqSjP@*TwHXM6e?}o(P8 zFXl6y9YzbBUVyX0(~$X zgN~4U6eN~zGLDKdjFaqaGP`U%ircY7T{k!ymjkq-;^y7Nr1(fH(n*q~gHtsx>=^PWD!CyW9Y^4(UQH90r6zp6j!#k7Am6m?u9slk|ncZ zSZpfZd;zh;#WbH?QsPrx?v}IfLE<^!&dhN;Hi1F}YJ=EjT6OobebEhN1;>x1goDpA zT>mr2<oX(#_hW?O^!rG)Gnc6U_-dAqu2lnaQT^xp-#qZ@ zKip$Lj{YSU@TV(;W|8Px4{~>C-K-%d66^c{cC}yPi zvPj;QPN589@(;_f{?TJ_)*$mA6_s3ox83>=)xCNl*?mW~vrL+0g}dYY9o3mm{T}|q zxQ3ZD#c;Wx-g|+siNY)i6jg>!{)0LG(NT}h@amgLy!s#ip`qZXS>kJnk~*sLeGDEE+%~wpM~fDA+We>mwv`i{rSJ~;LG6r zUswNrKL0<(=X3q%&*%T|aQ;WCuQn^&T3h)vUK$Q4cm@lFDUa|93G_F-|G_&qHQ-!n zop`(>0g{!7N;9RyA-t1QHQRKcMo|`vOT`h01i2^vZZ%C|!a6#_;#pjb)RX{Gm5=z%(;U9Z^(92A5>g=5O=QLBi2zUrnOF1jxm{4-KzK@NQKt852 z<;{JJxr4i6$PN6Aoi&Mc+Hei7HwLN2^!9fT6AE=9e*5b1>Dsq+h0I9H$0Zam#eHQ8 zNQGWp1f$=lFAWf=NByX7*zke0e4}ifG-{~hb5v`C7U2yQB}=AA8$eVYB5-}x!;%Q?JB2ToLU*7FFQ6rj22&puiRa^C zY6KS*vpb{N7TjdqIhk$s zl07;OxfAxo%Ou%h&(gf;^&PIFI&0SLOXXXs@k%bpx=Te?lleo4OtDH8Y=0Sj6# zb02v7`gd5)ZHu|i6L&3YdBi;l&KZRMMBKqt-LuquS|y6m8vR_QZNY6Fu(Cq&Fj(=P zIkuzw6|k}n*P}(l&%4*8bfwn618wIR7|j;`RMbQ(!8 zUfiX;X#3c^OxD4<;mt;_NaV@3aIb~+c)oM?9o_PYdO!6X^P|$6n%3_{W=Kj-?W5YF zK%a_=4u5^KG{IC;iG*B9FyMR_5Qxi}uj!=mAv}3mrp)D+nzH3T9`F4_>;+i%Mb({? zy>ULv(-B-nq~l963#^f8W@yU5P6{9&R>kLIETkyhf|;;FVpODvtvvz^h_0LLJvBRGalc$;xMKp=)u6UC^$g*qTN05~-Tn@1Lseq*SEw49ZJS-Afpg zP#GK4)tJEsT8+LPzlV96kFq_})3e4!8pM7QR=Q2H?AoCx+Q*qy8nvw&52Ks35D;uG zXGF2o>P>-0YJkK8zp!ga?ynGs8Nk`hAn);9u^y7OcRD6)0Se~UZcI%U(~5ld=tF-t z0oNH?@iOG$5v|$S0r+LoLQAnJ`Wwx09$=;+>c7+n4SydWqQQPwDKm_ftM@2k)yB1AVKbE&{jUzWu*|Jm_0G9L|c9X)&LOtD>?$D3w4y z>8F#Nn!Ue{*T75FUX3S*1qQSn0r!Iu!KE?WYojLs#-$3F(DW>wVQ8s31`RX{xM-i} zvanKTkrM%VDFz%En+SgbTg^G?<0Q9&XYRFn_#EFo%g2vL`3cAc8(WSPc4)4~4T%iq z(k21gxko>FyCN)%} z+8t$Q4A%!tx4s8zGiE~)wgXsCa@_Wc@E?-&-sRh2x|-is1T9|wDV-#b*rp{qtBf(% zx6Z*1VkZ_h8s%hpLXvA1$}MA}`E8)ImfP$Qoo6g;I2z`H%NoZT|Bjow_JXOICj@ih zJO*YEwKgByEO{6NxWT7c!A)n`GB93)N{-UOTRQDwxh9w^Kr4vFL<&d3=kNALYl9; z)K0xrXzwE!qc#=TW(tgHm?p0hSf!UmF204>i(vOs&24&uN2ubFc=3V0>-j7>Ek?s~ z;a-&KtanCPI<52*GZXC_*P(!_!YSed_f(XRE>1yEZ9C_l+EbdH|GKs&9=!?cF;|~a z_*9%Rk$0<@IsuMw=cU%|2ROVy%)wJk2`xpT@H+-VRWd3b-{=gd*uh!;Ka|S>6EP_b z0gd>Q*#p2~cAU;foSD4@I4POd*La`i__=fDQ_UNug=?_x2BWU+zDC7ER2= zUTL0}W?%Zev6$z&Z92)_(;=lLt~n48q^`xu8)WSfzG4!@RNmH58k^FFkFX3SFA#x# z29)OoiDfk8Ot+cIVY|#8NlEB4cA$vwh9lLHg`ybR9#s@p|lgc2S`VK&;`nW zNJtpxdX#0PDbBL7Qq_7VJvgO7CuZo07K-3E9etVtHo%-DQh!&R0%ahQ zfV`VCLen!W`;LOYYLBD=C;|^3S|`P}m{1hHCeuPG)T2uuo*wQ(T0){vd46=11fy@a z|4$puz!4^r+zs#ycU2pmSUxr->b^kV|uSajp+e8o)y_b~0K= z#^k&o+MMwYeKb^?o=;P6x}nio@`&)M#!UbS9F9#t-p%7xKv;HS+c9AqxU^}~sA314 z!)-i$DzcVU^K@N2pv#55+wYR^k`3_Yu1_K7mL;dE3-z5-%b@p?yMNbgrU0X^vu1N` z)){$+Q)0$N-BH_cc3jY@S%;kZabDXccgSsn9_Pz-sgpe}njf?a`pU;k%ngz|&TG2T z^U7mA&6nv4b2(y`zD2XbmO*D+aH!$DFn8m;t~))mQM9-DQr%I;MoQqgXm->#=up=O zs_3v<{8u0s=H??Mj=XQ2m$fa4Y+#L4a{rqu`x~ZV?(gZ6} z=e=FOHta6^XI>ec!L=6V!_qu8TpZs=GhRE3vx+X-pxipxp{Ded+qXpBf2nbR0@-BW zJ{}GsK>-K||41ComSvIB`j~Ic={PGU=$*wTr3-EyWt8 z@}e%_p5yDCEhGOZTs323j1Ck$K0Q8cx632M6}CK-lrB!F?XpPueQ@#tD3JnTJ&jxk z9Vkov4m4uAy#|dqLaR?BEE{SX!M4jGWt{wD{yU*%{vmkrWor*Ja}PD#d}-XM6Zg|2 zMt8bAR;>8%;e+IDMh3K6o=ScfRLrlB3#p;_)Zw558!pRAK+6C0=rFx5j}ECtdFWt` zh0%cxmnVCrg-#ebCa7I9zgen$*M@9 z3>O97;6dHK0UHQ0QO^YO=j0aGu!1nN`0!-cpSQ;!etU$G!V3fovM7Ce$wKtIylCMY z(>&qASjpBaijl=X*6y~9AX3__PhFeX2X6dvW92h*lg&5x%i~NzcM7yz3un)5Y9$4x z%KjMF^nk8+$K_nj$KF!o?fo=MhnH|FXW=!u>L%Gv{47=aQ^ySa7)^lmNISY<3a7eo zCDuz}mX-GyH9j^m{GnIX&o;jcIB^^p$AbY};j<5dVHOn#-Y;hRibG_>z;P^a!a}fH z3zkQZM6n8N@8beqFoTLUhqe7HxLZQ#L|V!f-i9gl1PUDtE@5Z>1>93g;v|N70R0~4 zPk2o>9P8z)C%sWaq^o=x|UnVp$rczFN}veqEL2wy+?4Wcg>Gj_|+qo=SN>(3c5b%?*?bP1^FwH9B43nHw>ImCaJ zr^S3U#4s|ARrD(!UnT_xK#?;o20(GR{5{&cWr%ffP68Q#lBvLzDuzQCUX~356jsN+ zd>|Hi5E^^rX^S}JxsDM(O^}Hj*Jj%s&(Fw{wk}}6G)f+RK(v5Osi9_+kI&UihPMA8 zgM-6*s!(nGHDeRepD@(OG=AfOcAc?Jvs$0*=V9d{!DW&Ic6~R&H8dFT|2rpEX2f*e z#1zJ(Tn-XiMIuC6+EKwtHtW+a0sgoNkg1q+-P-O!vCHNQ%N+#Es@>xG`I|Tb?TqVX zbm@`~7(CRt#nPO~KMD+rf)x0Sc8Vv&a57A0^pAJ{KrKjnjQ?Z!54ZCshc}7w)TQwy zrw$ueQ>5X5zB-=;4(TRq6E*^K*gxk=)8s}xcBc!N)59`DE_nQ?<#?&L?rj4M%s$c; zaJ5ELzRf2S#Z4BEeMn<>-0jJXLHQai?DQnf72|B=0)Pi>m)z^$@xMdR27z#0fBS#^ zD|rW}7CzSRgPwrP6Vty3F_B9IQxkhp=r0ZF$$C{>lb}A+)4vZr zeJ{vKQ`0iU^!*3Yl2ULt9(*RIe-C0JUmVSx$Vr8aO-)&oaag$~IlcN!Pya^r#12eM zP;*03{H-BT2{)+gQPmi@)R*@^Q`Nr*RcU{*f>_gX97?;9eE@&@OiBMPl!Q)nnmaKq z&6gl1k*SIN^Z4I})MR`ZKY%xVrlx-vY9i-N&73qn&6go4 z>;HLO%afvz(PvY^zX?T==POebJIi(x_H1cLQr3I+nj}STFP};3--D#siHT`SJ4r1| zRKgYNnpDNUWY%fvGjWM7{!k<%ox|b^oI@MjJfICZ;AQ-ScE?x?5{Au5x&J{)3N412 zF0lUF@?J9~JI@kug{cgCtUxt6hLrBF@V+VTCxg8vLNd*c$Qzj{z%4)Sorn>*kl*7lvFq({$2*M-y# zlD+6O7`A+!jcWE@t?|N=*l7!ooxrgN?$od2B5r-ObU_Sfk-3&T_)vpm2V@TRQINeM>!-&MQ=-Y86gXohQz6{;iP((4rT}-8>?gZG_`)c zm~c#ZVjX42YB*g7C>$@UaZ*B}=u1WjXK-`U{6W$4QosJP<}0gnhe1nFC{)7dB0ZIF&eun2lc$Q}zN=cmFAGVoC2n+tSVj-606^ieU0xURuRd&Vcw6Yw6_^Uw3V zW&dZ1kTpNWEU>s~ewNIpX^s{TN27$Z!{gKkx}^)p7+J5HMrrSvnh>aw1ui#wrL>f=C{I`)BR>yB?uvQOQUoQm(9Mgq(P` z;iQkY)Kvz>i0nI1y2-D}V9FQ8p&^om#A<{{lsOQ zeuFfZ?iy8q)5Gvbt4QLv4(wBZVECX88hlO)6#a(Jiege7-ggy!68|-zf!dXV&yf;D z-iD8^!kx|o-Avwlok!#>ZydnLD!AuUrIBMoAF~Ss>Kll z6x8F;AEa)XTHVr4ud4iIoVxFE?dGxv#g(2Pw{?vV1>My9SWRNJ_TU5JhLVB8!XQrO zclCY+L1W+8wLVIfkluvueL!XX${zQTrf8+`y~gqILO3DTVDHf_XYXanmz_#S;PYe@95fJ zybX^&0q8Cd8X2d2nM@y4D@@77CEeG6%#WpAMk6>|FOmzuB%vHa?n;iia|#4=B81|t zyRVxlA%1CF=%=LzT~Q#q?$Z*nax*uc%T9bbJN(mp%oUZ5i3uwxv%LkdMx^(TX;IAj zi2Q#nZ^(g{Nu%=ajmF}#NX-jXVlT|y>uk|Y3CJlgrrB}+&g<)!(`GSlRC=uX51wAS-UEhmLxGKd@Z>t)ZUDtU9n9cL8j~=PRhx4@i#e>6MuwX3 z9(Jw$5JjvrIOJ*=5S+RmY`y42;uiUr;=VxOl4_piWepCiZ14ig6E#=<8kWat?#__R zXZa|fT_(r#F&fiN0w!=w(3RKS2~gG?g<=QPKd4-ELtYg{*kd&cwbHU^si-FzUiwrc zic43+4#HbD)B!4uv7)ZfVS(>(`;W?-4*zhhJ?IMRi|!3!+bci6dq`-DsZxoKKG z1iolrD;!2rkAi+xM^^H4Fp>4OF-$r z-4Q|=`8uICloo0%YkMMt3+vsiFzc?dGOU&@bAVqU$4u5 z@$>aJU;Sm{%LiZH|N6_XzFhzEFYEWe{POE>{*tVJtVzqy98{+y`OEC0I35)j*|ehF zqUV3o&;RfL^Z$MLLaiqxnK~H0ebo9XA54p~IG*+PcMp5&C$+tiprP3$o6atOm=^Pi z-FgoX)2UL$o~r$rT@=%|+lqqp1@Zmf`onwvbAooy14qB^&9XBG{==Q(YywP#?!O+j z__aJMU$3sVl6xAQc(V%B?%7cWoabd$7W3)gMqut2Hr2kIYw(oKF=Bn0*q#R!r%G-&Z7 zR3TQ;&~=txq8VylW_+gu`#nRY2$Ps94lv47tOBfMC58ec!Tf&!qc|duS;v&Z4U=@n zgND!PY&O}vckkljqL<=;d&TtR-iVr(_nz(T?Cu}zuBk!uORvVGtSs$4-qB@}PSl_V zcw;w8FCedAdNSq2jyYBJMc+(dr57nkHmguNTS*>A^O=J)Jw!Eg*`NUD-=y{UAlW-; zCEq_j*gNO~kYDx=e|-MxF!|;2%a@P$5BGKtlIJgzo#*>c_73-+@2kI_CXe_3JNet* z{*!K!I=Xl37D$fdmms*yEfTdtk=ikof{>W<=#C0S z0i|}zg!J9wCO;x7Tld6ldJMe>{Op!1@}UUX?<=8?JTUutSDFF)M@RCiTs#q9xfZw; z1b^>cUj13f|LNLkd8RKu3;6&2`w#AW{D1wy##f*D|EKtT=Kr7h{~wh9v**rW1ce!J zWS5hyOpi0jdCV!z7czd%LToZ&$J1ZG_YDca`7z9A%YPywD={Lo%4~pdCy%I6XdH-) zQATkrJ1Qqu=_8CyrW;>&Y)-=U4;|Yd`7Hf2euDGAoDS~Ya365UfAjU%mG&R?anJva zuO58)D2UrmR!{nq zX>l@5&-x%*CKOXUnN7D$YgFZbNvGpBy(b`VZh{obwq*qj#}T>vrWecEaC4I{jqa$$ z9BVI!)8e9^y~}U}m!7O`|B%gIL1PXr_WE@I!w|A}`K-M`__T_Oz@X|15&Wz;(P$Ke z70?F_4O`Ii^ap7-PAOGgefD8PkqTG4(8^iS#A?B!gYTvy$?xgBQjX5hI4^P17@ek%*uoUNI9 zbd8z)wY2`Hc3uDs0%su)({j^+5jLF6&Wjf?g8|J$iM8-$=kDDW?Xf6$^X_Ew!^;;t z-I|%HCgH>s_~bN8hrmGaXhmD~JvLvrJa}q^4d?Ph0h2hPshkw>?+-Q6o&g7(~q0Zgpa+eA<9U@*-mGd;y+c1nCI1TVq#T8(F;2G?*NPtxfOW-z3A4Ar0T z?Fs$6PPngbL!in`R?ExX6!Z?qEy!3C5|vEaZ9!WD(P&o`ON+&yD%U6)H$I zs?nYj!e4;HZJD6oEhYe-bJ*WH$-VGLrMPtj6i|oz#}I)E zYPEQPW&?VH8gKn%$UxAhcM(3vL6Iejo3%k}g#d*v>X48Z3Dw@;+gf|`pQ!yG1RJqr zA9@K;Z~y;t{r-L5|9Aas_0MPf|0noF?Eeow+y6h?|38ua-{&-7>o+YQQf=$r>9+iF zaF))TUmSqoDA?dW0Wu1sbtxOR`#&A*^nc!c*}3%v7?abJvov9|dwVi3Ph-EUg~FxZ zOupK<|4j!CnLF@x6AXKQ{cz6jX$NALmo01H6B4GrbcA$bpEl(SvQ9wQ+cSN6_US zw@g0}d;d1Oq)eZ)3&_d?;1p=^N4>fth@oX3-r=d2^CJLG*t?tD@8aav7~gA{6-sya zG#?LT0{F5l200daH{4J`?j?{CC+GZkWfvI`&RoOcO!n^VyHp@mD%Vx zjxAhCAWZG2(?3fmeOgfUu2@fOblYjAX4)1x;UYH}=j;LmFRyKv8SE(S3B(JPJbWlU zguIMmGO6q1gM;0dhi$Y}!}jFU-}3vkK_y_burUr7-wvGE5mmNL@hAiYs1n z4p_|G01q_@U?X?MVVUo8(yuGMZRI9~ArcGgQu7$m9IhhV%H=a=WeHevhyG>01x3%#N09D& zKII{(zx!L3E2%$i)=?%y+NYCr*th#s-gN0+Ra;Ojwy7z6ZFlswpBlNmmik_u9Oe#) zlm_-v)N0l67v}yn7*-rwup7>xYfR>Gq8o#d;3jh@QIr8Kq}#HWudQT=iG-80LLCa1 zExy#X`@tL7zCJXb1OJnOzuqZ-*^aj>=W60le8)Zyc&QRQF3Qo^4;UOoKInd zyU32(0M=aN(lC3)WLEZyan=WUucOfkEuonyLa=M zCQo;ycONCIIpi^W`$CQQK#z&GmX9LmRl^{3K6?4^8WiF!5&bBLxDEm>SHFvBSO1L5 z&Y}@KosUKaMqBPOvoOtxteVQ=Gy>nYKJBstRl5V|Q9cGZ>D&jHY}KWNuz{kQN&=wi z)#Lw};(-QViaX5WWMBkG2QNlm#KGzQofh`ofn!9r6N75ET0|(YPm$Ouw_%Eo+#o*b zV0V&apk|PnX0L8~zf9A~-WVoxAPCG(ZgpHPL0(ZlNDeG^pU8($2?#>JhuY78ez=4~ z8xt}0W29BCZA%P#+B18J zuD%tr+eiu+$w0QAY4gwDnz;Mi0X*p@5YOjL)fMdEAC~Hqo z?iBPTLfbG>%X%B&%ipOdt)7#HryaCZqI5O}vw#u%zthFP?6dxI zv)K#>WI!1{Y;FirC3Oxk`w(SPRx|^Dbb+AK^E4k(!d(>Y)k{0{SgzqYhIKp?E3vKi zT zxys+wy(AlMCart-?vWPmaZbC|np^7ib~&WRo*dR)J|iV&7Ov4Ky~!mq(A*VL^fXg+ zsT9NPA;?0=P!6{pc8WDXbp|b$uR4$R6r8rOIAKi&bxZkmlkMhnXD~`}4D-jCBju+UnqU&T$nZ1{i=~G(h}Ajp+-NJ_AE6 zSo_RhkRWX1+x2ypN72m~@RXP1blg7gn8SKM-EW@@+Y`6S$H%hO9{#zdZLoHuQ{-9u zyld>Vz7p2@qD5LKtMSP!SL=rNkN#oUboCmpT&Wn=;1tr+*`)%)>4CYs58n)pLZJNj zj}LbHhyVRzxBv9n^T&sqdIB-pd5AS?w+S?3&ja%0`K#}r?bbt~TBI%*KG8N8+U1P> zO(S}5|L4cg_MY_37+OLk!Ltn9u!dDwu6ZaCPR9$4oSBAu@|0MaSw9NiRP+bA?T^4I z$T_fzvahI4KMUW>PQ-H}>&?U&N5Ad-+0n=|Ph)I_??#(-*lcH$_) zw>`WF39fAi>$VA#K*m0sNxQWM3FHkJZ>6FNcU9v+KCi_R@jfmne7>X{9bM6KI*_QO zxR`UHnyLoNjS*H+k1#?hdZEvutXS9EPNA+@+ZB*%tgCPqny^2pJHkE{3;$%u2Z`i3 zv^FnX0v_h#6>xdgHC7V-GnB>5DjIeaFptf_GFcy#&`~1PxZe_sU|@y-{% zO%o<}135Arn6V!j%R#f@!1inPFvbUz``(~z1s$9z9QUVls4Nt!{oG5Yc2U{yoo9g+ zAZ$QJ0gVM|8z6-jn)a1O0#+NxdV1T8#5&5DpFrJjB`tj!Zihe2r)dAWOl)rO#g}Mw zht^G2^=LxdG|#~qf$i*WV%}scq`OeYS`|kqI{??A)M>H^snq!6ptTmA2In4L?SPiY zta!H`x9&7HX&!`YaylFK$@SVG6^mifBQS@TFSEgXTIT0j+cJQN^~(k4FPQf%ZJOe5U${TnD=I&*S zA!}aLlL*jUKRA)1TM)jU(Spe|naZ*gao?_7kH`RNWwA^mEU*u%a!MkTKxB`91c9d+ z%~!8M%^fVj!Gxgt=yjJ!HJX6GO*{N~S91r!cRN0K%QZFph06=0K@ci#F=#O$_)aHU{>}Xv~I@%`xd&fllzv)w*|LaClf8nP-{^!A0 z8xOwr@_#+}od4?+eLm-Z{T%=EN67yr0(_uu_jXNijI^!-`@%EjKusO|vh@)qEh*+P zMvG957$?3J5IdVe-SxEcaNaL^PErR|J?35_IXXISAlRJ!qzE(@~%<)CSV;+j1 zkNSQ~P?$q`$A~O?h;#!V_R@nuCkiL1fbS;bs)eB4Cbm2w4^i3H$Pi8g5pch*zln>N zdM0K(T5_RuSK2UyLBS?Lk-cTU^2S6Bt%>42W$lWA;dEuuxjV5Ki(15q@L7=#NzkNF z^mZK?HZlz_B-LvG(5n}U@K4}$$gu!cJJe_p(p>%eZGQ%;jZX!>BIA#P8tVWKXUSVo z1DqJ|Na2uDV_kC0z7@!E+A)HNFGpQ^w|EjfyH6>)ysu-{+i}gKIJ=cFvq~43WRC8odi9Y%2n0ojyX(2dRKJ|x_ z*9{&fzdnBfr`?-gua`XCd$tSz{<^lNzrSH$E7XpFYWh^1a2+ZRJ~Xp;Qxl*jWSzZW zoHi-kofERR;CmuVLH>qA^}^{uU00(9CiYJgMO9|Ai}uEYj`?Af9b=5uiS40ZOlJL2 zF+SOfTuGt_vfo&(+jN$ESMfVwR9jvpu+wZb@qockdUCWT2`z`2Ne}lfJ;En;ff8JFfb!Huzrf} z=ND7TtN1ocK=r1?9(6-nJ7?iL0tVwr`7vEpg20T@7)G%|LQEGJQ1)PkNms=D@W?1Xs!oFV9?Iq_ z-l&WFCqbhJap9ms@vNBal}0y6L1l!w=CgmVexgN<<0C`wW%(&!Q)4iR`0 zvQk1|vuy(!Q0i}XMuIWeF>rEv62S@vv(qc6*}3jmz?Fwc113d8Nf>eu35Bo; zhRLc;(S{7A_c%pfCtQZJIr3c#*2a$v23c7`J}2BegLIrgSP{|9^rs|P*mq8GF2sYIjiw_I81iX z38TNSpXnJ+=!U2V7N7reo`Qph0~`D_))cu6-e%KdsL}~fpi(Dx@LwH;^T4u1!VI1B%;YA}7(Ofn;%{^9!r)=pH7Y zS-DJYmWymOYR*24ZEOw+?ov&>hH9CL+ehXwT?`s$r)%A5O8x7BVtFuVL;{{HkVj%Z zp(84Xt=xb|!NA=08%!pe7ujZN{lKu77=v6s_BcogWHOts;eI=0AP3ou=^81Tsgd?% zTXSqwdFJE%pL2|%>V$C11#op5oDeKM=`>i)!_7@&i8=%ojQt7#^XhNy-mN9taz25y zW;uQ`Jz6esKrwk`1^PU2p}y20}82142&& z$fwT6b$!-VVB5Z)BpgU*2C3&FuwH^Gd;;~j)DKGy9VcsRF_vB2o^6mbSChL5EkksW zis`+0(LXqRLV!d_BWxJlBXb&MhAsu-O#YdVesqP^4fL=r;-T0~IboD1HKjjeH3W)r)3B-zz*5Zsh z^15~Sx)lg;zm;oh_KGfy2uCHL%Vb}nruSRPA~QAOI8#(MyaTi-PAo76^$Ly?c+%rO zR&hpyQGq!WG@USTAoK|2tb7mvCdj)xKRxCI)(`XmX|y%NT(h?^`eOwx5T(zRmYXZd zRQLfXKtB|4NCv=^3Lr*R1B@HO^yMXxf!u&Y4y|7Px^<#9+x!SJS=>9pJI;IZGU}vw z-SP-|nqF{42LY2*NK=6Uc5uPsYCj)C3L`M=Oqs`n`rYWPP$PtE%)vA*PuUc6lAmXA zqp4{8O#OYTP{P`#tG?y`ldYL#8vNNqi-#E{h@#NWCf(p^yPzi1LJiyd5U3o4oE|m+ zZ=!Tuj&Q|_q<4H217nFs?^}=!@f5OD0=BjBrL(+-(KuK;c=hz@-robf;W>icsn@+x zD6yzGUQ-f=v{@aRk<(lfY=tMX9YlgJ`9k<~G6?#&RT5sl*s*nnOaCraAzTZL9$1!Q z-Oqu(OEH>Uu1;Y^N^qE=&7uul8c&b4Badp2)OxIOOuOMwfcs%FNX+k?*v+{viJ1`* z8aQYvUgJRH-;#93m)1+d1kpb`J2E8ugL6iL11LU3fU3O)rxVf@^Qo_0LfmyRJ;Pf7 zi%ML8OOVhR0HgyfU=yR%!uTyuF)`dx14&v*$Ju!?nxlMG9QTqRa^U@F=quq;OsOr= zfw`JL`+SjGsv#E|Bkg=zUNbE)F*{)Oy_Rz>g4D@>r!P>mdvPod8``V^Fbym#*p0b_faT+h8sg&6yF&dbItHa<%&XC>8t0mR^`nh**|Awae20WN zZP{xs^!2)h6Mx-;>jw1)XXELDkx8?Wa`6zc@D=H=#1z)h_3Wt{#P`0y2FDx;jYn2a zXrrY93`B}XKr(|Aw(>mon;c;q0~EKQk}t?Y*5bWNe@qfTl5hR%jnTyezX5fK`Ayp;2eegWnz^J<}gb z_A_~dEj;x^7l(vKZ*0(_`t#TIH<)gDZN;X|gGXi&*$EAjMnUt9HxO6>&2MQI&TeV> zSe@1RP}^a_uM)nHN1A$7L?wy-q4U;S(XzeMqq}!;cH21cK}UCjuDIZpOF9HAHC25i zawqv7qdUcTC!Im$jBy(^ui@e8_#+l&{tlyTEFRTC)3Uj=ImNUyt-X&mt(xs498;H^ zPCfi8xOGDz4^YRIwhC2R>HAn13pSNc4pEde8L?4v|E!B$!y!NRqPxoJM`HpIVycI% za|3>IW*{|WXQntjcxYCD*_BOcUjb1#sf7^fVIRGL8o+YB%0h7Xysw`3(IKk=MCrg4 z!sDmSp~-8A$ZX%{6E)YqzNcsa(Y=ibK|)Y6w6S3OEO@%WMf2T*2eHNx{QyWwIlysSm zP7Og@|E?Oq)Toaa$9-@GYY3cOzM_!(+8?X|Ea8{4?4~w$;D%Cw>Z))26s>U6SmYDV zKg$jk4W`SClb1y;NS_^w(16;UDM^>2PdQqm(Q6LYc;tj+74HUK0N0Yfaoj1I?>zPt zjFYhOKx_al@G1n4!VoMp>-)``j@I1H#D{AyZyLaJTvnDAytOM!GxFdyGm>{hig0{j zmK~z;jah1l{8n>eguFRxcq{lZM#V*=Lg}letbdxN!))5@gq2?7PWO-y-uRAplPNmt zu-hbAX3FA>b2)Qt&hZ?ao_YoZ#saoDRmG<7q7HG}rUNK>2EQoTL|@s!^~I|s!?^lVlMP~BVH`M)8FjIOCOuUmVjI{2je|-9cmM*I*c4Z?HTmEk8twvk zba+cp+C`1oCVeAcZQtlU5)`{5saUlw`e_PS`}-^cZ2W?n0ya)Riy*@ZP7XlLlpW(4 zQK1bAi)`vsTkliX5?x2kJPvgw%x3`^E{T0qqO-k;v}PW_^65Z3S@W0jf-up~bXkN0F-b zi-w{-S?Y##oi0$dQb*HSKFiOuwbQg*JIiLLMTDiIdS#mSRntB-ZDfWZZVeW*HIv&T zriPZttf5`swrjL**0m;XO!OyxZjcdzqxOWR#^P_(zHGsGiez;E}`$ zmvq0BF=9(&c;Rq7|CO08IICAkb_JQAF#A{t)kG%3NV_8al85#=ZX=TdN#;HZfa==v z=+z>}@k2JV2~xw?)Rwnezww0K+@y3>_D!+dgPc|_I=kWEx&~XCvfW)`Q?)GtDq4K7 z>WbgrQC^hl!w8jZb3&2!IJ<~Z5pJeV$IEn2@j^4l>|pY>>W8=GQ$HO$H*#~b8&U32 zvdRs%u1}JFy0v8~5#D!maB1>GU%I?BD!x_yWgT~N3~Ot2tcAGhz_A_}bG9r-D2;eE zr8~LtbiUrwdd1-TO1x0oYYUtm_1|RwabI#&So?16cCl+<0N5QwgtfM95+v^^RyMU% z5IldO4i8&$UOH`jU{W^o&{Ve8V?gq;c0kYgN>M!{#r+`?4v5dhq+#laVS!QpgJL?( z%1JRc(IJFH2?|5sF+Q$KX-Lu`CNgvf9;ot0uWyPO?G#8pDJB#3J>sb5DQrW0hI4~W{Zw@ReObGO@1Xuyu+pQnuoU4%eGq(DQ7_Ga)Gt737gW)knK z$;w2Rp-!e%)V>3Ed$39ffHzy2tSfp~wAGU=B&zi(+3$A1SRIulU`%jms&_|Kpd0xy*8#OwkXpCXuupl@UA%AfW zQaQy}07oKe$g&xoem*O+*+StaiVCf>fi7=G-1zxYrinctk6K?d2d#>us-oaiDa2=SZU4^J-OtV}X^TF|r@Qa+a#prQP>aLvkjihz$H*&Mj7OJ|>Uj(H zMkh3iR&=9}8=o19Aun6!(G;BVt*W$tWF7j}E2E8@bFbBjGL!0+G^MhTdbBWvBdD;L zh9^`!iV%7b)pkrsVS|b7cy>mU7ncTktvR~W2`5omCa40iX%VU)HPn!J_6Ui>NKrix zwDwpaAB8w&IeY1sVmka~noimVbT|2Sy$cDQ;Xkh-C5|7u;w`tckEI(!cyCd^Nic=V z?MgDxO78*)H@MXpUsW5f>6UX-!vKX=5a@CwDp6lSu;{tw49^>jHL}BY!g{N4j{!<% z2Z$nxeA+2@~$?X4uB8pu3D! z-vagC@5X>Pf@PYpU;LIo>EL=rC%<|6{Xrc8OWupX&^D!wXcsBu_Ab2Z7oA!L6C39g zWvw$u0yqr^j9`$83B&IP1;+f(CrUxe|?hrUwFP~A9hYx z1bjwc-<`;z-(LmG#@LWHd70w)X7Qw zL772~v3OB3=wGP8P@b&1dYR6mSlLKTvijkCmgCdvYG<5!_pjw)`HUCi1;}67j2a}4 z6;n4c3kPR&bzOILQg4|qUa~9eldQzNTd;56d#{(5XGg`z2TG~%E;%? zEtVvBY|?{+M3=lz-s0@QG|^4acT|1dP+wo7Z~4%`WxYHLodj3d5;ecc3&RGyExS5+4>$fs0WY(En)vw&VXRSGCR}BUa1@l z!~{v<^diFxukniSNQCAqjDYQOxpsvu1rRQSsX`3p?dF_l`tsDt&EC63$%0|AZF#57 zWHLWe4Bf3d57?pKH`RQw*hO}%JwgO4@pAcamOOGt$k6?w;$6H4QcOw$=*D*9HWP*M zH&-tqH8|7w(E81_MVq;Z6C_8FG~F#pbUJyIwg(!AI*(YqAM|dst8V@%!@^@&iKP$Y*87A1 zAfWsQzs`Tbf2kKewq2NQ`;LHh$9=_&pyR%HK&WZoar*XNcIW3o*gGS-x7j7zcbu`; zzODC-|B@MRS|*15wB_ z!C|Jj52c~x%Ww78zJ9C*9>JMV^`9FWQzEYtGX*&A738Y)NJ(owJF~lwBmu}dt0!ie zx168F5( zNzWf7KT((tXXb+s9E1x&=*;d|JBAd2za9u62A^W`j@gHg%mc+{@OsqHlqf7eJM9Y| z+Sl$VH2+S$4+>F^tZaD;%)#fXi%mW#BEk)>=oVMs3n`fBrc{ryN@dMHm27AgMd-<_ zQK3>gbOJtj$J?qA$J#r*Z%ubw_L7S{^1$=Yg=1YT$-B~fN-mag@inuANF%iFmC&X3 zs&t-$VUBz+tFEq^-`lzVbp|p!gc5x~^sA;jwRSJq@rYIjp>)ldxO}bfY?TWxSmecN zJ~&Oz&>UcVkB%UuR``sVOu!~NCjo3ZnSxsh6@Vdip0&au>)ODG&G=7A%uPkRBA_3Bj~(Pj`Je+AA}K>|xJWeDN0beIibc z5E{p(`k+YjMgWhJ$m?qQ`W(To2p0}I?}9j9;l*jX@l<*JQgkc$E8~_zk zM8z`mJluu0eYq>{G>)=kT55Z97%ny;NK3g!X~WfW@f5CBfP<)Rbj6o*xGt`)QE~W2 zF)yy;8|VRtr4;u=X+Mm`Z*YM6W){PvO6lAbE~;(UuiCt_N&W7X$Pp=Q~U51RwBevQQ`p-aQaGC2IAodH~W6k~YrQPNg4(^=a#Ka~;EOvD?vW3LzjHNOQO zSpIy(O?)s{IC55cD_i1%Kh|W`^tKhYgWNV?x^Me$#@*u-R(~>1(jig@kj3XHQz%r- z=`N#0@-o~ZW;=% zqEeJ3#dJ0|$CILri;*pszur3+F;`?*dEM&COP|**N0LjDqiL4DZQvtqZ49fcugA%1 z@?R*M(^rER`179A!v{_5Ty05bv{Cy?G^?ub_@t=;z=}$B2PhY|S3q7=3UjJy7&T1AM#I%!45TT3zL3~_uUx|>RH=0%sP=TP~Eo&Lt3wWAm6p z6|^J&^|X83wm6xNHOS)MuFaL$K`Y8thk@oXtdKev^(OjDRt0UEp0BKtq!zK*!Y)L> zfosR$j3U?#O&~aYk9^wUx|m|89#N$cgUGF|Fk)dmqzGLzLt?A1$17ce;zcXK3%8r| zTgA&UWI=RL&X2%=i@>`$fHm&PSpYphng}$zn{0?n<*&ywr`}`x^qBJ=sUHQ zaPi(#uUF_E!au419A(q1Rwo=Bx{m#j&A|S30NRxqL!_~O)aOJhX1GlFZ7~`Eh-1*b zE~&1P=|zfH3oaPcOw`tU&s%{xg7*fZ0|fzZhAnHpCrpKes zE4UMRwk3(k>Mbq5HY9N}!E`sAkQz=B!00A^`x%ODK>fOon4rh(osE?w4%PP#ivtSj z%Fc0hRw9>zi_P?El>xR|!EP~|vZJ2$>PYH6>*o*9tyvQof#4B^`*`KUIO{$#{4UWxW zsI%RXT8kFBac;rzt?KZu{){{tph=2VYf! zb=v~m>c&r>J%20)@pM$AHKTa){MGl*cD?4+zz+^z?#ZDL=c#J_^X|^!bGv?;`jEPQ zMso!IZTCpn+p8Trmte<9&T6zaJ+woNIkm@h%$%Wk!z`Ny+ac;Ju9KUV*W4@~>QwLF zfB$#@sgTHxw%6y(%&ZeqXU#BE0ZbvP39wiDd(Zc4VNscb2B5+fXA}o(-nFng!5Vrt z$`gd)cxfe(aYC9d7$n?}?kLh2wI#)MICQS_3xL2{H zm;)H7Q?*iy8fm+>{WKpBzrUnmPy_5Z9nS)BeW|P6L~Jna?gwOo{?h@a#HP`ZR-&^) zzuXx8kdK^t$7RiR_QghnHSTvY&{cxCJ_jl`Tl0-}P4u;NPU#4$T32SDxn$w)H#aX5 zA2(t+4J9;<3a=~3-n!m`y@kW1jWwNTR`y?T_``){Wn&asYQ-5r{~*uwXNetJLW0F% zq2wZO=S1?b@PK#+Bee|FVKB>{6_D&B#Q*;M%xFzA(Jl&XkZD(J)4P-c=K3&_?d{~t z`vgg@ZH3_C&bD5DF?&9gVEBGxGN0AaDz?HJzWbu?g5x2MFkH4*cU%&YxXGjzv{#ah zQ}oDlE^B^NQ{OJ5WZPwn0R9VAh{w}udRcFP;@9G*u24d2j>fjZD=61(Z9DiEZkvwU zZEeRDJG$Agv&~zus98F$lFAqFObtjzcEeUO5H-y@E3;P~U;$fy&|&@~|3s}k4BUoN z)=C5y0&91jjN;7W`SEd?&3wJ!-n~SJR%28dRR_T(1LGbfWuW|q!{I&fo`9UkrEod% z_T?|re3t!$Kq_@@xMV1%7f@mYa-HTU<7}vlYn7Y`U_SPLAEFOO1{vS3LnzIaZEqGR zM!V{LrP}6c*Y%LFZ8a#VP}c)eEvvyebX^Yzns!2!L|hfRH*ZyBul9VEBGO3q+Qc12 zp3xd$>wuLybyPDU+pGE+MsCcqhtL{H_i#H#pFgFSN7>_{nmC;`o-~fXfthd}Iz)^2 zUFWdlmp1usc=^5oU)iGGH?a5BM!m;3VJr}&BDC8ZL~H*5y2WOY^OJi1F)dG@j?XvJ zhjSKKC?mhkxK*W&co8|VL8vpKZ==Q+5=aCDQXZZtmci^&-jlakHbK6ovu{9%WkPw8 zFVa%CW=4m}ATeFMhNy61GhJ!4z9`C>niX*&F%W=luK=BEBp8P&Hi3C6-TMy5VV&Et zAAg_Yq8vP#T^dQ;;JUW^z@#&BLg;uDiTtu5Uf^#^aX*1L?@ZCFTNb@zSwu)Q?8}R@e^sHgP5xE^#AU#itS>vH7 zPEzK>%>3O;4#6)JV<*t{LBb*d6q3hq`4KG3q1cX^TyaEc_Z0^!WoPL$OH3Jdg;EJG zO~zRXA=LD=tJ#@O7-6%W5gO2&$^dZbW7S7QPZEfv2|Ku|{b?5+!#)A7*5LX!|Yn95@JZ5g(}-ujjnGq?OTb47_2aD25u$ROe_H^RJs_txmqo zXsX;lgeAps$|WlLz8GFE!DWtA^QFCYLvDg_*k^z<`u9|eNt?A)^V~H#87^{`kJFLT z6Sb|X*;`n7B>x{`!R!aroViVx<8ISyus2%FiBh?9ouQ2BAcn7W*!EQg4}kCPboH5) zMp-Ftf_he3NBGxeoRLL^^pP10hEih>772j2TP~{$Ca~as&ko>jOIn)D?==&P&{dVm z7O7`HyV#{%lw4%cXA;HHZ`okxsB^~Nm0eKYX4}N&xeXjklxp4X2zO0K;G*OtMKPn> zx?A>rX+bFXIo!pfQlUl23w+_T#cFQg;KN!y>K5I`hbVq1A z%cf{ic5eMFfdXUF#LwCEs3^0xq{PNI|*>kkbq4eK?!dt$jk716a5`m z&}}c*;b8Co-tE75x%+hQ@BL@H`#&817(JPVFQvXyQ4;{Yln;dnGnJfk%ZJnH_4|HcA_i=oGJ zK{R0_E&~>$j7`5O2PT05*)M+$w@^b^Y*z>7tG^k>c~hM57|G)LydY!&xZ(H^iZVh% z%Vq>?iYwqmH$yCRuE*y-`Sct%=g!J!cOh%h(k6|%UC0YUFH2fb4zn%>q)V>qRquL! z&x)QEk!d#-P!_Zl`!EC}9#RyQ7YOD!HQ?Ji!-`lm zGs`8|pX3gXeL&i2{D5WCs4Dp6zr?6q8TGb5)7o7c4K53C<%Eny_6b9woZ57E7@ekN zI}+T|!MuWd$z%!QkuR(9O=_jrTa7{!7c3JuD@#OVQ?hpwhl(yRoY5l6U_LHx8sUT8 zN)P!)8@Mc@zgos+vw8bu*)MK_!8P!n-ny1|VnW>9r%-Ei=ux62lV zKSr8=k^sm^zsrO`dXupMkrB8Y5*c|C4~i5Nt0gF(11>B631O^yd$e7-ta8&Xvt^sG zu*wZA9~=egi3-zmaF(G5gzPm0q=zLDX;P)zEw_J_l) zYD)Ymg%KHZt|?)44SmwIMH;NpTK*&Q=p~1c>o3Q$;^-O450q#$FTFhNP6{ADt4oa@ zXVI8zJTCEey-lw%xGDo&2;rW+bXiiS3QzeKTQ+h&aKLO)Vf$KS(egHWm6nHf`_Qt^ zR3enH9IYl!ptLu6k96~`$a;&NcyttPpq&p?yU24x;Q?)9X{U5#h_p0CpJr6I5-9!R zG#i6Q)DZkr;2D-SERK^@!?U#!@Rz9ZT$+1iZf`K&I20oD)_xcyzDkYmXsa?NgVSvA zmiM+H!oYM6QK$`#Lec65i*@Edph@2BVdG34HKAYE5adedY!srg|CpA023ghi>&_+* z%r~=M%U~1f+O-@nwxaM(JUUX`;5048innXcAi<%w0wP((*@WsSojYdd;%>ikjDNKNr+706N2kp|ML_ad({t=90m24kxC;6dit&|L}V6f)Fnc_}&G z$GoY8@4daMw*h&syTEb|1F&X2T#Y3PCN4$p$BfdH%q<|cD6W1R?FGWgRl}y+||~! zcPn;q){;e|ahG~w(R6zUa1F|G#09szn!KIW8ipIzAMWU_2sGzb=p*g+kS$lyx<#jl z8&OilyFLi+f2uZt6~DrrU{f#zt5!+lEh$+L>N#YnN|cD4HNL?qTz1K~A&)As?Uqyv z?LU5>v_7{L32+h+;Wv8HLgyQl~=Tm;BFDe@5Xfipg>>0)1b7$ zqGj{>DUnBmiC5Zl?98jCyNKY`3e|{z-&LGZHLq+&m3VauQy)X zeFd*TNRdlbF4CWmAfhhdG~UR)9PQroid0mQ1BX(f^RW|*NoJ|qs(YC)S*2ubAI4b; znW)B))430BKI-*7{0n^L#M|-LNJR2hzMAfU$OvSd#Iz(*7qcTHw#B%;@kvQ8ot~5y zD+_LEu=^TdiW#D4;kMxWa!O04la_cubhaFW(keLxK5n5dLLSU`Ls>gWqvg*=?J0FkabwVouj*nW3P^$u(8@=%({SFy;)J2*|f%renV1C#~_MC&j@IH9QxFYz|Rw!$YR#?2O`Q>q+ROd*Ie z1outJWS+qqGbz;1v>81vysE+Rl1p!XC4`m|@D0q?WtbQ?@ z>CM+?cTKd*iX9TyIbFhQpmta4ZUN+lbiw;L)=T+-KJ>f4O ziC7z3da=)jG-U0&QrinHyLBR*Wf7Z+H<0+H*oYm2BazEh3rSf$qpQ}u56Fh*-r+l+ zW1ho3I@Gx(!FW62CtbjU~Y&C$2I}P|OWfqDklAq@GAXG#@h}tKHgdx=PP~M6?x0WG%{BJl-YZ{?DNS5WE*!_?D{@{f zG&3%f^9l;7so$h5Rl}^~T-iy;buC|C1MUNT(|kkrQgR4e372~2)iaRiAD_tyz+tNeS!E$GMvu{-_B+#KAi=yXse+DuTNncOXl}lB#E0^mdbJHOgD35`^HmsML(8Xy!IK{kH zGnD!<+5$4{ej9@vN0m+#6<{XJz#L&}5ih;yg<01x>9|aaDgQ!sNj_pwaCZS|5e-(Z zj1HKAyNY-l|H!`a0M6=TJIMu=sQI=faEkAsY&VdZp#n{ z-BWvM=GgIml#NejrwqK}2&|8nS)fITxWk+A#a>nyFl6@(r`qo5cxhlkX6N;NWAUeR zBBgOjF5g)+wqRHK+GfD$iHQ1Yxhrn!H!-&JcifuIB*nS4R*BL za8Dso2}#?~m)aW{Z9(`;9Z66z#NUU)9prE+UlK=Zw*WkOUCH5S0gE2xoJm%>qsS3nu?Wiar&ADhvm%;W;5fc+m1tfbYj-rP(f1_TYrHo{Y@Dk-1#iS zo2n#jom8?8DeZ{t6_bc5vnYScal%r`t-?h09T!*l}Zt=eAR{EkRX->OQYXBqw?V@FdnfcXenF_t86X?U@d;DgiY(REQgH)dSD zt4W6!(TPOfaWFK5h`3&wyxL*Mw6`$J-NM1f)I5yoq~ z2ypVknN%VLWMH4p2pDdW1P&p>(E=)8c5^OIrAKC8-#D@2v~P%3ubH2*ZV1iju6b5P z?CUuWAX163A~(GV!v}BUb|Cvd^&=P0 zz%?MG-H_ei-}UKmSS2RK?GE)BTe%jusk>1XJ_xIC&7Pg`NLb30vh*aPd0D-&H?x;( zcA|#x6gs_5RR9f6G99&9*XsASTUA4o!?M){r2RIMd`;jZ=<0Tp3%&~KTd!Z}8=Q&b z&Ql$<8j8u$Ty&jEYuEM7s*b@%d)+sV{vpN+WmY&tBgO4J*1xm`-0($kMyd?>(eH?t zs$?b-8P!j0cE;LgY4X?fJUtjp^T~`XRo)WcHswuZhrar`zzybueO?+e`Z&Z&2F_ho zJ|`8k_t4K_ATS~F7HroDi>+ZgORwjw6VETcyt9te{sX;rLgi^8Ux$x>p&bo?^U?b2 zo2cir-nD!Up;`fhp&MP``u|E~x#ntjV%oFyCnhYOxG=83{9^5nK8u1{D>c1FIQAwM zJw;(YOSl^O5;>#HFPUWcRF%b%I_}ZMR5z&3Y8YfyG_V*C3fOQDo~sE8sOmu8*it`k zLqf|WUt5b!)47ut!a+nXkUY;92{a{&HrGD?#Gqg{zvgf1=YLiA&{MUQ-oPtX2h-8Q zBjix8iR(j!mKc6N%0O<-pswh6uGFPV7gl;a6SpA?4v?zj{wpspSa}7Vxi>bO@`%H$+vv9r2!uE8`L0wdht=vr>C8^x3J{zQHh^oR>=*712Rg8xoaD zR-8>nnfjf5X(Oh@pc>S*asqz{9GMZ=XJl%?=h-+1t#$|H)V~{QZ_WVz>M#Je9=u{+ ziv1;l8_b7ZuzkS3xz^5cY^)VKsJXp1v6u-W73z$go=H#tBy7A<(@IrmA96$pp{eSs zMt9o@SmnpP^;_~_`F$_%^eTPIV_-2UFCyd{F^_A<43C7hQ9GpfMuzJuurx0p$)}jOVczu<#V@&)7u5{& z1{n!*3v1!IMV}1|BXU8h7|Yi2GQ2m#t_}y{PI)yxUKmib03bVzCQ&|&5;lwYn8CMe zG2>loFLsn%=Gx5Oy{V;SA!kdI63>&HY{iyW6F<$MqRuNl9GohhaMffwf(-5NWjBu7 z;BRGrR`$zN@E1g~gv^5wylD~sNjf`?kxry3mUkeY1XF{qH#h%U9+up*;>{=eTU&RD@Duwg%!E;58k@X7z_isO z$X;PKKol?MqwK?H)p&A|pja7W3Ct&X=rNw39{-)hW(gSJ0!^CpPzV&8o1h#|UZ}yg z!=HB{bh@NqO+7Y5-}Mpi9M%e{{i(vL8JS%VO-f419}x$z&|orM8E{Gh9T%72Y=(o; ze5jK&solV>6;IgXM*z5dnGNRCGC$AS?l8Sm-UVllcRtg-r;)?l&J<5QIMw(Vzp}(= z8X*Px+f1y^t-pN!{Fy#tC$DYvHtydmr-OU>aMV}i-v6yE#{F_|nw_P+!DMpXKGxOe z!B=0wzZ>6ty)OS%pZn|UU#|aUMzOqO-{7xGY5e` zN&YgsD2_+PMK-Nyx9Itw^mA|ZRK}g61rY&wbaKD9p0uGsiyxsaYc;tn z=8CsmCgWlT^~Nxv1M<-%dpF1?Gbpl$X2v`nD@KErlxMaB7)kQq45K)jrC5`1pk@QE zkD_6c&Un!9Ii1ZWoA>TrTwL^09B{9gp4=Nz)AHW4y`A0tgWWYXXnyI{STRpP02z0a zqst_ns6h?Tg>sZ$Bv8@$WSV7oEuuyNak=Uo=_X}yJiADz8Gsq)aJn4LXAaKv5Y^0O z1BErHarO8h**j>#ZS3Ab7l8b-clhJ;SBJ?jk6*rgynnd2dyqVTne05@f3kPD_k3Ud z^)z|B|KG{q_V%B2lT2Yu^^;LPFBoPH*rY{F4zkP{f-VJz5zEI)W;JTWNpYS{$7t2e zre`^(1}oJ7hX7`jpXJ*3KA0DDbnn)!FZd)%9_B>}@y=)4;zwz3yN5P3bKI7_(^f@8 z#iyzt&SyD3y%jyD)Zuqa%84-a_pWTEj`@8xsq+jKnD5oWjH(y%ioa3$k@AZo8iB0)07-%u%%2niw|3@FWkB{Q@XCAiR>YjuRh%N zl(g~F`Di4xw8Lc8%~IjWvZ}2y$4K)C{MJW818L7{m9^!KT}cnqiwHQqfsymnqubsy zBjt^avTp{bINk)FuVS1`tKaHqQ7>p>N9yY)VyaEB2>a$`c3g!-y)Lg?|4S)&uR+s$ z-TLAlq9;eLa*v%`%yL)&aQOu*dKGM5VRvwJ8LP(Fj$sRP$Iz9zR$B-eS;8I50km_M zF&^OJs9{fLQw(kOEDL_dKqXfz!Cf5%sT!SPR2dw5U;`K<;UM>k*dYMBuGK`Zs$^1O zBJlVwlIq>l$IqS}{`m6ws~>&@nt}lyr%EQ6olfU55PtG`-)jy$K()C`|E&qmT?pL6 zuUlR9HCH57;?M6BxAHbkI+l7bOpxh!=i^Qo@$99IZ&$7W(soY^IjoL=RyFbD%1%os zs&J5XRly*g$}$x+5nRtifl+$6=(-_=BHi@4h@e`jbbR?-ZPv}?R~a#7xt@Iqc$Y`^ zKc}P8+Mq3;n}bFx-V}UJJ$mRqGHh-hx#qVm!7x4d?9%q>+P3{(BOP0q;mAYt5%~c)tj1_fJVb^bz}YMf2&`6 z6KsQ^Z5;{2e>WSk#!xJ%*~r3bYD5P*>;9?)#{*Q5t+8n{naq!rZjgY;Xn$6Cn@sa_ zbxwj{w4-EPbk&@a`vRDU=rwp60edsny;{>Azq9EMyvk%o*Wvho5TC)Kixy&~YN%PL zL#%odE@JK=dX2naH4Lv6_vW3m^a3GG;~{QvAA8SD`U;l2{1S(#g*P;K%TuFrMOHr z$Ht$~T=>4_|B|TV`hU^r;Ba`|Vr3gfy2hghZF@LPk7sM^U)`hMzEB5!6qb+z0iN#A z9vPXjp!RW#mwmjpv<5j@|K!*0;Up<{-6goFtSqDj}u8 zwA!npHK_Fzp<3?*E>bXsPNv;&)QHULkiZZj} z^p!_drJyR>FFsva+?JBS+{Om8rKBt?BvI@66or&BKID-0%v(#7AYJy>ksxRKjY)L; zs1H(UY5WV+FqA55a|DNUD&JGDIbdS=Ox!na2d@~c%HLh*1pM9Tx=uS^7zY~FFHp>N zvW=+#4LFIPVOC6#&pINMh@xg$W)m2ut4f>+9} z^yP5th;Zjq0x(n4<7=+HDJSGo#Z?7Cy>3k?FH4@4hV~-lVq3YiVtq?OQh|41=b2Xp zAIE36-G_z};KwTW6n+8H1+BvOt?0s{y16$#F7STZUE95q@&IW_YugpMVG|cj7wXKr zpVzIke7r~cY*q17_(<>IShtkgrBCEkQxh;gg!J8w4`D(e`5x z6d*Cm?bJ%Vk3V?L=%NdS(bsx)&}rHQcIBY{m-8MH9FU%VbZ-*O^etz7Rv!HF&l{3TL6lFp%=RPG!$foVExA!+8=0TCpqi0oOU%DNC z%P=Y3&}ZY$fCDkhyj3M+v6!`B7=~(P&ok(*Z3blJFAGIF%vE@e;i9;1W5$?qQ*KsGT*%OKDrRH4 z5l^zuZakkqwf;Y&`3L#^=uf@>&x0=?e6!*D|7?8oVB^7O|DR9s`RxDm+5hL0`2WDg zwt55-6~T3q&ziC{@USft?lLNIrsa%~JXk(doG@ewmMp zsTu`{n%V5b-Hc?A(?{|}vcD^y3yw$%T|cJNA)?-ofA9C^4?jE{3L zPaSJRoRXH_YvaBW=hxOSSpaz=D5*y}=I_y>uK2?Gi;c&6rGtM3>{V~&`IClW|tRgE0w>#0b*k4Ew$?v zDbaVVoCSZih6VoE0)GimAK~Bp7{5__F3SkPwS`Sh+S0M1@ZhAFF%*0g{vFf5qhgSb z5Xv~`e~TGClE9{l!KC~-L!;~4|FI92Fuk;X= zEaZtfmB-=;7-S2?tkYS~y9T959tx7Gk1k^UK)l>0PJo&rqC6?MN{`vD5Y9kd_*qSx z6yqT~9`hULZMEd8v;XrbS%;bGgd`fF-@^jH*^XOyn5loK%m$MLu1=}N4m_?8NRp^p z)#2K9Cy1TNDyL~=uP0GUCj4O16-a76%Ks-Dit8Ecd!e?@RiaoYrcWq_^HFBP=4yfc zl+Dr*pl3amzzI2L>NU*m8uk)JD*BaS6BMr&;&@lJkKL@}Ys{KH0+KCwjZ`@s36m~j z{zeI>5GlxmB22R6?AXBU*z=Y=kbP|JI3t~YOD3EWX@xuOAw?x}gSI{~{~crL!bYW9xHvej8= zSaiGSChxISIn@SVn*U!l9=1;X6SQYJT@uzF>_;2~C-tHcfci*T!%=<9-YInz@5CX6 zp{5Om%a=tl1IzeR)#`iHaXx@7ejkeXA)CQ09+&>+tbebqRJyJ%mvSBAAmSQB&J>|_ z?)P4i$aT2+YCM6fisk$91-iP4xMNywdlZ{MJv2b@C|Gik*Co@jB}Z~{A!(*S^9K-U z7v1QQL{(!*fj@AC8w+ihA$>eS65A*)`Z612`FTY?tB%KqEnb!S8z3}x87dGf<#&&| zP)UY0u7n*!zSkbU$UiP=&hpsSC&oCDCok++TE|874GZMRE_ z@JGLW8hXN`p^}{bpa;hv=Lm>qU$a93hi#{bq%XQI7G6p51OrFG)aTJ-RRO)G?A~_Z z3dr6Pw$z>(=2J*QT`>@gw&l(nR;`bIvyP+fpem_O825`(O!xJLuSiI^(ur%@jg^E& zT;W~QEgT%AvAx_-yJ)N}fD5U+iPbNZNBib>@4wHs{QwPep?n%!;%b@j<5C6&0CTyg zG23<|oxFnQH%umfFe*XIVnfov77CMx&!0SRPcJ%~bbowNOy8ovj&`g8ET0s`Fu6!C z(PwG4sb58*K8L$D2!rmE#rKh6Uz%fq6uT)=e>K*LDIatpgDDqg`WNS!vR90NTQDotd<dR1MQ7;Xf3j)8y=aHsX;#ihvpS}=UyN^rZ&k9cMz4NmZ`WdP>CLe>Z}8XA3R>w6 zu*Rje23xFfL2{+N{T~8ey1-PmvT{8OM0iJbwTMfQ&mDT}K^+ySWUJj0%KStL%kydG z`rC1#mdDeRkLcOxF7Ps*5<9vajKct`U-Ea4ED58LWKv%%uBT(x3|}mf>!pb)HwA?o z6r27D3( z`&>o`JEgKb)bml6kIQ_RnZJ97CL8|SRq0KCbl#HLMm9TF@e^dZEZ^xz!jq> zMnZ`;J0=>n2A+2&MKTaJI40!|b8I+Wvf$V;hwT@fetu=PE5r7rS7Jl{rkct%xG|4K zeYn)}6DDF=wGJa8GA2H-m8eIV((kDXEWw&USh_G(I;8Y{F}(bg4CRBJQgxl2O(m;L ze20GLFc<9S3}UI(Vs`klsagUMFJTl8)G>86qed)ox*SiOUV|yp_UFaz)b64f_R?X1 zJRgB>4=xs#Vay@H{dss-sM=PEzz$(@wIy{Pab;^ewpj5eXIc&UnZF($U-Q@%qWvT9 zOt%oWj=A|@HyfyLhF+2g&%JR`j~ZQ=-Wa1^M$p9J`4KI)Dy{!SkM z$|z#ls%S*>8253E-5iXHS~3#;5@ys8!&eN8%_L_jSO)?Q(h?)wpUu;*+UK(*Jxlfy zjIIWMhXSh3sc+wDD9IA_y&&0m`?f>X(#lBCbjnNC-DwI?2B7VH$*~oznMMIyoCZR$F-B4>aqT%Eg;HQAfJ05IIDR@n6~;6Lz!d@ z=4F5WcE;Vn?bZyk$qA$neGVaJ+EncXUCQHL$qls2pO62jNB(ZWlWPuqk6dgpQhn>H+FVA~CYhB*-|vW=7s92jVG&HBeg%i6P+Tga7Ubj&ZWk#* zao6@npl(F~>Ixrq!hq1gq9r865-q}noU7bjund;`e06qt(qC{>9R$# zXX9+vw9EQ#{5mM`S^D<2lJN%C?Wzj3T&cwM{i^NI2R+oa zA0mZJ*CVFQ3Mbi$RiLG}tKfveEk=yArP^Z(ou0&%M;YbfEQzipR3b&NaokR9z4(xO zB;QU@ai7}Uw)fGmJNWRZt3i$cSZ;a*{jW6}Y^cjemRBp`0sVgnO(ijfVu>R}UyO%R zVC4mQK$9#}n4oYfl3s?w!v~O?cN%Hq{ZMNM_J&`}3Ziv$fk7mXsUzr`ya#o!q0(M` zrh1YZL^i;kKTBN3$Agdz8Gn+~v`mh&Y|Keo)c#LP3k*eCokdpPdBwx>D;L|4h%YOH zCl0SPjYW}8Del=YOOM)WYdUU2LoxGV7vAJh=QS!Y8v8=WZBMg#$-|C>WY>)NPs46P zsk+Ij5ZezSCE=}`kod)sVN@G`7F11B)7pK`Z-~AiK<1kZ94S|zA!QSk88*{ zJNIZH`rWj2bSzfl>H_3n3#8TkT>$`Af-=}3hh+N(JuWr^|^g>WDJ;SJAZHIrEjWCR& zWp{@+Ups;-ipGTQ>G)*r7?=)T4zW2Z%>D@P0nVMok6#S2V6CGx9X@IA?!Wq}3)Rkm z^t#DwZ@xS9^Iz$oALyTb`sW4x)9dv*;=M2EXY;r0?}y!eB=Li`I|_~EFChQyu{T7b zu&p>2#*!55#NG9OQ-}5!MLzy`ti&yQ++eHAT^8T#6=g(4;ZeswU8lFJWLIXTL~+<+ zesb!u7Bi#DsG+MGEW?zlFzjz+AJgBtJA{7R^w-Z=@FPQ*S=aUjg9y+!49h$#cu<|; z^8cEv!(Z_LI{ghAqbm>}nTa}T!srAhqb|Oc&|TmDW?c!4Rd?pdktDEk5=u7XldIsV zRyJ2k>mC^DnOcs$zPcGW+}22?PM<+zD2(a7;CZ|GT4Lze(-eLwp_ZRnvjTCA01lvf9u!>_u$koNTT~Sw(0yF^( z3NaYPQY*pU(M=o!&TS|7$ZvYaOSJH(Z)5ZfhE5h zynzPh;VohUzN4E};7+#HO}o10-7?5C9`;9cpVU()F^(x-k{GV*B_m zr?#awxJcl~QT7>J`een~M)f17IijBA4|1gMt55ETXV zX6*rkwR7ek##?aUip+8^V#vhLD;)t?Y5_nSNipY0Wj#OYfV|M3%~Oi|H7XviiXp0K zUfO7`cFZk|G#4ylxX=zCHE){2jvbdSgS9r8?ho@RZsFN!Uw9eZF%1A^XjljZ2&jos zjNufFng=WhpMi=e(VdS$+kQ(^``aD6TIRPe)aVbgNs6iJ_1+Ge3EqJr85SF`H2ukC zAJW<*r9JZBPFnXML8@xj?@caS0*)#7Gn73OG=wddEXC_HCPodkgAz1qH0sNW8LLibZ^Iy{^TD zx>UcLn@(AY8>!FrDGGE|)_C_lP49F6XkMMsxtBBQFbN&c+#c_g*@Ur9F35E-f!^LqMA9QYF&}yiW<$~W1Pw8&8OgK(`O1l7EVyP5t zd0NazL&bE#tc@$?lIf|!YCd>%`Cw8cH543ldj2vn6(zokAg99 z7H}NFXdjO+(M*Dh0o*Jlr=W(au^y?@X9^X8hlCf!tD$MB?k!#pElQX z(-aTx6!LhfCBja6w=|*c?); z+u!~Bp<<#u%l+)#OpU@${_ih#cMjXL;;m`2`)&d*;qX)E7E>7M_&+P%W~dhM)#1}` zV}_ton{FgS0u>#e{JM@eL?N##sX!j2s!`31kpLQF?Xjd?CJ(WdG%J9I4vyna*-?+z zMa6Wog0ZEz-RoMwHv4H@>Sejr>qOp%Ns2I(vt?WAx{1O1N_ z(&viGr*P4u{d%T6sdNCDl+^&~C-B`-{lZ4Ic(T``5gg{@E+HEM(2QjRv}T|!g9F&$ zv{>I#|2$N~wb)DYyLTNkOb+2r1XSB7$=YQZ1WaICEj-SyosZR$=^HV0Z9&V&_}Bam zaF&(+3P8-&vTeA1H-~@sZtnCTU&M~K+%+s4Eug=CB{=+?A@atMPGNvc>95FvZdJ21 zH@6&w#@mR-=J>h}w$Oh21oRSe6MIq&C@hzJrCsa6onbME2O|(Z=&tl3UAaFbM~&_6 zWL3R}H%=)Pw1RoUES;Z$Xjlxeu0o8wIeCBr{318(!Mbw|AoR+f&%3>iH(H-A#Hzmno(>wZ?A*eTt{)1%+Z&L*=<;OF$W zF;t<9H!4oRyDPaMQ!`~sz=q(1Xx~6v$5E!5kTGG{Hx!e6mpncyi;+@vz<}5eo5J-h zndD+JA&52kF^Dz&U-tIDyiYY34}RW(u&%d6J7yr(qSp4~t00D}uN_gmXFKZDE~YPv z5^cxrj)+tI0vQJMc`_J?F3J6`a@GzyH`!)pe~J%mbMe4vNaJCC92de9r(cApRz0K= zeRohvC5fh^7O%Yl)`1i0mLsfasQ6bcs~=8B8g*T?(9%@FzIzv*TjB9|(F<#9Vyy>R#mo=41LuUpVh`RwPe+VkXTe&( z`g=z@tZi;8EkGR{gFf0X%7=&Bo10+bQ$hr9_bqolmx83oM1y(L(notbD`uOCo|>^K zK!E0~$nUBD9B)$6zv;|m#YPY5F(#uyE0}71*o)#Px*?#?wj;E518ewUvZ`=M$isCT zLU{9|geH4fsF{O1r+O2bZJ85roEe$l>^uE*9nM4qE9r>s%Dk8I@i~ay5CW^ zEtHh)B_)-lQNJTxBN==sv`DTNb4>8%095K278#{WKF)2HU!UmIa`UNc)}(qRw0HrW zcYAxmx>SzQ`$(8#CEM^rhhPEr;7RhxY2608YjgGDa3!y9IijSwDcanOyD(K;pf{cf zeOTI5JE(6jI%OqM$TF2qN0(3>Vg|%^k+C+9>Uzq0hqIE%`v@TDA@Z}WZY!LE?fi+cJ)uAp#j4(BW3UCSCYPe)mSK4MiMK)Ou@?5-vccbmBchXB%-mfV9VOc+L{w~yV-~akSFKRlBS=-Q5 z_WWjACuCj&16*<31vpf#9K#u)Ft~Y68-0h_0yO=mt35Lfx3JBDKb&M!+5X!u z6f?)4BQHE173oYi|4%5B4nM}A8y!%!e6S9H%`dIaEiD=Mf7pMu)9>Hv_p^61g>g7< z`*d+WD<;VgJ3Dl>lICdFSGWXci)l($ZE)^S>B4307f6nxhL8_>w^-1`=!;5=Cg8Wd z|M;ifZu0ZvXRp*hIxpwjLs0izImYH15^eV?uXYBUO-OG4XSDC!8^eI16oK9^V;8#we zX1j$6k-fkZfn8V19#~b!!}dluW*^Wt0ju`gU?e^+RJYIZmWx)bC)@KPLPVz;B^wd0 zqPA~2HZ4>;WV1Kagkw)4-3?X=$JTatDlIpi@qJ!j>9Z~k&N`ZRVrQi~%M`OyXWBSF zIvQnIvVrPqo@VcoVSWPfy{i8LQeUEg(}n9iw)aL=0{r*K?46R{aO4cRo6OZ;557{z zm6a7Dkg z39hw@`E-z>PoU)=@_qQVuZE_co#*MOkG!@|G_D{Rk67HWTU%P%qxqqmsYF;pOcGuk zFiYlum4O$dvhsR;#RhvY5YpZ0V*n8=R8p0F79&s#IWd5_r>XzLY-Q6;inHyl=5_#wztzqqwFLd$*ZPu63EzFRYQTD`TF&WpZ#RfX5kC91g~w?DNM&vnf-H~ zjR%D@be@>bJ1(x=Tq|~y z+x78>M3f}=m&7THHJ~@DJ-8LLsX!KdylgD8w^@VUW;fQ`v_Wsv8|&?;L2pMl*4uG| z-i~jqH^G3ayKCKCcPkCLTe-RJUN`9O^^J9RZ*gz;ZmhF+a8Mxo+b?F?=jX2n&-a0) zE46<*T&5$VSty|`e9BH3QZKpRP40ja(g|C;Qx0`QZHjI%)vM^#=PR25)4SQ(eN{a> z%;=@~Ih=~`o_xE$4#x!i@DP9a{^`?@BkT9-PMi2!iw_w3)EqNs^U*AyDB-|8X3F54 zp-(htm_cNfEFxD~A-igk!xc3hx_I63@%r={czyIy@oJ85NSIQ6dN;r-NO=@N1`*Am zq`%t$AN!ii4(HRZLckpbY5hyU+{1^-#(IVB9RUb zR+bH$R}#w~YJ)YE&_x+-UV)rd{A^l zy~eqO6zd0XIyF%?2b2zoi0nxIaBAt_&sp=-6Hs?CIOmh~wwfS-6GMcrV(M{V2n5 zn*ScUG3;{1=S`}qMWwNN zHEVer0fS9b8|XL4W7DUWn2}|y+Ekd@%yL&@UrRt9qF(2&ne$sgPAu9w=pb{NmVK~up>I5*h?uu&cxK>g>xPZYvU!XS-I7NTSAo8tbP9vp(pb!4a{-spi?)_*MEFe2U|`^i zqtlI&9b%kJgw3397O{W3A!TF3h7DT2i*n>-Gq~Q9V5-=(i8#lBkz>`YKRO>J?)l9` z(^~=8*mc#}#uoENZ2jobx>8@Z%@rXyXQ{?$*me&;4IYlU7{-Eu67`SgXWvQUVm~Cv z*{RM-xKm4q*&shlM{DSnpfIyG%4gYBopxoo@A+8qCjcwEC>(NJI(gQ%Hesi-5hQ6&d?||2T1aB@&^g7jNCuKKK*uW>q-niEZ zd+WMPE^-y4iN<~hyL9$$jh#VKaB{+Y3`26xP-NhvWL_$v4E@XnH86#(ujnb_y;dJK zsFV}`z7iPN8C=JowUrr&!L7er!H|&CjB7D=lk#moQHQBJo0piWahzyx7`n`cyX-UY zF)33#A7Hv`q{mVNd72H2$QfnE=?nPGU+kUqA`Vii9$H9^0XZ5CzwaKWJzA2s!6*H> zwx(AP?@c=1X}|l99K9AJ3vknRU=cm=5VoCl4M<%_1nTa}`7zX0Bew+26ler`B{4K^ zJcNt}m%XZGLJnmZK*fd%UY2iH((sFw4)B`L;#T#-?n*H2#4>7dbwG@t&CzLhSYYrX zmIoQJ+f@Auz^Brx7>HMQQ7m(QTB{PO;Zw(qW@72qCcsVMjtj+?b7oVe2Tsx!grxHd zySw#+_eEshy~wSY#2XH6?-&_uN~-{=;~=q(niEb=QKjnx?5~1c)-^t3pK7TO@WRKU z|K3&grgpdGQ!I`7KiSP~z@@|)A<*a!wLIhbBpQLnaXoZlRZBG$95U{?q1~|qXKZ&K z6?&5W=49oH_w9FXA;=P3es^eFl4`Uu{OeYbyHAS4l zE~HxIL+z?Y;j`ZODLNXaK%VnT&sLk#dZ&?{YnHIDZ-uiON9GFGOmZ*0ttabcnAG|t zzGh;Pd>^}*rMg9ReU%h)0?`=3YkR{s_h>w*Db9H7)-t|b_^b0D|?W)nwfItF^(37wQM@ai5VCEg2tvs%QGFDBjK| zBImjrSvoXXE)KGEiYWsWvxTexa9Y|lW4DBhp><@BT?#3d0WaJ9RDGEe&8^xAJKF-= z#^c=nu(qc0OC{choLs%^_h?quoD}Oc9tDwO`hMfx-?6J<6UnQtJS21YD5BWrF7=Rn zAPZ^)XtukYbfcjugJ9>Q+HLXXD%C6HaBW*j>lldTIM9{D1*8q3FvM*9T)0kxw_!kb zbwnrcQ03ma131+Iw&Vnd>tz#r3L+F!G#T}ImAUSc{&{U1BdkZ4U>k;KVyR3MCDzO_ zk3`~EXz?>4zy|;p51b9ehI?arI@i-9%R!zE6sDKM9?pA9}m}F<$MAeOEH@f6`;;_G9t{m zH=v?VkmP}q4&}2IQD-Vk%N(O_AXMdTl1`NjG1U3nIh#BG2zH2kJXWo3S!DRQvOHZ| zaT6+Ghl&G_Q$1=!4kmO`LuUZW_|g(+_Ybg@O<`4#ICmP}eiT=dr7rFcl_DwWChJ`` zT;spf=`_7GVJyPPiOKKnEtghmi52%}1f|bVKt|;TsN7L%s300!1_$W4lsgiTn{1n) zsJ-X(^)T6}oTZ~WHJdUSy3Z-mHUv#8>S(!e%$#9?LzM8wMDn(~6OtYNcUNx`AuHM6 zO#byy<4dAgsoCT4K~SxYW}|RulAdtlvqz?_VMLBHw(^E-@XnCpMh6^bc{!e??^^Z3 zVN?oyJfz}fK>M z;p~pn)49&Lv+Wj3Gj-iqLloVJaq4RrBqYZ}7CYyn zdV~ZTv{gpKQsbt0MuQ^eqAhkzS|FyXHU?n2YR(7DO)(Y{^R8WFrXlvBzGz64$ZvX* z0cl{l6?`IQe1^PlctA8R8z#Fa*_xcoEPA%J|c=P;WxNy0Lr?@!T>aEkqcU7lCI$ZZP(V( zW}(G^x#lKuO=LTIEBd0Ek z-o>L&1JhD57k8yw)Si^}&0#E0U@pFaX-t8DvBd$t+z!&3TG6FfY2WJ7k<5Hch#8Bp zL$Z(WCh18wb?AH!q&}ETy2)>pGHzDtOuBj|8a%ctC>H zl5C68{Gs;}cG;LBm!e56`Y|E#v~-`1NJ^uEDNBQM2uy*fAuVKv*kPZ5MTA-2Azhbn zcn8-8xRy?tSBwKc5BrUYmHyP+i2Ir3< zdANR|)2K8@MQJq)$nSwy&`EEd(>8O#Rq!f+9i$Y#Hah))j|Ob=a*)$$Js=;660g?L zsU$D8GYN>A+`FZI@F>H=I_5OOn`$3(&h^cmS@sU-Gm0%*hDe?Zb5{&+o7|T|%!WHC ze8y@vJ<=||1t{dphQ5qLf(?}sfsoswhH_cB8shX5*oR1a7PIC0@ z>|mkWcSmtUm5%I+Y&ji8! zKH6&f#c{jU%XTL(TGGQbPThFJx$cyP5~sL-v%`)`-WdjB zDH6Gob%?T=+jLnXDqNEkKmn@nit6^-{mv7)!7P`D9l%^%ZKwU``%wDy z)w5@iW@%iGkj}wCvfk=d5vN)SG9lHm4DZuK%(>>X7sR9U(B|S@>&0Ql%ABWYW#Xoo~oVo{Usel(aAX`K(oDhRMz{&RUC#sHG|q( zu}9;>Y#}oz$wlkDnB6p|b3%J-A#N&@F?y3VinAyb$D+ML!F z{lW+YwOS|BY=HaHFtf-?*=8-wT_dy%9UO4lXN9{O3bbmOn`-lpUpBbW9{J9Z#1|Oo zBA?B~XD@mO_{Dj%-mE6CB#DeeIC?p;Bv6Wfm-tifJ4MiX(N+p+XqzsPMkF zmFU6V0AxU$zoVia0~E0o0JXjZM2&p~N*WATp!50Iv5h+|7tKsJY)Ms2KJY5vQvNr% zkf4**ti5`$w#{AY$4yR4m%dR{jd^;qr%F&l_z!l#kpLYx)}K|DCPs!*!eih5y1Q)39r$x&8o zHwaWHv3N4gSwz&n9XdZ$$%8mtJrOn>vr=RX2F`7+CR-jwkA1E9YXMKnuADAo2>bXVUvV67@aAeqKd14?d znJyXp?d^gvu@6mcWS*mDElG zoAhl)f4e;NNN#=lg}8pR?Nn)&3;MOUbJmAM?=&o*IV}p3UplR7CHi*6Zm+nlW2(O$ z_%D9gXj8T&LRrj9yf&65Z}P7+W-qk;k)KgIM~77#DB#ReSMlUEv|VdR7imdn8&LqL z?iZ)olyz`TdcnY>{Ny@|?=;zHLgCi-k;9MGoXfs_R`vsi|M6B- z4EH+gXYgU|@lrr^&Z!zc?K-4kH9a(Ox_3bEKDQF^Lit4h;@!LK9qW3saBx}p+LsNj zpee*E&e0qf#X0jd*ZWi*r;08DBtTpM4oRdWf&*<1F~ zerx4AK{Y%KoDbz!6EP3<;RbG0KX$I(?^R6nXr7OT*|gs?jDm`Y^or{Y8!EM@BEd7| zp@C{Rh1!rDqlVR_ItD@AYa!_Im~DaX^+~`H}0TW>?3_{$; zH34Hwr(u8!R}aj`Wj@UKzK}g#*jNUO8MkElCJ8WuTa!eUvDgr~6MX1JOLE~j3>oOp zpaKj2&@YZDaHLP#yCWLGhvG#c$;Wh>m6KvT#3bdGwOkNqCf35PL{wEw8<{a+ib7%3 zIiUH9LUTHJ+wV;=Q$QPv-XQ+AB+l`L+tqJ4i*6t!ckjZB(r1*f-iVXHLnW-^g|_7V zw(H;q2P`{g-TZ4E`%Yxl-3HfiUr1^_7_i!I5R+`LA(+47#Vks50> z5$f=stmpIeZn8>V+II*}m{GEd=B61|3%)bOG} zct|3$5jrS#lC=md9oPzv+Le3K6fau0e$9 zTB_)qT=szpfUFBSUyS<)^NGuB)=Li3A&4i*sKBcgOyLS;=(A}lK^61aWIp4P%jn}W zIZcl;6mKZAem6PGQmhG+V<4s?qx2Xn_b3XKplEj|2k0+af=V`lFjMOmfQmE9T3z@R z(>-MgB+W{!ly0YJ{Sv5{)39Ix&Ro(;ubrcev7CYG+sdAu$i%A#vZXJ)gm{wM3MW6!ptp@oqO+ z7ye;h4jREP^}A7K34~Y7I0A@V%wa#Whj|8Jy~Tw(r7jeskO{;V5{v4XC@VzhLS7p^ zEUTk^&hoPKL^I$?$edVZ1tf0Di*jd!%6K1j=#m3TK71$bvkS+hUs4l~wxwFYD5lwt zeY%taN41clyXpipuwmCkX@TIjw*4}Lx|SJpD-DXh;pB{OYdF5~+=*|=5!uzaHNo1b zvRZspi3k*U-3f6*ZsNdH+N)83(wXp%r1v^q#tB+qFk+YIzq5u%vz5RZ#ZzLmS08X` zlC(->&6MT?O)F3}su(*-X%2MBY_@(ap0S^a+bYLUnTDcpN;gSN6V=*?juFNve3O@4 zVXVLwUtu1Ala@W$CRJ{N!F~{O;8je|Bx=))EkbE#165O3NJFC6u)jTd*S=`xi^fli zlZEFfG+U8QNWHL%IQuQ*GNHtWW-flc{^fikkg5nyM!h>=2h`D*h z9$0sXdS?h@TozYbmCi!00$Y=)0nNkS(;9(BA_sgj+9(MKf9vQbsd9;?>3o++*XU%^ zN)iN|qKX#oC(p6sCi^sfRfbJE_0fHn>D#y>v6H@u(g&frU^fXl?V|3Vj#5_1zb7ck z9R|0+w04bgN7$jY*-ElRC!yF<}N z@Hi3{4sJLk#Bfw8fF_RDHEraHRZ*h7F6#=g{U;e#3Rhy1E*yMk3FB(TIk(V$$!EUJr6q!mY6l>?`Sgqk;e0>%HNdWhIJO zCk(V4+~Oj0YGSacU?Y13<d>hAiJd#`}sI3i267=<}-#!+Y;db^0J9Ai- zR+`?gm`Db%G9Eg=lQ|wtylObVDUB0z)bioPW3zoJ_9nY>82=cuQbp8# z5vW|Lnaqj;F0d|@8Upc_-8D6xto-ul6DeW5(>ZtZVwwpThTfuCk<>T1>_Dg zkvs#W0pnt%#~BCP@8Qd&)Y@blfoMoPpg3-`m|7u@R(<$o&a=@c7t4O+)(`ilVuD}{ zb)6Mh4;LdN{Iw%jhqVefJ3H8qu*y_M=9Te*Rdnqo z(+Wl+lWQ5Xc#~mtxjwoxlwfSmUy2BSOs7MLZ-qc}bxwj&e60lMeiW|JA8rj(Hud~B z_m1`H3?@Gtj;^I}zH39qF)E4)WFPU;d@CsVnL6fkMJOXLN&7-?0(W90N-y*#Amqi| z2V}31CIwOz=+j1e6Zj}btDyG+_wUySisvl>XY4a#vFJ@Ys4?JIcd9u&C!%?nM1Jio zx-QoBH`NFbk(f!oq|D04G{nr(Ny2sVW~cKq!BeH(@qt+g@G5$k43w-gDQ50!JL^R~ zpd$iT9rM>Q8_9p zi50SlI!5g`6JISean2w>RSZyTH@a+2Fuw1sjDSJ?Y)Fim;ucCPn;tAu@HmP9D3o;| zPGTndfJ5%+5`X`$QqZ9iNIDl6e(;TZk093p5dq#TLo)GVesY?y3m2-aQ@Vi!Aq)x& zUSunZOW=6%7FRJu461|c;|p~RmpyM|g}W+p_C>Z)aNhCuQyVMTRiqsD$x-G}0$U0_ zj<#eYz31W^?sgojPV6}0|4{;B>GR{Z=(J8J(e9Kr&HbCn0c5~gqYHkJNX64H$Uon) zA|F0!@9w|)sSBB^;aa4d?9e~GUayn9cIeN$KuNy%B7t`|y=Fp4e~}KgY&uDYuJneox9fEA6ai(&KQB*riJ++ha?se6N39tx-Xy50edJLG zpn?P-F<4|Bt3$%$;ZWm1j!>K9#RQ}H35X*U!6ZTcu)B}sbFUrpe!X+Pyp2y+HNm)6cQEUswu9Rgn_kb9ebte@(K(om+oqbl!4 zmb@k=^PRYuuvZ;-I2J};@#;G&zB_Gxze(8xuY#u#x)6DH|EYFZFuxpilkZc?_L>}9LDdMf`lsui(H;l>;tM*Lc?wY3 zl8pZk@x|?S+EJ_~{P`$C$P49<>hmG)0(Q$V%)Rk2NAnNcs3x>Cv5FzxTk$U@brs}Z z6T_a{e9Ecyo~dqhWEwGMRx6mu0blnKrd;m7_#NGib`jFO8DohqympW&^)CbW2iO=F zr`B~d%=s^=M~L^@S88vn`Q9W1k~w7sA}?UnInHVuYlAGf`cxz98n~R(zq3VFQrASt zJIi^&N-LIzg_O=QrzoBqqaWEfIj1Sm&60LLGQWvya!rUN@ArfmZgE&~o$YHG8N)LJ zG%^7=3cVhp;@W}E9`!K~Rou98PMSF^SfCI~y*%cGDFRC1(l6AgqXsW*>o#>2Z%-|Z zN>LSxcAb)4p;}%=IF?Wj6OYtGCD3XUQUfvP42A35S{&O-p#&hR2f-pH36si>TwbM_j|NbyRvF&7|a z?2pGoj?d)r?r`{i*{Py+3XH|G3Y#jR>K_g1WK(vl-O|B?J(M zg`5Oy^KqFTXQtnSf;?)9i!SsJ;Dm?<71PgcKGy(k5W_P}}jcINb=QUrIHlV)Km7|g!f(dBjiWFxXHTv~U19cqb zQB>ojj(k$LS6XLceC@^eVItuPc; zqQVhN?1z|y+K}z^*Zhr|iTZDiFVIz7dD3K5nZAKen1^V`+V+z;EJ0D=i3DNZYI_$k zR@Va-I4o>>BW6+@3kylRO!y-(B`P9_EO5LA!oW}oUJz-ZOAAWg1}xL?H>?SAIu`{7 zX*l(K&fOC(R=k}tv^!%M?2K-*5v6ShVzSydBC;JhG zKR+f=&yU;Dq$4#GG?0!OL5lG&wb>hSE01*4d)_ZG&UJ_hJJ93Ln)0P^0FS2G2-75T z?gq8LF5!0{7G5Tx!{AwVR!lERxDW;DhB<8A0a(pu7o2dUG-O)3mrlkqmB2qBkR2s! zNeYJzyLUL#^AH)4$`A}xYL6za5c@r{U*6m@Dt8AZY zUh@cuqsAa$>qJ4=V*}`6)!i}XkhpcN;0p?O<(NK<2NLDsxCk3Q-A+)!fzHC{LOxWi zO~J|ggkZynUu&GyrA6 zl6!77Hxv9_Q;k^0!`1}8UM*utqbc#~-DVS%pReI*SZPjcLQ2IT-_xE>G3#oOgjeHU zm1qY{{57Hu$~9TlzLixbm?q|%4{_G`AS;b;XsL(|?j}vmHx=F$)%KeYHQ&fs-G;`S zk7T+rH$aioWZfbAsF4RPIK-F?4%}<-W|!a%?!blV)^fs5^+jkkR}VraY|!@gl0E8= zb4Rh%n~SqP+N&nils?_5J<`8BcO{h(WHKtSoFCD*b{zDY(mgbE&aA3wm121)VXbii#%!+W zZB0>1L>3&v)TalPo)VQIJM@MFqz9Rp1=9K=8=C)q>~%=mg4vDfORNbvC8!4QPwa6l zRdVo~MHga%g4zwzDg6zgOk{xO4Xa>b$hPbgVWA#Zyv5A9ay-~U+rYM*(ww*YxE59kQixoIu@RSdyGA)G?&@psaIm0LJKc zbZNO6SY1skBf{J+-olpD(WJWErLnUOw#=Bh$(l ze$VAdXWXkx6JY9**@OWiexHNz<1x%tr7CPnbL?4X(I<+8!JA#3N+%P3Y18bV&B0)mcqOS0P6APG(pg^OXg3qeKS2DzH*da$RojPTIKTR1c>`4qyA;qyaap3V8s!@gREx?GQ&URj z@Es9ZqJD#(6Uvycvq?ruIm$atPKBy#x$26##W7UiO%(^p&y=c+m0{P=dZ%N`AVdy~ z^XgQ)v^R$E)>=*lM{R~rO2K=S4;A|aQ$Ab+*myMPOqQM%^<;^6W8GepspYn&T6Q|6 zs=hVRNYdB@qM~2X*J%L~Tj>{^UEjF>ilmVXwNK)vecfKokS;Tn?4 zuA0r$X>rCzET?d{J~e+W-2c;k+$GElj%F4JR`IT z6b!+<>hOAxG*^<9ECo+mw5zsyp_|-BVBhg@)^G}& z;S3lc&D4EI?ZDU@a@0Uv>X=qi%DM0FQdw?V9ZTn7RbTcaggMpR>kbRR4>a;|+FNn> zQyhcEtV*D?MTkpsZ^i@y$v@|cf-ptI(IpmJf@U!CmM0?eJV2+_1LVb6?_$3Zj%%wp za8V_|W?O=CrgYmPGM>u^p7YTcgW=>KjtmrVdsh^v!Bq+tNCI%ogEYkRy1~iQSu8Yc zzJIB%&gu5Be6KPO((?Zfy=#SoAUA7jow%Jz&97pRByZZ}(sos_>JZz|RJllBCpK-BL6QJ!O z7n*D%ytZIn#;w%3yJpLPY^q{emAT2SX%EFlUt^-D-R?o%(}jZb!7G24&4hKsMw8ieY_ z?JA(*aVtbrP?8vATeN$c2-b9phH&r!rPCFG=qc?D`O1jX?qa2qwF%W68BLTP8d#q? zApeZvo)2$Lc(zzWh7ygGy;#84cEUwQIljxldznFZ;!r$HOon7CoKl=;$tX)P{6MM3 z1v;s5ra+eVW14RDI?^pK#Ib#$z;K;Q=YsUd#|QoW-Cw{v{P6h`QeCT`D^+&Aa|@lF zr)T*nm1x;d_tEc{g%c$<=Gkmb{i?={y5Y1DN{h_E66+g}4<`Z18vGl9z z91__;%CBQ)D4VzZ>(e0hyP|+qeVYijiS~+!3z!wdVl&x&H=CvdwE?HaAfwtQYEwkI z!wh1Sfsucap#qenh96Xz`dQgsXy$Pkgah+#(%LlTVAJWz9B-Gj>8|UT?&8*QVUx35 z{K@lI-#^>+4RrjewCP0*fEDk5pq=l^Gn!a)cUvoo@pwHaQqOS-BGbP12HLXWcp7p? zLq_M@(O?dok(}~*uR#s@ywn$u{NuZB0&y7Yy>(|?RMf7}Hpr`JIVj2&_$rqRTdKuTE!BS)woIw} zv`POd4IphYf|Q1kI%DAAkoLuS(b)taVM^9GFSFzMD9)>FE{lk>km)Z@Rby-KQ45W! zz0B%Ya4F|L;T4+*VmuqK6kKYn7sgyHx7*y0N&}5bicBeQ7^0B_0 z38QDKHnHBqdr$ksGx`)R+trtC`VwuL{j&qZdu@*tvx(bnzG1@qV!UTKoI*tqfCN_a z*ODo@7k4*yvP>1~rZX+gFeUS-%4f+Czk`zR4>sm4X0W5@Kc5vm04{28f9%u6S zU}ta7I8H#%!F8#Sm3ZoWN7|gHxvb0zl!le*MSP4>HF#I0C7#dsaNG5g@9ocf`Zg1Z z4YPpsB5S%=!aHWQ{F3XiRC8Cn#Vq{rYfIXGIsDeDF< z5$s@)fv+4^Zi#MYAYy+H8weHqiZ^lBpMO{Eo$sGNx5?SP%V(3D*H>MLM(zoZ8*4i^ z_swusfb~N$x~v$FE^!qTeFF_g2wky6S4^waVv9lkeP<$a+i|*n91dnAXs^bMMp55- z?*fv}IPi?N2W^G-P3TNvut)BSDJjeM^sM=Yo}!@)AM#PqvC5Uo)16aqi4K#>k1;t^ipp#$4e!^rsJ!!)Y|EDhJ2s~eCj4p zn~71<)@!RBW;&H9VPJ9~-o}fq>Xpe6CLrqH>{RtHR=CvC;%!IUkw&kx@`PrpzChlu z)(%;4DN2GT?U-M6L6K*$iEr#Ue9c*}^kmhB#o(i0&f%`xn9%vNnhZb#)B73t~ex504(UP9dj zZEGZ!unksI3SZ%?J#?1MQp{mf9NQu=4~I zMyS4^E17T4pE57mPw87F@Xe=~!xh9BtlmL0PRxucS%TWm)9fUNOcpR;a6wCKp%(?r z&IaDp!P0VIe4%7<8O86G^Jdxa{iMV^ww<`k6N@ml{b`2Wa5;j-s9uN2C_e3qd?<1k zU~uQkAU8WGS=WGl0U@joJweNgon#wul?tnb%9laY62laa*48Zg zsuxUfjOF%oSB?T=kzXNcDr1)M1C*vyWb;82^HWj*dw?!`w^*4lhhvYD1WSla@tBHyO?|ok^Mw zm3#{KLes>gapBx8B*_4=VQg5bGxR}D)8bg?j1q&|Ly3gvtc`9lUPpWGC{t(8$s7`e zT$~bY3+osPm)MuIJkyJ<*{5D9_swFOpTJ=IqhgTaGATVYb9Mxfqm+M-^AUuKf^Cp% z?BJa!h{@8150~csj=v!yJVl-Np5rTgQLDYV`BSdrSXpcqz8gxW;Vf-uli6h(To&0^ zHrccfmC!t%kyKIHN&4;sJJERFu>_&_6{33V_|k0H@8s1e0Lkwh2YF-{fwT9x-F>lEg( zd0*{}i)=Jn)7wKGi92^eE#izy?XzMy9~H^{|7VP1xmjkB_Qd!{WgC#Wq~h6q!lR2T zSSF3IwzrPw8(rUV`uu)mDff&ItXjli17UtO1~$&$V6^?e|%6ktiow8=ByKQ0VNp4c2vh z9mTm2*g|xJVZn5;822Lcq@*RVOzD{mhYLtY;d1G1epq%w z@~ zq4W5uL6v`k*$2@izI;1s>ewLTvyALF468Nb?PMJO`#Zrh$92pQi<_>u^JE=RO`iV+v5PN zY5{P6Bp>JZc3dW$e0N)y1mXeUukX<~4*IR;`()$3+Oi;GFFy4A+_wd!c>s!};@D6@ z*pjey8aY6PK@?dR;ZMpc8aoBE^FI6K1;$fd{0AoFxDxW(>$tC;uT@Z6$iHA*?FG>T zIJOG#NZUM!E`$CqSyuv82%hv3y$PLr2>@!msjh>7u31Rq@VpQk&J67Zbln8cxsXwO z;Icn7neRU7@4kHb{AKbO{S)eEvS1y>z}X)EGdQw9`V~qh0Sy?rd{zS+v6GCs88YlP zy-V_<8_oO3Fo}*^JJb2p1GrL%v!IDAAxG&7;%PE#sq<% zNjid@+?Q-uB^MfDnyqy3TViGP5;CmdIbq6lx(GaSdHZ^$IP9Ek6_m}}f9ARnQzqSV z0G|H}lJktp?(outQnM#F>q8NH@WfT}ZHbA)iPJe~blV9bDI%>Ui6HY@DH?gNVPQn2MXop88 zf2elm{C2zYRM6&z1;eo!ddf*Qpc{&OEbvlH)j`)-lq!+z+?gY9f-O&L?4$w#_b9Zf+tUQBN^DY{?bZpsLWj zj+@wZIKC#RQE`zeDbYZ~up_1J46&l#Ib;h_!(3CS50s>(DBOtyt4z5~htnDO4T2XF zyt+x70yald%u%7QOdEwCSdPO2oKB`{IMk)CRp(}nr4$KKmUOQAJ8Q(0u5kvrf`CKi zXjvL(?`Gmwtk&tk?ElKF9oKh82tO6>3A%LP`|4Gxf)peW@u8Y3jEf=hQb->1&h0*O9NHhDyCh9YtUY9MU`PCSJ3L8ozcOQQ`bY?x+U}&tt1dLmnbmB?$vqX$ns}cOCv2;qiIMsQbOp963KP!frV2eLMX#f;jxPAcM)Fw)K zFVmCE76gLg!5{%ZQ(b&R!%$+6>Ol3SmYwfxNhh`$d~7P`Dz= z>Jq_=?W#Q}P4N*Js|;so(GG8Cy^~(@dSw&*;ij|KD_yXwDdvB{m&y9Zz$66o1ZC;M zZJ5r17?d*-$xr}!FovGgW66gOGU8X}jFQrX;Oz}^OgUOByT3PitrJ^`mY9pl6J!Kl zob@yvX{>4g7)vtf6@Yo1vAW4Ao93YUONxc96bX_`Iw7cfpdzu5+)P)uT&$WLrgtnT zk-D^Fxe^JGZZA%9ph{HfeHQB>b{A-a4sMHNKf!|gQQ12fIylpaBZjE=*jz1AMiq;G zNvC5B{qYVm5cJF_2=98~^aZm%>nQvHc!ZL6lXRqHTHF|CoHEMQY{r+Q(_oztD&lpl zWJOa`QQ$YwxE_f-uRdoIM~F5LX6Hw3cD6J;v>QO}2tK^>enmU^Jx)f&j3U{GS#pw% z(V|dtgg8|?)A_HcO4}N$e%i0q-Y-(_*_A8G+gHg`)o6Qx39khSEcFi2#S~P-(2imj zgASf-M&}k3KQ&rmSLRdS-cGCmsub4+x6NqnJ%Kl7n1>gVM2uX-B*~41)bSi!SS28G zXK;ru4Wg(BVz*4J_h_UK*X+C}y=>HhxUcuLyKpIf#j* z2M!bX#*V`Co+ajv(-xVnd@M35-j3G9^t9WiFGIkH^QwOs0y7{Loen~9>w!2m@c4(T z$9r_kagU_NmPC@DnARJWS{y`!KbdY4@w_(6*r{BG+u|UD4EbkDzR4#ens`4Sle+ z1TC1Ce9d7!Zk&#dlBBhFlAH%;m>3(of%zO8BRU{_dN4E9E<++U$2h(NmLy;~c&6jU zcs|Hy;)1m7$po#pLS)2_DS_I^u>#oswI17GNIV%s0tb=lyr8qVv@_%5j6)ICZe4k^ z(g}dMf6I_mt!wy${4QD9T2ZPvP@2DY$qpIt_HKL<nb}0E)cck5dR}%^*fj5J`)F!7~^lRvv+@4@4g!D9A zQ)wC5G!oU1TlXR#VtuZ-m*BU_Y62w*o*#!|EFseK6R^XHJG=$Yjnf2wT=xOtosh@! zKF^bi)SFCu2eV=V$&2#|J71O*q(()1PmHfL(@&+$4%9x!k_7^3=!RagIj<|C>%q*S z5T5O;s+%Loq5#~2?Y&d*On@)G@~o6MFZHWk3E!Uq?0p-ro;^z*K1}A0Tk)-A0dMnk zJI8&BOvFFwI~4VZGSz~gtoA>?r+tKbx-YWxpt;A>2=T}w*Y4)A1a@qAs(KELa5L^5 z&ad|Op6`n~FRrzwd1gGc0xgKAk|Mt_aB&WF8M&|;krnt_8S{mpA<~$M?5jN!0OQgT z#2O-<)?OoraO9n;cPCyXQq#>;LbtQ4bVfn{H^^(DUnCFNtX zu(P3ohXv6L-CL|;!;fkS&NInVNwJ0PzsgcEIHI*pz<}`g(lG=4D~rcsJvt^`xcCv~ zqp8;WxjE6`k2@pC1}UwaHYx9Ra)-*OFZRS0v}K2*J^ZMk8D)uI49eVG0)w&_C%WJx zo+eZErA+{1{<&QI-8&ul*UI{T$4pr4oNM(Pn{C= zM-2>aT>{`<&m)z>T?OM6{MS3gD6xvH4=$rac;b9+)7WeZ?$}s6&T&D|A+;H$T8@tK z(IdNH73T?37H<0E>U33LZl%KU10YR0QQ7JOMn0353)cKtLySYxWyRbe!peA6ybdrd zbv~J>Q>1)6JO`a&_zIMdr-~!ar&$>bGC}DOZjky)KGXw-rE`{=n{Rg}ljL8C^V45T|CdAci-QE`>5J14M0%e3zwI2TojR!l^tWO*Qe4q{@B4h5 zPA~mm4=(cKSqCWHU7hvC$j0Kbb;!OEEWWs$ofg4l2r<3H9Xvu_ zhd2VFX5ZLnOx*MyrwQr9H+JMcz&j{tCiU5UiDWf(~HYNUKf3z%!Z~Dfi zW*Q=P!P#}PFeQc61j#S5yjNcg8CZbc*=G--H&yEskFU~XSjx=UAFV7*hn5v!7+k?7)EAjcjsk=}Gs3m#(E3~tQ~ zTXS+T4c+K&vcb+WjizK#VUu74lCgr}ld8MC*PR9v1xDae$UhymT3RWY8m#n^YVpJV zbRzDCM@1^g%qr#+r=k4evQeO6y z3ID2lIPjlq+~B{W8}BAizT)2n%4eW^9$jLoYJKpuROrj2=n9z3O9!WJX9A)5pr1BvaQ7Q8czRgX5B?hG^vpsJ0tx2qI3q-Np?hUXAP+ zR~(g1>m=p9{X^IHnnv-^nd}*09)5=8diZ7AaUA;T@!$K8pY8py|I=<9+#Pl5;+GnW zjdS`-WEw+FnoNodO5-%UD2yinm|QS(BIj-MifYkBUaypDld0~~k*JiauQ+O>NT zf>II$nQP&UQe>hgp!&%XJ3|ZUD1%I7+I_($fY33I?qYVGW$Xr`iX7w5*%Z8gE;61F z$Skw`WDcU5(xxd3hK^cG&WWmv<74&A+;AXefnRox)TaJ<7)Cj}tDqX)y&DA5!DT(qZG6TW^<4dHmo{ra-TTFoadKNRLPwweiU(oN8Q z0Cp|x`2ejzZ71U%8iT1bhP(h|3q9UH3`Cy{KMs_im5}lL|v% zUszgx=PLz%o$nMd9TNRy@*#;P+Nv)*^_$e1_hmy@;`b&VG;W-A2eW_Co6FOQm{&z1 zcZsp+EiONgUZXhhYm^u^!16pTI@#3^Wz!&Ok3Cjl7hN_x{W=mjoBN~h2bLyc4D61s;b;J9S~FYbA( z&<{hH!(9=H66FL=UgCB((J06jr9L`o1H-GlabLdJ*-_G%zWr8D^nTULYai&|9z~`u zTk$v^#inH2RnnMU3Q5xgHA;$u)dTC|L@NA}VT!Tcq+gDZh22XqJbw>an%W|ox{7P* zF1u}T8aA?xil_1nB$r^s;g}!AadULI$J^4Y% zhN9F;2S9J%WAv3CjOIhQdQ$6H+FZB<$AAD~JmRk6-u6mI6?x<9s(P&C*-I}QjC>XL z61!E_{e*YRp{f5icW1L#Rt~@z^8F#0Loqf6=i#!`t(AO)0iTTgt^gA^^w(WOXty3t z%$b39h!OWtfbVTGpMc2|#-Y8h#ID{l710noTU%DWsk&ZPqYK1Zb?B5Y8f-*jpxFa!AiXvszujIVb13$ zwCvq18#^|7`o-E?%>zgXot|VZ8SQa+nn8l_srKF;P$*S-Ii98OVC;5uXfD*kE-XwK zRwQ-|A7jS%)Qo~7yjoZ!Nsqs?NN8l6oM9y8jsVhSWz-Hy>hJ@Q71Fqw%1uyBOV|ISRLR;ma z!4*`Q5_NrYUT0mIM~O#yMIriyw?0eYqQNB(Q$B@7=uyhEpWSK4ijCkIFLKkuu@EF$ zu01ulx1C~giP1n+*_!Eod9^xmEAYxW6Oe|YN00ZRi%tz8nNlmTpY>6M*CyX2iEM=G zAuV$pXfs`Q-oLlGJoAezC-wlsq1eW|He1}};|8y~Gd-z{-D0b6@ZZ0!n$22xoC zPfbTyYfPJpyW}58MJ%zd1Kzk~Q@EH=EN_$M9E;lxD*)-TZb7=AilNicva`viE{+36 z{h>*7hslk5$44bgtQ0MDsjpsRTh2ZY1`2{ zKoz=EI07AARyBe}!DbD*V1ae1SytIF{L$>pr=vV8xw4LXxW12=e3VS`S}h(h;^b8Q zp8~a1<4~;fDy2jX>D9%rYuw_hJ&r{vy_;OjhN8P6_EEc+_{1XcIUdjk?yl+-W5$kX zl5gnfKi$9cu-1HNTGbo1OfwVI`N+)91+$b@k@dlAc$-(*69qGgkQK%V#0(!6rZbz$ zAZBABF!=b+cU4U`-7EQ8z+#!5oDiY2zVmoVi@7|2Dm<4Rp?s-YBudFp5_n&o+Ql(Z za3i-3tB}V9>_97Ue{_y6%G{4qkE3{L$s%A&q>Z>joqIGwr%->ktY(XT2vwt?Ha_D5 z{uv4TVvqai&uaSo`<2_fX7txPO%|Ug4Ljl>fnjs($rx-D0N$GkUvXUec{ z^>ri+e;iH)?hNQ6(30yBB>9nHSa)QfGRiTDM}qsoX7csgC6`lZJ=V+PXOpw*HYnzh z_3iH6PSspoz20B(CX>;Z=)t>NA&vPn@^AT|1S|4(?BkA`IY2|fDyX~@Gs+8ICMVep zl6p@l)-dJZG3gDZcdsN0l5_|LY{DQdjA$k#t-!a!rAilS*g9Z^zZjn*j(Jo6qi?0Y z50UAYbPRY&u^K%W*i-b-f)XWf(ROZu9_yN&ZJh}U;xp&d0T%Hd*|FsVK9LMIf z^0?7f6EP(vNEum_Y6uIzQKm>NpGohEQc;GGHnk7n6oR&Aa~;S{mp|Y>3UT_=LXplH z_=kOG0O-M1huS%Yx+}Tt*v|NBE4?r9)TY|zU9M_R9;%irab%mTtU6+wLuIC%rk|}# z0}`@}{wgi3R9Fv@v{F(%Fe)wTyF_Y})q=zg!3`OF@R)gBE+rB5_TQrf(s6 zvQ3@bTHm<;vl88if%s4%CP<5c{|=k0S)lSW zJVgz-4&Ew6M{^KhdZrW;X%*R`U~OS?YNi*)5|S2~hgmsLf9}W~6rkaWwQXkZ^m}HDA_E@d>R@4hU6m8dn6}*risH z2!to48UE_3eq*b)J&SGP<(>_l_W6l@WuX(8w^MJ6SH;z!)@=L-p1T-*T=O82#fnbP zP?zSpwUY3y;Zf#@!lacQscjA^ILYxQ-)J zzeH2aRUpne&*1~vVVvY!_C>#$dsNG*22O1}tD5XdKBi&`yh63dH&nR}?F${!7T8nq zFgA4$*}AD1J2-IiD7-r?G=a6+Y451hQU~4T2r6}!v*Jvob0Uu9noI#`Zv_b4{t!sn z(Yq+)`N;syoxjFXodwgE;1O*S$#u<_yIrZ4Lkxmp=EO$=J+4}%&Y2&Qt4OSyKdIDoJ zIk3tFiq7lQ`VNus4t}?aHG*2q31=#On$*Uwa~%q;6=>yJwP{jxVjIYJH$pAMNzFxG zqhzwOT*{{VCkP<-ze&$9^NOd<57{$oOp|;&z?U% zls`Zo)8wkF-xwBi2r`x}o;-i`{j=SgcNA%VdEa?uZ@;b&OgQGWhZR8^cbyKZaC)BpeM zy=zw+NwO$9pZOJK^Vo-?eOHdJNhBQi2F-T{BDZE~$4w3O$1!)nAdkYoKYQ!s}|PDjm|_L6Jnr?Io_gzOjf=1TQjHlR+`Bu<*jV0|uHX zaHB6WEaQ^RN*=+=KR5bz*5KXU_aV25t{R`TJ)XM+eZov7on&n@vEX2f+Y+#Evf*6J zxDCUy(y~6RHj=B3BT3Or3?ER35A<_od5IB>-!T}^RcK{`c7hmuDRz$m7+ZG{cBYHF zbs*3sS(jM8=8XMSgdCUp?rTTa>AimmH>7^6PYcs%NcB*kF=-|v2k1y_%Rfrk&ND!{ zR*eGg%G7OTaKian)pM3+!d{NEkHpCkNRPzdy7G~d&sGoVm*tM8Oau?a<4GcWSldKv z($DGCtZl&&Cc$quvy!C+5A?!Q9Q=tKZPMk|AMfDL(q}&Squ~XNuP4Pueyl_y&c>=O zY|>Adgam);=t}U%99ShT&y0aZuX=kj0vti?F>l=pKt8Fe!8syNW`;FXjl=Qws9No| z7%CP7hMYXEOzY@ukMS^c;v_KP@F9Efg4mXz2dFjhBC^WH6z|*nvfLp2ZU*;r$TYG| zg{R?AZ?!2T#3b5eqGlosANrH&CU-~v&b+{G+N1DHt+C_#wD|hq_ktc93%uyWuJjYt zSi6@`yhj_Unsn*kUAo8Jxd}myb$0Db%?h`44wn^jNw#Yzw<$gwfNLpI`g*d};8sM+ zQ+yC)naxRHhZ^1)veQFy(eAiq3+~hs3iZ{7WaEnu9Ru%|`6Mq-ERA6CAp)$0 zAX=4il!IwnorE*W)WefDjyV_JNcXGq~=s}Lo{dmpsUXc zh`XB~fjAgtMWcZP*C61^4cmvkLF}&X#|A`nBNet@bI~f^c7Y4GL9DEI-t3Fjjdf^dS{L`jA60F38Z|x{j_KQQ;O2%Tik)!SJG>v)T+3t;n+#mS>Af z9*xH`hrl^_N=R>uKosU~bajw~pR9Au@Ri>(xz9VRdm!2b7 zneH$BVLBctNq{YCmL1_! zz7BIOR_?xy6Y5Wt8-yM{|`WkmZYaDt4b(fpXcRV!cXaJ3Tq%k?)33|i+j0Vz9Y6Q~R9 zdPavj0^f!QwcZd`x;su$YO8j)-~ww4ezrIQ61HX~>xHN<^raXQr#ONG-mkCY?bI~k z;M4?Xln<+3HcLxZ;{u;y?r4XmJFavdt)qVO%4#N4j61b1)DPxNoIrvS?N*?(bl7{V z{xxdAq-9EsW{`Uxi9J9`IgqyL+Sk`vCjv2Nooa#(D(Pgu<6?;0VsHxPb6-rTo2W+P zizKZGp;qhqa1`i-kwz6ptA?B_Bvh=Jn~v578Bzs6PQZU>LU~WXXpPALQZRtey4sZo zE7gIk_oxu8A9aQCsc>18bMfgp@Q1(;sp3SHile^8BsqRqsKp(6ftrT&n+XV#H?Rsz z^_;p=e3ND4JAP|DJ8Hr&8JZ<|p02Lad($g=H7+VNA-FVf(+n2BbA|nA!+_IQJpZbo zBDl$v6ij7NAcrm;`XIBkn5;cejA5Q0XQ9&p^9nda*H+^Qicm^|xm5w``kEa6r5e$e zyzumgL^A2sE|W{)OM&b}Fx~c8rOb5V)FGVauOR;2pPi&!=MP=(x zmEwh1PBGZh`r6G2xC;qOa(KdhQPcr5P4_jP2kJYv z!*zo(54-(FW}WaoU+o{~D=$gXb>f~UDI_RXxZjF;3R1AsKx`9~zoj%SckPeW+3Y6Q zw2U+gG}bc(IEKq=s+QnRUWp~e*3_)r^K^bV&WA16i<`#`9*xsV>$_hUW&ZKCIC~)* z%`)H+hzMdo;wQyQfog7IpKm)=HyLB0A-vS&P4feaBv@T+$j4&otNekTKU;O`E>}$Z zd0N4G;c%kbV^yl#9DW+W4Q;kD9Yb4nXzW*Z*aMGZ&~1@SM3bzy<`Z(EE)rxMGqq5h zy;F`)_3e@oYsHOj1y%T0&C=>Vcme8`4Q@Ua1%{O z_Nqun7`7<-pad&9YrwGwWDOv34i^UR{zp&bRU zH@6r)#WYl&;Z%W|cs>wLMwA>86Xt=zYF>eoT&K>2_(MB=X5WSyk{8874n&uo#L4+- zjg4x5DNQ&xYGw}v>H9I)5qaN=-*`WiD@xtItOT;3L)F*S0*;%+e->xJTIhrsfd)<* zf-Eg_$Z2w(UH}l4Fw5A4^a#9I3~u*%S1hSvDHEMio?mexQQFfn#oWT}j1ixutc!pQ zivt=OZ<3w&XpZN(_?!eka$goQGFh2S-2^!+-i>AjlJq#uC!|5e1y%Ve71V@|F*Yu* zjBrVnHhE7OjFpWjsL7=TA&`_bUSzgyl2T6!Bo@RmBT-U`xqgRoe;@_e zC6~3(@MBi(TUBHH&SgLq-dU#5D%#H;_!FDZBj@(Yf)qN z1LT)(DIu^bYo+5|5}jc?G?Dn=^_qatPkgq^_Z3*4;e(l3_J8m1?b@;>+$Vwr6DASv zU_G6k7q1eMc?GT1gbp)Yo{|tp*ADhbIS{c_Q_m(W=B&rhtDBuFmI2YpbQ^Gullshwx*Ma7g+|ynHW(>sV!ITZm=hszg@5;h7Z2> zjx4A5=(y@dZJvv1y*NA5su_Y68LOoi(S$Hd|MT5tUoavq58mplf=85 zn$qDxY|ef|wHxQ9(@-$9f~_59WvN{Z^)QBP%jNmKn$KB^XERUiQzZjrZJqnTQc>u7 zzO4Pxtbq7cf;k_vm=3IW_6(KsM)pb5Aw@tj`;g91>jDDRSWFx9VpAG~A#{$_Gm_`` z@BgnKAOKb}8DtX^A|{Q%OcU)@{92X=g%ydfIIz3nz1mDxH*FsCZcIDX>AaImc~8rA z%Vw)-c}27Ji)e912Xd&&g4!0o7E3Cz^pt}*u&Qm~y5d{}i75J5mLC_}m8awUf5_&q z%A}-_@2S7CQlW~v${xcdjg;zO(<-QpDoWbK16D-&Kvz#zm5U*bOG8JmR5}kNkIg0> zF7f@Lc0Ra@tN_c#Y+;6~oyiEwjqoTOnd4HdVKccaMiqbSVlqNTS;GpEq*l@i(8~Gn zM5_-mM;57`D0Um=FC0hdpE0X8S(P&^!;HgdvVmnHnUTAjXuEcogZ}ZsCT@D8nvT$g zUC>cH!D@#&R5k=nwqI0dU*d;~jSQKyY)EqVjKFJSr|;EwX3Wa(szrAb%oHNenLQ(HGLfA`6$$flr~&>hYOy&4A)@9br~*OJMdiFTuj z7rHUMIrY(q`Iqn&AfPZ;!$>2m!ywesaC&iE4<59FI+*wVrLnPF@s4zDK4D@o)}LQdX%#o(_pCZzun<&Vu?9TD5_R z)!tY#w2o^CL6kkb0Qhk&r%rm@(%$I>L|e8{{h@p?A><5q9XhfyW}*wXo(K^DIR_b3 zC9XBl7FiTeTRJxdGp=P}Mc#}Zlu6OLofMg6$M3{*K`7d}Sbi?Z+L;hagPAuk1#vzG&TII>snmV0Hnq0g&o+`AGiKdzB zq@=A-YlwYI+?jsBbi6Y3Lcdbir;C|N@l5(p%Q%d#J8)wQJP{~3!mM{H8+=;bE0xL; zBMYj;Fw|CMJ_+}o$yiW$G@`QwnD8Y>8xIV<1UI7#+q`WC!!BAn!*B873jtBA1W!sr%H_?={ zW+DFMZB>0B2+X6gPPa$})ah3A*G{KM#Yn>J7O^q<1biB1OEDoy-ninE`dP%KHQ_OA z&H_^{4k$miipw*L=80bZ9e4B8tbl@(BR(%xxy*EYo?etmYrRF4Ca^R|32sed1T4dPDA@hth&m&I_V6MpPf`@c z^QxEZ1K$FdEMQSFp|Q{ljjc&DZH7K9$Ft#gvit;HTgr8vh_b~OA-)F6`^hsDukv?? zO*wj3uWiEnAn(V~)XYs?7;2OSD5-8`FR?#&FVK1VF_+LFmmLObLD%3{bR_&HHkLTM zV>sRqzf-QLoWWEe(aeyVYTC5ws#TkcuJ9Xo3 zKv`kmlUAJrul9jTtYeC#*|j+6Jr%@|uRcKDVvSDq0)_l!fjprPF*@g#CwN4m+=&ob zk^WHKSSZB<84$85cuHi3?O$<{lNvF=WY;ks1Lj%6i;u~ZGcU7}C*>JfjbNBywEP`E zTAuT}enh<|=Tmivb{ z4?~z}s{69^R%kw4uJAhYap!7*JZg&6c;aXSmha8$=g-secr%f|d?E$g?Q?E_aaJtK zP|1CWvs6I11l9x%B3*sJAnkdXg&W1yF&YV$yV&Jt$?6b;OW4g04+0l^1HwvQ-at=7 z9fXesLeOA=wsT85L!ZiKFmYdFqaeJv6KWUkg}mYAbpH4wxucr147#*f?z~lZo|Tz5 zk-vOGo%xNm=8E=clQe*=MtcsggUWOBBA`1D^j)VxcS*+=r3lAGL>1xfz*7JH38wOh z^eH7lt4{mO&PWZ#t3Me&Ih5Tdt8zaqiWt!i%ce0Rp4akCb$uhy23dZ&QVR;tH}pea1?pzDXmFkA zdEVeg(S2N#*FrZWm888%5F9WHg5a5=bo z!q|Qn`D>=`G;NzK8ZiyRf?GiLdxJYb_Hct604S{5$iIxV%}AHddsRcV{u=ZLPEu6N z#$%%(A5IJQH2gg3s;GKXMv)+?Lag9tBD9%x7!f9AqT2*Aqgk{}A9_nT$CazMXR>IE zIlX$tp@WOxm^MovFZ}+RhYNx;H?e|!vtz|iL=;aj;?qMCZinUHs1k3RZKI%9d$sr* zsW@d{FjV9CMR5mxlYdXVHyI?=+qH+fC-f8;P<$oBH-gndWDB5_TM!Q;{$)~C6=!Sj zS(5N_SsWM!08Uvgx%H-kR^3;!mAZA=f85;qut2Yq6^ z`B8>=0~R7v(G*$>zu%h8(hGC9FKoYe_&Xo0Z6>i7Ld;KNXQSuawvMmDsYF1tP!^5L(J<-_!vM^?7X%vX}2 zw$I9VCFvh20r(3-!N;M-eD{CKF0e+$=H}+g3y_|2t-J8Jr<-sl%ZQ}S=ZxIGBbI(R zvu#G08dpoF;_V&(wH8u8oLL-q&XmJmG%vYVe>ekzGAoryDB!I_5^=N}icU1AJ7;_iwSw)=s?niBQ{9W zX6YO?c>o$Y{o?n2ScdFkon!=c?{S0=3k>*i_!|5rsy|`)xk~fVcO(8;cR9wzMEo=^ z9BQ;Lm4LTlNsh?Mf$aE;;pAveu)&hO19JoZYPMQ@#TqE~ka-`nvjhSpG)BX@Y$`eE zpd>lh6ffr97&|oY++p5tM5yJ1)m!>`Ai7FkY>N}Uv-N7{|7;!X?Cmb1df^}J>egmu zRR{8UIPj_ZL12rkYDZcV3a!RF2(n-6w~ zEFR3s3ccbk&C=TCMncim^7pi4AyLpTPiu9@nrY;VMPXLU$kAA&E9S3|@FH05a@C2} z+DZiH#o!3kB6j1a$z7X!goZpSjRR=$4 z$9-Hd@xIrM`IJe19K&fs9luDHe++z#NLJ}aVtf{^KqMC>lV8D_+{j_S1uL%b+ryGV$*#B{uf`ijd7%HbJe z3M~I;zo|N`&<=zqa@PqoRF2%Tt@@MSqgt+kYcO}?JG!hp|1$I|EyQj_t%j43K*fYx z4ulWZb#j?0figZ(%b!qBk=F`|t9~QMfXGvW3i^F4j+_^Ck_vDqHbaVDku~WGMBcjG z@-f#0{M%i@53r1WdcaQ)^%b2&QXXG8q9MI z5|AYMRwzdEc`<=-k%Pj~n1ilXGM1*^Tuq*xvbLz4TZhM@j;pJss&Z3bI?#O8yl5c% zh8> z4BTT-528z?gUVMRM}@iJCz+K3{n;F|mB}a$R;?5YdO=v0j624(W+yTeI;L=^J>z{r z?sQ0~KEdQ6^%pMbS1&BW-+=A2_^}0aKRc_f6-V!*<>5Fx1ub22 zk6)=manQgb`Rc7&pEc#RO}RwX%BT0%_b$oTLyO6;+ARYVPDWrd5(l3UdRh~xwH<;o zNpLV%NNWsp$CRlnfBVnRU-O3<9M>N99zJ?d&V~>2(Rgs2O$NtBZ#bRac#L)N^W8V! zz`qZ_|8|}KD}MXt>-F!y|J%c_zx(>pw_l4tzW$qd`^`5G|CX%Z!ul5e%%QYOlKibY zFODE*{w&gN(fhyX&x6&KWHotSOfP2n@ky0D5*w2ISQN*Ch1i)4d&$;#oZu^{)&jy1 zs%`PBS3)kDK!qmsutaWSOHc;!k9^Zjei6h0{MH`z){{0gXvtTYyn8jdDCQVE58?20 ztK44pahOf1YLH-R#yQm6q>6o&831OI{8RQ(9AfQ+RD3YKkflsj!vt!;|rAQwl~c{+%sUT+=zeXzg%dh5*=gh;o3`>n;^o_-MDv?txWfWhHpI3l4<6I4m2M$sxPSSA!h=QkRmK{9+ z^!e(i?BcwbjY^e`(kcOpzaJ0iyTJ$9lnQ|g#-oKia;uwYDN+31g8bOcf+f`aa$NxGiWdYsIWCO+r1h2(ukjF+L%EvB!+WmJ*!2%p1zu;P(ND^&!y|qt4UZhQG2B@x>+PB;3pI> zwF*`I?yA?8>?uR&bO_GfyLvHp{YRPM8U)YLAf4u1P89>~VmVzbj~*S*ZTK1sDKu>S zc2RQ|OeQ&rU60jTo#KH#xT(dN$DsFs)u!vsWM&U7Cxan92*dBbfr$r7p3;o*i;WbT;?ZRB%+Q zhwOsVE?MC6>-E$1X0=&+9F?cm&mb0&LRwYwDUry4Br4`I%d@kLD(gai5Gub7t)(Iz z9lE>JSH()*l82rt5DbQh-inR!rp6+fNPnOP6Mi+ zOBxx_IB{YDAO_ z8{TA*h20Db!$|1D)Bq9C(b-WkM`$QSmwaee?Sx$8m)^e9NyLR?Z>Zc`crcNLy|qOX zSliCbwm=&N8c>|vX$HAX^(U0!+q^!8Gw)$sG;8M3BoJ`H4HWp1i&RvNMTT*5++XPeuHY z?b_&eF{e|9&q%~h)2WZTc*lQ+(|nfB5+}qVYIRpm(_&|5J za)kaO$Ebh%=cx5t%bitl=-*o1*!l2~RGtw+(OUzbf8i^MLgrg;cLa`9Ck2{3aAuS! zm>~=`tV5Sun=UAsllGZ0k#lCOW2P*-QwPAp7r}iBzN`2EY_2(kp$^}jz2AU_!ES!5 zB)?LI-d=?s$`1r`xT85u+rLSEQ$X~r_p+FsrWK^U#?0C6Gf3*Y4hQ`=LFiikNdBfW z>PPW{LGt$trIGo3^p6Hyo&eG??-$a+dIBgR;Zs1I)~%7i&p`tl)0!0qn1{wp5$(Jt_sJ$6s;%@uU0y( zmsf62bVYLcBh|Iva?hC~&blN>NQ%k3_Yh#!)yjx~QlR4^RBJUNE4%H&WOd?dAH?*v z0zy1{M|0`F?}Wbo2w&fgOyDv|lt1=h?E%h5D$w)~ zh|~pLP>QG|8mD8GPe>z}>1|u~^oeyJf(Lk$4YT}A;4$x5_vLP-0-?s3ydkm#aX{;t zu<{CKFF2}g_Zkb(<&AdcbSiVrpbi7iw$jWo(H`|9Tk`pr2JHhGRpcpjBH+bmCXCT2 zi;Sk+$8!x_>{)}>(doy(GhZy`IB7HFty3Hc5=`krn6jm!oD8qBN&RjF^_*dX`}sEx z2$M0Cvxov9Pp{~Xb+-`eYE}59rOze(ZJaCB+h^cbpz^hjGhGj90AYL5Py=ystD!p6j8q$Y!-AZQ;Zg(6Hee3Zr&sx$F>hn z7cp0OxCj9E%A_2o+d=HM*gG{_jCJ2ipQ<2(_|?_BbEZAtA>$k30p?qMlxn^gekm zgV6Zw8t`#+D@iF0quwQt3|qu`Zfeq_ulAMetl+r!^$U%O_~c6`exDAjgm{vy1`x}; z=NdGM>fT@wNRpM0V|q1g0YZ2?g5*K*EQo5ND-!Q)k)ms%?tSOGCRETjooc)mV+IlU zmeCg}WfXhAP&)dlao2u&9W*8)TF1b-A&*{a;J9cd6%;P*Y#?08FZEYD6LL%sv58^1 z3;Jku{H}WdBS*025PppG!w(YnYV)|wE(f3I6?o~K7x9FOTi)J5`EpOlZSo9sb=Px0 z>}*CvnOoRaa)d?YY&wzpHA@(U_qV*qU(OaIC+5NpPOsb`*zDH@hGO`;L?$!zs#}Zz zfm6CH5*+^CO-Kcy_Z>V47qN-xe|SyoAY}ze9dyw;wjS{XS$X!3Sn~NBa(&4sWBg>M zL71q_Rpnhnv<&n~>fE~KqhJgPiaLWSH?&9EhzH-F7A03!8wP= zil7_Vc5FEW{g&D5z;z$J??adN$J$%^y7%R0J57^fvex)S?G}?;IaJYe)#%fIx3edY z)b(%l02-ys9JpK5VbxqB_3EltJB=y2eqUWe)m1|Rg6b}Q#ITsIAq=bl0xylbfAD5! z_s2SO3ul2?UFgMKKik`TRoAh7;uo54)|=aH$f*R=*nrn8r0AJXFb``|=Esv%^q1LA zCNj7D*6jFGdv62m;8CxfABv{*yYga4){;jZa6l)wR|7q zc8s-EO7ve&iEP4n#SBf&qbwbh4Vp?5Q&-f)_&3weG~O{DKnzQw0EHUjT)V|?`n`rJ z!Wv}=t4k1OlEqL_c6oDobcu&Sai2LC^zxR>CuKg$^xwVY020xZ;(SmT0Pky5K+Y=g zvp(rkU`UaiiYoyYR~!;%iZfU8!O~e~c3A+F_m`?_+w``pLPxYazfRj=;u1{$DIW_~ zYjhuVcX8Wbg*X80ZllQJPR9;BV%K9JkgUOt%}*3}a)S77Anj&4etQ-x5J>yxUf4nQ z*GoAl9i1AIOVN}A{q`KAgV&`OhZCeL5}6I6!OVd~N+g^>q>Dhc8Th(Q=INOFnx>-> zSG`y$^{g0Od``l$oLilw!#%+{O;H!dxdujNRLW>%8M<}S2Kz|mu68tPjY!{YIodb7 z2Js`rmz&q|QJ+C49eKd9zqVJn3~}|5<%^&{5gZXP808W}@w*F!zx zfQZv^S6a*Lb;3$|1wUFJDm`5_V1(!}q96q|W{s5)i9~ZUQ%*?ReKL<*DyS+#8E$!3;dC_eUqwvAq=tkTtu5sFni4OTCZC`2A z0LN7mQC%tjK7|?jPb1Y$j=$8J)f0iq{B(Pqot9FE=T0lapnJC`&QT87^3;@rW_n8f z6*9202q!7Tw$zh0>(R;;D;6=3)mS_o8gCss#`_QNE0$$-BGQsMJkqClPihuyYnbc@gQf^O#wIbxT z_WLu!63iw^EC{6&UYFL?kTgVoZb+L(SVNF9KITHo9iHTn_Zv=6+2Kp2wKWSFSeEA1 z0tksb)>f8u$*Q)jTC?&sEv3?yV%Wk<(=8zz3nEBj9>jd@(ipl}#tOTJHtH&L%~D#x zfa?xp4w!%k*`eLiOMSLOiy)y^5O67MvV066j+SX?8N+WBG*v-IhMZ5uSx?ypX=q>EqRSdLPwW{wp!PE}uc0Ho( zp8Zha+x38+GvXT;*#+8qvkO*?PZPid2h)|r`A~14|9$I?L#J4n$`Uk%T^>J^d8lD* z1+~8A@QUQKL|Di}XxLvXt&Fd}^Lr?j%X~|lmkAzlOPexiOPkA0Cyhk&Nz7g@eN#AJ zg!}_e+wH}29S%oX#JS#>Ke4A7SSSs)CM~5`^-Z&3+>FFv1WDI{87 zAcoo>mnJqNtc>@N?nXGb*n>#oAehW0O=ez1@H?Sl>EPiuHU1&B}7uOrC3q zp-IT@undl=&GfXu(F@SSNl89+${@;{R{8|;2I%IHALW}fQu(q~1|U1TvW;vbH-D<} zR#s$`x2~n7jE*jwie9IT4^RYWn`o0*>ff5SP6<+%LOVAe1XQMDodHH?e5D=KR zY$CIs3gVKMrZd!t7e%Gh;=xZiBhP0U{ z21F$XDs^;$`Gw9^CZbt3FXgNQNrq}B%%_TJiCGSv#8048M->A{h$xK7nEltHp1v`l z2_f>>r^O##`2dvWoH9f#{F5+L*^W!cwU1eL-08_Xvlg3eE{3O7mXb6W^|hA_Hq;vPQYO08St!AvBX9>` z@|tQUeV)|06*YG!prT(Z`<^iUDSvL_??nlySTlH{X!ld$1T`6IB z-KdhVXy{T%mfKZQ$48v-WoNye^8aMBqTTT+BU=NS8Z zvw29BL>EZ?R3d+eeziJ1zK1nYrwg*eEsxc2KOtU+5F4C60zh_AT24#mbanG5cAB4# zFp^7rg~_ngi~rYPD~4>hsIkFID_UA7V+E<9Pdpzx`(gCI>tD^Es7gqZ1b07hv@TI| z-CqQ&Zh|MbpM2~|-0LJ)*E@$zvSipN(XbSsiSLbFG8XUq`K}pNQf8fj{NI<$4v3;w zZAGQe3vs}z&KIC8!PlI~W%P1xptHw*%UJ*))_jg zydi&1y%KbuXhEkjsDd`Ab-sA-9Q5ktojYzCD5nc=7Vdtb2hEN5EJFWo#c=Uu zz?7dtndEr!DWr73kzQySW#hwK*6pyIl7xx)tG!^AHh`MDVfn-%if^Tw4un3p-@Om* zR5u{8!;e{yJyB;cxCnlSA=WpS2uv3~i^y7wH!prP8NXpirYtczCnlxq zz$i-X7;qgbv6egpXm1kK*VY^eFK;dO_QVgWtvx%neqWY9O{b$gqyil-@PD#8Y9LLt z7LZYaGOY+Ubw1(gzR}_Sty;XOnsX5Fkgnf(p1ImNrJW{+=;|8G&GuxZqNun)QdX z_Fh|L6O1o!iCw?0MAwW}Bjjnw^@|TsagI(S@N{2r%K683Fonl5c!su<8g{e+GkQCR zqh+(82Cu^TNJ1*mc$}<{VN0k;En~$s{L4RuF(VqmDs31Ch19cJGmHBhbpFzw1{{dk zn%vulfOe5vsT2D4y6TtPzsKTqZ7FXD-`_!FldWf0oVy1-Vt&D8KK8nOL*}nmDO@`% z+O@WvoOcGpll`|;2w!vad?HM>t8N8GHT=A-Q>;TSS@tZ-w%0Bk6-yz^Sme3WSPQbe zf@xVXy++#B_S9MyE>Pbx&{ypNzx%P9{N6~Qj&BDk&TdN&R6E_@J59OBrPa_j#fc4n z)A8fmZp_5>YLf>Kl7qb$d+mIf9TpdzezHHGPK#N^;_L9HD0}RqS}`}g<)2IC@hM1M zTQKe&4bNLvVE7yfIh8DCEs*65gVn5zIt$pT@axNfr!Gxjqi?N6>qu(lwFeBZW&N z!)6*`qpm1&wWQR=oKOGbnGy$UG+{U%kVBRbTxCjy8Ow~p28yj#Ja%_c?oF1mSNDC3 z`Fd~a-uh)qve&r)9T;CL&8?l|Hm(tBnPj)It~@{RXbxN_xwg0-?DX2PnfQb@N+@os zpZSl1=1)UwkFSesT9@MM^v-uw0ZEh#PlM_QP;Z+a^J{5(E?&px5mX zatxxCkg{GlB&WtlJ?^3&q%OW=x;iL)rDuziHVE-Z6FcaX(yFngW@lY@eA@q zS1vzwdSHcPX2_I({-Rk_tqbe7+}hq5uvPa;e}9I2Y47|wL7 zx`>5YtvVuUtUDqodiMmtkHqN{=X6%gk56LMVAWH2wwyB`fIKWS26f=?V&>P5;e;{& z3vp1$>#kbgh->RANCe!ye46+~E9tu!f{%l!q{nYUirDHs(!j5;og+#e2lvzjujQ`n zqLeok5lPs=3T`pVmM-{h_^g{0;_umcUS`yt{b^dRZPNc3oQH#UNB;|G} z3)HZNA2OgOI5P&voe%9bRPBx!w_ zrw8B+LFmg8Ehj-@;?2&JjhC5OO?dUS<`qPAj@2w z8J8G!^AT`b8_V?NI%61K!O0d7V*;Ab*;4N5+Zu+6F$$iy!nNbF@rPlv$AT=k8poj9 zd@;rw>z0>dqRqJgTVte@bLI+o>&k!bq<4~+mNICivK%G7y;quoqJGLcmG$ZR);g~M za7_vsYs@C*ET>$iE9P7M$(nWz`;!GKEqws1etdW#)se7fjR#Z<^A!9sO1lE`%UtKx zw#*JpM@a4h)CTk5PO@xN4rF%$6k|>EazNb5t)z{4B6YHS$QIxgJk?rKfB~SI`(NxlnMoi4(HnL=stDPb!ALRfc2JiS#N_NfDp_sj)z8p0buyFnBaXR z51ruQSA(XLW$s<`)3*oQ@Q9EIBpkDMj3UEjv2BNJfK0C|zm03PBnaZ9Ekel3e{0aQh3 zsy^%ryvk)Rz|oKjemo{6uhg(BD#EVeHtF^H$<|=-Y%qAun@LLWB*-X`(R1XZcobjOd5((ty?0gTEG+;bI4bITqKGil6M7SxR`Pd z7AER!UP6NNc~#~kM1J^C?~1z%F;OP46HTR;2>Z(E|MU55CU!s%OOS@mRz3&fjl2YE zxqy5km?xyJvNa!;z8JXVhn;2oj%^D60v(|w*BR7M)Pv5sSsf)x?Avf=GEFB9vGevP z08(nBwbzcX!G>B9+H7RmWZPL`fkH@rbDDlkApRkA9B;$=OZ_9{#jkB-#3-sk-%@Dp zweBFfX7QLncD1o`g{Q;Nr{iK}1sN3OKJsHBSNs}z^5ib@=0TiBp6hJQ|t|=(;JSlE`GlI<{S9;;rHLJ^MA$9*WZ4>{{7z` ze*N9okG}o-o3Gcu{@ePab@9KyCF{4c!UaEbLA_>4^0(@|I2sq{*(}m-(fhyX&x6&K zWHotSOfP2n@ky0DT7USBko!RBPj)85Ua}?FG<;PGLLrFASvC?30DkohUk9zA#8yd9aEA$;$O)iSLAg~w7q^Qt~qJk;{D28MohuO4( z0tCQqjq`Lels2)-3;;7p{wez?4yzRF6%55{6w59f3L!^M8h%cyYTAGB;Qai&m*Rwb z#q9XOn3|RkUhO>J-re6`6O)!7y`7A+5&-b8c|Ie3R5}%t8lrSOPS2qvM|wOH!Wqa< zVixDK910wCld?Ff&eK^2-HdV|>4)>mf|;74SUGPXpq5UO*4BQqv)=-(^3Hx2I{9_y z;O~2H50YQE-n`k`J=odaPxjs<&-Zp;>>TXu?TWu%CR@AzOn%zgebG&_9L$3Pnt~G) zD-hGn0h+Xl$$pkuQ&6lp7OglEcRIK(%z;-tF3z&q1g@iLHapGB5>xA^lM!??&QJ4- zye0j0!9WjIR>T#Oi&v0w2g22kX2od&bVl7-WeyvR`;YRg_!p1@ zg702g`AW1O&qYIC&7AA0dMB-DqwM3tMq+O9m-8yew=2P0QQXoiQXl?W+~Bgt@IiW< zTnZk7zmbM|^{UFm-iV9$v2qJZ4mK0s9vdQa4@M}bb9lUw@NT@19}{CA90MO8>z#KRej7J|<0xU+YPkrs^eq!2v4^Y(C;P(>plNcDa z?h%`-o+$C8>6dDah!28Q2TKH0jge@lO5WuELx^#hLFo-R!BavoA)_7ZA~d3J>Wx&6 zf(*dbV>JEJA3%IqB>#JVZ#N;8K#x8;(5lNZS?{42S@jW2_n{M|^qB1Uv=3ky#iet9 z2w35RRodp&2e#Sxndl{MG*(a(9fO>*4&DLi69CQurLNlC=smjY1% zb$$bbSS9G+Yy+R;^ef$8wfQ=+?Sb5qflbj-R*P0|GC};y>{U^Gm`~dz?hU{(<*@+I zci#-E&9+!&$Ff-X=*FLBXC!|4Z|wGQ$XHfbfUy99o^V>pYqadiNw*I3Y9MIzaoKVV zD;hiknzIXV^#y|i&<7 zj=M^Na>&wwqX0fgk@&}~+A81fz=Zv>=R$smWxG|)aan&nNsjDc)!LY_9c~~9FMnY| z#LaJF<6q$AQ38{OlL7eOd4hu*XI6ND(-FvkEuP30_MBnlch(j|!b$KhwKt+3|M zGr?hj%1`4_XG8k2l|ri4Si7Q^G)4NE3PxvsnhAPKq>i6^1tMPe(;$~+?QkwVh34!cT`1pma8jA7zVq+N9i zXY~is&#HnPrw`*y5M5$lpUCf$`zmBpz`_hUvHFl$%Nnl_N&3+$^XjOVlJrN5n2oiA55tK88VRh5LsBPl%*ifIg7z7wP(K-G{ znhq7d)PTqT(GUudQ$<37T>fjGfx;z9O;eAW)(}!>_qSjafAcw^ti=R(cd@_62SURB z_)`3Q@=X>{`gEHtI`2vtSlerb+Tz9;fJz$j$&M4MDJ6xcD;Me0$7_ZZ$rwLFon*iR zDEHD5%&o~u4(=CnXX)fPGij*;4zAWQp&t_KbWN!NP<}uI)s(pTt?_w!QNrv9h-MTNL>cg;Zpst4 zuydj#5e$>mOpdS_xWv?)#EtbPOGm`KJ)WEEL9fq};{4UA$)M4%>&c_t8hRqM4))31$h)y^Cto{BRBFh*!xcs1`7Z6vDdXt2FP(T@#f4nSb~#L^s>%p> zzbF?f@w>XH?gHD;O;E(dQoDnR*oJv^^h5o%g&#~(F-|(h<{n2Fi9ZZ#a0H%8H~hp4 zMA+ce<2ghKGDamy4~sKn_7HC`knzn@LG@IIJ?PSkGYkf>!jB@CYCa*7xnu;Z6SL~U zCHd;B1itKBjW{1Fi3;|&*>;-I51u78X>)!IEfGSX`<*Qm+HbF%zYk|w`hnA4X2h$-u?%qxXN!($I<($nqD9-6!z;%_tTuAc*upC6xA*@Ak(i5?p)1oYMLB)Gc2jQ__@9pd!Y{v($ zNB9`ZHcr8Ly>p^N8N=fQE>Z1t9B4mp=&QXKBkqM3J$c3*;v8J+3sc zYR%>odw>}!yYv+vA^(Q;B`MB0Nll;riX23vY82vPfIGmE$w+|n6XJGQxuOA2ETnsy zWKCAPBj=m0iw@ML!}y{j9?sk4Tv2>e1Xr@=Zi@wfOVYa8F1UNqP|>HwyF|yB8sn@} zOG8>fk@UDrD?w!E1WNr5xQWC}5vc&en_M7vB9K4+NOnpR@9j65z|h2(!6kYaSK7JQ zB#8=3usiA*mkuZ856M{u-=s1(E@*{ElfBJ~0<@{jfu!FToxTx|+bBehP>+bWnPQH0 z5AbnMn{j}&N`{#k+q;OW<`b6;+pC^rC3rI%*OSk&c)8xkcn~)EvfXJj5hyg-F80{9 z#TiE`BW#Lbqj%z-vb6@WY5`%gNs|q&SQqAsi;Jg0O2t&aN-(XxOz7xOGkxnkog%I&1;jSQ( z9UDllRKN%cNu-jI`Z3w1VmUGH-c9Zt@ro(jvcVQ8R%1;sSe`_*Kr<1y2Qfx8WD#0q z3rc*qs3Rxs>ms|iTiq)L48V?sl@A+zEAuFDx}#4_Kj0=~xkfY*1*6;fXUKz!r&7`& zFj10nxQD_wHs7kA;hMrclx9KVoJ4Gd7CSwgk8< zvS-e2ya=nTFGs~ILq!DplBm^Xvk?qSBSGERgLwS`4UH>r7YLh zkzgDZvi0%qYqNN!4zbK_Y{j^co{Uy#KU{zGjr7ENla1z(I^(pMPtZ;z7IcU1Z^Lf*|fq$+P_~3m|eK6cT?LV_!d1<;f;C7hXMlA$1< zVX-$UbEyYW)^h&!fc$b+oFW`+Ax%vDVn7ERtnnimP8S0-8Co>F#L<(8N`^1xucLd? zVCUb2vIa63b5V?&>l_mj(F_KB4Zs^1JJ-H`y|hj_TB+7kDEmA1szWf2^gz=@#wXD!ewG6kk=n)_S1I-Yu3pU+Zb7} zwWlgxm*<0oBb1MM61N7istt~f6T7Mtz+rJS{SqUkL2PN^1pU*UFF4qZC%brxa31-- zi>rRX6h5J30x)yB?&OEiet>)6Dts)A;(8wpwXp6?7E#pi>r58bPC0?Ti)(I}9~jT* z!|^Zeb)?MkSNc|(xNDRh!%f73*+*3wFdM3nTLx2b z_>Z?gR@nqX2HR$y9k+YPIy0EfvA|hmXJB;9pwyQ&3uR{;if+qjz0m+(m3pFbtpWf$ zvH^5nzR8C3S(%?@v3mlFSIcYE(Jk2nxk(9}w*e)RSu7;`NLaEDhPArUr;Ed{jsG4_ zr?(LPT_6Ab^|$NaegDvo|9<%WcMre&PyF}i`1w!#_kZHQ|Jw22kLQzOCdL%hw#=%{ zSY$XRpVT={YNNv&g^;J!@RJ3PgU5C@N+-aj3*14l9|`12&g5|dXq@sFX!bX(G*c)i)~_f;>JS!|JBtf{7mxFYSWf!C&56hj1j*j71p5GUxJ;hEem&Sfcrp0LI`{~5_1_O4vfsbQd?b+S zP)U<;4>AQkY?c^f&15oD*-K0#F|Hs!)A^xb-uSobdKizD_Y}wlGNJjs&|cf?s0)n>1e`$7I!D zH{ES_YWvA2n1WJ}la^6@`W3qH_Yc{{x!}Z0t4vob9SdeME%hk$pg3X=CL`;QLG~~9 zcX`VGsESkMQiZ56_8%9gVx8XONkO6{dm-q#`Rg#RtQm;Eiy8YzSZlb3X54ke;Wx z)n-6++;3wH%0;SnsA3+4? z^A=Bvnf15eh1en}?T+~qP6Ydt@B7hA2%2oQ#|1~4oZ&BJd}V*5tiu0hFUQ5?*!*=` zjOH8$r*pL1nFspvWRJ3Q_J@buLVB|PuuhYtMCO5F_1F{ZckAovEI&*6c7jt?CfH2| zCZgo#@0s8c1Xj-Y6WiZ`zd`j%$)DIrSe{srCOMV={XZF}EJUi@zXue8>OHn6L!1Nq z8zn&Y_r+v*GAkxxNHzoI;?JH4F_`}al{*LHLWs}S(}Ha(W{F~dU9dkW(%F8HkLgj~t~w+Bl^`Gx5(W z*NgMRxwuKxwX%_{emLI{&%l#VJ=E!&#XI9ds|w3V`S69*It}xzG%0Z=Rf1Wxs7)Eo zKdh>2Ot6^Fz*g;ts!tI6zu>b-f!L>;D?+_SVGVg1!I+& z^y&JQG}o|srpyv{Md=`PMo`KCK|VK9U>|9qbq#tb(&U@0Sv(YG-ArVachGu)a84fT z$@vM^k(x>C0vQ@~<(r;|%OIxblXP~Gl*5zkG)2|4Zr_D2_@s{tA&!U3^-x|B%Qb8@ zwj4t!Ox`C+Vx<1dre&5EREwtr%cm<;5ugS8-cnT$>@$3X9=(hz4YXd(X)67f8#J}* zW!W{WgtQBs0QtEZFIG0pbe0~A4L~p5q`VL;<>}x=uu|EK)kxBL^AhmtGIb2<;9y@F ztk47d%BI~t(Ki-*Z?`BGV6eOO^R|$x<(W0OD9fE7nqkp)a)$wJk%d+eKGoyXpw|O> zE!O!Fy|=85r_**&{5|`~+-3%~{xUT;TIlnA=@0a9{ab-8)DDyfL7b6i;PR5e`Ggc`tGTP6MO?dl#tZ#q%%5A%%1f zl^_1N;T;W6?`k75Z;3!^rr~B+bx>w0m};P_Y(7x?toh29nU1sDk{MkzAwr59j6;`r zqfy|3BTlFBaKU|R>9Yc4&ruC_Y&vB!KrtFJE=n@ed*&j0psyf@^but|3=`ukef$2J zVMOrnbGTqen0mT}Y(Pt!7IZy0ZZonjCA?~o={0o8XRV>ZfT>Lf$Ti_uHfL=0JW0m` zRv?nOQ7YhsDtzH=jNhK=CRFHar;;(2R45*`=7#OSgyq1P0E42lRqNV#n4hSW;gvG5 zx=ZCw;Sv*kIQ%Zz4gt?owA`rd)-YG2Op>Y7BC4tmFZCKxf1F`eYYip;mpN z7%IkNa$eNFYW>*+qRkrjD=QN7?yszY^r0ikQPyAwRmW(MPIG%$$x*8s)W}!sg4FKt zT|1V^)|u3;GrT%BEWs<7S3Dq`sIa%?Fg*L$JT;CJrB9^7@Pa67L&w10Bd%ow#OvYw z@>t&x?;B$N^>F{T(Zy`7<_kZO%5F7Nf0Q4Jz^XpmGW>cvN)b%#lRB{H$ z3Z@=CZ#ey`y;_mQwSntEl(lf{PQid1E6{Uy)MdHEQ%jb%DZru z&8Ps<7e>f5amvd9%@iQ{38>p!=p3PV4H9>|w>^+ZxMXm>*qRKuM<0G>iW1k&JXn4D!32r5dB z$Hk$L;!d)$VA^%)icHqS)je?oq!7TL?fO~Kh%wo_wA+wQ)7^=j+ctL?$|KMq9uU8xT= zZY3Q{r4A`G(QXTxe)O77hr_Hai&>u!fZ~%3Jo@c7PqoibvL$Wrmuk{iSIO$-C7;&S zm4LKk9OLyITD-f}!}(FnOc4rbSD7RD1UB2yNys7*GF`L+7QJgP-Cj!o4m@whCxnTY zp5r#)!|q3N)*QI0H|t@@-#TkgR>h=$re0mJP+k-V292N~JCp#hdUatChC)P5#^9kS z92yEtS;~Pfug8IdF-+PJZd%8o$6d{x6|n4o3jsq8NwK+-PtuaG;ETd!jsT8SmM}?9 z($ZODZ)p@kC6a4Y$L{~0mR?ENYhp>L!^r3GBfjgpbK)CuLyY1SS~2*&>tdUGRqVWw z7Z2<=pLEMG&TcZbU?19-y5lXt+gsekUtlR0!L4iushX2t;};1Es6j=)Eg**)b~G{# z<1$RwnnT#J%77J+?^>TfrF}84N%1Z7Y9UgX@d`d|vZOp)l zbMQ8!Kf%2a^XSp?+LWuS83nZhs%!eYjWf));C9aNUt&A>Qg3@}yuNS5aW=BMLKG{P zYVqvNCsW8_s$^QjhcKg?l<&BPfLq{xou$*ANk~#%WQ=p~&$qj;x1Rqrc(Z-*_RVfQ z{1B9cLO7!-ysl<#HceL4i#&T44+lvQZaf%iA(DMg)U0p@k(sn?a1-5f-lUCangD%Yn zE)Uc?LGrH;;JVe_7})g4Qj0C^pt#6Av8i+7ih;K4S=hv*@J4=wVn*OYNIF(XJcW*X zsOQG|T-x`~ciV;@X`$D0Ms!lhoA?IwiMJhw!94^cG$3_fFmOZrHp(bX45k;O{W^mr zJ+%4_9ZO>6OnS_`2{%p1%Z7B6#6&(7s6nx~_e5Wo9e7I!iU1>8V-)+ZbIIW5B=w--X=PEEUxY7HpV0oA%s z5-W2qTk8TIkW^fH)|SvpHl^-R7Wmg(Y}**%3Yj3RBqX7Ht-cO65bortWhIlqn}BN7 zQxe+`=u|1kaY%|Wm=-Kt5~DqncALNQyQR3Qd%b&I(}xg^?pW*wn_0ggtfuBew!>s4_%2ZL?>k@d}MM zC{a@P%|g>Jwhh=SsScJn^=#spvDGqF`?*yW;HZqyHdQZtD~|umbO;32hhiY`P4cxW z#R!zl6~FfTEa{7Jk@GT+I23fJL}w2iOwUopK0c?2Fn&y^tgTJ`?v94a6^o>9nfAqG za0=(KO_F}!(Y)#*p-WjHRWp**t?M#P>GOp=PE3KKH&SPiB59yBFDmT}C9fAm2UE_i z*prh??X$GZhu(h3;$?HVa*lAz2L3k1YK}ycfDBatx|IdwCN)RCqR0C0Wom}TD^ti= z^GM+bI2S%(F4?KE3LNX4xvq?~YB$?(J1~Ug2`41a_V!+xJiM5JBt+3PsO`SqM^T5@ zAn8()M?nz(C({X~(jkQYOTQ;_*QVMQtdJC)8mnAs=_W6eh^HdY>Pf+p#l`*l#tKkg zWccLRVRXIDYRd^@g=%>x$`$>Z&oaB_p;rKw@&d)EqSu^DuEB-KGHt*%n+XXTvAJk% zvM{v|s4$jB-Eh2)sk}Q^I2@6OAqLXO=Q^98zfYJ@!j(X3sU7&^O)ltkryY3;3@8^PamyofD)+Hg-|vir*QCf=`9D}4Z@pL|AuXH zYE?9Ay(l`yq92|}L>(QrOdTI2-_5ehmU3Q>vyV{Nn*f122f$mMq_8PWf|&JGN@2Bt z!kQv5VylDcMWre(*kPYnL!*K=E+`iO@JOzg)F`yn4F^JS$Sm}bOSgr3_TxYt$o1ZY zF70GEFDoI_D6vX$l^*r-6!oE6P3-!1HfAA(!QIYFzHVzX@2XQMz^TqTt=r(6ZUV`l zC+UZ5)ba?u-AGmO)JHrp&8LZa8+h5^kayhBqLyYVIX}tnsxTPL-BZgRsD2F>5!p@w z#UDc@x4m=3Y2e@(5t@`23+N@ZZL#ff-)T|HePPJWMm+-Dco(Pcp(`naEwkAB7VJCLGvof*AP%UN)vify{n-4xu-A~HVtM2W zgl4!<$65=RS<}VvxZ)!qB5&-c5EihoJQ;m(n2Bj* zsPgvu(TP8UKM{4xr&FYuwFQLst=TNSaA}IJOb>}rI6>i;l-gbHB8-8!DRfz4BAz)^ zQOYN{&EPm!%qzK_SSg9-pO#!#?{uY?xBNn{|INC4waVk?C#?hR)4nlF{@d5~SU z_MLwi*#96)?I>d`FnipzLg_l5m@P1-C3CuoaELPVemRfTgj!oEwk&b#7%`6T!itVBcygugXU%3 z)%5#1SBmc0BM%|iH0XP81ua$;JjKps?Uf6=5~&JV#|WxXr^q%?7B zAeE1^Nn7`cBE>$c%9I5ow6)k?+Cco2rBakR7gwF)c*xqL4q*smE@a(LI z!Keagz77XwO>?%JA=7`b6*zz^Q_-lLG?j5*k9NdL&Iax8KO=6(80?^5$?1+87ZF z+`Yd(#1LMK!WIS)x6lONX4y$vo;bi7sHZiPHZH^lQ(BUX3BwpuXi$z}7s|$}W_ji# zqy?V>UYSeOv|C_qiyDGMDdsk1R^$+YbDB2XlwjhlX?f!l%Um69gds_5H378v{6wGzO~RMe z?I21+RJjHq#f8^^dM`!<>#|#XXARWT+b6>`lY`<@?L<>jOoh2SxklN5tFN zR+N%{(hX8>zfLAS-{*uugdpi97oj91uDs`EJGLTrt)+s*j7Wk>K`?$tKQj5no#>p{ zD(aTdO>7=()n)G&u1x?qAlRyI{FU?EF5CZA6Juu$(dJ_)51rQjffUAZU&UVskTj%D zQpBIWNw>AOsq<}7e`o+u-GR4pOSxN;m@ht))jo~AE(#3hY%dif95OOld>G7ce;&Nt z*?#q6AG6b#jQVB*YF<&tbW{fTZ2*xr$z6hFi1xACi$7=_dZ2<2mWex99C<+Min2GI zmnVZmAmT`2$db#3Cr1#|iVS2Z|Mi%2oHm}=x&mj-X1Bz~lQ)#;W=mPq?7mQ^19gC=Q>qwyp%vf3#DrFaLjdb6%tV{mEIX3z0&y;-3ut;6L>&U9 zROz`13M*C+sw$CKc7L0|Yi)+3k;{R!sY-)PM7>3h2-c^EFlqDsQ~F+cyZ4>$tWVJ+ zMO!Kk#iIWB1N-weYQZgh3XRve{Sy8wuD9(2`Zf4fA6^UM`*wTx`L>4E1bt%%`3OQ+ z)*hOC@To+xGA zjrt6lw$^*OVB$Jen6pp5W^uA)V@ndn4!7drof#uTvoIN=$Fgpt=JukqoP=jd*M~Dh zdKjPGgw1Xeo}GLgE-vZM?yOXNjlS&Q*K9aPy+58^h)5g!Pc|zUInk&7?p=1b3TBr0 zqR!gpPWgYtCv7Vhy4`lUDsh$`VmUtZ;zbVLG(&^UJnbr)z~eKF@6nqvl?kg8%M>yIs~9+E-+_2s3~ zk6c|rekr38fWKYEX2MSy=rYWK>5}qY2a*Rq^-qEB0vQmbp|Dr%4=+!C?n)tOM~JA+o4V6A9t>DmJtg(WQhn&%JO13kR@`RJuzIE z?=A(KiZCHvEN(6SjXGH0;Hpy_u?%J*k3_!reQ`GQk|{fqe@fx(1%b7{{TlA=Zi{RA z(h+32nJN#VAfv4zQ_uMIEWWRyRy##Iz)K*O9xcLp_Lks0#93yCF>#4S z+PTx&s<@@j_--3fWFy!-{b)B&KkDWVVq53rk3WL$k_J@*K1?_@ah<}LH3`%ug8w!# zxL_5W2!t;j=G8#lMaN~UHZ$%56l(46?QXZ~^6W_zN?PkJw-v|r_-0;vYFoSiY%;SL zhQ`o)Ydn4laI3GJd|mWj3N-YA(r)7=_4CedayR*T>mSO4e4Le3kWO#-wE-&nZnV|) z1P6~>=b9_N{@l0zaVv9&b9l4e3bL3!L$lGUrzT8!()f6X(j#-C^J4qu&hGZ$+0Mbw zTl+r^_Fiwl**e&Jv){hF)Eo*is_ZiEM7ZWwpdP4TpET!$1ro&yGGfCNCk3B)W;jc_ z*2}i^mJxB*nt9T`D>qk+WW5pH_!Yj{46uZ^$K1P;fjt-(s9Jvxe}Rv29vC*sz3L5S zGv8!vPst9*&JvmiDuYo1=(a4qye({j5|M_!FT0FMS9RPz0kI*&@8g?E)Tr!MD67Ff zAdgDj_tb|<*v^GmlPl&PDd|}T86GhgBjp$w7W47QbT0&Ph^{)!k5yl?C|t5LqF4Du zehATM=cbmnnB-|XRRTvYb~8&y-9#K`8LOqU0iG6UZ4%#>`RP=2IVzwIwX7$d%C(6h z#CSb(kY?%RIOEu6w4@uV`z^7)RJ0n&SIJ>kooCrZgW#|@%OI)W@C1`O3Z%=ypGpp@ zx@lEKA)(ajCC^WAWHF)hT+PTjFIt`PLGV!f_}K)mt0&2~Mns_LKd=lK>cdTs7$lFK z&O%y*KpJ)V$10oTtf-CEWPT-*KRA^g8XROX2hWXu5$5R0Q(A>;vl zbZ>*S@Ts~ARkCHC+2&V1?1vzViMbPn5$`vXha1V-+J{&fV)*N~)^9DId)Jt}Boxj< zcQ1XZol956fMmSrI`^*okEHtLPh4kticNF&7$Y)*zF93vqE~`;5n{4X#5mWi7cC-r+98l%b*hEmWX*xV;$*xMB^=EKBkdYB z>6M@e9mZ{0Qjl$8+AStzW%96GU|LyoUj~5_#LBhc1m(quOhw1z40#9j2pA((?+`lg zmq1M%-S(0BglMW#OxP_xHT_y(a&vNg!yQdY%c49Jx=LELG<e+tga}?>M=+IWDjOBDu%=;|OjSs|6XBwkLIN!Z zr(#k0bWF1DAfM3PP%gB%e(KHF?=j&*#cj#X(6KA4$^;`Op!pb9hHf(lo6=HgO9KZ4 zXr2W=kUD{f8M={@5E@B|O{`$)$b9N%@wjEOoV7OXIjwl4u&M0OST0h*s@O4?5@O|M z!jC$we~@ko*r6>>W&J{8vtH+8{9@tf1Ln@ z&^SR`f%p~SY*iavdzc7O92xFjs6lPFI5axTLjU6KfLUK%b-IKNZBsWPY^<~UvHlX< zJMUPK-`(5($Mfyi2T4C7tJcpUW=)jFz4LUc@<&MLQ}zOTf;ae!c-3S%>eTREx*Mz! zlen#lDW$uA67FQ~FNQ0FXj%3*ReNr&NTw)>F4d~fSm|}kD{Zwp$;%KF>}f<-<-E8X z+35zWyw2vScnaQIbVV@G|L5ZSW%4E~AG%Xz)iL`+nctX7TY=<}qMiG?l4Y zd!p~%Np>zqY~#WOlXRZWMq<5yn6c6>@K?CJOYAwT!qR->YvpRV)Pq6O=GsCY#=i4| zJzVSiFGi}685NX2BU!a4RX(9Y3ZZVS>=hPvvbd{y-cUj(YMzSam{?q+Yj^8l=NIjG zz>}OU{Kn#w&gx7t@&l}=S;KdR>uu51p=Sx@bGJ(UD4@-A&koO1`Q(Y|4QfFqPiv0s zEW?yNQ_%s*``$!0=|bUYZEFr{@P)b57cNmYy}gC{q%yGR$OM;LIz9C%%T@hhw|IYW zk(j2lEPT@~Tpq7qs=1E{Q#C#*b-cOVi_>s+QpQmaW-of`sf%JJf7i+}@>7X?t&}Qb z{naJMq1&c|J|$d83vY7JO&^+~2Gw`YG~%=rer}DP!cTLWS_6q%y6Wz@)KXdJwr^n< z|H7NQKETA5w;P^hKLv419B#lmI^f`tzvL9;&kd(AGgH`zOu%Q3BDpcI4`v6f=y)V^ zT9aJv>ChTYa>f5GqB*Q%Dx1)eWtqpwO?55$dgqhUSH1o_fUX$AC7Rz(eZ(4dlKwoH zR?6~>4BT!tIhz)J$^&<-uyNw6i?UkerD|)!RJJBvE*38p5Pp@CjI7&`&oF^pquD4q zya2Wu8uUO!+XufSCa>gLIN49^_+B4p?~p6VQ?sxN;w5Pl>^?b4LW6SU7CLS?<9 zOpxbn0y_m}bVsEpIy*^A(NeS+7PHxWT4kedBE-)jm3LG_m1xFRw)dknA1CK0@DZ+6 z^;J+6@AFw96V|h$mrXr>SL}_HXGP0IwU`k0Qej!WJAaxtiVK5RThF#%HD&VaFMhXc z?HXOHLyzdLHOa>by5t@gcbrv_uQSLKV*`8}XO9hETpa#x5ut0RC=7pG6Od&ud;x=? z4ndEQ@3C6KW}BNhLqr<;l8M&Tg1gDOb4%CX!vyX|SFSTr3RS}Dw6QDohRF{XOtjfF zr9X%}&j3^wCOV8W2~4zd`)!W?H{*1o6IwCc(N2CjCr(<7Lx&-wgEG6~i$&?yFfymTM2F^P_>1}^Z$f;%s|v$iR-!s!g9pM@XrLBaz< zabCI>JpQG8+CK*q5IFJvee0=f!#aV5UPue+)L)KDeVO#>R<)YW&mtvftx@Oa81=*r z{`1JEwC#U+1Qb%0_GBVoDqe(SuqRy!mLP`C)PiA{B$Vb#l_!sGoys+He56 z8HK~2ODflIi%1F{kTjaZVtSFVVtdXvY?+FjTCR37x3&MQ?F;HHzZJL(DF-d-9pk%a zeNiO&W{jhS1^>=;xe--G)jS{|YHfKCH3Q}KXr#dJ7Z|#vW1wJKsKEt<2R$tgnwQC> zsPrUyb{7YGFZSB!XGN!`Y>5lRmVv^vJcE z+DL0s6&=~OQz>mzilR{_F0`S58&))SlV7qqxb+R;=T3uaatYsfmLI|Vcv_)Y~c1Z%Ep(nV=I21C9veu(3rNYz?@$g1)ah^3ACP0$2n9tB$F&% zdtio5siRSTgzmjnVsd84QkM{z1fevzZAa-)Ml!XB^BH^~#(-o(((Wv}LcCA`-(@dp zK@TleND+r0<>ETUTu_Lw$!V5OU_cy8cjV3liiVzK&O8D&G~ep)o~LD`qP5KZ4+Abf zEP>K(>qXnp|k9d4BT?sccAHh63 zb@b5Ih`%gg7yRKcmS@uJ-{`1rU&IH z&q|J~S~GU4CO)t#u-&a8CDBTia_L06XjAK`cU}%CGc_Z`HAJ!@QB*X>Bw&bky0-Z; zpNyVe&{RWUu+xK`aeh<6(zVRg4CbArg=gL7I#;881B1^PQBP1aC7hkh)tZ3Z8)QVS z7dOS1csAr4-B+P`z-O)X7G&E&Q4=%nz>54EnLZEXf*MYzvP`|56Wqa9_4su~HLBIg zh8axK580^Y=9Z@`!_A=)6s%R_fHWN2-PU?52xkXs0TRWjh3EeGI-RAbwda`FLAx|L zOpndb4GF{8?2^0Pr;KGJKn>MGp@urM4DdPdsTPeWTO%Iayj6 zzAZmpDy$Lo7Ce(eh}Tu9Xu4I1?M{GS2J;CJeVzX^Y^s;DZ2>Fw|+Nyep2J~Qmbs;NuQ}&;o_G~%c=3j&UlN6|1VNukkX3j5Azg0CKCYy- z3dwzBfabS`>_FX|nlRPguGXg@8~Bk&ZPnW4l%6){9SBAdR_m!p7;1Sk3pC>5*YjK3 zG@OvU9G6hy!L~`ajoPoE;Af;kf2p~t1MGIz*TkzPO)_R&S5!yu+k)oNo7|SGE-y{* zKs3z9S604xk4~UoN>T-80X9Jw^rQzl3#Y&W3wn}Qxey8eCxZll)E!X~Npeck##yw+ zq9>e>qT%W!`;eZ(%waYd=cUwFM@xV7YRznJ-4EV+Ve~<$=*Ml+!X>Gp=Qwmd(vnz# zjcO-ry0BaFj>MAbA|#Jna9vL@YvM>@NZGY+9&91+^U?r9GF?kZvU)3AY+(g3JWRs# zBr6sn>DaIgj-lqBvMk!vAsa1U|Ch8Km_M~V`Rtd5KrJp)S<99Xf8)(Xt0Cd%8q#OX zZ%OmcUOh$EK#Ub%tTp>4$(imPx52P=t0=u9nFlvtY)-Enqy zcMb-xcJ>ciU2lSZU>-|0q0%l~Ug|D{R1i|hGEJ+xcb`+Lf;|}rdtG%pzliC|+E=NZ zTdmM-EmI^6is^su>44O7Gr8rul8aov*&BZ1%~QgryLZ*0vd%hHI6-J&wNv|)bKKx2 zX{&3D>2K-Qq8nhMDQen#Zit2z8g1d)>2x5v+z7syov;E>!Idk_pgy~uQ6Y6}%9|!x_dm&)L!D-^NBjoM*&#wo2P{YG~6|*UcmaqN}h1}$XYudfh z?Pe!_)i;MUX*^h*`h*d@+opdB|c#ulG#xn?R<4YcAnVbP2)y z0{+yk>;la*3oM8bbFKn)2kd6&FVMB5i3EmagqU0~oR?K`IuKG|F?3>-8I_Jp3;g>~ z{RoUnWKLD93dZQ8N7>3`rsiQWpHwl|Dx>N*EEpe~^I`72*Lv)1&}O^S!>>{Vniv+6 z#*0}LM{TERCz4As>_fv>suwE#e%iGD8dvCBUn7$!$l{3fF_m#qHeh85S2-=|C)4@iI3L>802!czbOv|`FdbiBG9ZZF zbUH${v30H*9L_c%;LFd&0UG5s@O_rvJxByS<4jmvy7-K%EH7B@Xr5X)}4Kbcl6Z_;ZOHozTDqFppRWwD$knQ zL?;CS`Aw7_p@17{6{3y8*+9uxg3i^IpvSOKAlW9?{qmAlAZ{R?_dl9<-|C+h$_H4~ zt>nO^^Ku;w^w8ia$>fsVoxRl5u&MD9aZ1K}=hngLG|}h441;T1K>q4Xg;OABZ5a61&9X} zdy^U<(X|n2nPnhS)vw~}+H3Nth@RPvDG6VrrxmNA3lgK6PzTu)y+(4m^d39D`%Mq4 zN$6MCE@(NFY|O8Db%O9sPP6Ky7)eg!Lw4bF8$V@qgZ+#x%X*K3P!tq)D7v;PrYfgM zspxsKlz{^LngR+}>{CovF7j1YmIo)PNtthR#;b*!2Uq&N(Uh#LD|3OjO1ygvT)oAc z(^;>R?^bQ&ZW~IGtrlyW?&FyLDb#16%eo#tXlb2EWZAZuTQR_D>}hj86T=ML#KOUu zi;;$>)3!TRmhJ-jS|Ewq5DMU3BfFy-BH$7djRdeF#N!n>8^%B^YaX>H35k&+{!h|j z3(In?ww%f^R|dD=;HH$xCHC`J^wUq)9!g^Sw(!cRJj5WI9)=~G)bE-d-M;v{$4xLYL~o&}snUhhwG7o8^D)#% zq`w8O*jY1Eqb*P*xUqhvSvDzGal>WVP&KH~BG6leE{~Gb9O#;M+E-sAo=zzGK<=aQ z%B}GFz`_ZeZOSy(Ro5Cq+Q|E=xH*r~uxioYP!nY7hP6t@zA89%dAYN@^=k0^m7p?A zAq-lZS$+o4vaSYOVY5`SudsH<))lc`FhM0zdVeFW>(pT#I|Xb}B08o`0Q9 zroRGlMd~*3)E^IrXqFnM`2^~~&MVY+#%>2$i@xPu3-&f+a>$ord!Nzv{(?I}Y7Xlx z{&KMO>=nonFSg&j`lsLu@%D(vVK-G-UPEcQ*l^8*evIYXPkyni30p^YGw~s+Q6XiJ zqKk`EWi>r0&uLzov>i(ogC%O^-kQ@o0G~#qOB9x=Xuh!5f(1?S- zyp9joTnp@;a#ie--6o$Hoi7{2B{Gwy@ix$EhL7oVNGAtf4 z-U@aAuC48lRW>Op8`+A6q~dDjQ8o-M%&4h5`Vumrr@5;CQOoIRiM&3VhNfkrY246D z9&L%RtV!?uLEODa}ybG{m?T8B|10EXv!Z`$&!(_2x4d z@U_6?fk|L~wrFDNP%NTc5-0v!o+rS_Rl-xuU6Ga))JjL_zx93D3mj!4ejFxn8qHMb zuLb0gcj(gwMj#E|VLwzF_#Fg`!^K>*xO=*%s6Cydf^9&` z1I(^(*aOPFK#4GYqrdBk8)OG9CO!}q9Ojwp2eV}7uFJmo21_A%ThI_QXn2|;<>;qHSvBQ)^+%p$Avb*y zFAIK3G@iqjdLyuGT$Zd66Egfrp4x+8NviSI*GF{XBFrixsOhu4inMv`Zy3*-F~7Bl z;gVVM?aWHhKGcOCVvB*%;TqzF;bZuk6ICv3{YxoX6(>f`jkm0sADhl#RusT;VjSaa zAg3aA8Ey}3d(C`g4?1I>P4vB4sexqjIm7e}Vp*jeRH>O%(qvKgjDkt619nr^vCLii z<2;>-;iW1$`v@Uhf{Udw4NHRl&K>r7TyFXdB=AEzr{t~?rNA8Ei)ty*{5 zrgaIKOJxys$5UWnT1mh-iwwX?Ai=YuI!S0Ln0G?j9H6cewqKLRKv1uVagRWpL9L09 zJhB<@X;fq-=p#>4Fpv24pQ4*#El)x}3h#h3pW84{Ph-Kx*skE@ADow9xb^BDN-NG0 zsDSg5oNIdC{(@T!8Dw2I8(RYvvdV_)94e3W;Yl|9Aeu-X7jr(MgSwy$a3oZ3Wig)( zGb&y<$wY_ZYjAd%WhvNo=T(`HNFPo;`31C)8zN6CyMoj&YIb+kJlzu*yoQ)F<9?OS zD(#D9e5<~2U3@@|U^{KgTb@Vg5sGSGcNaWPL+z}$JsCMx%BY9WwTeu^#>?6`9dHr{ zeT9&AQl8M6j|glPPRK1R#ciO(-B27ytUjsN)j@G9oh?5rZ!@k`g*gFy^n3`#cC?IL zcl+aXoDT)^VOCj)8AkAxh{jaKOWT^hc=QIR_{K4_uEsNJX=P9=`TV}r{8@HZ-bI6XG}aGtUgy8HT75Io zx^qj>_xND5PR|!+ide4>m!nI)xC%kby#?yNIQn|0pT}ISrvnUYSZEeW?duzFUP)VA zNzzHFv}3PscEP1^tF5tHO}gMwNuhrz_h{s+&WqUxnINfx&!P#kiMMU z}ThKNVj}}*f=$}BAXZ&^mO!`9a^2X^UHw+t1j!tAuHV%wJI>%KfT4|xKc70i{ z@@6$U4%W=XyFoAD0IR=deLWR}7I~XJGP1u?c|p;ohinzj{*MZ)N0LSqLKrtPdb^oC zym`J&U)Swq*%ZrKoK>>{hR{#nQl+28^yf-^$NFH1pEUO^h0&-1G#CbFuB#%#jaJ>B zR;xq)0+d(1kRJ0^r1cmoyTgAr3C0~f&5jqomG9OHr`9B{Q7rY6F0TOz(so7eJ4|fY z8&>&@E**s&EIiC5M&t6Q#<(lCZ?Nz?Q>(ZHiE*G!&M2Fnq$;*yP|OB&5(d&S$|_*= zCGp)}h8y40@J9Ys(J-9lqHto}$qT4?cSN3TuGmaV2pzgpV1etm6cf2{=9u3qgepy( zq3rK8KV5H|3j0v{8uvg4)aoH@571a% zYm}|Cz}gB)JPntZG#Sy4<%wLojmFX0;2s%h6%LudLWbCy;&b(#+Py5)YBOMhhF@N~ zZcR}?OeRS(rPK;D2kiy-I*XjS3oKeEaK+P4AGZPP&c+ff(WMWU4Qv=HjhiS%#frDe zQVBRJx^pACZiwcLF>Q6cwY!WZ?NnW0KmGYw84xNX`PT!lRpD>gdaS>dJ zf|gbw6A*(CQ+~nlO@oQFMfYZbUo=x?!ZcN;lDZW8*SN~A8E#>dAeA|#U36WH0zHo9 z1i}aaPs|y#v=s4LKd!#pdDIFD6JiU_vPll7NvTh4!xU(3DYoLUILnmqp?U)EaQsX~ zKngh-Ly(S(UgS_pWmlu)!A_^DX>GM=2Qhz zh>8cF2z`?qE2;JdhmOqrh9bAMX2Hl-hZp$#sglFR&3c@bmAqsJ71Mp-4>a4Lz8*YD z9vJ{N+KAb*abL3=1%3sIPy0?71b(r*zz+fONdjq-_Ks@f$Lkrk287MA`N|FM)o@!i z3`vYKNxGzaS2tS2Y>A4Hya3}vZmjXZ=2jS#YOM1_U=U2n0X%@bdFN&FuX#4RATe#W zraH`vd5OmUe4MRS`Kj(s%3I6=*f?_5)9!|qE&V=)(?%i}=B2JU`NaJ+YzdViv=od3 zh$}d`nWWdEk05Cb7X7T%PzP=Yc_qA{!a(|BKy|uQ7f`PGv;@y0frSvPrGZ)_^4CWV zU+<2H-4ZnYb`Z2n-WFeCbZVnpgKQQ4rD0{2*p=R5=mI2R*`nQsBO|e0+Xs!sL{HOr zfe~9EO_~f2JS9NW!i{Pw(fFh1Z??A%bUMgd?=&{AP=Sw{g3w>+R^RlFShCK5Ri4b7 zS$UGqqN%I=4Ws@*XCUotNGxoh_@HFHOrJQVNY5)?hr!%aQkrt?uLGpt{81o?B9Zot z%S&s1{-rDUq;=bW_Zo#DLRAMsb~_EY62tP4zpkV>zqlJzFgs zoA1IY1|V^>s)P*|vS{kfs3XIfFiitIXs<3NcM+K*;;zE(e9u)T#_J`?Uy_@5Zk4cHm5|37Q1+|AdmkyY4xTs|>o~y+vOk0dLPyCT-{`V`bdS zh6706Gayso@*kt)Bz>%)rpbu%AoR#x8gzS!xD(q*mFM*rAL(|BOUBaH!D72!|srNo+vXd7#^nbU>k@wh7x* z>LfP;MezIxPCe{Yu@h~m96}{Bh?>}|?Zjw$$cnmneh%`o$`mwnXNQ&7X5Es!1zG1u z?w}z=OCY?LLfI5PcP<=jS!$*lA)dlSAozBWcPK-@O~P2iHnjw2Zq4*LiI^$I7P{&v zzr`>??DJ%BHR+8FSul{POL-h5;B-Sk*fu%AXD2%4C#QDFx~c(3N*kfB`h<&n1No zni3Av?!wD7Pl7KNFKspjh1VL2pN&CU^gmzao$rGgydH@;q?-*|;UnXw~m*|KRJ2@d4XgB!xHx1P8O zU*bb_WfR!z$!pCs^d;RGy5*^3y%C`ul2M^?p zr-mo~R&WT2mv;>}{M)1z>;cwgF7+hbsJ-e4AbyKkhJX}8d_{DX>WZyy58T_}l@A-LWT*{Btg&Iu`zS@RU7^6jF9 zLitIxogwbm9#mjL{kXhAqoLiaj?Re9Sk{%ON0yO<){Php9<0(_M}i0!)d_%q^}#Z{ z?@|j2p@=05*J`Z08bzeNa1meSUL{vP*%DW&rJZk5QwGU9XPE*aM2vS^+l*+L>Zb2T zyju5XfKm$jOjMGBe{1L?QY>>_OD=Da1!%h3RBJK%LNQ-13qM!BXtX2*VzV_VcX+KJ zni4xA3l7M-DqT&5LpP{eyJJhzeFyG9Up`Vn`F4V8j(&?k7m!BwKKcknP;7`@2R5laP>)}A(w|JXW+ zCpCat`fBc?wq5&sn%=_U0ujxMR>LT5Mo85{HbL?is;v|iiix5{)5uHZ*0rTc>h69c zSyH!^0-rSE*juj~c~b9Hlevbf!Lu4b4K4+b+Uk+e?BU@~H+7xcbVWKIl_pbVBz!_nd4NF9o$LuwqL( zmN4(iR8s;g%M7gynEp!$6V*H&k1sIy)#L)TTfD1$!sy^3r3-_6V5g#cprl%4yDtZ*Q0%QRz#*`?yK z#9?abZZf~hoD&rFz!JcGD;^(StZBdYGMkxPUzj%YbPj&R{+Hot2u#F41xh+2Zt{Yh zr=iFN1Qke<(hILJr1$etF60v*C~Ud886rh3m&@n!k_AMG#od|@-WmKBm@89t%#O=s zbs$KRqnTc~Qh6TW)3wb3tb1+KzWM2mHOqJTdrxlQV$_w8G{k3#hZ5Fbw^P#Bcxsw6 z6Y$sc-46oL@J{JGp0u_I)fF@o=K|H@$u$r*z@@hgtcZ zHAC`-lp;QZ;AxEF<5B?H{4$QJKA0*RV<^tVT1p z=qDfsU_uU+!VV)#J?`B~4ens@!UC)f1T08I27TEk+K#*?avA=o8W|g)od30b3DgXX z*wP~>st{rY_WecxcP$cTDxSi^%^iZ>#VN=IGkW`$~Bjdd?_!ppa_LP07vUOckJmzcB25HcKz0pGPtC zyCVaNE?rQH!P8@kQcc>~aW9b&?)Y4vc54I9I;0+I!Wpn~QhFAHkS@uL8^cA)MD<)M zkj$)NG`Y2D`!S%jW)nH>Um8VggV>SV*q}y3ZJYXuYTX!$EDt0*X5!DdiCq#0x`606 zj#kjt1-i7dhU17|*o$?Cf&OZ4<7IiU1uR`t(qwyl?Hn<^6yb}}OOZAWlw{ciLVzGC zLqN&K`)jUm3n|%nPceO)rTcPqS?&s;d{Uiy%u_o_oyJqGUy|x3zQqd^O%06HMu$Sq zwU~(_tN_JXnN~|$$bBhpncP_mMYvlN-xi2&#;1&vVI8dzITRjUt#v2YW8BqUE;KHB zJ0%gP&f4J7i$yj)b4Z>Bv}m+4ah(G>9t3o0p(w%HeRIW|&?1eH1fPe^(lp0K;NX)E z+oYE}eF45JVo+GhkQlVBVrq?&{2PF=LS71$%2=WlS*BTbJO?*{kPyx!Z{ zJ=lIzQ)e;|-k~ShRZawoHM()SZrJXB=$My|OwaA6+h+4r=|373rjX`wlZD%4JqmXY zv5y=?S6AS*{lt5U0W6o73^B0CVK_sJs&~;I6pZL7@2B7=ikmCM9X!qi2-_wBTZ6F0 zrNI{~#t{afKVPa8H7LTXk#E(S*-E~=5Zh3`lwg4doox{6_8_rFSN}gi$^QoswLb>M zA3f0!PhM%g1+z8?gR*Ro>@KISW;v!Z`eTs&0Y$kW-wL^+&(TY4 z!uumP7Lr|IGAg-{_$CrU({q5Ovj373VDsd^s1n%-LB3Q&IebwprbmS}sX07NR@-9l z??a+P3?nXo?=2iOc3=LRrx0(IE43KeTPXeewoG_PtrQX5e+u5{rIpS;y-P z>LIFm1gj}9dsv(14Cj*OaG!zqzUs>3b{i6p0F?$xAt?0ep;w`*%ulCd!d3ECJRTQ^ z>DYAT<`4914Y`DAS*q@n9ZNawCIL{~pv3?Nx@iui1~n#{l$NL?)^!CZmL%0`(X|x2 z2wXb$bt%(6ab<<&cyzfah1M$SW;g7wbY&R!a9JM9`na{448&?eqgeA(P#GvZ9aJzw z4J;vUfL`K>qei8iw+=d8Z749FrGL_uchk$xZ+pF*4cEvqRFh#S>K-1-G_)wskcIZ4 z96}qG9>}c7NgL#rFg8B0t`lX*S@ZwyJZo{<{TIySa%&7GGG9}{#0?HF2I*)t!-z;; z-(uOLNx*YreV5Q}HF8k6MfU_>!xeysDh^>ji|K8lR3n}tFPSkh#%G*S2j<-ZvNLil zL%dF+b2mg0taLTCN#S)ri@S+3csIy&gE>W&#^tqGGw_=hsd3}%E%LsKeWRiPY9p=Y zIu4&*1q%FVOKwBE!@MdNb=27uBP!?{?4Qycos_}w4GX3IM*M?uvDVIVGl=+L)9vwY z(temcew^gaaX4zL=gDgFLpNbl4F$fbe**z)ckCg>i2vXE$CsT>&9=xrF{yEz15hqP zzfo!RCKVNYKRqahnyV7y?e}JhH7jbbcWT$PTNKk9FUH*))uPc18O$yD#FQmfTm*6z ztU83OmB(0O*^=vDkR@&g8*bNSa+*#AYoRa{ zi&>jWd8+v@x_{8Vs8J6yDvvA%yufQX%_qq?uc~ndRTXkEZ8W?=O;_pXE&75|jp*l0 zy#s?+oT9#hb5@0*DfBp^jNN**^CJ{R;akZ%IL#-rzzw;%!dNj9|coC&L+pz3DThVHK7hx)(njx?UqS97%J;C5C{p&f!yUr zVyL%jJoaHI$-{g=ZM!>n5M$ndgFw2g{kh@)+oShf1_RvM4?Ds$H7TnYu zQ~cn~7to=Yd9D5|MtMVn=%=injVrflq0gc+?8~sS>PoY(iW>Vs7Lcz5lfl(Q%?c_? z+bF*!{c=98TH*)otDn+V~q72i{?#_W~tf$$r z@K8SvrK+#H5`cf9nFtylGVDpsWnX-$=OI{Zb60xX)_6GHiTpQ2n7>4Ge4}%3cP0&# zHB^)X&tX(${Xub|$vYa3g6t7Juhy-i(&kMkQmL`_cr0L2muFK!`4GJ7KU6`qMTU-9$#Vq;w_=*SA-tfTI^URaByA zAHyy&J(08m85L0%?T?*^XMx00w?MSAP&B5LxF0McWf3yB%>Uh32u?2oQXozSqYBvB z3FU&PA5XKu(a+N_Y$mJ0URtiVj-o!%3s+JlFCA3fiu`eJeTjjkZGv>D))%TV93tQg zxrVsLr8)4NNi~>L7|BoP6;o$K&|Io7sOf+<$HQ>(dcaAx15iq z@+y;8%Uwsk*!999TXknGhS`Z-ZoLujdW$sKo>qh&JBK4qux^Vg3s|c2>XDf6R@T$z zrc^okR7(vmaUp$Ph%;eF0GCSN1e7(Dx@LCuR2#~urn9_+;;hP_LRV!aj#?ZB!IwiE zTk;{h;MANTO?A4U)1R^n^egz8gbSt4@(awyqBCM49is0&S=N0wJAaXlv&w4bs#LGE z$6mzh_UxZfu5ai6Y{%_)hZhy5{qA_ZbGw=BZ+rKJtLkON!S!eir>vNZ0d0f0hlt69 z-_?RGt#`{f$T>C#WiS>?P3BX|Ia!l;H$15%c#X9`;8r=srUzDC^8 zuI_tvqMd*+vFn@^gadp#0M9FIAkW?g6Q*H*=F+ViYd%XEdR$DPJT@GuQ*nySGRVcN-&Vi>tlxj$@4vv6OiJ@`yWfA=@BgUv`?R$5Bb1SATu0dP10CPXp>nwRnUrWHdAD$A!{U_X#8dJ)H{tnN+2UEbS`|mF$nr zL@e{E_)qjJ{#kIMvgh*?z_E6q$zwD9yLUryGZ)j$rSXcv>_%?QomPU!!-GMoA^R~QE3q)vt}bcjV9uPs{FAf~! z{}{Oe+LY<^M};W{XWEL&AW2CyxuA$5oUNRzK{K_1V1y9VkJB5m^WdlRWMt)7^*$+pqvdK z$$>9AjPleD}>a@bAO#zg_45ir>~Be)sU(zdii=yRRR8`}H?puYdhF z@%H;~zx!LVek&_n@G}RoGD-ecofk*r;yjy0+AVtj7yWs#x{|C4p>%pN%a2d0O{+3|xhH7y^!+IhacyT83ACM`dDI~hY{kxG69G5G0J zOlpY6iE(<4iN=p-GEPs-;#}NDXp%3Bqv||`oJ_09C@*EK6Nj0aqF6a^0Pf|Jq_wr5 z?CiH7Vryr=3!VJBbMW`Qw+G3u;QYFKu(Q3N?7c~z@9nE!6IsS59<@k0b zNSg)0XD@(=!V~}g9(;It2p)V#hvL40Wc6Z`0AovuvhXGB3nK0DJRb>^Rr{oWS*wcq z?5ht{PP>F|F0y<)TJ0ujc`+Hnzmrjdzb54gh%A$P6-X`9Ss`%x6cdb{i&G_#G5L^X zQ<)}0Y}#bSa`dH2<7%XL?;#Re++3~nP^>2%R;{i$aV^1-W;1bgK--=l;WPMYm2-hl z)Fds<<57XerWPP8CNF?@En+2AYuI(!`GtUMKE%F-0IMG3>Zl)FwCcVE46Xd5l~2s5 zfPM5AeXvUX0H-LYW2T7BXT`Z{f1c)*{9PP2IfvFbKb)nr3;Z2;Z|Vk9CYjj?)@n9M z$1r^%T8S~^MgM*QH<3mz5O=WoF*I4x^-a{|vGr;TwGE)Gs#F$>SusC8LHB2q zTB=m)PB;_CUcRa}iVj`rQdD+Vq}G%2SatCCVqQ(f$-xr?uC_qmj>n|8?yVre#eF)f z4BOn7PW{O<4bxlDj42u8mwLS@&>p8z1n_BW-=c-k`*G z*}iKx>DX;lp$Wg0qYwOW28uPSk&#Hm%RAm%Es=aUvsHw>;aEsYZOiKkwT+a^Q9u(! z6_dQ9)B@G7DKZLew{EJ{gN;kjU4A?%X4yb228E@*;3VddCyXe1SN*9HJ~$o7x8q`% zjt44k@V5yL&y0(AKAb_qq?zTco9G;Ql3e4`bW2RjKwTMwr~ZVdm)rxS?c@0zKy=_e zxK+=Sw~);D6~sQcfZ*Rk0vK^;_kmd$aCSuffp`8rh|GYQA%oy5Q`8jsYI*Xvp5#~t ziU$)IaFV5hS+nBS#V$%Q`-N(kX9DwJ&Rqi&=7+5`2@DAz)nehJC>ru50Y*M@J^95W zwJD+xGaJuN9`OB8!1nb}zA7M~ZE5u$1pQw$1sjN+0sMO8AQ*T74G5w10z0^vEg=?_IAPOwN22S3YygOV0h|PwmYE>zBm3O!3knP1aY54vc0lU~jer-2 z-EyD{yzdR6L{r=QmM~ieS~5g(b&v*vR(eaV4;J830j(=K7LH#L?w$p1Ph&pjkHtt@ zE!v|DZtaQ6<|yrF(*ks5&_G;_qxQYty^dwYg!gx>RAK%8L_9PrhHq!tbPRTF@uHhN zM6WGb=WxK zzgc>kl=Sb0c@0Ver%P?=a7F^!1QtUVbFiO+dLHBzF*uwh+ic5mk+abrZ%8G@q``-s z-uDnQ(FkWv2oqp#c*atez5@0`6?nJuSl}S2nF9MjfxopyYb=4oPBt~UgF6fF>=PiK zpr^eFN(6CllfH8O5}}uMu!!S_Xk&#(fQuG^hB@aG5Z~Iljbqpq50kqJ96V&?EK?SA zs5cM(kR~kax%i;q(+;4iN@ozd0llORi=6NS$6N51CaIEwiK9ZB2nxmr(1(M}=)&CugCeS4w-X{?4AC2cD&y&R7J-lHS(=YQIP0x= zNu^#CLlq3e4ha<_uQULmQB7TRcKBcZe3lQ^Dv{K zW^Khi$-4JrM+HD%6rj|Q$_esll%Pxwg`}&Jh^{Bt1(-b4Yar+>d9U<0)xS;db*UGJzk(0yS@r;6IpPD|S=WT``19^qqEA8Fefo zLHr*Gd;vLaRe$Ye3ROaBP%~ z)+N3^%&URmOOH$5YakP)Zl~v?F`aby=s77zPA)ZK>sFxSfEquL+lmTjgxcuj#=5WG z0MowoWOX>H+jF}nyN>-KSXL=6L66x9&x}w(hT=Yvj`FHX-TH>nCVJo8p}d$qnH{YZ ztgWnL7Z^$(M48S2ZA9RmnqW?M@*}&qGZ7sK6sPh3ADKR1&`z-hAVQEMT;i-kt@>Uk z2v>3ZBCC+hEUn`m{_{TA#T8epqN99{ZuF;FIzc0WKms{U!W7l;M97+FVCk3?XuL?P zWIjPp(3UJ_(jp@;d8c8&xWr)!CHM027|j}{J8&Q}cOyq)TXQcbZW(C-6;AWXFLHBz z9jHD8I%>t7@}K30aLGE##oF=GkJlVee&~&NwGzWCIXK#B6jFMJ;0dd7+)AP-Y$?(m zPDsyDbY8YQE?eZju3^5jGdHUMu{>v-9!8m);AnC(+@b;81W#e(kl?cj@zs&%^p&SL zc6Qq$f_VF%X>ts^?_o=j-rqrmP%izrK`NG`(Wh$K!+Om4dz(Ys0c9y(M!iY39b zP6PZ_M2iz)JA*8$La16%%;7fz`z8Yo$|VsZxJ|c=96)^GR8nBorVt{Q?1a;Ji^>2x zx2*+}8TvU0z8{5rJfwuW1(qfT+8@Ts1R0SC0~I)D?oB4+vJE%uR8{S85cY7^Ff}=? z5Z7|0K@?LbsxfJL@N06Hm-{0(sif@|n$b)`s4BHGqC680Xhd{K#?s4EEDmx zOtF`mLBKJ_+*39LfKwIHk=_Fjzq*MJ`2;hKt3u0Oht9S(?sDPgU>X(AscA{T(J^cU zI69&3{%8!)S-YZXC-q!d&2$gB0^SgzU8&uO9`T)?ezj(CvObQcD%KThgVX`l4QCgl z23DW9OAA7jfKGZk8mW3%u`{mru4Y@;HnpluY_NlUcb>SkHL&B&sH}_P{`i5ewcmEE zm&jE8a#HcR+IS~L#~U$7hcyfmUg>q$eViAm;XUeDqgqy|hKp;&nnC%GT85+<`!Oo! z=E}Vx_aV6lcjtdjFAg)PimMN!?GzB?GQc;09kZuNYY)rs$p@{FuUUkc)bmOn;={!k zN>Fs|OK;%~W!4!;R@B<`*(HjvXs>rRBFEJ=%=iLvU{gX)F=AnZ8qqoW6Qa8d%(VVb zuyBzjm_y&9V{jgfm_y9y0rLmtn$q8^T>iltU4FTg#QxrLj|oksw$fJ2TH_UXXW#pG z%nc`G$a8c$D>j7GSeUIFt}*v1+aZs8a-A;uw;j!HXdJBLF5E^dVdLIsDyS1c^yN%| zEGt#O2x#zM7VTyZRKbnrRc;zv1y{@wBqbOd%+ByjaL4wLKjSpF$}ea|pk=NNmEDP1 z7>Ned^-Pq`5K{T) zs>1>;l^t>bS}XP29%-+P@(N8CR)*z|*(hk0;wDRHs5ah;P;D*2G-+Iu>}c(36o0d# z5HclrG%3YiI9$U143&P*VAM8^9Win-w^7&wQs~JcyUBX&v#Y4_BBYgUq~*7OXD!ZG z_CtG)-~jAS%*H_W>KX_O1PJQsjiFnkh&6`iKQT@78SHo0+`p@vo5`e@3`{Ld3iv65 zwwzCv0ldEAhhMJKt>!_89YNr@Ph&AbF`u>i$?Mq%fzg}J%aZ|mhs#L757_{q*`_hO zijT2xRPbz8@wDi7LKSdI+siLBpfRJ^o9^0Sq?FG4yUP&I{ACBZNm|!v9-MZvKkW`J z656QS0A-G_lJ1ZeMA-7|^ARA^zBS6dYJJ-jzXtRLjNHvjLBQUEpv_DJ7N4C@`0bxh zq`TSRUI)}rA+It;>vEH+FceuK`U+g|d6n^Si;xp1CQ9?aoRktkGIRys9cLBzRJBX7 zSs!#nP+t@sqWdx*1zi2E z(~G10GcF7^=po*ZO+Dy-4zDHGE;myL$=ER*K}x5u=7oIqjczNelWUoM9lQ-+1F@Sf z+&X^K!O|3NbDY-~AT;PXVSxc5m)KboAV3$T8z$g>tmP_7b8RU=7~c75uj@n(S-HWC zD|@nKgh0s!-v`EL=PZ#C`Drk6o5BWM@wiMeZchhm#^kK9!;va&rait=(mw5ZE`Vl( zc^pJkQa2wNq|7FRI&T`=Ul@Pri#iV*Y!S$Vt{iRfy_GaxPDS)5(WmZ8lx{a(YINob zfZDrSHzcE`3}&-4b1}w+-WggPA#*zlQJOi9rB9o9BCrqt+ zbl#(4a=m3kOBrIpj^4ZyptWYq7i3e>h^AK0n z6$Q}p^)9X6*OvnbWxT2ZL5O;UYE}LjH&b{X`Zd3kEz!r;7P@-3+jq}(^zP`X+X`yn zz9hNCf+hohojgK3-TwoNZX&iA!L?>7phK{-3bB&6uA_Io$Fn8SO5IwW=xWQGOz{>8 zh`W|}YTns46L&n`64Ln98+8IU3rAz^xj7rtLj%i0k07QL=s`Q5X^hN<)7F}%Sg?h0EwK_mb9lV@T^>!3N(No72&J;jednhrXLy+{y3MYayS+^_9G&{?V zNz$8F{Fqu_Mx!7@Ey=QYbf_IuB3F}iMbQnB{@U_yAYjjV95icDce>*xjwPXw{O(yX zx@cl$(7PEVKS*?L*ki;#!%u=gb4^OVbfu_6?0}&zPugETXTrD#!7Y<(M{NZ_!+Rh0 zto>T7e_5#Gs8-P;nKtPfxCVdNyQi6zCTc%>>hp@bar>oFq33{>(Z`$xa+-xNQrF$u2uW9+ zka*=SL1q2ZD=CyCL)}edsH;cB|BA#FSE5w;kxSV?2=jX2y4&?P@|RAgSqtsEfekS8 zq)Wz^QhGQympHyLku!QUJ}+T+N%|ro+O;o5a78YB0bK}abU6)->2TGUO(gkGL{==d z*mA_T5ngbKDF|>!26KTg=j7fh7DPS4s-EmOnK$X`Z#;Y1&C;{?W#tBh{ssZQrW1I8^-%mbTYN6Jlm!I~oke(I6phq#8 zL6iGpBc^g{tUTaydaX+24yi<3=y3vc%}u|?wnsO<(boIiy0=xIg-wWUe$tRz=+;x` zt4_@O8tO^>m3OVt*$5^~s<&g*=GzzLQt5_c99D6R@DGWAYhpznLvN>3v1>F_08uP(fo2bvra;siC)E#qY8TWG_~=aXg^+0$NL?iSTHW@mjurC8&V z5d1u4Odphq%OLKBeW3jLyan+<&`{Vijus5AwU(wQ)NZiA;C;~Qou<=v`w*=*&SE4_ ze%R^sssdKl?r^g={5bhO7smKTcbEq^20WIm#qJ&L%Vf-rw$+{UBDaKV!_tA~VedGr z_GZt?c1H4!AvkfEUe!Zbxc~+cAtsI6>0FTM8pNvjHoKgnI~kO_2`)#3Wzvn+62h$= zSm0b?uByK009h9R+b~&&@(GSHQ zzS~#+^y6#?l1=Ag6|?tIMx&lTQ8AGAhi?0bz9C`OdBA95lR~Ibuhx(ss=zdRW%a5K z+mIPFWBOR|$ut}ak7Rxp@d;E1c?;>@$|3NiPoz-PK-c~si`DH}tZdQ8+ZO9a83c8> zw!y1cgX*8o0Cw}ae;$KbgA;EnQXSLSmwRBXV9Bs%vatc|OPBCii%)3j`fP@u>mHuJ zrlE*t^of^`dMKr~sI%=|!@v~ks*zCUlV&Fb%9>e|a3x3PSFmlYfw&Hcnpm-x)&H3p zHHJu-8*XHnx`HywSD1;#q=kW8_A4t$YE?QMP(Snce z@jG2NOY~dGT7KAb`6e6AXJvktwe1NySGC=pn=Zh48@Q9rQnmktyqK>H&gn*oXzpoZ z`?B)4&;RrC)z-nYw=Z9Azu8}V*n9ZsK{*>f$VcOW*fTL{A?*&Q(;JSlE`GlI<{S9; z;rHLJ^MBXZAFY4)@aymY_VDZPzJB!W*WY}-{`KF)v-L;c{ViF)l@%`dnFCXoB!8>U zizCcy5NWsQ{a^Iw!Rkt~nmjM27qk5Mq)HyGKl~y2u_&M@x;Rn2WNSQ5@Kq`BSsb;q zY}6BZ@S4Z0<9q@>7(8{mS=>Fi1-R?POK&u#ax_^ z3n-nE%*%|5qrh=XvX8@T3iU-2sFOO*Q&7!dinYoN05eJcDf=i6p}t3wCQ$JeYNWA- zNm|KC!_P@oP5TcXoS&cfQk-zFm>oYDQ`7RntDWcDyZhT~V$$-Xw-YS?GRyuo&u3!6 zhZjjY6_Xmmb(xIQ^8`}89nZ21^+sYApbkV+R#_ZX=jkkiZbo@23lDLasVRz;^9EQg zDrs%)Cp-HsP)P3VccGJCcMkr(_x2$9b?eQWt=)s2?fqo$P4awi_r=b^&fc#0>t(XF z`_JU3o!uARBoh$B$^n9J6e|$Zgb<6IHZj@HGHVJz9^vnDnho=#e3-xmG*6GS_-d=gG+L+NauSS}U|62#;as8s6bo)ph1+YROVM6YMr8KBc>j;FE82>(zpqH;}dA$!gMK{Nmu$+Byu=>v?-Z(qOOe)D{5e_Op) z1r>3=YsUmPiP-x5%1eoGu2Lj+z zVn%e?l(>j3JG7$h$uOOk^D!pX6e}4Q$6_PLr0>GyD<#lVT{Wwh9Kf%lX>l&7O$iV{ z+Y59?A7K|*7#OK2+H}a)nn6-20f;Oe!3A+R6Sv9<6x5s`#hT6vG2<%FN<4l->XR=( zmC=J|2YWB}+OzXcpGtwH!w-VCk4n6w1ON{6ab8_?lk*IUd)=cujrM9-CI3xTzrcU( zzo9ep!%GATa-H==hjXzK(GT5&QUgrNLQlSvZSavQ7xsNu=2bOt#fU{j#;0mbSmu-D2iXDHD))}e z-2YeCdZE{_Za+)%J15 zO@YH~wy2$bJV^z6Dx{cBZ$&>AqK^7KQW!>byxV&Q2PG3%3|z;=3<*pEtzZslmX1ro zG-0kRMCGy&sDh`h2Vh>Vgn`l|$j!o|-nbOP+vf*f!uuL~0C7N$zds*Kvghq$tgbPNOcoz6x9_ZQ*WSdA2ifr`*kAI;;W~Of@K@Ya}_qX31v{6+D zS!7yd=fH~1SR6rITr=QNN^v(9Gr{J!nEu&v=1^18Ih~do5e8IaM@PJh9t@ov<4&-+ z^}@;zi%wXq1_g(4P=hW{OuXKS+ZEqrS2bJ<$NA*Lq&T0@wlQwR17C#nSQzHOcv0CF zmdPc$P(6@2$IC%VcVJN=s}H0v4SHa7$N@(q5DQ2hg4n~Lt8=c`c7U3Jtjfk0iRw#r zvSQV6EheG~ICKaWt30|u<_JE{$Jy*&sTPuoTk)dIk@t7NOT6k1=@iJg2C7HFUx^zO zMxaTFw;=kbQ*4JeHqu#;_V#hF*W0uhbSRpzGoiOTN4t<-np6x{t?N=+HkJN?wgC3X ze!{tHXDd`TJ2??d{(>njl%9Wy1*W4>(wk3Uo$ZdboUPI5Ek55lx}}AnHAs!siY=?u zi!j1CC@(<@r+AS!;xD7k?kCgvp%568X9BIwW6>u)2f}ZCaZ@uut@dBG?%T78DcWVq z%6=dIJ1cqJlHpv2%}}o4wnMsU5+I!=s~@t9bHN*y?}Q-E1?n++DG9N0f-0K6c>_wb zvj5=0s2KJDwrijpLdBWb(hm;^XVx-eFCU~+(DdXV*VZ4dsj+$|)#+GwXnGb>4oYx| zYADWY270ChG64(Ll!_{oP?cn9nTykm64wkd@)XaQ=})Y1a-5-LC_g-$f(|PuVeV)y za3<@mun(xN$M|Ax4l<*fhHTJEO8_+~Bia;8JUCuBIKYL?~|{X$Db(%J>GU8YC$8?EFVL{|Dm zAZe@1AWN`LrbSuiATxQOJrv`oLmVd^=f{%*r*|lj=@h#>EIz_NqXK~iuHyuY;jA!o zCW|V4s{HT}8ngV2{w;{6Aj;aQCcRH9$pTlLHQ2mBNNJAe2c{z3oDzkY;&(LeL=zA^L5;SXs4 zh`yHp9#PXHY-(nCfHEJR5}YJmd!x)4`0>yZa zS_`mml}S4-1;bhnXZfKxN1$_esmHN6sq@30*xm=4h~RGz^0J&~<%37xt*?LI!K&+c zdb^3}b6iZ0ObWr2zZ>NXBnv4bZr|#aU33qF#3#v zI*+0PXiE@p$`1Z605=t|!;1tpFr~Pk#~I$Z05;@UjJtroOh?(E_k5hCGv(EVkI)h< zhNJX)`o3?!9H$i}#wxWD{8!T9NuPV}o-BOa@$5#?C;sMNNb<8g7Aj+1g& zO!3M?;dN3llXDJwQ;8vHmLaBZC;*V^RbzmmPP0tal}e}*<}9@pm=f#~8F8@dd}an0 z@GmBimsXJY6m*+FQ<71Up}9x(gf=FiFKc2EsOA)x9#ELx3L{IF-taAHS~JmETBUNg z;qJkjKcl%=w}C(g&R5c41^BGGGe3Ln$?EE(4typS7*+N}Uv@%U9!EolO-4y)K;gVA>laE zTF{I+kLw0e^11WW<5oZ=u>Cs!wilp^s)v%C+hjPIlhX5!{+N}ux@6PZ&niIReJn^9 zAIVyZEC;Wf2a6jl+5t&w7QkG#8boT@B+dp{%_G699PUPk8iagZaPVT-y8$3r=YlaHU z%m@Q7Sla<EoO*P~roWoxO0*%zD`jMLUL=nY{9pqi=ah5KCtZh(*` zW(b;w=i>FRpb41KKpUnKQdZd|g*4}=RDEa_23hihlygX?XP16iC=~)Ro7|<9+&SpuP-#J|7WIZamCU0d#6R+8KsJ_VEyK(aFvcr6K){?R=a0J_Gla!$U-nb7 zXW7rtN@YJ%<{SIzQYIOtyahaKU{6tHKWB5VigXg&At`tY*h(!x!yOCm#FpqLv=hP9 zKK7;r^y7Yg#&Dd9)hxTkqI&$g(_twKOY7OxmNl)%*-$WrwWGn}QW`ZDA3zBG1U_P# zi&NGH7?RYRfEKK}=-6j4rSA51p;z2&`t#=u>>OLDtiVNp+vTkvIviiB(@QDe_pQFxnVe|GuzikKM---`#=YX(NFVi3 z2#D|EOyWx2cA^^Z2sHcRt(fMD8%G1%olb~K>&Mbtlr)~O$4c#(8>J&aYAE}I+00Aw z!3M>$U8hZs3kUNjrH>>khmZt5vt z_beHZ;7qHmwW|6m<2yp-q3@1{%9VzkQ#2pZ(skFYIr~$x-~zBFu=+H`G(>W{!9T@93iSu{3HK}WT>6TbX=_#oFWTF? zZ-4GW>@@hCc9ZAy&;O=>ex!eP>7Up1&u?|t;u~ty>-9Ri;aBu<#fQp|+q>xNy5F{E zMYq^j5T(da0E zMPPRN?MFPo`Ke|)UGzCsO^R;3GgcoIyb;4Nm|L^+NAx}J9yHD{yXnM zJV1OU#f)wyt^SLg*`Mw0z0!g)xOHtTeau3a4W6(I`lDTFDS1Wtc&ocup|V z5lp?2OB#zS4sJL+S#}F_xz*Nb{c@xmq_bIysRq#gP%_pRok$|^v0cdavr5(q2Z}z} z-THaE)pb_Rc~N#`tC99#aellC}4Iy}o*fVYiro z6XV$_I30E3o%b!jITG2+OFhr4E3cWx713PG5>mE29}97&i%BWE;BsH(>3I8J-30zG z|J<96x(WX0?`e4=1m5g$ejJ#>bbd%XWn+!PrsOh`!%c%jK)Qq^%(hdM`+A`nEIZRS zF!`3l{)|Jti-i#MVRZNWr|o}g^f>E8oBgu&>Mfv!o~gFkdtb?fP?wjY>#HkZJ1+6- ztE;Xh8rlbU^;F!K98y2$WQc8BE(E4|`q6HlezckgI4>MQFkbL!hvjY6j)(SxEAL5* ze{_?Brau_$?{57BpV_js)5*eKo^QS0mz_{}etma!{r$YNJHYc5-*Mck`cAg~vA7f1 zg^&X(MYYSC<S!uDx;ud{Rwc`1p(cOM)*9}QVtJCwWOW(< zd1xCJBajy#a1#(wZ+|r)T?l@-{qyzdGKG3=5}zvDSq?P{T|T0CD=Q*>1MX=j{>17p)??gjdlJ)ZQH>7UKjhJ%jccFzkbqCHeo39d4q5oyUt2c%8erv;j5 zAUF}|%|$XQAXG}(td$pI1!saCfEfm)v3+=v19Laj%)01)ORNuqYDe-_a+p=;SvJui zI4sV@tWR$g_7W%@Th`VtCf8}^j0O=tX(wLW1LT*|tkou`Av=#3w|40ykP0HVy}wzS0Kgl-wZe{FN8+$|>m zCz}=Rj)xFukaH@2DXa!M>;yMpP0@P1?ds;~Z@@-)<+X?O9l-=G3xZ~Zj{8-3TWJoO zFzFTg8FTp6Q(Y7pa-0;`-WC)Ijt(Udis*alv>ASD%)c|(d--yI`yi|c0m!2Mor*b7 z6}Euh2(VMShg@EI-2(f1)iU>RNS};FA7}vPv8hWzkO#Sw7NqsdfzBgO3KUZUvt#tZ z&PXpv#r|BXDjJQ{v`o~?S9@CrF6n-1eXdj_0NR$@v>$A`U?n8i(w^2XCS=B`sY0+Y zg;@ZYc2)6)smzw`CzjMK1ZA;3t@RcVb4s}Z1RYaZMEZ_Im;`*)&Gi*P=!5Lg(*i90vo$E;N$&B|b5feL zBv&pZuU*vZD;G?`pk5@H8CmrPS$?KuRHr;e%B4PT9BV zV3HQ%RThh*0(go2rYWLbm7EzVhXh6Nl;loK4R(u4T*S!j0;>;+-%;AkCxYFU`Dn6) z4@904wAd~swiu6BRF(_)mUwj`6+D~E+#7HI0kC|@at-HlrrLWIT>B^|0+bx1{vbaV z+(OWZb_5bm%PkV%m=u+@H#3aJjy}^BY`LJW&V+q%h91Ul9f_Z^d zPz_8)_92aHkJ#5I>1+fSjp~5S4LpXkGdSgEQar;jqg?qjuBd2!T&y&jEA#l*4acy$ z)(|TZq{;sX9>=w+yCGeIv`tu=PKP!(cEJsJ?W+JY+atyp@dKnud5V83gFZW&OhF*J z#sH8fH5U7XolIWeYGG2!;5cC?s9=P$lN0SOa1=Up@4l?(pnBH1(gIWSoD6PxW|lhb z*PYkIEj@Oo_t>7li(akG&FBK6DK{6)zz3OFPJEc=ZXQbjFp#OC5kA$1jxJcY^%y(S z@2u6^BRXkc`{B19uhi@~+39GPMJ4Mo^|T0`x`}r_;ycfi&fXB*O=kzbT@7Xw;O}k) z`98&xmWTHzCz^lm^8O^kd#%+urrjAPj(>Y?Qr>9quSL{ve|3>-V(}8fG&;1&)|3{U z)u3+U3%~*I1FJbs_HjT#%mEt!s^#?E+S&bO>($PS!SlTr+u+5!{bmcQ#ei8@tVwRsJ)(-fHUF8LLs|ZH|oC&(M9gyEr>=zSl+U9 zw0Pq`{bhS?8WbrpY2C7a+9mrX->!>*@~80de<~WpSohoK*{{#o{OKIcpC)bmsi;ve zN`JOZ3A0PrREd15o%*wsTnn{ZpIy0iEv;4^I@W0{Ke>7`MjnBrEw6T4O5e87qV**y z)~=yZ^8}htFVRRA^;t2xs246iHE(%qIPFUh-qwKMW$)42aa`L?nb+=ONZraGXYYcb=yf4aS*c6xDo-<0`? z(wQJxy*}e!!vNQ_b+vL~ope>p=w65Ey3S4vK8Nl|B@Nz^H5-7;Quy=j=mV0_nfvT2 zfxjXv@@W{)c!jHamUM|dr|r-cq&_pzi!ZR2i*4Qd0{?95>JnUF4N@!=;ywxNqMZ0r z=qu3V_NVrzXq>42-`*s9c2Nb&okmJ8Hh`J~od6KuIb6(p5c|WB;x6CIjofOi7_k{D zdv}-HzzA7h3d7z34S)aXknB@wJ-*;=`7&<40M>u1J^uU`VS{a5j>Bu9@L#UOdWF*` zxNsKgvTyIicvHWoa{ET7?AB^0gM;TqRwcm-%2q`v%*%eBft6P_rb=mDR;*8lrBTi# zs-U3zGC$(-Eo9Lsna8&ocs2MMx0y_N)JfclxFvAELfHe6P3(g(%E z3}hZhOO)g7WHZMx#QNBz5(-41aKrYWDJa%UbxX^R>;q})AFx2(g$8~{w=fXM-#gAi z8p1X_;G2M#_I@Ei8z9W~Ttw4O$1eB_Kc7ol4HYo9>FxCv4^ax8YZ1^-bz3jL(q&ET z-a&UR)!T+tt;BiT%dNSDqmEji1prGpAT6aekNXF(w zz5vC8RW07}hP%BZYWQTUQN5H|e_$;plq$%s9bQx!PYHn>BE+GO^Pyohud8b*F#xxB zDp2x|vp_5&%#&S6lixBkp&g@ z@u>jyd6Z0FlrqQEl0+>AS<*{V8%7i8e<4}DLuLAO5Q0|_kVo~aR4Gtw^GK}VV(nk^ zRA3sEXheh(*be1UF>MMY&&sQlWNqy@o2HCd)+#pG)SI9&Xf>I5j)kJv?BCC)~w ztZ2Lg-q6{qY12GfQuXG#o=jbV{#uapO4=g{9sxld!DS4rQg2f>{1uZWjaQkW8q_uW6w9~0fAHq* zb36YH`zVqxS7x_?q%W_wo^8Jhd>*g*$kbO zzJbrKne6kHvi!nNO-@(!69Y2T(SjMMM8JLcPjcnMU3C=7zIucFs|5GRUSNA(h#tzL z_f!r>S;m|05p!+U-6ZVd;4~OXtGeQr*h@{+_04fKagTGmB6dBD-%gUa)`FdM0M%&E zDfP-V_S2_xWObY5kkFE@hgEYwU~(C8(^Ktt$~`IX71fhT+>@FDUV<>0PKG)Qrc1AX z-umaW?ZI2f+0J&3)xoA*l8|$19WqW0D1Bgr`crSO4#HOD`oiG+nt6()L`0KKe>jFd zl(~}Xk`7MiRe2I>{uEyqNA0m#X;xM$ z%X-xj81c}^EZA&=`g-sL(kv;8)u<@^%&!VxUl)EB%p7cK3xcKL9TPsXzht6-q-nsp zKnfnz>{QO{JVGLr(k3H*+8~ZcJ5#9Y`=P8 zv70PmAzwpGo~tn@vk~q5p$f58=YW-H^!O;qRRZUucX?@MCg{4ChahQPDkScuqtU=_ zFFM*tO#Vywxk@sSufuawbE)YuFssgnJ;I9n8<(nMLqVnP@>~& zyf8(=YOjXPWPQm)x9pj__K}0RyQjv+EUeh950p%tcaFKFA%h)Cbh#AE*tk_bnP(ep ztJtK1Qk6U#QJ)MqAFSVlDqzC1Ci7!MvQj9qtgZZl{}^dp3qFL54ny?|&GP^XmfF4O z@0?N~q0F=`iwCsw!JcGVYA$=pb~-#sPSO&Ym*Oa?&I>@O(^AzURvqjd308*;n6jQ8 zsMvJHVtwVKQURm@U8g~&K7m|!#ru5uC6Ku*#zwQUoqyGo@4mAcH= zSMj+*H9o2%33RH-blrL0beh`I>&};*CEK0oiKhf$7y0rBed%Mq=#Vcu;>#a!_jnuY z@v^s``@x&-gST&XWnHuI{k$NEh@91u*1fb9ALixT-RFa6doTXE%pm9+$GY#@c%U^j zmI^}xzP`j)q_~Q&u@EP~xi0VOgK&AtfX75M_pTEisMTMEQdct>EKv4Y2*h~_N;$5b zL(WXzjz@sNaEy}=*+tJoLGci(){0Jl$}W)5|D07P#i&%i9af52HugktxPYFT_$G2I zMBnLjI?jhF!%?c`L7ynEJzd+g?7hPB3GEiO7?@7Y#=w;6H9sp{muqWg#W2(yigKF_ z&fatev!VcUpfn3K71CX{-5#8%46BDHs<^d%U3;@q0}bah!I@V^-#dsYe4QgvGpVG> zqU;$312_U^8rCs6ASFM})0r4PqeQ&f#~ez(UdS;GacYBZxWhi*XvliR#DQg&LgqogrPtF(jP z)HjA7u-25QcMRxHHv-@LY@0oJt;x~>XAq8?Vk)F9g>`fV8{es4##J}|J_fEUhcPr< z;PywLYl5C)tzU79{s*fTm8K+s;SIldmIJGA$202R;TiR@n1Cb5ggFlKW2{TZzHw{J z34K5*|M|Bxv?Okxls6yXUd-1v-4ZMK<_>?Qfeo*`A{wgV61=o8hU~0|=rC}AYLzM2 z-P_%UyH~@K3uWZQ0axe%^~jZZ!S<`|pSO1p23W)+ zz(cw#n1`n7rpBLRU79-a(+qsD_3V|d%IiS0Q3n#ELM1`ZIvo!bQO41IZz>p-&@EOl z>V%p%yQ$K0aCvFZ+TukW+Ny7=5Trs^772wWr9Ev@QVvhDQz85!XOl{flgea`(-QJK zqOm{a{XnVtITWjcvhlMs(aU)OwS(bm7-FfZq?%nIM^O&bDM{SaGY)fj!F$6L_9HB@ zK9Rb~VKK{k$pNsyqUR5k#vjlBX;D%EuTr4U$6+?Dcvs>E?IpiLoRYLFL9Y%`7bIjd z5;GSU808Paq?M<<8zs?>FL3d)q5*8Dt{FB;Aftb$i>0eSR+DbLHk5FW)u%u1#G$B-=PXM;r|PP5ZPs9=B_Se#|E zaXQ5{EZ)BOutk!hZvcuUQ(2%57J&qf?Q33AO^5k}_ML##Uw9yp;9A_1b0LW1QK9oL zRv~e9LIEOW$!hh?O^|CNXTSL$b%XKa*6+KP!g&o_qx88oe2-OzBc@t)--MrsSs}}~ zOPrB77NGu$`Yi;_h!fO#OGc+}zW8MJxtMV-?y%q*s5IceB-YNev)HVf4bj(IZ?=A} zw|s;@k=JOnvW8K`B&|rbu!JtK%S$r}s^@G*xMD1O*p3i3Nly^%010KBiqwQ;$tqTN z0UFSEdeVLe{F9~#u%wrVRB5&s)!v1lz!d&n@J`tTYSUIPM94SW|K(+(fFn*7apd3l z+B)zzIS!t997AGhUg(1U@4v2mVKUs~GTt;k@;iQeE}qB9o3`tx{p8+ZSNuQh`~=yn zz=nU=$VtF=*V3?TVG+?;HR#7|`^*Etca?cNA-iEVGARkeu3;XK6o{0jw2V*#28bA} zY&iXeP9$`IUdnkXy%R(qJ0*qm)mhV-pyNk(Q%qSba+|Y*#&oMT0eISUG)hRHLnUe} zsMX~f)M6YaGgg5eP8Y)J^F~O4!MOdg{ZU{Z_rKOH2+6IqgSJuX<3NTe$`Lq;V0Nk6 zDMB$Y^k}!svfX&HCZC);=lCSA;Sv%4gtWF1=UYlHmo+2y;Pg1YBjKctz5jH8+ zW)!WZ_H}}ML^2P!P+;+PD&}s#>H8vrfddqd7tq_b6So@AMN1c;RC$L=d!^8Vu z+!?T%9x($X_IAP!)Ztl2*=J%^p!1lot71U#-*BT4JI1*R<-MdYGFRSRYHww40d-w0 zMRTo@$p?7>e2LCND!2;?aePML=;&(9Uo<}h9)wOHT8+$MU1>U7413h6?J7 z%nqozw~MZe-u=Gg;M8^Qh~HW^>H4kJab1~0LPi*o$ltXfrq4vGhv?oykA5$}$YS!y zS0Ph*nm~`LD!U$Dpl<@{DAh;Q*XdlK2&yd)GxQ11MyhTqp^W%Y)=mYDG!I2`{TXUg zhlk=4wSNTnDQ2kg)Am0V!_v?Prq$^L{F9d;6k;|53CMwfVmYWz(h2GLyGbFg1_zm3 zYC_C^m`;Q((2tYCd)Np`0*$V@P*6cr?hmz6@dwD;Y6Al61PMimDZ|rgzrQzw2q+<~ z#OWho(UUL)!gmtjtLVIa7hKe$@TfTv7(_>|T$Jl9L6;3upcaR}XTxgQ1Hn27pNKc2 z0l71aF3ToSdVyo1TuNZHf%#&95xi^)Amqq7DD?|@-H;cI@1hqxV|G{|^H-^9SQEvE z9W?0S`PS=w`7xEW4|T&L01p)SL5xMqhBg_wT871kKpp`IIYl6&aWA+&y7=wLF*kyu zX?UA;Kd7;VJQ?CNU_>A5<4)VxTs;O|keiQm9jb)|TX+C0O<}=_kp~lhMiB_HxhxAT z9_DtGgT;E3O)7BM5)a1N&BYP;#xrELhT~^)zx6<#rE#O zjv#(-`c^>mZ@-0{5}!~sa}t{=#V2nBKDC?~pPyvI59l(ONH0QA;@Au6d0o^wW@-a@ z1WN`q&OnUA<{Mn3#2}46*5zrR9u+>%5hgfL8{;d3!!6jAE(PazLNE>ajRqdw2yPbc z2Nw_q?Ux2c$YQW85U(OfGy7O&lM-BI!YA{MOq?L_SvOtwTyFp4V0(9eXK&X&mT&?? z>kk}DGt1gj$rdW6{t{(Ub1un~BRS5`vI*UF#HEDIw%3o$ue~NTnv%pEzY8QPD%FM? z7b!)e<+LebO#xm4jpG!>)nK%GOHz8r9-|_tU;x9QFiHj1A9>X|oXm^-b<)V%{n_@%-eB&8A zr8zo*f$&W(Xzluj_~&u*jriyOeXeq%U_)3fIPp~w(X{FIcsFT3OddZ@awkaisI8tS ztH}@DgiSS6SV8@pmIAS=_8of|G0Ok9{_&*}Z>~aHA@|`0$YnW(u{H*poVb4D%I(xM z+&x@F+=fp9V1n2W2K{92Hh{75Z8wZOa>hGGx4!h0DF6Y!*zlGhh)=VLue15pC()ss z60sEiEGY9)$_a^V$z^jHo6$ffw^A;hrjvO(R?tVSnt;;r1R|Sys`)SJESFk0Atg*_ z`5D^7Gw>ipso?^qTc!CV8Ru0cgm>_%6e|O1HUa;~@mTdW6{}$O*e1zgG?8HZ!Pcvt zA9v%{gwuS2%V;30hqDDalnK&+2K^tp1TONeyu8F|UHO;VSMO%obk=?@o40k#4m#~B z;OKNZ(wp&UK29)~E+pX506(V?&|xu~3I1t(p;e<&AF6r-w8=17r@8F=QWks)8C zGsXh|T$p@8lqrbP1~teR6n8RV zDO|+Kl(;IcL?o*#n$&&)CWl(9w9K0;{s{#C*Y~g}s6k%7j#qa-vF*WKBRbbyTegRW zd~JZnZew}v-j!|4Z(?t{{#_BWkvrYTyhM?5MDZi!tpmSnD52MNl3E2depWgw(33eIg*4xFZjQH?%Ia}oHYW5-WvIHYv8oXO z12g!G0nu6=^@$wnK;WsbwcNZVupT;K)hDD`#wrBo-n^R5D?dliHS?>1la#U?jY=WX zp+62mRR9B@kajBWk&Au{vmwNdO)ztKo!Z#^NHY|s`xTo29{#iRd?0S{s^nrSOir-% zz?yn`AOS1$W-g=z+Is0_5uI@aTJR%)&>MnK17u@adc#&q0xWizMx zJ%wrwxGhz>Gl00-3VdOh6Ru?#qV-L}1_9-8oQA_4ok~LF4R*CR`zB%ORwXM@`_&;tS^TQcpw6_+RpY|9nCo zQ?J#;y^vukDDe5sYdt9clvJ+P&7*X7r{o=`g*c;``x5X#Q6YGRZxq37dN z0Ch%BCx=4YR|kHeia#wyy4dw%{XA9U1Ia=@!y#IJu0#3LmtTI0%n>S#%uWdqZm(k3 zaIrNhvtEieJEhd<2Cvb(IxNi~_;oT=1+xJl<+L)@*lMJjOjKg-S68aFbh{5W{KUh@1T7xV{TYRY4CieSS-I-no6`A6O4UUJX7(^|nh zMG?PzG91rGkYp$yXXPMDO=9hX^n{qBwaqdUs~h#Q(`j`9ypWr;h5Ye~>?6#`R!xc3 zO}CZX0PD;qC=eee89x8>*J*Xq#@;*j!3D5LUlYnG- zT=HvR0XNBhU^F(2=mrI(S<)usBLuCU%w;rC3`txN=tspEB0mEaJS)a*8gz}WZ{&85 z6T3H9ovn?H=NlW_8yhchCIZa*;m*d!%Z-g6v{sw&0{f6*&~#C(dy$cYVIdAS8MFFP z7r1Km>pRsF!VFuISN*EQ10sMF_nNiiLg|_N1On3u;yOlwT?X9UP^`6VkBvl3^O^W3 zT8mc;4pg=*D6zn?cA&^(GyJ=ELv1`Ze{e)X%q7f9lAEfwGhwZPdx_vQWj zE(5y_=+peSO&fu1EO??Yr-4^IpRl2w{`dY%{aGRZ!sXOj_R27%|}WE*#w z*-dsF^xp*B6;LQ4-5HbuG1{t zU{;MM$KSy#OlqVkUxf9mVkk}X1c7VAjt}e%0;x>}VD!igNEJ2=UyAA|A)3};GF=t6 z6toc?tpr?>jIPagYyMp{JKL47pyBdOCh)Zc>j`_94)7H zmTIFbgHO5;B9XgO*y=1TS$Q5bsBk|flOPfFu)_ zkM(4!C3uo9$p{DZxkxYRX5b8O1|Zy;48TV5g`13>`0#WfSm~h*+Xk^+;p<2ooTEmq zs7Z4>1G0-DV}HWYHPLj|P=K2yL!xr|{j zK4?=#I0GdQT7-$VDY2j#)mi%SXg>XriLo6*Xf-vjBhfkVtSXpaI>bit-NQ!@*B*X@Zy$b>tlh%=YCm((4kXDpvy0+b zFuU0_Qm*d(AM~@jyp$}X!Q~X`$%D1^?~)&i;$#FSMuT3mH5w)Os?^~(1rLt3UgZP9 zTVec6lmG=a*%F7W_#@wRlb^*|59-(ly|ttb1zPeI>Xw(2%VLg113Y)hFc?*~U=EDI zQh^|W$S@M8-$3SZotX|`B*{Nz8^sZt6N5f@a;dZloWu>=c@O(F6-i|I=z5#JuT_j*`eKM7C>BT56ruhu7 zzRKcwc9BAm)#YTEmoohThnX58D$7g(uz?;Yt*wJ(_n-wKoOchp(8w>lhd=JWJxqSt zdh=#$?{IhLAlZMDZ13;A*oBnA;@8V$Yww@QKX&(Ch*K~}+Mj(Ca*vpS7$z_dIW1yx zkY&~o^o_FGxKV&GNPcgN6mRp$>MRCF$@S9D%g{Z#?^ z+p9VUe28`~t5sZ|v#T*leM(Pgnb#%R*y!9xImS}o|Y_}juGujZ^ z1qHootbH+;sW)bFe*{v@*L|>_(lN=IfCiZQJ1lm=lVs)T0Biu@&Pr>JNs;8HBwwTO z!-wSFayL;a(RS5fzchp`gNu6b-{# z3q%z~r=ldB4Ix?m!S$t_U2#?!0@D-+~p2)a=yBz zX9A_Lr?(a|WO0!!v=Y{#MT>8i;>uOqvC;<%;!myK&RQ}QJ(Fb;LCF&iTJD1iX|&8HK%VD0h-;bIP>h3@Si!cC{(^RUgMEcyGv{+{Nf zkPym3UiHZQ%f=T)1Nbg^?gJfsqHty9DLpLP9ZW;jW@xi%SBFzWw}5t5PzPaYO5!Dr zE(CEa$sESeL{duRV5Sm?0LhZ3C_AbM`cTSqfj!Rd zS^$~M@RLu*U{ntS2O#EdLyL4}b|O(nIjk$LJarVq>`f%Us99oF)=My|7UvFw)5=qf z!-BRpZBulqV>8Tvx{`1M)j>9`tFA=c(BMh7 zt*ov@+(7RjoYwX(>xX(L;kL56l5qpQBVyI{F6)PSC+W7bx)O5T>L{`#wF{^COSig| z2b}79E0^;LUQtTpaeE8v3K1Fbph5<8cXFi!=_B5hZrP}m12yS(UCy}?C;C_2tg=-#tko(g!t?RW>v$(sT_IK223 z5X~neW3dVLui$;5qNa4=9^{??!9YL~P1?&dh<^feKP%>Am`QSyPLDtnukG&)kdpSi zqzm}9Uhl6oF&fV9F*H483vsV>g0_Or3PFaPvh^CrB`>t0wMkmy@?et~8&is%7=2Xn zF_4F^lsF(n)EU@%3k07C)XoR65v*73s7B$g(kN)DNJdXb6$1j;XOWR5Cym@=xrLu5 z%aRlKi~F7Lqui7%v+0>E%d-B41Xf?k<)&dAp(xsK(;)h=Yuai6Akbk6Rs$!}Z{r{K zSd{LYdaQVQ(jS22)K}I{f?X%u)m5}vEson7U>y~}QBoLk$w|u(=u+SWkdl=pcFLp= zq75*Hl=OtgDk`_-xBJ){M4S5m;0u*PQI86Av>`JPo?kEu%{{S>xM^S)<8Fb7(uv!z zHXGd&yHgO}@d<_B09;k1LNq}A^vv{R8fxN}A^fnl5&%}E8aj{^!-(D8Wc{gMyqi!U zvRFHW&nVuv8QMIvkZ40`J$mM0TQ=R%Cm}%y2@+(e!F#Q<0s{FUu}b9@=xI31b$FVr zDddEa^@x<=D3z&~LcOj88Dui1enGtAl?8Dwvq;_0Q-L6IwYfWi4zr4JN^tp(ve`wJ zLDape*mZ5ZPKSBI)ntdlv!lm{rsd_5Ha({q!CmsC5a`+i(w5IqUDZ|SVNfGU74;t# zgQQ&?%Z7kZ9d_v+d6-XW(29c&ROlO1sAcsV1UOZh=ru}kUl4#+xWt@%T z5aWUw;&T2^*<+)2V9XSVQ@Yu1V&5aO{_d+SyDTQDc!C${ z2`&yV_ykoBhEY(fh)P7-!*G;=nQNAg=_D24LwhXc0|J&&<)on8sB#^^jMq4IK%m_t zz<{%2h(48KM~fqGc!|qd7F`*rN|1;QV}64Dnb$V8rP2+RIUMce-G!(jt?g3Xw?6mt z(MX)zqePsj`AEjt!R&3st_)^$h{|lGJq7{vBZ27W0Jn@{T7midr85jY{132&$tX$^ zt0j|PRkJGSH-UvN3Q-GsRD9=My-FOhJXAuG2WLV){1{g~niF){LXVY@im6E(F;b{* zLG&T@s~p>VO&V|_oMcANV52kcD&;9@eAqV9P@*^Xc|(;XiY4cHDfZ8h9Tn&5e8<12 zgZ}wYriD=yc_&40pSj?PX^B~!2tkwbVaD|Dh$e`HfFYP^UKCPIoe5Gtz3i&};!rd| z(4*|QnDPc?+_rL7OfWYs8$1>O>K0gaExbIHa4`q7Avceo=r*hX!kTag2S^m$j1ZY~ z1xe{PB^!eLDcKM#Y*k(X+8eaL(6oo##!N^K=nO0|;h>q(ZZqv+FgmD}Qbar0fSmM$ zNhP=U05e6%z^AV(^nqWR1Ahfhe97(dKgyTDRz7O*pz8+tj>=aHXy9*A`(C4Y*DjOF ziMvYeZK{Ffa54r|ByNml!UG&K4L*JnkY_`>d()!aJ4lmZg+Trg_&v>r&Xti)^k;S_ zB@s_YEFn(d?!dkgx+C+zRSrs85j!I&=SS??%A%DFrcYejg|_oHat=AJ}iv8ovSyPG1ayxk;Dz^l?Tx$HnOj2kS9P!olSs0;Fk?%DQKd zGQe=K@vvOsl6MOcY~Ak^(cJWYKXQR)`m!$bLmWTjq^5A#gsP%i13WDl9F+dYJzpnhw^=N7g%d;(jX1J~N; z)&pmw2SA4hj$z&vTyoX%`{vI`iIg>tS4(T6Lut~S`))T_xvPDU+^NJeWM+Yty#!G^q+2k} z`~)oLu%I(?l$Ki13o|20dIZXvat2Z!;Bsl2!X_vtOAR|J+0gS4J8DT$5Q+d?4WCFE z%GSBwgYtt~2c_@v_TqKTMYg8Qr9evx|fbdZ=N32^yb`1b^sC zBe)t2y*lca8I1xbmpB57$_@dattjw#!$f3{Jbug#69r>gLdXv$mvxZ$jF{v##C^Oj z5L8OzB0(1J6S2jGu6)E)Z~>!-liaV=^!~BvC0Q0Dx7wJW_2Hy0D_Sw7k^dv4S@1UL zMkEAB7@{K*0zo38XJ$4uHAeRZPk7VIesw)nQ(eqfQJ}_@M%0N?ngF2*qqFY!zRy)Q zHu~2S60@~oV=hbRdX(u98H^q$Yql;737N7K6C3Crs^_M~1%`ONNG}yxD~mHhVQ@zg z9g|nIrY%xSS?VGTZ8E?75-w2r`3Me&-DHNC1` zwUIl-_P;{<$Tet^fCA~#@D?Ci>-C6r0zK%FI8e^BDLB{7vN2r$;pG6NB%Qi$STL>Xu51m-ptD^dY@)u6Z-*JFdJ1ExjBZ#!4j0PAI}(SGw&mLbV}nHE zoV~dwX*pAm{o7x4YTuj6>ync~-mnZ#iwQ8*CKIeOxX_`I)E%`t8}24P7lHT7o#bEt zGPAuCuNS_a)+XMn9>m-3AEiR*T+^qc467k|{XQwRqXfww0XBj$j7nv4581T@HDER_ zXF{wk4K%O=LQK_|yq8r!QK^etL;MO6KK4$or>3X4AGMA&tOKLU9Z42EK|O%N41--r z-RE6yYJ)0ZICe4kdE3ixCNTNvs2eyAQD!oF>zH8%$(eVhAS4kR&1Up38OUde(ug6J zw<(X$rhEn=;lZx6B*6*-s8F0t{~+^q$<&W->rn8EF`M-+mVnk3#t0C})NQs00K$yK z^a#jmkGonH>KZNh*KQ3Pv6l%$AqjR5@yQB$8J@=1!@rW69x- z3fz;QnA}&F)QZ-XmR%W8ZUhsNDT?HsYtI!%jeby;(A%m0=H`}eZ7ObT;0XMXPv8)f zVN1z83w*ahl95JVWig))GIQRMTSadne+t9yY7(NtP>)lCtL7FOq8m6#5EoCxFB zGu%kqD5)HrrqgU_Ea0WPhr|;##!jbKgxlKSZ8d1cD3vUY)KzS{j!K^v*ShCfZU z!_NH`Z!PZCq@e78!B85c#G2EyZW@*qNDh6F1i;FnbhRM7JiTQQVgco4M0d#<(kK{i z41~q;!oOiL*B92<^qKUUxP`}DoP8{h4;=i{r{N>Yx$TwAbHDP|H*k?tdp8njxcSxR zt=@bp66KAk=Kjs6P1qVcAgtqB`+Gjs5=oogmxS(?P~M5#;n=H6?bEV?2))3aO~0(U zwNV59HGjVV<+TRv=2AEp(lM$!&kZUc*5K?H7Pk?vOA zvAN)m;*etb8llk5F6x{`53Wmu61w+T%j{y!8{?Jp`x*-ryx+ZwEST-o^K$FetHU4P z?7#it$F|fZ9Ftk_bj~X2>!VL0FaV=PhAm`u>RNxm$0DXO?yt-fgABe;d~SAc8{WXG zXQv`6X9yW=q9K;x05lJB1sK+JttJEYjGV;odF@)sauUa03lDb2Lw^X(ZwhPJ3cb2) zC3~SNEPAiB0{Wz71EV64?>4n=M_Qdr5e00>NMlYWqSEpW1~9`uaOXIIA=W=RO<|*)_&bYp0}Wk zQl?zxuLjfa-+#}iZ%v+`5+O>WmGlnn57GSkg`_JboA?IEn<7F1!w0^9pJ+nHmbf+q z2A@L!>$77O-|PA~598L`v2x843I6Y?>cms&q6w4g$W(LCB~fDpCr!QnG@e4(E^>aBtNS4z?!W*KI&sM?|3kZk z3_NO106?*h&w@>2!%Efv9M{diP4PN@6E8Vp-In$FgjkiCH#K*@pCoqSeGr!t{735n3IkWdB)P zJ7{xP4XSAH*?8C=jN20#p?SC92p1k8h3|9nrrSULW{FMd!lHqO|_avjaT&F7);0|81os4DjvcoiXm2_ z0T&ULqt3e?IGJi=xt1HyA5&fN!&R(E_>aewz zsB#}SUz*B=Ay;*X>q=wxL}}%@r>(1>w*8$D4ph+aBTe75=ActKPc_!aWUq&2kUa-d zo}ws3QAl()R7mhg$_#sV&ZTsv2Bc6haAU7r^V|Xqw@Dgis4Zn!l4IC;Q$bGyxZ5D) zruTBM7|TPlfsa?M2=Is0R2Xqzx&kOIkSbww_$yCouGl1A?X=GWD;2*y2JMkDWeeUS zvuy8l^bSUsQZ2t!j!cH5yv>nW0!_Sg z6|D0%yS62qc;8B#xm_%_)K3*V-%^2u`nP-y#OIEq#0oN2D#myhB7Vs>I}Rrpo>s(| zq4wsJ9p8!xW2|XlFxLWW=puY@n!|NQg;*4z{^472-Bg7I6`QEMGCVgE3@o5{shh19 zk62|U7F~mV_XGb|3l2@k7s9l|5T<>hd4`duxjFBGoUXafLw?lwvjkS%c(E8(-eHC{ z&#WT`Im`g>#RNb!^(kh4{S|Ay)Er#I#dz~M#{0E4xPOIMxn4~eY{yy(FD0d5q_6lx zT4PHVuzB*v&io+`8)I!+@O7Zio!rRHn($n6{pW2gBHwK;)!w{Ncu#j3RrpYhzpAcD zYSJ42-sv0{{si-X=EG6{Bpdfnrjx-fWB{$s|M~E{hu^Qe`9J^q{h#?izsM($|MStG z`9J^6|9R{AKLyDI262*6VsvB*fX+}>d*ww>p5Bm(Te`lD1xRJQRmfwN48+*Svoe+{ z6sq>6vtThTA!}&3Ap0IKB*qtisj`xqw?H}pSq1t|R|VNL1F0x?QZy>|TY8>0Dmb~E zofhLoipuyvjjEIv`SHvyDT*E^loe*!?*DYK-T!&#O=s!bMDTm*i8%8G_K(quX0Eph`laVZxDbTyJLpZ#JI|eD^Jsz zq$6eTRZ)DHPueYAy~U!z>Qol&!OBw{7)0lJ@+4`|j%#(C{%M|@l~wV$n0i$SGSyX& z@?q{(1V}oZDy$VxdH5~osBU73iD-HuhsB^jprV^<&O8JbW$bcCp{v8|O~2L;0_o7VQaic{q5B26sFcIt zcv=>7O!N6k94O*wc-(@(1w+goCcd?vwt@pa0lVe#WIjU$aX&on_Oc#1oV5@$uNRpD zv{%e$i!=!F!-dGhTSTD+nmQ|n^HJ6p2U`D(<|R(1*(nXVZDGc8vI=3vmRFl6XtPmk zby*XaKJYp?_Jx|mVe4ifXJUVH^Z@zwn+!{^2E}yNr(BN9t3gaqcR$U*M=PYn

xT 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 z≺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

I4nPr-OK>`RhWGWnkDAH6wNy9X~7CUR?? zv{Go_8DpO}Nx&J}EG<@9Nt zvT#mM&(#0pBTA5ly|_#MOj*3*aSpL=Jh~dw33YrCq4(NYVSKO2mEv6(Zb6|vfm2jt z!uE04Y5I}z9}qfN2n478KBtH&9;2#2sZv)G zRSWA}73XwAHuZ=Ec{#)H4ff7(UA_1dQDBV0B4AO%!6VnYDd?Gu81C)^>OPD1SeSfx=vq`TdD)NxNi?_fCab`*M= zqnE`n!0ZNqwVogx(G*JhnxW{;zyz}NwOcI~YPcN~7@2O0du>ng#dGOq>i*+&*p^<< zQ&Hc@2Z!hGFzAK8LVJ+a?VsZDr<_nA{~pAc2v(Lj1HkeCJDqsR4z7G%`e$JWfQ$& zi4lhjENYYYa+q|d39a(f;*jK}sF6RB#7mc^;8GL8?W^WTjNCd3yHkqQt<3oVfiQ|W5rtLtAjTxr_FVO<`6-;yuT8i044kX-*aSrRILtIk9*mtJ zh)nVxMA~jAorDQS?uhVA30Nlvdh>FP3a>T&SP*S^>U&hHu-QG+eXUuk<0&t$!mEXw z8Ecx^QaONCQdtC2C_(^d(&9)j_!%80QNtSr7>Qi7ALV7b2vOIF;%J3NK7k(NpDP%f z_Y^ly@s&TAT&?uS0Vb9M#qK=rQiQSWYOc?vD36->!8cGH#F#mGNtcRq_RRw-0;7%w zJ}|m@BN+`ENbP}9EbM)ot>zn9Ov-Yu#e(IWWD5__rZqrseH6NXfYGT*fE3_qbluV_ z62_DhhkLy7gMGC|4+Ft9*qa@-JdF_Tqhn5kfC=FTAb$B@HVE>-7&+jE{wFb`dxj+7 zG19I2qtO7jn-t7|sEo2pQ0or-aF|G0t8+j_0%B9w$W0v)A*9*aN1s@|qX|O2_Sq93 zBRilQdLS!eCOkdghR9UV9nzQghygK1At`w-a?->F4uxLpJParZ<8Sst2}mmBX)`F3 z@RU*TPksE%XCJ&O*U|;c=+=0SzCr}UZcvJL*y7ROc(q0i{EK87PUvn$aB8wEXyj9! zAR`rL2{?}CO^7G5)lT7b)4{pY-O}SRBu#aKF6h`TmlL(hq&K57Q}jk7onx}(X|0M^ z2b`ifdYV`KL@R7PyZ|N4XQ&(U4XqxFf-O%S1-50#qo#-cRVvxCQV8T=sC64h9?j75 zXh=|PgvO>I+`^J!PxDv3&zG*HNF=hwM(`DJ`SA&5qTkgtauPvFC}kM{jh@=nz(jWBWRei}$fP=w>Q+_hRBVevwOZlGA~(N&{{vaj+e-0zy{CVfjUdSvj)id1}3&5fm}hQSwLIix78GT2SJutPI$ zJh5*mu?C;*s7th79fg(|f9rlq_2BTNwYOhYl(>XkPy9r;fhxDvjy=095l}Ewa+Ia< zvOwvrr%yMgX^)vQT~J6?6iPnYc`Yy^)JQULN2m*pMd)gQDW-5CUHP^n2)w|@J7AG* zrI#vJ99z7Aku)`$zQ!@7bxI+JtCE>GJhg?n!({fnb`4PIZ*1fY-P}r7z&!lei9KP^ z7hTW2ztWN z1P`0Gta&K{xzj3V6*gKo+LB3?Mm}=PGG?ce*$FrU@|ZR|auUFM)l>__00(>p4+kx{ zgoe5yZhr})bwFb58&_E^(lTaQW~@@vRjTR#%wC+1A%|7Hj%|HjW^1PXJrPZdVHgCf zu04JF$n_86305+vHxnJq8#iSWN>+xV36~KKTQn{@Y zAsya7m{jg@r#R z*JQ*7Lk*JeNk@jlY&y_Vo3qdA*`&u!R)c~ifGPFFQ!m4WwoV*!T5Z51)E$V6>W&dc@Yc5JZYW0-fsZa4^~KLD1yFIul#CqEMly=t8t>>soHz^=e}yL$u!_U`rWk$U^?=32nZ7YrjSzMDC{ z9cbKD+!=@!iu9IM6g#N-@=*)r-<#>@G6W-fT7c93kw;53uuema z&Mn6xK1Xq(*C@;g@(9M zxYh)%R>v6B4gGJmRilya@4&#mvw~=S8zF6?U*2GI_{A^8pMiKK#mO8D1)n< zzQ;cK6z3PpHSH&%Igc00>2p_Oo=J@ybPw)aOUm>x@%-)0@g}nAT<-4CN1)?@b<^w% zr8hY{?EqN`u+XiyLsda-YdNA%Wm)$ z$rRNmCskHp)1jwi%u3NetE;Q9|0c&;ZBa*J8Y_haR}x=zP>ndc0@WSS{u!gxj#ZoA zH$mA-{C@C;1NI*I7jGH3X$-abB>b0&Y8wm!j>4)vHw6o6_>uV9jBs-mohJE)IVAQm zfNZK(gg5)wOk;bf9*SAv9en)O291}bach|e*_CPp(52Px0PV>`%T_FjL6wd>!I3r# zdsE?UDWFTf!_bru;27GDT2IjykS);0F2#QcJ@OT=^>rL<8iHt8uMghi$!PWu`r+Q1Xqr&O9`KT&<@X3c_(48s0Y!pMje zhd|HZV__;d*aWm0&FZTKG$Fouci8V^%rj( z?iEvy7O`Gf1_ukhIkUk5_;|N#{OrWk3KS{Fx(j0u7MDYixq%Yq&5VK! z!801&=jlkKoQWW_G?RCuD_q}_8ixE9_zm`WEUq?}6KVpbcyv;bzuw@WaWb`;_#~mWKskHG@T4x>SLEBparm*}-JJ1`03jz2uUO*T1+-=e}4_fIb38%`qxyXgE8 zbfJdKA|K{GF@~1}gFg9V1mxe>RBL9S(`6$&UWRs`E0wSCgaX;O_I&5Dw^qNANJF!6 zc#0Pwi2sVlN1}-1Tg*}2b!#-$3(i)9wxdy?Pl3cjXbodppZe+#@h1+qadhh#46J$h zOj^kx`aBIxkVgoA*pV!$L9GheOmjU(SWJ>yqqq1(E9j4h(}&TczhP|ys;ba-x0)fi zp{$2j`gVIgw84GpN^$s@5iFmD&A!4*hyBdFH~eH9IEAl};~r=wk8?(%<2ewJ4sr0l z)eDy!%6Uwy4O$0~Vam&f3{!PAPo=D6^gZV>zQqH|}zYXDDs1~mf>SLw|Sd+>rNnT~_w5eAKC z-9rl33+eS}N7Z;BUi(+^bfOhP(oP;CXXwW2^@14*f|?y(^9u#OGppQjjrC{D! zYO*`5GN**FnMq~xvvn5LPlSAOS&5m0vVe4g8Ka3=(GhYtXNOajFo8lJjy2b&2xaQ35&RDLoMR%v@T8jO&{4Rw!{{&dT83|l(? zG-&mAb&QhQFL+(G?-3qiSNeLbVNi!jQpafdQ8=&=`OJay(--)lXva2x41){YY6=QY z8MM9Oe#{iiis=kYd~Dg=70lM%{IuyrE|kVJ4_uZ&CB$KgcPFn1p#^5GkSQZ<&+W=) zdTRMdJa(;9m^8L zrZ{^OSa-C-MW!k~Vp7FRF{Pu0j+*1Z7PcAbnue~n;#>{Vje`OAlD;$*D7>)_^Vqe{ zL$nG>rwbQko2DoV7kul912X}xOiRT1JE~7UVr?jmlV^yZ|Uy?ZLKN!jRU&4Yu$A167ZS^1$fae5bPAu5hy=o*#Q z7HrNOBRNer#8+F4kz`21>>;*$M;4*E$l#OrXjp=fx6Z9O8HdDoUWzP`P87!lUmM2_jrHuA!}wN51*Z4V&i(o z>UG`I#A50B0^P^x{M@F#2FfPToua*3jT&rnyS8n=#^&;z20~sQHG(*Mh%5VKh% z>+5OJEd6HA>Dv0JE32WUr*n>i4-6+EcKBY~GeXL`y5w#Rxb z-Nm+iQ7vz@9u~HeNf>)%+MQ}O&5JCf#!3PvzHX|2Xme{_uCdwsZupp2LVr=VE9pNR z-@k@NA^k%_@$^LMJ&Vg(S)>1B3J0J{gd2jnB#$uroL_kr; z3cAV9x$?Te+OIT&Z}u!P|4(pZb)Ae*v*-W%Cd~Tm{9oU^zyEP3Px<{1nYc9SGHK)+$f%5F1XD0aC3t-m2@#bo%&-8fBhQe5TBkF9kuoS zi9guKWN)=9?h-IDriJA4PD7#S75QDv%yXW z{jfXkOX)m((Ot?ambTIs%QXU&dg-1mYc58;_?&DW43-*Lfu=pwGkBj(b;PXZ5VIx~ z^4f8%p1Cl7)XRSX6mGv>SKo0{wQTF_+cWB^M=GvE33@)$ATT5$3+L)K?h}NN}>VTIXyDi+%c0(|KoM)??klGDp+d1MTu|2*i+ZCmnvCm5#3%y<&ec z3xF_^AYgHlhT3qfRWnR5^CpnPM56SSPo9hohAk7?RH^;*vgfmN4R3LlVZ>9Q(9*)I zk?jQ}u1u*N6M4yIqXH`?^B$6j&OT3MU^NTIbLMI>ZErF==}O?m8Ko(pMNw`iL>8q! ze*9?tH3%l@OlY?!o-9=B)^!N{_Yi~iq~OV`6r-kKJ6Gyc6kosx&IO?(p6IH@oS~8s z&myqN^8jyJAZNO53YinjIz5Z=thxv_GdQS&@c3BeGLIfO7b$s-=eLZ+L+yQ&C+P2o z%hT@9DNQcg$y7-yx}N!bdVogm{`Ishe~L*&wZ3GYXrXSISXLbOFoCs*;be23rx zDM6uxK<}=Fgfw*GiHh66qRS~OIf-E7b{b56B>uxg0bbNHrv9v%Hr=)2N|Fu)>(eoM zu&s$At+7Xsw$Irv>BRz1B=m8FJr(L^&5*b9S6t|Vt+*Gm7Gh>pRuHrSbl=?hPQy*p zaX_(N&@`8p?HRy`ST#g!;8iDSNBczQ2yJ&_wrWl)0}|Q1iBUz!vS7T7-jJU#OV%u= zHWP_uRq-i?OV#Cwn%*6EPED)|1q9A6rq`tI^|l3%)K-yY+TIM)Ka?n-J8z%TP;J$B zceQ0(m;O?Fmsxn^wvP>UerRjDjafLk`^yvAU$P#FI2sc;E-+bQOZi7u`GvNFx|-k# zpcf?vSFP6zgk%9}Fs7J4#`AU{(z-QB@aM+T)ZwE7Ih|ir)5eNzY@LPh`Z7Zg#p`)U z8w_bPiv;%XrB;w%U3x7s|I^=JOv6EM z@i2h1=l|B@wZ~cikH=g0=f8LJEEfMSH}u&sfIpIfHeqwGV?f|M8hGP}K46%942_H> zu2L(sb|S$D;&ckTAN5X5hE6BI9v>P?B(Kel4=~%_f3OVX6ceKx7?k*q4<-uLrswi| zTLleK%rM@MW}32G{wS7J(LJI()B;f{m*3BV?kDZQqK~7K%=L0|vcX^tR`icaA)UhH zAV~16TJCw7mR^S4Nla0LI5C&p8B{}mMA7%f&xg;>Z1OEeHm=76d*hFQdVGqcjnHyC z^EnZ-`5{LD$_Pu2JebovIjc3Boa>bGv|&gazD5j!a*SD(nf8{ax{VBjC?G<u`?{2^kJq+uP2<#34XEa~aV zRI#5|8vo7erd(>%W7<$vD!`+K*Vr&|9P>vf0OAIkgX;Nv zmDX;rmQ4)9TW0E7bSIJr=o(C+E2fmdjDn+R_A|mV@BdFs4ONaQq!Zvze#<7T;f$z; zh0fA;*XR3U&$(VOizFp}{D5GVG)L9}o|ZU^h2oq~kM0O(#sIHCP`}KP%5TV}$|4-% z(8ue9QBN+HxrA(30W-U!wAONC=Q_IEaF!u)F*!TpeQo=KTk|`AD$M|Qg^^!mE?bvJ znM=p6pv7Nh<`=oJ&lq0gtZgy1yvxf{^=9|zZ%wjXgqhgC>2=4PQ$%@m#@x*l6Nj-$ zO<*pivI}Oq5`5}H@m);Vf?2hACCde7%6jiwW0Z{nneM{Y*EY5q`Q=ZyS$54_hSLr! z`5f*X}(Lb`aOZ4N^Md381w5uG(iQcR5)h5#mJ8baV# z?PjV{gH|MCO(Wi<`E*sDaLP{K0#gQcq9#p?>Mgq3^M+~aJ@27xdLn-G8K;G`m$w?* zOQW@#A{Vo>QJgmqQyqPj$N;!TdjZ2bl`pV4hsoh5x?Si3DRWn~aNrC2)WbT(>sfPv zYbizWK{%z`J(CVdR5+7bn_%UnLoN`X>`AOyY=z%DlSETtPtWbh1oGrnZ2^uec84tD zxIxhBHDOkK(w(6Cxsny=AcvaT=jaQTRCu%LMI;2-lA|b)>YEZKjA(F;wkg|COwW2PST<$u2T4+LP*xl?W-^o2Ed9$20EvZE;Sm#PtuR)K)pTwbr1>B_s)jzGQO_Az9#=cs z0bh8w=)@UCD1%=xNr`U_F(w?*>b?x9->1yDqWYLY@Et9z!GT9Gx37o=AxcI&OC^&o zpKb^!yI>|h5uz)6B^jwx9;Za0R#}QNb=jp$37mP7vf=3@@g&eTzJE~|ij={MJJvC& zLL3~;y$mU|v>+ItI-DNkI4+Q+HaiAlMz8r3r^)p8Vlh7&<%d}i)~-F#L4uSqm_R1V zxFJ+EFD~Q$kH%%(k()mj%FWd2>`>Qo<=C<0SXrx=m`?~1cFPsl5eMFiO^s}?vvr@T zwaaxps#-p7mifhG#?cX_Qe_hrZuXmP{1b!h>It5U#v8zlri|G6n2#V2|H%%!dA|y$ zSq+<2lx(av{d7Q){^YzZ^0{z&K&~dJsPqDfrDV0cqSi6M8|=f@F>n(8%=SQT6{IM> z4?pld-0$GOW_I|}LJkz8GU>>DhpB_J=uHkg7VYHo`d0>6hCk*2&O)iZBM-=$MV?1$ zqplt*`QXBt1AX2q&esFmc??cg2(Ie6n#JtElA%1rRu5 z53-6K_iv43{-sW$^@_u0TFRT_xdJw_Za|f7PxJf8^U3;*KAAW}xq9LomfOM>__!7W zfvH;~s(iV_!2b7tf#Kl~jQl$s)c~;u(yKw(laE&&@03%jif9^Goa+n}!FGm}{m+UB z;Li7gsQ^ei*M2g9^J8G9t2Am)-vY}3MQO|pm#LJSEk9EiqAKWGumm=Utuo4*#}kU`OV3zhp2IKKK052-e=3(clY)tJGrW z_+?v*Ua_f+&iE5$olqMEgjdKUb8ea{Pkc=?(aK&f45Ovzs=T`KrKN93d~dDt@uA^x znoz!LR2R9KYO$U4%#TjRq5-?9a5xQz*2!yex{LBSpY{z^-#Jvxb9R z^M7QrA96H$ccA1`+D7py$6e80%_Y8j19K*emyU$(OPCId?NVqDi93Gqk28Kn**;3{ z36qf#)-l8?GxX;=u#Z}E7vv7-fQ%jmd7T>IXN-fr*k~bOgRPm^nxzLk7AM@fAMAS|T2z#W*MLb9OTZT5GFx zo;U&|Bfgd~n&svd?6)k~dP2Ulj*aSzSGG9OdSA$6`%y2kZa>m7-$ck%U?5AE?*d!s zzP5#0)U!-jA~LlKrOo-ddzzW#8>Pu0GlnG@EhneKl9nVvxzLov->QW}Td3W-7NS-r z$wz7Q#&jIqyfpKqW16n1#86x=F^mzj<@c<50R<1pi8{Dtx;0roL+ZLPC++tRk1bG0NV$Q&Dw+6zB$_*L#+5|_CQqMB(AQJ4&zB6$+bv=w2x2lr|*fJo- zWH~JJE-RanW_34x#?KQgk4t76TKPxIwE~@mc7aKCVqJKZA(@@V$XRIZ=4{@V)jY%c zXg=gjuWs7+B5P*e@c4*4X=d0e^QSfI#gm0Kgvr>-Tbs7)sCo90&W$D%4>}F&nU7a2 ziqZ|wo=RQ5tC8MN=JxUOGR!Qx_>y4udAw9L39HZ<_EI-|ZXh)-c#x$wvN=E~>%gwq<4iaB4}mL;-XU>Vj7d1yg% zT!YI`sogUeoP@ogA4b5h+4C6t+PN!>%bd^W9L9uti|R>kip`5)i3EmD+a4Eg3Hj`q$#mWSyEt+U=v*b=oE}-pj()Q=NuW3{8 zvgEL$)W_Z{44+xFnvva@YVV$(3EyTuXXR4wmz}!FWGaBtt^UDMrKU#4->c z5kK!7Qg9YoJ`L>3xp|P<(hZs?!eS>2O$al9J3ClSee;d_GI!fDyWO~riITdcx_lwO zq)^<(;O7EUhd2Rmo?zZ=4lHpEDc6moo)B+O|GLQNn|ilC;57>+?4ArY|GLO%oBF?- z@kakt3$5?eI$hei+4zg-q%Fk4qrb>Zcjc!&YIfZ3h2>|_^evN~j<#BKl2x#mT*$ix zpPU7s(Gm4^cHQDzFHUIRT!^DP%sof7n&$04(8;rI72-=bnNq))8(Nq=PXAsnxQ~zD z^Ao6H4%G3x{{8MU&)q-2hi8fSZ?M0AWai5~RxZVTyPMNRmoanfV-i*QlD?Wsi*T6b|+IEJ0_^Cl?&^F=t9c z>kEc2$z)1cx8x|oLz|Dbqqqhe^UGI&x!sC zdhjAG@l{~EMrF%mS*s|hH2pjxky^3yuSe>Cci^Ar&kNse6ujFic=v4)ylq?ai|En| zAnR&&5k~b-TmGEh2TB75*Nl#JzfvvtEFu5j1WDqbULynK9QnVww)Hq0|99)j#=ZQ% zi^qiNjVEz89$0Gd_eXE{WXQ9N00?Kku8h+8EbN|H$=>j_ZPN`-!zi+HnHR(YL-{y1 z^1k#S3WV#a$*^j0ITji2!Ywe!rXdSSjAvnS;9)ECv=y2jcM7!l2{|y%04zXw=#KGC zaqSdq4@XXnmCkmTfQVIg*pH7=?jxWF6;IC)O#+VeR)erJ_QzL6F<7PX!o)+(nd3&( zLqNr$-7;fq1&2%Ruf|_S{3`TtmuH`{v-T2}-K3xhoctaEHDE??g0J!|e*V=ka}tTu`) zBW3;WT!B_Ly3%g!x~H*INT1(CR!|y?J!vOh+;6?>Pdlw+9TPDz+5^EL@xq5D7$VC8 zb`VeSSPs-c^-;iiPH3L`N9~i3=f7!3xEbvQ&!JoBj#G4nIj&AQmPVf98s#v;dN~V5 zG3|Bi?lkIR*3#evce_`q{?O00;=l8%v#=sKNB`Si+sMU#-*|kV|KU!ag8e@Q1v5fW z-@_)?lIhrr+-R#cazn6Eu1*S^UyLr+Zs%2zFCD#%gj`!?vq6~T3tBDH&P=%A&B0UL z#jskgs*h-Q0r7zxiTR+|<~_nBK4~|O`r&v;N*l z$>-5=%hWD{b>je$C$a8AUM&=2ONOR${nsks-W4{bK38(h;#B}?0M@d&8{&G%I%3m4 z!L)#DANxE8_*KmMCw8NxbWbPF2yydl#^Ip0Q_{MJP;O$bA<3DFl_XMh(`-L9_+QLI zJ|zECYv4I1y2OMMXm6#yLEq(4r*|pP!o0kz!dQ#7EROA8a0sS! ztfHoyL()Fe;s6ZPN&DPNGgiY*+d@(Qvevx9tfw4@Ftz7pqkYIt5-XkuIZ+e_8xfFJ zM7_e1yP$Y>$7oyJ*~zD!v`15W6LhZ3{YjY$3#Z)vO-Ic>a?B52TcyL75_r}SazhIY zqrB!eGh-QRglDJYeo12tb7g2vE$yz58<3nK+@6s7K-V{ESWlyPtoI1o*lvce6!>pG zhRpYGZZwjM3{8FU`QzlUy5$L!aq#}FseiK>bU!8jAX6fp5y#>dZ*+ps*?h*%x8Hg{ za{t`zv&8;?4|X3Igg#mJ1Zd9wf9vtaX7>Ca%HQw*@8ZEj=Qydo4x=fE53vU9#pA%T z2=I||-clgv$Fxh^g&i&XGP2MS&p4ux-=0Zc#Y z#_cSv{=Xs@**Tg1&jlKdMk3X>@Yg%cjGZ19wbh5$w{e#03b+Uhavj0+D1#)s? z9bV0$jP1nXPHMCFA}<*~`I3m_In~n*nSJj}`v_sM#|e2ReyK(vUlQUR6hr`?{DH8` zaycM`;ZS`i$-*eE7H$+?##2&#WCWe`w&|>N5ansLy;XR=mm?9EU6?ptC!D{-w)!`_(Sb%G8r{jS5M>kbP#x_uwR^Z zyf9w1c2leU(V&f`+sP1i_TC617g#GpWhZ?qmG@u1Cm#q53Ot!6+(H6POve6sn5=X| zFCL#tgLX4sa(?<XN_0L}@6exK$c^ z;jo|+X(mOp}-Tt4`za^VT)XE73$sU7-g3cbL?M%4XDktMyg|Q$$N+wr>V25l!1DlPw zG>sn=tye0QhYu}Qe)y2xO}gV4w4V`t=*Qz>CK-kJYST5HZ~`oV&JyvsG8C(wxL5>e zmk5KXdxdr_?Xv<)F&b}r`m{`(E37=Tn7=pm3H8V5FS_L<|LLFm^5dB9=psgO2c-=DHpP!`e|U@kUIFDJ_~i&z z2>5~aVX0Dih#oIP^y7uuO<2I#d7j5K*@Kxf4m(rQrXQ;J7GD7pI4RZl<9Ip}5+mPF zT^*Zgz|l&2!Fe!1;wI#0Gfi;LXTi4kVjRV<55@vGnRYN8owk!vC6II7Pv{7aYZX+J@e>)HVQl^fz?1tKjLRu-5|Z3P06FP|?7rOdfDcY#4vBPZJfuT` z_j`v2Cr7Op?@n3=-vfLe(br)YH|f!7<)P)P_mB?_@Dq$RY)=9<=Q^(lI>(<`>!STMvpH2a$n8lE*s7Y`+ftH_j#?R?8nTu6Z>9vp-L6i}BS-RR8 z#GO@MpH}Vqpg9rDJUN?2_~%Ap)zEhpN}dKiyD0)!%0wyDQ~Vni3tqwzD)fFjT>a`x z{y7U|hJhBO0pG_YV0p$eF!H@k0+gBG{m)JoB}W2TJ)5DGP(3~3O?!P^aPXwAH@Ter z3AaHx{oi91YxNe}Kqddl*_yLiO@7bp6Be{4?rL656cFaQs+LK`&Ln6f_UUm7au z`cWHH*am6^!DV~ICpdNVvFbiq*h>4xEhhI%la z^jE%p@S&lq(lV6*SZygbAL>zwZ;sK7tTfef;k6N>=OF^;so$S4VC(RWzhDc#Cch z@Bzfnynn(zvBnr%p#fM890gI0$~Qpg>;*dy#+?Ush1bzQXJ{ijDO{yGsNG=p8~u@u zpjLnY9#2Enic{0;1@JmBgS?ZDHf7#UcB&W@usg96v-H#9+}VMl!$GW)y2>yq;BbPy zsdXOw<8uGiKQ3Rqfd5`SsN1NI{SdS>I(v~*V2cS}9KO-iujV`R`W0w*5ubwVV50cI z`yD}oqHSXP(Sx-9-ckPh_<_^4y`wL8)@Gv_M(40;>p3Viq3zl)pX;l8NBE=a;SSQD z)Ly@807$x+iXkaR6SYr2P=&>W(&I6=H5!M2PIdq2=nT8 zu>!APv;_3oY1(hsDi!EWyN#9FZ5pU{d+3KzyIsYCU#Ssoc~pVqS29D@0}uau_z$nv z9gY6?tp4?b3O0~n97$J7fb{VE@#*>dbxaVU|Jbnq*!+NKwxX`5c3}%mpU?;Zi}-oa^Qg}2r8eoRh6Sqfsak&u5YQSn z^si_I#Z=W~Iv~$Ik&(EF!g4BYr`=M%DIQ5F+HqIZQWfG|~g7Y)!pCl~p8co681=%fvu%m)62LQBc%IAEW(&;%#~ zc^CI57e44!U!ko>3?w$OGhO6?x}tp3(IcnOO`NLD0RM$txvNQOv88alF;*a&EA z30`;z`{-XNRyB2s`*#ZCF$%^**!Pny2v&H2G6;tuTLqJRr@RU!WezHpZjW@5q~fh= zsnQmyg5Nx}1*wz6mxoQZ5t9Be^t*Aw_tMFh)SJTa_d?vo!%72d1$o|4f{b`b!9|id z@+z?V=k*9}r$6lPzEmqK-1_Pe9Ylk18~^e;{Z2BgFeQ>m=9Nu+AZcJgb4grmzf@Qr z(S>5bIKkv8(gwYng6>(Yo`18Uf8~X~sUBYQsTKjF$k}8N$C0zicEM!Bz$Jbk&FkYS zM`;1{3PCYFvID0$(4nW)8~>^k&^(60IF0GL48Saeg+@tgbm)^QyDQn6Y1&E?m|Fw} z6=*&5zbk}tE0p42tx*DDh&KI|6;w%R)vNTwKd=r`y8icq(ICEBVV&^0D?dN;>_3m! zvitwF?X~;+|9A4(>wo2C>u9I?bxnad#viahrfq8V>tA5?`JsJ$`0i+L|NYvB&s9~g zVqD(QMGtd>6Q{W-L2OEeHAJ`6pQ@?w)TCJe7abXP- zwfXXu$p!AM7@qnE?Q2$6@GwL$eEaj;R{P!Y{?WnioBgUOfpJzY@Q_kUyxl!M{^{`O zWm*DVgp&~D&DF|uJiu~pXLQNx*Fc?B&s*ggfyb4T)#}5_zqxE!*#38eHs;y?)*ol} z|Fy^W{vUVq-0T1M`v2YN|DX@QS1Zt=uh*;UgHeRFC3XkjS0s3;^|GcH8BoJ2qpLQ_ zr2xOI!8!pe11p7CgN(Q0Qh}ZqJLxM@$0mss!FD}D$q^$Wi^btgJ};G_3A&ov#+a*R!hd+D)By4gKjNek(B=`S1fH$ ztiC1HxV_(6+TB0dr*!?NJ@*Q5{t7VhN-xGiUcnJvK`##wvE+NUkXkTD|KHqtl8gVf zwsC*{cPG#N`hUOv-_82ZB7|*uYGuN|v|(LnheK>)r7uP%F1Y%Ve$WXfzK6DP5%GFK z2biGUnWCvpVK0uR5!YnCNPLVR`Y&8tRc&RZ>yK9aUQZqWeB6F}acnOHe(B7^o?MBW;G5G+j4!>T(f`O2r(>e_b0SF25NMQuV(uWyg~`v}c_&OXDMSBt%>DBpgD~ zzK^$|5b89-JjRijppyn#t2-bZt`sk*-Ed_b4DbfJ$Knkz8i)FJ5kkGd^a(I1zUl`T zir{+&fusHZde=JIf7u2(u-|UI+5LWh=j&S6pOkcB_3K~ypP?sbcx`?K^AW%w{>3Nt zK>fmJ%j)Y5^;tcrr}6Z0ranq@2Sht-=J#nd2?wCRe+>))qy1Q=*{Llm(O)Tl7wDa}*ucJrnAMBUOAP7cQ;Qt;wlBB4jP{}lFgv-}8+7<{Ay3+}Ym(3Nm z(oJa4|5;z#cyh1*-^HWT@$gBT;)H)Do-Z3SOsEzN!0L}F zYY!D~fbRDRBj0jN7Ct%?;|gcXM#dpU-mB=Ax;l4&j_<-ZM<|YmUPNuzbf3&9xEvCU z7Dtc?&VxvKo@XH!PAJMq?MqnFP-jU-m@_D4;-?kH0ZOYO3_D;l#^8Q&Wa7L97}OTD zgRnR7&b$i!`NB^E3J%ny0Jd`J7XADtD9E9=}?E7J}~L2l^aftaJJDvVv#nr>OsSy)UW{y_syTPck2 zGxkxL9f#+VEV>99+y5fsFBczt<6_gt077jJSJj+`7!m{exHf~HLKZVDU^idk zMaV__S&l;3YfclCOfrO=(NtbB17=b?pL6@EY|#@VwxXjleR`aEvK?|o5r>1XNlWrde)- z`^A#80ENl%Axn=8|ImWNGA>uNpg8Jo?GWuWW`!`XgG=`%9&fb?LoM~`b;8twMul!4 zOk>#=p@$5F)1YH7L2FBK2w|d(RrSTI7su776^(>Mc&eh`9lZXnRqB7MXV(5N>+N^l zHs%MQyJCPGOFjDRVz_&!S%kD#!5?805YEwtibC}4N1YE>}BL3uk@`n7V(%VjQJVuz+s z1y^CbeKqU>ZW_sDy_FZ2{~dp+q_5U9FaF=w`g&IWZ*AS%|L){zHeHebc)rMQHsSv_ z7Wx3<#!??(EGqUH=_Lg}SGo<+4`6OC`vJ(U34eff8`2-Z%n|=2|LI%~mHM5dfPy%q z?5zG}y}GkgUFlSnTjnntD4+lF7xU&X|KMZf;4)xgJx^p8uNxTwjw#mhmWNvHsP#YV zt|2G?2OJIQH;(`Dc>VtT?@pf5@wY?F$-C>eF?;>r*j&$!{|2c9_v`;1Jd{&lIhQ|5 z`*7p+A3)rA{RbF}uK$emlIuTLx{dW8z}$TO#}ky>TL01Lb_?r2fSI@c>u55Qo_#E9 zNARj}km)l3sAHMHr?%J@IT}Wsw9$r5N4Re=@*aGQqf>OR0tKOm2h}+Gk^hMfl_m{5 zH;ynQT{3|^C?|lE^9(kdGB`ToMGJY{=l99K+TW@j@jHS)==5zv|FyY631j51?JfB0 zTm9F0tW&s{;#2d=)JgKMhQY+gXpe0^1|R@J6P@B)69v9i|8cptc9Z`kj1K45Y4ZV2 z^F2`G14g=GA1rbk+fi%Iv4SBBo1EK0l$K#INfu5-sXO#3g_w>pBf9mnB1Ygt3WDMf zB=x9x$&p~<>%x>8H=8w`)8Y*LF{WAxId!~A4@1`Ups!QP*W`O!ABkAuXDs4Yy>T#t zrrOMaPAzelL!ieP)uv{F;6Uhd?&Xi^st3rm{&c{>JmpX`Jq%=r0vlOA-{4r@)4s>2 zz-ooYBW;-v;WACj$s9QuhUY=*($Lf5u%sbj$X|HSKb!G5qR&Z2rjDdzDL^HDnAjmo z9Ls5!WRz1^j+Ag1-xOCJy9W{%O4lo*Ml$WlhzRK$6d=YOkOTG=E&z?o4D(~^;;7k^ z^068~`%!NOm=X{cGaF?17^DU=hNrd#%|+I{cBdtP9r`1?zDj*Eomof>qQ5v3*6x+y zW1(Sp)#j$RWAe|1!T(n=tC4=C@!CR|X_q;!8sxPtL~i^z;n3{KW5i$!e%h(DhOOOhA*N z+~NGehKA%pUpJI)F-BPi)Pf+AD8&QP2+^IrM~?**5bdBwjdo z+|@5_qO=~Am0B3?u1#)J@UmuL){Bta!*JM|r!D-9K#F9!#x*Vpdlcdt1I!e_YSCz1 z5v#yf!5=hCvTx2#R!AY3yNJAyU=t8ND}J5eG~_$KG&@7>O6ztL`IHt# zWYkR@?^LD9l61xGOG?W$!k>lT%dkfrg2RBBt%Y<+tUlp&irKV-`Uk-!hLkTMkhOx| zF?k$$;1HxCcU;QIq}eK@twp-Vgo)J^iPcZngUIJFTfJw?Z5LaSgx+YHoYfGQwC${h z?JkF(Q{C*X->XH|)C5GFzlACd&E5`$z6AocaiOEu8@S@9fML0;;k<_pajzHh4y#Se zs=Ixl(BI<6eR_%)!hl3DPN2-eoTF@!q*IcS!?Pze39jd<^O=LRrHW!XSF4bf!5?Di zO>!8=qLlL+Z$hD;`IJ^f;GU0I{iEWL@*~_pIqS2a#v8?tX;;zvI{DaigccPx3SRC1 zh-{$JF^&mSH2i`C`xR8oGqHndxBb(#AhQ%`$`Q_FwkqP&&aJ!ksidXTS>69u-2JEH2Z}nnP+}Hb9l_}#2(sS0={cdOT;wi4kY1$=7MNC z7_3atQW((^KvoW}vDDxt>2AxKc*SDSXhOCi1o}|&o^1Eh5z$TLdWv#GmzmSsI9j>w z9$&Y&WP@0)yT^lgQcK_;;D))zjv0Gd6d?kpncKqR%D7f$-W z8#-&Gq-w!18&=MMOH(5Ty=#MJnUGzu@$y#f(zOULrf*#q00RMVYrs&Je|xLY*0&vS zS_G|cI~qc1E3~yAAAz20=&&Az3zrCl#tC6)q% zt4baWtq;q<;RU4_(E9eWkU*9dgWB9!78FeJVo=*#%YuSAoCnofl2EN;I<=N1Q>%nZ zt!0VSDxpzpSrWBMC?t}08Bkg{mjGK1FD-xD^DQdQ6whmbILE!XP`%Bm$qP>VV{#** zujfm(i|0{%=aP;G{K18Pm9QlePi-)73~l8j@geF2?Cb!4kpn~)*oB;+%5ei8{&?sL z^nvn^fYYUbw4s-<%B^=|BA<_ALc9wtlC+Gi}$z^#P zZOpmd$kvyhrOvkNYn9xFx0M&yXdWnM%*+dbed<^&Y+6P*CYr! zM%r5q4cza#$;Hlky;9qIy<2a{**zf-c=lrSgaWDVk;4TPq5N!Ihb~9aAh11 zuo{NF!5(ytH!jeb`^xDt8XWV6PdJ1KX`*3-6=Os??Vkk&Do7=a6vAd=2`@)gQ$J2@ z264ck1*r#WN@Y*|gbP*@SFFV;nU3*j5UpKdJWb^6oDPhV|KI=T|J$b!5HPVQ*dU(X zT=Cj83E-b;B8yT5gZ-HA+#J;FqhS+c5Jdua6vU%J@BsUUDaaFA>M$PMg9*E0U_3+M z0OBX4HAky6l)?inPKG?u{imiVOJHyw1{c(OA5Q^0Q02@|D*k9Rj?Zbq#N+fy%4;yT zAqE$)`EoRMyC500HT}Jc#oX-Spu;|+qp$W}w8xW~J2|6ZNvCua1l`2bFf{a6rU_^N zHeM~*bxOyJ;RY!d9(jHcQeiLfkpqa*g7H}}7?D#Hx*3dOUei@gw#m|oyuuA z*pC?F;b<9Qy+$`{NggfZk~Fjrwtzy`tRU}xPsx5c=%B=yNX!`tydFab(G~d^a|BJo z5r2ef-gE0TD-SWEhZ(yHB{seSc|wFqg+(qQF_of17y*=i@y;g0K^>Krc98K~hIrwffBZZ;#k>2N?n=I-CvwkAbFJB4Qh< zkxaWNosp>Fkmf$Nk*L)Qhh$2OJm!hV5E{lYNG1O>^^-GM>4q!>VA{b8(;k4TeZ*u# zV-gE$CHz>oj<_|(VUn$=NKTJ5Q^@%VfWs729>-O~sVNgfFSo=wTFX?S=vyE%q|J&Ste1HG%E}pXS zx5F*mw2kG*|H<}~wfphEi)WGXr<}gQ^%KC)+5c~@0dHjUzpicE@Bi=O$wd6wx{vsC zAMxj=5q~Na?$lFq^D~;_?u}x)C79y;AoG1h`&D^Kp6wI;iQRHhrKReHW%vg=vyHAA z>HvQn$(X&0$EX^=3Vd?utwbZWAcZvy$4$n~ZJ(m0+DWg7GfIfOylOTwziuKW( zM)+47yIaf906n4Wg2DT>4;6zZyLjy;M1dN0&qkTj+c%VRTp_qaO{r#j!EsI5RzX`w zyEi^xjro|i@v$LC681aZ$3zU5x@Ec?_7#i;n#5rGW3=G(xJ2gw<}lId1na?Cbeiz- zuUs;Ch0Z}Z>uA%1X&@8zPiRP+Lo8_goW(Kh`o(B7jA3tr_b)v!=e`+@Jfu1=eSE8e zgOb<;C0gz{6y34i&Rx&^u^$4mGZ?e>fQf(MGePJYf`^$IXl!ZUU&4e961YJl_Ne{HU3^}mg^`~1Im^W5uy z_xj(jr2pv%4Wg&L;6LS~+`QsXC**itq}+}1=&Ox4%TaUWZ*6*+Q6i_4aB#mqE&nVc z|C#K++WvoQYb%@odHwO`ef-}$dF~_r-pl`6m;bmI*;PObCmYbmB)t4c;S=!enMO2? z&OkDz6Ar@374lsY%c(GiVPN?a@>i;j`#nURHf6krF>$7@l1geBs8rtmj3JxgttY5} zGTV<~=ZtP`8~Ay<@OyKEO4K$QYP*ibp3E!ugo`~cF1PtXJ(qF~^%#q8mgBXFpSMaI zL_nJjwNC_z!xWu|Hr) z8uLasJMB)|n2w^|3%Zz-qWT@D@-D_}8inM)Q`fct8Uz?lB%kH~ok1GAlR??tXctu| z=-Or4H>>R$4KbcPqDZ2Fw46HgMChb3jmn7dtWzz(axOh*&=lt055)c^*bW^__x&!; z4LmFz`s7x^R+m^JYLPPzI%dHv)bfmN3$8KDSQ6-`j{ig#{|VkO?oCl^>!SwXggI7in=vo#^{# zvR1UH7yIb%+AOQ{Dhr>NOfa3`!9Fc0R0O3yA#=4Wil_#y>=L_vnhrSEIeHIXqe_TVuV80snShyhT*wBx{ zK3>)L6SuBK5YMKvm}3EFG&PM1xRXqzI9wImHVH*_M7mjatRv3TM(t*piQcg(482PN z^5uhdgHxc23!3UPAaaM0_Nk#nu-2EH&;0Y0%MFQL4jTcs6`U~#hbQ|@^@_qesQ)DO zRwdIB8<;_V&`w3yOv&;J!o{$F`Jj?Z!V1T4_gZxrGhC~;U>ps)&0rxn^ek&d4zXhH=rZ7SoO9d{Ft zB@+$W1`Jzg49ecBB$z{A%dY_3(4VYy&-}69onT~HXJbGmGiD>+TEH@anpWUbm>P!2 z_jPJw`^f5Jpm}3z#&2`wtIu9k>lKr>G5hfc`ABhOvmaaXk;2MmKmJ!0wf*8IJMgWn z#WVg^*kH)t3fuc1Y)`Ju7JTl*XSwQH@U@4P)8Mz<<4PL(mj6C?3aoGcftT6e?X$@G zkG9q8um1D>e>T=~>;K03{rZ0=PiFmpa=-rHum88~|HJ+WGn|5o{eiS!OB(Wpy%CHD z%NMY^?_c6|7m2xzv2!E`3@2RH5wB4V_(Ha_;>?)(^?0-xiBE+IYbK3z`=8A!Ul zBx8hG+ynfk-rZkO!h2Yx@G4>#Z^8#b6W-3ZEOlAlk!6X0U+F_5L>u#CsXTxTG}jF#Az&G96LKX}%qw@a1x79(i?45wIIV8e6 z9KEdt(A(Tt6ur%jIp}R}IOuI}%|Y+m1<>2x zS`@wQtvTpzZyEGj%d_1IV)LY4fZ~%yFr34C4(qk9!Fhz<`r{(};O$}v&g4Ir{aV-H zK0r7cNEI*fM_G_O+3$QJM5 zCbs(-*hvW&zom(IBm8DkPf8g4j&18fE#*l;QV#q@A~|I(@mW;($6R5VkZxhza6<>f zUYmDBwWy)`RmA(FS0wYU4Ahl}8?}<%7{YApG;IRKCsj9z_(^f623-+Wk)%8Uv9mI7K zw|V_BDKdy*dg(zF{#~m+Tklut-Vzq6b>j#$T2Qka^|T;gw8NsWp(ul?+XG-~2@c#a zq9EAJ#o@Ju123qo5{?ot9>dnA_Tbrr+r8EITW|TDXA%34T;#a+5#Vh5 z&-TVvHvZqn`u2VNzq@(v?LYVSpWo>Izw=(o)*QX9;i`NMS5d1kbkL|z6AS%*W?wn` ztwIuf^_BW7P6hhK@AW3}={Uv^e5p)svzTuCqmWAdQlTH1MN?TKz1=!E**`kieJzSE zlW!OJ^lTVR!Y*rMbKQ0+N+r(#of}B`1_%cl_SD;P-0?e}=<_Q(!sH;Pd^3;H6 zd0$^zB2`K9{ zRG)<(yKxcuS`@|f_Q>Z8aCENr73Bz|7^K?YTNE}c$er-p40NRtJOo`DQT#23Dy~2Q zr=~me$2}A(J(hXMz-)>+4NrZ#MN@>BgBMWom88yD8a>MO=??}Jw}GL2)KM(V`@`vE zG@X1b@qXvDvu`Bb?<>ys8zvGTQ1b3&vjGE3;WU)ghF+x*2wJ63=mb#@ zglQT>^VCC*BrHy$xEqDeV>m)9X*kfA0Kon+>~aD}4JA!+%h~S;B#C44A*h29d9kkYs5;$zGey2sN!gPBJSn7sEXS#;+$xQRN82fakHm<~Wu z#qy!F!Z6dl%Zj43h)om}bejr!z~W{u<7Vk!e`TbrJfdbp1Ryg7p(iwA+}eENQpVMpz}ZVXs4a}EU<&@!DZB7@95Tr0y$|1m~>r0fUW_m?Rg;4a}gp_YLEK~3p}MaPSnQt9areQ+5LN)Dc3%3{r5gf z2sHK@@)}8jFF3&)M*vYkuD=$kCBdMN=u#@kV>eAPK(}*5rqd;s+@l|*9_2*)D19ZN zVY8HU54wasjOYdwB^mYXmeR^%>#D7xZOg(8|J1CQ>2M}t>o7TGL#I*OnraW^1`2g{ z$No&f^Rv8_LE|c3fUQ+Co#U9Wn*7eXNQf-V zt1mII%d37y-6jNh5C*RYrOqk|}ISY@Og(kl)YxN>6VY|rcn#*@qK`pix$gRtB&2tDr zxN;sCvy-xW_Z3-gHaAu}q#5I^PfVP7Yk$~_g9KysSRq)ds><%q=H3m5KT?LqLgxQn#nKmIcjT^Hygj!#+a}sc-ccG3tJB|6%BQfjv}_rqDuTb(|%N z;cx;1I;&t+LpyK>y{Aqop;z6ZEuzvSo_qeNSmb}DGkifu{}qg5pqCFina<20!co1Z zoEAhS~zDDN? zKU3;_^0(j$Tg#teAFG*u5UWPrLSh{ECmX*w1i*d~U~D=NpS zGb@FDL2>=Gqz0>JsGZ4TfyT{>ClMi~|Dg_u?$SfAZK^2cM&&|bV>&2-$=Gu>>Fg}wW622|Pv z=lXse;3mNCgG}V&KAh`4{*0-ZGGn0`xCq`u*lK$&^7(68OZJ*U=X-n*T*8=Xi5EDF zJb6M}qRigGa&0-&SV0RGt1fz}#KQ%Wns_!6$+?rIHxx1hW#!s&4;)Po42`)bKw6tW zTy~HONdC`U>6)^Q~1@i#cNcRKl;cwYn3IU6{ix zW5d!Mmg+F&+_ivZP%I!egmmi-qSUfzWUkKYZ!91Zwr8a&6lv{4vx^q>#-IdT>e)MY z+l--Y7|yp6d!cQ-v5f>)USPB&BdyuwR?v%=@szf(=Dwsh0pb9m>j~%M>c*o`O2va7 zcU3NW*yA?ge3=>uN=UJK)!4*#5wLmBHi#LcTHVXsGS1GKol?X0e7km+?b>vBpc@XZ z(5hwGv#(*!hCa$VJq)AVEPtpr8l}c~$DpUd7<<|LR!`J|Grz13T^E~x^Aop)4lIoK)>}i89eIu`&!4;kMr^lF<+H?@(o_HD! z!cT#u*)$;q8GJ@%VJ{k!hvD(sY5cDUmEmi-%*a3q*(VRrnIjSsXDwU-t zPdO22W7$fZ#Ea>avH}KVuq$3D^2?4TbL|A;GLmV&20;lvsL{X&sYZr}M6U$Kh{)a# zHbRtLHESEK7c9(g5pHd*0UBL;^D2|oT(Cmf1fraB<UKr)0S(m5e^~BhY&gsEg2TmnZ;=o z>A(*q+v((tcMo)`jzzf^&ej%3d_c*|6A6JENWAeNyA-85)kh#Hi=%CIFmRB;tgzBf z!|tyfqi7T6TFIFMY<)0oEAwD@dK%Cjdz`)(0j95&vs&E?&z5bpt~_@S(+ar3I%mTg zcH6H)p@cuw9wDjKmTlKono1d3%&a=bgvo;|3zWt&H}Fh|6fY_$lDGDMvnLYlU$U%; z@74=|eS>jd@s|~~?i^Jd1zPQL4x+SeK1GZT5O(X&QuvP$_M;3knf=6|2kLPP6elOb-n7j1sX?Ip7-5v!ksYc*n&%;D%ar)e3YF zKugK9%&unvPk=H%$|5DH*l&|GCBt_8{zi81g`9PMAP9u{DG%PdL&B3g9T-Yqibdy_dt+gEON-SqMqq)H?1=@eXM0pdl=+%x8{jH zR???)Czgvw;K0!vdW~%%&z79HJD=TS>_QF>3xe#VBl1uWNLJWmZ8JngE_o=153zlL2>kNxCMRNRy55}@jbTp1y z(oku@SCg|KBhfEW)_QD&t7&p+GTHXdGT3&Wf+E;6Os2WZv{4+b2w~CXn>Qo&pj5N= zx_QeSsp6OQN9*;6_P#MKk`)^>cA5ALN)=DiVe8i#;h74pt-G(NL4D@DEb+~(m~6TEg`gb?ce%&}q6zbRnk011C1vKzMVPseS*@PsI5wr#(p<~bFXh+; zx$*Lk&W&^zZ9Y3%#cZKYi6iK~0BP(QgPFrFqcYbAz7!o=7b}fZl4O4<;GtR9tgA)W ztZn`&YBS&8({J0S6AfoUyc~ zg-CED7yhUgMr50nD4#Ztd;Xw19gsCCp7;Z?I4&S&xEVte^-vln%KS#s$LiN4X2(5t z6G>FJO};9p(|DTX$-l6;;w}pC8o}9lu=k+ERXP=8mqL~$wxW(!Kf|GS58Z3oqItow z*W~PEG>*v?9`8iq(Gn*o&`yBG8#mi$oEO%jX_mW@YRTv9n!Xt3=*B~bH~KOh_QSg6 zDb`HM3WZWOwHk<1V~bF42#RRMrwjPFWh(9K{C>{rq`qIumrJ$5sPoTKs{r4}^U0QT z;a?@}$IycTLjbgM(spIpn&=^W4kFbAk|z8(#bgF{uW%THCMEWLtLAZ zMw$W-^vREGVh;2&S8ny0m_A>v)(eKZUc6hf2;ups zb$NG7eM`4+^v&Ex1|}YLqBm$KJY5lmihMzdCG@RP9c{(SRK95z9vfP=A2@KC5Kzxw zz*Ei54p;iPhv2oD6kb_xDE==aTDf~&=ow5R1Lo%qp4?mQe zM{GaJ+*yj5r1eFnLsEF==pLmy6>>s>wR3wamAkr9nVPG$(s%2wq+?pJH9SGa$jdTm zd503xM$d?E3l-V}#-U z^)SW-PZRqla>=5Cv=GED3uYprALqM;5umNEJ@m&I2-3Y)gr5&Q6DwMm%EJQsE(R+V zZ>7Mf$gM(K-(DP?1ofN=Z*yaDcp@Ta!rR_j9G-+;Err)wn0T$Zv}-L+y4HNkwH7B_ zYd+mti<7N2pK20ycQJT&h~614({rC$xGdT?I~?z?fJ;`{u#n0N!2&0qqWX>_k1n9% z$hq*S{B%PtTRhxgsgLnTvPQ7q>Bq~DI?hOsg{1n}`-rZofm~_DI_L_L?)lJdi~RBF z4kzdG7W)VX>{`fi^5$6>~HKXW70iUb5$-3x)0qU3)Dr zz?VMHK^W9oO4#r_LV10AF`U=8?-FUVRF`0ld$vWe-rTrLtc^-gg7x;+Vpwl)-6hsW z^(er)wGdC=B}=zva&&7khQ3pNZp~)r)?(azr_9`%&C9LDSouylxiy=SMJ1lQph!IY zyTaPh)JyT6OKidW?x^i(^$y;zY%Nn;CG2-4svu}wWRb)UBzT>MkZI_<^Ae58=*G~; zaBOsHQnn4KUW{>?@HotbIO^qmJqS^XP^QDEjX2jkP$ji9acD9V8mt#4qlIV9>w0+3~U*HVaHsKn*k^V&c6!vSw0@*Ka6$;|d# zNPk~Z8Ka&l@7#IjLRQY0@@vm|3g*Ya5e#+_n}X(DhF&>38EA9lLJ^TN<3FEV^J6s> zT^2u|cUe47wd46zJ1(Hw@!e4Ecv-5=yAE@$EB1M+y_-+9cLh{?cQ;fk*;|)WZQcoR z5!I%PPz@_P8}Qjul@c+LC1W2gL^ag6~Q?y}VsgA`Na_nNo(xJo7h6&CU01{+i0pMvhbREwpH#bwg z(@^0oOdRjF<4@rz6TX@Ch%oALoDSbzgq;himvqgZRHTw{N)(;ka81q?@5)3T;-DV| zfrqpK=qi5>>(I}7L5Gnxm*MZ9e_!c%?)+IK{|`@w>t_F%m;YydeI34K^8ajZZrtbp zxr^sM|IdB?pInWKZjNsv;V}~btW7(jL9Ui z!)Rl4xpN;jkqGMJ+ZZ-KdOc%@L-cLX3}mICFhO$B^E0$=5|$(c0LWEK6U+p;2dRo4 z8_6se1Nc14EiYB2FpA8ypRqN>_!>Z@C|D6u8nUiSG^e8YDCu^dDQOG{FhLv$tdm(Q>dP)Zv}(qdYOH-#K^O? z7Mm>USqdlP;B5KC%z&&Oxn#~NcWdd`v?2~LQg(4(f5)nzh@ER*)i>nj4!s=OVSEm8 zm7VSXNp3NvdjK7_i>{DcT4i<*P`U??O>{@T9&TMS&Gk?&zn!AruMYLXPRVp?YmOsz zD0?dr)=m0Io!hy7@64o=z;uh=x0m2Fx6rbT5?`uFOT8zIn)L4@Zta6L7<{$jwRi~@ zjE57iVU)UF2XfG^`SzSaCv^B;HCO4aSo2E`U3TpB^uP5n9aE^^+Zv#; z9rVt*eOE&J4B<_rFPaZC>2DK06$y7n|4TCuED4#(>E}@BD?P`dhlxZDGSOf0>XFac ze5@X4u3BBP_xHS6Cqr?W4#c|%6c`zY$wt9gPyr_pg&$qLVh1)ohVBK z0DT|W>zKAn3r7Rp z3%9ej`@ADI)hRUxhy5(D@*4_8el=N49KLZcxgaL)jU_CSv*-u`ds&Ja4$Cx|s&2rB zuCf57SqknZbjNfF-g4>*3W+T{$cUwd4X8>Yj1lNZYc^(XJDc5SDTjKrxsbcj>2&ke zN6ZxZkyU7I{EXEw?uGp;E!P-Nl+@Cib@s(2xhe{9x{q4S^0EfV@L4^}YaUh!)B~^( zhM+rqdDzs2K=;R3aD2RJIa!lE;5!47lW@}wT)NBQ=H>1e&a!k~x#MiXa;F!g)Q}!# zSVd+?e!|UQ6RS>UsZEc0&Lk2e%NkMZ^>s4?&17^S*G22rnu(r?zKGw{O-@J|4_Fs55 zGWOr~C!3Gg@9n>L@vNf#akaAg&}SsWf4sfE znVtXZYg_m8|1O@)`Oo(K`Op3N&yCN2f1&|{l5YjkdF{cEAiRJM4;r-lt!wWKDVyr5U#T~7G697i6noq<;~9m% z0m~fr;QWEA1(!Y$ENH6pb#KGltTS&~ySlF=cXBUC!m&yH2z$z~?~|Q=6el6LDDso0}Y7|2SVkS5ov!EcFru60gdh>(THIH|)h3WF3;BnC7CyF0tTfbn@8Vo>>HO8E^%E-?}Q|LlDWlw?PB z7y>1r4za-qWSpGjsb@#iJ=^_y{=3_n8L4-scXxK@r|Fsfu(PyYcfX!_Qg^@Bd)@O9 z;9whb2nmFbErH@+EDR*U$0r74@l8Z9<{)*_3 zCnGS^N1E+^ujwBgYJaK!T>nCUZXmE#>p-|hEGi^}0b;`OTuxW3c7Qoy6WGr! zJ;O9{Fg4T0D_+%_JjZ0Vi3!}K&o4YlBM}l{F?vd9j$g=l*;4Y!xSYq5Tnbp zcRXhDuK z6_qPE6j;j5f%=8J_D~ak3*N~IEkrKNdhMk;1`s0K=XQbHs|*JBqA3U!=80Z<*>g8M zt6IhK^HM>88lf)jqa=PCxxV6~dhu1a4q^|Us;&vz(8Px4^TdW2tr!4Va7#_lrK76A zY-No^hs~jAa0NtkZS_p@Wc04-$fQu13@Bamr=r4_Rs{>PrhC(tmljfE<`6=mlIb1a zQKixIwmr(J17X}#mlx)9P_b$yWoB-uq5P1!}pPVKDlc~@%EVSXd&H7wv8q_vZD1VyTHkp#|v8;6cz)!7A@;+VXSn-e*SB$z#x>}BOT(VEEzAfolsyb_Lx*2yJW}jKk-llOvC~kf@qkG0F%B6z z9w$?wM}qho+Uf_B#4lDwq3b6OR08yCb}?wmFaTg0s;Bn_3JD=lY=t!0ur{XAK{T1d zZ-lu(c2XLtMLDpkwkJ9j7OWcBm1x!p(0y)8Q%P0!NDT*D6J+DVC1=ryde8+LDTE_mEZ&(gHjXaKsR zZC{^_vTLqYX&DFc6y}8o9m8+oDyg^0(D3 z&#F!*FGFh;r`AZ<+#0AGt?xV6U8*x}y4RK^_QB&%n>7Tjen5GnY*(C}eendVg8O5M zQBY<(vIOl8(SFTz!)w~u4fWt@bwe$I`(y8#z0>rQpyZ=2o8*q|t4G7;gn8m=_Ne!A zh2ad|oLcVHZ@mzqz1C~>2Srbo2~Cz}!(Fi}w&mLyJku~!aY6lJL_5FRYpNWQ1lp)? z>~~svbdV4Ge zz=y)(%{2>E-X_s_wEdl48;lph#LJ$GfjiS>Ot=90ZrcmJniqQIPmAA^sn!>IB_1l! z@hk3b+raH>zQ|v~To78dG9jlUKdkP~HvgYyY93S@z5Rb96T_nut^U7>6DN-Re~0mD z5t_dc@nAr4?f>mrWQe z+2xg`*|iyboS;&(bGg-(xzlU-ga8bi81_WR_&|uOQ|l@o(eO8|O2w$!7AKa*duv64 zKexsjtuF%`5){D#;*dNQa@!Zn>NY%{ASz*#95$F^zlLuN1ALJC8=J-{bsuy|>^QgL z<$7(16;-6ByIsROzdTIN<1}_f)CXnjE9?q5ooa9nAK%#UEOB8(C~i+8U~gCzBTJy$ zkl?e8BFYk0BZMNcz=VqG>=NhQrdKL`5c*0yx00aHJMp7JNGB+o5-;YWc*3Gvt*TQt zqM!^au^OU5%0$Rlwl%%F>)XLC+a)#Na*Uz^N|d_d8!;!&RdG%y3~|PuE*|Ia35-`N zS{$4z`JP`91w2e{C|otzuHP(f3dHC(aOys$KEQS@f~`|#+d*yuC@}{xq2cj2ojO7) zJ7u6|-3H4SK*z?1`s37`>&c^%5>T^&eshW&6)^JhcGoA!AEN!gD(GpRLlWFBN z49&DXaYJa8BLn#!J31661JmRHvtxyCzH3l$7lo;En4yfwHQ1pb5|fwoXbqLVf8u|%h++fm0e!t@M=0|OalE`zFF+_Y+dwVWWp6*Zp&gakv;lZyC>p@2*t z0+$GPP{3&ErtsmFWX2NiW{{6P)L~tE73t0?Uj|z8+^yOr$D%WjTDLE`-u-PtVH+M2 z#w3T3(}F4B)MPi*1XvPxT2Z$sU@*ul*%nWnKxr!Tv~U=+!->V4@UfLB zQWV4rISiOW5?cWxM)w_3yf0Q1nNLdcD#%P7-OA1mOeR?S;{g?B@%T6A-#~)+(N+$? zHagQl^j<(>8c4(omd|0}I2Gj)bL1YDRoD~_SD6m}W0+?k3}CR(`3y7Dv$q@-b_=8t z^Te=$!mcNO0SGb7`fL)wRGemlPs46+x)qufP|exED~LLbdAmH8a;hAf{t2U%e1dv> zLoSUSffuWWhtZ5`e%k1oqM&P_3w5JvgYmEn^PqM=5w3u@?E;_R7ED|YI^K-tf%!y> zL_%956;a!eqI^GYIT&>nZwZv@IO45yKuuY#Jw-qn;40Qqg$Du)jS2Cv@U2ObQm=qE zR14mBLq4HR3f!h3_gqDaZ69nq%y>vsL+TxzZV)l|gNqfpp~7w_wL*?EEq6Uv3tdq` zsZ7lyzZYyECY~?tuDtATwKt-wPcnFy*Lx+Fp%R#yMbHI67$qNM?UT<)c{> z)HiSz=tQ>zLLr^#Y>b#JM1Mcgb171?OK$`I3ve zDIy)05KgotN(AYjSzA%MmQM79CPqSBpi!IBk2rp@>HB;kZV8WTcpSVf86=@CP4WgSk)PuE}V$MD!K^twhVo)n$h+t z+@fErZp#_Cr^=%f5anaFrBP$k!YfiJi%!udNe)Qs?F5t1w;yN%|5UKqq6_rq6V&n~LS#p~7yv+v zfL*C1#9uxD#sj0|fKZf$R!Cw-_KfayrZJMO(h)se8t^y@Q5j{IK&@D@9H2Ls)j9`M zBp^Cf2ay+V}snXS8*sU-pN|i|(qhh8=LnD}DV#$+Q6|W9BMRD{buUP7WSUOfEm`_nx zoMJ7pC>S%16a@obBd%V6&*_D>-ET+Ato;K;g<(5t8vAqGq>eL;1-c60Y zcACv-@2GydHyE)yX&IGm7PL0maTBQ0l$o+(`|Q;5kcGf}aOglBTaS9`G!$`)r4c5Y z?i)l5z8KgKw_Ma(_G~yGRQv>*8%s|KgHJKiZE(2C(5Y*>hZ<)bC8!Jaqe?PrSZ~0Nz>D!JhEhC z>PN~ukyT`<3ne@hLX9K?cZ9l-*biMbFr6vfk*>sS00Vc>(45k}^zt@qHCrizl%JMp zS`x>A*4mMDovZwwINYfV^AMBi^E&K+!lTSe&ft={5v+ji@Ley_PGw8#dYYF@*ajV_ zRBo9mq(IGF~O?dC$gu|y-_OJwNkSx z)z%QlNWsGPpB&CsnMMTGu8-_EnaEp(hloiJ@S3gs7fzej>P+rPqQI&kwUwOptN^q) zzOB1RsUmGRjhEu>?W`GET;h*a=`;HP8ZmQ>NFzL9=%-t%if< zFG18cNR)qLw^fVOoLQP1tE=lO(DbpzR!0u2dIjD4d_AU_yp{#g)EI_AuTR>;+7KD3l6_FY+)*Jz8ZLAeCF&5kkU?4K-F@sK)626{{%cu5R3_(&)Yn4Q{-15*h|^gNbOQamwjvG&gB5gM!R533jS8smLJm z+C?l7b9HWYKAQllFDB;}&#cTXp3N>~7grO;LUv{59AKF_JvTqMdWBf%%-rf?Hpk%u z83AKCvjPLQHlJBBme*F6mvUKNZF#q_f;$Ul7Q)>_&3?8)yjRus^uUvUbg&=GPQ=#NnQ7P83(Mj-g{9+{X(dWMz{bjrvft2pxRCmeAa zTC0kQB6J@<)|MdwL2v+NObVQAP~5?^31TY&6&UvE;)a|6DkXG~CC2i2&)f>GPUKzV zRzsJl*;F#7$xob`7)(vB>K=nw&pmO86?VJgGQ)1TZV8i-YQDq!F^_n?ZlRYDbpkr- zRkoZ;)8p01s+60xK)InsutOvW?gAk5>h$1-Z66p1`4AQKR!5wGp`7VfY0IHa(y|EO zfEgf&AcGqOc%A{LOe0goH36w0B`>y|2^MN?Qe4_Z)jd32TQ>`PZnsG-w7BWI>;)xH zXK1sXe6FCkm2HwI3A$*bPA4F_6Lofj3hB2?d}mi{7+MlI#F(rViM!zzD#8mzdP@q$ z4kvFzZiN2ed=d7Q%4VeujC1ZbT0(3}DuNL`HNZjtD59lS(RM?W&Mme@$cZZ+qaY2$ zDB(+*POys-Nc6x{q_KhU38T&AlvzIGgy;EAv?(jvl%InTe#Tu=+Gh+B9ObbXl!%+GulB z0>3@ZSN@QhDza1+>;Mm89cat8wl|A&#oq;azN0om$*32Jg^Wv3vEt(f6_*x%QYpo$ zEb|R;8EUL-b_EA)X$2Qss|7mbDra?b=#4$CWU;*K>lL$H~6br(L@jH`q<;MuavdX0+TXbwmKal=U<4P)O~Af+ea zI2HGdwYpJg~aKKsr8bbB(X83-8gpxqXnpmgy<#ipGA zb>2b`svofYq~i~S_e?NrPVafEbo^v#Gtx zrf(|`n?zM?%`69-6^^gR3;|S(=~A9Hy_d38VbBRjMBshXd|$$P&|+{X3pVAoZixK_ zEkr@&$1yN2ud++wi+TWIu&ob7ggQkLhC_9RdAC~M#?XwP z2F9UZWaQ$@ZO`S1$snFx5fZ~XuuL1BBv~*@kvDVbNCBAZHFvvWmo};#)Wx1XvE362 z88Ehm87~I5j`z;8lN>#q;u17zt%)HW!J7<#z)(oUQmeBrGPO471WqjoL#EKW;wF!e zMkj_BCYC{-N$J&Zrv}#<6t{OO{)8)t*- zAAcx)r2ikvXFuc5*HHLRJ1y+q`u})%{6~gQ9OeH$oKJ84zu71s>HkOi|B?Rx{ipv! z^PC$|CI{uag6O`PD9|O3-=b}X02f13Km+hh3!$UWGxq7a{@+(~o=N?G>`4DRluys` z_nI}lt-Vpav0GsF82^zI<1P8WMusMahmQ2W!}#>wo@)ADs^HYb%`cIb8$rj9ZUjBL z5%d6W1Qp+K6Bc6({<*uY+ByNR@|60 zMp)y=M}7mS@0rMXc5GnHr+s~0kQ-Y5{XF1nT%|X8faPCXuZc?(#g&G`@T0h0QunRG z_ZSvn45yH^MjOB>L*jOI_*#TdFd>0(Mzm9qQI|xghKQQ?xj?KI&~*59Q$GgS_Mr>@ ziUY=RqZ99_%VA|J_6{aliHYXp@~%|bKk;0KIbu8lj{r(3D%6eg41tQdFe5q=7OX-X z^C>X|*8pinxsf!IPLvL2hgq~Lm6)g*hly1-@}%nL^Thf2JTh56KY(~4Fz8?-#*7#5 zpy>Bp|1hKHf^hf|I$%pP8Duy z{h67E#Xf&AvyzJo#21!k*XFZ`y0*NFciGF!ODn7Snf%=1%>3FcD7Mr?ohUNmKU`sX z8J4oMR6GeQ@l^5n@!?@3S!Uyb*d}=i>9kJEaB@~#`SPnPnVAdui`kXAGjrLMJU`Co z=3bW7N9Od}-27}lx0+dr~@6G)r)gm&{>4K<)HveiHrqM zDCODZ3g=OZ@Ez!C7zo%l9-{@pfCD5CTUmTW5_Gnsv_m~u*$ zJmP12X0cuu#A^A-a)5sxJJh>m(6!#>V764Sgv3lI^Bg1C&Muji1?(mD(L!rt@4?bp zw*m5YpA-x`Qah1PpRA~Gb?+`V>!-^f2KhI@Wva=e03SrJRaFV5ILULA1t8K+W<|N5L3JXkHVmc}R z&)P%(50;!8(lPhQ{|^ShiB|o8WOU?6|38dRi??s=$lG`1?Ym)bpD-9{I7NmmDA?d* z%2I#f!?(i45a>IcGU1GNMkiAG<@x~ z!MF30BaZy3tp4+Fvjq~rV-+Ir0s3h{(iyN;8kFme-Wgs~n)(7hjAD?`i@^wsET|yo zH5nf22GCt`gjV?QYM1-B@?Y+vx8BZ-s12wx@e$h zHYFeLVq*WUiHfj|!)|Butno+SE4MY@*3V**DX)}Tiv>wrw z4FAA{K=(|48t}xV%MBx)NE?Y`!|`~cqaq2P00RXeiEge=K+wisURgSyomtH<3Bz>> zObJCC_z`a_;cp^wn{?JX>Bl3uWdT)cSO=S|l|*QV=p?`iKmg{g5Jj;F@={s+6_O&D z`vpAZrWiKN$~;j-9ugp&WM=Z103^@Wb|!Qo#!QZ<$@Q_GS;$<-&d=pmNn|31Nb4R> z3*16D7OPl(15fC|R|J4oRvw`Mm8C)pxTtGy4RyZ^EjE&wPq#3$DF&l6Ekk(oP6cYv zcTI))I^3fUwRm7v54!OdlmT&6Dq7Ub&TYq2gDKv*v3KnF4T1^)#RX$9HRhdKbLamn zEqY=U!e!R-fJ1+P2IfEpMGZ)Yo@Q3g=I)w`koKmwE{mwHm=Wr6er0KCbsrO}%_a=} zR~PfzPZ4ZQY*8cecrRw5B1~Pa%K>(A(6gaha_8hkO6;jo%mFnu7~cm6s1~@BU0Nwo zU2G;lF{WA?1ckqznh{w$gNDQHc998tcWs+1_UM@2j7Creno0+sjg`h+N6#p*b9!Tv)bfly-i;;d-fiktF(ox`JAVC zjq3QS#aA2j>8x05Kvmmx(S0A*`&t#^eF|*8WsQOH4tJi?OWToKdWwd1p}4Pu;=Zoc zHVd$d1oK%AmC;+ypyFBHTcY+r_IR;XM6|SF+;NA(OJQY(pYlT`Qd>~!Jgs`*v>%kB z5|^lvJbRh0Cg!hIqtWKDV@ZC5;NBR;tT#gp}gQtKQV^ z8ki?&y$6Tlgl)ySQ{M!kT2N2pbfR^o!vG;wkHtC}kU}G*$PG}M>&*szC4HW39ARJT zE(qN23oK09R1KLWtwxp-hIl)WFpt=D7Z`U!40vhbN**Ty(^hD%UEc-w|oE9E%Od zp)(`+2N2?i{n%`xUxx;-N8@oR;o1he%|@0ZPEMN6qt7%J??gxNg`iyL`N_LrIz7W& zAy|gEMWGMS1dr%fsWG#0c|?;8FLu0iA{kvfg%9_fw{g zYnY@>z5=5ISrh1Gbn7zjHr*7(iS*DXh56PtZQONiA&W0ODyS;(&T!}@UOkEVu^V)o z2_|aO!sI$|U>kW*aBo=^U1#kwQ;A7O@ekP8X=2?r(JI9gsuoGRxalUTrKI1m>j`2A zr9bMQuvc-%<3 z*Z>*F$L@xoMfi78o0+RivrB1Xg`yK^OxY4p%VZzG;TRAB`+ooG|7PuN#up3l+ze5X?eKnVt@xV!mfn6N1;A!mGh*= z%fxKEPE1HHG3Eay%mJ%Bdb}NUk@zS)KD8Q?sRLUuLl!WS%kSQ=tt zDWI|G*3*f&p^r=2SQHr%<>DliDZasj+Vr_mWHo&uG9(HN_(a^$M_3z3J3NqDUQoS7 zKj=O)S6jE)iM%FvP2?sCN@{I;gftQ$xC>n2Rag+yXK*46^6Oo57Gsu73C{V#w<+7MIr>4lp0?Mnsl zBf-kYTtiE0qKitJ*B`9r z6;UgZsI>J*g4H(}daD;b`@lVwJ)os;LgKrRwpFJl6wU{OP9nhPL>oRG=^H|4Uo;M2 zoYCSfWREkSpb5y&*K18_Fzr~5U2w>6hN@9^IQPcIjExn z1nfXg2MDqp!~p>w*uBmOP^kkvCP476^P~XPIl#jLccb$HuZ)O9KuhNB9-A&MD%lll z7ye2so(j+qkR{yzlcoAlBtl#+407640&UV4+3rOw6mF_uReC+uKisJfE08KUM5~Y& z@ABO02sgQCZ`(d|xLYaVS1pR;qT}N=+PMbI7jf3W!7wepyMql7pYA{BcDAT|-ly_; z0VrR!cBV##dKGYLQzsdnS#d!RGjWZP>xvQHR-AFCS#QJ-f29v@)N)n4Q1f z$lkMN3=uAd(Y@;cjY@2|FmSZZlHbBiJMjo8PS}UFn!Hx0qy|D>Ak;#7KcK!zeZ%vL zM(VUN97!wCG^Ptyy`{1oP{pjLHfjO!o z@x1_^+AqLU2=F8W>;lg?o4v;v9@;0sEeHa2hLiCwvVDPsHAC4s~t>Y}+-+(D$Uu>hDcYy?dFOzU@h zeR}Hu@)XMrwy}r)KRnu+|6};XiJ{>m{r@mNslkXbc-?0J461d*H^2-Yzzr~i`#uN2 zNaN=^P6BYH1Gxoe@E~r189cySUM`nsUt`ugDd`e~=89^f&A zXva)W?PxwtKpKe|f^?3Ui`munGtea)V^#>F4$Khgx=`A{oRxNN%t{yqnZ0X<-x;th zwDOurmo4QE9^f)AnU~Eg=F2EYGP-TKD4)s`&6JEPhOhX}MvX3QtdaW-6N!Z$Vm?`I z|2ts}kC>xlx~lkoMVdFm9kmhm=3K41$2CA{{zA&wWJ7lkpQ(uFjL z2E1^4@8VE?h4NI1hKA6u)M)7?-$q%)`;jg5{5cejE?`3W0c!w-Uj@S(__kL@8t;0x zTFVx+vL+7*{Ix7hJx1u6J_xu5j#$);V&bhrq4ixsy+e`bpv3V&)Y+&oj)_EI(3WLg zb*>q=PXPq@BJ+cZyj;a<9aYpA6^i4jRDg@+;_)DP*HCNnH=$PfIA!QAO^QB7zbW0X z9iCU*B9*|k5Z!_q z47C;{B&W>4(rWrj+Te)|yH<+5!WaO=2hvo`+yKE`j{%_vF#lhgDDtYfMpDxU0UN7W z)k4WKb`r*Jx@{KkTqTG=hF$S3j!5Badur9gvc2=(Jd+BySSh)uC%wsRP%p@kod!g$hgsEr>iP9HTdx({m!ZkZl#58xQjpmD0d!;odIEq z|DJQJHq(m=W-n)_6qJ7XsNYCR^HT&a+%e1))i!ULs6$b12Xm4{XscOlI8({AhK^Wl zuxTU)s3HT{@_Dp2`iI6@rFv1ckTbPKJ15B3gNJ~XZ<`?xf9i8eA(>Zw8Po7^zcO)%k#0CPZ zA-Ep85-Cu@g5q0N#VG~H!qSG;?Dqu~sYiEn~Ma#d!Xu&I@ z7BUix1Y$y6C{R-%LQd^0WGTBkG#UEIqNoCjrpnxd+!scGC*RBhCJ9{3(#f$YdX&#y zm|LD2X#tw|5gtvEo&KL3W~_DuGBCtNP(~viK=uy;Ao*U($YwiYqV9Yqm&55W)h`89 z|2k_7>)Rfh?wH1*?ga zUTCiaT%6;`q$+wxP6a`L?B%)D{LIo~ZfQP?&I;7mImH;XpJ%b1S6(VSmXGtmw(#T~ z0i|;6da#XDF;w<;#E?0g`KP}_t3W@Bav2s8TLsMGl$PjsTPQJ|1uv9o5Lp}6(T?mh z{MkeQKaeY+p2q!8Cq_n&?teOr&tBs%1mg|y!F7-S$k5o((fA+AXTRg$zMAj9jh^=Z z@c78Y#7N8dj}46;-T!qcpXCeBx%t*-J?nYTgYOx7@@(W1Y}8eu5#LS8-4?Rpx&56zXoPaN8R%c z7q4u8;o=Ma&#g~B)HnN*jhmi(?)kTW{4<5zd1r1j^7t(`|H9qH=Z*i1TYvkbTQ~pa z&wXm-*w=3R;CFuXFTV2dM<2TFpFfm)@UOn|k3N6b={qm|=8w7cM}EP7`1k(p+wbiA zzu)`c-`RN9%b!>pIlXiB6V_7~F2AXs`GjG9>hj0?U;6Ny|M1DTJpUg)WUoK5_R$Z& z=;M)hz2j#dzvDlD@HO$5e(mzl*JnQQp--NB`~%PZ-sQK}kH79akAL8+uiX9R)b>v= z+wbW+e*0}Vf8%#P{7+|M=f3=c7o2?brH@Pv{JY!U@avHmytVwsTmEFx`P)~`+kW@$ z&4p{f`;UL@{M)ar<=2vL_)quGe*OyK}K{+DjKocN9R zzULR~yT?v_`E8H?`9f;ZUw!qDyx~W@uOy(>*EfImt1mV` zlZ>7G>3jaxsPUq|e)*fyH=Un4`^2^1f9tOtkA3V-_YQvfy6Jr{{<#P8 zPdumc!l4JxJn&ohJ@Ap&p8UxtKJe|!w^qI}^ZH+U*Du{WZZAD?_Pdww{;c`GKJ{1E z{?%(DSY^Ok?G%g_ibMCt}ov5!N)%H@BjMr6Ayp){1Y>; zTzK`@9)IUY9)IUIUi4eB;}5;Yz4ev<pjmj^ z*6h2!bMJd!_mL-}=3}qh81??^@1LI?o%9wT9ohU-yLS7?^u?d}XUD($5AOPl&;38f z{xM3nXxSDB~!wCrvA zwW`%s$D9x|xKiD8U)AO`!9k*9IEa}An5Ng@GMd+BxXALwSno#DNSyoYFE~40YJ{Gl zOOwoYMpWPMPPy#q=6*(W+iWR1AnVyKO~;n#X{|`POH&lnpGXhZDYk(+8=B)Nczs5jI%Zdn%?8nUhP-bVN9AjO{3d z+=nM6oMjVB9DeYqEkiM|uB`$EkUg3|tt62RJn*G+x>6E(z$sw?09{0K?Ds18OL9 zz99pGJfo86;Y{`bY=U;qAIM)BB1De3A z^2q|?6!D?0i3t;O#IHP@H?NZ45dW$V$7OnPEqB^_&{&h14dPVGB!pOAqTgWo9KECV6m(YXl7iG@a*# zh_3Xgzkg*EkdPCjSVuJawzRwi5BACcrC)~%vpwX4K%5H}BA-twLGUawtdJNcI6-Li zAe<{5p6|a|hjDvuXM}%Pl;8ib5Ul@y)~MiQ@8K!v`i}to|Eq^_vHwpak*ut1k1UAF z2a+^wYU!w2w=89A9a2I2iUv0lY#<{OW~pRr^v)UrRkd~Pg9TAf|4vSv@+RamG~jS*su7GD zc7iV7owA!j)fxD~^lx1}!{W{Z!X`|}vsofF`Ze<(Lq364_jV)jSlnbxrxQf1OWieP zxVw3dgHpNtg7|&H5YY<97+Sz^%W}5SXw+v5O;U(Ae8lZ3?hkNjJ2;glB~M!{M9H8q zOe|)^k9!(yx>jLSw{XgUNX)$nf|4(V0WddOfpfzg#F@p7Nw z{|d0UJEUm@cpxBhTp%E#{~loffu?Lt7z5N5S3ddmEv#rU0yT*`gIl9M^mlBWAUaqv zWMI$)L@m+=1Dn_;5=%oil9_2>CvA=x;eA%w%_c{FIU@==trz`E02-9sWeJ_#!txh~ zZKB^i`&WnMyR3la{KK|ila?)NY0J~G@3rqskM~UXP0!Ow@7vi7@G2EOro?VXfMJ)) z;I_HRgXUTnZKnF+Im78|R)GExgXOC!2)G@l2U@SuuKJzl+Gj@5o9o&qAYkJz1F*Ar zs|}uUy8%A%iU-mgxsy22%aAEBdj|&MA1076MKEz33EmSdz;s*s%Ch$92`B2SIjWEO zD*c%Zs6BnP2GPFb5d7Ir;SFKUN0cso#G<9vs(Ay)L1G z!$}O>0f|ja%Bp*sQ5LTZpto{0wDL$LKZjLhz;>DYt2>M>8_YIW6xrNiu`Civzl?>6 zap8(IHw@l>BW6WxH$P0SYqLB&)e&}X6iIJ4H>}p)&n|1TGE|+>CJO|7oOiIQ=RYo0 zde!vXW6_y`?$PX1bK@>iIpxL+_^Hr%6d@LF$Oz$?0E>W&oDdxB{9TvgN;^itu$bAc zxvB8C?aphyFo-e22)I0g3P^Ppbn#|7R%WLSu2XmVHP(z&(dBhy4x50z_m;4gW9^@j zyD&C$<&b>mWw1&fQ!bV*rX6W+79I9BUPJG*q6g0^7tTiSg#8Y>a#V$k2vsIGxcF7GMOWBe|%}pvA zHbYNIu4#Gj&oxrnZcDc0@`BC^vz;l&ol_>e45#d~Lz$7r3B|lgy)EkSM-jcKzNzYV zU)+rwd;*-wn+%su1Ty88b;U+xsB&ngrmQ3d$yG~%v%{Af{b}h?1f7hAlOy7E92jN@ zyq#xU8c|bby$YKZqJ|~&`IZ$P;}b(kae1?nQZk{6=|@EZ3KkAC>8%S7HLX#ChHNbV zlpY#`ik&G5BG5NQrT88LwM?B~PKp~(6GP01xZY9~QeYKZQ4F(6ywi740=ajVCL0+C zPO1r)Q^QMwr7nun+6&KZsi>{%f(0RNwc2ME)nw`&ela7 zwxT+DOHRV}p7fmjo-s1>qfXZ~2it#+iW3ql+Jh?~GlBPVkO$)KNq8=#I90B3lwb}u z2R*XKi)eT788Y@##E4{Gb)*N`;o?l1AWzYFLGn=p~UlO&i5g+%xy_=ZyDBJVu6K zJmLXy=kC-K)<(5d*JvLy_4=FU5g-NXD84197!Yhs1`)#hbyP7D!#N2UI<`v3zZh6< zHlSp2*@l^zNM}`Gw+=+hphTe2!}OZw=a5I}%Q4mt*Ft5Ph-0jc7EvjN(#VZUIH^Wt znTP-~j#AiLv?1wK+rb#JNU2knbJh_o4eq@*(+zt)2x+fapH;sV-l=UPAclp3Q{|sx zqG4ljV`567Do^7RDZ~q@1w~TL!^*t-p9@liZ5N(Vo&8*>phSE*v?SUUnMDz3{KZka z6uS;h#O7T@#g*9?f=r|-uz2TW6b-5((-QY}7~UlK394ztee}uGmHIKq%PFdbZTw(o z(v6gIoTNx)3?nU%QN}hz>&t8SljB*Lj3YGrWh`lnvB~cm%Gigw9Qk9jl%8!xW8QTt zxXzAU#v`d^IWr{EjpS}Y`|lD%xuF=eyBP3IQu}d6!?z>r_}XpOJRJYxNv4 zOm4{ZyZq^}T%L3W5h&Fns1>mfcyqFBWYUuusCLV7GBab?r3pWH?3r+8E65LKkaKaMnvP+SVIY!BT%r=i`g5^wZ@1Q?? zNJ}R;dQcX&<7{OuY(J~fS!Z3s)K@b>0j@8IiJ|7tBPvWC_-k!X;TGtw)s z<8Eu}vTP1+Nz6qo9h+A6rx*u}S5+BYd*)HH09ht0>*@Z;Y$UOVGAcC{ zs9kfsTDk-xl|FV}%2A~xtiVqet%OSd2+2vcVxefX=gK=!BThJ!*q*`kI=8tit*MR0 zl!3(|Gp}nOSO2a zm&l+x3+wZzevBp9!ipxD{=uN*?ryemzG~RjiJ^L1X6|nNW5u6TrsEYET%v0~eLoSS z(659R6IVKbPRa=T3x~+2k1NYu04xvM8JCC57{l;Q#6W;nAL{(#q}PY$);jK)n7&?j z*8*1%^tIa38Urso5|N?I7GE(~)uwQk>f3wOdID+R`_A{=y$zd8qAUkhwb)BZqZ#?m zLT?B~C%$*n)=pbQRr^C#rPWnWG>;CsEy6&1R5iFQk8W%WHu_3UYGdT9r4YZ>h|W$J zw0bodPbyWn9WS!OhW@-k6R%O~iMKfNQnDI2>qwfac8RE%td6}(Uw8^`Q9l$3BeJX; z+*vZ!Yj{Tx$E5&jOR6RPU}6H@1J<6%bvkV+q9>muvV-2IhPW$5}N}3P~O9}>d-6jYGpQUI3s7)HTYm< z4z+Z*Inn|@Jh;K)463PF95_-#T)OsVjyS-9w7|5U2>Gv(vAlyUjo!Ss3s;9_t72nm zimu8VPRsLR^J0a) zt@M@FCb#;-w)yP#v!qI>cH^p}lLI}YdxADVHkILvw;@)s zBls}oqK$7mWmvBN82)=}tXKE5Gt$2kqy9u{ZkK4-p@!6?Wmi}QOcaTLoGkat6KUp< zW$wG>S#*kiWQZq=Q;hS%BuZP{^fkP?p&!-#<&gp?Cw|#_Et4Vvt8R5lk>A$AM*5&6 z8%n0|{Qjnehj$N#Xy8QU*ZIQ29W!z$BJoj)i3Q_~QRgtE99hfxT}s&RfKI~KFF}ZW zc<#{E!RNypsfZla6^Nq#7dx?vVnT!DmtuGqg#+Hf-EDBxgEE7%WcFX&gI=cNu2GeH z4-vdIgAcWu9=Hq7GP{x_`~v%^*GMGCld$&Du-4IVR?C8qv$v+MdU&@SFAn}|1_;(ji@e+{2a+mA@z-U!Z%RR z#~HHz4J~z}a(?^^%le)3`8zX~ka|*Wm%+ z=m5f)JfqE+JSFd18O;k~U4RB}pFAUaX{zy^LNFg4a>Ve{vql%wV!zl$lA3AzY-qVWLS4a406Aq$eVTwd56u;;RNv#8%7BY{C7t+nhkq~Esb(&ea?Mi#|QrpjD&Ak-g zeL5TB5!z5@UU_;#PkeVyyw&}`Zo~;dIDapUDFdq4JtEfxR3Xm z3JWgVuWq>&hdEqc%g!Jb@dju4?Ro z1#Iu5T~s8y(z>(rAicKm@X*dCziet5wD%_+Ef)V4H&8E9M;NgNE+DKcKj)I`hb!`5 za#}QW8gjeZ4f{>3OKDO9y6;&lGkjfeHLmy;SkoI9ijMCjXO(UM5#X;*&zF%Q?^IpLTzx+f#OSZZGLS1B)Q>_T-z-ync_Qx8Rue_f%)-c%C3LfUk#mFYZwY;2EYaYd6@6VAp7ppsr^u1iz) zu}DCWK3OgM7(b&@@mCT+(#S^>B+2Z!>4rO;+TsF8Ue~EdH#6D3v%0t0N(mTrdE$FS z(i0J-VBnFG)(#k5jyGgQ(c5BRyJ-KIyTnl%2M0I|_W9v5GKPTwi&v%1Q~7pOnwS|-QZ0AI;|o{dvic+gDDXwe0@D1a`fy(B z0lAp3RNJfyLv4UutXD2SqFFAiX7Ghhn+grTF>@Pa5lgw0DeDwG#_idUdugUZ4r_2d zNOE(l*W`fK+ZKR6&MTaq#cOqdyMxjo9Z2sWoZ7kJBb_0FUx{5mM^i=wQBVo7UN#g9 zddOztbd}#Iwy2yYnx37c`LcDoA;2QZ|e9C@`!NY-SPy_6r zkLfm}8{4#enM+5Oo<(Av)+;g<-!P65&!0bgC4s>{zC8_>2~l0w$W2aWJ5s2#$Ytm!dpOvAGKnb>)ClT2>x=I{Bm%Cmx+YU z2VXzT4Xj~!>262X#n7_L>V2C(j!9CA$ZTq))?R0*?G!LU3 z+&w#n{4maGM~JZ1_9Hy^(xxxWSLA8B-TAX!339P_stZ9UgA-J|G;n6g=yk+XH<| z@5e5RsZrh16a2dhJ@)EaXXM2fjR9_3=oT;+-!KRZtAt9vEU?chVet>uN_&sY$ye4K zvBjqO_fNRID2MYZ9eX8~DH2M$L$ChSWK+e8brqUK2wzbE@qlyHgs9q((cDB*S%Vkb z@};WUiR|);E$)b|4_2=@yni3#_8=toqSg>4AO2t|XN-UTAOA+*9|(#&AW@`2%lti9 zMO+MD%CIX=BDebXnd}1uaOZ0C+Uh%O4=5GLR&dJQ2u?j~D+i00*m*maNUL}z~%;$-CspM1X zAWz;G{d=o{{88)#t~}u6M0iq+bYZBpQeHv%hxSC{RpmRXCct}@;+B4Y_{9^q)Bs~k z?D#znQeO?1;qH^zp8j^NYtyI)r_@19bVh^<(fsG%PE0_{y|#3Kfq)c2|F37w|52x? z>TK%t&(dERdwXkFhyOflR@Ros7DW9Pi`QL#QmUeCZn4q^)n3!OQe7CDs0deqB!rST z34yAi?=;c0`Bd*FwX&KY<$n}s;~P|>`5V+Z);{Il<9s|hc_Sd;2dptr5vVvh=25Uq zl_MEO1!sH@bPc9!kz7-!syrZH_ei^`e8koNY9H?&x&$H_7D1{<_;+V2fn zd4b_)Z8KXhd5O;#$f0BeWzU8tiz<@Iyy378-JHS+MaS^vG##YItfBgJW8B3{hXaU= zJ638fu~DP1@OO%JPwU#29^tdLbzUrbj62F@xLWzPts|LZMpJy!WyG}DYGpCrYpq$i z7awMoXdfE|5UoeDP$j5}FbeAMdp1ah9U=*mb}7|Ip@Bb{!Ts`=y}hn4^9VvidqFfy zJI(nJTWF%*G=LQMj(!yTcwdNcP5B zA;P|iB0}%=B}4AE8}ncw%yx`BZS$1^SWMM0(r$-0@Bumg&N&FhydL#l9HHHMSM7e zz>43TD6eJm$U5koddhJ973dZ6JH8COCpaiIj$3%(6aNb*!N~P_itPY9BkQWS5{>`;`ix}<<>mN${JYaW8CgfD2>h{d=uQ0u+ zvPvw01_ENm1OlS|?_m1BmDB#`;e-Z+56TMGk3gL7{hLjY4I?qEAP|F1NPO^W@M1k2 zrU{OXv5|xj{9BzUX?DC^l$k3jo@8c=3;7m{B!#5*T6ALEbkmvK%QnrHckE9m&qRaE zAO9~ffvu12)|C&voX?rT*k7xJ?B%O~PF0&B>BD=a_AZ%zOEfZMoxoHYj}U^|#{*TT z7b~>dem=j=i&1N|E;-TUp^AZvqdgpzE+l)UTQf8^_*XjWmV@eqla0fBxArdKVc$0W zeh}#Gv(Zk{(=FHC=kx*}JRH+DK zM}`6=pB^G11hL~^#?aFh9=HT*cSt@e!=ty3Xt{kT(ER1Q%G52pl4!Xj=AW-sMQ>RI zx$3uIZ*QWWuSvY02_}z81o|maJ^Q2TXg;#*POUmU*Q0p%FF1Za%EPUf0sr`ZpDqo@eDt67O!S9>{*73fsvg2!nKfpWQ5w0#T&bSv_&FeaA)d>-~ zDhO5v9r?Jh@#~c84=pl(i-xAlXnW0L&mUk36RL=8ud%IecA#)|p4i3bqy9T$f-QD=SXeA4MpuVALYXmfG8t8Q8qZ9O-N9+Hb zJ8c)2f>Pr}rD1L3N8<2Sk)9n?F7rc0cT zAz5DHOcw7}W@JxYT(MD$$7Ambpb`>DjBS@+r33bR}&qbLc`$uDZf3IoG zU+Gd5jx=#Cz5kWSM*vk@o{1T`%!aKx4|8PIQ~a~7fH@IT!~VvMzKD_kb|52Ju0fBN z06^Pb*_mPy~-QIWtrZ$^&Bw!SdL z^cdV%kY%?ub{BQ@N3sHMdT5{IOKdSLTAPbC)3=8l=Yk=NT=ZCy9+*uh@`5#LUk$Ex*9=bFk)?Ak1X^;C;g(fb^;Av_;17d4e+HHhvURsWP7 z+_^)5v)nA-L(M7gAJB#VF5lCCZS7M+cyXQR!HsR)<%Q>&LK$l-kFA5?q@ZBs#b}jg z_%?6ZD4}{jUDojCwk2IvR7Sq()j0)T^d!`u%1LQyi@(FFbJ@@!8saU{^7ES232iVf zp_6KPK-*go38!2N9teXLP(?9q%eFn#0`N48U%WuY*{(aNideKnY5UKy=d33bcardgfu{Pes@K6 z(}$dm+<04z>iA5Il|}$Rka6A9x&~9`H{9lIjj4oB+?8f< zdF)};aRyy(E-6>>P#l}KcX!dkRjQ{B=BF@^9CYAHDOuv%l;UGVIP zW?DNhe-H6ExgKzJ9z1-Af_MHo30a}+G+yRmH_G5J8^H>F2Z0mT2#H<=?+Yaa)a4G| zvTaTYW4#P|dMAH?cz!@%BOG7RND>`_04|<_gNY!l^NJjS_XTl|5Lc9N+JHG6mT*B& zIVX3RMVN$Wks}vS)#X+X64Nj&Glo9#>3g=Rl3?ZYnwBsgP4h1i-lgzE{5|~gYz$U0 z6d)QXXM{3;sY^H%gWlu{p;2KprGQG4UJc~cGDZHjJ|nHrPR-ROFHmDEW0x~rbEva{o@fVGlQB916;jqU1aSbEVtrib+|X(}gP&B}plS%B;udnG5W8S!Y}Qy<-z7cq*v_*r)?@&qHGv zP|F%g=8SaPa6gFg4|5-a-zM>4kD1FgP5T|C&rFDL_lpbgmh=E*Cz7S%ZActI>gxyc z!!lN!gbTlqsiZ(znLT)d`I*#`9@_}po?W6h>KJ&(X*q#(y2qh!Z!C(dr$=+kfM(T+zH=TK;`Dw3lntfHO5$h@`)Ew9OX}8_ zOZK9-IvcxLJyqnCrcTDNY3Gfws0p+-Ad}o-SaaM0uF16iNsl(ZuP?bryx!Ks@1T#g zCZmsKx?{Xae)pWEr3&nE>ZpdCh7tsm&?anFaM6H3*<2v;6kF})m-ZkcU&kN+);-P5 zB}R{#5$C^_Tp7A)`aQ9_)wyPj(yu%s)gXA9F4uNGz-TRIx-r}v=|eokvWu4H%6UAI zx_lR!(&{*YF=0Z4sukgEzX*wZ>~= zUg)3Hg>Yy#V9he+$ui??GMD6DTr&Rp{jaS|0q#t$?ku*&&CyA-Ls9KPdU0D7cr$|! zQ2LUmFlp0)X-AKJFjp4Ej-}Qw_&nifb0fbs(Y>z)qZ3;;FQ*Fhz5J0uSh#i%%SEBE zXibSG4+o$-|B$-FxVLY%wr$dsGD?)7B{2s0hG4lW^@D5*kEM;Zd)ZnO%zK+=V|A zf}k%HnI{Oy!xBQvQOK%rDOep`SDV8b4`HD+j+T6<24 zY$J{OE<9Ea{q3Lt(H@MhGhr+3dbRp!bJOP!@~olvrIGjxqO+?ctPwZCfhg}E99Cn&6acc z5)&EQOG!WLfNef;qrJJfPhzJ7pJFak*Fp(K1%taxqXNF$40nq) zD(WH>bCFFJYp6C>!G|4{c@C3pLzqWrM%GCg4J6;n6WTKE=F6Ls?+uQY3^<0JNv#2o z1HF9;shQ@WdV9#bT~HN44!7ws*?a?2o3dNqS5!p3yg`L}#6` z-S9-Fq12<+a%&hijCbGIhF$s*zsdUoz4q{m3vX=g1!kXJ_E)j~s2kSxAUqzAV{goh zL28VkPf$;W@YyRlzKX_%RN)ue1btdgOr(e4=s#j8HOEMAtx%aJ6|k6 z!iLmaVK{;LJ7#*Sp_U}rnUy6uPDJibz@i?X&3XxgoCG!b^3(LfmbHiOAP%=*tboO9VT{1u z%;av{L$E4!IDL3szkB&B`FFuVt%vmJ#{T^u8^0|jT8c_7>+(9kF(YJ?L6aO_X!lJ{ z`2J(Qga#x3)$bLT^?gSFKRTP3Bm z$V;z+gTXfd;t=ooD=#f;=kuoV!KZmJhd0mBYNs$~GRNyo>Q1*|j#$iZRutpI2eWit z51J2$>y!`cOj}kbS92Wm=$=B!8LSst_JPNN=gkc-=3$rtsm1`e%sgBD5$^RC3iW9^ z^q?ksO5y5IT2;UzTsG5a%f$KSc1_(VcH9Tp{cg1|e!02L3mkv3m>5dP+i0h@Qb!$Y zAdTR2LcO>y{wLiK^7x{m{m)LFb`(Y~G7Y*E=0-)fEHC$b_ti|*^{|qY**axx#>px1 zIjAz;u`h7rjhcZ|Y^GfUOz@8E!`-S2C5J`@M=j2*8)TEXJ+Kywv)!?DEyOC-FSnl^ z^+GfCmmMeUVk|(n>mn}=vCXb8+r-AS+E8c5VI3%^eeD!4b5MGz8RNq)tXWP~SBLm3 z&Je!V&Cvpq%kP6OrbbV6YUdkTZf4)cFcYOYO%ayzRa&ET6(97wNmYwlg+>qrho}Yo zs2{x;uiGx94@TJbpgV~I^lo?FkTef<27U2;k7H==3Jo{Bu>7gDp%DR_3@i`8%$OX6<*tZx zolNFQJyi%pOU4RL4)Mj{bq$kuiiUUU34Zzzze5b0@gWw;^`w|RG~o8~Dt3`ve}yhS zV5&0g*Lgw3f>)&vF~55PAu!1$rDWbQ=I*~zXtD+dnZSE0AE;>Yg^Rzc87kqCW(Xgu z3to*tgj#cmg~xfY5z@%0tEC-1q4L+-`d3g zQGN^MgLc#`K=E=sJvCJaCe&6%f;@)r#!88uKv+*iXvjpkPwJi!wMjOeEscDkMxds3 zRp8o+=3Z8;s)Ec)-Qv2ku2tQ#yligmdsXRMYk%flYfp9KZ)Rs_mmt`8m}+m{m>p!iKd5m7y`|ZfE6jsnwu)|Gf&8nc;bZg;)2CP zA|k4Ta)@@%%Mv@YpG?G`IRmvqY)z~rIGU;&wUE=5(?lHf7@NCAH5lFAj1Xd6}EjMVCF_%=wUCBeohs;*tP21hM zQCE?fC0GS3hRk9tC-KbV%`6K$)&-%b!r=1l5k9K z+>FI$wXqRx;#7^lg11)DP05Pu{Od2+H0wc0cfwggg8baY0vlJ)YH?=zr)zjvKWqM2 z6wXw+My8?dA`>OPMAOCGREO!fd&h#kd;0CBjQsR|rUmMJ=sKNnYqS@OB}?{Rc|ZCK zRH$28HkEYil!%XL`GpIMA{{%DdXsr9OL7h)l?CcA?+F{}u@`chan@}z0*=dbSsZMp zVn%N%>C_&JvxXFvC`%<3mD(9>l~*elj_7MMEhinC8JtGwEVMlxdo8SdfU6qbvG$bR z8-OkVrBFMC4DB0uGqC?z1@(9e*~4y3g1T65?%;yiD}8lHpM-a9FK#BBh}5ioAyLeG zocXHfyt$9~kA}S)wxqV2Ti5~&3orJ;78Q;Q93MatfCX$@9s?iXHQk~b*3!c7q2KCS z!prDNW{|-yR-rmgJ)5>>x!Qd9*KClP=e@zUnJ69zfF3n(U*%<-r<`=P;YG4$m%ddq z@rkq$$c?gvh8e5IksgzyEH2bj>kaw5Ue<0NbJ(J^eC=c^FRrhfI@iNO)uXA!thRn& z71XCqQ@f~vC6wHH@iq2F5N37KG9 zcy%0$?Ff6AW^u1IIo)PgN^4vNBew9!IM!5T$nglQ(xTzmZ8$t>qVy+90bAC#u};Q} zG0+bKN6aKBUwktwt%)Q&Df?H!pqJ}F#97Og%&mC*;P28-xHry;vWx0qlND7emUE?n z9I?bvxi}*V z9!oNIOb4r}dCj#&E;Ohzq?xXo=@?7}fJ6C-L8Y`ZQsrsCU!HepHaCGrDZeb_w~w)2 zk%*EqBlOLL?znR5ldi%5H9*S0bV%&9eX;a8-A3lE4vWCC;z_~8O`QIKP9w&kNJ=C!VCRHT7n@P>-MK5>5$OqQf<6QC4-ij}ocneCU>xVG%b; zKxTX12RB6K5NSom%3rj#n}#hT=c{@$H?_bs=ut<`2m}mMWU4!RVIVkblIy5tHFY)? znuaajCoZd@w{o6sigsn>sFN+Ic;)CRoPoT1C%;?%F3w$GQ1QyhQ9Q$b=Si+9VJLfq zo=eXlQTv|hEFhUbK1q-oPdqeM0J=%D6OTO9V ziP!3~1`oMWddvKzOTIborrsHRk#f!*e~AP~DElPr4$?L7xC5wcvKxz$YqRM3)D*_0 z=u%odL?%8oOAJU@-33?Ye>SVsQQJyg!eyJe%fwNG<~HB6?szh6Xy@}XM$1-OztRJGHeyz|N5>O z-w9G}o1ki(bffrX9S%Pw4S>gkkH}KOWr%47F3fQ@^2%K)4pK3 zs;Ss-Q;ySSn!291Wd*SBG;Ze9x@tqwHhr*abjW+OOH|5`R8#Z8H?vV`v9UlMXcUm( zqQsMB*a6LpZmke== zmyGa`NG(8SA9RcLB@>5h#d2^4iTcVLz4fL6HVwH7x3t}UrCSdgfw*&DEUoGXc!5kd zSLGj=q5AYq_9rx;#m2F=jL?cS1ThqD&RXWvnY6@%suW8+{m37fsr&@KA&6LKQpKJA z#Q$jGDY=jQOpthLV6Ba+Qz#i%epB5JG7|hGs()OcZHiyY7_XgZTFcd|V@h5kQ<@7y zIUhTq46F9?U7Sq}WRaEYM^1F$CZ4`@U|cO6wGAIZJ%;?AM`|ob3jZxhTDf0#rSgr` zn|~Xt`VG}velPz<7xmO;nm2C$Fr(rZx+4mAuK7{+$n)Jk(5vvqCpgOR_C+KeTRx}q zjpj4ii+(wYdlltM647e8M#beV@p&822BtkraE1~hpz_Ay6FuI0v=Ofz+f9-8jCUR% zC;rrWdMH08oa4Hgdc?9gu2h)9%0BM=4W$Q}XElW6O!^g0R$OQgS`wX+rG|_3aoI*O%5LG2|zQ4cx-Bi?&g2oU|zH&r@?jOMVnVT!`z$$fda9% z3bUd7_tmkp1AagEZH&zs%KGse)*@|KfV33BE#^$?2?{h3(xOUqA z?Gc`R^;b4`|bY|5CAK#yY_71aqcQY_IwrlNrKE7a~OaH=(V%!moqjXju<^!0RNAo=4~!zVSW({`9S_MRUI zM|_IJ`LKAf;~p?9a;j1~isG2$q^MqAPKM6`B-aCaPB)sc2EgrkO=B%lW+&fjs(<1h z%|Hp9nFW)6O&e9?vZnx5lcO{nR!~%H!S?LsoceHJ(2~W+m(oX(v{x9YA8UuH$5H@YZ!fZDIa%Phe|gYMx5RRN?4@hP|4I{{@MGBt+99br$ht+nF2c65P!{? z*ORi0FlyTsZxpgr?8J`ttYhaPf>I~JHZcB$B_sy~vYei;o!t-%PLTvK3EOu#8m-6k z#pN|PIaCGA(y^iwuqyXL&CYu{!oU2x-l8zbop*WO7SbU+BZsM;8v-kM|@QPC;*t zFhmRHo?>nSqzJ#j(ex-(f~t~JB%onjnDskvaL=o0f$EPNy_ywkh+GKlg9SU`B)8qY7h*Tamv^S+t)^jp8^xlCZ2c}BV z0+%=RZQ!Oy^9TT&<%G=F;!FjAj03YL|%1xJj(wvNOZ zwE|QcIQVq9yjEXGRgV;~fA`P=X<8)4t$|y(|KjIUfxv+U>EafBi&oH7V&{?TYFHi+L3 z)7zdu7a!eES>7B^S^mAsP_cN(FD4BtFZ~s2x#loA?V9BThN&m|7B)&1_Ls5X&IQsR zeyA4W+1|m3@DD#)y$2ZRm^dl#SykX~EH<1|DvFc8nY#j`o;WW1q}t`B`B9aNnI2MA zOYrkf25&A<*DTk0!yb#Q>inIPH`*!MVHRz%P2&TbqHF;UdAVwvf>xrOx1`zW!rE>p zz+(g|upeeE!nbB+t}s5%G|98JkQW<7x79A}L)g%_$a!DCC?rZ1xcH>S>-?}CzO#Zl zux`&@?owz5Oi`<3KbRn`_Pev9UN&=tPu~KJ*9rD-JiYp5OF#MB4)cGE3D0Q{BZ7id z7Pa*1oc>JfO-u#3$XV}VRoFhERRvx^pYL+M*7lhG!QT9A!G43Q;dzncMuU>WDq>?) zGw%2NdgO9s#s6Adl(&AFI~TOF!z*&FX7>LA$`<&IL#o_l&_9ALI@wy@O#VuY7Lpt| zMZY-MpW?UP0Ol!*JODQJVNo&RMx*zzwE~m*t8?Dwcte~ewLdWO)(No#gSg;@B=+xv z;~^4;4Td7py0^iXw_*K9{Tc5cEQC2D5ns6DTA|5Sq5*Hj6LpfFT5*{h(IMV2%2fnz zgu(6D{Vzw2cSIv&Ls_Yq7JymC^2FUIvF+=E~HKPC02RTfIJCqxL z!dtyc)aYLf85C-}`$2|%WY*{oH1I$p_lBdobS3>%w?IP0$WQsB%VgFq-W$AnfP|kg z>#O06J^4c;et3lCOjoMUb37hj`Tbb40xpLP$^oYodg-1`-_SwImzj_w+odzWI3kB} zJ;zff!CD>{i|KM!%Bk?aOh^WFcfE{c4B~PIq)7=h;CChk00A+9*;M|p$b$J)(co&h zIh`7CNp%6wZgsf*D`?S#hm&?h>bl_xh(jo*?|^5Ub1iK7>(2C!FB`mVXu*%bt@eAO z@l@DkGWPeG@$5Sv*(pNaqxYiW52RLZUdxcvAaiRYZr4A22?qR z;;wT#df^oXuIWGY*!qfRi>Q1bG2c*ir=q;2VyVtQfzLbn&-x0BpBT`cy+Wry)4zd5 zScuVu`@dqsh86Be%u_QDF1PrBO2WR^#;UN8D8SEV@~$IR|8N!5DXGO&({bt~f#L?T z-~uvuLcZCi6!QJ0H$(!$8mi6Qun>P>zQnmTD(;dr+Y<`9BrC5$Q(4h}C|vM5IUS55 zf1p9yBdzDp2zOD5w=1_C5KNk&+q43ve`bHY=I#hMCAojZ{iQQK`Km}HDIi%2nV<0< ziw;v5`eBO+Dce815Z&Bfo(S>q+H2)u2jni#rg81rbFZg68#bY;9N<5?qWZU+Vhv%( z4HfA^ozEWRz^4TVoc-+%%FB>ne}4UIhOO9>u|w^jLwUe}fC&EU44aI-`TwlP4@vy6 zpI1N~?M)q}u4Ni=kV!+yh89%^*fWs>OrVempv=fWs*^Hnq%HGNqkkg&L$HU32tGkR z3!_e2;E**M<8pGGZ@6+FsQtgc-{E!<gf^2M2nFM3 z4Z>ne!QHV|p$6Ak2AMZcVC&B%uhR5vz5B2lbO-lqO9%Pgx66GK<=8u0N-{<(j3IeA zquNVHg7X)f@pR|#Tn(I#3JYVzHWfQsYMe`l5!r0B^A)ag$_Z44Rp@lj+G7mQ=;9Ty z@5e-zx?ySHRdWAmf&fO0d_GiMy?l6&Qbx3!Ln1M9S}Em7m&uI*MjE)nEJaIhN+)4d3wx#RvPiEMBwg8iv9Bym@=Al)kZyR_K8Eh=vl6WC4A(m+GHikVxYy?G# z!Z2|mUi5vaBdBcV!I7U&`*(AlLg3bEnsBR{h5U(!KTe~5r@EVKXosjn>5)im1x@&^;^rXi3T+f`6X&#uYb{E9&~>HG z!_HfAhs?847Gcdxhj62HZ{F@d|2rhD2(99pegOfgfdBz<{@0NFCvCQ%v$LtKk&USn zsmwn|TZ;Ve!{7g-cSK2B8b=WId)jPk$)(b`n4K7hO@<*y);Wl5Ok3f}|1u$QR=(C#c{6X)$I?;>a57aeW$u((E4T&Hx`#6a^ zQ&2O`UM)vmJn62y*ksUkzdI*nhJv_?aa_&YN z&mZaSZrgy+drHJ=#FId!;yyOG`~2}va&FA#A#QiMp@o!|RfSpr&Re#;7YZDCKx`R# zEPrlot}wfUghN4CFQKsb0{--9PqnL`za+JzRjJvmqG; zTD5Wfo|V1oA$RXiK>4=?uLCN!@b7XH+I4t)WQ4b@lS>}lHXjCYZ4!k?9JxoYV4+7d z(3hwScy!v(bj8U<(?tYYBEfJPQ!`^)^tYhss1#$8Lc1n>L(?j>2U(vPkrT?SZ;}@q zMeTuIF9R6~4Eo;*{m@HjX;#_N8uJ)Pn986K{i@g+v6bCVp=m(|O`;i=6GCr7zX(fi=xoSW z_iC5Dq2ZhFc=1hKj^CV>fAIb(YuQ=TmCg$ze-pf!^3LFR$iA7(Z2kG3x%dHgfd7fw zSm;{H|{11$fUL#cEHtU zvYGHdxvi(<#~-A)9mC`g!4Q3KikSKPxrQls;LPl!H*AZhAGROKM0%@)s*ilcFma2< z^r#x&gFd`s`YndK6Sc3+CM!v*ncfWHhYcU}ER@99|wi{9aB1raglZO*UFro5U_U$YGqq9GfW`L{1!= zk#eS|G@FB~qlTD4c~tJ^vfdPxK7O2G$Z&2gD*OraYP!X4if04Zgi9>@D3x|2I_MnV zY?q+GiJ}(~dtkkY#B(bjVzluu9w*QNn^$eDGGKo@zU&U-)Hczp zULB_CXcP>`eV-Sm>uTcl@mO%c?KY6f02_2fdwi3Msn@HQb>~Uk_~3XNwBD#uG2du* z1cNr_tsgyKT<%bWe+}o%fgxXJXP4%PE~i_m!;L+TJE|{?gCmok4xIeRdV=^%H#kK* zWsU%;d8*S8`9$1PD|=vCqAM6BHkwWp`}G~PW})IQfjk}rQykH4_m`Z;S066=FBDv% zhsq#?qv9|PHuzaDAH2zusenA6VjcP7b07|18dl0nD!V>doWh?gyNM7 zYs}&`y-MuP8Lk)cEVZq0_mijAq5h5rt#_F*NOSvq^N@tvuW+yRH9KO*`W586Y)}5R zF_0c%m3+!{WJ+k+DLOFkJ(4nNzc$c9y#Ee8U2S$@HM>@$rrq-LtDA~)K=Y{d@qq6-P3cWPp6aIF8>ig$Z) zX`~zbLaL?e-0X;}Hi2Y+e4GZ;PurcFlIl#s&XTAt$UGC<4n}z5JKHe!o(UX=U{GU1 zt*f`X^}epP-`cxe(c%5)5v!NNr2EAtTVomz_bnj0qVWwBDn4howN}AN0b8R;Y5Ibm zFF0{!1k8k#04R*K@q0Slq{XyBsruc+*xYfDek&n$F0wl)miu4~N4s%l*d)?HVvMM? z!L8RiN5)M1BN?9n6>ujc%D^rA{Mmqq*VU05XgkWr?yGlliQIT%M*w7kW&Wz_lA~{M zbvi=Q1_X9UGC`|cp>hknNS9CN{Pu6$<7L|4%A_+qY=39 zHEpC3^HrhwxNi@p+}keJ!+CI`%4ggslZ6N}~!76{(_-;l!;)+8U zUz;?1oW0Td!t}f_eM5RNg~r~BK)j`o0@r61cZHELWW;m7Bhh-FZXA*!#PLhpQnl8& zeA=La#6E2ca&{-$-Bj;y!RhVmvC1}37|kgd{96@jQ_Q*RQ7asF|}22vj4{i7fVxTiGPmR|6jv0wUxD1QNP(G zIf-z`yICc)i<*PAl^7(*ny67p(SXSyi}lWlvdBXL&iiAMzwDphwe)e#&x7pYr#Yt= zQyKq8`M!vk@5m7!E{~fuJg-~(dS5m7Hm}HizkkFE{OSs`_dOjh3Zx=9XFMBpm7z(y z>nDk}f6FsalOtB@%-^*IbA~?lwHPSEP~@VQsHYs3B~YQ2^r%9b@F+#1(MaA;**q-4 zu;$+!=~U{X82&(h7bLmf6$Z-)bPZ@()w1#Q`kvmOA0QrnlsQC?cFtV)rAwNh6rns` z1*Aj5u*zzhjm*iIi%Ofi2E0QJvSwKW*Yx#8{P7-N&ujLaW_+@6v({}*ZGK9k57Km` zYc5y=WMM?xQCOOQ0#0aokfc`E#iS5yH?+8`H}Y5QM&?@4Z6i@eXIUCEBwMFY*HJo@ z&(JstD2uE$^07#YLm{9Ss*H<-9<(wpx&ULwr|T(ydSP*x7BowDiDek;r%C)x48167 zO7RAn&z!dw+_5AYQP$|TT*kSyG}p3ab8m~afwC=a|jz6eeVkA6{9MC#bKkNkmSA~Cd*TQ-Y2Gk5mg zPwPobZa(k|2+Ju|;iXKiXbZyt)rx&dUt*TLE+(uo0@2w~jD!lPDOJ%D>#8IUD1sIq zv8z>d3b;NPN^K}HwmBo4t{GRBHH%&Z*um(ewb)KvZ^@mT*^ke&pI-3QXy51_JE;zn zUrWX-)q^RMo0Ll&kbt#Bz~X{wqP^r-u8zPnh#iOJ}}WmJlwO_{=W<(2b{rfT{2HsrVlyIv~bBf-!KI1B{9~ zv^_zY61%O!i`7?yY#eDpAL1SWe1ZXC;jd4kBsQ^Uu~2a*i`|u02-i(IwBC;~e6PY# zg%$WYk;n3uV_YrF0=CeWc;6-gtm*s%plsl|c#te~kKhZH4$zS;KX-WZ^-nGrfq~n( z!**P+5k5bJ=G#dVSO^}`fP& zzY8+jA-&-vqI>$funWqXxR-5LMCvUw)sjOHjrbW^b5o4t4pP31XU(gSTqSA;Tne&lB1YSUA8-AOJb(G5Q07g z)2wdoysC{M4wgoihup6#osW;18GWGRTgp)Bv@rwrNZcscYe#|6DPE43cF(>kA?!#? zUX$5pTY=k4HI6#nP{TS$3qNMKQRt}Zsi#udknQ(~Gi})7vQlGi+d@gUQo5IJmLL#7 z{N-8!cA@c2lN6;t;EH7tyJm%bJ*R!AK58b%)5pehMD9>s_oQ&^+Kb$rXwzo{dYQhf zOQCwqe&)kTx!$Wjv@xYvac+US^nkvm{IW75wBqNvLQK3f!CYhwsl3&EkHl|BDMp1r zPC$J6hEF8fqOB}Uc;U*+dl@G4#!sbrSMwVi4h?orPv4HdmhHS#d+Po8x-y!>6)xy6 z3v8Aj(KpQ<8*1E&!hXqQG~gGU=r-q5u6}Y2xBkA^#q2r^RoaMa<~t9%AX zebgROAFf9bgRI<3#%wprnZL0Y0)3B4m!OW_Nt}YrQObr{-{|8EiFjfOHegT@PDxY- zNd-(%3Tlzyie^!~gJ?QvshEeJbmjiy#}{jLNr+jIiRSx`Iw<>O=P`G7zVD#v&Ph6H z*@RP54g(I0WaPIJFgxQ8cpRvNj#ws2P|Sh{X#bFk#IU;&Ab%J?_qCrc$xZ7jqn*hf)E>vtHoTY`(r`(u>!Ps@yR6OJ<2v%U@yUVi^RPeMRGlpn`6cRKlfdEfJK@oJFhuj?+N) z0`>H{)7jFre-XN(9B(-?Ndkoo9E-^nYQjj{2F!En92e{S2!;}yj77OU16te-juLQ7 zd{j_guKCL{DvgUfj%CqiJQ3WGPd>b|oTY}~(8az-q2clhgosuluatGd8dFo0M^&Ac z2eYSwxdo&uI>?M!>8 z=(ts)6_tCV_*ExO5tDvdU3oUOqNI5_?g%NU{6^eV(hR`z>0+38Of@Tcf!OmZL0eH7 zQX5Nu;QdFJ`7p3mjBLp2(6(Was8y{aBapR6w&E)y!;|5Tv1->;r|?_H(W5M>^$}~T znh{EJw8R38ms@f~gu|ZJF2)Rw_yK^nG_7=IQ=Cz504L`l486w#-4l6JKX9YG(LQmm zc#GYo){oSs)vx$S?U6QxH=Gtoup3%LaY402-&{u37J~x~P59$Clu<#dRKZrgJ4$;8 z&63$Y`Pd|7FW13ZxJ$tg(;dNBfLX?}Z@SnYU5>n5^$FMV;JVobqw> zDmIukKAR$v-!SI$SGX1bqzV?Gd4-`6MK>n;RPEoneL?)r-_3nZ4HgCwQz7+vjIxsz zCzvz4nqWY0isCP~UMtIaaXmB3P|&No!%oSW(mvOe_sQ6-;XPg3+WinwP3B|k6=lM| z5CujD<5Eiov`|XU7IGG9ef#!pa9wph*;7sV=|(Lw&@J=hPZ%%Vpi8qY@iJwps{CUY zRb@p5_Ou|Ud$mokwwBl$>~c^lJ+xC~N=F_8tL5mupfZhvcyKercG{cakm%R9QtN3YIKAzWG%`p z9H!GR0wIapggejR+|hF|)+n*VK4}f!4$7P!=;A1{6%EZzd2tf-IZTQu(Dm89xn!43 zAc2?``ZK~YFbi{fJu#9!I^YmVh(Sp zn=^nT?wSJ`E$@wJKen@D!3cr4F8*(er%mrKF8npMk(dLRRW9n3&hb^ z?Rf}6D_4CTBD4;@+ zQK}9+e+8uUxh}SX9w)3I$xK&w{{eUe-n+_GoNCK!^l9GvrbxXjMhqJt_8(}-YOVq( z!hez|Vy*9`PA&#jIe$5M8uEOa?Z5B+iLfJb0&Reh*1JUW?(6*Ds&3P7S znp)aAK27mSYLR7- zk{Od+lARtqq^6#t8Jm=C2K#SFND%(j*D|)T{67dt;{HCS@c!d|Ww8HaX4^k+7c}}O z^@y>H@PBai$ynN%{v-TwGyPvAB*`lOIbR+olBOlhwt8n4;i%AMgFjtJ0&>nmr~+wz zXw(#I#nvDdR=CiAV5fkAb#91YHCy0Ho6GyA-T8QXOTgd%8{8p`1O)2x zbd?RUKjs(SO!AA6Cl{V49vGe|z+?DHN7FRy_`VcYYwN}VumrfcOEj#V5}mMehmz#* z**MS98c_X!DjS=*{*%9W8)D2oaPHD>>8{(jj4xrh)^Ppem9nB};=03gBIKlKw%RuN z)clisb4o6ftX8Q*#=eD50z2A1lJ07kt~=p8*@{}AF~PFh`Hah$O?EZ)gb% ztdX3zreETt{Tuy4$Fv)0Xsy>b*}nt9gxY`B9&_Zcx!l>t4@2TI>!!cVw z>QH&cokJEHSaWG$$zCuH;X*)NeNY!+h1nq_QwAfpKmeWD>!wQ3sQzTDza%-(XA78g zT}c*!`jAZHl-S3I$BN)cLj){#kpr&CL_#JeMV5kDE*<6NGKJvskvi+Q1s4X#2bwA?i!r5^;n<45sy|XRRJj#Dvwhf7gVC65}($ z7P0C!s$+h}pRxzlStY3e=Bi36ah|LM*$5~@pdAA>3(Hhm(3B5cBB@qCGKL*UtkaG} z)>Nd>*c7WQ!d?0sWQ#5A)5T=jY-O9(qODZtq&F`s@(GZY*%Dz7OrJzfEgH?N0n>~~ z;bsU)!eokyk50tqnHhTIbCR*Ax=U(J5n4uR*LRcD8Cnzj3UjkBO-@^+fA{`c*5S>z zpe_*W@FIb*y-YH`Sa~L}Yt>9A*b$Rh*00L^8pK1uUNI>brdg8_+ghd$cP&nd$|)#! zVhf7sV^%3DJ)dh)DzCJT&M4PPfuUNZ$>g`qVcs&N76wza34$PD532VPIS?HsTv}H> z#yDZ^h}^0<28Lc6xFR#eG^6y+^BKW=KMS+#TZ_qg(TK4bTit9$tX zKeVz#hhHqnj`GrkE&e%g(Cbd-#?a&F<|?#9U-HJTRjlfj(>rW2=?a$w)GxT1c12@h zHKrlmMMt^EW876oQ4mIReHZ;H2t)QSh*(Wj9O;3S9r=|LQTDe?DAh1e%>A<^>gKmB z6c)M-mMiZ4kv9NF_ijAQD4=Gw{TMmq3p@Q;%3Y#(%Bj2J-0fqWd9uqYYnC^f_g;-+ zea3`q@O8{1`33C)6rJtTr-)&Yq-cpvIhEuGMMPeJvFN@2YR=lbZ9SJ%7T(LY?I7VE zG0LUIL#@$cBHG0n6TEpjt*Cvuc$SGjNRNI>Wqb#)UbppfDeUEZPCxfmW8K0(E9$?0scYh_gGnYM^}Ve zFXuqgcm=!U!t+ThaP2b%Fv4xO)&S8TKodgxu(o~JSmz$_jCBRQrOaVVepvQm!+gT; z)cE%Y2(;47k{{xNE+4>yFCU0Rz=ON%dBF+gxkmhvTex8!IE*Jrv(&OCc91uGHz(zg zLZK8|k3v95*x&vkz5!`EG2W~VB7&G_0${P%;0c?(B3Zk%_1yyZXb%)2;yN;R;6j4= z4R~bztv8PPsDEH73A+ykh7U{>q9o*XycN5c6X+eujfd!se1|SZO^RkTHHvSzB|0LK zeRYFcGaeWw(I66u44Y$+I~~v%U@HKNS_m}&zG=TZ+PFJ9bG`@RxbSRf5xLW~UkPwd z33m>f26nXDC*kmK9hpC*k)+58B5U|-D+N>k3M0b_$;VUJqBQX!o!OJJglgCMG2Z*H zO6rsZ?q2*C5Rf$Re`zoOH%5)|6A2Y{FD?36KaV3J4<16i#PE{t2|Fi8ox3#w)D z@x&i7;fl7g6CyJ z6mwD<>$1RBGMjImVtICVUQOSjw&&LIZ8g~&LNqpjO=b^HmPf!@J1)4b@c*0Xpa7MC z#rnstnEzu}sQ%ks`8Pumttcx!_#4r8VR>Y+wt`}Ym=#*AR+dUa5fvC3YPuq1?mHM3|_Ug@ltiSHy)kv)@%4XSv(z3~FCh`pyKdn6Q9bVD7@C|t?nw9lL3 z5!|uYinl6TDd~uNPoBB?9fPcW&I?Lsr$!@q;%YjxHu%~fW?;%~KM4ITil%W*CG)q1 zfolex5DH!9)moM59Xl=#Tb;hjW`@+L8oYzXko6(t=&<+wzl{zg_8QYl_=ks>4)=c) zs{GsYmH*$Mo~@?s{7-(mB+Q9=QDoSmJ$%AH#weTUz)3|4kgdNZ{uD_H=ZV& z2gb1yb(0hoc+igIjXH=^?nL(^R**R^qasL)Nsf3Un^Yd2rf8ZEB>)y&8!T}neIgKAb>-9U9#kJQI6Bx>@;j(rY&SYqvBGS2L&^BnD z(O}XMKpQA8zVI`cDm_G1X>`*3(2?gY|MxuZwqlpiX1)o1uRu%B1Qm>@URq2uAgCMq zIZRy4f*SgIKPTz~8s9*s|Ks(ykh`(GiBH)siO(ebvQF%UX$#L%!3OlRVQ--PZFwMw z&^v5y zisB#4rNBI6ZW|g%JXLA3siJ;^1xrglJ4IoZ$iV)p{y!7%nwcGGcUSTq1bq zxaz_bL&U_qBAAKgl&Vf_w}*$$pq+Qhgg{e+!Zz!|9$CbxotKB)EQceI?eQ9CS!nuq zH%-40GGx!Vdm<}8aUNnF2Gbm&abDq#P30y5LDC17Wj?{ClK4iNFv&J22QWNjd?TEY zwxNgc@JWU+*qYy#gO`Q=-|yku>+uK92u42FML(z=vyu464VfB%@FU)0Cao@!NLV^R z{O(Qq7jOD;rn*DgYZ$aRBWT;a94^_taS~dM>Xf5EtvXe9UaFNE*LCm|_phsZ>p15~ z%A}9YJXMN|E+AD3lErR7Vosv94j3d?B3iP?|){94$MRbI>QJYn7(7=!CuU`fuc#j$rYIrz@k3@)x9#hI~Oy@7QW zmxeRlgDSljXG+a_dgDRbl|$#E#);Key#2N79#fl)DRPcT_W{Z?=rQ~m_9w*d=$7u9 zG3J&*9pJ1{FotI3em%F)3YLC7xt7!7H0r%e;x{cs5&tQp3>m*gr@Bl(y{wU zK0HQz@xe+N);N8&63;+tSn+h*@9MUlTqriZP3D~oJkcbfD7xBO?yZoG%)mA%mut%8 zYWk2F2ojg-fmNm6R+YAgwQ7?qq*I{(<5=e3w7P5CkJ}oKcUFVIvo!D z4*~>c+ZSIRz<&`#A%#VPPi^d4c5SQE>i)`ZNahVH6*T*YlOSg4}UrNr4UV7@6kLHGv`3c~Gg+D^Y5Q>h5VN*gSg5RO}0PTR7 zyC|ePex5|d?hA8XOG2YcXAUqPF+Aj(bj?ZS#N9&N9|E=^SOPH#R zFY2i$vaY03RYo@{%)@kSz*lEm>KbKSy0H1K4I$H`tB-u4WAaFNuNase0DI8Jp^3Ot z(Hlz~%AId?@{?p&>0yB8+tdZL3|T8Pk*gLOn6;=y#KF!-l~9Q>GhjeNYesXvV{*cW z?mwC#(8Ws1dXmD*cBC+w4$9bPRMbF8FgLAt`Jz$D4tT9)Y9;QB$9Gj75o0YVH0yNf z%%5C(VzIyNrbOe)>tNyga&0wGlu8LnW0)@Wblaj}+{XQay)eJu%N~r>9NzQ7IFgQH z2!ajJnq~^a@}2lgok(4q7Q>wRD+eFSFDr3ZxblxUGC32Ofz|*lK8p*NX^`RMT)g>u zM~S`lXwq+UvgBatwn@Cq!gbd!gBD;PaWYZ=Xg1RoSXbqe)yO{f0`9~{&0+<}D6w*>P%Dcl{!W%w88(9SM2@#bPU1ZH zX)1DI`Lj5Q+A1nd=O3GLi9X=y3X=S80lTg&L7tT9bgfMHmJ&sEn)MrH*qn1Ty{RtF zr*HN10SZA;jISf@Q@%bQe*W1o=fU9;U)G88(8^GVTAe1-)b0y3;*e}L;N2M3jfW&@;`h`=l@LgB-_d_D4>r1wBlTxg5ywz&AuSeiUvb7C905{sq;AQDSfH8qO~a z!HvI+%(|Y#J5CEndV`yg!3S(=X6#r>I;ZtnRd{=8iDO4~Xi3>%Z`3R@xTa7+ zEqnHn9bbL%)lBP0EDVv8B%xhCx)X&284+ZFBawHOo92_=2d}K{RYPUw5mz*hdyNrU zK)dl1;ozlXORnMF>y{t~r>8nrNr&8H-2*k*8-2s2({(e z(ev4~5a2bjjcdw%P(&%@iBqodc*qWs!bc?~0-h?^L$b{XrX+Y_qKWc2CO{+70aA6| zDW#A2dwagWn3UP-%NbA|7O=GK3Va>nuVR-N(vZW4Hq87RF*fT2D<4s@ol+|&0F;R) zO-jWwk+rRpB04|*-F19yRPJ0fU)>f6R!^8|+L;4$8>(Gq5;-H5nG2T=mna3;sf!#n zENuLWCH73w5yEUA7`8NPL0YkYUI3Rx@4v;W!gJ`y=l`%oJ}^K)y#Ftl7cw=ocQXCI zm?B$c6m?WTy9NixMhgkJWQp%sT&EJcE3Iz%loZXEf$?N(ZO8_}Q#=rEhC)#KDPTFU3yq~|HZtNL=iX8Pv zG$*qQ%=E-CopG_SkkL(PceQ||+otcr(`cQmAXnkAnANzJ!Rw|Sl}Et&lbOO~W3r+U zTR87=Te04+Zl1hc0EV>YTFENIO74|f6*^1GnvH78(OX$|7Okq4HJVsL4Ceql2Bs=` zuR(or&f_h19W$#;!SRev>_IHURjWm*3|DbF)iLDpbkpXJYORhmj_8EE2}HfQ>&%pv z5^dMcSxPovW=b4DzUEqmR;@K?<^w6#)p?j4HYLID&aeefbb#Oj=O1QU&B0_vc(I3m$LyA^av- z#xa7dayPgg7rs>@^-h$d4tskVta!jLScDxZYR^VFp0i_bW|E913p1>O?n~o17_&iY z<%}>~-0MCWn0B!;235;N)OEq+X;Jc#3zP-e z{-DpvRD285%mW@UYAIr)^u3Ccc=lM%S%(*}z0~u;WR6ORZbTsU&sp4Os}-W3Zq*Jc*b!(T}3~+sOA+#)|#*zfu2VIYC z9=&q4W_b5Cn*FZ_aMRU;mEA9d*WMx7tRWiW$2{l-o+z-Og06+X2)Sj?k1t-aW*nB+ zrF!AR{vizL@|XQTEK|N7x$6r?(YnF}+CqPHM-ZjRhy4O>u-;K-`h8zPKw##>9x3K)-WKH{uY~DGEV09o^gKIa`;AVU z9(krwzs3ewKwlUioe1&^R^xHw^@w&n*9*Myvrf&z&*7pPRHN$INLRU;ZGyHVxkA59 zQ#B%c08lc>woDlX@d&>z2?3yZzDb9YM%YS-q?#R`ZM@YIi_YS7Zh30G4uj~~j7%8w z(1T3U=Pc$fV{_6`^asW#-a3vxI>VuhwQ+9dXIwrxw2J4WWOq7xzq9m)sm_sb|9H@w z6X6fSw28JcKIKaC`^bKABzzM5s}4C#Wi96ZhuvHT1p*TLf6*brhR&vvcFv}D&Xz8g zZl=mEPL_7&|G3S;(CL3sMHl~*Dq0bd&;3n910%9Yw7R?+jZWW9T0bfPF?q-Y#YooT z-7f%U+icC&Ep?=K-+y;FHRIL5=UE)S zKoqx3a#mt5n)ll>UtKk@-D3axA3 zm+gEl?HU_2cH0e$`OM9W*n$Je5trYE+&!Cm-F6^L-##NeW6o&1g6226CrC)S=eqJR zy1aTKEUp0}t`(f^;9geIbKpn#IgdR~9>gDJw>VVPUFbZ4jFUVrz?R0tX zQ1RIEy*SI76!T;)QEeJ+TfpqKy+{zoq0~LFuhdSGY%HHw@LYbMW+CU#n0$JJi1U6ARxZ~7j#P7*}L2QhgbI>-zd8nx|se4 zD7Q4MJy4I({CZ6iHOWB$AwU6Yr5SN1&IrFEXPbx$iU7ZHWTKkZ4c%qHIaoH**FZ0a zrL?cM^k&)Byz3#?y#l0=g}N5?>I$Bg3*>*EJ>PkK-*@^kwb|u)iN`HSxjOF&wx7 z-0_WU>Id8$Ky-a748*>^auNs-9$?}p-5O)L)(yNu`BQj$Qby(8zZhc0?wZSdJV1W} zzevB5ReGun)RJZ)DpVC&YaIWv#yJiPP7Gd44av8ywLEu|7jnkkLbAAOyo_H)(vGBf zT-F${xT+r+VP)AiPkZ;9a&ze<&-4%d-CirWdp=S3&MTselqrsGaHlfVh#6iy>By#@ z>JZW9ivo^GG^@0^OzXw&sv}+Wnv(1r+U!`(c7z?i%s=ROW;6VU$;irXoV=e6CdU2! zBiJma3H;^S$nm;tZ?Q@D`?v>8rh9=Au2p(h$k*}SR4l*kB$CI=bJ_+I74k{L@qQUi zWHCNiCAwL)UK_khF&FpP<4J8jtsRF-cdUIG za+hbR^wb7mGO_5s^{TE*S66Lky6GzD3B08%UqLKtNrt8~TfK%OQU1S(P_Zz-cXxge z&NQ<+wxstGNNl^B9_ITe(Dk@I*T#Jfpo}ODK-9)#&Nwi$jxX*lv^XuGpYzoF;o9Q! zDL|JeLmM_)nhQ70Ur$${-SWlz)oj!q@Z2a@!(*vVg<1mzVWC**?UcC_-3Y?Z1YcW0 zN2j2;)xXrLmzTI*tv2WJmYQ^ymaZ37r&JcOc7(nh`43+3xT{A0Htg(5vfogF$sb?^ zWN(#J#FA|lVzIjlIWHF3M&mg0)odHCS=k^h(ILyxa0cyAw}Elcc%TbIPa8=LB=xlv z(C=7t!kZ@Na=9Gk`KzD}HBK#%Ht)O>0;tpaxu6=Mr;X+Mojt?`9-#I5BBA|8`%l+1 zrP{!kti(DBHG3Og!x**-&3SHRedmat;5R}x$BNpgpl%!s`}Y9&0B`?YC5Z9i29c0d=6k}GzR zw~^YOL7uGwV#r!za7&iJ9Hu7D1%YKJED0Y=Fc^FTI`+GnEm#-VE$K>aFc-=e>=|yw z@GiAATpw-FJiIEPyX|Rt^D{}V%StO#^-?#n0JB=WJ%4zr0eabxd}3SS!EEuKv8VMZ zu;v^XZ>A`Ny`_EqWSk^FcX|~dG))>Z?pYbn)9hAjeA3IwSU}JoPJ6}odU)6{W0q&7 zk1fD44Bt&h(O{8$eJVXMju>Z~U98~DF-Gi|QBCphfX{q4WAd^y{$#YxI;1GXM~fpy z^Jw&VA6nDlvg6xLVergtb6a0wKPAV%`*&}9B@s65{1$>HUvXxcdb-EV=0=I(FKRI_ z&t$M<0hohUTk_1CM430EL9Hn==j6qH&i(xk@n#*yzrG64xa0}ks$Cj2mHSYgbQ0X5 z@aO+xajdIRbD1~{*G%92{F&a?P7fkP*CuEjVX3d}&Jv$5=YUN1OK_W)-*yu5C?cdB zP!4%1?jQJ=x7qWxyv}>O@d}i#>ug8}MT>0=Hpk$8BfY{Gp)&kaVsU^fRbT{OJjWFb zFjrs?xwlwAEwXq(9hf_xMO722grdp4Bi$CcNRG1@;{Bz`8$o@C$sUo|CY2>r?SUJ; z5A}#7OEIp&|7=9CD`?{$4pDzpj6c*8${SYAKyvYXpp1n>7TY7h9UT!`X9+*ofE?90IxoocRxI0StUmxejf`(Z=W@sfeag;*Idfdy80I%QC<*PKJlrvUNcKD z)dRhIzsfzxwdZ*f?)}aZHqI6p!ok_#;Qz_xhKu~OeBDN#+l0fKP1B4>I z7f}dFfIvtfg(6@BL1`)~iUJ}k_JSfd#Ddrhh>8VN#DWbwV*l^mB_xC*K9Aq`{r$h6 z@4c@Nxx3xj+1Z)dnc3OBF#&F^Mv;zA2?{@#XUx;Z+G5S{I%)k@5B7^6^S^{f>Xf9GPb$$d}lzDxQFBpd+hLXm;K74LMzI*A2IWuZ8Rh{jIegvyNdRA&-DVf zKOA%O(AyG!caqd2!eWiOR4K2R3_sGyoRAwYCDOOH=vuu{2{5f}k>9g8ds)M08O5KI zUddBaBpF+EG;93b9_(HCi)NmF&icLRWfkVSr@cNG{mdtD#@K&R^6 z*-1a3IS!>t?@o_?n4wp^70aIFbT(i0wW-^+FGYxM-$3AB~maH^Txw%tw z+}iwbqQ(_l{G?h#@!GpwX(wC!Io*%9S8j;8SN~di%`vmojv2$7=jb14Z5ve+bZ7t0 z+8tN4%H}lTa~`ep*%|AhG0FP#57pcm`5G596-Ry^Ut?W4Z3bhd;{Grn^$i5t;cCBo zw_a~ezPEX0%l?BkS6mpAJV%dGY9En$eM%z5Bv)~V)%(iX(mMiQ6%*~5cR4J@!9Y^IcA7U66Oyq&+T-OGmKjx`%e^olw%5n3o?xpmS$%7IZZJNK?6mjg{A| z*L!f&^+^2a1_g+^RCVRCQtN}^?DZZ&;bJu(s?+Z`Z+(olYrB$NwOU>^xAk-h!#}Ab zciy$-S$cENgavlk)Igr^jrJ01C8bBmH|$$7N#SZsRk}*HJz>|EpB+m`JJyWXzua+u zM!NIoI9=zdWRrQD$m#6EHam>p&h}bE3fpg~ll|gg=-#psYZRNWOr(8XP^;)i^LI^E z9;RrjXf_e^+KQ$)SM~iXv5mjXow$+9SQjb07(;ulz=PuGWc^MnwH2A7brrSiQ;Mc;KDMJ=-H(T{4 z`mozn-0Mk3F&D(tR90^ub76w+)CEj&$+IeJS=p5~(~Y0VGiS8sOl~+VTQc6la)w9l zwV#LFR=2+z5&CLpMnkgAv^9fWNSFmpikJ31*f(U~k4nlMW1rhi`!3CC-NG5Rat<-z zn!df^kcqYtCMpqQzsP8jhQ6QRdzyG%|D&yDhsbvyDo^fXuLTmYLwtZXGLQA+Ni1p) z8+o_SYgZZIxE-2(^mSah+{SqiB|^s@m6b9)zg21+URiqG4TDqj54{eYaOl|6_FK#B z-${ON9qnS9qinTcG_`P6e&F?W#77&hUcFqvE{lKK{`32%9})`^cd0EuBQ0&V|2@rm z(}wj5s`xF65;nCJ;&S?$VTQ`OTSMm%S11Os*SoaeaLOm^Wv>aUS2q~C_g>h>uvv@G z1to-~d>9?JTJh=hLshSWrg0|hnXRro_X&r6@ z(-*rx$qBF4sMK!x^2FEBL4V}u5l#oqN9wD!T3Ad|SbP8V%u#m_)_PrCuJ}tQeEXxR zL*!L!{LIZC%xOOSWQug0HobA8c;fM&4|8-97T?2u(7ELOe9H@utpy(fuY9a}b$asb zrlm4L#3P$^eqLH_`TP$3^R}YmD2vHkv+E~$ZjBEgTEnVc$O*(*5=-49mZeARe|BDL zEM}%rwDX(PiLbOUcJrpyyAiP4^51KZ&bb2BhT*NtM?Tq>{)$-gL)j2>CQ)+Jpbt`C z#>wfIotW{Zt|g1G$EQ3??N-au7QH?0#Vwj=2Nzq8tneVV>X+IY&d5)(c{N2(!aaVA zn*NQ%@0Okt4zI-bbd+z}?kHZl#AMcs>)1F>)*PG)zOS0?WAJOWsLH(+_=fwR15lw!3{lv{(l{5;pjdJi-C z*3!!(mU#JwtQxe)-8RPV0AkGyPlN zt{Hbrh3-%{@v9N`oK&U!xMAE41npxPsx|I6S(lrAKb-t%r66JCxZmdxV7AbEz?X zOtO+MX3qVx-b&qg`~9ctb>B{7M_D^rPTlBxe(Sw5r;!cs#2eqL9F1ytbP{VKG*6@5Ec}w>$Uc z7bzLspLOcTGZnhl?NgK19bf-C+N1bJ@vlengiJZKK ztp4Nk$6ws?ee98&jtAPOPM$vg#?kbNX@rO=86zUk$=|p<k=O5;n0FI+Y`HsFzg8STjOLd%S| zIl~7jeSFnw9k$^p7#zilg z;1FAV!bGu{V0Ap#Xvt7DJf^N_=*B5rm5-B&Pi_pcpoj0zw6~4Dv^62yaevXqESIq= zVR7om)I8#%HY=WRxOR1RXh`wTaCcYckx;Xd`7I}sA5)546?cq&IeWvqF)>B7ao64! zezefZ)tM2!;lN@0X9hR5b1(rh$vS4v;-P zf6~xH_pkPx4W9yKc_83GoQq4vx<~QZ8g?2C*BMHY#a5j|zHZ8lHySi{1TAjr)uLn1iZ5Bl|NQ>r$H<3cf|Duxp4ZftW#nZqWKE5n&e&TH=f#! zymDle<*JEK?|5&C%vG{atwPpBXMWf$wI-*KVnB(x`5od|Q&emT=2v zaZAKkA6?lwV1~5F#7TeQDy)&exYC}a?t(d@GWRs@)tbfRaEzbj(P3#6d|1c)Q(qhx zrrudaN%^&KwCppxp`D$>4_6pzZ$7-lO#??Vtu4JWL7Pq0o@d>bU3=$5ZI;q$2a6ov z^8EbWTfa+3j=o53qm!u-g%-D9u|NGGE*Dxc*Y2t5{e_@eHt zSC)rTxl(V`3fm{zcQBN7dL?^@8E?uNM#=xJ#Q;)svqb zJ-ye|V))S4flA|z z+2fslq?K9kZH-DeFP7do;d-Lgh8^pOuNoE^_|V2rl$vk~Kl0t&*+U5%k5-}_sXLdXdD&@6E=k^uCBNe7id^lHp>wuJB$=Z_Z?k71j{>tDy0sL9dFwm-ernr`vq@W>yxxFE!a6@IgNIK=P6_)A-?WB_(93{F2Y6X+sv?A-D3EQTkPSvU!UE6je^D zwRv+RCqeg%Y-#cM-7Fp2RKgqaWg7;onTOb9Ua@~Doo9b~W5JUnG`c*Dd_W*tp>Eiqp|e&q;QVjuz0u*ShSyj=!uq>&)h(dGgKemO(@XW7^^s zUq$wO%m0XeJRh*Q&#*r-aC={wDDuAM2AKV4*iq{9z>@cyLJltR-?-(x=ZxVRig~K4 zE(-Wl%iE&WSL#uYImI11Gk=)teT&;!6C4ci#XmM>Gb`m*ml{ruyT9zq{iFv~i3fsD zqus}SJ_<9 z_4L}4d_Quk!>=tSG3WIIKfE}7XF~P$xJFufP;yeeFj;|Tztlf3(r^=w# zC2v!%`DwnyeK5W>^2mIGzH*0mjU35oTFOhew7`w5R~vrZePP*dv}J!sRetcAYpbe{ zaomomOj%_+&c<<9=#?`v>047weQlwg45)d=;uqc>qfY&>8a_K#b*bd$iJpy#`tzTYuczQYAmm3-xxLaTHI1*oon{vI_q~ME)&1v zbZ`H>^gP3?*7I)hbJaV_7mn>F8y~_o4+_HPvLIHt zXn79O*Q4d!ZJ(NV=WMK8#3Bj6*~f;8iRlf5vwP0jUJu|?Mt{0$$c|v2Z1OgcF`lez z^debb-bH!c+U3!bVWX3d&8RuLY8Lw*_UrU7kFr#)Iwd;oCti!bLrEPT=D8>oJH^a&u`qb{ISp0>-W-5`zJp?M&I7n=Jj}3-o80+inE+rI<0dH=WMnx zjWl+Z4A7~Pa+EZ@t{whpxYoi#>jV3o)K$Mq-`~p4ysUBTaM48i)8!)`lc2rltBV@1 zU1v)@_C9ZJT2ZIGz>}_=t&zk$n5wfERc6`V{$N4w*JtezrpkZb`?TG)SDMxH>8VNbSrIX1Ix*C- zivn6sXbt({ulsR$f@J({mj8RZ<@bLKYMrDdPX8HJvGU^i@?y8a{(AJ#wdn&r+j z?iNxb+QKwGO?lclOLsl~vfa46lV|QKv@ev8nKC-`VTQU*aISsht99j9-b9C)dtFvv zs2y?sisK`v{Ii>P7d(D1yWykZ27}^~aa%2OS`Wpnp-$AW+D@viFx`~?O?%n_Tg7h^ zMAmdeZ>qHbwqi(#iA^5}Tlp}e`dK$S?sLf#wht^XD3kPN3?K7u@e=h5V}`eE#4N#V z+2SJ`xOjg^qq0|P8zp_6^Qz#t!9fmk%QPp)9{+fA=#ALp*ja>;tx{&X^@N>;x+_mVu06GC5$&>VtYK!fn(thTOZ=OwC$zX>I{2v^F-Uu;qQ}mS0Ie9-V04@)ZkbY8Zc0e1 z4Zk(<^d;97nJ>p{>|A}ltljGA`h47+$FheHUY+grDmCPGQe>XP5Vu#)DnDGGclb#` z?vFKv*i|v())qgYw%t#eGl!Kk)A5j{r*YApl6_>a^q0F|+`FH*JV~#JcE~N0Jni~@ zS_$J`Hhxa466@mq{TzI#vHZ!mFQyypON_hE++Sg-;XG%p2B~bKUErXGOScL$XVtwa zDE+aWY%238M{{ny+U2TDhO+lZXuCCGO(toiU3tkP)$yAS*Xn-0)^JMqrtCej8x2r? zv_0i(i_wHkl0kE%e)>A;K{jeS&2lZg?X=UQzWS#xmzCb= z<5_Y3^9_~vV;xFU49b60Xw~DKtAoiB%PYz)(<)v~J$<)x>f!;FLKuZIacr6 z(8Vcs#@`lMb;xaPbKW^FL@Zovz6y@=b7qjn7cCFxC8go9xWi^nb1052jR%u3nA6o? zsvCGzKdj4E#6WwA$**e*jdfr?NC7_h5tRZ5eeQm56;p0V6t-M5aktOsTP>g z7sz>H)(hV1*?O-UsyjSABf-HnZ{Y>!wFP1x+e&X}e)8B^8j|9-eYY>saf!#S-ETsp zccwI3(cirObbr>pBxcgRxlyy8pXewkxa>SiwWGE5m1WZ2%3{lHdC(Wtlb)0W+&SuAU~y|+Pm z>YG;mC!dFYahA<=G@D(sG9^e`Df~isX14REcLos`8d8!g&+2oebh93h%W>lF2T@_eS$7jLOl%@%Vns{^)f2N`iwH`K5ndzHKkDjLr|c=iJ)@ZtxL(f_lefG24R7TJLB~Hm%3SbqutMM4X z7UwU?pPZM&iNY6^M}E31w^O0g;KnVT2m6o4y}tWoje+hLKZbt%I-l9^Lbho}9t=3P z(|3Jrgxs9-2AZK-$99=tAbTrtkKd78*K{^y*6r+gVtLJzf&*EvHYn;h`BL@$59^JK z-Wi#-4y(9xhpFtYnXjU$y7W62aY2KNhlQ!j-tn2%6n#+na_yN(`-T`6x9D7!t@JPQ z*m=jhdV#F^=t+0?r>@X6tguozqwBu`?_#a7Bfoe;cEK#0@!IXPWk*W1I;{;-+46F2 z6L-aUC!-Mu6^@!!2G$?69`2!~bR{gJXrlDRH)l%B?@vur@UkK?E5mJ*9cS)Mn~Bkx zp0xZzgND=Er>p6c>9yDC9+~r<*3F1HA2qd8^3*80Ska|;<5?FLoSFJ`&1R3?3sm>l zv@F$Mby{*h{>sGb3Jv1a(T5Xu$BfVuZ=bWUE>pSHPy0lS{Wp9lI%g`!+58D0G9oxYgs!@g_!= z-ipL^3i&=O_8cBsvMK%ij6$0!FHFu;V&}2s4V3g(rL&61>{@v1CaFDa-MVPPxwl36 z2RAk>$8zqbH{e?j&uyN>(w@I%@<+p19Dj|>&@A!4IlG15=vx;&A8fLFhuLwrHkzJ11$IYJWBZt|Hmrx zE_?3Kmba=Q)GlU@ZF_BP&CKQy*L&1R<}RCfrqokjo-?62x{h$Pp+u)HTD+MMub1QY ze$Gv~H?J~l%JT1!vg%A2b)yeoDygZGnKswm>a*Aw38RFK6QjC8?QGsnf3+<9$YI7#x2wKESwDe%rR&311#vc_RMcw8Y5j z)h9yUMt>)!GM|}?MW{HDw@7BzuBM7VxFE5(4VU`nsf}2)p-zLIQ^pxu;?nZWE32sv zmkdl?eAi#vRPS)(VNL1B+ou91jXdaJt>3{NJ^#YPq%T&fZzkASoQ#+M4o#=Ltw}!r zP^ZCCeER8Sk;!Ks?j3%JE-~t&QR=kTy{bbnGAC5#RTs59Jm_(0QEEY$^nnGF4!bVp zo>1LzPI}&=6U9y8gKyN_+nw(nb9IvHhgVL8r`+CqO`Nqo$Ll4JeMuZ% zu&3(d;K3TcLE0lmS}D>@&Q8oRoafPYVj)Ilx80Dtl?95j$7WA4+2ys^BvL2oWvzbM zxiDD?a&6e~;dR$%H!Z?I^n>1+<8_u6oS?luq&z9|#pM{2aXLn`0yC9pQ7*fr)$G?r zuk&xpKAROkto3!RzGj%W0%t_?j(3;DPn_Nx`F$Pb%qQnb85hb;#*N}mFt$G(t1q8k zTJ$gtrdi1W`V)|a!(3SQT!r=?fV9F;Pmc*|zFQMU$v^45)|N>qf=bzW-*nmEQ7w%C!mf8dyBivVu;}mT4A5XYG8}i4WkNj2T zVo+Fr<@KYcCjZ7;{+QCNnZ73ZkCc?#8Y=glB8~WU`SrbbMPU@{wekVS8!t~j%bs!T zK*soyhy0H{nSC+`yL-&Ey)pOK?SW>hXf^1mH6{n%NnEP06<6N0?s(bJkAr7VPTWuy zxN_0-sgBLfPLqA6-3hDp_%w3c-g|Oh=f@d*NZ%DRt7`UQjgiYINE+Loc9eLkb@8!L ze%8D_M%yo6jmxy$deU{s2%qamVv3d4-Ys3R=jtcIidkz%Nxy%r7;8*!yX~%!{c@FB_Losc6U$at z-P_He7Oxnx@>7}rIi*38CpvFLWtP4pd^h`+Q=7GCidBc!g_9!)bBB){^zylmhD@Pe z5_H>Q_r^W)6$-B@E|WLcWH+Drc5;#1mZ&gy^HQatJv(3Mg=P2#zT5CFezEKx;?377 z3p}Q1p0#_pQ;t!oN2loQJv0H!c^I_ev9V^(`tMhUue}~xyU1gw3U{>3(6-ZQx3Xh= z4t;QQn457Tvo4?_b={KlPOV!K>~!oIZ_a!q-aIkoSL_PEAE`UAtCFldewrN7dh}Vn z?$O=cH*4NT;g8hUL{Fy11<2{8WWc-kH1oo*&Z8@nK~8;4H0|h5j7=b^q(V z4}aBsls}TT{U!AA+K@@{)1MfB@*;TMIq>+!ChM{}W}DX7HGcN;@CsOvM02)DoR(92 z^W5wB&-Er{Hp!lI=an(*Iu+a&KWMNCpZZAcd+}*WOM`b~o_ucDtQuf;q-FSntd>Vp z&0dblk=XKJ$RiC)gM(*Ze(tC<81rh-DGt&7`RB(v?Hdp5JZ?lCs~$Fd_tVDjolmN* zcVkbLj@Y-&8*AoNf7DXabKj?=DvhHT?6<$tm0Ngp@{J6Qy&DF<^{&nGA6%-imwxZf z>rEFY+g_G$dE(;WeYgIY!V+v@cLUP$K8&3w|7{^LmU;~SUk zBQ!V{9Q};(^V7;kZ&@?u`9HC6tSUG>daR#UA7s>bBE7C5lRQmz4}( zw#BvB!Tj*r(x92K#rCtC9Ly7yUO$Syaixf(@@C70EKk(W^h0?>mEc{4_aUZ*+aq(624GPM#n#rr77ZJ_+`iW+}1$vl`yc!*aul z-9~4QFZd}r7r)SqjmJ#fyZVS?8C`5R{o&C`Gbhg+g2N6Ak|+vzJTf#^$G_>>$wr@NwE~7AO#mT(dMY;>82C^RC_(mOqHyw6M6)G5ASuph2W!qWOI=dE5 zyH#^-(nI6=c$x2plCdF4M0@4=GCPA#z908>d_4vKr&vDyg{nVpwh}DeiFwr=kMyN7cKVtC}seK*}OR`U> zUw9m<8?f~9EjsSV&!(YkxYwO#RNx@b*Yg&Xb*8w=H`sp)iAz%*?d&FRk@CXh#NK@^ zCc78OZrc}lyLx6~=4aA%sm73#udjW2nZjA>)E16`9+yO{vdoH`BPqV6S^2q2MMI{= zBDb68Xwe5HS`?jdQ>r%WAEXrdZ(Me3=aZS81q)AKYSMA{S~)o4#?c(him{Ud$2hcy z%T#&}t-BYLaOAj#n|R_%^HcS65?-jLwd8O0QcO@8>@-7lg5)cEsWsNe?l?TWIedf+ zZIa!(cI$OB^P5NSc7;Cv95$$fomT&C_?icbX;#*C$ZuTS1ZIRQs`=d%jO*BG?jbdw6C~U=V__-sh3>{7s zRogI?EFNs7i5*kJe=|?(_DsNj!TX=tTTgLD&LGudc*-p0`*ZC#jtE53pJ-u4u?tMGPt5?ImZl9^|xXv^p z+H!vW^$*X)d}iI!(GB~Mpj6vxxp@mt=hD)z&(=h2l$d?P&F^GE*zqLCFHw)NK?nBl zT2@&$LDj(0W9^Lu)v0AoVpqRE{WQq*Rp91lgZF_-5)V>vd&F|!4Ia_M|{_je*+hElmXaT~Qg_{yOMZ;+^kZSv9%MLwd2{Bu|6*SYrSWmoi+#fV zwiabg{W^`br1qE7iFp547jf5%Cj2_(!MgfZ`PZ}`^(6dg8@*Sbw%*viE%3^ioaw1? zAKhjB+pVk_jr!ic^5(AZ!qq=k>`)ZXOpAPc-s0oWUCJp-c8$-TOq_jq@6yl-rOi3! zvyAU*)#GfE@ho$szrVphHN8&hqrui$j+FkVOh*?rT`=FgJDk6fSr^V_-U zLppWuyOqeKkJMi{CVTv2kNjATh1-tpJF^K><62VNyj`_C?UG~Asr?BSO05ACH#n8Z z*$uw;E@<3_bJU5~)ZPuQ9&;{BuX^pnavhbr+%=SB(}fKA*koc=aBi#IyLmnlRjRJ@ z443Y0+3!DZwpg;w*_W|ZD`t$YE;zG3!Kp3T$)XxNm3g5vH|=%Uom(!OSd_$Dm7GoI zi?(OI$TRal`04t?iomHW>hyL_kiNMma9!HUoM8KAs-t0MD(>X@RWvz?to%TATIvUt zl((@rcSJ0fWXF0KN_idKSj5uK%^qv&Q(2K2cDvnoOMU0AFs~=4&K8NKj#|D2yUr=_ zl*CM$AKhunq0F1aN9;`pM}^PaJG|}qq)MYxxj8S=8Ot2oF5l89+-K-y8GPOD>s5y( z&TEIcBwTwNwj=AA;Y-PPF4l7|7Wz4bdpk{tVjie{QmC3{z0yTeF=cOBhuNFSP~{HgL$Y<+iJ09)#Mi!!*V)0pmb%9!S#JXdpu%IX3YQC z-oCUVdnlFc6iz(*s4_sSvx6S&K5mthPs6RVIj1*oA5ogRavDd^_uX=;-?riHJL8T- zI<-Zb#cbZOMrxhI?p=Pl&wUpMUN>EFr{wKmEe_|>(S<)NwYRMfw0wQuDC%SV`dh

|UcO6}Nw?l5yw{>-~oHHS=&AE%7pTA{N0@#k>+ef!gb9%i@#wBLGsavx4EjFKTgwzWxO+zoUQi! zaA3g0#ib!)-_~2jyw$2=j#f8G8#1mv;FlRA;KhE(|CIe(kBNSARjKjRWWQ6N-=++k zesZ)5rTwX~ex;$W?Dyf*^WVuBov~}De)m3g?JZkU;&`k{;q&c}^>ZeyIM$djYw2~`7 zImJG)IXh&l>`l)rJ2&|~HC&caw8yRBe(_A^S>4ypgkvY$Mtmcl#hpJ~_R!2%t}0`z z?DwfVuRJ!wt0@-BaaS;kJql}%#vd&AR#;coe$mPJ4a=YP_44vr5=FbG;0uYZy75bu zZ5I{2yjEN(Zqt4(J6d{@bd;}3j!DC}n97M&iE4GJw}&+ZdN0Mx)>XRA(>?$7N@@P3 zbsqlHl5FzWVOW)Jp&D=k6yQVWR-f`?Fk%p^~>I^w_6ikwObN4>dw*h zh;f=W`mJB2+I(C4<>RM}mP`#={8n+#owlD}p~eiOUGNV@SIIY2%Su!yn8@3tbiZO8hYw?^?b%^pSYpk0^I#=Vy)kTu%EGZeE`E){nm7NRID| z3Tb>P4Kzr|>Ow!+LV-*a9eE-g0h$<-6S?N#wbSJUvPpAAw^ zj?;fU<@@djgzYP*Uy0XyoP=NcO*d=yzVb;rkLFdSjhv(ExXB{6aL^rx7E-oi`qug^ z`?vU?=hmt`GRYmo%uTAyPXIJ~PK|eE;7FlN8v9W%?Hm7~A#M0!i)$4C_jW*nWU3IjS zyYubsi$y1%F(3VmvTFWTbg|85vZ-<#F<)*?`0go`x*Fw_LT+54tSc*gouR75_@UwD z63e_Y_g}Z0tfzF^Ys2M+&Hl?LKD*H1Y`vuI+`FN)5shQDE3S49-LT~h;nmpWMdlbn z^pT(I&lk$Q()%i5?f;49Y$A2K$bRxUv!)@{GSQbm%eN>OeiOT$p!-Uh#`ztSa@7)R+(n6OqkLnp}6DF(a<3fsTaMLv^cSRf@^W?+Y z&ubMpYloDUIvQ2lZr`I)I<;gHaiLp|U9GLJs2bYNcu2KdHT!-+M2Z1^Upw=nWT11Q=GNW!jhrq{G=<*s=4a zuBldcmMopQ+%~1de&V%PxGl1d$puBG--_NOKQPrl@hkb)ppdaz8WeYT4`;eFs%`~B`!_$T&XkZEmvosP>qjWtf2m{b{r73x z{nB1ywc55z)r8Ku)FKsjbl#dp2QFya?46stFZ+q!7Uh|z$IS^K7zE$;9y9CU%T-N& zW5>_0kEFAo$=x&3$lHC}FpQyRf1WL$ooK#j)nlzna$9=M3)RM`vbtcsb5B0`9IFrU zo$VQIU`d+gjibddy%gMa*Xk6mv7PEV#ufU3czTuWGzD;M3Z3jrVUIzD8WKz`*a&k@W}YI)`dH5^@8+nf}t# zK5y-%IW;n2Xr6__X~)Dp^?H`|28jzel7XkmM=hFN4OhLht?|-`==gOuF`Y_ITGO~K zW6jmv^!6oM_lF&ywRLs6mVNns)34BKjy)}8cB<5`3)MSM&yF%Up5lCc{e`;J(uEgr z*W(wsjva|{O^P(#y+*^+ZdKfjvV`GV60**%o6xEgNOqD7c23zb2 zz42^X&B5h6C$g||Clu!`xKCED-2Xx`Z+cen7<22<6H}(8d#SawjlDkh`=XBRvt!p9 zEQ;Ljy=g<|x$V|dWv1L*bNA*sjj8W;zU1WndX)ERm3E87R`a}8602_c#5-=aJxx6?CZ7hfAT)HZp# zdrj%9lS36`xUEVa*;=K_@}F(Y^YPIcBZo--n(3#C`#$KWdaLXX{B7G-?e^z|rVl>G zl`-Gw>=o-MpEYCrq(u7D^40bl8LubD4ZkBZRw~8L!7nO4&DrZ{;l8GEiYKOu&)&lb zXf(bW=wx>J1bL?IhufFiisi@T26`q|Ze9D5eed1kX5aS<&F?yF(86|>XrwM!%&3U+ zN>?JYxX%8cI>RH98yc!P*xg((M&XnD@Z3S&tm$j)hI(IgytdhAUo;&Vc z?&93Zni;`Mw>>#N{nM=_X6l#>i-mX7Lg&xCnbRRTValG}w=+5K!(PtObozYw<`k(1 z2g{`WeeS zOWPs`uPY#GNpAZRueSY@$Yzz5V&(<#n0^P^&9vv3ihg&kzahOevu1{D`nB6TqYqZz zio8i|GPIVlJ+pYi^6?Wqb=;rNTQ{{~g|dulgZ2HHev3v-JDD*~jW%XG;Y>iG{)cP7 zt`r`rnqO5t=;R>!8fbJ5hAz7?AnnRvx?Bknnt#zk^_nE*?$_lrDleULD7hOx^}ciS z*vup|)+*08>?|`IO@9N|GrJ1+Eja11VZ^GwlA4{#{Hb1?84STGo$xJ%-g^F>@)*Y!t2O{ z-3ROUN$+l8*FQ&&cq1}gc^sgE}a3?lS4E-)P?-Y74oc*6b|3viM`QZRy^PGm6VnUZZ z_!r8h(J5FKl}x3@aD*W_VKuK_zV<=uipxywe#3fwF#t$nc+>jN3NoM?@QK0wpuw_Y zy&q{WDJFWk5WNXlM^`%?(P~Q$r^myYr{3V7Y_}>J!qr>|u8wv@FK?WSU4IP^?FL~j zPYU|0F``?Is_t^rUqzX26`zUq{$XGd0B`07u6YBHyeWx(KTL2b4C)ntHIvDKPgIX_ zA<-CK9FjmvML|a_qJ(V%zix`5Un~$@-r~K2wq?dM=u8rYe_)^}z>7>X_Gr-C8312{ zZU9Xr`UePeI5e^#L0GhfQclcW^gcb(%MV zUl+yDQ=f%KXF|W=0KVZYDx2+011gfy7lG$3(H%Pnkn+h0bUT3HYLxF4JTZYx=dx*W zR681-N+6R#3oaxEDZH-+hHjtR90UmNF;tW}(x>&>fIV{S!=|zvn9)>cCNqi~(>v&w zyCPSXNr7<$m}+(djm7i>n)kggnD=^z`|OoJdk6S+5B$S)gF6Y^D_qC^hp=d;eCG{7 zW24cc@P^@fhsR)ZNOXE%JX|#5$P8s5sU1L>K;7U?H0%#v@6fg+>K^rl<$oBejs;he zL$A=Bnc;now&(}u1=lF2fBw07(N;u1UM{%gT?YCob5WS0A9)pAw%&jK$yZV6qMuk4 zT*?7|_`yX{(4t?s6S{&1{=GX97c zm;*hKT@1kIoVmZ9w#>M!_YW z^6w6A6m3cLKsUkVvG8vl?rLvvJhV%+Dbe#j1lQQ?e|P$aXiK8!DhMu_ zW&iS&1<|HN_p=Kw@w|V$yIr&q(QU$lOLp}?-!v@Ris*h+q48Psk9MbuHXyniP;h-L z_?LSFMVk`co+Y@h7XFjXT0KNnWS@@UQrYmYckPHaCc3RbaFI&~y2(Klrs(>!;2L#s zpsUfMFhyhEg6nD3pGCq&A&bTn1=p(+|0cTFqX!}}F2Qy3)W3*$i8dn|cM)7GYknJr z;REBxT!e!r^4)+vuIW1hzAD~!Q0x$&W`Bmg~j9WdOCnQ zEGCU2Y6f?=VTF(!KpVQ?GBPlL|Kg1eb%p;Tm%f3K7+&8PE-l6jOaRp0EA!v> zQc)PL4~uRY!QsTPgUobvfVPaIk*RDP%8@u&;Drmb`K)Sy)5XGWJZMDfzfI-;cdy^b zfBfHQ1D0`PL&HAgAFropq}yHo@y5Cay8kEt|A|XUQHRTB>4ef4I#fm+#Ey`XQu?cx z6!L!vLZgWgOl=Otje*!?7MSfI;CMkkuA2U6vA^1|loCYXfY~3k!eK&eDhDFCyQ7Ln z3G$?}ne;d+M5i&L*k+KjE)-2lghHth33(*t?rCp{QP#zPqOMdb1>!`|*a)&LDxCzo zZV)>$8mWwvijSayp3Q==5fDZhRK!5DA#;erl!CxTXIqBQ*$|da;k^t&AA_ovA=HEz z7I;i&V<0m~4X25nn4=MYG^T97{o6FDj=nFaGGk66{JI<#_2G)bh1=AZmmw4yi$RK}^3lQQ2$c?_Nl};#s+1SOmuPE=35kiPgkYq|0QatH zK}fwIOo$E!vg*P@iced#5`=Z<)l)`5bt{ULbtS-MQLexsPpk=EMD%e;D3~sIIO+m| z!6QkbKon{iiA(1|GkskkCK?WaBB(4X4hmp$Au@>pk?3qD#AQ>VnFL=#u$#NL7XC<`0xg;w4UytV zG&(7iPSu70W55jNLhui^vxO8bAuJw-%0+6BI8Ypk&ZXj{(D#-Y9s~dtIIs*zT?0x& z2a6oR1bx=TNTo=DDUjOzjYyI1j}vGd2oG0Ng510yb_|tF3!{-CHa8}Q2_#NE8YnuL zztuP_E>#UiSS<{Yi>ju9lOiXE$I}>=$h}lFJCV)%Ng=TW&l!MH9A*sfjtoW!Fa;ML zr4S56gBj2!z~0Z%RTI)cFyaA)n?vr+&A}gA1R}o(4}SL|E{?ADe*6*$GTq<1y19CJ zySejA@X-=3L}io6Qc^a~1TQalg13XEGM_nrW7S5ISTOx;slY3U1}*|J+B3m0LuAx% z=o(WdPmv)7d|hx-Kv01KV1SW9SPyOtRRdkZiIzwZE};|2qUx|CbRt0$BBkL=339Y{ zQ3I@M>jVKSjo|1HHxAg8!y+-lf$n0GNhkxuiU~%AibSblmSlH9fm^e20Zi9NyFIl; zZ#CcvLQov2iLNdw3OWTU_0kQPk1<3~Pd869s0$zjiyIZu%s_5w3?LvhAmKFN1p$jN z5oqMDqEb@sbSlhlNcjk=5Rf<$iw5@=0DuyiTowf8FdB==fTO=K=x7p$90Avn;?oLZ zhM^^qVWpuIAvG9) zS{GN5nOrbE0O1T{a=|UXGcfPs{&T$tJqOJ1$RM+j_JlQE7(BsIsHlEqve@v*$5UaI z%j0~EGAiyE$P&UJ8Uh1hN#HRm;yoVoq*nNOH@-m?Ww5iOi=($?2$xQa2I>v#03<9# zz)cSK-3 zfimd@PlTi3831F0umGP;`k*_6v}49^r3WtEq*xSgESJhk93W)jz5>`NXV`&asQ%W5 zNMJe^nmy6!CJ~BY#v^wv0l3LJ3Xk-RE2!j#oh7H23+7LUQ28#$By+Qin zM`KW!@rXVJ!UH4{UKfB^Tn2;22uGwIK^^6F;lb>(enP-I7#xJf^8Q5A5~#2p#4o}# z00pyC-&MNG@apjOkx&gTgC?wffvtqj9DY;?2`eEq2W7#G#z3YC5^r$~=`T<@WL_AI z0>#lt5Dzc{#M8~q+t$(3%ah<|84?C417V#Z9S93ph(e74t>eLO@RiAKeU5q;1Pu)gk20_&NrER*78V$kV3wzJ(Fd^useB>`NF`7?U3@8M2LqdX zm|k><2sX;y?vvGfLel6qxdO)nNE2^(P^sz~IxG}L=TgEELj=`5K&i8-z@b5g6`8Hs z=qS=Gb-PFp0KlZjD9^xjC%KIS(Z&h5BdBc&ieZwe;Vfzlge5~Fhyx88(!HKuUlr$I zF(J_JF5syN8C-zKz#Kp|iVbK_zFi3?R6a=dt8U{)R$O746>!|6d1%XRVO()zO z&(@g(9+(Fyvvq=$b<`mEjSi}(crt)$83=Zc;mMTXY3~K31!)#mX&NwcHcUDlxDXJq zYlZYB-#>~J^5wL_Q#zd)56|rIJ9Y#W)QROXIq(zMEN62=*&G@N2`d950CuVndFGK? zSu{+*^I6cJ!uAUgd8{py70!=Y1JVgBnXa%0&PR#|12L;@d3HZfQDc-*kJu89I{Cp~V%KDe1c}rIK`C8~ z25PFo&aALw36JsrMe_gh)o1)4u^xH+_AlG`&HA6AF5W;i{%>rc|9|oS|HP%E15{>A zB8wIt!GYAt8pygIWY1)V!(mNF1{o)%qXYgAc_RT*W*7%fxq(F==!`fTY*49@*jO4{ zO&bEv6i_g5kRuU^T2t9sT4Hsl*nXI1z}p@{_$Ik;j&_wY^pX4h{hlTqZ@7%oqp04*WC@{m&qS2@{{0eAr4jSx1fy46~ zpL~!b5-#dVACw%xlm<*d;gUIU&mj^SPMD!nDd9)}iZ{jZO2B?UG8v-=Lp*OHLWVL1 zU<-%|4+}i^@WN#*YAlxqh=&ZK9u6YFLb9Uk?;dQupI;X#h}9x4ptOw z<^ycGrNuLXegC$2K1x?Ttgp+3IKpXBU6#JA|AO})M#cL00J`%4?~0} zVea9fD!e?6lafL-D{uJ=wv_-XOg4=J8V24QYCJ`fVa!BZq**K&LGgNt->EZNox@rid6e%fS81ZI%_P|<)>i{MPvOwno%9wR; zPl*53Ktc2=0tb|eq0y;clqg?xs31K<@EAeO8^)6VSWV9mr;F25gZ({}VqR1ZAD3v@ zs7c~ky1;Csg#ppPu~}qzLj|NpKpS-7zetFttMLkh;f2fD(ttj(gl4IMd$rlfyA-5f zQt+Y)?DmuBuE>g>H4-mD+hS0ngl{1THOV3wG#y@(xM<#zSr;^(p@k-)^C}9F$B}|k zj+#IM3XzJlqmww++^{ez%Nt;hV$IjH;Qxs1fFf`x3L>~W!X_DzbQDzrPz`Z3P9jjK zaA1vt$QUdt2$g6IA)=ci&%hiPPUVP>1;m#k8zzMc;f0^LBE}dVr;9<|aCF!bW4f3G zCNLV9=HDYN)4~#o3|MIcF(3)N;S?UJcxzRCk-Znf^KoZGnC=u4xX%JRAA^m=Kw6L| zm%*V$Q*odg@29#3u4}co@)RiOd>lMAq5#<(I`JiNDhg6y|_>L8pb1V`6^O z3$##wz35)z@AQJKr8R&qz%IuECD3HqJh_M0LJ*q=6RT-$FpZ0ZZ`ox+MnAq=xgT$r03O z61&Th11zdTiiy$z-Qv#y7$L(V1`HAqC`(M=E)2#B)c_W7k`gwIO5$(;?kJf-# z+!%ggtFF>WuiUs`p!xnA&(VXuD-ISgzKDAl#sX-?Gg(onyN~PMER%Bo@ViMxjs{eS=_=fm&p^32Itm=-?f)KqW+X0Y|}0JR&rP z>5nWu0MY>&!~vEPCJ|CMn#v(zDI^Z5A36csy)P~j!Wc;ewG?$_Asvwh;W1=zuwjTx zYSjZGGHyZ)-5ndWG7c?+b0Ijo_GkbB0;GZ(&Sdp9_&hNDQaERVkE@LX(X%JEI(?vP zp~L4do-pbNNh6SS3mv|a;wdzut8E|Z?7wX%{cpW`*#9;zK$pO$ps;bA1kPV+qksF~ z&GQ$6#{r?}i$kAZ9gWj$Gt38CVW5VtO997iPLUTH}h$CzR ztfX+X=X~k%0@E#rHW$#F3=3!9@&&1flgZ#GogNph(YLU95nLU;9RrCxtTAxfr5ZMj z&1*&t8A&8tQ4Q`hA~0%*#31)%H38F0_1|xIf{hc9>R>=&2ZDDOg;9dZ;3O>prHtz0t<9^u#j3M3_(JXw>P*4|ePgHXw zv0pQEe2ugrdm9^LZOFyT*9KYER}XgavIz!C5VEp_@TPbJ4G1=QBWU3fRMtQdbtc*q zYy!G4_2e=j8^B$N9_39EOrFrHVX@?x7$CY776mhzd zbu1hPZ3Ld-`kDifZcrH%&=IsdwnRHeS7LXx;naF}0+?4_?Ex>jdpp2zl^{1T^{{9Z zbg=o!S}ZD=i!Rim>D5S*FYNx2D3o9%Z5c^mWvgRt`)$HfIjP_GDKHLv^1&L}2TW-F z)-Z#92NEMh3MGiZi|B>{m|(z6=IQNb7wkL`KwO%U!Ea&tI6I@z;Cy2B{=xn& z^aR8oFmk*>MhMwgqdkPTC!7Hd7+$zV{@?zEp(hkeWeaOJH)pf%9Ne>9iWOj81Sx?b z8tjZt!@*vjHeOQv;4W21Xu{Tkx2@tL`gkeyyU#MPLDJ_-8in5HJ1&RT_e&Nv496cUTm z%i|~}hfZVk@*#|g8zwP{U{>s{Njfu>&FPiObZ&3WvVbq$ z%j2-n-f)3U=U!0Y%St&frDPgW;GLD~v=&CBA1dP+b6RKxN3}UJHdYe+#Eukv_n{yz*N>B8B<@g>uYq zf$@sPuxN2WpA5A4Z(B#0;J463O95a);_d(xZGW1)OA+(A3VG0n&Oq$OKUWw6xj?K_ z9r(XJ9t4xvK+&PfujkVqr4h@P$`UQke=J;{frP<>;az*dNFjLnRj6X`@BfxqDWN-1 zyP`^=iCx<)yBvRD{i3Ku!@GrnPDUsM$cB&^jW*f%czS{vJJ{OMmEai=Z0qQmqSLdI z$fP#F#^G={8@O|5|8=_G;s=aK|6oK=`cA=nFHw^mJo=Es( zU^?`lC`a%cZ;ZZ0e*VX_5ZZ7T3DDO2(@pL5jbI?edvuoY^R9+VIjuqA6RHY-Ua%Wzn0MR4KF^& zuNo{=>c414A<;cFK?yi>Om2)i-*<-W9G!_?B5d2uu};wtt&7H9d#fn0u2az1D$(B& zS$E^(?(OF0Y~w(1bPa~9r(n>qDusz8SfN{2;9RP3;Lf6(cj3$!^?@23N`^yvpc4dlcV{9$Sjig$ ze}6dOZ7^nE}&VP}SYj(U;)etv#$ClTHE1CBi4;lGq$UZ+Up2OTwPP zj$Xm`u0Eh9FL;O^943ebil8mUTWaWwD&Y(+OczX7#PnanL)I!Vf`Fp{TwlQc8L5$b zOLhc_!i@hHG)B(H5JIbq%=|@s-d~`Gi2#}>WPme3Y4eBmjB&dB4KsaHwGU#1k?6E= z2H+|NHNLCsNWw3f#)03`IT1h+ghyEF@zz@e;pIObL_Zz}v#4x%F@@ip=u=TX7cJOb z0J_#RU_D_2bUQ5W{2f|oz9^guO7L{y>mE4&m5uZo0?Gl-Rqoo2j{rf>5+jq4L)~a> zHkXPnQ9)!L^icRXFg6;pN0R^ki2uUSwy|*A@ZktNSrjfRkVxAU$c5l-;{enz23;NQ zkKcbMJpXY$gQyXBm1qF$45k<_NFQf{vKF1j=D^yRD6&q`YrDNMECzmo^a{(SM$^#S zZfu0czp!C6DxHGS00U1&S9jqPy1?uq)z4B&k2QV1;3A#v8U(KZFK?m?a*9L>Mlg`b zsy${PGTZlnicB=!<&uYo`tK02V^OKrUbYy>%~PmJXrLYF*#n|K5zxlf2@G$a9T6=9 z`a=iZAcfL;^yGhwZ1a~hl^|aln+vQRFAkSNV?teWh^~h7Z6Fj=lnWsn14F!?8R#Yq z8cD>5k9bEG^T}O@$b|~I6C$V#2l@mc$~n!7Xh5V)|%FKt)16y)?>B^}Ri}VOg3;uwUb@&dTYzo}$_r4k#A$XF zfl12bXRq)xSAZVucD9}X20ScXHG9PeutnFS6n-nh^Gy#{=AA*=3mbQ}gh;cHx&(K? z_FzWCvp8&cU}KobQl7wm6=bOiBZT6Zf)T+4K07-YP0^!K`$c4cZ~TJb7u9>cSb8Xbx(B0XB3tC2%53@I=pBYvtNsmuu!3oqzYvkWVf%-+2{6|Rku${Y29$i{gXj<%JN);g97qFBb z=})5RiwI5NiQmS@n_!JbdV2acqNTuyqXLhFi_8!_ta`L1$XQc@;LREkngszUk62ge zSuqfDWF2z$1w^6K(IfDvBzB_cX2QO8DsKpUtes&Iu~4?KadWkEv^Voa&;776({b_c ziCv#5-uwX&px?U&?LP9nyzkz!zxw7zIP1F)hJ@Ggc|6&b1Ca7Nm|f}H{pgZlUbZ$` zwttm^=oBxsJkMh0?MO9)52TJ{vT)Hf2J&Ari-|#sVJ)A!kU9*I!Ho_D zHfCQyc}E}H7FZX zD9iWbn8oZ6Q(!JgfT}G6@^#2_~^qeAp1W59pb`aD7d0>QWxM9KI{G& z;d!0z-p@Y!A>3L1M9@tz(4;poNy0g6V2Xf%E(-DkDb+oR_C$Zklj!aYj5i1q6a=rj zDPtgv@Io2}0z((beK}Q%A7Su#f^5Jc^6SbE_?FH*_Sm+t>LVROp#%0iAxzu3r5<)Z& z;$R?~SP)_Z(wmyVM_zS@hz>#bMjVBLHNDus=yE0B!sc{4Aq=SxpHQA8G;ktR=oh$P zT+m9W$K{1`V;AY+M0I3CjDWq&V0IE3CX}oC6hrlHg?qSJ0~b-gc*3m>NS@V&*cgMw z0vDM_O13%53uNSTuDsyXKR^miArZ#wPsNl6K^+I@IA}m9=>I_}McP5dleaGJW#d3} zA&5vR`bh~2l|!NdJDU^=?=V8+8EmQ`TpEn{zM&+JIy+*(vfZuOeTqxC)vUp1b~u$o|VY`ESq2J?7)zC=vcWj4;A0!d3s3PU&Bx z{E3C13!g!+^ux&GR4oO!30} z0cc*z-yZ=-WX{{o)(!dSj=E_68N_=4AFP8ugm?dFV6A#Qk4C=L@>@Gbz4tt~-`hZ_ z7W>E51x^2c%fMfQruV1N`c3j5xSiN{emsQ1{3#fb41d`2WRroHOTkhZ+~}UB=N}EE z2=L$67MZmB;D_It$AYv7IMj*6J&|8si*?8%9o|$|SA(}m_os~AwI2XZCV}%Kf%0M^ z2Y#W8S&%EiRmaiQuKPUS+v3A-^FR9tqoVui`$^Q~?^G9@TP{>jC>@?jq4o%G@D{zg z$|E%D3U=@hmO=J|BP(ED-9qokGBr$VM>|KNXE6FW*o%J@g>X?@qP34buc!^8(Sq#= zj?O-wL`(ed78HKJgwRE!0WBj8NRWHU)E=-*^ilUuxq=6#557R(@{Gdn?9p>8Lf>rC z4NOmN>9b)tp}+TLyY>kDu2BTUA2bP@Ieqk$pThs!e!?I`o9xR;KsL(o&3;=ZLrs7! ze|-{O>gi%YbjJz*#1LLRp)~U7QGTeK~EJdFR zWm@-GyoGV$AH@~S_dtbwWB6o3W0Io-2(jzx*W|*NJG*kk(^BUq#WdgqA(JT4snt1 zvJn7a9Up7(U%r0tOVouaI3TM;G&4sXIUZ7ncaSeXl}A*(d4nmO3Is&i5k1rn{vZoF zy`lH&d~OKnZCh72catyfd}iTsAZ(W)5RnyQ>tM7H996=I)^K*TMywb#BT1+o49^1N z?fMBuy<()Zc7jsqw_UpR9!Fz9{g;4dxVbt9XbAO~uji57PMAJgK`UeW^|6108$Zm^ zorb~`f1_{JG%5wZ6E|fSoPLg-|)NWME>tks9QSGUluHW z21`#~LGz9N24@dsd_?PjdrzRX{^zCne-8ovo(kz(GWsEns5a?V=6^_H`u7*QqT%TN zDa1F0SBQ{4!X^*=K)owd3yr_<7hk%b^tt>dfx7$f@HU1-1{oVlVgd7&1t)pA!8v(d_AXYC)5ZUs8W}mdl%FfA*4;8zL(YH2 zN%fW&rQU7XO#>b)kteVTD!Qsj5UeBe+TbIPShz@{mYH%=H%RCLh0p>V-;C`BM+)UK zbQMPw0`jtJDTe?IXOV~B-*MO^&o|?J&`-U$P*mE(rvoGca*1XX~M4q#<4mgma+CsNXpei#mGBPqUGBPq^mI+!` z!P1&&pE{iRm(PM9gFSfsf6QfZ5njd9owc=rQU34k?q=ox!jBXGfA`+ry|38r+AzrZ z{K@10=ZrsHP2E;2h_vP|-bO+Gx8}XNlmENfY_{%w$^UQ@pMwJmz28e zY#MS;8o=Um8lhrZ)rrplj85ui(oCX=Zd*nH%IyRC$_YC@*<_CoPmWKj2m}KGynJ)Y zem#8s`tSw6pzs>>8`uohXafY`%>6-UMWyhoJ76^tylk3-3T0e(9UT?HPByF;KfvCY3UHXlCR z-0p5NyUbsj8yh_lvVDUNl~N~4=+F2URpT%0U;hFef%bSbvVlmU)5!r7t&!0bG^P0& zZzYkZS3q*q7Bor>3=|_ zsMvN7cyt=pg83}&O^1Vrv+0~;0(HU<(TpT5dfsvO0$CpmV|9Wjy z5{v>>@Tv&UsjR- z>GdD*KA(5jr~NIZ|FuCS_@e*az$bJ3k55kC9Cc2Qetrcd@WgQWHVX8=2K>*_|M1xt z{qH6|rywSOlM)uQ%fez#nInTS4_ou~#>OkYiqctDg2$a$To?dU`ou&kT>&fyJ=9^O z`cO%>syZ5>xadj`d4L&;zCi{N>m@GQhP}mjhcf*NlUfpjY!=OiplEE6LF_ynPCySq zf0~&{?=mIt6buTFDk6e{Rb&{)zcHT6R=}yTpX4Q0k>8ZMbQZ^b-b8Y+v9ZmbOz{RW z-Fgjru;x+xKaD4Q`{eH3D#H{>?b?2oy*y!muR`OfFT8{5bnJlVjL;p-5QeJAEbh(+ z>`wOzmcu)Y!bDMCB<=uz2fK?NLc+(M6y&$47D)mpRf|xKTKnC`#<3Wmm{dUJ^qx@8 z+;}p@q`5>i82{w!FNT{x=_gbEzry@1uaEwZH%BL@D=q^?^1t1@m&*U%_#*#r{H;vPigK$swY({T%UqIZdkozx8EY*%!E??X1;%Y7>Q?mGs{g1eJ>PXQ3n zaBvX}@$dyVuUoXVncdeR)p#~ay>-F2uU^;qVGre&rry#`{Tc3d60wO7iD(jUs3N;Z zB$K}C>ZWs*O~lr?Py1exPF~o4u+#~LS7blVBPhIq2=~d?6!&3xW&>5G6fr=YZm<|b z6RTxDVU@6UR%5@Mpe{#==wLnEtF^YO8%ZqAMR1?}gd*BXAlS(ZSg6pl0zJU%S~wbn zH7ANfTx%nIEZBS8IAXpDWSeaa+!xHYL73q%d|3{&KAG|#PmIIb)o8eS8^!W}&)NUo zYqpzTft3-U!=l5{-G($!$iSn}89(rxGYe<}#AbD470cZh}xyaG3O8s&F8l ze8ZmbOOOa*65}<>ToCAuOJ&~=f?*QA$FCyS!Vj_H^gnnd=`%| zC=!Mwr*@#n5;~3$1|3Cv7}h}B`gwp|RzrXbeS=+v!L-JXXDZ*N4VI4wo*<87HkeOI zY$_0O57SfP@%3rUehb41HV3=8ag`-#V!q;p2pC3t7I#Ez>}O+5rdFrnHdbPEyAO-X z25`YM^%7dgV0=~JDvD#{r8^?d^%sM~@@P&yRYa%ve;)Clmj9FU$vSO+_*vZk*WPQj zzS#e6;^Q5E?e+CoWUGLu%eRrg{_oXy8@sOk@7|a7|7Jcv{1t|FW9Rz~_Pu=&ylWl= z!&@w{>X|QqSozEsV3a-cXxl1JM?!l?*bp- z;|JXDoKOelq|fX-FsDbaUmQMnQ6lhoNBdjqrVV-;&$#J>zmyI5YLAc-(b+f)fPBv$ zvPK3RI-h3WUeN8TEI9ma9wxj*CmMHPLg#}Kg#f2|21E?;&AuIn$xOx!NyFlA0SGB; zd8qMNDIJRKodwM-Q-)AUeiswKi^^BF@-Q3EN1XzAMv)wX@gd-;7O_T%x5hG2bjlSD z1WcA5-E+nGL78CfQhH$2>KTvbZAeasTPO*NrnQ5|-liY}*>)EVL&We0O}ta>XGqXyOC2u^$Yu_~7o{6c_7mP3AMCWo3KY>{FF({#Si-%Vlm)C#NBXNLi4t#4{P4 z7+9PRO4WNz!*iLi|2iW5F^b3e)Bzkg3suuFz0;f~?;}YDxvXYT$yjy2wP&y(2bHBF zQYus`|GQF#b=X}G!dvjPvBkcVcy7Zl-R9*vD%KSX$`4lacR8BO3*7BuJnVO1+$7>& zq#yWsM;8Yb(3<$NgukZfc}8PMArmP-wRugN7P{w|0z~keR{_ixa)oRt#2=4luG!I{ zhTW>mj;R62B0chL#V-9FtABVRj4fO2YtL#$9Ux(=`%u|5)b9hlGKBj!L|QJY-S7%m zmL@FJX92NIS`rZM0NBFa!tRG3l#)t?v11@z z;;@ie71mAkBdzq#<8Bmg9pHwI_N&8aHbYZuGy_csJv;tt)Zqi|USoRO6XLCFlF;)aRi;=nUa7nm> zCrsl$2*f#VMui8)@Tx`zzP&bz$x+ZdkH%rmUL3N9C%QiBJdej`*GZj!WK(AtQ(j?3 z9lR+TEJGjO2ZuiUYvP{arz6nZrI0xNL@&wniD7iCG2xbU8ALN>(8D~ru3#EZ5bv+#aG@fJdq#_(-v8Ct$8{;B0Uh=&&7EK|7+6p|^n*_PhCe;OA z++|PDFcXYt$-x0eN_+VXnJ{G^k&;yaRCp8Gd4?#wogT3Fs1v-G&QMY*<8di5i#ShO z$z7ArNZr@vSOw3@L-Ys^rdM0F)Hu<#8QlWL)wUYSH%&qDe4U3jwy>OAsHZzI8MtqOe|_&pmSgW6TmS1{NYAeyVkQ6Sj-iD6Pb-Eo?r=&Rg{`y0 zpvqlmhb>L}r`e z*y%+*i~w71>%K;ENMriK8tC;;B!-uSk;*`tS-p=(JDB9pxhi^)Lok zxnc7kQ5ahx?@h zW(`|2^cU{<9P?bKW9_jgNwDNZB9H7}$YZ&po39!zR*gn7Mr<6Ah~w*lYBln;M2^!%Xgn3hA7h$V>k@Pxyrxx~Wfgf)Q}kBeVo&9?6yTWd#8sXq(0B z_CdT$ETeC>|EuI;3qkEHt`LZl&U`l5=f0|5*GpNn6Ml%2SyH((>Vlj^IGu3Zi_u~B zPHM6UWVm||OV<$XRoR_4r_Z+c?^GGaWXm`x9){@2sXU|z(FYceROAQr!L-rX`5rU8 z938;0UE(chfmVZGQk?`zLf4crzz=2XnS(?XBjxl2(eq-*-Mz?_tIBMbUO>w4%nM9K zVG7ydljCFS4RwZkhz?xr;X}6hUmxoCH(^1t$`M^s zDoEiiz_3JDo;3YkdHQVA2fzk$w;{poI~2h;9MI!uKodVv&~vt#rVDb$^3;Mwc{nF; zbxE@%i(2e1Yv$7{#Ue*_FIjvfIm{zT3EDO@Y1=HOEtN(3wtV!37aqEor7*~afbLTc=Tj*%QrK1SIr?h^3GznEC6Ugm%kMS`js8dd=4*wQg$per%Bc@ z`sUB#GX-?QX?zwS%PcvV=}~xEdZJ`g_Wt17C(5-^cQOpdK+nc@D@Xja7NqL4^5WPq*Rz&)ycQKN`7p{2Rz8e! zLuCQD=3z5`%28vDy)_J(m`d$62&nyAHkFD8NGS&EpIc%_VBD@1rJMqK%TU7Nb2R43 zcrXe8-;cciHhC|D&xvE`ImOhW9g}PA5tY78$mAn_&0j(`1-H-vS?R`ZPs5CEfZqBoN$WvWmv7)`_41+w319j_Omp08X4Sfd* zL-aP%u$AMmE~m@r3rbdTc_E28HndYz_n*slmG=`Nc>4;^ z$tqQBeMgkDN%k$33E%1z7#Yi169a zAY)2fu>CVxvb8AnZUL4|8SEFOGpu7m;N&bRX;Jd^YKzT*ZkJ)q$goc_TVm3*lnl(* zQiM~AEl0l2xJ`Z4wu>tjWI|u)R@JiE;M}U88iXggPSRqy_zSlTCu5jH-*g>Mh8kAww?U+ z^7W~6ML!9HY44oKO((_?C52^z-)H9m-maYW&Onu6)M9W&;&>Hfm^+)tD7;wR0)1WWi)UW&eZTUww&VX$m(Hcou=1Zqz{`i zMn≦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 MfGT$8@i^4ocPQ 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

!FR*H@{nWV|69&;y=x1&DOCB$J+h9l0jVY#L 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�VOYzO8qdpFy6=nK`r#tl6cl+2|-T(7ioB)I~ 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 z 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

prX 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 diff --git a/packaging/FLATBUFFERS-2.0.tar.gz b/packaging/FLATBUFFERS-2.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..5e8a3361da705becfbb3d3112419e010e7a43189 GIT binary patch literal 345691 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≥lz>Aoj+l;hj-4p%=SZ_HG05QQ?Q`MHJ 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

6}h#ek94}%fJN%3&Bh&7_^+MD)zD0 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 z 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Ξ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�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�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

D7FcGpAow^f2u|_?FJLlwm$Y!ml2bz%Euet#u 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⁡|&P_Wj00cbvMthkjC@*kL>| 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 literal 0 HcmV?d00001 diff --git a/packaging/TENSORFLOW-2.3.0-EIGEN.tar.gz b/packaging/TENSORFLOW-2.3.0-EIGEN.tar.gz deleted file mode 100644 index 396d12fa764dc1c7fa321293b2d14b2fa965a4e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2664733 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 $IyHUa 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