diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c8cfa4f0..27c128e13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,22 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(spla LANGUAGES CXX) +###################################################################### +## Package version +## python/VERSION.md is the single source of truth for the version: +## python/setup.py reads it for the PyPI package and the C++ library +## gets it as the SPLA_VERSION compile definition. + +file(READ "${CMAKE_CURRENT_LIST_DIR}/python/VERSION.md" SPLA_VERSION_RAW) +string(STRIP "${SPLA_VERSION_RAW}" SPLA_VERSION) +message(STATUS "SPLA package version: ${SPLA_VERSION}") + ###################################################################### ## public options option(SPLA_BUILD_TESTS "Build test folder with modules tests" YES) option(SPLA_BUILD_EXAMPLES "Build library example applications with algorithms" YES) +option(SPLA_BUILD_TOOLS "Build command line tools (spla)" YES) option(SPLA_BUILD_OPENCL "Build library with opencl backend" YES) ###################################################################### @@ -178,18 +189,40 @@ 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") - find_package(OpenCL REQUIRED) + message(STATUS "Use Apple OpenCL framework for linking, headers from deps") + find_library(OPENCL_FRAMEWORK OpenCL REQUIRED) add_library(OpenCLHeaders INTERFACE) add_library(OpenCL::Headers ALIAS OpenCLHeaders) - target_include_directories(OpenCLHeaders INTERFACE ${OpenCL_INCLUDE_DIRS}) + target_include_directories(OpenCLHeaders INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/deps/opencl-headers) + + add_library(HeadersCpp INTERFACE) + add_library(OpenCL::HeadersCpp ALIAS HeadersCpp) + target_include_directories(HeadersCpp INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/deps/opencl-headers-hpp/include) + target_link_libraries(HeadersCpp INTERFACE OpenCL::Headers) add_library(OpenCL INTERFACE) - target_include_directories(OpenCL INTERFACE ${OpenCL_INCLUDE_DIRS}) - target_link_libraries(OpenCL INTERFACE ${OpenCL_LIBRARIES}) + target_link_libraries(OpenCL INTERFACE ${OPENCL_FRAMEWORK}) else () message(STATUS "Add Khronos OpenCL C API header files") add_subdirectory(deps/opencl-headers) @@ -199,14 +232,13 @@ if (SPLA_BUILD_OPENCL) set(OPENCL_ICD_LOADER_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(OPENCL_ICD_LOADER_BUILD_TESTING OFF CACHE BOOL "" FORCE) add_subdirectory(deps/opencl-icd-loader) - endif () - - message(STATUS "Add Khronos OpenCL HPP bindings") - add_library(HeadersCpp INTERFACE) - add_library(OpenCL::HeadersCpp ALIAS HeadersCpp) - target_include_directories(HeadersCpp INTERFACE deps/opencl-headers-hpp/include) - target_link_libraries(HeadersCpp INTERFACE OpenCL::Headers) + message(STATUS "Add Khronos OpenCL HPP bindings") + add_library(HeadersCpp INTERFACE) + add_library(OpenCL::HeadersCpp ALIAS HeadersCpp) + target_include_directories(HeadersCpp INTERFACE deps/opencl-headers-hpp/include) + target_link_libraries(HeadersCpp INTERFACE OpenCL::Headers) + endif () endif () if (SPLA_BUILD_EXAMPLES) @@ -297,6 +329,9 @@ add_library(spla SHARED include/spla/vector.hpp src/core/accelerator.hpp src/core/common.hpp + src/core/config_status.hpp + src/core/configure.cpp + src/core/configure.hpp src/core/dispatcher.cpp src/core/dispatcher.hpp src/core/logger.cpp @@ -368,14 +403,21 @@ target_include_directories(spla PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src) set_target_properties(spla PROPERTIES OUTPUT_NAME "spla_${SPLA_ARCH}") target_compile_definitions(spla PRIVATE SPLA_EXPORTS) +target_compile_definitions(spla PRIVATE SPLA_VERSION="${SPLA_VERSION}") 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) +target_link_libraries(spla PRIVATE ${CMAKE_DL_LIBS}) 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=120 + CL_HPP_TARGET_OPENCL_VERSION=120 + CL_HPP_MINIMUM_OPENCL_VERSION=120) list(APPEND SPLA_DEFINES SPLA_BUILD_OPENCL) endif () @@ -383,6 +425,13 @@ spla_target_compile_definitions(spla) spla_target_compile_options(spla) spla_target_link_options(spla) +set(SPLA_CONF_SRC "${CMAKE_CURRENT_LIST_DIR}/python/pyspla/spla_conf.json") +set(SPLA_CONF_DST "${CMAKE_BINARY_DIR}/spla_conf.json") +add_custom_command(TARGET spla POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${SPLA_CONF_SRC}" "${SPLA_CONF_DST}" + COMMENT "Copying shipped configuration file spla_conf.json next to the library") + ###################################################################### ## Add examples directory @@ -404,6 +453,18 @@ if (SPLA_BUILD_EXAMPLES) spla_example_application(convert) endif () +###################################################################### +## Add command line tools + +if (SPLA_BUILD_TOOLS) + message(STATUS "Add command line tools to build") + + add_executable(spla_cli tools/soft_check.cpp) + target_link_libraries(spla_cli PRIVATE spla) + target_link_libraries(spla_cli PRIVATE CLI11::CLI11) + set_target_properties(spla_cli PROPERTIES OUTPUT_NAME spla) +endif () + ###################################################################### ## Add unit-tests directory diff --git a/build.py b/build.py index 95cdeb90f..bbf548da3 100644 --- a/build.py +++ b/build.py @@ -35,6 +35,7 @@ def main(): parser.add_argument("--build-type", default="Release", help="type of build: `Debug`, `Release` or `RelWithDebInfo`") parser.add_argument("--tests", default="YES", help="build tests") parser.add_argument("--examples", default="YES", help="build example applications") + parser.add_argument("--tools", default="YES", help="build command line tools") parser.add_argument("--opencl", default="YES", help="build opencl acceleration backend") parser.add_argument("--target", default="all", help="which target to build") parser.add_argument("--nt", default="4", help="number of os threads for build") @@ -44,6 +45,7 @@ def main(): build_config_args = ["cmake", ".", "-B", args.build_dir, "-G", "Ninja", f"-DCMAKE_BUILD_TYPE={args.build_type}", f"-DSPLA_BUILD_TESTS={args.tests}", f"-DSPLA_BUILD_EXAMPLES={args.examples}", + f"-DSPLA_BUILD_TOOLS={args.tools}", f"-DSPLA_BUILD_OPENCL={args.opencl}"] if args.arch: diff --git a/docs/UserGuide.md b/docs/UserGuide.md new file mode 100644 index 000000000..82505977a --- /dev/null +++ b/docs/UserGuide.md @@ -0,0 +1,258 @@ +# User Guide + +The user guide for the spla library. Contains usage scenarios for the configuration mechanism: from a quick start without configuration to running tests in CI. The complete reference of all parameters, flags and environment variables is given in the [User Manual](UserManual.md). + +## Contents + +1. [Environment check](#environment-check) +2. [Scenario 1. Quick start without configuration](#scenario-1-quick-start-without-configuration) +3. [Scenario 2. Choosing between CPU and GPU without specifying indexes](#scenario-2-choosing-between-cpu-and-gpu-without-specifying-indexes) +4. [Scenario 3. Multiple devices and modes in one file](#scenario-3-multiple-devices-and-modes-in-one-file) +5. [Scenario 4. Running in CI with a file from the repository](#scenario-4-running-in-ci-with-a-file-from-the-repository) +6. [Priority of configuration sources](#priority-of-configuration-sources) +7. [Where the configuration files are located](#where-the-configuration-files-are-located) + +## Environment check + +After installation, run the command that confirms the library is ready to use: + +```bash +./program spla --softcheck +``` + +The command: + +1. Reads the configuration file shipped with the library. +2. Initializes OpenCL, selects the platform and device. +3. Runs a simple test task `2 + 3`. +4. Reports the status. + +Example of successful output: + +```bash +$ s./program spla --softcheck +[spla:check] Starting sanity check... +[spla:check] Initializing OpenCL... +[spla:check] Selected accelerator: OpenCL NVIDIA CUDA | device: NVIDIA GeForce RTX 3070 Laptop GPU | vendor: nvidia | mcu: 40 | wave: 32 | mwgs: 1024 +[spla:check] Running test task: 2 + 3 = ... +[spla:check] Result: 5 (expected 5) +[spla:check] SUCCESS - spla works correctly +``` + +## Scenario 1. Quick start without configuration + +You want a working application without configuration - nothing needs to be configured. The library uses the default parameters from the configuration file shipped with the library. + +```bash +./program +``` + +The scenario works the same on Linux, macOS and Windows. If OpenCL acceleration is unavailable, the library automatically switches to the pure native CPU by default (the `if_opencl_unavailable: "use_cpu"` parameter), reporting it. + +Before the first run it is recommended to run [`spla --softcheck`](#environment-check) to make sure that everything required for spla is installed. + +## Scenario 2. Choosing between CPU and GPU without specifying indexes + +You want to control the device type, but not specify concrete OpenCL platform and device indexes. + +The device type is set by the `backend` parameter in the configuration file: + +```json +{ + "gpu_only": { "backend": "opencl_gpu" }, + "cpu_only": { "backend": "cpu" } +} +``` + +- `"opencl_gpu"` - use the first available OpenCL GPU, +- `"cpu"` - use the pure native CPU without touching OpenCL. + +The `platform_index` and `device_index` indexes are ignored with these values. + +### Behavior when OpenCL is unavailable + +If `backend: "opencl_gpu"` is selected, but no OpenCL GPU is available, the behavior is defined by the `if_opencl_unavailable` parameter: + +```json +{ + "gpu_or_cpu": { + "backend": "opencl_gpu", + "if_opencl_unavailable": "use_cpu" + }, + "gpu_only": { + "backend": "opencl_gpu", + "if_opencl_unavailable": "abort" + } +} +``` + +- `"use_cpu"` - the library switches to the pure native CPU and continues working, +- `"abort"` - the library fails initialization and the process exits with the error status. + +Running the required configuration: + +```bash +./program --spla-config=gpu_or_cpu +./program --spla-config=gpu_only +``` + +`if_opencl_unavailable` is applied with `backend: "opencl_gpu"` and `"by_index"`. With `backend: "cpu"` the parameter is ignored. + +## Scenario 3. Multiple devices and modes in one file + +You regularly run the same program on different GPUs and in different modes - for example, on GPU 0 and 1, in debug mode and in performance measurement mode. Instead of several configuration files, you describe everything in one. + +```json +{ + "default": { "backend": "opencl_gpu", "verbosity": 2 }, + + "gpu0": { + "extends": ["default"], + "backend": "by_index", + "platform_index": 0, + "device_index": 0 + }, + "gpu1": { + "extends": ["default"], + "backend": "by_index", + "platform_index": 0, + "device_index": 1 + }, + + "debug": { + "extends": ["default"], + "verbosity": 3, + "profiling": true + }, + "bench": { + "extends": ["default"], + "verbosity": 0 + }, + + "debug_gpu1": { "extends": ["debug", "gpu1"] }, + "bench_gpu0": { "extends": ["bench", "gpu0"] } +} +``` + +Running: + +```bash +./program --spla-config=gpu1 +./program --spla-config=debug +./program --spla-config=debug_gpu1 +./program --spla-config=bench_gpu0 +``` + +Configurations inherited through `extends` are applied in order: each next parent overrides the previous one, and the parameters of the configuration itself override all parents. Circular inheritance is detected and results in an error. + +## Scenario 4. Running in CI with a file from the repository + +You need to run tests in CI with a separate configuration file. + +In a CI environment it is inconvenient and undesirable to touch the system and user configuration files, and the test configurations must live in the repository. Therefore the configurations are described in a separate file inside the repository and its path is given at launch together with the configuration name - these are the command line arguments `--spla-config-file` and `--spla-config`. + +``` +my_project/ +├── .github/ +│ └── workflows/ +│ ├── ci.yml +│ └── ci_spla_conf.json +├── src/ +└── tests/ +``` + +### ci_spla_conf.json + +```json +{ + "test_cpu": { + "backend": "cpu", + "queues_count": 1, + "profiling": false, + "allocator_type": "general", + "verbosity": 1 + }, + "test_gpu0": { + "backend": "by_index", + "platform_index": 0, + "device_index": 0, + "if_opencl_unavailable": "abort", + "queues_count": 2, + "profiling": false, + "allocator_type": "general", + "verbosity": 0 + }, + "test_gpu1": { + "backend": "by_index", + "platform_index": 0, + "device_index": 1, + "if_opencl_unavailable": "abort", + "queues_count": 2, + "profiling": false, + "allocator_type": "linear", + "linear_alloc_size": 8, + "verbosity": 0 + } +} +``` + +### ci.yml + +```yml +name: CI + +on: [push, pull_request] + +jobs: + test: + name: Test (${{ matrix.config }}) + runs-on: [self-hosted, gpu] + + strategy: + fail-fast: false + matrix: + config: [test_cpu, test_gpu0, test_gpu1] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyspla pytest + + - name: Run tests + run: python src/my_app.py --spla-config-file=.github/workflows/ci_spla_conf.json --spla-config=${{ matrix.config }} +``` + +The file path and the configuration name are passed at launch as command arguments, not through environment variables - in CI this makes the launch command self-contained and reproducible. + +## Priority of configuration sources + +Sources are applied in descending priority order: parameters from a higher-priority source override lower-priority ones. This matters for the scenarios: any flag or environment variable overrides values from files. + +| # | Source | +|---:|:---| +| 1 | Command line arguments (`--spla-*`) | +| 2 | Environment variables (`SPLA_*`) | +| 3 | File with an explicitly specified path (`--spla-config-file`) | +| 4 | User configuration file | +| 5 | System configuration file | +| 6 | Configuration file shipped with the library | + +If the configuration name is not specified, the `"default"` configuration from the file shipped with the library is used. For details on the collection order and merging of sources, see the [User Manual](UserManual.md#configuration-mechanism-overview). + +## Where the configuration files are located + +- **Shipped with the library** - next to the library file (in the pyspla package - inside the installed package). +- **System** - Linux: `/etc/spla/spla_conf.json`. +- **User** - Linux: `~/.config/spla/spla_conf.json`. +- **Explicit** - any path via `--spla-config-file`. + +Paths for macOS and Windows and the rules for "file missing / file cannot be opened" are in the [User Manual](UserManual.md#configuration-files). \ No newline at end of file diff --git a/docs/UserManual.md b/docs/UserManual.md new file mode 100644 index 000000000..d7411b0a1 --- /dev/null +++ b/docs/UserManual.md @@ -0,0 +1,317 @@ +# User Manual + +A manual for the spla library configuration mechanism. This is a reference: a complete description of the parameters, configuration sources, validation rules and statuses. Practical usage scenarios are given in the [User Guide](UserGuide.md). + +## Contents + +1. [NAME](#name) +2. [CONFIGURATION MECHANISM OVERVIEW](#configuration-mechanism-overview) +3. [COMMAND LINE PARAMETERS](#command-line-parameters) +4. [ENVIRONMENT VARIABLES](#environment-variables) +5. [CONFIGURATION FILES](#configuration-files) +6. [CONFIGURATIONS AND INHERITANCE](#configurations-and-inheritance) +7. [PARAMETER VALIDATION](#parameter-validation) +8. [STATUSES AND ERROR BEHAVIOR](#statuses-and-error-behavior) + +## NAME + +spla - a framework for GPU-accelerated mathematical computations. The library provides sparse linear algebra primitives (matrices, vectors, scalars) and a wide range of operations. + +For most users it matters on which device their project runs, so the library provides a configuration mechanism: explicit GPU selection, OpenCL platform, number of command queues and other parameters that affect performance and user convenience. + +## CONFIGURATION MECHANISM OVERVIEW + +The spla configuration is assembled from six sources, applied in descending order of priority. + +| # | Source | How it is set | +|---:|:---|:---| +| 1 | Command line arguments | `--spla-*` flags | +| 2 | Environment variables | `SPLA_*` variables | +| 3 | File with an explicitly specified path | the `--spla-config-file` flag or `SPLA_CONFIG_FILE` | +| 4 | User file | `~/.config/spla/spla_conf.json` (Linux) and equivalents | +| 5 | System file | `/etc/spla/spla_conf.json` (Linux) and equivalents | +| 6 | Library file | `spla_conf.json` next to the library file | + +Collection order: + +1. Configuration files are read (6 → 5 → 4 → 3) and **merged by configuration name**: configurations with the same name from different files are merged parameter by parameter, unique ones are added. +2. The active configuration is selected: the name from `--spla-config` / `SPLA_CONFIG`, `"default"` by default. +3. The selected configuration is applied together with the parents from `extends`. +4. Environment variables are applied on top, then command line arguments (highest priority). + +The result goes into the final configuration, which is used when initializing the accelerator (platform and device selection, creation of command queues, allocator, verbosity level). + +## COMMAND LINE PARAMETERS + +All spla flags start with `--spla-`. Flags that do not start with `--spla-` belong to the program using the library and are ignored by the configuration mechanism. + +| Flag | Type | Description | +|:---|:---|:---| +| `--spla-help` | - | Show help and exit | +| `--spla-version` | - | Show the library version and exit | +| `--spla-config` | string | Name of the configuration to apply. By default the `"default"` configuration is used | +| `--spla-config-file` | path | Path to a configuration file. | +| `--spla-backend` | string | Backend: `opencl_gpu`, `cpu`, `by_index` | +| `--spla-platform-index` | int | OpenCL platform index. Only when `backend=by_index` | +| `--spla-device-index` | int | Index of the OpenCL device within the platform. Only when `backend=by_index` | +| `--spla-if-opencl-unavailable` | string | Behavior when OpenCL acceleration is unavailable: `use_cpu` - switch to pure native CPU, `abort` - return an error | +| `--spla-queues-count` | int | Number of OpenCL command queues | +| `--spla-profiling` | bool | Profiling of command queues: `true`, `false` | +| `--spla-allocator-type` | string | Allocator type: `general`, `linear` | +| `--spla-linear-alloc-size` | int | Linear allocator size in bytes. Required when `allocator_type=linear`, ignored when `general` | +| `--spla-verbosity` | int | Verbosity level: `0` - no output, `1` - errors only, `2` - errors and warnings, `3` - all messages | + +### Example + +```bash +./my_program --spla-config-file=/etc/spla/spla_conf.json --spla-config=bench --spla-verbosity=0 +``` + +## ENVIRONMENT VARIABLES + +For each command line parameter (except `--spla-help` and `--spla-version`) there is an environment variable equivalent. + +| Variable | Flag equivalent | +|:---|:---| +| `SPLA_CONFIG` | `--spla-config` | +| `SPLA_CONFIG_FILE` | `--spla-config-file` | +| `SPLA_BACKEND` | `--spla-backend` | +| `SPLA_PLATFORM_INDEX` | `--spla-platform-index` | +| `SPLA_DEVICE_INDEX` | `--spla-device-index` | +| `SPLA_IF_OPENCL_UNAVAILABLE` | `--spla-if-opencl-unavailable` | +| `SPLA_QUEUES_COUNT` | `--spla-queues-count` | +| `SPLA_PROFILING` | `--spla-profiling` | +| `SPLA_ALLOCATOR_TYPE` | `--spla-allocator-type` | +| `SPLA_LINEAR_ALLOC_SIZE` | `--spla-linear-alloc-size` | +| `SPLA_VERBOSITY` | `--spla-verbosity` | + +### Example + +```bash +SPLA_CONFIG=bench SPLA_VERBOSITY=1 ./my_program +``` + +## CONFIGURATION FILES + +The file format is JSON. A file consists of a set of named configurations; each configuration is a set of library parameters. + +### Configuration keys + +| Key | Type | Description | Requirements | +|:---|:---|:---|:---| +| `extends` | array of string | List of parent configurations | Names of configurations from all file levels | +| `backend` | string | Backend: `opencl_gpu`, `cpu`, `by_index` | Required | +| `platform_index` | int | OpenCL platform index | Required when `backend=by_index`, otherwise ignored | +| `device_index` | int | OpenCL device index | Required when `backend=by_index`, otherwise ignored | +| `if_opencl_unavailable` | string | Behavior when OpenCL acceleration is unavailable: `use_cpu`, `abort` | Required when `backend != cpu`, ignored when `backend=cpu` | +| `queues_count` | int | Number of command queues | Required, ≥ 1 | +| `profiling` | bool | Queue profiling | Required | +| `allocator_type` | string | Allocator type: `general`, `linear` | Required | +| `linear_alloc_size` | int | Linear allocator size (bytes) | Required when `allocator_type=linear`, ignored when `general` | +| `verbosity` | int | Verbosity level 0..3 | Required | + +### File shipped with the library + +The configuration name `"default"` can be defined **only** in the file shipped with the library. Defining a configuration named `"default"` in any other file is a parsing error. + +The shipped file has the following structure: + +```json +{ + "default": { + "backend": "opencl_gpu", + "if_opencl_unavailable": "use_cpu", + + "queues_count": 1, + "profiling": false, + "allocator_type": "general", + + "verbosity": 2 + } +} +``` + +### File locations + +**Library file** - next to the library file: Windows - next to the DLL, Linux/macOS - next to the `.so`/`.dylib` (the path is determined via the address of the loaded library). Within the pyspla package - inside the installed Python package directory. + +**System file:** + +> Linux: `/etc/spla/spla_conf.json` \ +> macOS: `/Library/Application Support/spla/spla_conf.json` \ +> Windows: `%ProgramData%\spla\spla_conf.json` + +**User file:** + +> Linux: `~/.config/spla/spla_conf.json` \ +> macOS: `~/Library/Application Support/spla/spla_conf.json` \ +> Windows: `%APPDATA%\spla\spla_conf.json` + +**Explicitly set file** - any path given via `--spla-config-file` / `SPLA_CONFIG_FILE`. + +### Semantics of missing and unopenable files + +| File | Missing | Exists but cannot be opened | +|:---|:---|:---| +| Library file | error `FileNotExists` (required) | error `OpenFileError` | +| System | skipped | error `OpenFileError` | +| User | skipped | error `OpenFileError` | +| Explicit (if specified) | error `FileNotExists` | error `OpenFileError` | + +## CONFIGURATIONS AND INHERITANCE + +### Choosing the active configuration + +```bash +./program --spla-config=gpu1 # flag +SPLA_CONFIG=gpu1 ./program # or environment variable +``` + +If the name is not specified, the `"default"` configuration from the file shipped with the library is used. + +### Merging files + +Configurations from the library, system, user and explicit files are merged by name: configurations with the same name are merged parameter by parameter, unique ones are added. Parameters of a configuration from a file with higher priority override the parameters of the same-named configuration from a file with lower priority. + +### Inheritance via `extends` + +If a configuration contains an `extends` field, the library first applies the configurations listed in it in order: the last one in the list overrides the previous ones. The parameters of the configuration itself are applied last and override all parents. + +If a configuration does not contain `extends`, only its own parameters are applied. + +Names in `extends` are taken from all configurations defined in all files. This allows inheriting configurations defined at another level, for example, a user configuration can inherit `"default"` from the library file. + +Circular inheritance is detected and results in the `ConfigurationCycle` status. + +Example of a cycle - configurations inherit each other directly: + +```json +{ + "a": { "extends": ["b"], "backend": "opencl_gpu" }, + "b": { "extends": ["a"], "backend": "cpu" } +} +``` + +Such a configuration is rejected when loading (`ConfigurationCycle`). A self-reference is also a cycle (`"extends": ["a"]` inside configuration `a`). Diamond inheritance (`a` → `b`, `a` → `c`, where `b` and `c` both inherit `d`) is not a cycle. + +### Example + +File shipped with the library: + +```json +{ + "default": { + "backend": "opencl_gpu", + "if_opencl_unavailable": "use_cpu", + "queues_count": 1, + "profiling": false, + "allocator_type": "general", + "verbosity": 2 + } +} +``` + +User file: + +```json +{ + "gpu0": { + "extends": ["default"], + "backend": "by_index", + "platform_index": 0, + "device_index": 0, + "if_opencl_unavailable": "abort", + "queues_count": 1, + "profiling": true + } +} +``` + +Final set of parameters of the `gpu0` configuration: + +| Parameter | Value | Source | +|:---|:---|:---| +| backend | `by_index` | configuration `gpu0` | +| platform_index | `0` | configuration `gpu0` | +| device_index | `0` | configuration `gpu0` | +| if_opencl_unavailable | `abort` | configuration `gpu0` | +| queues_count | `1` | library file (`default`) | +| profiling | `true` | configuration `gpu0` | +| allocator_type | `general` | library file (`default`) | +| verbosity | `2` | library file (`default`) | + +## PARAMETER VALIDATION + +After assembly, the configuration from all sources is validated. + +### Errors (the configuration is invalid) + +| Parameter | Requirement | +|:---|:---| +| `backend` | Required, one of: `opencl_gpu`, `cpu`, `by_index` | +| `platform_index`, `device_index` | Required when `backend=by_index`, must be ≥ 0 | +| `if_opencl_unavailable` | Required when `backend != cpu`, one of: `use_cpu`, `abort` | +| `queues_count` | Required, ≥ 1 | +| `profiling` | Required | +| `allocator_type` | Required, one of: `general`, `linear` | +| `linear_alloc_size` | Required when `allocator_type=linear` | +| `verbosity` | Required, 0..3 | + +### Warnings (the parameter is ignored, but the configuration is valid) + +| Situation | Message | +|:---|:---| +| `platform_index`/`device_index` set when `backend != by_index` | the parameter is ignored | +| `if_opencl_unavailable` set when `backend=cpu` | the parameter is ignored | +| `linear_alloc_size` set when `allocator_type=general` | the parameter is ignored | + +### Behavior when the device is unavailable + +Implemented in accelerator initialization: + +| Configuration | OpenCL available | OpenCL unavailable | +|:---|:---|:---| +| `backend=opencl_gpu`, `if_opencl_unavailable=use_cpu` | the GPU is used | computations fall back to the pure native CPU; OpenCL is not used | +| `backend=opencl_gpu`, `if_opencl_unavailable=abort` | the GPU is used | error `DeviceNotFound`; the process exits with that status (through `pyspla`, `SplaDeviceNotFound` is raised) | +| `backend=by_index` | the device with the given indexes is used | the platform/device is selected by indexes; if it is unavailable, `if_opencl_unavailable` decides | +| `backend=cpu` | - | pure native CPU is used; OpenCL is never initialized | + +## STATUSES AND ERROR BEHAVIOR + +The configuration mechanism returns one of the following statuses. + +| Status | Meaning | +|:---|:---| +| `Ok` | The configuration is assembled and valid | +| `HelpRequested` | Help requested (`--spla-help`) - the text has already been printed | +| `VersionRequested` | Version requested (`--spla-version`) - already printed | +| `CliOrEnvParseError` | Error parsing command line arguments or environment variables | +| `FileNotExists` | A required file is missing (the library file or an explicitly specified one) | +| `OpenFileError` | The file exists but cannot be opened | +| `ParseConfError` | JSON parsing error, invalid value type, or `"default"` in a non-library file | +| `ConfigurationNotFound` | The specified configuration name was not found in any file | +| `ConfigurationCycle` | A cycle in `extends` was detected | +| `MissedParameters` | Required parameters are missing | +| `PlatformNotFound` / `DeviceNotFound` | The device required by the configuration is unavailable | +| `InvalidConfigParams` | Invalid parameter value | + +### Python (`pyspla`) + +The same statuses are propagated to Python: + +- `configure()` and `init_library()` raise the matching `Spla*` exception + (`SplaInvalidArgument`, `SplaPlatformNotFound`, `SplaDeviceNotFound`, ...) + or `SplaFailedConfigure` / `SplaFailedInitialize` for generic errors; +- every raised exception keeps the numeric status in the `status` attribute; +- `acceleration_status()` returns the current accelerator status + (`0` - accelerator is used, `2` - pure CPU, other values - initialization error); +- `has_acceleration()` returns `True` when an accelerated backend is active. + +### CLI behavior + +The `spla` command line tool: + +- `spla --softcheck` - environment sanity check (configuration, OpenCL initialization, task 2 + 3), +- `spla --spla-help` - help, exit code 0, +- `spla --spla-version` - library version (for example, `SPLA version: 0.0.5`), exit code 0, +- on a configuration error - an error message and exit code 1. \ No newline at end of file diff --git a/include/spla.h b/include/spla.h index 907865fab..3acba577d 100644 --- a/include/spla.h +++ b/include/spla.h @@ -135,6 +135,10 @@ typedef void(spla_MessageCallback)(spla_Status, const char* message, const char* SPLA_API void spla_Library_finalize(); SPLA_API spla_Status spla_Library_initialize(); +SPLA_API spla_Status spla_Configure(int argc, char** argv); +SPLA_API void spla_Config_help(); +SPLA_API void spla_Config_version(); +SPLA_API spla_Status spla_Library_get_acceleration_status(); SPLA_API spla_Status spla_Library_set_accelerator(spla_AcceleratorType accelerator); SPLA_API spla_Status spla_Library_set_platform(int index); SPLA_API spla_Status spla_Library_set_device(int index); diff --git a/include/spla/library.hpp b/include/spla/library.hpp index a572cb770..2507dd9bd 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 * @@ -149,6 +164,27 @@ namespace spla { */ SPLA_API Status time_profile_reset(); + /** + * @brief Configure the library from command line arguments and environment variables + * @note It is recommended to call this function before any computations, however the call + * @param argc Number of arguments (including program name) + * @param argv Array of arguments (may be nullptr when argc is 0) + * @return Function call status + */ + SPLA_API Status configure(int argc, char** argv); + + /** + * @brief Get status of accelerator initialization + * @return Function call status + */ + SPLA_API Status get_acceleration_status(); + + /** + * @brief Apply configuration using already parsed config_final + * @return Function call status + */ + SPLA_API Status apply_configuration(); + /** * @warning Internal usage only! * @return Library computations accelerator if presented @@ -198,9 +234,9 @@ namespace spla { std::unique_ptr m_dispatcher; std::unique_ptr m_logger; std::unique_ptr m_time_profiler; - bool m_force_no_acc = false; + bool m_force_no_acc = false; + Status m_acceleration_status = Status::NoAcceleration; }; - /** * @} */ diff --git a/python/pyspla/__init__.py b/python/pyspla/__init__.py index 9c6032403..37fc72a5b 100644 --- a/python/pyspla/__init__.py +++ b/python/pyspla/__init__.py @@ -269,6 +269,20 @@ __version__ = VERSIONS[-1] __all__ = [ + "SplaError", + "SplaFailedInitialize", + "SplaFailedConfigure", + "SplaNoAcceleration", + "SplaPlatformNotFound", + "SplaDeviceNotFound", + "SplaInvalidState", + "SplaInvalidArgument", + "SplaNoValue", + "SplaCompilationError", + "SplaConfigError", + "SplaNotImplemented", + "acceleration_status", + "has_acceleration", "Type", "BOOL", "INT", diff --git a/python/pyspla/bridge.py b/python/pyspla/bridge.py index 808f6ff17..4882e3a2d 100644 --- a/python/pyspla/bridge.py +++ b/python/pyspla/bridge.py @@ -27,6 +27,20 @@ """ __all__ = [ + "SplaError", + "SplaFailedInitialize", + "SplaFailedConfigure", + "SplaNoAcceleration", + "SplaPlatformNotFound", + "SplaDeviceNotFound", + "SplaInvalidState", + "SplaInvalidArgument", + "SplaNoValue", + "SplaCompilationError", + "SplaConfigError", + "SplaNotImplemented", + "acceleration_status", + "has_acceleration", "backend", "check", "is_docs", @@ -35,6 +49,7 @@ ] import os +import sys import ctypes import pathlib import platform @@ -66,6 +81,10 @@ class SplaFailedInitialize(SplaError): pass +class SplaFailedConfigure(SplaError): + pass + + class SplaNoAcceleration(SplaError): pass @@ -94,6 +113,10 @@ class SplaCompilationError(SplaError): pass +class SplaConfigError(SplaError): + pass + + class SplaNotImplemented(SplaError): pass @@ -155,10 +178,35 @@ class FormatVector(enum.Enum): 5: SplaInvalidState, 6: SplaInvalidArgument, 7: SplaNoValue, + 8: SplaCompilationError, + 9: SplaConfigError, 1024: SplaNotImplemented, } +def _status_name(status): + return "OK" if status == 0 else _status_mapping.get(status, SplaError).__name__ + + +def _raise_status(status, message, fallback=SplaError): + """ + Raise the exception matching a C status code and keep the numeric status on it. + + :param status: + Status returned by the C library. + :param message: + Human-readable operation description. + :param fallback: + Exception type used for generic or unknown statuses. + """ + exc_type = _status_mapping.get(status, fallback) + if exc_type is SplaError: + exc_type = fallback + exc = exc_type(f"{message} (status {status})") + exc.status = status + raise exc + + def load_library(lib_path): global _spla global _int @@ -191,6 +239,12 @@ def load_library(lib_path): _spla.spla_Library_finalize.argtypes = [] _spla.spla_Library_initialize.restype = _status_t _spla.spla_Library_initialize.argtypes = [] + _spla.spla_Configure.restype = _status_t + _spla.spla_Configure.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)] + _spla.spla_Config_help.restype = None + _spla.spla_Config_help.argtypes = [] + _spla.spla_Config_version.restype = None + _spla.spla_Config_version.argtypes = [] _spla.spla_Library_set_accelerator.restype = _status_t _spla.spla_Library_set_accelerator.argtypes = [_enum_t] _spla.spla_Library_set_platform.restype = _status_t @@ -203,6 +257,10 @@ def load_library(lib_path): _spla.spla_Library_set_message_callback.argtypes = [_callback_t, ctypes.c_void_p] _spla.spla_Library_set_default_callback.restype = _status_t _spla.spla_Library_set_default_callback.argtypes = [] + _spla.spla_Library_initialize.restype = _status_t + _spla.spla_Library_initialize.argtypes = [] + _spla.spla_Library_get_acceleration_status.restype = _status_t + _spla.spla_Library_get_acceleration_status.argtypes = [] _spla.spla_Library_get_accelerator_info.restype = _status_t _spla.spla_Library_get_accelerator_info.argtypes = [ctypes.c_char_p, _int] @@ -612,14 +670,50 @@ def load_library(lib_path): def default_callback(status, msg, file, function, line, user_data): decoded_msg = msg.decode("utf-8") - decoded_file = file.decode("utf-8") - print(f"pyspla: [{decoded_file}:{line}] {_status_mapping[status]}: {decoded_msg}") + print(f"pyspla: {_status_name(status)}: {decoded_msg}") + + +def configure(): + """Configure spla from command line arguments""" + if _spla: + args = sys.argv[1:] + argc = len(args) + argv = (ctypes.c_char_p * argc)(*[arg.encode("utf-8") for arg in args]) + status = _spla.spla_Configure(argc, argv) + + if status != 0: + _raise_status(status, "spla configuration failed", SplaFailedConfigure) def init_library(): if _spla: - if _spla.spla_Library_initialize() != 0: - raise SplaFailedInitialize + status = _spla.spla_Library_initialize() + if status != 0: + _raise_status(status, "spla initialization failed", SplaFailedInitialize) + + +def acceleration_status(): + """ + Return the current accelerator initialization status. + + Status 0 means that an accelerated backend was initialized successfully, + status 2 means that the library works on pure CPU, and other values are + initialization errors (see the corresponding Spla* exceptions). + + :return: Status code returned by the C library or None if it is not loaded. + """ + if _spla: + return int(_spla.spla_Library_get_acceleration_status()) + return None + + +def has_acceleration(): + """ + Check whether an accelerated backend is currently used. + + :return: True if an accelerator was initialized successfully. + """ + return acceleration_status() == 0 def finalize(): @@ -656,6 +750,15 @@ def initialize(): raise Exception(f"no compiled spla file {TARGET} to load") load_library(_spla_path) + + if "--spla-help" in sys.argv[1:]: + _spla.spla_Config_help() + sys.exit(0) + if "--spla-version" in sys.argv[1:]: + _spla.spla_Config_version() + sys.exit(0) + + configure() init_library() _default_callback = _callback_t(default_callback) @@ -678,7 +781,7 @@ def check(status): Status to check. """ if status != 0: - raise _status_mapping[status] + _raise_status(status, "spla call failed") def is_docs(): diff --git a/python/pyspla/spla_conf.json b/python/pyspla/spla_conf.json new file mode 100644 index 000000000..6f17d8085 --- /dev/null +++ b/python/pyspla/spla_conf.json @@ -0,0 +1,12 @@ +{ + "default": { + "backend": "opencl_gpu", + "if_opencl_unavailable": "use_cpu", + + "queues_count": 1, + "profiling": false, + "allocator_type": "general", + + "verbosity": 2 + } +} diff --git a/python/setup.py b/python/setup.py index 695e42f8a..ddf51b11c 100644 --- a/python/setup.py +++ b/python/setup.py @@ -73,7 +73,7 @@ def get_lib_names(): ], packages=["pyspla"], package_dir={'': '.'}, - package_data={'': get_lib_names()}, + package_data={'': get_lib_names() + ["spla_conf.json"]}, python_requires=">=3.0", include_package_data=True ) diff --git a/src/array.cpp b/src/array.cpp index 1ab17cc61..a06d1677d 100644 --- a/src/array.cpp +++ b/src/array.cpp @@ -31,13 +31,13 @@ namespace spla { ref_ptr Array::make(uint n_values, const ref_ptr& type) { + Library::get(); + if (!type) { LOG_MSG(Status::InvalidArgument, "passed null type"); return ref_ptr{}; } - Library::get(); - if (type == INT) { return ref_ptr(new TArray(n_values)); } diff --git a/src/binding/c_config.hpp b/src/binding/c_config.hpp index 4e1302570..1f6a30e1c 100644 --- a/src/binding/c_config.hpp +++ b/src/binding/c_config.hpp @@ -28,6 +28,7 @@ #ifndef SPLA_C_CONFIG_HPP #define SPLA_C_CONFIG_HPP +#include #include #include @@ -48,6 +49,31 @@ static spla_Status to_c_status(spla::Status status) { return static_cast(status); } +static spla_Status to_c_status(spla::ConfigStatus status) { + switch (status) { + case spla::ConfigStatus::Ok: + case spla::ConfigStatus::HelpRequested: + case spla::ConfigStatus::VersionRequested: + return SPLA_STATUS_OK; + case spla::ConfigStatus::CliOrEnvParseError: + case spla::ConfigStatus::ConfigurationNotFound: + case spla::ConfigStatus::ConfigurationCycle: + case spla::ConfigStatus::MissedParameters: + case spla::ConfigStatus::InvalidConfigParams: + return SPLA_STATUS_INVALID_ARGUMENT; + case spla::ConfigStatus::FileNotExists: + case spla::ConfigStatus::OpenFileError: + case spla::ConfigStatus::ParseConfError: + return SPLA_STATUS_ERROR; + case spla::ConfigStatus::PlatformNotFound: + return SPLA_STATUS_PLATFORM_NOT_FOUND; + case spla::ConfigStatus::DeviceNotFound: + return SPLA_STATUS_DEVICE_NOT_FOUND; + default: + return SPLA_STATUS_ERROR; + } +} + static spla::AcceleratorType from_c_accelerator_type(spla_AcceleratorType accelerator) { return static_cast(accelerator); } diff --git a/src/binding/c_library.cpp b/src/binding/c_library.cpp index 9f907a8d6..92a64c034 100644 --- a/src/binding/c_library.cpp +++ b/src/binding/c_library.cpp @@ -26,12 +26,51 @@ /**********************************************************************************/ #include "c_config.hpp" +#include + +#include void spla_Library_finalize() { spla::Library::get()->finalize(); } spla_Status spla_Library_initialize() { - return spla::Library::get() ? SPLA_STATUS_OK : SPLA_STATUS_ERROR; + auto* lib = spla::Library::get(); + if (!lib || !spla::config_initialized) return SPLA_STATUS_ERROR; + + const spla::Status accel_status = lib->get_acceleration_status(); + if (accel_status != spla::Status::Ok && accel_status != spla::Status::NoAcceleration) { + return to_c_status(accel_status); + } + + return SPLA_STATUS_OK; +} + +spla_Status spla_Library_get_acceleration_status() { + return to_c_status(spla::Library::get()->get_acceleration_status()); +} + +spla_Status spla_Configure(int argc, char** argv) { + const spla::ConfigStatus config_status = spla::make_config_final(argc, argv); + + if (config_status == spla::ConfigStatus::HelpRequested || + config_status == spla::ConfigStatus::VersionRequested) + return SPLA_STATUS_OK; + + if (config_status != spla::ConfigStatus::Ok) + return to_c_status(config_status); + + return to_c_status(spla::Library::get()->apply_configuration()); +} + +void spla_Config_help() { + char arg0[] = "spla"; + char arg1[] = "--spla-help"; + char* argv[] = {arg0, arg1, nullptr}; + spla::make_config_final(2, argv); +} + +void spla_Config_version() { + std::cout << spla::get_spla_version() << std::endl; } spla_Status spla_Library_set_accelerator(spla_AcceleratorType accelerator) { return to_c_status(spla::Library::get()->set_accelerator(from_c_accelerator_type(accelerator))); diff --git a/src/core/config_status.hpp b/src/core/config_status.hpp new file mode 100644 index 000000000..b1260f333 --- /dev/null +++ b/src/core/config_status.hpp @@ -0,0 +1,48 @@ +/**********************************************************************************/ +/* This file is part of spla project */ +/* https://github.com/JetBrains-Research/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 + +namespace spla { + + enum ConfigStatus { + Ok, + HelpRequested, + VersionRequested, + CliOrEnvParseError, + FileNotExists, + OpenFileError, + ParseConfError, + ConfigurationNotFound, + ConfigurationCycle, + MissedParameters, + PlatformNotFound, + DeviceNotFound, + InvalidConfigParams + }; + +}// namespace spla \ No newline at end of file diff --git a/src/core/configure.cpp b/src/core/configure.cpp new file mode 100644 index 000000000..1bcb24865 --- /dev/null +++ b/src/core/configure.cpp @@ -0,0 +1,593 @@ +/**********************************************************************************/ +/* 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 "configure.hpp" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include +#else + #include +#endif + +namespace spla { + + ConfigFile config_default; + ConfigFile config_system; + ConfigFile config_user; + ConfigFile config_explicit; + CliMeta config_cli_meta; + Configuration config_cli_and_env; + Configuration config_final; + bool config_initialized = false; + + + void Configuration::merge(const Configuration& source) { + if (source.extends.has_value()) extends = source.extends; + if (source.backend.has_value()) backend = source.backend; + if (source.platform_index.has_value()) platform_index = source.platform_index; + if (source.device_index.has_value()) device_index = source.device_index; + if (source.if_opencl_unavailable.has_value()) if_opencl_unavailable = source.if_opencl_unavailable; + if (source.queues_count.has_value()) queues_count = source.queues_count; + if (source.profiling.has_value()) profiling = source.profiling; + if (source.allocator_type.has_value()) allocator_type = source.allocator_type; + if (source.linear_alloc_size.has_value()) linear_alloc_size = source.linear_alloc_size; + if (source.verbosity.has_value()) verbosity = source.verbosity; + } + + void Configuration::reset() { + *this = Configuration{}; + } + + + void ConfigFile::merge(const ConfigFile& source) { + if (!source.configs.has_value()) return; + if (!configs.has_value()) configs.emplace(); + + for (const auto& [name, source_config] : *source.configs) { + (*configs)[name].merge(source_config); + } + } + + void ConfigFile::reset() { + configs.reset(); + } + + void CliMeta::reset() { + *this = CliMeta{}; + } + + + std::string get_spla_version() { +#ifdef SPLA_VERSION + return "SPLA version: " SPLA_VERSION; +#else + return "SPLA version: unknown (SPLA_VERSION not defined at build time)"; +#endif + } + + + std::string get_default_config_path() { +#ifdef _WIN32 + HMODULE hModule = nullptr; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + reinterpret_cast(&get_default_config_path), &hModule); + char path[MAX_PATH]; + GetModuleFileNameA(hModule, path, MAX_PATH); + std::string dir = path; + dir = dir.substr(0, dir.find_last_of("\\/")); + return dir + "\\spla_conf.json"; +#elif defined(__APPLE__) || defined(__linux__) + Dl_info info; + dladdr((void*) &get_default_config_path, &info); + std::string path = info.dli_fname; + std::string dir = path.substr(0, path.find_last_of("/")); + return dir + "/spla_conf.json"; +#else + #error "spla supports only Windows, macOS and Linux" +#endif + } + + std::string get_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_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, CliMeta& meta, Configuration& cfg) { + CLI::App app{"SPLA configuration"}; + app.name("spla"); + + app.allow_extras(); + + app.set_help_flag("--spla-help", "Show help and exit"); + app.add_flag("--spla-version", meta.version, "Show version and exit"); + + app.add_option("--spla-config", meta.configuration, + "Name of the configuration to apply.\n" + "If not specified, the configuration named 'default' is used.\n" + "Config key: configuration") + ->envname("SPLA_CONFIG"); + + app.add_option("--spla-config-file", meta.path_to_conf, + "Path to a configuration file to load explicitly.\n" + "Has higher priority than system and user configuration files,\n" + "but lower than command-line arguments and environment variables.\n" + "Config key: path_to_conf") + ->envname("SPLA_CONFIG_FILE"); + + app.add_option("--spla-backend", cfg.backend, + "Device type to use:\n" + " cpu - pure native CPU (no OpenCL)\n" + " opencl_gpu - only GPU devices via OpenCL\n" + " by_index - select by platform_index and device_index\n" + "Config key: backend") + ->envname("SPLA_BACKEND"); + + app.add_option("--spla-platform-index", cfg.platform_index, + "OpenCL platform index\n" + "Used only with backend=by_index.\n" + "Config key: platform_index") + ->envname("SPLA_PLATFORM_INDEX"); + + app.add_option("--spla-device-index", cfg.device_index, + "OpenCL device index\n" + "Used only with backend=by_index.\n" + "Config key: device_index") + ->envname("SPLA_DEVICE_INDEX"); + + app.add_option("--spla-if-opencl-unavailable", cfg.if_opencl_unavailable, + "Behavior when OpenCL acceleration is unavailable:\n" + " use_cpu - fall back to pure CPU\n" + " abort - return error\n" + "Config key: if_opencl_unavailable") + ->envname("SPLA_IF_OPENCL_UNAVAILABLE"); + + app.add_option("--spla-queues-count", cfg.queues_count, + "Number of command queues\n" + "Config key: queues_count") + ->envname("SPLA_QUEUES_COUNT"); + + app.add_option("--spla-profiling", cfg.profiling, + "Enable profiling of command queues: true or false\n" + "Config key: profiling") + ->envname("SPLA_PROFILING"); + + app.add_option("--spla-allocator-type", cfg.allocator_type, + "Allocator type:\n" + " general\n" + " linear\n" + "Config key: allocator_type") + ->envname("SPLA_ALLOCATOR_TYPE"); + + app.add_option("--spla-linear-alloc-size", cfg.linear_alloc_size, + "Linear allocator size in bytes\n" + "Required for allocator_type=linear. Ignored for general.\n" + "Config key: linear_alloc_size") + ->envname("SPLA_LINEAR_ALLOC_SIZE"); + + app.add_option("--spla-verbosity", cfg.verbosity, + "Verbosity level:\n" + " 0 - no output\n" + " 1 - errors only\n" + " 2 - errors and warnings\n" + " 3 - all messages\n" + "Config key: verbosity") + ->envname("SPLA_VERBOSITY"); + + std::vector args; + if (argv != nullptr && argc > 0) { + args.reserve(static_cast(argc)); + for (int i = argc - 1; i >= 0; --i) { + args.emplace_back(argv[i]); + } + } + + try { + app.parse(std::move(args)); + } catch (const CLI::CallForHelp& e) { + std::cout << app.help() << std::endl; + return ConfigStatus::HelpRequested; + } catch (const CLI::ParseError& e) { + std::cerr << "[spla:cli_env] ERROR: failed to parse: " << e.what() << std::endl; + return ConfigStatus::CliOrEnvParseError; + } + + if (meta.version.value_or(false)) { + std::cout << get_spla_version() << std::endl; + return ConfigStatus::VersionRequested; + } + + return ConfigStatus::Ok; + } + + + ConfigStatus parse_file(const std::string& path, ConfigFile& cfg, bool allow_default) { + std::ifstream file(path); + if (!file.is_open()) { + std::cerr << "[spla:config] ERROR: cannot open: " << path << std::endl; + return ConfigStatus::OpenFileError; + } + + try { + nlohmann::json data = nlohmann::json::parse(file); + if (!cfg.configs.has_value()) cfg.configs.emplace(); + + for (auto& [name, value] : data.items()) { + if (!allow_default && name == "default") { + std::cerr << "[spla:config] ERROR: '" << path << "': " + << "configuration 'default' can only be defined in the library configuration file" + << std::endl; + return ConfigStatus::ParseConfError; + } + + Configuration config; + if (value.contains("extends")) config.extends = value["extends"].get>(); + if (value.contains("backend")) config.backend = value["backend"].get(); + if (value.contains("platform_index")) config.platform_index = value["platform_index"].get(); + if (value.contains("device_index")) config.device_index = value["device_index"].get(); + if (value.contains("if_opencl_unavailable")) config.if_opencl_unavailable = value["if_opencl_unavailable"].get(); + if (value.contains("queues_count")) config.queues_count = value["queues_count"].get(); + if (value.contains("profiling")) config.profiling = value["profiling"].get(); + if (value.contains("allocator_type")) config.allocator_type = value["allocator_type"].get(); + if (value.contains("linear_alloc_size")) { + const std::int64_t size = value["linear_alloc_size"].get(); + if (size <= 0) { + std::cerr << "[spla:config] ERROR: '" << path << "': " + << "linear_alloc_size must be > 0 (got " << size << ")" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + config.linear_alloc_size = static_cast(size); + } + if (value.contains("verbosity")) config.verbosity = value["verbosity"].get(); + + (*cfg.configs)[name].merge(config); + } + + 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; + } + } + + + Configuration apply_extends(const std::map& configs, + const std::string& name, + std::set& stack) { + if (stack.count(name)) { + throw std::runtime_error("extends cycle detected: " + name); + } + stack.insert(name); + + auto it = configs.find(name); + if (it == configs.end()) { + throw std::runtime_error("extends configuration not found: " + name); + } + + const Configuration& source = it->second; + Configuration result; + + if (source.extends.has_value()) { + for (const auto& parent_name : *source.extends) { + result.merge(apply_extends(configs, parent_name, stack)); + } + } + result.merge(source); + stack.erase(name); + return result; + } + + ConfigStatus apply_configuration(const ConfigFile& file, const std::string& name, Configuration& cfg) { + + std::set stack; + try { + cfg = apply_extends(*file.configs, name, stack); + } catch (const std::runtime_error& e) { + const std::string msg = e.what(); + if (msg.find("cycle") != std::string::npos) { + std::cerr << "[spla:configure] ERROR: extends cycle detected in configuration '" + << name << "'" << std::endl; + return ConfigStatus::ConfigurationCycle; + } + std::cerr << "[spla:configure] ERROR: " << msg << std::endl; + return ConfigStatus::ConfigurationNotFound; + } + + return ConfigStatus::Ok; + } + + + ConfigStatus validation(const Configuration& cfg) { + if (!cfg.backend.has_value()) { + std::cerr << "[spla:validation] ERROR: required parameter missing: backend" << std::endl; + return ConfigStatus::MissedParameters; + } + + const std::string& backend = *cfg.backend; + if (backend != "opencl_gpu" && backend != "cpu" && backend != "by_index") { + std::cerr << "[spla:validation] ERROR: backend must be " + << "'opencl_gpu', 'cpu' or 'by_index' (got '" << backend << "')" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + + if (backend == "by_index") { + if (!cfg.platform_index.has_value()) { + std::cerr << "[spla:validation] ERROR: platform_index is required " + << "when backend='by_index'" << std::endl; + return ConfigStatus::MissedParameters; + } + if (!cfg.device_index.has_value()) { + std::cerr << "[spla:validation] ERROR: device_index is required " + << "when backend='by_index'" << std::endl; + return ConfigStatus::MissedParameters; + } + + if (*cfg.platform_index < 0) { + std::cerr << "[spla:validation] ERROR: platform_index must be >= 0 (got " + << *cfg.platform_index << ")" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + if (*cfg.device_index < 0) { + std::cerr << "[spla:validation] ERROR: device_index must be >= 0 (got " + << *cfg.device_index << ")" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + } else if (cfg.platform_index.has_value()) { + std::cerr << "[spla:validation] WARNING: platform_index: " << *cfg.platform_index + << " ignored for backend='" << backend << "'" << std::endl; + + } else if (cfg.device_index.has_value()) { + std::cerr << "[spla:validation] WARNING: device_index " << *cfg.device_index + << "ignored for backend='" << backend << "'" << std::endl; + } + + + if (backend != "cpu" && !cfg.if_opencl_unavailable.has_value()) { + std::cerr << "[spla:validation] ERROR: if_gpu_unavailable is required " + << "when backend=" << backend << std::endl; + return ConfigStatus::MissedParameters; + } + if (cfg.if_opencl_unavailable.has_value()) { + const std::string& behavior = *cfg.if_opencl_unavailable; + if (behavior != "use_cpu" && behavior != "abort") { + std::cerr << "[spla:validation] ERROR: if_opencl_unavailable must be " + << "'use_cpu' or 'abort' (got '" << behavior << "')" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + if (backend == "cpu") { + std::cerr << "[spla:validation] WARNING: if_opencl_unavailable " + << "ignored for backend='cpu'" << std::endl; + } + } + + + if (!cfg.queues_count.has_value()) { + std::cerr << "[spla:validation] ERROR: required parameter missing: queues_count" << std::endl; + return ConfigStatus::MissedParameters; + } + if (*cfg.queues_count < 1) { + std::cerr << "[spla:validation] ERROR: queues_count must be >= 1 (got " + << *cfg.queues_count << ")" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + + + if (!cfg.profiling.has_value()) { + std::cerr << "[spla:validation] ERROR: required parameter missing: profiling" << std::endl; + return ConfigStatus::MissedParameters; + } + + + if (!cfg.allocator_type.has_value()) { + std::cerr << "[spla:validation] ERROR: required parameter missing: allocator_type" << std::endl; + return ConfigStatus::MissedParameters; + } + if (*cfg.allocator_type != "general" && *cfg.allocator_type != "linear") { + std::cerr << "[spla:validation] ERROR: allocator_type must be " + << "'general' or 'linear' (got '" << *cfg.allocator_type << "')" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + + if (*cfg.allocator_type == "linear") { + if (!cfg.linear_alloc_size.has_value()) { + std::cerr << "[spla:validation] ERROR: linear_alloc_size is required " + << "when allocator_type='linear'" << std::endl; + return ConfigStatus::MissedParameters; + } + if (*cfg.linear_alloc_size == 0) { + std::cerr << "[spla:validation] ERROR: linear_alloc_size must be > 0" << std::endl; + return ConfigStatus::InvalidConfigParams; + } + } else if (cfg.linear_alloc_size.has_value()) { + std::cerr << "[spla:validation] WARNING: linear_alloc_size " + << "ignored for allocator_type='" << *cfg.allocator_type << "'" << std::endl; + } + + + if (!cfg.verbosity.has_value()) { + std::cerr << "[spla:validation] ERROR: required parameter missing: verbosity" << std::endl; + return ConfigStatus::MissedParameters; + } + 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; + } + + + ConfigStatus make_config_final(int argc, char** argv) { + config_default.reset(); + config_system.reset(); + config_user.reset(); + config_explicit.reset(); + config_cli_meta.reset(); + config_cli_and_env.reset(); + config_final.reset(); + config_initialized = false; + + ConfigStatus status; + + status = parse_cli_and_env(argc, argv, config_cli_meta, config_cli_and_env); + if (status != ConfigStatus::Ok) return status; + + std::string path; + + path = get_default_config_path(); + if (!std::filesystem::exists(path)) { + std::cerr << "[spla:configure] ERROR: default configuration file not found: " << path << std::endl; + return ConfigStatus::FileNotExists; + } + status = parse_file(path, config_default, true); + if (status != ConfigStatus::Ok) { + std::cerr << "[spla:configure] ERROR: failed to load default configuration file: " << path << std::endl; + return status; + } + + + path = get_system_config_path(); + if (std::filesystem::exists(path)) { + status = parse_file(path, config_system, false); + if (status != ConfigStatus::Ok) { + std::cerr << "[spla:configure] ERROR: failed to load system configuration file: " << path << std::endl; + return status; + } + } + + + path = get_user_config_path(); + if (std::filesystem::exists(path)) { + status = parse_file(path, config_user, false); + if (status != ConfigStatus::Ok) { + std::cerr << "[spla:configure] ERROR: failed to load user configuration file: " << path << std::endl; + return status; + } + } + + + if (config_cli_meta.path_to_conf.has_value()) { + path = *config_cli_meta.path_to_conf; + if (!std::filesystem::exists(path)) { + std::cerr << "[spla:configure] ERROR: configuration file not found: " << path << std::endl; + return ConfigStatus::FileNotExists; + } + status = parse_file(path, config_explicit, false); + if (status != ConfigStatus::Ok) { + std::cerr << "[spla:configure] ERROR: failed to load configuration file: " << path << std::endl; + return status; + } + } + + ConfigFile merged; + merged.merge(config_default); + merged.merge(config_system); + merged.merge(config_user); + merged.merge(config_explicit); + + const std::string active = config_cli_meta.configuration.value_or("default"); + + status = apply_configuration(merged, active, config_final); + if (status != ConfigStatus::Ok) return status; + + config_final.merge(config_cli_and_env); + + status = validation(config_final); + if (status != ConfigStatus::Ok) return status; + + config_initialized = true; + return ConfigStatus::Ok; + } + +}// namespace spla diff --git a/src/core/configure.hpp b/src/core/configure.hpp new file mode 100644 index 000000000..79a7c2399 --- /dev/null +++ b/src/core/configure.hpp @@ -0,0 +1,108 @@ +/**********************************************************************************/ +/* 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 "config_status.hpp" +#include + +#ifdef _WIN32 +#elif defined(__APPLE__) || defined(__linux__) || defined(__unix__) + #include + #include +#endif + +namespace spla { + + struct Configuration { + std::optional> extends; + + std::optional backend; + std::optional platform_index; + std::optional device_index; + std::optional if_opencl_unavailable; + + std::optional queues_count; + std::optional profiling; + std::optional allocator_type; + std::optional linear_alloc_size; + std::optional verbosity; + + void merge(const Configuration& source); + void reset(); + }; + + struct ConfigFile { + std::optional> configs; + + void merge(const ConfigFile& source); + void reset(); + }; + + struct CliMeta { + std::optional version; + std::optional configuration; + std::optional path_to_conf; + void reset(); + }; + + extern ConfigFile config_default; + extern ConfigFile config_system; + extern ConfigFile config_user; + extern ConfigFile config_explicit; + extern CliMeta config_cli_meta; + extern Configuration config_cli_and_env; + extern Configuration config_final; + extern bool config_initialized; + + std::string get_spla_version(); + + std::string get_default_config_path(); + std::string get_system_config_path(); + std::string get_home_directory(); + std::string get_user_config_path(); + + ConfigStatus parse_cli_and_env(int argc, char** argv, CliMeta& meta, Configuration& cfg); + ConfigStatus parse_file(const std::string& path, ConfigFile& cfg, bool allow_default = false); + + Configuration apply_extends(const std::map& configs, + const std::string& name, + std::set& stack); + ConfigStatus apply_configuration(const ConfigFile& file, const std::string& name, Configuration& cfg); + + ConfigStatus validation(const Configuration& cfg); + + ConfigStatus make_config_final(int argc, char** argv); + +}// namespace spla diff --git a/src/core/logger.cpp b/src/core/logger.cpp index 8fab9010d..00852ab05 100644 --- a/src/core/logger.cpp +++ b/src/core/logger.cpp @@ -31,10 +31,41 @@ namespace spla { - void Logger::log_msg(Status status, const std::string& msg, const std::string& file, const std::string& function, int line) { - std::lock_guard guard(m_mutex); + 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) { + + std::lock_guard guard(m_mutex); + + if (!m_verbosity.has_value()) return; + + int level = status_to_level(status); + if (level > *m_verbosity) return; + 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 +73,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..c1344f6c8 100644 --- a/src/core/logger.hpp +++ b/src/core/logger.hpp @@ -32,6 +32,7 @@ #include #include +#include #include namespace spla { @@ -49,10 +50,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; + std::optional m_verbosity; mutable std::mutex m_mutex; }; @@ -62,22 +64,11 @@ namespace spla { }// namespace spla -#ifndef SPLA_RELEASE - #define LOG_MSG(status, msg) \ - do { \ - std::stringstream __ss; \ - __ss << msg; \ - Library::get()->get_logger()->log_msg(status, __ss.str(), __FILE__, __FUNCTION__, static_cast(__LINE__)); \ - } while (false); -#else - #define LOG_MSG(status, msg) \ - do { \ - if ((status) != Status::Ok) { \ - std::stringstream __ss; \ - __ss << msg; \ - Library::get()->get_logger()->log_msg(status, __ss.str(), __FILE__, __FUNCTION__, static_cast(__LINE__)); \ - } \ - } while (false); -#endif +#define LOG_MSG(status, msg) \ + do { \ + std::stringstream __ss; \ + __ss << msg; \ + Library::get()->get_logger()->log_msg(status, __ss.str(), __FILE__, __FUNCTION__, static_cast(__LINE__)); \ + } while (false); #endif//SPLA_LOGGER_HPP diff --git a/src/library.cpp b/src/library.cpp index d0a5494a1..e9a3d3a5b 100644 --- a/src/library.cpp +++ b/src/library.cpp @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -35,6 +36,7 @@ #include +#include #include #if defined(SPLA_BUILD_OPENCL) @@ -46,6 +48,30 @@ namespace spla { + static Status to_status(ConfigStatus status) { + switch (status) { + case ConfigStatus::Ok: + case ConfigStatus::HelpRequested: + case ConfigStatus::VersionRequested: + return Status::Ok; + case ConfigStatus::CliOrEnvParseError: + case ConfigStatus::ConfigurationNotFound: + case ConfigStatus::ConfigurationCycle: + case ConfigStatus::MissedParameters: + case ConfigStatus::InvalidConfigParams: + return Status::InvalidArgument; + case ConfigStatus::PlatformNotFound: + return Status::PlatformNotFound; + case ConfigStatus::DeviceNotFound: + return Status::DeviceNotFound; + case ConfigStatus::FileNotExists: + case ConfigStatus::OpenFileError: + case ConfigStatus::ParseConfError: + default: + return Status::Error; + } + } + static const char* to_string(Status status) { #define STATUS_MAP(value) \ case Status::value: \ @@ -101,6 +127,73 @@ namespace spla { LOG_MSG(Status::Ok, "release accelerator: " << m_accelerator->get_name()); m_accelerator.reset(); } + m_acceleration_status = Status::NoAcceleration; + } + + Status Library::configure(int argc, char** argv) { + ConfigStatus config_status = spla::make_config_final(argc, argv); + + if (config_status == ConfigStatus::HelpRequested || config_status == ConfigStatus::VersionRequested) { + return Status::Ok; + } + if (config_status != ConfigStatus::Ok) { + return to_status(config_status); + } + + return apply_configuration(); + } + + Status Library::get_acceleration_status() { + return m_acceleration_status; + } + + Status Library::apply_configuration() { + if (!spla::config_initialized) { + ConfigStatus config_status = spla::make_config_final(0, nullptr); + if (config_status != ConfigStatus::Ok) { + return to_status(config_status); + } + } + + set_verbosity(spla::config_final.verbosity.value_or(2)); + set_default_callback(); + + const std::string backend = spla::config_final.backend.value_or("opencl_gpu"); + + if (backend == "cpu") { + return set_accelerator(AcceleratorType::None); + } + +#if defined(SPLA_BUILD_OPENCL) + Status status = set_accelerator(AcceleratorType::OpenCL); + + if (status != Status::Ok) { + const std::string if_unavail = spla::config_final.if_opencl_unavailable.value_or("abort"); + if (if_unavail == "use_cpu") { + LOG_MSG(Status::NoAcceleration, + "OpenCL acceleration is unavailable (status: " + << static_cast(status) + << "), falling back to pure CPU computations"); + set_accelerator(AcceleratorType::None); + return Status::Ok; + } + LOG_MSG(status, + "OpenCL acceleration is unavailable (status: " + << static_cast(status) + << "), configuration requested abort"); + return status; + } + return Status::Ok; +#else + const std::string if_unavail = spla::config_final.if_opencl_unavailable.value_or("abort"); + if (if_unavail == "use_cpu") { + set_accelerator(AcceleratorType::None); + return Status::Ok; + } + LOG_MSG(Status::Error, + "OpenCL support not built, but OpenCL backend requested"); + return Status::Error; +#endif } Status Library::set_accelerator(AcceleratorType accelerator) { @@ -108,20 +201,25 @@ namespace spla { if (accelerator == AcceleratorType::OpenCL) { m_accelerator = std::make_unique(); - if (m_accelerator->init() != Status::Ok) { + Status status = m_accelerator->init(); + if (status != Status::Ok) { m_accelerator.reset(); - return Status::NoAcceleration; + m_acceleration_status = status; + return status; } + m_acceleration_status = Status::Ok; return Status::Ok; } #endif if (accelerator == AcceleratorType::None) { LOG_MSG(Status::Ok, "disable acceleration"); m_accelerator.reset(); + m_acceleration_status = Status::NoAcceleration; return Status::Ok; } + m_acceleration_status = Status::NoAcceleration; return Status::NoAcceleration; } @@ -143,21 +241,20 @@ 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, - const std::string& file, - const std::string& function, - int line) { - std::stringstream to_output; - - to_output << "[" << file << ":" << line << "] " - << to_string(status) << ": " << msg << std::endl; - - if (status == Status::Ok) - std::cout << to_output.str(); - else - std::cerr << to_output.str(); + auto callback = [](spla::Status status, + const std::string& msg, + [[maybe_unused]] const std::string& file, + [[maybe_unused]] const std::string& function, + [[maybe_unused]] int line) { + std::ostream& out = (status == Status::Ok) ? std::cout : std::cerr; + out << to_string(status) << ": " << msg << std::endl; }; return set_message_callback(callback); } @@ -221,20 +318,20 @@ namespace spla { if (!g_library) { g_library = std::make_unique(); -#ifndef SPLA_RELEASE - // In debug mode we automatically set default callback - // to get log (error) messages from library start-up - g_library->set_default_callback(); -#endif + if (!spla::config_initialized) { + ConfigStatus status = spla::make_config_final(0, nullptr); -#ifdef SPLA_BUILD_OPENCL - // On init we by default attempt to set up OpenCL runtime - // If setup is failed error is ignored and library fallbacks to CPU computations only - g_library->set_accelerator(AcceleratorType::OpenCL); -#endif + if (status == ConfigStatus::Ok) { + g_library->apply_configuration(); + } else { + std::cerr << "failed to apply SPLA configuration (status: " + << static_cast(status) + << "); continuing with pure CPU computations" << std::endl; + } + } } return g_library.get(); } -}// namespace spla \ No newline at end of file +}// namespace spla diff --git a/src/matrix.cpp b/src/matrix.cpp index 4ecab16aa..331e95dd1 100644 --- a/src/matrix.cpp +++ b/src/matrix.cpp @@ -31,6 +31,8 @@ namespace spla { ref_ptr Matrix::make(uint n_rows, uint n_cols, const ref_ptr& type) { + Library::get(); + if (n_rows <= 0 || n_cols <= 0) { LOG_MSG(Status::InvalidArgument, "passed 0 dim"); return ref_ptr{}; @@ -40,8 +42,6 @@ namespace spla { return ref_ptr{}; } - Library::get(); - if (type == INT) { return ref_ptr(new TMatrix(n_rows, n_cols)); } diff --git a/src/opencl/cl_accelerator.cpp b/src/opencl/cl_accelerator.cpp index 6a7a0517a..5912f5c2e 100644 --- a/src/opencl/cl_accelerator.cpp +++ b/src/opencl/cl_accelerator.cpp @@ -27,6 +27,8 @@ #include "cl_accelerator.hpp" +#include +#include #include #include #include @@ -36,71 +38,167 @@ namespace spla { + std::optional> find_first_device() { + std::vector platforms; +#if defined(CL_HPP_ENABLE_EXCEPTIONS) + try { + cl::Platform::get(&platforms); + } catch (const cl::Error& e) { + LOG_MSG(Status::PlatformNotFound, "failed to query OpenCL platforms: " << e.what()); + return std::nullopt; + } +#else + cl::Platform::get(&platforms); +#endif + if (platforms.empty()) { + LOG_MSG(Status::PlatformNotFound, "no OpenCL platforms available"); + return std::nullopt; + } + + for (size_t p = 0; p < platforms.size(); p++) { + std::vector devices; +#if defined(CL_HPP_ENABLE_EXCEPTIONS) + try { + platforms[p].getDevices(CL_DEVICE_TYPE_GPU, &devices); + } catch (const cl::Error& e) { + LOG_MSG(Status::DeviceNotFound, "failed to query devices of platform " << p << ": " << e.what()); + continue; + } +#else + platforms[p].getDevices(CL_DEVICE_TYPE_GPU, &devices); +#endif + if (!devices.empty()) { + return std::make_pair(static_cast(p), 0); + } + } + + return std::nullopt; + } + CLAccelerator::CLAccelerator() = default; CLAccelerator::~CLAccelerator() = default; Status CLAccelerator::init() { - m_description = "no platform or device"; + if (!config_initialized) { + ConfigStatus status = make_config_final(0, nullptr); + if (status != ConfigStatus::Ok) return Status::Error; + } - 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); + std::string backend = config_final.backend.value_or("opencl_gpu"); + if (backend == "cpu") { + return Status::NoAcceleration; + } - if (set_platform(platform_index) != Status::Ok) - return Status::PlatformNotFound; + int platform_index = 0; + int device_index = 0; + int queues_count = config_final.queues_count.value_or(1); + bool profiling = config_final.profiling.value_or(false); + std::string allocator_type = config_final.allocator_type.value_or("general"); + size_t linear_alloc_size = config_final.linear_alloc_size.value_or(0); + + if (backend == "opencl_gpu") { + auto found = find_first_device(); + + if (!found) { + LOG_MSG(Status::DeviceNotFound, "no OpenCL device found for backend='" << backend << "'"); + return Status::DeviceNotFound; + } + + platform_index = found->first; + device_index = found->second; + } else if (backend == "by_index") { + if (!config_final.platform_index.has_value() || !config_final.device_index.has_value()) { + LOG_MSG(Status::InvalidArgument, "platform_index and device_index are required for backend='by_index'"); + return Status::InvalidArgument; + } + platform_index = *config_final.platform_index; + device_index = *config_final.device_index; + } else { + LOG_MSG(Status::InvalidArgument, "unknown backend: '" << backend << "'"); + return Status::InvalidArgument; + } - if (set_device(device_index) != Status::Ok) - return Status::DeviceNotFound; + LOG_MSG(Status::Ok, "Initializing accelerator..."); + + LOG_MSG(Status::Ok, "Configuration parameters:"); + LOG_MSG(Status::Ok, " backend = " << backend); + 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= " << linear_alloc_size); + } + LOG_MSG(Status::Ok, " verbosity = " << config_final.verbosity.value()); + + Status status = set_platform(platform_index); + if (status != Status::Ok) return status; + + status = set_device(device_index); + if (status != Status::Ok) return status; - if (set_queues_count(1) != Status::Ok) - return Status::Error; + status = set_profiling(profiling); + if (status != Status::Ok) return status; + + status = set_queues_count(queues_count); + if (status != Status::Ok) return status; + + if (allocator_type == "linear") { + status = set_linear_allocator(linear_alloc_size); + } else { + status = set_general_allocator(); + } + if (status != Status::Ok) return status; 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"); + if (index < 0 || static_cast(index) >= available_platforms.size()) { + LOG_MSG(Status::PlatformNotFound, "no OpenCL platform with index " << index + << " (available: " << available_platforms.size() << ")"); 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"); + if (index < 0 || static_cast(index) >= available_devices.size()) { + LOG_MSG(Status::DeviceNotFound, "no OpenCL device with index " << index + << " (available: " << available_devices.size() << ")"); return Status::DeviceNotFound; } 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(); @@ -158,60 +256,84 @@ 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 Acc " + << 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) { + if (count < 1) { + LOG_MSG(Status::InvalidArgument, "queues count must be >= 1 (got " << count << ")"); + return Status::InvalidArgument; + } + m_context = cl::Context(m_device); m_queues.clear(); m_queues.reserve(count); 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(); + m_counter_pool = std::make_unique(); - if (!is_nvidia()) { - m_alloc_linear = std::make_unique(CLAllocLinear::DEFAULT_SIZE, m_addr_align); - m_alloc_tmp = m_alloc_linear.get(); + LOG_MSG(Status::Ok, "configure " << count << " queues" + << " (profiling: " << (m_profiling_enabled ? "ON" : "OFF") << ")"); + return Status::Ok; + } + + + Status CLAccelerator::set_linear_allocator(size_t size) { + if (size == 0) { + LOG_MSG(Status::InvalidArgument, "linear allocator size must be > 0 (got " << size << ")"); + return Status::InvalidArgument; } - LOG_MSG(Status::Ok, "configure " << count << " queues for computations"); + 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_name() { - return m_name; - } - const std::string& CLAccelerator::get_description() { - return m_description; - } - 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..13fd731e0 100644 --- a/src/opencl/cl_accelerator.hpp +++ b/src/opencl/cl_accelerator.hpp @@ -36,6 +36,7 @@ #include #include +#include #include #ifndef SPLA_RELEASE @@ -65,15 +66,20 @@ namespace spla { * @class CLAccelerator * @brief Single-device OpenCL acceleration implementation */ + class CLAccelerator final : public Accelerator { public: 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 +112,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 +137,7 @@ namespace spla { bool m_is_intel = false; bool m_is_img = false; - ankerl::svector m_queues; + bool m_profiling_enabled = false; }; /** @@ -148,4 +155,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/scalar.cpp b/src/scalar.cpp index e48ce958e..43946cac2 100644 --- a/src/scalar.cpp +++ b/src/scalar.cpp @@ -31,13 +31,13 @@ namespace spla { ref_ptr Scalar::make(const ref_ptr& type) { + Library::get(); + if (!type) { LOG_MSG(Status::InvalidArgument, "passed null type"); return ref_ptr{}; } - Library::get(); - if (type == INT) { return ref_ptr(new TScalar()); } diff --git a/src/vector.cpp b/src/vector.cpp index e6427d90f..d633263a9 100644 --- a/src/vector.cpp +++ b/src/vector.cpp @@ -31,6 +31,8 @@ namespace spla { ref_ptr Vector::make(uint n_rows, const ref_ptr& type) { + Library::get(); + if (n_rows <= 0) { LOG_MSG(Status::InvalidArgument, "passed 0 dim"); return ref_ptr{}; @@ -40,8 +42,6 @@ namespace spla { return ref_ptr{}; } - Library::get(); - if (type == INT) { return ref_ptr(new TVector(n_rows)); } diff --git a/tools/soft_check.cpp b/tools/soft_check.cpp new file mode 100644 index 000000000..0c590a00e --- /dev/null +++ b/tools/soft_check.cpp @@ -0,0 +1,132 @@ +/**********************************************************************************/ +/* 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 +#include +#include + +#include +#include + +namespace { + + const char* CHECK_TAG = "[spla:check]"; + + /** + * Runs a sanity check of the already configured environment: + * 1. verifies the accelerator initialization status; + * 2. runs a simple test task (2 + 3); + * 3. verifies the result. + * + * @return Exit code: 0 if everything is ready, 1 otherwise. + */ + int softcheck() { + using namespace spla; + + std::cout << CHECK_TAG << " Initializing..." << std::endl; + + Library::get()->set_default_callback(); + + if (spla_Library_initialize() != SPLA_STATUS_OK) { + std::cout << CHECK_TAG << " FAILED - no OpenCL platform/device available" << std::endl; + return 1; + } + + std::string info; + Library::get()->get_accelerator_info(info); + std::cout << CHECK_TAG << " Selected accelerator: " << info << std::endl; + + auto a = Vector::make(1, spla::INT); + auto b = Vector::make(1, spla::INT); + auto c = Vector::make(1, spla::INT); + + if (!a || !b || !c) { + std::cout << CHECK_TAG << " FAILED - cannot allocate test vectors" << std::endl; + return 1; + } + + a->set_int(0, 2); + b->set_int(0, 3); + + std::cout << CHECK_TAG << " Running test task: 2 + 3 = ..." << std::endl; + + if (exec_v_eadd(c, a, b, PLUS_INT) != Status::Ok) { + std::cout << CHECK_TAG << " FAILED - test task execution error" << std::endl; + return 1; + } + + spla::T_INT result = 0; + c->get_int(0, result); + std::cout << CHECK_TAG << " Result: " << result << " (expected 5)" << std::endl; + + if (result != 5) { + std::cout << CHECK_TAG << " FAILED - wrong result" << std::endl; + return 1; + } + + std::cout << CHECK_TAG << " SUCCESS - spla works correctly" << std::endl; + return 0; + } + +}// namespace + +int main(int argc, char** argv) { + + bool do_softcheck = false; + + CLI::App app{"spla - sparse linear algebra library"}; + app.description("Command-line tool for SPLA configuration and environment checks."); + app.add_flag("--softcheck", do_softcheck, "Run environment sanity check"); + + try { + app.parse(argc, argv); + } catch (const CLI::CallForHelp& e) { + std::cout << app.help() << std::endl; + return 0; + } catch (const CLI::ParseError& e) { + std::cerr << "[spla] ERROR: " << e.what() << std::endl + << std::endl; + std::cout << app.help() << std::endl; + return 1; + } + + if (do_softcheck) { + std::cout << CHECK_TAG << " Starting sanity check..." << std::endl; + } + + if (spla_Configure(0, nullptr) != SPLA_STATUS_OK) { + std::cout << CHECK_TAG << " FAILED - invalid configuration" << std::endl; + return 1; + } + + if (do_softcheck) { + return softcheck(); + } + + std::cout << CHECK_TAG << " Configuration applied successfully" << std::endl; + return 0; +}