diff --git a/README.md b/README.md index ccc91bfe225..c5785e6cb08 100644 --- a/README.md +++ b/README.md @@ -151,14 +151,14 @@ Install and update cuGraph using the conda command: ```bash -# CUDA 10.1 -conda install -c nvidia -c rapidsai -c numba -c conda-forge -c defaults cugraph cudatoolkit=10.1 - -# CUDA 10.2 -conda install -c nvidia -c rapidsai -c numba -c conda-forge -c defaults cugraph cudatoolkit=10.2 - # CUDA 11.0 conda install -c nvidia -c rapidsai -c numba -c conda-forge -c defaults cugraph cudatoolkit=11.0 + +# CUDA 11.1 +conda install -c nvidia -c rapidsai -c numba -c conda-forge -c defaults cugraph cudatoolkit=11.1 + +# CUDA 11.2 +conda install -c nvidia -c rapidsai -c numba -c conda-forge -c defaults cugraph cudatoolkit=11.2 ``` Note: This conda installation only applies to Linux and Python versions 3.7/3.8. diff --git a/SOURCEBUILD.md b/SOURCEBUILD.md index 0cbf6ccdaa3..0c825197cee 100644 --- a/SOURCEBUILD.md +++ b/SOURCEBUILD.md @@ -7,13 +7,13 @@ The cuGraph package include both a C/C++ CUDA portion and a python portion. Bot ## Prerequisites __Compiler__: -* `gcc` version 5.4+ -* `nvcc` version 10.0+ +* `gcc` version 9.3+ +* `nvcc` version 11.0+ * `cmake` version 3.18+ __CUDA:__ -* CUDA 10.1+ -* NVIDIA driver 396.44+ +* CUDA 11.0+ +* NVIDIA driver 450.80.02+ * Pascal architecture or better __Other__ @@ -47,16 +47,14 @@ __Create the conda development environment__ ```bash # create the conda environment (assuming in base `cugraph` directory) +# for CUDA 11.0 +conda env create --name cugraph_dev --file conda/environments/cugraph_dev_cuda11.0.yml +# for CUDA 11.1 +conda env create --name cugraph_dev --file conda/environments/cugraph_dev_cuda11.1.yml -# for CUDA 10.1 -conda env create --name cugraph_dev --file conda/environments/cugraph_dev_cuda10.1.yml - -# for CUDA 10.2 -conda env create --name cugraph_dev --file conda/environments/cugraph_dev_cuda10.2.yml - -# for CUDA 11 -conda env create --name cugraph_dev --file conda/environments/cugraph_dev_cuda11.0.yml +# for CUDA 11.2 +conda env create --name cugraph_dev --file conda/environments/cugraph_dev_cuda11.2.yml # activate the environment conda activate cugraph_dev @@ -70,14 +68,14 @@ conda deactivate ```bash -# for CUDA 10.1 -conda env update --name cugraph_dev --file conda/environments/cugraph_dev_cuda10.1.yml +# for CUDA 11.0 +conda env update --name cugraph_dev --file conda/environments/cugraph_dev_cuda11.0.yml -# for CUDA 10.2 -conda env update --name cugraph_dev --file conda/environments/cugraph_dev_cuda10.2.yml +# for CUDA 11.1 +conda env update --name cugraph_dev --file conda/environments/cugraph_dev_cuda11.1.yml -# for CUDA 11 -conda env update --name cugraph_dev --file conda/environments/cugraph_dev_cuda11.0.yml +# for CUDA 11.2 +conda env update --name cugraph_dev --file conda/environments/cugraph_dev_cuda11.2.yml conda activate cugraph_dev ``` @@ -232,8 +230,8 @@ Next the env_vars.sh file needs to be edited vi ./etc/conda/activate.d/env_vars.sh #!/bin/bash -export PATH=/usr/local/cuda-10.1/bin:$PATH # or cuda-10.2 if using CUDA 10.2 -export LD_LIBRARY_PATH=/usr/local/cuda-10.1/lib64:$LD_LIBRARY_PATH # or cuda-10.2 if using CUDA 10.2 +export PATH=/usr/local/cuda-11.0/bin:$PATH # or cuda-11.1 if using CUDA 11.1 and cuda-11.2 if using CUDA 11.2, respectively +export LD_LIBRARY_PATH=/usr/local/cuda-11.0/lib64:$LD_LIBRARY_PATH # or cuda-11.1 if using CUDA 11.1 and cuda-11.2 if using CUDA 11.2, respectively ``` ``` diff --git a/benchmarks/bench_algos.py b/benchmarks/bench_algos.py index f9f8bf9cf53..5284ffbd37b 100644 --- a/benchmarks/bench_algos.py +++ b/benchmarks/bench_algos.py @@ -51,9 +51,9 @@ def createGraph(csvFileName, graphType=None): # complexity lower, and assume tests have coverage to verify # correctness for those combinations. if "/directed/" in csvFileName: - graphType = cugraph.structure.graph.DiGraph + graphType = cugraph.structure.graph_classes.DiGraph else: - graphType = cugraph.structure.graph.Graph + graphType = cugraph.structure.graph_classes.Graph return cugraph.from_cudf_edgelist( utils.read_csv_file(csvFileName), @@ -122,7 +122,7 @@ def graphWithAdjListComputed(request): csvFileName = request.param[0] reinitRMM(request.param[1], request.param[2]) - G = createGraph(csvFileName, cugraph.structure.graph.Graph) + G = createGraph(csvFileName, cugraph.structure.graph_classes.Graph) G.view_adj_list() return G @@ -166,7 +166,7 @@ def bench_create_graph(gpubenchmark, edgelistCreated): gpubenchmark(cugraph.from_cudf_edgelist, edgelistCreated, source="0", destination="1", - create_using=cugraph.structure.graph.Graph, + create_using=cugraph.structure.graph_classes.Graph, renumber=False) @@ -183,7 +183,7 @@ def bench_create_digraph(gpubenchmark, edgelistCreated): gpubenchmark(cugraph.from_cudf_edgelist, edgelistCreated, source="0", destination="1", - create_using=cugraph.structure.graph.DiGraph, + create_using=cugraph.structure.graph_classes.DiGraph, renumber=False) diff --git a/build.sh b/build.sh index 54634e2ca6e..7c99b27f632 100755 --- a/build.sh +++ b/build.sh @@ -170,6 +170,6 @@ if buildAll || hasArg docs; then fi cd ${LIBCUGRAPH_BUILD_DIR} cmake --build "${LIBCUGRAPH_BUILD_DIR}" -j${PARALLEL_LEVEL} --target docs_cugraph ${VERBOSE_FLAG} - cd ${REPODIR}/docs + cd ${REPODIR}/docs/cugraph make html fi diff --git a/ci/cpu/prebuild.sh b/ci/cpu/prebuild.sh index ee471329b35..6665757181d 100644 --- a/ci/cpu/prebuild.sh +++ b/ci/cpu/prebuild.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright (c) 2018-2020, NVIDIA CORPORATION. +# Copyright (c) 2018-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -18,7 +18,7 @@ if [[ -z "$PROJECT_FLASH" || "$PROJECT_FLASH" == "0" ]]; then export BUILD_LIBCUGRAPH=1 fi -if [[ "$CUDA" == "10.1" ]]; then +if [[ "$CUDA" == "11.0" ]]; then export UPLOAD_CUGRAPH=1 else export UPLOAD_CUGRAPH=0 diff --git a/ci/docs/build.sh b/ci/docs/build.sh index 6ce223d8b2b..279faa6a61d 100644 --- a/ci/docs/build.sh +++ b/ci/docs/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2021, NVIDIA CORPORATION. ################################# # cuGraph Docs build script for CI # ################################# @@ -28,11 +28,6 @@ gpuci_logger "Activate conda env" . /opt/conda/etc/profile.d/conda.sh conda activate rapids -# TODO: Move installs to docs-build-env meta package -gpuci_conda_retry install -c anaconda markdown beautifulsoup4 jq -pip install sphinx-markdown-tables - - gpuci_logger "Check versions" python --version $CC --version @@ -47,10 +42,10 @@ conda list --show-channel-urls gpuci_logger "Build Doxygen docs" cd $PROJECT_WORKSPACE/cpp/build make docs_cugraph - + # Build Python docs gpuci_logger "Build Sphinx docs" -cd $PROJECT_WORKSPACE/docs +cd $PROJECT_WORKSPACE/docs/cugraph make html #Commit to Website @@ -60,10 +55,10 @@ for PROJECT in ${PROJECTS[@]}; do if [ ! -d "api/$PROJECT/$BRANCH_VERSION" ]; then mkdir -p api/$PROJECT/$BRANCH_VERSION fi - rm -rf $DOCS_WORKSPACE/api/$PROJECT/$BRANCH_VERSION/* + rm -rf $DOCS_WORKSPACE/api/$PROJECT/$BRANCH_VERSION/* done mv $PROJECT_WORKSPACE/cpp/doxygen/html/* $DOCS_WORKSPACE/api/libcugraph/$BRANCH_VERSION -mv $PROJECT_WORKSPACE/docs/build/html/* $DOCS_WORKSPACE/api/cugraph/$BRANCH_VERSION +mv $PROJECT_WORKSPACE/docs/cugraph/build/html/* $DOCS_WORKSPACE/api/cugraph/$BRANCH_VERSION diff --git a/conda/environments/cugraph_dev_cuda11.0.yml b/conda/environments/cugraph_dev_cuda11.0.yml index 84c07524a00..20d56b281d2 100644 --- a/conda/environments/cugraph_dev_cuda11.0.yml +++ b/conda/environments/cugraph_dev_cuda11.0.yml @@ -5,10 +5,10 @@ channels: - rapidsai-nightly - conda-forge dependencies: +- cudatoolkit=11.0 - cudf=0.20.* - libcudf=0.20.* - rmm=0.20.* -- cuxfilter=0.20.* - librmm=0.20.* - dask>=2.12.0 - distributed>=2.12.0 @@ -19,8 +19,6 @@ dependencies: - ucx-proc=*=gpu - scipy - networkx>=2.5.1 -- python-louvain -- cudatoolkit=11.0 - clang=8.0.1 - clang-tools=8.0.1 - cmake>=3.18 @@ -32,18 +30,16 @@ dependencies: - libfaiss=1.7.0 - faiss-proc=*=cuda - scikit-learn>=0.23.1 -- colorcet -- holoviews - sphinx - sphinx_rtd_theme - sphinxcontrib-websupport - sphinx-markdown-tables +- sphinx-copybutton - nbsphinx - numpydoc - ipython - recommonmark - pip -- libcypher-parser - rapids-pytest-benchmark - doxygen - pytest-cov diff --git a/conda/environments/cugraph_dev_cuda10.1.yml b/conda/environments/cugraph_dev_cuda11.1.yml similarity index 87% rename from conda/environments/cugraph_dev_cuda10.1.yml rename to conda/environments/cugraph_dev_cuda11.1.yml index 8d717c205c7..0eba2baccaa 100644 --- a/conda/environments/cugraph_dev_cuda10.1.yml +++ b/conda/environments/cugraph_dev_cuda11.1.yml @@ -5,10 +5,10 @@ channels: - rapidsai-nightly - conda-forge dependencies: +- cudatoolkit=11.1 - cudf=0.20.* - libcudf=0.20.* - rmm=0.20.* -- cuxfilter=0.20.* - librmm=0.20.* - dask>=2.12.0 - distributed>=2.12.0 @@ -19,8 +19,6 @@ dependencies: - ucx-proc=*=gpu - scipy - networkx>=2.5.1 -- python-louvain -- cudatoolkit=10.1 - clang=8.0.1 - clang-tools=8.0.1 - cmake>=3.18 @@ -32,18 +30,16 @@ dependencies: - libfaiss=1.7.0 - faiss-proc=*=cuda - scikit-learn>=0.23.1 -- colorcet -- holoviews - sphinx - sphinx_rtd_theme - sphinxcontrib-websupport - sphinx-markdown-tables +- sphinx-copybutton - nbsphinx - numpydoc - ipython - recommonmark - pip -- libcypher-parser - rapids-pytest-benchmark - doxygen - pytest-cov diff --git a/conda/environments/cugraph_dev_cuda10.2.yml b/conda/environments/cugraph_dev_cuda11.2.yml similarity index 87% rename from conda/environments/cugraph_dev_cuda10.2.yml rename to conda/environments/cugraph_dev_cuda11.2.yml index 771f6141a68..55f6ad75cec 100644 --- a/conda/environments/cugraph_dev_cuda10.2.yml +++ b/conda/environments/cugraph_dev_cuda11.2.yml @@ -5,10 +5,10 @@ channels: - rapidsai-nightly - conda-forge dependencies: +- cudatoolkit=11.2 - cudf=0.20.* - libcudf=0.20.* - rmm=0.20.* -- cuxfilter=0.20.* - librmm=0.20.* - dask>=2.12.0 - distributed>=2.12.0 @@ -19,8 +19,6 @@ dependencies: - ucx-proc=*=gpu - scipy - networkx>=2.5.1 -- python-louvain -- cudatoolkit=10.2 - clang=8.0.1 - clang-tools=8.0.1 - cmake>=3.18 @@ -32,18 +30,16 @@ dependencies: - libfaiss=1.7.0 - faiss-proc=*=cuda - scikit-learn>=0.23.1 -- colorcet -- holoviews - sphinx - sphinx_rtd_theme - sphinxcontrib-websupport - sphinx-markdown-tables +- sphinx-copybutton - nbsphinx - numpydoc - ipython - recommonmark - pip -- libcypher-parser - rapids-pytest-benchmark - doxygen - pytest-cov diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 9394f7b38d1..3f421da5e19 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -52,12 +52,12 @@ option(BUILD_STATIC_FAISS "Build the FAISS library for nearest neighbors search ################################################################################################### # - compiler options ------------------------------------------------------------------------------ -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_C_COMPILER $ENV{CC}) set(CMAKE_CXX_COMPILER $ENV{CXX}) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CUDA_STANDARD 14) +set(CMAKE_CUDA_STANDARD 17) set(CMAKE_CUDA_STANDARD_REQUIRED ON) if(CMAKE_COMPILER_IS_GNUCXX) @@ -225,8 +225,7 @@ message("Fetching Thrust") FetchContent_Declare( thrust GIT_REPOSITORY https://github.com/thrust/thrust.git - # August 28, 2020 - GIT_TAG 52a8bda46c5c2128414d1d47f546b486ff0be2f0 + GIT_TAG 1.12.0 ) FetchContent_GetProperties(thrust) @@ -242,7 +241,7 @@ message("Fetching cuco") FetchContent_Declare( cuco GIT_REPOSITORY https://github.com/NVIDIA/cuCollections.git - GIT_TAG 2196040f0562a0280292eebef5295d914f615e63 + GIT_TAG 7678a5ecaa192b8983b02a0191a140097171713e ) FetchContent_GetProperties(cuco) @@ -276,7 +275,7 @@ message("set LIBCUDACXX_INCLUDE_DIR to: ${LIBCUDACXX_INCLUDE_DIR}") FetchContent_Declare( cuhornet GIT_REPOSITORY https://github.com/rapidsai/cuhornet.git - GIT_TAG e58d0ecdbc270fc28867d66c965787a62a7a882c + GIT_TAG 6d2fc894cc56dd2ca8fc9d1523a18a6ec444b663 GIT_SHALLOW true SOURCE_SUBDIR hornet ) @@ -302,8 +301,7 @@ else(DEFINED ENV{RAFT_PATH}) FetchContent_Declare( raft GIT_REPOSITORY https://github.com/rapidsai/raft.git - GIT_TAG f0cd81fb49638eaddc9bf18998cc894f292bc293 - + GIT_TAG 66f82b4e79a3e268d0da3cc864ec7ce4ad065296 SOURCE_SUBDIR raft ) diff --git a/cpp/include/algorithms.hpp b/cpp/include/algorithms.hpp index 0b45b799357..7a7a0219d74 100644 --- a/cpp/include/algorithms.hpp +++ b/cpp/include/algorithms.hpp @@ -1280,5 +1280,6 @@ random_walks(raft::handle_t const &handle, typename graph_t::vertex_type const *ptr_d_start, index_t num_paths, index_t max_depth); + } // namespace experimental } // namespace cugraph diff --git a/cpp/include/patterns/copy_v_transform_reduce_key_aggregated_out_nbr.cuh b/cpp/include/patterns/copy_v_transform_reduce_key_aggregated_out_nbr.cuh index f904c35ef9e..f6eac67e4e7 100644 --- a/cpp/include/patterns/copy_v_transform_reduce_key_aggregated_out_nbr.cuh +++ b/cpp/include/patterns/copy_v_transform_reduce_key_aggregated_out_nbr.cuh @@ -256,9 +256,7 @@ void copy_v_transform_reduce_key_aggregated_out_nbr( kv_map_ptr.reset(); kv_map_ptr = std::make_unique>( - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max(static_cast(static_cast(map_keys.size()) / load_factor), static_cast(thrust::distance(map_key_first, map_key_last)) + 1), invalid_vertex_id::value, @@ -270,18 +268,14 @@ void copy_v_transform_reduce_key_aggregated_out_nbr( [] __device__(auto val) { return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (map_keys.size()) { kv_map_ptr->insert(pair_first, pair_first + map_keys.size()); } + kv_map_ptr->insert(pair_first, pair_first + map_keys.size()); } else { handle.get_stream_view().synchronize(); // cuco::static_map currently does not take stream kv_map_ptr.reset(); kv_map_ptr = std::make_unique>( - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max(static_cast( static_cast(thrust::distance(map_key_first, map_key_last)) / load_factor), static_cast(thrust::distance(map_key_first, map_key_last)) + 1), @@ -293,11 +287,7 @@ void copy_v_transform_reduce_key_aggregated_out_nbr( [] __device__(auto val) { return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (thrust::distance(map_key_first, map_key_last) > 0) { - kv_map_ptr->insert(pair_first, pair_first + thrust::distance(map_key_first, map_key_last)); - } + kv_map_ptr->insert(pair_first, pair_first + thrust::distance(map_key_first, map_key_last)); } // 2. aggregate each vertex out-going edges based on keys and transform-reduce. diff --git a/cpp/include/utilities/collect_comm.cuh b/cpp/include/utilities/collect_comm.cuh index 481717d7c38..f5a904ad875 100644 --- a/cpp/include/utilities/collect_comm.cuh +++ b/cpp/include/utilities/collect_comm.cuh @@ -64,9 +64,7 @@ collect_values_for_keys(raft::comms::comms_t const &comm, // 1. build a cuco::static_map object for the map k, v pairs. auto kv_map_ptr = std::make_unique>( - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max(static_cast( static_cast(thrust::distance(map_key_first, map_key_last)) / load_factor), static_cast(thrust::distance(map_key_first, map_key_last)) + 1), @@ -78,11 +76,7 @@ collect_values_for_keys(raft::comms::comms_t const &comm, [] __device__(auto val) { return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (thrust::distance(map_key_first, map_key_last) > 0) { - kv_map_ptr->insert(pair_first, pair_first + thrust::distance(map_key_first, map_key_last)); - } + kv_map_ptr->insert(pair_first, pair_first + thrust::distance(map_key_first, map_key_last)); } // 2. collect values for the unique keys in [collect_key_first, collect_key_last) @@ -113,12 +107,8 @@ collect_values_for_keys(raft::comms::comms_t const &comm, CUDA_TRY(cudaStreamSynchronize(stream)); // cuco::static_map currently does not take stream - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (rx_unique_keys.size() > 0) { - kv_map_ptr->find( - rx_unique_keys.begin(), rx_unique_keys.end(), values_for_rx_unique_keys.begin()); - } + kv_map_ptr->find( + rx_unique_keys.begin(), rx_unique_keys.end(), values_for_rx_unique_keys.begin()); rmm::device_uvector rx_values_for_unique_keys(0, stream); std::tie(rx_values_for_unique_keys, std::ignore) = @@ -135,9 +125,7 @@ collect_values_for_keys(raft::comms::comms_t const &comm, kv_map_ptr.reset(); kv_map_ptr = std::make_unique>( - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max(static_cast(static_cast(unique_keys.size()) / load_factor), unique_keys.size() + 1), invalid_vertex_id::value, @@ -150,21 +138,15 @@ collect_values_for_keys(raft::comms::comms_t const &comm, return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (unique_keys.size() > 0) { kv_map_ptr->insert(pair_first, pair_first + unique_keys.size()); } + kv_map_ptr->insert(pair_first, pair_first + unique_keys.size()); } // 4. find values for [collect_key_first, collect_key_last) auto value_buffer = allocate_dataframe_buffer( thrust::distance(collect_key_first, collect_key_last), stream); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (thrust::distance(collect_key_first, collect_key_last) > 0) { - kv_map_ptr->find( - collect_key_first, collect_key_last, get_dataframe_buffer_begin(value_buffer)); - } + kv_map_ptr->find( + collect_key_first, collect_key_last, get_dataframe_buffer_begin(value_buffer)); return value_buffer; } @@ -200,9 +182,7 @@ collect_values_for_unique_keys(raft::comms::comms_t const &comm, // 1. build a cuco::static_map object for the map k, v pairs. auto kv_map_ptr = std::make_unique>( - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max(static_cast( static_cast(thrust::distance(map_key_first, map_key_last)) / load_factor), static_cast(thrust::distance(map_key_first, map_key_last)) + 1), @@ -214,11 +194,7 @@ collect_values_for_unique_keys(raft::comms::comms_t const &comm, [] __device__(auto val) { return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (thrust::distance(map_key_first, map_key_last)) { - kv_map_ptr->insert(pair_first, pair_first + thrust::distance(map_key_first, map_key_last)); - } + kv_map_ptr->insert(pair_first, pair_first + thrust::distance(map_key_first, map_key_last)); } // 2. collect values for the unique keys in [collect_unique_key_first, collect_unique_key_last) @@ -245,12 +221,8 @@ collect_values_for_unique_keys(raft::comms::comms_t const &comm, CUDA_TRY(cudaStreamSynchronize(stream)); // cuco::static_map currently does not take stream - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (rx_unique_keys.size() > 0) { - kv_map_ptr->find( - rx_unique_keys.begin(), rx_unique_keys.end(), values_for_rx_unique_keys.begin()); - } + kv_map_ptr->find( + rx_unique_keys.begin(), rx_unique_keys.end(), values_for_rx_unique_keys.begin()); rmm::device_uvector rx_values_for_unique_keys(0, stream); std::tie(rx_values_for_unique_keys, std::ignore) = @@ -267,9 +239,7 @@ collect_values_for_unique_keys(raft::comms::comms_t const &comm, kv_map_ptr.reset(); kv_map_ptr = std::make_unique>( - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max(static_cast(static_cast(unique_keys.size()) / load_factor), unique_keys.size() + 1), invalid_vertex_id::value, @@ -282,22 +252,16 @@ collect_values_for_unique_keys(raft::comms::comms_t const &comm, return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (unique_keys.size() > 0) { kv_map_ptr->insert(pair_first, pair_first + unique_keys.size()); } + kv_map_ptr->insert(pair_first, pair_first + unique_keys.size()); } // 4. find values for [collect_unique_key_first, collect_unique_key_last) auto value_buffer = allocate_dataframe_buffer( thrust::distance(collect_unique_key_first, collect_unique_key_last), stream); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (thrust::distance(collect_unique_key_first, collect_unique_key_last)) { - kv_map_ptr->find(collect_unique_key_first, - collect_unique_key_last, - get_dataframe_buffer_begin(value_buffer)); - } + kv_map_ptr->find(collect_unique_key_first, + collect_unique_key_last, + get_dataframe_buffer_begin(value_buffer)); return value_buffer; } diff --git a/cpp/include/utilities/cython.hpp b/cpp/include/utilities/cython.hpp index d8c476760f0..0d6cb2f63d0 100644 --- a/cpp/include/utilities/cython.hpp +++ b/cpp/include/utilities/cython.hpp @@ -207,6 +207,26 @@ struct random_walk_ret_t { std::unique_ptr d_sizes_; }; +// aggregate for random_walks() COO return type +// to be exposed to cython: +// +struct random_walk_coo_t { + size_t num_edges_; // total number of COO triplets (for all paths) + size_t num_offsets_; // offsets of where each COO set starts for each path; + // NOTE: this can differ than num_paths_, + // because paths with 0 edges (one vertex) + // don't participate to the COO + + std::unique_ptr + d_src_; // coalesced set of COO source vertices; |d_src_| = num_edges_ + std::unique_ptr + d_dst_; // coalesced set of COO destination vertices; |d_dst_| = num_edges_ + std::unique_ptr + d_weights_; // coalesced set of COO edge weights; |d_weights_| = num_edges_ + std::unique_ptr + d_offsets_; // offsets where each COO subset for each path starts; |d_offsets_| = num_offsets_ +}; + // wrapper for renumber_edgelist() return // (unrenumbering maps, etc.) // @@ -448,9 +468,9 @@ void call_bfs(raft::handle_t const& handle, vertex_t* identifiers, vertex_t* distances, vertex_t* predecessors, - double* sp_counters, + vertex_t depth_limit, const vertex_t start_vertex, - bool directed); + bool direction_optimizing); // Wrapper for calling SSSP through a graph container template @@ -479,6 +499,12 @@ call_random_walks(raft::handle_t const& handle, edge_t num_paths, edge_t max_depth); +// convertor from random_walks return type to COO: +// +template +std::unique_ptr random_walks_to_coo(raft::handle_t const& handle, + random_walk_ret_t& rw_ret); + // wrapper for shuffling: // template diff --git a/cpp/include/utilities/path_retrieval.hpp b/cpp/include/utilities/path_retrieval.hpp index e626d6af1ab..fd0d36b67d6 100644 --- a/cpp/include/utilities/path_retrieval.hpp +++ b/cpp/include/utilities/path_retrieval.hpp @@ -42,4 +42,30 @@ void get_traversed_cost(raft::handle_t const &handle, weight_t *out, vertex_t stop_vertex, vertex_t num_vertices); + +namespace experimental { +/** + * @brief returns the COO format (src_vector, dst_vector) from the random walks (RW) + * paths. + * + * @tparam vertex_t Type of vertex indices. + * @tparam index_t Type used to store indexing and sizes. + * @param handle RAFT handle object to encapsulate resources (e.g. CUDA stream, communicator, and + * handles to various CUDA libraries) to run graph algorithms. + * @param coalesced_sz_v coalesced vertex vector size. + * @param num_paths number of paths. + * @param d_coalesced_v coalesced vertex buffer. + * @param d_sizes paths size buffer. + * @return tuple of (src_vertex_vector, dst_Vertex_vector, path_offsets), where + * path_offsets are the offsets where the COO set of each path starts. + */ +template +std:: + tuple, rmm::device_uvector, rmm::device_uvector> + convert_paths_to_coo(raft::handle_t const &handle, + index_t coalesced_sz_v, + index_t num_paths, + rmm::device_buffer &&d_coalesced_v, + rmm::device_buffer &&d_sizes); +} // namespace experimental } // namespace cugraph diff --git a/cpp/src/experimental/relabel.cu b/cpp/src/experimental/relabel.cu index 8d8fb0322a8..918feeb7a10 100644 --- a/cpp/src/experimental/relabel.cu +++ b/cpp/src/experimental/relabel.cu @@ -121,9 +121,7 @@ void relabel(raft::handle_t const& handle, handle.get_stream())); // cuco::static_map currently does not take stream cuco::static_map relabel_map{ - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max( static_cast(static_cast(rx_label_pair_old_labels.size()) / load_factor), rx_label_pair_old_labels.size() + 1), @@ -136,11 +134,7 @@ void relabel(raft::handle_t const& handle, [] __device__(auto val) { return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the - // grid size is 0; this leads to cudaErrorInvaildConfiguration. - if (rx_label_pair_old_labels.size() > 0) { - relabel_map.insert(pair_first, pair_first + rx_label_pair_old_labels.size()); - } + relabel_map.insert(pair_first, pair_first + rx_label_pair_old_labels.size()); rx_label_pair_old_labels.resize(0, handle.get_stream()); rx_label_pair_new_labels.resize(0, handle.get_stream()); @@ -162,15 +156,11 @@ void relabel(raft::handle_t const& handle, CUDA_TRY(cudaStreamSynchronize( handle.get_stream())); // cuco::static_map currently does not take stream - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the - // grid size is 0; this leads to cudaErrorInvaildConfiguration. - if (rx_unique_old_labels.size() > 0) { - relabel_map.find( - rx_unique_old_labels.begin(), - rx_unique_old_labels.end(), - rx_unique_old_labels.begin()); // now rx_unique_old_lables hold new labels for the - // corresponding old labels - } + relabel_map.find( + rx_unique_old_labels.begin(), + rx_unique_old_labels.end(), + rx_unique_old_labels + .begin()); // now rx_unique_old_lables hold new labels for the corresponding old labels std::tie(new_labels_for_unique_old_labels, std::ignore) = shuffle_values( handle.get_comms(), rx_unique_old_labels.begin(), rx_value_counts, handle.get_stream()); @@ -180,9 +170,7 @@ void relabel(raft::handle_t const& handle, handle.get_stream_view().synchronize(); // cuco::static_map currently does not take stream cuco::static_map relabel_map( - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max(static_cast(static_cast(unique_old_labels.size()) / load_factor), unique_old_labels.size() + 1), invalid_vertex_id::value, @@ -195,19 +183,11 @@ void relabel(raft::handle_t const& handle, return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (unique_old_labels.size() > 0) { - relabel_map.insert(pair_first, pair_first + unique_old_labels.size()); - } - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (num_labels > 0) { relabel_map.find(labels, labels + num_labels, labels); } + relabel_map.insert(pair_first, pair_first + unique_old_labels.size()); + relabel_map.find(labels, labels + num_labels, labels); } else { cuco::static_map relabel_map( - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max(static_cast(static_cast(num_label_pairs) / load_factor), static_cast(num_label_pairs) + 1), invalid_vertex_id::value, @@ -220,12 +200,8 @@ void relabel(raft::handle_t const& handle, return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (num_label_pairs > 0) { relabel_map.insert(pair_first, pair_first + num_label_pairs); } - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (num_labels > 0) { relabel_map.find(labels, labels + num_labels, labels); } + relabel_map.insert(pair_first, pair_first + num_label_pairs); + relabel_map.find(labels, labels + num_labels, labels); } if (do_expensive_check) { diff --git a/cpp/src/experimental/renumber_edgelist.cu b/cpp/src/experimental/renumber_edgelist.cu index 127bd507271..dbf0250b88a 100644 --- a/cpp/src/experimental/renumber_edgelist.cu +++ b/cpp/src/experimental/renumber_edgelist.cu @@ -551,9 +551,7 @@ renumber_edgelist(raft::handle_t const& handle, handle.get_stream())); // cuco::static_map currently does not take stream cuco::static_map renumber_map{ - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max(static_cast( static_cast(partition.get_matrix_partition_major_size(i)) / load_factor), static_cast(partition.get_matrix_partition_major_size(i)) + 1), @@ -567,18 +565,10 @@ renumber_edgelist(raft::handle_t const& handle, [] __device__(auto val) { return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (partition.get_matrix_partition_major_size(i) > 0) { - renumber_map.insert(pair_first, pair_first + partition.get_matrix_partition_major_size(i)); - } - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (edgelist_edge_counts[i]) { - renumber_map.find(edgelist_major_vertices[i], - edgelist_major_vertices[i] + edgelist_edge_counts[i], - edgelist_major_vertices[i]); - } + renumber_map.insert(pair_first, pair_first + partition.get_matrix_partition_major_size(i)); + renumber_map.find(edgelist_major_vertices[i], + edgelist_major_vertices[i] + edgelist_edge_counts[i], + edgelist_major_vertices[i]); } { @@ -601,9 +591,7 @@ renumber_edgelist(raft::handle_t const& handle, handle.get_stream())); // cuco::static_map currently does not take stream cuco::static_map renumber_map{ - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max( static_cast(static_cast(renumber_map_minor_labels.size()) / load_factor), renumber_map_minor_labels.size() + 1), @@ -616,19 +604,11 @@ renumber_edgelist(raft::handle_t const& handle, [] __device__(auto val) { return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (renumber_map_minor_labels.size()) { - renumber_map.insert(pair_first, pair_first + renumber_map_minor_labels.size()); - } + renumber_map.insert(pair_first, pair_first + renumber_map_minor_labels.size()); for (size_t i = 0; i < edgelist_major_vertices.size(); ++i) { - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the - // grid size is 0; this leads to cudaErrorInvaildConfiguration. - if (edgelist_edge_counts[i]) { - renumber_map.find(edgelist_minor_vertices[i], - edgelist_minor_vertices[i] + edgelist_edge_counts[i], - edgelist_minor_vertices[i]); - } + renumber_map.find(edgelist_minor_vertices[i], + edgelist_minor_vertices[i] + edgelist_edge_counts[i], + edgelist_minor_vertices[i]); } } @@ -682,9 +662,7 @@ std::enable_if_t> renumber_edgelist( // footprint and execution time cuco::static_map renumber_map{ - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max(static_cast(static_cast(renumber_map_labels.size()) / load_factor), renumber_map_labels.size() + 1), invalid_vertex_id::value, @@ -695,21 +673,11 @@ std::enable_if_t> renumber_edgelist( [] __device__(auto val) { return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (renumber_map_labels.size()) { - renumber_map.insert(pair_first, pair_first + renumber_map_labels.size()); - } - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (num_edgelist_edges > 0) { - renumber_map.find(edgelist_major_vertices, - edgelist_major_vertices + num_edgelist_edges, - edgelist_major_vertices); - renumber_map.find(edgelist_minor_vertices, - edgelist_minor_vertices + num_edgelist_edges, - edgelist_minor_vertices); - } + renumber_map.insert(pair_first, pair_first + renumber_map_labels.size()); + renumber_map.find( + edgelist_major_vertices, edgelist_major_vertices + num_edgelist_edges, edgelist_major_vertices); + renumber_map.find( + edgelist_minor_vertices, edgelist_minor_vertices + num_edgelist_edges, edgelist_minor_vertices); return renumber_map_labels; #else diff --git a/cpp/src/experimental/renumber_utils.cu b/cpp/src/experimental/renumber_utils.cu index 8f59683d9d6..eef6ca88b3c 100644 --- a/cpp/src/experimental/renumber_utils.cu +++ b/cpp/src/experimental/renumber_utils.cu @@ -108,9 +108,7 @@ void renumber_ext_vertices(raft::handle_t const& handle, renumber_map_ptr.reset(); renumber_map_ptr = std::make_unique>( - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max( static_cast(static_cast(sorted_unique_ext_vertices.size()) / load_factor), sorted_unique_ext_vertices.size() + 1), @@ -123,20 +121,14 @@ void renumber_ext_vertices(raft::handle_t const& handle, [] __device__(auto val) { return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (sorted_unique_ext_vertices.size()) { - renumber_map_ptr->insert(kv_pair_first, kv_pair_first + sorted_unique_ext_vertices.size()); - } + renumber_map_ptr->insert(kv_pair_first, kv_pair_first + sorted_unique_ext_vertices.size()); } else { handle.get_stream_view().synchronize(); // cuco::static_map currently does not take stream renumber_map_ptr.reset(); renumber_map_ptr = std::make_unique>( - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max(static_cast( static_cast(local_int_vertex_last - local_int_vertex_first) / load_factor), static_cast(local_int_vertex_last - local_int_vertex_first) + 1), @@ -149,21 +141,13 @@ void renumber_ext_vertices(raft::handle_t const& handle, [] __device__(auto val) { return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if ((local_int_vertex_last - local_int_vertex_first) > 0) { - renumber_map_ptr->insert(pair_first, - pair_first + (local_int_vertex_last - local_int_vertex_first)); - } + renumber_map_ptr->insert(pair_first, + pair_first + (local_int_vertex_last - local_int_vertex_first)); } if (do_expensive_check) { rmm::device_uvector contains(num_vertices, handle.get_stream()); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (num_vertices > 0) { - renumber_map_ptr->contains(vertices, vertices + num_vertices, contains.begin()); - } + renumber_map_ptr->contains(vertices, vertices + num_vertices, contains.begin()); auto vc_pair_first = thrust::make_zip_iterator(thrust::make_tuple(vertices, contains.begin())); CUGRAPH_EXPECTS(thrust::count_if(rmm::exec_policy(handle.get_stream())->on(handle.get_stream()), vc_pair_first, @@ -179,22 +163,7 @@ void renumber_ext_vertices(raft::handle_t const& handle, "(aggregate) renumber_map_labels."); } - // FIXME: a temporary workaround for https://github.com/NVIDIA/cuCollections/issues/74 -#if 1 - thrust::transform(rmm::exec_policy(handle.get_stream())->on(handle.get_stream()), - vertices, - vertices + num_vertices, - vertices, - [view = renumber_map_ptr->get_device_view()] __device__(auto v) { - return v != invalid_vertex_id::value - ? view.find(v)->second.load(cuda::std::memory_order_relaxed) - : invalid_vertex_id::value; - }); -#else - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (num_vertices > 0) { renumber_map_ptr->find(vertices, vertices + num_vertices, vertices); } -#endif + renumber_map_ptr->find(vertices, vertices + num_vertices, vertices); #endif } @@ -338,9 +307,7 @@ void unrenumber_int_vertices(raft::handle_t const& handle, handle.get_stream_view().synchronize(); // cuco::static_map currently does not take stream cuco::static_map unrenumber_map( - // FIXME: std::max(..., ...) as a temporary workaround for - // https://github.com/NVIDIA/cuCollections/issues/72 and - // https://github.com/NVIDIA/cuCollections/issues/73 + // cuco::static_map requires at least one empty slot std::max( static_cast(static_cast(sorted_unique_int_vertices.size()) / load_factor), sorted_unique_int_vertices.size() + 1), @@ -354,27 +321,8 @@ void unrenumber_int_vertices(raft::handle_t const& handle, [] __device__(auto val) { return thrust::make_pair(thrust::get<0>(val), thrust::get<1>(val)); }); - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (sorted_unique_int_vertices.size()) { - unrenumber_map.insert(pair_first, pair_first + sorted_unique_int_vertices.size()); - } - // FIXME: a temporary workaround for https://github.com/NVIDIA/cuCollections/issues/74 -#if 1 - thrust::transform(rmm::exec_policy(handle.get_stream())->on(handle.get_stream()), - vertices, - vertices + num_vertices, - vertices, - [view = unrenumber_map.get_device_view()] __device__(auto v) { - return v != invalid_vertex_id::value - ? view.find(v)->second.load(cuda::std::memory_order_relaxed) - : invalid_vertex_id::value; - }); -#else - // FIXME: a temporary workaround. cuco::static_map currently launches a kernel even if the grid - // size is 0; this leads to cudaErrorInvaildConfiguration. - if (num_vertices > 0) { unrenumber_map.find(vertices, vertices + num_vertices, vertices); } -#endif + unrenumber_map.insert(pair_first, pair_first + sorted_unique_int_vertices.size()); + unrenumber_map.find(vertices, vertices + num_vertices, vertices); } else { unrenumber_local_int_vertices(handle, vertices, diff --git a/cpp/src/sampling/random_walks.cu b/cpp/src/sampling/random_walks.cu index 88d5d9ed5c8..d1d0382d46f 100644 --- a/cpp/src/sampling/random_walks.cu +++ b/cpp/src/sampling/random_walks.cu @@ -17,7 +17,7 @@ // Andrei Schaffer, aschaffer@nvidia.com // #include -#include +#include "random_walks.cuh" namespace cugraph { namespace experimental { @@ -73,6 +73,30 @@ template std:: int64_t const* ptr_d_start, int64_t num_paths, int64_t max_depth); + +template std:: + tuple, rmm::device_uvector, rmm::device_uvector> + convert_paths_to_coo(raft::handle_t const& handle, + int32_t coalesced_sz_v, + int32_t num_paths, + rmm::device_buffer&& d_coalesced_v, + rmm::device_buffer&& d_sizes); + +template std:: + tuple, rmm::device_uvector, rmm::device_uvector> + convert_paths_to_coo(raft::handle_t const& handle, + int64_t coalesced_sz_v, + int64_t num_paths, + rmm::device_buffer&& d_coalesced_v, + rmm::device_buffer&& d_sizes); + +template std:: + tuple, rmm::device_uvector, rmm::device_uvector> + convert_paths_to_coo(raft::handle_t const& handle, + int64_t coalesced_sz_v, + int64_t num_paths, + rmm::device_buffer&& d_coalesced_v, + rmm::device_buffer&& d_sizes); //} } // namespace experimental } // namespace cugraph diff --git a/cpp/src/experimental/random_walks.cuh b/cpp/src/sampling/random_walks.cuh similarity index 82% rename from cpp/src/experimental/random_walks.cuh rename to cpp/src/sampling/random_walks.cuh index aea8f3d8420..82665003769 100644 --- a/cpp/src/experimental/random_walks.cuh +++ b/cpp/src/sampling/random_walks.cuh @@ -40,6 +40,7 @@ #include #include #include +#include #include #include @@ -103,6 +104,12 @@ struct device_const_vector_view { index_t size_; }; +template +value_t const* raw_const_ptr(device_const_vector_view& dv) +{ + return dv.begin(); +} + // raft random generator: // (using upper-bound cached "map" // giving out_deg(v) for each v in [0, |V|); @@ -840,6 +847,156 @@ random_walks_impl(raft::handle_t const& handle, CUGRAPH_FAIL("Not implemented yet."); } +// provides conversion to (coalesced) path to COO format: +// (which in turn provides an API consistent with egonet) +// +template +struct coo_convertor_t { + coo_convertor_t(raft::handle_t const& handle, index_t num_paths) + : handle_(handle), num_paths_(num_paths) + { + } + + std::tuple, device_vec_t, device_vec_t> operator()( + device_const_vector_view& d_coalesced_v, + device_const_vector_view& d_sizes) const + { + CUGRAPH_EXPECTS(static_cast(d_sizes.size()) == num_paths_, "Invalid size vector."); + + auto tupl_fill = fill_stencil(d_sizes); + auto&& d_stencil = std::move(std::get<0>(tupl_fill)); + auto total_sz_v = std::get<1>(tupl_fill); + auto&& d_sz_incl_scan = std::move(std::get<2>(tupl_fill)); + + CUGRAPH_EXPECTS(static_cast(d_coalesced_v.size()) == total_sz_v, + "Inconsistent vertex coalesced size data."); + + auto src_dst_tpl = gather_pairs(d_coalesced_v, d_stencil, total_sz_v); + + auto&& d_src = std::move(std::get<0>(src_dst_tpl)); + auto&& d_dst = std::move(std::get<1>(src_dst_tpl)); + + device_vec_t d_sz_w_scan(num_paths_, handle_.get_stream()); + + // copy vertex path sizes that are > 1: + // (because vertex_path_sz translates + // into edge_path_sz = vertex_path_sz - 1, + // and edge_paths_sz == 0 don't contribute + // anything): + // + auto new_end_it = + thrust::copy_if(rmm::exec_policy(handle_.get_stream())->on(handle_.get_stream()), + d_sizes.begin(), + d_sizes.end(), + d_sz_w_scan.begin(), + [] __device__(auto sz_value) { return sz_value > 1; }); + + // resize to new_end: + // + d_sz_w_scan.resize(thrust::distance(d_sz_w_scan.begin(), new_end_it), handle_.get_stream()); + + // get paths' edge number exclusive scan + // by transforming paths' vertex numbers that + // are > 1, via tranaformation: + // edge_path_sz = (vertex_path_sz-1): + // + thrust::transform_exclusive_scan( + rmm::exec_policy(handle_.get_stream())->on(handle_.get_stream()), + d_sz_w_scan.begin(), + d_sz_w_scan.end(), + d_sz_w_scan.begin(), + [] __device__(auto sz) { return sz - 1; }, + index_t{0}, + thrust::plus{}); + + return std::make_tuple(std::move(d_src), std::move(d_dst), std::move(d_sz_w_scan)); + } + + std::tuple, index_t, device_vec_t> fill_stencil( + device_const_vector_view& d_sizes) const + { + device_vec_t d_scan(num_paths_, handle_.get_stream()); + thrust::inclusive_scan(rmm::exec_policy(handle_.get_stream())->on(handle_.get_stream()), + d_sizes.begin(), + d_sizes.end(), + d_scan.begin()); + + index_t total_sz{0}; + CUDA_TRY(cudaMemcpy( + &total_sz, raw_ptr(d_scan) + num_paths_ - 1, sizeof(index_t), cudaMemcpyDeviceToHost)); + + device_vec_t d_stencil(total_sz, handle_.get_stream()); + + // initialize stencil to all 1's: + // + thrust::copy_n(rmm::exec_policy(handle_.get_stream())->on(handle_.get_stream()), + thrust::make_constant_iterator(1), + d_stencil.size(), + d_stencil.begin()); + + // set to 0 entries positioned at inclusive_scan(sizes[]), + // because those are path "breakpoints", where a path end + // and the next one starts, hence there cannot be an edge + // between a path ending vertex and next path starting vertex; + // + thrust::scatter(rmm::exec_policy(handle_.get_stream())->on(handle_.get_stream()), + thrust::make_constant_iterator(0), + thrust::make_constant_iterator(0) + num_paths_, + d_scan.begin(), + d_stencil.begin()); + + return std::make_tuple(std::move(d_stencil), total_sz, std::move(d_scan)); + } + + std::tuple, device_vec_t> gather_pairs( + device_const_vector_view& d_coalesced_v, + device_vec_t const& d_stencil, + index_t total_sz_v) const + { + auto total_sz_w = total_sz_v - num_paths_; + device_vec_t valid_src_indx(total_sz_w, handle_.get_stream()); + + // generate valid vertex src indices, + // which is any index in {0,...,total_sz_v - 2} + // provided the next index position; i.e., (index+1), + // in stencil is not 0; (if it is, there's no "next" + // or dst index, because the path has ended); + // + thrust::copy_if(rmm::exec_policy(handle_.get_stream())->on(handle_.get_stream()), + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(total_sz_v - 1), + valid_src_indx.begin(), + [ptr_d_stencil = raw_const_ptr(d_stencil)] __device__(auto indx) { + auto dst_indx = indx + 1; + return ptr_d_stencil[dst_indx] == 1; + }); + + device_vec_t d_src_v(total_sz_w, handle_.get_stream()); + device_vec_t d_dst_v(total_sz_w, handle_.get_stream()); + + // construct pair of src[], dst[] by gathering + // from d_coalesced_v all pairs + // at entries (valid_src_indx, valid_src_indx+1), + // where the set of valid_src_indx was + // generated at the previous step; + // + thrust::transform( + rmm::exec_policy(handle_.get_stream())->on(handle_.get_stream()), + valid_src_indx.begin(), + valid_src_indx.end(), + thrust::make_zip_iterator(thrust::make_tuple(d_src_v.begin(), d_dst_v.begin())), // start_zip + [ptr_d_vertex = raw_const_ptr(d_coalesced_v)] __device__(auto indx) { + return thrust::make_tuple(ptr_d_vertex[indx], ptr_d_vertex[indx + 1]); + }); + + return std::make_tuple(std::move(d_src_v), std::move(d_dst_v)); + } + + private: + raft::handle_t const& handle_; + index_t num_paths_; +}; + } // namespace detail /** @@ -883,5 +1040,41 @@ random_walks(raft::handle_t const& handle, std::move(std::get<1>(quad_tuple)), std::move(std::get<2>(quad_tuple))); } + +/** + * @brief returns the COO format (src_vector, dst_vector) from the random walks (RW) + * paths. + * + * @tparam vertex_t Type of vertex indices. + * @tparam index_t Type used to store indexing and sizes. + * @param handle RAFT handle object to encapsulate resources (e.g. CUDA stream, communicator, and + * handles to various CUDA libraries) to run graph algorithms. + * @param coalesced_sz_v coalesced vertex vector size. + * @param num_paths number of paths. + * @param d_coalesced_v coalesced vertex buffer. + * @param d_sizes paths size buffer. + * @return tuple of (src_vertex_vector, dst_Vertex_vector, path_offsets), where + * path_offsets are the offsets where the COO set of each path starts. + */ +template +std:: + tuple, rmm::device_uvector, rmm::device_uvector> + convert_paths_to_coo(raft::handle_t const& handle, + index_t coalesced_sz_v, + index_t num_paths, + rmm::device_buffer&& d_coalesced_v, + rmm::device_buffer&& d_sizes) +{ + detail::coo_convertor_t to_coo(handle, num_paths); + + detail::device_const_vector_view d_v_view( + static_cast(d_coalesced_v.data()), coalesced_sz_v); + + detail::device_const_vector_view d_sz_view(static_cast(d_sizes.data()), + num_paths); + + return to_coo(d_v_view, d_sz_view); +} + } // namespace experimental } // namespace cugraph diff --git a/cpp/src/traversal/mg/common_utils.cuh b/cpp/src/traversal/mg/common_utils.cuh index 2cda827b471..d922636e740 100644 --- a/cpp/src/traversal/mg/common_utils.cuh +++ b/cpp/src/traversal/mg/common_utils.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ #include #include +#include #include #include "../traversal_common.cuh" diff --git a/cpp/src/traversal/mg/vertex_binning.cuh b/cpp/src/traversal/mg/vertex_binning.cuh index 3d8c963c466..b4ed881a06e 100644 --- a/cpp/src/traversal/mg/vertex_binning.cuh +++ b/cpp/src/traversal/mg/vertex_binning.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ #include "common_utils.cuh" #include "vertex_binning_kernels.cuh" +#include + namespace cugraph { namespace mg { diff --git a/cpp/src/utilities/cython.cu b/cpp/src/utilities/cython.cu index 4a2b98ea815..b4dcd84a7e1 100644 --- a/cpp/src/utilities/cython.cu +++ b/cpp/src/utilities/cython.cu @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -689,31 +690,11 @@ void call_bfs(raft::handle_t const& handle, vertex_t* identifiers, vertex_t* distances, vertex_t* predecessors, - double* sp_counters, + vertex_t depth_limit, const vertex_t start_vertex, - bool directed) + bool direction_optimizing) { - if (graph_container.graph_type == graphTypeEnum::GraphCSRViewFloat) { - graph_container.graph_ptr_union.GraphCSRViewFloatPtr->get_vertex_identifiers( - reinterpret_cast(identifiers)); - bfs(handle, - *(graph_container.graph_ptr_union.GraphCSRViewFloatPtr), - reinterpret_cast(distances), - reinterpret_cast(predecessors), - sp_counters, - static_cast(start_vertex), - directed); - } else if (graph_container.graph_type == graphTypeEnum::GraphCSRViewDouble) { - graph_container.graph_ptr_union.GraphCSRViewDoublePtr->get_vertex_identifiers( - reinterpret_cast(identifiers)); - bfs(handle, - *(graph_container.graph_ptr_union.GraphCSRViewDoublePtr), - reinterpret_cast(distances), - reinterpret_cast(predecessors), - sp_counters, - static_cast(start_vertex), - directed); - } else if (graph_container.graph_type == graphTypeEnum::graph_t) { + if (graph_container.is_multi_gpu) { if (graph_container.edgeType == numberTypeEnum::int32Type) { auto graph = detail::create_graph(handle, graph_container); @@ -721,7 +702,9 @@ void call_bfs(raft::handle_t const& handle, graph->view(), reinterpret_cast(distances), reinterpret_cast(predecessors), - static_cast(start_vertex)); + static_cast(start_vertex), + direction_optimizing, + static_cast(depth_limit)); } else if (graph_container.edgeType == numberTypeEnum::int64Type) { auto graph = detail::create_graph(handle, graph_container); @@ -729,9 +712,31 @@ void call_bfs(raft::handle_t const& handle, graph->view(), reinterpret_cast(distances), reinterpret_cast(predecessors), - static_cast(start_vertex)); - } else { - CUGRAPH_FAIL("vertexType/edgeType combination unsupported"); + static_cast(start_vertex), + direction_optimizing, + static_cast(depth_limit)); + } + } else { + if (graph_container.edgeType == numberTypeEnum::int32Type) { + auto graph = + detail::create_graph(handle, graph_container); + cugraph::experimental::bfs(handle, + graph->view(), + reinterpret_cast(distances), + reinterpret_cast(predecessors), + static_cast(start_vertex), + direction_optimizing, + static_cast(depth_limit)); + } else if (graph_container.edgeType == numberTypeEnum::int64Type) { + auto graph = + detail::create_graph(handle, graph_container); + cugraph::experimental::bfs(handle, + graph->view(), + reinterpret_cast(distances), + reinterpret_cast(predecessors), + static_cast(start_vertex), + direction_optimizing, + static_cast(depth_limit)); } } } @@ -840,6 +845,27 @@ call_random_walks(raft::handle_t const& handle, } } +template +std::unique_ptr random_walks_to_coo(raft::handle_t const& handle, + random_walk_ret_t& rw_tri) +{ + auto triplet = cugraph::experimental::convert_paths_to_coo( + handle, + static_cast(rw_tri.coalesced_sz_v_), + static_cast(rw_tri.num_paths_), + std::move(*rw_tri.d_coalesced_v_), + std::move(*rw_tri.d_sizes_)); + + random_walk_coo_t rw_coo{std::get<0>(triplet).size(), + std::get<2>(triplet).size(), + std::make_unique(std::get<0>(triplet).release()), + std::make_unique(std::get<1>(triplet).release()), + std::move(rw_tri.d_coalesced_w_), // pass-through + std::make_unique(std::get<2>(triplet).release())}; + + return std::make_unique(std::move(rw_coo)); +} + // Wrapper for calling SSSP through a graph container template void call_sssp(raft::handle_t const& handle, @@ -1149,36 +1175,37 @@ template void call_bfs(raft::handle_t const& handle, int32_t* identifiers, int32_t* distances, int32_t* predecessors, - double* sp_counters, + int32_t depth_limit, const int32_t start_vertex, - bool directed); + bool direction_optimizing); template void call_bfs(raft::handle_t const& handle, graph_container_t const& graph_container, int32_t* identifiers, int32_t* distances, int32_t* predecessors, - double* sp_counters, + int32_t depth_limit, const int32_t start_vertex, - bool directed); + bool direction_optimizing); template void call_bfs(raft::handle_t const& handle, graph_container_t const& graph_container, int64_t* identifiers, int64_t* distances, int64_t* predecessors, - double* sp_counters, + int64_t depth_limit, const int64_t start_vertex, - bool directed); + bool direction_optimizing); template void call_bfs(raft::handle_t const& handle, graph_container_t const& graph_container, int64_t* identifiers, int64_t* distances, int64_t* predecessors, - double* sp_counters, + int64_t depth_limit, const int64_t start_vertex, - bool directed); + bool direction_optimizing); + template std::unique_ptr call_egonet( raft::handle_t const& handle, graph_container_t const& graph_container, @@ -1228,6 +1255,15 @@ template std::unique_ptr call_random_walks( int64_t num_paths, int64_t max_depth); +template std::unique_ptr random_walks_to_coo( + raft::handle_t const& handle, random_walk_ret_t& rw_tri); + +template std::unique_ptr random_walks_to_coo( + raft::handle_t const& handle, random_walk_ret_t& rw_tri); + +template std::unique_ptr random_walks_to_coo( + raft::handle_t const& handle, random_walk_ret_t& rw_tri); + template void call_sssp(raft::handle_t const& handle, graph_container_t const& graph_container, int32_t* identifiers, diff --git a/cpp/src/utilities/high_res_timer.hpp b/cpp/src/utilities/high_res_timer.hpp index a731c5edc9d..807496c8f86 100644 --- a/cpp/src/utilities/high_res_timer.hpp +++ b/cpp/src/utilities/high_res_timer.hpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include //#define TIMING @@ -52,6 +54,19 @@ class HighResTimer { it->second.second += stop_time.tv_sec * 1000000000 + stop_time.tv_nsec; } + double get_average_runtime(std::string const &label) + { + auto it = timers.find(label); + if (it != timers.end()) { + return (static_cast(it->second.second) / (1000000.0 * it->second.first)); + } else { + std::stringstream ss; + ss << "ERROR: timing label: " << label << "not found."; + + throw std::runtime_error(ss.str()); + } + } + // // Add display functions... specific label or entire structure // diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 89975f673ae..80484fdfad6 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -25,6 +25,7 @@ add_library(cugraphtestutil STATIC "${CMAKE_CURRENT_SOURCE_DIR}/utilities/generate_graph_from_edgelist.cu" "${CMAKE_CURRENT_SOURCE_DIR}/utilities/thrust_wrapper.cu" "${CMAKE_CURRENT_SOURCE_DIR}/utilities/misc_utilities.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/components/wcc_graphs.cu" "${CMAKE_CURRENT_SOURCE_DIR}/../../thirdparty/mmio/mmio.c") set_property(TARGET cugraphtestutil PROPERTY POSITION_INDEPENDENT_CODE ON) @@ -68,8 +69,8 @@ function(ConfigureTest CMAKE_TEST_NAME CMAKE_TEST_SRC) PRIVATE "${CUB_INCLUDE_DIR}" "${THRUST_INCLUDE_DIR}" - "${CUCO_INCLUDE_DIR}" - "${LIBCUDACXX_INCLUDE_DIR}" + "${CUCO_INCLUDE_DIR}" + "${LIBCUDACXX_INCLUDE_DIR}" "${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}" "${RMM_INCLUDE}" "${NCCL_INCLUDE_DIRS}" @@ -165,6 +166,117 @@ function(ConfigureTest CMAKE_TEST_NAME CMAKE_TEST_SRC) add_test(NAME ${CMAKE_TEST_NAME} COMMAND ${CMAKE_TEST_NAME}) endfunction() +function(ConfigureTestMG CMAKE_TEST_NAME CMAKE_TEST_SRC) + add_executable(${CMAKE_TEST_NAME} + ${CMAKE_TEST_SRC}) + + target_include_directories(${CMAKE_TEST_NAME} + PRIVATE + "${CUB_INCLUDE_DIR}" + "${THRUST_INCLUDE_DIR}" + "${CUCO_INCLUDE_DIR}" + "${LIBCUDACXX_INCLUDE_DIR}" + "${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}" + "${RMM_INCLUDE}" + "${NCCL_INCLUDE_DIRS}" + "${CMAKE_CURRENT_SOURCE_DIR}/../../thirdparty/mmio" + "${CMAKE_CURRENT_SOURCE_DIR}/../include" + "${CMAKE_CURRENT_SOURCE_DIR}/../src" + "${CMAKE_CURRENT_SOURCE_DIR}" + "${RAFT_DIR}/cpp/include" + ) + + target_link_directories(${CMAKE_TEST_NAME} + PRIVATE + # CMAKE_CUDA_IMPLICIT_LINK_DIRECTORIES is an undocumented/unsupported + # variable containing the link directories for nvcc. + "${CMAKE_CUDA_IMPLICIT_LINK_DIRECTORIES}") + + target_link_libraries(${CMAKE_TEST_NAME} + PRIVATE + cugraphtestutil + cugraph + GTest::GTest + GTest::Main + ${NCCL_LIBRARIES} + cudart + cuda + cublas + cusparse + cusolver + curand) + + if(OpenMP_CXX_FOUND) + target_link_libraries(${CMAKE_TEST_NAME} PRIVATE +################################################################################################### +### Use ${OpenMP_CXX_LIB_NAMES} instead of OpenMP::OpenMP_CXX to avoid the following warnings. +### +### Cannot generate a safe runtime search path for target TARGET_NAME +### because files in some directories may conflict with libraries in implicit +### directories: +### ... +### +### libgomp.so is included in the conda base environment and copied to every new conda +### environment. If a full file path is provided (e.g ${CUDF_LIBRARY}), cmake +### extracts the directory path and adds the directory path to BUILD_RPATH (if BUILD_RPATH is not +### disabled). +### +### cmake maintains a system specific implicit directories (e.g. /lib, /lib/x86_64-linux-gnu, +### /lib32, /lib32/x86_64-linux-gnu, /lib64, /lib64/x86_64-linux-gnu, /usr/lib, +### /usr/lib/gcc/x86_64-linux-gnu/7, /usr/lib/x86_64-linux-gnu, /usr/lib32, +### /usr/lib32/x86_64-linux-gnu, /usr/lib64, /usr/lib64/x86_64-linux-gnu, +### /usr/local/cuda-10.0/lib64", /usr/local/cuda-10.0/lib64/stubs). +### +### If a full path to libgomp.so is provided (which is the case with OpenMP::OpenMP_CXX), cmake +### checks whether there is any other libgomp.so with the different full path (after resolving +### soft links) in the search paths (implicit directoires + BUILD_RAPTH). There is one in the +### path included in BUILD_RPATH when ${CUDF_LIBRARY} is added; this one can +### potentially hide the one in the provided full path and cmake generates a warning (and RPATH +### is searched before the directories in /etc/ld.so/conf; ld.so.conf does not coincide but +### overlaps with implicit directories). +### +### If we provide just the library names (gomp;pthread), cmake does not generate warnings (we +### did not specify which libgomp.so should be loaded in runtime), and the one first found in +### the search order is loaded (we can change the loaded library by setting LD_LIBRARY_PATH or +### manually editing BUILD_RPATH). +### +### Manually editing BUILD_RPATH: +### set(TARGET_BUILD_RPATH "") +### foreach(TMP_VAR_FULLPATH IN LISTS OpenMP_CXX_LIBRARIES) +### get_filename_component(TMP_VAR_DIR ${TMP_VAR_FULLPATH} DIRECTORY) +### string(APPEND TARGET_BUILD_RPATH "${TMP_VAR_DIR};") +### get_filename_component(TMP_VAR_REALPATH ${TMP_VAR_FULLPATH} REALPATH) +### get_filename_component(TMP_VAR_DIR ${TMP_VAR_REALPATH} DIRECTORY) +### # cmake automatically removes duplicates, so skip checking. +### string(APPEND TARGET_BUILD_RPATH "${TMP_VAR_DIR};") +### endforeach() +### string(APPEND TARGET_BUILD_RPATH "${CONDA_PREFIX}/lib") +### message(STATUS "TARGET_BUILD_RPATH=${TARGET_BUILD_RPATH}") +### set_target_properties(target PROPERTIES +### BUILD_RPATH "${TARGET_BUILD_RPATH}") + ${OpenMP_CXX_LIB_NAMES}) + endif(OpenMP_CXX_FOUND) + + # CUDA_ARCHITECTURES=OFF implies cmake will not pass arch flags to the + # compiler. CUDA_ARCHITECTURES must be set to a non-empty value to prevent + # cmake warnings about policy CMP0104. With this setting, arch flags must be + # manually set! ("evaluate_gpu_archs(GPU_ARCHS)" is the current mechanism + # used in cpp/CMakeLists.txt for setting arch options). + # Run "cmake --help-policy CMP0104" for policy details. + # NOTE: the CUDA_ARCHITECTURES=OFF setting may be removed after migrating to + # the findcudatoolkit features in cmake 3.17+ + set_target_properties(${CMAKE_TEST_NAME} PROPERTIES + CUDA_ARCHITECTURES OFF) + + add_test(NAME ${CMAKE_TEST_NAME} + COMMAND ${MPIEXEC_EXECUTABLE} + ${MPIEXEC_NUMPROC_FLAG} + ${GPU_COUNT} + ${MPIEXEC_PREFLAGS} + ${CMAKE_TEST_NAME} + ${MPIEXEC_POSTFLAGS}) +endfunction() + ################################################################################################### # - set rapids dataset path ---------------------------------------------------------------------- @@ -303,6 +415,14 @@ set(SCC_TEST_SRC ConfigureTest(SCC_TEST "${SCC_TEST_SRC}") +################################################################################################### +# - WEAKLY CONNECTED COMPONENTS tests ---------------------------------------------------------- + +set(WCC_TEST_SRC + "${CMAKE_CURRENT_SOURCE_DIR}/components/wcc_test.cpp") + +ConfigureTest(WCC_TEST "${WCC_TEST_SRC}") + ################################################################################################### #-Hungarian (Linear Assignment Problem) tests --------------------------------------------------------------------- @@ -419,22 +539,39 @@ ConfigureTest(EXPERIMENTAL_KATZ_CENTRALITY_TEST "${EXPERIMENTAL_KATZ_CENTRALITY_ ################################################################################################### # - Experimental RANDOM_WALKS tests ------------------------------------------------------------ -set(EXPERIMENTAL_RANDOM_WALKS_TEST_SRCS - "${CMAKE_CURRENT_SOURCE_DIR}/experimental/random_walks_test.cu") +set(RANDOM_WALKS_TEST_SRCS + "${CMAKE_CURRENT_SOURCE_DIR}/sampling/random_walks_test.cu") + +ConfigureTest(RANDOM_WALKS_TEST "${RANDOM_WALKS_TEST_SRCS}") + +################################################################################################### +set(RANDOM_WALKS_LOW_LEVEL_SRCS + "${CMAKE_CURRENT_SOURCE_DIR}/sampling/rw_low_level_test.cu") -ConfigureTest(EXPERIMENTAL_RANDOM_WALKS_TEST "${EXPERIMENTAL_RANDOM_WALKS_TEST_SRCS}") +ConfigureTest(RANDOM_WALKS_LOW_LEVEL_TEST "${RANDOM_WALKS_LOW_LEVEL_SRCS}") ################################################################################################### -set(EXPERIMENTAL_RANDOM_WALKS_LOW_LEVEL_SRCS - "${CMAKE_CURRENT_SOURCE_DIR}/experimental/rw_low_level_test.cu") +set(RANDOM_WALKS_PROFILING_SRCS + "${CMAKE_CURRENT_SOURCE_DIR}/sampling/random_walks_profiling.cu") -ConfigureTest(EXPERIMENTAL_RANDOM_WALKS_LOW_LEVEL_TEST "${EXPERIMENTAL_RANDOM_WALKS_LOW_LEVEL_SRCS}") +# FIXME: since this is technically not a test, consider refactoring the the +# ConfigureTest function to share common code with a new ConfigureBenchmark +# function (which would not link gtest, etc.) +ConfigureTest(RANDOM_WALKS_PROFILING "${RANDOM_WALKS_PROFILING_SRCS}") ################################################################################################### # - MG tests -------------------------------------------------------------------------------------- if(BUILD_CUGRAPH_MG_TESTS) + execute_process( + COMMAND nvidia-smi -L + COMMAND wc -l + OUTPUT_VARIABLE GPU_COUNT) + + string(REGEX REPLACE "\n$" "" GPU_COUNT ${GPU_COUNT}) + MESSAGE(STATUS "GPU_COUNT: " ${GPU_COUNT}) + if(MPI_CXX_FOUND) ########################################################################################### # - MG PAGERANK tests --------------------------------------------------------------------- @@ -442,7 +579,7 @@ if(BUILD_CUGRAPH_MG_TESTS) set(MG_PAGERANK_TEST_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/pagerank/mg_pagerank_test.cpp") - ConfigureTest(MG_PAGERANK_TEST "${MG_PAGERANK_TEST_SRCS}") + ConfigureTestMG(MG_PAGERANK_TEST "${MG_PAGERANK_TEST_SRCS}") target_link_libraries(MG_PAGERANK_TEST PRIVATE MPI::MPI_C MPI::MPI_CXX) ########################################################################################### @@ -451,7 +588,7 @@ if(BUILD_CUGRAPH_MG_TESTS) set(MG_KATZ_CENTRALITY_TEST_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/experimental/mg_katz_centrality_test.cpp") - ConfigureTest(MG_KATZ_CENTRALITY_TEST "${MG_KATZ_CENTRALITY_TEST_SRCS}") + ConfigureTestMG(MG_KATZ_CENTRALITY_TEST "${MG_KATZ_CENTRALITY_TEST_SRCS}") target_link_libraries(MG_KATZ_CENTRALITY_TEST PRIVATE MPI::MPI_C MPI::MPI_CXX) ########################################################################################### @@ -460,7 +597,7 @@ if(BUILD_CUGRAPH_MG_TESTS) set(MG_BFS_TEST_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/experimental/mg_bfs_test.cpp") - ConfigureTest(MG_BFS_TEST "${MG_BFS_TEST_SRCS}") + ConfigureTestMG(MG_BFS_TEST "${MG_BFS_TEST_SRCS}") target_link_libraries(MG_BFS_TEST PRIVATE MPI::MPI_C MPI::MPI_CXX) ########################################################################################### @@ -469,17 +606,17 @@ if(BUILD_CUGRAPH_MG_TESTS) set(MG_SSSP_TEST_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/experimental/mg_sssp_test.cpp") - ConfigureTest(MG_SSSP_TEST "${MG_SSSP_TEST_SRCS}") + ConfigureTestMG(MG_SSSP_TEST "${MG_SSSP_TEST_SRCS}") target_link_libraries(MG_SSSP_TEST PRIVATE MPI::MPI_C MPI::MPI_CXX) ########################################################################################### # - MG LOUVAIN tests ---------------------------------------------------------------------- set(MG_LOUVAIN_TEST_SRCS - "${CMAKE_CURRENT_SOURCE_DIR}/community/mg_louvain_helper.cu" + "${CMAKE_CURRENT_SOURCE_DIR}/community/mg_louvain_helper.cu" "${CMAKE_CURRENT_SOURCE_DIR}/community/mg_louvain_test.cpp") - ConfigureTest(MG_LOUVAIN_TEST "${MG_LOUVAIN_TEST_SRCS}") + ConfigureTestMG(MG_LOUVAIN_TEST "${MG_LOUVAIN_TEST_SRCS}") target_link_libraries(MG_LOUVAIN_TEST PRIVATE MPI::MPI_C MPI::MPI_CXX) else(MPI_CXX_FOUND) diff --git a/cpp/tests/centrality/betweenness_centrality_test.cu b/cpp/tests/centrality/betweenness_centrality_test.cu index d680574e10b..89168618b9c 100644 --- a/cpp/tests/centrality/betweenness_centrality_test.cu +++ b/cpp/tests/centrality/betweenness_centrality_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ #include #include +#include #include @@ -363,6 +364,9 @@ TEST_P(Tests_BC, CheckFP32_NO_NORMALIZE_NO_ENDPOINTS) run_current_test(GetParam()); } +#if 0 +// Temporarily disable some of the test combinations +// Full solution will be explored for issue #1555 TEST_P(Tests_BC, CheckFP64_NO_NORMALIZE_NO_ENDPOINTS) { run_current_test(GetParam()); @@ -372,6 +376,7 @@ TEST_P(Tests_BC, CheckFP32_NO_NORMALIZE_ENDPOINTS) { run_current_test(GetParam()); } +#endif TEST_P(Tests_BC, CheckFP64_NO_NORMALIZE_ENDPOINTS) { @@ -384,6 +389,9 @@ TEST_P(Tests_BC, CheckFP32_NORMALIZE_NO_ENDPOINTS) run_current_test(GetParam()); } +#if 0 +// Temporarily disable some of the test combinations +// Full solution will be explored for issue #1555 TEST_P(Tests_BC, CheckFP64_NORMALIZE_NO_ENDPOINTS) { run_current_test(GetParam()); @@ -393,18 +401,29 @@ TEST_P(Tests_BC, CheckFP32_NORMALIZE_ENDPOINTS) { run_current_test(GetParam()); } +#endif TEST_P(Tests_BC, CheckFP64_NORMALIZE_ENDPOINTS) { run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P(simple_test, - Tests_BC, - ::testing::Values(BC_Usecase("test/datasets/karate.mtx", 0), - BC_Usecase("test/datasets/netscience.mtx", 0), - BC_Usecase("test/datasets/netscience.mtx", 4), - BC_Usecase("test/datasets/wiki2003.mtx", 4), - BC_Usecase("test/datasets/wiki-Talk.mtx", 4))); +#if 0 +// Temporarily disable some of the test combinations +// Full solution will be explored for issue #1555 +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_BC, + ::testing::Values(BC_Usecase("test/datasets/karate.mtx", 0), + BC_Usecase("test/datasets/netscience.mtx", 0), + BC_Usecase("test/datasets/netscience.mtx", 4), + BC_Usecase("test/datasets/wiki2003.mtx", 4), + BC_Usecase("test/datasets/wiki-Talk.mtx", 4))); +#else +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_BC, + ::testing::Values(BC_Usecase("test/datasets/karate.mtx", 0), + BC_Usecase("test/datasets/netscience.mtx", 0), + BC_Usecase("test/datasets/netscience.mtx", 4))); +#endif CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/centrality/edge_betweenness_centrality_test.cu b/cpp/tests/centrality/edge_betweenness_centrality_test.cu index b6cce8684e8..50cbef86e11 100644 --- a/cpp/tests/centrality/edge_betweenness_centrality_test.cu +++ b/cpp/tests/centrality/edge_betweenness_centrality_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ #include #include +#include #include @@ -296,6 +297,9 @@ TEST_P(Tests_EdgeBC, CheckFP32_NO_NORMALIZE) run_current_test(GetParam()); } +#if 0 +// Temporarily disable some of the test combinations +// Full solution will be explored for issue #1555 TEST_P(Tests_EdgeBC, CheckFP64_NO_NORMALIZE) { run_current_test(GetParam()); @@ -306,18 +310,29 @@ TEST_P(Tests_EdgeBC, CheckFP32_NORMALIZE) { run_current_test(GetParam()); } +#endif TEST_P(Tests_EdgeBC, CheckFP64_NORMALIZE) { run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P(simple_test, - Tests_EdgeBC, - ::testing::Values(EdgeBC_Usecase("test/datasets/karate.mtx", 0), - EdgeBC_Usecase("test/datasets/netscience.mtx", 0), - EdgeBC_Usecase("test/datasets/netscience.mtx", 4), - EdgeBC_Usecase("test/datasets/wiki2003.mtx", 4), - EdgeBC_Usecase("test/datasets/wiki-Talk.mtx", 4))); +#if 0 +// Temporarily disable some of the test combinations +// Full solution will be explored for issue #1555 +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_EdgeBC, + ::testing::Values(EdgeBC_Usecase("test/datasets/karate.mtx", 0), + EdgeBC_Usecase("test/datasets/netscience.mtx", 0), + EdgeBC_Usecase("test/datasets/netscience.mtx", 4), + EdgeBC_Usecase("test/datasets/wiki2003.mtx", 4), + EdgeBC_Usecase("test/datasets/wiki-Talk.mtx", 4))); +#else +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_EdgeBC, + ::testing::Values(EdgeBC_Usecase("test/datasets/karate.mtx", 0), + EdgeBC_Usecase("test/datasets/netscience.mtx", 0), + EdgeBC_Usecase("test/datasets/netscience.mtx", 4))); +#endif CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/centrality/katz_centrality_test.cu b/cpp/tests/centrality/katz_centrality_test.cu index c4f17192955..114a89858b8 100644 --- a/cpp/tests/centrality/katz_centrality_test.cu +++ b/cpp/tests/centrality/katz_centrality_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020, NVIDIA CORPORATION. + * Copyright (c) 2019-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -156,7 +156,7 @@ class Tests_Katz : public ::testing::TestWithParam { } }; -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Tests_Katz, ::testing::Values(Katz_Usecase("test/datasets/karate.mtx", "ref/katz/karate.csv"), diff --git a/cpp/tests/community/egonet_test.cu b/cpp/tests/community/egonet_test.cu index d61080c685e..27a235ee15b 100644 --- a/cpp/tests/community/egonet_test.cu +++ b/cpp/tests/community/egonet_test.cu @@ -168,7 +168,7 @@ TEST_P(Tests_InducedEgo, CheckInt32Int32FloatUntransposed) run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Tests_InducedEgo, ::testing::Values( @@ -182,7 +182,7 @@ INSTANTIATE_TEST_CASE_P( // For perf analysis /* -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Tests_InducedEgo, ::testing::Values( diff --git a/cpp/tests/community/louvain_test.cpp b/cpp/tests/community/louvain_test.cpp index 2ebf9a85902..43d274e6723 100644 --- a/cpp/tests/community/louvain_test.cpp +++ b/cpp/tests/community/louvain_test.cpp @@ -313,7 +313,7 @@ TEST_P(Tests_Louvain, CheckInt32Int32FloatFloat) } // FIXME: Expand testing once we evaluate RMM memory use -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Tests_Louvain, ::testing::Values(Louvain_Usecase("test/datasets/karate.mtx", true, 3, 0.408695))); diff --git a/cpp/tests/community/mg_louvain_test.cpp b/cpp/tests/community/mg_louvain_test.cpp index 8a1a3010a6f..4b398f0a4aa 100644 --- a/cpp/tests/community/mg_louvain_test.cpp +++ b/cpp/tests/community/mg_louvain_test.cpp @@ -43,7 +43,7 @@ void compare(double mg_modularity, double sg_modularity) //////////////////////////////////////////////////////////////////////////////// // Test param object. This defines the input and expected output for a test, and // will be instantiated as the parameter to the tests defined below using -// INSTANTIATE_TEST_CASE_P() +// INSTANTIATE_TEST_SUITE_P() // struct Louvain_Usecase { std::string graph_file_full_path{}; @@ -226,7 +226,7 @@ TEST_P(Louvain_MG_Testfixture, CheckInt32Int32Float) run_test(GetParam()); } -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Louvain_MG_Testfixture, ::testing::Values(Louvain_Usecase("test/datasets/karate.mtx", true, 100, 1) diff --git a/cpp/tests/components/con_comp_test.cu b/cpp/tests/components/con_comp_test.cu index 15d60867753..fdae77f2384 100644 --- a/cpp/tests/components/con_comp_test.cu +++ b/cpp/tests/components/con_comp_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved. + * Copyright (c) 2019-2021, NVIDIA CORPORATION. All rights reserved. * * NVIDIA CORPORATION and its licensors retain all intellectual property * and proprietary rights in and to this software, related documentation @@ -141,11 +141,11 @@ std::vector Tests_Weakly_CC::weakly_cc_time; TEST_P(Tests_Weakly_CC, Weakly_CC) { run_current_test(GetParam()); } // --gtest_filter=*simple_test* -INSTANTIATE_TEST_CASE_P(simple_test, - Tests_Weakly_CC, - ::testing::Values(Usecase("test/datasets/dolphins.mtx"), - Usecase("test/datasets/coPapersDBLP.mtx"), - Usecase("test/datasets/coPapersCiteseer.mtx"), - Usecase("test/datasets/hollywood.mtx"))); +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_Weakly_CC, + ::testing::Values(Usecase("test/datasets/dolphins.mtx"), + Usecase("test/datasets/coPapersDBLP.mtx"), + Usecase("test/datasets/coPapersCiteseer.mtx"), + Usecase("test/datasets/hollywood.mtx"))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/components/scc_test.cu b/cpp/tests/components/scc_test.cu index a74b5a0ad27..b875a459bd0 100644 --- a/cpp/tests/components/scc_test.cu +++ b/cpp/tests/components/scc_test.cu @@ -211,7 +211,7 @@ std::vector Tests_Strongly_CC::strongly_cc_counts; TEST_P(Tests_Strongly_CC, Strongly_CC) { run_current_test(GetParam()); } // --gtest_filter=*simple_test* -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Tests_Strongly_CC, ::testing::Values( diff --git a/cpp/tests/components/wcc_graphs.cu b/cpp/tests/components/wcc_graphs.cu new file mode 100644 index 00000000000..fb11f872fb8 --- /dev/null +++ b/cpp/tests/components/wcc_graphs.cu @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. + * + * NVIDIA CORPORATION and its licensors retain all intellectual property + * and proprietary rights in and to this software, related documentation + * and any modifications thereto. Any use, reproduction, disclosure or + * distribution of this software and related documentation without an express + * license agreement from NVIDIA CORPORATION is strictly prohibited. + * + */ + +#include +#include + +#include + +#include + +#include + +namespace cugraph { +namespace test { + +template +std::tuple, + rmm::device_uvector> +LineGraph_Usecase::construct_graph(raft::handle_t const& handle, + bool test_weighted, + bool renumber) const +{ + uint64_t seed{0}; + raft::random::Rng rng(seed); + + edge_t num_edges = 2 * (num_vertices_ - 1); + + rmm::device_uvector vertices_v(num_vertices_, handle.get_stream()); + rmm::device_uvector src_v(num_edges, handle.get_stream()); + rmm::device_uvector dst_v(num_edges, handle.get_stream()); + rmm::device_uvector order_v(num_vertices_, handle.get_stream()); + rmm::device_uvector weights_v(edge_t{0}, handle.get_stream()); + + thrust::sequence( + rmm::exec_policy(handle.get_stream()), vertices_v.begin(), vertices_v.end(), vertex_t{0}); + + rng.uniform(order_v.data(), num_vertices_, 0.0f, 1.0f, handle.get_stream()); + + thrust::sort_by_key( + rmm::exec_policy(handle.get_stream()), order_v.begin(), order_v.end(), vertices_v.begin()); + + raft::copy(src_v.begin(), vertices_v.begin(), (num_vertices_ - 1), handle.get_stream()); + raft::copy(dst_v.begin(), vertices_v.begin() + 1, (num_vertices_ - 1), handle.get_stream()); + + raft::copy(src_v.begin() + (num_vertices_ - 1), + vertices_v.begin() + 1, + (num_vertices_ - 1), + handle.get_stream()); + raft::copy(dst_v.begin() + (num_vertices_ - 1), + vertices_v.begin(), + (num_vertices_ - 1), + handle.get_stream()); + + thrust::sequence( + rmm::exec_policy(handle.get_stream()), vertices_v.begin(), vertices_v.end(), vertex_t{0}); + + handle.get_stream_view().synchronize(); + + return generate_graph_from_edgelist( + handle, + std::move(vertices_v), + std::move(src_v), + std::move(dst_v), + std::move(weights_v), + true, + false, + false); +} + +template std::tuple, + rmm::device_uvector> +LineGraph_Usecase::construct_graph(raft::handle_t const&, bool, bool) const; + +} // namespace test +} // namespace cugraph diff --git a/cpp/tests/components/wcc_graphs.hpp b/cpp/tests/components/wcc_graphs.hpp new file mode 100644 index 00000000000..2b5955c2b78 --- /dev/null +++ b/cpp/tests/components/wcc_graphs.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. + * + * NVIDIA CORPORATION and its licensors retain all intellectual property + * and proprietary rights in and to this software, related documentation + * and any modifications thereto. Any use, reproduction, disclosure or + * distribution of this software and related documentation without an express + * license agreement from NVIDIA CORPORATION is strictly prohibited. + * + */ + +#include + +#include + +namespace cugraph { +namespace test { + +class LineGraph_Usecase { + public: + LineGraph_Usecase() = delete; + + LineGraph_Usecase(size_t num_vertices) : num_vertices_(num_vertices) {} + + template + std::tuple< + cugraph::experimental::graph_t, + rmm::device_uvector> + construct_graph(raft::handle_t const& handle, bool test_weighted, bool renumber = true) const; + + private: + size_t num_vertices_{0}; +}; + +} // namespace test +} // namespace cugraph diff --git a/cpp/tests/components/wcc_test.cpp b/cpp/tests/components/wcc_test.cpp new file mode 100644 index 00000000000..962ecefe8f3 --- /dev/null +++ b/cpp/tests/components/wcc_test.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. + * + * NVIDIA CORPORATION and its licensors retain all intellectual property + * and proprietary rights in and to this software, related documentation + * and any modifications thereto. Any use, reproduction, disclosure or + * distribution of this software and related documentation without an express + * license agreement from NVIDIA CORPORATION is strictly prohibited. + * + */ + +#include +#include +#include + +#include +#include + +#include +#include + +struct WCC_Usecase { + bool validate_results{true}; +}; + +template +class Tests_WCC : public ::testing::TestWithParam> { + public: + Tests_WCC() {} + static void SetupTestCase() {} + static void TearDownTestCase() {} + + virtual void SetUp() {} + virtual void TearDown() {} + + static std::vector weakly_cc_time; + + template + void run_current_test(WCC_Usecase const& param, input_usecase_t const& input_usecase) + { + raft::handle_t handle{}; + + cugraph::experimental::graph_t graph(handle); + + std::tie(graph, std::ignore) = + input_usecase.template construct_graph( + handle, false, false); + + auto graph_view = graph.view(); + + rmm::device_uvector component_labels_v(graph_view.get_number_of_vertices(), + handle.get_stream()); + + // cugraph::weakly_connected_components(handle, graph_view, component_labels_v.begin()); + + // TODO: validate result + } +}; + +using Tests_WCC_File = Tests_WCC; +using Tests_WCC_Rmat = Tests_WCC; +using Tests_WCC_LineGraph = Tests_WCC; + +TEST_P(Tests_WCC_File, WCC) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} +TEST_P(Tests_WCC_Rmat, WCC) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} +TEST_P(Tests_WCC_LineGraph, WCC) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} + +// --gtest_filter=*simple_test* +INSTANTIATE_TEST_SUITE_P( + file_test, + Tests_WCC_File, + ::testing::Values( + std::make_tuple(WCC_Usecase{}, cugraph::test::File_Usecase("test/datasets/dolphins.mtx")), + std::make_tuple(WCC_Usecase{}, cugraph::test::File_Usecase("test/datasets/coPapersDBLP.mtx")), + std::make_tuple(WCC_Usecase{}, + cugraph::test::File_Usecase("test/datasets/coPapersCiteseer.mtx")), + std::make_tuple(WCC_Usecase{}, cugraph::test::File_Usecase("test/datasets/hollywood.mtx")))); + +INSTANTIATE_TEST_SUITE_P( + line_graph_test, + Tests_WCC_LineGraph, + ::testing::Values(std::make_tuple(WCC_Usecase{}, cugraph::test::LineGraph_Usecase(1000)), + std::make_tuple(WCC_Usecase{}, cugraph::test::LineGraph_Usecase(100000)))); + +CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/experimental/bfs_test.cpp b/cpp/tests/experimental/bfs_test.cpp index ded57dd1855..1de439e1430 100644 --- a/cpp/tests/experimental/bfs_test.cpp +++ b/cpp/tests/experimental/bfs_test.cpp @@ -81,63 +81,13 @@ void bfs_reference(edge_t const* offsets, return; } -typedef struct BFS_Usecase_t { - cugraph::test::input_graph_specifier_t input_graph_specifier{}; - +struct BFS_Usecase { size_t source{0}; bool check_correctness{false}; +}; - BFS_Usecase_t(std::string const& graph_file_path, size_t source, bool check_correctness = true) - : source(source), check_correctness(check_correctness) - { - std::string graph_file_full_path{}; - if ((graph_file_path.length() > 0) && (graph_file_path[0] != '/')) { - graph_file_full_path = cugraph::test::get_rapids_dataset_root_dir() + "/" + graph_file_path; - } else { - graph_file_full_path = graph_file_path; - } - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH; - input_graph_specifier.graph_file_full_path = graph_file_full_path; - }; - - BFS_Usecase_t(cugraph::test::rmat_params_t rmat_params, - size_t source, - bool check_correctness = true) - : source(source), check_correctness(check_correctness) - { - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::RMAT_PARAMS; - input_graph_specifier.rmat_params = rmat_params; - } -} BFS_Usecase; - -template -std::tuple, - rmm::device_uvector> -read_graph(raft::handle_t const& handle, BFS_Usecase const& configuration, bool renumber) -{ - return configuration.input_graph_specifier.tag == - cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH - ? cugraph::test:: - read_graph_from_matrix_market_file( - handle, configuration.input_graph_specifier.graph_file_full_path, false, renumber) - : cugraph::test:: - generate_graph_from_rmat_params( - handle, - configuration.input_graph_specifier.rmat_params.scale, - configuration.input_graph_specifier.rmat_params.edge_factor, - configuration.input_graph_specifier.rmat_params.a, - configuration.input_graph_specifier.rmat_params.b, - configuration.input_graph_specifier.rmat_params.c, - configuration.input_graph_specifier.rmat_params.seed, - configuration.input_graph_specifier.rmat_params.undirected, - configuration.input_graph_specifier.rmat_params.scramble_vertex_ids, - false, - renumber, - std::vector{0}, - size_t{1}); -} - -class Tests_BFS : public ::testing::TestWithParam { +template +class Tests_BFS : public ::testing::TestWithParam> { public: Tests_BFS() {} static void SetupTestCase() {} @@ -147,7 +97,7 @@ class Tests_BFS : public ::testing::TestWithParam { virtual void TearDown() {} template - void run_current_test(BFS_Usecase const& configuration) + void run_current_test(BFS_Usecase const& bfs_usecase, input_usecase_t const& input_usecase) { constexpr bool renumber = true; @@ -163,17 +113,19 @@ class Tests_BFS : public ::testing::TestWithParam { cugraph::experimental::graph_t graph(handle); rmm::device_uvector d_renumber_map_labels(0, handle.get_stream()); std::tie(graph, d_renumber_map_labels) = - read_graph(handle, configuration, renumber); + input_usecase.template construct_graph( + handle, true, renumber); + if (PERF) { CUDA_TRY(cudaDeviceSynchronize()); // for consistent performance measurement double elapsed_time{0.0}; hr_clock.stop(&elapsed_time); - std::cout << "read_graph took " << elapsed_time * 1e-6 << " s.\n"; + std::cout << "construct_graph took " << elapsed_time * 1e-6 << " s.\n"; } auto graph_view = graph.view(); - ASSERT_TRUE(static_cast(configuration.source) >= 0 && - static_cast(configuration.source) < graph_view.get_number_of_vertices()) + ASSERT_TRUE(static_cast(bfs_usecase.source) >= 0 && + static_cast(bfs_usecase.source) < graph_view.get_number_of_vertices()) << "Invalid starting source."; rmm::device_uvector d_distances(graph_view.get_number_of_vertices(), @@ -190,7 +142,7 @@ class Tests_BFS : public ::testing::TestWithParam { graph_view, d_distances.data(), d_predecessors.data(), - static_cast(configuration.source), + static_cast(bfs_usecase.source), false, std::numeric_limits::max()); @@ -201,12 +153,13 @@ class Tests_BFS : public ::testing::TestWithParam { std::cout << "BFS took " << elapsed_time * 1e-6 << " s.\n"; } - if (configuration.check_correctness) { + if (bfs_usecase.check_correctness) { cugraph::experimental::graph_t unrenumbered_graph( handle); if (renumber) { std::tie(unrenumbered_graph, std::ignore) = - read_graph(handle, configuration, false); + input_usecase.template construct_graph( + handle, true, false); } auto unrenumbered_graph_view = renumber ? unrenumbered_graph.view() : graph_view; @@ -223,7 +176,7 @@ class Tests_BFS : public ::testing::TestWithParam { handle.get_stream_view().synchronize(); - auto unrenumbered_source = static_cast(configuration.source); + auto unrenumbered_source = static_cast(bfs_usecase.source); if (renumber) { std::vector h_renumber_map_labels(d_renumber_map_labels.size()); raft::update_host(h_renumber_map_labels.data(), @@ -233,7 +186,7 @@ class Tests_BFS : public ::testing::TestWithParam { handle.get_stream_view().synchronize(); - unrenumbered_source = h_renumber_map_labels[configuration.source]; + unrenumbered_source = h_renumber_map_labels[bfs_usecase.source]; } std::vector h_reference_distances(unrenumbered_graph_view.get_number_of_vertices()); @@ -312,24 +265,49 @@ class Tests_BFS : public ::testing::TestWithParam { } }; +using Tests_BFS_File = Tests_BFS; +using Tests_BFS_Rmat = Tests_BFS; + // FIXME: add tests for type combinations -TEST_P(Tests_BFS, CheckInt32Int32) { run_current_test(GetParam()); } +TEST_P(Tests_BFS_File, CheckInt32Int32) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} + +TEST_P(Tests_BFS_Rmat, CheckInt32Int32) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} -INSTANTIATE_TEST_CASE_P( - simple_test, - Tests_BFS, +INSTANTIATE_TEST_SUITE_P( + file_test, + Tests_BFS_File, + ::testing::Values( + // enable correctness checks + std::make_tuple(BFS_Usecase{0}, cugraph::test::File_Usecase("test/datasets/karate.mtx")), + std::make_tuple(BFS_Usecase{0}, cugraph::test::File_Usecase("test/datasets/polbooks.mtx")), + std::make_tuple(BFS_Usecase{0}, cugraph::test::File_Usecase("test/datasets/netscience.mtx")), + std::make_tuple(BFS_Usecase{100}, cugraph::test::File_Usecase("test/datasets/netscience.mtx")), + std::make_tuple(BFS_Usecase{1000}, cugraph::test::File_Usecase("test/datasets/wiki2003.mtx")), + std::make_tuple(BFS_Usecase{1000}, + cugraph::test::File_Usecase("test/datasets/wiki-Talk.mtx")))); + +INSTANTIATE_TEST_SUITE_P( + rmat_small_test, + Tests_BFS_Rmat, ::testing::Values( // enable correctness checks - BFS_Usecase("test/datasets/karate.mtx", 0), - BFS_Usecase("test/datasets/polbooks.mtx", 0), - BFS_Usecase("test/datasets/netscience.mtx", 0), - BFS_Usecase("test/datasets/netscience.mtx", 100), - BFS_Usecase("test/datasets/wiki2003.mtx", 1000), - BFS_Usecase("test/datasets/wiki-Talk.mtx", 1000), - BFS_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, 0), + std::make_tuple(BFS_Usecase{0}, + cugraph::test::Rmat_Usecase(10, 16, 0.57, 0.19, 0.19, 0, false, false)))); + +INSTANTIATE_TEST_SUITE_P( + rmat_large_test, + Tests_BFS_Rmat, + ::testing::Values( // disable correctness checks for large graphs - BFS_Usecase(cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, - 0, - false))); + std::make_pair(BFS_Usecase{0, false}, + cugraph::test::Rmat_Usecase(20, 32, 0.57, 0.19, 0.19, 0, false, false)))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/experimental/coarsen_graph_test.cpp b/cpp/tests/experimental/coarsen_graph_test.cpp index 0fc0634bbbc..5943a5cd286 100644 --- a/cpp/tests/experimental/coarsen_graph_test.cpp +++ b/cpp/tests/experimental/coarsen_graph_test.cpp @@ -370,7 +370,7 @@ TEST_P(Tests_CoarsenGraph, CheckInt32Int32FloatUntransposed) run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Tests_CoarsenGraph, ::testing::Values(CoarsenGraph_Usecase("test/datasets/karate.mtx", 0.2, false), diff --git a/cpp/tests/experimental/degree_test.cpp b/cpp/tests/experimental/degree_test.cpp index 581b6b29f64..ea7cc246df0 100644 --- a/cpp/tests/experimental/degree_test.cpp +++ b/cpp/tests/experimental/degree_test.cpp @@ -157,11 +157,11 @@ TEST_P(Tests_Degree, CheckInt32Int32FloatUntransposed) run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P(simple_test, - Tests_Degree, - ::testing::Values(Degree_Usecase("test/datasets/karate.mtx"), - Degree_Usecase("test/datasets/web-Google.mtx"), - Degree_Usecase("test/datasets/ljournal-2008.mtx"), - Degree_Usecase("test/datasets/webbase-1M.mtx"))); +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_Degree, + ::testing::Values(Degree_Usecase("test/datasets/karate.mtx"), + Degree_Usecase("test/datasets/web-Google.mtx"), + Degree_Usecase("test/datasets/ljournal-2008.mtx"), + Degree_Usecase("test/datasets/webbase-1M.mtx"))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/experimental/generate_rmat_test.cpp b/cpp/tests/experimental/generate_rmat_test.cpp index 221accea4f7..60c3a322725 100644 --- a/cpp/tests/experimental/generate_rmat_test.cpp +++ b/cpp/tests/experimental/generate_rmat_test.cpp @@ -279,12 +279,12 @@ class Tests_GenerateRmat : public ::testing::TestWithParam TEST_P(Tests_GenerateRmat, CheckInt32) { run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P(simple_test, - Tests_GenerateRmat, - ::testing::Values(GenerateRmat_Usecase(20, 16, 0.57, 0.19, 0.19, true), - GenerateRmat_Usecase(20, 16, 0.57, 0.19, 0.19, false), - GenerateRmat_Usecase(20, 16, 0.45, 0.22, 0.22, true), - GenerateRmat_Usecase(20, 16, 0.45, 0.22, 0.22, false))); +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_GenerateRmat, + ::testing::Values(GenerateRmat_Usecase(20, 16, 0.57, 0.19, 0.19, true), + GenerateRmat_Usecase(20, 16, 0.57, 0.19, 0.19, false), + GenerateRmat_Usecase(20, 16, 0.45, 0.22, 0.22, true), + GenerateRmat_Usecase(20, 16, 0.45, 0.22, 0.22, false))); typedef struct GenerateRmats_Usecase_t { size_t n_edgelists{0}; size_t min_scale{0}; @@ -343,7 +343,7 @@ class Tests_GenerateRmats : public ::testing::TestWithParam(GetParam()); } -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Tests_GenerateRmats, ::testing::Values( diff --git a/cpp/tests/experimental/graph_test.cpp b/cpp/tests/experimental/graph_test.cpp index 6ce32e0c836..bdf56ae7aff 100644 --- a/cpp/tests/experimental/graph_test.cpp +++ b/cpp/tests/experimental/graph_test.cpp @@ -230,15 +230,15 @@ TEST_P(Tests_Graph, CheckStoreTransposedTrue) run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P(simple_test, - Tests_Graph, - ::testing::Values(Graph_Usecase("test/datasets/karate.mtx", false), - Graph_Usecase("test/datasets/karate.mtx", true), - Graph_Usecase("test/datasets/web-Google.mtx", false), - Graph_Usecase("test/datasets/web-Google.mtx", true), - Graph_Usecase("test/datasets/ljournal-2008.mtx", false), - Graph_Usecase("test/datasets/ljournal-2008.mtx", true), - Graph_Usecase("test/datasets/webbase-1M.mtx", false), - Graph_Usecase("test/datasets/webbase-1M.mtx", true))); +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_Graph, + ::testing::Values(Graph_Usecase("test/datasets/karate.mtx", false), + Graph_Usecase("test/datasets/karate.mtx", true), + Graph_Usecase("test/datasets/web-Google.mtx", false), + Graph_Usecase("test/datasets/web-Google.mtx", true), + Graph_Usecase("test/datasets/ljournal-2008.mtx", false), + Graph_Usecase("test/datasets/ljournal-2008.mtx", true), + Graph_Usecase("test/datasets/webbase-1M.mtx", false), + Graph_Usecase("test/datasets/webbase-1M.mtx", true))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/experimental/induced_subgraph_test.cpp b/cpp/tests/experimental/induced_subgraph_test.cpp index 4e0ca9e7d92..2d49c174d7e 100644 --- a/cpp/tests/experimental/induced_subgraph_test.cpp +++ b/cpp/tests/experimental/induced_subgraph_test.cpp @@ -295,7 +295,7 @@ TEST_P(Tests_InducedSubgraph, CheckInt32Int32FloatUntransposed) run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Tests_InducedSubgraph, ::testing::Values( diff --git a/cpp/tests/experimental/katz_centrality_test.cpp b/cpp/tests/experimental/katz_centrality_test.cpp index c7756699acd..af70b90dd02 100644 --- a/cpp/tests/experimental/katz_centrality_test.cpp +++ b/cpp/tests/experimental/katz_centrality_test.cpp @@ -96,68 +96,14 @@ void katz_centrality_reference(edge_t const* offsets, return; } -typedef struct KatzCentrality_Usecase_t { - cugraph::test::input_graph_specifier_t input_graph_specifier{}; - +struct KatzCentrality_Usecase { bool test_weighted{false}; bool check_correctness{false}; +}; - KatzCentrality_Usecase_t(std::string const& graph_file_path, - bool test_weighted, - bool check_correctness = true) - : test_weighted(test_weighted), check_correctness(check_correctness) - { - std::string graph_file_full_path{}; - if ((graph_file_path.length() > 0) && (graph_file_path[0] != '/')) { - graph_file_full_path = cugraph::test::get_rapids_dataset_root_dir() + "/" + graph_file_path; - } else { - graph_file_full_path = graph_file_path; - } - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH; - input_graph_specifier.graph_file_full_path = graph_file_full_path; - }; - - KatzCentrality_Usecase_t(cugraph::test::rmat_params_t rmat_params, - bool test_weighted, - bool check_correctness = true) - : test_weighted(test_weighted), check_correctness(check_correctness) - { - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::RMAT_PARAMS; - input_graph_specifier.rmat_params = rmat_params; - } -} KatzCentrality_Usecase; - -template -std::tuple, - rmm::device_uvector> -read_graph(raft::handle_t const& handle, KatzCentrality_Usecase const& configuration, bool renumber) -{ - return configuration.input_graph_specifier.tag == - cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH - ? cugraph::test:: - read_graph_from_matrix_market_file( - handle, - configuration.input_graph_specifier.graph_file_full_path, - configuration.test_weighted, - renumber) - : cugraph::test:: - generate_graph_from_rmat_params( - handle, - configuration.input_graph_specifier.rmat_params.scale, - configuration.input_graph_specifier.rmat_params.edge_factor, - configuration.input_graph_specifier.rmat_params.a, - configuration.input_graph_specifier.rmat_params.b, - configuration.input_graph_specifier.rmat_params.c, - configuration.input_graph_specifier.rmat_params.seed, - configuration.input_graph_specifier.rmat_params.undirected, - configuration.input_graph_specifier.rmat_params.scramble_vertex_ids, - configuration.test_weighted, - renumber, - std::vector{0}, - size_t{1}); -} - -class Tests_KatzCentrality : public ::testing::TestWithParam { +template +class Tests_KatzCentrality + : public ::testing::TestWithParam> { public: Tests_KatzCentrality() {} static void SetupTestCase() {} @@ -167,7 +113,8 @@ class Tests_KatzCentrality : public ::testing::TestWithParam - void run_current_test(KatzCentrality_Usecase const& configuration) + void run_current_test(KatzCentrality_Usecase const& katz_usecase, + input_usecase_t const& input_usecase) { constexpr bool renumber = true; @@ -181,12 +128,14 @@ class Tests_KatzCentrality : public ::testing::TestWithParam graph(handle); rmm::device_uvector d_renumber_map_labels(0, handle.get_stream()); std::tie(graph, d_renumber_map_labels) = - read_graph(handle, configuration, renumber); + input_usecase.template construct_graph( + handle, true, renumber); + if (PERF) { CUDA_TRY(cudaDeviceSynchronize()); // for consistent performance measurement double elapsed_time{0.0}; hr_clock.stop(&elapsed_time); - std::cout << "read_graph took " << elapsed_time * 1e-6 << " s.\n"; + std::cout << "construct_graph took " << elapsed_time * 1e-6 << " s.\n"; } auto graph_view = graph.view(); @@ -226,12 +175,13 @@ class Tests_KatzCentrality : public ::testing::TestWithParam unrenumbered_graph( handle); if (renumber) { std::tie(unrenumbered_graph, std::ignore) = - read_graph(handle, configuration, false); + input_usecase.template construct_graph( + handle, true, false); } auto unrenumbered_graph_view = renumber ? unrenumbered_graph.view() : graph_view; @@ -311,35 +261,47 @@ class Tests_KatzCentrality : public ::testing::TestWithParam; +using Tests_KatzCentrality_Rmat = Tests_KatzCentrality; + // FIXME: add tests for type combinations -TEST_P(Tests_KatzCentrality, CheckInt32Int32FloatFloat) +TEST_P(Tests_KatzCentrality_File, CheckInt32Int32FloatFloat) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} + +TEST_P(Tests_KatzCentrality_Rmat, CheckInt32Int32FloatFloat) { - run_current_test(GetParam()); + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); } -INSTANTIATE_TEST_CASE_P( - simple_test, - Tests_KatzCentrality, - ::testing::Values( +INSTANTIATE_TEST_SUITE_P( + file_test, + Tests_KatzCentrality_File, + ::testing::Combine( // enable correctness checks - KatzCentrality_Usecase("test/datasets/karate.mtx", false), - KatzCentrality_Usecase("test/datasets/karate.mtx", true), - KatzCentrality_Usecase("test/datasets/web-Google.mtx", false), - KatzCentrality_Usecase("test/datasets/web-Google.mtx", true), - KatzCentrality_Usecase("test/datasets/ljournal-2008.mtx", false), - KatzCentrality_Usecase("test/datasets/ljournal-2008.mtx", true), - KatzCentrality_Usecase("test/datasets/webbase-1M.mtx", false), - KatzCentrality_Usecase("test/datasets/webbase-1M.mtx", true), - KatzCentrality_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - false), - KatzCentrality_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - true), - // disable correctness checks for large graphs - KatzCentrality_Usecase(cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, - false, - false), - KatzCentrality_Usecase(cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, - true, - false))); + ::testing::Values(KatzCentrality_Usecase{false}, KatzCentrality_Usecase{true}), + ::testing::Values(cugraph::test::File_Usecase("test/datasets/karate.mtx"), + cugraph::test::File_Usecase("test/datasets/web-Google.mtx"), + cugraph::test::File_Usecase("test/datasets/ljournal-2008.mtx"), + cugraph::test::File_Usecase("test/datasets/webbase-1M.mtx")))); + +INSTANTIATE_TEST_SUITE_P(rmat_small_test, + Tests_KatzCentrality_Rmat, + // enable correctness checks + ::testing::Combine(::testing::Values(KatzCentrality_Usecase{false}, + KatzCentrality_Usecase{true}), + ::testing::Values(cugraph::test::Rmat_Usecase( + 10, 16, 0.57, 0.19, 0.19, 0, false, false)))); + +INSTANTIATE_TEST_SUITE_P(rmat_large_test, + Tests_KatzCentrality_Rmat, + // disable correctness checks for large graphs + ::testing::Combine(::testing::Values(KatzCentrality_Usecase{false, false}, + KatzCentrality_Usecase{true, false}), + ::testing::Values(cugraph::test::Rmat_Usecase( + 20, 32, 0.57, 0.19, 0.19, 0, false, false)))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/experimental/mg_bfs_test.cpp b/cpp/tests/experimental/mg_bfs_test.cpp index 64ffedd2492..ebb2824fb87 100644 --- a/cpp/tests/experimental/mg_bfs_test.cpp +++ b/cpp/tests/experimental/mg_bfs_test.cpp @@ -40,72 +40,13 @@ // static int PERF = 0; -typedef struct BFS_Usecase_t { - cugraph::test::input_graph_specifier_t input_graph_specifier{}; - +struct BFS_Usecase { size_t source{0}; bool check_correctness{false}; +}; - BFS_Usecase_t(std::string const& graph_file_path, size_t source, bool check_correctness = true) - : source(source), check_correctness(check_correctness) - { - std::string graph_file_full_path{}; - if ((graph_file_path.length() > 0) && (graph_file_path[0] != '/')) { - graph_file_full_path = cugraph::test::get_rapids_dataset_root_dir() + "/" + graph_file_path; - } else { - graph_file_full_path = graph_file_path; - } - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH; - input_graph_specifier.graph_file_full_path = graph_file_full_path; - }; - - BFS_Usecase_t(cugraph::test::rmat_params_t rmat_params, - size_t source, - bool check_correctness = true) - : source(source), check_correctness(check_correctness) - { - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::RMAT_PARAMS; - input_graph_specifier.rmat_params = rmat_params; - } -} BFS_Usecase; - -template -std::tuple, - rmm::device_uvector> -read_graph(raft::handle_t const& handle, BFS_Usecase const& configuration, bool renumber) -{ - auto& comm = handle.get_comms(); - auto const comm_size = comm.get_size(); - auto const comm_rank = comm.get_rank(); - - std::vector partition_ids(multi_gpu ? size_t{1} : static_cast(comm_size)); - std::iota(partition_ids.begin(), - partition_ids.end(), - multi_gpu ? static_cast(comm_rank) : size_t{0}); - - return configuration.input_graph_specifier.tag == - cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH - ? cugraph::test:: - read_graph_from_matrix_market_file( - handle, configuration.input_graph_specifier.graph_file_full_path, false, renumber) - : cugraph::test:: - generate_graph_from_rmat_params( - handle, - configuration.input_graph_specifier.rmat_params.scale, - configuration.input_graph_specifier.rmat_params.edge_factor, - configuration.input_graph_specifier.rmat_params.a, - configuration.input_graph_specifier.rmat_params.b, - configuration.input_graph_specifier.rmat_params.c, - configuration.input_graph_specifier.rmat_params.seed, - configuration.input_graph_specifier.rmat_params.undirected, - configuration.input_graph_specifier.rmat_params.scramble_vertex_ids, - false, - renumber, - partition_ids, - static_cast(comm_size)); -} - -class Tests_MGBFS : public ::testing::TestWithParam { +template +class Tests_MGBFS : public ::testing::TestWithParam> { public: Tests_MGBFS() {} static void SetupTestCase() {} @@ -116,7 +57,7 @@ class Tests_MGBFS : public ::testing::TestWithParam { // Compare the results of running BFS on multiple GPUs to that of a single-GPU run template - void run_current_test(BFS_Usecase const& configuration) + void run_current_test(BFS_Usecase const& bfs_usecase, input_usecase_t const& input_usecase) { using weight_t = float; @@ -144,19 +85,20 @@ class Tests_MGBFS : public ::testing::TestWithParam { cugraph::experimental::graph_t mg_graph(handle); rmm::device_uvector d_mg_renumber_map_labels(0, handle.get_stream()); std::tie(mg_graph, d_mg_renumber_map_labels) = - read_graph(handle, configuration, true); + input_usecase.template construct_graph( + handle, false, true); + if (PERF) { CUDA_TRY(cudaDeviceSynchronize()); // for consistent performance measurement double elapsed_time{0.0}; hr_clock.stop(&elapsed_time); - std::cout << "MG read_graph took " << elapsed_time * 1e-6 << " s.\n"; + std::cout << "MG construct_graph took " << elapsed_time * 1e-6 << " s.\n"; } auto mg_graph_view = mg_graph.view(); - ASSERT_TRUE(static_cast(configuration.source) >= 0 && - static_cast(configuration.source) < - mg_graph_view.get_number_of_vertices()) + ASSERT_TRUE(static_cast(bfs_usecase.source) >= 0 && + static_cast(bfs_usecase.source) < mg_graph_view.get_number_of_vertices()) << "Invalid starting source."; // 3. run MG BFS @@ -175,7 +117,7 @@ class Tests_MGBFS : public ::testing::TestWithParam { mg_graph_view, d_mg_distances.data(), d_mg_predecessors.data(), - static_cast(configuration.source), + static_cast(bfs_usecase.source), false, std::numeric_limits::max()); @@ -186,14 +128,15 @@ class Tests_MGBFS : public ::testing::TestWithParam { std::cout << "MG BFS took " << elapsed_time * 1e-6 << " s.\n"; } - // 5. copmare SG & MG results + // 5. compare SG & MG results - if (configuration.check_correctness) { + if (bfs_usecase.check_correctness) { // 5-1. create SG graph cugraph::experimental::graph_t sg_graph(handle); std::tie(sg_graph, std::ignore) = - read_graph(handle, configuration, false); + input_usecase.template construct_graph( + handle, false, false); auto sg_graph_view = sg_graph.view(); @@ -202,7 +145,7 @@ class Tests_MGBFS : public ::testing::TestWithParam { vertex_partition_lasts[i] = mg_graph_view.get_vertex_partition_last(i); } - rmm::device_scalar d_source(static_cast(configuration.source), + rmm::device_scalar d_source(static_cast(bfs_usecase.source), handle.get_stream()); cugraph::experimental::unrenumber_int_vertices( handle, @@ -306,21 +249,46 @@ class Tests_MGBFS : public ::testing::TestWithParam { } }; -TEST_P(Tests_MGBFS, CheckInt32Int32) { run_current_test(GetParam()); } +using Tests_MGBFS_File = Tests_MGBFS; +using Tests_MGBFS_Rmat = Tests_MGBFS; + +TEST_P(Tests_MGBFS_File, CheckInt32Int32) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} + +TEST_P(Tests_MGBFS_Rmat, CheckInt32Int32) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} -INSTANTIATE_TEST_CASE_P( - simple_test, - Tests_MGBFS, +INSTANTIATE_TEST_SUITE_P( + file_test, + Tests_MGBFS_File, + ::testing::Combine( + // enable correctness checks + ::testing::Values(BFS_Usecase{0}), + ::testing::Values(cugraph::test::File_Usecase("test/datasets/karate.mtx"), + cugraph::test::File_Usecase("test/datasets/web-Google.mtx"), + cugraph::test::File_Usecase("test/datasets/ljournal-2008.mtx"), + cugraph::test::File_Usecase("test/datasets/webbase-1M.mtx")))); + +INSTANTIATE_TEST_SUITE_P( + rmat_small_test, + Tests_MGBFS_Rmat, ::testing::Values( // enable correctness checks - BFS_Usecase("test/datasets/karate.mtx", 0), - BFS_Usecase("test/datasets/web-Google.mtx", 0), - BFS_Usecase("test/datasets/ljournal-2008.mtx", 0), - BFS_Usecase("test/datasets/webbase-1M.mtx", 0), - BFS_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, 0), + std::make_tuple(BFS_Usecase{0}, + cugraph::test::Rmat_Usecase(10, 16, 0.57, 0.19, 0.19, 0, false, false, true)))); + +INSTANTIATE_TEST_SUITE_P( + rmat_large_test, + Tests_MGBFS_Rmat, + ::testing::Values( // disable correctness checks for large graphs - BFS_Usecase(cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, - 0, - false))); + std::make_tuple(BFS_Usecase{0, false}, + cugraph::test::Rmat_Usecase(20, 32, 0.57, 0.19, 0.19, 0, false, false, true)))); CUGRAPH_MG_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/experimental/mg_katz_centrality_test.cpp b/cpp/tests/experimental/mg_katz_centrality_test.cpp index 937bd33472b..b4a7968e955 100644 --- a/cpp/tests/experimental/mg_katz_centrality_test.cpp +++ b/cpp/tests/experimental/mg_katz_centrality_test.cpp @@ -37,77 +37,14 @@ // static int PERF = 0; -typedef struct KatzCentrality_Usecase_t { - cugraph::test::input_graph_specifier_t input_graph_specifier{}; - +struct KatzCentrality_Usecase { bool test_weighted{false}; bool check_correctness{false}; +}; - KatzCentrality_Usecase_t(std::string const& graph_file_path, - bool test_weighted, - bool check_correctness = true) - : test_weighted(test_weighted), check_correctness(check_correctness) - { - std::string graph_file_full_path{}; - if ((graph_file_path.length() > 0) && (graph_file_path[0] != '/')) { - graph_file_full_path = cugraph::test::get_rapids_dataset_root_dir() + "/" + graph_file_path; - } else { - graph_file_full_path = graph_file_path; - } - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH; - input_graph_specifier.graph_file_full_path = graph_file_full_path; - }; - - KatzCentrality_Usecase_t(cugraph::test::rmat_params_t rmat_params, - bool test_weighted, - bool check_correctness = true) - : test_weighted(test_weighted), check_correctness(check_correctness) - { - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::RMAT_PARAMS; - input_graph_specifier.rmat_params = rmat_params; - } -} KatzCentrality_Usecase; - -template -std::tuple, - rmm::device_uvector> -read_graph(raft::handle_t const& handle, KatzCentrality_Usecase const& configuration, bool renumber) -{ - auto& comm = handle.get_comms(); - auto const comm_size = comm.get_size(); - auto const comm_rank = comm.get_rank(); - - std::vector partition_ids(multi_gpu ? size_t{1} : static_cast(comm_size)); - std::iota(partition_ids.begin(), - partition_ids.end(), - multi_gpu ? static_cast(comm_rank) : size_t{0}); - - return configuration.input_graph_specifier.tag == - cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH - ? cugraph::test:: - read_graph_from_matrix_market_file( - handle, - configuration.input_graph_specifier.graph_file_full_path, - configuration.test_weighted, - renumber) - : cugraph::test:: - generate_graph_from_rmat_params( - handle, - configuration.input_graph_specifier.rmat_params.scale, - configuration.input_graph_specifier.rmat_params.edge_factor, - configuration.input_graph_specifier.rmat_params.a, - configuration.input_graph_specifier.rmat_params.b, - configuration.input_graph_specifier.rmat_params.c, - configuration.input_graph_specifier.rmat_params.seed, - configuration.input_graph_specifier.rmat_params.undirected, - configuration.input_graph_specifier.rmat_params.scramble_vertex_ids, - configuration.test_weighted, - renumber, - partition_ids, - static_cast(comm_size)); -} - -class Tests_MGKatzCentrality : public ::testing::TestWithParam { +template +class Tests_MGKatzCentrality + : public ::testing::TestWithParam> { public: Tests_MGKatzCentrality() {} static void SetupTestCase() {} @@ -118,7 +55,8 @@ class Tests_MGKatzCentrality : public ::testing::TestWithParam - void run_current_test(KatzCentrality_Usecase const& configuration) + void run_current_test(KatzCentrality_Usecase const &katz_usecase, + input_usecase_t const &input_usecase) { // 1. initialize handle @@ -126,7 +64,7 @@ class Tests_MGKatzCentrality : public ::testing::TestWithParam mg_graph(handle); rmm::device_uvector d_mg_renumber_map_labels(0, handle.get_stream()); std::tie(mg_graph, d_mg_renumber_map_labels) = - read_graph(handle, configuration, true); + input_usecase.template construct_graph( + handle, true, true); + if (PERF) { CUDA_TRY(cudaDeviceSynchronize()); // for consistent performance measurement double elapsed_time{0.0}; hr_clock.stop(&elapsed_time); - std::cout << "MG read_graph took " << elapsed_time * 1e-6 << " s.\n"; + std::cout << "MG construct_graph took " << elapsed_time * 1e-6 << " s.\n"; } auto mg_graph_view = mg_graph.view(); @@ -174,7 +114,7 @@ class Tests_MGKatzCentrality : public ::testing::TestWithParam(nullptr), + static_cast(nullptr), d_mg_katz_centralities.data(), alpha, beta, @@ -191,12 +131,13 @@ class Tests_MGKatzCentrality : public ::testing::TestWithParam sg_graph(handle); std::tie(sg_graph, std::ignore) = - read_graph(handle, configuration, false); + input_usecase.template construct_graph( + handle, true, false); auto sg_graph_view = sg_graph.view(); @@ -207,7 +148,7 @@ class Tests_MGKatzCentrality : public ::testing::TestWithParam(nullptr), + static_cast(nullptr), d_sg_katz_centralities.data(), alpha, beta, @@ -258,34 +199,48 @@ class Tests_MGKatzCentrality : public ::testing::TestWithParam; +using Tests_MGKatzCentrality_Rmat = Tests_MGKatzCentrality; + +TEST_P(Tests_MGKatzCentrality_File, CheckInt32Int32FloatFloat) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} + +TEST_P(Tests_MGKatzCentrality_Rmat, CheckInt32Int32FloatFloat) { - run_current_test(GetParam()); + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); } -INSTANTIATE_TEST_CASE_P( - simple_test, - Tests_MGKatzCentrality, - ::testing::Values( +INSTANTIATE_TEST_SUITE_P( + file_test, + Tests_MGKatzCentrality_File, + ::testing::Combine( // enable correctness checks - KatzCentrality_Usecase("test/datasets/karate.mtx", false), - KatzCentrality_Usecase("test/datasets/karate.mtx", true), - KatzCentrality_Usecase("test/datasets/web-Google.mtx", false), - KatzCentrality_Usecase("test/datasets/web-Google.mtx", true), - KatzCentrality_Usecase("test/datasets/ljournal-2008.mtx", false), - KatzCentrality_Usecase("test/datasets/ljournal-2008.mtx", true), - KatzCentrality_Usecase("test/datasets/webbase-1M.mtx", false), - KatzCentrality_Usecase("test/datasets/webbase-1M.mtx", true), - KatzCentrality_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - false), - KatzCentrality_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - true), - // disable correctness checks for large graphs - KatzCentrality_Usecase(cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, - false, - false), - KatzCentrality_Usecase(cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, - true, - false))); + ::testing::Values(KatzCentrality_Usecase{false}, KatzCentrality_Usecase{true}), + ::testing::Values(cugraph::test::File_Usecase("test/datasets/karate.mtx"), + cugraph::test::File_Usecase("test/datasets/web-Google.mtx"), + cugraph::test::File_Usecase("test/datasets/ljournal-2008.mtx"), + cugraph::test::File_Usecase("test/datasets/webbase-1M.mtx")))); + +INSTANTIATE_TEST_SUITE_P(rmat_small_test, + Tests_MGKatzCentrality_Rmat, + ::testing::Combine( + // enable correctness checks + ::testing::Values(KatzCentrality_Usecase{false}, + KatzCentrality_Usecase{true}), + ::testing::Values(cugraph::test::Rmat_Usecase( + 10, 16, 0.57, 0.19, 0.19, 0, false, false, true)))); + +INSTANTIATE_TEST_SUITE_P(rmat_large_test, + Tests_MGKatzCentrality_Rmat, + ::testing::Combine( + // disable correctness checks for large graphs + ::testing::Values(KatzCentrality_Usecase{false, false}, + KatzCentrality_Usecase{true, false}), + ::testing::Values(cugraph::test::Rmat_Usecase( + 20, 32, 0.57, 0.19, 0.19, 0, false, false, true)))); CUGRAPH_MG_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/experimental/mg_sssp_test.cpp b/cpp/tests/experimental/mg_sssp_test.cpp index de39b8da128..c49efefacd5 100644 --- a/cpp/tests/experimental/mg_sssp_test.cpp +++ b/cpp/tests/experimental/mg_sssp_test.cpp @@ -40,72 +40,13 @@ // static int PERF = 0; -typedef struct SSSP_Usecase_t { - cugraph::test::input_graph_specifier_t input_graph_specifier{}; - +struct SSSP_Usecase { size_t source{0}; bool check_correctness{false}; +}; - SSSP_Usecase_t(std::string const& graph_file_path, size_t source, bool check_correctness = true) - : source(source), check_correctness(check_correctness) - { - std::string graph_file_full_path{}; - if ((graph_file_path.length() > 0) && (graph_file_path[0] != '/')) { - graph_file_full_path = cugraph::test::get_rapids_dataset_root_dir() + "/" + graph_file_path; - } else { - graph_file_full_path = graph_file_path; - } - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH; - input_graph_specifier.graph_file_full_path = graph_file_full_path; - }; - - SSSP_Usecase_t(cugraph::test::rmat_params_t rmat_params, - size_t source, - bool check_correctness = true) - : source(source), check_correctness(check_correctness) - { - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::RMAT_PARAMS; - input_graph_specifier.rmat_params = rmat_params; - } -} SSSP_Usecase; - -template -std::tuple, - rmm::device_uvector> -read_graph(raft::handle_t const& handle, SSSP_Usecase const& configuration, bool renumber) -{ - auto& comm = handle.get_comms(); - auto const comm_size = comm.get_size(); - auto const comm_rank = comm.get_rank(); - - std::vector partition_ids(multi_gpu ? size_t{1} : static_cast(comm_size)); - std::iota(partition_ids.begin(), - partition_ids.end(), - multi_gpu ? static_cast(comm_rank) : size_t{0}); - - return configuration.input_graph_specifier.tag == - cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH - ? cugraph::test:: - read_graph_from_matrix_market_file( - handle, configuration.input_graph_specifier.graph_file_full_path, true, renumber) - : cugraph::test:: - generate_graph_from_rmat_params( - handle, - configuration.input_graph_specifier.rmat_params.scale, - configuration.input_graph_specifier.rmat_params.edge_factor, - configuration.input_graph_specifier.rmat_params.a, - configuration.input_graph_specifier.rmat_params.b, - configuration.input_graph_specifier.rmat_params.c, - configuration.input_graph_specifier.rmat_params.seed, - configuration.input_graph_specifier.rmat_params.undirected, - configuration.input_graph_specifier.rmat_params.scramble_vertex_ids, - true, - renumber, - partition_ids, - static_cast(comm_size)); -} - -class Tests_MGSSSP : public ::testing::TestWithParam { +template +class Tests_MGSSSP : public ::testing::TestWithParam> { public: Tests_MGSSSP() {} static void SetupTestCase() {} @@ -116,10 +57,9 @@ class Tests_MGSSSP : public ::testing::TestWithParam { // Compare the results of running SSSP on multiple GPUs to that of a single-GPU run template - void run_current_test(SSSP_Usecase const& configuration) + void run_current_test(SSSP_Usecase const& sssp_usecase, input_usecase_t const& input_usecase) { // 1. initialize handle - raft::handle_t handle{}; HighResClock hr_clock{}; @@ -142,19 +82,20 @@ class Tests_MGSSSP : public ::testing::TestWithParam { cugraph::experimental::graph_t mg_graph(handle); rmm::device_uvector d_mg_renumber_map_labels(0, handle.get_stream()); std::tie(mg_graph, d_mg_renumber_map_labels) = - read_graph(handle, configuration, true); + input_usecase.template construct_graph( + handle, true, true); + if (PERF) { CUDA_TRY(cudaDeviceSynchronize()); // for consistent performance measurement double elapsed_time{0.0}; hr_clock.stop(&elapsed_time); - std::cout << "MG read_graph took " << elapsed_time * 1e-6 << " s.\n"; + std::cout << "MG construct_graph took " << elapsed_time * 1e-6 << " s.\n"; } auto mg_graph_view = mg_graph.view(); - ASSERT_TRUE(static_cast(configuration.source) >= 0 && - static_cast(configuration.source) < - mg_graph_view.get_number_of_vertices()) + ASSERT_TRUE(static_cast(sssp_usecase.source) >= 0 && + static_cast(sssp_usecase.source) < mg_graph_view.get_number_of_vertices()) << "Invalid starting source."; // 3. run MG SSSP @@ -174,7 +115,7 @@ class Tests_MGSSSP : public ::testing::TestWithParam { mg_graph_view, d_mg_distances.data(), d_mg_predecessors.data(), - static_cast(configuration.source), + static_cast(sssp_usecase.source), std::numeric_limits::max()); if (PERF) { @@ -186,12 +127,13 @@ class Tests_MGSSSP : public ::testing::TestWithParam { // 5. copmare SG & MG results - if (configuration.check_correctness) { + if (sssp_usecase.check_correctness) { // 5-1. create SG graph cugraph::experimental::graph_t sg_graph(handle); std::tie(sg_graph, std::ignore) = - read_graph(handle, configuration, false); + input_usecase.template construct_graph( + handle, true, false); auto sg_graph_view = sg_graph.view(); @@ -200,7 +142,7 @@ class Tests_MGSSSP : public ::testing::TestWithParam { vertex_partition_lasts[i] = mg_graph_view.get_vertex_partition_last(i); } - rmm::device_scalar d_source(static_cast(configuration.source), + rmm::device_scalar d_source(static_cast(sssp_usecase.source), handle.get_stream()); cugraph::experimental::unrenumber_int_vertices( handle, @@ -315,23 +257,45 @@ class Tests_MGSSSP : public ::testing::TestWithParam { } }; -TEST_P(Tests_MGSSSP, CheckInt32Int32Float) +using Tests_MGSSSP_File = Tests_MGSSSP; +using Tests_MGSSSP_Rmat = Tests_MGSSSP; + +TEST_P(Tests_MGSSSP_File, CheckInt32Int32Float) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} + +TEST_P(Tests_MGSSSP_Rmat, CheckInt32Int32Float) { - run_current_test(GetParam()); + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); } -INSTANTIATE_TEST_CASE_P( - simple_test, - Tests_MGSSSP, +INSTANTIATE_TEST_SUITE_P( + file_test, + Tests_MGSSSP_File, + ::testing::Values( + // enable correctness checks + std::make_tuple(SSSP_Usecase{0}, cugraph::test::File_Usecase("test/datasets/karate.mtx")), + std::make_tuple(SSSP_Usecase{0}, cugraph::test::File_Usecase("test/datasets/dblp.mtx")), + std::make_tuple(SSSP_Usecase{1000}, + cugraph::test::File_Usecase("test/datasets/wiki2003.mtx")))); + +INSTANTIATE_TEST_SUITE_P( + rmat_small_test, + Tests_MGSSSP_Rmat, ::testing::Values( // enable correctness checks - SSSP_Usecase("test/datasets/karate.mtx", 0), - SSSP_Usecase("test/datasets/dblp.mtx", 0), - SSSP_Usecase("test/datasets/wiki2003.mtx", 1000), - SSSP_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, 0), + std::make_tuple(SSSP_Usecase{0}, + cugraph::test::Rmat_Usecase(10, 16, 0.57, 0.19, 0.19, 0, false, false)))); + +INSTANTIATE_TEST_SUITE_P( + rmat_large_test, + Tests_MGSSSP_Rmat, + ::testing::Values( // disable correctness checks for large graphs - SSSP_Usecase(cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, - 0, - false))); + std::make_tuple(SSSP_Usecase{0, false}, + cugraph::test::Rmat_Usecase(20, 32, 0.57, 0.19, 0.19, 0, false, false)))); CUGRAPH_MG_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/experimental/ms_bfs_test.cpp b/cpp/tests/experimental/ms_bfs_test.cpp index 264382c22a3..eec51f105ab 100644 --- a/cpp/tests/experimental/ms_bfs_test.cpp +++ b/cpp/tests/experimental/ms_bfs_test.cpp @@ -153,7 +153,7 @@ TEST_P(Tests_MsBfs, DISABLED_CheckInt32Int32FloatUntransposed) run_current_test(GetParam()); } /* -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Tests_MsBfs, ::testing::Values( @@ -167,7 +167,7 @@ INSTANTIATE_TEST_CASE_P( */ // For perf analysis -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Tests_MsBfs, ::testing::Values( diff --git a/cpp/tests/experimental/pagerank_test.cpp b/cpp/tests/experimental/pagerank_test.cpp index 0340140d14b..27739cee01b 100644 --- a/cpp/tests/experimental/pagerank_test.cpp +++ b/cpp/tests/experimental/pagerank_test.cpp @@ -131,75 +131,15 @@ void pagerank_reference(edge_t const* offsets, return; } -typedef struct PageRank_Usecase_t { - cugraph::test::input_graph_specifier_t input_graph_specifier{}; - +struct PageRank_Usecase { double personalization_ratio{0.0}; bool test_weighted{false}; bool check_correctness{false}; +}; - PageRank_Usecase_t(std::string const& graph_file_path, - double personalization_ratio, - bool test_weighted, - bool check_correctness = true) - : personalization_ratio(personalization_ratio), - test_weighted(test_weighted), - check_correctness(check_correctness) - { - std::string graph_file_full_path{}; - if ((graph_file_path.length() > 0) && (graph_file_path[0] != '/')) { - graph_file_full_path = cugraph::test::get_rapids_dataset_root_dir() + "/" + graph_file_path; - } else { - graph_file_full_path = graph_file_path; - } - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH; - input_graph_specifier.graph_file_full_path = graph_file_full_path; - }; - - PageRank_Usecase_t(cugraph::test::rmat_params_t rmat_params, - double personalization_ratio, - bool test_weighted, - bool check_correctness = true) - : personalization_ratio(personalization_ratio), - test_weighted(test_weighted), - check_correctness(check_correctness) - { - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::RMAT_PARAMS; - input_graph_specifier.rmat_params = rmat_params; - } -} PageRank_Usecase; - -template -std::tuple, - rmm::device_uvector> -read_graph(raft::handle_t const& handle, PageRank_Usecase const& configuration, bool renumber) -{ - return configuration.input_graph_specifier.tag == - cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH - ? cugraph::test:: - read_graph_from_matrix_market_file( - handle, - configuration.input_graph_specifier.graph_file_full_path, - configuration.test_weighted, - renumber) - : cugraph::test:: - generate_graph_from_rmat_params( - handle, - configuration.input_graph_specifier.rmat_params.scale, - configuration.input_graph_specifier.rmat_params.edge_factor, - configuration.input_graph_specifier.rmat_params.a, - configuration.input_graph_specifier.rmat_params.b, - configuration.input_graph_specifier.rmat_params.c, - configuration.input_graph_specifier.rmat_params.seed, - configuration.input_graph_specifier.rmat_params.undirected, - configuration.input_graph_specifier.rmat_params.scramble_vertex_ids, - configuration.test_weighted, - renumber, - std::vector{0}, - size_t{1}); -} - -class Tests_PageRank : public ::testing::TestWithParam { +template +class Tests_PageRank + : public ::testing::TestWithParam> { public: Tests_PageRank() {} static void SetupTestCase() {} @@ -209,7 +149,8 @@ class Tests_PageRank : public ::testing::TestWithParam { virtual void TearDown() {} template - void run_current_test(PageRank_Usecase const& configuration) + void run_current_test(PageRank_Usecase const& pagerank_usecase, + input_usecase_t const& input_usecase) { constexpr bool renumber = true; @@ -223,18 +164,19 @@ class Tests_PageRank : public ::testing::TestWithParam { cugraph::experimental::graph_t graph(handle); rmm::device_uvector d_renumber_map_labels(0, handle.get_stream()); std::tie(graph, d_renumber_map_labels) = - read_graph(handle, configuration, renumber); + input_usecase.template construct_graph( + handle, true, renumber); if (PERF) { CUDA_TRY(cudaDeviceSynchronize()); // for consistent performance measurement double elapsed_time{0.0}; hr_clock.stop(&elapsed_time); - std::cout << "read_graph took " << elapsed_time * 1e-6 << " s.\n"; + std::cout << "construct_graph took " << elapsed_time * 1e-6 << " s.\n"; } auto graph_view = graph.view(); std::vector h_personalization_vertices{}; std::vector h_personalization_values{}; - if (configuration.personalization_ratio > 0.0) { + if (pagerank_usecase.personalization_ratio > 0.0) { std::default_random_engine generator{}; std::uniform_real_distribution distribution{0.0, 1.0}; h_personalization_vertices.resize(graph_view.get_number_of_local_vertices()); @@ -244,8 +186,8 @@ class Tests_PageRank : public ::testing::TestWithParam { h_personalization_vertices.erase( std::remove_if(h_personalization_vertices.begin(), h_personalization_vertices.end(), - [&generator, &distribution, configuration](auto v) { - return distribution(generator) >= configuration.personalization_ratio; + [&generator, &distribution, pagerank_usecase](auto v) { + return distribution(generator) >= pagerank_usecase.personalization_ratio; }), h_personalization_vertices.end()); h_personalization_values.resize(h_personalization_vertices.size()); @@ -308,12 +250,13 @@ class Tests_PageRank : public ::testing::TestWithParam { std::cout << "PageRank took " << elapsed_time * 1e-6 << " s.\n"; } - if (configuration.check_correctness) { + if (pagerank_usecase.check_correctness) { cugraph::experimental::graph_t unrenumbered_graph( handle); if (renumber) { std::tie(unrenumbered_graph, std::ignore) = - read_graph(handle, configuration, false); + input_usecase.template construct_graph( + handle, true, false); } auto unrenumbered_graph_view = renumber ? unrenumbered_graph.view() : graph_view; @@ -434,53 +377,57 @@ class Tests_PageRank : public ::testing::TestWithParam { } }; +using Tests_PageRank_File = Tests_PageRank; +using Tests_PageRank_Rmat = Tests_PageRank; + +// FIXME: add tests for type combinations +TEST_P(Tests_PageRank_File, CheckInt32Int32FloatFloat) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} + // FIXME: add tests for type combinations -TEST_P(Tests_PageRank, CheckInt32Int32FloatFloat) +TEST_P(Tests_PageRank_Rmat, CheckInt32Int32FloatFloat) { - run_current_test(GetParam()); + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); } -INSTANTIATE_TEST_CASE_P( - simple_test, - Tests_PageRank, - ::testing::Values( +INSTANTIATE_TEST_SUITE_P( + file_test, + Tests_PageRank_File, + ::testing::Combine( + // enable correctness checks + ::testing::Values(PageRank_Usecase{0.0, false}, + PageRank_Usecase{0.5, false}, + PageRank_Usecase{0.0, true}, + PageRank_Usecase{0.5, true}), + ::testing::Values(cugraph::test::File_Usecase("test/datasets/karate.mtx"), + cugraph::test::File_Usecase("test/datasets/web-Google.mtx"), + cugraph::test::File_Usecase("test/datasets/ljournal-2008.mtx"), + cugraph::test::File_Usecase("test/datasets/webbase-1M.mtx")))); + +INSTANTIATE_TEST_SUITE_P( + rmat_small_tests, + Tests_PageRank_Rmat, + ::testing::Combine( // enable correctness checks - PageRank_Usecase("test/datasets/karate.mtx", 0.0, false), - PageRank_Usecase("test/datasets/karate.mtx", 0.5, false), - PageRank_Usecase("test/datasets/karate.mtx", 0.0, true), - PageRank_Usecase("test/datasets/karate.mtx", 0.5, true), - PageRank_Usecase("test/datasets/web-Google.mtx", 0.0, false), - PageRank_Usecase("test/datasets/web-Google.mtx", 0.5, false), - PageRank_Usecase("test/datasets/web-Google.mtx", 0.0, true), - PageRank_Usecase("test/datasets/web-Google.mtx", 0.5, true), - PageRank_Usecase("test/datasets/ljournal-2008.mtx", 0.0, false), - PageRank_Usecase("test/datasets/ljournal-2008.mtx", 0.5, false), - PageRank_Usecase("test/datasets/ljournal-2008.mtx", 0.0, true), - PageRank_Usecase("test/datasets/ljournal-2008.mtx", 0.5, true), - PageRank_Usecase("test/datasets/webbase-1M.mtx", 0.0, false), - PageRank_Usecase("test/datasets/webbase-1M.mtx", 0.5, false), - PageRank_Usecase("test/datasets/webbase-1M.mtx", 0.0, true), - PageRank_Usecase("test/datasets/webbase-1M.mtx", 0.5, true), - PageRank_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - 0.0, - false), - PageRank_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - 0.5, - false), - PageRank_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - 0.0, - true), - PageRank_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - 0.5, - true), + ::testing::Values(PageRank_Usecase{0.0, false}, + PageRank_Usecase{0.5, false}, + PageRank_Usecase{0.0, true}, + PageRank_Usecase{0.5, true}), + ::testing::Values(cugraph::test::Rmat_Usecase(10, 16, 0.57, 0.19, 0.19, 0, false, false)))); + +INSTANTIATE_TEST_SUITE_P( + rmat_large_tests, + Tests_PageRank_Rmat, + ::testing::Combine( // disable correctness checks for large graphs - PageRank_Usecase( - cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, 0.0, false, false), - PageRank_Usecase( - cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, 0.5, false, false), - PageRank_Usecase( - cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, 0.0, true, false), - PageRank_Usecase( - cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, 0.5, true, false))); + ::testing::Values(PageRank_Usecase{0.0, false, false}, + PageRank_Usecase{0.5, false, false}, + PageRank_Usecase{0.0, true, false}, + PageRank_Usecase{0.5, true, false}), + ::testing::Values(cugraph::test::Rmat_Usecase(20, 32, 0.57, 0.19, 0.19, 0, false, false)))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/experimental/sssp_test.cpp b/cpp/tests/experimental/sssp_test.cpp index e8ab3ec5426..a9c12043a7f 100644 --- a/cpp/tests/experimental/sssp_test.cpp +++ b/cpp/tests/experimental/sssp_test.cpp @@ -87,63 +87,13 @@ void sssp_reference(edge_t const* offsets, return; } -typedef struct SSSP_Usecase_t { - cugraph::test::input_graph_specifier_t input_graph_specifier{}; - +struct SSSP_Usecase { size_t source{0}; bool check_correctness{false}; +}; - SSSP_Usecase_t(std::string const& graph_file_path, size_t source, bool check_correctness = true) - : source(source), check_correctness(check_correctness) - { - std::string graph_file_full_path{}; - if ((graph_file_path.length() > 0) && (graph_file_path[0] != '/')) { - graph_file_full_path = cugraph::test::get_rapids_dataset_root_dir() + "/" + graph_file_path; - } else { - graph_file_full_path = graph_file_path; - } - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH; - input_graph_specifier.graph_file_full_path = graph_file_full_path; - }; - - SSSP_Usecase_t(cugraph::test::rmat_params_t rmat_params, - size_t source, - bool check_correctness = true) - : source(source), check_correctness(check_correctness) - { - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::RMAT_PARAMS; - input_graph_specifier.rmat_params = rmat_params; - } -} SSSP_Usecase; - -template -std::tuple, - rmm::device_uvector> -read_graph(raft::handle_t const& handle, SSSP_Usecase const& configuration, bool renumber) -{ - return configuration.input_graph_specifier.tag == - cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH - ? cugraph::test:: - read_graph_from_matrix_market_file( - handle, configuration.input_graph_specifier.graph_file_full_path, true, renumber) - : cugraph::test:: - generate_graph_from_rmat_params( - handle, - configuration.input_graph_specifier.rmat_params.scale, - configuration.input_graph_specifier.rmat_params.edge_factor, - configuration.input_graph_specifier.rmat_params.a, - configuration.input_graph_specifier.rmat_params.b, - configuration.input_graph_specifier.rmat_params.c, - configuration.input_graph_specifier.rmat_params.seed, - configuration.input_graph_specifier.rmat_params.undirected, - configuration.input_graph_specifier.rmat_params.scramble_vertex_ids, - true, - renumber, - std::vector{0}, - size_t{1}); -} - -class Tests_SSSP : public ::testing::TestWithParam { +template +class Tests_SSSP : public ::testing::TestWithParam> { public: Tests_SSSP() {} static void SetupTestCase() {} @@ -153,7 +103,7 @@ class Tests_SSSP : public ::testing::TestWithParam { virtual void TearDown() {} template - void run_current_test(SSSP_Usecase const& configuration) + void run_current_test(SSSP_Usecase const& sssp_usecase, input_usecase_t const& input_usecase) { constexpr bool renumber = true; @@ -167,17 +117,19 @@ class Tests_SSSP : public ::testing::TestWithParam { cugraph::experimental::graph_t graph(handle); rmm::device_uvector d_renumber_map_labels(0, handle.get_stream()); std::tie(graph, d_renumber_map_labels) = - read_graph(handle, configuration, renumber); + input_usecase.template construct_graph( + handle, true, renumber); if (PERF) { CUDA_TRY(cudaDeviceSynchronize()); // for consistent performance measurement double elapsed_time{0.0}; hr_clock.stop(&elapsed_time); - std::cout << "read_graph took " << elapsed_time * 1e-6 << " s.\n"; + std::cout << "construct_graph took " << elapsed_time * 1e-6 << " s.\n"; } + auto graph_view = graph.view(); - ASSERT_TRUE(static_cast(configuration.source) >= 0 && - static_cast(configuration.source) < graph_view.get_number_of_vertices()); + ASSERT_TRUE(static_cast(sssp_usecase.source) >= 0 && + static_cast(sssp_usecase.source) < graph_view.get_number_of_vertices()); rmm::device_uvector d_distances(graph_view.get_number_of_vertices(), handle.get_stream()); @@ -193,7 +145,7 @@ class Tests_SSSP : public ::testing::TestWithParam { graph_view, d_distances.data(), d_predecessors.data(), - static_cast(configuration.source), + static_cast(sssp_usecase.source), std::numeric_limits::max(), false); @@ -204,12 +156,13 @@ class Tests_SSSP : public ::testing::TestWithParam { std::cout << "SSSP took " << elapsed_time * 1e-6 << " s.\n"; } - if (configuration.check_correctness) { + if (sssp_usecase.check_correctness) { cugraph::experimental::graph_t unrenumbered_graph( handle); if (renumber) { std::tie(unrenumbered_graph, std::ignore) = - read_graph(handle, configuration, false); + input_usecase.template construct_graph( + handle, true, false); } auto unrenumbered_graph_view = renumber ? unrenumbered_graph.view() : graph_view; @@ -231,7 +184,7 @@ class Tests_SSSP : public ::testing::TestWithParam { handle.get_stream_view().synchronize(); - auto unrenumbered_source = static_cast(configuration.source); + auto unrenumbered_source = static_cast(sssp_usecase.source); if (renumber) { std::vector h_renumber_map_labels(d_renumber_map_labels.size()); raft::update_host(h_renumber_map_labels.data(), @@ -241,7 +194,7 @@ class Tests_SSSP : public ::testing::TestWithParam { handle.get_stream_view().synchronize(); - unrenumbered_source = h_renumber_map_labels[configuration.source]; + unrenumbered_source = h_renumber_map_labels[sssp_usecase.source]; } std::vector h_reference_distances(unrenumbered_graph_view.get_number_of_vertices()); @@ -330,21 +283,44 @@ class Tests_SSSP : public ::testing::TestWithParam { } }; +using Tests_SSSP_File = Tests_SSSP; +using Tests_SSSP_Rmat = Tests_SSSP; + // FIXME: add tests for type combinations -TEST_P(Tests_SSSP, CheckInt32Int32Float) { run_current_test(GetParam()); } +TEST_P(Tests_SSSP_File, CheckInt32Int32Float) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} +TEST_P(Tests_SSSP_Rmat, CheckInt32Int32Float) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} -INSTANTIATE_TEST_CASE_P( - simple_test, - Tests_SSSP, +INSTANTIATE_TEST_SUITE_P( + file_test, + Tests_SSSP_File, + // enable correctness checks + ::testing::Values( + std::make_tuple(SSSP_Usecase{0}, cugraph::test::File_Usecase("test/datasets/karate.mtx")), + std::make_tuple(SSSP_Usecase{0}, cugraph::test::File_Usecase("test/datasets/dblp.mtx")), + std::make_tuple(SSSP_Usecase{1000}, + cugraph::test::File_Usecase("test/datasets/wiki2003.mtx")))); + +INSTANTIATE_TEST_SUITE_P( + rmat_small_test, + Tests_SSSP_Rmat, + // enable correctness checks + ::testing::Values(std::make_tuple( + SSSP_Usecase{0}, cugraph::test::Rmat_Usecase(10, 16, 0.57, 0.19, 0.19, 0, false, false)))); + +INSTANTIATE_TEST_SUITE_P( + rmat_large_test, + Tests_SSSP_Rmat, + // disable correctness checks for large graphs ::testing::Values( - // enable correctness checks - SSSP_Usecase("test/datasets/karate.mtx", 0), - SSSP_Usecase("test/datasets/dblp.mtx", 0), - SSSP_Usecase("test/datasets/wiki2003.mtx", 1000), - SSSP_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, 0), - // disable correctness checks for large graphs - SSSP_Usecase(cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, - 0, - false))); + std::make_tuple(SSSP_Usecase{0, false}, + cugraph::test::Rmat_Usecase(20, 32, 0.57, 0.19, 0.19, 0, false, false)))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/experimental/weight_sum_test.cpp b/cpp/tests/experimental/weight_sum_test.cpp index 9ab47b69baa..d04cba2d132 100644 --- a/cpp/tests/experimental/weight_sum_test.cpp +++ b/cpp/tests/experimental/weight_sum_test.cpp @@ -178,11 +178,11 @@ TEST_P(Tests_WeightSum, CheckInt32Int32FloatUntransposed) run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P(simple_test, - Tests_WeightSum, - ::testing::Values(WeightSum_Usecase("test/datasets/karate.mtx"), - WeightSum_Usecase("test/datasets/web-Google.mtx"), - WeightSum_Usecase("test/datasets/ljournal-2008.mtx"), - WeightSum_Usecase("test/datasets/webbase-1M.mtx"))); +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_WeightSum, + ::testing::Values(WeightSum_Usecase("test/datasets/karate.mtx"), + WeightSum_Usecase("test/datasets/web-Google.mtx"), + WeightSum_Usecase("test/datasets/ljournal-2008.mtx"), + WeightSum_Usecase("test/datasets/webbase-1M.mtx"))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/layout/force_atlas2_test.cu b/cpp/tests/layout/force_atlas2_test.cu index d564765d0df..c6067407b70 100644 --- a/cpp/tests/layout/force_atlas2_test.cu +++ b/cpp/tests/layout/force_atlas2_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + * Copyright (c) 2020-2021, NVIDIA CORPORATION. All rights reserved. * * NVIDIA CORPORATION and its licensors retain all intellectual property * and proprietary rights in and to this software, related documentation @@ -229,12 +229,12 @@ TEST_P(Tests_Force_Atlas2, CheckFP32_T) { run_current_test(GetParam()); } TEST_P(Tests_Force_Atlas2, CheckFP64_T) { run_current_test(GetParam()); } // --gtest_filter=*simple_test* -INSTANTIATE_TEST_CASE_P(simple_test, - Tests_Force_Atlas2, - ::testing::Values(Force_Atlas2_Usecase("test/datasets/karate.mtx", 0.73), - Force_Atlas2_Usecase("test/datasets/dolphins.mtx", 0.69), - Force_Atlas2_Usecase("test/datasets/polbooks.mtx", 0.76), - Force_Atlas2_Usecase("test/datasets/netscience.mtx", - 0.80))); +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_Force_Atlas2, + ::testing::Values(Force_Atlas2_Usecase("test/datasets/karate.mtx", 0.73), + Force_Atlas2_Usecase("test/datasets/dolphins.mtx", 0.69), + Force_Atlas2_Usecase("test/datasets/polbooks.mtx", 0.76), + Force_Atlas2_Usecase("test/datasets/netscience.mtx", + 0.80))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/pagerank/mg_pagerank_test.cpp b/cpp/tests/pagerank/mg_pagerank_test.cpp index bbc80a60a3d..0eae6a62f31 100644 --- a/cpp/tests/pagerank/mg_pagerank_test.cpp +++ b/cpp/tests/pagerank/mg_pagerank_test.cpp @@ -40,84 +40,15 @@ // static int PERF = 0; -typedef struct PageRank_Usecase_t { - cugraph::test::input_graph_specifier_t input_graph_specifier{}; - +struct PageRank_Usecase { double personalization_ratio{0.0}; bool test_weighted{false}; bool check_correctness{false}; +}; - PageRank_Usecase_t(std::string const& graph_file_path, - double personalization_ratio, - bool test_weighted, - bool check_correctness = true) - : personalization_ratio(personalization_ratio), - test_weighted(test_weighted), - check_correctness(check_correctness) - { - std::string graph_file_full_path{}; - if ((graph_file_path.length() > 0) && (graph_file_path[0] != '/')) { - graph_file_full_path = cugraph::test::get_rapids_dataset_root_dir() + "/" + graph_file_path; - } else { - graph_file_full_path = graph_file_path; - } - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH; - input_graph_specifier.graph_file_full_path = graph_file_full_path; - }; - - PageRank_Usecase_t(cugraph::test::rmat_params_t rmat_params, - double personalization_ratio, - bool test_weighted, - bool check_correctness = true) - : personalization_ratio(personalization_ratio), - test_weighted(test_weighted), - check_correctness(check_correctness) - { - input_graph_specifier.tag = cugraph::test::input_graph_specifier_t::RMAT_PARAMS; - input_graph_specifier.rmat_params = rmat_params; - } -} PageRank_Usecase; - -template -std::tuple, - rmm::device_uvector> -read_graph(raft::handle_t const& handle, PageRank_Usecase const& configuration, bool renumber) -{ - auto& comm = handle.get_comms(); - auto const comm_size = comm.get_size(); - auto const comm_rank = comm.get_rank(); - - std::vector partition_ids(multi_gpu ? size_t{1} : static_cast(comm_size)); - std::iota(partition_ids.begin(), - partition_ids.end(), - multi_gpu ? static_cast(comm_rank) : size_t{0}); - - return configuration.input_graph_specifier.tag == - cugraph::test::input_graph_specifier_t::MATRIX_MARKET_FILE_PATH - ? cugraph::test:: - read_graph_from_matrix_market_file( - handle, - configuration.input_graph_specifier.graph_file_full_path, - configuration.test_weighted, - renumber) - : cugraph::test:: - generate_graph_from_rmat_params( - handle, - configuration.input_graph_specifier.rmat_params.scale, - configuration.input_graph_specifier.rmat_params.edge_factor, - configuration.input_graph_specifier.rmat_params.a, - configuration.input_graph_specifier.rmat_params.b, - configuration.input_graph_specifier.rmat_params.c, - configuration.input_graph_specifier.rmat_params.seed, - configuration.input_graph_specifier.rmat_params.undirected, - configuration.input_graph_specifier.rmat_params.scramble_vertex_ids, - configuration.test_weighted, - renumber, - partition_ids, - static_cast(comm_size)); -} - -class Tests_MGPageRank : public ::testing::TestWithParam { +template +class Tests_MGPageRank + : public ::testing::TestWithParam> { public: Tests_MGPageRank() {} static void SetupTestCase() {} @@ -128,10 +59,10 @@ class Tests_MGPageRank : public ::testing::TestWithParam { // Compare the results of running PageRank on multiple GPUs to that of a single-GPU run template - void run_current_test(PageRank_Usecase const& configuration) + void run_current_test(PageRank_Usecase const& pagerank_usecase, + input_usecase_t const& input_usecase) { // 1. initialize handle - raft::handle_t handle{}; HighResClock hr_clock{}; @@ -154,12 +85,13 @@ class Tests_MGPageRank : public ::testing::TestWithParam { cugraph::experimental::graph_t mg_graph(handle); rmm::device_uvector d_mg_renumber_map_labels(0, handle.get_stream()); std::tie(mg_graph, d_mg_renumber_map_labels) = - read_graph(handle, configuration, true); + input_usecase.template construct_graph(handle, true); + if (PERF) { CUDA_TRY(cudaDeviceSynchronize()); // for consistent performance measurement double elapsed_time{0.0}; hr_clock.stop(&elapsed_time); - std::cout << "MG read_graph took " << elapsed_time * 1e-6 << " s.\n"; + std::cout << "MG construct_graph took " << elapsed_time * 1e-6 << " s.\n"; } auto mg_graph_view = mg_graph.view(); @@ -168,7 +100,7 @@ class Tests_MGPageRank : public ::testing::TestWithParam { std::vector h_mg_personalization_vertices{}; std::vector h_mg_personalization_values{}; - if (configuration.personalization_ratio > 0.0) { + if (pagerank_usecase.personalization_ratio > 0.0) { std::default_random_engine generator{ static_cast(comm.get_rank()) /* seed */}; std::uniform_real_distribution distribution{0.0, 1.0}; @@ -179,8 +111,8 @@ class Tests_MGPageRank : public ::testing::TestWithParam { h_mg_personalization_vertices.erase( std::remove_if(h_mg_personalization_vertices.begin(), h_mg_personalization_vertices.end(), - [&generator, &distribution, configuration](auto v) { - return distribution(generator) >= configuration.personalization_ratio; + [&generator, &distribution, pagerank_usecase](auto v) { + return distribution(generator) >= pagerank_usecase.personalization_ratio; }), h_mg_personalization_vertices.end()); h_mg_personalization_values.resize(h_mg_personalization_vertices.size()); @@ -238,12 +170,13 @@ class Tests_MGPageRank : public ::testing::TestWithParam { // 5. copmare SG & MG results - if (configuration.check_correctness) { + if (pagerank_usecase.check_correctness) { // 5-1. create SG graph cugraph::experimental::graph_t sg_graph(handle); std::tie(sg_graph, std::ignore) = - read_graph(handle, configuration, false); + input_usecase.template construct_graph( + handle, true, false); auto sg_graph_view = sg_graph.view(); @@ -251,7 +184,7 @@ class Tests_MGPageRank : public ::testing::TestWithParam { rmm::device_uvector d_sg_personalization_vertices(0, handle.get_stream()); rmm::device_uvector d_sg_personalization_values(0, handle.get_stream()); - if (configuration.personalization_ratio > 0.0) { + if (pagerank_usecase.personalization_ratio > 0.0) { rmm::device_uvector d_unrenumbered_personalization_vertices( d_mg_personalization_vertices.size(), handle.get_stream()); rmm::device_uvector d_unrenumbered_personalization_values( @@ -371,52 +304,51 @@ class Tests_MGPageRank : public ::testing::TestWithParam { } }; -TEST_P(Tests_MGPageRank, CheckInt32Int32FloatFloat) +using Tests_MGPageRank_File = Tests_MGPageRank; +using Tests_MGPageRank_Rmat = Tests_MGPageRank; + +TEST_P(Tests_MGPageRank_File, CheckInt32Int32FloatFloat) +{ + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); +} + +TEST_P(Tests_MGPageRank_Rmat, CheckInt32Int32FloatFloat) { - run_current_test(GetParam()); + auto param = GetParam(); + run_current_test(std::get<0>(param), std::get<1>(param)); } -INSTANTIATE_TEST_CASE_P( - simple_test, - Tests_MGPageRank, - ::testing::Values( +INSTANTIATE_TEST_SUITE_P( + file_tests, + Tests_MGPageRank_File, + ::testing::Combine( // enable correctness checks - PageRank_Usecase("test/datasets/karate.mtx", 0.0, false), - PageRank_Usecase("test/datasets/karate.mtx", 0.5, false), - PageRank_Usecase("test/datasets/karate.mtx", 0.0, true), - PageRank_Usecase("test/datasets/karate.mtx", 0.5, true), - PageRank_Usecase("test/datasets/web-Google.mtx", 0.0, false), - PageRank_Usecase("test/datasets/web-Google.mtx", 0.5, false), - PageRank_Usecase("test/datasets/web-Google.mtx", 0.0, true), - PageRank_Usecase("test/datasets/web-Google.mtx", 0.5, true), - PageRank_Usecase("test/datasets/ljournal-2008.mtx", 0.0, false), - PageRank_Usecase("test/datasets/ljournal-2008.mtx", 0.5, false), - PageRank_Usecase("test/datasets/ljournal-2008.mtx", 0.0, true), - PageRank_Usecase("test/datasets/ljournal-2008.mtx", 0.5, true), - PageRank_Usecase("test/datasets/webbase-1M.mtx", 0.0, false), - PageRank_Usecase("test/datasets/webbase-1M.mtx", 0.5, false), - PageRank_Usecase("test/datasets/webbase-1M.mtx", 0.0, true), - PageRank_Usecase("test/datasets/webbase-1M.mtx", 0.5, true), - PageRank_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - 0.0, - false), - PageRank_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - 0.5, - false), - PageRank_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - 0.0, - true), - PageRank_Usecase(cugraph::test::rmat_params_t{10, 16, 0.57, 0.19, 0.19, 0, false, false}, - 0.5, - true), - // disable correctness checks for large graphs - PageRank_Usecase( - cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, 0.0, false, false), - PageRank_Usecase( - cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, 0.5, false, false), - PageRank_Usecase( - cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, 0.0, true, false), - PageRank_Usecase( - cugraph::test::rmat_params_t{20, 32, 0.57, 0.19, 0.19, 0, false, false}, 0.5, true, false))); + ::testing::Values(PageRank_Usecase{0.0, false}, + PageRank_Usecase{0.5, false}, + PageRank_Usecase{0.0, true}, + PageRank_Usecase{0.5, true}), + ::testing::Values(cugraph::test::File_Usecase("test/datasets/karate.mtx"), + cugraph::test::File_Usecase("test/datasets/web-Google.mtx"), + cugraph::test::File_Usecase("test/datasets/ljournal-2008.mtx"), + cugraph::test::File_Usecase("test/datasets/webbase-1M.mtx")))); + +INSTANTIATE_TEST_SUITE_P(rmat_small_tests, + Tests_MGPageRank_Rmat, + ::testing::Combine(::testing::Values(PageRank_Usecase{0.0, false}, + PageRank_Usecase{0.5, false}, + PageRank_Usecase{0.0, true}, + PageRank_Usecase{0.5, true}), + ::testing::Values(cugraph::test::Rmat_Usecase( + 10, 16, 0.57, 0.19, 0.19, 0, false, false, true)))); + +INSTANTIATE_TEST_SUITE_P(rmat_large_tests, + Tests_MGPageRank_Rmat, + ::testing::Combine(::testing::Values(PageRank_Usecase{0.0, false, false}, + PageRank_Usecase{0.5, false, false}, + PageRank_Usecase{0.0, true, false}, + PageRank_Usecase{0.5, true, false}), + ::testing::Values(cugraph::test::Rmat_Usecase( + 20, 32, 0.57, 0.19, 0.19, 0, false, false, true)))); CUGRAPH_MG_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/sampling/random_walks_profiling.cu b/cpp/tests/sampling/random_walks_profiling.cu new file mode 100644 index 00000000000..397196c4c78 --- /dev/null +++ b/cpp/tests/sampling/random_walks_profiling.cu @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2021, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include // cugraph::test::create_memory_resource() +#include +#include + +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +/** + * @internal + * @brief Populates the device vector d_start with the starting vertex indices + * to be used for each RW path specified. + */ +template +void fill_start(raft::handle_t const& handle, + rmm::device_uvector& d_start, + index_t num_vertices) +{ + index_t num_paths = d_start.size(); + + thrust::transform(rmm::exec_policy(handle.get_stream())->on(handle.get_stream()), + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(num_paths), + + d_start.begin(), + [num_vertices] __device__(auto indx) { return indx % num_vertices; }); +} + +/** + * @internal + * @brief Calls the random_walks algorithm and displays the time metrics (total + * time for all requested paths, average time for each path). + */ +template +void output_random_walks_time(graph_vt const& graph_view, typename graph_vt::edge_type num_paths) +{ + using vertex_t = typename graph_vt::vertex_type; + using edge_t = typename graph_vt::edge_type; + using weight_t = typename graph_vt::weight_type; + + raft::handle_t handle{}; + rmm::device_uvector d_start(num_paths, handle.get_stream()); + + vertex_t num_vertices = graph_view.get_number_of_vertices(); + fill_start(handle, d_start, num_vertices); + + // 0-copy const device view: + // + cugraph::experimental::detail::device_const_vector_view d_start_view{ + d_start.data(), num_paths}; + + edge_t max_depth{10}; + + HighResTimer hr_timer; + std::string label("RandomWalks"); + hr_timer.start(label); + cudaProfilerStart(); + auto ret_tuple = + cugraph::experimental::detail::random_walks_impl(handle, graph_view, d_start_view, max_depth); + cudaProfilerStop(); + hr_timer.stop(); + try { + auto runtime = hr_timer.get_average_runtime(label); + + std::cout << "RW for num_paths: " << num_paths + << ", runtime [ms] / path: " << runtime / num_paths << ":\n"; + + } catch (std::exception const& ex) { + std::cerr << ex.what() << '\n'; + return; + + } catch (...) { + std::cerr << "ERROR: Unknown exception on timer label search." << '\n'; + return; + } + hr_timer.display(std::cout); +} + +/** + * @struct RandomWalks_Usecase + * @brief Used to specify input to a random_walks benchmark/profile run + * + * @var RandomWalks_Usecase::graph_file_full_path Computed during construction + * to be an absolute path consisting of the value of the RAPIDS_DATASET_ROOT_DIR + * env var and the graph_file_path constructor arg. This is initialized to an + * empty string. + * + * @var RandomWalks_Usecase::test_weighted Bool representing if the specified + * graph is weighted or not. This is initialized to false (unweighted). + */ +struct RandomWalks_Usecase { + std::string graph_file_full_path{}; + bool test_weighted{false}; + + RandomWalks_Usecase(std::string const& graph_file_path, bool test_weighted) + : test_weighted(test_weighted) + { + if ((graph_file_path.length() > 0) && (graph_file_path[0] != '/')) { + graph_file_full_path = cugraph::test::get_rapids_dataset_root_dir() + "/" + graph_file_path; + } else { + graph_file_full_path = graph_file_path; + } + }; +}; + +/** + * @brief Runs random_walks on a specified input and outputs time metrics + * + * Creates a graph_t instance from the configuration specified in the + * RandomWalks_Usecase instance passed in (currently by reading a dataset to + * populate the graph_t), then runs random_walks to generate 1, 10, and 100 + * random paths and output statistics for each. + * + * @tparam vertex_t Type of vertex identifiers. + * @tparam edge_t Type of edge identifiers. + * @tparam weight_t Type of weight identifiers. + * + * @param[in] configuration RandomWalks_Usecase instance containing the input + * file to read for constructing the graph_t. + */ +template +void run(RandomWalks_Usecase const& configuration) +{ + raft::handle_t handle{}; + + cugraph::experimental::graph_t graph(handle); + std::tie(graph, std::ignore) = + cugraph::test::read_graph_from_matrix_market_file( + handle, configuration.graph_file_full_path, configuration.test_weighted, false); + + auto graph_view = graph.view(); + + // FIXME: the num_paths vector might be better specified via the + // configuration input instead of hardcoding here. + std::vector v_np{1, 10, 100}; + for (auto&& num_paths : v_np) { output_random_walks_time(graph_view, num_paths); } +} + +/** + * @brief Performs the random_walks benchmark/profiling run + * + * main function for performing the random_walks benchmark/profiling run. The + * resulting executable takes the following options: "rmm_mode" which can be one + * of "binning", "cuda", "pool", or "managed. "dataset" which is a path + * relative to the env var RAPIDS_DATASET_ROOT_DIR to a input .mtx file to use + * to populate the graph_t instance. + * + * To use the default values of rmm_mode=pool and + * dataset=test/datasets/karate.mtx: + * @code + * RANDOM_WALKS_PROFILING + * @endcode + * + * To specify managed memory and the netscience.mtx dataset (relative to a + * particular RAPIDS_DATASET_ROOT_DIR setting): + * @code + * RANDOM_WALKS_PROFILING --rmm_mode=managed --dataset=test/datasets/netscience.mtx + * @endcode + * + * @return An int representing a successful run. 0 indicates success. + */ +int main(int argc, char** argv) +{ + // Add command-line processing, provide defaults + cxxopts::Options options(argv[0], " - Random Walks benchmark command line options"); + options.add_options()( + "rmm_mode", "RMM allocation mode", cxxopts::value()->default_value("pool")); + options.add_options()( + "dataset", "dataset", cxxopts::value()->default_value("test/datasets/karate.mtx")); + auto const cmd_options = options.parse(argc, argv); + auto const rmm_mode = cmd_options["rmm_mode"].as(); + auto const dataset = cmd_options["dataset"].as(); + + // Configure RMM + auto resource = cugraph::test::create_memory_resource(rmm_mode); + rmm::mr::set_current_device_resource(resource.get()); + + // Run benchmarks + std::cout << "Using dataset: " << dataset << std::endl; + run(RandomWalks_Usecase(dataset, true)); + + // FIXME: consider returning non-zero for situations that warrant it (eg. if + // the algo ran but the results are invalid, if a benchmark threshold is + // exceeded, etc.) + return 0; +} diff --git a/cpp/tests/experimental/random_walks_test.cu b/cpp/tests/sampling/random_walks_test.cu similarity index 98% rename from cpp/tests/experimental/random_walks_test.cu rename to cpp/tests/sampling/random_walks_test.cu index 9fb1716f62b..9e4ecd0d024 100644 --- a/cpp/tests/experimental/random_walks_test.cu +++ b/cpp/tests/sampling/random_walks_test.cu @@ -24,8 +24,8 @@ #include #include -#include #include +#include #include #include @@ -141,7 +141,7 @@ TEST_P(Tests_RandomWalks, Initialize_i32_i32_f) run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P( +INSTANTIATE_TEST_SUITE_P( simple_test, Tests_RandomWalks, ::testing::Values(RandomWalks_Usecase("test/datasets/karate.mtx", true), diff --git a/cpp/tests/experimental/random_walks_utils.cuh b/cpp/tests/sampling/random_walks_utils.cuh similarity index 99% rename from cpp/tests/experimental/random_walks_utils.cuh rename to cpp/tests/sampling/random_walks_utils.cuh index 863094dc310..b0b06e7f65a 100644 --- a/cpp/tests/experimental/random_walks_utils.cuh +++ b/cpp/tests/sampling/random_walks_utils.cuh @@ -16,8 +16,8 @@ #pragma once #include -#include #include +#include #include diff --git a/cpp/tests/experimental/rw_low_level_test.cu b/cpp/tests/sampling/rw_low_level_test.cu similarity index 86% rename from cpp/tests/experimental/rw_low_level_test.cu rename to cpp/tests/sampling/rw_low_level_test.cu index 8b562bc41f6..dd7fd14b3a2 100644 --- a/cpp/tests/experimental/rw_low_level_test.cu +++ b/cpp/tests/sampling/rw_low_level_test.cu @@ -24,8 +24,8 @@ #include #include -#include #include +#include #include #include @@ -782,3 +782,121 @@ TEST_F(RandomWalksPrimsTest, SimpleGraphRandomWalk) ASSERT_TRUE(test_all_paths); } + +TEST(RandomWalksSpecialCase, SingleRandomWalk) +{ + using vertex_t = int32_t; + using edge_t = vertex_t; + using weight_t = float; + using index_t = vertex_t; + + raft::handle_t handle{}; + + edge_t num_edges = 8; + vertex_t num_vertices = 6; + + std::vector v_src{0, 1, 1, 2, 2, 2, 3, 4}; + std::vector v_dst{1, 3, 4, 0, 1, 3, 5, 5}; + std::vector v_w{0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1}; + + auto graph = make_graph(handle, v_src, v_dst, v_w, num_vertices, num_edges, true); + + auto graph_view = graph.view(); + + edge_t const* offsets = graph_view.offsets(); + vertex_t const* indices = graph_view.indices(); + weight_t const* values = graph_view.weights(); + + std::vector v_ro(num_vertices + 1); + std::vector v_ci(num_edges); + std::vector v_vals(num_edges); + + raft::update_host(v_ro.data(), offsets, v_ro.size(), handle.get_stream()); + raft::update_host(v_ci.data(), indices, v_ci.size(), handle.get_stream()); + raft::update_host(v_vals.data(), values, v_vals.size(), handle.get_stream()); + + std::vector v_start{2}; + vector_test_t d_v_start(v_start.size(), handle.get_stream()); + raft::update_device(d_v_start.data(), v_start.data(), d_v_start.size(), handle.get_stream()); + + index_t num_paths = v_start.size(); + index_t max_depth = 5; + + // 0-copy const device view: + // + detail::device_const_vector_view d_start_view{d_v_start.data(), num_paths}; + auto quad = detail::random_walks_impl(handle, graph_view, d_start_view, max_depth); + + auto& d_coalesced_v = std::get<0>(quad); + auto& d_coalesced_w = std::get<1>(quad); + auto& d_sizes = std::get<2>(quad); + auto seed0 = std::get<3>(quad); + + bool test_all_paths = + cugraph::test::host_check_rw_paths(handle, graph_view, d_coalesced_v, d_coalesced_w, d_sizes); + + if (!test_all_paths) std::cout << "starting seed on failure: " << seed0 << '\n'; + + ASSERT_TRUE(test_all_paths); +} + +TEST(RandomWalksUtility, PathsToCOO) +{ + using namespace cugraph::experimental::detail; + + using vertex_t = int32_t; + using edge_t = vertex_t; + using weight_t = float; + using index_t = vertex_t; + + raft::handle_t handle{}; + + std::vector v_sizes{2, 1, 3, 5, 1}; + std::vector v_coalesced{5, 3, 4, 9, 0, 1, 6, 2, 7, 3, 2, 5}; + std::vector w_coalesced{0.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1}; + + auto num_paths = v_sizes.size(); + auto total_sz = v_coalesced.size(); + auto num_edges = w_coalesced.size(); + + ASSERT_TRUE(num_edges == total_sz - num_paths); + + vector_test_t d_coalesced_v(total_sz, handle.get_stream()); + vector_test_t d_sizes(num_paths, handle.get_stream()); + + raft::update_device( + d_coalesced_v.data(), v_coalesced.data(), d_coalesced_v.size(), handle.get_stream()); + raft::update_device(d_sizes.data(), v_sizes.data(), d_sizes.size(), handle.get_stream()); + + index_t coalesced_v_sz = d_coalesced_v.size(); + + auto tpl_coo_offsets = convert_paths_to_coo(handle, + coalesced_v_sz, + static_cast(num_paths), + d_coalesced_v.release(), + d_sizes.release()); + + auto&& d_src = std::move(std::get<0>(tpl_coo_offsets)); + auto&& d_dst = std::move(std::get<1>(tpl_coo_offsets)); + auto&& d_offsets = std::move(std::get<2>(tpl_coo_offsets)); + + ASSERT_TRUE(d_src.size() == num_edges); + ASSERT_TRUE(d_dst.size() == num_edges); + + std::vector v_src(num_edges, 0); + std::vector v_dst(num_edges, 0); + std::vector v_offsets(d_offsets.size(), 0); + + raft::update_host(v_src.data(), raw_const_ptr(d_src), d_src.size(), handle.get_stream()); + raft::update_host(v_dst.data(), raw_const_ptr(d_dst), d_dst.size(), handle.get_stream()); + raft::update_host( + v_offsets.data(), raw_const_ptr(d_offsets), d_offsets.size(), handle.get_stream()); + + std::vector v_src_exp{5, 9, 0, 6, 2, 7, 3}; + std::vector v_dst_exp{3, 0, 1, 2, 7, 3, 2}; + std::vector v_offsets_exp{0, 1, 3}; + + EXPECT_EQ(v_src, v_src_exp); + EXPECT_EQ(v_dst, v_dst_exp); + EXPECT_EQ(v_offsets, v_offsets_exp); +} diff --git a/cpp/tests/traversal/bfs_test.cu b/cpp/tests/traversal/bfs_test.cu index d90da4367a0..9027d73b83e 100644 --- a/cpp/tests/traversal/bfs_test.cu +++ b/cpp/tests/traversal/bfs_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -224,13 +224,13 @@ TEST_P(Tests_BFS, CheckInt64_SP_COUNTER) run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P(simple_test, - Tests_BFS, - ::testing::Values(BFS_Usecase("test/datasets/karate.mtx", 0), - BFS_Usecase("test/datasets/polbooks.mtx", 0), - BFS_Usecase("test/datasets/netscience.mtx", 0), - BFS_Usecase("test/datasets/netscience.mtx", 100), - BFS_Usecase("test/datasets/wiki2003.mtx", 1000), - BFS_Usecase("test/datasets/wiki-Talk.mtx", 1000))); +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_BFS, + ::testing::Values(BFS_Usecase("test/datasets/karate.mtx", 0), + BFS_Usecase("test/datasets/polbooks.mtx", 0), + BFS_Usecase("test/datasets/netscience.mtx", 0), + BFS_Usecase("test/datasets/netscience.mtx", 100), + BFS_Usecase("test/datasets/wiki2003.mtx", 1000), + BFS_Usecase("test/datasets/wiki-Talk.mtx", 1000))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/traversal/sssp_test.cu b/cpp/tests/traversal/sssp_test.cu index ea56d1d79cb..e151ab64e68 100644 --- a/cpp/tests/traversal/sssp_test.cu +++ b/cpp/tests/traversal/sssp_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved. + * Copyright (c) 2019-2021, NVIDIA CORPORATION. All rights reserved. * * NVIDIA CORPORATION and its licensors retain all intellectual property * and proprietary rights in and to this software, related documentation @@ -425,10 +425,10 @@ TEST_P(Tests_SSSP, CheckFP64_RANDOM_DIST_PREDS) // --gtest_filter=*simple_test* -INSTANTIATE_TEST_CASE_P(simple_test, - Tests_SSSP, - ::testing::Values(SSSP_Usecase(MTX, "test/datasets/dblp.mtx", 100), - SSSP_Usecase(MTX, "test/datasets/wiki2003.mtx", 100000), - SSSP_Usecase(MTX, "test/datasets/karate.mtx", 1))); +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_SSSP, + ::testing::Values(SSSP_Usecase(MTX, "test/datasets/dblp.mtx", 100), + SSSP_Usecase(MTX, "test/datasets/wiki2003.mtx", 100000), + SSSP_Usecase(MTX, "test/datasets/karate.mtx", 1))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/traversal/tsp_test.cu b/cpp/tests/traversal/tsp_test.cu index d4e9ff90f35..47a72757bd8 100644 --- a/cpp/tests/traversal/tsp_test.cu +++ b/cpp/tests/traversal/tsp_test.cu @@ -242,5 +242,5 @@ class Tests_Tsp : public ::testing::TestWithParam { TEST_P(Tests_Tsp, CheckFP32_T) { run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P(simple_test, Tests_Tsp, ::testing::ValuesIn(euc_2d)); +INSTANTIATE_TEST_SUITE_P(simple_test, Tests_Tsp, ::testing::ValuesIn(euc_2d)); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/tree/mst_test.cu b/cpp/tests/tree/mst_test.cu index 949d6bae59b..e3d7b70d51e 100644 --- a/cpp/tests/tree/mst_test.cu +++ b/cpp/tests/tree/mst_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -144,8 +144,8 @@ TEST_P(Tests_Mst, CheckFP32_T) { run_current_test(GetParam()); } TEST_P(Tests_Mst, CheckFP64_T) { run_current_test(GetParam()); } -INSTANTIATE_TEST_CASE_P(simple_test, - Tests_Mst, - ::testing::Values(Mst_Usecase("test/datasets/netscience.mtx"))); +INSTANTIATE_TEST_SUITE_P(simple_test, + Tests_Mst, + ::testing::Values(Mst_Usecase("test/datasets/netscience.mtx"))); CUGRAPH_TEST_PROGRAM_MAIN() diff --git a/cpp/tests/utilities/base_fixture.hpp b/cpp/tests/utilities/base_fixture.hpp index 79a86e1fc95..770fbc99397 100644 --- a/cpp/tests/utilities/base_fixture.hpp +++ b/cpp/tests/utilities/base_fixture.hpp @@ -95,7 +95,7 @@ inline std::shared_ptr create_memory_resource( if (allocation_mode == "binning") return make_binning(); if (allocation_mode == "cuda") return make_cuda(); if (allocation_mode == "pool") return make_pool(); - if (allocation_mode == "managed") make_managed(); + if (allocation_mode == "managed") return make_managed(); CUGRAPH_FAIL("Invalid RMM allocation mode"); } diff --git a/cpp/tests/utilities/test_utilities.hpp b/cpp/tests/utilities/test_utilities.hpp index e81a76b4163..196128e37c0 100644 --- a/cpp/tests/utilities/test_utilities.hpp +++ b/cpp/tests/utilities/test_utilities.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -167,21 +168,120 @@ generate_graph_from_rmat_params(raft::handle_t const& handle, std::vector const& partition_ids, size_t num_partitions); -struct rmat_params_t { - size_t scale{}; - size_t edge_factor{}; - double a{}; - double b{}; - double c{}; - uint64_t seed{}; - bool undirected{}; - bool scramble_vertex_ids{}; +class File_Usecase { + public: + File_Usecase() = delete; + + File_Usecase(std::string const& graph_file_path) + { + if ((graph_file_path.length() > 0) && (graph_file_path[0] != '/')) { + graph_file_full_path_ = cugraph::test::get_rapids_dataset_root_dir() + "/" + graph_file_path; + } else { + graph_file_full_path_ = graph_file_path; + } + } + + template + std::tuple< + cugraph::experimental::graph_t, + rmm::device_uvector> + construct_graph(raft::handle_t const& handle, bool test_weighted, bool renumber = true) const + { + return read_graph_from_matrix_market_file( + handle, graph_file_full_path_, test_weighted, renumber); + } + + private: + std::string graph_file_full_path_{}; }; -struct input_graph_specifier_t { - enum { MATRIX_MARKET_FILE_PATH, RMAT_PARAMS } tag{}; - std::string graph_file_full_path{}; - rmat_params_t rmat_params{}; +class Rmat_Usecase { + public: + Rmat_Usecase() = delete; + + Rmat_Usecase(size_t scale, + size_t edge_factor, + double a, + double b, + double c, + uint64_t seed, + bool undirected, + bool scramble_vertex_ids, + bool multi_gpu_usecase = false) + : scale_(scale), + edge_factor_(edge_factor), + a_(a), + b_(b), + c_(c), + seed_(seed), + undirected_(undirected), + scramble_vertex_ids_(scramble_vertex_ids), + multi_gpu_usecase_(multi_gpu_usecase) + { + } + + template + std::tuple< + cugraph::experimental::graph_t, + rmm::device_uvector> + construct_graph(raft::handle_t const& handle, bool test_weighted, bool renumber = true) const + { + std::vector partition_ids(1); + size_t comm_size; + + if (multi_gpu_usecase_) { + auto& comm = handle.get_comms(); + comm_size = comm.get_size(); + auto const comm_rank = comm.get_rank(); + + partition_ids.resize(multi_gpu ? size_t{1} : static_cast(comm_size)); + + std::iota(partition_ids.begin(), + partition_ids.end(), + multi_gpu ? static_cast(comm_rank) : size_t{0}); + } else { + comm_size = 1; + partition_ids[0] = size_t{0}; + } + + return generate_graph_from_rmat_params( + handle, + scale_, + edge_factor_, + a_, + b_, + c_, + seed_, + undirected_, + scramble_vertex_ids_, + test_weighted, + renumber, + partition_ids, + comm_size); + } + + private: + size_t scale_{}; + size_t edge_factor_{}; + double a_{}; + double b_{}; + double c_{}; + uint64_t seed_{}; + bool undirected_{}; + bool scramble_vertex_ids_{}; + bool multi_gpu_usecase_{}; }; } // namespace test diff --git a/docs/Makefile b/docs/cugraph/Makefile similarity index 100% rename from docs/Makefile rename to docs/cugraph/Makefile diff --git a/docs/README.md b/docs/cugraph/README.md similarity index 100% rename from docs/README.md rename to docs/cugraph/README.md diff --git a/docs/make.bat b/docs/cugraph/make.bat similarity index 100% rename from docs/make.bat rename to docs/cugraph/make.bat diff --git a/docs/requirement.txt b/docs/cugraph/requirement.txt similarity index 100% rename from docs/requirement.txt rename to docs/cugraph/requirement.txt diff --git a/docs/source/_static/EMPTY b/docs/cugraph/source/_static/EMPTY similarity index 100% rename from docs/source/_static/EMPTY rename to docs/cugraph/source/_static/EMPTY diff --git a/docs/source/_static/copybutton.css b/docs/cugraph/source/_static/copybutton.css similarity index 100% rename from docs/source/_static/copybutton.css rename to docs/cugraph/source/_static/copybutton.css diff --git a/docs/cugraph/source/_static/copybutton_pydocs.js b/docs/cugraph/source/_static/copybutton_pydocs.js new file mode 100644 index 00000000000..cec05777e6b --- /dev/null +++ b/docs/cugraph/source/_static/copybutton_pydocs.js @@ -0,0 +1,65 @@ +$(document).ready(function() { + /* Add a [>>>] button on the top-right corner of code samples to hide + * the >>> and ... prompts and the output and thus make the code + * copyable. */ + var div = $('.highlight-python .highlight,' + + '.highlight-python3 .highlight,' + + '.highlight-pycon .highlight,' + + '.highlight-default .highlight'); + var pre = div.find('pre'); + + // get the styles from the current theme + pre.parent().parent().css('position', 'relative'); + var hide_text = 'Hide the prompts and output'; + var show_text = 'Show the prompts and output'; + var border_width = pre.css('border-top-width'); + var border_style = pre.css('border-top-style'); + var border_color = pre.css('border-top-color'); + var button_styles = { + 'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0', + 'border-color': border_color, 'border-style': border_style, + 'border-width': border_width, 'text-size': '75%', + 'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '1.5em', + 'border-radius': '0 3px 0 0', + 'transition': "0.5s" + } + + // create and add the button to all the code blocks that contain >>> + div.each(function(index) { + var jthis = $(this); + if (jthis.find('.gp').length > 0) { + var button = $('>>>'); + button.css(button_styles) + button.attr('title', hide_text); + button.data('hidden', 'false'); + jthis.prepend(button); + } + // tracebacks (.gt) contain bare text elements that need to be + // wrapped in a span to work with .nextUntil() (see later) + jthis.find('pre:has(.gt)').contents().filter(function() { + return ((this.nodeType == 3) && (this.data.trim().length > 0)); + }).wrap(''); + }); + + // define the behavior of the button when it's clicked + $('.copybutton').click(function(e){ + e.preventDefault(); + var button = $(this); + if (button.data('hidden') === 'false') { + // hide the code output + button.parent().find('.go, .gp, .gt').hide(); + button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden'); + button.css('text-decoration', 'line-through'); + button.attr('title', show_text); + button.data('hidden', 'true'); + } else { + // show the code output + button.parent().find('.go, .gp, .gt').show(); + button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible'); + button.css('text-decoration', 'none'); + button.attr('title', hide_text); + button.data('hidden', 'false'); + } + }); +}); + diff --git a/docs/source/_static/example_mod.js b/docs/cugraph/source/_static/example_mod.js similarity index 100% rename from docs/source/_static/example_mod.js rename to docs/cugraph/source/_static/example_mod.js diff --git a/docs/source/_static/params.css b/docs/cugraph/source/_static/params.css similarity index 100% rename from docs/source/_static/params.css rename to docs/cugraph/source/_static/params.css diff --git a/docs/source/_static/references.css b/docs/cugraph/source/_static/references.css similarity index 100% rename from docs/source/_static/references.css rename to docs/cugraph/source/_static/references.css diff --git a/docs/source/api.rst b/docs/cugraph/source/api.rst similarity index 100% rename from docs/source/api.rst rename to docs/cugraph/source/api.rst diff --git a/docs/source/conf.py b/docs/cugraph/source/conf.py similarity index 98% rename from docs/source/conf.py rename to docs/cugraph/source/conf.py index 5e87622bd09..a4633d04f8d 100644 --- a/docs/source/conf.py +++ b/docs/cugraph/source/conf.py @@ -42,17 +42,17 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'numpydoc', - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', + "sphinx.ext.intersphinx", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "numpydoc", + "sphinx_markdown_tables", 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', 'sphinx.ext.linkcode', "IPython.sphinxext.ipython_console_highlighting", "IPython.sphinxext.ipython_directive", "nbsphinx", "recommonmark", - "sphinx_markdown_tables", ] diff --git a/docs/source/cugraph_blogs.rst b/docs/cugraph/source/cugraph_blogs.rst similarity index 100% rename from docs/source/cugraph_blogs.rst rename to docs/cugraph/source/cugraph_blogs.rst diff --git a/docs/source/cugraph_intro.md b/docs/cugraph/source/cugraph_intro.md similarity index 100% rename from docs/source/cugraph_intro.md rename to docs/cugraph/source/cugraph_intro.md diff --git a/docs/source/cugraph_ref.rst b/docs/cugraph/source/cugraph_ref.rst similarity index 61% rename from docs/source/cugraph_ref.rst rename to docs/cugraph/source/cugraph_ref.rst index 591619fb338..e0f113eaba4 100644 --- a/docs/source/cugraph_ref.rst +++ b/docs/cugraph/source/cugraph_ref.rst @@ -2,22 +2,35 @@ References ########## +************ +Architecture +************ + +2-D Data Partitioning + +- Kang, S., Fender, A., Eaton, J., & Rees, B. (2020, September) *Computing PageRank Scores of Web Crawl Data Using DGX A100 Clusters*. In 2020 IEEE High Performance Extreme Computing Conference (HPEC) (pp. 1-4). IEEE. + + +| + +| + ********** Algorithms ********** Betweenness Centrality -- Brandes, U. (2001). A faster algorithm for betweenness centrality. Journal of mathematical sociology, 25(2), 163-177. -- Brandes, U. (2008). On variants of shortest-path betweenness centrality and their generic computation. Social Networks, 30(2), 136-145. -- McLaughlin, A., & Bader, D. A. (2018). Accelerating GPU betweenness centrality. Communications of the ACM, 61(8), 85-92. +- Brandes, U. (2001). *A faster algorithm for betweenness centrality*. Journal of mathematical sociology, 25(2), 163-177. +- Brandes, U. (2008). *On variants of shortest-path betweenness centrality and their generic computation*. Social Networks, 30(2), 136-145. +- McLaughlin, A., & Bader, D. A. (2018). *Accelerating GPU betweenness centrality*. Communications of the ACM, 61(8), 85-92. Katz - J. Cohen, *Trusses: Cohesive subgraphs for social network analysis* National security agency technical report, 2008 - O. Green, J. Fox, E. Kim, F. Busato, et al. *Quickly Finding a Truss in a Haystack* IEEE High Performance Extreme Computing Conference (HPEC), 2017 https://doi.org/10.1109/HPEC.2017.8091038 -- O. Green, P. Yalamanchili, L.M. Munguia, “*ast Triangle Counting on GPU* Irregular Applications: Architectures and Algorithms (IA3), 2014 +- O. Green, P. Yalamanchili, L.M. Munguia, *Fast Triangle Counting on GPU* Irregular Applications: Architectures and Algorithms (IA3), 2014 Hungarian Algorithm @@ -27,6 +40,15 @@ Hungarian Algorithm | +************* +Other Papers +************* +- Hricik, T., Bader, D., & Green, O. (2020, September). *Using RAPIDS AI to Accelerate Graph Data Science Workflows*. In 2020 IEEE High Performance Extreme Computing Conference (HPEC) (pp. 1-4). IEEE. + +| + +| + ********** Data Sets ********** diff --git a/docs/source/dask-cugraph.rst b/docs/cugraph/source/dask-cugraph.rst similarity index 100% rename from docs/source/dask-cugraph.rst rename to docs/cugraph/source/dask-cugraph.rst diff --git a/docs/source/images/Nx_Cg_1.png b/docs/cugraph/source/images/Nx_Cg_1.png similarity index 100% rename from docs/source/images/Nx_Cg_1.png rename to docs/cugraph/source/images/Nx_Cg_1.png diff --git a/docs/source/images/Nx_Cg_2.png b/docs/cugraph/source/images/Nx_Cg_2.png similarity index 100% rename from docs/source/images/Nx_Cg_2.png rename to docs/cugraph/source/images/Nx_Cg_2.png diff --git a/docs/source/index.rst b/docs/cugraph/source/index.rst similarity index 100% rename from docs/source/index.rst rename to docs/cugraph/source/index.rst diff --git a/docs/source/nx_transition.rst b/docs/cugraph/source/nx_transition.rst similarity index 100% rename from docs/source/nx_transition.rst rename to docs/cugraph/source/nx_transition.rst diff --git a/docs/source/sphinxext/github_link.py b/docs/cugraph/source/sphinxext/github_link.py similarity index 88% rename from docs/source/sphinxext/github_link.py rename to docs/cugraph/source/sphinxext/github_link.py index a7a46fdd9df..fa8fe3f5fe3 100644 --- a/docs/source/sphinxext/github_link.py +++ b/docs/cugraph/source/sphinxext/github_link.py @@ -1,3 +1,17 @@ +# Copyright (c) 2019-2021, NVIDIA CORPORATION. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# NOTE: # This contains code with copyright by the scikit-learn project, subject to the # license in /thirdparty/LICENSES/LICENSE.scikit_learn diff --git a/notebooks/README.md b/notebooks/README.md index a5706720235..3769ceb6957 100644 --- a/notebooks/README.md +++ b/notebooks/README.md @@ -10,6 +10,7 @@ This repository contains a collection of Jupyter Notebooks that outline how to r | Folder | Notebook | Description | | --------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | Centrality | | | +| | [Centrality](centrality/Centrality.ipynb) | Compute and compare multiple centrality scores | | | [Katz](centrality/Katz.ipynb) | Compute the Katz centrality for every vertex | | | [Betweenness](centrality/Betweenness.ipynb) | Compute both Edge and Vertex Betweenness centrality | | Community | | | @@ -33,6 +34,8 @@ This repository contains a collection of Jupyter Notebooks that outline how to r | Traversal | | | | | [BFS](traversal/BFS.ipynb) | Compute the Breadth First Search path from a starting vertex to every other vertex in a graph | | | [SSSP](traversal/SSSP.ipynb) | Single Source Shortest Path - compute the shortest path from a starting vertex to every other vertex | +| Sampling | +| | [Random Walk](sampling/RandomWalk.ipynb) | Compute Random Walk for a various number of seeds and path lengths | | Structure | | | | | [Renumbering](structure/Renumber.ipynb)
[Renumbering 2](structure/Renumber-2.ipynb) | Renumber the vertex IDs in a graph (two sample notebooks) | | | [Symmetrize](structure/Symmetrize.ipynb) | Symmetrize the edges in a graph | @@ -49,22 +52,21 @@ Running the example in these notebooks requires: * Download via Docker, Conda (See [__Getting Started__](https://rapids.ai/start.html)) * cuGraph is dependent on the latest version of cuDF. Please install all components of RAPIDS -* Python 3.6+ +* Python 3.7+ * A system with an NVIDIA GPU: Pascal architecture or better -* CUDA 9.2+ -* NVIDIA driver 396.44+ +* CUDA 11.0+ +* NVIDIA driver 450.51+ #### Notebook Credits - Original Authors: Bradley Rees -- Last Edit: 04/24/2020 +- Last Edit: 04/19/2021 -RAPIDS Versions: 0.14 +RAPIDS Versions: 0.19 Test Hardware - - GV100 32G, CUDA 9,2 diff --git a/notebooks/centrality/Centrality.ipynb b/notebooks/centrality/Centrality.ipynb new file mode 100644 index 00000000000..591c27419ba --- /dev/null +++ b/notebooks/centrality/Centrality.ipynb @@ -0,0 +1,443 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Centrality" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this notebook, we will compute vertex centrality scores using the various cuGraph algorithms. We will then compare the similarities and differences.\n", + "\n", + "| Author Credit | Date | Update | cuGraph Version | Test Hardware |\n", + "| --------------|------------|--------------|-----------------|----------------|\n", + "| Brad Rees | 04/16/2021 | created | 0.19 | GV100, CUDA 11.0\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Centrality is measure of how important, or central, a node or edge is within a graph. It is useful for identifying influencer in social networks, key routing nodes in communication/computer network infrastructures, \n", + "\n", + "The seminal paper on centrality is: Freeman, L. C. (1978). Centrality in social networks conceptual clarification. Social networks, 1(3), 215-239.\n", + "\n", + "\n", + "__Degree centrality – done but need new API__
\n", + "Degree centrality is based on the notion that whoever has the most connects must be important. \n", + "\n", + "
\n", + " Cd(v) = degree(v)\n", + "
\n", + "\n", + "cuGraph currently does not have a Degree Centrality function call. However, since Degree Centrality is just the degree of a node, we can use _G.degree()_ function.\n", + "Degree Centrality for a Directed graph can be further divided in _indegree centrality_ and _outdegree centrality_ and can be obtained using _G.degrees()_\n", + "\n", + "\n", + "__Closeness centrality – coming soon__
\n", + "Closeness is a measure of the shortest path to every other node in the graph. A node that is close to every other node, can reach over other node in the fewest number of hops, means that it has greater influence on the network versus a node that is not close.\n", + "\n", + "__Betweenness Centrality__
\n", + "Betweenness is a measure of the number of shortest paths that cross through a node, or over an edge. A node with high betweenness means that it had a greater influence on the flow of information. \n", + "\n", + "Betweenness centrality of a node 𝑣 is the sum of the fraction of all-pairs shortest paths that pass through 𝑣\n", + "\n", + "
\n", + " \n", + "
\n", + "\n", + "To speedup runtime of betweenness centrailty, the metric can be computed on a limited number of nodes (randomly selected) and then used to estimate the other scores. For this example, the graphs are relatively smalled (under 5,000 nodes) so betweenness on every node will be computed.\n", + "\n", + "__Eigenvector Centrality - coming soon__
\n", + "Eigenvectors can be thought of as the balancing points of a graph, or center of gravity of a 3D object. High centrality means that more of the graph is balanced around that node.\n", + "\n", + "__Katz Centrality__
\n", + "Katz is a variant of degree centrality and of eigenvector centrality. \n", + "Katz centrality is a measure of the relative importance of a node within the graph based on measuring the influence across the total number of walks between vertex pairs. \n", + "\n", + "
\n", + " \n", + "
\n", + "\n", + "See:\n", + "* [Katz on Wikipedia](https://en.wikipedia.org/wiki/Katz_centrality) for more details on the algorithm.\n", + "* https://www.sci.unich.it/~francesc/teaching/network/katz.html\n", + "\n", + "__PageRank Centrality__
\n", + "PageRank is classified as both a Link Analysis tool and a centrality measure. PageRank is based on the assumption that important nodes point (directed edge) to other important nodes. From a social network perspective, the question is who do you seek for an answer and then who does that person seek. PageRank is good when there is implied importance in the data, for example a citation network, web page linkages, or trust networks. \n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Test Data\n", + "We will be using the Zachary Karate club dataset \n", + "*W. W. Zachary, An information flow model for conflict and fission in small groups, Journal of\n", + "Anthropological Research 33, 452-473 (1977).*\n", + "\n", + "\n", + "![Karate Club](../img/zachary_black_lines.png)\n", + "\n", + "\n", + "Because the test data has vertex IDs starting at 1, the auto-renumber feature of cuGraph (mentioned above) will be used so the starting vertex ID is zero for maximum efficiency. The resulting data will then be auto-unrenumbered, making the entire renumbering process transparent to users." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the modules\n", + "import cugraph\n", + "import cudf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd \n", + "from IPython.display import display_html " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Functions\n", + "using underscore variable names to avoid collisions. \n", + "non-underscore names are expected to be global names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute Centrality\n", + "# the centrality calls are very straight forward with the graph being the primary argument\n", + "# we are using the default argument values for all centrality functions\n", + "\n", + "def compute_centrality(_graph) :\n", + " # Compute Degree Centrality\n", + " _d = _graph.degree()\n", + " \n", + " # Compute the Betweenness Centrality\n", + " _b = cugraph.betweenness_centrality(_graph)\n", + "\n", + " # Compute Katz Centrality\n", + " _k = cugraph.katz_centrality(_graph)\n", + " \n", + " # Compute PageRank Centrality\n", + " _p = cugraph.pagerank(_graph)\n", + " \n", + " return _d, _b, _k, _p" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Print function\n", + "# being lazy and requiring that the dataframe names are not changed versus passing them in\n", + "def print_centrality(_n):\n", + " dc_top = dc.sort_values(by='degree', ascending=False).head(_n).to_pandas()\n", + " bc_top = bc.sort_values(by='betweenness_centrality', ascending=False).head(_n).to_pandas()\n", + " katz_top = katz.sort_values(by='katz_centrality', ascending=False).head(_n).to_pandas()\n", + " pr_top = pr.sort_values(by='pagerank', ascending=False).head(_n).to_pandas()\n", + " \n", + " df1_styler = dc_top.style.set_table_attributes(\"style='display:inline'\").set_caption('Degree').hide_index()\n", + " df2_styler = bc_top.style.set_table_attributes(\"style='display:inline'\").set_caption('Betweenness').hide_index()\n", + " df3_styler = katz_top.style.set_table_attributes(\"style='display:inline'\").set_caption('Katz').hide_index()\n", + " df4_styler = pr_top.style.set_table_attributes(\"style='display:inline'\").set_caption('PageRank').hide_index()\n", + "\n", + " display_html(df1_styler._repr_html_()+df2_styler._repr_html_()+df3_styler._repr_html_()+df4_styler._repr_html_(), raw=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Read the data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define the path to the test data \n", + "datafile='../data/karate-data.csv'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "cuGraph does not do any data reading or writing and is dependent on other tools for that, with cuDF being the preferred solution. \n", + "\n", + "The data file contains an edge list, which represents the connection of a vertex to another. The `source` to `destination` pairs is in what is known as Coordinate Format (COO). In this test case, the data is just two columns. However a third, `weight`, column is also possible" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gdf = cudf.read_csv(datafile, delimiter='\\t', names=['src', 'dst'], dtype=['int32', 'int32'] )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "it was that easy to load data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# create a Graph using the source (src) and destination (dst) vertex pairs from the Dataframe \n", + "G = cugraph.Graph()\n", + "G.from_cudf_edgelist(gdf, source='src', destination='dst')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compute Centrality" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dc, bc, katz, pr = compute_centrality(G)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Results\n", + "Typically, analyst look just at the top 10% of results. Basically just those vertices that are the most central or important. \n", + "The karate data has 32 vertices, so let's round a little and look at the top 5 vertices" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print_centrality(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### A Different Dataset\n", + "The Karate dataset is not that large or complex, which makes it a perfect test dataset since it is easy to visually verify results. Let's look at a larger dataset with a lot more edges" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define the path to the test data \n", + "datafile='../data/netscience.csv'\n", + "\n", + "gdf = cudf.read_csv(datafile, delimiter=' ', names=['src', 'dst', 'wt'], dtype=['int32', 'int32', 'float'] )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# create a Graph using the source (src) and destination (dst) vertex pairs from the Dataframe \n", + "G = cugraph.Graph()\n", + "G.from_cudf_edgelist(gdf, source='src', destination='dst')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "(G.number_of_nodes(), G.number_of_edges())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dc, bc, katz, pr = compute_centrality(G)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print_centrality(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now see a larger discrepancy between the centrality scores and which nodes rank highest.\n", + "Which centrality measure to use is left to the analyst to decide and does require insight into the difference algorithms and graph structure." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### And One More Dataset\n", + "Let's look at a Cyber dataset. The vertex ID are IP addresses" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define the path to the test data \n", + "datafile='../data/cyber.csv'\n", + "\n", + "gdf = cudf.read_csv(datafile, delimiter=',', names=['idx', 'src', 'dst'], dtype=['int32', 'str', 'str'] )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# create a Graph using the source (src) and destination (dst) vertex pairs from the Dataframe \n", + "G = cugraph.Graph()\n", + "G.from_cudf_edgelist(gdf, source='src', destination='dst')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "(G.number_of_nodes(), G.number_of_edges())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dc, bc, katz, pr = compute_centrality(G)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print_centrality(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are differences in how each centrality measure ranks the nodes. In some cases, every algorithm returns similar results, and in others, the results are different. Understanding how the centrality measure is computed and what edge represent is key to selecting the right centrality metric." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----\n", + "Copyright (c) 2019-2021, NVIDIA CORPORATION.\n", + "\n", + "Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n", + "\n", + "Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cugraph_dev", + "language": "python", + "name": "cugraph_dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/cugraph_benchmarks/random_walk_benchmark.ipynb b/notebooks/cugraph_benchmarks/random_walk_benchmark.ipynb new file mode 100644 index 00000000000..be50c075455 --- /dev/null +++ b/notebooks/cugraph_benchmarks/random_walk_benchmark.ipynb @@ -0,0 +1,544 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Random Walk Performance\n", + "# Skip notebook test¶ \n", + "\n", + "Random walk performance is governed by the length of the paths to find, the number of seeds, and the size or structure of the graph.\n", + "This benchmark will use several test graphs of increasingly larger sizes. While not even multiples in scale, the four test graphs should give an indication of how well Random Walk performs as data size increases. \n", + "\n", + "### Test Data\n", + "Users must run the _dataPrep.sh_ script before running this notebook so that the test files are downloaded\n", + "\n", + "| File Name | Num of Vertices | Num of Edges |\n", + "| ---------------------- | --------------: | -----------: |\n", + "| preferentialAttachment | 100,000 | 999,970 |\n", + "| dblp-2010 | 326,186 | 1,615,400 |\n", + "| coPapersCiteseer | 434,102 | 32,073,440 |\n", + "| as-Skitter | 1,696,415 | 22,190,596 |" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the modules\n", + "import cugraph\n", + "import cudf" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# system and other\n", + "import gc\n", + "import os\n", + "import time\n", + "import random\n", + "\n", + "# MTX file reader\n", + "from scipy.io import mmread" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "try: \n", + " import matplotlib\n", + "except ModuleNotFoundError:\n", + " os.system('pip install matplotlib')\n", + "\n", + "import matplotlib.pyplot as plt; plt.rcdefaults()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Test File\n", + "data = {\n", + " 'preferentialAttachment' : './data/preferentialAttachment.mtx',\n", + " 'dblp' : './data/dblp-2010.mtx',\n", + " 'coPapersCiteseer' : './data/coPapersCiteseer.mtx',\n", + " 'as-Skitter' : './data/as-Skitter.mtx'\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Read the data and create a graph" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# Data reader - the file format is MTX, so we will use the reader from SciPy\n", + "def read_and_create(datafile):\n", + " print('Reading ' + str(datafile) + '...')\n", + " M = mmread(datafile).asfptype()\n", + "\n", + " _gdf = cudf.DataFrame()\n", + " _gdf['src'] = M.row\n", + " _gdf['dst'] = M.col\n", + " _gdf['wt'] = 1.0\n", + " \n", + " _g = cugraph.Graph()\n", + " _g.from_cudf_edgelist(_gdf, source='src', destination='dst', edge_attr='wt', renumber=False)\n", + " \n", + " print(\"\\t{:,} nodes, {:,} edges\".format(_g.number_of_nodes(), _g.number_of_edges() ))\n", + " \n", + " return _g" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define the call to RandomWalk\n", + "We are only interested in the runtime, so throw away the results" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def run_rw(_G, _seeds, _depth):\n", + " t1 = time.time()\n", + " _, _ = cugraph.random_walks(_G, _seeds, _depth)\n", + " t2 = time.time() - t1\n", + " return t2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test 1: Runtime versus path depth" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reading ./data/preferentialAttachment.mtx...\n", + "\t100,000 nodes, 499,985 edges\n", + "update i\n", + "Reading ./data/dblp-2010.mtx...\n", + "\t326,183 nodes, 807,700 edges\n", + "update i\n", + "Reading ./data/coPapersCiteseer.mtx...\n", + "\t434,102 nodes, 16,036,720 edges\n", + "update i\n", + "Reading ./data/as-Skitter.mtx...\n", + "\t1,696,415 nodes, 11,095,298 edges\n", + "update i\n" + ] + } + ], + "source": [ + "# some parameters\n", + "max_depth = 6\n", + "num_seeds = 500\n", + "\n", + "# arrays to capture performance gains\n", + "names = []\n", + "\n", + "# Two dimension data\n", + "time_algo_cu = [] # will be two dimensional\n", + "\n", + "i = 0\n", + "for k,v in data.items():\n", + " time_algo_cu.append([])\n", + " \n", + " # Saved the file Name\n", + " names.append(k)\n", + "\n", + " # read data\n", + " G = read_and_create(v)\n", + " \n", + " num_nodes = G.number_of_nodes()\n", + " nodes = G.nodes().to_array().tolist()\n", + "\n", + " seeds = random.sample(nodes, num_seeds)\n", + "\n", + " for j in range (2, max_depth+1) :\n", + " t = run_rw(G, seeds, j)\n", + " time_algo_cu[i].append(t)\n", + "\n", + " # update i\n", + " i = i + 1\n", + " print(\"update i\")\n", + " \n", + " del G\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "list" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(nodes)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "seed_idx = list(range(2,max_depth +1))\n", + "\n", + "plt.figure(figsize=(10,5))\n", + "\n", + "for i in range(len(data)):\n", + " plt.plot(seed_idx, time_algo_cu[i], label = names[i])\n", + "\n", + "\n", + "plt.title(f'Runtime vs. Path Length ({num_seeds} Seeds)')\n", + "plt.xlabel('Path length')\n", + "plt.ylabel('Runtime')\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "12979" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "del time_algo_cu\n", + "gc.collect()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test 2: Runtime versus number of seeds\n", + "The number of seeds will be increased over a range in increments of 10. \n", + "The runtime will be the sum of runtime per increment. Increaing number of seeds by 1 would make for very long execution times " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reading ./data/preferentialAttachment.mtx...\n", + "\t100,000 nodes, 499,985 edges\n", + "\t.................................................................................................... \n", + "Reading ./data/dblp-2010.mtx...\n", + "\t326,183 nodes, 807,700 edges\n", + "\t.................................................................................................... \n", + "Reading ./data/coPapersCiteseer.mtx...\n", + "\t434,102 nodes, 16,036,720 edges\n", + "\t.................................................................................................... \n", + "Reading ./data/as-Skitter.mtx...\n", + "\t1,696,415 nodes, 11,095,298 edges\n", + "\t.................................................................................................... \n" + ] + } + ], + "source": [ + "# some parameters\n", + "rw_depth = 4\n", + "max_seeds = 1000\n", + "\n", + "# arrays to capture performance gains\n", + "names = []\n", + "\n", + "# Two dimension data\n", + "time_algo_cu = [] # will be two dimensional\n", + "\n", + "i = 0\n", + "for k,v in data.items():\n", + " time_algo_cu.append([])\n", + " \n", + " # Saved the file Name\n", + " names.append(k)\n", + "\n", + " # read data\n", + " G = read_and_create(v)\n", + " \n", + " num_nodes = G.number_of_nodes()\n", + " nodes = G.nodes().to_array().tolist()\n", + " \n", + " print('\\t', end='')\n", + " for j in range (10, max_seeds +1, 10) :\n", + " print('.', end='')\n", + " seeds = random.sample(nodes, j+1)\n", + " t = run_rw(G, seeds, rw_depth)\n", + " time_algo_cu[i].append(t)\n", + "\n", + " # update i\n", + " i = i + 1\n", + " print(\" \")\n", + " \n", + " del G\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "seed_idx = list(range (10, max_seeds +1, 10))\n", + "\n", + "plt.figure(figsize=(10,5))\n", + "\n", + "for i in range(len(data)):\n", + " plt.plot(seed_idx, time_algo_cu[i], label = names[i])\n", + "\n", + "\n", + "plt.title('Runtime vs. Number of Seeds')\n", + "plt.xlabel('Number of Seeds')\n", + "plt.ylabel('Runtime')\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4094" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "del time_algo_cu\n", + "gc.collect()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test 3: Multi-seed versus Sequential\n", + "This test uses a single files since sequential execution is slow" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reading ./data/coPapersCiteseer.mtx...\n", + "\t434,102 nodes, 16,036,720 edges\n" + ] + } + ], + "source": [ + "G = read_and_create('./data/coPapersCiteseer.mtx')\n", + "nodes = G.nodes().to_array().tolist()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "rw_depth = 4\n", + "max_seeds = 100\n", + "num_nodes = G.number_of_nodes()\n", + "runtime_seq = [0] * max_seeds" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "# sequenctial = so also get a single random seed\n", + "for i in range (max_seeds) :\n", + " for j in range(i):\n", + " seeds = random.sample(nodes, 1)\n", + " t = run_rw(G, seeds, rw_depth)\n", + " runtime_seq[i] = runtime_seq[i] + t" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "runtime = [None] * max_seeds\n", + "\n", + "for i in range (max_seeds) :\n", + " seeds = random.sample(nodes, i+1)\n", + " t = run_rw(G, seeds, rw_depth)\n", + " runtime[i] = t" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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AMaMGbPO49OmTWPcuHGsWrWK3r17c+SRR67z+H333UfLli2ZOHEixcXFDBw4kCFDhqzTZf2VV16hXbt2/Pe//wVg2bJllJaW8vvf/54XXniBtm3b8sQTT3DxxRdz//33c9ZZZ3HXXXfRuXNnxo8fz29/+1veeOMN/vCHP/Cb3/yG0047jdtvv71i/Y8++ihDhw7l4osvJpVKsXr16mq9dhGpZ74aD69eCPMnw/Y94cf/hI4Da7WErAWYmTUFEu6+Ivp9CPCXbG0vmzp16kSPHj0A6NatG4cccghmRo8ePX5wlrUljjnmGBo3bkzjxo056KCDmDBhAr169ap4fNSoUUybNo2nn34aCOH06aefrhNgPXr04E9/+hPnn38+w4YNY//992fGjBnMmDGDwYMHA+FMbvvtt2flypW89957HH/88RXLFxcXA/Duu+/yzDPPADB8+HDOP/98APr378+ZZ55JaWkpxx577Dr1iUgDsPgzGH05zHwRmm8Px94Je54IidrvUpHNM7BtgeeibtV5wKPu/sqWrHBjZ0rZ0qhRo4rfE4lExe1EIkFZWRkAeXl5pNPpiudtznehKndBr3zb3bn11lsZOnRolevo0qULU6ZM4aWXXuKSSy7hkEMO4Uc/+hHdunXj/fffX+e5y5cvp1WrVkydOrVa9QAccMABvP322/z3v//l9NNP549//COnnXZaNV+hiOSs1d/DW3+DifdCsgAGXQT7ng0FTWMrKWuR6e6fu3vP6Kebu1+drW3VBR07dmTKlCkATJkyhS+++OIHz2nevDkrVqyoch0vvPACRUVFLF68mDFjxtC/f/91Hh86dCh33nknpaWha+qsWbNYtWrVOs/5+uuvadKkCaeeeiojRoxgypQp7LbbbixcuLAiwEpLS/noo49o0aIFnTp14qmnngJCQH744YcADBw4kMcffxyARx55pGL9X375Jdtuuy2//OUv+cUvflHxmkWknipdA+/8A27uBRPuhl4nwTlTYND5sYYX5MBQUrniuOOO4+GHH6Zbt27svffedOnS5QfP2XPPPUkmk/Ts2ZPTTz+d3r17/+Dxgw46iEWLFnHppZfSrl27dZoof/GLXzBnzhz69OmDu9O2bdsfXH+bPn06I0aMIJFIkJ+fz5133klBQQFPP/0055xzDsuWLaOsrIxzzz2Xbt268cgjj/Cb3/yG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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "seed_idx = list(range(1,max_seeds +1))\n", + "\n", + "\n", + "plt.figure(figsize=(7,4))\n", + "plt.plot(seed_idx, runtime, label = \"multiple seeds\")\n", + "plt.plot(seed_idx, runtime_seq, label = \"sequential\")\n", + "\n", + "\n", + "plt.title('Runtime vs. Number of Seeds')\n", + "plt.xlabel('Number of Seeds')\n", + "plt.ylabel('Runtime')\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "-----\n", + "Copyright (c) 2021, NVIDIA CORPORATION.\n", + "\n", + "Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n", + "\n", + "Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cugraph_dev", + "language": "python", + "name": "cugraph_dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/cugraph_benchmarks/random_walk_perf.ipynb b/notebooks/cugraph_benchmarks/random_walk_perf.ipynb new file mode 100644 index 00000000000..738298767c5 --- /dev/null +++ b/notebooks/cugraph_benchmarks/random_walk_perf.ipynb @@ -0,0 +1,621 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Random Walk Performance\n", + "# Skip notebook test¶ \n", + "\n", + "Random walk performance is governed by the length of the paths to find, the number of seeds, and the size or structure of the graph.\n", + "This benchmark will use several test graphs of increasingly larger sizes. While not even multiples in scale, the four test graphs should give an indication of how well Random Walk performs as data size increases. \n", + "\n", + "### Test Data\n", + "Users must run the _dataPrep.sh_ script before running this notebook so that the test files are downloaded\n", + "\n", + "| File Name | Num of Vertices | Num of Edges |\n", + "| ---------------------- | --------------: | -----------: |\n", + "| preferentialAttachment | 100,000 | 999,970 |\n", + "| dblp-2010 | 326,186 | 1,615,400 |\n", + "| coPapersCiteseer | 434,102 | 32,073,440 |\n", + "| as-Skitter | 1,696,415 | 22,190,596 |" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the modules\n", + "import cugraph\n", + "import cudf" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# system and other\n", + "import gc\n", + "import os\n", + "import time\n", + "import random\n", + "\n", + "# MTX file reader\n", + "from scipy.io import mmread\n", + "\n", + "import networkx as nx" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "try: \n", + " import matplotlib\n", + "except ModuleNotFoundError:\n", + " os.system('pip install matplotlib')\n", + "\n", + "import matplotlib.pyplot as plt; plt.rcdefaults()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "try: \n", + " import pybind11\n", + "except ModuleNotFoundError:\n", + " os.system('pip install pybind11')\n", + " \n", + "import pybind11" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "try: \n", + " import walker\n", + "except ModuleNotFoundError:\n", + " os.system('pip install graph-walker')\n", + "\n", + "import walker" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# Test File\n", + "data = {\n", + " 'preferentialAttachment' : './data/preferentialAttachment.mtx',\n", + " 'dblp' : './data/dblp-2010.mtx',\n", + " 'coPapersCiteseer' : './data/coPapersCiteseer.mtx',\n", + " 'as-Skitter' : './data/as-Skitter.mtx'\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Read the data and create a graph" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Data reader - the file format is MTX, so we will use the reader from SciPy\n", + "def read_data(datafile):\n", + " print('Reading ' + str(datafile) + '...')\n", + " M = mmread(datafile).asfptype()\n", + "\n", + " _gdf = cudf.DataFrame()\n", + " _gdf['src'] = M.row\n", + " _gdf['dst'] = M.col\n", + " _gdf['wt'] = 1.0\n", + " \n", + " return _gdf" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def create_cu_ugraph(_df):\n", + " _g = cugraph.Graph()\n", + " _g.from_cudf_edgelist(_df, source='src', destination='dst', edge_attr='wt', renumber=False)\n", + " return _g" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def create_nx_ugraph(_df):\n", + " _gnx = nx.from_pandas_edgelist(_df, source='src', target='dst', edge_attr='wt', create_using=nx.Graph)\n", + " return _gnx" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define the call to RandomWalk\n", + "We are only interested in the runtime, so throw away the results" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def run_cu_rw(_G, _seeds, _depth):\n", + " t1 = time.time()\n", + " _, _ = cugraph.random_walks(_G, _seeds, _depth)\n", + " t2 = time.time() - t1\n", + " return t2" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "def run_wk_rw(_G, _seeds, _depth):\n", + " t1 = time.time()\n", + " _ = walker.random_walks(_G, n_walks=1, walk_len=_depth, start_nodes=_seeds)\n", + " t2 = time.time() - t1\n", + " return t2 \n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test 1: Runtime versus path depth" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reading ./data/preferentialAttachment.mtx...\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=2.23s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=2.48s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=2.02s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=2.31s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=2.01s\n", + "update i\n", + "Reading ./data/dblp-2010.mtx...\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=4.21s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=4.03s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=3.59s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=3.95s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=3.68s\n", + "update i\n", + "Reading ./data/coPapersCiteseer.mtx...\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=59.64s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=49.43s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=47.45s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=54.66s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=46.96s\n", + "update i\n", + "Reading ./data/as-Skitter.mtx...\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=53.14s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=44.36s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=46.38s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=41.96s\n", + "\tcuGraph\n", + "\tWalkerRandom walks - T=53.18s\n", + "update i\n" + ] + } + ], + "source": [ + "# some parameters\n", + "max_depth = 6\n", + "num_seeds = 100\n", + "\n", + "# arrays to capture performance gains\n", + "names = []\n", + "\n", + "# Two dimension data\n", + "time_algo_cu = [] # will be two dimensional\n", + "time_algo_wk = [] # will be two dimensional\n", + "\n", + "i = 0\n", + "for k,v in data.items():\n", + " time_algo_cu.append([])\n", + " time_algo_wk.append([])\n", + " \n", + " # Saved the file Name\n", + " names.append(k)\n", + "\n", + " # read data\n", + " gdf = read_data(v)\n", + " pdf = gdf.to_pandas()\n", + " \n", + " # Create the Graphs\n", + " Gcg = create_cu_ugraph(gdf)\n", + " Gnx = create_nx_ugraph(pdf)\n", + " \n", + " num_nodes = Gcg.number_of_nodes()\n", + " nodes = Gcg.nodes().to_array().tolist()\n", + "\n", + " seeds = random.sample(nodes, num_seeds)\n", + "\n", + " for j in range (2, max_depth+1) :\n", + " print(\"\\tcuGraph\")\n", + " tc = run_cu_rw(Gcg, seeds, j)\n", + " time_algo_cu[i].append(tc)\n", + " \n", + " print(\"\\tWalker\", end='')\n", + " tw = run_wk_rw(Gnx, seeds, j)\n", + " time_algo_wk[i].append(tw)\n", + "\n", + " # update i\n", + " i = i + 1\n", + " print(\"update i\")\n", + " \n", + " del Gcg\n", + " del Gnx\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAFNCAYAAADRi2EuAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Z1A+gAAAACXBIWXMAAAsTAAALEwEAmpwYAACN0ElEQVR4nOzdd3hUxf748ffsphcgQIBQEyAJ6QlJ6CACAopgQYoVrGDv2Lv+roUrKnq/qAjoVRABEfUiIgoSejO0BAgl9B5Ib7s7vz/O5pCQEIKShPJ5Pc8+u2fPzJw5Z9tn58yZUVprhBBCCCFE9bPUdgWEEEIIIS4XEngJIYQQQtQQCbyEEEIIIWqIBF5CCCGEEDVEAi8hhBBCiBoigZcQQgghRA2RwEuIi4hSqqVSKkcpZa3tutQ2pdQipdQ9tV2Pc6GUelUp9fU55pmmlLq+mqpU65RSI5VSS6qQ7t9Kqftrok5CVCcJvIT4h5RS6UqpfGdAdEgpNUUp5XMey+5Tsqy13qO19tFa289H+TXNeWyKnMcqQyn1m1KqXRXynXPAclr+nkqpfX83f21tUykVDcQAc5zLAUqpH5VSB5RSWikVeFp6d6XUJKVUlvO9+MRp63srpbYopfKUUguVUq0q2XY3pdQypVSm87VaqpRK/Cf78w+NBZ5XSrnVYh2E+Mck8BLi/BiotfYBYoE44Lnarc4F7V3nsWoOHAGm1G51LmijgG/0qZGuHcA8YPAZ0r8KBAOtgCuBMUqp/gBKqYbA98BLQH1gDTC9okKUUnWAn4HxzrTNgNeAwn+8R3+T1vogsAUYVFt1EOJ8kMBLiPNIa30I+BUjAKuw1aN0K5azJec7pdRXSqlspdRmpVSCc91/gZbAT84WojFKqUBnS4eLM80ipdSbzpaJHKXUT0qpBkqpb5ytHqtLt4oopdo5W5kylFJblVJDK9oPpdQwpdSa0557XCn1o/PxNUqpFGed9yulnvobxyoPmApEOsv8UCm111nvtUqp7s7n+wPPA8Oc+7i+VDGtnC0x2Uqp+c7g4pwopZoqpWYppY4qpXYppR4pte6Mr49zfXul1F/OdTOUUtOdr4c38AvQ1FnnHKVUU2c2tzOVV4GrgT9LHbPDWuv/AKvPkH4E8IbW+oTWOhX4HBjpXHcjsFlrPUNrXYARpMWcocUxxLm9aVpru9Y6X2s9X2u9odS+36WUSlVKnVBK/Vq69ayy95nz/fmj83VeBbQptU4ppcYppY44129USkWWqtciYEAlx0uIC54EXkKcR0qp5hg/ltvPIdsg4FugHvAj8DGA1vp2YA/O1jSt9btnyD8cuB2jVaINsByYjNFSkQq84qybN/AbRrDTyJnvP0qp8ArK/AkIVUoFl3ruFmdegC+AUVprX4zA6Y9z2F+c9fEBbgX+cj61GiNgre/czgyllIfWeh7w/4DpzuMQc1qd7nTujxtwTgGgUsqCsa/rMY5fb+AxpVS/UskqfH2cp7xmY7TY1QemATcAaK1zMd4HB5x19tFaH6isvArq5g0EAVuruC9+QIBzX0qsByKcjyNKr3PWcUep9aVtA+xKqS+VUlc7yy69reswguEbAX8gybn/VXmffQIUOOt6l/NWoi/QAyPwqwsMBY6XWp+KcepViIuWBF5CnB8/KKWygb0Yp89eOYe8S7TWc539tv7Luf+wTNZa79BaZ2K0suzQWi/QWtuAGRinPgGuBdK11pO11jat9V/ALGDI6QU6W6PmADcDOAOwdhiBAkAxEK6UquNsXVl3DvV9Sil1EiM49cHZIqO1/lprfdxZt38D7kBoFfZ9m9Y6H/gOZ0vjOUgE/LXWr2uti7TWOzFaiYaXSnOm16cT4AJ8pLUu1lp/D6yqwjar+nrXc95nV3FfSvoVZpZ6LhPwLbU+k7JKrzdprbOAboDGOB5Hna1UjZ1JRgP/0lqnOt9n/w+IdbZ6nfF9poyLQgYDL2utc7XWm4AvS2262FmfdoByln+w1PpsTh0XIS5KEngJcX5c72z96Ynxo3Eup7wOlXqcB3iUnEqsosOlHudXsFzyg9wK6KiUOllyw2hxanKGcqfiDLwwWpZ+cAZkYPx4XgPsVkr9qZTqfA71Hau1rqe1bqK1HqS13gGglHrKeeoq01m3upz9OJ5+7M71ooZWGKcDSx+T54HGpdKc6fVpCuwv1f8KjMD7bKr6ep903pcLjM4gx3lfp9RzdTgVuOWctu709WU4g56RWuvmGK2aTYEPnKtbAR+WOmYZgMJoNazsfeaPEayWPk67S23zD4wWwE+AI0qpz5TR36yEL6eOixAXJQm8hDiPtNZ/Ypx6Gut8KhfwKlnv/Mfvfy5FnrfKGT92fzqDnpKbj9b6TJfo/wb4K6ViMQKwktOMaK1Xa62vwziV9ANGa9Pf5uzPNQbj1JKf1roeRmuMKtnkPym/EnuBXacdE1+t9TVVyHsQaKaUUqWea1Hq8T+qc6lTgSFVTH/CWafSLWgxwGbn482l1zlPCbYptb6ysrdgvK9L+lvtxTjVXPq4eWqtl1H5++woYKPscWp52rY+0lrHA+HOfX+61Oowyp5KFeKiI4GXEOffB8BVSqkYjL4yHkqpAUopV+BFjFNoVXUYaH2e6vUzEKKUul0p5eq8JSqlwipKrLUuxjhV+R5GH6bfwOjbpJS6VSlV15kmC+Nqu3/CF+MH+SjgopR6mbKtM4eBQGefrL9NKeVR+oZxajBbKfWMUspTKWVVSkWqqg2bsBywAw8ppVyc/Z46nFbnBkqpuv+gynOBK07fB069h9ydyyW+Al5USvk5O83fy6mrRmcDkUqpwc48LwMbnEFVGc7O8U86+yyilGqBEXyvcCaZADynlIpwrq+rlCo5ZX3G95nz9Or3wKtKKS9nv68RpbabqJTq6Pys5GL0BSv93roC43S6EBctCbyEOM+01kcxfgBfdva7egCYCOzH+DE5l7Gd/oXxQ3pS/Y0rB0+rVzZG5+XhwAGMU17vUHkgOBXoA8xw9uUpcTuQrpTKwujvcyuUGeC1ZfmiKvUrxjAJ2zBOPRVQ9nTUDOf9caXUufQnK60ZxqnX0rcgjD5JscAu4BjGa3XWYElrXYTRufxujNNft2EEHYXO9VswOpzvdL5+Tc9QVGU+A249rVUtn1OnFbc4l0u8gtFKthvjasj3nBcnlLwvBwNvASeAjpTty1ZatnP9SqVULkbAtQl40lnWbIz3zrfO98AmjIsJqvI+ewjjlPAhjKBwcqnt1sHoU3bCuQ/HMQJ/lFIBGK1gP5zpYAlxMVBluycIIYT4u5RSK4EJWuvJZ01c9TKnAt9prX84X2VejJRS/8a4cOQ/tV0XIf4JCbyEEOJvUkpdgTHcwzGMVr8JQOvTrsQTQgjTuVw5JYQQoqxQjAsLvIGdwE0SdAkhKiMtXkIIIYQQNUQ61wshhBBC1BAJvIQQQgghashF0cerYcOGOjAwsLarIYQQQghxVmvXrj2mta5wsOyLIvAKDAxkzZo1tV0NIYQQQoizUkrtPtM6OdUohBBCCFFDJPASQgghhKghEngJIYQQQtQQCbyEEEIIIWqIBF5CCCGEEDWkWgMvpVQ9pdRMpdQWpVSqUqqzUqq+Uuo3pVSa896vOusghBBCCHGhqO4Wrw+BeVrrdkAMkAo8C/yutQ4GfncuCyGEEEJc8qot8FJK1QV6AF8AaK2LtNYngeuAL53JvgSur646CCGEEEJcSKqzxSsIOApMVkr9pZSaqJTyBhprrQ860xwCGldjHYQQQgghLhjVGXi5AO2B/9NaxwG5nHZaUWutAV1RZqXUfUqpNUqpNUePHq3GahqO7UmnuKCg2rcjhBBCiMtXdU4ZtA/Yp7Ve6VyeiRF4HVZKBWitDyqlAoAjFWXWWn8GfAaQkJBQYXB2vmit+WHsm+SePEHruERCOnWjdVwCrh4e1blZIYQQQlxmqi3w0lofUkrtVUqFaq23Ar2BFOdtBPC2835OddXhXPQb9QhbVywlbeVStq1Ygou7O63bdyC0U1eC4hJwdZcgTAghhBD/jDLO9lVT4UrFAhMBN2AncCfG6c3vgJbAbmCo1jqjsnISEhJ0TU2S7XDY2ZeymW0rlpC2ahl5mSfNIKzDoME0bt22RuohhBBCiIuTUmqt1jqhwnXVGXidLzUZeJV2KghLYtvKZVz76BhaRsaQcWAfx/bupnX7Dri4utZ4vYQQQghx4aos8KrOPl4XPYvFSsvIaFpGRtPrztEopQBIWfwHq3/8nvs//xoXV1dOHj6Et58frm7utVxjIYQQQlzIJPCqIovVaj7uMuRWQjp1w8PbB4BfPv43R3fvonV8B0I7dyMwNl6CMCGEEEKUI4HX32CxWmkU2Npc7jrsNrYuTyJt5TK2LluMq4cnbeI7ENKpqwRhQgghhDBJH6/zyGG3s3fzRratWMK2VcsoyM4yg7C4/gNpGtKutqsohBBCiGomfbxqiMVqpVV0LK2iY+l99/3s3byRrSuSSFu1nNbtE2ka0o6cjOMc2pFGYGy8dMwXQgghLjPS4lUD7DYboLG6uLL2fz+w6KuJ3P3RROo1bkJOxnE8fHxxcXOr7WoKIYQQ4jyQFq9aZnU5dZhj+11LkzYh1GvcBIA/Jn/K7o1/0Sa+IyGduxMYHSdBmBBCCHGJksCrhlldXGjWLtxcjul7De7e3mxftZzUJYtw8/SkTUInQjp1IzCmvZyOFEIIIS4hcqrxAmG32dizaT3bVixh+6rlFOTm4ObpRZuEjkT36U/zdhG1XUUhhBBCVIGcarwIWF1cCIqNJyg2nj73PMCejevZumIJ21cvp1Fga5q3i6AwL499qZtoFR0nLWFCCCHERUhavC5wdlsxDpsdVw8PUpMWMvfjfzP89fdoFhpGfk42bh4eWF0kCBNCCCEuFNLidRGzuriagVVI5254+tahaXAoAEunf82WpYtom9CJkM7daBUVK0GYEEIIcQGTwOsiYnVxJTA23lwO6dgFW2EB21evYPOfv+Pu7U3bhM6Edu5Gy6gYCcKEEEKIC4wEXhexlpExtIyMwVZczJ6NyUbH/NXL2fznAiMIS+xM5BV9aB4eWdtVFUIIIQQSeF0SXFxdad0+kdbtE7EVF7N7w19sW7GEtJXL8PStQ/PwSBx2O3s2JtMiMqbMuGJCCCGEqDnyC3yJcXF1pU18B9rEd8BWXIytqBCAvZs3MutfrzDoyecJ7tCFooJ8Z/8xeQsIIYQQNUV+dS9hLq6u5rATzcIiuH7MS7SMigVg7f9+YN3cH2mb2JnQTl2lJUwIcUkrLizg0PZt7NuymfysLHrdOQowrhyX/rCiJskv7WXCaAnraC43D4vkxMEDbFuRxKaF8/Hw8TWCsM7daBERLUGYEOKilp+dxf4tKezfmsL+1M0c3rUdh90OShHX/1oAtNZMfOhuInv1pevQ29BaU1xYgJuHZy3XXlzK5Nf1MtUiPIoW4VHYiopI3/AX25YnlQnCgjt0JqxbT1pERNd2VYUQospW/ziLzX/+zvF9ewBjcOrGbUJIuPYGmrWLoGlIGB4+PgDYi4uJ7NWXAOcQPScPH2Ty46Np0jqYFhFRtIiIplloOK4eHrW2P+LSI4HXZc7FzY22CR1pm9DRCMLWr2PbiiVsWZaEvbiYFhHRaK3Zn7qZpqFhWKzW2q6yEOIypx0OAJTFQkrSQpbPmMqIf/8HF1dXbEVF1GnoT1i3njRrF06TNiG4uLlVWI6Lmxtdh952atnVjQ7X3cSezRtY8/NsVs2ZicVqpUmbECMQC4+maWg7XN0lEBN/n4xcLypUXFRIUV4e3vX8OLYnnS+ffog+9zxIzFVXYysuxmKxSBAmhKgRtqIiDu1MM04dbtnMga2p3PjcqzQNCSN9w19s+mM+V468D+96fudtm0UF+RzYksLelI3s3byRQzvT0A4HFqsLN7/xHk3aBFOQk4PVzRVXN/fztl1xaZCR68U5c3VzN79M6jVpyqCnXqBZaDgAKX/+zpJvvyK4QxdCOnWjRUSUBGFCiPOmIDeHA9tS2Z+6mf1bUzi0Iw17cTEADZq3JLRzd7MfVmB0HIHRcee9Dm4engTGxpuDVhfm5XFgqxGINWjeAoDVP83ir19+4oGJU3FxcyPr2BG86vrJXLqiUhJ4Oe3fv5969erh7e1d21W54Li4uRGc2Nlcrt+8BS2jYkldsogNv8/D07cOwR2dQVi4BGFCiHOTffwYDruduo0ac+LgfiY9Phq0xmK10rh1W+L6D3T2z2qHV526tVJHdy8vguISCIo71YjRun0HvOvWM09l/vLx+xzavo2AkHbOU5NRNGkbKoHYBcDhcLBr1y7q1q1Lw4YNa7UucqoR48qWjz76iBMnTtCsWTOCg4MJDg4mICAAi8VSbdu92BUXFpCevI6ty5PYuW41xYUFeNapS3CHzrTr0kM65gshytEOBxkH9pGfnUXzsEi0w8En99xMSKdu9L3vYbTDwao5M2ka0o4mbUMuqv5Uu/5aw+6Nf7Fn80aO7t4FWuPi5k7TkHbGBU0R0TRpGyzDV9SgwsJC3N3dKSgoYOzYsSQkJNC/f/9q325lpxol8MIIvA4cOEBaWhppaWns378fAG9vb9q2bUvbtm1p06YNXl5e1VaHi11xYQG7kteybfkSdqxbRbPQcG564Q0ADu/cjn9gEBaLtIQJcbmx24o5vHM7+7eksM/ZP6sgJ5sGzVsy8t//ASBt9XL8mjSlYYtWtVzb8yc/J5t9qZvYt3kjezdv4OiedADCu1/J1Q89idaawzvS8A9sLcP3nGe5ubls2rSJ9evXA3DfffcBsHfvXpo0aYJrDbRASuB1jnJzc9mxYwdpaWls376d/Px8lFL069ePTp064XA4UEqhlKqxOl1MigsLyMvMpG6jxuSePMGE0XfQbdjtdLxhKA6HHUCCMCEuYQe2bWHXX6vZt2Uzh9K2YSsuAsAvoCnN2kXQLDScZmER+DVpWss1rTn52VnsS9mEVz0/moWGcfLwIb545B5633U/sf0GkJ+TzclDB2gc1Fa6a/wNNpuNbdu2sX79etLS0nA4HAQEBBATE0OHDh1q/OyVBF7/gMPhYP/+/aSlpRESEkLz5s3ZvXs3M2bM4Oabb6ZZs2a1Uq+Lha2oiJ3rVtG4dVvqNmrCjrUrmf/peII7diW0U1eahUVIECbERe7wrh2kLP6DHrfeidXFhYVTPuOveT/TKKi1EWi1C6dZaPh5verwYldUkE968lqatA2lTkN/Uhb/wS+fvI+bpyfN2kWYpyYbBbWW78gz0Fqzb98+1q9fz6ZNmygoKMDX15fo6Giio6Np3LhxrdVNAq/z7MCBAyxdupSBAwfi4eHBsmXL2Lp1K8HBwbRt25bGjRtLa9gZHEzbypqfZ7Nz3WpsRYV41a1HSKeuhHTqRrN24fIFI8QFTGvNiYMH2L9lM/u3pBB/7fX4twxk6/Ik5v3fB9z2rw9o0KwFeZkncXFzw81TumdUVX52Fns2rWfv5g3s3byRjAP7AHDz9KJ5WAQtIqJpER5Fo8DWqMu877HWGqUUW7Zs4dtvv8XV1ZWwsDBiYmIICgq6IPpmS+BVzdatW8eqVas4dOgQAL6+vmYH/datW+PuLmO8nK64oICdf61m2/Il7PxrDbaiQrzr+ZlXRzYPi5TgVYha5rDbOZK+0wy09m9NIS/zJAAevnXoN/pR2iZ0xG4rBpT0VTqPck5ksM85htjelI2cOLgfZbHw0KRvcfP04mDaVqyurjQKbF3bVa0xNpuN//73v7Rt25bu3btTXFzM5s2bCQsLu+B+ZyXwqiFZWVls376dtLQ0du7cSWFhIRaLhZYtWxIREUFiYmJtV/GCVFSQz66/1rB1eRK7/lpLvSYBjHjvYwCO79+LX0BTaQkTogaUtCTkZZ7kf+PHcnDbFooLCwCo26ix2TerWWgE9Zs1lz9HNSg74xhHd++idZzxO/Ld689TmJvL7e98CEDaqmXUaxxAwxatLpkWMbvdzs6dOzly5Ahdu3YFYM6cObRo0YL27dvXcu0qJ4FXLbDb7ezdu9e8UtLf358hQ4YA8McffxAcHEyLFi1quZYXnqKCfLKPHaVB85YUFxXyf/fcSnSffvS841601qD1JfOlIkRtsxUVmWNQzXjjeRq2COTKkffhcNiZ/sqzNApqY/TPaheOb/3aHftIlJV9/Bh5mSdp3LotdpuNj+8ahq2wEA/fOrQIizTnmmzQvOVFFyAfOnSI9evXs2HDBnJzc/Hx8eHRRx+tkasRzxcJvC4ANpsNFxcXcnNz+fDDD+nVqxedOnUiNzeXDRs2EBwcTIMGDS66D0h1shUXs331cuo3bU6jwNYc3L6VH8e+RXDHroR07kazkDAJwoSoIq01mUcOO08bGqcOXdzczRaTRf/9gnqNmhDbb0At11T8HVlHj5jTG+1N2UDW0SMAeNapa3TUD48iKC6Buo1qr8N5ZbKzs9m4cSPr16/n8OHDWCwWQkJCiI2NpW3btrhcZKexJfC6wNhsNhwOB25ubqSmpjJ9+nQA/Pz8aNu2LcHBwQQGBuJ2holdL1eHd+1g5ffT2fXXGmzFRfj41Se4pGO+BGFClOFw2Dm6O92c33D/1hRyT2QA4OHtQ9PQMJqHR5Fw7Q3yh+8SlHnksNFR3xmMZR8/ypUjR9H+6oHkZZ4kbdUygjt2rbWZAEocO3aMefPmsWPHDrTWNGvWjJiYGCIjIy/qsTNrLfBSSqUD2YAdsGmtE5RS9YHpQCCQDgzVWp+orJxLLfA63YkTJ8y+Ybt27aK4uBgXFxcCAwPNKyUbNGhQ29W8YBTl57FjndExf1fyGuzFxfjUb0Bwxy6EdupO05B2EoSJy05xUSGHt6fRtF0YFouVP6Z8yl+//ASAb0N/moWG0zzMGEOrQfOW8hm5jJS0drp5euJVpy5blyfx8wfvcNu/PqBx67Yc2LaFY3vSaRERRb0mTas1ENdas3v3bpRStGrVipycHL744gsiIyOJiYmp9el8zpfaDrwStNbHSj33LpChtX5bKfUs4Ke1fqayci71wKu04uJidu/ebQZix48fRynFmDFj8PT05OTJk3h7e19U57qr06kgLIldyWtx9/Jm1IQvsVisZB07gm/9hvIDIy5J+TnZHNiaQqPANvg2aEhq0kLmfvxvbn/nIxoFtubQ9m2cOHSAZu3CqdOwUW1XV1xAtNacPHSAuo2bYLFY+fPrSaz56XsAfOo3MMcQaxEeRd3GTc5LIJaXl4eXlxdaa8aPH0/9+vW57bbbzPpcaq2uF1rgtRXoqbU+qJQKABZprUMrK+dyCrxOl5GRwYEDB4iMjATgyy+/pLCw0JwCITc3Vyb2dirMy+PEwf00aROMdjj47ME7aRUVR/8HHgMuzQ+3uHxkHT3C/i2b2efsn3V83x4Aet01mrh+15KXlcnBtC20iIjGzcOzlmsrLiZaazIO7GNfykb2bN7IvpSN5rAhvg38aREeSYvIGCKu6H1O36F5eXls3ryZ9evXc+zYMZ566ilcXFw4fPgwfn5+l3R3mtoMvHYBJwANfKq1/kwpdVJrXc+5XgEnSpZPy3sfcB9Ay5Yt43fv3l1t9byYbN++HZvNRrt27bDZbLz77rvUqVPHHDesZcuWF10nxOrgsNvZumwxPg0a0iI8iqyjR/j2lWeM05GduxHQNlRawsQFrbiokM2Lfjc7wmcfPwoYA2o2DQ0zTh22i6Bx22Bc3S6sMYzExU1rTcb+vUZHfWc/MZ/6Dbjj3fEArPl5Nn4BTWkT37FcXpvNxvbt20lOTmbbtm04HA4aNWpETEwMiYmJl3SwVVptBl7NtNb7lVKNgN+Ah4EfSwdaSqkTWutK55G4nFu8KlNUVMS6detIS0sjPT0du92Oq6srrVu3NvuG1atXr7areUE4vm8Pi6dOYff6ddhtNnwaNCTU2TFfgjBxoVg3dw6uHp5E9eprDBFw5zDcvb3N+Q2bh0XQsGUrGddO1CjtcJCXlYl3PT+01kx8+B6C4hLoc/f9OBx2fv9iAp5Nm3Os0MbWNGN+Y29vb6KiooiJiaFJk/NzuvJickFc1aiUehXIAe5FTjWed0VFRezatcscNywzMxMAf39/BgwYQGBgYO1W8AJRmJfLjjUr2bpiiRmEWaxW3L198PD2Zvhr7+JVtx5pq5aRvn4dfe5+AGWxsH9rKjkZx/Hw9sHd29u49/HB3ctLfgTFOSvMy+XA1lT2b00hPzuLq+59CIDprz2Ll29dBj7xHGCMXu5dz++y+9ESFzbtcFBcWICLuwfZx44w5cUxnGjaGhwOPG0FBDZpTGRsHK0iY/Cpf3leGFZZ4FVt56SUUt6ARWud7XzcF3gd+BEYAbztvJ9TXXW4nLi5uREaGkpoaChaa44dO2YGYSV9wLZs2cL69esZOHDgRX2Z7j/h7uVNeI9ehPfoZQRha1dxfO9uCvNyKcjJwdXdA4CMA/vZlbzWbAlbP/9/pC5ZVGGZbp5euHt741WnHrf9axwAmxb+RnbGMToPvhmAPZs2YCsqdAZ4p4I3l8uk2f1yl51x7NSwDltSOLon3RwMOKBtKA6HHYvFyk0vvIHV5dSFMz5+9Wuv0kKcgbJY+OGnn3FxceHGG2/kkQlTWL54EZbMDA5tS2XfX6uYt3QhAH4BzWgREUX7q6+jQXMZNByqMfACGgOznf/UXICpWut5SqnVwHdKqbuB3cDQaqzDZUkphb+/P/7+/nTp0sV8Pj8/nxMnTuDhYQQXy5YtIy8vj+DgYJo3b47Venm13Lh7eRPe/coK13W8fggdrx9iLvcccS8drruJgtwcCnJzKczNobDU44LcHLTDYabfvzWVY3vTzcBr6fT/cmBbarntWF1dnYGYD/4tA7n2MeMC33Vz5+Di7kF0734ApG/4C4vFeqq1zdsbd08vOUV6ASo5i6CUYuPC+az8fjqZRw4D4OruQUBIOzoPvpnmYREEtA3F1fl5BMoEXUJcKBwOh3lGpV+/fiilaNy4sfmboSwWuvTsVSq9naPpu8z+YVuW/klUL+O7bPeGZNJWLaPr8Nvx9PGtlf2pbTKA6mVs9uzZbNy4EYfDgYeHR5m+Yb6+l+cHorpkHjlMXuZJM0graWEreVyYk4NXvXr0vut+AKa/+iwePj5c99SLAEwYdTu5J8sOd6eUBXcvo7XN3duHVtFx9LhlJAArvp+Of6sg2sR3MMbNWb8O99KnSb295Uf+PLHbinE4HLi6ubN7YzI/f/guw175Fw1btGLbyqVsWfKn0UerXTiNAltjucz+4IiL15EjR8ype7Kzs3F3d2f06NH4+VXaLbsch92OUgplsZA8fy4rZk3j3k8mY3VxYc3Ps8k8cogW4VE0D4+q9QFdz5cLoo/XPyGBV/UpKChg586d5mnJnJwcAAICAggODiYsLIyAgIBaruXlqfTwF0f3pFOQnUWBM0grHbwV5hmtbo1bt6XLkFsB+L/7bqNdlx5cOfI+iosK+ej2weXKd3F3N4IwL288fHxo17UnsX2vweGws2LWtwTGxNM0pB3FRYUc2r7NbJnz8PbG1cPzsu13VJSfx4FtW9i/NYX9qZs5uH0bV468l+je/ck8cojlM78l8brBNGgmp1XExScnJ4dNmzaxfv16Dh48iFKK4OBgYmJiCAkJOS9jSGqHw2yt/2Pyp2xa+Js5GXvDloHOccSiaB4WiadvnX+8vdoggZeoEq01hw4dIi0tje3bt7N37146duxI//79sdvtbNq0ieDg4Mu2f9jFpuTLzWG3c2jHNiNYy8k5Fbw5A7aS4C24Q2fi+g8kPyeb/9x9M1eOvI/2Vw/i2N7dfPnUg2XKVhaLGYSVBGSx/a6lbUJH8rIy2bTwN9omdqZ+02YU5OZw8tDBU61tXt4XVatP7skTZt+sfVs2czR9F1o7UMqCf2AQzdtF0K7bFQS0rfQaISEuaLm5ucyZM4e0tDS01gQEBJhT9/j4+FTrtu02G4d3pjnnmdzI/q0p2AoLQSn8WwbSrusVdLjupmqtw/lWK53rxcVHKUVAQAABAQH06NGD/Px8bDYbAPv372f27NkMGTKEiIgIsrKyyMrKomnTplikn9EFqeQfpcVqpWlIWJXzefr48vi0OWiH8aesjn8jbnrxzXItbCWPC5z93Ry2YsAY6DNp6hQaNG9B/abN2L8lhR/efb3MNtw8PY3AzcvbeXWoD51uHEaTNsGcPHyInetW065Ld7zq1iMvK5O8zJOlLkhwr9bWthOHDlCYm0uTNsHYiov5/KG7sBcX4+LmTkDbEDreOJRmoeE0DWmHm6f8CREXr71795KdnU14eDienp7k5ubSpUsXoqOjady45ibTtrq40DQkjKYhYXS8YSh2WzGHtqc555ncYHaz0A4H373xPDFXXUO7Lj1qrH7nmwRe4ow8PU+Nft28eXPuu+8+c87IDRs2sGDBAry8vMyJvdu0aSOtYZcIi8UKznjazcOTVlGxVc7buHVbHvlqptmq1aRNMNePeYnCXCNIKxO8OS9OyDxyCHuxEbgd3pnGwimf0jIyGq+69diy9E8WTvnMLN/q4uLsr3YqcPPw9qH7LSOp09CfI+k7ObQjjfDuV+Li5kbuyRPmFaWnX5DgsNs5unsXJw7up13XKwD45ZP3UShufuM9XFxd6TfqEeo2DqBx6zbSL05c9LKzs80+vEuWLOHo0aOEhYVhsVi49957a7l2BquLK83ahdOsXTidbhxmPp+fk41Spz6/x/ft5ZdP/m1Ob9SsXQTuF8FvkJxqFH9LXl6eOZ/kjh07yMvLQylFs2bNzFH0mzRpIq1h4pw57HYKcnPw8PbBYrVy8tBBDu/aXq6FrezVpTkMefEt6vg3YuUPM1gy7Use+Womru4eLPzyc9bNdY5ao5RxQYKXMQbbycOHKC7Ix+riwkOTv8PFzY2DaVtx8/SSS9/FJaOgoMCcumfPnj08+uij+Pn5cfLkSTw9PXF3vzhnPji0fRt/fj2Jg2lbsNtsKGWhces2RiAWEU2z0LBaa5WWPl6iWjkcDg4cOGB20D9w4AAADRs25MEHH0Qphd1uv+yGqxC1o7iokPysTHwb+KOU4vDO7Rzdk15u+I/C3Bzq+DeiWWg4zdpF4NugYW1XXYjzxm63s2PHDtavX8+WLVuw2+00bNiQmJgY2rdvf0nN8VtcVMjBbVvMU5MH07bhsNtQFgtN2gRz7WPP1PhE8dLHS1Qri8VC8+bNad68OVdeeSU5OTns2LGD/Px8sy/Op59+Sps2bejXzxjLRSasFtXF1c0d11Jfso1bt6Vx67a1WCMhakbJBVLr169n48aN5Obm4unpSXx8PDExMTRt2vSS/N51dXOnZWQMLSNjACguLODAViMQO7AtFe96xvAXSdO+xMPbh8RB5a/yrkkSeInzzsfHh5iYGHPZbrcTGhpqdtbMzc01A7Hg4GBat25tDuoqhBDi3NhsNlxcXCgqKmLSpEnmd25MTAxt27bFxeXy+ql3dfegVXQsraJjyzzv28Cf+k2b1U6lSpFTjaLGZWRksGDBAnbs2EFhYSEWi4WWLVuag7c2atTokvxXJmpXfn6+eSsoKKCgoAAPDw8aNGhAnTp1pD+iuCjNmzePPXv2cN999wGwY8cOAgIC5EKnWiZ9vMQFyW63s3fvXrOT/uHDxrQqderUITg4mCuuuII6dS7OwfPE+edwOCgoKDADp4rurVYrvXv3BuCnn36ioKCAIUOMqZ8+/vhjjh07VmHZVquV+vXr07ZtW/N0+IEDB6hTp061j2EkRFU5HA52797Nhg0b6NevHx4eHmzYsIFjx45xxRVXSD/aC4j08RIXJKvVSmBgIIGBgfTp04fMzEwzCNu8ebP5A5qSksLJkyfp1KmTtEpc5Ox2e6XBU0FBAVdddRVKKZYuXcrOnTu5/fbbAZg2bRppaWlnLLskeCp539StW7fMkCg9e/bEZrPh4eGBp6cnHh4e5OXlcfz4cTIyMjh+/HiZ8r7++mvatWvHoEGDcDgczJo1Cz8/P+rXr0+DBg2oX78+Pj4+0jorqt2xY8fMqXsyMzNxc3MjNjaWVq1aER0dXdvVE+dIAi9xwahbty7x8fHEx8fjcDjMICstLY19+/aZE36vWbMGX19fgoKCcHNzq80qX5bsdnulrU6JiYl4eXmxefNmVq9ezW233YaLiwu//vory5cvr7RsFxcXrrjiCtzd3XF1dS3T9y8uLo42bdqUCZxK358+lUmPHmUHWIyMjKxwm0FBQeWe01ozePBg83RNfn4+hw4dIjU1FUepydDd3NzMQKxBgwaEhITQvHnzMhNlC/F35OXlmVP37N+/H6UUbdq0oU+fPoSGhsp330VMAi9xQSrdsnXddddRWFgIGE3tixYtIicnx2wxKxk3rGRwV3F2NpvtjIFTSEgIfn5+7N27lyVLlnD11VdTr1491qxZw6+//kqxc6DTMwkNDcXLywutNQ6Hw+z427p1a9zd3SsMmjw8PPDw8CgTPHXo0IEOHTqYy+Hh4dV2PE5X8iNXwtvbm4cffhi73U5mZmaZVrKMjAwOHDhASkoKnp6eNG/enIyMDD7//HOuv/562rVrR1ZWFunp6WaQVrolTojS7HY7M2fOZOvWrTgcDho1akTfvn2JiooyBz4VFzcJvMRFoWSAP4vFwmOPPcbu3bvNOSXnzZvHvHnz8PPzM4OwwMDA8zKZ64WsuLi4wuApICCARo0acfLkSRYuXEjHjh1p2rQpO3fuZPbs2RQUFFQaPNWpUwc/Pz9sNhsnTpygqKgIAH9/f+Lj488YOJXcl1xBFRkZWaaVqeS1uZiVnM6sX79+uXU2m81sDbNarURGRuLnZ1zGvnv3br7//nszraenp3m6svS9v7//Jf++FeUdPHiQffv2kZiYiNVqxWq10rFjR2JiYmjSpEltV0+cZ9K5Xlz0MjIyzL5hu3btwmaz0bdvX7p06UJhYSG5ubkV/lBeCIqLi8sFTnXq1CEgIICioiJ+//13QkJCaNOmDRkZGUybNs1MVzKP5ulK9v348eN89dVXDBgwgJCQEI4cOcKyZcvKBEpnCp6kk+75ZbPZyMjIKNNKVnKflZVlprvnnnto3rw527dvZ9OmTfTr1w9PT0/y8vJwcXGR00uXkJMnT+Lr64vVamXBggWsWrWKJ5988qIdRV6UJZ3rxSWtfv365mmp4uJidu/ejb+/P2D0D5s5cyb33nsvzZo1Iy8vDzc3t/M2ro3WulzLk6urK02bNgUgKSmJhg0bEhYWhs1m48svvywTaNnt9nJldujQgYCAACwWC8nJyfj5+dGmTRvc3Nxo2LBhpa1NHh4e5umIBg0a8Pjjj5vlNmrUiOuvv/687Lc4Ny4uLjRq1IhGjcqPnl1UVMSJEyc4fvy4+b7NzMxkx44dZuvXn3/+ycqVK/H19S3XSlbSAictZRe+wsJCUlJSWL9+Penp6dx6660EBwfTpUsXunXrJkHXZUJavMQlLTMzk61bt5KQkIDFYmHu3Ln89ddfBAUFmae+6tatS1FRkXlVncPhICAgAID169djsViIiooCYNasWZw4caJMoHV68BQcHMytt94KwPvvv09oaCgDBgxAa83UqVMJDAzEw8MDpVSFN4vFIldvijJsNpt5KtPhcGC32zn9u9tiseDr64tSCpvNhtb6ognGPDw8aN68+UVT33PhcDjYuXMn69evJzU1FZvNhp+fHzExMcTFxVG3bt3arqKoBtLiJS5bdevWLddBW2tNWloa27ZtA4wfrNJXqjVs2JCHHnoIgLVr15YJvIqLi3Fzc6NOnTpnbHUqPfbYY489ZgZRSim6dOmCr68vDRo0kCvexD9ScuGCzWbDbrfjcDjMH/Fjx46htTZb0DIyMtBaY7VacXFxMW9Wq7XW34daa44fP86+ffsqvML0YnX48GFzCIicnBw8PDyIjY0lOjqaFi1a1PpxF7VHAi9xWSkZN0xrzbFjx9i+fTu5ubllAqfSA2aWDIVQYvjw4ee0vdNbrgoKCggMDJQvXfGPWSwW3NzcKuz35efnV6ZFzGKxUFxcTFFRUbmWspJgzN3d3Xzv2+12LBZLjbxPlVI0aNCAo0ePVvu2qltRURFubm5orfnuu+84ceIEwcHBxMTEEBwcfEm26IlzJ4GXuCwppfD39zdbBM6kOjozS9AlqtvpF0fUq1cPoMwQH3a73WwxK7mVpDly5AheXl7UrVsXrTVZWVllWsnOd0vZpfCZWLZsGUlJSTzxxBO4urpy4403Uq9ePby9vWu7auICI4GXEEJcJpRSZuBUmTp16pitM3a7ndzc3HJpTj9l6e7uftlMxqy1Zs+ePaxfv56OHTvSuHFjmjVrRvv27bHZbLi6utKsWe1PxiwuTNKDVwhxTpKSkoiIiCA2Npb8/Pxq286UKVM4cOCAuXzPPfeQkpJSaZ6ePXtS+kKc5ORklFLMmzfPfC49PZ2pU6eWSTN37ty/Xc/AwMAzzgFZHf5pfU+Xk5PD/fffT5s2bWjfvj0JCQlMnTrVbO11cXExx4Zr0KABdevWxdvbG6vVis1mIycnh8zMTHOQ4+LiYo4cOWIuX3HFFSxbtqzCCwIuNhkZGSxcuJCPPvqIyZMns3HjRnOO2VatWnHVVVfJ4LjirC6PvydCiHNit9vP2CryzTff8Nxzz3HbbbdVqSytNVrrc75Sc8qUKURGRppDc0ycOPGc8oMxv2O3bt2YNm0a/fv3B04FXrfccgtgBDJr1qzhmmuuOefya8P5ru8999xD69atSUtLw2KxcPToUSZNmlQmTcmpQHd393JDHmitsdvtZU4XWq1W8/V2OBxkZmZy+PBhlFJmC9npLWYXqvz8fDZv3sz69evZu3cvAK1bt6Znz560a9dOhoAQ567kS/FCvsXHx2shLgUpKSm1XQW9a9cuHRoaqm+55Rbdrl07PXjwYJ2bm6tbtWqlx4wZo+Pi4vS0adP0r7/+qjt16qTj4uL0TTfdpLOzs/Xnn3+u/fz8dGBgoL7lllu01lq/++67OiEhQUdFRemXX37Z3EZISIi+/fbbdXh4uE5PTz9junbt2ul77rlHh4eH66uuukrn5eXpGTNmaG9vbx0SEqJjYmJ0Xl6evuKKK/Tq1au11lqPHj1ax8fH6/DwcLMsrXWZNA6HQwcFBent27frgIAAnZ+fr7XWumPHjrpOnTo6JiZGv/3227pFixa6YcOGOiYmRn/77bd65cqVulOnTjo2NlZ37txZb9myRWuttc1m008++aSOiIjQUVFR+qOPPtJaa92qVSv98ssv67i4OB0ZGalTU1O11lq/8sor+o477tDdunXTLVu21LNmzdJPP/20joyM1P369dNFRUVaa63XrFmje/Toodu3b6/79u2rDxw4YO7LmDFjdGJiog4ODtaLFy/WhYWF5ep7ulWrVunOnTvr6OhonZiYqLOysvTkyZP1gw8+aKYZMGCAXrhwod6+fbsOCgrSdru9wvfKwoULdbdu3fTAgQN1cHCw1lrr6667Trdv316Hh4frTz/91Ezr7e2tH3vsMR0eHq579eqljxw5orXWukePHvqJJ57Q8fHxuk2bNvqnn37Shw4d0vv37y9z27hxo9Za6x07duiFCxeax8dms1X+hq5GP/30k3799df1K6+8osePH6+TkpL0yZMna60+4uIBrNFniGmkxUuIWvLaT5tJOZB19oTnILxpHV4ZGHHWdFu3buWLL76ga9eu3HXXXfznP/8BjEFX161bx7Fjx7jxxhtZsGAB3t7evPPOO7z//vu8/PLLLFmyhGuvvZabbrqJ+fPnk5aWxqpVq9BaM2jQIBYvXkzLli1JS0vjyy+/pFOnTmdNN23aND7//HOGDh3KrFmzuO222/j4448ZO3YsCQnlh8J56623qF+/Pna7nd69e7Nhwwaio6PLpFm2bBlBQUG0adOGnj178r///Y/Bgwfz9ttvM3bsWH7++WcAGjduzJo1a/j4448ByMrKIikpCRcX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IYQQta5aAy+lVHNgADDRuayAXsBMZ5Ivgeursw5CCCGEEBeK6m7x+gAYA5SM3NcAOKm1LhlBbx/QrJrrIIQQQghxQai2wEspdS1wRGu99m/mv08ptUYptebo0aPnuXZCiL8rKSmJiIgIYmNjzzrK/D8xZcoUc9oaMMbRSklJqTRPz549KT30THJyMkop5s2bZz6Xnp7O1KlTy6SZO3fu365nYGAgx44d+9v5z9U/re+ZvPrqq4wdOxYofxyFEOdPdbZ4dQUGKaXSgW8xTjF+CNRTSpUM3Noc2F9RZq31Z1rrBK11QlXn3BJCnB8lo5hX5JtvvuG5554jOTm5SqN8a63LTFdTVacHXhMnTiQ8PPycypg2bRrdunUrM/XR+Q68atqFXN+qTAckxOWu2gIvrfVzWuvmWutAYDjwh9b6VmAhcJMz2QhgTnXVQQhRXnp6Ou3atePWW28lLCyMm266iby8PAIDA3nmmWdo3749M2bMYP78+XTu3Jn27dszZMgQcnJymDhxIt999x0vvfQSt956KwDvvfceiYmJREdH88orr5jbCA0N5Y477iAyMpK9e/eeMV1YWBj33nsvERER9O3bl/z8fGbOnMmaNWu49dZbzZa10q0w999/PwkJCURERJhlnU5rzYwZM5gyZQq//fabOafgs88+S1JSErGxsbzzzju8/PLLTJ8+ndjYWKZPn86qVavo3LkzcXFxdOnSha1btwJGMPrUU08RGRlJdHR0mZH+x48fT/v27YmKimLLli2A0YI0YsQIunfvTqtWrfj+++8ZM2YMUVFR9O/f35x8e+3atVxxxRXEx8fTr18/Dh48CBitTs888wwdOnQgJCSEpKQkioqKytW3tNWrV5sTUs+ZMwdPT0+KioooKCigdevWAHz++eckJiYSExPD4MGDycvLO+N7xeFwMHLkSHNKoaefftp8DT/99FPAmL+xe/fuDBo06JwDYyEuS1rrar8BPYGfnY9bA6uA7cAMwP1s+ePj47UQl4KUlJSyT0y65uy3JR+WTb/ua+NxzrHyaatg165dGtBLlizRWmt955136vfee0+3atVKv/POO1prrY8ePaq7d++uc3JytNZav/322/q1117TWms9YsQIPWPGDK211r/++qu+9957tcPh0Ha7XQ8YMED/+eefeteuXVoppZcvX37WdFarVf/1119aa62HDBmi//vf/2qttb7iiiv06tWrzXqXXj5+/LjWWmubzaavuOIKvX79+nJplixZonv16qW11vrmm2/WM2fO1FprvXDhQj1gwACz3MmTJ+sHH3zQXM7MzNTFxcVaa61/++03feONN2qttf7Pf/6jBw8ebK4rqUOrVq30Rx99pLXW+pNPPtF333231lrrV155RXft2lUXFRXp5ORk7enpqefOnau11vr666/Xs2fP1kVFRbpz5876yJEjWmutv/32W33nnXea+/LEE09orbX+3//+p3v37l1hfUsrLi7WQUFBWmutn3zySZ2QkKCXLFmiFy1apIcPH6611vrYsWNm+hdeeMGs+yuvvKLfe+89c9vLly/Xw4cP12+++abWWutPP/1Uv/HGG1prrQsKCnR8fLzeuXOnXrhwofby8tI7d+6ssE5VVe6zIcRFDFijzxDT1MhcjVrrRcAi5+OdQIea2K4QomKl5zi87bbb+OijjwDMef9WrFhBSkqKmaaoqMicZ7G0+fPnM3/+fOLi4gBjEM20tDRatmxJq1atzLn8KksXFBREbGwsAPHx8aSnp5+1/t999x2fffYZNpuNgwcPkpKSQnR0dJk006ZNY/jw4QAMHz6cr776isGDB5+17MzMTEaMGEFaWhpKKbNlasGCBYwePRoXF+Nrs379+maeklam+Ph4vv/+e/P5q6++GldXV6KiorDb7fTv3x+AqKgo0tPT2bp1K5s2bTIndbbb7QQEBFRYblWOi4uLC23atCE1NZVVq1bxxBNPsHjxYux2O927dwdg06ZNvPjii5w8eZKcnBz69etXYVmjRo1i6NChvPDCC4DxGm7YsMGcCDszM5O0tDTc3Nzo0KEDQUFBZ62fEEImyRaidt35v7+f3rvBued3MkZ2Kb/s7e0NGC3hV111VZm+URXRWvPcc88xatSoMs+np6ebZZ0tnbu7u7lstVrP2mF/165djB07ltWrV+Pn58fIkSPN04gl7HY7s2bNYs6cObz11ltorTl+/DjZ2dmVlg3w0ksvceWVVzJ79mzS09Pp2bPnWfOU7IPVai3Tz6nkeYvFgqurq3mcLRYLNpsNrTURERFlJgGvSrml9evXj8OHD5OQkMDEiRPp0aMHv/zyC66urvTp04eRI0dit9t57733AGPy8R9++IGYmBimTJnCokWLKiy3S5cuLFy4kCeffBIPDw+01owfP75coLZo0aIyr7UQonIyZZAQl6E9e/aYP/ZTp06lW7duZdZ36tSJpUuXsn37dgByc3PZtm1buXL69evHpEmTzOli9u/fz5EjR/52utJ8fX0rDJSysrLw9vambt26HD58mF9++aVcmt9//53o6Gj27t1Leno6u3fvZvDgwcyePbtcuacvZ2Zm0qyZMcrNlClTzOevuuoqPv30UzMAysjIqLT+VREaGsrRo0fN16K4uJjNmzdXmuf0+v76668kJyczceJEALp3784HH3xA586d8ff35/jx42zdupXIyEgAsrOzCQgIoLi4mG+++eaM27n77ru55pprGDp0KDabjX79+vF///d/Zgvgtm3byM3N/Uf7L8TlSAIvIS5DoaGhfPLJJ4SFhXHixAnuv//+Muv9/f2ZMmUKN998M9HR0XTu3NnsNF5a3759ueWWW+jcuTNRUVHcdNNNFQZLVU1X2siRIxk9enS5YStiYmKIi4ujXbt23HLLLebp0NKmTZvGDTfcUOa5wYMHM23aNKKjo7FarcTExDBu3DiuvPJKUlJSzM7qY8aM4bnnniMuLq5MK9M999xDy5YtiY6OJiYmpsyVkX+Xm5sbM2fO5JlnniEmJobY2FiWLVtWaZ7T63u6jh07cvjwYXr06AFAdHQ0UVFRZmvbG2+8QceOHenatSvt2rWrdFtPPPEEcXFx3H777dxzzz2Eh4fTvn17IiMjGTVqlFzFKMTfoIw+YBe2hIQELWPKiEtBamoqYWFhtVqH9PR0rr32WpkoW1xQLoTPhhDni1JqrdY6oaJ10uIlhBBCCFFDJPAS4jITGBgorV1CCFFLJPASQgghhKghEngJIYQQQtQQCbyEEEIIIWqIBF5CCCGEEDVEAi8hLnOvvvoqY8eOLTMJdWlTpkzhoYceqtY62Gw2nn/+eYKDg4mNjSU2Npa33nrrvJU/cuRIc6obIYSoTRJ4CSFq3YsvvsiBAwfYuHEjycnJJCUlmSOkl6a1xuFw1EINhRDi/JDAS4jL0FtvvUVISAjdunVj69at5vP//e9/iY2NJTIyklWrVpXLVzKafEJCAiEhIfz8888Vlr99+3b69OlDTEwM7du3Z8eOHSxatIhrr73WTPPQQw8xZcoU8vLy+Pzzzxk/fjweHh6AMS3Oq6++ChgDvoaGhnLHHXcQGRnJ3r17uf/++0lISCAiIoJXXnnFLDMwMJAxY8YQFRVFhw4dzCmPABYvXkyXLl1o3bq1tH4JIWqNTJItRC15Z9U7bMkoPw3PP9Gufjue6fBMpWnWrl3Lt99+S3JyMjabjfbt2xMfHw9AXl4eycnJLF68mLvuuqvC8b7S09NZtWoVO3bs4Morr2T79u1mwFTi1ltv5dlnn+WGG26goKAAh8PB3r17K6zP9u3badmyJb6+vmesc1paGl9++SWdOnUCjMCxfv362O12evfuzYYNG4iOjgagbt26bNy4ka+++orHHnvMDA4PHjzIkiVL2LJlC4MGDeKmm26q9DgJIUR1kBYvIS4zSUlJ3HDDDXh5eVGnTh0GDRpkrrv55psB6NGjB1lZWZw8ebJc/qFDh2KxWAgODqZ169bl5nDMzs5m//795lyJHh4eeHl5Vbl+kydPJjY2lhYtWpjBWqtWrcygC+C7776jffv2xMXFsXnzZlJSUsrtw80332xOPg1w/fXXY7FYCA8P5/Dhw1WujxBCnE/S4iVELTlby1RtKJlI+UzLZ0pz55138tdff9G0adMKJ24GcHFxKdM/q6CgAIC2bduyZ88esrOz8fX15c477+TOO+8kMjISu90OgLe3t5lv165djB07ltWrV+Pn58fIkSPNsk6vX+nH7u7u5uOLYY5aIcSlSVq8hLjM9OjRgx9++IH8/Hyys7P56aefzHUlQdOSJUuoW7cudevWLZd/xowZOBwOduzYwc6dOwkNDWXy5MkkJyczd+5cfH19ad68OT/88AMAhYWF5OXl0apVK1JSUigsLOTkyZP8/vvvAHh5eXH33Xfz0EMPmQGU3W6nqKiowvpnZWXh7e1N3bp1OXz4ML/88kuZ9SX7MH36dDp37vzPDpYQQpxn0uIlxGWmffv2DBs2jJiYGBo1akRiYqK5zsPDg7i4OIqLi5k0aVKF+Vu2bEmHDh3IyspiwoQJ5fp3gdFJf9SoUbz88su4uroyY8YMWrduzdChQ4mMjCQoKIi4uDgz/VtvvcVLL71EZGQkvr6+eHp6MmLECJo2bcqBAwfKlB0TE0NcXBzt2rWjRYsWdO3atcz6EydOEB0djbu7O9OmTfsnh0oIIc47dS5N7kopL611XjXWp0IJCQm6ovGFhLjYpKamEhYWVtvV+NtGjhzJtddee8F2TA8MDGTNmjU0bNiwtqsiztHF/tkQojSl1FqtdUJF66p0qlEp1UUplQJscS7HKKX+cx7rKIQQQghxyavqqcZxQD/gRwCt9XqlVI9qq5UQ4oI0ZcqU2q5CpdLT02u7CkIIUakqd67XWp8+CI/9PNdFCCGEEOKSVtUWr71KqS6AVkq5Ao8CqdVXLSGEEEKIS09VW7xGAw8CzYD9QKxzWQghhBBCVFGVWry01seAW6u5LkIIIYQQl7SqXtUYpJR6Xyn1vVLqx5JbdVdOCFH9Xn31VcaOHUvPnj2paNiWKVOm8NBDD9VIXUpva+TIkTKZtRDiklPVPl4/AF8APwGOypMKIUTt0VqjtcZikYk5hBAXnqp+MxVorT/SWi/UWv9ZcqvWmgkhqs1bb71FSEgI3bp1Y+vWrebz//3vf4mNjSUyMpJVq1aVyzdy5EhGjx5NQkICISEh/Pzzz+XSHDlyhPj4eADWr1+PUoo9e/YA0KZNG/Ly8vjpp5/o2LEjcXFx9OnT56yTVr/00kuMHDkSu93Oe++9R2JiItHR0bzyyiuAMYxEaGgod9xxB5GRkebk2kIIcaGpaovXh0qpV4D5QGHJk1rrddVSKyEuE7tvv+OsaXx69qTB3XeZ6evecAP1brwB24kT7H/k0TJpW/33q7OWt3btWr799luSk5Ox2Wy0b9/eDJTy8vJITk5m8eLF3HXXXWzatKlc/vT0dFatWsWOHTu48sor2b59e5lpgxo1akRBQQFZWVkkJSWRkJBAUlIS3bp1o1GjRnh5edGtWzdWrFiBUoqJEyfy7rvv8u9//7vC+j799NNkZ2czefJkfvvtN9LS0li1ahVaawYNGsTixYtp2bIlaWlpfPnll3Tq1Omsx0AIIWpLVQOvKOB2oBenTjVq57IQ4iKSlJTEDTfcgJeXFwCDBg0y1918882AMZF2VlYWJ0+eLJd/6NChWCwWgoODad26NVu2bCE2NrZMmi5durB06VIWL17M888/z7x589Ba0717dwD27dvHsGHDOHjwIEVFRQQFBVVY1zfeeIOOHTvy2WefATB//nzmz59vzvOYk5NDWloaLVu2pFWrVhJ0CSEueFUNvIYArbXWRdVZGSEuN1VpoTpTehc/v3POfzZKqUqXz5Tmzjvv5K+//qJp06bMnTuXHj16kJSUxO7du7nuuut45513UEoxYMAAAB5++GGeeOIJBg0axKJFi3j11VcrrE9iYiJr164lIyOD+vXro7XmueeeY9SoUWXSpaen4+3t/Q/2XAghakZV+3htAupVYz2EEDWkR48e/PDDD+Tn55Odnc1PP/1krps+fToAS5YsoW7dutStW7dc/hkzZuBwONixYwc7d+4kNDSUyZMnk5yczNy5cwHo3r07X3/9NcHBwVgsFurXr8/cuXPp1q0bAJmZmTRr1gyAL7/88ox17d+/P88++ywDBgwgOzubfv36MWnSJHJycgDYv38/R44cOT8HRgghakBVW7zqAVuUUqsp28dr0BlzCCEuSO3bt2fYsGHExMTQqFEjEhMTzXUeHh7ExcVRXFzMpEmTKszfsmVLOnToQFZWFhMmTCjTv6tEYGAgWmt69DCmdO3WrRv79u3Dz88PMIawGDJkCH5+fvTq1Ytdu3adsb5DhgwhOzubQYMGMXfuXG655RY6d+4MgI+PD19//TVWq/VvHw8hhKhJSmt99kRKXVHR85Vd2aiU8gAWA+4YAd5MrfUrSqkg4FugAbAWuP1spzATEhJ0ReMLCXGxSU1NJSwsrLar8beNHDmSa6+9lptuuqm2qyIuMRf7Z0OI0pRSa7XWCRWtq+rI9X9n6IhCoJfWOsc5v+MSpdQvwBPAOK31t0qpCcDdwP/9jfKFEEIIIS4qlQZeSqklWutuSqlsjKsYzVWA1lrXOVNebTSl5TgXXZ23kishb3E+/yXwKhJ4CXFRmDJlSm1XQQghLmqVBl5a627Oe9+/U7hSyopxOrEt8AmwAziptbY5k+zDmHi7orz3AfeB0adECCGEEOJiV9W5Gv9bledOp7W2a61jgeZAB6BdVSumtf5Ma52gtU7w9/evajYhhBBCiAtWVYeTiCi9oJRyAeKruhGt9UlgIdAZqOfMD0ZAtr+q5QghhBBCXMwqDbyUUs85+3dFK6WynLds4DAw5yx5/ZVS9ZyPPYGrgFSMAKzkkqgRZytHCCGEEOJSUWngpbX+l7N/13ta6zrOm6/WuoHW+rmzlB0ALFRKbQBWA79prX8GngGeUEptxxhS4ovzsB9CiGrw6quv0qxZM3Pi7B9//LFW6rFt2zauueYagoODad++PUOHDuXw4cOsWbOGRx55BIBFixaxbNmyWqnfP5WTk8P9999PmzZtzLkzP//88/NWfs+ePZEheYS4MFR1OInnlFLNgFal82itF1eSZwMQV8HzOzH6ewkhLgKPP/44Tz31FKmpqXTv3p0jR45gsVS1l0LV2Ww2XFzKfyUVFBQwYMAA3n//fQYOHAgYQdbRo0dJSEggISHBfM7Hx4cuXbqc97qdjd1u/0eDuN5zzz20bt2atLQ0LBYLR48erXAA2zMdIyHExaOqnevfBpYCLwJPO29PVWO9hBDV6KuvviI6OpqYmBhuv/120tPT6dWrF9HR0fTu3Zs9e/aUyxMWFoaLiwvHjh3j+uuvJz4+noiICHMCazBGkn/88ceJiIigd+/eHD16FIAdO3bQv39/4uPj6d69O1u2bAGMAVlHjx5Nx44dGTNmDH/++SexsbHExsYSFxdHdnY2U6dOpXPnzmbQBUYLTmRkJIsWLeLaa68lPT2dCRMmMG7cOGJjY0lKSuLo0aMMHjyYxMREEhMTWbp0KUCF2wB47733SExMJDo6mldeecXc1tdff02HDh2IjY1l1KhR2O12c1+ffPJJYmJiWL58eZljtXr1arp06UJMTAwdOnQgOzubKVOm8NBDD5lprr32WhYtWsSOHTtYtWoVb775phnQ+vv788wzzwBGQNm9e3cGDRpEeHg4wDkffzCmeurQoQMhISEkJSVV7Y0ihDjvqvrX6QYgVGtdeNaUQogqSfpuG8f25pw94Tlo2MKH7kNDKk2zefNm3nzzTZYtW0bDhg3JyMhgxIgR5m3SpEk88sgj/PDDD2XyrVy5EovFgr+/P5MmTaJ+/frk5+eTmJjI4MGDadCgAbm5uSQkJDBu3Dhef/11XnvtNT7++GPuu+8+JkyYQHBwMCtXruSBBx7gjz/+AGDfvn0sW7YMq9XKwIED+eSTT+jatSs5OTl4eHiwadMm4uMrv5YnMDCQ0aNH4+Pjw1NPGf8Jb7nlFh5//HG6devGnj176NevH6mpqYwdO7bcNubPn09aWhqrVq1Ca82gQYNYvHgx/v7+TJ8+naVLl+Lq6soDDzzAN998wx133EFubi4dO3bk3//+d5m6FBUVMWzYMKZPn05iYiJZWVl4enpW+nrExMRU2oq4bt06Nm3aRFBQEMA5H38wWstWrVrF3Llzee2111iwYEGlx1QIUT2qGnjtxBgAVQIvIS5yf/zxB0OGDKFhw4YA1K9fn+XLl/P9998DcPvttzNmzBgz/bhx4/j666/x9fVl+vTpKKX46KOPmD17NgB79+4lLS2NBg0aYLFYGDZsGAC33XYbN954Izk5OSxbtowhQ4aYZRYWnvoqGTJkiHmarmvXrjzxxBPceuut3HjjjTRv3vxv7+eCBQtISUkxl7OyssjJyalwG/Pnz2f+/PnExRm9I3JyckhLS2PDhg2sXbvWnM8yPz+fRo0aAWC1Whk8eHC57W7dupWAgAAzT506ZxxnukJvvfUWM2bM4MiRIxw4cACADh06mEEXcE7Hv0TJ4/j4eNLT08+pTkKI86eqgVcekKyU+p2yk2Q/Ui21EuIycLaWqQtFSR+vEosWLWLBggUsX74cLy8vevbsSUFBQYV5lVI4HA7q1atHcnJyhWm8vb3Nx88++ywDBgxg7ty5dO3alV9//ZWIiAj+/PPcZy1zOBysWLGi3CTeFW1Da81zzz3HqFGjyqQdP348I0aM4F//+le58j08PMyAsV+/fhw+fJiEhAQeffTRCuvj4uKCw+Ewl0uOWXh4OOvXr8fhcGCxWHjhhRd44YUX8PHxMdOWPkbnevxLuLu7A0bAaLPZKkwvhKh+Ve0h+yPwBrAMYyT6kpsQ4iLTq1cvZsyYwfHjxwHIyMigS5cufPvttwB88803dO/e/Yz5MzMz8fPzw8vLiy1btrBixQpzncPhYObMmQBMnTqVbt26UadOHYKCgpgxYwYAWmvWr19fYdk7duwgKiqKZ555hsTERLZs2cItt9zCsmXL+N///memW7x4MZs2bSqT19fX1+yvBdC3b1/Gjx9vLpcEfhVto1+/fkyaNImcHOPU7/79+zly5Ai9e/dm5syZHDlyxDxWu3fvLlfvX3/9leTkZCZOnEhoaCgHDx5k9erVAGRnZ2Oz2QgMDCQ5ORmHw8HevXtZtWoVAG3btiUhIYEXX3zR7D9WUFCAMevaPz/+QogLS1WvavyyuisihKgZERERvPDCC1xxxRVYrVbi4uIYP348d955J++99x7+/v5Mnjz5jPn79+/PhAkTCAsLIzQ0lE6dOpnrvL29zY7ijRo1Yvr06YARzN1///28+eabFBcXM3z4cGJiYsqV/cEHH7Bw4UIsFgsRERFcffXVuLu78/PPP/PYY4/x2GOP4erqSnR0NB9++CHHjh0z8w4cOJCbbrqJOXPmMH78eD766CMefPBBoqOjsdls9OjRgwkTJpxxG6mpqXTu3BkwOql//fXXhIeH8+abb9K3b18cDgeurq588skntGrV6ozHx83NjenTp/Pwww+Tn5+Pp6cnCxYsoGvXrgQFBREeHk5YWBjt27c380ycOJGnn36atm3b0qBBAzw9PXn33XfP2/EXQlw41Jn+VZVJpNQuyk6SDYDWunV1VOp0CQkJWsagEZeC1NRUwsLCarsa1cbHx8dsNRI172I+/pf6Z0NcXpRSa7XWCRWtq2ofr9KZPYAhQP1/WjEhhBBCiMtJlfp4aa2Pl7rt11p/AAyo3qoJIS42F2try6VCjr8QF74qtXgppdqXWrRgtIDJ8MlCCCGEEOegqsFT6RECbUA6xulGIYQQQghRRVW9qvHK0stKKSswHNhWHZUSQgghhLgUVdrHSylVRyn1nFLqY6XUVcrwELAdGFozVRRCCCGEuDScrXP9f4FQYCNwL7AQ4xTjDVrr66q5bkKIWvbqq6/SrFkzYmNjiYyM5Mcff6yVemzbto1rrrmG4OBg2rdvz9ChQzl8+DBr1qzhkUeMCTQWLVrEsmXLaqV+58urr77K2LFjAWMicBlGR4hLz9lONbbWWkcBKKUmAgeBllrriuenEEJcckqmDEpNTaV79+4cOXKk0gmd/y6bzYaLS/mvpIKCAgYMGMD777/PwIEDASPIOnr0KAkJCSQkJJjP+fj40KVLl/Net7Ox2+3m9EG17UzHUQhxYTjbt2dxyQOttR3YJ0GXEBe/r776iujoaGJiYrj99ttJT0+nV69eREdH07t3b/bs2VMuT1hYGC4uLhw7dozrr7+e+Ph4IiIi+Oyzz8w0Pj4+PP7440RERNC7d2+OHj0KGNP09O/fn/j4eLp3786WLVsAGDlyJKNHj6Zjx46MGTOGP//8k9jYWGJjY4mLiyM7O5upU6fSuXNnM+gCozUoMjKSRYsWce2115Kens6ECRMYN24csbGxJCUlcfToUQYPHkxiYiKJiYksXboUoMJtALz33nskJiYSHR3NK6+8Ym7r66+/pkOHDsTGxjJq1ChzWh8fHx+efPJJYmJiWL58uZl+9erV5oTUc+bMwdPTk6KiIgoKCmjd2hhz+vPPPycxMZGYmBgGDx5MXl7eGV8rh8PByJEjzSmFnn76abOen376KWAEnd27d2fQoEGEh4dX9W0ghKgFZ/tbFKOUynI+VoCnc1kBWmtdp1prJ8Qlbvprz541Tev2HUgceKOZPuKKPkT27ENeViY/jSs7efOwV94+a3mbN2/mzTffZNmyZTRs2JCMjAxGjBhh3iZNmsQjjzzCDz/8UCbfypUrsVgs+Pv7M2nSJOrXr09+fj6JiYkMHjyYBg0akJubS0JCAuPGjeP111/ntdde4+OPP+a+++5jwoQJBAcHs3LlSh544AH++OMPAPbt28eyZcuwWq0MHDiQTz75hK5du5KTk4OHhwebNm0iPj6+0n0KDAxk9OjR+Pj4mBN633LLLTz++ON069aNPXv20K9fP1JTUxk7dmy5bcyfP5+0tDRWrVqF1ppBgwaxePFi/P39mT59OkuXLsXV1ZUHHniAb775hjvuuIPc3Fw6duzIv//97zJ1iYuLM+eFTEpKIjIyktWrV2Oz2ejYsSMAN954I/feey8AL774Il988QUPP/xwuf2y2WzceuutREZG8sILL/DZZ59Rt25dVq9eTWFhIV27dqVv374ArFu3jk2bNhEUFHTW94AQovZUGnhprS+MtnMhxHnzxx9/MGTIEBo2bAhA/fr1Wb58Od9//z0At99+O2PGjDHTjxs3jq+//hpfX1+mT5+OUoqPPvqI2bNnA7B3717S0tJo0KABFouFYcOGAXDbbbdx4403kpOTw7Jlyxgy5NQINIWFhebjIUOGmKfpunbtyhNPPMGtt97KjTfeSPPmzf/2fi5YsICUlBRzOSsri5ycnAq3MX/+fObPn09cXBxgDESalpbGhg0bWLt2LYmJiQDk5+fTqFEjAKxWK4MHDy63XRcXF9q0aUNqaiqrVq3iiSeeYPHixdjtdnPy8U2bNvHiiy9y8uRJcnJy6NevX4X7MGrUKIYOHcoLL7wAwPz589mwYYM5EXZmZiZpaWm4ubnRoUMHCbqEuAhIRwAhalFVWqjOlN6rTt1zzv93lPTxKrFo0SIWLFjA8uXL8fLyomfPnhQUVNwDQSmFw+GgXr16ZivQ6by9vc3Hzz77LAMGDGDu3Ll07dqVX3/9lYiICP78889zrrfD4WDFihV4eHiUeb6ibWitee655xg1alSZtOPHj2fEiBH8619lWxYBPDw8zICxX79+HD58mISEBCZOnEiPHj345ZdfcHV1pU+fPowcORK73c57770HGKdYf/jhB2JiYpgyZQqLFi2qcB+6dOnCwoULefLJJ/Hw8EBrzfjx48sFaosWLSpzHIUQF67z30NWCHFB69WrFzNmzOD48eMAZGRk0KVLF7799lsAvvnmG7NlpiKZmZn4+fnh5eXFli1bWLFihbnO4XCYrTFTp06lW7du1KlTh6CgIGbMmAGA1pr169dXWPaOHTuIiorimWeeITExkS1btnDLLbewbNky/ve//5npFi9ezKZNm8rk9fX1NftrAfTt25fx48ebyyWBX0Xb6NevH5MmTTKn3Nm/fz9Hjhyhd+/ezJw5kyNHjpjHavfu3eXq/euvv5KcnMzEiRMB6N69Ox988AGdO3fG39+f48ePs3XrViIjIwHIzs4mICCA4uJivvnmmzMe67vvvptrrrmGoUOHYrPZ6NevH//3f/9HcbHR/Xbbtm3k5uaeMb8Q4sIjLV5CXGYiIiJ44YUXuOKKK7BarcTFxTF+/HjuvPNO3nvvPfz9/Zk8efIZ8/fv358JEyYQFhZGaGgonTp1Mtd5e3uzatUq3nzzTRo1asT06dMBI5i7//77efPNNykuLmb48OHExMSUK/uDDz5g4cKFWCwWIiIiuPrqq3F3d+fnn3/mscce47HHHsPV1ZXo6Gg+/PBDjh07ZuYdOHAgN910E3PmzGH8+PF89NFHPPjgg0RHR2Oz2ejRowcTJkw44zZSU1Pp3LkzYHSc//rrrwkPD+fNN9+kb9++OBwOXF1d+eSTT2jVqlWlx7hjx44cPnyYHj16ABAdHc2hQ4dQSgHwxhtv0LFjR/z9/enYsWOZgPF0TzzxBJmZmdx+++188803pKen0759e7TW+Pv7l+uLJ4S4sCmtdW3X4awSEhK0jGcjLgWpqamEhYXVdjWqjY+Pj0zULP6WS/2zIS4vSqm1WuuEitbJqUYhhBBCiBoigZcQ4ryR1i4hhKicBF5CCCGEEDVEAi8hhBBCiBoigZcQQgghRA2RwEsIIYQQooZI4CWEqLK33nqLiIgIoqOjiY2NZeXKlYAxV2LpMbVKdOnSBYD09HSmTp1qPp+cnMzcuXNrptKlrFq1ip49exIcHEz79u0ZMGAAGzduPC9lp6enmwOkCiHEmcgAqkKIKlm+fDk///wz69atw93dnWPHjlFUVFRpnmXLlgGnAq9bbrkFMAKvNWvWcM0111R5+zabDReXv/+VdfjwYYYOHcrUqVPNgHDJkiXmSPbnc1tCCHEm0uIlxGXo+uuvJz4+noiICD777DPsdjsjR44kMjKSqKgoxo0bVy7PwYMHadiwIe7u7gA0bNiQpk2blkmTn5/P1Vdfzeeffw4YA6qCMT9iUlISsbGxvPPOO7z88stMnz6d2NhYpk+fTm5uLnfddRcdOnQgLi6OOXPmADBlyhQGDRpEr1696N27d7k6vfPOO0RFRRETE8Ozzz4LQM+ePSkZcPnYsWMEBgYC8PHHHzNixAgz6ALo1q0b119/PWDMnzh69Gg6duzImDFjWLVqFZ07dyYuLo4uXbqwdetWs07XXXed2XL22muvmeXZ7XbuvfdeIiIi6Nu3L/n5+ef2wgghLnnyl06IWnLypx0UHTi/8+y5NfWm3sA2Z003adIk6tevT35+PomJicTHx7N//35z/sOTJ0+Wy9O3b19ef/11QkJC6NOnD8OGDeOKK64w1+fk5DB8+HDuuOMO7rjjjjJ53377bcaOHcvPP/8MQOPGjVmzZg0ff/wxAM8//zy9evVi0qRJnDx5kg4dOtCnTx8A1q1bx4YNG6hfv36ZMn/55RfmzJnDypUr8fLyIiMjo9J93rx5MyNGjKg0zb59+1i2bBlWq5WsrCySkpJwcXFhwYIFPP/888yaNQswTllu2rQJLy8vEhMTGTBgAA0bNiQtLY1p06bx+eefM3ToUGbNmsVtt91W6TaFEJeXamvxUkq1UEotVEqlKKU2K6UedT5fXyn1m1IqzXnvV111EEJU7KOPPiImJoZOnTqxd+9eioqK2LlzJw8//DDz5s2jTp065fL4+Piwdu1aPvvsM/z9/Rk2bBhTpkwx11933XXceeed5YKuqpg/fz5vv/02sbGx9OzZk4KCAvbs2QPAVVddVS7oAliwYAF33nknXl5eABWmqUzHjh0JCwvj0UcfNZ8bMmQIVqsVMCYDHzJkCJGRkTz++ONs3rzZTHfVVVfRoEEDPD09ufHGG1myZAkAQUFBxMbGAhAfH096evo51UkIcemrzhYvG/Ck1nqdUsoXWKuU+g0YCfyutX5bKfUs8CzwTDXWQ4gLUlVapqrDokWLWLBgAcuXL8fLy4uePXtSWFjI+vXr+fXXX5kwYQLfffcdr732GgMHDgRg9OjRjB49GqvVSs+ePenZsydRUVF8+eWXjBw5EoCuXbsyb948brnlFnMy6KrSWjNr1ixCQ0PLPL9y5Uq8vb3Nx6NGjQLg9ddfP2NZLi4uOBwOAAoKCsznIyIiWLduHdddd51Z3syZM81WOMDcFsBLL73ElVdeyezZs0lPT6dnz57mutP3r2S55DQsgNVqlVONQohyqq3FS2t9UGu9zvk4G0gFmgHXAV86k30JXF9ddRBClJeZmYmfnx9eXl5s2bKFFStWcOzYMRwOB4MHD+bNN99k3bp1tGjRguTkZJKTkxk9ejRbt24lLS3NLCc5OZlWrVqZy6+//jp+fn48+OCD5bbp6+tLdnb2GZf79evH+PHj0VoD8Ndff5Uro2PHjmZ9Bg0axFVXXcXkyZPJy8sDME81BgYGsnbtWgBmzpxp5n/wwQeZMmWK2eEfMPOe6Tg1a9YMoEzLHsBvv/1GRkYG+fn5/PDDD3Tt2vWM5QghRGk10rleKRUIxAErgcZa64POVYeAxjVRByGEoX///thsNsLCwnj22Wfp1KkT+/fvp2fPnsTGxnLbbbfxr3/9q1y+nJwcRowYQXh4ONHR0aSkpPDqq6+WSfPhhx+Sn5/PmDFjyjwfHR2N1WolJiaGcePGceWVV5KSkmJ2rn/ppZcoLi4mOjqaiIgIXnrppSrtx6BBg0hISCA2NpaxY8cC8NRTT/F///d/xMXFlRniokmTJkyfPp3nnnuOtm3b0qVLF2bOnMlDDz1UYfljxozhueeeIy4uDpvNVmZdhw4dGDx4MNHR0QwePJiEhISz1lcIIQBUyT/MatuAUj7An8BbWuvvlVIntdb1Sq0/obUu189LKXUfcB9Ay5Yt43fv3l2t9RSiJqSmphIWFlbb1RD/wJQpU8pcGCDOD/lsiEuJUmqt1rrCf2TV2uKllHIFZgHfaK2/dz59WCkV4FwfABypKK/W+jOtdYLWOsHf3786qymEEEIIUSOq86pGBXwBpGqt3y+16keg5JruEcCc6qqDEEKcbyNHjpTWLiHE31adVzV2BW4HNiqlkp3PPQ+8DXynlLob2A0MrcY6CCGEEEJcMKot8NJaLwHOdE15+SGohRBCCCEucTJlkBBCCCFEDZHASwghhBCihkjgJYSosrfeeouIiAiio6OJjY1l5cqVgDFoaekxs0qUTEidnp7O1KlTzeeTk5OZO3duzVS6AiX1TU9PJzIystbqIYS4/EjgJYSokuXLl/Pzzz+bk1YvWLCAFi1aVJqnZJT48xF4nT6IaW270OojhLg4SOAlxGXo+uuvJz4+noiICD777DPsdjsjR44kMjKSqKgoxo0bVy7PwYMHadiwoTkfYcOGDWnatGmZNPn5+Vx99dV8/vnngDGxNsCzzz5LUlISsbGxvPPOO7z88stMnz7dHLk+NzeXu+66iw4dOhAXF8ecOcYoM1OmTGHQoEH06tWL3r3LXpPz4IMP8uOPPwJwww03cNdddwEwadIkXnjhhQr3szI7d+4kLi6O1atXs2PHDvr37098fDzdu3dny5YtgDGUxOjRo+nYsWO50fmFEKIqqnM4CSHEWUyePPmsaUJCQsy5ACdPnkxsbCxxcXHk5uby3XfflUl75513Vmm7kyZNon79+uTn55OYmEh8fDz79+9n06ZNAJw8ebJcnr59+/L6668TEhJCnz59GDZsGFdccYW5Picnh+HDh3PHHXdwxx13lMn79ttvM3bsWHNC6saNG5cZ/f3555+nV69eTJo0iZMnT9KhQwf69OkDYLaw1a9fv0yZ3bt3JykpiUGDBrF//34OHjRmIktKSmL48OEV7ufgwYNp0KBBuX3bunUrw4cPZ8qUKcTExNC7d28mTJhAcHAwK1eu5IEHHuCPP/4AYN++fSxbtgyr1VqlYy2EEKVJi5cQl6GPPvqImJgYOnXqxN69eykqKmLnzp08/PDDzJs3jzp16pTL4+Pjw9q1a/nss8/w9/dn2LBhZSaPvu6667jzzjvLBV1VMX/+fN5++21iY2Pp2bMnBQUF7NmzB4CrrrqqXNAFpwKvlJQUwsPDady4MQcPHmT58uVm37LT97P0JN8ljh49ynXXXcc333xDTEwMOTk5LFu2jCFDhhAbG8uoUaPMoA5gyJAhEnQJIf42afESohZVtYWqovTe3t7nnB9g0aJFLFiwgOXLl+Pl5UXPnj0pLCxk/fr1/Prrr0yYMIHvvvuO1157jYEDBwIwevRoRo8ejdVqpWfPnvTs2ZOoqCi+/PJLRo4cCUDXrl2ZN28et9xyC8bEFVWntWbWrFmEhoaWeX7lypV4e3ubj0eNGgXA66+/zqBBgzh58iTz5s2jR48eZGRk8N133+Hj44Ovr2+F+1lQUFBu23Xr1qVly5YsWbKE8PBwHA4H9erVIzk5ucK6ltRHCCH+DmnxEuIyk5mZiZ+fH15eXmzZsoUVK1Zw7NgxHA4HgwcP5s0332TdunW0aNGC5ORkkpOTGT16NFu3bi3TYpScnEyrVq3M5ddffx0/Pz8efPDBctv09fUlOzv7jMv9+vVj/PjxaK0B+Ouvv8qV0bFjR7M+gwYNAqBTp0588MEH9OjRg+7duzN27Fi6d+9+xv2siJubG7Nnz+arr75i6tSp1KlTh6CgIGbMmAEYQeH69eurfHyFEKIyEngJcZnp378/NpuNsLAwnn32WTp16sT+/fvp2bMnsbGx3HbbbfzrX/8qly8nJ4cRI0YQHh5OdHQ0KSkpvPrqq2XSfPjhh+Tn55freB4dHY3VaiUmJoZx48Zx5ZVXkpKSYnauf+mllyguLiY6OpqIiAheeumlKu1L9+7dsdlstG3blvbt25ORkWEGXhXt55l4e3vz888/M27cOH788Ue++eYbvvjiC2JiYoiIiDA7+wshxD+lSv5hXsgSEhL0mjVrarsaQvxjqamphIWF1XY1hLjgyGdDXEqUUmu11gkVrZMWLyGEEEKIGiKBlxBCCCFEDZHAS4gadjGc3heiJslnQlxOJPASogZ5eHhw/Phx+aERwklrzfHjx/Hw8KjtqghRI2QcLyFqUPPmzdm3bx9Hjx6t7aoIccHw8PCgefPmtV0NIWqEBF5C1CBXV1eCgoJquxpCCCFqiZxqFEIIIYSoIRJ4CSGEEELUEAm8hBBCCCFqiAReQgghhBA1RAIvIYQQQogaIoGXEEIIIUQNkcBLCCGEEKKGSOAlhBBCCFFDJPASQgghhKghEngJIYQQQtQQCbyEEEIIIWqIBF5CCCGEEDVEAi8hhBBCiBoigZcQQgghRA2RwEsIIYQQooZI4CWEEEIIUUOqLfBSSk1SSh1RSm0q9Vx9pdRvSqk0571fdW1fCCGEEOJCU50tXlOA/qc99yzwu9Y6GPjduSyEEEIIcVmotsBLa70YyDjt6euAL52PvwSur67tCyGEEEJcaGq6j1djrfVB5+NDQOMzJVRK3aeUWqOUWnP06NGaqZ0QQgghRDWqtc71WmsN6ErWf6a1TtBaJ/j7+9dgzYQQQgghqkdNB16HlVIBAM77IzW8fSGEEEKIWlPTgdePwAjn4xHAnBrevhBCCCFEranO4SSmAcuBUKXUPqXU3cDbwFVKqTSgj3NZCCGEEOKy4FJdBWutbz7Dqt7VtU0hhBBCiAuZjFwvhBBCCFFDJPASQgghhKghEngJIYQQQtQQCbyEEEIIIWqIBF5CCCGEEDVEAi8hhBBCiBoigZcQQgghRA2RwEsIIYQQooZI4CWEEEIIUUMk8BJCCCGEqCESeAkhhBBC1BAJvIQQQgghaogEXkIIIYQQNUQCLyGEEEKIGiKBlxBCCCFEDZHASwghhBCihkjgJYQQQghRQ1xquwIXioItW3Dk56MsFlAKVMk95nMWT0/cWrUCoGjfPpSrK66NGwNQvH8/WmuUUkY+sxxlPqfc3bH6+gJgz85Gubpi8fBAa40uKDDzKTiV32Ix8gshhBDioieBl9OBZ56lcOvWStN4REURNOM7APY9+BCuzZvT4pOPAdg1ZCj2jIxK8/v07m2m39GvP779+hLwyitgt7M1rn3lFVQKv5uH0+Tll9HO9A0ffJCGo+6j+NAhdg64tsJgr+Q5pRQN7rmb+iNGUHz4CLtvvplGTz1JnWuuoWDrVvY/8mipYK8k/6nysCj8H3gA3z59KExL48CLL9L4mWfxah9H3po1HPngA5SylNom5Zb9H34Yz6go8tev59hnn9P42Wdwa9GCnKQlnPx+ljPALFVnS9ll/4cexLVpU3JXrSLrf3Np9PRTWH18yF60iNylyyrcZun9aDjqPize3uSuWEHe2rX4P/ggADl//klhWhpYrCgXK1itKKsLWC0oq4v5XN0BAwAjSLefPIl3p07G8rZtOHJyjAC9JL2zLGV1lufmZgbp9pxco4re3gBou10CbCH+Bq01OBxgt6NL7p03HA603Y7Fyxurjze6uJji/fuxNmyI1ccHR34+xQcPGd8zVisoi/HYYjE+j857i48PFjc3tM2GLixEeXigrFZje1rLZ1ecMwm8nJq88gqO3BzQ+tQHqtRNOxxY69Qx0zd68gksnp6n8r/4Ao6CQmd6h/MLQZdZdmvWzEzv/8jDZusZFgv+Tz4BGjP9qXpgfLGg8YiMMvPXv+N2PKONZYunJ/VuGlzhNin1nGvLlgAoN1e8EhNx8fc38nt44BERAWh0SX7nNs1lrVEl+2u1YvXxRbmeevsoi9VZZzuUfOlpjdan9kEX2wCML7x9+8xl+8mTFG7dZmxTazSl9sPhMJcb3DkSgOK9+8hesAD/xx4FoDA1lcw5c07l16X2oeT105r6I+4wA6/jn080A6+sX+eT+f33lb9BXFzMwCvjy6/IXbGC4IV/AHDkvbHkJiVVnj0gwEy//7HHsGdlEfTddAB23TjYCPqtVuPL3sXlVMDmvHlERtLi//4DwN4HH8I1IIAmL74AwJ577sWRm3sqULRawcVqvCYuRhDpGRNDg7vuBODwv/6FR2QUdQdei3Y4OPLOu+XSK6sziLRawWrBo10Y3p06oh0OMmf/gEdkBB6hoTjy88lJSipXX8wyrCgXF1waN8a1USN0cTFFe/fh0sgfq48PuqgIe2bmqX0+PWC1XFq9IUremyX75SgsRBcVlQscyixrjXtQEABF+/bjyMvFIyQEgIKt27CfPAkOO9rucN47P382OzjsKA8PfK+8EoCcpCS0zWYuZ/74I7aMDLA7nJ/d8veuTQPwGz4cgGMTPsXFvyH1Bg8G4NDrb+DIzXVus3QdTgVDXvHtaTh6NAB77r0Pn25dqT9iBI6CAtKH31x2Xyu4r3fTYBo99hiO/Hy2de2G/8MP0+DOkRTt3s2Ofv3PeswbPfUkDe65h+IDB9jR/2qavvM2da+7joLNm9l92+1nzV+SPj85md233U7LyZPw7tyZ7F9/Zf/jTxiJVPmATSkjoGv+8cd4d+xA9h9/cPCVV2j11Ve4BwVxctYsjv3fBONzqyzO/Mp4bLWaj5t9MA63Fi3I+uUXMr7+hhafforVx5sTM2aQPe/XU/ksVuf2nX82neUGvPkGFi8vsn75hdwVKwl47VUATn4/m4JNG8ukLVeWm6v5PZn9xx/YDh/G7+abAciaN4/iQ4ec++usb+ljoCxYfH2o07cvALkrV4HWeHfqaCyvWIkjP8/4rCvndk8LgC2+vuZ7vXD7dpSHJ27Njd/Rwp27jENvBsvWU4+VBWW1oDw8sPr4AGDPyUG5uWFxczvra17dJPBy8mofd07pfXr0KLNc55przil/yRcZGKcyG957b5XzKquVRk89ZS5b69al8XPPVTm/i58fTd9521x2a9WKZu//u8r53Vu3puUXE81lr4QEWn31ZZXze3fqROs5P5jLdQdeS92B11Y5f73BN1Jv8I3mcsP776fh/fdXOX+jxx7D/9FHzeWA116lyUsvnvrBs9vRNpvzsQPsNuO+ZHsPPkD9O059YTd64nFsI0eU/dG02c0fQW2zY/FwN9P7DR+Go7Dw1PLNw7EdOWoGreY2bXbjOZsd11JBu1vLlrg0bGguW3x8jG3Z7Gi78a/c+NdvM35A7TYzyAbIXbYM5e4BgLbZODljBtrZOoDNZgSrp/G79VYj8LLZOPjCC/g/9hgeoaHYjmcYraVn4f/YYzQcPYriw0fYec01BLz1FvUG30j+5s3svvmWM2dUClxcCHjjdepdfz35Gzey9977aPbBB3h36khOUhKHXn+jwmDTDGItFhqNeRrPqCjy1v3F8S++oMnzz+HarBnZixaROWtWmUChovum772LW4sWZP70M8c//5xW33yN1deX4198wYlp35YLVk4vI/jPRVjr1uXov/9Nxlf/pd2G9QAcevllMuf8WOmxU25uZvqjH31I/rq/aLvgNwAOv/0v8pavqDS/a7NmZqCVMXkyjvwCc/nYZ59RtH3HmY+91YpX+/bm91XOokW4BQWZgVfe6tU48vJOBckV3Dvy8svsCy7Gz46yWHBt2tQI0C3WcvdYLSiLFY/QUCO9iwt+w4bhERYGgLVePRo++KCZrsJ7qxXPmBgjfYOGNH33HTzjjO96t6Agmo4da3x2HA7jz17px9qBtjvwiDL+4Lo2b06jp5/CzfkH1r1tWxo+/NCptA6H8dprh/GH1W5HawcujYzPnot/I3yv7GW2dLs0aoRn+zjndh3Gn1SzDs4/jg4Hynm8UBaUq6vzTADooiIcOTlGMO/cllmWw17qsfF5Ltq9h7wVp94r+RvWk/3rfOf+Osw/y6X/OFvc3E79Qf1lHvnr15uB14lp35K3cmXl772WLc3A69h//oO228zA69Brr1G0a1el+T0T4gn8+msA9j38CO7tQmk+bhwA6UOH4sjJqTS/79X9zfTbe15JvcE3ntNvZXVRuoIv2QtNQkKCXrNmTW1XQ4jLQpmgw2ZD2+0oFxcsXl5orbEdOIDF1xdrnTrooiIKd+0yAlWHo4KA1Qg+3QMDcQsMxJGbS/YfC/GMjcGtRQtsR4+S/fvvRqBaOti0nwoksTvw7dsXz8gIivbuJWPyFPxuvQX3Nm3I37CBjP9+XeoU06lgE9upfWj0zBg8o6LIWbqUI++NpfmHH+DWqhWZP/3E8c8+rzRwUFYLTV5/HbfmzcleuJCTs2bR9O23sfr4kDV3LtmLFp3hh/9UGf4PPYjFy4vcZcvIX7/e/KOQs3gxhTt2Vhx8OFsNlYuL+ccuf/NmHJmZeHfpAkBBSgr2rOxTrYtmnU9tX7m5ma3rtmPHQClcGjQAwJ6VZbzo5jZLlSGnz0QpJZ9x5WwxcuTlnfrcl5ydsNtPnamwO1AWhWvTpsCpftBuzZsDUJiWhqOgwBkclgr+zKDXjtXXF8/oaAByly/H4uODpzMQzvp1Prq42ExbUQDs2rwFPt27AZDx9Te4t21rBn7VTSm1VmudUOE6CbyEEEIIIc6fygKvS6sDhRBCCCHEBUz6eIlLX0ln+5IrNO3FYC8CN6OvBcUFxgUNFhejb0nJlZFCCCHEeSaBV4niAtB25xWFxpU5p64wdJz6YfZxdlLO3G/8SPs2MZaPpRk/6KXL0I6yZXjUg0btjPTpS8HbH/xDjHPbafNPy1dBGQ3aQPMEI/1fX0FALDSNhcJsSJ5aSX5nGYHdoFUXyD8JSz+EsIHQrD2c3AMrJpwhn/1U/rjbjPzHd8DCt6DrYxAQDXtXw9IPyuZzVHAcrnrNqP+uJFjwKtz4mbFPm2ZB0rgKjt1p+W+daRy/df+F31+DB1eBV334811Y+tGZ8+I8nf70DvBuCIv+Zez/y8eN5//3JCR/Xfb9YHEpdbOC1Q2e3m6s++1l2LMS7v71VP59q8vmUZayyz7+MGi8kX7pR1CcDz2fMZaT3oecI84rg1zKb9tiBd+mED3ESJ/yI7j7QhujgzRb5hrHTllL5XEpW56nn3GswXj93OsYddLauW2XCrZvlQBU/DMlf3pKrk4tyjPeVy7uxvN5GWW/Z8p9bzjAoy74NDLWHd4EPk3At7HxGTq4/rQ8JeWUyu8farz3C7Nh26/QPBH8WkHuMdi5yPlHy1LqT1fJVXbO5xuFG9/zBVlwJAX824FnPcg/YXx3mumtp8oqvezVEFw9wFYIRbnG/lisxu+Fw1Z+m+KSJ4FXiYl94PDGytM0i4d7jSEBmDoM6rWEm6cay5P6Q96xyvOHDjiV/rs7IPw6uPZ948th2rCz1zHhbiNw0Q746VG48kUj8CrIhF/GnD3/lS8agVNRDiwbb3wZNWtvfAGt+6rUuF2Wir9A2vY2yinOM77wCrNOLZ9IryDvaV9EJf0JrW7Gl0/Jj7qbr3EslSr/5VX6VtJC5RdoBI1WV2M5IBba31Fx3UvfXJ3DYbTtY2y/RMQN0DDY+BJ02I0vb4ft1LLjtCv96rcGW9GpZa+G4BtQPo+t4NRj+6mrGDmSYvwIlNg2D46knrY9e9nXrnniqcBr0b+M164k8JrzgPEjUJl218Lwb4zHE/tA5GAYMNb48v93yJnzlQSQifdC//9npH8/DLo/CZ3uh6wDMOXaUsGapeLgMe52iLrJeK/9/Dh0uBeCehhB4JL3y+c5PXBtdw00iYLsQ7B5NrQbYLxnTuyG3UudQWclgWvjSCNIL/mxbBhivB/yMowyy/zw6/KBQLP2xvvvxG44tg1a9zTef4dT4Hha+YDBXHbex95qBBu7l8OhjdDxPuP4bv0FDm8+wx8W52NlNf60APz1DZzYBb1eNJaXfGC8d8oFHqX+OHo3hOuM8QOZ/6Lx3r3mXWN55t1GeeX+bJYqJyAGhkw20k+5Fhq0hYEfGMsfREPByVKBzmnHDQ3h18NQ51XP/24HsbfA1W8bgdN7rSt/3wJ0HA1Xv2MELp/2gD6vQrfHIfsgTOp39vwl6XOOwKy74YZPjcDr2DZj+Wxu+BRihhtB3+Sr4Y45xuu/4w+YedfZ85ek3/I/mHknPLDS+AO56nP4tYIr7MoEYla470/jD/rqL4w/vA+vMwK/xWNh7RTn997pwV+pQG7Ez+BRx9he6k8wwnkV7ZJxsGtx5YGjiwdcbwxjw9ovje/5Pq8Yyys/g4wdzvyqgm1bwbP+qff6pu+N90TUTcbyhhlQmFk+j7msjIA7qIeRftdi4w9j01hjOX2J8/Nxev5Sx8PTD+q1MNJn7AT3uuDd4OyvWTWTwKtEp/uNwKlc4FDqx9z71CX59HoR3LxOLQ/6yPhRqugNXFJG6fw3f2v8EICR/t6FlQcNSp0KFixWeCLVaPUA40d/zK6zBx7K+W+qbnN4uVSQ2Kw9PL+v6seqSRQ8vPbUcusr4P6lVc/fsiPc/v2p5ZC+xq2qgrobt7+bv1UX41YiuI9xq6r4kWWXrzzHy5NvmFB2+e755dNofSoIc9jKrrtjzqnXEuCu+cap09KBW0nwVvKcV6kvm2vHnfoyUhYY8G/nFUW28sFjSTnNE0/lDxto/PgCWFyhaVzZfKcHrsVFxmcDjB/PY2lG6wEYP9o7Fp552yX7Xq+l8b47kQ7znjVaMeq1hAPr4IcqDCVy+2xo8//bu/9gO8r6juPvT24SjQEJiiQZQnqZgUGTDEZsERAziJYSpTAqHdIZKVg7FAsV7Q9G/cOi/1jrjLV1HCjir7YwpERhIo0ITqmDU5JCkBJItJMGMIlIIpQQhCbm5ts/9jnczTlnz9mb3LN7757PK3Mn5+w+u/t8z7Nnz3ef/XUebPsh3H45fOQBmL8EHl2dza+fP1kPx78JfrouK3/dE9n399Hbst7Tfpa+L0u8/vtuWH/D+I/R43dm88jL77C0fvxaidfPH4adG8cTr2ceg+0bOqfJb7/yxtrWp9mvyXri26fJz+e4XGK++Mxse9Oy5OJs3eu2o9aqU6uXH+Cdnxp/PzIbVn6hc3ntsbR6ame+ClbdmvU4Qdbzddkd3afJ1+e12VV1HHMiXP1g1lsG2Q7b1Q/27nE7ODa+rh//Jvjgd2DBaemzOAsuvaWgpy03v+NOTct7M6z8myyZgGw7+O7ri5P91vs587Lyrz95fD2CbAfwpBWd5dtjmDGSlW/1NLYc2Jd9D3vVfyR3z6tnHst2uFu23Zcduem67LHxOrbW9Ye+no1vJV4//Hy209LL4rPHE6+7/gwWLIPf+2b2/tZVsH9v4aQALH3/+E7DjSvg9Mvggs/1nqYCvqrRzKa2gwez/2fMyBKH/Xth1lyYOTs7dPOr3blkrUtv5cEDMH9plii98HSWvIy+I+sFePZ/sh6oXoeaNDLe47X3GdizPfsRHZmV9Za99GzxdK35zj0+G77/pSxRaf2YtnpO83vrZk3Q6j1tJX6/fjkb1uqweOm53M5at97asSxRPHY0K//M5uyQ7etSL+nPNsDBX7dNH4e+P3phdpQIslNajj0p+y5XwLeTMDMzM6uIbydhZmZmNgU48TIzMzOrSC2Jl6QLJP1U0lZJJc5sNTMzM5v+Kk+8JI0AXwFWAkuA35e0pOp6mJmZmVWtjttJnAFsjYhtAJJuAy4GNtdQFwB+cMt6xp7KLksVeuV+mzD+8tBrjQouSGgbrBLXLYjOq5iCKJy28Jqn9mWnku2zOWToRJYRPceWqpeIriVVXJVDPoeJXu9V5vOfyHy71r7HMrpHOwGTeN1LUT0KP/ca6jJhR1inbt+9ic1cPd6Vnaq8I/ncBr3M9nLF6365Ob5Sqk8z1PFZHonDW273LWTv9Xdwev8+9Cp0mPMqPaco+HXp9Nz58zjn3csPZ0GToo7E6wRge+79DqCax4UX+L8tu1h+4Ni+5YquAC27npQpNxnz6jeP6JU4qmAe5XLNguGD+dwK4ygx/85x5b/tg7wOOLq8mvx5H6Yemexk17bM+lNu2omXPJK1deLr9GA+0/Zpo2COg98mDepzKx46mdvjiUwxmd+B8jFMdA+93/IOs72OIAcsP69+W/zyxjRnkuZ0eKbsDVQlXQlcCbB48eKBLmv0mtPYtGd3fuHjL1HXW+sU7W1I3feC24d3luic/0SW274MFQwvM68Zfe4l1D7dIctVcbn8mIksV2p/0Tl/FQw/dB7FcXVbdvf6qOP4fOHnq2516WzXVplubVbcdl2mz3/2betwx5QF7aTO0YXr8Pj7znboXEe6fTc64x6vQ7c2LNdGSv86hnc5sWJGr+8T6tG23dfubp9lfppur81suNSReO0ETsy9X5SGHSIibgJuguw+XoOs0LIFoyxbMDrIRZiZmZnVclXjg8Apkk6SNBtYBaytoR5mZmZmlaq8xysiDki6Bvg+MAJ8PSIer7oeZmZmZlWr5RyviFgHrKtj2WZmZmZ18Z3rzczMzCrixMvMzMysIk68zMzMzCrixMvMzMysIk68zMzMzCrixMvMzMysIk68zMzMzCqiogc/TyWSdgNPDXgxxwG/HPAyprJhjt+xD69hjn+YY4fhjt+xD95vRMQbuo2YFolXFSQ9FBG/WXc96jLM8Tv24Ywdhjv+YY4dhjt+x15v7D7UaGZmZlYRJ15mZmZmFXHiNe6muitQs2GO37EPr2GOf5hjh+GO37HXyOd4mZmZmVXEPV5mZmZmFRmqxEvSiZLuk7RZ0uOSru1SRpL+XtJWSY9KOr2Ouk62krGfK2mPpEfS36frqOsgSHq1pP+U9F8p/s90KfMqSatT22+QNFpDVSddydivkLQ71/Z/VEddB0XSiKQfS7qry7hGtnten/ib3vZPStqUYnuoy/hGbvOhVOxN3ubPk7RG0k8kbZF0Vtv42tp9ZlULmiIOAH8eEQ9LOhrYKOneiNicK7MSOCX9vQ24If0/3ZWJHeD+iLiwhvoN2j7gvIh4UdIs4EeSvhcR63NlPgz8b0ScLGkV8Hng0joqO8nKxA6wOiKuqaF+VbgW2AK8tsu4prZ7Xq/4odltD/DOiCi6d1NTt/ktvWKH5m7z/w64OyIukTQbeE3b+Nrafah6vCLi6Yh4OL3eS7YhOqGt2MXAP0ZmPTBP0sKKqzrpSsbeWKk9X0xvZ6W/9hMcLwa+lV6vAd4lSRVVcWBKxt5YkhYB7wVuLijSyHZvKRH/sGvkNn+YSToGWAF8DSAi9kfE823Famv3oUq88tLhhLcAG9pGnQBsz73fQcMSlB6xA5yVDkl9T9LSams2WOlwyyPALuDeiChs+4g4AOwBXl9pJQekROwAH0hd7msknVhtDQfqS8B1wMGC8Y1t9+RL9I4fmtv2kO1k3CNpo6Qru4xv8ja/X+zQzG3+ScBu4BvpEPvNkua2lamt3Ycy8ZJ0FPBt4GMR8ULd9alSn9gfJnvMwZuBLwN3Vly9gYqIsYhYDiwCzpC0rOYqVaZE7N8FRiPiNOBexnuApjVJFwK7ImJj3XWpQ8n4G9n2OedExOlkh5aulrSi7gpVqF/sTd3mzwROB26IiLcAvwI+UW+Vxg1d4pXOcfk2cEtEfKdLkZ1Afo9vURo27fWLPSJeaB2Sioh1wCxJx1VczYFLXc73ARe0jXql7SXNBI4Bnq20cgNWFHtEPBsR+9Lbm4G3Vly1QXk7cJGkJ4HbgPMk/XNbmSa3e9/4G9z2AETEzvT/LuAO4Iy2Io3d5veLvcHb/B3AjlzP/hqyRCyvtnYfqsQrnbfxNWBLRHyxoNha4A/SFQ9nAnsi4unKKjkgZWKXtKB1boukM8jWj0b8AEl6g6R56fUc4LeBn7QVWwtcnl5fAvxbNOBGd2Vibzu34SKycwCnvYj4ZEQsiohRYBVZm36wrVgj2x3Kxd/UtgeQNDddTEQ61HQ+8FhbsaZu8/vG3tRtfkT8Atgu6dQ06F1A+4VktbX7sF3V+HbgMmBTOt8F4FPAYoCIuBFYB7wH2Aq8BHyo+moORJnYLwE+IukA8DKwqik/QMBC4FuSRsg2Lv8SEXdJ+izwUESsJUtM/0nSVuA5sh+qJigT+0clXUR29etzwBW11bYCQ9LuhYao7ecDd6TcYiZwa0TcLekqaPw2v0zsTd7m/ylwS7qicRvwoanS7r5zvZmZmVlFhupQo5mZmVmdnHiZmZmZVcSJl5mZmVlFnHiZmZmZVcSJl5mZmVlFnHiZ2ZQhaUzSI5Iek3S7pPYH2+bLnivp7Nz7b0q6pMQyXuxXZqIkLZf0ntz76yX9xWQvx8ymPydeZjaVvBwRyyNiGbAfuKpH2XOBs3uMr9JysnsCmZn15MTLzKaq+4GTJf2upA3pYbc/kDQ/Pej9KuDjqYfsHWmaFZL+Q9K2kr1ffynpwfSA6M+kYaOStkj6qqTHJd2T7viPpN9KZR+R9IXUMzcb+CxwaRp+aZr9Ekn/nury0cn+cMxsenLiZWZTTnpm4kpgE/Aj4Mz0sNvbgOsi4kngRuBvUw/Z/WnShcA5wIXAX/dZxvnAKWTPr1sOvDX3EOFTgK9ExFLgeeADafg3gD9ODxwfA4iI/cCngdWpLqtT2TcCv5Pm/1fpWalmNuSG7ZFBZja1zck90up+ssf5nAqsTs8UnA080WP6OyPiILBZ0vw+yzo//f04vT+KLOH6GfBERLTqsREYTc+7PDoiHkjDbyVL8Ir8a3r49D5Ju8ge4bKjT53MrOGceJnZVPJy6k16haQvA1+MiLWSzgWu7zH9vvykfZYl4HMR8Q9tyxttm88YMKfPvPrVZQxvb80MH2o0s6nvGGBnen15bvhe4OgjmO/3gT+UdBSApBMkHV9UOCKeB/ZKelsalH+Y9pHWxcyGhBMvM5vqrgdul7QR+GVu+HeB97WdXF9aRNxDdrjwAUmbgDX0T54+DHw1HQ6dC+xJw+8jO5k+f3K9mVkHRUTddTAzmxYkHRURL6bXnwAWRsS1NVfLzKYRn3NgZlbeeyV9kmzb+RRwRb3VMbPpxj1eZmZmZhXxOV5mZmZmFXHiZWZmZlYRJ15mZmZmFXHiZWZmZlYRJ15mZmZmFXHiZWZmZlaR/wd+5LmtinetbwAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "seed_idx = list(range(2,max_depth +1))\n", + "\n", + "plt.figure(figsize=(10,5))\n", + "\n", + "for i in range(len(data)):\n", + " plt.plot(seed_idx, time_algo_cu[i], label = (str(names[i] + \"-cuGraph\")))\n", + "\n", + " plt.plot(seed_idx, time_algo_wk[i], label = (str(names[i] + \"-walker\")), linestyle='-.')\n", + "\n", + "\n", + "plt.title(f'Runtime vs. Path Length ({num_seeds} Seeds)')\n", + "plt.xlabel('Path length')\n", + "plt.ylabel('Runtime')\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "5164" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "del time_algo_cu\n", + "del time_algo_wk\n", + "gc.collect()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test 2: Runtime Speedup versus number of seeds\n", + "The number of seeds will be increased over a range in increments of 50. \n", + "The runtime will be the sum of runtime per increment. Increaing number of seeds by 1 would make for very long execution times " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reading ./data/preferentialAttachment.mtx...\n", + "\t.Random walks - T=2.28s\n", + ".Random walks - T=2.29s\n", + ".Random walks - T=2.28s\n", + ".Random walks - T=2.21s\n", + ".Random walks - T=1.95s\n", + ".Random walks - T=2.38s\n", + ".Random walks - T=2.00s\n", + ".Random walks - T=2.19s\n", + ".Random walks - T=1.99s\n", + ".Random walks - T=2.40s\n", + ".Random walks - T=1.95s\n", + ".Random walks - T=2.17s\n", + ".Random walks - T=1.95s\n", + ".Random walks - T=2.39s\n", + ".Random walks - T=1.97s\n", + ".Random walks - T=2.23s\n", + ".Random walks - T=2.63s\n", + ".Random walks - T=4.08s\n", + ".Random walks - T=3.44s\n", + ".Random walks - T=3.77s\n", + " \n", + "Reading ./data/dblp-2010.mtx...\n", + "\t.Random walks - T=6.61s\n", + ".Random walks - T=6.57s\n", + ".Random walks - T=6.48s\n", + ".Random walks - T=6.69s\n", + ".Random walks - T=6.11s\n", + ".Random walks - T=6.18s\n", + ".Random walks - T=4.98s\n", + ".Random walks - T=5.64s\n", + ".Random walks - T=3.83s\n", + ".Random walks - T=4.28s\n", + ".Random walks - T=4.34s\n", + ".Random walks - T=4.14s\n", + ".Random walks - T=3.79s\n", + ".Random walks - T=4.37s\n", + ".Random walks - T=4.00s\n", + ".Random walks - T=3.66s\n", + ".Random walks - T=4.01s\n", + ".Random walks - T=3.67s\n", + ".Random walks - T=4.32s\n", + ".Random walks - T=3.70s\n", + " \n", + "Reading ./data/coPapersCiteseer.mtx...\n", + "\t.Random walks - T=56.64s\n", + ".Random walks - T=52.26s\n", + ".Random walks - T=45.66s\n", + ".Random walks - T=48.81s\n", + ".Random walks - T=56.16s\n", + ".Random walks - T=56.73s\n", + ".Random walks - T=45.43s\n", + ".Random walks - T=44.96s\n", + ".Random walks - T=51.77s\n", + ".Random walks - T=58.39s\n", + ".Random walks - T=43.35s\n", + ".Random walks - T=42.89s\n", + ".Random walks - T=57.96s\n", + ".Random walks - T=45.03s\n", + ".Random walks - T=64.27s\n", + ".Random walks - T=52.57s\n", + ".Random walks - T=46.91s\n", + ".Random walks - T=55.62s\n", + ".Random walks - T=46.85s\n", + ".Random walks - T=44.84s\n", + " \n", + "Reading ./data/as-Skitter.mtx...\n", + "\t.Random walks - T=51.36s\n", + ".Random walks - T=52.06s\n", + ".Random walks - T=44.91s\n", + ".Random walks - T=49.73s\n", + ".Random walks - T=47.45s\n", + ".Random walks - T=52.21s\n", + ".Random walks - T=47.65s\n", + ".Random walks - T=45.49s\n", + ".Random walks - T=47.84s\n", + ".Random walks - T=43.48s\n", + ".Random walks - T=45.67s\n", + ".Random walks - T=45.75s\n", + ".Random walks - T=55.03s\n", + ".Random walks - T=46.39s\n", + ".Random walks - T=50.64s\n", + ".Random walks - T=43.87s\n", + ".Random walks - T=40.98s\n", + ".Random walks - T=49.42s\n", + ".Random walks - T=51.94s\n", + ".Random walks - T=49.28s\n", + " \n" + ] + } + ], + "source": [ + "# some parameters\n", + "rw_depth = 4\n", + "max_seeds = 1000\n", + "\n", + "# arrays to capture performance gains\n", + "names = []\n", + "\n", + "# Two dimension data\n", + "time_algo_cu = [] # will be two dimensional\n", + "time_algo_wk = [] # will be two dimensional\n", + "perf = [] # will be two dimensional\n", + "\n", + "i = 0\n", + "for k,v in data.items():\n", + " time_algo_cu.append([])\n", + " time_algo_wk.append([])\n", + " perf.append([])\n", + " \n", + " # Saved the file Name\n", + " names.append(k)\n", + "\n", + " # read data\n", + " gdf = read_data(v)\n", + " pdf = gdf.to_pandas()\n", + " \n", + " # Create the Graphs\n", + " Gcg = create_cu_ugraph(gdf)\n", + " Gnx = create_nx_ugraph(pdf)\n", + " \n", + " num_nodes = Gcg.number_of_nodes()\n", + " nodes = Gcg.nodes().to_array().tolist()\n", + " \n", + " print('\\t', end='')\n", + " for j in range (50, max_seeds +1, 50) :\n", + " print('.', end='')\n", + " seeds = random.sample(nodes, j)\n", + " tc = run_cu_rw(Gcg, seeds, rw_depth)\n", + " tw = run_wk_rw(Gnx, seeds, rw_depth)\n", + " \n", + " time_algo_cu[i].append(tc)\n", + " time_algo_wk[i].append(tw) \n", + " perf[i].append(tw/tc)\n", + " \n", + "\n", + " # update i\n", + " i = i + 1\n", + " print(\" \")\n", + " \n", + " del Gcg\n", + " del Gnx\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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6CIqKyv585//zD//+U9uueUWLr/8cgB+/etfM378eAoKCrj11lujrzF06FC+8pWvMHLkSDZv3nzA9YYNG8bXv/51RowYwemnn05NTQ1PPvkkixYt4vLLL4/2xMX2fn3rW99i3LhxjBgxIrqvppxzPPHEE8yZM4f//Oc/0bFVN954I/PmzaOwsJA777yTn/zkJzz++OMUFhby+OOPs3DhQiZOnMiYMWM4+eSTWb16NRAJqddddx0jR46koKCA++67L/pa9913H0VFRYwaNYpVq1YB8NOf/pRZs2Zx6qmn0r9/f5566imuv/56Ro0axZlnnkl9fT0AixcvZsqUKYwdO5YzzjiD7du3A5HevhtuuIEJEyYwZMgQ5s2bRyAQ2K/ez8w512kfY8eOdW1p7qdz3cg5I93C7Qvb9HVERKR9rVy5Mt4luI0bNzrAvf32284557761a+6X//6165///7uzjvvdM45V1xc7E499VRXWVnpnHPujjvucD/72c+cc87NmjXLPfHEE845515++WX39a9/3YXDYRcKhdzZZ5/t3nzzTbdx40ZnZm7+/PmHXM/v97sPPvjAOefcRRdd5B566CHnnHNTpkxx77//frTu2Pndu3c755wLBoNuypQpbtmyZfut8/bbb7vp06c755y79NJL3ZNPPumcc27u3Lnu7LPPju73b3/7m7vmmmui82VlZa6+vt4559x//vMf98UvftE559wf/vAHd8EFF0SXNdTQv39/d++99zrnnPv973/vvva1rznnnLv11lvdKaec4gKBgFu6dKlLTU11L774onPOufPPP989/fTTLhAIuIkTJ7pdu3Y555x77LHH3Fe/+tXoe/nBD37gnHPuhRdecDNmzGi23qaa+xkDFrkDZBpd9uIgxnQbA8CSnUsY32N8nKsREZG28LPnPmLltvJW3efwXlnceu6IQ67Xt29fTjnlFACuuOIK7r33XgAuvvhiABYsWMDKlSuj6wQCASZOnLjffl555RVeeeUVxoyJ/L9VWVnJ2rVr6devH/379+ekk0465HoDBw6ksLAQgLFjx7Jp06ZD1v/Pf/6TBx54gGAwyPbt21m5ciUFBQWN1nn00Ue55JJLALjkkkv4+9//zgUXXHDIfZeVlTFr1izWrl2LmUV7sl599VW++c1vkpAQiTC5ubnRbb74xS9G63/qqaei7Wed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dgguHG403yWlC8d3OV5X7BcREZFWo0B2EJVvvsnaSadSv2NHo/aibkUsLV5KKByKU2UiIiJyNFEgO4jEvv0IlZZS8frrjdqLuhdRVV/F6j2rD7CliIiISMspkB1E8qCBJA0aROWrrzVq143GRUREpDUpkB1C5ozpVC1cSKi8PNrWI70HvdJ76XpkIiLSLn7605/Su3fv6A3Hn3322bjUsWbNGj73uc8xePBgioqK+NKXvsTOnTtZtGgR3/3udwF44403ePfdd+NSX2emQHYImTNmQDBI5ZtvNWov6l7Ekp1L6MzXcRMRkc7j2muvZenSpTzxxBNceeWVhJucdNZagsFgs+21tbWcffbZfOtb32Lt2rUsWbKE//f//h/FxcWMGzeOe++9F4hvIAuFOu/YbgWyQ0gpKMCfn0fFa40PWxZ1L2J37W4+rfg0TpWJiEhn9/e//52CggJGjx7Nl7/8ZTZt2sT06dMpKChgxowZfPrp/v/HDBs2jISEBEpKSjj//PMZO3YsI0aM4IEHHoiuk5GRwbXXXsuIESOYMWMGxcXFAKxfv54zzzyTsWPHcuqpp7Jq1SogcqHZb37zm5x44olcf/31vPnmmxQWFlJYWMiYMWOoqKjgkUceYeLEiZx77rnR15k6dSojR47kjTfe4JxzzmHTpk3cf//93H333RQWFjJv3jyKi4u54IILGD9+POPHj+edd94BaPY1AH79618zfvx4CgoKuPXWW6Ov9Y9//IMJEyZQWFjI1VdfHQ1fGRkZ/PCHP2T06NHMnz+/lb+hduSc67SPsWPHuvaw7Se3ulVjilyotjbatm7POjdyzkj31Jqn2qUGERFpPStXrox3CW7FihVu8ODBrri42Dnn3O7du90555zj5syZ45xz7i9/+Ys777zznHPO3Xrrre7Xv/61c865BQsWuJ49e7pwOOx2797tnHOuurrajRgxwpWUlDjnnAPcP/7xD+eccz/72c/cNddc45xzbvr06W7NmjXR/UybNs0559ysWbPc2Wef7YLBoHPOuXPOOce9/fbbzjnnKioqXH19vbv22mvdb3/722bfy9y5c93ZZ5+9X63OOXfppZe6efPmOeec++STT9wJJ5xwwNd4+eWX3de//nUXDoddKBRyZ599tnvzzTfdypUr3TnnnOMCgYBzzrlvfetb7sEHH4y+18cff/ywP/+21tzPGLDIHSDT6MKwLZA5cwZ7H3+c6gULyJgyBYBB2YPoktyFxTsX84XBX4hzhSIicqTuXHgnq0pXteo+T8g9gRsm3HDQdV5//XUuuugi8vLyAMjNzWX+/Pk89dRTAHz5y1/m+uuvj65/9913849//IPMzEwef/xxzIx7772Xp59+GoDNmzezdu1aunbtis/n4+KLLwbgiiuu4Itf/CKVlZW8++67XHTRRdF91tXVRacvuugi/H4/AKeccgo/+MEPuPzyy/niF79Inz59jvizePXVV1m5cmV0vry8nMrKymZf45VXXuGVV15hzJjIRdgrKytZu3Yty5cvZ/HixdH7fdbU1NCtWzcA/H4/F1xwwRHX11EokLVA2okn4ktPp+LV16KBzMwY022MBvaLiEi7uPbaa7nuuuui82+88Qavvvoq8+fPJy0tjalTp1JbW9vstmZGOBymS5cuLF26tNl10tPTo9M33ngjZ599Ni+++CKnnHIKL7/8MiNGjODNN9887LrD4TALFizY7+bnzb2Gc46bbrqJq6++utG69913H7NmzeKXv/zlfvtPSUmJBsnOTIGsBXxJSWRMmUzF3Ln0CIcxX2To3djuY5m7eS7F1cXkp+XHuUoRETkSh+rJaivTp0/nC1/4Aj/4wQ/o2rUrpaWlnHzyyTz22GN8+ctf5uGHH+bUU0894PZlZWXk5OSQlpbGqlWrWLBgQXRZOBzmySef5JJLLuGRRx5h0qRJZGVlMXDgQJ544gkuuuginHMsX76c0aNH77fv9evXM2rUKEaNGsX777/PqlWruOyyy/jlL3/JCy+8wNlnnw3AW2+9RW5ubqNtMzMzKY+5MsHpp5/Offfdx49+9CMAli5dSmFhYbOvccYZZ3DLLbdw+eWXk5GRwdatW0lMTGTGjBmcd955XHvttXTr1o3S0lIqKiro37//Z/oOOhIN6m+hjBkzCJWUULNsWbStqFsRAIt3LY5XWSIi0kmNGDGCH//4x0yZMoXRo0fzgx/8gPvuu4+//e1vFBQU8NBDD3HPPfcccPszzzyTYDDIsGHDuPHGGznppJOiy9LT01m4cCEjR47k9ddf5yc/+QkADz/8MH/5y18YPXo0I0aM4Jlnnml237/97W8ZOXIkBQUFJCYmctZZZ5Gamsrzzz/Pfffdx+DBgxk+fDh/+MMfyM9v3CFx7rnn8vTTT0cH9d97770sWrSIgoIChg8fzv3333/A1zj99NO57LLLmDhxIqNGjeLCCy+koqKC4cOH8/Of/5zTTz+dgoICTjvtNLZv3/5Zv4IOxVwnvmzDuHHj3KJFi9rltUIVFaw5+RS6zvoK3bwu4/pwPac8egrnH38+N594c7vUISIin93HH3/MsGHD4l1Gm8nIyKCysjLeZRzTmvsZM7PFzrlxza2vHrIW8mdmkj5hAhX/eTV67bFEXyIF+QW6Yr+IiIh8JgpkhyFz5gwCn3xCYMOGaNvYbmNZs2cN5YHyg2wpIiLSftQ71vkokB2GjOnTAaiIubdlUfciHI6lu5bGqSoRERHp7BTIDkNi9+6kFBQ0ump/QX4BCZbAB7s+iGNlIiIi0pkpkB2mzBkzqF2+nPqdOwFITUhleNfhGkcmIiIiR0yB7DBlzpwBQOXrr0fbiroX8WHJh9SF6g60mYiIiMgBKZAdpqRBg0gaMKDROLIx3cZQH65nRcmKOFYmIiLHittvv50RI0ZQUFBAYWEh7733HgADBgygpKRkv/VPPvlkADZt2sQjjzwSbV+6dCkvvvhi+xQtB6VAdpjMjMyZM6hauJCQd2f6hgvE6rCliIi0tfnz5/P888+zZMkSli9fzquvvkrfvn0Pus27774LtE4gCwaDh1+0HJIC2RHImDED6uupfOstALqkdOG47ON0xX4RETks559/PmPHjmXEiBE88MADhEIhZs+ezciRIxk1ahR33333ftts376dvLw8kpOTAcjLy6NXr16N1qmpqeGss87iT3/6ExC5UCxE7h85b948CgsLufPOO/nJT37C448/TmFhIY8//jhVVVVceeWVTJgwgTFjxkSv5D9nzhw+//nPM336dGbMmNGWH8kxS/eyPAKpo0fjz8uj8rXXyPbu51XUvYh/b/w3oXAIv6/z3+RURETa3l//+ldyc3Opqalh/PjxjB07lq1bt7JiRWQIzN69e/fb5vTTT+e2225jyJAhzJw5k4svvpgpU6ZEl1dWVnLJJZfwla98ha985SuNtr3jjju46667eP755wHo3r07ixYt4ne/+x0AN998M9OnT+evf/0re/fuZcKECcycORMg2iPX9N6V0joUyI6A+XxkTptG+YsvEg4E8CUlUdS9iCfWPMGaPWsY1vXovR2HiMjRZscvfkHdx6tadZ/Jw06gx82H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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "seed_idx = list(range (50, max_seeds +1, 50))\n", + "\n", + "plt.figure(figsize=(10,5))\n", + "\n", + "for i in range(len(data)):\n", + " plt.plot(seed_idx, perf[i], label = names[i] )\n", + "\n", + "plt.title('Speedup vs. Number of Seeds')\n", + "plt.xlabel('Number of Seeds')\n", + "plt.ylabel('Speedup')\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3786" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "del time_algo_cu\n", + "gc.collect()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "-----\n", + "Copyright (c) 2021, NVIDIA CORPORATION.\n", + "\n", + "Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n", + "\n", + "Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cugraph_dev", + "language": "python", + "name": "cugraph_dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/cugraph_benchmarks/release.ipynb b/notebooks/cugraph_benchmarks/release.ipynb index 3c6da55abc0..a6eeeb65cdf 100644 --- a/notebooks/cugraph_benchmarks/release.ipynb +++ b/notebooks/cugraph_benchmarks/release.ipynb @@ -22,6 +22,7 @@ "| Triangle Counting | X | |\n", "\n", "### Test Data\n", + "Users must run the _dataPrep.sh_ script before running this notebook so that the test files are downloaded\n", "\n", "| File Name | Num of Vertices | Num of Edges |\n", "| ---------------------- | --------------: | -----------: |\n", @@ -594,7 +595,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" } }, "nbformat": 4, diff --git a/notebooks/layout/Force-Atlas2.ipynb b/notebooks/layout/Force-Atlas2.ipynb index fa9ec0fd180..456af3c62de 100644 --- a/notebooks/layout/Force-Atlas2.ipynb +++ b/notebooks/layout/Force-Atlas2.ipynb @@ -4,7 +4,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Force Atlas 2" + "# Force Atlas 2\n", + "# Skip notebook test" ] }, { @@ -521,4 +522,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/notebooks/sampling/RandomWalk.ipynb b/notebooks/sampling/RandomWalk.ipynb new file mode 100644 index 00000000000..31a521db1c1 --- /dev/null +++ b/notebooks/sampling/RandomWalk.ipynb @@ -0,0 +1,313 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Random Walk Sampling\n", + "\n", + "In this notebook, we will compute the Random Walk from a set of seeds using cuGraph. \n", + "\n", + "\n", + "| Author Credit | Date | Update | cuGraph Version | Test Hardware |\n", + "| --------------|------------|--------------|-----------------|----------------|\n", + "| Brad Rees | 04/20/2021 | created | 0.19 | GV100, CUDA 11.0\n", + "\n", + "Currently NetworkX does not have a random walk function. There is code on StackOverflow that generats a random walk by getting a vertice and then randomly selection a neighbor and then repeating the process. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Test Data\n", + "We will be using the Zachary Karate club dataset \n", + "*W. W. Zachary, An information flow model for conflict and fission in small groups, Journal of\n", + "Anthropological Research 33, 452-473 (1977).*\n", + "\n", + "\n", + "![Karate Club](../img/zachary_black_lines.png)\n", + "\n", + "\n", + "Because the test data has vertex IDs starting at 1, the auto-renumber feature of cuGraph (mentioned above) will be used so the starting vertex ID is zero for maximum efficiency. The resulting data will then be auto-unrenumbered, making the entire renumbering process transparent to users." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the modules\n", + "import cugraph\n", + "import cudf" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Read The Data\n", + "# Define the path to the test data \n", + "datafile='../data/karate-data.csv'\n", + "\n", + "gdf = cudf.read_csv(datafile, delimiter='\\t', names=['src', 'dst'], dtype=['int32', 'int32'] )" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "gdf['wt'] = 1.0" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a Graph - using the source (src) and destination (dst) vertex pairs from the Dataframe \n", + "G = cugraph.Graph()\n", + "G.from_cudf_edgelist(gdf, source='src', destination='dst', edge_attr='wt')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(34, 78)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# some stats on the graph\n", + "(G.number_of_nodes(), G.number_of_edges() )" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# create a list with the seeds\n", + "seeds = [17,19]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "rw, so = cugraph.random_walks(G, seeds, 4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A random walk generates a path from a seed vertex. At each step on the random walk (starting from the seed), the random walker picks a random departing edge to traverse. The random walk will terminate in two situations, when the maximum path length is reached, or when the current vertex on the path has no departing edges to traverse. The result of a single random walk will be a path of some length less than or equal to the maximum path length.\n", + "\n", + "cugraph.random_walks performs a random walk from each of the specified seeds. The output will be a path for each of the seeds. Because the path lengths might be variable length, the return value consists of a pair of outputs.\n", + "\n", + "The first output provides the edges used on the paths.\n", + "\n", + "The second output represents the seed offset, which is a cuDF Series. The seed offset identifies the offset of the first entry in the first output for a particular seed." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 0\n", + "1 3\n", + "2 6\n", + "dtype: int64" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "so" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
srcdstweight
01761.0
16171.0
21761.0
319331.0
433311.0
53121.0
\n", + "
" + ], + "text/plain": [ + " src dst weight\n", + "0 17 6 1.0\n", + "1 6 17 1.0\n", + "2 17 6 1.0\n", + "3 19 33 1.0\n", + "4 33 31 1.0\n", + "5 31 2 1.0" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rw" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "seed 17 starts at index 0 and is 3 rows\n", + "seed 19 starts at index 3 and is 3 rows\n" + ] + } + ], + "source": [ + "for i in range(len(seeds)):\n", + " print(f\"seed {seeds[i]} starts at index {so[i]} and is {so[1 + 1] - so[1]} rows\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "-----\n", + "Copyright (c) 2021, NVIDIA CORPORATION.\n", + "\n", + "Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n", + "\n", + "Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cugraph_dev", + "language": "python", + "name": "cugraph_dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/cugraph/__init__.py b/python/cugraph/__init__.py index d4632708591..1a113b93d8d 100644 --- a/python/cugraph/__init__.py +++ b/python/cugraph/__init__.py @@ -33,6 +33,8 @@ DiGraph, MultiGraph, MultiDiGraph, + BiPartiteGraph, + BiPartiteDiGraph, from_edgelist, from_cudf_edgelist, from_pandas_edgelist, @@ -48,7 +50,11 @@ symmetrize, symmetrize_df, symmetrize_ddf, -) + is_weighted, + is_directed, + is_multigraph, + is_bipartite, + is_multipartite) from cugraph.centrality import ( betweenness_centrality, diff --git a/python/cugraph/centrality/betweenness_centrality_wrapper.pyx b/python/cugraph/centrality/betweenness_centrality_wrapper.pyx index 855de3327ba..e63b6996816 100644 --- a/python/cugraph/centrality/betweenness_centrality_wrapper.pyx +++ b/python/cugraph/centrality/betweenness_centrality_wrapper.pyx @@ -17,7 +17,7 @@ # cython: language_level = 3 from cugraph.centrality.betweenness_centrality cimport betweenness_centrality as c_betweenness_centrality -from cugraph.structure.graph import DiGraph +from cugraph.structure.graph_classes import DiGraph from cugraph.structure.graph_primtypes cimport * from libc.stdint cimport uintptr_t from libcpp cimport bool diff --git a/python/cugraph/centrality/edge_betweenness_centrality_wrapper.pyx b/python/cugraph/centrality/edge_betweenness_centrality_wrapper.pyx index 136bde1b0e3..095d291c45e 100644 --- a/python/cugraph/centrality/edge_betweenness_centrality_wrapper.pyx +++ b/python/cugraph/centrality/edge_betweenness_centrality_wrapper.pyx @@ -18,7 +18,7 @@ from cugraph.centrality.betweenness_centrality cimport edge_betweenness_centrality as c_edge_betweenness_centrality from cugraph.structure import graph_primtypes_wrapper -from cugraph.structure.graph import DiGraph, Graph +from cugraph.structure.graph_classes import DiGraph, Graph from cugraph.structure.graph_primtypes cimport * from libc.stdint cimport uintptr_t from libcpp cimport bool diff --git a/python/cugraph/centrality/katz_centrality.py b/python/cugraph/centrality/katz_centrality.py index ce52d15f5db..4a2b41cfe59 100644 --- a/python/cugraph/centrality/katz_centrality.py +++ b/python/cugraph/centrality/katz_centrality.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -42,13 +42,13 @@ def katz_centrality( NOTE The maximum acceptable value of alpha for convergence - alpha_max = 1/(lambda_max) where lambda_max is the largest eigenvalue - of the graph. + alpha_max = 1/(lambda_max) where lambda_max is the largest + eigenvalue of the graph. Since lambda_max is always lesser than or equal to degree_max for a graph, alpha_max will always be greater than or equal to (1/degree_max). Therefore, setting alpha to (1/degree_max) will - guarantee that it will never exceed alpha_max thus in turn fulfilling - the requirement for convergence. + guarantee that it will never exceed alpha_max thus in turn + fulfilling the requirement for convergence. beta : None A weight scalar - currently Not Supported max_iter : int diff --git a/python/cugraph/community/ktruss_subgraph.py b/python/cugraph/community/ktruss_subgraph.py index 8e4f1471955..f4e4f7fb1cc 100644 --- a/python/cugraph/community/ktruss_subgraph.py +++ b/python/cugraph/community/ktruss_subgraph.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -12,7 +12,7 @@ # limitations under the License. from cugraph.community import ktruss_subgraph_wrapper -from cugraph.structure.graph import Graph +from cugraph.structure.graph_classes import Graph from cugraph.utilities import check_nx_graph from cugraph.utilities import cugraph_to_nx diff --git a/python/cugraph/community/leiden.py b/python/cugraph/community/leiden.py index 8c1b79b8b63..641cf552192 100644 --- a/python/cugraph/community/leiden.py +++ b/python/cugraph/community/leiden.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019 - 2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -12,7 +12,7 @@ # limitations under the License. from cugraph.community import leiden_wrapper -from cugraph.structure.graph import Graph +from cugraph.structure.graph_classes import Graph from cugraph.utilities import check_nx_graph from cugraph.utilities import df_score_to_dictionary diff --git a/python/cugraph/community/louvain.py b/python/cugraph/community/louvain.py index d4d56a1100c..a761e060038 100644 --- a/python/cugraph/community/louvain.py +++ b/python/cugraph/community/louvain.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -12,7 +12,7 @@ # limitations under the License. from cugraph.community import louvain_wrapper -from cugraph.structure.graph import Graph +from cugraph.structure.graph_classes import Graph from cugraph.utilities import check_nx_graph from cugraph.utilities import df_score_to_dictionary diff --git a/python/cugraph/community/subgraph_extraction.py b/python/cugraph/community/subgraph_extraction.py index 8c702c2f58f..7815851d465 100644 --- a/python/cugraph/community/subgraph_extraction.py +++ b/python/cugraph/community/subgraph_extraction.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -12,7 +12,7 @@ # limitations under the License. from cugraph.community import subgraph_extraction_wrapper -from cugraph.structure.graph import null_check +from cugraph.structure.graph_classes import null_check from cugraph.utilities import check_nx_graph from cugraph.utilities import cugraph_to_nx diff --git a/python/cugraph/community/triangle_count.py b/python/cugraph/community/triangle_count.py index ff4dc9a5c5f..d28424a513e 100644 --- a/python/cugraph/community/triangle_count.py +++ b/python/cugraph/community/triangle_count.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -12,7 +12,7 @@ # limitations under the License. from cugraph.community import triangle_count_wrapper -from cugraph.structure.graph import Graph +from cugraph.structure.graph_classes import Graph from cugraph.utilities import check_nx_graph diff --git a/python/cugraph/components/connectivity.py b/python/cugraph/components/connectivity.py index df33f8b8e03..94eea312fb9 100644 --- a/python/cugraph/components/connectivity.py +++ b/python/cugraph/components/connectivity.py @@ -139,7 +139,7 @@ def weakly_connected_components(G, directed : bool, optional NOTE - For non-Graph-type (eg. sparse matrix) values of G only. + For non-Graph-type (eg. sparse matrix) values of G only. Raises TypeError if used with a Graph object. If True (default), then convert the input matrix to a cugraph.DiGraph diff --git a/python/cugraph/components/connectivity_wrapper.pyx b/python/cugraph/components/connectivity_wrapper.pyx index 76d279a8116..ac173de3564 100644 --- a/python/cugraph/components/connectivity_wrapper.pyx +++ b/python/cugraph/components/connectivity_wrapper.pyx @@ -22,7 +22,7 @@ from cugraph.structure import utils_wrapper from cugraph.structure import graph_primtypes_wrapper from libc.stdint cimport uintptr_t from cugraph.structure.symmetrize import symmetrize -from cugraph.structure.graph import Graph as type_Graph +from cugraph.structure.graph_classes import Graph as type_Graph import cudf import numpy as np diff --git a/python/cugraph/cores/k_core.py b/python/cugraph/cores/k_core.py index ce67665764b..ca17bdd5c81 100644 --- a/python/cugraph/cores/k_core.py +++ b/python/cugraph/cores/k_core.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -14,7 +14,7 @@ from cugraph.cores import k_core_wrapper, core_number_wrapper from cugraph.utilities import cugraph_to_nx from cugraph.utilities import check_nx_graph -from cugraph.structure.graph import Graph +from cugraph.structure.graph_classes import Graph def k_core(G, k=None, core_number=None): diff --git a/python/cugraph/dask/centrality/katz_centrality.py b/python/cugraph/dask/centrality/katz_centrality.py index 45deda8b7ae..cd6af8e7906 100644 --- a/python/cugraph/dask/centrality/katz_centrality.py +++ b/python/cugraph/dask/centrality/katz_centrality.py @@ -71,13 +71,13 @@ def katz_centrality(input_graph, NOTE The maximum acceptable value of alpha for convergence - alpha_max = 1/(lambda_max) where lambda_max is the largest eigenvalue - of the graph. + alpha_max = 1/(lambda_max) where lambda_max is the largest + eigenvalue of the graph. Since lambda_max is always lesser than or equal to degree_max for a graph, alpha_max will always be greater than or equal to (1/degree_max). Therefore, setting alpha to (1/degree_max) will - guarantee that it will never exceed alpha_max thus in turn fulfilling - the requirement for convergence. + guarantee that it will never exceed alpha_max thus in turn + fulfilling the requirement for convergence. beta : None A weight scalar - currently Not Supported max_iter : int diff --git a/python/cugraph/dask/link_analysis/pagerank.py b/python/cugraph/dask/link_analysis/pagerank.py index fb9f4ad3a25..f90e5c72231 100644 --- a/python/cugraph/dask/link_analysis/pagerank.py +++ b/python/cugraph/dask/link_analysis/pagerank.py @@ -119,7 +119,7 @@ def pagerank(input_graph, edge_attr='value') >>> pr = dcg.pagerank(dg) """ - from cugraph.structure.graph import null_check + from cugraph.structure.graph_classes import null_check nstart = None diff --git a/python/cugraph/dask/traversal/bfs.py b/python/cugraph/dask/traversal/bfs.py index d108730f665..03b9844bf6c 100644 --- a/python/cugraph/dask/traversal/bfs.py +++ b/python/cugraph/dask/traversal/bfs.py @@ -28,6 +28,7 @@ def call_bfs(sID, num_edges, vertex_partition_offsets, start, + depth_limit, return_distances): wid = Comms.get_worker_id(sID) handle = Comms.get_handle(sID) @@ -38,12 +39,14 @@ def call_bfs(sID, wid, handle, start, + depth_limit, return_distances) def bfs(graph, start, - return_distances=False): + depth_limit=None, + return_distances=True): """ Find the distances and predecessors for a breadth first traversal of a graph. @@ -59,7 +62,9 @@ def bfs(graph, start : Integer Specify starting vertex for breadth-first search; this function iterates over edges in the component reachable from this node. - return_distances : bool, optional, default=False + depth_limit : Integer or None + Limit the depth of the search + return_distances : bool, optional, default=True Indicates if distances should be returned Returns @@ -99,9 +104,15 @@ def bfs(graph, data = get_distributed_data(ddf) if graph.renumbered: - start = graph.lookup_internal_vertex_id(cudf.Series([start], - dtype='int32')).compute() - start = start.iloc[0] + if isinstance(start, dask_cudf.DataFrame)\ + or isinstance(start, cudf.DataFrame): + start = graph.lookup_internal_vertex_id(start, start.columns).\ + compute() + start = start.iloc[0] + else: + start = graph.lookup_internal_vertex_id(cudf.Series([start], + dtype='int32')).compute() + start = start.iloc[0] result = [client.submit( call_bfs, @@ -111,6 +122,7 @@ def bfs(graph, num_edges, vertex_partition_offsets, start, + depth_limit, return_distances, workers=[wf[0]]) for idx, wf in enumerate(data.worker_to_parts.items())] @@ -120,5 +132,5 @@ def bfs(graph, if graph.renumbered: ddf = graph.unrenumber(ddf, 'vertex') ddf = graph.unrenumber(ddf, 'predecessor') - ddf["predecessor"] = ddf["predecessor"].fillna(-1) + ddf = ddf.fillna(-1) return ddf diff --git a/python/cugraph/dask/traversal/mg_bfs.pxd b/python/cugraph/dask/traversal/mg_bfs.pxd index afd209158c4..6a0277f8713 100644 --- a/python/cugraph/dask/traversal/mg_bfs.pxd +++ b/python/cugraph/dask/traversal/mg_bfs.pxd @@ -17,6 +17,9 @@ from cugraph.structure.graph_utilities cimport * from libcpp cimport bool +cdef extern from "limits.h": + cdef int INT_MAX + cdef long LONG_MAX cdef extern from "utilities/cython.hpp" namespace "cugraph::cython": @@ -26,6 +29,6 @@ cdef extern from "utilities/cython.hpp" namespace "cugraph::cython": vertex_t *identifiers, vertex_t *distances, vertex_t *predecessors, - double *sp_counters, + vertex_t depth_limit, const vertex_t start_vertex, - bool directed) except + + bool direction_optimizing) except + diff --git a/python/cugraph/dask/traversal/mg_bfs_wrapper.pyx b/python/cugraph/dask/traversal/mg_bfs_wrapper.pyx index 44630ba5fb3..e2f44ada32c 100644 --- a/python/cugraph/dask/traversal/mg_bfs_wrapper.pyx +++ b/python/cugraph/dask/traversal/mg_bfs_wrapper.pyx @@ -28,11 +28,11 @@ def mg_bfs(input_df, rank, handle, start, + depth_limit, return_distances=False): """ - Call pagerank + Call BFS """ - cdef size_t handle_size_t = handle.getHandle() handle_ = handle_size_t @@ -43,7 +43,7 @@ def mg_bfs(input_df, if num_global_edges > (2**31 - 1): edge_t = np.dtype("int64") else: - edge_t = np.dtype("int32") + edge_t = vertex_t if "value" in input_df.columns: weights = input_df['value'] weight_t = weights.dtype @@ -86,9 +86,9 @@ def mg_bfs(input_df, # Generate the cudf.DataFrame result df = cudf.DataFrame() df['vertex'] = cudf.Series(np.arange(vertex_partition_offsets.iloc[rank], vertex_partition_offsets.iloc[rank+1]), dtype=vertex_t) - df['predecessor'] = cudf.Series(np.zeros(len(df['vertex']), dtype=np.int32)) + df['predecessor'] = cudf.Series(np.zeros(len(df['vertex']), dtype=vertex_t)) if (return_distances): - df['distance'] = cudf.Series(np.zeros(len(df['vertex']), dtype=np.int32)) + df['distance'] = cudf.Series(np.zeros(len(df['vertex']), dtype=vertex_t)) # Associate to cudf Series cdef uintptr_t c_distance_ptr = NULL # Pointer to the DataFrame 'distance' Series @@ -96,14 +96,28 @@ def mg_bfs(input_df, if (return_distances): c_distance_ptr = df['distance'].__cuda_array_interface__['data'][0] - cdef bool direction = 1 - # MG BFS path assumes directed is true - c_bfs.call_bfs[int, float](handle_[0], - graph_container, - NULL, - c_distance_ptr, - c_predecessor_ptr, - NULL, - start, - direction) + cdef bool direction_optimizing = 0 + + if vertex_t == np.int32: + if depth_limit is None: + depth_limit = c_bfs.INT_MAX + c_bfs.call_bfs[int, float](handle_[0], + graph_container, + NULL, + c_distance_ptr, + c_predecessor_ptr, + depth_limit, + start, + direction_optimizing) + else: + if depth_limit is None: + depth_limit = c_bfs.LONG_MAX + c_bfs.call_bfs[long, float](handle_[0], + graph_container, + NULL, + c_distance_ptr, + c_predecessor_ptr, + depth_limit, + start, + direction_optimizing) return df diff --git a/python/cugraph/layout/force_atlas2.py b/python/cugraph/layout/force_atlas2.py index 4c6859c6c03..0b745d8ca15 100644 --- a/python/cugraph/layout/force_atlas2.py +++ b/python/cugraph/layout/force_atlas2.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2020-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -12,7 +12,7 @@ # limitations under the License. from cugraph.layout import force_atlas2_wrapper -from cugraph.structure.graph import null_check +from cugraph.structure.graph_classes import null_check def force_atlas2( diff --git a/python/cugraph/link_analysis/pagerank.py b/python/cugraph/link_analysis/pagerank.py index 8a03ee077f6..4f5f8f6aae0 100644 --- a/python/cugraph/link_analysis/pagerank.py +++ b/python/cugraph/link_analysis/pagerank.py @@ -12,7 +12,7 @@ # limitations under the License. from cugraph.link_analysis import pagerank_wrapper -from cugraph.structure.graph import null_check +from cugraph.structure.graph_classes import null_check import cugraph diff --git a/python/cugraph/link_prediction/jaccard.py b/python/cugraph/link_prediction/jaccard.py index 71cf0925342..2a9e9625050 100644 --- a/python/cugraph/link_prediction/jaccard.py +++ b/python/cugraph/link_prediction/jaccard.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -13,9 +13,8 @@ import pandas as pd import cudf -from cugraph.structure.graph import Graph +from cugraph.structure.graph_classes import Graph, null_check from cugraph.link_prediction import jaccard_wrapper -from cugraph.structure.graph import null_check from cugraph.utilities import check_nx_graph from cugraph.utilities import df_edge_score_to_dictionary diff --git a/python/cugraph/link_prediction/overlap.py b/python/cugraph/link_prediction/overlap.py index a5ca1e22979..077080bda1d 100644 --- a/python/cugraph/link_prediction/overlap.py +++ b/python/cugraph/link_prediction/overlap.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -13,7 +13,7 @@ import pandas as pd from cugraph.link_prediction import overlap_wrapper -from cugraph.structure.graph import null_check +from cugraph.structure.graph_classes import null_check import cudf from cugraph.utilities import check_nx_graph from cugraph.utilities import df_edge_score_to_dictionary diff --git a/python/cugraph/link_prediction/wjaccard.py b/python/cugraph/link_prediction/wjaccard.py index 2a4e2417102..9679d1ba9cf 100644 --- a/python/cugraph/link_prediction/wjaccard.py +++ b/python/cugraph/link_prediction/wjaccard.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -11,9 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cugraph.structure.graph import Graph +from cugraph.structure.graph_classes import Graph, null_check from cugraph.link_prediction import jaccard_wrapper -from cugraph.structure.graph import null_check import cudf diff --git a/python/cugraph/link_prediction/woverlap.py b/python/cugraph/link_prediction/woverlap.py index c93ad28ea54..fe64f812957 100644 --- a/python/cugraph/link_prediction/woverlap.py +++ b/python/cugraph/link_prediction/woverlap.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -12,7 +12,7 @@ # limitations under the License. from cugraph.link_prediction import overlap_wrapper -from cugraph.structure.graph import null_check +from cugraph.structure.graph_classes import null_check import cudf diff --git a/python/cugraph/structure/__init__.py b/python/cugraph/structure/__init__.py index ad67fe91876..b70854d61ce 100644 --- a/python/cugraph/structure/__init__.py +++ b/python/cugraph/structure/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -11,7 +11,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cugraph.structure.graph import Graph, DiGraph, MultiGraph, MultiDiGraph +from cugraph.structure.graph_classes import (Graph, + DiGraph, + MultiGraph, + MultiDiGraph, + BiPartiteGraph, + BiPartiteDiGraph) +from cugraph.structure.graph_classes import (is_weighted, + is_directed, + is_multigraph, + is_bipartite, + is_multipartite) from cugraph.structure.number_map import NumberMap from cugraph.structure.symmetrize import symmetrize, symmetrize_df , symmetrize_ddf from cugraph.structure.convert_matrix import (from_edgelist, diff --git a/python/cugraph/structure/convert_matrix.py b/python/cugraph/structure/convert_matrix.py index edd1c630185..5b3c375ea9d 100644 --- a/python/cugraph/structure/convert_matrix.py +++ b/python/cugraph/structure/convert_matrix.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -18,7 +18,7 @@ import cudf import dask_cudf -from cugraph.structure.graph import DiGraph, Graph +from cugraph.structure.graph_classes import DiGraph, Graph # optional dependencies used for handling different input types try: diff --git a/python/cugraph/structure/graph.py b/python/cugraph/structure/graph.py deleted file mode 100644 index a3024f9d081..00000000000 --- a/python/cugraph/structure/graph.py +++ /dev/null @@ -1,1509 +0,0 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from cugraph.structure import graph_primtypes_wrapper -from cugraph.structure.symmetrize import symmetrize -from cugraph.structure.number_map import NumberMap -import cugraph.dask.common.mg_utils as mg_utils -import cudf -import dask_cudf -import cugraph.comms.comms as Comms -import pandas as pd -import numpy as np -from cugraph.dask.structure import replication - - -def null_check(col): - if col.null_count != 0: - raise ValueError("Series contains NULL values") - - -class Graph: - class EdgeList: - def __init__(self, *args): - if len(args) == 1: - self.__from_dask_cudf(*args) - else: - self.__from_cudf(*args) - - def __from_cudf(self, source, destination, edge_attr=None): - self.edgelist_df = cudf.DataFrame() - self.edgelist_df["src"] = source - self.edgelist_df["dst"] = destination - self.weights = False - if edge_attr is not None: - self.weights = True - if type(edge_attr) is dict: - for k in edge_attr.keys(): - self.edgelist_df[k] = edge_attr[k] - else: - self.edgelist_df["weights"] = edge_attr - - def __from_dask_cudf(self, ddf): - self.edgelist_df = ddf - self.weights = False - # FIXME: Edge Attribute not handled - - class AdjList: - def __init__(self, offsets, indices, value=None): - self.offsets = offsets - self.indices = indices - self.weights = value # Should be a dataframe for multiple weights - - class transposedAdjList: - def __init__(self, offsets, indices, value=None): - Graph.AdjList.__init__(self, offsets, indices, value) - - """ - cuGraph graph class containing basic graph creation and transformation - operations. - """ - - def __init__( - self, - m_graph=None, - symmetrized=False, - bipartite=False, - multi=False, - dynamic=False, - ): - """ - Returns - ------- - G : cuGraph.Graph. - - Examples - -------- - >>> import cuGraph - >>> G = cuGraph.Graph() - - """ - self.symmetrized = symmetrized - self.renumbered = False - self.renumber_map = None - self.bipartite = False - self.multipartite = False - self._nodes = {} - self.multi = multi - self.distributed = False - self.dynamic = dynamic - self.self_loop = False - self.edgelist = None - self.adjlist = None - self.transposedadjlist = None - self.edge_count = None - self.node_count = None - - # MG - Batch - self.batch_enabled = False - self.batch_edgelists = None - self.batch_adjlists = None - self.batch_transposed_adjlists = None - - if m_graph is not None: - if type(m_graph) is MultiGraph or type(m_graph) is MultiDiGraph: - elist = m_graph.view_edge_list() - if m_graph.edgelist.weights: - weights = "weights" - else: - weights = None - self.from_cudf_edgelist(elist, - source="src", - destination="dst", - edge_attr=weights) - else: - msg = ( - "Graph can only be initialized using MultiGraph " - "or MultiDiGraph" - ) - raise Exception(msg) - - def enable_batch(self): - client = mg_utils.get_client() - comms = Comms.get_comms() - - if client is None or comms is None: - msg = ( - "MG Batch needs a Dask Client and the " - "Communicator needs to be initialized." - ) - raise Exception(msg) - - self.batch_enabled = True - - if self.edgelist is not None: - if self.batch_edgelists is None: - self._replicate_edgelist() - - if self.adjlist is not None: - if self.batch_adjlists is None: - self._replicate_adjlist() - - if self.transposedadjlist is not None: - if self.batch_transposed_adjlists is None: - self._replicate_transposed_adjlist() - - def _replicate_edgelist(self): - client = mg_utils.get_client() - comms = Comms.get_comms() - - # FIXME: There might be a better way to control it - if client is None: - return - work_futures = replication.replicate_cudf_dataframe( - self.edgelist.edgelist_df, client=client, comms=comms - ) - - self.batch_edgelists = work_futures - - def _replicate_adjlist(self): - client = mg_utils.get_client() - comms = Comms.get_comms() - - # FIXME: There might be a better way to control it - if client is None: - return - - weights = None - offsets_futures = replication.replicate_cudf_series( - self.adjlist.offsets, client=client, comms=comms - ) - indices_futures = replication.replicate_cudf_series( - self.adjlist.indices, client=client, comms=comms - ) - - if self.adjlist.weights is not None: - weights = replication.replicate_cudf_series(self.adjlist.weights) - else: - weights = {worker: None for worker in offsets_futures} - - merged_futures = { - worker: [ - offsets_futures[worker], - indices_futures[worker], - weights[worker], - ] - for worker in offsets_futures - } - self.batch_adjlists = merged_futures - - # FIXME: Not implemented yet - def _replicate_transposed_adjlist(self): - self.batch_transposed_adjlists = True - - def clear(self): - """ - Empty this graph. This function is added for NetworkX compatibility. - """ - self.edgelist = None - self.adjlist = None - self.transposedadjlist = None - - self.batch_edgelists = None - self.batch_adjlists = None - self.batch_transposed_adjlists = None - - def add_nodes_from(self, nodes, bipartite=None, multipartite=None): - """ - Add nodes information to the Graph. - - Parameters - ---------- - nodes : list or cudf.Series - The nodes of the graph to be stored. If bipartite and multipartite - arguments are not passed, the nodes are considered to be a list of - all the nodes present in the Graph. - bipartite : str - Sets the Graph as bipartite. The nodes are stored as a set of nodes - of the partition named as bipartite argument. - multipartite : str - Sets the Graph as multipartite. The nodes are stored as a set of - nodes of the partition named as multipartite argument. - """ - if bipartite is None and multipartite is None: - self._nodes["all_nodes"] = cudf.Series(nodes) - else: - set_names = [i for i in self._nodes.keys() if i != "all_nodes"] - if multipartite is not None: - if self.bipartite: - raise Exception( - "The Graph is already set as bipartite. " - "Use bipartite option instead." - ) - self.multipartite = True - elif bipartite is not None: - if self.multipartite: - raise Exception( - "The Graph is set as multipartite. " - "Use multipartite option instead." - ) - self.bipartite = True - multipartite = bipartite - if multipartite not in set_names and len(set_names) == 2: - raise Exception( - "The Graph is set as bipartite and " - "already has two partitions initialized." - ) - self._nodes[multipartite] = cudf.Series(nodes) - - def is_bipartite(self): - """ - Checks if Graph is bipartite. This solely relies on the user call of - add_nodes_from with the bipartite parameter. This does not parse the - graph to check if it is bipartite. - """ - # TO DO: Call coloring algorithm - return self.bipartite - - def is_multipartite(self): - """ - Checks if Graph is multipartite. This solely relies on the user call - of add_nodes_from with the partition parameter. This does not parse - the graph to check if it is multipartite. - """ - # TO DO: Call coloring algorithm - return self.multipartite or self.bipartite - - def is_multigraph(self): - """ - Returns True if the graph is a multigraph. Else returns False. - """ - return self.multi - - def sets(self): - """ - Returns the bipartite set of nodes. This solely relies on the user's - call of add_nodes_from with the bipartite parameter. This does not - parse the graph to compute bipartite sets. If bipartite argument was - not provided during add_nodes_from(), it raise an exception that the - graph is not bipartite. - """ - # TO DO: Call coloring algorithm - set_names = [i for i in self._nodes.keys() if i != "all_nodes"] - if self.bipartite: - top = self._nodes[set_names[0]] - if len(set_names) == 2: - bottom = self._nodes[set_names[1]] - else: - bottom = cudf.Series( - set(self.nodes().values_host) - set(top.values_host) - ) - return top, bottom - else: - return {k: self._nodes[k] for k in set_names} - - def from_cudf_edgelist( - self, - input_df, - source="source", - destination="destination", - edge_attr=None, - renumber=True, - ): - """ - Initialize a graph from the edge list. It is an error to call this - method on an initialized Graph object. The passed input_df argument - wraps gdf_column objects that represent a graph using the edge list - format. source argument is source column name and destination argument - is destination column name. - - By default, renumbering is enabled to map the source and destination - vertices into an index in the range [0, V) where V is the number - of vertices. If the input vertices are a single column of integers - in the range [0, V), renumbering can be disabled and the original - external vertex ids will be used. - - If weights are present, edge_attr argument is the weights column name. - - Parameters - ---------- - input_df : cudf.DataFrame or dask_cudf.DataFrame - A DataFrame that contains edge information - If a dask_cudf.DataFrame is passed it will be reinterpreted as - a cudf.DataFrame. For the distributed path please use - from_dask_cudf_edgelist. - source : str or array-like - source column name or array of column names - destination : str or array-like - destination column name or array of column names - edge_attr : str or None - the weights column name. Default is None - renumber : bool - Indicate whether or not to renumber the source and destination - vertex IDs. Default is True. - - Examples - -------- - >>> df = cudf.read_csv('datasets/karate.csv', delimiter=' ', - >>> dtype=['int32', 'int32', 'float32'], header=None) - >>> G = cugraph.Graph() - >>> G.from_cudf_edgelist(df, source='0', destination='1', - edge_attr='2', renumber=False) - - """ - if self.edgelist is not None or self.adjlist is not None: - raise Exception("Graph already has values") - - s_col = source - d_col = destination - if not isinstance(s_col, list): - s_col = [s_col] - if not isinstance(d_col, list): - d_col = [d_col] - if not ( - set(s_col).issubset(set(input_df.columns)) - and set(d_col).issubset(set(input_df.columns)) - ): - raise Exception( - "source column names and/or destination column " - "names not found in input. Recheck the source and " - "destination parameters" - ) - - # FIXME: update for smaller GPUs - # Consolidation - if isinstance(input_df, cudf.DataFrame): - if len(input_df[source]) > 2147483100: - raise Exception( - "cudf dataFrame edge list is too big " - "to fit in a single GPU" - ) - elist = input_df - elif isinstance(input_df, dask_cudf.DataFrame): - if len(input_df[source]) > 2147483100: - raise Exception( - "dask_cudf dataFrame edge list is too big " - "to fit in a single GPU" - ) - elist = input_df.compute().reset_index(drop=True) - else: - raise Exception( - "input should be a cudf.DataFrame or " - "a dask_cudf dataFrame" - ) - - renumber_map = None - if renumber: - # FIXME: Should SG do lazy evaluation like MG? - elist, renumber_map = NumberMap.renumber( - elist, source, destination, store_transposed=False - ) - source = "src" - destination = "dst" - self.renumbered = True - self.renumber_map = renumber_map - else: - if type(source) is list and type(destination) is list: - raise Exception("set renumber to True for multi column ids") - - if (elist[source] == elist[destination]).any(): - self.self_loop = True - source_col = elist[source] - dest_col = elist[destination] - - if edge_attr is not None: - value_col = elist[edge_attr] - else: - value_col = None - - if value_col is not None: - source_col, dest_col, value_col = symmetrize( - source_col, dest_col, value_col, multi=self.multi, - symmetrize=not self.symmetrized) - else: - source_col, dest_col = symmetrize( - source_col, dest_col, multi=self.multi, - symmetrize=not self.symmetrized) - - self.edgelist = Graph.EdgeList(source_col, dest_col, value_col) - - if self.batch_enabled: - self._replicate_edgelist() - - self.renumber_map = renumber_map - - def from_pandas_edgelist( - self, - pdf, - source="source", - destination="destination", - edge_attr=None, - renumber=True, - ): - """ - Initialize a graph from the edge list. It is an error to call this - method on an initialized Graph object. Source argument is source - column name and destination argument is destination column name. - - By default, renumbering is enabled to map the source and destination - vertices into an index in the range [0, V) where V is the number - of vertices. If the input vertices are a single column of integers - in the range [0, V), renumbering can be disabled and the original - external vertex ids will be used. - - If weights are present, edge_attr argument is the weights column name. - - Parameters - ---------- - input_df : pandas.DataFrame - A DataFrame that contains edge information - source : str or array-like - source column name or array of column names - destination : str or array-like - destination column name or array of column names - edge_attr : str or None - the weights column name. Default is None - renumber : bool - Indicate whether or not to renumber the source and destination - vertex IDs. Default is True. - - Examples - -------- - >>> df = pandas.read_csv('datasets/karate.csv', delimiter=' ', - >>> dtype=['int32', 'int32', 'float32'], header=None) - >>> G = cugraph.Graph() - >>> G.from_pandas_edgelist(df, source='0', destination='1', - edge_attr='2', renumber=False) - - """ - gdf = cudf.DataFrame.from_pandas(pdf) - self.from_cudf_edgelist(gdf, source=source, destination=destination, - edge_attr=edge_attr, renumber=renumber) - - def to_pandas_edgelist(self, source='source', destination='destination'): - """ - Returns the graph edge list as a Pandas DataFrame. - - Parameters - ---------- - source : str or array-like - source column name or array of column names - destination : str or array-like - destination column name or array of column names - - Returns - ------- - df : pandas.DataFrame - """ - - gdf = self.view_edge_list() - return gdf.to_pandas() - - def from_pandas_adjacency(self, pdf): - """ - Initializes the graph from pandas adjacency matrix - """ - np_array = pdf.to_numpy() - columns = pdf.columns - self.from_numpy_array(np_array, columns) - - def to_pandas_adjacency(self): - """ - Returns the graph adjacency matrix as a Pandas DataFrame. - """ - - np_array_data = self.to_numpy_array() - pdf = pd.DataFrame(np_array_data) - if self.renumbered: - nodes = self.renumber_map.implementation.df['0'].\ - values_host.tolist() - pdf.columns = nodes - pdf.index = nodes - return pdf - - def to_numpy_array(self): - """ - Returns the graph adjacency matrix as a NumPy array. - """ - - nlen = self.number_of_nodes() - elen = self.number_of_edges() - df = self.edgelist.edgelist_df - np_array = np.full((nlen, nlen), 0.0) - for i in range(0, elen): - np_array[df['src'].iloc[i], df['dst'].iloc[i]] = df['weights'].\ - iloc[i] - return np_array - - def to_numpy_matrix(self): - """ - Returns the graph adjacency matrix as a NumPy matrix. - """ - np_array = self.to_numpy_array() - return np.asmatrix(np_array) - - def from_numpy_array(self, np_array, nodes=None): - """ - Initializes the graph from numpy array containing adjacency matrix. - """ - src, dst = np_array.nonzero() - weight = np_array[src, dst] - df = cudf.DataFrame() - if nodes is not None: - df['src'] = nodes[src] - df['dst'] = nodes[dst] - else: - df['src'] = src - df['dst'] = dst - df['weight'] = weight - self.from_cudf_edgelist(df, 'src', 'dst', edge_attr='weight') - - def from_numpy_matrix(self, np_matrix): - """ - Initializes the graph from numpy matrix containing adjacency matrix. - """ - np_array = np.asarray(np_matrix) - self.from_numpy_array(np_array) - - def from_dask_cudf_edgelist( - self, - input_ddf, - source="source", - destination="destination", - edge_attr=None, - renumber=True, - ): - """ - Initializes the distributed graph from the dask_cudf.DataFrame - edgelist. Undirected Graphs are not currently supported. - - By default, renumbering is enabled to map the source and destination - vertices into an index in the range [0, V) where V is the number - of vertices. If the input vertices are a single column of integers - in the range [0, V), renumbering can be disabled and the original - external vertex ids will be used. - - Note that the graph object will store a reference to the - dask_cudf.DataFrame provided. - - Parameters - ---------- - input_ddf : dask_cudf.DataFrame - The edgelist as a dask_cudf.DataFrame - source : str or array-like - source column name or array of column names - destination : str - destination column name or array of column names - edge_attr : str - weights column name. - renumber : bool - If source and destination indices are not in range 0 to V where V - is number of vertices, renumber argument should be True. - """ - if self.edgelist is not None or self.adjlist is not None: - raise Exception("Graph already has values") - if not isinstance(input_ddf, dask_cudf.DataFrame): - raise Exception("input should be a dask_cudf dataFrame") - if type(self) is Graph: - raise Exception("Undirected distributed graph not supported") - - s_col = source - d_col = destination - if not isinstance(s_col, list): - s_col = [s_col] - if not isinstance(d_col, list): - d_col = [d_col] - if not ( - set(s_col).issubset(set(input_ddf.columns)) - and set(d_col).issubset(set(input_ddf.columns)) - ): - raise Exception( - "source column names and/or destination column " - "names not found in input. Recheck the source " - "and destination parameters" - ) - ddf_columns = s_col + d_col - if edge_attr is not None: - if not (set([edge_attr]).issubset(set(input_ddf.columns))): - raise Exception( - "edge_attr column name not found in input." - "Recheck the edge_attr parameter") - ddf_columns = ddf_columns + [edge_attr] - input_ddf = input_ddf[ddf_columns] - - if edge_attr is not None: - input_ddf = input_ddf.rename(columns={edge_attr: 'value'}) - - # - # Keep all of the original parameters so we can lazily - # evaluate this function - # - - # FIXME: Edge Attribute not handled - self.distributed = True - self.local_data = None - self.edgelist = None - self.adjlist = None - self.renumbered = renumber - self.input_df = input_ddf - self.source_columns = source - self.destination_columns = destination - self.store_tranposed = None - - def view_edge_list(self): - """ - Display the edge list. Compute it if needed. - - NOTE: If the graph is of type Graph() then the displayed undirected - edges are the same as displayed by networkx Graph(), but the direction - could be different i.e. an edge displayed by cugraph as (src, dst) - could be displayed as (dst, src) by networkx. - - cugraph.Graph stores symmetrized edgelist internally. For displaying - undirected edgelist for a Graph the upper trianglar matrix of the - symmetrized edgelist is returned. - - networkx.Graph renumbers the input and stores the upper triangle of - this renumbered input. Since the internal renumbering of networx and - cugraph is different, the upper triangular matrix of networkx - renumbered input may not be the same as cugraph's upper trianglar - matrix of the symmetrized edgelist. Hence the displayed source and - destination pairs in both will represent the same edge but node values - could be swapped. - - Returns - ------- - df : cudf.DataFrame - This cudf.DataFrame wraps source, destination and weight - - df[src] : cudf.Series - contains the source index for each edge - df[dst] : cudf.Series - contains the destination index for each edge - df[weight] : cusd.Series - Column is only present for weighted Graph, - then containing the weight value for each edge - """ - if self.distributed: - if self.edgelist is None: - raise Exception("Graph has no Edgelist.") - return self.edgelist.edgelist_df - if self.edgelist is None: - src, dst, weights = graph_primtypes_wrapper.view_edge_list(self) - self.edgelist = self.EdgeList(src, dst, weights) - - edgelist_df = self.edgelist.edgelist_df - - if self.renumbered: - edgelist_df = self.unrenumber(edgelist_df, "src") - edgelist_df = self.unrenumber(edgelist_df, "dst") - - if type(self) is Graph or type(self) is MultiGraph: - edgelist_df = edgelist_df[edgelist_df["src"] <= edgelist_df["dst"]] - edgelist_df = edgelist_df.reset_index(drop=True) - self.edge_count = len(edgelist_df) - - return edgelist_df - - def delete_edge_list(self): - """ - Delete the edge list. - """ - # decrease reference count to free memory if the referenced objects are - # no longer used. - self.edgelist = None - - def from_cudf_adjlist(self, offset_col, index_col, value_col=None): - """ - Initialize a graph from the adjacency list. It is an error to call this - method on an initialized Graph object. The passed offset_col and - index_col arguments wrap gdf_column objects that represent a graph - using the adjacency list format. - If value_col is None, an unweighted graph is created. If value_col is - not None, a weighted graph is created. - Undirected edges must be stored as directed edges in both directions. - - Parameters - ---------- - offset_col : cudf.Series - This cudf.Series wraps a gdf_column of size V + 1 (V: number of - vertices). - The gdf column contains the offsets for the vertices in this graph. - Offsets must be in the range [0, E] (E: number of edges). - index_col : cudf.Series - This cudf.Series wraps a gdf_column of size E (E: number of edges). - The gdf column contains the destination index for each edge. - Destination indices must be in the range [0, V) (V: number of - vertices). - value_col : cudf.Series, optional - This pointer can be ``None``. - If not, this cudf.Series wraps a gdf_column of size E (E: number of - edges). - The gdf column contains the weight value for each edge. - The expected type of the gdf_column element is floating point - number. - - Examples - -------- - >>> gdf = cudf.read_csv('datasets/karate.csv', delimiter=' ', - >>> dtype=['int32', 'int32', 'float32'], header=None) - >>> M = gdf.to_pandas() - >>> M = scipy.sparse.coo_matrix((M['2'],(M['0'],M['1']))) - >>> M = M.tocsr() - >>> offsets = cudf.Series(M.indptr) - >>> indices = cudf.Series(M.indices) - >>> G = cugraph.Graph() - >>> G.from_cudf_adjlist(offsets, indices, None) - - """ - if self.edgelist is not None or self.adjlist is not None: - raise Exception("Graph already has values") - self.adjlist = Graph.AdjList(offset_col, index_col, value_col) - - if self.batch_enabled: - self._replicate_adjlist() - - def compute_renumber_edge_list(self, transposed=False): - """ - Compute a renumbered edge list - - This function works in the MNMG pipeline and will transform - the input dask_cudf.DataFrame into a renumbered edge list - in the prescribed direction. - - This function will be called by the algorithms to ensure - that the graph is renumbered properly. The graph object will - cache the most recent renumbering attempt. For benchmarking - purposes, this function can be called prior to calling a - graph algorithm so we can measure the cost of computing - the renumbering separately from the cost of executing the - algorithm. - - When creating a CSR-like structure, set transposed to False. - When creating a CSC-like structure, set transposed to True. - - Parameters - ---------- - transposed : (optional) bool - If True, renumber with the intent to make a CSC-like - structure. If False, renumber with the intent to make - a CSR-like structure. Defaults to False. - """ - # FIXME: What to do about edge_attr??? - # currently ignored for MNMG - - if not self.distributed: - raise Exception( - "compute_renumber_edge_list should only be used " - "for distributed graphs" - ) - - if not self.renumbered: - self.edgelist = self.EdgeList(self.input_df) - self.renumber_map = None - else: - if self.edgelist is not None: - if type(self) is Graph: - return - - if self.store_transposed == transposed: - return - - del self.edgelist - - renumbered_ddf, number_map = NumberMap.renumber( - self.input_df, - self.source_columns, - self.destination_columns, - store_transposed=transposed, - ) - self.edgelist = self.EdgeList(renumbered_ddf) - self.renumber_map = number_map - self.store_transposed = transposed - - def view_adj_list(self): - """ - Display the adjacency list. Compute it if needed. - - Returns - ------- - offset_col : cudf.Series - This cudf.Series wraps a gdf_column of size V + 1 (V: number of - vertices). - The gdf column contains the offsets for the vertices in this graph. - Offsets are in the range [0, E] (E: number of edges). - index_col : cudf.Series - This cudf.Series wraps a gdf_column of size E (E: number of edges). - The gdf column contains the destination index for each edge. - Destination indices are in the range [0, V) (V: number of - vertices). - value_col : cudf.Series or ``None`` - This pointer is ``None`` for unweighted graphs. - For weighted graphs, this cudf.Series wraps a gdf_column of size E - (E: number of edges). - The gdf column contains the weight value for each edge. - The expected type of the gdf_column element is floating point - number. - """ - if self.distributed: - raise Exception("Not supported for distributed graph") - - if self.adjlist is None: - if self.transposedadjlist is not None and type(self) is Graph: - off, ind, vals = ( - self.transposedadjlist.offsets, - self.transposedadjlist.indices, - self.transposedadjlist.weights, - ) - else: - off, ind, vals = graph_primtypes_wrapper.view_adj_list(self) - self.adjlist = self.AdjList(off, ind, vals) - - if self.batch_enabled: - self._replicate_adjlist() - - return self.adjlist.offsets, self.adjlist.indices, self.adjlist.weights - - def view_transposed_adj_list(self): - """ - Display the transposed adjacency list. Compute it if needed. - - Returns - ------- - offset_col : cudf.Series - This cudf.Series wraps a gdf_column of size V + 1 (V: number of - vertices). - The gdf column contains the offsets for the vertices in this graph. - Offsets are in the range [0, E] (E: number of edges). - index_col : cudf.Series - This cudf.Series wraps a gdf_column of size E (E: number of edges). - The gdf column contains the destination index for each edge. - Destination indices are in the range [0, V) (V: number of - vertices). - value_col : cudf.Series or ``None`` - This pointer is ``None`` for unweighted graphs. - For weighted graphs, this cudf.Series wraps a gdf_column of size E - (E: number of edges). - The gdf column contains the weight value for each edge. - The expected type of the gdf_column element is floating point - number. - - """ - if self.distributed: - raise Exception("Not supported for distributed graph") - if self.transposedadjlist is None: - if self.adjlist is not None and type(self) is Graph: - off, ind, vals = ( - self.adjlist.offsets, - self.adjlist.indices, - self.adjlist.weights, - ) - else: - ( - off, - ind, - vals, - ) = graph_primtypes_wrapper.view_transposed_adj_list(self) - self.transposedadjlist = self.transposedAdjList(off, ind, vals) - - if self.batch_enabled: - self._replicate_transposed_adjlist() - - return ( - self.transposedadjlist.offsets, - self.transposedadjlist.indices, - self.transposedadjlist.weights, - ) - - def delete_adj_list(self): - """ - Delete the adjacency list. - """ - self.adjlist = None - - def get_two_hop_neighbors(self): - """ - Compute vertex pairs that are two hops apart. The resulting pairs are - sorted before returning. - - Returns - ------- - df : cudf.DataFrame - df[first] : cudf.Series - the first vertex id of a pair, if an external vertex id - is defined by only one column - df[second] : cudf.Series - the second vertex id of a pair, if an external vertex id - is defined by only one column - """ - if self.distributed: - raise Exception("Not supported for distributed graph") - df = graph_primtypes_wrapper.get_two_hop_neighbors(self) - if self.renumbered is True: - df = self.unrenumber(df, "first") - df = self.unrenumber(df, "second") - - return df - - def number_of_vertices(self): - """ - Get the number of nodes in the graph. - - """ - if self.node_count is None: - if self.distributed: - if self.edgelist is not None: - ddf = self.edgelist.edgelist_df[["src", "dst"]] - self.node_count = ddf.max().max().compute() + 1 - else: - raise Exception("Graph is Empty") - elif self.adjlist is not None: - self.node_count = len(self.adjlist.offsets) - 1 - elif self.transposedadjlist is not None: - self.node_count = len(self.transposedadjlist.offsets) - 1 - elif self.edgelist is not None: - df = self.edgelist.edgelist_df[["src", "dst"]] - self.node_count = df.max().max() + 1 - else: - raise Exception("Graph is Empty") - return self.node_count - - def number_of_nodes(self): - """ - An alias of number_of_vertices(). This function is added for NetworkX - compatibility. - - """ - return self.number_of_vertices() - - def number_of_edges(self, directed_edges=False): - """ - Get the number of edges in the graph. - - """ - if self.distributed: - if self.edgelist is not None: - return len(self.edgelist.edgelist_df) - else: - raise ValueError("Graph is Empty") - if directed_edges and self.edgelist is not None: - return len(self.edgelist.edgelist_df) - if self.edge_count is None: - if self.edgelist is not None: - if type(self) is Graph or type(self) is MultiGraph: - self.edge_count = len( - self.edgelist.edgelist_df[ - self.edgelist.edgelist_df["src"] - >= self.edgelist.edgelist_df["dst"] - ] - ) - else: - self.edge_count = len(self.edgelist.edgelist_df) - elif self.adjlist is not None: - self.edge_count = len(self.adjlist.indices) - elif self.transposedadjlist is not None: - self.edge_count = len(self.transposedadjlist.indices) - else: - raise ValueError("Graph is Empty") - return self.edge_count - - def in_degree(self, vertex_subset=None): - """ - Compute vertex in-degree. Vertex in-degree is the number of edges - pointing into the vertex. By default, this method computes vertex - degrees for the entire set of vertices. If vertex_subset is provided, - this method optionally filters out all but those listed in - vertex_subset. - - Parameters - ---------- - vertex_subset : cudf.Series or iterable container, optional - A container of vertices for displaying corresponding in-degree. - If not set, degrees are computed for the entire set of vertices. - - Returns - ------- - df : cudf.DataFrame - GPU DataFrame of size N (the default) or the size of the given - vertices (vertex_subset) containing the in_degree. The ordering is - relative to the adjacency list, or that given by the specified - vertex_subset. - - df[vertex] : cudf.Series - The vertex IDs (will be identical to vertex_subset if - specified). - df[degree] : cudf.Series - The computed in-degree of the corresponding vertex. - - Examples - -------- - >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', - >>> dtype=['int32', 'int32', 'float32'], header=None) - >>> G = cugraph.Graph() - >>> G.from_cudf_edgelist(M, '0', '1') - >>> df = G.in_degree([0,9,12]) - - """ - return self._degree(vertex_subset, x=1) - - def out_degree(self, vertex_subset=None): - """ - Compute vertex out-degree. Vertex out-degree is the number of edges - pointing out from the vertex. By default, this method computes vertex - degrees for the entire set of vertices. If vertex_subset is provided, - this method optionally filters out all but those listed in - vertex_subset. - - Parameters - ---------- - vertex_subset : cudf.Series or iterable container, optional - A container of vertices for displaying corresponding out-degree. - If not set, degrees are computed for the entire set of vertices. - - Returns - ------- - df : cudf.DataFrame - GPU DataFrame of size N (the default) or the size of the given - vertices (vertex_subset) containing the out_degree. The ordering is - relative to the adjacency list, or that given by the specified - vertex_subset. - - df[vertex] : cudf.Series - The vertex IDs (will be identical to vertex_subset if - specified). - df[degree] : cudf.Series - The computed out-degree of the corresponding vertex. - - Examples - -------- - >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', - >>> dtype=['int32', 'int32', 'float32'], header=None) - >>> G = cugraph.Graph() - >>> G.from_cudf_edgelist(M, '0', '1') - >>> df = G.out_degree([0,9,12]) - - """ - if self.distributed: - raise Exception("Not supported for distributed graph") - return self._degree(vertex_subset, x=2) - - def degree(self, vertex_subset=None): - """ - Compute vertex degree, which is the total number of edges incident - to a vertex (both in and out edges). By default, this method computes - degrees for the entire set of vertices. If vertex_subset is provided, - then this method optionally filters out all but those listed in - vertex_subset. - - Parameters - ---------- - vertex_subset : cudf.Series or iterable container, optional - a container of vertices for displaying corresponding degree. If not - set, degrees are computed for the entire set of vertices. - - Returns - ------- - df : cudf.DataFrame - GPU DataFrame of size N (the default) or the size of the given - vertices (vertex_subset) containing the degree. The ordering is - relative to the adjacency list, or that given by the specified - vertex_subset. - - df['vertex'] : cudf.Series - The vertex IDs (will be identical to vertex_subset if - specified). - df['degree'] : cudf.Series - The computed degree of the corresponding vertex. - - Examples - -------- - >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', - >>> dtype=['int32', 'int32', 'float32'], header=None) - >>> G = cugraph.Graph() - >>> G.from_cudf_edgelist(M, '0', '1') - >>> all_df = G.degree() - >>> subset_df = G.degree([0,9,12]) - - """ - if self.distributed: - raise Exception("Not supported for distributed graph") - return self._degree(vertex_subset) - - # FIXME: vertex_subset could be a DataFrame for multi-column vertices - def degrees(self, vertex_subset=None): - """ - Compute vertex in-degree and out-degree. By default, this method - computes vertex degrees for the entire set of vertices. If - vertex_subset is provided, this method optionally filters out all but - those listed in vertex_subset. - - Parameters - ---------- - vertex_subset : cudf.Series or iterable container, optional - A container of vertices for displaying corresponding degree. If not - set, degrees are computed for the entire set of vertices. - - Returns - ------- - df : cudf.DataFrame - GPU DataFrame of size N (the default) or the size of the given - vertices (vertex_subset) containing the degrees. The ordering is - relative to the adjacency list, or that given by the specified - vertex_subset. - - df['vertex'] : cudf.Series - The vertex IDs (will be identical to vertex_subset if - specified). - df['in_degree'] : cudf.Series - The in-degree of the vertex. - df['out_degree'] : cudf.Series - The out-degree of the vertex. - - Examples - -------- - >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', - >>> dtype=['int32', 'int32', 'float32'], header=None) - >>> G = cugraph.Graph() - >>> G.from_cudf_edgelist(M, '0', '1') - >>> df = G.degrees([0,9,12]) - - """ - if self.distributed: - raise Exception("Not supported for distributed graph") - ( - vertex_col, - in_degree_col, - out_degree_col, - ) = graph_primtypes_wrapper._degrees(self) - - df = cudf.DataFrame() - df["vertex"] = vertex_col - df["in_degree"] = in_degree_col - df["out_degree"] = out_degree_col - - if self.renumbered is True: - df = self.unrenumber(df, "vertex") - - if vertex_subset is not None: - df = df[df['vertex'].isin(vertex_subset)] - - return df - - def _degree(self, vertex_subset, x=0): - vertex_col, degree_col = graph_primtypes_wrapper._degree(self, x) - df = cudf.DataFrame() - df["vertex"] = vertex_col - df["degree"] = degree_col - - if self.renumbered is True: - df = self.unrenumber(df, "vertex") - - if vertex_subset is not None: - df = df[df['vertex'].isin(vertex_subset)] - - return df - - def to_directed(self): - """ - Return a directed representation of the graph. - This function sets the type of graph as DiGraph() and returns the - directed view. - - Returns - ------- - G : DiGraph - A directed graph with the same nodes, and each edge (u,v,weights) - replaced by two directed edges (u,v,weights) and (v,u,weights). - - Examples - -------- - >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', - >>> dtype=['int32', 'int32', 'float32'], header=None) - >>> G = cugraph.Graph() - >>> G.from_cudf_edgelist(M, '0', '1') - >>> DiG = G.to_directed() - - """ - if self.distributed: - raise Exception("Not supported for distributed graph") - if type(self) is DiGraph: - return self - if type(self) is Graph: - DiG = DiGraph() - DiG.renumbered = self.renumbered - DiG.renumber_map = self.renumber_map - DiG.edgelist = self.edgelist - DiG.adjlist = self.adjlist - DiG.transposedadjlist = self.transposedadjlist - return DiG - - def to_undirected(self): - """ - Return an undirected copy of the graph. - - Returns - ------- - G : Graph - A undirected graph with the same nodes, and each directed edge - (u,v,weights) replaced by an undirected edge (u,v,weights). - - Examples - -------- - >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', - >>> dtype=['int32', 'int32', 'float32'], header=None) - >>> DiG = cugraph.DiGraph() - >>> DiG.from_cudf_edgelist(M, '0', '1') - >>> G = DiG.to_undirected() - - """ - - if type(self) is Graph: - return self - if type(self) is DiGraph: - G = Graph() - df = self.edgelist.edgelist_df - G.renumbered = self.renumbered - G.renumber_map = self.renumber_map - G.multi = self.multi - if self.edgelist.weights: - source_col, dest_col, value_col = symmetrize( - df["src"], df["dst"], df["weights"] - ) - else: - source_col, dest_col = symmetrize(df["src"], df["dst"]) - value_col = None - G.edgelist = Graph.EdgeList(source_col, dest_col, value_col) - - return G - - def is_directed(self): - if type(self) is DiGraph: - return True - else: - return False - - def has_node(self, n): - """ - Returns True if the graph contains the node n. - """ - if self.edgelist is None: - raise Exception("Graph has no Edgelist.") - if self.distributed: - ddf = self.edgelist.edgelist_df[["src", "dst"]] - return (ddf == n).any().any().compute() - if self.renumbered: - tmp = self.renumber_map.to_internal_vertex_id(cudf.Series([n])) - return tmp[0] is not cudf.NA and tmp[0] >= 0 - else: - df = self.edgelist.edgelist_df[["src", "dst"]] - return (df == n).any().any() - - def has_edge(self, u, v): - """ - Returns True if the graph contains the edge (u,v). - """ - if self.edgelist is None: - raise Exception("Graph has no Edgelist.") - if self.renumbered: - tmp = cudf.DataFrame({"src": [u, v]}) - tmp = tmp.astype({"src": "int"}) - tmp = self.add_internal_vertex_id( - tmp, "id", "src", preserve_order=True - ) - - u = tmp["id"][0] - v = tmp["id"][1] - - df = self.edgelist.edgelist_df - if self.distributed: - return ((df["src"] == u) & (df["dst"] == v)).any().compute() - return ((df["src"] == u) & (df["dst"] == v)).any() - - def edges(self): - """ - Returns all the edges in the graph as a cudf.DataFrame containing - sources and destinations. It does not return the edge weights. - For viewing edges with weights use view_edge_list() - """ - return self.view_edge_list()[["src", "dst"]] - - def nodes(self): - """ - Returns all the nodes in the graph as a cudf.Series - """ - if self.distributed: - raise Exception("Not supported for distributed graph") - if self.edgelist is not None: - df = self.edgelist.edgelist_df - if self.renumbered: - # FIXME: If vertices are multicolumn - # this needs to return a dataframe - # FIXME: This relies on current implementation - # of NumberMap, should not really expose - # this, perhaps add a method to NumberMap - return self.renumber_map.implementation.df["0"] - else: - return cudf.concat([df["src"], df["dst"]]).unique() - if self.adjlist is not None: - return cudf.Series(np.arange(0, self.number_of_nodes())) - if "all_nodes" in self._nodes.keys(): - return self._nodes["all_nodes"] - else: - n = cudf.Series(dtype="int") - set_names = [i for i in self._nodes.keys() if i != "all_nodes"] - for k in set_names: - n = n.append(self._nodes[k]) - return n - - def neighbors(self, n): - if self.edgelist is None: - raise Exception("Graph has no Edgelist.") - if self.distributed: - ddf = self.edgelist.edgelist_df - return ddf[ddf["src"] == n]["dst"].reset_index(drop=True) - if self.renumbered: - node = self.renumber_map.to_internal_vertex_id(cudf.Series([n])) - if len(node) == 0: - return cudf.Series(dtype="int") - n = node[0] - - df = self.edgelist.edgelist_df - neighbors = df[df["src"] == n]["dst"].reset_index(drop=True) - if self.renumbered: - # FIXME: Multi-column vertices - return self.renumber_map.from_internal_vertex_id(neighbors)["0"] - else: - return neighbors - - def unrenumber(self, df, column_name, preserve_order=False): - """ - Given a DataFrame containing internal vertex ids in the identified - column, replace this with external vertex ids. If the renumbering - is from a single column, the output dataframe will use the same - name for the external vertex identifiers. If the renumbering is from - a multi-column input, the output columns will be labeled 0 through - n-1 with a suffix of _column_name. - - Note that this function does not guarantee order in single GPU mode, - and does not guarantee order or partitioning in multi-GPU mode. If you - wish to preserve ordering, add an index column to df and sort the - return by that index column. - - Parameters - ---------- - df: cudf.DataFrame or dask_cudf.DataFrame - A DataFrame containing internal vertex identifiers that will be - converted into external vertex identifiers. - - column_name: string - Name of the column containing the internal vertex id. - - preserve_order: (optional) bool - If True, preserve the order of the rows in the output - DataFrame to match the input DataFrame - - Returns - --------- - df : cudf.DataFrame or dask_cudf.DataFrame - The original DataFrame columns exist unmodified. The external - vertex identifiers are added to the DataFrame, the internal - vertex identifier column is removed from the dataframe. - """ - return self.renumber_map.unrenumber(df, column_name, preserve_order) - - def lookup_internal_vertex_id(self, df, column_name=None): - """ - Given a DataFrame containing external vertex ids in the identified - columns, or a Series containing external vertex ids, return a - Series with the internal vertex ids. - - Note that this function does not guarantee order in single GPU mode, - and does not guarantee order or partitioning in multi-GPU mode. - - Parameters - ---------- - df: cudf.DataFrame, cudf.Series, dask_cudf.DataFrame, dask_cudf.Series - A DataFrame containing external vertex identifiers that will be - converted into internal vertex identifiers. - - column_name: (optional) string - Name of the column containing the external vertex ids - - Returns - --------- - series : cudf.Series or dask_cudf.Series - The internal vertex identifiers - """ - return self.renumber_map.to_internal_vertex_id(df, column_name) - - def add_internal_vertex_id( - self, - df, - internal_column_name, - external_column_name, - drop=True, - preserve_order=False, - ): - """ - Given a DataFrame containing external vertex ids in the identified - columns, return a DataFrame containing the internal vertex ids as the - specified column name. Optionally drop the external vertex id columns. - Optionally preserve the order of the original DataFrame. - - Parameters - ---------- - df: cudf.DataFrame or dask_cudf.DataFrame - A DataFrame containing external vertex identifiers that will be - converted into internal vertex identifiers. - - internal_column_name: string - Name of column to contain the internal vertex id - - external_column_name: string or list of strings - Name of the column(s) containing the external vertex ids - - drop: (optional) bool, defaults to True - Drop the external columns from the returned DataFrame - - preserve_order: (optional) bool, defaults to False - Preserve the order of the data frame (requires an extra sort) - - Returns - --------- - df : cudf.DataFrame or dask_cudf.DataFrame - Original DataFrame with new column containing internal vertex - id - """ - return self.renumber_map.add_internal_vertex_id( - df, - internal_column_name, - external_column_name, - drop, - preserve_order, - ) - - -class DiGraph(Graph): - """ - cuGraph directed graph class. Drops parallel edges. - """ - def __init__(self, m_graph=None): - super().__init__( - m_graph=m_graph, symmetrized=True - ) - - -class MultiGraph(Graph): - """ - cuGraph class to create and store undirected graphs with parallel edges. - """ - def __init__(self, renumbered=True): - super().__init__(multi=True) - - -class MultiDiGraph(Graph): - """ - cuGraph class to create and store directed graphs with parallel edges. - """ - def __init__(self, renumbered=True): - super().__init__(symmetrized=True, multi=True) diff --git a/python/cugraph/structure/graph_classes.py b/python/cugraph/structure/graph_classes.py new file mode 100644 index 00000000000..3cd1863a054 --- /dev/null +++ b/python/cugraph/structure/graph_classes.py @@ -0,0 +1,743 @@ +# Copyright (c) 2021, NVIDIA CORPORATION. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from .graph_implementation import (simpleGraphImpl, + simpleDistributedGraphImpl, + npartiteGraphImpl) +import cudf +import warnings + + +# TODO: Move to utilities +def null_check(col): + if col.null_count != 0: + raise ValueError("Series contains NULL values") + + +class Graph: + class Properties: + def __init__(self, directed): + self.directed = directed + self.weights = False + + def __init__(self, m_graph=None, directed=False): + self._Impl = None + self.graph_properties = Graph.Properties(directed) + if m_graph is not None: + if m_graph.is_multigraph(): + elist = m_graph.view_edge_list() + if m_graph.is_weighted(): + weights = "weights" + else: + weights = None + self.from_cudf_edgelist(elist, + source="src", + destination="dst", + edge_attr=weights) + else: + msg = ( + "Graph can only be initialized using MultiGraph " + "or MultiDiGraph" + ) + raise Exception(msg) + + def __getattr__(self, name): + if self._Impl is None: + raise AttributeError(name) + if hasattr(self._Impl, name): + return getattr(self._Impl, name) + # FIXME: Remove access to Impl properties + elif hasattr(self._Impl.properties, name): + return getattr(self._Impl.properties, name) + else: + raise AttributeError(name) + + def __dir__(self): + return dir(self._Impl) + + def from_cudf_edgelist( + self, + input_df, + source="source", + destination="destination", + edge_attr=None, + renumber=True + ): + """ + Initialize a graph from the edge list. It is an error to call this + method on an initialized Graph object. The passed input_df argument + wraps gdf_column objects that represent a graph using the edge list + format. source argument is source column name and destination argument + is destination column name. + By default, renumbering is enabled to map the source and destination + vertices into an index in the range [0, V) where V is the number + of vertices. If the input vertices are a single column of integers + in the range [0, V), renumbering can be disabled and the original + external vertex ids will be used. + If weights are present, edge_attr argument is the weights column name. + Parameters + ---------- + input_df : cudf.DataFrame or dask_cudf.DataFrame + A DataFrame that contains edge information + If a dask_cudf.DataFrame is passed it will be reinterpreted as + a cudf.DataFrame. For the distributed path please use + from_dask_cudf_edgelist. + source : str or array-like + source column name or array of column names + destination : str or array-like + destination column name or array of column names + edge_attr : str or None + the weights column name. Default is None + renumber : bool + Indicate whether or not to renumber the source and destination + vertex IDs. Default is True. + Examples + -------- + >>> df = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_cudf_edgelist(df, source='0', destination='1', + edge_attr='2', renumber=False) + """ + if self._Impl is None: + self._Impl = simpleGraphImpl(self.graph_properties) + elif type(self._Impl) is not simpleGraphImpl: + raise Exception("Graph is already initialized") + elif (self._Impl.edgelist is not None or + self._Impl.adjlist is not None): + raise Exception("Graph already has values") + self._Impl._simpleGraphImpl__from_edgelist(input_df, + source=source, + destination=destination, + edge_attr=edge_attr, + renumber=renumber) + + def from_cudf_adjlist(self, offset_col, index_col, value_col=None): + """ + Initialize a graph from the adjacency list. It is an error to call this + method on an initialized Graph object. The passed offset_col and + index_col arguments wrap gdf_column objects that represent a graph + using the adjacency list format. + If value_col is None, an unweighted graph is created. If value_col is + not None, a weighted graph is created. + Undirected edges must be stored as directed edges in both directions. + Parameters + ---------- + offset_col : cudf.Series + This cudf.Series wraps a gdf_column of size V + 1 (V: number of + vertices). + The gdf column contains the offsets for the vertices in this graph. + Offsets must be in the range [0, E] (E: number of edges). + index_col : cudf.Series + This cudf.Series wraps a gdf_column of size E (E: number of edges). + The gdf column contains the destination index for each edge. + Destination indices must be in the range [0, V) (V: number of + vertices). + value_col : cudf.Series, optional + This pointer can be ``None``. + If not, this cudf.Series wraps a gdf_column of size E (E: number of + edges). + The gdf column contains the weight value for each edge. + The expected type of the gdf_column element is floating point + number. + Examples + -------- + >>> gdf = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> M = gdf.to_pandas() + >>> M = scipy.sparse.coo_matrix((M['2'],(M['0'],M['1']))) + >>> M = M.tocsr() + >>> offsets = cudf.Series(M.indptr) + >>> indices = cudf.Series(M.indices) + >>> G = cugraph.Graph() + >>> G.from_cudf_adjlist(offsets, indices, None) + """ + if self._Impl is None: + self._Impl = simpleGraphImpl(self.graph_properties) + elif type(self._Impl) is not simpleGraphImpl: + raise Exception("Graph is already initialized") + elif (self._Impl.edgelist is not None or + self._Impl.adjlist is not None): + raise Exception("Graph already has values") + self._Impl._simpleGraphImpl__from_adjlist(offset_col, + index_col, + value_col) + + def from_dask_cudf_edgelist( + self, + input_ddf, + source="source", + destination="destination", + edge_attr=None, + renumber=True, + ): + """ + Initializes the distributed graph from the dask_cudf.DataFrame + edgelist. Undirected Graphs are not currently supported. + By default, renumbering is enabled to map the source and destination + vertices into an index in the range [0, V) where V is the number + of vertices. If the input vertices are a single column of integers + in the range [0, V), renumbering can be disabled and the original + external vertex ids will be used. + Note that the graph object will store a reference to the + dask_cudf.DataFrame provided. + Parameters + ---------- + input_ddf : dask_cudf.DataFrame + The edgelist as a dask_cudf.DataFrame + source : str or array-like + source column name or array of column names + destination : str + destination column name or array of column names + edge_attr : str + weights column name. + renumber : bool + If source and destination indices are not in range 0 to V where V + is number of vertices, renumber argument should be True. + """ + if self._Impl is None: + self._Impl = simpleDistributedGraphImpl(self.graph_properties) + elif type(self._Impl) is not simpleDistributedGraphImpl: + raise Exception("Graph is already initialized") + elif (self._Impl.edgelist is not None): + raise Exception("Graph already has values") + self._Impl._simpleDistributedGraphImpl__from_edgelist(input_ddf, + source, + destination, + edge_attr, + renumber) + + # Move to Compat Module + def from_pandas_edgelist( + self, + pdf, + source="source", + destination="destination", + edge_attr=None, + renumber=True, + ): + """ + Initialize a graph from the edge list. It is an error to call this + method on an initialized Graph object. Source argument is source + column name and destination argument is destination column name. + By default, renumbering is enabled to map the source and destination + vertices into an index in the range [0, V) where V is the number + of vertices. If the input vertices are a single column of integers + in the range [0, V), renumbering can be disabled and the original + external vertex ids will be used. + If weights are present, edge_attr argument is the weights column name. + Parameters + ---------- + input_df : pandas.DataFrame + A DataFrame that contains edge information + source : str or array-like + source column name or array of column names + destination : str or array-like + destination column name or array of column names + edge_attr : str or None + the weights column name. Default is None + renumber : bool + Indicate whether or not to renumber the source and destination + vertex IDs. Default is True. + Examples + -------- + >>> df = pandas.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_pandas_edgelist(df, source='0', destination='1', + edge_attr='2', renumber=False) + """ + gdf = cudf.DataFrame.from_pandas(pdf) + self.from_cudf_edgelist(gdf, source=source, destination=destination, + edge_attr=edge_attr, renumber=renumber) + + def from_pandas_adjacency(self, pdf): + """ + Initializes the graph from pandas adjacency matrix + """ + np_array = pdf.to_numpy() + columns = pdf.columns + self.from_numpy_array(np_array, columns) + + def from_numpy_array(self, np_array, nodes=None): + """ + Initializes the graph from numpy array containing adjacency matrix. + """ + src, dst = np_array.nonzero() + weight = np_array[src, dst] + df = cudf.DataFrame() + if nodes is not None: + df['src'] = nodes[src] + df['dst'] = nodes[dst] + else: + df['src'] = src + df['dst'] = dst + df['weight'] = weight + self.from_cudf_edgelist(df, 'src', 'dst', edge_attr='weight') + + def from_numpy_matrix(self, np_matrix): + """ + Initializes the graph from numpy matrix containing adjacency matrix. + """ + np_array = np.asarray(np_matrix) + self.from_numpy_array(np_array) + + def unrenumber(self, df, column_name, preserve_order=False): + """ + Given a DataFrame containing internal vertex ids in the identified + column, replace this with external vertex ids. If the renumbering + is from a single column, the output dataframe will use the same + name for the external vertex identifiers. If the renumbering is from + a multi-column input, the output columns will be labeled 0 through + n-1 with a suffix of _column_name. + Note that this function does not guarantee order in single GPU mode, + and does not guarantee order or partitioning in multi-GPU mode. If you + wish to preserve ordering, add an index column to df and sort the + return by that index column. + Parameters + ---------- + df: cudf.DataFrame or dask_cudf.DataFrame + A DataFrame containing internal vertex identifiers that will be + converted into external vertex identifiers. + column_name: string + Name of the column containing the internal vertex id. + preserve_order: (optional) bool + If True, preserve the order of the rows in the output + DataFrame to match the input DataFrame + Returns + --------- + df : cudf.DataFrame or dask_cudf.DataFrame + The original DataFrame columns exist unmodified. The external + vertex identifiers are added to the DataFrame, the internal + vertex identifier column is removed from the dataframe. + """ + return self.renumber_map.unrenumber(df, column_name, preserve_order) + + def lookup_internal_vertex_id(self, df, column_name=None): + """ + Given a DataFrame containing external vertex ids in the identified + columns, or a Series containing external vertex ids, return a + Series with the internal vertex ids. + Note that this function does not guarantee order in single GPU mode, + and does not guarantee order or partitioning in multi-GPU mode. + Parameters + ---------- + df: cudf.DataFrame, cudf.Series, dask_cudf.DataFrame, dask_cudf.Series + A DataFrame containing external vertex identifiers that will be + converted into internal vertex identifiers. + column_name: (optional) string + Name of the column containing the external vertex ids + Returns + --------- + series : cudf.Series or dask_cudf.Series + The internal vertex identifiers + """ + return self.renumber_map.to_internal_vertex_id(df, column_name) + + def add_internal_vertex_id( + self, + df, + internal_column_name, + external_column_name, + drop=True, + preserve_order=False, + ): + """ + Given a DataFrame containing external vertex ids in the identified + columns, return a DataFrame containing the internal vertex ids as the + specified column name. Optionally drop the external vertex id columns. + Optionally preserve the order of the original DataFrame. + Parameters + ---------- + df: cudf.DataFrame or dask_cudf.DataFrame + A DataFrame containing external vertex identifiers that will be + converted into internal vertex identifiers. + internal_column_name: string + Name of column to contain the internal vertex id + external_column_name: string or list of strings + Name of the column(s) containing the external vertex ids + drop: (optional) bool, defaults to True + Drop the external columns from the returned DataFrame + preserve_order: (optional) bool, defaults to False + Preserve the order of the data frame (requires an extra sort) + Returns + --------- + df : cudf.DataFrame or dask_cudf.DataFrame + Original DataFrame with new column containing internal vertex + id + """ + return self.renumber_map.add_internal_vertex_id( + df, + internal_column_name, + external_column_name, + drop, + preserve_order, + ) + + def clear(self): + """ + Empty the graph. + """ + self._Impl = None + + def is_bipartite(self): + """ + Checks if Graph is bipartite. This solely relies on the user call of + add_nodes_from with the bipartite parameter. This does not parse the + graph to check if it is bipartite. + """ + # TO DO: Call coloring algorithm + return False + + def is_multipartite(self): + """ + Checks if Graph is multipartite. This solely relies on the user call + of add_nodes_from with the partition parameter. This does not parse + the graph to check if it is multipartite. + """ + # TO DO: Call coloring algorithm + return False + + def is_multigraph(self): + """ + Returns True if the graph is a multigraph. Else returns False. + """ + # TO DO: Call coloring algorithm + return False + + def is_directed(self): + """ + Returns True if the graph is a directed graph. + Returns False if the graph is an undirected graph. + """ + return self.graph_properties.directed + + def is_renumbered(self): + """ + Returns True if the graph is renumbered. + """ + return self.properties.renumbered + + def is_weighted(self): + """ + Returns True if the graph has edge weights. + """ + return self.properties.weighted + + def has_isolated_vertices(self): + """ + Returns True if the graph has isolated vertices. + """ + return self.properties.isolated_vertices + + def to_directed(self): + """ + Return a directed representation of the graph. + This function sets the type of graph as DiGraph() and returns the + directed view. + Returns + ------- + G : DiGraph + A directed graph with the same nodes, and each edge (u,v,weights) + replaced by two directed edges (u,v,weights) and (v,u,weights). + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_cudf_edgelist(M, '0', '1') + >>> DiG = G.to_directed() + """ + directed_graph = type(self)() + directed_graph.graph_properties.directed = True + directed_graph._Impl = type(self._Impl)(directed_graph. + graph_properties) + self._Impl.to_directed(directed_graph._Impl) + return directed_graph + + def to_undirected(self): + """ + Return an undirected copy of the graph. + Returns + ------- + G : Graph + A undirected graph with the same nodes, and each directed edge + (u,v,weights) replaced by an undirected edge (u,v,weights). + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> DiG = cugraph.DiGraph() + >>> DiG.from_cudf_edgelist(M, '0', '1') + >>> G = DiG.to_undirected() + """ + + if self.graph_properties.directed is False: + undirected_graph = type(self)() + elif self.__class__.__bases__[0] == object: + undirected_graph = type(self)() + else: + undirected_graph = self.__class__.__bases__[0]() + undirected_graph._Impl = type(self._Impl)(undirected_graph. + graph_properties) + self._Impl.to_undirected(undirected_graph._Impl) + return undirected_graph + + def add_nodes_from(self, nodes): + """ + Add nodes information to the Graph. + Parameters + ---------- + nodes : list or cudf.Series + The nodes of the graph to be stored. + """ + self._Impl._nodes["all_nodes"] = cudf.Series(nodes) + + # TODO: Add function + # def properties(): + + +class DiGraph(Graph): + def __init__(self, m_graph=None): + warnings.warn( + "DiGraph is deprecated, use Graph(directed=True) instead", + DeprecationWarning + ) + super(DiGraph, self).__init__(m_graph, directed=True) + + +class MultiGraph(Graph): + def __init__(self, directed=False): + super(MultiGraph, self).__init__(directed=directed) + self.graph_properties.multi_edge = True + + def is_multigraph(self): + """ + Returns True if the graph is a multigraph. Else returns False. + """ + # TO DO: Call coloring algorithm + return True + + +class MultiDiGraph(MultiGraph): + def __init__(self): + warnings.warn( + "MultiDiGraph is deprecated,\ + use MultiGraph(directed=True) instead", + DeprecationWarning + ) + super(MultiDiGraph, self).__init__(directed=True) + + +class Tree(Graph): + def __init__(self, directed=False): + super(Tree, self).__init__(directed=directed) + self.graph_properties.tree = True + + +class NPartiteGraph(Graph): + def __init__(self, bipartite=False, directed=False): + super(NPartiteGraph, self).__init__(directed=directed) + self.graph_properties.bipartite = bipartite + self.graph_properties.multipartite = True + + def from_cudf_edgelist( + self, + input_df, + source="source", + destination="destination", + edge_attr=None, + renumber=True + ): + """ + Initialize a graph from the edge list. It is an error to call this + method on an initialized Graph object. The passed input_df argument + wraps gdf_column objects that represent a graph using the edge list + format. source argument is source column name and destination argument + is destination column name. + By default, renumbering is enabled to map the source and destination + vertices into an index in the range [0, V) where V is the number + of vertices. If the input vertices are a single column of integers + in the range [0, V), renumbering can be disabled and the original + external vertex ids will be used. + If weights are present, edge_attr argument is the weights column name. + Parameters + ---------- + input_df : cudf.DataFrame or dask_cudf.DataFrame + A DataFrame that contains edge information + If a dask_cudf.DataFrame is passed it will be reinterpreted as + a cudf.DataFrame. For the distributed path please use + from_dask_cudf_edgelist. + source : str or array-like + source column name or array of column names + destination : str or array-like + destination column name or array of column names + edge_attr : str or None + the weights column name. Default is None + renumber : bool + Indicate whether or not to renumber the source and destination + vertex IDs. Default is True. + Examples + -------- + >>> df = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.BiPartiteGraph() + >>> G.from_cudf_edgelist(df, source='0', destination='1', + edge_attr='2', renumber=False) + """ + if self._Impl is None: + self._Impl = npartiteGraphImpl(self.graph_properties) + # API may change in future + self._Impl._npartiteGraphImpl__from_edgelist(input_df, + source=source, + destination=destination, + edge_attr=edge_attr, + renumber=renumber) + + def from_dask_cudf_edgelist( + self, + input_ddf, + source="source", + destination="destination", + edge_attr=None, + renumber=True, + ): + """ + Initializes the distributed graph from the dask_cudf.DataFrame + edgelist. Undirected Graphs are not currently supported. + By default, renumbering is enabled to map the source and destination + vertices into an index in the range [0, V) where V is the number + of vertices. If the input vertices are a single column of integers + in the range [0, V), renumbering can be disabled and the original + external vertex ids will be used. + Note that the graph object will store a reference to the + dask_cudf.DataFrame provided. + Parameters + ---------- + input_ddf : dask_cudf.DataFrame + The edgelist as a dask_cudf.DataFrame + source : str or array-like + source column name or array of column names + destination : str + destination column name or array of column names + edge_attr : str + weights column name. + renumber : bool + If source and destination indices are not in range 0 to V where V + is number of vertices, renumber argument should be True. + """ + raise Exception("Distributed N-partite graph not supported") + + def add_nodes_from(self, nodes, bipartite=None, multipartite=None): + """ + Add nodes information to the Graph. + Parameters + ---------- + nodes : list or cudf.Series + The nodes of the graph to be stored. If bipartite and multipartite + arguments are not passed, the nodes are considered to be a list of + all the nodes present in the Graph. + bipartite : str + Sets the Graph as bipartite. The nodes are stored as a set of nodes + of the partition named as bipartite argument. + multipartite : str + Sets the Graph as multipartite. The nodes are stored as a set of + nodes of the partition named as multipartite argument. + """ + if self._Impl is None: + self._Impl = npartiteGraphImpl(self.graph_properties) + if bipartite is None and multipartite is None: + self._Impl._nodes["all_nodes"] = cudf.Series(nodes) + else: + self._Impl.add_nodes_from(nodes, bipartite=bipartite, + multipartite=multipartite) + + def is_multipartite(self): + """ + Checks if Graph is multipartite. This solely relies on the user call + of add_nodes_from with the partition parameter and the Graph created. + This does not parse the graph to check if it is multipartite. + """ + return True + + +class BiPartiteGraph(NPartiteGraph): + def __init__(self, directed=False): + super(BiPartiteGraph, self).__init__(directed=directed, bipartite=True) + + def is_bipartite(self): + """ + Checks if Graph is bipartite. This solely relies on the user call of + add_nodes_from with the bipartite parameter and the Graph created. + This does not parse the graph to check if it is bipartite. + """ + return True + + +class BiPartiteDiGraph(BiPartiteGraph): + def __init__(self): + warnings.warn( + "BiPartiteDiGraph is deprecated,\ + use BiPartiteGraph(directed=True) instead", + DeprecationWarning + ) + super(BiPartiteDiGraph, self).__init__(directed=True) + + +class NPartiteDiGraph(NPartiteGraph): + def __init__(self): + warnings.warn( + "NPartiteDiGraph is deprecated,\ + use NPartiteGraph(directed=True) instead", + DeprecationWarning + ) + super(NPartiteGraph, self).__init__(directed=True) + + +def is_directed(G): + """ + Returns True if the graph is a directed graph. + Returns False if the graph is an undirected graph. + """ + return G.is_directed() + + +def is_multigraph(G): + """ + Returns True if the graph is a multigraph. Else returns False. + """ + return G.is_multigraph() + + +def is_multipartite(G): + """ + Checks if Graph is multipartite. This solely relies on the Graph + type. This does not parse the graph to check if it is multipartite. + """ + return G.is_multipatite() + + +def is_bipartite(G): + """ + Checks if Graph is bipartite. This solely relies on the Graph type. + This does not parse the graph to check if it is bipartite. + """ + return G.is_bipartite() + + +def is_weighted(G): + """ + Returns True if the graph has edge weights. + """ + return G.is_weighted() diff --git a/python/cugraph/structure/graph_implementation/__init__.py b/python/cugraph/structure/graph_implementation/__init__.py new file mode 100644 index 00000000000..eeef73c0f64 --- /dev/null +++ b/python/cugraph/structure/graph_implementation/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021, NVIDIA CORPORATION. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .simpleGraph import simpleGraphImpl +from .simpleDistributedGraph import simpleDistributedGraphImpl +from .npartiteGraph import npartiteGraphImpl + diff --git a/python/cugraph/structure/graph_implementation/npartiteGraph.py b/python/cugraph/structure/graph_implementation/npartiteGraph.py new file mode 100644 index 00000000000..111d9f792fa --- /dev/null +++ b/python/cugraph/structure/graph_implementation/npartiteGraph.py @@ -0,0 +1,100 @@ +# Copyright (c) 2021, NVIDIA CORPORATION. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .simpleGraph import simpleGraphImpl +import cudf + + +class npartiteGraphImpl(simpleGraphImpl): + def __init__(self, properties): + super(npartiteGraphImpl, self).__init__(properties) + self.properties.bipartite = properties.bipartite + + # API may change in future + def __from_edgelist( + self, + input_df, + source="source", + destination="destination", + edge_attr=None, + renumber=True, + ): + self._simpleGraphImpl__from_edgelist( + input_df, + source=source, + destination=destination, + edge_attr=edge_attr, + renumber=renumber, + ) + + def sets(self): + """ + Returns the bipartite set of nodes. This solely relies on the user's + call of add_nodes_from with the bipartite parameter. This does not + parse the graph to compute bipartite sets. If bipartite argument was + not provided during add_nodes_from(), it raise an exception that the + graph is not bipartite. + """ + # TO DO: Call coloring algorithm + set_names = [i for i in self._nodes.keys() if i != "all_nodes"] + if self.properties.bipartite: + top = self._nodes[set_names[0]] + if len(set_names) == 2: + bottom = self._nodes[set_names[1]] + else: + bottom = cudf.Series( + set(self.nodes().values_host) - set(top.values_host) + ) + return top, bottom + else: + return {k: self._nodes[k] for k in set_names} + + # API may change in future + def add_nodes_from(self, nodes, bipartite=None, multipartite=None): + """ + Add nodes information to the Graph. + Parameters + ---------- + nodes : list or cudf.Series + The nodes of the graph to be stored. If bipartite and multipartite + arguments are not passed, the nodes are considered to be a list of + all the nodes present in the Graph. + bipartite : str + Sets the Graph as bipartite. The nodes are stored as a set of nodes + of the partition named as bipartite argument. + multipartite : str + Sets the Graph as multipartite. The nodes are stored as a set of + nodes of the partition named as multipartite argument. + """ + if bipartite is None and multipartite is None: + raise Exception("Partition not provided") + else: + set_names = [i for i in self._nodes.keys() if i != "all_nodes"] + if multipartite is not None: + if self.properties.bipartite: + raise Exception( + "The Graph is bipartite. " + "Use bipartite option instead." + ) + elif bipartite is not None: + if not self.properties.bipartite: + raise Exception( + "The Graph is set as npartite. " + "Use multipartite option instead.") + multipartite = bipartite + if multipartite not in set_names and len(set_names) == 2: + raise Exception( + "The Graph is set as bipartite and " + "already has two partitions initialized." + ) + self._nodes[multipartite] = cudf.Series(nodes) diff --git a/python/cugraph/structure/graph_implementation/simpleDistributedGraph.py b/python/cugraph/structure/graph_implementation/simpleDistributedGraph.py new file mode 100644 index 00000000000..e85f3b6ab6c --- /dev/null +++ b/python/cugraph/structure/graph_implementation/simpleDistributedGraph.py @@ -0,0 +1,473 @@ +# Copyright (c) 2021, NVIDIA CORPORATION. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cugraph.structure import graph_primtypes_wrapper +from cugraph.structure.number_map import NumberMap +import cudf +import dask_cudf + + +class simpleDistributedGraphImpl: + class EdgeList: + def __init__(self, ddf): + self.edgelist_df = ddf + self.weights = False + # FIXME: Edge Attribute not handled + + # class AdjList: + # Not Supported + + # class transposedAdjList: + # Not Supported + + class Properties: + def __init__(self, properties): + self.multi_edge = getattr(properties, 'multi_edge', False) + self.directed = properties.directed + self.renumbered = False + self.store_transposed = False + self.self_loop = None + self.isolated_vertices = None + self.node_count = None + self.edge_count = None + self.weighted = False + + def __init__(self, properties): + # Structure + self.edgelist = None + self.renumber_map = None + self.properties = simpleDistributedGraphImpl.Properties(properties) + self.source_columns = None + self.destination_columns = None + + # Functions + def __from_edgelist( + self, + input_ddf, + source="source", + destination="destination", + edge_attr=None, + renumber=True, + store_transposed=False, + ): + if not isinstance(input_ddf, dask_cudf.DataFrame): + raise Exception("input should be a dask_cudf dataFrame") + if self.properties.directed is False: + raise Exception("Undirected distributed graph not supported") + + s_col = source + d_col = destination + if not isinstance(s_col, list): + s_col = [s_col] + if not isinstance(d_col, list): + d_col = [d_col] + if not ( + set(s_col).issubset(set(input_ddf.columns)) + and set(d_col).issubset(set(input_ddf.columns)) + ): + raise Exception( + "source column names and/or destination column " + "names not found in input. Recheck the source " + "and destination parameters" + ) + ddf_columns = s_col + d_col + if edge_attr is not None: + if not (set([edge_attr]).issubset(set(input_ddf.columns))): + raise Exception( + "edge_attr column name not found in input." + "Recheck the edge_attr parameter") + self.weighted = True + ddf_columns = ddf_columns + [edge_attr] + input_ddf = input_ddf[ddf_columns] + + if edge_attr is not None: + input_ddf = input_ddf.rename(columns={edge_attr: 'value'}) + + # + # Keep all of the original parameters so we can lazily + # evaluate this function + # + + # FIXME: Edge Attribute not handled + self.properties.renumbered = renumber + self.input_df = input_ddf + self.source_columns = source + self.destination_columns = destination + + def view_edge_list(self): + """ + Display the edge list. Compute it if needed. + NOTE: If the graph is of type Graph() then the displayed undirected + edges are the same as displayed by networkx Graph(), but the direction + could be different i.e. an edge displayed by cugraph as (src, dst) + could be displayed as (dst, src) by networkx. + cugraph.Graph stores symmetrized edgelist internally. For displaying + undirected edgelist for a Graph the upper trianglar matrix of the + symmetrized edgelist is returned. + networkx.Graph renumbers the input and stores the upper triangle of + this renumbered input. Since the internal renumbering of networx and + cugraph is different, the upper triangular matrix of networkx + renumbered input may not be the same as cugraph's upper trianglar + matrix of the symmetrized edgelist. Hence the displayed source and + destination pairs in both will represent the same edge but node values + could be swapped. + Returns + ------- + df : cudf.DataFrame + This cudf.DataFrame wraps source, destination and weight + df[src] : cudf.Series + contains the source index for each edge + df[dst] : cudf.Series + contains the destination index for each edge + df[weight] : cusd.Series + Column is only present for weighted Graph, + then containing the weight value for each edge + """ + if self.edgelist is None: + raise Exception("Graph has no Edgelist.") + return self.edgelist.edgelist_df + + def delete_edge_list(self): + """ + Delete the edge list. + """ + # decrease reference count to free memory if the referenced objects are + # no longer used. + self.edgelist = None + + def clear(self): + """ + Empty this graph. This function is added for NetworkX compatibility. + """ + self.edgelist = None + + def number_of_vertices(self): + """ + Get the number of nodes in the graph. + """ + if self.properties.node_count is None: + if self.edgelist is not None: + ddf = self.edgelist.edgelist_df[["src", "dst"]] + self.properties.node_count = ddf.max().max().compute() + 1 + else: + raise Exception("Graph is Empty") + return self.properties.node_count + + def number_of_nodes(self): + """ + An alias of number_of_vertices(). This function is added for NetworkX + compatibility. + """ + return self.number_of_vertices() + + def number_of_edges(self, directed_edges=False): + """ + Get the number of edges in the graph. + """ + if self.edgelist is not None: + return len(self.edgelist.edgelist_df) + else: + raise Exception("Graph is Empty") + + def in_degree(self, vertex_subset=None): + """ + Compute vertex in-degree. Vertex in-degree is the number of edges + pointing into the vertex. By default, this method computes vertex + degrees for the entire set of vertices. If vertex_subset is provided, + this method optionally filters out all but those listed in + vertex_subset. + Parameters + ---------- + vertex_subset : cudf.Series or iterable container, optional + A container of vertices for displaying corresponding in-degree. + If not set, degrees are computed for the entire set of vertices. + Returns + ------- + df : cudf.DataFrame + GPU DataFrame of size N (the default) or the size of the given + vertices (vertex_subset) containing the in_degree. The ordering is + relative to the adjacency list, or that given by the specified + vertex_subset. + df[vertex] : cudf.Series + The vertex IDs (will be identical to vertex_subset if + specified). + df[degree] : cudf.Series + The computed in-degree of the corresponding vertex. + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_cudf_edgelist(M, '0', '1') + >>> df = G.in_degree([0,9,12]) + """ + return self._degree(vertex_subset, x=1) + + def out_degree(self, vertex_subset=None): + """ + Compute vertex out-degree. Vertex out-degree is the number of edges + pointing out from the vertex. By default, this method computes vertex + degrees for the entire set of vertices. If vertex_subset is provided, + this method optionally filters out all but those listed in + vertex_subset. + Parameters + ---------- + vertex_subset : cudf.Series or iterable container, optional + A container of vertices for displaying corresponding out-degree. + If not set, degrees are computed for the entire set of vertices. + Returns + ------- + df : cudf.DataFrame + GPU DataFrame of size N (the default) or the size of the given + vertices (vertex_subset) containing the out_degree. The ordering is + relative to the adjacency list, or that given by the specified + vertex_subset. + df[vertex] : cudf.Series + The vertex IDs (will be identical to vertex_subset if + specified). + df[degree] : cudf.Series + The computed out-degree of the corresponding vertex. + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_cudf_edgelist(M, '0', '1') + >>> df = G.out_degree([0,9,12]) + """ + # TODO: Add support + raise Exception("Not supported for distributed graph") + + def degree(self, vertex_subset=None): + """ + Compute vertex degree, which is the total number of edges incident + to a vertex (both in and out edges). By default, this method computes + degrees for the entire set of vertices. If vertex_subset is provided, + then this method optionally filters out all but those listed in + vertex_subset. + Parameters + ---------- + vertex_subset : cudf.Series or iterable container, optional + a container of vertices for displaying corresponding degree. If not + set, degrees are computed for the entire set of vertices. + Returns + ------- + df : cudf.DataFrame + GPU DataFrame of size N (the default) or the size of the given + vertices (vertex_subset) containing the degree. The ordering is + relative to the adjacency list, or that given by the specified + vertex_subset. + df['vertex'] : cudf.Series + The vertex IDs (will be identical to vertex_subset if + specified). + df['degree'] : cudf.Series + The computed degree of the corresponding vertex. + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_cudf_edgelist(M, '0', '1') + >>> all_df = G.degree() + >>> subset_df = G.degree([0,9,12]) + """ + raise Exception("Not supported for distributed graph") + + # FIXME: vertex_subset could be a DataFrame for multi-column vertices + def degrees(self, vertex_subset=None): + """ + Compute vertex in-degree and out-degree. By default, this method + computes vertex degrees for the entire set of vertices. If + vertex_subset is provided, this method optionally filters out all but + those listed in vertex_subset. + Parameters + ---------- + vertex_subset : cudf.Series or iterable container, optional + A container of vertices for displaying corresponding degree. If not + set, degrees are computed for the entire set of vertices. + Returns + ------- + df : cudf.DataFrame + GPU DataFrame of size N (the default) or the size of the given + vertices (vertex_subset) containing the degrees. The ordering is + relative to the adjacency list, or that given by the specified + vertex_subset. + df['vertex'] : cudf.Series + The vertex IDs (will be identical to vertex_subset if + specified). + df['in_degree'] : cudf.Series + The in-degree of the vertex. + df['out_degree'] : cudf.Series + The out-degree of the vertex. + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_cudf_edgelist(M, '0', '1') + >>> df = G.degrees([0,9,12]) + """ + raise Exception("Not supported for distributed graph") + + def _degree(self, vertex_subset, x=0): + vertex_col, degree_col = graph_primtypes_wrapper._degree(self, x) + df = cudf.DataFrame() + df["vertex"] = vertex_col + df["degree"] = degree_col + + if self.renumbered is True: + df = self.unrenumber(df, "vertex") + + if vertex_subset is not None: + df = df[df['vertex'].isin(vertex_subset)] + + return df + + def to_directed(self, DiG): + """ + Return a directed representation of the graph. + This function sets the type of graph as DiGraph() and returns the + directed view. + Returns + ------- + G : DiGraph + A directed graph with the same nodes, and each edge (u,v,weights) + replaced by two directed edges (u,v,weights) and (v,u,weights). + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_cudf_edgelist(M, '0', '1') + >>> DiG = G.to_directed() + """ + # TODO: Add support + raise Exception("Not supported for distributed graph") + + def to_undirected(self, G): + """ + Return an undirected copy of the graph. + Returns + ------- + G : Graph + A undirected graph with the same nodes, and each directed edge + (u,v,weights) replaced by an undirected edge (u,v,weights). + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> DiG = cugraph.DiGraph() + >>> DiG.from_cudf_edgelist(M, '0', '1') + >>> G = DiG.to_undirected() + """ + + # TODO: Add support + raise Exception("Not supported for distributed graph") + + def has_node(self, n): + """ + Returns True if the graph contains the node n. + """ + if self.edgelist is None: + raise Exception("Graph has no Edgelist.") + # FIXME: Check renumber map + ddf = self.edgelist.edgelist_df[["src", "dst"]] + return (ddf == n).any().any().compute() + + def has_edge(self, u, v): + """ + Returns True if the graph contains the edge (u,v). + """ + # TODO: Verify Correctness + if self.properties.renumbered: + tmp = cudf.DataFrame({"src": [u, v]}) + tmp = tmp.astype({"src": "int"}) + tmp = self.add_internal_vertex_id( + tmp, "id", "src", preserve_order=True + ) + + u = tmp["id"][0] + v = tmp["id"][1] + + df = self.edgelist.edgelist_df + return ((df["src"] == u) & (df["dst"] == v)).any().compute() + + def edges(self): + """ + Returns all the edges in the graph as a cudf.DataFrame containing + sources and destinations. It does not return the edge weights. + For viewing edges with weights use view_edge_list() + """ + return self.view_edge_list()[["src", "dst"]] + + def nodes(self): + """ + Returns all the nodes in the graph as a cudf.Series + """ + # FIXME: Return renumber map nodes + raise Exception("Not supported for distributed graph") + + def neighbors(self, n): + if self.edgelist is None: + raise Exception("Graph has no Edgelist.") + # FIXME: Add renumbering of node n + ddf = self.edgelist.edgelist_df + return ddf[ddf["src"] == n]["dst"].reset_index(drop=True) + + def compute_renumber_edge_list(self, transposed=False): + """ + Compute a renumbered edge list + This function works in the MNMG pipeline and will transform + the input dask_cudf.DataFrame into a renumbered edge list + in the prescribed direction. + This function will be called by the algorithms to ensure + that the graph is renumbered properly. The graph object will + cache the most recent renumbering attempt. For benchmarking + purposes, this function can be called prior to calling a + graph algorithm so we can measure the cost of computing + the renumbering separately from the cost of executing the + algorithm. + When creating a CSR-like structure, set transposed to False. + When creating a CSC-like structure, set transposed to True. + Parameters + ---------- + transposed : (optional) bool + If True, renumber with the intent to make a CSC-like + structure. If False, renumber with the intent to make + a CSR-like structure. Defaults to False. + """ + # FIXME: What to do about edge_attr??? + # currently ignored for MNMG + + if not self.properties.renumbered: + self.edgelist = self.EdgeList(self.input_df) + self.renumber_map = None + else: + if self.edgelist is not None: + if self.properties.directed is False: + return + + if self.properties.store_transposed == transposed: + return + + del self.edgelist + + renumbered_ddf, number_map = NumberMap.renumber( + self.input_df, + self.source_columns, + self.destination_columns, + store_transposed=transposed, + ) + self.edgelist = self.EdgeList(renumbered_ddf) + self.renumber_map = number_map + self.properties.store_transposed = transposed diff --git a/python/cugraph/structure/graph_implementation/simpleGraph.py b/python/cugraph/structure/graph_implementation/simpleGraph.py new file mode 100644 index 00000000000..4e632a72231 --- /dev/null +++ b/python/cugraph/structure/graph_implementation/simpleGraph.py @@ -0,0 +1,823 @@ +# Copyright (c) 2021, NVIDIA CORPORATION. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cugraph.structure import graph_primtypes_wrapper +from cugraph.structure.symmetrize import symmetrize +from cugraph.structure.number_map import NumberMap +import cugraph.dask.common.mg_utils as mg_utils +import cudf +import dask_cudf +import cugraph.comms.comms as Comms +import pandas as pd +import numpy as np +from cugraph.dask.structure import replication + + +# FIXME: Change to consistent camel case naming +class simpleGraphImpl: + + class EdgeList: + def __init__(self, source, destination, edge_attr=None): + self.edgelist_df = cudf.DataFrame() + self.edgelist_df["src"] = source + self.edgelist_df["dst"] = destination + self.weights = False + if edge_attr is not None: + self.weights = True + if type(edge_attr) is dict: + for k in edge_attr.keys(): + self.edgelist_df[k] = edge_attr[k] + else: + self.edgelist_df["weights"] = edge_attr + + class AdjList: + def __init__(self, offsets, indices, value=None): + self.offsets = offsets + self.indices = indices + self.weights = value # Should be a dataframe for multiple weights + + class transposedAdjList: + def __init__(self, offsets, indices, value=None): + simpleGraphImpl.AdjList.__init__(self, offsets, indices, value) + + class Properties: + def __init__(self, properties): + self.multi_edge = getattr(properties, 'multi_edge', False) + self.directed = properties.directed + self.renumbered = False + self.self_loop = None + self.isolated_vertices = None + self.node_count = None + self.edge_count = None + self.weighted = False + + def __init__(self, properties): + # Structure + self.edgelist = None + self.adjlist = None + self.transposedadjlist = None + self.renumber_map = None + self.properties = simpleGraphImpl.Properties(properties) + self._nodes = {} + + # TODO: Move to new batch class + # MG - Batch + self.batch_enabled = False + self.batch_edgelists = None + self.batch_adjlists = None + self.batch_transposed_adjlists = None + + # Functions + # FIXME: Change to public function + # FIXME: Make function more modular + def __from_edgelist( + self, + input_df, + source="source", + destination="destination", + edge_attr=None, + renumber=True, + ): + + # Verify column names present in input DataFrame + s_col = source + d_col = destination + if not isinstance(s_col, list): + s_col = [s_col] + if not isinstance(d_col, list): + d_col = [d_col] + if not ( + set(s_col).issubset(set(input_df.columns)) + and set(d_col).issubset(set(input_df.columns)) + ): + # FIXME: Raise concrete Exceptions + raise Exception( + "source column names and/or destination column " + "names not found in input. Recheck the source and " + "destination parameters" + ) + + # FIXME: check if the consolidated graph fits on the + # device before gathering all the edge lists + + # Consolidation + if isinstance(input_df, cudf.DataFrame): + if len(input_df[source]) > 2147483100: + raise Exception( + "cudf dataFrame edge list is too big " + "to fit in a single GPU" + ) + elist = input_df + elif isinstance(input_df, dask_cudf.DataFrame): + if len(input_df[source]) > 2147483100: + raise Exception( + "dask_cudf dataFrame edge list is too big " + "to fit in a single GPU" + ) + elist = input_df.compute().reset_index(drop=True) + else: + raise Exception( + "input should be a cudf.DataFrame or " + "a dask_cudf dataFrame" + ) + + # Renumbering + self.renumber_map = None + if renumber: + # FIXME: Should SG do lazy evaluation like MG? + elist, renumber_map = NumberMap.renumber( + elist, source, destination, store_transposed=False + ) + source = "src" + destination = "dst" + self.properties.renumbered = True + self.renumber_map = renumber_map + else: + if type(source) is list and type(destination) is list: + raise Exception("set renumber to True for multi column ids") + + # Populate graph edgelist + source_col = elist[source] + dest_col = elist[destination] + + if edge_attr is not None: + self.weighted = True + value_col = elist[edge_attr] + else: + value_col = None + + # TODO: Update Symmetrize to work on Graph and/or DataFrame + if value_col is not None: + source_col, dest_col, value_col = symmetrize( + source_col, dest_col, value_col, + multi=self.properties.multi_edge, + symmetrize=not self.properties.directed) + if isinstance(value_col, cudf.DataFrame): + value_dict = {} + for i in value_col.columns: + value_dict[i] = value_col[i] + value_col = value_dict + else: + source_col, dest_col = symmetrize( + source_col, dest_col, multi=self.properties.multi_edge, + symmetrize=not self.properties.directed) + + self.edgelist = simpleGraphImpl.EdgeList(source_col, dest_col, + value_col) + + if self.batch_enabled: + self._replicate_edgelist() + + def to_pandas_edgelist(self, source='source', destination='destination'): + """ + Returns the graph edge list as a Pandas DataFrame. + Parameters + ---------- + source : str or array-like + source column name or array of column names + destination : str or array-like + destination column name or array of column names + Returns + ------- + df : pandas.DataFrame + """ + + gdf = self.view_edge_list() + return gdf.to_pandas() + + def to_pandas_adjacency(self): + """ + Returns the graph adjacency matrix as a Pandas DataFrame. + """ + + np_array_data = self.to_numpy_array() + pdf = pd.DataFrame(np_array_data) + if self.properties.renumbered: + nodes = self.renumber_map.implementation.df['0'].\ + values_host.tolist() + pdf.columns = nodes + pdf.index = nodes + return pdf + + def to_numpy_array(self): + """ + Returns the graph adjacency matrix as a NumPy array. + """ + + nlen = self.number_of_nodes() + elen = self.number_of_edges() + df = self.edgelist.edgelist_df + np_array = np.full((nlen, nlen), 0.0) + for i in range(0, elen): + np_array[df['src'].iloc[i], df['dst'].iloc[i]] = df['weights'].\ + iloc[i] + return np_array + + def to_numpy_matrix(self): + """ + Returns the graph adjacency matrix as a NumPy matrix. + """ + np_array = self.to_numpy_array() + return np.asmatrix(np_array) + + def view_edge_list(self): + """ + Display the edge list. Compute it if needed. + NOTE: If the graph is of type Graph() then the displayed undirected + edges are the same as displayed by networkx Graph(), but the direction + could be different i.e. an edge displayed by cugraph as (src, dst) + could be displayed as (dst, src) by networkx. + cugraph.Graph stores symmetrized edgelist internally. For displaying + undirected edgelist for a Graph the upper trianglar matrix of the + symmetrized edgelist is returned. + networkx.Graph renumbers the input and stores the upper triangle of + this renumbered input. Since the internal renumbering of networx and + cugraph is different, the upper triangular matrix of networkx + renumbered input may not be the same as cugraph's upper trianglar + matrix of the symmetrized edgelist. Hence the displayed source and + destination pairs in both will represent the same edge but node values + could be swapped. + Returns + ------- + df : cudf.DataFrame + This cudf.DataFrame wraps source, destination and weight + df[src] : cudf.Series + contains the source index for each edge + df[dst] : cudf.Series + contains the destination index for each edge + df[weight] : cusd.Series + Column is only present for weighted Graph, + then containing the weight value for each edge + """ + if self.edgelist is None: + src, dst, weights = graph_primtypes_wrapper.view_edge_list(self) + self.edgelist = self.EdgeList(src, dst, weights) + + edgelist_df = self.edgelist.edgelist_df + + if self.properties.renumbered: + edgelist_df = self.renumber_map.unrenumber(edgelist_df, "src") + edgelist_df = self.renumber_map.unrenumber(edgelist_df, "dst") + + if not self.properties.directed: + edgelist_df = edgelist_df[edgelist_df["src"] <= edgelist_df["dst"]] + edgelist_df = edgelist_df.reset_index(drop=True) + self.properties.edge_count = len(edgelist_df) + + return edgelist_df + + def delete_edge_list(self): + """ + Delete the edge list. + """ + # decrease reference count to free memory if the referenced objects are + # no longer used. + self.edgelist = None + + def __from_adjlist(self, offset_col, index_col, value_col=None): + self.adjlist = simpleGraphImpl.AdjList(offset_col, index_col, + value_col) + + if self.batch_enabled: + self._replicate_adjlist() + + def view_adj_list(self): + """ + Display the adjacency list. Compute it if needed. + Returns + ------- + offset_col : cudf.Series + This cudf.Series wraps a gdf_column of size V + 1 (V: number of + vertices). + The gdf column contains the offsets for the vertices in this graph. + Offsets are in the range [0, E] (E: number of edges). + index_col : cudf.Series + This cudf.Series wraps a gdf_column of size E (E: number of edges). + The gdf column contains the destination index for each edge. + Destination indices are in the range [0, V) (V: number of + vertices). + value_col : cudf.Series or ``None`` + This pointer is ``None`` for unweighted graphs. + For weighted graphs, this cudf.Series wraps a gdf_column of size E + (E: number of edges). + The gdf column contains the weight value for each edge. + The expected type of the gdf_column element is floating point + number. + """ + + if self.adjlist is None: + if self.transposedadjlist is not None and\ + self.properties.directed is False: + off, ind, vals = ( + self.transposedadjlist.offsets, + self.transposedadjlist.indices, + self.transposedadjlist.weights, + ) + else: + off, ind, vals = graph_primtypes_wrapper.view_adj_list(self) + self.adjlist = self.AdjList(off, ind, vals) + + if self.batch_enabled: + self._replicate_adjlist() + + return self.adjlist.offsets, self.adjlist.indices, self.adjlist.weights + + def view_transposed_adj_list(self): + """ + Display the transposed adjacency list. Compute it if needed. + Returns + ------- + offset_col : cudf.Series + This cudf.Series wraps a gdf_column of size V + 1 (V: number of + vertices). + The gdf column contains the offsets for the vertices in this graph. + Offsets are in the range [0, E] (E: number of edges). + index_col : cudf.Series + This cudf.Series wraps a gdf_column of size E (E: number of edges). + The gdf column contains the destination index for each edge. + Destination indices are in the range [0, V) (V: number of + vertices). + value_col : cudf.Series or ``None`` + This pointer is ``None`` for unweighted graphs. + For weighted graphs, this cudf.Series wraps a gdf_column of size E + (E: number of edges). + The gdf column contains the weight value for each edge. + The expected type of the gdf_column element is floating point + number. + """ + + if self.transposedadjlist is None: + if self.adjlist is not None and self.properties.directed is False: + off, ind, vals = ( + self.adjlist.offsets, + self.adjlist.indices, + self.adjlist.weights, + ) + else: + ( + off, + ind, + vals, + ) = graph_primtypes_wrapper.view_transposed_adj_list(self) + self.transposedadjlist = self.transposedAdjList(off, ind, vals) + + if self.batch_enabled: + self._replicate_transposed_adjlist() + + return ( + self.transposedadjlist.offsets, + self.transposedadjlist.indices, + self.transposedadjlist.weights, + ) + + def delete_adj_list(self): + """ + Delete the adjacency list. + """ + self.adjlist = None + + # FIXME: Update batch workflow and refactor to suitable file + def enable_batch(self): + client = mg_utils.get_client() + comms = Comms.get_comms() + + if client is None or comms is None: + msg = ( + "MG Batch needs a Dask Client and the " + "Communicator needs to be initialized." + ) + raise Exception(msg) + + self.batch_enabled = True + + if self.edgelist is not None: + if self.batch_edgelists is None: + self._replicate_edgelist() + + if self.adjlist is not None: + if self.batch_adjlists is None: + self._replicate_adjlist() + + if self.transposedadjlist is not None: + if self.batch_transposed_adjlists is None: + self._replicate_transposed_adjlist() + + def _replicate_edgelist(self): + client = mg_utils.get_client() + comms = Comms.get_comms() + + # FIXME: There might be a better way to control it + if client is None: + return + work_futures = replication.replicate_cudf_dataframe( + self.edgelist.edgelist_df, client=client, comms=comms + ) + + self.batch_edgelists = work_futures + + def _replicate_adjlist(self): + client = mg_utils.get_client() + comms = Comms.get_comms() + + # FIXME: There might be a better way to control it + if client is None: + return + + weights = None + offsets_futures = replication.replicate_cudf_series( + self.adjlist.offsets, client=client, comms=comms + ) + indices_futures = replication.replicate_cudf_series( + self.adjlist.indices, client=client, comms=comms + ) + + if self.adjlist.weights is not None: + weights = replication.replicate_cudf_series(self.adjlist.weights) + else: + weights = {worker: None for worker in offsets_futures} + + merged_futures = { + worker: [ + offsets_futures[worker], + indices_futures[worker], + weights[worker], + ] + for worker in offsets_futures + } + self.batch_adjlists = merged_futures + + # FIXME: Not implemented yet + def _replicate_transposed_adjlist(self): + self.batch_transposed_adjlists = True + + def get_two_hop_neighbors(self): + """ + Compute vertex pairs that are two hops apart. The resulting pairs are + sorted before returning. + Returns + ------- + df : cudf.DataFrame + df[first] : cudf.Series + the first vertex id of a pair, if an external vertex id + is defined by only one column + df[second] : cudf.Series + the second vertex id of a pair, if an external vertex id + is defined by only one column + """ + + df = graph_primtypes_wrapper.get_two_hop_neighbors(self) + + if self.properties.renumbered is True: + df = self.renumber_map.unrenumber(df, "first") + df = self.renumber_map.unrenumber(df, "second") + + return df + + def number_of_vertices(self): + """ + Get the number of nodes in the graph. + """ + if self.properties.node_count is None: + if self.adjlist is not None: + self.properties.node_count = len(self.adjlist.offsets) - 1 + elif self.transposedadjlist is not None: + self.properties.node_count = len( + self.transposedadjlist.offsets) - 1 + elif self.edgelist is not None: + df = self.edgelist.edgelist_df[["src", "dst"]] + self.properties.node_count = df.max().max() + 1 + else: + raise Exception("Graph is Empty") + return self.properties.node_count + + def number_of_nodes(self): + """ + An alias of number_of_vertices(). This function is added for NetworkX + compatibility. + """ + return self.number_of_vertices() + + def number_of_edges(self, directed_edges=False): + """ + Get the number of edges in the graph. + """ + # TODO: Move to Outer graphs? + if directed_edges and self.edgelist is not None: + return len(self.edgelist.edgelist_df) + if self.properties.edge_count is None: + if self.edgelist is not None: + if self.properties.directed is False: + self.properties.edge_count = len( + self.edgelist.edgelist_df[ + self.edgelist.edgelist_df["src"] + >= self.edgelist.edgelist_df["dst"] + ] + ) + else: + self.properties.edge_count = len(self.edgelist.edgelist_df) + elif self.adjlist is not None: + self.properties.edge_count = len(self.adjlist.indices) + elif self.transposedadjlist is not None: + self.properties.edge_count = len( + self.transposedadjlist.indices) + else: + raise ValueError("Graph is Empty") + return self.properties.edge_count + + def in_degree(self, vertex_subset=None): + """ + Compute vertex in-degree. Vertex in-degree is the number of edges + pointing into the vertex. By default, this method computes vertex + degrees for the entire set of vertices. If vertex_subset is provided, + this method optionally filters out all but those listed in + vertex_subset. + Parameters + ---------- + vertex_subset : cudf.Series or iterable container, optional + A container of vertices for displaying corresponding in-degree. + If not set, degrees are computed for the entire set of vertices. + Returns + ------- + df : cudf.DataFrame + GPU DataFrame of size N (the default) or the size of the given + vertices (vertex_subset) containing the in_degree. The ordering is + relative to the adjacency list, or that given by the specified + vertex_subset. + df[vertex] : cudf.Series + The vertex IDs (will be identical to vertex_subset if + specified). + df[degree] : cudf.Series + The computed in-degree of the corresponding vertex. + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_cudf_edgelist(M, '0', '1') + >>> df = G.in_degree([0,9,12]) + """ + return self._degree(vertex_subset, x=1) + + def out_degree(self, vertex_subset=None): + """ + Compute vertex out-degree. Vertex out-degree is the number of edges + pointing out from the vertex. By default, this method computes vertex + degrees for the entire set of vertices. If vertex_subset is provided, + this method optionally filters out all but those listed in + vertex_subset. + Parameters + ---------- + vertex_subset : cudf.Series or iterable container, optional + A container of vertices for displaying corresponding out-degree. + If not set, degrees are computed for the entire set of vertices. + Returns + ------- + df : cudf.DataFrame + GPU DataFrame of size N (the default) or the size of the given + vertices (vertex_subset) containing the out_degree. The ordering is + relative to the adjacency list, or that given by the specified + vertex_subset. + df[vertex] : cudf.Series + The vertex IDs (will be identical to vertex_subset if + specified). + df[degree] : cudf.Series + The computed out-degree of the corresponding vertex. + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_cudf_edgelist(M, '0', '1') + >>> df = G.out_degree([0,9,12]) + """ + return self._degree(vertex_subset, x=2) + + def degree(self, vertex_subset=None): + """ + Compute vertex degree, which is the total number of edges incident + to a vertex (both in and out edges). By default, this method computes + degrees for the entire set of vertices. If vertex_subset is provided, + then this method optionally filters out all but those listed in + vertex_subset. + Parameters + ---------- + vertex_subset : cudf.Series or iterable container, optional + a container of vertices for displaying corresponding degree. If not + set, degrees are computed for the entire set of vertices. + Returns + ------- + df : cudf.DataFrame + GPU DataFrame of size N (the default) or the size of the given + vertices (vertex_subset) containing the degree. The ordering is + relative to the adjacency list, or that given by the specified + vertex_subset. + df['vertex'] : cudf.Series + The vertex IDs (will be identical to vertex_subset if + specified). + df['degree'] : cudf.Series + The computed degree of the corresponding vertex. + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_cudf_edgelist(M, '0', '1') + >>> all_df = G.degree() + >>> subset_df = G.degree([0,9,12]) + """ + return self._degree(vertex_subset) + + # FIXME: vertex_subset could be a DataFrame for multi-column vertices + def degrees(self, vertex_subset=None): + """ + Compute vertex in-degree and out-degree. By default, this method + computes vertex degrees for the entire set of vertices. If + vertex_subset is provided, this method optionally filters out all but + those listed in vertex_subset. + Parameters + ---------- + vertex_subset : cudf.Series or iterable container, optional + A container of vertices for displaying corresponding degree. If not + set, degrees are computed for the entire set of vertices. + Returns + ------- + df : cudf.DataFrame + GPU DataFrame of size N (the default) or the size of the given + vertices (vertex_subset) containing the degrees. The ordering is + relative to the adjacency list, or that given by the specified + vertex_subset. + df['vertex'] : cudf.Series + The vertex IDs (will be identical to vertex_subset if + specified). + df['in_degree'] : cudf.Series + The in-degree of the vertex. + df['out_degree'] : cudf.Series + The out-degree of the vertex. + Examples + -------- + >>> M = cudf.read_csv('datasets/karate.csv', delimiter=' ', + >>> dtype=['int32', 'int32', 'float32'], header=None) + >>> G = cugraph.Graph() + >>> G.from_cudf_edgelist(M, '0', '1') + >>> df = G.degrees([0,9,12]) + """ + ( + vertex_col, + in_degree_col, + out_degree_col, + ) = graph_primtypes_wrapper._degrees(self) + + df = cudf.DataFrame() + df["vertex"] = vertex_col + df["in_degree"] = in_degree_col + df["out_degree"] = out_degree_col + + if self.properties.renumbered is True: + df = self.renumber_map.unrenumber(df, "vertex") + + if vertex_subset is not None: + df = df[df['vertex'].isin(vertex_subset)] + + return df + + def _degree(self, vertex_subset, x=0): + vertex_col, degree_col = graph_primtypes_wrapper._degree(self, x) + df = cudf.DataFrame() + df["vertex"] = vertex_col + df["degree"] = degree_col + + if self.properties.renumbered is True: + df = self.renumber_map.unrenumber(df, "vertex") + + if vertex_subset is not None: + df = df[df['vertex'].isin(vertex_subset)] + + return df + + def to_directed(self, DiG): + """ + Return a directed representation of the graph Implementation. + This function copies the internal structures and returns the + directed view. + """ + DiG.properties.renumbered = self.properties.renumbered + DiG.renumber_map = self.renumber_map + DiG.edgelist = self.edgelist + DiG.adjlist = self.adjlist + DiG.transposedadjlist = self.transposedadjlist + + def to_undirected(self, G): + """ + Return an undirected copy of the graph. + """ + G.properties.renumbered = self.properties.renumbered + G.renumber_map = self.renumber_map + if self.properties.directed is False: + G.edgelist = self.edgelist + G.adjlist = self.adjlist + G.transposedadjlist = self.transposedadjlist + else: + df = self.edgelist.edgelist_df + if self.edgelist.weights: + source_col, dest_col, value_col = symmetrize( + df["src"], df["dst"], df["weights"] + ) + else: + source_col, dest_col = symmetrize(df["src"], df["dst"]) + value_col = None + G.edgelist = simpleGraphImpl.EdgeList(source_col, dest_col, + value_col) + + def has_node(self, n): + """ + Returns True if the graph contains the node n. + """ + if self.properties.renumbered: + tmp = self.renumber_map.to_internal_vertex_id(cudf.Series([n])) + return tmp[0] is not cudf.NA and tmp[0] >= 0 + else: + df = self.edgelist.edgelist_df[["src", "dst"]] + return (df == n).any().any() + + def has_edge(self, u, v): + """ + Returns True if the graph contains the edge (u,v). + """ + if self.properties.renumbered: + tmp = cudf.DataFrame({"src": [u, v]}) + tmp = tmp.astype({"src": "int"}) + tmp = self.renumber_map.add_internal_vertex_id( + tmp, "id", "src", preserve_order=True + ) + + u = tmp["id"][0] + v = tmp["id"][1] + + df = self.edgelist.edgelist_df + return ((df["src"] == u) & (df["dst"] == v)).any() + + def has_self_loop(self): + """ + Returns True if the graph has self loop. + """ + # Detect self loop + if self.properties.self_loop is None: + elist = self.edgelist.edgelist_df + if (elist["src"] == elist["dst"]).any(): + self.properties.self_loop = True + else: + self.properties.self_loop = False + return self.properties.self_loop + + def edges(self): + """ + Returns all the edges in the graph as a cudf.DataFrame containing + sources and destinations. It does not return the edge weights. + For viewing edges with weights use view_edge_list() + """ + return self.view_edge_list()[["src", "dst"]] + + def nodes(self): + """ + Returns all the nodes in the graph as a cudf.Series + """ + if self.edgelist is not None: + df = self.edgelist.edgelist_df + if self.properties.renumbered: + # FIXME: If vertices are multicolumn + # this needs to return a dataframe + # FIXME: This relies on current implementation + # of NumberMap, should not really expose + # this, perhaps add a method to NumberMap + return self.renumber_map.implementation.df["0"] + else: + return cudf.concat([df["src"], df["dst"]]).unique() + if self.adjlist is not None: + return cudf.Series(np.arange(0, self.number_of_nodes())) + + def neighbors(self, n): + if self.edgelist is None: + raise Exception("Graph has no Edgelist.") + if self.properties.renumbered: + node = self.renumber_map.to_internal_vertex_id(cudf.Series([n])) + if len(node) == 0: + return cudf.Series(dtype="int") + n = node[0] + + df = self.edgelist.edgelist_df + neighbors = df[df["src"] == n]["dst"].reset_index(drop=True) + if self.properties.renumbered: + # FIXME: Multi-column vertices + return self.renumber_map.from_internal_vertex_id(neighbors)["0"] + else: + return neighbors diff --git a/python/cugraph/structure/hypergraph.py b/python/cugraph/structure/hypergraph.py index a11c937d83d..c5a1ac39e4f 100644 --- a/python/cugraph/structure/hypergraph.py +++ b/python/cugraph/structure/hypergraph.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. +# Copyright (c) 2020-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -36,7 +36,7 @@ import cudf import numpy as np -from cugraph.structure.graph import Graph +from cugraph.structure.graph_classes import Graph def hypergraph( @@ -66,24 +66,20 @@ def hypergraph( components as dataframes. The transform reveals relationships between the rows and unique values. This transform is useful for lists of events, samples, relationships, and other structured high-dimensional data. - The transform creates a node for every row, and turns a row's column entries into node attributes. If direct=False (default), every unique value within a column is also turned into a node. Edges are added to connect a row's nodes to each of its column nodes, or if direct=True, to one another. Nodes are given the attribute specified by ``NODETYPE`` that corresponds to the originating column name, or if a row ``EVENTID``. - Consider a list of events. Each row represents a distinct event, and each column some metadata about an event. If multiple events have common metadata, they will be transitively connected through those metadata values. Conversely, if an event has unique metadata, the unique metadata will turn into nodes that only have connections to the event node. - For best results, set ``EVENTID`` to a row's unique ID, ``SKIP`` to all non-categorical columns (or ``columns`` to all categorical columns), and ``categories`` to group columns with the same kinds of values. - Parameters ---------- values : cudf.DataFrame @@ -130,7 +126,6 @@ def hypergraph( The name to use as the node type column in the graph and node DFs. EDGETYPE : str, optional, default "edge_type" The name to use as the edge type column in the graph and edge DF. - Returns ------- result : dict {"nodes", "edges", "graph", "events", "entities"} diff --git a/python/cugraph/structure/number_map.py b/python/cugraph/structure/number_map.py index cd24dfc0434..2b7c2b2f296 100644 --- a/python/cugraph/structure/number_map.py +++ b/python/cugraph/structure/number_map.py @@ -173,12 +173,16 @@ def __init__( self.numbered = False def to_internal_vertex_id(self, ddf, col_names): - return self.ddf.merge( - ddf, - right_on=col_names, - left_on=self.col_names, + tmp_ddf = ddf[col_names].rename( + columns=dict(zip(col_names, self.col_names))) + for name in self.col_names: + tmp_ddf[name] = tmp_ddf[name].astype(self.ddf[name].dtype) + x = self.ddf.merge( + tmp_ddf, + on=self.col_names, how="right", - )["global_id"] + ) + return x['global_id'] def from_internal_vertex_id( self, df, internal_column_name, external_column_names @@ -342,11 +346,7 @@ def to_internal_vertex_id(self, df, col_names=None): reply = self.implementation.to_internal_vertex_id(tmp_df, tmp_col_names) - - if type(df) in [cudf.DataFrame, dask_cudf.DataFrame]: - return reply["0"] - else: - return reply + return reply def add_internal_vertex_id( self, df, id_column_name="id", col_names=None, drop=False, diff --git a/python/cugraph/structure/symmetrize.py b/python/cugraph/structure/symmetrize.py index 8720f7ad343..442701f6508 100644 --- a/python/cugraph/structure/symmetrize.py +++ b/python/cugraph/structure/symmetrize.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cugraph.structure import graph as csg +from cugraph.structure import graph_classes as csg import cudf import dask_cudf @@ -201,8 +201,12 @@ def symmetrize(source_col, dest_col, value_col=None, multi=False, csg.null_check(source_col) csg.null_check(dest_col) if value_col is not None: - weight_name = "value" - input_df.insert(len(input_df.columns), "value", value_col) + if isinstance(value_col, cudf.Series): + weight_name = "value" + input_df.insert(len(input_df.columns), "value", value_col) + elif isinstance(value_col, cudf.DataFrame): + input_df = cudf.concat([input_df, value_col], axis=1) + output_df = None if type(source_col) is dask_cudf.Series: output_df = symmetrize_ddf( @@ -211,11 +215,17 @@ def symmetrize(source_col, dest_col, value_col=None, multi=False, else: output_df = symmetrize_df(input_df, "source", "destination", multi, symmetrize) - if value_col is not None: - return ( - output_df["source"], - output_df["destination"], - output_df["value"], - ) + if isinstance(value_col, cudf.Series): + return ( + output_df["source"], + output_df["destination"], + output_df["value"], + ) + elif isinstance(value_col, cudf.DataFrame): + return ( + output_df["source"], + output_df["destination"], + output_df[value_col.columns], + ) return output_df["source"], output_df["destination"] diff --git a/python/cugraph/tests/dask/test_mg_bfs.py b/python/cugraph/tests/dask/test_mg_bfs.py index 36d1f436b52..3e83491c87a 100644 --- a/python/cugraph/tests/dask/test_mg_bfs.py +++ b/python/cugraph/tests/dask/test_mg_bfs.py @@ -63,9 +63,8 @@ def test_dask_bfs(client_connection): dg.from_dask_cudf_edgelist(ddf, "src", "dst") expected_dist = cugraph.bfs(g, 0) - result_dist = dcg.bfs(dg, 0, True) + result_dist = dcg.bfs(dg, 0, depth_limit=2) result_dist = result_dist.compute() - compare_dist = expected_dist.merge( result_dist, on="vertex", suffixes=["_local", "_dask"] ) @@ -79,3 +78,65 @@ def test_dask_bfs(client_connection): ): err = err + 1 assert err == 0 + + +@pytest.mark.skipif( + is_single_gpu(), reason="skipping MG testing on Single GPU system" +) +def test_dask_bfs_multi_column_depthlimit(client_connection): + gc.collect() + + # FIXME: update this to allow dataset to be parameterized and have dataset + # part of test param id (see other tests) + input_data_path = r"../datasets/netscience.csv" + print(f"dataset={input_data_path}") + chunksize = dcg.get_chunksize(input_data_path) + + ddf = dask_cudf.read_csv( + input_data_path, + chunksize=chunksize, + delimiter=" ", + names=["src_a", "dst_a", "value"], + dtype=["int32", "int32", "float32"], + ) + ddf['src_b'] = ddf['src_a'] + 1000 + ddf['dst_b'] = ddf['dst_a'] + 1000 + + df = cudf.read_csv( + input_data_path, + delimiter=" ", + names=["src_a", "dst_a", "value"], + dtype=["int32", "int32", "float32"], + ) + df['src_b'] = df['src_a'] + 1000 + df['dst_b'] = df['dst_a'] + 1000 + + g = cugraph.DiGraph() + g.from_cudf_edgelist(df, ["src_a", "src_b"], ["dst_a", "dst_b"]) + + dg = cugraph.DiGraph() + dg.from_dask_cudf_edgelist(ddf, ["src_a", "src_b"], ["dst_a", "dst_b"]) + + start = cudf.DataFrame() + start['a'] = [0] + start['b'] = [1000] + + depth_limit = 18 + expected_dist = cugraph.bfs(g, start, depth_limit=depth_limit) + result_dist = dcg.bfs(dg, start, depth_limit=depth_limit) + result_dist = result_dist.compute() + + compare_dist = expected_dist.merge( + result_dist, on=["0_vertex", "1_vertex"], suffixes=["_local", "_dask"] + ) + + err = 0 + for i in range(len(compare_dist)): + if ( + compare_dist["distance_local"].iloc[i] <= depth_limit and + compare_dist["distance_dask"].iloc[i] <= depth_limit and + compare_dist["distance_local"].iloc[i] + != compare_dist["distance_dask"].iloc[i] + ): + err = err + 1 + assert err == 0 diff --git a/python/cugraph/tests/test_betweenness_centrality.py b/python/cugraph/tests/test_betweenness_centrality.py index 29c012e95a2..ee1a269e532 100755 --- a/python/cugraph/tests/test_betweenness_centrality.py +++ b/python/cugraph/tests/test_betweenness_centrality.py @@ -334,6 +334,7 @@ def test_betweenness_centrality( @pytest.mark.parametrize("result_dtype", RESULT_DTYPE_OPTIONS) @pytest.mark.parametrize("use_k_full", [True]) @pytest.mark.parametrize("edgevals", WEIGHTED_GRAPH_OPTIONS) +@pytest.mark.skip(reason="Skipping large tests") def test_betweenness_centrality_k_full( graph_file, directed, @@ -377,6 +378,7 @@ def test_betweenness_centrality_k_full( @pytest.mark.parametrize("subset_seed", [None]) @pytest.mark.parametrize("result_dtype", RESULT_DTYPE_OPTIONS) @pytest.mark.parametrize("edgevals", WEIGHTED_GRAPH_OPTIONS) +@pytest.mark.skip(reason="Skipping large tests") def test_betweenness_centrality_fixed_sample( graph_file, directed, @@ -415,6 +417,7 @@ def test_betweenness_centrality_fixed_sample( @pytest.mark.parametrize("subset_seed", SUBSET_SEED_OPTIONS) @pytest.mark.parametrize("result_dtype", RESULT_DTYPE_OPTIONS) @pytest.mark.parametrize("edgevals", WEIGHTED_GRAPH_OPTIONS) +@pytest.mark.skip(reason="Skipping large tests") def test_betweenness_centrality_weight_except( graph_file, directed, diff --git a/python/cugraph/tests/test_bfs.py b/python/cugraph/tests/test_bfs.py index d04ef957104..a8547d692c2 100644 --- a/python/cugraph/tests/test_bfs.py +++ b/python/cugraph/tests/test_bfs.py @@ -51,6 +51,7 @@ DEFAULT_EPSILON = 1e-6 +DEPTH_LIMITS = [None, 1, 5, 18] # Map of cuGraph input types to the expected output type for cuGraph # connected_components calls. @@ -148,28 +149,14 @@ def compare_single_sp_counter(result, expected, epsilon=DEFAULT_EPSILON): return np.isclose(result, expected, rtol=epsilon) -def compare_bfs(benchmark_callable, G, nx_values, start_vertex, - return_sp_counter=False): +def compare_bfs(benchmark_callable, G, nx_values, start_vertex, depth_limit): """ Genereate both cugraph and reference bfs traversal. """ if isinstance(start_vertex, int): - result = benchmark_callable(cugraph.bfs_edges, G, start_vertex, - return_sp_counter=return_sp_counter) + result = benchmark_callable(cugraph.bfs_edges, G, start_vertex) cugraph_df = convert_output_to_cudf(G, result) - - if return_sp_counter: - # This call should only contain 3 columns: - # 'vertex', 'distance', 'predecessor', 'sp_counter' - assert len(cugraph_df.columns) == 4, ( - "The result of the BFS has an invalid " "number of columns" - ) - - if return_sp_counter: - compare_func = _compare_bfs_spc - - else: - compare_func = _compare_bfs + compare_func = _compare_bfs # NOTE: We need to take 2 different path for verification as the nx # functions used as reference return dictionaries that might @@ -185,18 +172,15 @@ def compare_bfs(benchmark_callable, G, nx_values, start_vertex, def func_to_benchmark(): for sv in start_vertex: cugraph_df = cugraph.bfs_edges( - G, sv, return_sp_counter=return_sp_counter) + G, sv, depth_limit=depth_limit) all_cugraph_distances.append(cugraph_df) benchmark_callable(func_to_benchmark) - compare_func = _compare_bfs_spc if return_sp_counter else _compare_bfs + compare_func = _compare_bfs for (i, sv) in enumerate(start_vertex): cugraph_df = convert_output_to_cudf(G, all_cugraph_distances[i]) - if return_sp_counter: - assert len(cugraph_df.columns) == 4, ( - "The result of the BFS has an invalid " "number of columns" - ) + compare_func(cugraph_df, all_nx_values[i], sv) else: # Unknown type given to seed @@ -272,55 +256,6 @@ def _compare_bfs(cugraph_df, nx_distances, source): assert invalid_predecessor_error == 0, "There are invalid predecessors" -def _compare_bfs_spc(cugraph_df, nx_sp_counter, unused): - """ - Compare BFS with shortest path counters. - """ - sorted_nx = [nx_sp_counter[key] for key in sorted(nx_sp_counter.keys())] - # We are not checking for distances / predecessors here as we assume - # that these have been checked in the _compare_bfs tests - # We focus solely on shortest path counting - - # cugraph return a dataframe that should contain exactly one time each - # vertex - # We could us isin to filter only vertices that are common to both - # But it would slow down the comparison, and in this specific case - # nxacb._single_source_shortest_path_basic is a dictionary containing all - # the vertices. - # There is no guarantee when we get `df` that the vertices are sorted - # thus we enforce the order so that we can leverage faster comparison after - sorted_df = cugraph_df.sort_values("vertex").rename( - columns={"sp_counter": "cu_spc"}, copy=False - ) - - # This allows to detect vertices identifier that could have been - # wrongly present multiple times - cu_vertices = set(sorted_df['vertex'].values_host) - nx_vertices = nx_sp_counter.keys() - assert len(cu_vertices.intersection(nx_vertices)) == len( - nx_vertices - ), "There are missing vertices" - - # We add the nx shortest path counter in the cudf.DataFrame, both the - # the DataFrame and `sorted_nx` are sorted base on vertices identifiers - sorted_df["nx_spc"] = sorted_nx - - # We could use numpy.isclose or cupy.isclose, we can then get the entries - # in the cudf.DataFrame where there are is a mismatch. - # numpy / cupy allclose would get only a boolean and we might want the - # extra information about the discrepancies - shortest_path_counter_errors = sorted_df[ - ~cupy.isclose( - sorted_df["cu_spc"], sorted_df["nx_spc"], rtol=DEFAULT_EPSILON - ) - ] - if len(shortest_path_counter_errors) > 0: - print(shortest_path_counter_errors) - assert len(shortest_path_counter_errors) == 0, ( - "Shortest path counters " "are too different" - ) - - def get_nx_graph_and_params(dataset, directed): """ Helper for fixtures returning a Nx graph obj and params. @@ -329,21 +264,17 @@ def get_nx_graph_and_params(dataset, directed): utils.generate_nx_graph_from_file(dataset, directed)) -def get_nx_results_and_params(seed, use_spc, dataset, directed, Gnx): +def get_nx_results_and_params(seed, depth_limit, dataset, directed, Gnx): """ Helper for fixtures returning Nx results and params. """ random.seed(seed) start_vertex = random.sample(Gnx.nodes(), 1)[0] - if use_spc: - _, _, nx_sp_counter = \ - nxacb._single_source_shortest_path_basic(Gnx, start_vertex) - nx_values = nx_sp_counter - else: - nx_values = nx.single_source_shortest_path_length(Gnx, start_vertex) + nx_values = nx.single_source_shortest_path_length(Gnx, start_vertex, + cutoff=depth_limit) - return (dataset, directed, nx_values, start_vertex, use_spc) + return (dataset, directed, nx_values, start_vertex, depth_limit) # ============================================================================= @@ -353,7 +284,7 @@ def get_nx_results_and_params(seed, use_spc, dataset, directed, Gnx): DIRECTED = [pytest.param(d) for d in DIRECTED_GRAPH_OPTIONS] DATASETS = [pytest.param(d) for d in utils.DATASETS] DATASETS_SMALL = [pytest.param(d) for d in utils.DATASETS_SMALL] -USE_SHORTEST_PATH_COUNTER = [pytest.param(False), pytest.param(True)] +DEPTH_LIMIT = [pytest.param(d) for d in DEPTH_LIMITS] # Call genFixtureParamsProduct() to caluculate the cartesian product of # multiple lists of params. This is required since parameterized fixtures do @@ -362,7 +293,7 @@ def get_nx_results_and_params(seed, use_spc, dataset, directed, Gnx): # full test name. algo_test_fixture_params = utils.genFixtureParamsProduct( (SEEDS, "seed"), - (USE_SHORTEST_PATH_COUNTER, "spc")) + (DEPTH_LIMIT, "depth_limit")) graph_fixture_params = utils.genFixtureParamsProduct( (DATASETS, "ds"), @@ -377,7 +308,7 @@ def get_nx_results_and_params(seed, use_spc, dataset, directed, Gnx): # was covered elsewhere). single_algo_test_fixture_params = utils.genFixtureParamsProduct( ([SEEDS[0]], "seed"), - ([USE_SHORTEST_PATH_COUNTER[0]], "spc")) + ([DEPTH_LIMIT[0]], "depth_limit")) single_small_graph_fixture_params = utils.genFixtureParamsProduct( ([DATASETS_SMALL[0]], "ds"), @@ -446,7 +377,7 @@ def test_bfs(gpubenchmark, dataset_nxresults_startvertex_spc, """ Test BFS traversal on random source with distance and predecessors """ - (dataset, directed, nx_values, start_vertex, use_spc) = \ + (dataset, directed, nx_values, start_vertex, depth_limit) = \ dataset_nxresults_startvertex_spc # special case: ensure cugraph and Nx Graph types are DiGraphs if @@ -463,8 +394,7 @@ def test_bfs(gpubenchmark, dataset_nxresults_startvertex_spc, compare_bfs( gpubenchmark, - G_or_matrix, nx_values, start_vertex, return_sp_counter=use_spc - ) + G_or_matrix, nx_values, start_vertex, depth_limit) @pytest.mark.parametrize("cugraph_input_type", @@ -477,36 +407,6 @@ def test_bfs_nonnative_inputs(gpubenchmark, cugraph_input_type) -@pytest.mark.parametrize("cugraph_input_type", utils.CUGRAPH_INPUT_TYPES) -def test_bfs_spc_full(gpubenchmark, dataset_nxresults_allstartvertices_spc, - cugraph_input_type): - """ - Test BFS traversal on every vertex with shortest path counting - """ - (dataset, directed, all_nx_values, start_vertices, use_spc) = \ - dataset_nxresults_allstartvertices_spc - - # use_spc is currently always True - - # special case: ensure cugraph and Nx Graph types are DiGraphs if - # "directed" is set, since the graph type parameterization is currently - # independent of the directed parameter. Unfortunately this does not - # change the "id" in the pytest output. - if directed: - if cugraph_input_type is cugraph.Graph: - cugraph_input_type = cugraph.DiGraph - elif cugraph_input_type is nx.Graph: - cugraph_input_type = nx.DiGraph - - G_or_matrix = utils.create_obj_from_csv(dataset, cugraph_input_type) - - compare_bfs( - gpubenchmark, - G_or_matrix, all_nx_values, start_vertex=start_vertices, - return_sp_counter=use_spc - ) - - def test_scipy_api_compat(): graph_file = utils.DATASETS[0] @@ -522,7 +422,7 @@ def test_scipy_api_compat(): # Ensure cugraph-compatible options work as expected cugraph.bfs(input_cugraph_graph, i_start=0) - cugraph.bfs(input_cugraph_graph, i_start=0, return_sp_counter=True) + cugraph.bfs(input_cugraph_graph, i_start=0) # cannot have start and i_start with pytest.raises(TypeError): cugraph.bfs(input_cugraph_graph, start=0, i_start=0) @@ -531,7 +431,6 @@ def test_scipy_api_compat(): cugraph.bfs(input_coo_matrix, i_start=0) cugraph.bfs(input_coo_matrix, i_start=0, directed=True) cugraph.bfs(input_coo_matrix, i_start=0, directed=False) - result = cugraph.bfs(input_coo_matrix, i_start=0, - return_sp_counter=True) + result = cugraph.bfs(input_coo_matrix, i_start=0) assert type(result) is tuple - assert len(result) == 3 + assert len(result) == 2 diff --git a/python/cugraph/tests/test_edge_betweenness_centrality.py b/python/cugraph/tests/test_edge_betweenness_centrality.py index 8c5aad7dc61..6caad0d9fad 100644 --- a/python/cugraph/tests/test_edge_betweenness_centrality.py +++ b/python/cugraph/tests/test_edge_betweenness_centrality.py @@ -341,6 +341,7 @@ def test_edge_betweenness_centrality( @pytest.mark.parametrize("result_dtype", RESULT_DTYPE_OPTIONS) @pytest.mark.parametrize("use_k_full", [True]) @pytest.mark.parametrize("edgevals", WEIGHTED_GRAPH_OPTIONS) +@pytest.mark.skip(reason="Skipping large tests") def test_edge_betweenness_centrality_k_full( graph_file, directed, @@ -381,6 +382,7 @@ def test_edge_betweenness_centrality_k_full( @pytest.mark.parametrize("subset_seed", [None]) @pytest.mark.parametrize("result_dtype", RESULT_DTYPE_OPTIONS) @pytest.mark.parametrize("edgevals", WEIGHTED_GRAPH_OPTIONS) +@pytest.mark.skip(reason="Skipping large tests") def test_edge_betweenness_centrality_fixed_sample( graph_file, directed, @@ -417,6 +419,7 @@ def test_edge_betweenness_centrality_fixed_sample( @pytest.mark.parametrize("subset_seed", SUBSET_SEED_OPTIONS) @pytest.mark.parametrize("result_dtype", RESULT_DTYPE_OPTIONS) @pytest.mark.parametrize("edgevals", WEIGHTED_GRAPH_OPTIONS) +@pytest.mark.skip(reason="Skipping large tests") def test_edge_betweenness_centrality_weight_except( graph_file, directed, diff --git a/python/cugraph/tests/test_graph.py b/python/cugraph/tests/test_graph.py index 348f7e2e130..933a34aef3c 100644 --- a/python/cugraph/tests/test_graph.py +++ b/python/cugraph/tests/test_graph.py @@ -200,6 +200,7 @@ def test_add_adj_list_to_edge_list(graph_file): # cugraph add_adj_list to_edge_list call G = cugraph.DiGraph() G.from_cudf_adjlist(offsets, indices, None) + edgelist = G.view_edge_list() sources_cu = edgelist["src"] destinations_cu = edgelist["dst"] @@ -535,6 +536,7 @@ def test_to_directed(graph_file): DiG = G.to_directed() DiGnx = Gnx.to_directed() + assert DiG.is_directed() assert DiG.number_of_nodes() == DiGnx.number_of_nodes() assert DiG.number_of_edges() == DiGnx.number_of_edges() @@ -569,6 +571,7 @@ def test_to_undirected(graph_file): G = DiG.to_undirected() Gnx = DiGnx.to_undirected() + assert not G.is_directed() assert G.number_of_nodes() == Gnx.number_of_nodes() assert G.number_of_edges() == Gnx.number_of_edges() @@ -627,17 +630,13 @@ def test_bipartite_api(graph_file): set2_exp = cudf.Series(set(nodes.values_host) - set(set1_exp.values_host)) - G = cugraph.Graph() - assert not G.is_bipartite() + G = cugraph.BiPartiteGraph() + assert G.is_bipartite() # Add a set of nodes present in one partition G.add_nodes_from(set1_exp, bipartite='set1') G.from_cudf_edgelist(cu_M, source='0', destination='1') - # Check if Graph is bipartite. It should return True since we have - # added the partition in add_nodes_from() - assert G.is_bipartite() - # Call sets() to get the bipartite set of nodes. set1, set2 = G.sets() diff --git a/python/cugraph/tests/test_utils.py b/python/cugraph/tests/test_utils.py index 55256d6b74e..175cf389d16 100644 --- a/python/cugraph/tests/test_utils.py +++ b/python/cugraph/tests/test_utils.py @@ -73,6 +73,7 @@ def test_bfs_paths_array(): @pytest.mark.parametrize("graph_file", utils.DATASETS) +@pytest.mark.skip(reason="Skipping large tests") def test_get_traversed_cost(graph_file): cu_M = utils.read_csv_file(graph_file) diff --git a/python/cugraph/traversal/bfs.pxd b/python/cugraph/traversal/bfs.pxd index 0467bf05090..b6465a6698c 100644 --- a/python/cugraph/traversal/bfs.pxd +++ b/python/cugraph/traversal/bfs.pxd @@ -19,6 +19,8 @@ from cugraph.structure.graph_utilities cimport * from libcpp cimport bool +cdef extern from "limits.h": + cdef int INT_MAX cdef extern from "utilities/cython.hpp" namespace "cugraph::cython": cdef void call_bfs[vertex_t, weight_t]( @@ -27,6 +29,6 @@ cdef extern from "utilities/cython.hpp" namespace "cugraph::cython": vertex_t *identifiers, vertex_t *distances, vertex_t *predecessors, - double *sp_counters, + vertex_t depth_limit, const vertex_t start_vertex, - bool directed) except + + bool direction_optimizing) except + diff --git a/python/cugraph/traversal/bfs.py b/python/cugraph/traversal/bfs.py index a483b96850b..d397b5a4241 100644 --- a/python/cugraph/traversal/bfs.py +++ b/python/cugraph/traversal/bfs.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, NVIDIA CORPORATION. +# Copyright (c) 2019-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -14,7 +14,7 @@ import cudf from cugraph.traversal import bfs_wrapper -from cugraph.structure.graph import Graph, DiGraph +from cugraph.structure.graph_classes import Graph, DiGraph from cugraph.utilities import (ensure_cugraph_obj, is_matrix_type, is_cp_matrix_type, @@ -41,7 +41,7 @@ import_from="scipy.sparse.csc") -def _ensure_args(G, start, return_sp_counter, i_start, directed): +def _ensure_args(G, start, i_start, directed): """ Ensures the args passed in are usable for the API api_name and returns the args with proper defaults if not specified, or raises TypeError or @@ -52,9 +52,6 @@ def _ensure_args(G, start, return_sp_counter, i_start, directed): raise TypeError("cannot specify both 'start' and 'i_start'") if (start is None) and (i_start is None): raise TypeError("must specify 'start' or 'i_start', but not both") - if (return_sp_counter is not None) and \ - (return_sp_counter not in [True, False]): - raise ValueError("'return_sp_counter' must be a bool") G_type = type(G) # Check for Graph-type inputs @@ -67,10 +64,8 @@ def _ensure_args(G, start, return_sp_counter, i_start, directed): start = start if start is not None else i_start if directed is None: directed = True - if return_sp_counter is None: - return_sp_counter = False - return (start, return_sp_counter, directed) + return (start, directed) def _convert_df_to_output_type(df, input_type): @@ -92,30 +87,23 @@ def _convert_df_to_output_type(df, input_type): if is_cp_matrix_type(input_type): distances = cp.fromDlpack(sorted_df["distance"].to_dlpack()) preds = cp.fromDlpack(sorted_df["predecessor"].to_dlpack()) - if "sp_counter" in df.columns: - return (distances, preds, - cp.fromDlpack(sorted_df["sp_counter"].to_dlpack())) - else: - return (distances, preds) + return (distances, preds) else: distances = sorted_df["distance"].to_array() preds = sorted_df["predecessor"].to_array() - if "sp_counter" in df.columns: - return (distances, preds, - sorted_df["sp_counter"].to_array()) - else: - return (distances, preds) + return (distances, preds) else: raise TypeError(f"input type {input_type} is not a supported type.") def bfs(G, start=None, - return_sp_counter=None, + depth_limit=None, i_start=None, directed=None, return_predecessors=None): - """Find the distances and predecessors for a breadth first traversal of a + """ + Find the distances and predecessors for a breadth first traversal of a graph. Parameters @@ -128,13 +116,13 @@ def bfs(G, start : Integer The index of the graph vertex from which the traversal begins - return_sp_counter : bool, optional, default=False - Indicates if shortest path counters should be returned - i_start : Integer, optional Identical to start, added for API compatibility. Only start or i_start can be set, not both. + depth_limit : Integer or None + Limit the depth of the search + directed : bool, optional NOTE For non-Graph-type (eg. sparse matrix) values of G only. Raises @@ -156,10 +144,6 @@ def bfs(G, df['predecessor'] for each i'th position in the column, the vertex ID immediately preceding the vertex at position i in the 'vertex' column - df['sp_counter'] for each i'th position in the column, the number of - shortest paths leading to the vertex at position i in the 'vertex' - column (Only if retrun_sp_counter is True) - If G is a networkx.Graph, returns: pandas.DataFrame with contents equivalent to the cudf.DataFrame @@ -191,34 +175,30 @@ def bfs(G, >>> df = cugraph.bfs(G, 0) """ - (start, return_sp_counter, directed) = \ - _ensure_args(G, start, return_sp_counter, i_start, directed) + (start, directed) = \ + _ensure_args(G, start, i_start, directed) # FIXME: allow nx_weight_attr to be specified (G, input_type) = ensure_cugraph_obj( G, nx_weight_attr="weight", matrix_graph_type=DiGraph if directed else Graph) - if type(G) is Graph: - is_directed = False - else: - is_directed = True - if G.renumbered is True: - start = G.lookup_internal_vertex_id(cudf.Series([start]))[0] - - df = bfs_wrapper.bfs(G, start, is_directed, return_sp_counter) + if isinstance(start, cudf.DataFrame): + start = G.lookup_internal_vertex_id(start, start.columns).iloc[0] + else: + start = G.lookup_internal_vertex_id(cudf.Series([start]))[0] + df = bfs_wrapper.bfs(G, start, depth_limit) if G.renumbered: df = G.unrenumber(df, "vertex") df = G.unrenumber(df, "predecessor") - df["predecessor"].fillna(-1, inplace=True) + df.fillna(-1, inplace=True) return _convert_df_to_output_type(df, input_type) -def bfs_edges(G, source, reverse=False, depth_limit=None, sort_neighbors=None, - return_sp_counter=False): +def bfs_edges(G, source, reverse=False, depth_limit=None, sort_neighbors=None): """ Find the distances and predecessors for a breadth first traversal of a graph. @@ -239,14 +219,10 @@ def bfs_edges(G, source, reverse=False, depth_limit=None, sort_neighbors=None, depth_limit : Int or None Limit the depth of the search - Currently not implemented sort_neighbors : None or Function Currently not implemented - return_sp_counter : bool, optional, default=False - Indicates if shortest path counters should be returned - Returns ------- Return value type is based on the input type. If G is a cugraph.Graph, @@ -260,10 +236,6 @@ def bfs_edges(G, source, reverse=False, depth_limit=None, sort_neighbors=None, df['predecessor'] for each i'th position in the column, the vertex ID immediately preceding the vertex at position i in the 'vertex' column - df['sp_counter'] for each i'th position in the column, the number of - shortest paths leading to the vertex at position i in the 'vertex' - column (Only if retrun_sp_counter is True) - If G is a networkx.Graph, returns: pandas.DataFrame with contents equivalent to the cudf.DataFrame @@ -300,9 +272,4 @@ def bfs_edges(G, source, reverse=False, depth_limit=None, sort_neighbors=None, "reverse processing of graph is currently not supported" ) - if depth_limit is not None: - raise NotImplementedError( - "depth limit implementation of BFS is not currently supported" - ) - - return bfs(G, source, return_sp_counter) + return bfs(G, source, depth_limit) diff --git a/python/cugraph/traversal/bfs_wrapper.pyx b/python/cugraph/traversal/bfs_wrapper.pyx index f475842a7bf..f524b133d02 100644 --- a/python/cugraph/traversal/bfs_wrapper.pyx +++ b/python/cugraph/traversal/bfs_wrapper.pyx @@ -24,54 +24,44 @@ from libc.stdint cimport uintptr_t import cudf import numpy as np -def bfs(input_graph, start, directed=True, - return_sp_counter=False): +def bfs(input_graph, start, depth_limit, direction_optimizing=False): """ Call bfs """ # Step 1: Declare the different varibales cdef graph_container_t graph_container - # FIXME: Offsets and indices are currently hardcoded to int, but this may - # not be acceptable in the future. + numberTypeMap = {np.dtype("int32") : numberTypeEnum.int32Type, np.dtype("int64") : numberTypeEnum.int64Type, np.dtype("float32") : numberTypeEnum.floatType, np.dtype("double") : numberTypeEnum.doubleType} - # Pointers required for CSR Graph - cdef uintptr_t c_offsets_ptr = NULL # Pointer to the CSR offsets - cdef uintptr_t c_indices_ptr = NULL # Pointer to the CSR indices - cdef uintptr_t c_weights = NULL - cdef uintptr_t c_local_verts = NULL; - cdef uintptr_t c_local_edges = NULL; - cdef uintptr_t c_local_offsets = NULL; weight_t = np.dtype("float32") + [src, dst] = graph_primtypes_wrapper.datatype_cast([input_graph.edgelist.edgelist_df['src'], input_graph.edgelist.edgelist_df['dst']], [np.int32]) + weights = None # Pointers for SSSP / BFS cdef uintptr_t c_identifier_ptr = NULL # Pointer to the DataFrame 'vertex' Series cdef uintptr_t c_distance_ptr = NULL # Pointer to the DataFrame 'distance' Series cdef uintptr_t c_predecessor_ptr = NULL # Pointer to the DataFrame 'predecessor' Series - cdef uintptr_t c_sp_counter_ptr = NULL # Pointer to the DataFrame 'sp_counter' Series + if depth_limit is None: + depth_limit = c_bfs.INT_MAX # Step 2: Verifiy input_graph has the expected format - if input_graph.adjlist is None: - input_graph.view_adj_list() cdef unique_ptr[handle_t] handle_ptr handle_ptr.reset(new handle_t()) handle_ = handle_ptr.get(); - # Step 3: Extract CSR offsets, indices, weights are not expected - # - offsets: int (signed, 32-bit) - # - indices: int (signed, 32-bit) - [offsets, indices] = graph_primtypes_wrapper.datatype_cast([input_graph.adjlist.offsets, input_graph.adjlist.indices], [np.int32]) - c_offsets_ptr = offsets.__cuda_array_interface__['data'][0] - c_indices_ptr = indices.__cuda_array_interface__['data'][0] - - # Step 4: Setup number of vertices and edges + # Step 3: Setup number of vertices and edges num_verts = input_graph.number_of_vertices() num_edges = input_graph.number_of_edges(directed_edges=True) + # Step 4: Extract COO + cdef uintptr_t c_src_vertices = src.__cuda_array_interface__['data'][0] + cdef uintptr_t c_dst_vertices = dst.__cuda_array_interface__['data'][0] + cdef uintptr_t c_edge_weights = NULL + # Step 5: Check if source index is valid if not 0 <= start < num_verts: raise ValueError("Starting vertex should be between 0 to number of vertices") @@ -79,30 +69,29 @@ def bfs(input_graph, start, directed=True, # Step 6: Generate the cudf.DataFrame result # Current implementation expects int (signed 32-bit) for distance df = cudf.DataFrame() - df['vertex'] = cudf.Series(np.zeros(num_verts, dtype=np.int32)) + df['vertex'] = cudf.Series(np.arange(num_verts), dtype=np.int32) df['distance'] = cudf.Series(np.zeros(num_verts, dtype=np.int32)) df['predecessor'] = cudf.Series(np.zeros(num_verts, dtype=np.int32)) - if (return_sp_counter): - df['sp_counter'] = cudf.Series(np.zeros(num_verts, dtype=np.double)) # Step 7: Associate to cudf Series c_identifier_ptr = df['vertex'].__cuda_array_interface__['data'][0] c_distance_ptr = df['distance'].__cuda_array_interface__['data'][0] c_predecessor_ptr = df['predecessor'].__cuda_array_interface__['data'][0] - if return_sp_counter: - c_sp_counter_ptr = df['sp_counter'].__cuda_array_interface__['data'][0] # Step 8: Proceed to BFS - # FIXME: [int, int, float] or may add an explicit [int, int, int] in graph.cu? - populate_graph_container_legacy(graph_container, - ((graphTypeEnum.LegacyCSR)), - handle_[0], - c_offsets_ptr, c_indices_ptr, c_weights, - ((numberTypeEnum.int32Type)), - ((numberTypeEnum.int32Type)), - ((numberTypeMap[weight_t])), - num_verts, num_edges, - c_local_verts, c_local_edges, c_local_offsets) + populate_graph_container(graph_container, + handle_[0], + c_src_vertices, c_dst_vertices, c_edge_weights, + NULL, + ((numberTypeEnum.int32Type)), + ((numberTypeEnum.int32Type)), + ((numberTypeMap[weight_t])), + num_edges, + num_verts, num_edges, + False, + False, + False, + False) # Different pathing wether shortest_path_counting is required or not c_bfs.call_bfs[int, float](handle_ptr.get()[0], @@ -110,8 +99,8 @@ def bfs(input_graph, start, directed=True, c_identifier_ptr, c_distance_ptr, c_predecessor_ptr, - c_sp_counter_ptr, + depth_limit, start, - directed) + direction_optimizing) return df diff --git a/python/cugraph/traversal/sssp_wrapper.pyx b/python/cugraph/traversal/sssp_wrapper.pyx index 36e4797e0c8..46966cd3e99 100644 --- a/python/cugraph/traversal/sssp_wrapper.pyx +++ b/python/cugraph/traversal/sssp_wrapper.pyx @@ -46,7 +46,7 @@ def sssp(input_graph, source): cdef uintptr_t c_local_verts = NULL; cdef uintptr_t c_local_edges = NULL; cdef uintptr_t c_local_offsets = NULL; - weight_t = np.dtype("int32") + weight_t = np.dtype("float32") # Pointers for SSSP / BFS cdef uintptr_t c_identifier_ptr = NULL # Pointer to the DataFrame 'vertex' Series @@ -110,31 +110,21 @@ def sssp(input_graph, source): num_verts, num_edges, c_local_verts, c_local_edges, c_local_offsets) - if weights is not None: - if weight_t == np.float32: - c_sssp.call_sssp[int, float](handle_[0], - graph_container, - c_identifier_ptr, - c_distance_ptr, - c_predecessor_ptr, - source) - elif weight_t == np.float64: - c_sssp.call_sssp[int, double](handle_[0], - graph_container, - c_identifier_ptr, - c_distance_ptr, - c_predecessor_ptr, - source) - else: # This case should not happen - raise NotImplementedError - else: - c_bfs.call_bfs[int, float](handle_[0], - graph_container, - c_identifier_ptr, - c_distance_ptr, - c_predecessor_ptr, - NULL, - source, - 1) + if weight_t == np.float32: + c_sssp.call_sssp[int, float](handle_[0], + graph_container, + c_identifier_ptr, + c_distance_ptr, + c_predecessor_ptr, + source) + elif weight_t == np.float64: + c_sssp.call_sssp[int, double](handle_[0], + graph_container, + c_identifier_ptr, + c_distance_ptr, + c_predecessor_ptr, + source) + else: # This case should not happen + raise NotImplementedError return df diff --git a/python/cugraph/traversal/traveling_salesperson.py b/python/cugraph/traversal/traveling_salesperson.py index 7aea7ae603f..53d411c92ae 100644 --- a/python/cugraph/traversal/traveling_salesperson.py +++ b/python/cugraph/traversal/traveling_salesperson.py @@ -12,7 +12,7 @@ # limitations under the License. from cugraph.traversal import traveling_salesperson_wrapper -from cugraph.structure.graph import null_check +from cugraph.structure.graph_classes import null_check import cudf diff --git a/python/cugraph/tree/minimum_spanning_tree.py b/python/cugraph/tree/minimum_spanning_tree.py index 45e996aa083..6a5f7b5bf38 100644 --- a/python/cugraph/tree/minimum_spanning_tree.py +++ b/python/cugraph/tree/minimum_spanning_tree.py @@ -12,7 +12,7 @@ # limitations under the License. from cugraph.tree import minimum_spanning_tree_wrapper -from cugraph.structure.graph import Graph +from cugraph.structure.graph_classes import Graph from cugraph.utilities import check_nx_graph from cugraph.utilities import cugraph_to_nx diff --git a/python/setup.py b/python/setup.py index 59292f32032..799cb805afa 100644 --- a/python/setup.py +++ b/python/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018-2020, NVIDIA CORPORATION. +# Copyright (c) 2018-2021, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -111,7 +111,7 @@ def run(self): runtime_library_dirs=[conda_lib_dir], libraries=['cugraph', 'nccl'], language='c++', - extra_compile_args=['-std=c++14']) + extra_compile_args=['-std=c++17']) ] for e in EXTENSIONS: diff --git a/python/utils/ECG_Golden.ipynb b/python/utils/ECG_Golden.ipynb deleted file mode 100644 index 0da04869d78..00000000000 --- a/python/utils/ECG_Golden.ipynb +++ /dev/null @@ -1,487 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This notebook was used to generate the golden data results for ECG. It requires that the python-igraph package be installed to run. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from scipy.io import mmread\n", - "import networkx as nx\n", - "#mmFile='/datasets/kron_g500-logn21/kron_g500-logn21.mtx'\n", - "mmFile='/datasets/golden_data/graphs/dblp.mtx'\n", - "#mmFile='/datasets/networks/karate.mtx'\n", - "#mmFile='/home/jwyles/code/mycugraph/datasets/dolphins.mtx'\n", - "#mmFile='/home/jwyles/code/mycugraph/datasets/netscience.mtx'\n", - "M = mmread(mmFile).asfptype()\n", - "import cugraph\n", - "import cudf\n", - "import numpy as np\n", - "rows = cudf.Series(M.row)\n", - "cols = cudf.Series(M.col)\n", - "values = cudf.Series(M.data)\n", - "G = cugraph.Graph()\n", - "G.add_edge_list(rows, cols, values)" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 326 ms, sys: 400 ms, total: 726 ms\n", - "Wall time: 796 ms\n" - ] - } - ], - "source": [ - "%%time\n", - "parts = cugraph.ecg(G, .05, 16)" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "49204" - ] - }, - "execution_count": 47, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "numParts = parts['partition'].max() + 1\n", - "numParts" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.850147008895874" - ] - }, - "execution_count": 48, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "mod = cugraph.analyzeClustering_modularity(G, numParts, parts['partition'])\n", - "mod" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.7506256512679915" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "parts2, mod2 = cugraph.louvain(G)\n", - "mod2" - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "metadata": {}, - "outputs": [], - "source": [ - "import igraph as ig\n", - "import numpy as np\n", - "\n", - "def community_ecg(self, weights=None, ens_size=16, min_weight=0.05):\n", - " W = [0]*self.ecount()\n", - " ## Ensemble of level-1 Louvain \n", - " for i in range(ens_size):\n", - " p = np.random.permutation(self.vcount()).tolist()\n", - " g = self.permute_vertices(p)\n", - " l = g.community_multilevel(weights=weights, return_levels=True)[0].membership\n", - " b = [l[p[x.tuple[0]]]==l[p[x.tuple[1]]] for x in self.es]\n", - " W = [W[i]+b[i] for i in range(len(W))]\n", - " W = [min_weight + (1-min_weight)*W[i]/ens_size for i in range(len(W))]\n", - " part = self.community_multilevel(weights=W)\n", - " ## Force min_weight outside 2-core\n", - " core = self.shell_index()\n", - " ecore = [min(core[x.tuple[0]],core[x.tuple[1]]) for x in self.es]\n", - " part.W = [W[i] if ecore[i]>1 else min_weight for i in range(len(ecore))]\n", - " return part\n", - "\n", - "ig.Graph.community_ecg = community_ecg" - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "metadata": {}, - "outputs": [], - "source": [ - "Gi = ig.Graph.Read_Edgelist('./dblp2.txt', directed=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 3min 49s, sys: 1.67 s, total: 3min 51s\n", - "Wall time: 3min 50s\n" - ] - } - ], - "source": [ - "%%time\n", - "ec = Gi.community_ecg()" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": {}, - "outputs": [], - "source": [ - "ecg = np.zeros(len(Gi.vs), dtype=np.int32)" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([0, 0, 0, ..., 0, 0, 0], dtype=int32)" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ecg" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": {}, - "outputs": [], - "source": [ - "for i in range(len(ec)):\n", - " for j in ec[i]:\n", - " ecg[j] = i" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([275, 275, 0, ..., 435, 435, 107], dtype=int32)" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ecg" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": {}, - "outputs": [], - "source": [ - "ecg_col = cudf.Series(ecg)" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": {}, - "outputs": [], - "source": [ - "numParts = ecg_col.max() + 1" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": {}, - "outputs": [], - "source": [ - "mod4 = cugraph.analyzeClustering_modularity(G, numParts, ecg_col)" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.9279554486274719" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "mod4" - ] - }, - { - "cell_type": "code", - "execution_count": 94, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "34" - ] - }, - "execution_count": 94, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "maxId = 0\n", - "for i in range(len(ec)):\n", - " for j in ec[i]:\n", - " if j > maxId:\n", - " maxId = j\n", - "maxId" - ] - }, - { - "cell_type": "code", - "execution_count": 79, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "156" - ] - }, - "execution_count": 79, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "len(Gi.es)" - ] - }, - { - "cell_type": "code", - "execution_count": 80, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "156" - ] - }, - "execution_count": 80, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "len(M.row)" - ] - }, - { - "cell_type": "code", - "execution_count": 81, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "156" - ] - }, - "execution_count": 81, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "78 *2" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "metadata": {}, - "outputs": [], - "source": [ - "filename = \"dblp2.txt\"\n", - "f = open(filename, 'w')\n", - "for i in range(len(M.row)):\n", - " f.write(str(M.row[i]) + ' ' + str(M.col[i]) + '\\n')\n", - "f.close()" - ] - }, - { - "cell_type": "code", - "execution_count": 82, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "igraph.Edge(, 1, {})" - ] - }, - "execution_count": 82, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Gi.es[1]" - ] - }, - { - "cell_type": "code", - "execution_count": 84, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "igraph.Edge(, 1, {})" - ] - }, - "execution_count": 84, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Gi.es.select()[1]" - ] - }, - { - "cell_type": "code", - "execution_count": 85, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0" - ] - }, - "execution_count": 85, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Gi.es[0].source" - ] - }, - { - "cell_type": "code", - "execution_count": 88, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1" - ] - }, - "execution_count": 88, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Gi.es[0].target" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -}