From f6bfc98eb62aff7f45bef6aa77f52c4e229d026a Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 10 Sep 2026 12:02:25 +0000 Subject: [PATCH 1/4] Add C++ API reference to Sphinx docs --- .gitignore | 1 + .../all_cuda-129_arch-aarch64.yaml | 1 + .../all_cuda-129_arch-x86_64.yaml | 1 + .../all_cuda-133_arch-aarch64.yaml | 1 + .../all_cuda-133_arch-x86_64.yaml | 1 + cpp/Doxyfile.in | 2 +- dependencies.yaml | 1 + docs/source/conf.py | 52 +++++++++++++++++++ docs/source/cpp/genetic.rst | 5 ++ docs/source/cpp/index.rst | 11 ++++ docs/source/cpp/ml.rst | 5 ++ docs/source/cpp/mlcommon.rst | 5 ++ docs/source/index.rst | 1 + 13 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 docs/source/cpp/genetic.rst create mode 100644 docs/source/cpp/index.rst create mode 100644 docs/source/cpp/ml.rst create mode 100644 docs/source/cpp/mlcommon.rst diff --git a/.gitignore b/.gitignore index ae4c19b503..163f62534f 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ doxygen_check/ ## Doxygen cpp/html +cpp/xml cpp/Doxyfile # clang tooling diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 58428b1bc1..e6d24281ee 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -4,6 +4,7 @@ channels: - rapidsai-nightly - conda-forge dependencies: +- breathe - c-compiler - ccache - certifi diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 82339adaae..572f154249 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -4,6 +4,7 @@ channels: - rapidsai-nightly - conda-forge dependencies: +- breathe - c-compiler - ccache - certifi diff --git a/conda/environments/all_cuda-133_arch-aarch64.yaml b/conda/environments/all_cuda-133_arch-aarch64.yaml index eb16159951..4fbd3cc815 100644 --- a/conda/environments/all_cuda-133_arch-aarch64.yaml +++ b/conda/environments/all_cuda-133_arch-aarch64.yaml @@ -4,6 +4,7 @@ channels: - rapidsai-nightly - conda-forge dependencies: +- breathe - c-compiler - ccache - certifi diff --git a/conda/environments/all_cuda-133_arch-x86_64.yaml b/conda/environments/all_cuda-133_arch-x86_64.yaml index 7884d3eef9..e8551a8c31 100644 --- a/conda/environments/all_cuda-133_arch-x86_64.yaml +++ b/conda/environments/all_cuda-133_arch-x86_64.yaml @@ -4,6 +4,7 @@ channels: - rapidsai-nightly - conda-forge dependencies: +- breathe - c-compiler - ccache - certifi diff --git a/cpp/Doxyfile.in b/cpp/Doxyfile.in index 5486118f70..abb1c34acc 100644 --- a/cpp/Doxyfile.in +++ b/cpp/Doxyfile.in @@ -2011,7 +2011,7 @@ MAN_LINKS = NO # captures the structure of the code including all documentation. # The default value is: NO. -GENERATE_XML = NO +GENERATE_XML = YES # The XML_OUTPUT tag is used to specify where the XML pages will be put. If a # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of diff --git a/dependencies.yaml b/dependencies.yaml index f35cd7d6a6..a1047def81 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -526,6 +526,7 @@ dependencies: common: - output_types: [conda, requirements] packages: + - breathe - graphviz - ipython - ipykernel diff --git a/docs/source/conf.py b/docs/source/conf.py index 28a780d736..7b37ec7d88 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,9 +16,11 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import datetime +import glob import os import sys import textwrap +import xml.etree.ElementTree as ET from packaging.version import Version @@ -43,6 +45,7 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "breathe", "numpydoc", "sphinx.ext.autodoc", "sphinx.ext.autosummary", @@ -58,6 +61,55 @@ "sphinx_design", ] +breathe_projects = { + "cuml": os.path.abspath( + os.path.join(os.path.dirname(__file__), "../../cpp/xml") + ) +} +breathe_default_project = "cuml" + + +def clean_doxygen_xml(path: str) -> None: + # Doxygen 1.9.1 emits concepts and instantiations that Sphinx cannot parse, + # duplicates enum IDs, and gives TSNE_INIT::PCA the same C++ target as ML::PCA. + for filename in glob.glob(os.path.join(path, "*.xml")): + tree = ET.parse(filename) + changed = False + for section in tree.findall(".//sectiondef"): + for member in list(section.findall("memberdef")): + type_node = member.find("type") + type_text = ( + "".join(type_node.itertext()) + if type_node is not None + else "" + ) + if type_text in {"concept", "template void"}: + section.remove(member) + changed = True + continue + + if member.get("kind") != "enum": + continue + member_id = member.get("id", "") + for value in list(member.findall("enumvalue")): + if ( + member.findtext("name") == "TSNE_INIT" + and value.findtext("name") == "PCA" + ): + member.remove(value) + changed = True + elif not value.get("id", "").startswith(member_id): + value.set( + "id", f"{member_id}_{value.findtext('name')}" + ) + changed = True + if changed: + tree.write(filename, encoding="UTF-8", xml_declaration=True) + + +for project_path in breathe_projects.values(): + clean_doxygen_xml(project_path) + ipython_mplbackend = "str" # Add any paths that contain templates here, relative to this directory. diff --git a/docs/source/cpp/genetic.rst b/docs/source/cpp/genetic.rst new file mode 100644 index 0000000000..072f76d341 --- /dev/null +++ b/docs/source/cpp/genetic.rst @@ -0,0 +1,5 @@ +cuml::genetic Namespace +======================= + +.. doxygennamespace:: cuml::genetic + :members: diff --git a/docs/source/cpp/index.rst b/docs/source/cpp/index.rst new file mode 100644 index 0000000000..748726456b --- /dev/null +++ b/docs/source/cpp/index.rst @@ -0,0 +1,11 @@ +C++ API +======= + +This section documents the C++ API for cuML, also called ``libcuml``. + +.. toctree:: + :maxdepth: 1 + + ml + mlcommon + genetic diff --git a/docs/source/cpp/ml.rst b/docs/source/cpp/ml.rst new file mode 100644 index 0000000000..6135faab15 --- /dev/null +++ b/docs/source/cpp/ml.rst @@ -0,0 +1,5 @@ +ML Namespace +============ + +.. doxygennamespace:: ML + :members: diff --git a/docs/source/cpp/mlcommon.rst b/docs/source/cpp/mlcommon.rst new file mode 100644 index 0000000000..e409e4b4aa --- /dev/null +++ b/docs/source/cpp/mlcommon.rst @@ -0,0 +1,5 @@ +MLCommon Namespace +================== + +.. doxygennamespace:: MLCommon + :members: diff --git a/docs/source/index.rst b/docs/source/index.rst index f2db8ff3b2..2ecee066df 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -78,4 +78,5 @@ Community & Support user_guide.rst Zero Code Change Acceleration api/index + cpp/index cuml_blogs.rst From 9c269267350ab2695dfd33944f369e143174db09 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Thu, 10 Sep 2026 12:03:22 +0000 Subject: [PATCH 2/4] Move developer documentation into Sphinx --- .coderabbit.yaml | 8 +- .github/CODEOWNERS | 9 +- .github/workflows/pr.yaml | 4 - CONTRIBUTING.md | 4 +- docs/source/cpp/genetic.rst | 5 - docs/source/cpp/index.rst | 11 - docs/source/cpp/ml.rst | 5 - docs/source/cpp/mlcommon.rst | 5 - .../source/developer_guide/benchmarking.md | 32 +- docs/source/developer_guide/contributing.rst | 20 + .../developer_guide/cpp/api/genetic.rst | 13 + docs/source/developer_guide/cpp/api/index.rst | 26 + docs/source/developer_guide/cpp/api/ml.rst | 13 + .../developer_guide/cpp/api/mlcommon.rst | 13 + .../source/developer_guide/cpp/development.md | 157 ++++++ docs/source/developer_guide/cpp/index.rst | 8 + docs/source/developer_guide/index.rst | 33 ++ .../developer_guide/python/development.md | 52 +- .../developer_guide/python/estimators.md | 61 +-- docs/source/index.rst | 5 +- wiki/DEFINITION_OF_DONE_CRITERIA.md | 82 ---- wiki/README.md | 11 - wiki/cpp/DEVELOPER_GUIDE.md | 454 ------------------ wiki/mnmg/Using_Infiniband_for_MNMG.md | 392 --------------- 24 files changed, 377 insertions(+), 1046 deletions(-) delete mode 100644 docs/source/cpp/genetic.rst delete mode 100644 docs/source/cpp/index.rst delete mode 100644 docs/source/cpp/ml.rst delete mode 100644 docs/source/cpp/mlcommon.rst rename wiki/BENCHMARK.md => docs/source/developer_guide/benchmarking.md (97%) create mode 100644 docs/source/developer_guide/contributing.rst create mode 100644 docs/source/developer_guide/cpp/api/genetic.rst create mode 100644 docs/source/developer_guide/cpp/api/index.rst create mode 100644 docs/source/developer_guide/cpp/api/ml.rst create mode 100644 docs/source/developer_guide/cpp/api/mlcommon.rst create mode 100644 docs/source/developer_guide/cpp/development.md create mode 100644 docs/source/developer_guide/cpp/index.rst create mode 100644 docs/source/developer_guide/index.rst rename wiki/python/DEVELOPER_GUIDE.md => docs/source/developer_guide/python/development.md (89%) rename wiki/python/ESTIMATOR_GUIDE.md => docs/source/developer_guide/python/estimators.md (90%) delete mode 100644 wiki/DEFINITION_OF_DONE_CRITERIA.md delete mode 100644 wiki/README.md delete mode 100644 wiki/cpp/DEVELOPER_GUIDE.md delete mode 100644 wiki/mnmg/Using_Infiniband_for_MNMG.md diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 083e6b9270..2716013d59 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -45,7 +45,7 @@ reviews: For public C++ API headers, additionally check: - Doxygen documentation for all public functions/classes - API changes flagged for docs/ updates - - Breaking changes require deprecation warnings and migration guide updates + - Consequential changes called out clearly; the C++ API currently has no stability or deprecation guarantee - path: "cpp/{src,src_prims,include}/**/*.{cu,cuh,cpp,hpp,h}" instructions: | @@ -108,6 +108,6 @@ knowledge_base: - "cpp/agents.md" - "python/agents.md" - "CONTRIBUTING.md" - - "wiki/cpp/DEVELOPER_GUIDE.md" - - "wiki/python/DEVELOPER_GUIDE.md" - - "wiki/python/ESTIMATOR_GUIDE.md" + - "docs/source/developer_guide/cpp/development.md" + - "docs/source/developer_guide/python/development.md" + - "docs/source/developer_guide/python/estimators.md" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4ea2b086a5..fc5b0756a5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,11 +5,10 @@ cpp/ @NVIDIA/cuml-cpp-codeowners # docs -/CONTRIBUTING.md @NVIDIA/cuml-python-codeowners -/README.md @NVIDIA/cuml-python-codeowners -/docs/ @NVIDIA/cuml-python-codeowners -/wiki/ @NVIDIA/cuml-python-codeowners -/wiki/cpp @NVIDIA/cuml-cpp-codeowners +/CONTRIBUTING.md @NVIDIA/cuml-python-codeowners +/README.md @NVIDIA/cuml-python-codeowners +/docs/ @NVIDIA/cuml-python-codeowners +/docs/source/developer_guide/cpp/ @NVIDIA/cuml-cpp-codeowners #python code owners python/ @NVIDIA/cuml-python-codeowners diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 594be3309c..a5fa9572f5 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -159,7 +159,6 @@ jobs: - '!notebooks/**' - '!python/**' - '!thirdparty/LICENSES/**' - - '!wiki/**' test_notebooks: - '**' - '!**/*/agents.md' @@ -197,7 +196,6 @@ jobs: - '!cpp/README.md' - '!cpp/header.html' - '!thirdparty/LICENSES/**' - - '!wiki/**' test_python_conda: - '**' - '!**/*/agents.md' @@ -240,7 +238,6 @@ jobs: - '!img/**' - '!notebooks/**' - '!thirdparty/LICENSES/**' - - '!wiki/**' test_python_wheels: - '**' - '!**/*/agents.md' @@ -287,7 +284,6 @@ jobs: - '!img/**' - '!notebooks/**' - '!thirdparty/LICENSES/**' - - '!wiki/**' checks: needs: telemetry-setup permissions: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e90f54be30..c77599c437 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,8 +30,8 @@ into three categories: or [help wanted](https://github.com/NVIDIA/cuml/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) labels 3. Comment on the issue saying you are going to work on it. 4. Get familiar with the developer guide relevant for you: - * For C++ developers it is available here [DEVELOPER_GUIDE.md](wiki/cpp/DEVELOPER_GUIDE.md) - * For Python developers, a [Python DEVELOPER_GUIDE.md](wiki/python/DEVELOPER_GUIDE.md) is available as well. + * For Python developers, read the [Python Developer Guide](docs/source/developer_guide/python/development.md) and [Estimator Guide](docs/source/developer_guide/python/estimators.md). + * For C++ developers, read the [C++ and CUDA Developer Guide](docs/source/developer_guide/cpp/development.md). 5. Code! Make sure to update unit tests! 6. When done, [create your pull request](https://github.com/NVIDIA/cuml/compare). 7. Verify that CI passes all [status checks](https://help.github.com/articles/about-status-checks/), or fix if needed. diff --git a/docs/source/cpp/genetic.rst b/docs/source/cpp/genetic.rst deleted file mode 100644 index 072f76d341..0000000000 --- a/docs/source/cpp/genetic.rst +++ /dev/null @@ -1,5 +0,0 @@ -cuml::genetic Namespace -======================= - -.. doxygennamespace:: cuml::genetic - :members: diff --git a/docs/source/cpp/index.rst b/docs/source/cpp/index.rst deleted file mode 100644 index 748726456b..0000000000 --- a/docs/source/cpp/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -C++ API -======= - -This section documents the C++ API for cuML, also called ``libcuml``. - -.. toctree:: - :maxdepth: 1 - - ml - mlcommon - genetic diff --git a/docs/source/cpp/ml.rst b/docs/source/cpp/ml.rst deleted file mode 100644 index 6135faab15..0000000000 --- a/docs/source/cpp/ml.rst +++ /dev/null @@ -1,5 +0,0 @@ -ML Namespace -============ - -.. doxygennamespace:: ML - :members: diff --git a/docs/source/cpp/mlcommon.rst b/docs/source/cpp/mlcommon.rst deleted file mode 100644 index e409e4b4aa..0000000000 --- a/docs/source/cpp/mlcommon.rst +++ /dev/null @@ -1,5 +0,0 @@ -MLCommon Namespace -================== - -.. doxygennamespace:: MLCommon - :members: diff --git a/wiki/BENCHMARK.md b/docs/source/developer_guide/benchmarking.md similarity index 97% rename from wiki/BENCHMARK.md rename to docs/source/developer_guide/benchmarking.md index f6574f03d0..56f573db2f 100644 --- a/wiki/BENCHMARK.md +++ b/docs/source/developer_guide/benchmarking.md @@ -12,6 +12,7 @@ The benchmark runner also supports YAML manifests. A manifest is the declarative - [Running the benchmarks](#running-the-benchmarks) - [Common options](#common-options) - [Examples](#examples) +- [Adding algorithm coverage](#adding-algorithm-coverage) - [YAML manifests](#yaml-manifests) - [Manifest structure](#top-level-schema) - [`suite`](#suite) @@ -61,25 +62,6 @@ python -m cuml.benchmark \ --backends cpu ``` -To run a YAML-defined suite: - -```bash -python -m cuml.benchmark \ - --config python/cuml/cuml/benchmark/configs/single_gpu.yaml \ - --profile default \ - --backends gpu \ - --csv results.csv -``` - -To run the tiny harness-validation manifest: - -```bash -python -m cuml.benchmark \ - --config python/cuml/cuml/benchmark/configs/test.yaml \ - --profile default \ - --backends cpu -``` - ### Standalone mode (from the repository) From the `python/cuml/cuml/benchmark/` directory, you can run without installing cuML: @@ -345,6 +327,18 @@ When multiple backends are present, timings are grouped on one row: CSV output remains available through `--csv`, but it is a flat compatibility export. Prefer JSON for regression tracking and reproducibility. +## Adding algorithm coverage + +New algorithms should include benchmark coverage for every applicable +implementation layer. Add a Python estimator to the registry in +`python/cuml/cuml/benchmark/algorithms.py` and add or update the appropriate +manifest. For a new C++ algorithm, add a Google Benchmark case under +`cpp/bench/sg` and list its source in `cpp/bench/CMakeLists.txt`. + +Use benchmarks and profiling for performance-sensitive changes to establish +baselines and investigate bottlenecks, regressions, and unexpected memory +behavior. + ## YAML manifests A manifest defines a benchmark suite, default settings for the suite, and the individual benchmark entries to run. diff --git a/docs/source/developer_guide/contributing.rst b/docs/source/developer_guide/contributing.rst new file mode 100644 index 0000000000..642e53cb98 --- /dev/null +++ b/docs/source/developer_guide/contributing.rst @@ -0,0 +1,20 @@ +Contributing +============ + +Start with the repository's `contribution guidelines +`_ for proposing +changes, preparing pull requests, running repository checks, and working with +continuous integration. Those guidelines are the canonical source for the +contribution process. + +Use the implementation-specific sections of this Developer Guide after choosing +a change: + +* :doc:`Python development ` covers Python style, testing, + validation, memory management, and documentation. +* :doc:`Python estimator development ` describes the + ``cuml.Base`` estimator contract and implementation patterns. +* :doc:`C++ and CUDA development ` covers C++/CUDA source layout, + resources, testing, and the internal C++ API reference. +* :doc:`Benchmarking ` explains the benchmark CLI, manifests, and + adding algorithm coverage. diff --git a/docs/source/developer_guide/cpp/api/genetic.rst b/docs/source/developer_guide/cpp/api/genetic.rst new file mode 100644 index 0000000000..ecf84682ea --- /dev/null +++ b/docs/source/developer_guide/cpp/api/genetic.rst @@ -0,0 +1,13 @@ +cuml::genetic Namespace +======================= + +.. warning:: + + Primarily internal API: it may change or disappear without notice and has no + stability, deprecation, backward-compatibility, or input-validation + guarantees. Callers must validate inputs and satisfy all memory, stream, and + lifetime preconditions. Prefer the supported :doc:`Python API + <../../../api/index>`. + +.. doxygennamespace:: cuml::genetic + :members: diff --git a/docs/source/developer_guide/cpp/api/index.rst b/docs/source/developer_guide/cpp/api/index.rst new file mode 100644 index 0000000000..d3595e8d8c --- /dev/null +++ b/docs/source/developer_guide/cpp/api/index.rst @@ -0,0 +1,26 @@ +C++ API Reference +================= + +This reference is generated from the headers and implementation documentation +for ``libcuml``. + +.. warning:: + + These C++ interfaces are primarily internal implementation interfaces. They + may change or be removed without notice. cuML provides **no stability, + deprecation, or backward-compatibility guarantees** for them and **no + guarantees that inputs are validated**. Callers are responsible for meeting + every documented precondition, validating dimensions, types, memory + locations, device state, stream ordering, and resource lifetimes, and + checking outputs and failures. Prefer the supported :doc:`Python APIs + <../../../api/index>` whenever possible. + +Namespaces +---------- + +.. toctree:: + :maxdepth: 1 + + ml + mlcommon + genetic diff --git a/docs/source/developer_guide/cpp/api/ml.rst b/docs/source/developer_guide/cpp/api/ml.rst new file mode 100644 index 0000000000..c1701bc562 --- /dev/null +++ b/docs/source/developer_guide/cpp/api/ml.rst @@ -0,0 +1,13 @@ +ML Namespace +============ + +.. warning:: + + Primarily internal API: it may change or disappear without notice and has no + stability, deprecation, backward-compatibility, or input-validation + guarantees. Callers must validate inputs and satisfy all memory, stream, and + lifetime preconditions. Prefer the supported :doc:`Python API + <../../../api/index>`. + +.. doxygennamespace:: ML + :members: diff --git a/docs/source/developer_guide/cpp/api/mlcommon.rst b/docs/source/developer_guide/cpp/api/mlcommon.rst new file mode 100644 index 0000000000..87a320b84f --- /dev/null +++ b/docs/source/developer_guide/cpp/api/mlcommon.rst @@ -0,0 +1,13 @@ +MLCommon Namespace +================== + +.. warning:: + + Primarily internal API: it may change or disappear without notice and has no + stability, deprecation, backward-compatibility, or input-validation + guarantees. Callers must validate inputs and satisfy all memory, stream, and + lifetime preconditions. Prefer the supported :doc:`Python API + <../../../api/index>`. + +.. doxygennamespace:: MLCommon + :members: diff --git a/docs/source/developer_guide/cpp/development.md b/docs/source/developer_guide/cpp/development.md new file mode 100644 index 0000000000..f6a239748c --- /dev/null +++ b/docs/source/developer_guide/cpp/development.md @@ -0,0 +1,157 @@ +# C++ and CUDA Developer Guide + +This guide summarizes current conventions for contributions to cuML's C++ and +CUDA implementation. Read the repository +[contribution guidelines](https://github.com/NVIDIA/cuml/blob/main/CONTRIBUTING.md) +before starting. + +## Source and API layout + +Installed libcuml headers live under +[`cpp/include/cuml`](https://github.com/NVIDIA/cuml/tree/main/cpp/include/cuml). +Algorithm implementations and internal headers live under +[`cpp/src`](https://github.com/NVIDIA/cuml/tree/main/cpp/src). Keep a public +declaration in the appropriate installed header and place implementation details +with the corresponding algorithm in `cpp/src`. + +The C++ interfaces are used by cuML's bindings and by some direct libcuml +consumers. The C++ API currently has no backward-compatibility or deprecation +guarantee. Describe behavior and preconditions precisely, keep changes focused, +and clearly describe consequential API changes during review. + +## Formatting and implementation style + +The configured pre-commit hooks and +[`CONTRIBUTING.md`](https://github.com/NVIDIA/cuml/blob/main/CONTRIBUTING.md#code-formatting) +are the formatting authority. Install pre-commit and run the hooks on changed +files before opening a pull request: + +```bash +pre-commit run --files cpp/include/cuml/example.hpp cpp/src/example.cu +``` + +Follow neighboring code and use existing RAFT primitives rather than creating +local alternatives. Factor generally reusable low-level operations into the +appropriate primitive layer rather than duplicating them inside individual +algorithms. Use the RAFT error-checking facilities appropriate to the CUDA +library call. Avoid unnecessary host/device transfers and synchronization. Keep +algorithm array inputs and outputs device-accessible; do not require host +staging unless the API contract requires it. + +## Memory and streams + +Use RMM RAII containers for temporary allocations, such as +`rmm::device_uvector`, `rmm::device_scalar`, and `rmm::host_uvector`. Construct +and use them with the operation's explicit stream so allocation, work, and +lifetime follow the same ordering. Do not introduce raw `cudaMalloc` ownership +when an RMM container expresses the lifetime. + +A `raft::handle_t` is a RAFT resource container. Obtain the caller's stream from +it (for example, `handle.get_stream()` in handle-based code or +`raft::resource::get_cuda_stream(resources)` for `raft::resources`) and enqueue +work on that stream. Avoid the default CUDA stream and avoid synchronizing +unless the API contract requires host-visible completion. + +When concurrency is useful, use the stream pool supplied by the RAFT resource +container rather than creating handles or reusable CUDA resources per stream. +Current handle-based code uses `get_stream_pool_size()` and +`get_stream_from_stream_pool(index)`. Preserve ordering between the caller's +primary stream and pool work, and do not assume that a pool exists or has a +particular size. Follow a nearby implementation using the same RAFT resource +type because RAFT resource APIs evolve. + +Algorithms should be safe to invoke concurrently when each invocation has its +own resources and output storage. Shared process-wide state and unnecessary CPU +threading should be avoided. + +## Logging + +Include [`cuml/common/logger.hpp`](https://github.com/NVIDIA/cuml/blob/main/cpp/include/cuml/common/logger.hpp) +and use `CUML_LOG_TRACE`, `CUML_LOG_DEBUG`, `CUML_LOG_INFO`, `CUML_LOG_WARN`, +`CUML_LOG_ERROR`, or `CUML_LOG_CRITICAL` as appropriate. The logger is the +RAPIDS Logger instance returned by `ML::default_logger()`; its levels use +`rapids_logger::level_enum`. Do not append a newline to log messages, and avoid +formatting expensive diagnostic values unless the level will be logged. + +## Multi-GPU communication + +cuML's distributed C++ algorithms use one process per GPU. Communication is +provided through `raft::comms` attached to the RAFT handle. With CUDA-aware MPI, +current tests initialize the handle as follows: + +```cpp +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + MPI_Init(&argc, &argv); + + int local_rank = 0; + MPI_Comm local_comm; + MPI_Comm_split_type( + MPI_COMM_WORLD, MPI_COMM_TYPE_SHARED, 0, MPI_INFO_NULL, &local_comm); + MPI_Comm_rank(local_comm, &local_rank); + cudaSetDevice(local_rank); + + { + raft::handle_t handle; + raft::comms::initialize_mpi_comms(&handle, MPI_COMM_WORLD); + auto const& comm = handle.get_comms(); + // All ranks in comm must enter collective algorithm calls cooperatively. + } + + MPI_Comm_free(&local_comm); + MPI_Finalize(); + return 0; +} +``` + +Check every MPI and CUDA return value in production code. The snippet focuses +on the current `initialize_mpi_comms` signature and resource lifetime; consult +[`cpp/tests/mg`](https://github.com/NVIDIA/cuml/tree/main/cpp/tests/mg) for +complete test setup and error handling. + +## Testing + +Add focused GoogleTest coverage alongside the corresponding tests under +`cpp/tests`. Use focused GoogleTests for reusable primitives and end-to-end +GoogleTests for algorithms, covering representative inputs and datasets. Add +every new test source to the appropriate `CMakeLists.txt` so it is built and +run. Configure and build the relevant targets, then run CTest from the build +tree: + +```bash +./build.sh libcuml +ctest --test-dir cpp/build --output-on-failure +``` + +To select a subset while iterating: + +```bash +ctest --test-dir cpp/build --output-on-failure -R '' +``` + +Installed CI test packages can also be exercised through `ci/run_ctests.sh`. +Tests should cover meaningful shapes, dtypes, failure conditions, and stream or +distributed behavior affected by the change without inflating the fast suite +unnecessarily. + +## Doxygen documentation + +Document interfaces in installed headers with Doxygen comments. State parameter +and output shapes, dtypes, host or device memory location, ownership and +lifetime, stream behavior, preconditions, errors, and algorithm references. +Do not imply that the generated C++ reference makes an interface stable or that +libcuml validates every input. + +Build Doxygen XML before Sphinx so Breathe can resolve declarations: + +```bash +./build.sh cppdocs pydocs +``` + +The published C++ API reference is part of the Sphinx Developer Guide. Doxygen +XML is an intermediate input, not a separately published API site. diff --git a/docs/source/developer_guide/cpp/index.rst b/docs/source/developer_guide/cpp/index.rst new file mode 100644 index 0000000000..700a0ced7d --- /dev/null +++ b/docs/source/developer_guide/cpp/index.rst @@ -0,0 +1,8 @@ +C++ and CUDA Development +======================== + +.. toctree:: + :maxdepth: 2 + + development + api/index diff --git a/docs/source/developer_guide/index.rst b/docs/source/developer_guide/index.rst new file mode 100644 index 0000000000..7ab134b733 --- /dev/null +++ b/docs/source/developer_guide/index.rst @@ -0,0 +1,33 @@ +Developer Guide +=============== + +This guide collects the information needed to contribute to, develop, test, +document, and benchmark cuML. Start with :doc:`contributing`, then use the +implementation-specific guidance below. + +* :doc:`Contributing ` covers how to propose changes, prepare pull + requests, run repository checks, and work with continuous integration. +* :doc:`Python development ` covers style, testing, + validation, memory management, and Python documentation. + + * :doc:`Python estimator development ` describes the + ``cuml.Base`` estimator contract and provides implementation patterns. + +* :doc:`C++ and CUDA development ` covers source layout, + resources, testing, and Doxygen documentation. + + * :doc:`Internal C++ API reference ` exposes primarily + internal libcuml interfaces for developers. Prefer the supported + :doc:`Python API <../api/index>` for applications. + +* :doc:`Benchmarking ` explains the benchmark CLI and manifests. + +.. toctree:: + :hidden: + :maxdepth: 3 + + contributing + python/development + python/estimators + cpp/index + benchmarking diff --git a/wiki/python/DEVELOPER_GUIDE.md b/docs/source/developer_guide/python/development.md similarity index 89% rename from wiki/python/DEVELOPER_GUIDE.md rename to docs/source/developer_guide/python/development.md index 73b78a44c0..fed1a22dd3 100644 --- a/wiki/python/DEVELOPER_GUIDE.md +++ b/docs/source/developer_guide/python/development.md @@ -17,22 +17,22 @@ This document provides comprehensive guidelines and best practices for contribut 11. [Deprecation Policy](#deprecation-policy) 12. [Logging](#logging) 13. [Multi-GPU Support](#multi-gpu-support) -14. [Benchmarking](#benchmarking) +14. [Profiling](#profiling) ## Prerequisites Before diving into Python development for cuML, please ensure you have: -1. Reviewed our [contribution guidelines](../../CONTRIBUTING.md) for general project standards -2. Read the [Python cuML README](../../python/README.md) for setup and installation instructions +1. Reviewed our [contribution guidelines](https://github.com/NVIDIA/cuml/blob/main/CONTRIBUTING.md) for general project standards +2. Follow the repository [build-from-source guide](https://github.com/NVIDIA/cuml/blob/main/BUILD.md) to set up the development environment and build cuML -If you are working on C++/CUDA code or need to understand the underlying implementation details, you should also familiarize yourself with the [C++ Developer Guide](../cpp/DEVELOPER_GUIDE.md). +If you are working on C++/CUDA code or need to understand the underlying implementation details, you should also familiarize yourself with the [C++ Developer Guide](../cpp/development.md). ## Guide Map Use this document for repository-wide Python development policy: style, docstrings, testing, memory management, deprecations, logging, multi-GPU structure, and benchmarking. -Use [Estimator Guide](ESTIMATOR_GUIDE.md) when creating or modifying a `cuml.Base` estimator. It contains the estimator contract, copyable estimator skeleton, input validation, array descriptor guidance, reflection guidance, and estimator-specific do's and don'ts. +Use [Estimator Guide](estimators.md) when creating or modifying a `cuml.Base` estimator. It contains the estimator contract, copyable estimator skeleton, input validation, array descriptor guidance, reflection guidance, and estimator-specific do's and don'ts. ## Getting Started @@ -61,7 +61,7 @@ The docstring should include the following sections in order: 3. **Parameters** - Description of function arguments, keywords and their types - Format: - ```python + ```text Parameters ---------- x : type @@ -73,7 +73,7 @@ The docstring should include the following sections in order: 4. **Returns** - Description of returned values and their types - Format: - ```python + ```text Returns ------- int @@ -129,6 +129,9 @@ def function_name(param1, param2): 6. Document default values for optional parameters 7. Use the `@generate_docstring` decorator for common parameter documentation 8. Include examples that demonstrate typical usage +9. Document public API restrictions and non-obvious behavior +10. Cite scientific papers or standards underlying an algorithm; for a + nonstandard algorithm, describe the approach used For more details, refer to the [NumPy docstring style guide](https://numpydoc.readthedocs.io/en/stable/format.html). @@ -168,8 +171,8 @@ We support three main approaches for test input generation: - Must include at least one `@example` for deterministic testing - Preferred for dataset generation and most hyperparameter testing ```python - @example(dataset=small_regression_dataset(np.float32), alpha=floats(0.1, 10.0)) - @given(dataset=standard_regression_datasets(), alpha=1.0) + @example(dataset=small_regression_dataset(np.float32), alpha=1.0) + @given(dataset=standard_regression_datasets(), alpha=floats(0.1, 10.0)) def test_estimator(dataset, alpha): pass ``` @@ -186,6 +189,8 @@ We provide three test parameter levels: ```python unit_param(2) # For number of components ``` + Keep unit-level cases fast. Check CI's slowest-test report when adding tests + and move expensive coverage to an appropriate quality or stress level. 2. **Quality Tests** (`quality_param`): Medium values for thorough testing ```python @@ -209,10 +214,16 @@ Control via these pytest options: - Document origin of reference values - Use appropriate quality metrics for equivalent but different results - Ensure reproducibility rather than using retry logic + - When cuML implements an estimator from a reference library, test its public + wrapper behavior and numerical correctness against that implementation on + representative datasets 2. **Minimize resources** - Use minimal dataset sizes - Only test different scales if they would actually hit different code paths + - Cover supported shape regimes and numerical precisions that can expose + distinct behavior—for example short-wide, tall-narrow, FP32, and FP64—using + quality or stress levels rather than slowing the unit suite 3. **Best Practices** - Write small, focused tests @@ -246,12 +257,14 @@ Running pytest from outside `python/cuml/` can result in import errors or missed Code should use `cuml.internals.validation` for user-facing input validation. These helpers are the standard path for matching scikit-learn validation behavior, simplifying input ingest, and avoiding module-specific validation -pipelines. See the [Estimator Guide](ESTIMATOR_GUIDE.md#input-validation) for -estimator-specific patterns and examples. +pipelines. See [Ingesting Arrays in the Estimator Guide](estimators.md#ingesting-arrays) +for estimator-specific patterns and examples. Prefer `check_inputs` for estimator methods that validate `X` and optional `y` / `sample_weight` values. Use lower-level helpers directly only when a method -has a non-standard shape that the higher-level helper cannot express. +has a non-standard shape that the higher-level helper cannot express. Where +practical, reject unsupported user inputs gracefully with an actionable error +explaining how to correct the call. Validation helpers should be configured to describe what the estimator actually supports. Set `dtype`, `mem_type`, `order`, `accept_sparse`, @@ -308,7 +321,7 @@ Additional considerations: ## Thread Safety Algorithms implemented in C++/CUDA should be implemented in a thread-safe manner. The Python code is generally not thread safe. -Refer to the section on thread safety in [C++ DEVELOPER_GUIDE.md](../cpp/DEVELOPER_GUIDE.md#thread-safety) +Refer to the section on thread safety in [C++ Developer Guide](../cpp/development.md#memory-and-streams) ## Creating New Estimators @@ -321,7 +334,7 @@ When implementing a new estimator in cuML, follow these key steps: - Is placed in the appropriate subdirectory matching scikit-learn's structure - Uses `cuml.internals.validation` for public input validation -For detailed implementation guidelines, including file organization, API design, output type handling, and a copyable estimator skeleton, refer to the [Estimator Guide](ESTIMATOR_GUIDE.md). +For detailed implementation guidelines, including file organization, API design, output type handling, and a copyable estimator skeleton, refer to the [Estimator Guide](estimators.md). ## Deprecation Policy @@ -432,7 +445,7 @@ Use the appropriate log level based on the message's importance and target audie ```python from cuml.internals import logger - if logger.should_log_for(logging.DEBUG): + if logger.should_log_for(logger.level_enum.debug): logger.debug(f"Expensive operation result: {expensive_operation()}") ``` @@ -520,9 +533,12 @@ Key points for implementing multi-GPU estimators: - The dask layer should focus on distributed computation, with base algorithms implemented in standard estimators - See currently implemented estimators, e.g., LogisticRegression for examples on how to implement dask-based Multi-GPU estimators -## Benchmarking +## Profiling -The cuML code including its Python operations can be profiled. The `nvtx_benchmark.py` is a helper script that produces a simple benchmark summary. To use it, run `python nvtx_benchmark.py "python test.py"`. +The cuML code, including its Python operations, can be profiled with the +`nvtx_benchmark.py` helper script. From the repository root, run +`python python/cuml/cuml/benchmark/nvtx_benchmark.py "python test.py"` to +produce a simple benchmark summary. Here is an example with the following script: ```python @@ -536,7 +552,7 @@ model.fit(X) embeddings = model.transform(X) ``` -that once benchmarked can have its profiling summarized: +Running the script through `nvtx_benchmark.py` produces a profiling summary: ``` datasets.make_blobs : 1.3571 s diff --git a/wiki/python/ESTIMATOR_GUIDE.md b/docs/source/developer_guide/python/estimators.md similarity index 90% rename from wiki/python/ESTIMATOR_GUIDE.md rename to docs/source/developer_guide/python/estimators.md index e1539ca2cc..d05f5326f8 100644 --- a/wiki/python/ESTIMATOR_GUIDE.md +++ b/docs/source/developer_guide/python/estimators.md @@ -9,18 +9,8 @@ This guide documents the patterns expected for new or updated `cuml.Base` estima - [Recommended Scikit-Learn Documentation](#recommended-scikit-learn-documentation) - [API Matching Policy](#api-matching-policy) - [Quick Start Guide](#quick-start-guide) - - [Copyable Estimator Skeleton](#copyable-estimator-skeleton) - [Background](#background) - - [Array I/O and Output Types in cuML](#array-io-and-output-types-in-cuml) - - [Ingesting Arrays](#ingesting-arrays) - - [Returning Arrays](#returning-arrays) - [Estimator Design](#estimator-design) - - [Initialization](#initialization) - - [Implementing `_get_param_names()`](#implementing-_get_param_names) - - [Estimator Tags and cuML Specific Tags](#estimator-tags-and-cuml-specific-tags) - - [Estimator Array-Like Attributes](#estimator-array-like-attributes) - - [Estimator Methods](#estimator-methods) -- [Do's and Don'ts](#dos-and-donts) ## Recommended Scikit-Learn Documentation @@ -143,11 +133,14 @@ At a high level, all cuML Estimators must: ``` 7. Override estimator tags only when the defaults are wrong. Prefer existing - [Mixins](../../python/cuml/cuml/internals/mixins.py) for common capabilities + [mixins](https://github.com/NVIDIA/cuml/blob/main/python/cuml/cuml/internals/mixins.py) for common capabilities such as preferred input order, sparse support, string input, or NaN support. See [Estimator Tags and cuML-Specific Tags](#estimator-tags-and-cuml-specific-tags) for custom tag overrides. +8. Support pickle round trips both before and after fitting. Add the applicable + coverage to `python/cuml/tests/test_pickle.py`. + For most estimators, the checklist and skeleton below are enough. The later sections explain the contract and uncommon cases. @@ -175,7 +168,7 @@ class MyEstimator(Base): @mlfunc(set_input_type=True) def fit(self, X) -> "MyEstimator": - X = check_inputs(self, X, order="K", reset=True) + X = check_inputs(self, X, order="A", reset=True) # Replace this placeholder with estimator training. self.result_ = X return self @@ -183,7 +176,7 @@ class MyEstimator(Base): @mlfunc(preserve_index=True) def transform(self, X): check_is_fitted(self) - X = check_inputs(self, X, order="K") + X = check_inputs(self, X, order="A") # Return an array-like object directly; @mlfunc handles conversion. return X ``` @@ -219,10 +212,13 @@ Users choose output types in three ways: 2. Set a global override with `cuml.set_global_output_type("numpy")`. 3. Temporarily set a global override with `cuml.using_output_type("numpy")`. -The global setting stored in `cuml.global_settings.output_type` takes -precedence over an estimator's `output_type`. When neither is set, reflected -estimator methods normally mirror the call input type, and descriptor -attributes mirror the fit-time input type. +An explicit global output type such as `"numpy"` or `"cupy"` takes precedence +over an estimator's `output_type`. The global `"input"` setting is a legacy +exception: reflected methods still respect an explicit estimator output type, +while descriptor attributes mirror the fit-time input type. When neither a +global nor estimator output type is set, reflected estimator methods normally +mirror the call input type, and descriptor attributes mirror the fit-time input +type. Accepted output types are: @@ -258,7 +254,7 @@ def fit(self, X, y): X, y, dtype=("float32", "float64"), - order="K", + order="A", reset=True, ) rows, cols = X.shape @@ -273,7 +269,7 @@ def transform(self, X): self, X, dtype=self.result_.dtype, - order="K", + order="A", ) ... ``` @@ -285,8 +281,12 @@ specialized checks not already covered by the higher-level helpers. ### Returning Arrays -Return ``cupy`` or ``numpy`` arrays directly from reflected methods. The -reflection machinery will coerce these to the proper output type. +Return CuPy or NumPy arrays directly from reflected methods. Methods that +support sparse results may likewise return the corresponding CuPy or SciPy +sparse arrays. The reflection machinery will coerce supported arrays and nested +containers to the proper output type. Specialized outputs, such as classifier +labels that may have non-numeric dtypes, should use the wrappers described +below. ## Estimator Design @@ -411,7 +411,11 @@ fallback behavior in newer scikit-learn versions. ### Estimator Array-Like Attributes Array-like fitted attributes should use `cuml.internals.ReflectedAttr` so -user-facing attribute reads respect cuML output-type settings. +user-facing attribute reads respect cuML output-type settings. Values assigned +to a `ReflectedAttr` must be NumPy or CuPy arrays, their corresponding sparse +array types, `ArrayIndexPair` objects, or supported nested containers of those +types. Do not assign pandas or cuDF objects directly; validation helpers should +first normalize them to NumPy or CuPy arrays. Internally, a descriptor behaves like a normal attribute and returns the value that was set. Externally, it lazily converts the value to the requested output @@ -457,7 +461,7 @@ class SampleEstimator(Base): @mlfunc(set_input_type=True) def fit(self, X): # reset=True on check_inputs sets n_features_in_ and feature_names_in_ - X = check_inputs(self, X, order="K", reset=True) + X = check_inputs(self, X, order="A", reset=True) # Set descriptor-managed fitted attributes with validated arrays # When accessed in any `mlfunc`-decorated method, these will have the @@ -493,14 +497,14 @@ Externally, descriptor attributes lazily convert to the active output type: my_est = SampleEstimator() # Call fit() with a numpy array as the input -np_arr = np.ones((10,)) +np_arr = np.ones((10, 1)) my_est.fit(np_arr) # This will load data into attributes # Externally, descriptors reflect the fit-time input type by default print(type(my_est.my_array_)) # Output: NumPy (saved from the input of `fit`) # Calling fit again with cupy arrays, will have a similar effect -my_est.fit(cp.ones((10,))) +my_est.fit(cp.ones((10, 1))) print(type(my_est.my_array_)) # Output: CuPy # Setting the `output_type` will change all descriptor properties @@ -539,13 +543,13 @@ class MyEstimator(Base): @mlfunc(set_input_type=True) def fit(self, X): - self.coef_ = check_inputs(self, X, order="K", reset=True) + self.coef_ = check_inputs(self, X, order="A", reset=True) return self @mlfunc(preserve_index=True) def predict(self, X): check_is_fitted(self) - X = check_inputs(self, X, order="K") + X = check_inputs(self, X, order="A") return X + cp.ones(X.shape) ``` @@ -554,6 +558,9 @@ class MyEstimator(Base): | `@mlfunc(set_input_type=True)` | Fit-like methods that store `_input_type` through reflection while validation helpers set or check `n_features_in_`. | | `@mlfunc(preserve_index=True)` | Transform/predict methods that return arrays with `n_samples` aligned with `X` | | `@mlfunc(array_arg=None)` | Methods with no array input (e.g., `KernelDensity.sample()`). Uses fit-time input type. | +| `@mlfunc(model_arg=...)` | Functions or methods where the estimator argument is not the default `self`; pass its name or position, or `None` to disable estimator-based inference. | +| `@mlfunc(column_names="feature_names_in")` | DataFrame-returning methods whose output columns match `feature_names_in_`. | +| `@mlfunc(column_names="feature_names_out")` | DataFrame-returning methods whose columns come from `get_feature_names_out()`. | #### Handling Class Labels diff --git a/docs/source/index.rst b/docs/source/index.rst index 2ecee066df..f51a809d78 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -67,7 +67,8 @@ Community & Support =================== * :doc:`User Guide ` - Comprehensive usage documentation -* :doc:`API Reference ` - Complete API documentation +* :doc:`Python API Reference ` - Supported user-facing API documentation +* :doc:`Developer Guide ` - Contributor guidance and internal C++ reference * `GitHub Issues `_ - Report bugs and request features * `CUDA-X Data Science Community `_ - Join our community @@ -78,5 +79,5 @@ Community & Support user_guide.rst Zero Code Change Acceleration api/index - cpp/index + developer_guide/index cuml_blogs.rst diff --git a/wiki/DEFINITION_OF_DONE_CRITERIA.md b/wiki/DEFINITION_OF_DONE_CRITERIA.md deleted file mode 100644 index 25394c2395..0000000000 --- a/wiki/DEFINITION_OF_DONE_CRITERIA.md +++ /dev/null @@ -1,82 +0,0 @@ -# Defining cuML's Definition of Done Criteria - - -## Algorithm Completion Checklist - -Below is a quick and simple checklist for developers to determine whether an algorithm is complete and ready for release. Most of these items contain more detailed descriptions in their corresponding developer guide. The checklist is broken down by layer (C++ or Python) and categorized further into - -- **Design:** All algorithms should be designed with an eye on maintainability, performance, readability, and robustness. -- **Testing:** The goal for automated testing is to increase both the spread and the depth of code coverage as much as possible in order to ease time spent fixing bugs and developing new features. Additionally, a very important factor for a tool like `cuml` is to provide testing with multiple datasets that really stress the mathematical behavior of the algorithms. A comprehensive set of tests lowers the possibility for regressions and the introduction of bugs as the code evolves between versions. This covers both correctness & performance. -- **Documentation:** User-facing documentation should be complete and descriptive. Developer-facing documentation should be used for constructs which are complex and/or not immediately obvious. -- **Performance:** Algorithms should be [benchmarked] and profiled regularly to spot potential bottlenecks, performance regressions, and memory problems. - -### C++ - -#### Design - -- Existing prims are used wherever possible -- Array inputs and outputs to algorithms are accepted on device -- New prims created wherever there is potential for reuse across different algorithms or prims -- User-facing API is [stateless](cpp/DEVELOPER_GUIDE.md#public-cuml-interface) and follows the [plain-old data (POD)](https://en.wikipedia.org/wiki/Passive_data_structure) design paradigm -- Public API contains a C-Wrapper around the stateless API -- (optional) Public API contains an Scikit-learn-like stateful wrapper around the stateless API - -#### Testing - -- Prims: GTests with different inputs -- Algorithms: End-to-end GTests with different inputs and different datasets - -#### Documentation - -- Complete and comprehensive [Doxygen](http://www.doxygen.nl/manual/docblocks.html) strings explaining the public API, restrictions, and gotchas. Any array parameters should also note whether the underlying memory is host or device. -- Array inputs/outputs should also mention their expected size/dimension. -- If there are references to the underlying algorithm, they must be cited too. - - -### Python - -#### Design - -- Python class is as "near drop-in replacement" for Scikit-learn (or relevant industry standard) API as possible. This means parameters have the same names as Scikit-learn, and where differences exist, they are clearly documented in docstrings. -- It is recommended to open an initial PR with the API design if there are going to be significant differences with reference APIs, or lack of a reference API, to have a discussion about it. -- Python class is pickleable and a test has been added to `cuml/tests/test_pickle.py` -- Estimators follow the implementation guidelines in [ESTIMATOR_GUIDE.md](python/ESTIMATOR_GUIDE.md) - -#### Testing - -- Pytests for wrapper functionality against Scikit-learn using relevant datasets -- Stress tests against reasonable inputs (e.g short-wide, tall-narrow, different numerical precision) -- Pytests for pickle capability -- Pytests to evaluate correctness against Scikit-learn on a variety of datasets -- Add algorithm to benchmarks package in `python/cuml/benchmarks/algorithms.py` and benchmarks notebook in `python/cuml/notebooks/tools/cuml_benchmarks.ipynb` -- PyTests that run in the "unit"-level marker should be quick to execute and should, in general, not significantly increase end-to-end test execution. - -#### Documentation - -- Complete and comprehensive Pydoc strings explaining public API, restrictions, a usage example, and gotchas. This should be in [Numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) format -- Docstrings include references to any scientific papers or standard publications on the underlying algorithm (e.g paper or Scikit-learn algorithm being implemented or a description of the algorithm used if nonstandard). - - -## Review Checklist - -Aside from the general algorithm expectations outlined in the checklists above, code reviewers should use the following checklist to make sure the algorithm meets cuML standards. - -### All - -- New files contain necessary license headers -- Diff does not contain files with excess formatting changes, without other changes also being made to the file -- Code does not contain any known serious memory leaks or garbage collection issues -- Modifications are cohesive and in-scope for the PR's intended purpose -- Changes to the public API will not have a negative impact to existing users between minor versions (eg. large changes to very popular public APIs go through a deprecation cycle to preserve backwards compatibility) -- Where it is reasonable to do so, unexpected inputs fail gracefully and provide actionable feedback to the user -- Automated tests properly exercise the changes in the PR -- New algorithms provide benchmarks (both C++ and Python) - - -### C++ - -- New GTests are being enabled in `CMakeLists.txt` - -### Python - -- Look at the list of slowest PyTests printed in the CI logs and check that any newly committed PyTests are not going to have a significant impact on the end-to-end execution. diff --git a/wiki/README.md b/wiki/README.md deleted file mode 100644 index efbab1415c..0000000000 --- a/wiki/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# cuML Wiki Documentation - -This wiki is provided as an extension to cuML's public documentation, geared toward developers on the project. - -If you are interested in contributing to cuML, read through our [contributing guide](../CONTRIBUTING.md). You are -also encouraged to read through our Python [developer guide](python/DEVELOPER_GUIDE.md) and C++ -[developer guide](cpp/DEVELOPER_GUIDE.md) to gain an understanding for how we design our algorithms. - -We have criteria for defining our [definition of done](DEFINITION_OF_DONE_CRITERIA.md) to allow us to provide high performance, maintainable and overall high quality implementations, while giving as much transparency as possible about the status of our algorithms with our users. - -For running the benchmark suite (with or without cuML installed), see [BENCHMARK.md](BENCHMARK.md). diff --git a/wiki/cpp/DEVELOPER_GUIDE.md b/wiki/cpp/DEVELOPER_GUIDE.md deleted file mode 100644 index 8a38cdcfb5..0000000000 --- a/wiki/cpp/DEVELOPER_GUIDE.md +++ /dev/null @@ -1,454 +0,0 @@ -# cuML developer guide -This document summarizes rules and best practices for contributions to the cuML C++ component of NVIDIA/cuml. This is a living document and contributions for clarifications or fixes and issue reports are highly welcome. - -## General -Please start by reading [CONTRIBUTING.md](../../CONTRIBUTING.md). - -## Performance -1. In performance critical sections of the code, favor `cudaDeviceGetAttribute` over `cudaDeviceGetProperties`. See corresponding CUDA devblog [here](https://devblogs.nvidia.com/cuda-pro-tip-the-fast-way-to-query-device-properties/) to know more. -2. If an algo requires you to launch GPU work in multiple cuda streams, do not create multiple `raft::handle_t` objects, one for each such work stream. Instead, expose a `n_streams` parameter in that algo's cuML C++ interface and then rely on `raft::handle_t::get_internal_stream()` to pick up the right cuda stream. Refer to the section on [CUDA Resources](#cuda-resources) and the section on [Threading](#TBD) for more details. TIP: use `raft::handle_t::get_num_internal_streams` to know how many such streams are available at your disposal. - -## Threading Model - -With the exception of the raft::handle_t, cuML algorithms should maintain thread-safety and are, in general, -assumed to be single threaded. This means they should be able to be called from multiple host threads so -long as different instances of `raft::handle_t` are used. - -Exceptions are made for algorithms that can take advantage of multiple CUDA streams within multiple host threads -in order to oversubscribe or increase occupancy on a single GPU. In these cases, the use of multiple host -threads within cuML algorithms should be used only to maintain concurrency of the underlying CUDA streams. -Multiple host threads should be used sparingly, be bounded, and should steer clear of performing CPU-intensive -computations. - -A good example of an acceptable use of host threads within a cuML algorithm might look like the following - -``` -handle.sync_stream(); - -int n_streams = handle.get_num_internal_streams(); - -#pragma omp parallel for num_threads(n_threads) -for(int i = 0; i < n; i++) { - int thread_num = omp_get_thread_num() % n_threads; - cudaStream_t s = handle.get_stream_from_stream_pool(thread_num); - ... possible light cpu pre-processing ... - my_kernel1<<>>(...); - ... - ... some possible async d2h / h2d copies ... - my_kernel2<<>>(...); - ... - handle.sync_stream(s); - ... possible light cpu post-processing ... -} -``` - -In the example above, if there is no CPU pre-processing at the beginning of the for-loop, an event can be registered in -each of the streams within the for-loop to make them wait on the stream from the handle. If there is no CPU post-processing -at the end of each for-loop iteration, `handle.sync_stream(s)` can be replaced with a single `handle.sync_stream_pool()` -after the for-loop. - -To avoid compatibility issues between different threading models, the only threading programming allowed in cuML is OpenMP. -Though cuML's build enables OpenMP by default, cuML algorithms should still function properly even when OpenMP has been -disabled. If the CPU pre- and post-processing were not needed in the example above, OpenMP would not be needed. - -The use of threads in third-party libraries is allowed, though they should still avoid depending on a specific OpenMP runtime. - -## Public cuML interface -### Terminology -We have the following supported APIs: -1. Core cuML interface aka stateless C++ API aka C++ API aka `libcuml.so` -2. Stateful convenience C++ API - wrapper around core API (WIP) - -### Motivation -The cuML C++ API is stateless so that algorithm state (models, hyper-parameters, and similar data) can be serialized in a straightforward way, which supports features such as pickling in the Python layer, and so that a small, explicit surface is presented to the bindings above this library. - -This section lays out guidelines for managing state along the API of cuML. - -### General guideline -As mentioned before, functions exposed via the C++ API must be stateless. Things that are OK to be exposed on the interface: -1. Any [POD](https://en.wikipedia.org/wiki/Passive_data_structure) - see [std::is_pod](https://en.cppreference.com/w/cpp/types/is_pod) as a reference for C++11 POD types. -2. `raft::handle_t` - since it stores GPU-related state which has nothing to do with the model/algo state. -3. Pointers to POD types (explicitly putting it out, even though it can be considered as a POD). -Internal to the C++ API, these stateless functions are free to use their own temporary classes, as long as they are not exposed on the interface. - -### Stateless C++ API -Using the Decision Tree Classifier algorithm as an example, the following way of exposing its API would be wrong according to the guidelines in this section, since it exposes a non-POD C++ class object in the C++ API: -```cpp -template -class DecisionTreeClassifier { - TreeNode* root; - DTParams params; - const raft::handle_t &handle; -public: - DecisionTreeClassifier(const raft::handle_t &handle, DTParams& params, bool verbose=false); - void fit(const T *input, int n_rows, int n_cols, const int *labels); - void predict(const T *input, int n_rows, int n_cols, int *predictions); -}; - -void decisionTreeClassifierFit(const raft::handle_t &handle, const float *input, int n_rows, int n_cols, - const int *labels, DecisionTreeClassifier *model, DTParams params, - bool verbose=false); -void decisionTreeClassifierPredict(const raft::handle_t &handle, const float* input, - DecisionTreeClassifier *model, int n_rows, - int n_cols, int* predictions, bool verbose=false); -``` - -An alternative correct way to expose this could be: -```cpp -// NOTE: this example assumes that TreeNode and DTParams are the model/state that need to be stored -// and passed between fit and predict methods -template struct TreeNode { /* nested tree-like data structure, but written as a POD! */ }; -struct DTParams { /* hyper-params for building DT */ }; -typedef TreeNode TreeNodeF; -typedef TreeNode TreeNodeD; - -void decisionTreeClassifierFit(const raft::handle_t &handle, const float *input, int n_rows, int n_cols, - const int *labels, TreeNodeF *&root, DTParams params, - bool verbose=false); -void decisionTreeClassifierPredict(const raft::handle_t &handle, const double* input, int n_rows, - int n_cols, const TreeNodeD *root, int* predictions, - bool verbose=false); -``` -The above example understates the complexity involved with exposing a tree-like data structure across the interface! However, this example should be simple enough to drive the point across. - -### Other functions on state -These guidelines also mean that it is the responsibility of C++ API to expose methods to load and store (aka marshalling) such a data structure. Further continuing the Decision Tree Classifier example, the following methods could achieve this: -```cpp -void storeTree(const TreeNodeF *root, std::ostream &os); -void storeTree(const TreeNodeD *root, std::ostream &os); -void loadTree(TreeNodeF *&root, std::istream &is); -void loadTree(TreeNodeD *&root, std::istream &is); -``` -It is also worth noting that for algorithms such as the members of GLM, where models consist of an array of weights and are therefore easy to manipulate directly by the users, such custom load/store methods might not be explicitly needed. - -### Stateful C++ API -This scikit-learn-esq C++ API should always be a wrapper around the stateless C++ API, NEVER the other way around. The design discussion about the right way to expose such a wrapper around `libcuml.so` is [still going on](https://github.com/NVIDIA/cuml/issues/456) So, stay tuned for more details. - -### File naming convention -1. An ML algorithm `` is to be contained inside the folder named `src/`. -2. `.hpp` and `.[cpp|cu]` contain C++ API declarations and definitions respectively. - -## Coding style - -## Code format -### Introduction -cuML relies on `clang-format` to enforce code style across all C++ and CUDA source code. The coding style is based on the [Google style guide](https://google.github.io/styleguide/cppguide.html#Formatting). The only digressions from this style are the following. -1. Do not split empty functions/records/namespaces. -2. Two-space indentation everywhere, including the line continuations. -3. Disable reflowing of comments. -The reasons behind these deviations from the Google style guide are given in comments [here](../../cpp/.clang-format). - -### How is the check done? -All formatting checks are done by this python script: [run-clang-format.py](../../cpp/scripts/run-clang-format.py) which is effectively a wrapper over `clang-format`. An error is raised if the code diverges from the format suggested by clang-format. It is expected that the developers run this script to detect and fix formatting violations before creating PR. - -#### As part of CI -[run-clang-format.py](../../cpp/scripts/run-clang-format.py) is executed as part of our CI tests. If there are any formatting violations, PR author is expected to fix those to get CI passing. Steps needed to fix the formatting violations are described in the subsequent sub-section. - -#### Manually -Developers can also manually (or setup this command as part of git pre-commit hook) run this check by executing: -```bash -python ./cpp/scripts/run-clang-format.py -``` -From the root of the cuML repository. - -### How to know the formatting violations? -When there are formatting errors, [run-clang-format.py](../../cpp/scripts/run-clang-format.py) prints a `diff` command, showing where there are formatting differences. Unfortunately, unlike `flake8`, `clang-format` does NOT print descriptions of the violations, but instead directly formats the code. So, the only way currently to know about formatting differences is to run the diff command as suggested by this script against each violating source file. - -### How to fix the formatting violations? -When there are formatting violations, [run-clang-format.py](../../cpp/scripts/run-clang-format.py) prints at the end, the exact command that can be run by developers to fix them. This is the easiest way to fix formatting errors. [This screencast](https://asciinema.org/a/287367) shows how developers can check for formatting violations in their branches and also how to fix those, before sending out PRs. - -In short, to bulk-fix all the formatting violations, execute the following command: -```bash -python ./cpp/scripts/run-clang-format.py -inplace -``` -From the root of the cuML repository. - -### clang-format version? -To avoid spurious code style violations we specify the exact clang-format version required, currently `8.0.0`. This is enforced by the [run-clang-format.py](../../cpp/scripts/run-clang-format.py) script itself. Refer [here](../../cpp/README.md#dependencies) for the list of build-time dependencies. - -### Additional scripts -Along with clang, there are are the include checker and copyright checker scripts for checking style, which can be performed as part of CI, as well as manually. - -#### #include style -[include_checker.py](../../cpp/scripts/include_checker.py) is used to enforce the include style as follows: -1. `#include "..."` should be used for referencing local files only. It is acceptable to be used for referencing files in a sub-folder/parent-folder of the same algorithm, but should never be used to include files in other algorithms or between algorithms and the primitives or other dependencies. -2. `#include <...>` should be used for referencing everything else - -Manually, run the following to bulk-fix include style issues: -```bash -python ./cpp/scripts/include_checker.py --inplace [cpp/include cpp/src cpp/src_prims cpp/test ... list of folders which you want to fix] -``` - -#### Copyright header -RAPIDS [pre-commit-hooks](https://github.com/rapidsai/pre-commit-hooks) checks the Copyright -header for all git-modified files. - -Manually, you can run the following to bulk-fix the header on all files in the repository: -```bash -pre-commit run -a verify-copyright -``` -Keep in mind that this only applies to files tracked by git that have been modified. - -## Error handling -Call CUDA APIs via the provided helper macros `RAFT_CUDA_TRY`, `RAFT_CUBLAS_TRY` and `RAFT_CUSOLVER_TRY`. These macros take care of checking the return values of the used API calls and generate an exception when the command is not successful. If you need to avoid an exception, e.g. inside a destructor, use `RAFT_CUDA_TRY_NO_THROW`, `RAFT_CUBLAS_TRY_NO_THROW ` and `RAFT_CUSOLVER_TRY_NO_THROW ` (currently not available, see https://github.com/NVIDIA/cuml/issues/229). These macros log the error but do not throw an exception. - -## Logging -### Introduction -Anything and everything about logging is defined inside [logger.hpp](../../cpp/include/cuml/common/logger.hpp). It uses [spdlog](https://github.com/gabime/spdlog) underneath, but this information is transparent to all. - -### Usage -```cpp -#include - -// Inside your method or function, use any of these macros -CUML_LOG_TRACE("Hello %s!", "world"); -CUML_LOG_DEBUG("Hello %s!", "world"); -CUML_LOG_INFO("Hello %s!", "world"); -CUML_LOG_WARN("Hello %s!", "world"); -CUML_LOG_ERROR("Hello %s!", "world"); -CUML_LOG_CRITICAL("Hello %s!", "world"); -``` - -### Changing logging level -There are 7 logging levels with each successive level becoming quieter: -1. CUML_LEVEL_TRACE -2. CUML_LEVEL_DEBUG -3. CUML_LEVEL_INFO -4. CUML_LEVEL_WARN -5. CUML_LEVEL_ERROR -6. CUML_LEVEL_CRITICAL -7. CUML_LEVEL_OFF -Pass one of these as per your needs into the `setLevel()` method as follows: -```cpp -ML::Logger::get.setLevel(CUML_LEVEL_WARN); -// From now onwards, this will print only WARN and above kind of messages -``` - -### Changing logging pattern -Pass the [format string](https://github.com/gabime/spdlog/wiki/3.-Custom-formatting) as follows in order use a different logging pattern than the default. -```cpp -ML::Logger::get.setPattern(YourFavoriteFormat); -``` -One can also use the corresponding `getPattern()` method to know the current format as well. - -### Temporarily changing the logging pattern -Sometimes, we need to temporarily change the log pattern (eg: for reporting decision tree structure). This can be achieved in a RAII-like approach as follows: -```cpp -{ - PatternSetter _(MyNewTempFormat); - // new log format is in effect from here onwards - doStuff(); - // once the above temporary object goes out-of-scope, the old format will be restored -} -``` - -### Tips -* Do NOT end your logging messages with a newline! It is automatically added by spdlog. -* The `CUML_LOG_TRACE()` is by default not compiled due to the `CUML_ACTIVE_LEVEL` macro setup, for performance reasons. If you need it to be enabled, change this macro accordingly during compilation time - -## Documentation -All external interfaces need to have a complete [doxygen](http://www.doxygen.nl) API documentation. This is also recommended for internal interfaces. - -## Testing and Unit Testing -TODO: Add this - -## Device and Host memory allocations -To enable `libcuml` users to control how memory for temporary data is allocated, allocate device memory using the allocator provided: -```cpp -template -void foo(const raft::handle_t& h, cudaStream_t stream, ... ) -{ - T* temp_h = h.get_device_allocator()->allocate(n*sizeof(T), stream); - ... - h.get_device_allocator()->deallocate(temp_h, n*sizeof(T), stream); -} -``` -The same rule applies to larger amounts of host heap memory: -```cpp -template -void foo(const raft::handle_t& h, cudaStream_t stream, ... ) -{ - T* temp_h = h.get_host_allocator()->allocate(n*sizeof(T), stream); - ... - h.get_host_allocator()->deallocate(temp_h, n*sizeof(T), stream); -} -``` -Small host memory heap allocations, e.g. as internally done by STL containers, are fine, e.g. an `std::vector` managing only a handful of integers. -Both the Host and the Device Allocators might allow asynchronous stream ordered allocation and deallocation. This can provide significant performance benefits so a stream always needs to be specified when allocating or deallocating (see [Asynchronous operations and stream ordering](#asynchronous-operations-and-stream-ordering)). `ML::deviceAllocator` returns pinned device memory on the current device, while `ML::hostAllocator` returns host memory. A user of cuML can write customized allocators and pass them into cuML. If a cuML user does not provide custom allocators default allocators will be used. For `ML::deviceAllocator` the default is to use `cudaMalloc`/`cudaFree`. For `ML::hostAllocator` the default is to use `cudaMallocHost`/`cudaFreeHost`. -There are two simple container classes compatible with the allocator interface `MLCommon::device_buffer` available in `src_prims/common/device_buffer.hpp` and `MLCommon::host_buffer` available in `src_prims/common/host_buffer.hpp`. These allow to follow the [RAII idiom](https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization) to avoid resources leaks and enable exception safe code. These containers also allow asynchronous allocation and deallocation using the `resize` and `release` member functions: -```cpp -template -void foo(const raft::handle_t& h, ..., cudaStream_t stream ) -{ - ... - MLCommon::device_buffer temp( h.get_device_allocator(), stream, 0 ) - - temp.resize(n, stream); - kernelA<<>>(..., temp.data(), ...); - kernelB<<>>(..., temp.data(), ...); - temp.release(stream); -} -``` -The motivation for `MLCommon::host_buffer` and `MLCommon::device_buffer` over using `std::vector` or `thrust::device_vector` (which would require thrust 1.9.4 or later) is to enable exception safe asynchronous allocation and deallocation following stream semantics with an explicit interface while avoiding the overhead of implicitly initializing the underlying allocation. -To use `ML::hostAllocator` with a STL container the header `src/common/allocatorAdapter.hpp` provides `ML::stdAllocatorAdapter`: -```cpp -template -void foo(const raft::handle_t& h, ..., cudaStream_t stream ) -{ - ... - std::vector > temp( n, val, ML::stdAllocatorAdapter(h.get_host_allocator(), stream) ) - ... -} -``` -If thrust 1.9.4 or later is available for use in cuML a similar allocator can be provided for `thrust::device_vector`. - -### Using Thrust -To ensure that thrust algorithms allocate temporary memory via the provided device memory allocator, use the `ML::thrustAllocatorAdapter` available in `src/common/allocatorAdapter.hpp` with the `thrust::cuda::par` execution policy: -```cpp -void foo(const raft::handle_t& h, ..., cudaStream_t stream ) -{ - ML::thrustAllocatorAdapter alloc( h.get_device_allocator(), stream ); - auto execution_policy = thrust::cuda::par(alloc).on(stream); - thrust::for_each(execution_policy, ... ); -} -``` -The header `src/common/allocatorAdapter.hpp` also provides a helper function to create an execution policy: -```cpp -void foo(const raft::handle_t& h, ... , cudaStream_t stream ) -{ - auto execution_policy = ML::thrust_exec_policy(h.get_device_allocator(),stream); - thrust::for_each(execution_policy->on(stream), ... ); -} -``` - -## Asynchronous operations and stream ordering -All ML algorithms should be as asynchronous as possible avoiding the use of the default stream (aka as NULL or `0` stream). Implementations that require only one CUDA Stream should use the stream from `raft::handle_t`: -```cpp -void foo(const raft::handle_t& h, ...) -{ - cudaStream_t stream = h.get_stream(); -} -``` -When multiple streams are needed, e.g. to manage a pipeline, use the internal streams available in `raft::handle_t` (see [CUDA Resources](#cuda-resources)). If multiple streams are used all operations still must be ordered according to `raft::handle_t::get_stream()`. Before any operation in any of the internal CUDA streams is started, all previous work in `raft::handle_t::get_stream()` must have completed. Any work enqueued in `raft::handle_t::get_stream()` after a cuML function returns should not start before all work enqueued in the internal streams has completed. E.g. if a cuML algorithm is called like this: -```cpp -void foo(const double* const srcdata, double* const result) -{ - cudaStream_t stream; - CUDA_RT_CALL( cudaStreamCreate( &stream ) ); - raft::handle_t raftHandle( stream ); - - ... - - RAFT_CUDA_TRY( cudaMemcpyAsync( srcdata, h_srcdata.data(), n*sizeof(double), cudaMemcpyHostToDevice, stream ) ); - - ML::algo(raft::handle_t, dopredict, srcdata, result, ... ); - - RAFT_CUDA_TRY( cudaMemcpyAsync( h_result.data(), result, m*sizeof(int), cudaMemcpyDeviceToHost, stream ) ); - - ... -} -``` -No work in any stream should start in `ML::algo` before the `cudaMemcpyAsync` in `stream` launched before the call to `ML::algo` is done. And all work in all streams used in `ML::algo` should be done before the `cudaMemcpyAsync` in `stream` launched after the call to `ML::algo` starts. - -This can be ensured by introducing interstream dependencies with CUDA events and `cudaStreamWaitEvent`. For convenience, the header `raft/core/handle.hpp` provides the class `raft::stream_syncer` which lets all `raft::handle_t` internal CUDA streams wait on `raft::handle_t::get_stream()` in its constructor and in its destructor and lets `raft::handle_t::get_stream()` wait on all work enqueued in the `raft::handle_t` internal CUDA streams. The intended use would be to create a `raft::stream_syncer` object as the first thing in a entry function of the public cuML API: - -```cpp -void cumlAlgo(const raft::handle_t& handle, ...) -{ - raft::streamSyncer _(handle); -} -``` -This ensures the stream ordering behavior described above. - -### Using Thrust -To ensure that thrust algorithms are executed in the intended stream the `thrust::cuda::par` execution policy should be used (see [Using Thrust](#allocationsthrust) in [Device and Host memory allocations](#device-and-host-memory-allocations)). - -## CUDA Resources - -Do not create reusable CUDA resources directly in implementations of ML algorithms. Instead, use the existing resources in `raft::handle_t` to avoid constant creation and deletion of reusable resources such as CUDA streams, CUDA events or library handles. Please file a feature request if a resource handle is missing in `raft::handle_t`. -The resources can be obtained like this -```cpp -void foo(const raft::handle_t& h, ...) -{ - cublasHandle_t cublasHandle = h.get_cublas_handle(); - const int num_streams = h.get_num_internal_streams(); - const int stream_idx = ... - cudaStream_t stream = h.get_internal_stream(stream_idx); - ... -} -``` - -The example below shows one way to create `nStreams` number of internal cuda streams which can later be used by the algos inside cuML. For a full working example of how to use internal streams to schedule work on a single GPU, the reader is further referred to [this PR](https://github.com/NVIDIA/cuml/pull/1015). In this PR, the internal streams inside `raft::handle_t` are used to schedule more work onto a GPU for Random Forest building. -```cpp -int main(int argc, char** argv) -{ - int nStreams = argc > 1 ? atoi(argv[1]) : 0; - raft::handle_t handle(nStreams); - foo(handle, ...); -} -``` - -## Multi-GPU - -The multi GPU paradigm of cuML is **O**ne **P**rocess per **G**PU (OPG). Each algorithm should be implemented in a way that it can run with a single GPU without any specific dependencies to a particular communication library. A multi-GPU implementation should use the methods offered by the class `raft::comms::comms_t` from [raft/core/comms.hpp] for inter-rank/GPU communication. It is the responsibility of the user of cuML to create an initialized instance of `raft::comms::comms_t`. - -E.g. with a CUDA-aware MPI, a cuML user could use code like this to inject an initialized instance of `raft::comms::mpi_comms` into a `raft::handle_t`: - -```cpp -#include -#include -#include -#include -... -int main(int argc, char * argv[]) -{ - MPI_Init(&argc, &argv); - int rank = -1; - MPI_Comm_rank(MPI_COMM_WORLD, &rank); - - int local_rank = -1; - { - MPI_Comm local_comm; - MPI_Comm_split_type(MPI_COMM_WORLD, MPI_COMM_TYPE_SHARED, rank, MPI_INFO_NULL, &local_comm); - - MPI_Comm_rank(local_comm, &local_rank); - - MPI_Comm_free(&local_comm); - } - - cudaSetDevice(local_rank); - - mpi_comms raft_mpi_comms; - MPI_Comm_dup(MPI_COMM_WORLD, &raft_mpi_comms); - - { - raft::handle_t raftHandle; - initialize_mpi_comms(raftHandle, raft_mpi_comms); - - ... - - ML::mlalgo(raftHandle, ... ); - } - - MPI_Comm_free(&raft_mpi_comms); - - MPI_Finalize(); - return 0; -} -``` - -A cuML developer can assume the following: - * A instance of `raft::comms::comms_t` was correctly initialized. - * All processes that are part of `raft::comms::comms_t` call into the ML algorithm cooperatively. - -The initialized instance of `raft::comms::comms_t` can be accessed from the `raft::handle_t` instance: - -```cpp -void foo(const raft::handle_t& h, ...) -{ - const MLCommon::cumlCommunicator& communicator = h.get_comms(); - const int rank = communicator.get_rank(); - const int size = communicator.get_size(); - ... -} -``` diff --git a/wiki/mnmg/Using_Infiniband_for_MNMG.md b/wiki/mnmg/Using_Infiniband_for_MNMG.md deleted file mode 100644 index 34c2b14d38..0000000000 --- a/wiki/mnmg/Using_Infiniband_for_MNMG.md +++ /dev/null @@ -1,392 +0,0 @@ -> [!WARNING] -> Instructions on this page are deprecated and will not work with the latest version of cuML. - -# Using Infiniband for Multi-Node Multi-GPU cuML - -These instructions outline how to run multi-node multi-GPU cuML on devices with Infiniband. These instructions assume the necessary Infiniband hardware has already been installed and the relevant software has already been configured to enable communication over the Infiniband devices. - -The steps in this wiki post have been largely adapted from the [Experiments in High Performance Networking with UCX and DGX](https://blog.dask.org/2019/06/09/ucx-dgx) blog by Matthew Rocklin and Rick Zamora. - -## 1. Install UCX - -### From Conda - -Note: this package is experimental and will eventually be supported under the rapidsai channel. Currently, it requires CUDA9.2 but a CUDA10 package is also in the works. - -`conda install -c conda-forge -c jakirkham/label/ucx cudatoolkit=9.2 ucx-proc=*=gpu ucx python=3.7` - -### From Source - -Install autogen if it's not already installed: -```bash -sudo apt-get install autogen autoconf libtool -``` - -Optionally install `gdrcopy` for faster GPU-Network card data transfer: - -From the [ucx wiki](https://github.com/openucx/ucx/wiki/NVIDIA-GPU-Support), `gdrcopy` can be installed, and might be necessary, to enable faster GPU-Network card data transfer. - -Here are the install instructions, taken from [gdrcopy github](https://github.com/NVIDIA/gdrcopy) -```bash -git clone https://github.com/NVIDIA/gdrcopy.git -cd gdrcopy -make -j PREFIX=$CONDA_INSTALL_PREFIX CUDA=/usr/local/cuda && make -j install -sudo ./insmod.sh -``` - - -```bash -git clone https://github.com/cjnolet/ucx-py.git -cd ucx -git checkout fea-ext-expose_worker_and_ep -./autogen.sh -mkdir build && cd build -../configure --prefix=$CONDA_PREFIX --with-cuda=/usr/local/cuda --enable-mt --disable-cma CPPFLAGS="-I//usr/local/cuda/include" -make -j install -``` - -Note: If you have installed `gdrcopy`, you can add `--with-gdrcopy=/path/to/gdrcopy` to the options in `configure` - -Verify with `ucx_info -d`. You should expect to see line(s) with the `rc` transport: - -``` -# Transport: rc -# -# Device: mlx5_0:1 -# -# capabilities: -# bandwidth: 11794.23 MB/sec -# latency: 600 nsec + 1 * N -# overhead: 75 nsec -# put_short: <= 124 -# put_bcopy: <= 8K -# put_zcopy: <= 1G, up to 8 iov -# put_opt_zcopy_align: <= 512 -# put_align_mtu: <= 4K -# get_bcopy: <= 8K -# get_zcopy: 65..1G, up to 8 iov -# get_opt_zcopy_align: <= 512 -# get_align_mtu: <= 4K -# am_short: <= 123 -# am_bcopy: <= 8191 -# am_zcopy: <= 8191, up to 7 iov -# am_opt_zcopy_align: <= 512 -# am_align_mtu: <= 4K -# am header: <= 127 -# domain: device -# connection: to ep -# priority: 30 -# device address: 3 bytes -# ep address: 4 bytes -# error handling: peer failure - -``` - -You should also expect to see lines with `cuda_copy` and `cuda_ipc` transports: - -``` -# Transport: cuda_copy -# -# Device: cudacopy0 -# -# capabilities: -# bandwidth: 6911.00 MB/sec -# latency: 10000 nsec -# overhead: 0 nsec -# put_short: <= 4294967295 -# put_zcopy: unlimited, up to 1 iov -# put_opt_zcopy_align: <= 1 -# put_align_mtu: <= 1 -# get_short: <= 4294967295 -# get_zcopy: unlimited, up to 1 iov -# get_opt_zcopy_align: <= 1 -# get_align_mtu: <= 1 -# connection: to iface -# priority: 0 -# device address: 0 bytes -# iface address: 8 bytes -# error handling: none -``` - -``` -# Memory domain: cuda_ipc -# component: cuda_ipc -# register: <= 1G, cost: 0 nsec -# remote key: 104 bytes -# -# Transport: cuda_ipc -# -# Device: cudaipc0 -# -# capabilities: -# bandwidth: 24000.00 MB/sec -# latency: 1 nsec -# overhead: 0 nsec -# put_zcopy: <= 1G, up to 1 iov -# put_opt_zcopy_align: <= 1 -# put_align_mtu: <= 1 -# get_zcopy: <= 1G, up to 1 iov -# get_opt_zcopy_align: <= 1 -# get_align_mtu: <= 1 -# connection: to iface -# priority: 0 -# device address: 8 bytes -# iface address: 4 bytes -# error handling: none -# - -``` - - -If you configured UCX with the `gdrcopy` option, you should also expect to see transports in this list: - -```bash -# Memory domain: gdr_copy -# component: gdr_copy -# register: unlimited, cost: 0 nsec -# remote key: 32 bytes -# -# Transport: gdr_copy -# -# Device: gdrcopy0 -# -# capabilities: -# bandwidth: 6911.00 MB/sec -# latency: 1000 nsec -# overhead: 0 nsec -# put_short: <= 4294967295 -# get_short: <= 4294967295 -# connection: to iface -# priority: 0 -# device address: 0 bytes -# iface address: 8 bytes -# error handling: none -``` - -To better understand the CUDA-based transports in UCX, refer to [this wiki](https://github.com/openucx/ucx/wiki/NVIDIA-GPU-Support) for more details. - - -## 2. Install ucx-py - -### From Conda - -Note: this package is experimental and will eventually be supported under the rapidsai channel. Currently, it requires CUDA9.2 but a CUDA10 package is also in the works. - -`conda install -c conda-forge -c jakirkham/label/ucx cudatoolkit=9.2 ucx-py python=3.7` - - -### From Source - -```bash -git clone git@github.com:rapidsai/ucx-py -cd ucx-py - -export UCX_PATH=$CONDA_PREFIX -make -j install -``` - -## 3. Install NCCL - -It's important that NCCL 2.4+ be installed and no previous versions of NCCL are conflicting on your library path. This will cause compile errors during the build of cuML. - - -```bash -conda install -c nvidia nccl -``` - -Create the file `.nccl.conf` in your home dir with the following: -```bash -NCCL_SOCKET_IFNAME=ib0 -``` - -## 4. Enable IP over IB interface at ib0 - -Follow the instructions at [this link](https://docs.oracle.com/cd/E19436-01/820-3522-10/ch4-linux.html#50536461_82843) to create an IP interface for the IB devices. - -From the link above, when the IP over IB kernel module has already been installed, mapping to an IP interface is simple: -``` -sudo ifconfig ib0 10.0.0.50/24 -``` - -You can verify the interface was created properly with `ifconfig ib0` - -The output should look like this: - -``` -ib0 Link encap:UNSPEC HWaddr 80-00-00-68-FE-80-00-00-00-00-00-00-00-00-00-00 - inet addr:10.0.0.50 Bcast:10.0.0.255 Mask:255.255.255.0 - inet6 addr: fe80::526b:4b03:f5:ce9c/64 Scope:Link - UP BROADCAST RUNNING MULTICAST MTU:65520 Metric:1 - RX packets:2655 errors:0 dropped:0 overruns:0 frame:0 - TX packets:2697 errors:0 dropped:10 overruns:0 carrier:0 - collisions:0 txqueuelen:256 - RX bytes:183152 (183.1 KB) TX bytes:194696 (194.6 KB) - -``` - -## 5. Set UCX environment vars - -Use `ibstatus` to see your open IB devices. Output will look like this: - -``` -Infiniband device 'mlx5_0' port 1 status: - default gid: fe80:0000:0000:0000:506b:4b03:00f5:ce9c - base lid: 0xf - sm lid: 0x1 - state: 4: ACTIVE - phys state: 5: LinkUp - rate: 100 Gb/sec (4X EDR) - link_layer: InfiniBand - -Infiniband device 'mlx5_1' port 1 status: - default gid: fe80:0000:0000:0000:506b:4b03:0049:4236 - base lid: 0x6 - sm lid: 0x1 - state: 4: ACTIVE - phys state: 5: LinkUp - rate: 100 Gb/sec (4X EDR) - link_layer: InfiniBand - -Infiniband device 'mlx5_2' port 1 status: - default gid: fe80:0000:0000:0000:506b:4b03:00f5:cf04 - base lid: 0x2 - sm lid: 0x1 - state: 4: ACTIVE - phys state: 5: LinkUp - rate: 100 Gb/sec (4X EDR) - link_layer: InfiniBand - -Infiniband device 'mlx5_3' port 1 status: - default gid: fe80:0000:0000:0000:506b:4b03:0049:3eb2 - base lid: 0x11 - sm lid: 0x1 - state: 4: ACTIVE - phys state: 5: LinkUp - rate: 100 Gb/sec (4X EDR) - link_layer: InfiniBand - -``` - -Put the devices and ports in a `UCX_NET_DEVICES` environment variable: - - -```bash -export UCX_NET_DEVICES=mlx5_0:1,mlx5_3:1,mlx5_2:1,mlx5_1:1 -``` - -Set transports for UCX to use: -```bash -export UCX_TLS=rc,cuda_copy,cuda_ipc -``` - -Note: if `gdrcopy` was installed, add `gdr_copy` to the end of `UCX_TLS` - -## 6. Start Dask cluster on ib0 interface: - -Run this on the node designated for the scheduler: -```bash -dask-scheduler --protocol ucx --interface ib0 -``` - -Then run this on each worker (for example, if the IP over IB device address running the scheduler is `10.0.0.50`): -```bash -dask-cuda-worker ucx://10.0.0.50:8786 -``` - -## 7. Run cumlCommunicator test: - -### First, create a Dask `Client` and cuML `Comms`: -```python -from dask.distributed import Client, wait -from cuml.raft.dask.common.comms import Comms -from cuml.dask.common import get_raft_comm_state -from cuml.dask.common import perform_test_comms_send_recv -from cuml.dask.common import perform_test_comms_allreduce - -import random - -c = Client("ucx://10.0.0.50:8786") -cb = Comms(comms_p2p=True) -cb.init() -``` - -### Test Point-to-Point Communications: -```python -n_trials = 2 - -def func_test_send_recv(sessionId, n_trials, r): - handle = get_raft_comm_state(sessionId)["handle"] - return perform_test_comms_send_recv(handle, n_trials) - -p2p_dfs=[c.submit(func_test_send_recv, cb.sessionId, n_trials, random.random(), workers=[w]) for wid, w in zip(range(len(cb.worker_addresses)), cb.worker_addresses)] -wait(p2p_dfs) - -p2p_result = list(map(lambda x: x.result(), p2p_dfs)) -print(str(p2p_result)) - -assert all(p2p_result) -``` - -You should see the following output on your workers: -``` - -========================= -Trial 0 -Rank 0 received: [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 8, 9, 14, 15] -Rank 1 received: [0, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 8, 9, 14, 15] -Rank 2 received: [0, 1, 3, 4, 5, 6, 7, 10, 11, 12, 13, 8, 9, 14, 15] -Rank 3 received: [0, 1, 2, 4, 5, 6, 7, 10, 11, 12, 13, 8, 9, 14, 15] -Rank 4 received: [0, 1, 2, 3, 5, 6, 7, 10, 11, 12, 13, 8, 9, 14, 15] -Rank 5 received: [0, 1, 2, 3, 4, 6, 7, 10, 11, 12, 13, 8, 9, 14, 15] -Rank 6 received: [0, 1, 2, 3, 4, 5, 7, 10, 11, 12, 13, 8, 9, 14, 15] -Rank 7 received: [0, 1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 8, 9, 14, 15] -========================= -========================= -Trial 1 -Rank 0 received: [11, 2, 13, 12, 9, 10, 15, 14, 1, 8, 5, 4, 3, 6, 7] -Rank 1 received: [2, 12, 11, 10, 9, 14, 13, 8, 15, 4, 5, 6, 3, 0, 7] -Rank 2 received: [12, 1, 11, 10, 9, 14, 13, 8, 15, 4, 5, 6, 3, 0, 7] -Rank 3 received: [2, 11, 12, 10, 9, 14, 13, 8, 15, 4, 1, 6, 5, 0, 7] -Rank 4 received: [2, 11, 12, 9, 13, 10, 15, 14, 1, 8, 3, 6, 5, 0, 7] -Rank 5 received: [2, 11, 12, 9, 10, 14, 13, 8, 15, 4, 1, 6, 3, 0, 7] -Rank 6 received: [2, 11, 12, 9, 10, 13, 15, 14, 1, 8, 5, 4, 3, 0, 7] -Rank 7 received: [2, 11, 12, 9, 10, 13, 14, 8, 15, 4, 1, 6, 5, 0, 3] -========================= - -``` - -### Test collective communications: -```python -def func_test_allreduce(sessionId, r): - handle = get_raft_comm_state(sessionId)["handle"] - return perform_test_comms_allreduce(handle) - -coll_dfs = [c.submit(func_test_allreduce, cb.sessionId, random.random(), workers=[w]) for wid, w in zip(range(len(cb.worker_addresses)), cb.worker_addresses)] -wait(coll_dfs) - -coll_result = list(map(lambda x: x.result(), coll_dfs)) - -coll_result - -assert all(coll_result) -``` - -You should see the following output on your workers: -``` -Clique size: 16 -Clique size: 16 -Clique size: 16 -Clique size: 16 -Clique size: 16 -Clique size: 16 -final_size: 16 -Clique size: 16 -Clique size: 16 -final_size: 16 -final_size: 16 -final_size: 16 -final_size: 16 -final_size: 16 -final_size: 16 -final_size: 16 -``` From b8674f2ab0aa59e20cf9378e63ca4daf253f3f50 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Thu, 10 Sep 2026 12:03:22 +0000 Subject: [PATCH 3/4] Publish C++ API through Sphinx --- build.sh | 6 ++++-- ci/build_docs.sh | 31 ++++++++++++++++++++++++++----- cpp/Doxyfile.in | 2 +- docs/README.md | 26 +++++++++++++++++++------- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/build.sh b/build.sh index e342dec3ba..2f5b0fb209 100755 --- a/build.sh +++ b/build.sh @@ -259,7 +259,7 @@ fi ################################################################################ # Configure for building all C++ targets -if completeBuild || hasArg libcuml || hasArg prims || hasArg bench || hasArg prims-bench || hasArg cppdocs || hasArg cpp-mgtests; then +if completeBuild || hasArg libcuml || hasArg prims || hasArg bench || hasArg prims-bench || hasArg cppdocs || hasArg pydocs || hasArg cpp-mgtests; then if (( BUILD_ALL_GPU_ARCH == 0 )); then CUML_CMAKE_CUDA_ARCHITECTURES="NATIVE" echo "Building for the architecture of the GPU in the system..." @@ -343,7 +343,9 @@ if (! hasArg --configure-only) && (completeBuild || hasArg libcuml || hasArg pri fi fi -if (! hasArg --configure-only) && hasArg cppdocs; then +if (! hasArg --configure-only) && (hasArg cppdocs || hasArg pydocs); then + # Sphinx consumes the Doxygen XML through Breathe, so pydocs also needs the + # docs_cuml prerequisite when it is invoked on its own. cmake --build "${LIBCUML_BUILD_DIR}" --target docs_cuml fi diff --git a/ci/build_docs.sh b/ci/build_docs.sh index 2404d96dc1..dadaaca8ed 100755 --- a/ci/build_docs.sh +++ b/ci/build_docs.sh @@ -1,5 +1,5 @@ #!/bin/bash -# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 set -euo pipefail @@ -33,18 +33,39 @@ rapids-print-env RAPIDS_DOCS_DIR="$(mktemp -d)" export RAPIDS_DOCS_DIR -rapids-logger "Build CPP docs" +rapids-logger "Generate C++ API XML for Breathe" pushd cpp doxygen Doxyfile.in -mkdir -p "${RAPIDS_DOCS_DIR}/libcuml/html" -mv html/* "${RAPIDS_DOCS_DIR}/libcuml/html" popd -rapids-logger "Build Python docs" +rapids-logger "Build the combined Python and C++ Sphinx documentation" pushd docs sphinx-build -b dirhtml ./source _html -W mkdir -p "${RAPIDS_DOCS_DIR}/cuml/html" mv _html/* "${RAPIDS_DOCS_DIR}/cuml/html" popd +# The publishing workflow still expects the historical libcuml project. Keep +# that entry point without duplicating API content: publish only a redirect to +# the version-matched C++ API inside the combined Sphinx site. This uses the +# already-initialized RAPIDS_VERSION_MAJOR_MINOR, so it is safe under `set -u`. +LIBCUML_REDIRECT_DIR="${RAPIDS_DOCS_DIR}/libcuml/html" +CUML_CPP_API_URL="https://docs.nvidia.com/cuml/${RAPIDS_VERSION_MAJOR_MINOR}/developer_guide/cpp/api/" +mkdir -p "${LIBCUML_REDIRECT_DIR}" +cat > "${LIBCUML_REDIRECT_DIR}/index.html" < + + + + cuML C++ API moved + + + + + +

The cuML C++ API reference moved to the cuML Developer Guide.

+ + +EOF + RAPIDS_VERSION_NUMBER="${RAPIDS_VERSION_MAJOR_MINOR}" rapids-upload-docs diff --git a/cpp/Doxyfile.in b/cpp/Doxyfile.in index abb1c34acc..a6b8cf6abf 100644 --- a/cpp/Doxyfile.in +++ b/cpp/Doxyfile.in @@ -1130,7 +1130,7 @@ IGNORE_PREFIX = # If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output # The default value is: YES. -GENERATE_HTML = YES +GENERATE_HTML = NO # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of diff --git a/docs/README.md b/docs/README.md index 935cf12ba1..c94aa6f090 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,14 +1,26 @@ -# Building Documentation -## Building locally: +# Building the documentation -#### [Build and install cuML](../BUILD.md) +## Build locally + +First [build and install cuML](../BUILD.md). Generate Doxygen XML before the +Sphinx documentation because Breathe reads that XML while rendering the C++ API +pages: -#### Generate the docs ```bash -bash build.sh cppdocs pydocs +./build.sh cppdocs pydocs ``` -#### Once the process finishes, documentation can be found in build/html +The `pydocs` target automatically generates the Doxygen XML prerequisite, so it +also works on its own. Naming both targets as above makes the prerequisite +explicit without generating it twice. Doxygen writes XML under `cpp/xml/`; it +does not produce a separately published HTML API site. The Sphinx Makefile +writes the complete documentation, including the C++ API reference, to +`docs/build/html/`: + ```bash -xdg-open build/html/api.html +xdg-open docs/build/html/index.html +xdg-open docs/build/html/developer_guide/cpp/api/index.html ``` + +CI uses the `dirhtml` builder instead, staging its version of the same Sphinx +site from `docs/_html/`. From fb1b1c2a32e4e0a3d168ff31b3a7a1f7e2b411a0 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Fri, 11 Sep 2026 09:22:40 +0000 Subject: [PATCH 4/4] Fix reflected attribute example imports --- docs/source/developer_guide/python/estimators.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/developer_guide/python/estimators.md b/docs/source/developer_guide/python/estimators.md index d05f5326f8..5cc2753c5f 100644 --- a/docs/source/developer_guide/python/estimators.md +++ b/docs/source/developer_guide/python/estimators.md @@ -494,6 +494,9 @@ This uses the same lazy conversion and caching path as external user reads. Externally, descriptor attributes lazily convert to the active output type: ```python +import cupy as cp +import numpy as np + my_est = SampleEstimator() # Call fit() with a numpy array as the input