Skip to content
Snippets Groups Projects
Commit 00c0bdeb authored by Lucas RAKOTOARIVONY's avatar Lucas RAKOTOARIVONY
Browse files

Merge branch aidge_learning:main into main

parents 3309f38d 0ce89002
No related branches found
No related tags found
No related merge requests found
Showing
with 728 additions and 135 deletions
# C++ Build # C++ Build
build*/ build*/
install*/ install*/
include/aidge/learning_version.h
# VSCode # VSCode
.vscode .vscode
......
################################################################################ ###############################################################################
# Pre-configured CI/CD for your Aidge module. # Aidge Continuous Integration and Deployment #
# # #
# Three stages are already pre-configured to run on Eclipse Aidge CI: ###############################################################################
# - build: ubuntu_cpp, ubuntu_python and windows_cpp;
# - test: ubuntu_cpp, ubuntu_python and windows_cpp;
# - coverage: ubuntu_cpp and ubuntu_python.
#
# If your project is pure C++ or pure Python, you can remove the "_python" or
# "_cpp" jobs respectively.
# "ubuntu" jobs require an Ubuntu runner with a docker executor with tag
# "docker".
# "windows" jobs require a Windows runner with a docker-windows executor with
# tag "windows".
#
# You can change the docker images in the YML scripts directly. The default
# images are:
# - nvidia/cuda:12.2.0-devel-ubuntu22.04 for Ubuntu jobs;
# - buildtools for Windows jobs, built on top of
# mcr.microsoft.com/windows/servercore:ltsc2022 with Microsoft Visual Studio
# 2022 BuildTools installed.
#
# See Aidge project wiki for more details on how to setup your own docker images
# and Gitlab runners.
################################################################################
stages: stages:
# Analyse code
- static_analysis - static_analysis
# Build Aidge
- build - build
# Unit test stage
- test - test
# Code coverage
- coverage - coverage
- release
- deploy
include: include:
- local: '/.gitlab/ci/_global.gitlab-ci.yml' - project: 'eclipse/aidge/gitlab_shared_files'
- local: '/.gitlab/ci/build.gitlab-ci.yml' ref: 'main'
- local: '/.gitlab/ci/test.gitlab-ci.yml' file:
- local: '/.gitlab/ci/coverage.gitlab-ci.yml' # choose which jobs to run by including the corresponding files.
- '.gitlab/ci/ubuntu_cpp.gitlab-ci.yml'
- '.gitlab/ci/ubuntu_python.gitlab-ci.yml'
- '.gitlab/ci/release/cibuildwheel_ubuntu.gitlab-ci.yml'
- '.gitlab/ci/windows_cpp.gitlab-ci.yml'
- '.gitlab/ci/windows_python.gitlab-ci.yml'
- '.gitlab/ci/release/cibuildwheel_windows.gitlab-ci.yml'
$ErrorActionPreference = "Stop"
# Retrieve and clean the dependencies string from the environment variable
$AIDGE_DEPENDENCIES = $env:AIDGE_DEPENDENCIES -split ' '
Write-Host "Aidge dependencies : $AIDGE_DEPENDENCIES"
if ( $($AIDGE_DEPENDENCIES.Length) -eq 0) {
Write-Host "- No dependencies provided for current repsitory"
New-Item -ItemType Directory -Force -Path ".\build" | Out-Null
Remove-Item -Path ".\build\*" -Recurse -Force
} else {
Write-Host "Retrieving given dependencies to build current package : $AIDGE_DEPENDENCIES"
foreach ($dep in $($AIDGE_DEPENDENCIES -split " ")) {
Write-Host "Retrieving : $dep"
$curr_loc=$(Get-Location)
Set-Location ../$dep
Get-Location
Get-ChildItem .
New-Item -Path ".\build" -ItemType Directory -Force | Out-Null
Get-ChildItem -Path ".\build" -File | Remove-Item -Force
python -m pip install . -v
Set-Location $curr_loc
}
}
#!/bin/bash
set -e
if [[ "$1" == "" ]]; then
echo "build aidge deps in cibuildwheel container before building wheel."
echo "search path defines where the dependencies will be searched."
echo "Hint : In wheel containers, files are mounted on /host by default."
echo "\nusage : ./cibuildwheel_build_deps_before_build_wheel.sh $search_path"
fi
set -x
if [[ $AIDGE_DEPENDENCIES == "" ]]; then # case for aidge_ core
mkdir -p build # creating build if its not already there to hold the build of cpp files
rm -rf build/* # build from scratch
else
for repo in $AIDGE_DEPENDENCIES ; do # case for other projects
search_path=$1
REPO_PATH=$(find $search_path ! -writable -prune -o -type d \
-name "$repo" \
-not -path "*/install/*" \
-not -path "*/.git/*" \
-not -path "*/miniconda/*" \
-not -path "*/conda/*" \
-not -path "*/.local/*" \
-not -path "*/lib/*" \
-not -path "*/$repo/$repo/*" \
-not -path "*/proc/*" \
-print -quit)
if [[ -z "$REPO_PATH" ]]; then
echo "ERROR : dependency $repo not found in search_path \"$search_path\". ABORTING."
exit -1
fi
cd $REPO_PATH
mkdir -p build # creating build if its not already there to hold the build of cpp files
rm -rf build/* # build from scratch
pip install . -v
cd -
done
fi
set +x
set +e
# Version 0.2.3 (January 31, 2025)
# Version 0.2.2 (December 12, 2024)
# Version 0.1.1 (May 14, 2024) # Version 0.1.1 (May 14, 2024)
* Fix loss function to return Tensor with gradient * Fix loss function to return Tensor with gradient
......
cmake_minimum_required(VERSION 3.15) cmake_minimum_required(VERSION 3.18)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
file(STRINGS "${CMAKE_SOURCE_DIR}/version.txt" version)
# Parse version.txt to retrieve Major, Minor and Path
string(REGEX MATCH "([0-9]+\\.[0-9]+\\.[0-9]+)" _ MATCHES ${version})
set(PROJECT_VERSION_MAJOR ${CMAKE_MATCH_1})
set(PROJECT_VERSION_MINOR ${CMAKE_MATCH_2})
set(PROJECT_VERSION_PATCH ${CMAKE_MATCH_3})
# Retrieve latest git commit
execute_process(
COMMAND git rev-parse --short HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
file(READ "${CMAKE_SOURCE_DIR}/version.txt" version) project(aidge_learning
file(READ "${CMAKE_SOURCE_DIR}/project_name.txt" project) VERSION ${version}
DESCRIPTION "Functions and alogrithms to train models in the AIDGE framework"
LANGUAGES CXX)
message(STATUS "Project name: ${project}") message(STATUS "Project name: ${CMAKE_PROJECT_NAME}")
message(STATUS "Project version: ${version}") message(STATUS "Project version: ${version}")
message(STATUS "Latest git commit: ${GIT_COMMIT_HASH}")
# Note : project name is {project} and python module name is also {project} # Note : project name is {project} and python module name is also {project}
set(module_name _${project}) # target name set(module_name _${CMAKE_PROJECT_NAME}) # target name
project(${project})
set(CXX_STANDARD 14)
############################################## ##############################################
# Define options # Define options
option(PYBIND "python binding" ON) option(PYBIND "python binding" OFF)
option(WERROR "Warning as error" OFF) option(WERROR "Warning as error" OFF)
option(TEST "Enable tests" ON) option(TEST "Enable tests" ON)
option(COVERAGE "Enable coverage" OFF) option(COVERAGE "Enable coverage" OFF)
option(ENABLE_ASAN "Enable ASan (AddressSanitizer) for runtime analysis of memory use (over/underflow, memory leak, ...)" OFF)
############################################## ##############################################
# Import utils CMakeLists # Import utils CMakeLists
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
include(PybindModuleCreation)
if(CMAKE_COMPILER_IS_GNUCXX AND COVERAGE) if(CMAKE_COMPILER_IS_GNUCXX AND COVERAGE)
Include(CodeCoverage) Include(CodeCoverage)
endif() endif()
############################################## ##############################################
# Find system dependencies # FIND Dependencies
if(NOT $ENV{AIDGE_INSTALL} STREQUAL "")
set(CMAKE_INSTALL_PREFIX $ENV{AIDGE_INSTALL})
list(APPEND CMAKE_PREFIX_PATH $ENV{AIDGE_INSTALL})
message(WARNING "Env var AIDGE_INSTALL detected : $ENV{AIDGE_INSTALL}. Set CMAKE_INSTALL_PREFIX to AIDGE_INSTALL & added to CMAKE_PREFIX_PATH"
"\n\tCMAKE_INSTALL_PREFIX = ${CMAKE_INSTALL_PREFIX}"
"\n\tCMAKE_PREFIX_PATH = ${CMAKE_PREFIX_PATH}")
endif()
find_package(aidge_core REQUIRED) find_package(aidge_core REQUIRED)
find_package(aidge_backend_cpu REQUIRED)
############################################## ##############################################
# Create target and set properties # Create target and set properties
...@@ -43,8 +70,7 @@ file(GLOB_RECURSE inc_files "include/*.hpp") ...@@ -43,8 +70,7 @@ file(GLOB_RECURSE inc_files "include/*.hpp")
add_library(${module_name} ${src_files} ${inc_files}) add_library(${module_name} ${src_files} ${inc_files})
target_link_libraries(${module_name} target_link_libraries(${module_name}
PUBLIC PUBLIC
_aidge_core # _ is added because we link the target not the project _aidge_core # _ is added because we link the exported target and not the project
_aidge_backend_cpu
) )
#Set target properties #Set target properties
...@@ -59,14 +85,15 @@ target_include_directories(${module_name} ...@@ -59,14 +85,15 @@ target_include_directories(${module_name}
# PYTHON BINDING # PYTHON BINDING
if (PYBIND) if (PYBIND)
generate_python_binding(${project} ${module_name}) include(PybindModuleCreation)
generate_python_binding(${CMAKE_PROJECT_NAME} ${module_name})
# Handles Python + pybind11 headers dependencies # Handles Python + pybind11 headers dependencies
target_link_libraries(${module_name} target_link_libraries(${module_name}
PUBLIC PUBLIC
pybind11::pybind11 pybind11::pybind11
PRIVATE PRIVATE
Python::Python Python::Module
) )
endif() endif()
...@@ -84,25 +111,29 @@ if(CMAKE_COMPILER_IS_GNUCXX AND COVERAGE) ...@@ -84,25 +111,29 @@ if(CMAKE_COMPILER_IS_GNUCXX AND COVERAGE)
append_coverage_compiler_flags() append_coverage_compiler_flags()
endif() endif()
message(STATUS "Creating ${CMAKE_CURRENT_SOURCE_DIR}/include/aidge/learning_version.h")
# Generate version.h file from config file version.h.in
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/include/aidge/version.h.in"
"${CMAKE_CURRENT_SOURCE_DIR}/include/aidge/learning_version.h"
)
############################################## ##############################################
# Installation instructions # Installation instructions
include(GNUInstallDirs) include(GNUInstallDirs)
set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/${project}) set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/${CMAKE_PROJECT_NAME})
install(TARGETS ${module_name} EXPORT ${project}-targets install(TARGETS ${module_name} EXPORT ${CMAKE_PROJECT_NAME}-targets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
# INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
) )
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
#Export the targets to a script #Export the targets to a script
install(EXPORT ${CMAKE_PROJECT_NAME}-targets
install(EXPORT ${project}-targets FILE "${CMAKE_PROJECT_NAME}-targets.cmake"
FILE "${project}-targets.cmake"
DESTINATION ${INSTALL_CONFIGDIR} DESTINATION ${INSTALL_CONFIGDIR}
COMPONENT ${module_name} COMPONENT ${module_name}
) )
...@@ -111,32 +142,34 @@ install(EXPORT ${project}-targets ...@@ -111,32 +142,34 @@ install(EXPORT ${project}-targets
include(CMakePackageConfigHelpers) include(CMakePackageConfigHelpers)
write_basic_package_version_file( write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/${project}-config-version.cmake" "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}-config-version.cmake"
VERSION ${version} VERSION ${version}
COMPATIBILITY AnyNewerVersion COMPATIBILITY AnyNewerVersion
) )
configure_package_config_file("${project}-config.cmake.in" configure_package_config_file("${CMAKE_PROJECT_NAME}-config.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/${project}-config.cmake" "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}-config.cmake"
INSTALL_DESTINATION ${INSTALL_CONFIGDIR} INSTALL_DESTINATION ${INSTALL_CONFIGDIR}
) )
#Install the config, configversion and custom find modules #Install the config, configversion and custom find modules
install(FILES install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/${project}-config.cmake" "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}-config.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/${project}-config-version.cmake" "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}-config-version.cmake"
DESTINATION ${INSTALL_CONFIGDIR} DESTINATION ${INSTALL_CONFIGDIR}
) )
############################################## ##############################################
## Exporting from the build tree ## Exporting from the build tree
export(EXPORT ${project}-targets export(EXPORT ${CMAKE_PROJECT_NAME}-targets
FILE "${CMAKE_CURRENT_BINARY_DIR}/${project}-targets.cmake") FILE "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}-targets.cmake")
############################################## ##############################################
## Add test ## Add test
if(TEST) if(TEST)
if(PYBIND)
message(FATAL_ERROR "PYBIND and TEST are both enabled. But cannot compile with catch_2.\nChoose between pybind and Catch2 for compilation.")
endif()
enable_testing() enable_testing()
add_subdirectory(unit_tests) add_subdirectory(unit_tests)
endif() endif()
include README.md LICENCE
recursive-include aidge_learning *.py
recursive-exclude aidge_learning/unit_tests *.py
recursive-include include *.hpp
recursive-include src *.cpp
recursive-include python_binding *.cpp
include CMakeLists.txt
...@@ -6,16 +6,25 @@ In this module, you can find functions and classes to train your models: ...@@ -6,16 +6,25 @@ In this module, you can find functions and classes to train your models:
- ``LRScheduler`` (ConstantLR, StepLR) - ``LRScheduler`` (ConstantLR, StepLR)
- loss functions (MSE) - loss functions (MSE)
## Dependencies ### Dependencies
- `GCC`
- aidge_core - `Make`/`Ninja`
- aidge_backend_cpu - `CMake`
- `Python` (optional, if you have no intend to use this library in python with pybind)
## Python installation
#### Aidge dependencies
```python - `aidge_core`
- `aidge_backend_cpu`
## Pip installation
``` bash
pip install . -v pip install . -v
``` ```
> **TIPS :** Use environment variables to change compilation options :
> - `AIDGE_INSTALL` : to set the installation folder. Defaults to /usr/local/lib. :warning: This path must be identical to aidge_core install path.
> - `AIDGE_PYTHON_BUILD_TYPE` : to set the compilation mode to **Debug** or **Release**
> - `AIDGE_BUILD_GEN` : to set the build backend with
## C++ installation ## C++ installation
......
@PACKAGE_INIT@ @PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(aidge_core)
include(${CMAKE_CURRENT_LIST_DIR}/aidge_learning-config-version.cmake) include(${CMAKE_CURRENT_LIST_DIR}/aidge_learning-config-version.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/aidge_learning-targets.cmake) include(${CMAKE_CURRENT_LIST_DIR}/aidge_learning-targets.cmake)
...@@ -8,4 +8,5 @@ http://www.eclipse.org/legal/epl-2.0. ...@@ -8,4 +8,5 @@ http://www.eclipse.org/legal/epl-2.0.
SPDX-License-Identifier: EPL-2.0 SPDX-License-Identifier: EPL-2.0
""" """
from aidge_learning.aidge_learning import * import aidge_core
from .aidge_learning import *
...@@ -14,7 +14,8 @@ import aidge_learning ...@@ -14,7 +14,8 @@ import aidge_learning
class test_dummy(unittest.TestCase): class test_dummy(unittest.TestCase):
"""Test tensor binding """
Dummy test to satisfy the CI
""" """
def setUp(self): def setUp(self):
pass pass
......
function(generate_python_binding name target_to_bind) function(generate_python_binding pybind_module_name target_to_bind)
if (PYBIND) add_definitions(-DPYBIND)
add_definitions(-DPYBIND) Include(FetchContent)
Include(FetchContent)
FetchContent_Declare( set(PYBIND_VERSION v2.10.4)
set(PYBIND11_FINDPYTHON ON)
message(STATUS "Retrieving pybind ${PYBIND_VERSION} from git")
FetchContent_Declare(
PyBind11 PyBind11
GIT_REPOSITORY https://github.com/pybind/pybind11.git GIT_REPOSITORY https://github.com/pybind/pybind11.git
GIT_TAG v2.10.4 # or a later release GIT_TAG ${PYBIND_VERSION} # or a later release
) )
# Use the New FindPython mode, recommanded. Requires CMake 3.15+ # Use the New FindPython mode, recommanded. Requires CMake 3.15+
find_package(Python COMPONENTS Interpreter Development) find_package(Python COMPONENTS Interpreter Development.Module)
FetchContent_MakeAvailable(PyBind11) FetchContent_MakeAvailable(PyBind11)
message(STATUS "Creating binding for module ${name}") message(STATUS "Creating binding for module ${pybind_module_name}")
file(GLOB_RECURSE pybind_src_files "python_binding/*.cpp") file(GLOB_RECURSE pybind_src_files "python_binding/*.cpp")
pybind11_add_module(${name} MODULE ${pybind_src_files} "NO_EXTRAS") # NO EXTRA recquired for pip install pybind11_add_module(${pybind_module_name} MODULE ${pybind_src_files} "NO_EXTRAS") # NO EXTRA recquired for pip install
target_include_directories(${name} PUBLIC "python_binding") target_include_directories(${pybind_module_name} PUBLIC "python_binding")
target_link_libraries(${name} PUBLIC ${target_to_bind}) target_link_libraries(${pybind_module_name} PUBLIC ${target_to_bind})
endif()
endfunction() endfunction()
...@@ -100,7 +100,7 @@ public: ...@@ -100,7 +100,7 @@ public:
* @note Else, the learning rate is updated using the provided function. * @note Else, the learning rate is updated using the provided function.
*/ */
constexpr void update() { constexpr void update() {
mLR = (mStep++ < mSwitchStep) ? mLR = (++mStep < mSwitchStep) ?
static_cast<float>(mStep) * mInitialWarmUp : static_cast<float>(mStep) * mInitialWarmUp :
mStepFunc(mLR, mStep); mStepFunc(mLR, mStep);
}; };
......
/********************************************************************************
* Copyright (c) 2024 CEA-List
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
********************************************************************************/
#ifndef AIDGE_LEARNING_METRICS_ACCURACY_H_
#define AIDGE_LEARNING_METRICS_ACCURACY_H_
#include <cstddef> // std::size_t
#include <memory>
#include "aidge/data/Tensor.hpp"
namespace Aidge {
namespace metrics {
/**
* @brief Compute the Accuracy.
* This function returns the loss and set the ``grad()`` of the prediction
* input.
* @param prediction Tensor returned by the Aidge Graph, it is important that
* this tensor is not a copy as overwise the backward function will not have a
* gradient to start.
* @param target Tensor representing the ground truth, it must be one hot encoded.
* @param axis The classes axis.
*/
Tensor Accuracy(std::shared_ptr<Tensor>& prediction,
const std::shared_ptr<Tensor>& target,
std::int32_t axis);
} // namespace Metrics
} // namespace Aidge
#endif /* AIDGE_LEARNING_METRICS_ACCURACY_H_ */
...@@ -35,12 +35,12 @@ class Adam: public Optimizer, public StaticAttributes<AdamAttr, float, float, fl ...@@ -35,12 +35,12 @@ class Adam: public Optimizer, public StaticAttributes<AdamAttr, float, float, fl
private: private:
std::vector<Tensor> mMomentum1; std::vector<Tensor> mMomentum1;
std::vector<Tensor> mMomentum2; std::vector<Tensor> mMomentum2;
Tensor mLR{std::vector<std::size_t>({1})}; Tensor mLR{1.0f};
Tensor mBeta1{std::vector<std::size_t>({1})}; Tensor mBeta1;
Tensor mReversedBeta1{std::vector<std::size_t>({1})}; Tensor mReversedBeta1;
Tensor mBeta2{std::vector<std::size_t>({1})}; Tensor mBeta2;
Tensor mReversedBeta2{std::vector<std::size_t>({1})}; Tensor mReversedBeta2;
Tensor mEpsilon{std::vector<std::size_t>({1})}; Tensor mEpsilon;
public: public:
using Attributes_ = StaticAttributes<AdamAttr, float, float, float>; using Attributes_ = StaticAttributes<AdamAttr, float, float, float>;
...@@ -51,41 +51,33 @@ public: ...@@ -51,41 +51,33 @@ public:
: Optimizer(), : Optimizer(),
Attributes_(attr<AdamAttr::Beta1>(beta1), Attributes_(attr<AdamAttr::Beta1>(beta1),
attr<AdamAttr::Beta2>(beta2), attr<AdamAttr::Beta2>(beta2),
attr<AdamAttr::Epsilon>(epsilon)) attr<AdamAttr::Epsilon>(epsilon)),
mBeta1(beta1),
mReversedBeta1(1.0f - beta1),
mBeta2(beta2),
mReversedBeta2(1.0f - beta2),
mEpsilon(epsilon)
{ {
mBeta1.setBackend("cpu");
mBeta1.set<float>(0, beta1);
mReversedBeta1.setBackend("cpu");
mReversedBeta1.set<float>(0, 1.0f - beta1);
mBeta2.setBackend("cpu");
mBeta2.set<float>(0, beta2);
mReversedBeta2.setBackend("cpu");
mReversedBeta2.set<float>(0, 1.0f - beta2);
mEpsilon.setBackend("cpu");
mEpsilon.set<float>(0, epsilon);
} }
void update() override final { void update() override final {
mLR = Tensor(learningRate());
mLR.setBackend(mParameters[0]->getImpl()->backend()); mLR.setBackend(mParameters[0]->getImpl()->backend());
mLR.set<float>(0, learningRate());
if (mParameters[0]->getImpl()->backend() != mBeta1.getImpl()->backend()) { if (mParameters[0]->getImpl()->backend() != mBeta1.getImpl()->backend()) {
mBeta1.setBackend(mParameters[0]->getImpl()->backend()); mBeta1.setBackend(mParameters[0]->getImpl()->backend());
mReversedBeta1.setBackend(mParameters[0]->getImpl()->backend()); mReversedBeta1.setBackend(mParameters[0]->getImpl()->backend());
mBeta2.setBackend(mParameters[0]->getImpl()->backend()); mBeta2.setBackend(mParameters[0]->getImpl()->backend());
mReversedBeta2.setBackend(mParameters[0]->getImpl()->backend()); mReversedBeta2.setBackend(mParameters[0]->getImpl()->backend());
} }
Tensor alpha{std::vector<std::size_t>({1})}; Tensor alpha = Tensor(learningRate() * std::sqrt(1.0f - std::pow(this->getAttr<AdamAttr::Beta2>(), static_cast<float>(mLRScheduler.step() + 1)))
/ (1.0f - std::pow(this->getAttr<AdamAttr::Beta1>(), static_cast<float>(mLRScheduler.step() + 1))));
alpha.setBackend(mParameters[0]->getImpl()->backend()); alpha.setBackend(mParameters[0]->getImpl()->backend());
alpha.set<float>(0, learningRate() * std::sqrt(1.0f - std::pow(mBeta2.get<float>(0), mLRScheduler.step() + 1))
/ (1.0f - std::pow(mBeta1.get<float>(0), mLRScheduler.step() + 1)));
Tensor epsilon{std::vector<std::size_t>({1})}; Tensor epsilon = Tensor(this->getAttr<AdamAttr::Epsilon>() * std::sqrt(1.0f - std::pow(this->getAttr<AdamAttr::Beta2>(), static_cast<float>(mLRScheduler.step() + 1))));
epsilon.setBackend(mParameters[0]->getImpl()->backend()); epsilon.setBackend(mParameters[0]->getImpl()->backend());
epsilon.set<float>(0, mEpsilon.get<float>(0) * std::sqrt(1.0f - std::pow(mBeta2.get<float>(0), mLRScheduler.step() + 1)));
if (mLRScheduler.step() == 0) { if (mLRScheduler.step() == 0) {
for (std::size_t i = 0; i < mParameters.size(); ++i) { for (std::size_t i = 0; i < mParameters.size(); ++i) {
mMomentum1[i].setBackend(mParameters[i]->getImpl()->backend()); mMomentum1[i].setBackend(mParameters[i]->getImpl()->backend());
...@@ -96,13 +88,13 @@ public: ...@@ -96,13 +88,13 @@ public:
mMomentum2[i].zeros(); mMomentum2[i].zeros();
} }
} }
for (std::size_t i = 0; i < mParameters.size(); ++i) { for (std::size_t i = 0; i < mParameters.size(); ++i) {
mMomentum1[i] = mBeta1 * mMomentum1[i] + mReversedBeta1 * (*mParameters[i]->grad()); mMomentum1[i] = mBeta1 * mMomentum1[i] + mReversedBeta1 * (*mParameters[i]->grad());
mMomentum2[i] = mBeta2 * mMomentum2[i] + mReversedBeta2 * (*mParameters[i]->grad()) * (*mParameters[i]->grad()); mMomentum2[i] = mBeta2 * mMomentum2[i] + mReversedBeta2 * (*mParameters[i]->grad()) * (*mParameters[i]->grad());
*mParameters[i] = *mParameters[i] - alpha * mMomentum1[i] / (mMomentum2[i].sqrt() + epsilon); *mParameters[i] -= alpha * mMomentum1[i] / (mMomentum2[i].sqrt() + epsilon);
} }
mLRScheduler.update(); mLRScheduler.update();
} }
...@@ -112,7 +104,9 @@ public: ...@@ -112,7 +104,9 @@ public:
mMomentum2 = std::vector<Tensor>(parameters.size()); mMomentum2 = std::vector<Tensor>(parameters.size());
for (std::size_t i = 0; i < parameters.size(); ++i) { for (std::size_t i = 0; i < parameters.size(); ++i) {
mMomentum1[i] = Tensor(parameters[i]->dims()); mMomentum1[i] = Tensor(parameters[i]->dims());
mMomentum1[i].setBackend(parameters[i]->getImpl()->backend());
mMomentum2[i] = Tensor(parameters[i]->dims()); mMomentum2[i] = Tensor(parameters[i]->dims());
mMomentum2[i].setBackend(parameters[i]->getImpl()->backend());
} }
} }
}; };
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
#include <vector> #include <vector>
#include "aidge/data/Tensor.hpp" #include "aidge/data/Tensor.hpp"
#include "aidge/backend/cpu/data/TensorImpl.hpp"
#include "aidge/learning/optimizer/Optimizer.hpp" #include "aidge/learning/optimizer/Optimizer.hpp"
#include "aidge/utils/StaticAttributes.hpp" #include "aidge/utils/StaticAttributes.hpp"
#include "aidge/utils/Registrar.hpp" #include "aidge/utils/Registrar.hpp"
...@@ -46,29 +47,23 @@ public: ...@@ -46,29 +47,23 @@ public:
Attributes_(attr<SGDAttr::Momentum>(momentum), Attributes_(attr<SGDAttr::Momentum>(momentum),
attr<SGDAttr::Dampening>(dampening)) attr<SGDAttr::Dampening>(dampening))
{ {
mMomentum.setBackend("cpu"); mMomentum = Tensor(momentum);
mMomentum.set<float>(0, momentum); mReversedDampening = Tensor(1.0f - dampening);
mReversedDampening.setBackend("cpu");
mReversedDampening.set<float>(0, 1.0f - dampening);
} }
void update() override final { void update() override final {
mLR = Tensor(learningRate());
mLR.setBackend(mParameters[0]->getImpl()->backend()); mLR.setBackend(mParameters[0]->getImpl()->backend());
mLR.set<float>(0, learningRate());
if (mParameters[0]->getImpl()->backend() != mMomentum.getImpl()->backend()) {
mMomentum.setBackend(mParameters[0]->getImpl()->backend());
mReversedDampening.setBackend(mParameters[0]->getImpl()->backend());
}
if (mLRScheduler.step() == 0) { if (mLRScheduler.step() == 0) {
for (std::size_t i = 0; i < mParameters.size(); ++i) { for (std::size_t i = 0; i < mParameters.size(); ++i) {
mGradientInertia[i] = mParameters[i]->grad()->clone(); mGradientInertia[i] = mParameters[i]->grad()->clone();
*mParameters[i] = *mParameters[i] - mLR*mGradientInertia[i]; *mParameters[i] -= mLR*mGradientInertia[i];
} }
} else { } else {
for (std::size_t i = 0; i < mParameters.size(); ++i) { for (std::size_t i = 0; i < mParameters.size(); ++i) {
mGradientInertia[i] = mMomentum*mGradientInertia[i] + mReversedDampening*(*mParameters[i]->grad()); mGradientInertia[i] = mMomentum*mGradientInertia[i] + mReversedDampening*(*mParameters[i]->grad());
*mParameters[i] = *mParameters[i] - mLR*mGradientInertia[i]; *mParameters[i] -= mLR*mGradientInertia[i];
} }
} }
mLRScheduler.update(); mLRScheduler.update();
...@@ -79,6 +74,11 @@ public: ...@@ -79,6 +74,11 @@ public:
mGradientInertia = std::vector<Tensor>(parameters.size()); mGradientInertia = std::vector<Tensor>(parameters.size());
for (std::size_t i = 0; i < parameters.size(); ++i) { for (std::size_t i = 0; i < parameters.size(); ++i) {
mGradientInertia[i] = Tensor(parameters[i]->dims()); mGradientInertia[i] = Tensor(parameters[i]->dims());
mGradientInertia[i].setBackend(parameters[i]->backend());
}
if (parameters.size() > 0) {
mReversedDampening.setBackend(mParameters[0]->getImpl()->backend());
mMomentum.setBackend(mParameters[0]->getImpl()->backend());
} }
} }
}; };
......
#ifndef AIDGE_UTILS_SYS_INFO_LEARNING_VERSION_INFO_H
#define AIDGE_UTILS_SYS_INFO_LEARNING_VERSION_INFO_H
#include "aidge/utils/Log.hpp"
#include "aidge/learning_version.h"
namespace Aidge {
constexpr inline const char * getLearningProjectVersion(){
return PROJECT_VERSION;
}
constexpr inline const char * getLearningGitHash(){
return PROJECT_GIT_HASH;
}
void showLearningVersion() {
Log::info("Aidge Learning: {} ({}), {} {}", getLearningProjectVersion(), getLearningGitHash(), __DATE__, __TIME__);
// Compiler version
#if defined(__clang__)
/* Clang/LLVM. ---------------------------------------------- */
Log::info("Clang/LLVM compiler version: {}.{}.{}\n", __clang_major__ , __clang_minor__, __clang_patchlevel__);
#elif defined(__ICC) || defined(__INTEL_COMPILER)
/* Intel ICC/ICPC. ------------------------------------------ */
Log::info("Intel ICC/ICPC compiler version: {}\n", __INTEL_COMPILER);
#elif defined(__GNUC__) || defined(__GNUG__)
/* GNU GCC/G++. --------------------------------------------- */
Log::info("GNU GCC/G++ compiler version: {}.{}.{}", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#elif defined(_MSC_VER)
/* Microsoft Visual Studio. --------------------------------- */
Log::info("Microsoft Visual Studio compiler version: {}\n", _MSC_VER);
#else
Log::info("Unknown compiler\n");
#endif
}
} // namespace Aidge
#endif // AIDGE_UTILS_SYS_INFO_LEARNING_VERSION_INFO_H
#ifndef VERSION_H
#define VERSION_H
namespace Aidge {
static constexpr const int PROJECT_VERSION_MAJOR = @PROJECT_VERSION_MAJOR@;
static constexpr const int PROJECT_VERSION_MINOR = @PROJECT_VERSION_MINOR@;
static constexpr const int PROJECT_VERSION_PATCH = @PROJECT_VERSION_PATCH@;
static constexpr const char * PROJECT_VERSION = "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@";
static constexpr const char * PROJECT_GIT_HASH = "@GIT_COMMIT_HASH@";
}
#endif // VERSION_H
[project]
name = "aidge_learning"
description="Functions and alogrithms to train models in the AIDGE framework"
dependencies = []
requires-python = ">= 3.8"
readme = "README.md"
license = { file = "LICENSE" }
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)",
"Programming Language :: C++",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Software Development"
]
dynamic = ["version"] # defined in pbr
[project.urls]
Homepage = "https://www.deepgreen.ai/en/platform"
Documentation = "https://eclipse-aidge.readthedocs.io/en/latest/"
Repository = "https://gitlab.eclipse.org/eclipse/aidge/aidge_learning"
Issues = "https://gitlab.eclipse.org/eclipse/aidge/aidge_learning/-/issues/"
Changelog = "https://gitlab.eclipse.org/eclipse/aidge/aidge_learning/-/releases"
[build-system]
requires = [
"setuptools>=64",
"cmake>=3.15.3.post1",
"toml",
"pbr"
]
build-backend = "setuptools.build_meta"
#####################################################
# SETUPTOOLS
[tool.setuptools]
[tool.setuptools.packages.find]
where = ["."] # list of folders that contain the packages (["."] by default)
include = ["aidge_learning*"] # package names should match these glob patterns (["*"] by default)
exclude = ["aidge_learning.unit_tests*"] # exclude packages matching these glob patterns (empty by default)
namespaces = false # to disable scanning PEP 420 namespaces (true by default)
#####################################################
# CIBUILDWHEEL
[tool.cibuildwheel]
build-frontend = "build"
test-requires = "pytest"
test-command = "pytest {project}/aidge_learning/unit_tests"
# Uncomment to run cibuildwheel locally on selected distros
# build=[
# "cp38-manylinux_x86_64",
# "cp39-manylinux_x86_64",
# "cp310-manylinux_x86_64"
# ]
### AIDGE DEPENDENCIES DECLARATION
[tool.cibuildwheel.environment]
AIDGE_DEPENDENCIES = "aidge_core aidge_backend_cpu" # format => "dep_1 dep_2 ... dep_n"
AIDGE_INSTALL="/AIDGE_INSTALL_CIBUILDWHEEL"
[tool.cibuildwheel.linux]
before-build = [
"bash .gitlab/ci/cibuildwheel_build_deps_before_build_wheel.sh /host"
]
before-test = [
"pip install aidge-core",
"pip install aidge-backend-cpu",
]
[tool.cibuildwheel.windows]
before-build = [
"powershell -File .\\.gitlab\\ci\\cibuildwheel_build_deps_before_build_wheel.ps1"
]
before-test = [
"pip install aidge-core",
"pip install aidge-backend-cpu",
]
#####################################################
# PYLINT
[tool.pylint.main]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list = ["aidge_core", "aidge_backend_cpu", "torch", "tensorflow"]
# Files or directories to be skipped. They should be base names, not paths.
ignore = ["CVS"]
# List of module names for which member attributes should not be checked (useful
# for modules/projects where namespaces are manipulated during runtime and thus
# existing member attributes cannot be deduced by static analysis). It supports
# qualified module names, as well as Unix pattern matching.
ignored-modules = ["aidge_core", "aidge_backend_cpu"]
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs = 1
# Control the amount of potential inferred values when inferring a single object.
# This can help the performance when dealing with large functions or complex,
# nested conditions.
limit-inference-results = 100
# Pickle collected data for later comparisons.
persistent = true
# Minimum Python version to use for version dependent checks. Will default to the
# version used to run pylint.
py-version = "3.8"
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode = true
[tool.pylint.basic]
# Naming style matching correct argument names.
argument-naming-style = "snake_case"
# Naming style matching correct attribute names.
attr-naming-style = "snake_case"
# Bad variable names which should always be refused, separated by a comma.
bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"]
# Naming style matching correct class attribute names.
class-attribute-naming-style = "any"
# Naming style matching correct class constant names.
class-const-naming-style = "UPPER_CASE"
# Naming style matching correct class names.
class-naming-style = "PascalCase"
# Naming style matching correct constant names.
const-naming-style = "UPPER_CASE"
# Minimum line length for functions/classes that require docstrings, shorter ones
# are exempt.
docstring-min-length = -1
# Naming style matching correct function names.
function-naming-style = "snake_case"
# Good variable names which should always be accepted, separated by a comma.
good-names = ["i", "j", "k", "ex", "Run", "_"]
# Naming style matching correct inline iteration names.
inlinevar-naming-style = "any"
# Naming style matching correct method names.
method-naming-style = "snake_case"
# Naming style matching correct module names.
module-naming-style = "snake_case"
# Regular expression which should only match function or class names that do not
# require a docstring.
no-docstring-rgx = "^_"
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties. These
# decorators are taken in consideration only for invalid-name.
property-classes = ["abc.abstractproperty"]
# Naming style matching correct variable names.
variable-naming-style = "snake_case"
[tool.pylint.classes]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods = ["__init__", "__new__", "setUp", "__post_init__"]
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make"]
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg = ["cls"]
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg = ["cls"]
[tool.pylint.design]
# Maximum number of arguments for function / method.
max-args = 5
# Maximum number of attributes for a class (see R0902).
max-attributes = 7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr = 5
# Maximum number of branch for function / method body.
max-branches = 12
# Maximum number of locals for function / method body.
max-locals = 15
# Maximum number of parents for a class (see R0901).
max-parents = 7
# Maximum number of public methods for a class (see R0904).
max-public-methods = 20
# Maximum number of return / yield for function / method body.
max-returns = 6
# Maximum number of statements in function / method body.
max-statements = 50
# Minimum number of public methods for a class (see R0903).
min-public-methods = 2
[tool.pylint.exceptions]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions = ["BaseException", "Exception"]
[tool.pylint.format]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
# expected-line-ending-format =
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines = "^\\s*(# )?<?https?://\\S+>?$"
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren = 4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string = " "
# Maximum number of characters on a single line.
max-line-length = 200
# Maximum number of lines in a module.
max-module-lines = 1000
[tool.pylint.imports]
# Force import order to recognize a module as part of a third party library.
known-third-party = ["enchant"]
[tool.pylint.logging]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style = "old"
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules = ["logging"]
[tool.pylint."messages control"]
# Only show warnings with the listed confidence levels. Leave empty to show all.
# Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"]
# Disable the message, report, category or checker with the given id(s). You can
# either give multiple identifiers separated by comma (,) or put this option
# multiple times (only on the command line, not in the configuration file where
# it should appear only once). You can also use "--disable=all" to disable
# everything first and then re-enable specific checks. For example, if you want
# to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable = ["raw-checker-failed", "bad-inline-option", "locally-disabled", "file-ignored", "suppressed-message", "useless-suppression", "deprecated-pragma", "use-symbolic-message-instead", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero", "too-many-locals", "missing-class-docstring", "missing-function-docstring", "too-many-arguments", "protected-access", "too-many-branches", "too-many-ancestors", "wrong-import-order", "wrong-import-position"]
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where it
# should appear only once). See also the "--disable" option for examples.
enable = ["c-extension-no-member"]
[tool.pylint.method_args]
# List of qualified names (i.e., library.method) which require a timeout
# parameter e.g. 'requests.api.get,requests.api.post'
timeout-methods = ["requests.api.delete", "requests.api.get", "requests.api.head", "requests.api.options", "requests.api.patch", "requests.api.post", "requests.api.put", "requests.api.request"]
[tool.pylint.miscellaneous]
# List of note tags to take in consideration, separated by a comma.
notes = ["FIXME", "XXX", "TODO"]
# Regular expression of note tags to take in consideration.
# notes-rgx =
[tool.pylint.refactoring]
# Maximum number of nested blocks for function / method body
max-nested-blocks = 5
# Complete name of functions that never returns. When checking for inconsistent-
# return-statements if a never returning function is called then it will be
# considered as an explicit return statement and no message will be printed.
never-returning-functions = ["sys.exit", "argparse.parse_error"]
# Let 'consider-using-join' be raised when the separator to join on would be non-
# empty (resulting in expected fixes of the type: ``"- " + " - ".join(items)``)
suggest-join-with-non-empty-separator = true
[tool.pylint.reports]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each category,
# as well as 'statement' which is the total number of statements analyzed. This
# score is used by the global evaluation report (RP0004).
evaluation = "10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)"
# Activate the evaluation score.
score = true
[tool.pylint.similarities]
# Comments are removed from the similarity computation
ignore-comments = true
# Docstrings are removed from the similarity computation
ignore-docstrings = true
# Minimum lines number of a similarity.
min-similarity-lines = 4
[tool.pylint.spelling]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions = 4
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives = "fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:"
[tool.pylint.typecheck]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators = ["contextlib.contextmanager"]
# Tells whether missing members accessed in mixin class should be ignored. A
# class is considered mixin if its name matches the mixin-class-rgx option.
# Tells whether to warn about missing members when the owner of the attribute is
# inferred to be None.
ignore-none = true
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference can
# return multiple potential results while evaluating a Python object, but some
# branches might not be evaluated, which results in partial inference. In that
# case, it might be useful to still emit no-member and other checks for the rest
# of the inferred objects.
ignore-on-opaque-inference = true
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins = ["no-member", "not-async-context-manager", "not-context-manager", "attribute-defined-outside-init"]
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes = ["optparse.Values", "thread._local", "_thread._local", "aidge.global_variables", "aidge.cells.abstract_cell.Trainable", "torch", "tensorflow"]
# Show a hint with possible names when a member name was not found. The aspect of
# finding the hint is based on edit distance.
missing-member-hint = true
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance = 1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices = 1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx = ".*[Mm]ixin"
[tool.pylint.variables]
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables = true
# List of strings which can identify a callback function by name. A callback name
# must start or end with one of those strings.
callbacks = ["cb_", "_cb"]
# A regular expression matching the name of dummy variables (i.e. expected to not
# be used).
dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_"
# Argument names that match this expression will be ignored.
ignored-argument-names = "_.*|^ignored_|^unused_"
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules = ["six.moves", "past.builtins", "future.builtins", "builtins", "io"]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment