From 3c20ee014b27db29c3457b8b2d48821f30b11752 Mon Sep 17 00:00:00 2001 From: liufeiyang Date: Fri, 21 Jun 2024 11:42:59 +0800 Subject: [PATCH] Add batch inference feature and optimizer model. --- aiframe/CMakeLists.txt | 8 +- aiframe/ONNXRunner.cpp | 353 ++++++++++++++++++++++------------- aiframe/include/ONNXRunner.h | 244 +++++++++++++++++++----- models/optimizer.onnx | Bin 0 -> 41037 bytes 4 files changed, 426 insertions(+), 179 deletions(-) create mode 100644 models/optimizer.onnx diff --git a/aiframe/CMakeLists.txt b/aiframe/CMakeLists.txt index 8ece1ce6..9f8022f5 100644 --- a/aiframe/CMakeLists.txt +++ b/aiframe/CMakeLists.txt @@ -4,8 +4,6 @@ project(ONNXRunner) set(CMAKE_CXX_STANDARD 17) -#set(CMAKE_CXX_FLAGS_DEBUG "-g -Wall") - set(INC_DIR /usr/include) set(INC_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/include) set(LIB_DIR /usr/lib64) @@ -18,6 +16,6 @@ link_directories(${LIB_DIR}) add_library(ONNXRunner SHARED ONNXRunner.cpp) -#target_link_libraries(ONNXRunner -#PRIVATE -#libonnxruntime.so) +target_link_libraries(ONNXRunner +PRIVATE +libcrypto.so) # libonnxruntime.so diff --git a/aiframe/ONNXRunner.cpp b/aiframe/ONNXRunner.cpp index 3ec159c7..3aefae33 100644 --- a/aiframe/ONNXRunner.cpp +++ b/aiframe/ONNXRunner.cpp @@ -1,151 +1,244 @@ -#include -#include +#include "include/ONNXRunner.h" #include +#include #include -#include "include/ONNXRunner.h" +#include -namespace boltONNXRunner{ +namespace compilerONNXRunner { -Ort::Value ONNXRunner::getInputValueFloat(Ort::Session *session, +Ort::Value ONNXRunner::getInputValueFloat(Ort::Session *session, std::vector &input, - int inputIdx) { - auto typeInfo = session->GetInputTypeInfo(inputIdx); - auto tensorInfo = typeInfo.GetTensorTypeAndShapeInfo(); - auto inputDims = tensorInfo.GetShape(); - std::replace_if( - inputDims.begin(), inputDims.end(), [](int64_t &i) { return i < 0; }, 1); - - size_t inputTensorSize = std::accumulate(inputDims.begin(), inputDims.end(), - 1, std::multiplies()); - auto memory_info = - Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); - auto inputTmp = Ort::Value::CreateTensor( - memory_info, input.data(), inputTensorSize, inputDims.data(), - inputDims.size()); - auto inputTensor = &inputTmp; - return inputTmp; + int inputIdx, int batchSize) { + auto typeInfo = session->GetInputTypeInfo(inputIdx); + auto tensorInfo = typeInfo.GetTensorTypeAndShapeInfo(); + auto inputDims = tensorInfo.GetShape(); + std::replace_if( + inputDims.begin(), inputDims.end(), [](int64_t &i) { return i < 0; }, 1); + + size_t inputTensorSize = std::accumulate(inputDims.begin(), inputDims.end(), + 1, std::multiplies()); + // try to add batch size + inputDims[0] = batchSize; + inputTensorSize = inputTensorSize * batchSize; + auto memoryInfo = + Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); + auto inputTmp = + Ort::Value::CreateTensor(memoryInfo, input.data(), inputTensorSize, + inputDims.data(), inputDims.size()); + auto inputTensor = &inputTmp; + return inputTmp; } -Ort::Value ONNXRunner::getInputValueString(Ort::AllocatorWithDefaultOptions allocator, - Ort::Session *session, - std::vector &input, - int inputIdx) { - auto typeInfo = session->GetInputTypeInfo(inputIdx); - auto tensorInfo = typeInfo.GetTensorTypeAndShapeInfo(); - auto inputDims = tensorInfo.GetShape(); - - std::replace_if( - inputDims.begin(), inputDims.end(), [](int64_t &i) { return i < 0; }, 1); - - size_t inputTensorSize = std::accumulate(inputDims.begin(), inputDims.end(), - 1, std::multiplies()); - const char* input_strings[inputTensorSize]; - for(int i = 0; i < inputTensorSize; i++) { - input_strings[i] = input[i].c_str(); - } - - auto memory_info = - Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); - auto inputTmp = Ort::Value::CreateTensor(allocator, inputDims.data(), - inputDims.size(), - ONNX_TENSOR_ELEMENT_DATA_TYPE_STRING); - inputTmp.FillStringTensor(input_strings, inputTensorSize); - auto inputTensor = &inputTmp; - return inputTmp; +Ort::Value ONNXRunner::getInputValueString( + Ort::AllocatorWithDefaultOptions allocator, Ort::Session *session, + std::vector &input, int inputIdx, int batchSize) { + auto typeInfo = session->GetInputTypeInfo(inputIdx); + auto tensorInfo = typeInfo.GetTensorTypeAndShapeInfo(); + auto inputDims = tensorInfo.GetShape(); + + std::replace_if( + inputDims.begin(), inputDims.end(), [](int64_t &i) { return i < 0; }, 1); + + size_t inputTensorSize = std::accumulate(inputDims.begin(), inputDims.end(), + 1, std::multiplies()); + inputDims[0] = batchSize; + inputTensorSize = inputTensorSize * batchSize; + const char *inputStrings[inputTensorSize]; + for (int i = 0; i < inputTensorSize; i++) { + inputStrings[i] = input[i].c_str(); + } + + auto memoryInfo = + Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); + auto inputTmp = + Ort::Value::CreateTensor(allocator, inputDims.data(), inputDims.size(), + ONNX_TENSOR_ELEMENT_DATA_TYPE_STRING); + inputTmp.FillStringTensor(inputStrings, inputTensorSize); + auto inputTensor = &inputTmp; + return inputTmp; } -Ort::Value ONNXRunner::getInputValueInt64(Ort::Session *session, +Ort::Value ONNXRunner::getInputValueInt64(Ort::Session *session, std::vector &input, - int inputIdx) { - auto typeInfo = session->GetInputTypeInfo(inputIdx); - auto tensorInfo = typeInfo.GetTensorTypeAndShapeInfo(); - auto inputDims = tensorInfo.GetShape(); - std::replace_if( - inputDims.begin(), inputDims.end(), [](int64_t &i) { return i < 0; }, 1); - - size_t inputTensorSize = std::accumulate(inputDims.begin(), inputDims.end(), - 1, std::multiplies()); - auto memory_info = - Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); - auto inputTmp = Ort::Value::CreateTensor( - memory_info, input.data(), inputTensorSize, inputDims.data(), - inputDims.size()); - auto inputTensor = &inputTmp; - return inputTmp; + int inputIdx, int batchSize) { + auto typeInfo = session->GetInputTypeInfo(inputIdx); + auto tensorInfo = typeInfo.GetTensorTypeAndShapeInfo(); + auto inputDims = tensorInfo.GetShape(); + std::replace_if( + inputDims.begin(), inputDims.end(), [](int64_t &i) { return i < 0; }, 1); + + size_t inputTensorSize = std::accumulate(inputDims.begin(), inputDims.end(), + 1, std::multiplies()); + inputDims[0] = batchSize; + inputTensorSize = inputTensorSize * batchSize; + auto memoryInfo = + Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); + auto inputTmp = Ort::Value::CreateTensor( + memoryInfo, input.data(), inputTensorSize, inputDims.data(), + inputDims.size()); + auto inputTensor = &inputTmp; + return inputTmp; } -float ONNXRunner::runONNXModel(std::vector input_string, std::vector input_int64, std::vector input_float){ - Ort::AllocatorWithDefaultOptions allocator; - - //Try to get input; - int input_count = session->GetInputCount(); - - //Get input name - std::vector inputNameList; - for (int i = 0; i < input_count; i++) { - auto inputName = session->GetInputNameAllocated(i, allocator); - auto inputNameStr = inputName.get(); - inputNameList.push_back(inputNameStr); - } - - //Form input tensor(s) - std::vector input_final; - std::vector inputNameStr_final; - - int currentIdx = 0; - if(!input_string.empty()) { - input_final.push_back(getInputValueString(allocator, session, input_string, currentIdx)); - currentIdx ++; - } - - if(!input_int64.empty()) { - input_final.push_back(getInputValueInt64(session, input_int64, currentIdx)); - currentIdx ++; - } - - if(!input_float.empty()) { - input_final.push_back(getInputValueFloat(session, input_float, currentIdx)); - currentIdx ++; +std::vector +ONNXRunner::runONNXModel(std::vector inputString, + std::vector inputInt64, + std::vector inputFloat, int batchSize) { + Ort::AllocatorWithDefaultOptions allocator; + + // Get input count + int inputCount = session->GetInputCount(); + + // Get input name + std::vector inputNameList; + for (int i = 0; i < inputCount; i++) { + auto inputName = session->GetInputNameAllocated(i, allocator); + auto inputNameStr = inputName.get(); + inputNameList.push_back(inputNameStr); + } + + // Form input tensor(s) + std::vector inputFinal; + std::vector inputNameStrFinal; + int currentIdx = 0; + if (!inputString.empty()) { + inputFinal.push_back(getInputValueString(allocator, session, inputString, + currentIdx, batchSize)); + currentIdx++; + } + + if (!inputInt64.empty()) { + inputFinal.push_back( + getInputValueInt64(session, inputInt64, currentIdx, batchSize)); + currentIdx++; + } + + if (!inputFloat.empty()) { + inputFinal.push_back( + getInputValueFloat(session, inputFloat, currentIdx, batchSize)); + currentIdx++; + } + + for (int i = 0; i < inputCount; i++) { + inputNameStrFinal.push_back(inputNameList[i].c_str()); + } + + // Run model + int outputCount = session->GetOutputCount(); + std::vector outputNameList; + for (int i = 0; i < outputCount; i++) { + auto outputName = session->GetOutputNameAllocated(i, allocator); + std::string outputNameStr = outputName.get(); + if (!outputNameStr.empty()) { + outputNameList.push_back(outputNameStr); + } else { + std::string outputNameDefault = "Output_" + std::to_string(i); + outputNameList.push_back(outputNameDefault); } + } + + std::vector outputNameStrFinal; + for (int i = 0; i < outputCount; i++) { + outputNameStrFinal.push_back(outputNameList[i].c_str()); + } + + auto outputTensors = session->Run( + Ort::RunOptions{nullptr}, inputNameStrFinal.data(), inputFinal.data(), + inputCount, outputNameStrFinal.data(), outputCount); + + // Get result and return + std::vector probs; + float *outputProbability = outputTensors[0].GetTensorMutableData(); + for (int i = 0; i < batchSize; i++) { + Ort::Value mapOut = + outputTensors[1].GetValue(static_cast(i), allocator); + Ort::Value keysOrt = mapOut.GetValue(0, allocator); + int64_t *keysRet = keysOrt.GetTensorMutableData(); + Ort::Value valuesOrt = mapOut.GetValue(1, allocator); + float *valuesRet = valuesOrt.GetTensorMutableData(); + probs.push_back((*(valuesRet + 1))); + } + + return probs; +} - for (int i = 0; i < input_count; i++) { - inputNameStr_final.push_back(inputNameList[i].c_str()); +int64_t ONNXRunner::runONNXModelOptimizer(std::vector inputString, + std::vector inputInt64, + std::vector inputFloat, + int batchSize) { + Ort::AllocatorWithDefaultOptions allocator; + + // Get input count + int inputCount = session->GetInputCount(); + std::vector inputInt64Tensor(FEATURE_SIZE_INT64_OPT); + std::vector inputStringTensor; + std::vector inputFinal; + + inputInt64.clear(); + inputInt64.resize(FEATURE_SIZE_INT64_OPT); + for (int i = 0; i < FEATURE_SIZE_INT64_OPT; i++) { + auto inputName = session->GetInputNameAllocated(i, allocator); + auto inputNameStr = inputName.get(); + + inputInt64Tensor.clear(); + inputInt64Tensor.push_back(inputInt64[i]); + inputFinal.push_back( + getInputValueInt64(session, inputInt64Tensor, i, batchSize)); + } + + for (int i = FEATURE_SIZE_INT64_OPT; + i < FEATURE_SIZE_INT64_OPT + FEATURE_SIZE_STRING_OPT; i++) { + inputStringTensor.clear(); + inputStringTensor.push_back(inputString[i - FEATURE_SIZE_INT64_OPT]); + inputFinal.push_back(getInputValueString(allocator, session, + inputStringTensor, i, batchSize)); + } + + // Get input name from model + std::vector inputNameList; + for (int i = 0; i < inputCount; i++) { + auto inputName = session->GetInputNameAllocated(i, allocator); + auto inputNameStr = inputName.get(); + inputNameList.push_back(inputNameStr); + } + + // Form input tensor(s) + std::vector inputNameStrFinal; + for (int i = 0; i < inputCount; i++) { + inputNameStrFinal.push_back(inputNameList[i].c_str()); + } + + // Run model + int outputCount = session->GetOutputCount(); + std::vector outputNameList; + for (int i = 0; i < outputCount; i++) { + auto outputName = session->GetOutputNameAllocated(i, allocator); + std::string outputNameStr = outputName.get(); + if (!outputNameStr.empty()) { + outputNameList.push_back(outputNameStr); + } else { + std::string outputNameDefault = "Output_" + std::to_string(i); + outputNameList.push_back(outputNameDefault); } + } - //Run the model - int output_count = session->GetOutputCount(); - std::vector outputNameList; - for (int i = 0; i < output_count; i++) { - auto outputName = session->GetOutputNameAllocated(i, allocator); - std::string outputNameStr = outputName.get(); - if(!outputNameStr.empty()) { - outputNameList.push_back(outputNameStr); - } else { - std::string outputNameDefault = "Output_" + std::to_string(i); - outputNameList.push_back(outputNameDefault); - } - } + std::vector outputNameStrFinal; + for (int i = 0; i < outputCount; i++) { + outputNameStrFinal.push_back(outputNameList[i].c_str()); + } - std::vector outputNameStr_final; - for(int i = 0; i < output_count; i++) { - outputNameStr_final.push_back(outputNameList[i].c_str()); - } - - auto outputTensors = - session->Run(Ort::RunOptions{nullptr}, inputNameStr_final.data(), - input_final.data(), input_count, outputNameStr_final.data(), output_count); + auto outputTensors = session->Run( + Ort::RunOptions{nullptr}, inputNameStrFinal.data(), inputFinal.data(), + inputCount, outputNameStrFinal.data(), outputCount); - //Try to get the result & return - float* output_probability = outputTensors[0].GetTensorMutableData(); - Ort::Value map_out = outputTensors[1].GetValue(static_cast(0), allocator); + // Get result and return + int64_t label = 0; + for (int i = 0; i < batchSize; i++) { + int64_t *outputLabel = outputTensors[0].GetTensorMutableData(); + label = *outputLabel; + } - Ort::Value keys_ort = map_out.GetValue(0, allocator); - int64_t* keys_ret = keys_ort.GetTensorMutableData(); - Ort::Value values_ort = map_out.GetValue(1, allocator); - float* values_ret = values_ort.GetTensorMutableData(); - - return *(values_ret + 1); + return label; } -} // namespace boltONNXRunner - +} // namespace compilerONNXRunner diff --git a/aiframe/include/ONNXRunner.h b/aiframe/include/ONNXRunner.h index 54bf341e..bc8535f1 100644 --- a/aiframe/include/ONNXRunner.h +++ b/aiframe/include/ONNXRunner.h @@ -1,64 +1,220 @@ -#ifndef BOLT_PROFILE_ONNXRUNNER_H -#define BOLT_PROFILE_ONNXRUNNER_H +#ifndef COMPILER_PROFILE_ONNXRUNNER_H +#define COMPILER_PROFILE_ONNXRUNNER_H -#include -#include -#include -#include #include "onnxruntime_c_api.h" #include "onnxruntime_cxx_api.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include extern "C" { -namespace boltONNXRunner { +namespace compilerONNXRunner { + +const char* MODEL_PATH_OPT = "/usr/lib64/AI4C/optimizer.onnx"; +const int FEATURE_SIZE_INT64_OPT = 6; +const int FEATURE_SIZE_STRING_OPT = 11; + class ONNXRunner { public: - explicit ONNXRunner() {} - explicit ONNXRunner(const char* model_path) { - // prepare model and env - session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_BASIC); - session = new Ort::Session(env, model_path, session_options); - } - ~ONNXRunner() { - delete session; - } - float runONNXModel(std::vector input_string, std::vector input_int64, std::vector input_float); + explicit ONNXRunner() {} + explicit ONNXRunner(const char *modelPath) { + // Prepare model and env + sessionOptions.SetGraphOptimizationLevel( + GraphOptimizationLevel::ORT_ENABLE_BASIC); + session = new Ort::Session(env, modelPath, sessionOptions); + } + ~ONNXRunner() { delete session; } + std::vector runONNXModel(std::vector inputString, + std::vector inputInt64, + std::vector inputFloat, int batchSize); + int64_t runONNXModelOptimizer(std::vector inputString, + std::vector inputInt64, + std::vector inputFloat, int batchSize); private: - static Ort::Value getInputValueFloat(Ort::Session *session, - std::vector &input, - int inputIdx); - - static Ort::Value getInputValueString(Ort::AllocatorWithDefaultOptions allocator, - Ort::Session *session, - std::vector &input, - int inputIdx); - - static Ort::Value getInputValueInt64(Ort::Session *session, - std::vector &input, - int inputIdx); - - Ort::SessionOptions session_options; - Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "test"}; - Ort::Session *session; + static Ort::Value getInputValueFloat(Ort::Session *session, + std::vector &input, int inputIdx, + int batchSize); + + static Ort::Value + getInputValueString(Ort::AllocatorWithDefaultOptions allocator, + Ort::Session *session, std::vector &input, + int inputIdx, int batchSize); + + static Ort::Value getInputValueInt64(Ort::Session *session, + std::vector &input, + int inputIdx, int batchSize); + + Ort::SessionOptions sessionOptions; + Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "test"}; + Ort::Session *session; }; -extern ONNXRunner* createONNXRunner(const char* model_path) { - return new ONNXRunner(model_path); +extern ONNXRunner *createONNXRunner(const char *modelPath) { + std::ifstream file(modelPath); + if (file.good()) { + return new ONNXRunner(modelPath); + } else { + return nullptr; + } +} + +extern void deleteONNXRunner(ONNXRunner *instance) { + if (instance != nullptr) { + delete instance; + } +} + +extern std::vector runONNXModel(ONNXRunner *instance, + std::vector inputString, + std::vector inputInt64, + std::vector inputFloat, + int batchSize) { + std::vector nullResult; + if (instance != nullptr) { + return instance->runONNXModel(inputString, inputInt64, inputFloat, + batchSize); + } else { + return nullResult; + } +} + +static bool startsWithPatt(const std::string &str, const std::string &pattern) { + return str.rfind(pattern, 0) == 0; } -extern void deleteONNXRunner(ONNXRunner* instance) { - if (instance != nullptr) { - delete instance; +static bool optionComparator(const std::string &str1, const std::string &str2) { + for (size_t i = 0; i < str1.size() && i < str2.size(); ++i) { + char c1 = str1[i]; + char c2 = str2[i]; + if (std::isupper(c1) && !std::isupper(c2)) { + return 1; + } else if (!std::isupper(c1) && std::isupper(c2)) { + return 0; + } else if (c1 != c2) { + return c1 > c2; } + } + + return str1.size() < str2.size(); +} + +static void truncatePrefix(const std::string &str, std::string &strPrefix) { + const char prefixIndicator = '_'; + size_t idx = str.find(prefixIndicator); + if (idx == std::string::npos) + strPrefix = str; + else + strPrefix = str.substr(0, idx + 1); +} + +static std::string encodeStringFeature(const std::string &str) { + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256_CTX sha256; + SHA256_Init(&sha256); + SHA256_Update(&sha256, str.c_str(), str.size()); + SHA256_Final(hash, &sha256); + + std::stringstream ss; + for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { + ss << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i]; + } + return ss.str(); } -extern float runONNXModel(ONNXRunner* instance, std::vector input_string, std::vector input_int64, std::vector input_float) { - if (instance != nullptr) { - return instance->runONNXModel(input_string, input_int64, input_float); - } else { - return -1; +static void preprocessData(std::vector &inputString, + std::vector &inputInt64, int argcSW, + const char **argvSW, const char *mcpuOption, + int argcHW, int64_t *argvHW) { + const char *outputOption = "-o"; + const char *macroPrefix = "-D"; + const char *needle = "--param"; + const char *flagPrefix = "-"; + const char *defaultOption = "-fdefault-option"; + const int defaultIntVal = 0; + + // Preprocessing string features. + std::string outputFile = ""; + std::vector inputStringRaw = {std::string(mcpuOption)}; + std::vector macroOptions; + for (int i = 0; i < argcSW; ++i) { + std::string opt = std::string(argvSW[i]); + if (startsWithPatt(opt, macroPrefix)) { + macroOptions.push_back(opt); + } + if (i + 1 < argcSW && opt.compare(outputOption) == 0) { + truncatePrefix(std::string(argvSW[i + 1]), outputFile); + } + } + inputStringRaw.push_back(outputFile); + + std::sort(macroOptions.begin(), macroOptions.end(), optionComparator); + for (size_t i = 0; i < macroOptions.size() && + (int)inputStringRaw.size() < FEATURE_SIZE_STRING_OPT; + ++i) { + inputStringRaw.push_back(macroOptions[i]); + } + + for (int i = 0; + i < argcSW && (int)inputStringRaw.size() < FEATURE_SIZE_STRING_OPT; + ++i) { + std::string opt = std::string(argvSW[i]); + if (!startsWithPatt(opt, macroPrefix) && !startsWithPatt(opt, needle) && + startsWithPatt(opt, flagPrefix)) { + inputStringRaw.push_back(opt); } + } + + for (int i = (int)inputStringRaw.size(); i < FEATURE_SIZE_STRING_OPT; ++i) { + inputStringRaw.push_back(defaultOption); + } + for (size_t i = 0; i < inputStringRaw.size(); ++i) { + inputString.push_back(encodeStringFeature(inputStringRaw[i])); + } + + // Preprocessing int64 features. + for (int i = 0; i < argcHW && i < FEATURE_SIZE_INT64_OPT; ++i) { + inputInt64.push_back(argvHW[i]); + } + + for (int i = (int)inputInt64.size(); i < FEATURE_SIZE_INT64_OPT; ++i) { + inputInt64.push_back(defaultIntVal); + } +} + +extern int64_t runONNXModelOptimizer(int argcSW, const char **argvSW, + const char *mcpuOption, int argcHW, + int64_t *argvHW) { + // Create model runner. + ONNXRunner *instance = createONNXRunner(MODEL_PATH_OPT); + if (instance == nullptr) { + return -1; + } + + // Preprocess data. + std::vector inputString; + std::vector inputInt64; + std::vector inputFloat; + preprocessData(inputString, inputInt64, argcSW, argvSW, mcpuOption, argcHW, + argvHW); + + // Run model. + int64_t output; + if (instance != nullptr) { + output = + instance->runONNXModelOptimizer(inputString, inputInt64, inputFloat, 1); + } + + // Delete model runner. + deleteONNXRunner(instance); + return output; } -} // namespace boltONNXRunner +} // namespace compilerONNXRunner } // extern "C" #endif diff --git a/models/optimizer.onnx b/models/optimizer.onnx new file mode 100644 index 0000000000000000000000000000000000000000..8218b0c2f589a4fd6d9010e67f4e23dab6358f0e GIT binary patch literal 41037 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 kA?zB3eD)ic`87&i2#%*f>cNumgAvO7{G$