diff --git a/.cspell-wordlist.txt b/.cspell-wordlist.txt index 73fe480ed..89fd842b5 100644 --- a/.cspell-wordlist.txt +++ b/.cspell-wordlist.txt @@ -53,6 +53,10 @@ QINT FNUZ wordlist jitpack +googletest +ctest +gtest +rnexecutorch coreml mobilenetv flac \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 05b14e37f..f1aeca054 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "tokenizers-cpp"] path = third-party/tokenizers-cpp url = https://github.com/software-mansion-labs/tokenizers-cpp +[submodule "third-party/googletest"] + path = third-party/googletest + url = https://github.com/google/googletest.git diff --git a/packages/react-native-executorch/common/rnexecutorch/data_processing/Numerical.cpp b/packages/react-native-executorch/common/rnexecutorch/data_processing/Numerical.cpp index 6e3d9739c..ef8e4baee 100644 --- a/packages/react-native-executorch/common/rnexecutorch/data_processing/Numerical.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/data_processing/Numerical.cpp @@ -2,73 +2,77 @@ #include #include +#include +#include #include -#include +#include namespace rnexecutorch::numerical { -void softmax(std::vector &v) { - float max = *std::max_element(v.begin(), v.end()); - float sum = 0.0f; - for (float &x : v) { - x = std::exp(x - max); - sum += x; - } - for (float &x : v) { - x /= sum; +void softmax(std::span input) { + if (input.empty()) { + return; } -} -void normalize(std::span span) { - auto sum = 0.0f; - for (const auto &val : span) { - sum += val * val; - } + const auto maxElement = *std::ranges::max_element(input); - if (isClose(sum, 0.0f)) { - return; + for (auto &value : input) { + value = std::exp(value - maxElement); } - float norm = std::sqrt(sum); - for (auto &val : span) { - val /= norm; + const auto sum = std::reduce(input.begin(), input.end()); + + // sum is at least 1 since exp(max - max) == exp(0) == 1 + for (auto &value : input) { + value /= sum; } } -void normalize(std::vector &v) { - float sum = 0.0f; - for (float &x : v) { - sum += x * x; - } +void normalize(std::span input) { + const auto sumOfSquares = + std::inner_product(input.begin(), input.end(), input.begin(), 0.0F); + + constexpr auto kEpsilon = 1.0e-15F; - float norm = - std::max(std::sqrt(sum), 1e-9f); // Solely for preventing division by 0 - for (float &x : v) { - x /= norm; + const auto norm = std::sqrt(sumOfSquares) + kEpsilon; + + for (auto &value : input) { + value /= norm; } } std::vector meanPooling(std::span modelOutput, std::span attnMask) { + if (attnMask.empty() || modelOutput.size() % attnMask.size() != 0) { + throw std::invalid_argument( + std::format("Invalid dimensions for mean pooling, expected model " + "output size to be divisible " + "by the size of attention mask but got size: {} for model " + "output and size: {} for attention mask", + modelOutput.size(), attnMask.size())); + } + auto attnMaskLength = attnMask.size(); auto embeddingDim = modelOutput.size() / attnMaskLength; - float maskSum = 0; - for (const auto &v : attnMask) { - maskSum += static_cast(v); + auto maskSum = std::reduce(attnMask.begin(), attnMask.end()); + std::vector result(embeddingDim, 0.0F); + if (maskSum == 0LL) { + return result; } - maskSum = std::max(maskSum, 1e-9f); - - auto result = std::vector(); - result.reserve(embeddingDim); - for (size_t i = 0; i < embeddingDim; i++) { - float dimensionSum = 0; - for (size_t j = 0; j < attnMaskLength; j++) { - dimensionSum += - modelOutput[j * embeddingDim + i] * static_cast(attnMask[j]); + + for (std::size_t i = 0; i < attnMaskLength; ++i) { + if (attnMask[i] != 0LL) { + for (std::size_t j = 0; j < embeddingDim; ++j) { + result[j] += modelOutput[i * embeddingDim + j]; + } } - result.push_back(dimensionSum / maskSum); } + + for (auto &value : result) { + value /= static_cast(maskSum); + } + return result; } @@ -79,4 +83,4 @@ template bool isClose(T a, T b, T atol) { template bool isClose(float, float, float); template bool isClose(double, double, double); -} // namespace rnexecutorch::numerical \ No newline at end of file +} // namespace rnexecutorch::numerical diff --git a/packages/react-native-executorch/common/rnexecutorch/data_processing/Numerical.h b/packages/react-native-executorch/common/rnexecutorch/data_processing/Numerical.h index 77a13f44f..78dd482dd 100644 --- a/packages/react-native-executorch/common/rnexecutorch/data_processing/Numerical.h +++ b/packages/react-native-executorch/common/rnexecutorch/data_processing/Numerical.h @@ -3,13 +3,51 @@ #include #include +/** + * @namespace rnexecutorch::numerical + * @brief Namespace for numerical operations and transformations. + */ namespace rnexecutorch::numerical { -void softmax(std::vector &v); -void normalize(std::span span); -void normalize(std::vector &v); -void normalize(std::span span); + +/** + * @brief Applies the softmax function in-place to a sequence of numbers. + * + * @param input A mutable span of floating-point numbers. After the function + * returns, `input` contains the softmax probabilities. + */ +void softmax(std::span input); + +/** + * @brief Normalizes the elements of the given float span in-place using the + * L2 norm method. + * + * This function scales the input vector such that its L2 norm (Euclidean norm) + * becomes 1. If the norm is zero, the result is a zero vector with the same + * size as the input. + * + * @param input A mutable span of floating-point values representing the data to + * be normalized. + */ +void normalize(std::span input); + +/** + * @brief Computes mean pooling across the modelOutput adjusted by an attention + * mask. + * + * This function aggregates the `modelOutput` span by sections defined by + * `attnMask`, computing the mean of sections influenced by the mask. The result + * is a vector where each element is the mean of a segment from the original + * data. + * + * @param modelOutput A span of floating-point numbers representing the model + * output. + * @param attnMask A span of integers where each integer is a weight + * corresponding to the elements in `modelOutput`. + * @return A std::vector containing the computed mean values of segments. + */ std::vector meanPooling(std::span modelOutput, std::span attnMask); + /** * @brief Checks if two floating-point numbers are considered equal. */ @@ -20,4 +58,4 @@ bool isClose(T a, T b, extern template bool isClose(float, float, float); extern template bool isClose(double, double, double); -} // namespace rnexecutorch::numerical \ No newline at end of file +} // namespace rnexecutorch::numerical diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/CMakeLists.txt b/packages/react-native-executorch/common/rnexecutorch/tests/CMakeLists.txt new file mode 100644 index 000000000..66758f6b2 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.10) +project(RNExecutorchTests) + +# C++ standard +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED TRUE) + +# googletest subdirectory +# Using an absolute path from the top-level source directory +add_subdirectory(${CMAKE_SOURCE_DIR}/../../../../../third-party/googletest ${PROJECT_BINARY_DIR}/googletest) + +# Directories to include +include_directories(${CMAKE_SOURCE_DIR}/../data_processing) +include_directories(${CMAKE_SOURCE_DIR}/..) + +# Source files +set(SOURCE_FILES ${CMAKE_SOURCE_DIR}/../data_processing/Numerical.cpp) + +# Executables for the tests +add_executable(NumericalTests NumericalTest.cpp ${SOURCE_FILES}) +add_executable(LogTests LogTest.cpp) + +# Libraries linking +target_link_libraries(NumericalTests gtest gtest_main) +target_link_libraries(LogTests gtest gtest_main) + +# Testing functionalities +enable_testing() +add_test(NAME NumericalTests COMMAND NumericalTests) +add_test(NAME LogTests COMMAND LogTests) diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/NumericalTest.cpp b/packages/react-native-executorch/common/rnexecutorch/tests/NumericalTest.cpp new file mode 100644 index 000000000..050c9579a --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/tests/NumericalTest.cpp @@ -0,0 +1,110 @@ +#include "../data_processing/Numerical.h" +#include +#include +#include +#include +#include + +namespace rnexecutorch::numerical { + +// Helper function to check if two float vectors are approximately equal +void expect_vectors_eq(const std::vector &vector1, + const std::vector &vector2, float atol = 1.0e-6F) { + ASSERT_EQ(vector1.size(), vector2.size()); + for (size_t i = 0; i < vector1.size(); i++) { + EXPECT_NEAR(vector1[i], vector2[i], atol); + } +} + +TEST(SoftmaxTests, SoftmaxBasic) { + std::vector input = {1.0F, 2.0F, 3.0F}; + softmax(input); + const std::vector expected = {0.09003057F, 0.24472847F, 0.66524095F}; + expect_vectors_eq(input, expected); +} + +TEST(SoftmaxTests, SoftmaxWithBigValues) { + std::vector input = {100000.0F, 100000.0F, 100000.0F}; + softmax(input); + const std::vector expected = {0.3333333F, 0.3333333F, 0.3333333F}; + expect_vectors_eq(input, expected); +} + +TEST(SoftmaxTests, SoftmaxOfEmptyVector) { + std::vector emptyVector{}; + EXPECT_NO_THROW(softmax(emptyVector)); +} + +TEST(NormalizeTests, NormalizeBasic) { + std::vector input = {1.0F, 2.0F, 3.0F}; + normalize(input); + const auto normOfInput = std::sqrtf(14.0F); + const std::vector expected = {1.0F / normOfInput, 2.0F / normOfInput, + 3.0F / normOfInput}; + expect_vectors_eq(input, expected); +} + +TEST(NormalizeTests, NormalizationOfExtremelySmallValues) { + constexpr auto epsilon = std::numeric_limits::epsilon(); + std::vector input(3, epsilon); + const auto normOfInput = std::sqrtf(3.0F); + const std::vector expected(3, 1.0F / normOfInput); + normalize(input); + expect_vectors_eq(input, expected); +} + +TEST(NormalizeTests, NormalizationOfZeroVector) { + std::vector zeroVector(3, 0.0F); + EXPECT_NO_THROW(normalize(zeroVector)); +} + +TEST(NormalizeTests, NormalizationOfEmptyVector) { + std::vector emptyVector{}; + EXPECT_NO_THROW(normalize(emptyVector)); +} + +TEST(MeanPoolingTests, MeanPoolingBasic) { + const std::vector modelOutputVec = {1.0F, 2.0F, 3.0F, + 4.0F, 5.0F, 6.0F}; + const std::vector attnMaskVec = {1, 1, 0}; + + std::span modelOutput(modelOutputVec); + std::span attnMask(attnMaskVec); + + const auto result = meanPooling(modelOutput, attnMask); + const std::vector expected = {2.0F, 3.0F}; + expect_vectors_eq(result, expected); +} + +TEST(MeanPoolingTests, MeanPoolingWithZeroAttentionMask) { + const std::vector modelOutputVec = {1.0F, 2.0F, 3.0F, + 4.0F, 5.0F, 6.0F}; + const std::vector attnMaskVec = {0, 0, 0}; + + std::span modelOutput(modelOutputVec); + std::span attnMask(attnMaskVec); + + const auto result = meanPooling(modelOutput, attnMask); + const std::vector expected = {0.0F, 0.0F}; + expect_vectors_eq(result, expected); +} + +TEST(MeanPoolingTests, InvalidDimensionSize) { + const std::vector modelOutput = {1.0F, 2.0F, 3.0F, 4.0F}; + const std::vector attnMask = {1, 1, 1}; + + EXPECT_THROW( + { meanPooling(modelOutput, attnMask); }, + std::invalid_argument); +} + +TEST(MeanPoolingTests, EmptyAttentionMask) { + const std::vector modelOutput = {1.0F, 2.0F, 3.0F, 4.0F}; + const std::vector attnMask = {}; + + EXPECT_THROW( + { meanPooling(modelOutput, attnMask); }, + std::invalid_argument); +} + +} // namespace rnexecutorch::numerical diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/README.md b/packages/react-native-executorch/common/rnexecutorch/tests/README.md index 53a2d67d0..9ce6f5b36 100644 --- a/packages/react-native-executorch/common/rnexecutorch/tests/README.md +++ b/packages/react-native-executorch/common/rnexecutorch/tests/README.md @@ -2,19 +2,36 @@ This guide provide information on how functions are tested, how to install all needed dependencies and how to run tests. ### Used Tools -To test the native code we use [`googletest`](https://github.com/google/googletest). It is a flexible tool for creating unit tests. +To test the native code we use [`googletest`](https://github.com/google/googletest). It's a flexible tool for creating unit tests. ### Installation -The easiest way to install `googletest` is following: -* Clone repo locally and checkout on newest release: - `git clone git@github.com:google/googletest.git && cd googletest && git switch --detach v1.17.0` -* Build library files: - * `mkdir build && cd build` - * `cmake ..` - * `make` -* Add `/usr/local/include` and `/usr/local/lib` to your path if not already there. +The googletest is already in repo in `react-native-executorch/third-party/googletest`. Firstly, you need to fetch googletest locally, run from root directory of project: +* `git submodule update --init --recursive third-party/googletest` -### Usage -To run tests please use: -* `run_test.sh` if you want to run one specific test, e.g. `run_test.sh LogTest.cpp`. -* `run_all_tests.sh` if you want to run all tests in the `tests` directory. \ No newline at end of file +### Build Test Files +To run tests navigate tests directory namely: +* `cd packages/react-native-executorch/common/rnexecutorch/tests` +and then type: +* `mkdir build && cd build` +* `cmake ..` +* `make` + +### Run Tests +To run tests use the following command in `packages/react-native-executorch/common/rnexecutorch/tests/build`: +* `ctest --verbose` + +Every time you updated the source code, you need to recompile the test files using: `cmake .. && make`. + +### How to add a new test +To add new test you need to: +* Place `*.cpp` file with tests using googletest in this directory. +* In `CMakeLists.txt`, add all executables and link them with googletest, e.g.: + ``` + set(SOURCE_FILES ${CMAKE_SOURCE_DIR}/../data_processing/Numerical.cpp) + add_executable(NumericalTests tests/NumericalTest.cpp ${SOURCE_FILES}) + target_link_libraries(NumericalTests gtest gtest_main) + ``` +* Add test execution, e.g.: + ``` + add_test(NAME NumericalTests COMMAND NumericalTests) + ``` diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/run_all_tests.sh b/packages/react-native-executorch/common/rnexecutorch/tests/run_all_tests.sh deleted file mode 100755 index a52db7dc9..000000000 --- a/packages/react-native-executorch/common/rnexecutorch/tests/run_all_tests.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# Get the directory where the script is located -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" - -# Loop through all .cpp files in that directory -for file in "$SCRIPT_DIR"/*.cpp -do - if [ -f "$file" ]; then - file_name=$(basename "$file") - echo "Processing $file_name" - "$SCRIPT_DIR"/run_test.sh "$file" - fi -done diff --git a/packages/react-native-executorch/common/rnexecutorch/tests/run_test.sh b/packages/react-native-executorch/common/rnexecutorch/tests/run_test.sh deleted file mode 100755 index f8ab22f3e..000000000 --- a/packages/react-native-executorch/common/rnexecutorch/tests/run_test.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Check if a file name is provided -if [ "$#" -ne 1 ]; then - echo "Usage: $0 " - exit 1 -fi - -file_name="$1" - -# Compile the file with specified libraries -g++ -std=c++20 -o test_executable "$file_name" -lgtest -lgtest_main -lpthread - -# Execute the binary -./test_executable - -# Remove the executable -rm -f test_executable diff --git a/third-party/googletest b/third-party/googletest new file mode 160000 index 000000000..eb2d85edd --- /dev/null +++ b/third-party/googletest @@ -0,0 +1 @@ +Subproject commit eb2d85edd0bff7a712b6aff147cd9f789f0d7d0b