diff --git a/.github/workflows/self-hosted.yml b/.github/workflows/self-hosted.yml index b15d036e5..5b6958d31 100644 --- a/.github/workflows/self-hosted.yml +++ b/.github/workflows/self-hosted.yml @@ -90,7 +90,7 @@ jobs: - name: Check for tests results run: | - ! grep -qi "FAILED" ${{ env.test-file }} + ! grep -qE "\[ FAILED \]" ${{ env.test-file }} clean: name: Cleanup workspace diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 6828c3d40..60c9bdd55 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -106,4 +106,4 @@ jobs: - name: Check for tests results run: | - ! grep -qi "FAILED" ${{ env.test-file }} + ! grep -qE "\[ FAILED \]" ${{ env.test-file }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c8cfa4f0..334095e71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -178,6 +178,22 @@ endfunction() add_subdirectory(deps/robin_hood) add_subdirectory(deps/svector) +message(STATUS "Add CLI11 as CLI/ENV parser") +include(FetchContent) + +FetchContent_Declare(CLI11 + GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git + GIT_TAG v2.4.2 +) +FetchContent_MakeAvailable(CLI11) + +message(STATUS "Add nlohmann/json as JSON parser") +FetchContent_Declare(nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 +) +FetchContent_MakeAvailable(nlohmann_json) + if (SPLA_BUILD_OPENCL) if (SPLA_TARGET_MACOSX) message(STATUS "Add standard Apple OpenCL package") @@ -235,6 +251,8 @@ set(SRC_OPENCL) if (SPLA_BUILD_OPENCL) set(SRC_OPENCL + src/opencl/cl_configure.cpp + src/opencl/cl_configure.hpp src/opencl/cl_debug.hpp src/opencl/cl_accelerator.cpp src/opencl/cl_accelerator.hpp @@ -371,11 +389,13 @@ target_compile_definitions(spla PRIVATE SPLA_EXPORTS) target_link_libraries(spla PRIVATE robin_hood) target_link_libraries(spla PRIVATE svector) +target_link_libraries(spla PRIVATE CLI11::CLI11) +target_link_libraries(spla PRIVATE nlohmann_json::nlohmann_json) if (SPLA_BUILD_OPENCL) target_link_libraries(spla PUBLIC OpenCL) target_link_libraries(spla PUBLIC OpenCL::HeadersCpp) - target_compile_definitions(spla PUBLIC CL_TARGET_OPENCL_VERSION=120) + target_compile_definitions(spla PUBLIC CL_TARGET_OPENCL_VERSION=300) list(APPEND SPLA_DEFINES SPLA_BUILD_OPENCL) endif () diff --git a/deps/opencl-headers/tests/pkgconfig/bare/CMakeLists.txt b/deps/opencl-headers/tests/pkgconfig/bare/CMakeLists.txt index f3e8466ee..b7a535f91 100644 --- a/deps/opencl-headers/tests/pkgconfig/bare/CMakeLists.txt +++ b/deps/opencl-headers/tests/pkgconfig/bare/CMakeLists.txt @@ -19,7 +19,7 @@ target_link_libraries(${PROJECT_NAME} target_compile_definitions(${PROJECT_NAME} PRIVATE - CL_TARGET_OPENCL_VERSION=120 + CL_TARGET_OPENCL_VERSION=300 ) include(CTest) diff --git a/deps/opencl-headers/tests/pkgconfig/sdk/CMakeLists.txt b/deps/opencl-headers/tests/pkgconfig/sdk/CMakeLists.txt index aa36d168b..5b0bb6d37 100644 --- a/deps/opencl-headers/tests/pkgconfig/sdk/CMakeLists.txt +++ b/deps/opencl-headers/tests/pkgconfig/sdk/CMakeLists.txt @@ -20,7 +20,7 @@ target_link_libraries(${PROJECT_NAME} target_compile_definitions(${PROJECT_NAME} PRIVATE - CL_TARGET_OPENCL_VERSION=120 + CL_TARGET_OPENCL_VERSION=300 ) include(CTest) diff --git a/deps/opencl-icd-loader/test/pkgconfig/bare/CMakeLists.txt b/deps/opencl-icd-loader/test/pkgconfig/bare/CMakeLists.txt index f58f21e9e..f9120c2bf 100644 --- a/deps/opencl-icd-loader/test/pkgconfig/bare/CMakeLists.txt +++ b/deps/opencl-icd-loader/test/pkgconfig/bare/CMakeLists.txt @@ -25,7 +25,7 @@ target_link_libraries(${PROJECT_NAME} target_compile_definitions(${PROJECT_NAME} PRIVATE - CL_TARGET_OPENCL_VERSION=120 + CL_TARGET_OPENCL_VERSION=300 ) include(CTest) diff --git a/deps/opencl-icd-loader/test/pkgconfig/pkgconfig/CMakeLists.txt b/deps/opencl-icd-loader/test/pkgconfig/pkgconfig/CMakeLists.txt index 436fffdb4..221153e79 100644 --- a/deps/opencl-icd-loader/test/pkgconfig/pkgconfig/CMakeLists.txt +++ b/deps/opencl-icd-loader/test/pkgconfig/pkgconfig/CMakeLists.txt @@ -39,7 +39,7 @@ target_compile_options(${PROJECT_NAME} target_compile_definitions(${PROJECT_NAME} PRIVATE - CL_TARGET_OPENCL_VERSION=120 + CL_TARGET_OPENCL_VERSION=300 ) include(CTest) diff --git a/deps/opencl-icd-loader/test/pkgconfig/sdk/CMakeLists.txt b/deps/opencl-icd-loader/test/pkgconfig/sdk/CMakeLists.txt index 83a52b70b..1b2021e1f 100644 --- a/deps/opencl-icd-loader/test/pkgconfig/sdk/CMakeLists.txt +++ b/deps/opencl-icd-loader/test/pkgconfig/sdk/CMakeLists.txt @@ -24,7 +24,7 @@ target_link_libraries(${PROJECT_NAME} target_compile_definitions(${PROJECT_NAME} PRIVATE - CL_TARGET_OPENCL_VERSION=120 + CL_TARGET_OPENCL_VERSION=300 ) include(CTest) diff --git a/include/spla/library.hpp b/include/spla/library.hpp index a572cb770..88a1c4a08 100644 --- a/include/spla/library.hpp +++ b/include/spla/library.hpp @@ -114,6 +114,21 @@ namespace spla { */ SPLA_API Status set_message_callback(MessageCallback callback); + /** + * @brief Set verbosity level for library logger + * + * Controls which messages are shown: + * 0 - no output + * 1 - errors only + * 2 - errors + warnings + * 3 - all messages (info, warnings, errors) + * + * @param verbosity Verbosity level [0, 3] + * + * @return Function call status + */ + SPLA_API Status set_verbosity(int verbosity); + /** * @brief Sets default library callback to log messages to console * diff --git a/src/core/logger.cpp b/src/core/logger.cpp index 8fab9010d..5530759d9 100644 --- a/src/core/logger.cpp +++ b/src/core/logger.cpp @@ -31,10 +31,38 @@ namespace spla { - void Logger::log_msg(Status status, const std::string& msg, const std::string& file, const std::string& function, int line) { + static int status_to_level(Status status) { + switch (status) { + case Status::Error: + case Status::PlatformNotFound: + case Status::DeviceNotFound: + case Status::InvalidArgument: + case Status::CompilationError: + return 1; + + case Status::NoAcceleration: + case Status::InvalidState: + case Status::NoValue: + case Status::NotImplemented: + return 2; + + case Status::Ok: + default: + return 3; + } + } + + void Logger::log_msg(Status status, const std::string& msg, + const std::string& file, const std::string& function, int line) { + + int level = status_to_level(status); + if (level > m_verbosity) return; + std::lock_guard guard(m_mutex); std::filesystem::path file_path(file); - if (m_callback) m_callback(status, msg, file_path.filename().string(), function, line); + if (m_callback) { + m_callback(status, msg, file_path.filename().string(), function, line); + } } void Logger::set_msg_callback(MessageCallback callback) { @@ -42,4 +70,9 @@ namespace spla { m_callback = std::move(callback); } + void Logger::set_verbosity(int verbosity) { + std::lock_guard guard(m_mutex); + m_verbosity = verbosity; + } + }// namespace spla \ No newline at end of file diff --git a/src/core/logger.hpp b/src/core/logger.hpp index 8afd7a04e..c00259c79 100644 --- a/src/core/logger.hpp +++ b/src/core/logger.hpp @@ -49,10 +49,11 @@ namespace spla { public: void log_msg(Status status, const std::string& msg, const std::string& file, const std::string& function, int line); void set_msg_callback(MessageCallback callback); + void set_verbosity(int verbosity); private: - MessageCallback m_callback; - + MessageCallback m_callback; + int m_verbosity = 3; mutable std::mutex m_mutex; }; diff --git a/src/library.cpp b/src/library.cpp index d0a5494a1..d5acc0c31 100644 --- a/src/library.cpp +++ b/src/library.cpp @@ -143,6 +143,12 @@ namespace spla { return Status::Ok; } + Status Library::set_verbosity(int verbosity) { + m_logger->set_verbosity(verbosity); + LOG_MSG(Status::Ok, "set verbosity: " << verbosity); + return Status::Ok; + } + Status Library::set_default_callback() { auto callback = [](spla::Status status, const std::string& msg, diff --git a/src/opencl/LibsCompare.md b/src/opencl/LibsCompare.md new file mode 100644 index 000000000..14f5ce656 --- /dev/null +++ b/src/opencl/LibsCompare.md @@ -0,0 +1,126 @@ +# Сравнение библиотек для создания механизма конфигурирования +Вся информация взята из репозиториев соответствующих проектов. Дата обращения: 21.04.26. + +## Критерии сравнения + +| Критерий | Метрика | Единицы измерения | +|:---|:---|:---| +| **Зависимости** | наличие | +/- | +| **Источники** | CLI (argv) / env / файлы | +/-, форматы | +| **Стандарт C++** | минимальная версия | C++хх | +| **Поддерживает стандарт иерархии** | Linux/Windows/MacOS | +/- | +| **Способ распространения** | header-only / статическая / динамическая | — | +| **ОС** | Linux / Windows / macOS | +/-, компиляторы | +| **Активность** | среднее число коммитов | коммитов/мес (за последний мес) | +| **Issues** | open, closed/open | n, коэффициент | +| **Сообщество** | GitHub Stars | n | +| **Проходит ли CI** | CI | +/- | + +> ? - не указано в README. + + +## Сравнение парсеров командной строки (CLI) + +| Критерий | CLI11 | cxxopts | +|:---|:---:|:---:| +| Зависимости | - | - | +| **Источники** | | | +|  CLI (argv) | + | + | +|  env | + | - | +|  Файлы | - | - | +| Стандарт C++ | ≥ C++11 | ≥ C++11 | +| **Поддерживает стандарт иерархии** | | | +|  Linux | + | + | +|  Windows | + | + | +|  MacOS | + | + | +| Способ распространения | header-only | header-only +| **ОС** | | | +|  Linux | + (GCC 4.8+, Clang 3.5+) | + (GCC ≥ 4.9, Clang ≥ 3.1) | +|  Windows | + (MSVC ≥ 2015) | + (MSVC ≥ 2015) | +|  macOS | + (AppleClang 7+) | + (Clang ≥ 3.1 с libc++) | +| **Сообщество** | | | +|  Активность (коммитов/мес) | 4 | 1 | +|  Issues всего | 506 | 307 | +|  Issues closed/open | 9 | 6 | +|  Stars | 4.3k | 4.7k | +| Проходит ли CI | + | + | + | + + +## Парсеры файлов + +| Критерий | nlohmann/json | yaml-cpp | toml++ | toml11 | config-cxx | taocpp/config +|:---|:---:|:---:|:---:|:---:|:---:|:---:| +| Зависимости | - | - | - | - | - | - | +| **Источники** | +|  CLI (argv) | - | - | - | - | - | - | +|  env | - | - | - | - | + | + | +|  Файлы | JSON | YAML | TOML | TOML | JSON, YAML, XML | JSON, JAXN | +| Стандарт C++ | ≥ C++11 | ≥ C++11 | ≥ C++17 | ≥ C++11 | ≥ C++20 | ≥ C++17 | +| **Поддерживает стандарт иерархии** | +|  Linux | + | + | + | + | + | + | +|  Windows | + | + | + | + | + | + | +|  MacOS | + | + | + | + | + | + | +| Способ распространения | header-only | статическая/динамическая¹ | header-only/статическая/динамическая¹ | header-only/статическая/динамическая¹ | header-only | header-only | +| **ОС** | | | +|  Linux | + (GCC 4.8–14.2, Clang 3.4–21.0) | + (GCC, Clang) | + (Clang 8+, GCC 8+) | + (GCC, Clang) | + (GCC 13+, Clang 16+) | + +|  Windows | + (MSVC 2015–2022) | + (MSVC) | + (MSVC VS2019+) | + (MSVC, MinGW) | + (MSVC 143+ (VS 2022)) | + +|  macOS | + (AppleClang 9.1–16.0) | + (Xcode, AppleClang) | + (AppleClang) | + (AppleClang) | + (AppleClang 16+) | + +| **Сообщество** | | | +|  Активность (коммитов/мес) | 2 | 9 | 2 | 4 | 2 | 13 | +|  Issues всего | 3271 | 905 | 193 | 194 | 24 | 5 | +|  Issues closed/open | 68 | 3 | 8 | 5 | 0 open | 0 open | +|  Stars | 49.4k | 15k | 2k | 1.3k | 31 | 194 | +| Проходит ли CI | + | + | + | - | + | ? | + +> ¹ Опционально. + +## Сравнение универсальных инструментов + +| Критерий | Boost.Program_options +|:---|:---:| +| Зависимости | +¹ | +| **Источники** | | | +|  CLI (argv) | + | + | +|  env | + | + | +|  Файлы | INI | +| Стандарт C++ | ≥ C++3 | +| **Поддерживает стандарт иерархии** | | | +|  Linux | + | +|  Windows | + | +|  MacOS | + | +| Способ распространения | статическая/динамическая² | +| **ОС** | | | +|  Linux | + (GCC 5+, Clang 3.6+) | +|  Windows | + (MSVC 2015 (vc140)+) | +|  macOS | + (AppleClang) | +| **Сообщество** | | | +|  Активность (коммитов/мес) | 12 | +|  Issues всего | 400 | +|  Issues closed/open | 1 | +|  Stars | 9.4k | +| Проходит ли CI | + | + | + +> ¹ Список зависимостей: +> 1. Boost.Any +> 2. Boost.Bind +> 3. Boost.Config +> 4. Boost.Core +> 5. Boost.Detail +> 6. Boost.Function +> 7. Boost.Iterator +> 8. Boost.Lexical Cast +> 9. Boost.Smart Ptr +> 10. Boost.ThrowException +> 11. Boost.Tokenizer +> 12. Boost.Type Traits + +> ² Опционально. + + +### Итог: +По результатам сравнения, вероятно, лучшим вариантом будет использование `CLI11` и `nlohmann/json`, так как они, в отличие от `Boost.program_options`, не требуют зависимостей и являются header-only. + + +# Механизм конфигурирования библиотеки SPLA + +Механизм конфигурирования описывается в разделах: [Мануал пользователя](#UserManual) и [Руководство пользователя](#UserGuide) \ No newline at end of file diff --git a/src/opencl/UserGuide.md b/src/opencl/UserGuide.md new file mode 100644 index 000000000..454d441cb --- /dev/null +++ b/src/opencl/UserGuide.md @@ -0,0 +1,282 @@ +# User Guide + +Руководство по установке, проверке и настройке библиотеки spla. + +## Содержание + +1. [Установка](#установка) +2. [Проверка работоспособности](#проверка-работоспособности) +3. [Конфигурационный файл](#конфигурационный-файл) +4. [Настройка](#настройка) +5. [Профили](#профили) +6. [Наследование профилей (`extends`)](#наследование-профилей) + +## Установка + +Инструкции по установке см. в [README](https://github.com/SparseLinearAlgebra/spla#installation). + +## Проверка работоспособности + +После установки выполните команду, которая подтверждает, что библиотека готова к использованию. + +```bash +spla --softcheck +``` +Команда выполняет следующую последовательность действий: + +1. Читает конфигурационный файл, предоставляющийся библиотекой. +2. Инициализирует OpenCL, выбирает платформу и устройство. +4. Запускает простое тестовое задание. +5. Сообщает статус. + +Пример успешного вывода: +```bash +$ spla --softcheck +[spla:check] Starting sanity check... +[spla:check] Loading factory config: /usr/share/spla/spla_conf.json +[spla:check] Initializing OpenCL... +[spla:check] Selected platform: NVIDIA CUDA +[spla:check] Selected device: NVIDIA GeForce RTX 3080 +[spla:check] Running test kernel: 2 + 3 = ... +[spla:check] Result: 5 (expected 5) +[spla:check] SUCCESS - spla works correctly +``` + +## Конфигурационный файл +Конфигурационный файл, предоставляющийся библиотекой, имеет следующую структуру: + +```json +{ + "backend": "gpu", + "if_gpu_unavailable": "use_cpu", + + "platform_index": null, + "device_index": null, + + "queues_count": 1, + "profiling": false, + "allocator_type": "general", + "linear_alloc_size": 0, + + "verbosity": 2, + + "profile": null, + "profiles": {} +} +``` +| Параметр | Тип | Значение по умолчанию | Описание | Допустимые значения | +| :--- | :--- | :--- | :--- | :--- | +| backend | string | "gpu" | На каком устройстве работать | `gpu` - только GPU
`cpu` - только CPU
`any` - любое доступное устройство
`by_index` - выбрать по индексам ниже | +| if_gpu_unavailable | string | "use_cpu" | Что делать, если GPU недоступен
Не применяется при backend: "`cpu`" или backend: "`by_index`" | `use_cpu` - переключиться на CPU
`abort` - вернуть ошибку | +| platform_index | int/null | null | Индекс OpenCL платформы
Используются только при backend: "`by_index`"
При других значениях backend - игнорируются | ≥ 0, null | +| device_index | int/null | null | Индекс устройства внутри платформы
Используются только при backend: "`by_index`"
При других значениях backend - игнорируются | ≥ 0, null | +| queues_count | int | 1 | Число командных очередей | ≥ 1 | +| profiling | bool | false | Профилирование очередей | true / false | +| allocator_type | string | "general" | Тип аллокатора | general, linear | +| linear_alloc_size | int | 0 | Размер линейного аллокатора (байт) | ≥ 0 (игнорируется для `general`) | +| verbosity | int | 2 | Уровень логирования | `0` - нет вывода
`1` - только ошибки
`2` - ошибки, предупреждения
`3` - ошибки, предупреждения, дополнительная информация | +| profile | string/null | null | Выбранный профиль | имя профиля из profiles или `null` | +| profiles | object | {} | Словарь профилей | { "name": { ... override ... } } | +| profiles.< name >.extends | array of string | [] | Список профилей-родителей | имена профилей из profiles | + +## Настройка + +Если параметры по умолчанию вас не устраивают, вы можете настроить библиотеку. Способы конфигурирования перечислены в порядке убывания приоритета: + +1. Аргументы командной строки. +2. Переменные окружения. +3. Пользовательский конфигурационный файл. +4. Системный конфигурационный файл. +5. Конфигурационный файл, поставляющийся библиотекой. + +### Расположение конфигурационных файлов + +Конфигурационный файл с параметрами по умолчанию: +>Linux `/usr/share/spla/spla_conf.json` \ +macOS `/usr/local/share/spla/spla_conf.json` \ +Windows `%ProgramData%\spla\spla_conf.json` + +Системный конфигурационный файл: +>Linux: `/etc/spla/spla_conf.json` \ +macOS: `/Library/Application Support/spla/spla_conf.json` \ +Windows: `%ProgramData%\spla\spla_conf.json` + +Пользовательский конфигурационный файл: +>Linux: `~/.config/spla/spla_conf.json` \ +macOS: `~/Library/Application Support/spla/spla_conf.json` \ +Windows: `%APPDATA%\spla\spla_conf.json` + + +## Профили + +Профили позволяют хранить несколько наборов настроек в одном конфигурационном файле и переключаться между ними при запуске. Это удобно, когда одну и ту же программу нужно запускать в разных режимах - например, на разных GPU или с разным уровнем логирования. + +- В конфигурационном файле описываются общие настройки, которые берутся в качестве базовых параметров. +- Описывается секция `profiles` - именованные наборы параметров. +- При запуске указывается имя профиля. +- Библиотека переопределяет базовые параметры параметрами из выбранного профиля. + +### Пример +```json +{ + "execution": { + "device": 0 + }, + "verbosity": 2, + "queues": 1, + + "profiles": { + "gpu0": { "execution": { "device": 0 } }, + "gpu1": { "execution": { "device": 1 } }, + "gpu2": { "execution": { "device": 2 } }, + "debug": { "verbosity": 3 }, + "bench": { "verbosity": 0 } + } +} +``` +### Запуск +```bash +SPLA_PROFILE=gpu1 ./program +SPLA_PROFILE=debug ./program +SPLA_PROFILE=bench ./program +``` +Или через CLI: +```bash +./program --spla-profile=gpu1 +``` + +### Итоговый конфигурационный файл +```json +{ + "execution": { + "device": 1 + }, + + "queues": 1, + "verbosity": 2, +} +``` + +## Наследование профилей + +Иногда профили пересекаются по смыслу - например, **debug_gpu1** должен включать и режим отладки, и выбор GPU 1. Чтобы не дублировать параметры, профиль может наследовать другие профили через поле `extends`. + +- Если профиль содержит поле `extends`, библиотека сначала применяет указанные в нём профили по порядку. +- Затем к базовым параметрам применяются параметры каждого родительского профиля по очереди: последний в списке переопределяет предыдущие. +- После этого применяются параметры самого выбранного профиля, которые переопределяют всё, что было задано раньше. + +### Пример + +``` json +{ + "execution": { "device": 0 }, + "verbosity": 2, + + "profiles": { + "gpu0": { "execution": { "device": 0 } }, + "gpu1": { "execution": { "device": 1 } }, + "gpu2": { "execution": { "device": 2 } }, + + "debug": { "verbosity": 3, "profiling": true }, + "bench": { "verbosity": 0 }, + + "debug_gpu1": { + "extends": ["debug", "gpu1"] + }, + "bench_gpu2": { + "extends": ["bench", "gpu2"] + } + } +} +``` +### Запуск +```bash +SPLA_PROFILE=debug_gpu1 ./program +SPLA_PROFILE=bench_gpu2 ./program +``` +Или через CLI: +```bash +./program --spla-profile=debug_gpu1 +``` + +### Итоговый конфигурационный файл +```json +{ + "execution": { "device": 1 }, + "verbosity": 3, + "profiling": true +} +``` + +--- +--- + +# Распространенные сценарии использования библиотеки + 1. Быстрый запуск без настройки. + 2. Выбор между CPU и GPU без указания индексов. + 3. Многократный запуск на разных устройствах с разными параметрами. + +## Быстрый запуск без настройки + +Если вы хотите получить работоспособное приложение без настройки - ничего настраивать не нужно. Библиотека использует параметры по умолчанию из [конфигурационного файла](#конфигурационный-файл). Этот сценарий работает одинаково на Linux, macOS и Windows. Если GPU недоступен, библиотека автоматически переключится на CPU, сообщив об этом. \ +Перед первым запуском рекомендуется [выполнить](#проверка-работоспособности) `spla --softcheck`, чтобы убедиться, что всё необходимое для работы spla установлено. + +## Выбор между CPU и GPU без указания индексов + +Если вы хотите управлять типом устройства, но не указывать конкретные индексы OpenCL-платформ и устройств. + +### Управление через backend + +Тип устройства задаётся параметром backend +```json +{ "backend": "gpu" } //Использовать первое доступное GPU + +{ "backend": "cpu" } //Использовать CPU + +{ "backend": "any" } //Использовать первое доступное устройство любого типа +``` + +Индексы `platform_index` и `device_index` при этих значениях игнорируются. Это позволяет запускать программу на GPU или CPU без знания индексов. + +### Поведение при отсутствии GPU + +Если выбран backend: "gpu", но GPU недоступен, поведение определяется параметром `if_gpu_unavailable`: + +```json +{ + "backend": "gpu", + "if_gpu_unavailable": "use_cpu" //Библиотека переключается на CPU и продолжает работу +} + +{ + "backend": "gpu", + "if_gpu_unavailable": "abort" //Библиотека возвращает ошибку и не продолжает работу + //Стоит использовать, если требуется гарантировать выполнение на GPU +} +``` +`if_gpu_unavailable` применяется только при backend: "gpu" и backend: "any". При backend: "cpu" параметр игнорируется. + +## Многократный запуск на разных устройствах с разными параметрами + +Вы регулярно запускаете одну и ту же программу на разных GPU и в разных режимах - например, на GPU 0 и 1, в режиме отладки и в режиме измерения производительности. Вместо того чтобы держать несколько конфигурационных файлов, вы описываете всё в одном. + +Подробнее в главе про [профили](#профили) и [наследование профилей (`extends`)](#наследование-профилей). + +```json +{ + "execution": { "device": 0 }, + "verbosity": 2, + + "profiles": { + "gpu0": { "execution": { "device": 0 } }, + "gpu1": { "execution": { "device": 1 } }, + "gpu2": { "execution": { "device": 2 } }, + + "debug": { "verbosity": 3, "profiling": true }, + "bench": { "verbosity": 0 }, + + "debug_gpu1": { "extends": ["debug", "gpu1"] }, + "bench_gpu2": { "extends": ["bench", "gpu2"] } + } +} +``` \ No newline at end of file diff --git a/src/opencl/UserManual.md b/src/opencl/UserManual.md new file mode 100644 index 000000000..189983e9d --- /dev/null +++ b/src/opencl/UserManual.md @@ -0,0 +1,239 @@ +# МАНУАЛ: + +## НАЗВАНИЕ +spla - это фреймворк для математических вычислений с использованием GPU/ускорения + + +## СИНОПСИС + +```cpp +#include + + +int main(int argc, char* argv[]) { + + configure(argc, argv); + + return 0; +} +``` +Функция `configure(argc, argv)` необходима для обработки аргументов командной строки, переменных окружения и конфигурационных файлов. + + +## ОПИСАНИЕ +**spla** - это библиотека с открытым исходным кодом, предоставляющая примитивы разреженной линейной алгебры (матрицы, векторы, скаляры) для математических вычислений с ускорением на GPU. Библиотека поддерживает широкий спектр операций, включая умножение матрицы на вектор, решение систем линейных уравнений и другие алгоритмы разреженной алгебры. + +Для большинства пользователей важно, на каком ускорителе будет запускаться их проект, поэтому необходима возможность явного указания графического ускорителя, OpenCL платформы, количества очередей команд и других параметров, влияющих на производительность и удобство пользователя. + +Библиотека предоставляет механизмы конфигурирования, показанные в порядке убывания приоритета. + +1. Аргументы командной строки 1. +2. Переменные окружения 2. +3. Пользовательский конфигурационный JSON файл 3. +4. Системный конфигурационный JSON файл 3. + +Системный конфигурационный файл имеет наименьший приоритет. \ +Его настройки переопределяются пользовательским конфигурационным файлом, переменными окружения и аргументами командной строки. + +Пользовательский конфигурационный файл имеет приоритет над системным +конфигурационным файлом. \ +Его настройки переопределяются переменными +окружения и аргументами командной строки. + +Переменные окружения имеют приоритет над системным и пользовательским конфигурационными файлами, но уступают аргументам командной строки. + +Аргументы командной строки имеют наивысший приоритет и переопределяют любые настройки, определенные предыдущими способами. + +Если источника конфигурации с меньшим приоритетом не существует, библиотека игнорирует его и переходит к следующему источнику конфигурации. + +Библиотека требует настройки всех обязательных параметров. Без этого будет генерироваться исключение `IncompleteConfigError`.\ +Чтобы исключение не возникало, пользователь должен явно настроить все параметры любым [способом](#configuration_methods). + +> 1 Описание аргументов командной строки приведены в разделе [ПАРАМЕТРЫ КОМАНДНОЙ СТРОКИ](#options). \ +2 Описание переменнных окружения приведены в разделе [ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ](#env_variables). \ +3 Шаблон конфигурационного файла с описанием приведен [здесь](#config). + +## ПАРАМЕТРЫ КОМАНДНОЙ СТРОКИ +```bash +-sh +--spla-help +``` +Выводит список всех доступных флагов spla с кратким описанием. + +```bash +-sv +--spla-version +``` +Показывает номер версии spla. + +```bash +-ss <путь> +--spla-sconf <путь> +``` +Путь к системному конфигурационному файлу. + +По умолчанию ищет файл в стандартном для данной ОС месте.\ +`/etc/spla/` на Linux. \ +`/Library/Preferences/spla/` на macOS. \ +`%ProgramData%\spla\` на Windows. \ +По умолчанию выбирается первый json файл в соответствующей директории. + +```bash +-su <путь> +--spla-uconf <путь> +``` +Путь к пользовательскому конфигурационному файлу. + +По умолчанию ищет файл в стандартном для данной ОС месте. \ +`~/.config/spla/` на Linux. \ +`~/Library/Application Support/spla/` на macOS. \ +`%APPDATA%\spla\` на Windows. \ +По умолчанию выбирается первый json файл в соответствующей директории. + +```bash +-sp <индекс> +--spla-platform <индекс> +``` +Индекс OpenCL платформы. + +```bash +-sd <индекс> +--spla-device <индекс> +``` +Индекс OpenCL-устройства выбранной платформы (GPU/CPU). + +```bash +-sq <число> +--spla-queues <число> +``` +Количество очередей команд. + +Увеличение числа очередей может повысить производительность +за счет параллельного выполнения независимых задач, но слишком большое значение может снизить производительность из-за накладных расходов. \ +Рекомендуется начинать с 1 и экспериментально подбирать оптимальное значение. + +```bash +-sV <уровень> +--spla-verbosity <уровень> +``` +Уровень детализации отладочной информации, выводимой библиотекой. + +| Уровень | Значение | Описание | +|:-------:|:---------|:---------| +| **0** | OFF | Вывод отключён. Сообщения не выводятся. | +| **1** | ERROR | Только критические ошибки. | +| **2** | WARNING | Ошибки и предупреждения. | +| **3** | INFO | Основные этапы работы библиотеки. | + +Что выводится на каждом уровне: + +1. ERROR — `Error`, `NoAcceleration`, `PlatformNotFound`, `DeviceNotFound`, `InvalidState`, `InvalidArgument`, `CompilationError` + +2. WARNING — добавляет к ERROR: `NoValue`, `NotImplemented` + +3. INFO — добавляет к WARNING: `Ok` - информация об инициализации, выборе платформы/устройства, загрузке конфигурации + +```bash +-sp +--spla-profiling +``` +Профилирование очередей команд OpenCL. + +Bключение профилирования добавляет накладные расходы на каждую +операцию и может замедлить выполнение. + +```bash +-sa <тип> +--spla-allocator <тип> +``` +Тип аллокатора памяти для OpenCL-устройства. \ +Ожидает: lineral | general. + +Выбор аллокатора влияет на производительность операций с памятью. +Линейный аллокатор даёт прирост скорости за счёт последовательного выделения, +но может привести к фрагментации памяти при длительной работе. +Общий аллокатор более надёжен, но медленнее. + +```bash +-sS <размер> +--spla-allocator-size <размер> +``` +Размер линейного аллокатора в байтах. + +Увеличение размера позволяет выделять больше памяти за один раз, но увеличивает потребление ресурсов. \ +Этот параметр необходим только при использовании линейного аллокатора `--spla-allocator linear`. +При использовании общего аллокатора можно не указывать. + +Пример: +```bash +./my_program --spla-uconf ~/my_configs/spla.toml --spla-platform 1 -sd 0 --spla-verbosity 3 -sp true --spla-allocator linear --spla-allocator-size 1048576 +``` + +## ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ +Для всех параметров командной строки (кроме --spla-help, --spla-version) существуют аналоги в виде переменных окружения. + +`SPLA_SYSTEM_CONFIG_PATH` \ +Путь к системному конфигурационному файлу. Аналог параметра --spla-sconf. + +`SPLA_USER_CONFIG_PATH` \ +Путь к пользовательскому конфигурационному файлу. Аналог параметра --spla-uconf. + +`SPLA_OPENCL_PLATFORM` \ +Индекс OpenCL платформы. Аналог параметра --spla-platform. + +`SPLA_OPENCL_DEVICE`\ +Индекс OpenCL-устройства на выбранной платформе. +Аналог параметра --spla-device. + +`SPLA_QUEUES`\ +Количество очередей команд OpenCL. +Аналог параметра --spla-queues. + +`SPLA_VERBOSITY`\ +Уровень детализации отладочной информации. +Аналог параметра --spla-verbosity.\ +Принимает значения от 0 до 3. + +`SPLA_PROFILING`\ +Включает профилирование очередей команд OpenCL. +Аналог параметра --spla-profiling. + +`SPLA_ALLOCATOR`\ +Тип аллокатора памяти для OpenCL-устройства. +Аналог параметра --spla-allocator.\ +Принимает значения: linear, general. + +`SPLA_ALLOCATOR_SIZE`\ +Размер линейного аллокатора в байтах. +Аналог параметра --spla-allocator-size.\ +Имеет смысл только при использовании линейного аллокатора. + + +## КОНФИГУРАЦИОННЫЕ ФАЙЛЫ +Для всех аргументов командной строки (кроме --spla-help, --spla-version, --spla-sconf, --spla-uconf) и всех переменных окружения существуют аналоги в виде параметров конфигурационного файла. + +Формат файла - JSON. + +Типы значений. +1. platform = int +2. device = int +3. queues = int +4. verbosity = int +5. profiling = true | false +6. allocator = linear | general +7. allocator_size = int + +Примеры конфигурационных файлов.\ +Этот формат справедлив как для пользовательского, так и для системного конфигурационного json файла. + +```json +{ + "platform": 1, + "device": 0, + "queues": 4, + "verbosity": 2, + "profiling": false, + "allocator": "linear", + "allocator_size": 1048576 +} +``` \ No newline at end of file diff --git a/src/opencl/cl_accelerator.cpp b/src/opencl/cl_accelerator.cpp index 6a7a0517a..ef3f40538 100644 --- a/src/opencl/cl_accelerator.cpp +++ b/src/opencl/cl_accelerator.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include @@ -39,68 +40,103 @@ namespace spla { CLAccelerator::CLAccelerator() = default; CLAccelerator::~CLAccelerator() = default; - Status CLAccelerator::init() { - m_description = "no platform or device"; + InitResult init_with_configure(int argc, char** argv) { + InitResult result; + result.config_status = configure(argc, argv); + + if (result.config_status == ConfigStatus::HelpRequested || + result.config_status == ConfigStatus::VersionRequested) { + result.status = Status::Ok; + return result; + } - const char* spla_opencl_platform = std::getenv(SPLA_OPENCL_PLATFORM); - const char* spla_opencl_device = std::getenv(SPLA_OPENCL_DEVICE); - int platform_index = (spla_opencl_platform ? std::atoi(spla_opencl_platform) : 0); - int device_index = (spla_opencl_device ? std::atoi(spla_opencl_device) : 0); + if (result.config_status != ConfigStatus::Ok) { + result.status = Status::Error; + return result; + } + + auto* acc = get_acc_cl(); + if (!acc) { + result.status = Status::NoAcceleration; + return result; + } - if (set_platform(platform_index) != Status::Ok) - return Status::PlatformNotFound; + result.status = acc->init(); + return result; + } + + + Status CLAccelerator::init() { + int platform_index = config_final.platform.value(); + int device_index = config_final.device.value(); + int queues_count = config_final.queues.value(); + bool profiling = config_final.profiling.value(); + std::string allocator_type = config_final.allocator.value(); + size_t lin_allocator_size; + if (allocator_type == "linear") lin_allocator_size = config_final.allocator_size.value(); + int verbosity = config_final.verbosity.value(); + + Library::get()->set_verbosity(verbosity); + + LOG_MSG(Status::Ok, "Initializing accelerator..."); + + LOG_MSG(Status::Ok, "Configuration parameters:"); + LOG_MSG(Status::Ok, " platform = " << platform_index); + LOG_MSG(Status::Ok, " device = " << device_index); + LOG_MSG(Status::Ok, " queues = " << queues_count); + LOG_MSG(Status::Ok, " profiling = " << (profiling ? "true" : "false")); + LOG_MSG(Status::Ok, " allocator = " << allocator_type); + if (allocator_type == "linear") { + LOG_MSG(Status::Ok, " alloc_size= " << lin_allocator_size); + } + LOG_MSG(Status::Ok, " verbosity = " << verbosity); - if (set_device(device_index) != Status::Ok) - return Status::DeviceNotFound; + set_platform(platform_index); + set_device(device_index); + set_profiling(profiling); + set_queues_count(queues_count); - if (set_queues_count(1) != Status::Ok) - return Status::Error; + if (allocator_type == "linear") { + set_linear_allocator(lin_allocator_size); + } else { + set_general_allocator(); + } m_cache = std::make_unique(); + LOG_MSG(Status::Ok, "program cache created"); - // Output handy info - LOG_MSG(Status::Ok, "Initialize accelerator: " << get_description()); + LOG_MSG(Status::Ok, "accelerator initialized: " << get_description()); return Status::Ok; } + + Status CLAccelerator::set_platform(int index) { std::vector available_platforms; cl::Platform::get(&available_platforms); - if (available_platforms.empty()) { - LOG_MSG(Status::PlatformNotFound, "no platform to select for OpenCL acceleration, check your system runtime"); - return Status::PlatformNotFound; - } - if (available_platforms.size() <= index) { - LOG_MSG(Status::InvalidArgument, "index out of list of available platforms"); - return Status::InvalidArgument; - } - m_counter_pool.reset(); m_alloc_general.reset(); m_alloc_linear.reset(); - m_alloc_tmp = nullptr; - m_device = cl::Device(); - m_platform = available_platforms[index]; - LOG_MSG(Status::Ok, "select OpenCL platform " << m_platform.getInfo()); + m_alloc_tmp = nullptr; + m_device = cl::Device(); + m_profiling_enabled = false; + + m_platform = available_platforms[index]; + LOG_MSG(Status::Ok, "select OpenCL platform: " + << m_platform.getInfo()); return Status::Ok; } + + Status CLAccelerator::set_device(int index) { std::vector available_devices; - m_platform.getDevices(CL_DEVICE_TYPE_GPU, &available_devices); - - if (available_devices.empty()) { - LOG_MSG(Status::DeviceNotFound, "no device in selected platform, check your OpenCL runtime"); - return Status::DeviceNotFound; - } - if (available_devices.size() <= index) { - LOG_MSG(Status::DeviceNotFound, "index out of list of available devices"); - return Status::DeviceNotFound; - } + m_platform.getDevices(CL_DEVICE_TYPE_ALL, &available_devices); m_device = available_devices[index]; - LOG_MSG(Status::Ok, "select OpenCL device " << m_device.getInfo()); + LOG_MSG(Status::Ok, "select OpenCL device: " + << m_device.getInfo()); m_vendor_code.clear(); m_vendor_name = m_device.getInfo(); @@ -108,7 +144,7 @@ namespace spla { m_max_cu = m_device.getInfo(); m_max_wgs = m_device.getInfo(); m_max_local_mem = m_device.getInfo(); - m_addr_align = m_device.getInfo() / 8;// from bits to bytes + m_addr_align = m_device.getInfo() / 8; m_is_nvidia = false; m_is_amd = false; @@ -144,7 +180,6 @@ namespace spla { m_wave_size = 64; m_is_amd = true; - // Likely, it is an integrated amd device if (m_max_wgs <= 256 || m_max_cu == 1) m_wave_size = 16; } if (m_vendor_name.find("Imagination Technologies") != std::string::npos || @@ -158,26 +193,34 @@ namespace spla { } if (m_vendor_code.empty()) { - LOG_MSG(Status::Error, "failed to match one of the pre-defined vendors"); + LOG_MSG(Status::InvalidArgument, "unknown vendor: " << m_vendor_name + << ", using default parameters"); m_default_wgs = 64; m_wave_size = 8; } - std::stringstream desc; - desc << "OpenCL Acc " << m_platform.getInfo() - << " device: " << m_device.getInfo() - << " vendor:" << m_vendor_code - << " mcu:" << m_max_cu - << " wave:" << m_wave_size - << " mwgs:" << m_max_wgs; + desc << "OpenCL " + << m_platform.getInfo() + << " | device: " << m_device.getInfo() + << " | vendor: " << m_vendor_code + << " | mcu: " << m_max_cu + << " | wave: " << m_wave_size + << " | mwgs: " << m_max_wgs; m_description = desc.str(); - LOG_MSG(Status::Ok, m_description); + return Status::Ok; + } + + Status CLAccelerator::set_profiling(bool enabled) { + m_profiling_enabled = enabled; + LOG_MSG(Status::Ok, "set profiling: " << (enabled ? "enabled" : "disabled")); return Status::Ok; } + + Status CLAccelerator::set_queues_count(int count) { m_context = cl::Context(m_device); m_queues.clear(); @@ -185,33 +228,38 @@ namespace spla { for (int i = 0; i < count; i++) { cl_command_queue_properties properties = 0; -#ifndef SPLA_RELEASE - properties = CL_QUEUE_PROFILING_ENABLE; -#endif + if (m_profiling_enabled) { + properties |= CL_QUEUE_PROFILING_ENABLE; + } cl::CommandQueue queue(m_context, properties); m_queues.emplace_back(std::move(queue)); } - m_counter_pool = std::make_unique(); - m_alloc_general = std::make_unique(); - m_alloc_tmp = m_alloc_general.get(); - - if (!is_nvidia()) { - m_alloc_linear = std::make_unique(CLAllocLinear::DEFAULT_SIZE, m_addr_align); - m_alloc_tmp = m_alloc_linear.get(); - } + m_counter_pool = std::make_unique(); - LOG_MSG(Status::Ok, "configure " << count << " queues for computations"); + LOG_MSG(Status::Ok, "configure " << count << " queues" + << " (profiling: " << (m_profiling_enabled ? "ON" : "OFF") << ")"); return Status::Ok; } - const std::string& CLAccelerator::get_name() { - return m_name; - } - const std::string& CLAccelerator::get_description() { - return m_description; + + + Status CLAccelerator::set_linear_allocator(size_t size) { + m_alloc_linear = std::make_unique(size, m_addr_align); + m_alloc_tmp = m_alloc_linear.get(); + LOG_MSG(Status::Ok, "set linear allocator (size: " << size << " bytes)"); + return Status::Ok; } - const std::string& CLAccelerator::get_suffix() { - return m_suffix; + + + Status CLAccelerator::set_general_allocator() { + m_alloc_general = std::make_unique(); + m_alloc_tmp = m_alloc_general.get(); + LOG_MSG(Status::Ok, "set general allocator"); + return Status::Ok; } -}// namespace spla + const std::string& CLAccelerator::get_name() { return m_name; } + const std::string& CLAccelerator::get_description() { return m_description; } + const std::string& CLAccelerator::get_suffix() { return m_suffix; } + +}// namespace spla \ No newline at end of file diff --git a/src/opencl/cl_accelerator.hpp b/src/opencl/cl_accelerator.hpp index 52a0497e1..1844dbcfe 100644 --- a/src/opencl/cl_accelerator.hpp +++ b/src/opencl/cl_accelerator.hpp @@ -56,6 +56,12 @@ namespace spla { + struct InitResult { + Status status; + ConfigStatus config_status; + }; + InitResult init_with_configure(int argc, char** argv); + /** * @addtogroup internal * @{ @@ -70,10 +76,14 @@ namespace spla { CLAccelerator(); ~CLAccelerator() override; - Status init() override; - Status set_platform(int index) override; - Status set_device(int index) override; - Status set_queues_count(int count) override; + Status init() override; + Status set_platform(int index) override; + Status set_device(int index) override; + Status set_profiling(bool enabled); + Status set_queues_count(int count) override; + Status set_linear_allocator(size_t size); + Status set_general_allocator(); + const std::string& get_name() override; const std::string& get_description() override; const std::string& get_suffix() override; @@ -106,6 +116,7 @@ namespace spla { cl::Platform m_platform; cl::Device m_device; cl::Context m_context; + ankerl::svector m_queues; std::unique_ptr m_cache; std::unique_ptr m_counter_pool; std::unique_ptr m_alloc_linear; @@ -130,7 +141,7 @@ namespace spla { bool m_is_intel = false; bool m_is_img = false; - ankerl::svector m_queues; + bool m_profiling_enabled = false; }; /** @@ -148,4 +159,4 @@ namespace spla { }// namespace spla -#endif//SPLA_CL_ACCELERATOR_HPP +#endif//SPLA_CL_ACCELERATOR_HPP \ No newline at end of file diff --git a/src/opencl/cl_configure.cpp b/src/opencl/cl_configure.cpp new file mode 100644 index 000000000..94ed1e99f --- /dev/null +++ b/src/opencl/cl_configure.cpp @@ -0,0 +1,523 @@ +/**********************************************************************************/ +/* This file is part of spla project */ +/* https://github.com/SparseLinearAlgebra/spla */ +/**********************************************************************************/ +/* MIT License */ +/* */ +/* Copyright (c) 2023 SparseLinearAlgebra */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining a copy */ +/* of this software and associated documentation files (the "Software"), to deal */ +/* in the Software without restriction, including without limitation the rights */ +/* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell */ +/* copies of the Software, and to permit persons to whom the Software is */ +/* furnished to do so, subject to the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be included in all */ +/* copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR */ +/* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, */ +/* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE */ +/* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER */ +/* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, */ +/* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE */ +/* SOFTWARE. */ +/**********************************************************************************/ + +#include "cl_configure.hpp" +#include "CL/opencl.hpp" +#include "cl_accelerator.hpp" + +#include +#include + +namespace spla { + + Config config_default; + Config config_system; + Config config_user; + Config config_cli_and_env; + Config config_final; + + + void Profile::merge(const Profile& src) { + if (src.platform.has_value()) platform = src.platform; + if (src.device.has_value()) device = src.device; + if (src.queues.has_value()) queues = src.queues; + if (src.profiling.has_value()) profiling = src.profiling; + if (src.allocator.has_value()) allocator = src.allocator; + if (src.allocator_size.has_value()) allocator_size = src.allocator_size; + if (src.verbosity.has_value()) verbosity = src.verbosity; + if (src.extends.has_value()) extends = src.extends; + } + + + void Config::merge(const Config& src) { + if (src.platform.has_value()) platform = src.platform; + if (src.device.has_value()) device = src.device; + if (src.queues.has_value()) queues = src.queues; + if (src.profiling.has_value()) profiling = src.profiling; + if (src.allocator.has_value()) allocator = src.allocator; + if (src.allocator_size.has_value()) allocator_size = src.allocator_size; + if (src.verbosity.has_value()) verbosity = src.verbosity; + + if (src.profile.has_value()) profile = src.profile; + + if (src.profiles.has_value()) { + if (!profiles.has_value()) { + profiles = src.profiles; + } else { + for (const auto& [name, src_prof] : *src.profiles) { + auto& dst_prof = (*profiles)[name]; + dst_prof.merge(src_prof); + } + } + } + } + + void Config::reset() { + *this = Config{}; + } + + std::string get_spla_version() { + return "SPLA version: 0.0.0"; + } + + + std::string get_default_config_path() { +#ifdef _WIN32 + if (const char* pd = std::getenv("PROGRAMDATA")) { + return std::string(pd) + "\\spla\\spla_conf.json"; + } + return "C:\\ProgramData\\spla\\spla_conf.json"; + +#elif defined(__APPLE__) + return "/usr/local/share/spla/spla_conf.json"; + +#elif defined(__linux__) + return "/usr/share/spla/spla_conf.json"; + +#else + #error "spla supports only Windows, macOS and Linux" +#endif + } + + + std::string get_default_system_config_path() { +#ifdef _WIN32 + const char* program_data = std::getenv("PROGRAMDATA"); + if (program_data) { + return std::string(program_data) + "\\spla\\spla_conf.json"; + } + return "C:\\ProgramData\\spla\\spla_conf.json"; + +#elif defined(__APPLE__) + return "/Library/Application Support/spla/spla_conf.json"; + +#elif defined(__linux__) + return "/etc/spla/spla_conf.json"; + +#else + #error "spla supports only Windows, macOS and Linux" +#endif + } + + + std::string get_home_directory() { +#ifdef _WIN32 + const char* home = std::getenv("USERPROFILE"); + if (home) return std::string(home); + + const char* drive = std::getenv("HOMEDRIVE"); + const char* path = std::getenv("HOMEPATH"); + if (drive && path) return std::string(drive) + std::string(path); + throw std::runtime_error("Cannot determine home directory"); + +#elif defined(__APPLE__) || defined(__linux__) + const char* home = std::getenv("HOME"); + if (home) return std::string(home); + + struct passwd* pw = getpwuid(getuid()); + if (pw) return std::string(pw->pw_dir); + throw std::runtime_error("Cannot determine home directory"); + +#else + #error "spla supports only Windows, macOS and Linux" +#endif + } + + + std::string get_default_user_config_path() { + std::string home = get_home_directory(); + +#ifdef _WIN32 + const char* app_data = std::getenv("APPDATA"); + if (app_data) return std::string(app_data) + "\\spla\\spla_conf.json"; + return home + "\\AppData\\Roaming\\spla\\spla_conf.json"; + +#elif defined(__APPLE__) + return home + "/Library/Application Support/spla/spla_conf.json"; + +#elif defined(__linux__) || defined(__unix__) + return home + "/.config/spla/spla_conf.json"; + +#else + #error "spla supports only Windows, macOS and Linux" +#endif + } + + + ConfigStatus parse_cli_and_env(int argc, char** argv, Config& cfg) { + CLI::App app{"SPLA configuration"}; + + app.add_flag("-sh,--spla-help", cfg.help, "Show help and exit"); + app.add_flag("-sv,--spla-version", cfg.version, "Show version and exit"); + + app.add_option("-sp,--spla-platform", cfg.platform, + "OpenCL platform index\n" + "Config key: platform") + ->envname("SPLA_OPENCL_PLATFORM"); + + app.add_option("-sd,--spla-device", cfg.device, + "OpenCL device index\n" + "Config key: device") + ->envname("SPLA_OPENCL_DEVICE"); + + app.add_option("-sq,--spla-queues", cfg.queues, + "Number of command queues\n" + "Config key: queues") + ->envname("SPLA_QUEUES"); + + app.add_flag("-pr,--spla-profiling", cfg.profiling, + "Enable profiling of command queues\n" + "Config key: profiling\n") + ->envname("SPLA_PROFILING"); + + app.add_option("-sa,--spla-allocator", cfg.allocator, + "Allocator type: linear or general\n" + "Config key: allocator") + ->envname("SPLA_ALLOCATOR"); + + app.add_option("-as,--spla-allocator-size", cfg.allocator_size, + "Linear allocator size in bytes\n" + "Required for 'linear' allocator. Ignored for 'general'.\n" + "Config key: allocator_size") + ->envname("SPLA_ALLOCATOR_SIZE"); + + app.add_option("-sV,--spla-verbosity", cfg.verbosity, + "Verbosity level:\n" + " 0: No output\n" + " 1: Errors only\n" + " 2: Errors + warnings\n" + " 3: All messages (info, warnings, errors)") + ->envname("SPLA_VERBOSITY"); + + app.add_option("-sP,--spla-profile", cfg.profile, + "Configuration profile name\n" + "Overrides base settings with the named profile.\n" + "Config key: profile") + ->envname("SPLA_PROFILE"); + + try { + app.parse(argc, argv); + } catch (const CLI::ParseError& e) { + std::cerr << "[spla:cli_env] ERROR: failed to parse: " << e.what() << std::endl; + return ConfigStatus::CliOrEnvParseError; + } + + if (cfg.help) { + std::cout << app.help() << std::endl; + return ConfigStatus::HelpRequested; + } + + if (cfg.version) { + std::cout << get_spla_version() << std::endl; + return ConfigStatus::VersionRequested; + } + + return ConfigStatus::Ok; + } + + + ConfigStatus parse_file(const std::string& path, Config& cfg) { + + if (!std::filesystem::exists(path)) { + std::cerr << "[spla:config] WARNING: file not found: " << path << std::endl; + return ConfigStatus::Ok; + } + + std::ifstream file(path); + if (!file.is_open()) { + std::cerr << "[spla:config] WARNING: cannot open: " << path << std::endl; + return ConfigStatus::OpenFileError; + } + + try { + nlohmann::json data = nlohmann::json::parse(file); + + if (data.contains("platform")) cfg.platform = data["platform"].get(); + if (data.contains("device")) cfg.device = data["device"].get(); + if (data.contains("queues")) cfg.queues = data["queues"].get(); + if (data.contains("profiling")) cfg.profiling = data["profiling"].get(); + if (data.contains("allocator")) cfg.allocator = data["allocator"].get(); + if (data.contains("allocator_size")) cfg.allocator_size = data["allocator_size"].get(); + if (data.contains("verbosity")) cfg.verbosity = data["verbosity"].get(); + if (data.contains("profile")) cfg.profile = data["profile"].get(); + + if (data.contains("profiles")) { + std::map profiles; + for (auto& [name, p] : data["profiles"].items()) { + Profile prof; + if (p.contains("platform")) prof.platform = p["platform"].get(); + if (p.contains("device")) prof.device = p["device"].get(); + if (p.contains("queues")) prof.queues = p["queues"].get(); + if (p.contains("profiling")) prof.profiling = p["profiling"].get(); + if (p.contains("allocator")) prof.allocator = p["allocator"].get(); + if (p.contains("allocator_size")) prof.allocator_size = p["allocator_size"].get(); + if (p.contains("verbosity")) prof.verbosity = p["verbosity"].get(); + if (p.contains("extends")) prof.extends = p["extends"].get>(); + profiles[name] = prof; + } + cfg.profiles = profiles; + } + + return ConfigStatus::Ok; + + } catch (const nlohmann::json::exception& e) { + std::cerr << "[spla:config] ERROR: failed to parse '" + << path << "': " << e.what() << std::endl; + return ConfigStatus::ParseConfError; + } + } + + + ConfigStatus check_platform_and_device(int platform_index, int device_index) { + + if (platform_index < 0) { + std::cerr << "[spla:opencl] ERROR: platform must be >= 0 (got " + << platform_index << ")" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + + std::vector platforms; + cl::Platform::get(&platforms); + + if (platforms.empty()) { + std::cerr << "[spla:opencl] ERROR: no platform available" << std::endl; + return ConfigStatus::PlatformNotFound; + } + + if (static_cast(platform_index) >= platforms.size()) { + std::cerr << "[spla:opencl] ERROR: platform index out of range (got " + << platform_index << ", max " << platforms.size() - 1 << ")" + << std::endl; + return ConfigStatus::PlatformNotFound; + } + + if (device_index < 0) { + std::cerr << "[spla:opencl] ERROR: device must be >= 0 (got " + << device_index << ")" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + + std::vector devices; + platforms[platform_index].getDevices(CL_DEVICE_TYPE_ALL, &devices); + + if (devices.empty()) { + std::cerr << "[spla:opencl] ERROR: no device available on platform " + << platform_index << std::endl; + return ConfigStatus::DeviceNotFound; + } + + if (static_cast(device_index) >= devices.size()) { + std::cerr << "[spla:opencl] ERROR: device index out of range (got " + << device_index << ", max " << devices.size() - 1 << ")" + << std::endl; + return ConfigStatus::DeviceNotFound; + } + + return ConfigStatus::Ok; + } + + + ConfigStatus validation(const Config& cfg) { + + if (!cfg.platform.has_value()) { + std::cerr << "[spla:validation] ERROR: required: platform" << std::endl; + return ConfigStatus::MissedParameters; + } + if (!cfg.device.has_value()) { + std::cerr << "[spla:validation] ERROR: required: device" << std::endl; + return ConfigStatus::MissedParameters; + } + if (!cfg.queues.has_value()) { + std::cerr << "[spla:validation] ERROR: required: queues" << std::endl; + return ConfigStatus::MissedParameters; + } + if (!cfg.profiling.has_value()) { + std::cerr << "[spla:validation] ERROR: required: profiling" << std::endl; + return ConfigStatus::MissedParameters; + } + if (!cfg.allocator.has_value()) { + std::cerr << "[spla:validation] ERROR: required: allocator" << std::endl; + return ConfigStatus::MissedParameters; + } + if (!cfg.verbosity.has_value()) { + std::cerr << "[spla:validation] ERROR: required: verbosity" << std::endl; + return ConfigStatus::MissedParameters; + } + + if (cfg.allocator.value() == "linear" && !cfg.allocator_size.has_value()) { + std::cerr << "[spla:validation] ERROR: allocator_size is required for 'linear' allocator" + << std::endl; + return ConfigStatus::MissedParameters; + } + + ConfigStatus status; + + status = check_platform_and_device(*cfg.platform, *cfg.device); + if (status != ConfigStatus::Ok) { + return status; + } + + if (*cfg.queues <= 0) { + std::cerr << "[spla:validation] ERROR: queues must be > 0 (got " + << *cfg.queues << ")" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + + if (*cfg.allocator != "linear" && *cfg.allocator != "general") { + std::cerr << "[spla:validation] ERROR: allocator must be 'linear' or 'general' (got '" + << *cfg.allocator << "')" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + + if (*cfg.allocator == "linear" && *cfg.allocator_size <= 0) { + std::cerr << "[spla:validation] ERROR: allocator_size must be > 0 for 'linear' (got " + << *cfg.allocator_size << ")" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + + if (*cfg.verbosity < 0 || *cfg.verbosity > 3) { + std::cerr << "[spla:validation] ERROR: verbosity must be in [0, 3] (got " + << *cfg.verbosity << ")" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + + return ConfigStatus::Ok; + } + + + Profile apply_extends(const std::map& profiles, + const std::string& name, + std::set& stack) { + if (stack.count(name)) { + throw std::runtime_error("Profile cycle detected: " + name); + } + stack.insert(name); + + auto it = profiles.find(name); + if (it == profiles.end()) { + throw std::runtime_error("Profile not found: " + name); + } + + const Profile& prof = it->second; + Profile result; + + if (prof.extends) { + for (const auto& parent_name : *prof.extends) { + Profile parent = apply_extends(profiles, parent_name, stack); + result.merge(parent); + } + } + + result.merge(prof); + + stack.erase(name); + return result; + } + + ConfigStatus apply_profile(Config& cfg) { + + if (!cfg.profile.has_value()) return ConfigStatus::Ok; + + std::string profile_name = *cfg.profile; + + if (!cfg.profiles || !cfg.profiles->count(profile_name)) { + std::cerr << "[spla:profile] ERROR: not found: " << profile_name << std::endl; + return ConfigStatus::ProfileNotFound; + } + + std::set stack; + Profile resolved; + try { + resolved = apply_extends(*cfg.profiles, profile_name, stack); + } catch (const std::runtime_error& e) { + std::string msg = e.what(); + if (msg.find("cycle") != std::string::npos) { + std::cerr << "[spla:profile] ERROR: cycle detected: " + << profile_name << std::endl; + return ConfigStatus::ProfileCycle; + } + std::cerr << "[spla:profile] ERROR: " << msg << std::endl; + return ConfigStatus::ProfileNotFound; + } + + if (resolved.platform) cfg.platform = resolved.platform; + if (resolved.device) cfg.device = resolved.device; + if (resolved.queues) cfg.queues = resolved.queues; + if (resolved.profiling) cfg.profiling = resolved.profiling; + if (resolved.allocator) cfg.allocator = resolved.allocator; + if (resolved.allocator_size) cfg.allocator_size = resolved.allocator_size; + if (resolved.verbosity) cfg.verbosity = resolved.verbosity; + + return ConfigStatus::Ok; + } + + + ConfigStatus configure(int argc, char** argv) { + config_default.reset(); + config_system.reset(); + config_user.reset(); + config_cli_and_env.reset(); + config_final.reset(); + + ConfigStatus status; + + status = parse_cli_and_env(argc, argv, config_cli_and_env); + if (status != ConfigStatus::Ok) return status; + + status = parse_file(get_default_user_config_path(), config_user); + if (status == ConfigStatus::ParseConfError) return status; + if (status == ConfigStatus::OpenFileError) { + std::cerr << "[spla:configure] WARNING: cannot open user config, skipping" << std::endl; + } + + status = parse_file(get_default_system_config_path(), config_system); + if (status == ConfigStatus::ParseConfError) return status; + if (status == ConfigStatus::OpenFileError) { + std::cerr << "[spla:configure] WARNING: cannot open system config, skipping" << std::endl; + } + + status = parse_file(get_default_config_path(), config_default); + if (status == ConfigStatus::ParseConfError) return status; + if (status == ConfigStatus::OpenFileError) { + std::cerr << "[spla:configure] WARNING: cannot open default config, skipping" << std::endl; + } + + config_final.merge(config_default); + config_final.merge(config_system); + config_final.merge(config_user); + config_final.merge(config_cli_and_env); + + status = apply_profile(config_final); + if (status != ConfigStatus::Ok) return status; + + status = validation(config_final); + if (status != ConfigStatus::Ok) return status; + + std::cerr << "[spla:configure]: configuration complete" << std::endl; + return ConfigStatus::Ok; + } +}// namespace spla diff --git a/src/opencl/cl_configure.hpp b/src/opencl/cl_configure.hpp new file mode 100644 index 000000000..6ddef1b2c --- /dev/null +++ b/src/opencl/cl_configure.hpp @@ -0,0 +1,124 @@ +/**********************************************************************************/ +/* This file is part of spla project */ +/* https://github.com/SparseLinearAlgebra/spla */ +/**********************************************************************************/ +/* MIT License */ +/* */ +/* Copyright (c) 2023 SparseLinearAlgebra */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining a copy */ +/* of this software and associated documentation files (the "Software"), to deal */ +/* in the Software without restriction, including without limitation the rights */ +/* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell */ +/* copies of the Software, and to permit persons to whom the Software is */ +/* furnished to do so, subject to the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be included in all */ +/* copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR */ +/* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, */ +/* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE */ +/* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER */ +/* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, */ +/* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE */ +/* SOFTWARE. */ +/**********************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "CLI/CLI.hpp" +#include + +#ifdef _WIN32 +#elif defined(__APPLE__) || defined(__linux__) || defined(__unix__) + #include + #include +#endif + +namespace spla { + + struct Profile { + std::optional platform; + std::optional device; + std::optional queues; + std::optional profiling; + std::optional allocator; + std::optional allocator_size; + std::optional verbosity; + std::optional> extends; + + void merge(const Profile& source); + }; + + struct Config { + std::optional help; + std::optional version; + std::optional platform; + std::optional device; + std::optional queues; + std::optional profiling; + std::optional allocator; + std::optional allocator_size; + std::optional verbosity; + + std::optional profile; + std::optional> profiles; + + void merge(const Config& source); + void reset(); + }; + + + enum ConfigStatus { + Ok, + + HelpRequested, + VersionRequested, + CliOrEnvParseError, + + ParseConfError, + OpenFileError, + + ProfileNotFound, + ProfileCycle, + + MissedParameters, + PlatformNotFound, + DeviceNotFound, + InvalidConfigParams + }; + + extern Config config_default; + extern Config config_system; + extern Config config_user; + extern Config config_cli_and_env; + extern Config config_final; + + std::string get_spla_version(); + + std::string get_default_config_path(); + std::string get_default_system_config_path(); + std::string get_home_directory(); + std::string get_default_user_config_path(); + + ConfigStatus parse_cli_and_env(int argc, char** argv, Config& cfg); + ConfigStatus parse_file(const std::string& path, Config& cfg); + + ConfigStatus check_platform_and_device(int platform_index, int device_index); + ConfigStatus validation(const Config& cfg); + + Profile apply_extends(const std::map& profiles, + const std::string& name, + std::set& stack); + ConfigStatus apply_profile(Config& cfg); + + ConfigStatus configure(int argc, char** argv); + +}// namespace spla \ No newline at end of file