From 4d2dce99a40267d5d1e14577e61464215ebe0b02 Mon Sep 17 00:00:00 2001 From: Ray Douglass Date: Fri, 19 Jul 2024 17:27:10 -0400 Subject: [PATCH 01/21] DOC v24.10 Updates [skip ci] --- VERSION | 2 +- python/nx-cugraph/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index ec8489fda..7c7ba0443 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -24.08.00 +24.10.00 diff --git a/python/nx-cugraph/pyproject.toml b/python/nx-cugraph/pyproject.toml index 50881d5db..065a3c6d0 100644 --- a/python/nx-cugraph/pyproject.toml +++ b/python/nx-cugraph/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "cupy-cuda11x>=12.0.0", "networkx>=3.0", "numpy>=1.23,<2.0a0", - "pylibcugraph==24.8.*,>=0.0.0a0", + "pylibcugraph==24.10.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [project.optional-dependencies] From 83b99b2bde0b5bfdb3735c61e645c3a1b1e28b60 Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Fri, 9 Aug 2024 13:30:41 -0400 Subject: [PATCH 02/21] Update pre-commit hooks (#4605) This PR updates pre-commit hooks to the latest versions that are supported without causing style check errors. Authors: - Kyle Edwards (https://github.com/KyleFromNVIDIA) Approvers: - James Lamb (https://github.com/jameslamb) URL: https://github.com/rapidsai/cugraph/pull/4605 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36c5fa841..7b5b16e3e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: types_or: [c, c++, cuda] args: ["-fallback-style=none", "-style=file", "-i"] - repo: https://github.com/rapidsai/pre-commit-hooks - rev: v0.2.0 + rev: v0.3.1 hooks: - id: verify-copyright files: | From 4909256c6edd0aebddecc38885f4433b72fb780e Mon Sep 17 00:00:00 2001 From: Ralph Liu <137829296+nv-rliu@users.noreply.github.com> Date: Fri, 16 Aug 2024 12:06:27 -0400 Subject: [PATCH 03/21] Add Additional Check for `NetworkX` Release Candidate Versions (#4613) Part of https://github.com/rapidsai/graph_dl/issues/579 This PR allows `nx-cugraph` to operate with release candidate (under development) versions of `networkx` What This Pattern Allows: - Just a single digit: `3.4`, `3.9`, etc - Release Candidate format: `3.4rc0`. `3.7rc2` This is needed to complete adding nightly test coverage for development branches of `nx` Authors: - Ralph Liu (https://github.com/nv-rliu) Approvers: - Erik Welch (https://github.com/eriknw) - Rick Ratzel (https://github.com/rlratzel) URL: https://github.com/rapidsai/cugraph/pull/4613 --- python/nx-cugraph/_nx_cugraph/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/python/nx-cugraph/_nx_cugraph/__init__.py b/python/nx-cugraph/_nx_cugraph/__init__.py index f58a6e229..41c18c27e 100644 --- a/python/nx-cugraph/_nx_cugraph/__init__.py +++ b/python/nx-cugraph/_nx_cugraph/__init__.py @@ -298,6 +298,7 @@ def get_info(): def _check_networkx_version(): import warnings + import re import networkx as nx @@ -310,7 +311,11 @@ def _check_networkx_version(): UserWarning, stacklevel=2, ) - if len(version_minor) > 1: + + # Allow single-digit minor versions, e.g. 3.4 and release candidates, e.g. 3.4rc0 + pattern = r"^\d(rc\d+)?$" + + if not re.match(pattern, version_minor): raise RuntimeWarning( f"nx-cugraph version {__version__} does not work with networkx version " f"{nx.__version__}. Please upgrade (or fix) your Python environment." From 65e5403ef8764d110d4b7f96baddcb8659befff9 Mon Sep 17 00:00:00 2001 From: James Lamb Date: Mon, 26 Aug 2024 13:03:15 -0500 Subject: [PATCH 04/21] Drop Python 3.9 support (#4625) Contributes to https://github.com/rapidsai/build-planning/issues/88 Finishes the work of dropping Python 3.9 support. This project stopped building / testing against Python 3.9 as of https://github.com/rapidsai/shared-workflows/pull/235. This PR updates configuration and docs to reflect that. ## Notes for Reviewers ### How I tested this Checked that there were no remaining uses like this: ```shell git grep -E '3\.9' git grep '39' git grep 'py39' ``` And similar for variations on Python 3.8 (to catch things that were missed the last time this was done). Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Bradley Dice (https://github.com/bdice) - Brad Rees (https://github.com/BradReesWork) URL: https://github.com/rapidsai/cugraph/pull/4625 --- docs/cugraph/source/installation/getting_cugraph.md | 2 +- docs/cugraph/source/tutorials/basic_cugraph.md | 4 ++-- notebooks/demo/nx_cugraph_demo.ipynb | 2 +- python/nx-cugraph/README.md | 4 ++-- python/nx-cugraph/pyproject.toml | 7 +++---- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/cugraph/source/installation/getting_cugraph.md b/docs/cugraph/source/installation/getting_cugraph.md index 126325c09..ff89fffea 100644 --- a/docs/cugraph/source/installation/getting_cugraph.md +++ b/docs/cugraph/source/installation/getting_cugraph.md @@ -45,7 +45,7 @@ conda install -c rapidsai -c conda-forge -c nvidia cugraph cuda-version=12.0 Alternatively, use `cuda-version=11.8` for packages supporting CUDA 11. -Note: This conda installation only applies to Linux and Python versions 3.9/3.10/3.11. +Note: This conda installation only applies to Linux and Python versions 3.10/3.11.
diff --git a/docs/cugraph/source/tutorials/basic_cugraph.md b/docs/cugraph/source/tutorials/basic_cugraph.md index 783254724..6a9b4877b 100644 --- a/docs/cugraph/source/tutorials/basic_cugraph.md +++ b/docs/cugraph/source/tutorials/basic_cugraph.md @@ -4,8 +4,8 @@ CuGraph is part of [Rapids](https://docs.rapids.ai/user-guide) and has the following system requirements: * NVIDIA GPU, Volta architecture or later, with [compute capability](https://developer.nvidia.com/cuda-gpus) 7.0+ - * CUDA 11.2, 11.4, 11.5, 11.8, 12.0 or 12.2 - * Python version 3.9, 3.10, or 3.11 + * CUDA 11.2, 11.4, 11.5, 11.8, 12.0, 12.2, or 12.5 + * Python version 3.10 or 3.11 * NetworkX >= version 3.3 or newer in order to use use [NetworkX Configs](https://networkx.org/documentation/stable/reference/backends.html#module-networkx.utils.configs) **This is required for use of nx-cuGraph, [see below](#cugraph-using-networkx-code).** ## Installation diff --git a/notebooks/demo/nx_cugraph_demo.ipynb b/notebooks/demo/nx_cugraph_demo.ipynb index 6e50370ed..2e3c3c861 100644 --- a/notebooks/demo/nx_cugraph_demo.ipynb +++ b/notebooks/demo/nx_cugraph_demo.ipynb @@ -20,7 +20,7 @@ "Using `nx-cugraph` with this notebook requires the following: \n", "- NVIDIA GPU, Pascal architecture or later\n", "- CUDA 11.2, 11.4, 11.5, 11.8, or 12.0\n", - "- Python versions 3.9, 3.10, or 3.11\n", + "- Python versions 3.10 or 3.11\n", "- NetworkX >= version 3.2\n", " - _NetworkX 3.0 supports dispatching and is compatible with `nx-cugraph`, but this notebook will demonstrate features added in 3.2_\n", " - At the time of this writing, NetworkX 3.2 is only available from source and can be installed by following the [development version install instructions](https://github.com/networkx/networkx/blob/main/INSTALL.rst#install-the-development-version).\n", diff --git a/python/nx-cugraph/README.md b/python/nx-cugraph/README.md index 458421e2b..b18b99516 100644 --- a/python/nx-cugraph/README.md +++ b/python/nx-cugraph/README.md @@ -8,8 +8,8 @@ to run supported algorithms with GPU acceleration. nx-cugraph requires the following: * NVIDIA GPU, Volta architecture or later, with [compute capability](https://developer.nvidia.com/cuda-gpus) 7.0+ - * CUDA 11.2, 11.4, 11.5, 11.8, or 12.0 - * Python version 3.9, 3.10, or 3.11 + * CUDA 11.2, 11.4, 11.5, 11.8, 12.0, 12.2, or 12.5 + * Python version 3.10 or 3.11 * NetworkX >= version 3.0 (version 3.2 or higher recommended) More details about system requirements can be found in the [RAPIDS System Requirements documentation](https://docs.rapids.ai/install#system-req). diff --git a/python/nx-cugraph/pyproject.toml b/python/nx-cugraph/pyproject.toml index 847444f9d..6784e9607 100644 --- a/python/nx-cugraph/pyproject.toml +++ b/python/nx-cugraph/pyproject.toml @@ -18,13 +18,12 @@ authors = [ { name = "NVIDIA Corporation" }, ] license = { text = "Apache 2.0" } -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Development Status :: 4 - Beta", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3 :: Only", @@ -90,7 +89,7 @@ matrix-entry = "cuda_suffixed=true" [tool.black] line-length = 88 -target-version = ["py39", "py310", "py311"] +target-version = ["py310", "py311"] [tool.isort] sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] @@ -156,7 +155,7 @@ exclude_lines = [ [tool.ruff] # https://github.com/charliermarsh/ruff/ line-length = 88 -target-version = "py39" +target-version = "py310" [tool.ruff.lint] unfixable = [ "F841", # unused-variable (Note: can leave useless expression) From 73bb63fe46c841410f0a293162bb128c66e226b8 Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Wed, 28 Aug 2024 12:14:37 -0400 Subject: [PATCH 05/21] Update rapidsai/pre-commit-hooks (#4633) This PR updates rapidsai/pre-commit-hooks to the version 0.4.0. Authors: - Kyle Edwards (https://github.com/KyleFromNVIDIA) Approvers: - James Lamb (https://github.com/jameslamb) URL: https://github.com/rapidsai/cugraph/pull/4633 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b5b16e3e..6f631e40c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: types_or: [c, c++, cuda] args: ["-fallback-style=none", "-style=file", "-i"] - repo: https://github.com/rapidsai/pre-commit-hooks - rev: v0.3.1 + rev: v0.4.0 hooks: - id: verify-copyright files: | From b8462fc27156784c8816de40b9c275d1e7af3056 Mon Sep 17 00:00:00 2001 From: James Lamb Date: Thu, 29 Aug 2024 20:32:15 -0500 Subject: [PATCH 06/21] update a few more Python references to Python 3.10 (#4637) Contributes to https://github.com/rapidsai/build-planning/issues/88. #4625 dropped Python 3.9 support here (as part of the RAPIDS-wide effort to drop Python 3.9 in this release). This PR fixes a few things that were missed in that PR. Authors: - James Lamb (https://github.com/jameslamb) - https://github.com/jakirkham Approvers: - Don Acosta (https://github.com/acostadon) - https://github.com/jakirkham - Bradley Dice (https://github.com/bdice) - Mike Sarahan (https://github.com/msarahan) - Alex Barghi (https://github.com/alexbarghi-nv) - Rick Ratzel (https://github.com/rlratzel) URL: https://github.com/rapidsai/cugraph/pull/4637 --- .pre-commit-config.yaml | 2 +- docs/cugraph/source/installation/source_build.md | 5 ++--- docs/cugraph/source/tutorials/cugraph_notebooks.md | 5 ++--- docs/cugraph/source/wholegraph/installation/source_build.md | 5 ++--- python/nx-cugraph/lint.yaml | 2 +- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f631e40c..5b351478f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: black language_version: python3 - args: [--target-version=py39] + args: [--target-version=py310] files: ^(python/.*|benchmarks/.*)$ exclude: ^python/nx-cugraph/ - repo: https://github.com/PyCQA/flake8 diff --git a/docs/cugraph/source/installation/source_build.md b/docs/cugraph/source/installation/source_build.md index 89e63bade..80f2d97d4 100644 --- a/docs/cugraph/source/installation/source_build.md +++ b/docs/cugraph/source/installation/source_build.md @@ -12,8 +12,7 @@ __Compilers:__ * `nvcc` version 11.5+ __CUDA:__ -* CUDA 11.2+ -* NVIDIA driver 470.42.01 or newer +* CUDA 11.8+ * NVIDIA GPU, Volta architecture or later, with [compute capability](https://developer.nvidia.com/cuda-gpus) 7.0+ Further details and download links for these prerequisites are available on the @@ -178,7 +177,7 @@ Run either the C++ or the Python tests with datasets make test ``` -Note: This conda installation only applies to Linux and Python versions 3.8/3.11. +Note: This conda installation only applies to Linux and Python versions 3.10 and 3.11. ### (OPTIONAL) Set environment variable on activation diff --git a/docs/cugraph/source/tutorials/cugraph_notebooks.md b/docs/cugraph/source/tutorials/cugraph_notebooks.md index 559ba36e9..6d7840dc3 100644 --- a/docs/cugraph/source/tutorials/cugraph_notebooks.md +++ b/docs/cugraph/source/tutorials/cugraph_notebooks.md @@ -55,10 +55,9 @@ 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.8+ -* A system with an NVIDIA GPU: Pascal architecture or better +* Python 3.10+ +* A system with an NVIDIA GPU: Volta architecture or newer * CUDA 11.4+ -* NVIDIA driver 450.51+ ## Copyright diff --git a/docs/cugraph/source/wholegraph/installation/source_build.md b/docs/cugraph/source/wholegraph/installation/source_build.md index a7727ac40..33d7c98b2 100644 --- a/docs/cugraph/source/wholegraph/installation/source_build.md +++ b/docs/cugraph/source/wholegraph/installation/source_build.md @@ -16,8 +16,7 @@ __Compiler__: __CUDA__: * CUDA 11.8+ -* NVIDIA driver 450.80.02+ -* Pascal architecture or better +* Volta architecture or better You can obtain CUDA from [https://developer.nvidia.com/cuda-downloads](https://developer.nvidia.com/cuda-downloads). @@ -177,7 +176,7 @@ Run either the C++ or the Python tests with datasets ``` -Note: This conda installation only applies to Linux and Python versions 3.8/3.10. +Note: This conda installation only applies to Linux and Python versions 3.10 and 3.11. ## Creating documentation diff --git a/python/nx-cugraph/lint.yaml b/python/nx-cugraph/lint.yaml index ce46360e2..b2184a185 100644 --- a/python/nx-cugraph/lint.yaml +++ b/python/nx-cugraph/lint.yaml @@ -43,7 +43,7 @@ repos: rev: v3.16.0 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-plus] - repo: https://github.com/psf/black rev: 24.4.2 hooks: From 202b90ca489188ed8803f0fe9a81a0b681ecbde3 Mon Sep 17 00:00:00 2001 From: Ralph Liu <137829296+nv-rliu@users.noreply.github.com> Date: Fri, 30 Aug 2024 20:45:08 -0400 Subject: [PATCH 07/21] Add `nx-cugraph` Benchmarking Scripts (#4616) Closes https://github.com/rapidsai/graph_dl/issues/596 This PR adds scripts written by @rlratzel to generate benchmarking numbers for `nx-cugraph` Authors: - Ralph Liu (https://github.com/nv-rliu) - Rick Ratzel (https://github.com/rlratzel) Approvers: - Don Acosta (https://github.com/acostadon) - Rick Ratzel (https://github.com/rlratzel) URL: https://github.com/rapidsai/cugraph/pull/4616 --- benchmarks/nx-cugraph/pytest-based/README.md | 45 +++ .../nx-cugraph/pytest-based/bench_algos.py | 31 +- .../create_results_summary_page.py | 291 ++++++++++++++++++ .../pytest-based/get_graph_bench_dataset.py | 35 +++ .../pytest-based/run-main-benchmarks.sh | 58 ++++ 5 files changed, 458 insertions(+), 2 deletions(-) create mode 100644 benchmarks/nx-cugraph/pytest-based/README.md create mode 100644 benchmarks/nx-cugraph/pytest-based/create_results_summary_page.py create mode 100644 benchmarks/nx-cugraph/pytest-based/get_graph_bench_dataset.py create mode 100755 benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh diff --git a/benchmarks/nx-cugraph/pytest-based/README.md b/benchmarks/nx-cugraph/pytest-based/README.md new file mode 100644 index 000000000..4ea0f127a --- /dev/null +++ b/benchmarks/nx-cugraph/pytest-based/README.md @@ -0,0 +1,45 @@ +## `nx-cugraph` Benchmarks + +### Overview + +This directory contains a set of scripts designed to benchmark NetworkX with the `nx-cugraph` backend and deliver a report that summarizes the speed-up and runtime deltas over default NetworkX. + +Our current benchmarks provide the following datasets: + +| Dataset | Nodes | Edges | Directed | +| -------- | ------- | ------- | ------- | +| netscience | 1,461 | 5,484 | Yes | +| email-Eu-core | 1,005 | 25,571 | Yes | +| cit-Patents | 3,774,768 | 16,518,948 | Yes | +| hollywood | 1,139,905 | 57,515,616 | No | +| soc-LiveJournal1 | 4,847,571 | 68,993,773 | Yes | + + + +### Scripts + +#### 1. `run-main-benchmarks.sh` +This script allows users to run selected algorithms across multiple datasets and backends. All results are stored inside a sub-directory (`logs/`) and output files are named based on the combination of parameters for that benchmark. + +NOTE: If running with all algorithms, datasets, and backends, this script may take a few hours to finish running. + +**Usage:** + ```bash + bash run-main-benchmarks.sh # edit this script directly + ``` + +#### 2. `get_graph_bench_dataset.py` +This script downloads the specified dataset using `cugraph.datasets`. + +**Usage:** + ```bash + python get_graph_bench_dataset.py [dataset] + ``` + +#### 3. `create_results_summary_page.py` +This script is designed to be run after `run-gap-benchmarks.sh` in order to generate an HTML page displaying a results table comparing default NetworkX to nx-cugraph. The script also provides information about the current system. + +**Usage:** + ```bash + python create_results_summary_page.py > report.html + ``` diff --git a/benchmarks/nx-cugraph/pytest-based/bench_algos.py b/benchmarks/nx-cugraph/pytest-based/bench_algos.py index d40b51308..f88d93c3f 100644 --- a/benchmarks/nx-cugraph/pytest-based/bench_algos.py +++ b/benchmarks/nx-cugraph/pytest-based/bench_algos.py @@ -271,9 +271,8 @@ def bench_from_networkx(benchmark, graph_obj): # normalized_param_values = [True, False] -# k_param_values = [10, 100] normalized_param_values = [True] -k_param_values = [10] +k_param_values = [10, 100, 1000] @pytest.mark.parametrize( @@ -282,6 +281,10 @@ def bench_from_networkx(benchmark, graph_obj): @pytest.mark.parametrize("k", k_param_values, ids=lambda k: f"{k=}") def bench_betweenness_centrality(benchmark, graph_obj, backend_wrapper, normalized, k): G = get_graph_obj_for_benchmark(graph_obj, backend_wrapper) + + if k > G.number_of_nodes(): + pytest.skip(reason=f"{k=} > {G.number_of_nodes()=}") + result = benchmark.pedantic( target=backend_wrapper(nx.betweenness_centrality), args=(G,), @@ -305,6 +308,10 @@ def bench_edge_betweenness_centrality( benchmark, graph_obj, backend_wrapper, normalized, k ): G = get_graph_obj_for_benchmark(graph_obj, backend_wrapper) + + if k > G.number_of_nodes(): + pytest.skip(reason=f"{k=} > {G.number_of_nodes()=}") + result = benchmark.pedantic( target=backend_wrapper(nx.edge_betweenness_centrality), args=(G,), @@ -473,6 +480,26 @@ def bench_pagerank_personalized(benchmark, graph_obj, backend_wrapper): assert type(result) is dict +def bench_shortest_path(benchmark, graph_obj, backend_wrapper): + """ + This passes in the source node with the highest degree, but no target. + """ + G = get_graph_obj_for_benchmark(graph_obj, backend_wrapper) + node = get_highest_degree_node(graph_obj) + + result = benchmark.pedantic( + target=backend_wrapper(nx.shortest_path), + args=(G,), + kwargs=dict( + source=node, + ), + rounds=rounds, + iterations=iterations, + warmup_rounds=warmup_rounds, + ) + assert type(result) is dict + + def bench_single_source_shortest_path_length(benchmark, graph_obj, backend_wrapper): G = get_graph_obj_for_benchmark(graph_obj, backend_wrapper) node = get_highest_degree_node(graph_obj) diff --git a/benchmarks/nx-cugraph/pytest-based/create_results_summary_page.py b/benchmarks/nx-cugraph/pytest-based/create_results_summary_page.py new file mode 100644 index 000000000..f1cc4b06c --- /dev/null +++ b/benchmarks/nx-cugraph/pytest-based/create_results_summary_page.py @@ -0,0 +1,291 @@ +# Copyright (c) 2024, 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 re +import pathlib +import json +import platform +import psutil +import socket +import subprocess + + +def get_formatted_time_value(time): + res = "" + if time < 1: + if time < 0.001: + units = "us" + time *= 1e6 + else: + units = "ms" + time *= 1e3 + else: + units = "s" + return f"{time:.3f}{units}" + + +def get_all_benchmark_info(): + benchmarks = {} + # Populate benchmarks dir from .json files + for json_file in logs_dir.glob("*.json"): + try: + data = json.loads(open(json_file).read()) + except json.decoder.JSONDecodeError: + continue + + for benchmark_run in data["benchmarks"]: + # example name: "bench_triangles[ds=netscience-backend=cugraph-preconverted]" + name = benchmark_run["name"] + + algo_name = name.split("[")[0] + if algo_name.startswith("bench_"): + algo_name = algo_name[6:] + # special case for betweenness_centrality + match = k_patt.match(name) + if match is not None: + algo_name += f", k={match.group(1)}" + + match = dataset_patt.match(name) + if match is None: + raise RuntimeError( + f"benchmark name {name} in file {json_file} has an unexpected format" + ) + dataset = match.group(1) + if dataset.endswith("-backend"): + dataset = dataset[:-8] + + match = backend_patt.match(name) + if match is None: + raise RuntimeError( + f"benchmark name {name} in file {json_file} has an unexpected format" + ) + backend = match.group(1) + if backend == "None": + backend = "networkx" + + runtime = benchmark_run["stats"]["mean"] + benchmarks.setdefault(algo_name, {}).setdefault(backend, {})[ + dataset + ] = runtime + return benchmarks + + +def compute_perf_vals(cugraph_runtime, networkx_runtime): + speedup_string = f"{networkx_runtime / cugraph_runtime:.3f}X" + delta = networkx_runtime - cugraph_runtime + if abs(delta) < 1: + if abs(delta) < 0.001: + units = "us" + delta *= 1e6 + else: + units = "ms" + delta *= 1e3 + else: + units = "s" + delta_string = f"{delta:.3f}{units}" + + return (speedup_string, delta_string) + + +def get_mem_info(): + return round(psutil.virtual_memory().total / (1024**3), 2) + + +def get_cuda_version(): + output = subprocess.check_output("nvidia-smi", shell=True).decode() + try: + return next( + line.split("CUDA Version: ")[1].split()[0] + for line in output.splitlines() + if "CUDA Version" in line + ) + except subprocess.CalledProcessError: + return "Failed to get CUDA version." + + +def get_first_gpu_info(): + try: + gpu_info = ( + subprocess.check_output( + "nvidia-smi --query-gpu=name,memory.total,memory.free,memory.used --format=csv,noheader", + shell=True, + ) + .decode() + .strip() + ) + if gpu_info: + gpus = gpu_info.split("\n") + num_gpus = len(gpus) + first_gpu = gpus[0] # Get the information for the first GPU + gpu_name, mem_total, _, _ = first_gpu.split(",") + return f"{num_gpus} x {gpu_name.strip()} ({round(int(mem_total.strip().split()[0]) / (1024), 2)} GB)" + else: + print("No GPU found or unable to query GPU details.") + except subprocess.CalledProcessError: + print("Failed to execute nvidia-smi. No GPU information available.") + + +def get_system_info(): + print('
') + print(f"

Hostname: {socket.gethostname()}

") + print( + f'

Operating System: {platform.system()} {platform.release()}

' + ) + print(f'

Kernel Version : {platform.version()}

') + with open("/proc/cpuinfo") as f: + print( + f'

CPU: {next(line.strip().split(": ")[1] for line in f if "model name" in line)} ({psutil.cpu_count(logical=False)} cores)

' + ) + print(f'

Memory: {get_mem_info()} GB

') + print(f"

GPU: {get_first_gpu_info()}

") + print(f"

CUDA Version: {get_cuda_version()}

") + + +if __name__ == "__main__": + logs_dir = pathlib.Path("logs") + + dataset_patt = re.compile(".*ds=([\w-]+).*") + backend_patt = re.compile(".*backend=(\w+).*") + k_patt = re.compile(".*k=(10*).*") + + # Organize all benchmark runs by the following hierarchy: algo -> backend -> dataset + benchmarks = get_all_benchmark_info() + + # dump HTML table + ordered_datasets = [ + "netscience", + "email_Eu_core", + "cit-patents", + "hollywood", + "soc-livejournal1", + ] + # dataset, # Node, # Edge, Directed info + dataset_meta = { + "netscience": ["1,461", "5,484", "Yes"], + "email_Eu_core": ["1,005", "25,571", "Yes"], + "cit-patents": ["3,774,768", "16,518,948", "Yes"], + "hollywood": ["1,139,905", "57,515,616", "No"], + "soc-livejournal1": ["4,847,571", "68,993,773", "Yes"], + } + + print( + """ + + + + + + + + """ + ) + for ds in ordered_datasets: + print( + f" " + ) + print( + """ + + + """ + ) + for algo_name in sorted(benchmarks): + algo_runs = benchmarks[algo_name] + print(" ") + print(f" ") + # Proceed only if any results are present for both cugraph and NX + if "cugraph" in algo_runs and "networkx" in algo_runs: + cugraph_algo_runs = algo_runs["cugraph"] + networkx_algo_runs = algo_runs["networkx"] + datasets_in_both = set(cugraph_algo_runs).intersection(networkx_algo_runs) + + # populate the table with speedup results for each dataset in the order + # specified in ordered_datasets. If results for a run using a dataset + # are not present for both cugraph and NX, output an empty cell. + for dataset in ordered_datasets: + if dataset in datasets_in_both: + cugraph_runtime = cugraph_algo_runs[dataset] + networkx_runtime = networkx_algo_runs[dataset] + (speedup, runtime_delta) = compute_perf_vals( + cugraph_runtime=cugraph_runtime, + networkx_runtime=networkx_runtime, + ) + nx_formatted = get_formatted_time_value(networkx_runtime) + cg_formatted = get_formatted_time_value(cugraph_runtime) + print( + f" " + ) + else: + print(f" ") + + # If a comparison between cugraph and NX cannot be made, output empty cells + # for each dataset + else: + for _ in range(len(ordered_datasets)): + print(" ") + print(" ") + print( + """ + \n
Dataset
Nodes
Edges
Directed
{ds}
{dataset_meta[ds][0]}
{dataset_meta[ds][1]}
{dataset_meta[ds][2]}
{algo_name}{nx_formatted} / {cg_formatted}
{speedup}
{runtime_delta}
+ \n
\n""") diff --git a/benchmarks/nx-cugraph/pytest-based/get_graph_bench_dataset.py b/benchmarks/nx-cugraph/pytest-based/get_graph_bench_dataset.py new file mode 100644 index 000000000..5a0a15da8 --- /dev/null +++ b/benchmarks/nx-cugraph/pytest-based/get_graph_bench_dataset.py @@ -0,0 +1,35 @@ +# Copyright (c) 2024, 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. + +""" +Checks if a particular dataset has been downloaded inside the datasets dir +(RAPIDS_DATAEST_ROOT_DIR). If not, the file will be downloaded using the +datasets API. + +Positional Arguments: + 1) dataset name (e.g. 'email_Eu_core', 'cit-patents') + available datasets can be found here: `python/cugraph/cugraph/datasets/__init__.py` +""" + +import sys + +import cugraph.datasets as cgds + + +if __name__ == "__main__": + # download and store dataset (csv) by using the Datasets API + dataset = sys.argv[1].replace("-", "_") + dataset_obj = getattr(cgds, dataset) + + if not dataset_obj.get_path().exists(): + dataset_obj.get_edgelist(download=True) diff --git a/benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh b/benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh new file mode 100755 index 000000000..1a81fe4b8 --- /dev/null +++ b/benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# Copyright (c) 2024, 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. + + +# location to store datasets used for benchmarking +export RAPIDS_DATASET_ROOT_DIR=/datasets/cugraph +mkdir -p logs + +# list of algos, datasets, and back-ends to use in combinations +algos=" + pagerank + betweenness_centrality + louvain + shortest_path + weakly_connected_components + triangles + bfs_predecessors +" +datasets=" + netscience + email_Eu_core + cit_patents + hollywood + soc-livejournal +" +# None backend is default networkx +# cugraph-preconvert backend is nx-cugraph +backends=" + None + cugraph-preconverted +" + +for algo in $algos; do + for dataset in $datasets; do + python get_graph_bench_dataset.py $dataset + for backend in $backends; do + name="${backend}__${algo}__${dataset}" + echo "Running: $backend, $dataset, bench_$algo" + # command to preproduce test + # echo "RUNNING: \"pytest -sv -k \"$backend and $dataset and bench_$algo and not 1000\" --benchmark-json=\"logs/${name}.json\" bench_algos.py" + pytest -sv \ + -k "$backend and $dataset and bench_$algo and not 1000" \ + --benchmark-json="logs/${name}.json" \ + bench_algos.py 2>&1 | tee "logs/${name}.out" + done + done +done From 6e7f999a34cceb7436687c55cfae76a652560e52 Mon Sep 17 00:00:00 2001 From: James Lamb Date: Wed, 11 Sep 2024 11:28:24 -0500 Subject: [PATCH 08/21] Add support for Python 3.12 (#4647) Contributes to https://github.com/rapidsai/build-planning/issues/40 This PR adds support for Python 3.12. ## Notes for Reviewers This is part of ongoing work to add Python 3.12 support across RAPIDS. It temporarily introduces a build/test matrix including Python 3.12, from https://github.com/rapidsai/shared-workflows/pull/213. A follow-up PR will revert back to pointing at the `branch-24.10` branch of `shared-workflows` once all RAPIDS repos have added Python 3.12 support. ### This will fail until all dependencies have been updates to Python 3.12 CI here is expected to fail until all of this project's upstream dependencies support Python 3.12. This can be merged whenever all CI jobs are passing. Authors: - James Lamb (https://github.com/jameslamb) Approvers: - Bradley Dice (https://github.com/bdice) - Rick Ratzel (https://github.com/rlratzel) URL: https://github.com/rapidsai/cugraph/pull/4647 --- docs/cugraph/source/installation/getting_cugraph.md | 2 +- docs/cugraph/source/installation/source_build.md | 2 +- docs/cugraph/source/tutorials/basic_cugraph.md | 2 +- docs/cugraph/source/wholegraph/installation/source_build.md | 2 +- notebooks/demo/nx_cugraph_demo.ipynb | 2 +- python/nx-cugraph/README.md | 2 +- python/nx-cugraph/pyproject.toml | 3 ++- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/cugraph/source/installation/getting_cugraph.md b/docs/cugraph/source/installation/getting_cugraph.md index ff89fffea..41ec9a67e 100644 --- a/docs/cugraph/source/installation/getting_cugraph.md +++ b/docs/cugraph/source/installation/getting_cugraph.md @@ -45,7 +45,7 @@ conda install -c rapidsai -c conda-forge -c nvidia cugraph cuda-version=12.0 Alternatively, use `cuda-version=11.8` for packages supporting CUDA 11. -Note: This conda installation only applies to Linux and Python versions 3.10/3.11. +Note: This conda installation only applies to Linux and Python versions 3.10/3.11/3.12.
diff --git a/docs/cugraph/source/installation/source_build.md b/docs/cugraph/source/installation/source_build.md index 80f2d97d4..243a62e5c 100644 --- a/docs/cugraph/source/installation/source_build.md +++ b/docs/cugraph/source/installation/source_build.md @@ -177,7 +177,7 @@ Run either the C++ or the Python tests with datasets make test ``` -Note: This conda installation only applies to Linux and Python versions 3.10 and 3.11. +Note: This conda installation only applies to Linux and Python versions 3.10, 3.11, and 3.12. ### (OPTIONAL) Set environment variable on activation diff --git a/docs/cugraph/source/tutorials/basic_cugraph.md b/docs/cugraph/source/tutorials/basic_cugraph.md index 6a9b4877b..a0c9ad576 100644 --- a/docs/cugraph/source/tutorials/basic_cugraph.md +++ b/docs/cugraph/source/tutorials/basic_cugraph.md @@ -5,7 +5,7 @@ CuGraph is part of [Rapids](https://docs.rapids.ai/user-guide) and has the following system requirements: * NVIDIA GPU, Volta architecture or later, with [compute capability](https://developer.nvidia.com/cuda-gpus) 7.0+ * CUDA 11.2, 11.4, 11.5, 11.8, 12.0, 12.2, or 12.5 - * Python version 3.10 or 3.11 + * Python version 3.10, 3.11, or 3.12 * NetworkX >= version 3.3 or newer in order to use use [NetworkX Configs](https://networkx.org/documentation/stable/reference/backends.html#module-networkx.utils.configs) **This is required for use of nx-cuGraph, [see below](#cugraph-using-networkx-code).** ## Installation diff --git a/docs/cugraph/source/wholegraph/installation/source_build.md b/docs/cugraph/source/wholegraph/installation/source_build.md index 33d7c98b2..7213cbfb0 100644 --- a/docs/cugraph/source/wholegraph/installation/source_build.md +++ b/docs/cugraph/source/wholegraph/installation/source_build.md @@ -176,7 +176,7 @@ Run either the C++ or the Python tests with datasets ``` -Note: This conda installation only applies to Linux and Python versions 3.10 and 3.11. +Note: This conda installation only applies to Linux and Python versions 3.10, 3.11, and 3.12. ## Creating documentation diff --git a/notebooks/demo/nx_cugraph_demo.ipynb b/notebooks/demo/nx_cugraph_demo.ipynb index 2e3c3c861..f1ce80aa1 100644 --- a/notebooks/demo/nx_cugraph_demo.ipynb +++ b/notebooks/demo/nx_cugraph_demo.ipynb @@ -20,7 +20,7 @@ "Using `nx-cugraph` with this notebook requires the following: \n", "- NVIDIA GPU, Pascal architecture or later\n", "- CUDA 11.2, 11.4, 11.5, 11.8, or 12.0\n", - "- Python versions 3.10 or 3.11\n", + "- Python versions 3.10, 3.11, or 3.12\n", "- NetworkX >= version 3.2\n", " - _NetworkX 3.0 supports dispatching and is compatible with `nx-cugraph`, but this notebook will demonstrate features added in 3.2_\n", " - At the time of this writing, NetworkX 3.2 is only available from source and can be installed by following the [development version install instructions](https://github.com/networkx/networkx/blob/main/INSTALL.rst#install-the-development-version).\n", diff --git a/python/nx-cugraph/README.md b/python/nx-cugraph/README.md index b18b99516..c3ca0b880 100644 --- a/python/nx-cugraph/README.md +++ b/python/nx-cugraph/README.md @@ -9,7 +9,7 @@ to run supported algorithms with GPU acceleration. nx-cugraph requires the following: * NVIDIA GPU, Volta architecture or later, with [compute capability](https://developer.nvidia.com/cuda-gpus) 7.0+ * CUDA 11.2, 11.4, 11.5, 11.8, 12.0, 12.2, or 12.5 - * Python version 3.10 or 3.11 + * Python version 3.10, 3.11, or 3.12 * NetworkX >= version 3.0 (version 3.2 or higher recommended) More details about system requirements can be found in the [RAPIDS System Requirements documentation](https://docs.rapids.ai/install#system-req). diff --git a/python/nx-cugraph/pyproject.toml b/python/nx-cugraph/pyproject.toml index 6784e9607..e7b4ea44d 100644 --- a/python/nx-cugraph/pyproject.toml +++ b/python/nx-cugraph/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", @@ -89,7 +90,7 @@ matrix-entry = "cuda_suffixed=true" [tool.black] line-length = 88 -target-version = ["py310", "py311"] +target-version = ["py310", "py311", "py312"] [tool.isort] sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] From 107c88afd04e978d32066f26ca15684c39c158c5 Mon Sep 17 00:00:00 2001 From: Ralph Liu <137829296+nv-rliu@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:01:15 -0400 Subject: [PATCH 09/21] Add `--cpu-only` or `--gpu-only` Arguments to `nx-cugraph` Benchmark (#4648) This PR is a follow-up to https://github.com/rapidsai/cugraph/pull/4616 This PR adds the following CLI argument options for `benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh` - `--cpu-only` - `--gpu-only` And adds minor updates to the README Authors: - Ralph Liu (https://github.com/nv-rliu) Approvers: - Rick Ratzel (https://github.com/rlratzel) URL: https://github.com/rapidsai/cugraph/pull/4648 --- benchmarks/nx-cugraph/pytest-based/README.md | 17 +++++++++++++---- .../pytest-based/run-main-benchmarks.sh | 18 +++++++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/benchmarks/nx-cugraph/pytest-based/README.md b/benchmarks/nx-cugraph/pytest-based/README.md index 4ea0f127a..781550fa5 100644 --- a/benchmarks/nx-cugraph/pytest-based/README.md +++ b/benchmarks/nx-cugraph/pytest-based/README.md @@ -19,13 +19,22 @@ Our current benchmarks provide the following datasets: ### Scripts #### 1. `run-main-benchmarks.sh` -This script allows users to run selected algorithms across multiple datasets and backends. All results are stored inside a sub-directory (`logs/`) and output files are named based on the combination of parameters for that benchmark. +This script allows users to run a small set of commonly-used algorithms across multiple datasets and backends. All results are stored inside a sub-directory (`logs/`) and output files are named based on the combination of parameters for that benchmark. -NOTE: If running with all algorithms, datasets, and backends, this script may take a few hours to finish running. +NOTE: If running with all algorithms and datasets using NetworkX without an accelerated backend, this script may take a few hours to finish running. **Usage:** + - Run with `--cpu-only`: ```bash - bash run-main-benchmarks.sh # edit this script directly + ./run-main-benchmarks.sh --cpu-only + ``` + - Run with `--gpu-only`: + ```bash + ./run-main-benchmarks.sh --gpu-only + ``` + - Run without any arguments (all backends): + ```bash + ./run-main-benchmarks.sh ``` #### 2. `get_graph_bench_dataset.py` @@ -37,7 +46,7 @@ This script downloads the specified dataset using `cugraph.datasets`. ``` #### 3. `create_results_summary_page.py` -This script is designed to be run after `run-gap-benchmarks.sh` in order to generate an HTML page displaying a results table comparing default NetworkX to nx-cugraph. The script also provides information about the current system. +This script is designed to be run after `run-gap-benchmarks.sh` in order to generate an HTML page displaying a results table comparing default NetworkX to nx-cugraph. The script also provides information about the current system, so it should be run on the machine on which benchmarks were run. **Usage:** ```bash diff --git a/benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh b/benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh index 1a81fe4b8..a1d32474e 100755 --- a/benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh +++ b/benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh @@ -40,10 +40,26 @@ backends=" None cugraph-preconverted " +# check for --cpu-only or --gpu-only args +if [[ "$#" -eq 1 ]]; then + case $1 in + --cpu-only) + backends="None" + ;; + --gpu-only) + backends="cugraph-preconverted" + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +fi for algo in $algos; do for dataset in $datasets; do - python get_graph_bench_dataset.py $dataset + # this script can be used to download benchmarking datasets by name via cugraph.datasets + python get_graph_bench_dataset.py $dataset for backend in $backends; do name="${backend}__${algo}__${dataset}" echo "Running: $backend, $dataset, bench_$algo" From 236044697ed1814c660d41c9a8843c68558a8553 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Wed, 18 Sep 2024 05:12:05 -0500 Subject: [PATCH 10/21] Update flake8 to 7.1.1. (#4652) We need to update flake8 to fix a false-positive that appears with older flake8 versions on Python 3.12. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - James Lamb (https://github.com/jameslamb) - Alex Barghi (https://github.com/alexbarghi-nv) - Kyle Edwards (https://github.com/KyleFromNVIDIA) URL: https://github.com/rapidsai/cugraph/pull/4652 --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5b351478f..8ff284210 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: files: ^(python/.*|benchmarks/.*)$ exclude: ^python/nx-cugraph/ - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 7.1.1 hooks: - id: flake8 args: ["--config=.flake8"] @@ -34,7 +34,7 @@ repos: hooks: - id: yesqa additional_dependencies: - - flake8==6.0.0 + - flake8==7.1.1 - repo: https://github.com/pre-commit/mirrors-clang-format rev: v16.0.6 hooks: From 768b789c1fc6b14e0f049d127a04a5e525890f11 Mon Sep 17 00:00:00 2001 From: Mike McCarty Date: Thu, 19 Sep 2024 01:43:32 -0400 Subject: [PATCH 11/21] Recommending `miniforge` for conda install (#4650) Recommending `miniforge` for conda install in installation docs. Authors: - Mike McCarty (https://github.com/mmccarty) - Bradley Dice (https://github.com/bdice) - Alex Barghi (https://github.com/alexbarghi-nv) Approvers: - Rick Ratzel (https://github.com/rlratzel) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cugraph/pull/4650 --- .../source/installation/getting_cugraph.md | 2 +- .../installation/getting_wholegraph.md | 2 +- notebooks/demo/mg_pagerank.ipynb | 280 +++++++++--------- 3 files changed, 142 insertions(+), 142 deletions(-) diff --git a/docs/cugraph/source/installation/getting_cugraph.md b/docs/cugraph/source/installation/getting_cugraph.md index 41ec9a67e..01bc9e379 100644 --- a/docs/cugraph/source/installation/getting_cugraph.md +++ b/docs/cugraph/source/installation/getting_cugraph.md @@ -21,7 +21,7 @@ The RAPIDS Docker containers contain all RAPIDS packages, including all from cuG ## Conda -It is easy to install cuGraph using conda. You can get a minimal conda installation with [Miniconda](https://conda.io/miniconda.html) or get the full installation with [Anaconda](https://www.anaconda.com/download). +It is easy to install cuGraph using conda. You can get a minimal conda installation with [miniforge](https://github.com/conda-forge/miniforge). cuGraph Conda packages * cugraph - this will also import: diff --git a/docs/cugraph/source/wholegraph/installation/getting_wholegraph.md b/docs/cugraph/source/wholegraph/installation/getting_wholegraph.md index 57314dcd4..80c666d65 100644 --- a/docs/cugraph/source/wholegraph/installation/getting_wholegraph.md +++ b/docs/cugraph/source/wholegraph/installation/getting_wholegraph.md @@ -21,7 +21,7 @@ The RAPIDS Docker containers (as of Release 23.10) contain all RAPIDS packages, ## Conda -It is easy to install WholeGraph using conda. You can get a minimal conda installation with [Miniconda](https://conda.io/miniconda.html) or get the full installation with [Anaconda](https://www.anaconda.com/download). +It is easy to install WholeGraph using conda. You can get a minimal conda installation with [miniforge](https://github.com/conda-forge/miniforge). WholeGraph conda packages * libwholegraph diff --git a/notebooks/demo/mg_pagerank.ipynb b/notebooks/demo/mg_pagerank.ipynb index bb3330484..e3314f80b 100644 --- a/notebooks/demo/mg_pagerank.ipynb +++ b/notebooks/demo/mg_pagerank.ipynb @@ -219,250 +219,250 @@ "text": [ "2023-05-12 09:25:01,974 - distributed.sizeof - WARNING - Sizeof calculation failed. Defaulting to 0.95 MiB\n", "Traceback (most recent call last):\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/sizeof.py\", line 17, in safe_sizeof\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/sizeof.py\", line 17, in safe_sizeof\n", " return sizeof(obj)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask/utils.py\", line 642, in __call__\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask/utils.py\", line 642, in __call__\n", " return meth(arg, *args, **kwargs)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask_cudf/backends.py\", line 430, in sizeof_cudf_dataframe\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask_cudf/backends.py\", line 430, in sizeof_cudf_dataframe\n", " + df._index.memory_usage()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 1594, in memory_usage\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 1594, in memory_usage\n", " if self.levels:\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 605, in levels\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 605, in levels\n", " self._compute_levels_and_codes()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 748, in _compute_levels_and_codes\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 748, in _compute_levels_and_codes\n", " code, cats = cudf.Series._from_data({None: col}).factorize()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/single_column_frame.py\", line 311, in factorize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/single_column_frame.py\", line 311, in factorize\n", " return cudf.core.algorithms.factorize(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/algorithms.py\", line 138, in factorize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/algorithms.py\", line 138, in factorize\n", " labels = values._column._label_encoding(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1385, in _label_encoding\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1385, in _label_encoding\n", " order = order.take(left_gather_map, check_bounds=False).argsort()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1101, in argsort\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1101, in argsort\n", " return self.as_frame()._get_sorted_inds(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 1572, in _get_sorted_inds\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 1572, in _get_sorted_inds\n", " return libcudf.sort.order_by(to_sort, ascending, na_position)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", " File \"sort.pyx\", line 141, in cudf._lib.sort.order_by\n", - "MemoryError: std::bad_alloc: out_of_memory: CUDA error at: /home/dacosta/miniconda3/envs/cugraph_0411/include/rmm/mr/device/cuda_memory_resource.hpp\n", + "MemoryError: std::bad_alloc: out_of_memory: CUDA error at: /home/dacosta/miniforge/envs/cugraph_0411/include/rmm/mr/device/cuda_memory_resource.hpp\n", "2023-05-12 09:25:01,976 - distributed.sizeof - WARNING - Sizeof calculation failed. Defaulting to 0.95 MiB\n", "Traceback (most recent call last):\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/sizeof.py\", line 17, in safe_sizeof\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/sizeof.py\", line 17, in safe_sizeof\n", " return sizeof(obj)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask/utils.py\", line 642, in __call__\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask/utils.py\", line 642, in __call__\n", " return meth(arg, *args, **kwargs)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask_cudf/backends.py\", line 430, in sizeof_cudf_dataframe\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask_cudf/backends.py\", line 430, in sizeof_cudf_dataframe\n", " + df._index.memory_usage()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 1594, in memory_usage\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 1594, in memory_usage\n", " if self.levels:\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 605, in levels\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 605, in levels\n", " self._compute_levels_and_codes()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 748, in _compute_levels_and_codes\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 748, in _compute_levels_and_codes\n", " code, cats = cudf.Series._from_data({None: col}).factorize()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/single_column_frame.py\", line 311, in factorize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/single_column_frame.py\", line 311, in factorize\n", " return cudf.core.algorithms.factorize(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/algorithms.py\", line 138, in factorize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/algorithms.py\", line 138, in factorize\n", " labels = values._column._label_encoding(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1385, in _label_encoding\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1385, in _label_encoding\n", " order = order.take(left_gather_map, check_bounds=False).argsort()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1101, in argsort\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1101, in argsort\n", " return self.as_frame()._get_sorted_inds(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 1572, in _get_sorted_inds\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 1572, in _get_sorted_inds\n", " return libcudf.sort.order_by(to_sort, ascending, na_position)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", " File \"sort.pyx\", line 141, in cudf._lib.sort.order_by\n", - "MemoryError: std::bad_alloc: out_of_memory: CUDA error at: /home/dacosta/miniconda3/envs/cugraph_0411/include/rmm/mr/device/cuda_memory_resource.hpp\n", + "MemoryError: std::bad_alloc: out_of_memory: CUDA error at: /home/dacosta/miniforge/envs/cugraph_0411/include/rmm/mr/device/cuda_memory_resource.hpp\n", "2023-05-12 09:25:03,767 - distributed.sizeof - WARNING - Sizeof calculation failed. Defaulting to 0.95 MiB\n", "Traceback (most recent call last):\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/sizeof.py\", line 17, in safe_sizeof\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/sizeof.py\", line 17, in safe_sizeof\n", " return sizeof(obj)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask/utils.py\", line 642, in __call__\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask/utils.py\", line 642, in __call__\n", " return meth(arg, *args, **kwargs)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask_cudf/backends.py\", line 430, in sizeof_cudf_dataframe\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask_cudf/backends.py\", line 430, in sizeof_cudf_dataframe\n", " + df._index.memory_usage()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 1594, in memory_usage\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 1594, in memory_usage\n", " if self.levels:\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 605, in levels\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 605, in levels\n", " self._compute_levels_and_codes()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 748, in _compute_levels_and_codes\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 748, in _compute_levels_and_codes\n", " code, cats = cudf.Series._from_data({None: col}).factorize()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/single_column_frame.py\", line 311, in factorize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/single_column_frame.py\", line 311, in factorize\n", " return cudf.core.algorithms.factorize(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/algorithms.py\", line 138, in factorize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/algorithms.py\", line 138, in factorize\n", " labels = values._column._label_encoding(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1385, in _label_encoding\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1385, in _label_encoding\n", " order = order.take(left_gather_map, check_bounds=False).argsort()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1101, in argsort\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1101, in argsort\n", " return self.as_frame()._get_sorted_inds(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 1572, in _get_sorted_inds\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 1572, in _get_sorted_inds\n", " return libcudf.sort.order_by(to_sort, ascending, na_position)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", " File \"sort.pyx\", line 141, in cudf._lib.sort.order_by\n", - "MemoryError: std::bad_alloc: out_of_memory: CUDA error at: /home/dacosta/miniconda3/envs/cugraph_0411/include/rmm/mr/device/cuda_memory_resource.hpp\n", + "MemoryError: std::bad_alloc: out_of_memory: CUDA error at: /home/dacosta/miniforge/envs/cugraph_0411/include/rmm/mr/device/cuda_memory_resource.hpp\n", "2023-05-12 09:25:03,768 - distributed.sizeof - WARNING - Sizeof calculation failed. Defaulting to 0.95 MiB\n", "Traceback (most recent call last):\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/sizeof.py\", line 17, in safe_sizeof\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/sizeof.py\", line 17, in safe_sizeof\n", " return sizeof(obj)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask/utils.py\", line 642, in __call__\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask/utils.py\", line 642, in __call__\n", " return meth(arg, *args, **kwargs)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask_cudf/backends.py\", line 430, in sizeof_cudf_dataframe\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask_cudf/backends.py\", line 430, in sizeof_cudf_dataframe\n", " + df._index.memory_usage()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 1594, in memory_usage\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 1594, in memory_usage\n", " if self.levels:\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 605, in levels\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 605, in levels\n", " self._compute_levels_and_codes()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 748, in _compute_levels_and_codes\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/multiindex.py\", line 748, in _compute_levels_and_codes\n", " code, cats = cudf.Series._from_data({None: col}).factorize()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/single_column_frame.py\", line 311, in factorize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/single_column_frame.py\", line 311, in factorize\n", " return cudf.core.algorithms.factorize(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/algorithms.py\", line 138, in factorize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/algorithms.py\", line 138, in factorize\n", " labels = values._column._label_encoding(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1385, in _label_encoding\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1385, in _label_encoding\n", " order = order.take(left_gather_map, check_bounds=False).argsort()\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1101, in argsort\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1101, in argsort\n", " return self.as_frame()._get_sorted_inds(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 1572, in _get_sorted_inds\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 1572, in _get_sorted_inds\n", " return libcudf.sort.order_by(to_sort, ascending, na_position)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", " File \"sort.pyx\", line 141, in cudf._lib.sort.order_by\n", - "MemoryError: std::bad_alloc: out_of_memory: CUDA error at: /home/dacosta/miniconda3/envs/cugraph_0411/include/rmm/mr/device/cuda_memory_resource.hpp\n", + "MemoryError: std::bad_alloc: out_of_memory: CUDA error at: /home/dacosta/miniforge/envs/cugraph_0411/include/rmm/mr/device/cuda_memory_resource.hpp\n", "2023-05-12 09:25:03,820 - distributed.worker - ERROR - Could not deserialize task ('len-chunk-319fe46af5510615b2fae86c6e732896-841a12bf4568ebb80eb2030cc4d9651d', 1)\n", "Traceback (most recent call last):\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2923, in loads_function\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2923, in loads_function\n", " result = cache_loads[bytes_object]\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/collections.py\", line 24, in __getitem__\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/collections.py\", line 24, in __getitem__\n", " value = super().__getitem__(key)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/collections/__init__.py\", line 1106, in __getitem__\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/collections/__init__.py\", line 1106, in __getitem__\n", " raise KeyError(key)\n", "KeyError: b'\\x80\\x05\\x95>\\x0b\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x11dask.optimization\\x94\\x8c\\x10SubgraphCallable\\x94\\x93\\x94(}\\x94(\\x8cKlen-chunk-319fe46af5510615b2fae86c6e732896-841a12bf4568ebb80eb2030cc4d9651d\\x94\\x8cZassign-getitem-len-chunk-319fe46af5510615b2fae86c6e732896-841a12bf4568ebb80eb2030cc4d9651d\\x94\\x8c*rename-01db283bd79fee66f232920c8dc6b55e_.0\\x94\\x8c;getitem-to_frame-rename-01db283bd79fee66f232920c8dc6b55e_.0\\x94\\x8c+getitem-3499fd71ac25ebbc1a06991edea6067c_.0\\x94\\x8c\\t_operator\\x94\\x8c\\x07getitem\\x94\\x93\\x94\\x8c/reset_index-f4c18304ca92859ccd09f44cf89b4b43_.0\\x94\\x8c\\x13__dask_blockwise__1\\x94\\x87\\x94h\\x0c(\\x8c\\ndask.utils\\x94\\x8c\\x05apply\\x94\\x93\\x94h\\x0f\\x8c\\x0cmethodcaller\\x94\\x93\\x94\\x8c\\x0breset_index\\x94\\x85\\x94R\\x94]\\x94\\x8c\\x13__dask_blockwise__5\\x94a\\x8c\\x08builtins\\x94\\x8c\\x04dict\\x94\\x93\\x94]\\x94]\\x94(\\x8c\\x04drop\\x94\\x89ea\\x86\\x94t\\x94h\\x07(h\\x11\\x8c\\x13dask.dataframe.core\\x94\\x8c\\x11apply_and_enforce\\x94\\x93\\x94]\\x94((h\\x11h#]\\x94h\\x0bh\\x0c\\x8c\\x13__dask_blockwise__0\\x94\\x87\\x94ah\\x1b]\\x94(]\\x94(\\x8c\\x05_func\\x94h\\x13\\x8c\\x08to_frame\\x94\\x85\\x94R\\x94e]\\x94(\\x8c\\x05_meta\\x94\\x8c\\x08builtins\\x94\\x8c\\x07getattr\\x94\\x93\\x94\\x8c\\x13cudf.core.dataframe\\x94\\x8c\\tDataFrame\\x94\\x93\\x94\\x8c\\x10host_deserialize\\x94\\x86\\x94R\\x94}\\x94(\\x8c\\x0ftype-serialized\\x94C0\\x80\\x04\\x95%\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x13cudf.core.dataframe\\x94\\x8c\\tDataFrame\\x94\\x93\\x94.\\x94\\x8c\\x0ccolumn_names\\x94C\\x14\\x80\\x04\\x95\\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x03src\\x94\\x85\\x94.\\x94\\x8c\\x07columns\\x94}\\x94(\\x8c\\x0ftype-serialized\\x94C=\\x80\\x04\\x952\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x1acudf.core.column.numerical\\x94\\x8c\\x0fNumericalColumn\\x94\\x93\\x94.\\x94\\x8c\\x05dtype\\x94CB\\x80\\x04\\x957\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x05numpy\\x94\\x8c\\x05dtype\\x94\\x93\\x94\\x8c\\x02i4\\x94\\x89\\x88\\x87\\x94R\\x94(K\\x03\\x8c\\x01<\\x94NNNJ\\xff\\xff\\xff\\xffJ\\xff\\xff\\xff\\xffK\\x00t\\x94b.\\x94\\x8c\\x18dtype-is-cudf-serialized\\x94\\x89\\x8c\\x04data\\x94}\\x94(\\x8c\\x0ftype-serialized\\x94CI\\x80\\x04\\x95>\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c!cudf.core.buffer.spillable_buffer\\x94\\x8c\\x14SpillableBufferSlice\\x94\\x93\\x94.\\x94\\x8c\\x0bframe_count\\x94K\\x01u\\x8c\\x04mask\\x94}\\x94(hGCD\\x80\\x04\\x959\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c!cudf.core.buffer.spillable_buffer\\x94\\x8c\\x0fSpillableBuffer\\x94\\x93\\x94.\\x94hIK\\x01u\\x8c\\x04size\\x94K\\x00hIK\\x02u\\x85\\x94\\x8c\\x05index\\x94}\\x94(\\x8c\\x0cindex_column\\x94}\\x94(\\x8c\\x05start\\x94K\\x00\\x8c\\x04stop\\x94K\\x00\\x8c\\x04step\\x94K\\x01u\\x8c\\x04name\\x94C\\x04\\x80\\x04N.\\x94hBCB\\x80\\x04\\x957\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x05numpy\\x94\\x8c\\x05dtype\\x94\\x93\\x94\\x8c\\x02i8\\x94\\x89\\x88\\x87\\x94R\\x94(K\\x03\\x8c\\x01<\\x94NNNJ\\xff\\xff\\xff\\xffJ\\xff\\xff\\xff\\xffK\\x00t\\x94b.\\x94\\x8c\\x0ftype-serialized\\x94C-\\x80\\x04\\x95\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x0fcudf.core.index\\x94\\x8c\\nRangeIndex\\x94\\x93\\x94.\\x94hIK\\x00u\\x8c\\x11index_frame_count\\x94K\\x00\\x8c\\x07is-cuda\\x94]\\x94(\\x88\\x88e\\x8c\\x07lengths\\x94]\\x94(K\\x00K\\x00e\\x8c\\twriteable\\x94NN\\x86\\x94u]\\x94(\\x8c\\x12numpy.core.numeric\\x94\\x8c\\x0b_frombuffer\\x94\\x93\\x94(C\\x00\\x94\\x8c\\x05numpy\\x94hB\\x93\\x94\\x8c\\x02u1\\x94\\x89\\x88\\x87\\x94R\\x94(K\\x03\\x8c\\x01|\\x94NNNJ\\xff\\xff\\xff\\xffJ\\xff\\xff\\xff\\xffK\\x00t\\x94bK\\x00\\x85\\x94\\x8c\\x01C\\x94t\\x94R\\x94he(C\\x00\\x94hkK\\x00\\x85\\x94hot\\x94R\\x94e\\x86\\x94R\\x94ee\\x86\\x94t\\x94\\x8c\\x13__dask_blockwise__2\\x94eh\\x1b]\\x94(]\\x94(h*h\\x13\\x8c\\x06rename\\x94\\x85\\x94R\\x94e]\\x94(h/h2h5h6\\x86\\x94R\\x94}\\x94(h:C0\\x80\\x04\\x95%\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x13cudf.core.dataframe\\x94\\x8c\\tDataFrame\\x94\\x93\\x94.\\x94h}\\x94(h@C=\\x80\\x04\\x952\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x1acudf.core.column.numerical\\x94\\x8c\\x0fNumericalColumn\\x94\\x93\\x94.\\x94hBCB\\x80\\x04\\x957\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x05numpy\\x94\\x8c\\x05dtype\\x94\\x93\\x94\\x8c\\x02i4\\x94\\x89\\x88\\x87\\x94R\\x94(K\\x03\\x8c\\x01<\\x94NNNJ\\xff\\xff\\xff\\xffJ\\xff\\xff\\xff\\xffK\\x00t\\x94b.\\x94hD\\x89hE}\\x94(hGCI\\x80\\x04\\x95>\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c!cudf.core.buffer.spillable_buffer\\x94\\x8c\\x14SpillableBufferSlice\\x94\\x93\\x94.\\x94hIK\\x01uhMK\\x00hIK\\x01u\\x85\\x94hO}\\x94(hQ}\\x94(hSK\\x00hTK\\x00hUK\\x01uhVC\\x04\\x80\\x04N.\\x94hBCB\\x80\\x04\\x957\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x05numpy\\x94\\x8c\\x05dtype\\x94\\x93\\x94\\x8c\\x02i8\\x94\\x89\\x88\\x87\\x94R\\x94(K\\x03\\x8c\\x01<\\x94NNNJ\\xff\\xff\\xff\\xffJ\\xff\\xff\\xff\\xffK\\x00t\\x94b.\\x94hYC-\\x80\\x04\\x95\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x0fcudf.core.index\\x94\\x8c\\nRangeIndex\\x94\\x93\\x94.\\x94hIK\\x00uh[K\\x00h\\\\]\\x94\\x88ah^]\\x94K\\x00ah`N\\x85\\x94u]\\x94he(C\\x00\\x94hkK\\x00\\x85\\x94hot\\x94R\\x94a\\x86\\x94R\\x94e]\\x94(h>h\\x1b]\\x94]\\x94(\\x8c\\x03src\\x94h\\x9eea\\x86\\x94ee\\x86\\x94t\\x94h\\x05(h\\x11h!\\x8c\\x10_reduction_chunk\\x94\\x93\\x94]\\x94h\\x0b(\\x8c\\x16dask.dataframe.methods\\x94\\x8c\\x06assign\\x94\\x93\\x94h\\x06h\\rh\\x08t\\x94h&\\x87\\x94ah\\x1b]\\x94]\\x94(\\x8c\\taca_chunk\\x94h0\\x8c\\x03len\\x94\\x93\\x94ea\\x86\\x94t\\x94\\x8c\\x13__dask_blockwise__0\\x94h\\x9e\\x8c\\x13__dask_blockwise__1\\x94\\x8c\\x03dst\\x94\\x8c\\x13__dask_blockwise__2\\x94N\\x8c\\x13__dask_blockwise__3\\x94\\x8c)to_frame-804980ae30b71d28f0a6bd3d5b7610f9\\x94\\x8c\\x13__dask_blockwise__4\\x94\\x8c(getitem-15414b72be12e28054238b44933937ab\\x94\\x8c\\x13__dask_blockwise__6\\x94\\x8c3cudf-aggregate-agg-c50c2d97de169ca4f41e43a92a042630\\x94uh\\x04\\x8c\\x13__dask_blockwise__5\\x94\\x85\\x94\\x8c6subgraph_callable-b4ca530e-8895-432e-b553-40a7b5892ab2\\x94t\\x94R\\x94.'\n", "\n", "During handling of the above exception, another exception occurred:\n", "\n", "Traceback (most recent call last):\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2244, in execute\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2244, in execute\n", " function, args, kwargs = await self._maybe_deserialize_task(ts)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2216, in _maybe_deserialize_task\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2216, in _maybe_deserialize_task\n", " function, args, kwargs = _deserialize(*ts.run_spec)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2937, in _deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2937, in _deserialize\n", " function = loads_function(function)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2925, in loads_function\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2925, in loads_function\n", " result = pickle.loads(bytes_object)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/protocol/pickle.py\", line 96, in loads\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/protocol/pickle.py\", line 96, in loads\n", " return pickle.loads(x)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py\", line 176, in host_deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py\", line 176, in host_deserialize\n", " obj = cls.device_deserialize(header, frames)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py\", line 130, in device_deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py\", line 130, in device_deserialize\n", " return typ.deserialize(header, frames)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/dataframe.py\", line 1019, in deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/dataframe.py\", line 1019, in deserialize\n", " obj = super().deserialize(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 106, in deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 106, in deserialize\n", " columns = deserialize_columns(header[\"columns\"], frames)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 2450, in deserialize_columns\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 2450, in deserialize_columns\n", " colobj = col_typ.deserialize(meta, frames[:col_frame_count])\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1216, in deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1216, in deserialize\n", " data, frames = unpack(header[\"data\"], frames)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1204, in unpack\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1204, in unpack\n", " obj = klass.deserialize(header, frames[:count])\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 574, in deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 574, in deserialize\n", " return SpillableBuffer.deserialize(header, frames)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/buffer.py\", line 335, in deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/buffer.py\", line 335, in deserialize\n", " return cls._from_device_memory(frame)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 235, in _from_device_memory\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 235, in _from_device_memory\n", " ret._finalize_init(ptr_desc={\"type\": \"gpu\"}, exposed=exposed)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 206, in _finalize_init\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 206, in _finalize_init\n", " raise ValueError(\n", "ValueError: cannot create without a global spill manager\n", "2023-05-12 09:25:03,817 - distributed.worker - ERROR - Could not deserialize task ('len-chunk-319fe46af5510615b2fae86c6e732896-841a12bf4568ebb80eb2030cc4d9651d', 0)\n", "Traceback (most recent call last):\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2923, in loads_function\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2923, in loads_function\n", " result = cache_loads[bytes_object]\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/collections.py\", line 24, in __getitem__\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/collections.py\", line 24, in __getitem__\n", " value = super().__getitem__(key)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/collections/__init__.py\", line 1106, in __getitem__\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/collections/__init__.py\", line 1106, in __getitem__\n", " raise KeyError(key)\n", "KeyError: b'\\x80\\x05\\x95>\\x0b\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x11dask.optimization\\x94\\x8c\\x10SubgraphCallable\\x94\\x93\\x94(}\\x94(\\x8cKlen-chunk-319fe46af5510615b2fae86c6e732896-841a12bf4568ebb80eb2030cc4d9651d\\x94\\x8cZassign-getitem-len-chunk-319fe46af5510615b2fae86c6e732896-841a12bf4568ebb80eb2030cc4d9651d\\x94\\x8c*rename-01db283bd79fee66f232920c8dc6b55e_.0\\x94\\x8c;getitem-to_frame-rename-01db283bd79fee66f232920c8dc6b55e_.0\\x94\\x8c+getitem-3499fd71ac25ebbc1a06991edea6067c_.0\\x94\\x8c\\t_operator\\x94\\x8c\\x07getitem\\x94\\x93\\x94\\x8c/reset_index-f4c18304ca92859ccd09f44cf89b4b43_.0\\x94\\x8c\\x13__dask_blockwise__1\\x94\\x87\\x94h\\x0c(\\x8c\\ndask.utils\\x94\\x8c\\x05apply\\x94\\x93\\x94h\\x0f\\x8c\\x0cmethodcaller\\x94\\x93\\x94\\x8c\\x0breset_index\\x94\\x85\\x94R\\x94]\\x94\\x8c\\x13__dask_blockwise__5\\x94a\\x8c\\x08builtins\\x94\\x8c\\x04dict\\x94\\x93\\x94]\\x94]\\x94(\\x8c\\x04drop\\x94\\x89ea\\x86\\x94t\\x94h\\x07(h\\x11\\x8c\\x13dask.dataframe.core\\x94\\x8c\\x11apply_and_enforce\\x94\\x93\\x94]\\x94((h\\x11h#]\\x94h\\x0bh\\x0c\\x8c\\x13__dask_blockwise__0\\x94\\x87\\x94ah\\x1b]\\x94(]\\x94(\\x8c\\x05_func\\x94h\\x13\\x8c\\x08to_frame\\x94\\x85\\x94R\\x94e]\\x94(\\x8c\\x05_meta\\x94\\x8c\\x08builtins\\x94\\x8c\\x07getattr\\x94\\x93\\x94\\x8c\\x13cudf.core.dataframe\\x94\\x8c\\tDataFrame\\x94\\x93\\x94\\x8c\\x10host_deserialize\\x94\\x86\\x94R\\x94}\\x94(\\x8c\\x0ftype-serialized\\x94C0\\x80\\x04\\x95%\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x13cudf.core.dataframe\\x94\\x8c\\tDataFrame\\x94\\x93\\x94.\\x94\\x8c\\x0ccolumn_names\\x94C\\x14\\x80\\x04\\x95\\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x03src\\x94\\x85\\x94.\\x94\\x8c\\x07columns\\x94}\\x94(\\x8c\\x0ftype-serialized\\x94C=\\x80\\x04\\x952\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x1acudf.core.column.numerical\\x94\\x8c\\x0fNumericalColumn\\x94\\x93\\x94.\\x94\\x8c\\x05dtype\\x94CB\\x80\\x04\\x957\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x05numpy\\x94\\x8c\\x05dtype\\x94\\x93\\x94\\x8c\\x02i4\\x94\\x89\\x88\\x87\\x94R\\x94(K\\x03\\x8c\\x01<\\x94NNNJ\\xff\\xff\\xff\\xffJ\\xff\\xff\\xff\\xffK\\x00t\\x94b.\\x94\\x8c\\x18dtype-is-cudf-serialized\\x94\\x89\\x8c\\x04data\\x94}\\x94(\\x8c\\x0ftype-serialized\\x94CI\\x80\\x04\\x95>\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c!cudf.core.buffer.spillable_buffer\\x94\\x8c\\x14SpillableBufferSlice\\x94\\x93\\x94.\\x94\\x8c\\x0bframe_count\\x94K\\x01u\\x8c\\x04mask\\x94}\\x94(hGCD\\x80\\x04\\x959\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c!cudf.core.buffer.spillable_buffer\\x94\\x8c\\x0fSpillableBuffer\\x94\\x93\\x94.\\x94hIK\\x01u\\x8c\\x04size\\x94K\\x00hIK\\x02u\\x85\\x94\\x8c\\x05index\\x94}\\x94(\\x8c\\x0cindex_column\\x94}\\x94(\\x8c\\x05start\\x94K\\x00\\x8c\\x04stop\\x94K\\x00\\x8c\\x04step\\x94K\\x01u\\x8c\\x04name\\x94C\\x04\\x80\\x04N.\\x94hBCB\\x80\\x04\\x957\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x05numpy\\x94\\x8c\\x05dtype\\x94\\x93\\x94\\x8c\\x02i8\\x94\\x89\\x88\\x87\\x94R\\x94(K\\x03\\x8c\\x01<\\x94NNNJ\\xff\\xff\\xff\\xffJ\\xff\\xff\\xff\\xffK\\x00t\\x94b.\\x94\\x8c\\x0ftype-serialized\\x94C-\\x80\\x04\\x95\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x0fcudf.core.index\\x94\\x8c\\nRangeIndex\\x94\\x93\\x94.\\x94hIK\\x00u\\x8c\\x11index_frame_count\\x94K\\x00\\x8c\\x07is-cuda\\x94]\\x94(\\x88\\x88e\\x8c\\x07lengths\\x94]\\x94(K\\x00K\\x00e\\x8c\\twriteable\\x94NN\\x86\\x94u]\\x94(\\x8c\\x12numpy.core.numeric\\x94\\x8c\\x0b_frombuffer\\x94\\x93\\x94(C\\x00\\x94\\x8c\\x05numpy\\x94hB\\x93\\x94\\x8c\\x02u1\\x94\\x89\\x88\\x87\\x94R\\x94(K\\x03\\x8c\\x01|\\x94NNNJ\\xff\\xff\\xff\\xffJ\\xff\\xff\\xff\\xffK\\x00t\\x94bK\\x00\\x85\\x94\\x8c\\x01C\\x94t\\x94R\\x94he(C\\x00\\x94hkK\\x00\\x85\\x94hot\\x94R\\x94e\\x86\\x94R\\x94ee\\x86\\x94t\\x94\\x8c\\x13__dask_blockwise__2\\x94eh\\x1b]\\x94(]\\x94(h*h\\x13\\x8c\\x06rename\\x94\\x85\\x94R\\x94e]\\x94(h/h2h5h6\\x86\\x94R\\x94}\\x94(h:C0\\x80\\x04\\x95%\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x13cudf.core.dataframe\\x94\\x8c\\tDataFrame\\x94\\x93\\x94.\\x94h}\\x94(h@C=\\x80\\x04\\x952\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x1acudf.core.column.numerical\\x94\\x8c\\x0fNumericalColumn\\x94\\x93\\x94.\\x94hBCB\\x80\\x04\\x957\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x05numpy\\x94\\x8c\\x05dtype\\x94\\x93\\x94\\x8c\\x02i4\\x94\\x89\\x88\\x87\\x94R\\x94(K\\x03\\x8c\\x01<\\x94NNNJ\\xff\\xff\\xff\\xffJ\\xff\\xff\\xff\\xffK\\x00t\\x94b.\\x94hD\\x89hE}\\x94(hGCI\\x80\\x04\\x95>\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c!cudf.core.buffer.spillable_buffer\\x94\\x8c\\x14SpillableBufferSlice\\x94\\x93\\x94.\\x94hIK\\x01uhMK\\x00hIK\\x01u\\x85\\x94hO}\\x94(hQ}\\x94(hSK\\x00hTK\\x00hUK\\x01uhVC\\x04\\x80\\x04N.\\x94hBCB\\x80\\x04\\x957\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x05numpy\\x94\\x8c\\x05dtype\\x94\\x93\\x94\\x8c\\x02i8\\x94\\x89\\x88\\x87\\x94R\\x94(K\\x03\\x8c\\x01<\\x94NNNJ\\xff\\xff\\xff\\xffJ\\xff\\xff\\xff\\xffK\\x00t\\x94b.\\x94hYC-\\x80\\x04\\x95\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x0fcudf.core.index\\x94\\x8c\\nRangeIndex\\x94\\x93\\x94.\\x94hIK\\x00uh[K\\x00h\\\\]\\x94\\x88ah^]\\x94K\\x00ah`N\\x85\\x94u]\\x94he(C\\x00\\x94hkK\\x00\\x85\\x94hot\\x94R\\x94a\\x86\\x94R\\x94e]\\x94(h>h\\x1b]\\x94]\\x94(\\x8c\\x03src\\x94h\\x9eea\\x86\\x94ee\\x86\\x94t\\x94h\\x05(h\\x11h!\\x8c\\x10_reduction_chunk\\x94\\x93\\x94]\\x94h\\x0b(\\x8c\\x16dask.dataframe.methods\\x94\\x8c\\x06assign\\x94\\x93\\x94h\\x06h\\rh\\x08t\\x94h&\\x87\\x94ah\\x1b]\\x94]\\x94(\\x8c\\taca_chunk\\x94h0\\x8c\\x03len\\x94\\x93\\x94ea\\x86\\x94t\\x94\\x8c\\x13__dask_blockwise__0\\x94h\\x9e\\x8c\\x13__dask_blockwise__1\\x94\\x8c\\x03dst\\x94\\x8c\\x13__dask_blockwise__2\\x94N\\x8c\\x13__dask_blockwise__3\\x94\\x8c)to_frame-804980ae30b71d28f0a6bd3d5b7610f9\\x94\\x8c\\x13__dask_blockwise__4\\x94\\x8c(getitem-15414b72be12e28054238b44933937ab\\x94\\x8c\\x13__dask_blockwise__6\\x94\\x8c3cudf-aggregate-agg-c50c2d97de169ca4f41e43a92a042630\\x94uh\\x04\\x8c\\x13__dask_blockwise__5\\x94\\x85\\x94\\x8c6subgraph_callable-b4ca530e-8895-432e-b553-40a7b5892ab2\\x94t\\x94R\\x94.'\n", "\n", "During handling of the above exception, another exception occurred:\n", "\n", "Traceback (most recent call last):\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2244, in execute\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2244, in execute\n", " function, args, kwargs = await self._maybe_deserialize_task(ts)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2216, in _maybe_deserialize_task\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2216, in _maybe_deserialize_task\n", " function, args, kwargs = _deserialize(*ts.run_spec)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py\", line 79, in inner\n", " return func(*args, **kwds)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2937, in _deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2937, in _deserialize\n", " function = loads_function(function)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2925, in loads_function\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py\", line 2925, in loads_function\n", " result = pickle.loads(bytes_object)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/protocol/pickle.py\", line 96, in loads\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/protocol/pickle.py\", line 96, in loads\n", " return pickle.loads(x)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py\", line 176, in host_deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py\", line 176, in host_deserialize\n", " obj = cls.device_deserialize(header, frames)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py\", line 130, in device_deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py\", line 130, in device_deserialize\n", " return typ.deserialize(header, frames)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/dataframe.py\", line 1019, in deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/dataframe.py\", line 1019, in deserialize\n", " obj = super().deserialize(\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 106, in deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py\", line 106, in deserialize\n", " columns = deserialize_columns(header[\"columns\"], frames)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 2450, in deserialize_columns\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 2450, in deserialize_columns\n", " colobj = col_typ.deserialize(meta, frames[:col_frame_count])\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1216, in deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1216, in deserialize\n", " data, frames = unpack(header[\"data\"], frames)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1204, in unpack\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py\", line 1204, in unpack\n", " obj = klass.deserialize(header, frames[:count])\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 574, in deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 574, in deserialize\n", " return SpillableBuffer.deserialize(header, frames)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/buffer.py\", line 335, in deserialize\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/buffer.py\", line 335, in deserialize\n", " return cls._from_device_memory(frame)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 235, in _from_device_memory\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 235, in _from_device_memory\n", " ret._finalize_init(ptr_desc={\"type\": \"gpu\"}, exposed=exposed)\n", - " File \"/home/dacosta/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 206, in _finalize_init\n", + " File \"/home/dacosta/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py\", line 206, in _finalize_init\n", " raise ValueError(\n", "ValueError: cannot create without a global spill manager\n" ] @@ -475,34 +475,34 @@ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[6], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[39m# Create a directed graph using the source (src) and destination (dst) vertex pairs from the Dataframe \u001b[39;00m\n\u001b[1;32m 2\u001b[0m G \u001b[39m=\u001b[39m cugraph\u001b[39m.\u001b[39mGraph(directed\u001b[39m=\u001b[39m\u001b[39mTrue\u001b[39;00m)\n\u001b[0;32m----> 3\u001b[0m G\u001b[39m.\u001b[39;49mfrom_dask_cudf_edgelist(e_list, source\u001b[39m=\u001b[39;49m\u001b[39m'\u001b[39;49m\u001b[39msrc\u001b[39;49m\u001b[39m'\u001b[39;49m, destination\u001b[39m=\u001b[39;49m\u001b[39m'\u001b[39;49m\u001b[39mdst\u001b[39;49m\u001b[39m'\u001b[39;49m)\n\u001b[1;32m 5\u001b[0m \u001b[39m# Print time\u001b[39;00m\n\u001b[1;32m 6\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m\"\u001b[39m\u001b[39mRead, load and renumber: \u001b[39m\u001b[39m\"\u001b[39m, time\u001b[39m.\u001b[39mtime()\u001b[39m-\u001b[39mt_start, \u001b[39m\"\u001b[39m\u001b[39ms\u001b[39m\u001b[39m\"\u001b[39m)\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cugraph/structure/graph_classes.py:309\u001b[0m, in \u001b[0;36mGraph.from_dask_cudf_edgelist\u001b[0;34m(self, input_ddf, source, destination, edge_attr, renumber, store_transposed, legacy_renum_only)\u001b[0m\n\u001b[1;32m 307\u001b[0m \u001b[39melif\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_Impl\u001b[39m.\u001b[39medgelist \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m 308\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mRuntimeError\u001b[39;00m(\u001b[39m\"\u001b[39m\u001b[39mGraph already has values\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[0;32m--> 309\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_Impl\u001b[39m.\u001b[39;49m_simpleDistributedGraphImpl__from_edgelist(\n\u001b[1;32m 310\u001b[0m input_ddf,\n\u001b[1;32m 311\u001b[0m source,\n\u001b[1;32m 312\u001b[0m destination,\n\u001b[1;32m 313\u001b[0m edge_attr,\n\u001b[1;32m 314\u001b[0m renumber,\n\u001b[1;32m 315\u001b[0m store_transposed,\n\u001b[1;32m 316\u001b[0m legacy_renum_only,\n\u001b[1;32m 317\u001b[0m )\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cugraph/structure/graph_implementation/simpleDistributedGraph.py:272\u001b[0m, in \u001b[0;36msimpleDistributedGraphImpl.__from_edgelist\u001b[0;34m(self, input_ddf, source, destination, edge_attr, renumber, store_transposed, legacy_renum_only)\u001b[0m\n\u001b[1;32m 268\u001b[0m dst_col_name \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mrenumber_map\u001b[39m.\u001b[39mrenumbered_dst_col_name\n\u001b[1;32m 270\u001b[0m ddf \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39medgelist\u001b[39m.\u001b[39medgelist_df\n\u001b[0;32m--> 272\u001b[0m num_edges \u001b[39m=\u001b[39m \u001b[39mlen\u001b[39;49m(ddf)\n\u001b[1;32m 273\u001b[0m edge_data \u001b[39m=\u001b[39m get_distributed_data(ddf)\n\u001b[1;32m 275\u001b[0m graph_props \u001b[39m=\u001b[39m GraphProperties(\n\u001b[1;32m 276\u001b[0m is_multigraph\u001b[39m=\u001b[39m\u001b[39mself\u001b[39m\u001b[39m.\u001b[39mproperties\u001b[39m.\u001b[39mmulti_edge,\n\u001b[1;32m 277\u001b[0m is_symmetric\u001b[39m=\u001b[39m\u001b[39mnot\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mproperties\u001b[39m.\u001b[39mdirected,\n\u001b[1;32m 278\u001b[0m )\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask/dataframe/core.py:4775\u001b[0m, in \u001b[0;36mDataFrame.__len__\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 4773\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39msuper\u001b[39m()\u001b[39m.\u001b[39m\u001b[39m__len__\u001b[39m()\n\u001b[1;32m 4774\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m-> 4775\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mlen\u001b[39;49m(s)\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask/dataframe/core.py:843\u001b[0m, in \u001b[0;36m_Frame.__len__\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 840\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39m__len__\u001b[39m(\u001b[39mself\u001b[39m):\n\u001b[1;32m 841\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mreduction(\n\u001b[1;32m 842\u001b[0m \u001b[39mlen\u001b[39;49m, np\u001b[39m.\u001b[39;49msum, token\u001b[39m=\u001b[39;49m\u001b[39m\"\u001b[39;49m\u001b[39mlen\u001b[39;49m\u001b[39m\"\u001b[39;49m, meta\u001b[39m=\u001b[39;49m\u001b[39mint\u001b[39;49m, split_every\u001b[39m=\u001b[39;49m\u001b[39mFalse\u001b[39;49;00m\n\u001b[0;32m--> 843\u001b[0m )\u001b[39m.\u001b[39;49mcompute()\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask/base.py:314\u001b[0m, in \u001b[0;36mDaskMethodsMixin.compute\u001b[0;34m(self, **kwargs)\u001b[0m\n\u001b[1;32m 290\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mcompute\u001b[39m(\u001b[39mself\u001b[39m, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mkwargs):\n\u001b[1;32m 291\u001b[0m \u001b[39m \u001b[39m\u001b[39m\"\"\"Compute this dask collection\u001b[39;00m\n\u001b[1;32m 292\u001b[0m \n\u001b[1;32m 293\u001b[0m \u001b[39m This turns a lazy Dask collection into its in-memory equivalent.\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 312\u001b[0m \u001b[39m dask.base.compute\u001b[39;00m\n\u001b[1;32m 313\u001b[0m \u001b[39m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 314\u001b[0m (result,) \u001b[39m=\u001b[39m compute(\u001b[39mself\u001b[39;49m, traverse\u001b[39m=\u001b[39;49m\u001b[39mFalse\u001b[39;49;00m, \u001b[39m*\u001b[39;49m\u001b[39m*\u001b[39;49mkwargs)\n\u001b[1;32m 315\u001b[0m \u001b[39mreturn\u001b[39;00m result\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/dask/base.py:599\u001b[0m, in \u001b[0;36mcompute\u001b[0;34m(traverse, optimize_graph, scheduler, get, *args, **kwargs)\u001b[0m\n\u001b[1;32m 596\u001b[0m keys\u001b[39m.\u001b[39mappend(x\u001b[39m.\u001b[39m__dask_keys__())\n\u001b[1;32m 597\u001b[0m postcomputes\u001b[39m.\u001b[39mappend(x\u001b[39m.\u001b[39m__dask_postcompute__())\n\u001b[0;32m--> 599\u001b[0m results \u001b[39m=\u001b[39m schedule(dsk, keys, \u001b[39m*\u001b[39;49m\u001b[39m*\u001b[39;49mkwargs)\n\u001b[1;32m 600\u001b[0m \u001b[39mreturn\u001b[39;00m repack([f(r, \u001b[39m*\u001b[39ma) \u001b[39mfor\u001b[39;00m r, (f, a) \u001b[39min\u001b[39;00m \u001b[39mzip\u001b[39m(results, postcomputes)])\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/client.py:3186\u001b[0m, in \u001b[0;36mClient.get\u001b[0;34m(self, dsk, keys, workers, allow_other_workers, resources, sync, asynchronous, direct, retries, priority, fifo_timeout, actors, **kwargs)\u001b[0m\n\u001b[1;32m 3184\u001b[0m should_rejoin \u001b[39m=\u001b[39m \u001b[39mFalse\u001b[39;00m\n\u001b[1;32m 3185\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[0;32m-> 3186\u001b[0m results \u001b[39m=\u001b[39m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mgather(packed, asynchronous\u001b[39m=\u001b[39;49masynchronous, direct\u001b[39m=\u001b[39;49mdirect)\n\u001b[1;32m 3187\u001b[0m \u001b[39mfinally\u001b[39;00m:\n\u001b[1;32m 3188\u001b[0m \u001b[39mfor\u001b[39;00m f \u001b[39min\u001b[39;00m futures\u001b[39m.\u001b[39mvalues():\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/client.py:2345\u001b[0m, in \u001b[0;36mClient.gather\u001b[0;34m(self, futures, errors, direct, asynchronous)\u001b[0m\n\u001b[1;32m 2343\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[1;32m 2344\u001b[0m local_worker \u001b[39m=\u001b[39m \u001b[39mNone\u001b[39;00m\n\u001b[0;32m-> 2345\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49msync(\n\u001b[1;32m 2346\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_gather,\n\u001b[1;32m 2347\u001b[0m futures,\n\u001b[1;32m 2348\u001b[0m errors\u001b[39m=\u001b[39;49merrors,\n\u001b[1;32m 2349\u001b[0m direct\u001b[39m=\u001b[39;49mdirect,\n\u001b[1;32m 2350\u001b[0m local_worker\u001b[39m=\u001b[39;49mlocal_worker,\n\u001b[1;32m 2351\u001b[0m asynchronous\u001b[39m=\u001b[39;49masynchronous,\n\u001b[1;32m 2352\u001b[0m )\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/utils.py:349\u001b[0m, in \u001b[0;36mSyncMethodMixin.sync\u001b[0;34m(self, func, asynchronous, callback_timeout, *args, **kwargs)\u001b[0m\n\u001b[1;32m 347\u001b[0m \u001b[39mreturn\u001b[39;00m future\n\u001b[1;32m 348\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m--> 349\u001b[0m \u001b[39mreturn\u001b[39;00m sync(\n\u001b[1;32m 350\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mloop, func, \u001b[39m*\u001b[39;49margs, callback_timeout\u001b[39m=\u001b[39;49mcallback_timeout, \u001b[39m*\u001b[39;49m\u001b[39m*\u001b[39;49mkwargs\n\u001b[1;32m 351\u001b[0m )\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/utils.py:416\u001b[0m, in \u001b[0;36msync\u001b[0;34m(loop, func, callback_timeout, *args, **kwargs)\u001b[0m\n\u001b[1;32m 414\u001b[0m \u001b[39mif\u001b[39;00m error:\n\u001b[1;32m 415\u001b[0m typ, exc, tb \u001b[39m=\u001b[39m error\n\u001b[0;32m--> 416\u001b[0m \u001b[39mraise\u001b[39;00m exc\u001b[39m.\u001b[39mwith_traceback(tb)\n\u001b[1;32m 417\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[1;32m 418\u001b[0m \u001b[39mreturn\u001b[39;00m result\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/utils.py:389\u001b[0m, in \u001b[0;36msync..f\u001b[0;34m()\u001b[0m\n\u001b[1;32m 387\u001b[0m future \u001b[39m=\u001b[39m wait_for(future, callback_timeout)\n\u001b[1;32m 388\u001b[0m future \u001b[39m=\u001b[39m asyncio\u001b[39m.\u001b[39mensure_future(future)\n\u001b[0;32m--> 389\u001b[0m result \u001b[39m=\u001b[39m \u001b[39myield\u001b[39;00m future\n\u001b[1;32m 390\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mException\u001b[39;00m:\n\u001b[1;32m 391\u001b[0m error \u001b[39m=\u001b[39m sys\u001b[39m.\u001b[39mexc_info()\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/tornado/gen.py:769\u001b[0m, in \u001b[0;36mRunner.run\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 766\u001b[0m exc_info \u001b[39m=\u001b[39m \u001b[39mNone\u001b[39;00m\n\u001b[1;32m 768\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[0;32m--> 769\u001b[0m value \u001b[39m=\u001b[39m future\u001b[39m.\u001b[39;49mresult()\n\u001b[1;32m 770\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mException\u001b[39;00m:\n\u001b[1;32m 771\u001b[0m exc_info \u001b[39m=\u001b[39m sys\u001b[39m.\u001b[39mexc_info()\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/client.py:2208\u001b[0m, in \u001b[0;36mClient._gather\u001b[0;34m(self, futures, errors, direct, local_worker)\u001b[0m\n\u001b[1;32m 2206\u001b[0m exc \u001b[39m=\u001b[39m CancelledError(key)\n\u001b[1;32m 2207\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m-> 2208\u001b[0m \u001b[39mraise\u001b[39;00m exception\u001b[39m.\u001b[39mwith_traceback(traceback)\n\u001b[1;32m 2209\u001b[0m \u001b[39mraise\u001b[39;00m exc\n\u001b[1;32m 2210\u001b[0m \u001b[39mif\u001b[39;00m errors \u001b[39m==\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mskip\u001b[39m\u001b[39m\"\u001b[39m:\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/contextlib.py:79\u001b[0m, in \u001b[0;36minner\u001b[0;34m()\u001b[0m\n\u001b[1;32m 76\u001b[0m \u001b[39m@wraps\u001b[39m(func)\n\u001b[1;32m 77\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39minner\u001b[39m(\u001b[39m*\u001b[39margs, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mkwds):\n\u001b[1;32m 78\u001b[0m \u001b[39mwith\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_recreate_cm():\n\u001b[0;32m---> 79\u001b[0m \u001b[39mreturn\u001b[39;00m func(\u001b[39m*\u001b[39margs, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mkwds)\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py:2937\u001b[0m, in \u001b[0;36m_deserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2934\u001b[0m \u001b[39m# Some objects require threadlocal state during deserialization, e.g. to\u001b[39;00m\n\u001b[1;32m 2935\u001b[0m \u001b[39m# detect the current worker\u001b[39;00m\n\u001b[1;32m 2936\u001b[0m \u001b[39mif\u001b[39;00m function \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m-> 2937\u001b[0m function \u001b[39m=\u001b[39m loads_function(function)\n\u001b[1;32m 2938\u001b[0m \u001b[39mif\u001b[39;00m args \u001b[39mand\u001b[39;00m \u001b[39misinstance\u001b[39m(args, \u001b[39mbytes\u001b[39m):\n\u001b[1;32m 2939\u001b[0m args \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(args)\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py:2925\u001b[0m, in \u001b[0;36mloads_function\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2923\u001b[0m result \u001b[39m=\u001b[39m cache_loads[bytes_object]\n\u001b[1;32m 2924\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mKeyError\u001b[39;00m:\n\u001b[0;32m-> 2925\u001b[0m result \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(bytes_object)\n\u001b[1;32m 2926\u001b[0m cache_loads[bytes_object] \u001b[39m=\u001b[39m result\n\u001b[1;32m 2927\u001b[0m \u001b[39mreturn\u001b[39;00m result\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/distributed/protocol/pickle.py:96\u001b[0m, in \u001b[0;36mloads\u001b[0;34m()\u001b[0m\n\u001b[1;32m 94\u001b[0m \u001b[39mreturn\u001b[39;00m pickle\u001b[39m.\u001b[39mloads(x, buffers\u001b[39m=\u001b[39mbuffers)\n\u001b[1;32m 95\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m---> 96\u001b[0m \u001b[39mreturn\u001b[39;00m pickle\u001b[39m.\u001b[39mloads(x)\n\u001b[1;32m 97\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mException\u001b[39;00m:\n\u001b[1;32m 98\u001b[0m logger\u001b[39m.\u001b[39minfo(\u001b[39m\"\u001b[39m\u001b[39mFailed to deserialize \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m\"\u001b[39m, x[:\u001b[39m10000\u001b[39m], exc_info\u001b[39m=\u001b[39m\u001b[39mTrue\u001b[39;00m)\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py:176\u001b[0m, in \u001b[0;36mhost_deserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 154\u001b[0m \u001b[39m\u001b[39m\u001b[39m\"\"\"Perform device-side deserialization tasks.\u001b[39;00m\n\u001b[1;32m 155\u001b[0m \n\u001b[1;32m 156\u001b[0m \u001b[39mParameters\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 170\u001b[0m \u001b[39m:meta private:\u001b[39;00m\n\u001b[1;32m 171\u001b[0m \u001b[39m\"\"\"\u001b[39;00m\n\u001b[1;32m 172\u001b[0m frames \u001b[39m=\u001b[39m [\n\u001b[1;32m 173\u001b[0m cudf\u001b[39m.\u001b[39mcore\u001b[39m.\u001b[39mbuffer\u001b[39m.\u001b[39mas_buffer(f) \u001b[39mif\u001b[39;00m c \u001b[39melse\u001b[39;00m f\n\u001b[1;32m 174\u001b[0m \u001b[39mfor\u001b[39;00m c, f \u001b[39min\u001b[39;00m \u001b[39mzip\u001b[39m(header[\u001b[39m\"\u001b[39m\u001b[39mis-cuda\u001b[39m\u001b[39m\"\u001b[39m], \u001b[39mmap\u001b[39m(\u001b[39mmemoryview\u001b[39m, frames))\n\u001b[1;32m 175\u001b[0m ]\n\u001b[0;32m--> 176\u001b[0m obj \u001b[39m=\u001b[39m \u001b[39mcls\u001b[39m\u001b[39m.\u001b[39mdevice_deserialize(header, frames)\n\u001b[1;32m 177\u001b[0m \u001b[39mreturn\u001b[39;00m obj\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py:130\u001b[0m, in \u001b[0;36mdevice_deserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 125\u001b[0m typ \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mtype-serialized\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[1;32m 126\u001b[0m frames \u001b[39m=\u001b[39m [\n\u001b[1;32m 127\u001b[0m cudf\u001b[39m.\u001b[39mcore\u001b[39m.\u001b[39mbuffer\u001b[39m.\u001b[39mas_buffer(f) \u001b[39mif\u001b[39;00m c \u001b[39melse\u001b[39;00m \u001b[39mmemoryview\u001b[39m(f)\n\u001b[1;32m 128\u001b[0m \u001b[39mfor\u001b[39;00m c, f \u001b[39min\u001b[39;00m \u001b[39mzip\u001b[39m(header[\u001b[39m\"\u001b[39m\u001b[39mis-cuda\u001b[39m\u001b[39m\"\u001b[39m], frames)\n\u001b[1;32m 129\u001b[0m ]\n\u001b[0;32m--> 130\u001b[0m \u001b[39mreturn\u001b[39;00m typ\u001b[39m.\u001b[39mdeserialize(header, frames)\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/dataframe.py:1019\u001b[0m, in \u001b[0;36mdeserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1016\u001b[0m \u001b[39m@classmethod\u001b[39m\n\u001b[1;32m 1017\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mdeserialize\u001b[39m(\u001b[39mcls\u001b[39m, header, frames):\n\u001b[1;32m 1018\u001b[0m index_nframes \u001b[39m=\u001b[39m header[\u001b[39m\"\u001b[39m\u001b[39mindex_frame_count\u001b[39m\u001b[39m\"\u001b[39m]\n\u001b[0;32m-> 1019\u001b[0m obj \u001b[39m=\u001b[39m \u001b[39msuper\u001b[39m()\u001b[39m.\u001b[39mdeserialize(\n\u001b[1;32m 1020\u001b[0m header, frames[header[\u001b[39m\"\u001b[39m\u001b[39mindex_frame_count\u001b[39m\u001b[39m\"\u001b[39m] :]\n\u001b[1;32m 1021\u001b[0m )\n\u001b[1;32m 1023\u001b[0m idx_typ \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mindex\u001b[39m\u001b[39m\"\u001b[39m][\u001b[39m\"\u001b[39m\u001b[39mtype-serialized\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[1;32m 1024\u001b[0m index \u001b[39m=\u001b[39m idx_typ\u001b[39m.\u001b[39mdeserialize(header[\u001b[39m\"\u001b[39m\u001b[39mindex\u001b[39m\u001b[39m\"\u001b[39m], frames[:index_nframes])\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py:106\u001b[0m, in \u001b[0;36mdeserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 104\u001b[0m cls_deserialize \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mtype-serialized\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[1;32m 105\u001b[0m column_names \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mcolumn_names\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[0;32m--> 106\u001b[0m columns \u001b[39m=\u001b[39m deserialize_columns(header[\u001b[39m\"\u001b[39m\u001b[39mcolumns\u001b[39m\u001b[39m\"\u001b[39m], frames)\n\u001b[1;32m 107\u001b[0m \u001b[39mreturn\u001b[39;00m cls_deserialize\u001b[39m.\u001b[39m_from_data(\u001b[39mdict\u001b[39m(\u001b[39mzip\u001b[39m(column_names, columns)))\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py:2450\u001b[0m, in \u001b[0;36mdeserialize_columns\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2448\u001b[0m col_frame_count \u001b[39m=\u001b[39m meta[\u001b[39m\"\u001b[39m\u001b[39mframe_count\u001b[39m\u001b[39m\"\u001b[39m]\n\u001b[1;32m 2449\u001b[0m col_typ \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(meta[\u001b[39m\"\u001b[39m\u001b[39mtype-serialized\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[0;32m-> 2450\u001b[0m colobj \u001b[39m=\u001b[39m col_typ\u001b[39m.\u001b[39mdeserialize(meta, frames[:col_frame_count])\n\u001b[1;32m 2451\u001b[0m columns\u001b[39m.\u001b[39mappend(colobj)\n\u001b[1;32m 2452\u001b[0m \u001b[39m# Advance frames\u001b[39;00m\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py:1216\u001b[0m, in \u001b[0;36mdeserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1214\u001b[0m dtype \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mdtype\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[1;32m 1215\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39m\"\u001b[39m\u001b[39mdata\u001b[39m\u001b[39m\"\u001b[39m \u001b[39min\u001b[39;00m header:\n\u001b[0;32m-> 1216\u001b[0m data, frames \u001b[39m=\u001b[39m unpack(header[\u001b[39m\"\u001b[39m\u001b[39mdata\u001b[39m\u001b[39m\"\u001b[39m], frames)\n\u001b[1;32m 1217\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[1;32m 1218\u001b[0m data \u001b[39m=\u001b[39m \u001b[39mNone\u001b[39;00m\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py:1204\u001b[0m, in \u001b[0;36munpack\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1202\u001b[0m count \u001b[39m=\u001b[39m header[\u001b[39m\"\u001b[39m\u001b[39mframe_count\u001b[39m\u001b[39m\"\u001b[39m]\n\u001b[1;32m 1203\u001b[0m klass \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mtype-serialized\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[0;32m-> 1204\u001b[0m obj \u001b[39m=\u001b[39m klass\u001b[39m.\u001b[39mdeserialize(header, frames[:count])\n\u001b[1;32m 1205\u001b[0m \u001b[39mreturn\u001b[39;00m obj, frames[count:]\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py:574\u001b[0m, in \u001b[0;36mdeserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 567\u001b[0m \u001b[39m@classmethod\u001b[39m\n\u001b[1;32m 568\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mdeserialize\u001b[39m(\u001b[39mcls\u001b[39m, header: \u001b[39mdict\u001b[39m, frames: \u001b[39mlist\u001b[39m):\n\u001b[1;32m 569\u001b[0m \u001b[39m# TODO: because of the hack in `SpillableBuffer.serialize()` where\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 572\u001b[0m \u001b[39m# deserialize into `SpillableBufferSlice` when the frames hasn't been\u001b[39;00m\n\u001b[1;32m 573\u001b[0m \u001b[39m# copied.\u001b[39;00m\n\u001b[0;32m--> 574\u001b[0m \u001b[39mreturn\u001b[39;00m SpillableBuffer\u001b[39m.\u001b[39mdeserialize(header, frames)\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/buffer.py:335\u001b[0m, in \u001b[0;36mdeserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 332\u001b[0m \u001b[39mreturn\u001b[39;00m frame \u001b[39m# The frame is already deserialized\u001b[39;00m\n\u001b[1;32m 334\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mhasattr\u001b[39m(frame, \u001b[39m\"\u001b[39m\u001b[39m__cuda_array_interface__\u001b[39m\u001b[39m\"\u001b[39m):\n\u001b[0;32m--> 335\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mcls\u001b[39m\u001b[39m.\u001b[39m_from_device_memory(frame)\n\u001b[1;32m 336\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mcls\u001b[39m\u001b[39m.\u001b[39m_from_host_memory(frame)\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py:235\u001b[0m, in \u001b[0;36m_from_device_memory\u001b[0;34m()\u001b[0m\n\u001b[1;32m 218\u001b[0m \u001b[39m\u001b[39m\u001b[39m\"\"\"Create a spillabe buffer from device memory.\u001b[39;00m\n\u001b[1;32m 219\u001b[0m \n\u001b[1;32m 220\u001b[0m \u001b[39mNo data is being copied.\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 232\u001b[0m \u001b[39m Buffer representing the same device memory as `data`\u001b[39;00m\n\u001b[1;32m 233\u001b[0m \u001b[39m\"\"\"\u001b[39;00m\n\u001b[1;32m 234\u001b[0m ret \u001b[39m=\u001b[39m \u001b[39msuper\u001b[39m()\u001b[39m.\u001b[39m_from_device_memory(data)\n\u001b[0;32m--> 235\u001b[0m ret\u001b[39m.\u001b[39m_finalize_init(ptr_desc\u001b[39m=\u001b[39m{\u001b[39m\"\u001b[39m\u001b[39mtype\u001b[39m\u001b[39m\"\u001b[39m: \u001b[39m\"\u001b[39m\u001b[39mgpu\u001b[39m\u001b[39m\"\u001b[39m}, exposed\u001b[39m=\u001b[39mexposed)\n\u001b[1;32m 236\u001b[0m \u001b[39mreturn\u001b[39;00m ret\n", - "File \u001b[0;32m~/miniconda3/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py:206\u001b[0m, in \u001b[0;36m_finalize_init\u001b[0;34m()\u001b[0m\n\u001b[1;32m 204\u001b[0m manager \u001b[39m=\u001b[39m get_global_manager()\n\u001b[1;32m 205\u001b[0m \u001b[39mif\u001b[39;00m manager \u001b[39mis\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m--> 206\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mValueError\u001b[39;00m(\n\u001b[1;32m 207\u001b[0m \u001b[39mf\u001b[39m\u001b[39m\"\u001b[39m\u001b[39mcannot create \u001b[39m\u001b[39m{\u001b[39;00m\u001b[39mself\u001b[39m\u001b[39m.\u001b[39m\u001b[39m__class__\u001b[39m\u001b[39m}\u001b[39;00m\u001b[39m without \u001b[39m\u001b[39m\"\u001b[39m\n\u001b[1;32m 208\u001b[0m \u001b[39m\"\u001b[39m\u001b[39ma global spill manager\u001b[39m\u001b[39m\"\u001b[39m\n\u001b[1;32m 209\u001b[0m )\n\u001b[1;32m 211\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_manager \u001b[39m=\u001b[39m manager\n\u001b[1;32m 212\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_manager\u001b[39m.\u001b[39madd(\u001b[39mself\u001b[39m)\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cugraph/structure/graph_classes.py:309\u001b[0m, in \u001b[0;36mGraph.from_dask_cudf_edgelist\u001b[0;34m(self, input_ddf, source, destination, edge_attr, renumber, store_transposed, legacy_renum_only)\u001b[0m\n\u001b[1;32m 307\u001b[0m \u001b[39melif\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_Impl\u001b[39m.\u001b[39medgelist \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m 308\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mRuntimeError\u001b[39;00m(\u001b[39m\"\u001b[39m\u001b[39mGraph already has values\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[0;32m--> 309\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_Impl\u001b[39m.\u001b[39;49m_simpleDistributedGraphImpl__from_edgelist(\n\u001b[1;32m 310\u001b[0m input_ddf,\n\u001b[1;32m 311\u001b[0m source,\n\u001b[1;32m 312\u001b[0m destination,\n\u001b[1;32m 313\u001b[0m edge_attr,\n\u001b[1;32m 314\u001b[0m renumber,\n\u001b[1;32m 315\u001b[0m store_transposed,\n\u001b[1;32m 316\u001b[0m legacy_renum_only,\n\u001b[1;32m 317\u001b[0m )\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cugraph/structure/graph_implementation/simpleDistributedGraph.py:272\u001b[0m, in \u001b[0;36msimpleDistributedGraphImpl.__from_edgelist\u001b[0;34m(self, input_ddf, source, destination, edge_attr, renumber, store_transposed, legacy_renum_only)\u001b[0m\n\u001b[1;32m 268\u001b[0m dst_col_name \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mrenumber_map\u001b[39m.\u001b[39mrenumbered_dst_col_name\n\u001b[1;32m 270\u001b[0m ddf \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39medgelist\u001b[39m.\u001b[39medgelist_df\n\u001b[0;32m--> 272\u001b[0m num_edges \u001b[39m=\u001b[39m \u001b[39mlen\u001b[39;49m(ddf)\n\u001b[1;32m 273\u001b[0m edge_data \u001b[39m=\u001b[39m get_distributed_data(ddf)\n\u001b[1;32m 275\u001b[0m graph_props \u001b[39m=\u001b[39m GraphProperties(\n\u001b[1;32m 276\u001b[0m is_multigraph\u001b[39m=\u001b[39m\u001b[39mself\u001b[39m\u001b[39m.\u001b[39mproperties\u001b[39m.\u001b[39mmulti_edge,\n\u001b[1;32m 277\u001b[0m is_symmetric\u001b[39m=\u001b[39m\u001b[39mnot\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mproperties\u001b[39m.\u001b[39mdirected,\n\u001b[1;32m 278\u001b[0m )\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask/dataframe/core.py:4775\u001b[0m, in \u001b[0;36mDataFrame.__len__\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 4773\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39msuper\u001b[39m()\u001b[39m.\u001b[39m\u001b[39m__len__\u001b[39m()\n\u001b[1;32m 4774\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m-> 4775\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mlen\u001b[39;49m(s)\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask/dataframe/core.py:843\u001b[0m, in \u001b[0;36m_Frame.__len__\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 840\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39m__len__\u001b[39m(\u001b[39mself\u001b[39m):\n\u001b[1;32m 841\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mreduction(\n\u001b[1;32m 842\u001b[0m \u001b[39mlen\u001b[39;49m, np\u001b[39m.\u001b[39;49msum, token\u001b[39m=\u001b[39;49m\u001b[39m\"\u001b[39;49m\u001b[39mlen\u001b[39;49m\u001b[39m\"\u001b[39;49m, meta\u001b[39m=\u001b[39;49m\u001b[39mint\u001b[39;49m, split_every\u001b[39m=\u001b[39;49m\u001b[39mFalse\u001b[39;49;00m\n\u001b[0;32m--> 843\u001b[0m )\u001b[39m.\u001b[39;49mcompute()\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask/base.py:314\u001b[0m, in \u001b[0;36mDaskMethodsMixin.compute\u001b[0;34m(self, **kwargs)\u001b[0m\n\u001b[1;32m 290\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mcompute\u001b[39m(\u001b[39mself\u001b[39m, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mkwargs):\n\u001b[1;32m 291\u001b[0m \u001b[39m \u001b[39m\u001b[39m\"\"\"Compute this dask collection\u001b[39;00m\n\u001b[1;32m 292\u001b[0m \n\u001b[1;32m 293\u001b[0m \u001b[39m This turns a lazy Dask collection into its in-memory equivalent.\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 312\u001b[0m \u001b[39m dask.base.compute\u001b[39;00m\n\u001b[1;32m 313\u001b[0m \u001b[39m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 314\u001b[0m (result,) \u001b[39m=\u001b[39m compute(\u001b[39mself\u001b[39;49m, traverse\u001b[39m=\u001b[39;49m\u001b[39mFalse\u001b[39;49;00m, \u001b[39m*\u001b[39;49m\u001b[39m*\u001b[39;49mkwargs)\n\u001b[1;32m 315\u001b[0m \u001b[39mreturn\u001b[39;00m result\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/dask/base.py:599\u001b[0m, in \u001b[0;36mcompute\u001b[0;34m(traverse, optimize_graph, scheduler, get, *args, **kwargs)\u001b[0m\n\u001b[1;32m 596\u001b[0m keys\u001b[39m.\u001b[39mappend(x\u001b[39m.\u001b[39m__dask_keys__())\n\u001b[1;32m 597\u001b[0m postcomputes\u001b[39m.\u001b[39mappend(x\u001b[39m.\u001b[39m__dask_postcompute__())\n\u001b[0;32m--> 599\u001b[0m results \u001b[39m=\u001b[39m schedule(dsk, keys, \u001b[39m*\u001b[39;49m\u001b[39m*\u001b[39;49mkwargs)\n\u001b[1;32m 600\u001b[0m \u001b[39mreturn\u001b[39;00m repack([f(r, \u001b[39m*\u001b[39ma) \u001b[39mfor\u001b[39;00m r, (f, a) \u001b[39min\u001b[39;00m \u001b[39mzip\u001b[39m(results, postcomputes)])\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/client.py:3186\u001b[0m, in \u001b[0;36mClient.get\u001b[0;34m(self, dsk, keys, workers, allow_other_workers, resources, sync, asynchronous, direct, retries, priority, fifo_timeout, actors, **kwargs)\u001b[0m\n\u001b[1;32m 3184\u001b[0m should_rejoin \u001b[39m=\u001b[39m \u001b[39mFalse\u001b[39;00m\n\u001b[1;32m 3185\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[0;32m-> 3186\u001b[0m results \u001b[39m=\u001b[39m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mgather(packed, asynchronous\u001b[39m=\u001b[39;49masynchronous, direct\u001b[39m=\u001b[39;49mdirect)\n\u001b[1;32m 3187\u001b[0m \u001b[39mfinally\u001b[39;00m:\n\u001b[1;32m 3188\u001b[0m \u001b[39mfor\u001b[39;00m f \u001b[39min\u001b[39;00m futures\u001b[39m.\u001b[39mvalues():\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/client.py:2345\u001b[0m, in \u001b[0;36mClient.gather\u001b[0;34m(self, futures, errors, direct, asynchronous)\u001b[0m\n\u001b[1;32m 2343\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[1;32m 2344\u001b[0m local_worker \u001b[39m=\u001b[39m \u001b[39mNone\u001b[39;00m\n\u001b[0;32m-> 2345\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49msync(\n\u001b[1;32m 2346\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_gather,\n\u001b[1;32m 2347\u001b[0m futures,\n\u001b[1;32m 2348\u001b[0m errors\u001b[39m=\u001b[39;49merrors,\n\u001b[1;32m 2349\u001b[0m direct\u001b[39m=\u001b[39;49mdirect,\n\u001b[1;32m 2350\u001b[0m local_worker\u001b[39m=\u001b[39;49mlocal_worker,\n\u001b[1;32m 2351\u001b[0m asynchronous\u001b[39m=\u001b[39;49masynchronous,\n\u001b[1;32m 2352\u001b[0m )\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/utils.py:349\u001b[0m, in \u001b[0;36mSyncMethodMixin.sync\u001b[0;34m(self, func, asynchronous, callback_timeout, *args, **kwargs)\u001b[0m\n\u001b[1;32m 347\u001b[0m \u001b[39mreturn\u001b[39;00m future\n\u001b[1;32m 348\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m--> 349\u001b[0m \u001b[39mreturn\u001b[39;00m sync(\n\u001b[1;32m 350\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mloop, func, \u001b[39m*\u001b[39;49margs, callback_timeout\u001b[39m=\u001b[39;49mcallback_timeout, \u001b[39m*\u001b[39;49m\u001b[39m*\u001b[39;49mkwargs\n\u001b[1;32m 351\u001b[0m )\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/utils.py:416\u001b[0m, in \u001b[0;36msync\u001b[0;34m(loop, func, callback_timeout, *args, **kwargs)\u001b[0m\n\u001b[1;32m 414\u001b[0m \u001b[39mif\u001b[39;00m error:\n\u001b[1;32m 415\u001b[0m typ, exc, tb \u001b[39m=\u001b[39m error\n\u001b[0;32m--> 416\u001b[0m \u001b[39mraise\u001b[39;00m exc\u001b[39m.\u001b[39mwith_traceback(tb)\n\u001b[1;32m 417\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[1;32m 418\u001b[0m \u001b[39mreturn\u001b[39;00m result\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/utils.py:389\u001b[0m, in \u001b[0;36msync..f\u001b[0;34m()\u001b[0m\n\u001b[1;32m 387\u001b[0m future \u001b[39m=\u001b[39m wait_for(future, callback_timeout)\n\u001b[1;32m 388\u001b[0m future \u001b[39m=\u001b[39m asyncio\u001b[39m.\u001b[39mensure_future(future)\n\u001b[0;32m--> 389\u001b[0m result \u001b[39m=\u001b[39m \u001b[39myield\u001b[39;00m future\n\u001b[1;32m 390\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mException\u001b[39;00m:\n\u001b[1;32m 391\u001b[0m error \u001b[39m=\u001b[39m sys\u001b[39m.\u001b[39mexc_info()\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/tornado/gen.py:769\u001b[0m, in \u001b[0;36mRunner.run\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 766\u001b[0m exc_info \u001b[39m=\u001b[39m \u001b[39mNone\u001b[39;00m\n\u001b[1;32m 768\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[0;32m--> 769\u001b[0m value \u001b[39m=\u001b[39m future\u001b[39m.\u001b[39;49mresult()\n\u001b[1;32m 770\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mException\u001b[39;00m:\n\u001b[1;32m 771\u001b[0m exc_info \u001b[39m=\u001b[39m sys\u001b[39m.\u001b[39mexc_info()\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/client.py:2208\u001b[0m, in \u001b[0;36mClient._gather\u001b[0;34m(self, futures, errors, direct, local_worker)\u001b[0m\n\u001b[1;32m 2206\u001b[0m exc \u001b[39m=\u001b[39m CancelledError(key)\n\u001b[1;32m 2207\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m-> 2208\u001b[0m \u001b[39mraise\u001b[39;00m exception\u001b[39m.\u001b[39mwith_traceback(traceback)\n\u001b[1;32m 2209\u001b[0m \u001b[39mraise\u001b[39;00m exc\n\u001b[1;32m 2210\u001b[0m \u001b[39mif\u001b[39;00m errors \u001b[39m==\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mskip\u001b[39m\u001b[39m\"\u001b[39m:\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/contextlib.py:79\u001b[0m, in \u001b[0;36minner\u001b[0;34m()\u001b[0m\n\u001b[1;32m 76\u001b[0m \u001b[39m@wraps\u001b[39m(func)\n\u001b[1;32m 77\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39minner\u001b[39m(\u001b[39m*\u001b[39margs, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mkwds):\n\u001b[1;32m 78\u001b[0m \u001b[39mwith\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_recreate_cm():\n\u001b[0;32m---> 79\u001b[0m \u001b[39mreturn\u001b[39;00m func(\u001b[39m*\u001b[39margs, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mkwds)\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py:2937\u001b[0m, in \u001b[0;36m_deserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2934\u001b[0m \u001b[39m# Some objects require threadlocal state during deserialization, e.g. to\u001b[39;00m\n\u001b[1;32m 2935\u001b[0m \u001b[39m# detect the current worker\u001b[39;00m\n\u001b[1;32m 2936\u001b[0m \u001b[39mif\u001b[39;00m function \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m-> 2937\u001b[0m function \u001b[39m=\u001b[39m loads_function(function)\n\u001b[1;32m 2938\u001b[0m \u001b[39mif\u001b[39;00m args \u001b[39mand\u001b[39;00m \u001b[39misinstance\u001b[39m(args, \u001b[39mbytes\u001b[39m):\n\u001b[1;32m 2939\u001b[0m args \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(args)\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/worker.py:2925\u001b[0m, in \u001b[0;36mloads_function\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2923\u001b[0m result \u001b[39m=\u001b[39m cache_loads[bytes_object]\n\u001b[1;32m 2924\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mKeyError\u001b[39;00m:\n\u001b[0;32m-> 2925\u001b[0m result \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(bytes_object)\n\u001b[1;32m 2926\u001b[0m cache_loads[bytes_object] \u001b[39m=\u001b[39m result\n\u001b[1;32m 2927\u001b[0m \u001b[39mreturn\u001b[39;00m result\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/distributed/protocol/pickle.py:96\u001b[0m, in \u001b[0;36mloads\u001b[0;34m()\u001b[0m\n\u001b[1;32m 94\u001b[0m \u001b[39mreturn\u001b[39;00m pickle\u001b[39m.\u001b[39mloads(x, buffers\u001b[39m=\u001b[39mbuffers)\n\u001b[1;32m 95\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m---> 96\u001b[0m \u001b[39mreturn\u001b[39;00m pickle\u001b[39m.\u001b[39mloads(x)\n\u001b[1;32m 97\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mException\u001b[39;00m:\n\u001b[1;32m 98\u001b[0m logger\u001b[39m.\u001b[39minfo(\u001b[39m\"\u001b[39m\u001b[39mFailed to deserialize \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m\"\u001b[39m, x[:\u001b[39m10000\u001b[39m], exc_info\u001b[39m=\u001b[39m\u001b[39mTrue\u001b[39;00m)\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py:176\u001b[0m, in \u001b[0;36mhost_deserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 154\u001b[0m \u001b[39m\u001b[39m\u001b[39m\"\"\"Perform device-side deserialization tasks.\u001b[39;00m\n\u001b[1;32m 155\u001b[0m \n\u001b[1;32m 156\u001b[0m \u001b[39mParameters\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 170\u001b[0m \u001b[39m:meta private:\u001b[39;00m\n\u001b[1;32m 171\u001b[0m \u001b[39m\"\"\"\u001b[39;00m\n\u001b[1;32m 172\u001b[0m frames \u001b[39m=\u001b[39m [\n\u001b[1;32m 173\u001b[0m cudf\u001b[39m.\u001b[39mcore\u001b[39m.\u001b[39mbuffer\u001b[39m.\u001b[39mas_buffer(f) \u001b[39mif\u001b[39;00m c \u001b[39melse\u001b[39;00m f\n\u001b[1;32m 174\u001b[0m \u001b[39mfor\u001b[39;00m c, f \u001b[39min\u001b[39;00m \u001b[39mzip\u001b[39m(header[\u001b[39m\"\u001b[39m\u001b[39mis-cuda\u001b[39m\u001b[39m\"\u001b[39m], \u001b[39mmap\u001b[39m(\u001b[39mmemoryview\u001b[39m, frames))\n\u001b[1;32m 175\u001b[0m ]\n\u001b[0;32m--> 176\u001b[0m obj \u001b[39m=\u001b[39m \u001b[39mcls\u001b[39m\u001b[39m.\u001b[39mdevice_deserialize(header, frames)\n\u001b[1;32m 177\u001b[0m \u001b[39mreturn\u001b[39;00m obj\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/abc.py:130\u001b[0m, in \u001b[0;36mdevice_deserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 125\u001b[0m typ \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mtype-serialized\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[1;32m 126\u001b[0m frames \u001b[39m=\u001b[39m [\n\u001b[1;32m 127\u001b[0m cudf\u001b[39m.\u001b[39mcore\u001b[39m.\u001b[39mbuffer\u001b[39m.\u001b[39mas_buffer(f) \u001b[39mif\u001b[39;00m c \u001b[39melse\u001b[39;00m \u001b[39mmemoryview\u001b[39m(f)\n\u001b[1;32m 128\u001b[0m \u001b[39mfor\u001b[39;00m c, f \u001b[39min\u001b[39;00m \u001b[39mzip\u001b[39m(header[\u001b[39m\"\u001b[39m\u001b[39mis-cuda\u001b[39m\u001b[39m\"\u001b[39m], frames)\n\u001b[1;32m 129\u001b[0m ]\n\u001b[0;32m--> 130\u001b[0m \u001b[39mreturn\u001b[39;00m typ\u001b[39m.\u001b[39mdeserialize(header, frames)\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/dataframe.py:1019\u001b[0m, in \u001b[0;36mdeserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1016\u001b[0m \u001b[39m@classmethod\u001b[39m\n\u001b[1;32m 1017\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mdeserialize\u001b[39m(\u001b[39mcls\u001b[39m, header, frames):\n\u001b[1;32m 1018\u001b[0m index_nframes \u001b[39m=\u001b[39m header[\u001b[39m\"\u001b[39m\u001b[39mindex_frame_count\u001b[39m\u001b[39m\"\u001b[39m]\n\u001b[0;32m-> 1019\u001b[0m obj \u001b[39m=\u001b[39m \u001b[39msuper\u001b[39m()\u001b[39m.\u001b[39mdeserialize(\n\u001b[1;32m 1020\u001b[0m header, frames[header[\u001b[39m\"\u001b[39m\u001b[39mindex_frame_count\u001b[39m\u001b[39m\"\u001b[39m] :]\n\u001b[1;32m 1021\u001b[0m )\n\u001b[1;32m 1023\u001b[0m idx_typ \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mindex\u001b[39m\u001b[39m\"\u001b[39m][\u001b[39m\"\u001b[39m\u001b[39mtype-serialized\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[1;32m 1024\u001b[0m index \u001b[39m=\u001b[39m idx_typ\u001b[39m.\u001b[39mdeserialize(header[\u001b[39m\"\u001b[39m\u001b[39mindex\u001b[39m\u001b[39m\"\u001b[39m], frames[:index_nframes])\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/frame.py:106\u001b[0m, in \u001b[0;36mdeserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 104\u001b[0m cls_deserialize \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mtype-serialized\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[1;32m 105\u001b[0m column_names \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mcolumn_names\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[0;32m--> 106\u001b[0m columns \u001b[39m=\u001b[39m deserialize_columns(header[\u001b[39m\"\u001b[39m\u001b[39mcolumns\u001b[39m\u001b[39m\"\u001b[39m], frames)\n\u001b[1;32m 107\u001b[0m \u001b[39mreturn\u001b[39;00m cls_deserialize\u001b[39m.\u001b[39m_from_data(\u001b[39mdict\u001b[39m(\u001b[39mzip\u001b[39m(column_names, columns)))\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py:2450\u001b[0m, in \u001b[0;36mdeserialize_columns\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2448\u001b[0m col_frame_count \u001b[39m=\u001b[39m meta[\u001b[39m\"\u001b[39m\u001b[39mframe_count\u001b[39m\u001b[39m\"\u001b[39m]\n\u001b[1;32m 2449\u001b[0m col_typ \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(meta[\u001b[39m\"\u001b[39m\u001b[39mtype-serialized\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[0;32m-> 2450\u001b[0m colobj \u001b[39m=\u001b[39m col_typ\u001b[39m.\u001b[39mdeserialize(meta, frames[:col_frame_count])\n\u001b[1;32m 2451\u001b[0m columns\u001b[39m.\u001b[39mappend(colobj)\n\u001b[1;32m 2452\u001b[0m \u001b[39m# Advance frames\u001b[39;00m\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py:1216\u001b[0m, in \u001b[0;36mdeserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1214\u001b[0m dtype \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mdtype\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[1;32m 1215\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39m\"\u001b[39m\u001b[39mdata\u001b[39m\u001b[39m\"\u001b[39m \u001b[39min\u001b[39;00m header:\n\u001b[0;32m-> 1216\u001b[0m data, frames \u001b[39m=\u001b[39m unpack(header[\u001b[39m\"\u001b[39m\u001b[39mdata\u001b[39m\u001b[39m\"\u001b[39m], frames)\n\u001b[1;32m 1217\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[1;32m 1218\u001b[0m data \u001b[39m=\u001b[39m \u001b[39mNone\u001b[39;00m\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/column/column.py:1204\u001b[0m, in \u001b[0;36munpack\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1202\u001b[0m count \u001b[39m=\u001b[39m header[\u001b[39m\"\u001b[39m\u001b[39mframe_count\u001b[39m\u001b[39m\"\u001b[39m]\n\u001b[1;32m 1203\u001b[0m klass \u001b[39m=\u001b[39m pickle\u001b[39m.\u001b[39mloads(header[\u001b[39m\"\u001b[39m\u001b[39mtype-serialized\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[0;32m-> 1204\u001b[0m obj \u001b[39m=\u001b[39m klass\u001b[39m.\u001b[39mdeserialize(header, frames[:count])\n\u001b[1;32m 1205\u001b[0m \u001b[39mreturn\u001b[39;00m obj, frames[count:]\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py:574\u001b[0m, in \u001b[0;36mdeserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 567\u001b[0m \u001b[39m@classmethod\u001b[39m\n\u001b[1;32m 568\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mdeserialize\u001b[39m(\u001b[39mcls\u001b[39m, header: \u001b[39mdict\u001b[39m, frames: \u001b[39mlist\u001b[39m):\n\u001b[1;32m 569\u001b[0m \u001b[39m# TODO: because of the hack in `SpillableBuffer.serialize()` where\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 572\u001b[0m \u001b[39m# deserialize into `SpillableBufferSlice` when the frames hasn't been\u001b[39;00m\n\u001b[1;32m 573\u001b[0m \u001b[39m# copied.\u001b[39;00m\n\u001b[0;32m--> 574\u001b[0m \u001b[39mreturn\u001b[39;00m SpillableBuffer\u001b[39m.\u001b[39mdeserialize(header, frames)\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/buffer.py:335\u001b[0m, in \u001b[0;36mdeserialize\u001b[0;34m()\u001b[0m\n\u001b[1;32m 332\u001b[0m \u001b[39mreturn\u001b[39;00m frame \u001b[39m# The frame is already deserialized\u001b[39;00m\n\u001b[1;32m 334\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mhasattr\u001b[39m(frame, \u001b[39m\"\u001b[39m\u001b[39m__cuda_array_interface__\u001b[39m\u001b[39m\"\u001b[39m):\n\u001b[0;32m--> 335\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mcls\u001b[39m\u001b[39m.\u001b[39m_from_device_memory(frame)\n\u001b[1;32m 336\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mcls\u001b[39m\u001b[39m.\u001b[39m_from_host_memory(frame)\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py:235\u001b[0m, in \u001b[0;36m_from_device_memory\u001b[0;34m()\u001b[0m\n\u001b[1;32m 218\u001b[0m \u001b[39m\u001b[39m\u001b[39m\"\"\"Create a spillabe buffer from device memory.\u001b[39;00m\n\u001b[1;32m 219\u001b[0m \n\u001b[1;32m 220\u001b[0m \u001b[39mNo data is being copied.\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 232\u001b[0m \u001b[39m Buffer representing the same device memory as `data`\u001b[39;00m\n\u001b[1;32m 233\u001b[0m \u001b[39m\"\"\"\u001b[39;00m\n\u001b[1;32m 234\u001b[0m ret \u001b[39m=\u001b[39m \u001b[39msuper\u001b[39m()\u001b[39m.\u001b[39m_from_device_memory(data)\n\u001b[0;32m--> 235\u001b[0m ret\u001b[39m.\u001b[39m_finalize_init(ptr_desc\u001b[39m=\u001b[39m{\u001b[39m\"\u001b[39m\u001b[39mtype\u001b[39m\u001b[39m\"\u001b[39m: \u001b[39m\"\u001b[39m\u001b[39mgpu\u001b[39m\u001b[39m\"\u001b[39m}, exposed\u001b[39m=\u001b[39mexposed)\n\u001b[1;32m 236\u001b[0m \u001b[39mreturn\u001b[39;00m ret\n", + "File \u001b[0;32m~/miniforge/envs/cugraph_0411/lib/python3.10/site-packages/cudf/core/buffer/spillable_buffer.py:206\u001b[0m, in \u001b[0;36m_finalize_init\u001b[0;34m()\u001b[0m\n\u001b[1;32m 204\u001b[0m manager \u001b[39m=\u001b[39m get_global_manager()\n\u001b[1;32m 205\u001b[0m \u001b[39mif\u001b[39;00m manager \u001b[39mis\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m--> 206\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mValueError\u001b[39;00m(\n\u001b[1;32m 207\u001b[0m \u001b[39mf\u001b[39m\u001b[39m\"\u001b[39m\u001b[39mcannot create \u001b[39m\u001b[39m{\u001b[39;00m\u001b[39mself\u001b[39m\u001b[39m.\u001b[39m\u001b[39m__class__\u001b[39m\u001b[39m}\u001b[39;00m\u001b[39m without \u001b[39m\u001b[39m\"\u001b[39m\n\u001b[1;32m 208\u001b[0m \u001b[39m\"\u001b[39m\u001b[39ma global spill manager\u001b[39m\u001b[39m\"\u001b[39m\n\u001b[1;32m 209\u001b[0m )\n\u001b[1;32m 211\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_manager \u001b[39m=\u001b[39m manager\n\u001b[1;32m 212\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_manager\u001b[39m.\u001b[39madd(\u001b[39mself\u001b[39m)\n", "\u001b[0;31mValueError\u001b[0m: cannot create without a global spill manager" ] } From 59c6b33d474028444a119684015fb03d18562027 Mon Sep 17 00:00:00 2001 From: Ray Douglass Date: Thu, 19 Sep 2024 12:06:39 -0400 Subject: [PATCH 12/21] DOC v24.12 Updates [skip ci] --- VERSION | 2 +- python/nx-cugraph/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 7c7ba0443..af28c42b5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -24.10.00 +24.12.00 diff --git a/python/nx-cugraph/pyproject.toml b/python/nx-cugraph/pyproject.toml index e7b4ea44d..239d894f5 100644 --- a/python/nx-cugraph/pyproject.toml +++ b/python/nx-cugraph/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "cupy-cuda11x>=12.0.0", "networkx>=3.0", "numpy>=1.23,<2.0a0", - "pylibcugraph==24.10.*,>=0.0.0a0", + "pylibcugraph==24.12.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [project.optional-dependencies] From 4f68f963fbed0b0989c77f83a870a4e98d0b117f Mon Sep 17 00:00:00 2001 From: Rick Ratzel <3039903+rlratzel@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:12:30 -0500 Subject: [PATCH 13/21] Drops duplicate edges in non-MultiGraph PLC `SGGraph` instances (#4658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Graph input with duplicate edges intended for `Graph`/`DiGraph` instances resulted in internal PLC `SGGraph` instances with duplicate edges, which were effectively treated as MultiGraphs and caused incorrect results from algorithms like `pagerank`. This PR sets the `drop_multi_edges` PLC `SGGraph` ctor option to have PLC remove duplicate edges on `SGGraph` creation. The overhead to drop duplicate edges for non-MultiGraphs is negligible, and in the case of a large test graph (wikipedia data, 37.5M nodes, 464.5M edges) resulted in an overall _speedup_ for pagerank going from 12.2 seconds to 10.7 seconds on my workstation, likely due to fewer edges to process a minor slowdown from 10.5s to 10.7s. _edit: after several re-runs, the pagerank runtime before the change settled to 10.5, and the runtime after the change was typically 10.7._ A test was added that uses pagerank to ensure Graphs vs. MultiGraphs are handled correctly and duplicate edges are dropped as needed. The results when run without `drop_multi_edges` set: ``` > assert actual_pr_for_G == approx(expected_pr_for_G) E assert {0: 0.0875795...7955580949783} == approx({0: 0....32 ± 1.8e-07}) E E comparison failed. Mismatched elements: 4 / 4: E Max absolute difference: 0.08785887916592061 E Max relative difference: 0.5007959662968462 E Index | Obtained | Expected E 0 | 0.08757955580949783 | 0.17543839772251532 ± 1.8e-07 E 1 | 0.41242048144340515 | 0.32456160227748454 ± 3.2e-07 E 2 | 0.41242048144340515 | 0.32456160227748454 ± 3.2e-07 E 3 | 0.08757955580949783 | 0.17543839772251532 ± 1.8e-07 ``` The same test passes when run with the changes in this PR to set `drop_multi_edges`. Authors: - Rick Ratzel (https://github.com/rlratzel) Approvers: - Erik Welch (https://github.com/eriknw) URL: https://github.com/rapidsai/cugraph/pull/4658 --- python/nx-cugraph/nx_cugraph/classes/graph.py | 9 +++++ .../nx_cugraph/tests/test_pagerank.py | 36 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 python/nx-cugraph/nx_cugraph/tests/test_pagerank.py diff --git a/python/nx-cugraph/nx_cugraph/classes/graph.py b/python/nx-cugraph/nx_cugraph/classes/graph.py index 7425eacb2..7c01365c0 100644 --- a/python/nx-cugraph/nx_cugraph/classes/graph.py +++ b/python/nx-cugraph/nx_cugraph/classes/graph.py @@ -689,6 +689,14 @@ def _get_plc_graph( src_indices = src_indices.astype(index_dtype) dst_indices = dst_indices.astype(index_dtype) + # This sets drop_multi_edges=True for non-multigraph input, which means + # the data in self.src_indices and self.dst_indices may not be + # identical to that contained in the returned pcl.SGGraph (the returned + # SGGraph may have fewer edges since duplicates are dropped). Ideally + # self.src_indices and self.dst_indices would be updated to have + # duplicate edges removed for non-multigraph instances, but that + # requires additional code which would be redundant and likely not as + # performant as the code in PLC. return plc.SGGraph( resource_handle=plc.ResourceHandle(), graph_properties=plc.GraphProperties( @@ -702,6 +710,7 @@ def _get_plc_graph( renumber=False, do_expensive_check=False, vertices_array=self._node_ids, + drop_multi_edges=not self.is_multigraph(), ) def _sort_edge_indices(self, primary="src"): diff --git a/python/nx-cugraph/nx_cugraph/tests/test_pagerank.py b/python/nx-cugraph/nx_cugraph/tests/test_pagerank.py new file mode 100644 index 000000000..0b437df2d --- /dev/null +++ b/python/nx-cugraph/nx_cugraph/tests/test_pagerank.py @@ -0,0 +1,36 @@ +# Copyright (c) 2024, 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 networkx as nx +import pandas as pd +from pytest import approx + + +def test_pagerank_multigraph(): + """ + Ensures correct differences between pagerank results for Graphs + vs. MultiGraphs generated using from_pandas_edgelist() + """ + df = pd.DataFrame({"source": [0, 1, 1, 1, 1, 1, 1, 2], + "target": [1, 2, 2, 2, 2, 2, 2, 3]}) + expected_pr_for_G = nx.pagerank(nx.from_pandas_edgelist(df)) + expected_pr_for_MultiG = nx.pagerank( + nx.from_pandas_edgelist(df, create_using=nx.MultiGraph)) + + G = nx.from_pandas_edgelist(df, backend="cugraph") + actual_pr_for_G = nx.pagerank(G, backend="cugraph") + + MultiG = nx.from_pandas_edgelist(df, create_using=nx.MultiGraph, backend="cugraph") + actual_pr_for_MultiG = nx.pagerank(MultiG, backend="cugraph") + + assert actual_pr_for_G == approx(expected_pr_for_G) + assert actual_pr_for_MultiG == approx(expected_pr_for_MultiG) From 32b2b833197f1b003c277ef03cbb890c5ae260b9 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 24 Sep 2024 17:20:07 -0500 Subject: [PATCH 14/21] nx-cugraph: Updates nxcg.Graph classes for API-compatibility with NetworkX Graph classes, needed for zero code change graph generators (#4629) This is an alternative approach to #4558 for enabling GPU-accelerated NetworkX to "just work". It has similarities to #4558. I opted to make separate classes such as `ZeroGraph`, which I think makes for cleaner separation and gives us and users more control. There are a few lingering TODOs and code comments to tidy up, but I don't think there are any show-stoppers. I have not updated methods (such as `number_of_nodes`) to optimistically try to use GPU if possible, b/c this is not strictly necessary, but we should update these soon. I have run NetworkX tests with these classes using https://github.com/networkx/networkx/pull/7585 and https://github.com/networkx/networkx/pull/7600. We need the behavior in 7585, and 7600 is only useful for testing. There are only 3 new failing tests, and 3 tests that hang (I'll run them overnight to see if they finish). Here's a test summary: ``` 5548 passed, 24 skipped, 16 xfailed, 25 xpassed ``` Note that 25 tests that were failing now pass. I have not investigated test failures, xfails, or xpasses yet. I would like to add tests too. We rely heavily on the networkx cache. I think this is preferred. It is late for me. I will describe and show how and why this works later. I opted for `zero=` and `ZeroGraph`, because I find them delightful! Renaming is trivial if other terms are preferred. CC @quasiben Authors: - Erik Welch (https://github.com/eriknw) Approvers: - Bradley Dice (https://github.com/bdice) - Rick Ratzel (https://github.com/rlratzel) URL: https://github.com/rapidsai/cugraph/pull/4629 --- python/nx-cugraph/_nx_cugraph/__init__.py | 17 +- python/nx-cugraph/lint.yaml | 16 +- python/nx-cugraph/nx_cugraph/__init__.py | 14 +- .../algorithms/bipartite/generators.py | 3 +- .../algorithms/community/louvain.py | 6 +- .../nx-cugraph/nx_cugraph/algorithms/core.py | 7 +- .../algorithms/link_analysis/hits_alg.py | 3 +- .../nx_cugraph/algorithms/operators/unary.py | 12 +- .../algorithms/shortest_paths/generic.py | 5 +- .../algorithms/shortest_paths/unweighted.py | 5 +- .../traversal/breadth_first_search.py | 13 +- .../nx-cugraph/nx_cugraph/classes/__init__.py | 10 +- .../nx-cugraph/nx_cugraph/classes/digraph.py | 89 +++- python/nx-cugraph/nx_cugraph/classes/graph.py | 399 +++++++++++++++--- .../nx_cugraph/classes/multidigraph.py | 35 +- .../nx_cugraph/classes/multigraph.py | 152 ++++--- python/nx-cugraph/nx_cugraph/convert.py | 132 +++++- .../nx-cugraph/nx_cugraph/convert_matrix.py | 8 +- .../nx_cugraph/generators/_utils.py | 16 +- .../nx_cugraph/generators/classic.py | 7 +- .../nx_cugraph/generators/community.py | 7 +- .../nx-cugraph/nx_cugraph/generators/ego.py | 11 +- .../nx-cugraph/nx_cugraph/generators/small.py | 10 +- .../nx_cugraph/generators/social.py | 29 +- python/nx-cugraph/nx_cugraph/interface.py | 248 ++++++----- python/nx-cugraph/nx_cugraph/relabel.py | 17 +- .../nx-cugraph/nx_cugraph/tests/test_bfs.py | 5 +- .../nx_cugraph/tests/test_classes.py | 77 ++++ .../nx_cugraph/tests/test_cluster.py | 5 +- .../nx_cugraph/tests/test_convert.py | 3 - .../nx_cugraph/tests/test_ego_graph.py | 36 +- .../nx_cugraph/tests/test_generators.py | 42 +- .../nx_cugraph/tests/test_graph_methods.py | 4 +- .../nx_cugraph/tests/test_match_api.py | 3 - .../nx_cugraph/tests/test_multigraph.py | 6 +- .../nx_cugraph/tests/test_pagerank.py | 20 +- .../nx_cugraph/tests/testing_utils.py | 2 +- .../nx-cugraph/nx_cugraph/utils/decorators.py | 21 +- python/nx-cugraph/nx_cugraph/utils/misc.py | 36 +- python/nx-cugraph/pyproject.toml | 4 +- python/nx-cugraph/run_nx_tests.sh | 4 + 41 files changed, 1192 insertions(+), 347 deletions(-) create mode 100644 python/nx-cugraph/nx_cugraph/tests/test_classes.py diff --git a/python/nx-cugraph/_nx_cugraph/__init__.py b/python/nx-cugraph/_nx_cugraph/__init__.py index 41c18c27e..428d266dd 100644 --- a/python/nx-cugraph/_nx_cugraph/__init__.py +++ b/python/nx-cugraph/_nx_cugraph/__init__.py @@ -22,6 +22,7 @@ $ python _nx_cugraph/__init__.py """ +import os from _nx_cugraph._version import __version__ @@ -293,12 +294,20 @@ def get_info(): for key in info_keys: del d[key] + + d["default_config"] = { + "use_compat_graphs": os.environ.get("NX_CUGRAPH_USE_COMPAT_GRAPHS", "true") + .strip() + .lower() + == "true", + } return d -def _check_networkx_version(): - import warnings +def _check_networkx_version() -> tuple[int, int]: + """Check the version of networkx and return ``(major, minor)`` version tuple.""" import re + import warnings import networkx as nx @@ -321,6 +330,10 @@ def _check_networkx_version(): f"{nx.__version__}. Please upgrade (or fix) your Python environment." ) + nxver_major = int(version_major) + nxver_minor = int(re.match(r"^\d+", version_minor).group()) + return (nxver_major, nxver_minor) + if __name__ == "__main__": from pathlib import Path diff --git a/python/nx-cugraph/lint.yaml b/python/nx-cugraph/lint.yaml index b2184a185..dab2ea70e 100644 --- a/python/nx-cugraph/lint.yaml +++ b/python/nx-cugraph/lint.yaml @@ -26,7 +26,7 @@ repos: - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.18 + rev: v0.19 hooks: - id: validate-pyproject name: Validate pyproject.toml @@ -40,29 +40,29 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.17.0 hooks: - id: pyupgrade args: [--py310-plus] - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black # - id: black-jupyter - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.4 + rev: v0.6.7 hooks: - id: ruff args: [--fix-only, --show-fixes] # --unsafe-fixes] - repo: https://github.com/PyCQA/flake8 - rev: 7.1.0 + rev: 7.1.1 hooks: - id: flake8 args: ['--per-file-ignores=_nx_cugraph/__init__.py:E501', '--extend-ignore=B020,SIM105'] # Why is this necessary? additional_dependencies: &flake8_dependencies # These versions need updated manually - - flake8==7.1.0 - - flake8-bugbear==24.4.26 + - flake8==7.1.1 + - flake8-bugbear==24.8.19 - flake8-simplify==0.21.0 - repo: https://github.com/asottile/yesqa rev: v1.5.0 @@ -77,7 +77,7 @@ repos: additional_dependencies: [tomli] files: ^(nx_cugraph|docs)/ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.4 + rev: v0.6.7 hooks: - id: ruff - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/python/nx-cugraph/nx_cugraph/__init__.py b/python/nx-cugraph/nx_cugraph/__init__.py index 542256fa7..4404e57f6 100644 --- a/python/nx-cugraph/nx_cugraph/__init__.py +++ b/python/nx-cugraph/nx_cugraph/__init__.py @@ -12,6 +12,11 @@ # limitations under the License. from networkx.exception import * +from _nx_cugraph._version import __git_commit__, __version__ +from _nx_cugraph import _check_networkx_version + +_nxver: tuple[int, int] = _check_networkx_version() + from . import utils from . import classes @@ -32,7 +37,10 @@ from . import algorithms from .algorithms import * -from _nx_cugraph._version import __git_commit__, __version__ -from _nx_cugraph import _check_networkx_version +from .interface import BackendInterface -_check_networkx_version() +BackendInterface.Graph = classes.Graph +BackendInterface.DiGraph = classes.DiGraph +BackendInterface.MultiGraph = classes.MultiGraph +BackendInterface.MultiDiGraph = classes.MultiDiGraph +del BackendInterface diff --git a/python/nx-cugraph/nx_cugraph/algorithms/bipartite/generators.py b/python/nx-cugraph/nx_cugraph/algorithms/bipartite/generators.py index 60276b7d4..214970235 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/bipartite/generators.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/bipartite/generators.py @@ -16,6 +16,7 @@ import networkx as nx import numpy as np +from nx_cugraph import _nxver from nx_cugraph.generators._utils import _create_using_class, _number_and_nodes from nx_cugraph.utils import index_dtype, networkx_algorithm @@ -48,7 +49,7 @@ def complete_bipartite_graph(n1, n2, create_using=None): nodes.extend(range(n2) if nodes2 is None else nodes2) if len(set(nodes)) != len(nodes): raise nx.NetworkXError("Inputs n1 and n2 must contain distinct nodes") - if nx.__version__[:3] <= "3.3": + if _nxver <= (3, 3): name = f"complete_bipartite_graph({orig_n1}, {orig_n2})" else: name = f"complete_bipartite_graph({n1}, {n2})" diff --git a/python/nx-cugraph/nx_cugraph/algorithms/community/louvain.py b/python/nx-cugraph/nx_cugraph/algorithms/community/louvain.py index ea1318060..52c512c45 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/community/louvain.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/community/louvain.py @@ -12,9 +12,9 @@ # limitations under the License. import warnings -import networkx as nx import pylibcugraph as plc +from nx_cugraph import _nxver from nx_cugraph.convert import _to_undirected_graph from nx_cugraph.utils import ( _dtype_param, @@ -27,7 +27,7 @@ __all__ = ["louvain_communities"] # max_level argument was added to NetworkX 3.3 -if nx.__version__[:3] <= "3.2": +if _nxver <= (3, 2): _max_level_param = { "max_level : int, optional": ( "Upper limit of the number of macro-iterations (max: 500)." @@ -81,7 +81,7 @@ def _louvain_communities( node_ids, clusters, modularity = plc.louvain( resource_handle=plc.ResourceHandle(), graph=G._get_plc_graph(weight, 1, dtype), - max_level=max_level, # TODO: add this parameter to NetworkX + max_level=max_level, threshold=threshold, resolution=resolution, do_expensive_check=False, diff --git a/python/nx-cugraph/nx_cugraph/algorithms/core.py b/python/nx-cugraph/nx_cugraph/algorithms/core.py index 8eb9a9946..e69ee88a1 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/core.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/core.py @@ -15,6 +15,7 @@ import pylibcugraph as plc import nx_cugraph as nxcg +from nx_cugraph import _nxver from nx_cugraph.convert import _to_undirected_graph from nx_cugraph.utils import ( _get_int_dtype, @@ -58,9 +59,12 @@ def _(G): @networkx_algorithm(is_incomplete=True, version_added="23.12", _plc="k_truss_subgraph") def k_truss(G, k): if is_nx := isinstance(G, nx.Graph): + is_compat_graph = isinstance(G, nxcg.Graph) G = nxcg.from_networkx(G, preserve_all_attrs=True) + else: + is_compat_graph = False if nxcg.number_of_selfloops(G) > 0: - if nx.__version__[:3] <= "3.2": + if _nxver <= (3, 2): exc_class = nx.NetworkXError else: exc_class = nx.NetworkXNotImplemented @@ -128,6 +132,7 @@ def k_truss(G, k): node_values, node_masks, key_to_id=key_to_id, + use_compat_graph=is_compat_graph, ) new_graph.graph.update(G.graph) return new_graph diff --git a/python/nx-cugraph/nx_cugraph/algorithms/link_analysis/hits_alg.py b/python/nx-cugraph/nx_cugraph/algorithms/link_analysis/hits_alg.py index e529b83ab..cc59fd5eb 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/link_analysis/hits_alg.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/link_analysis/hits_alg.py @@ -15,6 +15,7 @@ import numpy as np import pylibcugraph as plc +from nx_cugraph import _nxver from nx_cugraph.convert import _to_graph from nx_cugraph.utils import ( _dtype_param, @@ -53,7 +54,7 @@ def hits( if nstart is not None: nstart = G._dict_to_nodearray(nstart, 0, dtype) if max_iter <= 0: - if nx.__version__[:3] <= "3.2": + if _nxver <= (3, 2): raise ValueError("`maxiter` must be a positive integer.") raise nx.PowerIterationFailedConvergence(max_iter) try: diff --git a/python/nx-cugraph/nx_cugraph/algorithms/operators/unary.py b/python/nx-cugraph/nx_cugraph/algorithms/operators/unary.py index f53b34589..75dc5fbc7 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/operators/unary.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/operators/unary.py @@ -23,6 +23,7 @@ @networkx_algorithm(version_added="24.02") def complement(G): + is_compat_graph = isinstance(G, nxcg.Graph) G = _to_graph(G) N = G._N # Upcast to int64 so indices don't overflow. @@ -43,6 +44,7 @@ def complement(G): src_indices.astype(index_dtype), dst_indices.astype(index_dtype), key_to_id=G.key_to_id, + use_compat_graph=is_compat_graph, ) @@ -51,10 +53,16 @@ def reverse(G, copy=True): if not G.is_directed(): raise nx.NetworkXError("Cannot reverse an undirected graph.") if isinstance(G, nx.Graph): - if not copy: + is_compat_graph = isinstance(G, nxcg.Graph) + if not copy and not is_compat_graph: raise RuntimeError( "Using `copy=False` is invalid when using a NetworkX graph " "as input to `nx_cugraph.reverse`" ) G = nxcg.from_networkx(G, preserve_all_attrs=True) - return G.reverse(copy=copy) + else: + is_compat_graph = False + rv = G.reverse(copy=copy) + if is_compat_graph: + return rv._to_compat_graph() + return rv diff --git a/python/nx-cugraph/nx_cugraph/algorithms/shortest_paths/generic.py b/python/nx-cugraph/nx_cugraph/algorithms/shortest_paths/generic.py index 7d6d77f34..ab3c72143 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/shortest_paths/generic.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/shortest_paths/generic.py @@ -14,6 +14,7 @@ import numpy as np import nx_cugraph as nxcg +from nx_cugraph import _nxver from nx_cugraph.convert import _to_graph from nx_cugraph.utils import _dtype_param, _get_float_dtype, networkx_algorithm @@ -57,7 +58,7 @@ def shortest_path( paths = nxcg.all_pairs_dijkstra_path(G, weight=weight, dtype=dtype) else: # method == 'bellman-ford': paths = nxcg.all_pairs_bellman_ford_path(G, weight=weight, dtype=dtype) - if nx.__version__[:3] <= "3.4": + if _nxver <= (3, 4): paths = dict(paths) # To target elif method == "unweighted": @@ -129,7 +130,7 @@ def shortest_path_length( # To target elif method == "unweighted": lengths = nxcg.single_target_shortest_path_length(G, target) - if nx.__version__[:3] <= "3.4": + if _nxver <= (3, 4): lengths = dict(lengths) elif method == "dijkstra": lengths = nxcg.single_source_dijkstra_path_length( diff --git a/python/nx-cugraph/nx_cugraph/algorithms/shortest_paths/unweighted.py b/python/nx-cugraph/nx_cugraph/algorithms/shortest_paths/unweighted.py index 0e98c366e..e9c515632 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/shortest_paths/unweighted.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/shortest_paths/unweighted.py @@ -17,6 +17,7 @@ import numpy as np import pylibcugraph as plc +from nx_cugraph import _nxver from nx_cugraph.convert import _to_graph from nx_cugraph.utils import _groupby, index_dtype, networkx_algorithm @@ -43,7 +44,7 @@ def single_source_shortest_path_length(G, source, cutoff=None): def single_target_shortest_path_length(G, target, cutoff=None): G = _to_graph(G) rv = _bfs(G, target, cutoff, "Target", return_type="length") - if nx.__version__[:3] <= "3.4": + if _nxver <= (3, 4): return iter(rv.items()) return rv @@ -61,7 +62,7 @@ def bidirectional_shortest_path(G, source, target): # TODO PERF: do bidirectional traversal in core G = _to_graph(G) if source not in G or target not in G: - if nx.__version__[:3] <= "3.3": + if _nxver <= (3, 3): raise nx.NodeNotFound( f"Either source {source} or target {target} is not in G" ) diff --git a/python/nx-cugraph/nx_cugraph/algorithms/traversal/breadth_first_search.py b/python/nx-cugraph/nx_cugraph/algorithms/traversal/breadth_first_search.py index 5e4466d7d..72d0079cf 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/traversal/breadth_first_search.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/traversal/breadth_first_search.py @@ -18,6 +18,7 @@ import pylibcugraph as plc import nx_cugraph as nxcg +from nx_cugraph import _nxver from nx_cugraph.convert import _to_graph from nx_cugraph.utils import _groupby, index_dtype, networkx_algorithm @@ -57,7 +58,7 @@ def _bfs(G, source, *, depth_limit=None, reverse=False): return distances[mask], predecessors[mask], node_ids[mask] -if nx.__version__[:3] <= "3.3": +if _nxver <= (3, 3): @networkx_algorithm(is_incomplete=True, version_added="24.02", _plc="bfs") def generic_bfs_edges( @@ -132,13 +133,15 @@ def bfs_tree(G, source, reverse=False, depth_limit=None, sort_neighbors=None): raise NotImplementedError( "sort_neighbors argument in bfs_tree is not currently supported" ) + is_compat_graph = isinstance(G, nxcg.Graph) G = _check_G_and_source(G, source) if depth_limit is not None and depth_limit < 1: - return nxcg.DiGraph.from_coo( + return nxcg.CudaDiGraph.from_coo( 1, cp.array([], dtype=index_dtype), cp.array([], dtype=index_dtype), id_to_key=[source], + use_compat_graph=is_compat_graph, ) distances, predecessors, node_ids = _bfs( @@ -148,11 +151,12 @@ def bfs_tree(G, source, reverse=False, depth_limit=None, sort_neighbors=None): reverse=reverse, ) if predecessors.size == 0: - return nxcg.DiGraph.from_coo( + return nxcg.CudaDiGraph.from_coo( 1, cp.array([], dtype=index_dtype), cp.array([], dtype=index_dtype), id_to_key=[source], + use_compat_graph=is_compat_graph, ) # TODO: create renumbering helper function(s) unique_node_ids = cp.unique(cp.hstack((predecessors, node_ids))) @@ -170,11 +174,12 @@ def bfs_tree(G, source, reverse=False, depth_limit=None, sort_neighbors=None): old_index: new_index for new_index, old_index in enumerate(unique_node_ids.tolist()) } - return nxcg.DiGraph.from_coo( + return nxcg.CudaDiGraph.from_coo( unique_node_ids.size, src_indices, dst_indices, key_to_id=key_to_id, + use_compat_graph=is_compat_graph, ) diff --git a/python/nx-cugraph/nx_cugraph/classes/__init__.py b/python/nx-cugraph/nx_cugraph/classes/__init__.py index 19a5357da..71168e536 100644 --- a/python/nx-cugraph/nx_cugraph/classes/__init__.py +++ b/python/nx-cugraph/nx_cugraph/classes/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. +# Copyright (c) 2023-2024, 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 @@ -10,9 +10,9 @@ # 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 .graph import Graph -from .digraph import DiGraph -from .multigraph import MultiGraph -from .multidigraph import MultiDiGraph +from .graph import CudaGraph, Graph +from .digraph import CudaDiGraph, DiGraph +from .multigraph import CudaMultiGraph, MultiGraph +from .multidigraph import CudaMultiDiGraph, MultiDiGraph from .function import * diff --git a/python/nx-cugraph/nx_cugraph/classes/digraph.py b/python/nx-cugraph/nx_cugraph/classes/digraph.py index e5cfb8f68..178bf44f1 100644 --- a/python/nx-cugraph/nx_cugraph/classes/digraph.py +++ b/python/nx-cugraph/nx_cugraph/classes/digraph.py @@ -18,34 +18,108 @@ import cupy as cp import networkx as nx import numpy as np +from networkx.classes.digraph import ( + _CachedPropertyResetterAdjAndSucc, + _CachedPropertyResetterPred, +) import nx_cugraph as nxcg from ..utils import index_dtype -from .graph import Graph +from .graph import CudaGraph, Graph if TYPE_CHECKING: # pragma: no cover from nx_cugraph.typing import AttrKey -__all__ = ["DiGraph"] +__all__ = ["CudaDiGraph", "DiGraph"] networkx_api = nxcg.utils.decorators.networkx_class(nx.DiGraph) -class DiGraph(Graph): - ################# - # Class methods # - ################# +class DiGraph(nx.DiGraph, Graph): + _nx_attrs = ("_node", "_adj", "_succ", "_pred") + + name = Graph.name + _node = Graph._node + + @property + @networkx_api + def _adj(self): + if (adj := self.__dict__["_adj"]) is None: + self._reify_networkx() + adj = self.__dict__["_adj"] + return adj + + @_adj.setter + def _adj(self, val): + self._prepare_setter() + _CachedPropertyResetterAdjAndSucc.__set__(None, self, val) + if cache := getattr(self, "__networkx_cache__", None): + cache.clear() + + @property + @networkx_api + def _succ(self): + if (succ := self.__dict__["_succ"]) is None: + self._reify_networkx() + succ = self.__dict__["_succ"] + return succ + + @_succ.setter + def _succ(self, val): + self._prepare_setter() + _CachedPropertyResetterAdjAndSucc.__set__(None, self, val) + if cache := getattr(self, "__networkx_cache__", None): + cache.clear() + + @property + @networkx_api + def _pred(self): + if (pred := self.__dict__["_pred"]) is None: + self._reify_networkx() + pred = self.__dict__["_pred"] + return pred + + @_pred.setter + def _pred(self, val): + self._prepare_setter() + _CachedPropertyResetterPred.__set__(None, self, val) + if cache := getattr(self, "__networkx_cache__", None): + cache.clear() @classmethod @networkx_api def is_directed(cls) -> bool: return True + @classmethod + @networkx_api + def is_multigraph(cls) -> bool: + return False + + @classmethod + def to_cudagraph_class(cls) -> type[CudaDiGraph]: + return CudaDiGraph + @classmethod def to_networkx_class(cls) -> type[nx.DiGraph]: return nx.DiGraph + +class CudaDiGraph(CudaGraph): + ################# + # Class methods # + ################# + + is_directed = classmethod(DiGraph.is_directed.__func__) + is_multigraph = classmethod(DiGraph.is_multigraph.__func__) + to_cudagraph_class = classmethod(DiGraph.to_cudagraph_class.__func__) + to_networkx_class = classmethod(DiGraph.to_networkx_class.__func__) + + @classmethod + def _to_compat_graph_class(cls) -> type[DiGraph]: + return DiGraph + @networkx_api def size(self, weight: AttrKey | None = None) -> int: if weight is not None: @@ -57,7 +131,7 @@ def size(self, weight: AttrKey | None = None) -> int: ########################## @networkx_api - def reverse(self, copy: bool = True) -> DiGraph: + def reverse(self, copy: bool = True) -> CudaDiGraph: return self._copy(not copy, self.__class__, reverse=True) @networkx_api @@ -162,6 +236,7 @@ def to_undirected(self, reciprocal=False, as_view=False): node_masks, key_to_id=key_to_id, id_to_key=id_to_key, + use_compat_graph=False, ) if as_view: rv.graph = self.graph diff --git a/python/nx-cugraph/nx_cugraph/classes/graph.py b/python/nx-cugraph/nx_cugraph/classes/graph.py index 7c01365c0..cfe1e1c87 100644 --- a/python/nx-cugraph/nx_cugraph/classes/graph.py +++ b/python/nx-cugraph/nx_cugraph/classes/graph.py @@ -20,8 +20,13 @@ import networkx as nx import numpy as np import pylibcugraph as plc +from networkx.classes.graph import ( + _CachedPropertyResetterAdj, + _CachedPropertyResetterNode, +) import nx_cugraph as nxcg +from nx_cugraph import _nxver from ..utils import index_dtype @@ -40,57 +45,246 @@ any_ndarray, ) -__all__ = ["Graph"] +__all__ = ["CudaGraph", "Graph"] networkx_api = nxcg.utils.decorators.networkx_class(nx.Graph) +# The "everything" cache key is an internal implementation detail of NetworkX +# that may change between releases. +if _nxver < (3, 4): + _CACHE_KEY = ( + True, # Include all edge values + True, # Include all node values + True, # Include `.graph` attributes + ) +else: + _CACHE_KEY = ( + True, # Include all edge values + True, # Include all node values + # `.graph` attributes are always included now + ) + +# Use to indicate when a full conversion to GPU failed so we don't try again. +_CANT_CONVERT_TO_GPU = "_CANT_CONVERT_TO_GPU" + + +# `collections.UserDict` was the preferred way to subclass dict, but now +# subclassing dict directly is much better supported and should work here. +# This class should only be necessary if the user clears the cache manually. +class _GraphCache(dict): + """Cache that ensures Graph will reify into a NetworkX graph when cleared.""" + + _graph: Graph -class Graph: + def __init__(self, graph: Graph): + self._graph = graph + + def clear(self) -> None: + self._graph._reify_networkx() + super().clear() + + +class Graph(nx.Graph): # Tell networkx to dispatch calls with this object to nx-cugraph __networkx_backend__: ClassVar[str] = "cugraph" # nx >=3.2 __networkx_plugin__: ClassVar[str] = "cugraph" # nx <3.2 + # Core attributes of NetowkrX graphs that will be copied and cleared as appropriate. + # These attributes comprise the edge and node data model for NetworkX graphs. + _nx_attrs = ("_node", "_adj") + # Allow networkx dispatch machinery to cache conversions. # This means we should clear the cache if we ever mutate the object! - __networkx_cache__: dict | None + __networkx_cache__: _GraphCache | None # networkx properties graph: dict - graph_attr_dict_factory: ClassVar[type] = dict + # Should we declare type annotations for the rest? + + # Properties that trigger copying to the CPU + def _prepare_setter(self): + """Be careful when setting private attributes which may be used during init.""" + if ( + # If not present, then this must be in init + any(attr not in self.__dict__ for attr in self._nx_attrs) + # Already on the CPU + or not any(self.__dict__[attr] is None for attr in self._nx_attrs) + ): + return + if self._is_on_gpu: + # Copy from GPU to CPU + self._reify_networkx() + return + # Default values + for attr in self._nx_attrs: + if self.__dict__[attr] is None: + if attr == "_succ": + self.__dict__[attr] = self.__dict__["_adj"] + else: + self.__dict__[attr] = {} - # Not networkx properties - # We store edge data in COO format with {src,dst}_indices and edge_values. - src_indices: cp.ndarray[IndexValue] - dst_indices: cp.ndarray[IndexValue] - edge_values: dict[AttrKey, cp.ndarray[EdgeValue]] - edge_masks: dict[AttrKey, cp.ndarray[bool]] - node_values: dict[AttrKey, any_ndarray[NodeValue]] - node_masks: dict[AttrKey, any_ndarray[bool]] - key_to_id: dict[NodeKey, IndexValue] | None - _id_to_key: list[NodeKey] | None - _N: int - _node_ids: cp.ndarray[IndexValue] | None # holds plc.SGGraph.vertices_array data + @property + @networkx_api + def _node(self): + if (node := self.__dict__["_node"]) is None: + self._reify_networkx() + node = self.__dict__["_node"] + return node + + @_node.setter + def _node(self, val): + self._prepare_setter() + _CachedPropertyResetterNode.__set__(None, self, val) + if cache := getattr(self, "__networkx_cache__", None): + cache.clear() - # Used by graph._get_plc_graph - _plc_type_map: ClassVar[dict[np.dtype, np.dtype]] = { - # signed int - np.dtype(np.int8): np.dtype(np.float32), - np.dtype(np.int16): np.dtype(np.float32), - np.dtype(np.int32): np.dtype(np.float64), - np.dtype(np.int64): np.dtype(np.float64), # raise if abs(x) > 2**53 - # unsigned int - np.dtype(np.uint8): np.dtype(np.float32), - np.dtype(np.uint16): np.dtype(np.float32), - np.dtype(np.uint32): np.dtype(np.float64), - np.dtype(np.uint64): np.dtype(np.float64), # raise if x > 2**53 - # other - np.dtype(np.bool_): np.dtype(np.float32), - np.dtype(np.float16): np.dtype(np.float32), - } - _plc_allowed_edge_types: ClassVar[set[np.dtype]] = { - np.dtype(np.float32), - np.dtype(np.float64), - } + @property + @networkx_api + def _adj(self): + if (adj := self.__dict__["_adj"]) is None: + self._reify_networkx() + adj = self.__dict__["_adj"] + return adj + + @_adj.setter + def _adj(self, val): + self._prepare_setter() + _CachedPropertyResetterAdj.__set__(None, self, val) + if cache := getattr(self, "__networkx_cache__", None): + cache.clear() + + @property + def _is_on_gpu(self) -> bool: + """Whether the full graph is on device (in the cache). + + This returns False when only a subset of the graph (such as only + edge indices and edge attribute) is on device. + + The graph may be on host (CPU) and device (GPU) at the same time. + """ + cache = getattr(self, "__networkx_cache__", None) + if not cache: + return False + return _CACHE_KEY in cache.get("backends", {}).get("cugraph", {}) + + @property + def _is_on_cpu(self) -> bool: + """Whether the graph is on host as a NetworkX graph. + + This means the core data structures that comprise a NetworkX graph + (such as ``G._node`` and ``G._adj``) are present. + + The graph may be on host (CPU) and device (GPU) at the same time. + """ + return self.__dict__["_node"] is not None + + @property + def _cudagraph(self): + """Return the full ``CudaGraph`` on device, computing if necessary, or None.""" + nx_cache = getattr(self, "__networkx_cache__", None) + if nx_cache is None: + nx_cache = {} + elif _CANT_CONVERT_TO_GPU in nx_cache: + return None + cache = nx_cache.setdefault("backends", {}).setdefault("cugraph", {}) + if (Gcg := cache.get(_CACHE_KEY)) is not None: + if isinstance(Gcg, Graph): + # This shouldn't happen during normal use, but be extra-careful anyway + return Gcg._cudagraph + return Gcg + if self.__dict__["_node"] is None: + raise RuntimeError( + f"{type(self).__name__} cannot be converted to the GPU, because it is " + "not on the CPU! This is not supposed to be possible. If you believe " + "you have found a bug, please report a minimum reproducible example to " + "https://github.com/rapidsai/cugraph/issues/new/choose" + ) + try: + Gcg = nxcg.from_networkx( + self, preserve_edge_attrs=True, preserve_node_attrs=True + ) + except Exception: + # Should we warn that the full graph can't be on GPU? + nx_cache[_CANT_CONVERT_TO_GPU] = True + return None + Gcg.graph = self.graph + cache[_CACHE_KEY] = Gcg + return Gcg + + @_cudagraph.setter + def _cudagraph(self, val, *, clear_cpu=True): + """Set the full ``CudaGraph`` for this graph, or remove from device if None.""" + if (cache := getattr(self, "__networkx_cache__", None)) is None: + # Should we warn? + return + # TODO: pay close attention to when we should clear the cache, since + # this may or may not be a mutation. + cache = cache.setdefault("backends", {}).setdefault("cugraph", {}) + if val is None: + cache.pop(_CACHE_KEY, None) + else: + self.graph = val.graph + cache[_CACHE_KEY] = val + if clear_cpu: + for key in self._nx_attrs: + self.__dict__[key] = None + + @nx.Graph.name.setter + def name(self, s): + # Don't clear the cache when setting the name, since `.graph` is shared. + # There is a very small risk here for the cache to become (slightly) + # insconsistent if graphs from other backends are cached. + self.graph["name"] = s + + @classmethod + @networkx_api + def is_directed(cls) -> bool: + return False + + @classmethod + @networkx_api + def is_multigraph(cls) -> bool: + return False + + @classmethod + def to_cudagraph_class(cls) -> type[CudaGraph]: + return CudaGraph + + @classmethod + @networkx_api + def to_directed_class(cls) -> type[nxcg.DiGraph]: + return nxcg.DiGraph + + @classmethod + def to_networkx_class(cls) -> type[nx.Graph]: + return nx.Graph + + @classmethod + @networkx_api + def to_undirected_class(cls) -> type[Graph]: + return Graph + + def __init__(self, incoming_graph_data=None, **attr): + super().__init__(incoming_graph_data, **attr) + self.__networkx_cache__ = _GraphCache(self) + + def _reify_networkx(self) -> None: + """Copy graph to host (CPU) if necessary.""" + if self.__dict__["_node"] is None: + # After we make this into an nx graph, we rely on the cache being correct + Gcg = self._cudagraph + G = nxcg.to_networkx(Gcg) + for key in self._nx_attrs: + self.__dict__[key] = G.__dict__[key] + + def _become(self, other: Graph): + if self.__class__ is not other.__class__: + raise TypeError( + "Attempting to update graph inplace with graph of different type!" + ) + # Begin with the simplest implementation; do we need to do more? + self.__dict__.update(other.__dict__) + return self #################### # Creation methods # @@ -109,9 +303,10 @@ def from_coo( *, key_to_id: dict[NodeKey, IndexValue] | None = None, id_to_key: list[NodeKey] | None = None, + use_compat_graph: bool | None = None, **attr, - ) -> Graph: - new_graph = object.__new__(cls) + ) -> Graph | CudaGraph: + new_graph = object.__new__(cls.to_cudagraph_class()) new_graph.__networkx_cache__ = {} new_graph.src_indices = src_indices new_graph.dst_indices = dst_indices @@ -173,7 +368,8 @@ def from_coo( isolates = nxcg.algorithms.isolate._isolates(new_graph) if len(isolates) > 0: new_graph._node_ids = cp.arange(new_graph._N, dtype=index_dtype) - + if use_compat_graph or use_compat_graph is None and issubclass(cls, Graph): + new_graph = new_graph._to_compat_graph() return new_graph @classmethod @@ -188,8 +384,9 @@ def from_csr( *, key_to_id: dict[NodeKey, IndexValue] | None = None, id_to_key: list[NodeKey] | None = None, + use_compat_graph: bool | None = None, **attr, - ) -> Graph: + ) -> Graph | CudaGraph: N = indptr.size - 1 src_indices = cp.array( # cp.repeat is slow to use here, so use numpy instead @@ -205,6 +402,7 @@ def from_csr( node_masks, key_to_id=key_to_id, id_to_key=id_to_key, + use_compat_graph=use_compat_graph, **attr, ) @@ -220,8 +418,9 @@ def from_csc( *, key_to_id: dict[NodeKey, IndexValue] | None = None, id_to_key: list[NodeKey] | None = None, + use_compat_graph: bool | None = None, **attr, - ) -> Graph: + ) -> Graph | CudaGraph: N = indptr.size - 1 dst_indices = cp.array( # cp.repeat is slow to use here, so use numpy instead @@ -237,6 +436,7 @@ def from_csc( node_masks, key_to_id=key_to_id, id_to_key=id_to_key, + use_compat_graph=use_compat_graph, **attr, ) @@ -254,8 +454,9 @@ def from_dcsr( *, key_to_id: dict[NodeKey, IndexValue] | None = None, id_to_key: list[NodeKey] | None = None, + use_compat_graph: bool | None = None, **attr, - ) -> Graph: + ) -> Graph | CudaGraph: src_indices = cp.array( # cp.repeat is slow to use here, so use numpy instead np.repeat(compressed_srcs.get(), cp.diff(indptr).get()) @@ -270,6 +471,7 @@ def from_dcsr( node_masks, key_to_id=key_to_id, id_to_key=id_to_key, + use_compat_graph=use_compat_graph, **attr, ) @@ -287,8 +489,9 @@ def from_dcsc( *, key_to_id: dict[NodeKey, IndexValue] | None = None, id_to_key: list[NodeKey] | None = None, + use_compat_graph: bool | None = None, **attr, - ) -> Graph: + ) -> Graph | CudaGraph: dst_indices = cp.array( # cp.repeat is slow to use here, so use numpy instead np.repeat(compressed_dsts.get(), cp.diff(indptr).get()) @@ -303,13 +506,75 @@ def from_dcsc( node_masks, key_to_id=key_to_id, id_to_key=id_to_key, + use_compat_graph=use_compat_graph, **attr, ) - def __new__(cls, incoming_graph_data=None, **attr) -> Graph: + +class CudaGraph: + # Tell networkx to dispatch calls with this object to nx-cugraph + __networkx_backend__: ClassVar[str] = "cugraph" # nx >=3.2 + __networkx_plugin__: ClassVar[str] = "cugraph" # nx <3.2 + + # Allow networkx dispatch machinery to cache conversions. + # This means we should clear the cache if we ever mutate the object! + __networkx_cache__: dict | None + + # networkx properties + graph: dict + graph_attr_dict_factory: ClassVar[type] = dict + + # Not networkx properties + # We store edge data in COO format with {src,dst}_indices and edge_values. + src_indices: cp.ndarray[IndexValue] + dst_indices: cp.ndarray[IndexValue] + edge_values: dict[AttrKey, cp.ndarray[EdgeValue]] + edge_masks: dict[AttrKey, cp.ndarray[bool]] + node_values: dict[AttrKey, any_ndarray[NodeValue]] + node_masks: dict[AttrKey, any_ndarray[bool]] + key_to_id: dict[NodeKey, IndexValue] | None + _id_to_key: list[NodeKey] | None + _N: int + _node_ids: cp.ndarray[IndexValue] | None # holds plc.SGGraph.vertices_array data + + # Used by graph._get_plc_graph + _plc_type_map: ClassVar[dict[np.dtype, np.dtype]] = { + # signed int + np.dtype(np.int8): np.dtype(np.float32), + np.dtype(np.int16): np.dtype(np.float32), + np.dtype(np.int32): np.dtype(np.float64), + np.dtype(np.int64): np.dtype(np.float64), # raise if abs(x) > 2**53 + # unsigned int + np.dtype(np.uint8): np.dtype(np.float32), + np.dtype(np.uint16): np.dtype(np.float32), + np.dtype(np.uint32): np.dtype(np.float64), + np.dtype(np.uint64): np.dtype(np.float64), # raise if x > 2**53 + # other + np.dtype(np.bool_): np.dtype(np.float32), + np.dtype(np.float16): np.dtype(np.float32), + } + _plc_allowed_edge_types: ClassVar[set[np.dtype]] = { + np.dtype(np.float32), + np.dtype(np.float64), + } + + #################### + # Creation methods # + #################### + + from_coo = classmethod(Graph.from_coo.__func__) + from_csr = classmethod(Graph.from_csr.__func__) + from_csc = classmethod(Graph.from_csc.__func__) + from_dcsr = classmethod(Graph.from_dcsr.__func__) + from_dcsc = classmethod(Graph.from_dcsc.__func__) + + def __new__(cls, incoming_graph_data=None, **attr) -> CudaGraph: if incoming_graph_data is None: new_graph = cls.from_coo( - 0, cp.empty(0, index_dtype), cp.empty(0, index_dtype) + 0, + cp.empty(0, index_dtype), + cp.empty(0, index_dtype), + use_compat_graph=False, ) elif incoming_graph_data.__class__ is cls: new_graph = incoming_graph_data.copy() @@ -318,34 +583,30 @@ def __new__(cls, incoming_graph_data=None, **attr) -> Graph: else: raise NotImplementedError new_graph.graph.update(attr) + # We could return Graph here (if configured), but let's not for now return new_graph ################# # Class methods # ################# - @classmethod - @networkx_api - def is_directed(cls) -> bool: - return False + is_directed = classmethod(Graph.is_directed.__func__) + is_multigraph = classmethod(Graph.is_multigraph.__func__) + to_cudagraph_class = classmethod(Graph.to_cudagraph_class.__func__) + to_networkx_class = classmethod(Graph.to_networkx_class.__func__) @classmethod @networkx_api - def is_multigraph(cls) -> bool: - return False + def to_directed_class(cls) -> type[nxcg.CudaDiGraph]: + return nxcg.CudaDiGraph @classmethod @networkx_api - def to_directed_class(cls) -> type[nxcg.DiGraph]: - return nxcg.DiGraph - - @classmethod - def to_networkx_class(cls) -> type[nx.Graph]: - return nx.Graph + def to_undirected_class(cls) -> type[CudaGraph]: + return CudaGraph @classmethod - @networkx_api - def to_undirected_class(cls) -> type[Graph]: + def _to_compat_graph_class(cls) -> type[Graph]: return Graph ############## @@ -438,7 +699,7 @@ def clear_edges(self) -> None: cache.clear() @networkx_api - def copy(self, as_view: bool = False) -> Graph: + def copy(self, as_view: bool = False) -> CudaGraph: # Does shallow copy in networkx return self._copy(as_view, self.__class__) @@ -534,14 +795,19 @@ def size(self, weight: AttrKey | None = None) -> int: return int(cp.count_nonzero(self.src_indices <= self.dst_indices)) @networkx_api - def to_directed(self, as_view: bool = False) -> nxcg.DiGraph: + def to_directed(self, as_view: bool = False) -> nxcg.CudaDiGraph: return self._copy(as_view, self.to_directed_class()) @networkx_api - def to_undirected(self, as_view: bool = False) -> Graph: + def to_undirected(self, as_view: bool = False) -> CudaGraph: # Does deep copy in networkx return self._copy(as_view, self.to_undirected_class()) + def _to_compat_graph(self) -> Graph: + rv = self._to_compat_graph_class()() + rv._cudagraph = self + return rv + # Not implemented... # adj, adjacency, add_edge, add_edges_from, add_node, # add_nodes_from, add_weighted_edges_from, degree, @@ -552,8 +818,8 @@ def to_undirected(self, as_view: bool = False) -> Graph: # Private methods # ################### - def _copy(self, as_view: bool, cls: type[Graph], reverse: bool = False): - # DRY warning: see also MultiGraph._copy + def _copy(self, as_view: bool, cls: type[CudaGraph], reverse: bool = False): + # DRY warning: see also CudaMultiGraph._copy src_indices = self.src_indices dst_indices = self.dst_indices edge_values = self.edge_values @@ -593,6 +859,7 @@ def _copy(self, as_view: bool, cls: type[Graph], reverse: bool = False): node_masks, key_to_id=key_to_id, id_to_key=id_to_key, + use_compat_graph=False, ) if as_view: rv.graph = self.graph @@ -714,7 +981,7 @@ def _get_plc_graph( ) def _sort_edge_indices(self, primary="src"): - # DRY warning: see also MultiGraph._sort_edge_indices + # DRY warning: see also CudaMultiGraph._sort_edge_indices if primary == "src": stacked = cp.vstack((self.dst_indices, self.src_indices)) elif primary == "dst": @@ -736,7 +1003,7 @@ def _sort_edge_indices(self, primary="src"): {key: val[indices] for key, val in self.edge_masks.items()} ) - def _become(self, other: Graph): + def _become(self, other: CudaGraph): if self.__class__ is not other.__class__: raise TypeError( "Attempting to update graph inplace with graph of different type!" diff --git a/python/nx-cugraph/nx_cugraph/classes/multidigraph.py b/python/nx-cugraph/nx_cugraph/classes/multidigraph.py index 2e7a55a9e..5a6595567 100644 --- a/python/nx-cugraph/nx_cugraph/classes/multidigraph.py +++ b/python/nx-cugraph/nx_cugraph/classes/multidigraph.py @@ -16,24 +16,51 @@ import nx_cugraph as nxcg -from .digraph import DiGraph -from .multigraph import MultiGraph +from .digraph import CudaDiGraph, DiGraph +from .graph import Graph +from .multigraph import CudaMultiGraph, MultiGraph -__all__ = ["MultiDiGraph"] +__all__ = ["CudaMultiDiGraph", "MultiDiGraph"] networkx_api = nxcg.utils.decorators.networkx_class(nx.MultiDiGraph) -class MultiDiGraph(MultiGraph, DiGraph): +class MultiDiGraph(nx.MultiDiGraph, MultiGraph, DiGraph): + name = Graph.name + _node = Graph._node + _adj = DiGraph._adj + _succ = DiGraph._succ + _pred = DiGraph._pred + @classmethod @networkx_api def is_directed(cls) -> bool: return True + @classmethod + @networkx_api + def is_multigraph(cls) -> bool: + return True + + @classmethod + def to_cudagraph_class(cls) -> type[CudaMultiDiGraph]: + return CudaMultiDiGraph + @classmethod def to_networkx_class(cls) -> type[nx.MultiDiGraph]: return nx.MultiDiGraph + +class CudaMultiDiGraph(CudaMultiGraph, CudaDiGraph): + is_directed = classmethod(MultiDiGraph.is_directed.__func__) + is_multigraph = classmethod(MultiDiGraph.is_multigraph.__func__) + to_cudagraph_class = classmethod(MultiDiGraph.to_cudagraph_class.__func__) + to_networkx_class = classmethod(MultiDiGraph.to_networkx_class.__func__) + + @classmethod + def _to_compat_graph_class(cls) -> type[MultiDiGraph]: + return MultiDiGraph + ########################## # NetworkX graph methods # ########################## diff --git a/python/nx-cugraph/nx_cugraph/classes/multigraph.py b/python/nx-cugraph/nx_cugraph/classes/multigraph.py index 23d9faa87..c8c8f1dfb 100644 --- a/python/nx-cugraph/nx_cugraph/classes/multigraph.py +++ b/python/nx-cugraph/nx_cugraph/classes/multigraph.py @@ -22,7 +22,7 @@ import nx_cugraph as nxcg from ..utils import index_dtype -from .graph import Graph +from .graph import CudaGraph, Graph, _GraphCache if TYPE_CHECKING: from nx_cugraph.typing import ( @@ -34,32 +34,47 @@ NodeValue, any_ndarray, ) -__all__ = ["MultiGraph"] +__all__ = ["MultiGraph", "CudaMultiGraph"] networkx_api = nxcg.utils.decorators.networkx_class(nx.MultiGraph) -class MultiGraph(Graph): - # networkx properties - edge_key_dict_factory: ClassVar[type] = dict +class MultiGraph(nx.MultiGraph, Graph): + name = Graph.name + _node = Graph._node + _adj = Graph._adj - # Not networkx properties + @classmethod + @networkx_api + def is_directed(cls) -> bool: + return False - # In a MultiGraph, each edge has a unique `(src, dst, key)` key. - # By default, `key` is 0 if possible, else 1, else 2, etc. - # This key can be any hashable Python object in NetworkX. - # We don't use a dict for our data structure here, because - # that would require a `(src, dst, key)` key. - # Instead, we keep `edge_keys` and/or `edge_indices`. - # `edge_keys` is the list of Python objects for each edge. - # `edge_indices` is for the common case of default multiedge keys, - # in which case we can store it as a cupy array. - # `edge_indices` is generally preferred. It is possible to provide - # both where edge_indices is the default and edge_keys is anything. - # It is also possible for them both to be None, which means the - # default edge indices has not yet been calculated. - edge_indices: cp.ndarray[IndexValue] | None - edge_keys: list[EdgeKey] | None + @classmethod + @networkx_api + def is_multigraph(cls) -> bool: + return True + + @classmethod + def to_cudagraph_class(cls) -> type[CudaMultiGraph]: + return CudaMultiGraph + + @classmethod + @networkx_api + def to_directed_class(cls) -> type[nxcg.MultiDiGraph]: + return nxcg.MultiDiGraph + + @classmethod + def to_networkx_class(cls) -> type[nx.MultiGraph]: + return nx.MultiGraph + + @classmethod + @networkx_api + def to_undirected_class(cls) -> type[MultiGraph]: + return MultiGraph + + def __init__(self, incoming_graph_data=None, multigraph_input=None, **attr): + super().__init__(incoming_graph_data, multigraph_input, **attr) + self.__networkx_cache__ = _GraphCache(self) #################### # Creation methods # @@ -80,9 +95,10 @@ def from_coo( key_to_id: dict[NodeKey, IndexValue] | None = None, id_to_key: list[NodeKey] | None = None, edge_keys: list[EdgeKey] | None = None, + use_compat_graph: bool | None = None, **attr, - ) -> MultiGraph: - new_graph = super().from_coo( + ) -> MultiGraph | CudaMultiGraph: + new_graph = super(cls.to_undirected_class(), cls).from_coo( N, src_indices, dst_indices, @@ -92,6 +108,7 @@ def from_coo( node_masks, key_to_id=key_to_id, id_to_key=id_to_key, + use_compat_graph=False, **attr, ) new_graph.edge_indices = edge_indices @@ -102,6 +119,8 @@ def from_coo( and len(new_graph.edge_keys) != src_indices.size ): raise ValueError + if use_compat_graph or use_compat_graph is None and issubclass(cls, Graph): + new_graph = new_graph._to_compat_graph() return new_graph @classmethod @@ -118,8 +137,9 @@ def from_csr( key_to_id: dict[NodeKey, IndexValue] | None = None, id_to_key: list[NodeKey] | None = None, edge_keys: list[EdgeKey] | None = None, + use_compat_graph: bool | None = None, **attr, - ) -> MultiGraph: + ) -> MultiGraph | CudaMultiGraph: N = indptr.size - 1 src_indices = cp.array( # cp.repeat is slow to use here, so use numpy instead @@ -137,6 +157,7 @@ def from_csr( key_to_id=key_to_id, id_to_key=id_to_key, edge_keys=edge_keys, + use_compat_graph=use_compat_graph, **attr, ) @@ -154,8 +175,9 @@ def from_csc( key_to_id: dict[NodeKey, IndexValue] | None = None, id_to_key: list[NodeKey] | None = None, edge_keys: list[EdgeKey] | None = None, + use_compat_graph: bool | None = None, **attr, - ) -> MultiGraph: + ) -> MultiGraph | CudaMultiGraph: N = indptr.size - 1 dst_indices = cp.array( # cp.repeat is slow to use here, so use numpy instead @@ -173,6 +195,7 @@ def from_csc( key_to_id=key_to_id, id_to_key=id_to_key, edge_keys=edge_keys, + use_compat_graph=use_compat_graph, **attr, ) @@ -192,8 +215,9 @@ def from_dcsr( key_to_id: dict[NodeKey, IndexValue] | None = None, id_to_key: list[NodeKey] | None = None, edge_keys: list[EdgeKey] | None = None, + use_compat_graph: bool | None = None, **attr, - ) -> MultiGraph: + ) -> MultiGraph | CudaMultiGraph: src_indices = cp.array( # cp.repeat is slow to use here, so use numpy instead np.repeat(compressed_srcs.get(), cp.diff(indptr).get()) @@ -210,6 +234,7 @@ def from_dcsr( key_to_id=key_to_id, id_to_key=id_to_key, edge_keys=edge_keys, + use_compat_graph=use_compat_graph, **attr, ) @@ -229,8 +254,9 @@ def from_dcsc( key_to_id: dict[NodeKey, IndexValue] | None = None, id_to_key: list[NodeKey] | None = None, edge_keys: list[EdgeKey] | None = None, + use_compat_graph: bool | None = None, **attr, - ) -> Graph: + ) -> MultiGraph | CudaGraph: dst_indices = cp.array( # cp.repeat is slow to use here, so use numpy instead np.repeat(compressed_dsts.get(), cp.diff(indptr).get()) @@ -247,12 +273,46 @@ def from_dcsc( key_to_id=key_to_id, id_to_key=id_to_key, edge_keys=edge_keys, + use_compat_graph=use_compat_graph, **attr, ) + +class CudaMultiGraph(CudaGraph): + # networkx properties + edge_key_dict_factory: ClassVar[type] = dict + + # Not networkx properties + + # In a MultiGraph, each edge has a unique `(src, dst, key)` key. + # By default, `key` is 0 if possible, else 1, else 2, etc. + # This key can be any hashable Python object in NetworkX. + # We don't use a dict for our data structure here, because + # that would require a `(src, dst, key)` key. + # Instead, we keep `edge_keys` and/or `edge_indices`. + # `edge_keys` is the list of Python objects for each edge. + # `edge_indices` is for the common case of default multiedge keys, + # in which case we can store it as a cupy array. + # `edge_indices` is generally preferred. It is possible to provide + # both where edge_indices is the default and edge_keys is anything. + # It is also possible for them both to be None, which means the + # default edge indices has not yet been calculated. + edge_indices: cp.ndarray[IndexValue] | None + edge_keys: list[EdgeKey] | None + + #################### + # Creation methods # + #################### + + from_coo = classmethod(MultiGraph.from_coo.__func__) + from_csr = classmethod(MultiGraph.from_csr.__func__) + from_csc = classmethod(MultiGraph.from_csc.__func__) + from_dcsr = classmethod(MultiGraph.from_dcsr.__func__) + from_dcsc = classmethod(MultiGraph.from_dcsc.__func__) + def __new__( cls, incoming_graph_data=None, multigraph_input=None, **attr - ) -> MultiGraph: + ) -> CudaMultiGraph: if isinstance(incoming_graph_data, dict) and multigraph_input is not False: new_graph = nxcg.from_networkx( nx.MultiGraph(incoming_graph_data, multigraph_input=multigraph_input), @@ -267,28 +327,23 @@ def __new__( # Class methods # ################# - @classmethod - @networkx_api - def is_directed(cls) -> bool: - return False + is_directed = classmethod(MultiGraph.is_directed.__func__) + is_multigraph = classmethod(MultiGraph.is_multigraph.__func__) + to_cudagraph_class = classmethod(MultiGraph.to_cudagraph_class.__func__) + to_networkx_class = classmethod(MultiGraph.to_networkx_class.__func__) @classmethod @networkx_api - def is_multigraph(cls) -> bool: - return True + def to_directed_class(cls) -> type[nxcg.CudaMultiDiGraph]: + return nxcg.CudaMultiDiGraph @classmethod @networkx_api - def to_directed_class(cls) -> type[nxcg.MultiDiGraph]: - return nxcg.MultiDiGraph - - @classmethod - def to_networkx_class(cls) -> type[nx.MultiGraph]: - return nx.MultiGraph + def to_undirected_class(cls) -> type[CudaMultiGraph]: + return CudaMultiGraph @classmethod - @networkx_api - def to_undirected_class(cls) -> type[MultiGraph]: + def _to_compat_graph_class(cls) -> type[MultiGraph]: return MultiGraph ########################## @@ -308,7 +363,7 @@ def clear_edges(self) -> None: self.edge_keys = None @networkx_api - def copy(self, as_view: bool = False) -> MultiGraph: + def copy(self, as_view: bool = False) -> CudaMultiGraph: # Does shallow copy in networkx return self._copy(as_view, self.__class__) @@ -391,11 +446,11 @@ def has_edge(self, u: NodeKey, v: NodeKey, key: EdgeKey | None = None) -> bool: return any(edge_keys[i] == key for i in indices.tolist()) @networkx_api - def to_directed(self, as_view: bool = False) -> nxcg.MultiDiGraph: + def to_directed(self, as_view: bool = False) -> nxcg.CudaMultiDiGraph: return self._copy(as_view, self.to_directed_class()) @networkx_api - def to_undirected(self, as_view: bool = False) -> MultiGraph: + def to_undirected(self, as_view: bool = False) -> CudaMultiGraph: # Does deep copy in networkx return self._copy(as_view, self.to_undirected_class()) @@ -403,8 +458,8 @@ def to_undirected(self, as_view: bool = False) -> MultiGraph: # Private methods # ################### - def _copy(self, as_view: bool, cls: type[Graph], reverse: bool = False): - # DRY warning: see also Graph._copy + def _copy(self, as_view: bool, cls: type[CudaGraph], reverse: bool = False): + # DRY warning: see also CudaGraph._copy src_indices = self.src_indices dst_indices = self.dst_indices edge_indices = self.edge_indices @@ -451,6 +506,7 @@ def _copy(self, as_view: bool, cls: type[Graph], reverse: bool = False): key_to_id=key_to_id, id_to_key=id_to_key, edge_keys=edge_keys, + use_compat_graph=False, ) if as_view: rv.graph = self.graph @@ -460,7 +516,7 @@ def _copy(self, as_view: bool, cls: type[Graph], reverse: bool = False): return rv def _sort_edge_indices(self, primary="src"): - # DRY warning: see also Graph._sort_edge_indices + # DRY warning: see also CudaGraph._sort_edge_indices if self.edge_indices is None and self.edge_keys is None: return super()._sort_edge_indices(primary=primary) if primary == "src": diff --git a/python/nx-cugraph/nx_cugraph/convert.py b/python/nx-cugraph/nx_cugraph/convert.py index 56d16d837..a872f13ac 100644 --- a/python/nx-cugraph/nx_cugraph/convert.py +++ b/python/nx-cugraph/nx_cugraph/convert.py @@ -12,6 +12,7 @@ # limitations under the License. from __future__ import annotations +import functools import itertools import operator as op from collections import Counter, defaultdict @@ -23,9 +24,13 @@ import numpy as np import nx_cugraph as nxcg +from nx_cugraph import _nxver from .utils import index_dtype, networkx_algorithm -from .utils.misc import pairwise +from .utils.misc import _And_NotImplementedError, pairwise + +if _nxver >= (3, 4): + from networkx.utils.backends import _get_cache_key, _get_from_cache, _set_to_cache if TYPE_CHECKING: # pragma: no cover from nx_cugraph.typing import AttrKey, Dtype, EdgeValue, NodeValue, any_ndarray @@ -60,6 +65,27 @@ def _iterate_values(graph, adj, is_dicts, func): return func(it), False +# Consider adding this to `utils` if it is useful elsewhere +def _fallback_decorator(func): + """Catch and convert exceptions to ``NotImplementedError``; use as a decorator. + + ``nx.NetworkXError`` are raised without being converted. This allows + falling back to other backends if, for example, conversion to GPU failed. + """ + + @functools.wraps(func) + def inner(*args, **kwargs): + try: + return func(*args, **kwargs) + except nx.NetworkXError: + raise + except Exception as exc: + raise _And_NotImplementedError(exc) from exc + + return inner + + +@_fallback_decorator def from_networkx( graph: nx.Graph, edge_attrs: AttrKey | dict[AttrKey, EdgeValue | None] | None = None, @@ -74,7 +100,8 @@ def from_networkx( as_directed: bool = False, name: str | None = None, graph_name: str | None = None, -) -> nxcg.Graph: + use_compat_graph: bool | None = False, +) -> nxcg.Graph | nxcg.CudaGraph: """Convert a networkx graph to nx_cugraph graph; can convert all attributes. Parameters @@ -114,10 +141,16 @@ def from_networkx( The name of the algorithm when dispatched from networkx. graph_name : str, optional The name of the graph argument geing converted when dispatched from networkx. + use_compat_graph : bool or None, default False + Indicate whether to return a graph that is compatible with NetworkX graph. + For example, ``nx_cugraph.Graph`` can be used as a NetworkX graph and can + reside in host (CPU) or device (GPU) memory. The default is False, which + will return e.g. ``nx_cugraph.CudaGraph`` that only resides on device (GPU) + and is not fully compatible as a NetworkX graph. Returns ------- - nx_cugraph.Graph + nx_cugraph.Graph or nx_cugraph.CudaGraph Notes ----- @@ -145,6 +178,41 @@ def from_networkx( graph = G else: raise TypeError(f"Expected networkx.Graph; got {type(graph)}") + elif isinstance(graph, nxcg.Graph): + if ( + use_compat_graph + # Use compat graphs by default + or use_compat_graph is None + and (_nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs) + ): + return graph + if graph._is_on_gpu: + return graph._cudagraph + if not graph._is_on_cpu: + raise RuntimeError( + f"{type(graph).__name__} cannot be converted to the GPU, because it is " + "not on the CPU! This is not supposed to be possible. If you believe " + "you have found a bug, please report a minimum reproducible example to " + "https://github.com/rapidsai/cugraph/issues/new/choose" + ) + if _nxver >= (3, 4): + cache_key = _get_cache_key( + edge_attrs=edge_attrs, + node_attrs=node_attrs, + preserve_edge_attrs=preserve_edge_attrs, + preserve_node_attrs=preserve_node_attrs, + preserve_graph_attrs=preserve_graph_attrs, + ) + cache = getattr(graph, "__networkx_cache__", None) + if cache is not None: + cache = cache.setdefault("backends", {}).setdefault("cugraph", {}) + compat_key, rv = _get_from_cache(cache, cache_key) + if rv is not None: + if isinstance(rv, nxcg.Graph): + # This shouldn't happen during normal use, but be extra-careful + rv = rv._cudagraph + if rv is not None: + return rv if preserve_all_attrs: preserve_edge_attrs = True @@ -165,7 +233,12 @@ def from_networkx( else: node_attrs = {node_attrs: None} - if graph.__class__ in {nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph}: + if graph.__class__ in { + nx.Graph, + nx.DiGraph, + nx.MultiGraph, + nx.MultiDiGraph, + } or isinstance(graph, nxcg.Graph): # This is a NetworkX private attribute, but is much faster to use adj = graph._adj else: @@ -455,9 +528,9 @@ def func(it, edge_attr=edge_attr, dtype=dtype): # if vals.ndim > 1: ... if graph.is_multigraph(): if graph.is_directed() or as_directed: - klass = nxcg.MultiDiGraph + klass = nxcg.CudaMultiDiGraph else: - klass = nxcg.MultiGraph + klass = nxcg.CudaMultiGraph rv = klass.from_coo( N, src_indices, @@ -469,12 +542,13 @@ def func(it, edge_attr=edge_attr, dtype=dtype): node_masks, key_to_id=key_to_id, edge_keys=edge_keys, + use_compat_graph=False, ) else: if graph.is_directed() or as_directed: - klass = nxcg.DiGraph + klass = nxcg.CudaDiGraph else: - klass = nxcg.Graph + klass = nxcg.CudaGraph rv = klass.from_coo( N, src_indices, @@ -484,9 +558,22 @@ def func(it, edge_attr=edge_attr, dtype=dtype): node_values, node_masks, key_to_id=key_to_id, + use_compat_graph=False, ) if preserve_graph_attrs: rv.graph.update(graph.graph) # deepcopy? + if _nxver >= (3, 4) and isinstance(graph, nxcg.Graph) and cache is not None: + # Make sure this conversion is added to the cache, and make all of + # our graphs share the same `.graph` attribute for consistency. + rv.graph = graph.graph + _set_to_cache(cache, cache_key, rv) + if ( + use_compat_graph + # Use compat graphs by default + or use_compat_graph is None + and (_nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs) + ): + return rv._to_compat_graph() return rv @@ -535,14 +622,16 @@ def _iter_attr_dicts( return full_dicts -def to_networkx(G: nxcg.Graph, *, sort_edges: bool = False) -> nx.Graph: +def to_networkx( + G: nxcg.Graph | nxcg.CudaGraph, *, sort_edges: bool = False +) -> nx.Graph: """Convert a nx_cugraph graph to networkx graph. All edge and node attributes and ``G.graph`` properties are converted. Parameters ---------- - G : nx_cugraph.Graph + G : nx_cugraph.Graph or nx_cugraph.CudaGraph sort_edges : bool, default False Whether to sort the edge data of the input graph by (src, dst) indices before converting. This can be useful to convert to networkx graphs @@ -557,6 +646,9 @@ def to_networkx(G: nxcg.Graph, *, sort_edges: bool = False) -> nx.Graph: -------- from_networkx : The opposite; convert networkx graph to nx_cugraph graph """ + if isinstance(G, nxcg.Graph): + # These graphs are already NetworkX graphs :) + return G rv = G.to_networkx_class()() id_to_key = G.id_to_key if sort_edges: @@ -623,13 +715,13 @@ def _to_graph( edge_attr: AttrKey | None = None, edge_default: EdgeValue | None = 1, edge_dtype: Dtype | None = None, -) -> nxcg.Graph | nxcg.DiGraph: +) -> nxcg.CudaGraph | nxcg.CudaDiGraph: """Ensure that input type is a nx_cugraph graph, and convert if necessary. Directed and undirected graphs are both allowed. This is an internal utility function and may change or be removed. """ - if isinstance(G, nxcg.Graph): + if isinstance(G, nxcg.CudaGraph): return G if isinstance(G, nx.Graph): return from_networkx( @@ -644,15 +736,15 @@ def _to_directed_graph( edge_attr: AttrKey | None = None, edge_default: EdgeValue | None = 1, edge_dtype: Dtype | None = None, -) -> nxcg.DiGraph: - """Ensure that input type is a nx_cugraph DiGraph, and convert if necessary. +) -> nxcg.CudaDiGraph: + """Ensure that input type is a nx_cugraph CudaDiGraph, and convert if necessary. Undirected graphs will be converted to directed. This is an internal utility function and may change or be removed. """ - if isinstance(G, nxcg.DiGraph): + if isinstance(G, nxcg.CudaDiGraph): return G - if isinstance(G, nxcg.Graph): + if isinstance(G, nxcg.CudaGraph): return G.to_directed() if isinstance(G, nx.Graph): return from_networkx( @@ -670,13 +762,13 @@ def _to_undirected_graph( edge_attr: AttrKey | None = None, edge_default: EdgeValue | None = 1, edge_dtype: Dtype | None = None, -) -> nxcg.Graph: - """Ensure that input type is a nx_cugraph Graph, and convert if necessary. +) -> nxcg.CudaGraph: + """Ensure that input type is a nx_cugraph CudaGraph, and convert if necessary. Only undirected graphs are allowed. Directed graphs will raise ValueError. This is an internal utility function and may change or be removed. """ - if isinstance(G, nxcg.Graph): + if isinstance(G, nxcg.CudaGraph): if G.is_directed(): raise ValueError("Only undirected graphs supported; got a directed graph") return G @@ -688,7 +780,7 @@ def _to_undirected_graph( raise TypeError -@networkx_algorithm(version_added="24.08") +@networkx_algorithm(version_added="24.08", fallback=True) def from_dict_of_lists(d, create_using=None): from .generators._utils import _create_using_class diff --git a/python/nx-cugraph/nx_cugraph/convert_matrix.py b/python/nx-cugraph/nx_cugraph/convert_matrix.py index 38139b913..549759028 100644 --- a/python/nx-cugraph/nx_cugraph/convert_matrix.py +++ b/python/nx-cugraph/nx_cugraph/convert_matrix.py @@ -14,6 +14,8 @@ import networkx as nx import numpy as np +from nx_cugraph import _nxver + from .generators._utils import _create_using_class from .utils import _cp_iscopied_asarray, index_dtype, networkx_algorithm @@ -24,7 +26,7 @@ # Value columns with string dtype is not supported -@networkx_algorithm(is_incomplete=True, version_added="23.12") +@networkx_algorithm(is_incomplete=True, version_added="23.12", fallback=True) def from_pandas_edgelist( df, source="source", @@ -138,7 +140,7 @@ def from_pandas_edgelist( and ( # In nx <= 3.3, `edge_key` was ignored if `edge_attr` is None edge_attr is not None - or nx.__version__[:3] > "3.3" + or _nxver > (3, 3) ) ): try: @@ -161,7 +163,7 @@ def from_pandas_edgelist( return G -@networkx_algorithm(version_added="23.12") +@networkx_algorithm(version_added="23.12", fallback=True) def from_scipy_sparse_array( A, parallel_edges=False, create_using=None, edge_attribute="weight" ): diff --git a/python/nx-cugraph/nx_cugraph/generators/_utils.py b/python/nx-cugraph/nx_cugraph/generators/_utils.py index e38ace5b2..bc9ab84bd 100644 --- a/python/nx-cugraph/nx_cugraph/generators/_utils.py +++ b/python/nx-cugraph/nx_cugraph/generators/_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. +# Copyright (c) 2023-2024, 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 @@ -16,6 +16,7 @@ import networkx as nx import nx_cugraph as nxcg +from nx_cugraph import _nxver from ..utils import index_dtype @@ -74,7 +75,7 @@ def _common_small_graph(n, nodes, create_using, *, allow_directed=True): return G -def _create_using_class(create_using, *, default=nxcg.Graph): +def _create_using_class(create_using, *, default=nx.Graph): """Handle ``create_using`` argument and return a Graph type from nx_cugraph.""" inplace = False if create_using is None: @@ -85,16 +86,17 @@ def _create_using_class(create_using, *, default=nxcg.Graph): create_using, "is_multigraph" ): raise TypeError("create_using is not a valid graph type or instance") - elif not isinstance(create_using, nxcg.Graph): + elif not isinstance(create_using, (nxcg.Graph, nxcg.CudaGraph)): raise NotImplementedError( f"create_using with object of type {type(create_using)} is not supported " - "by the cugraph backend; only nx_cugraph.Graph objects are allowed." + "by the cugraph backend; only nx_cugraph.Graph or nx_cugraph.CudaGraph " + "objects are allowed." ) else: inplace = True G = create_using G.clear() - if not isinstance(G, nxcg.Graph): + if not isinstance(G, (nxcg.Graph, nxcg.CudaGraph)): if G.is_multigraph(): if G.is_directed(): graph_class = nxcg.MultiDiGraph @@ -104,10 +106,12 @@ def _create_using_class(create_using, *, default=nxcg.Graph): graph_class = nxcg.DiGraph else: graph_class = nxcg.Graph + if _nxver >= (3, 3) and not nx.config.backends.cugraph.use_compat_graphs: + graph_class = graph_class.to_cudagraph_class() if G.__class__ not in {nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph}: raise NotImplementedError( f"create_using with type {type(G)} is not supported by the cugraph " - "backend; only standard networkx or nx_cugraph Graph objects are " + "backend; only standard networkx or nx_cugraph graph objects are " "allowed (but not customized subclasses derived from them)." ) else: diff --git a/python/nx-cugraph/nx_cugraph/generators/classic.py b/python/nx-cugraph/nx_cugraph/generators/classic.py index a548beea3..cfcb2a3af 100644 --- a/python/nx-cugraph/nx_cugraph/generators/classic.py +++ b/python/nx-cugraph/nx_cugraph/generators/classic.py @@ -18,6 +18,7 @@ import numpy as np import nx_cugraph as nxcg +from nx_cugraph import _nxver from ..utils import _get_int_dtype, index_dtype, networkx_algorithm from ._utils import ( @@ -102,7 +103,9 @@ def complete_graph(n, create_using=None): @networkx_algorithm(version_added="23.12") def complete_multipartite_graph(*subset_sizes): if not subset_sizes: - return nxcg.Graph() + if _nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs: + return nxcg.Graph() + return nxcg.CudaGraph() try: subset_sizes = [_ensure_int(size) for size in subset_sizes] except TypeError: @@ -139,6 +142,8 @@ def complete_multipartite_graph(*subset_sizes): dst_indices, node_values={"subset": subsets_array}, id_to_key=nodes, + use_compat_graph=_nxver < (3, 3) + or nx.config.backends.cugraph.use_compat_graphs, ) diff --git a/python/nx-cugraph/nx_cugraph/generators/community.py b/python/nx-cugraph/nx_cugraph/generators/community.py index 9b0e0848d..4e5063cc3 100644 --- a/python/nx-cugraph/nx_cugraph/generators/community.py +++ b/python/nx-cugraph/nx_cugraph/generators/community.py @@ -11,8 +11,10 @@ # See the License for the specific language governing permissions and # limitations under the License. import cupy as cp +import networkx as nx import nx_cugraph as nxcg +from nx_cugraph import _nxver from ..utils import networkx_algorithm from ._utils import ( @@ -42,4 +44,7 @@ def caveman_graph(l, k): # noqa: E741 dst_cliques.extend(dst_clique + i * k for i in range(1, l)) src_indices = cp.hstack(src_cliques) dst_indices = cp.hstack(dst_cliques) - return nxcg.Graph.from_coo(l * k, src_indices, dst_indices) + use_compat_graph = _nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs + return nxcg.CudaGraph.from_coo( + l * k, src_indices, dst_indices, use_compat_graph=use_compat_graph + ) diff --git a/python/nx-cugraph/nx_cugraph/generators/ego.py b/python/nx-cugraph/nx_cugraph/generators/ego.py index 66c9c8b95..9a91fa0b6 100644 --- a/python/nx-cugraph/nx_cugraph/generators/ego.py +++ b/python/nx-cugraph/nx_cugraph/generators/ego.py @@ -32,7 +32,10 @@ def ego_graph( ): """Weighted ego_graph with negative cycles is not yet supported. `NotImplementedError` will be raised if there are negative `distance` edge weights.""" # noqa: E501 if isinstance(G, nx.Graph): + is_compat_graph = isinstance(G, nxcg.Graph) G = nxcg.from_networkx(G, preserve_all_attrs=True) + else: + is_compat_graph = False if n not in G: if distance is None: raise nx.NodeNotFound(f"Source {n} is not in G") @@ -100,7 +103,10 @@ def ego_graph( node_mask &= node_ids != src_index node_ids = node_ids[node_mask] if node_ids.size == G._N: - return G.copy() + rv = G.copy() + if is_compat_graph: + return rv._to_compat_graph() + return rv # TODO: create renumbering helper function(s) node_ids.sort() # TODO: is this ever necessary? Keep for safety node_values = {key: val[node_ids] for key, val in G.node_values.items()} @@ -137,6 +143,7 @@ def ego_graph( "node_values": node_values, "node_masks": node_masks, "key_to_id": key_to_id, + "use_compat_graph": False, } if G.is_multigraph(): if G.edge_keys is not None: @@ -147,6 +154,8 @@ def ego_graph( kwargs["edge_indices"] = G.edge_indices[edge_mask] rv = G.__class__.from_coo(**kwargs) rv.graph.update(G.graph) + if is_compat_graph: + return rv._to_compat_graph() return rv diff --git a/python/nx-cugraph/nx_cugraph/generators/small.py b/python/nx-cugraph/nx_cugraph/generators/small.py index 45487571c..d0c03cb7d 100644 --- a/python/nx-cugraph/nx_cugraph/generators/small.py +++ b/python/nx-cugraph/nx_cugraph/generators/small.py @@ -14,6 +14,7 @@ import networkx as nx import nx_cugraph as nxcg +from nx_cugraph import _nxver from ..utils import index_dtype, networkx_algorithm from ._utils import _IS_NX32_OR_LESS, _create_using_class @@ -449,7 +450,14 @@ def pappus_graph(): index_dtype, ) # fmt: on - return nxcg.Graph.from_coo(18, src_indices, dst_indices, name="Pappus Graph") + use_compat_graph = _nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs + return nxcg.CudaGraph.from_coo( + 18, + src_indices, + dst_indices, + name="Pappus Graph", + use_compat_graph=use_compat_graph, + ) @networkx_algorithm(version_added="23.12") diff --git a/python/nx-cugraph/nx_cugraph/generators/social.py b/python/nx-cugraph/nx_cugraph/generators/social.py index 07e82c63f..09d405e75 100644 --- a/python/nx-cugraph/nx_cugraph/generators/social.py +++ b/python/nx-cugraph/nx_cugraph/generators/social.py @@ -11,9 +11,11 @@ # See the License for the specific language governing permissions and # limitations under the License. import cupy as cp +import networkx as nx import numpy as np import nx_cugraph as nxcg +from nx_cugraph import _nxver from ..utils import index_dtype, networkx_algorithm @@ -77,7 +79,8 @@ def davis_southern_women_graph(): "E13", "E14", ] # fmt: on - return nxcg.Graph.from_coo( + use_compat_graph = _nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs + return nxcg.CudaGraph.from_coo( 32, src_indices, dst_indices, @@ -85,6 +88,7 @@ def davis_southern_women_graph(): id_to_key=women + events, top=women, bottom=events, + use_compat_graph=use_compat_graph, ) @@ -111,7 +115,14 @@ def florentine_families_graph(): "Salviati", "Strozzi", "Tornabuoni" ] # fmt: on - return nxcg.Graph.from_coo(15, src_indices, dst_indices, id_to_key=nodes) + use_compat_graph = _nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs + return nxcg.CudaGraph.from_coo( + 15, + src_indices, + dst_indices, + id_to_key=nodes, + use_compat_graph=use_compat_graph, + ) @networkx_algorithm(version_added="23.12") @@ -165,13 +176,15 @@ def karate_club_graph(): "Officer", "Officer", "Officer", "Officer", "Officer", "Officer", ]) # fmt: on - return nxcg.Graph.from_coo( + use_compat_graph = _nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs + return nxcg.CudaGraph.from_coo( 34, src_indices, dst_indices, edge_values={"weight": weights}, node_values={"club": clubs}, name="Zachary's Karate Club", + use_compat_graph=use_compat_graph, ) @@ -289,6 +302,12 @@ def les_miserables_graph(): "Zephine", ] # fmt: on - return nxcg.Graph.from_coo( - 77, src_indices, dst_indices, edge_values={"weight": weights}, id_to_key=nodes + use_compat_graph = _nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs + return nxcg.CudaGraph.from_coo( + 77, + src_indices, + dst_indices, + edge_values={"weight": weights}, + id_to_key=nodes, + use_compat_graph=use_compat_graph, ) diff --git a/python/nx-cugraph/nx_cugraph/interface.py b/python/nx-cugraph/nx_cugraph/interface.py index 4007230ef..1a3d08409 100644 --- a/python/nx-cugraph/nx_cugraph/interface.py +++ b/python/nx-cugraph/nx_cugraph/interface.py @@ -18,6 +18,7 @@ import networkx as nx import nx_cugraph as nxcg +from nx_cugraph import _nxver class BackendInterface: @@ -32,11 +33,19 @@ def convert_from_nx(graph, *args, edge_attrs=None, weight=None, **kwargs): "edge_attrs and weight arguments should not both be given" ) edge_attrs = {weight: 1} - return nxcg.from_networkx(graph, *args, edge_attrs=edge_attrs, **kwargs) + return nxcg.from_networkx( + graph, + *args, + edge_attrs=edge_attrs, + use_compat_graph=_nxver < (3, 3) + or nx.config.backends.cugraph.use_compat_graphs, + **kwargs, + ) @staticmethod def convert_to_nx(obj, *, name: str | None = None): - if isinstance(obj, nxcg.Graph): + if isinstance(obj, nxcg.CudaGraph): + # Observe that this does not try to convert Graph! return nxcg.to_networkx(obj) return obj @@ -62,19 +71,32 @@ def key(testpath): return (testname, frozenset({classname, filename})) return (testname, frozenset({filename})) + use_compat_graph = ( + _nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs + ) + fallback = use_compat_graph or nx.utils.backends._dispatchable._fallback_to_nx + # Reasons for xfailing + # For nx version <= 3.1 no_weights = "weighted implementation not currently supported" no_multigraph = "multigraphs not currently supported" + # For nx version <= 3.2 + nx_cugraph_in_test_setup = ( + "nx-cugraph Graph is incompatible in test setup in nx versions < 3.3" + ) + # For all versions louvain_different = "Louvain may be different due to RNG" - no_string_dtype = "string edge values not currently supported" sssp_path_different = "sssp may choose a different valid path" + tuple_elements_preferred = "elements are tuples instead of lists" + no_mixed_dtypes_for_nodes = ( + # This one is tricky b/c we don't raise; all dtypes are treated as str + "mixed dtypes (str, int, float) for single node property not supported" + ) + # These shouldn't fail if using Graph or falling back to networkx + no_string_dtype = "string edge values not currently supported" no_object_dtype_for_edges = ( "Edges don't support object dtype (lists, strings, etc.)" ) - tuple_elements_preferred = "elements are tuples instead of lists" - nx_cugraph_in_test_setup = ( - "nx-cugraph Graph is incompatible in test setup in nx versions < 3.3" - ) xfail = { # This is removed while strongly_connected_components() is not @@ -98,38 +120,6 @@ def key(testpath): "test_cycles.py:TestMinimumCycleBasis." "test_gh6787_and_edge_attribute_names" ): sssp_path_different, - key( - "test_graph_hashing.py:test_isomorphic_edge_attr" - ): no_object_dtype_for_edges, - key( - "test_graph_hashing.py:test_isomorphic_edge_attr_and_node_attr" - ): no_object_dtype_for_edges, - key( - "test_graph_hashing.py:test_isomorphic_edge_attr_subgraph_hash" - ): no_object_dtype_for_edges, - key( - "test_graph_hashing.py:" - "test_isomorphic_edge_attr_and_node_attr_subgraph_hash" - ): no_object_dtype_for_edges, - key( - "test_summarization.py:TestSNAPNoEdgeTypes.test_summary_graph" - ): no_object_dtype_for_edges, - key( - "test_summarization.py:TestSNAPUndirected.test_summary_graph" - ): no_object_dtype_for_edges, - key( - "test_summarization.py:TestSNAPDirected.test_summary_graph" - ): no_object_dtype_for_edges, - key("test_gexf.py:TestGEXF.test_relabel"): no_object_dtype_for_edges, - key( - "test_gml.py:TestGraph.test_parse_gml_cytoscape_bug" - ): no_object_dtype_for_edges, - key("test_gml.py:TestGraph.test_parse_gml"): no_object_dtype_for_edges, - key("test_gml.py:TestGraph.test_read_gml"): no_object_dtype_for_edges, - key("test_gml.py:TestGraph.test_data_types"): no_object_dtype_for_edges, - key( - "test_gml.py:TestPropertyLists.test_reading_graph_with_list_property" - ): no_object_dtype_for_edges, key( "test_relabel.py:" "test_relabel_preserve_node_order_partial_mapping_with_copy_false" @@ -138,48 +128,107 @@ def key(testpath): "test_gml.py:" "TestPropertyLists.test_reading_graph_with_single_element_list_property" ): tuple_elements_preferred, - key( - "test_relabel.py:" - "TestRelabel.test_relabel_multidigraph_inout_merge_nodes" - ): no_string_dtype, - key( - "test_relabel.py:TestRelabel.test_relabel_multigraph_merge_inplace" - ): no_string_dtype, - key( - "test_relabel.py:TestRelabel.test_relabel_multidigraph_merge_inplace" - ): no_string_dtype, - key( - "test_relabel.py:TestRelabel.test_relabel_multidigraph_inout_copy" - ): no_string_dtype, - key( - "test_relabel.py:TestRelabel.test_relabel_multigraph_merge_copy" - ): no_string_dtype, - key( - "test_relabel.py:TestRelabel.test_relabel_multidigraph_merge_copy" - ): no_string_dtype, - key( - "test_relabel.py:TestRelabel.test_relabel_multigraph_nonnumeric_key" - ): no_string_dtype, - key("test_contraction.py:test_multigraph_path"): no_object_dtype_for_edges, - key( - "test_contraction.py:test_directed_multigraph_path" - ): no_object_dtype_for_edges, - key( - "test_contraction.py:test_multigraph_blockmodel" - ): no_object_dtype_for_edges, - key( - "test_summarization.py:TestSNAPUndirectedMulti.test_summary_graph" - ): no_string_dtype, - key( - "test_summarization.py:TestSNAPDirectedMulti.test_summary_graph" - ): no_string_dtype, } + if not fallback: + xfail.update( + { + key( + "test_graph_hashing.py:test_isomorphic_edge_attr" + ): no_object_dtype_for_edges, + key( + "test_graph_hashing.py:test_isomorphic_edge_attr_and_node_attr" + ): no_object_dtype_for_edges, + key( + "test_graph_hashing.py:test_isomorphic_edge_attr_subgraph_hash" + ): no_object_dtype_for_edges, + key( + "test_graph_hashing.py:" + "test_isomorphic_edge_attr_and_node_attr_subgraph_hash" + ): no_object_dtype_for_edges, + key( + "test_summarization.py:TestSNAPNoEdgeTypes.test_summary_graph" + ): no_object_dtype_for_edges, + key( + "test_summarization.py:TestSNAPUndirected.test_summary_graph" + ): no_object_dtype_for_edges, + key( + "test_summarization.py:TestSNAPDirected.test_summary_graph" + ): no_object_dtype_for_edges, + key( + "test_gexf.py:TestGEXF.test_relabel" + ): no_object_dtype_for_edges, + key( + "test_gml.py:TestGraph.test_parse_gml_cytoscape_bug" + ): no_object_dtype_for_edges, + key( + "test_gml.py:TestGraph.test_parse_gml" + ): no_object_dtype_for_edges, + key( + "test_gml.py:TestGraph.test_read_gml" + ): no_object_dtype_for_edges, + key( + "test_gml.py:TestGraph.test_data_types" + ): no_object_dtype_for_edges, + key( + "test_gml.py:" + "TestPropertyLists.test_reading_graph_with_list_property" + ): no_object_dtype_for_edges, + key( + "test_relabel.py:" + "TestRelabel.test_relabel_multidigraph_inout_merge_nodes" + ): no_string_dtype, + key( + "test_relabel.py:" + "TestRelabel.test_relabel_multigraph_merge_inplace" + ): no_string_dtype, + key( + "test_relabel.py:" + "TestRelabel.test_relabel_multidigraph_merge_inplace" + ): no_string_dtype, + key( + "test_relabel.py:" + "TestRelabel.test_relabel_multidigraph_inout_copy" + ): no_string_dtype, + key( + "test_relabel.py:TestRelabel.test_relabel_multigraph_merge_copy" + ): no_string_dtype, + key( + "test_relabel.py:" + "TestRelabel.test_relabel_multidigraph_merge_copy" + ): no_string_dtype, + key( + "test_relabel.py:" + "TestRelabel.test_relabel_multigraph_nonnumeric_key" + ): no_string_dtype, + key( + "test_contraction.py:test_multigraph_path" + ): no_object_dtype_for_edges, + key( + "test_contraction.py:test_directed_multigraph_path" + ): no_object_dtype_for_edges, + key( + "test_contraction.py:test_multigraph_blockmodel" + ): no_object_dtype_for_edges, + key( + "test_summarization.py:" + "TestSNAPUndirectedMulti.test_summary_graph" + ): no_string_dtype, + key( + "test_summarization.py:TestSNAPDirectedMulti.test_summary_graph" + ): no_string_dtype, + } + ) + else: + xfail.update( + { + key( + "test_gml.py:" + "TestPropertyLists.test_reading_graph_with_list_property" + ): no_mixed_dtypes_for_nodes, + } + ) - from packaging.version import parse - - nxver = parse(nx.__version__) - - if nxver.major == 3 and nxver.minor <= 2: + if _nxver <= (3, 2): xfail.update( { # NetworkX versions prior to 3.2.1 have tests written to @@ -216,7 +265,7 @@ def key(testpath): } ) - if nxver.major == 3 and nxver.minor <= 1: + if _nxver <= (3, 1): # MAINT: networkx 3.0, 3.1 # NetworkX 3.2 added the ability to "fallback to nx" if backend algorithms # raise NotImplementedError or `can_run` returns False. The tests below @@ -332,24 +381,25 @@ def key(testpath): xfail[key("test_louvain.py:test_threshold")] = ( "Louvain does not support seed parameter" ) - if nxver.major == 3 and nxver.minor >= 2: - xfail.update( - { - key( - "test_convert_pandas.py:TestConvertPandas." - "test_from_edgelist_multi_attr_incl_target" - ): no_string_dtype, - key( - "test_convert_pandas.py:TestConvertPandas." - "test_from_edgelist_multidigraph_and_edge_attr" - ): no_string_dtype, - key( - "test_convert_pandas.py:TestConvertPandas." - "test_from_edgelist_int_attr_name" - ): no_string_dtype, - } - ) - if nxver.minor == 2: + if _nxver >= (3, 2): + if not fallback: + xfail.update( + { + key( + "test_convert_pandas.py:TestConvertPandas." + "test_from_edgelist_multi_attr_incl_target" + ): no_string_dtype, + key( + "test_convert_pandas.py:TestConvertPandas." + "test_from_edgelist_multidigraph_and_edge_attr" + ): no_string_dtype, + key( + "test_convert_pandas.py:TestConvertPandas." + "test_from_edgelist_int_attr_name" + ): no_string_dtype, + } + ) + if _nxver[1] == 2: different_iteration_order = "Different graph data iteration order" xfail.update( { @@ -366,7 +416,7 @@ def key(testpath): ): different_iteration_order, } ) - elif nxver.minor >= 3: + elif _nxver[1] >= 3: xfail.update( { key("test_louvain.py:test_max_level"): louvain_different, diff --git a/python/nx-cugraph/nx_cugraph/relabel.py b/python/nx-cugraph/nx_cugraph/relabel.py index 20d1337a9..e38e18c77 100644 --- a/python/nx-cugraph/nx_cugraph/relabel.py +++ b/python/nx-cugraph/nx_cugraph/relabel.py @@ -29,13 +29,18 @@ @networkx_algorithm(version_added="24.08") def relabel_nodes(G, mapping, copy=True): + G_orig = G if isinstance(G, nx.Graph): - if not copy: + is_compat_graph = isinstance(G, nxcg.Graph) + if not copy and not is_compat_graph: raise RuntimeError( "Using `copy=False` is invalid when using a NetworkX graph " "as input to `nx_cugraph.relabel_nodes`" ) G = nxcg.from_networkx(G, preserve_all_attrs=True) + else: + is_compat_graph = False + it = range(G._N) if G.key_to_id is None else G.id_to_key if callable(mapping): previd_to_key = [mapping(node) for node in it] @@ -225,12 +230,13 @@ def relabel_nodes(G, mapping, copy=True): node_masks=node_masks, id_to_key=newid_to_key, key_to_id=key_to_newid, + use_compat_graph=is_compat_graph, **extra_kwargs, ) rv.graph.update(G.graph) if not copy: - G._become(rv) - return G + G_orig._become(rv) + return G_orig return rv @@ -241,7 +247,10 @@ def convert_node_labels_to_integers( if ordering not in {"default", "sorted", "increasing degree", "decreasing degree"}: raise nx.NetworkXError(f"Unknown node ordering: {ordering}") if isinstance(G, nx.Graph): + is_compat_graph = isinstance(G, nxcg.Graph) G = nxcg.from_networkx(G, preserve_all_attrs=True) + else: + is_compat_graph = False G = G.copy() if label_attribute is not None: prev_vals = G.id_to_key @@ -279,4 +288,6 @@ def convert_node_labels_to_integers( key_to_id = G.key_to_id G.key_to_id = {i: key_to_id[n] for i, (d, n) in enumerate(pairs, first_label)} G._id_to_key = id_to_key + if is_compat_graph: + return G._to_compat_graph() return G diff --git a/python/nx-cugraph/nx_cugraph/tests/test_bfs.py b/python/nx-cugraph/nx_cugraph/tests/test_bfs.py index c2b22e989..ad2c62c1f 100644 --- a/python/nx-cugraph/nx_cugraph/tests/test_bfs.py +++ b/python/nx-cugraph/nx_cugraph/tests/test_bfs.py @@ -12,11 +12,10 @@ # limitations under the License. import networkx as nx import pytest -from packaging.version import parse -nxver = parse(nx.__version__) +from nx_cugraph import _nxver -if nxver.major == 3 and nxver.minor < 2: +if _nxver < (3, 2): pytest.skip("Need NetworkX >=3.2 to test clustering", allow_module_level=True) diff --git a/python/nx-cugraph/nx_cugraph/tests/test_classes.py b/python/nx-cugraph/nx_cugraph/tests/test_classes.py new file mode 100644 index 000000000..0ac238b35 --- /dev/null +++ b/python/nx-cugraph/nx_cugraph/tests/test_classes.py @@ -0,0 +1,77 @@ +# Copyright (c) 2024, 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 nx_cugraph as nxcg + + +def test_class_to_class(): + """Basic sanity checks to ensure metadata relating graph classes are accurate.""" + for prefix in ["", "Cuda"]: + for suffix in ["Graph", "DiGraph", "MultiGraph", "MultiDiGraph"]: + cls_name = f"{prefix}{suffix}" + cls = getattr(nxcg, cls_name) + assert cls.__name__ == cls_name + G = cls() + assert cls is G.__class__ + # cudagraph + val = cls.to_cudagraph_class() + val2 = G.to_cudagraph_class() + assert val is val2 + assert val.__name__ == f"Cuda{suffix}" + assert val.__module__.startswith("nx_cugraph") + assert cls.is_directed() == G.is_directed() == val.is_directed() + assert cls.is_multigraph() == G.is_multigraph() == val.is_multigraph() + # networkx + val = cls.to_networkx_class() + val2 = G.to_networkx_class() + assert val is val2 + assert val.__name__ == suffix + assert val.__module__.startswith("networkx") + val = val() + assert cls.is_directed() == G.is_directed() == val.is_directed() + assert cls.is_multigraph() == G.is_multigraph() == val.is_multigraph() + # directed + val = cls.to_directed_class() + val2 = G.to_directed_class() + assert val is val2 + assert val.__module__.startswith("nx_cugraph") + assert val.is_directed() + assert cls.is_multigraph() == G.is_multigraph() == val.is_multigraph() + if "Di" in suffix: + assert val is cls + else: + assert "Di" in val.__name__ + assert prefix in val.__name__ + assert cls.to_undirected_class() is cls + # undirected + val = cls.to_undirected_class() + val2 = G.to_undirected_class() + assert val is val2 + assert val.__module__.startswith("nx_cugraph") + assert not val.is_directed() + assert cls.is_multigraph() == G.is_multigraph() == val.is_multigraph() + if "Di" not in suffix: + assert val is cls + else: + assert "Di" not in val.__name__ + assert prefix in val.__name__ + assert cls.to_directed_class() is cls + # "zero" + if prefix == "Cuda": + val = cls._to_compat_graph_class() + val2 = G._to_compat_graph_class() + assert val is val2 + assert val.__name__ == suffix + assert val.__module__.startswith("nx_cugraph") + assert val.to_cudagraph_class() is cls + assert cls.is_directed() == G.is_directed() == val.is_directed() + assert cls.is_multigraph() == G.is_multigraph() == val.is_multigraph() diff --git a/python/nx-cugraph/nx_cugraph/tests/test_cluster.py b/python/nx-cugraph/nx_cugraph/tests/test_cluster.py index ad4770f1a..fd8e1b3cf 100644 --- a/python/nx-cugraph/nx_cugraph/tests/test_cluster.py +++ b/python/nx-cugraph/nx_cugraph/tests/test_cluster.py @@ -12,11 +12,10 @@ # limitations under the License. import networkx as nx import pytest -from packaging.version import parse -nxver = parse(nx.__version__) +from nx_cugraph import _nxver -if nxver.major == 3 and nxver.minor < 2: +if _nxver < (3, 2): pytest.skip("Need NetworkX >=3.2 to test clustering", allow_module_level=True) diff --git a/python/nx-cugraph/nx_cugraph/tests/test_convert.py b/python/nx-cugraph/nx_cugraph/tests/test_convert.py index 634b28e96..3d109af8a 100644 --- a/python/nx-cugraph/nx_cugraph/tests/test_convert.py +++ b/python/nx-cugraph/nx_cugraph/tests/test_convert.py @@ -13,13 +13,10 @@ import cupy as cp import networkx as nx import pytest -from packaging.version import parse import nx_cugraph as nxcg from nx_cugraph import interface -nxver = parse(nx.__version__) - @pytest.mark.parametrize( "graph_class", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph] diff --git a/python/nx-cugraph/nx_cugraph/tests/test_ego_graph.py b/python/nx-cugraph/nx_cugraph/tests/test_ego_graph.py index 5474f9d79..0697a744e 100644 --- a/python/nx-cugraph/nx_cugraph/tests/test_ego_graph.py +++ b/python/nx-cugraph/nx_cugraph/tests/test_ego_graph.py @@ -12,16 +12,13 @@ # limitations under the License. import networkx as nx import pytest -from packaging.version import parse import nx_cugraph as nxcg +from nx_cugraph import _nxver from .testing_utils import assert_graphs_equal -nxver = parse(nx.__version__) - - -if nxver.major == 3 and nxver.minor < 2: +if _nxver < (3, 2): pytest.skip("Need NetworkX >=3.2 to test ego_graph", allow_module_level=True) @@ -49,7 +46,12 @@ def test_ego_graph_cycle_graph( kwargs = {"radius": radius, "center": center, "undirected": undirected} Hnx = nx.ego_graph(Gnx, n, **kwargs) Hcg = nx.ego_graph(Gnx, n, **kwargs, backend="cugraph") + use_compat_graphs = _nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs + assert_graphs_equal(Hnx, Hcg._cudagraph if use_compat_graphs else Hcg) + Hcg = nx.ego_graph(Gcg, n, **kwargs) assert_graphs_equal(Hnx, Hcg) + Hcg = nx.ego_graph(Gcg._to_compat_graph(), n, **kwargs) + assert_graphs_equal(Hnx, Hcg._cudagraph) with pytest.raises(nx.NodeNotFound, match="not in G"): nx.ego_graph(Gnx, -1, **kwargs) with pytest.raises(nx.NodeNotFound, match="not in G"): @@ -61,20 +63,36 @@ def test_ego_graph_cycle_graph( kwargs["distance"] = "weight" H2nx = nx.ego_graph(Gnx, n, **kwargs) - is_nx32 = nxver.major == 3 and nxver.minor == 2 + is_nx32 = _nxver[:2] == (3, 2) if undirected and Gnx.is_directed() and Gnx.is_multigraph(): if is_nx32: # `should_run` was added in nx 3.3 match = "Weighted ego_graph with undirected=True not implemented" + elif _nxver >= (3, 4): + match = "not implemented by 'cugraph'" else: match = "not implemented by cugraph" - with pytest.raises(RuntimeError, match=match): + with pytest.raises( + RuntimeError if _nxver < (3, 4) else NotImplementedError, match=match + ): nx.ego_graph(Gnx, n, **kwargs, backend="cugraph") with pytest.raises(NotImplementedError, match="ego_graph"): - nx.ego_graph(Gcg, n, **kwargs) + nx.ego_graph(Gcg, n, **kwargs, backend="cugraph") + if _nxver < (3, 4): + with pytest.raises(NotImplementedError, match="ego_graph"): + nx.ego_graph(Gcg, n, **kwargs) + else: + # This is an interesting case. `nxcg.ego_graph` is not implemented for + # these arguments, so it falls back to networkx. Hence, as it is currently + # implemented, the input graph is `nxcg.CudaGraph`, but the output graph + # is `nx.Graph`. Should networkx convert back to "cugraph" backend? + # TODO: make fallback to networkx configurable. + H2cg = nx.ego_graph(Gcg, n, **kwargs) + assert type(H2nx) is type(H2cg) + assert_graphs_equal(H2nx, nxcg.from_networkx(H2cg, preserve_all_attrs=True)) else: H2cg = nx.ego_graph(Gnx, n, **kwargs, backend="cugraph") - assert_graphs_equal(H2nx, H2cg) + assert_graphs_equal(H2nx, H2cg._cudagraph if use_compat_graphs else H2cg) with pytest.raises(nx.NodeNotFound, match="not found in graph"): nx.ego_graph(Gnx, -1, **kwargs) with pytest.raises(nx.NodeNotFound, match="not found in graph"): diff --git a/python/nx-cugraph/nx_cugraph/tests/test_generators.py b/python/nx-cugraph/nx_cugraph/tests/test_generators.py index c751b0fe2..5c405f1c9 100644 --- a/python/nx-cugraph/nx_cugraph/tests/test_generators.py +++ b/python/nx-cugraph/nx_cugraph/tests/test_generators.py @@ -13,25 +13,24 @@ import networkx as nx import numpy as np import pytest -from packaging.version import parse import nx_cugraph as nxcg +from nx_cugraph import _nxver from .testing_utils import assert_graphs_equal -nxver = parse(nx.__version__) - - -if nxver.major == 3 and nxver.minor < 2: +if _nxver < (3, 2): pytest.skip("Need NetworkX >=3.2 to test generators", allow_module_level=True) def compare(name, create_using, *args, is_vanilla=False): exc1 = exc2 = None func = getattr(nx, name) - if isinstance(create_using, nxcg.Graph): + if isinstance(create_using, nxcg.CudaGraph): nx_create_using = nxcg.to_networkx(create_using) - elif isinstance(create_using, type) and issubclass(create_using, nxcg.Graph): + elif isinstance(create_using, type) and issubclass( + create_using, (nxcg.Graph, nxcg.CudaGraph) + ): nx_create_using = create_using.to_networkx_class() elif isinstance(create_using, nx.Graph): nx_create_using = create_using.copy() @@ -61,8 +60,27 @@ def compare(name, create_using, *args, is_vanilla=False): exc2 = exc if exc1 is not None or exc2 is not None: assert type(exc1) is type(exc2) + return + if isinstance(Gcg, nxcg.Graph): + # If the graph is empty, it may be on host, otherwise it should be on device + if len(G): + assert Gcg._is_on_gpu + assert not Gcg._is_on_cpu + assert_graphs_equal(G, Gcg._cudagraph) else: assert_graphs_equal(G, Gcg) + # Ensure the output type is correct + if is_vanilla: + if _nxver < (3, 3) or nx.config.backends.cugraph.use_compat_graphs: + assert isinstance(Gcg, nxcg.Graph) + else: + assert isinstance(Gcg, nxcg.CudaGraph) + elif isinstance(create_using, type) and issubclass( + create_using, (nxcg.Graph, nxcg.CudaGraph) + ): + assert type(Gcg) is create_using + elif isinstance(create_using, (nxcg.Graph, nxcg.CudaGraph)): + assert type(Gcg) is type(create_using) N = list(range(-1, 5)) @@ -76,6 +94,10 @@ def compare(name, create_using, *args, is_vanilla=False): nxcg.DiGraph, nxcg.MultiGraph, nxcg.MultiDiGraph, + nxcg.CudaGraph, + nxcg.CudaDiGraph, + nxcg.CudaMultiGraph, + nxcg.CudaMultiDiGraph, # These raise NotImplementedError # nx.Graph(), # nx.DiGraph(), @@ -85,6 +107,10 @@ def compare(name, create_using, *args, is_vanilla=False): nxcg.DiGraph(), nxcg.MultiGraph(), nxcg.MultiDiGraph(), + nxcg.CudaGraph(), + nxcg.CudaDiGraph(), + nxcg.CudaMultiGraph(), + nxcg.CudaMultiDiGraph(), None, object, # Bad input 7, # Bad input @@ -158,7 +184,7 @@ def compare(name, create_using, *args, is_vanilla=False): @pytest.mark.parametrize("create_using", COMPLETE_CREATE_USING) def test_generator_noarg(name, create_using): print(name, create_using, type(create_using)) - if isinstance(create_using, nxcg.Graph) and name in { + if isinstance(create_using, nxcg.CudaGraph) and name in { # fmt: off "bull_graph", "chvatal_graph", "cubical_graph", "diamond_graph", "house_graph", "house_x_graph", "icosahedral_graph", "krackhardt_kite_graph", diff --git a/python/nx-cugraph/nx_cugraph/tests/test_graph_methods.py b/python/nx-cugraph/nx_cugraph/tests/test_graph_methods.py index 3120995a2..40a361b10 100644 --- a/python/nx-cugraph/nx_cugraph/tests/test_graph_methods.py +++ b/python/nx-cugraph/nx_cugraph/tests/test_graph_methods.py @@ -47,7 +47,7 @@ def _create_Gs(): @pytest.mark.parametrize("Gnx", _create_Gs()) @pytest.mark.parametrize("reciprocal", [False, True]) def test_to_undirected_directed(Gnx, reciprocal): - Gcg = nxcg.DiGraph(Gnx) + Gcg = nxcg.CudaDiGraph(Gnx) assert_graphs_equal(Gnx, Gcg) Hnx1 = Gnx.to_undirected(reciprocal=reciprocal) Hcg1 = Gcg.to_undirected(reciprocal=reciprocal) @@ -62,6 +62,6 @@ def test_multidigraph_to_undirected(): Gnx.add_edge(0, 1) Gnx.add_edge(0, 1) Gnx.add_edge(1, 0) - Gcg = nxcg.MultiDiGraph(Gnx) + Gcg = nxcg.CudaMultiDiGraph(Gnx) with pytest.raises(NotImplementedError): Gcg.to_undirected() diff --git a/python/nx-cugraph/nx_cugraph/tests/test_match_api.py b/python/nx-cugraph/nx_cugraph/tests/test_match_api.py index 176b531a6..1a61c69b3 100644 --- a/python/nx-cugraph/nx_cugraph/tests/test_match_api.py +++ b/python/nx-cugraph/nx_cugraph/tests/test_match_api.py @@ -14,13 +14,10 @@ import inspect import networkx as nx -from packaging.version import parse import nx_cugraph as nxcg from nx_cugraph.utils import networkx_algorithm -nxver = parse(nx.__version__) - def test_match_signature_and_names(): """Simple test to ensure our signatures and basic module layout match networkx.""" diff --git a/python/nx-cugraph/nx_cugraph/tests/test_multigraph.py b/python/nx-cugraph/nx_cugraph/tests/test_multigraph.py index a8f189a47..9208eea09 100644 --- a/python/nx-cugraph/nx_cugraph/tests/test_multigraph.py +++ b/python/nx-cugraph/nx_cugraph/tests/test_multigraph.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. +# Copyright (c) 2023-2024, 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 @@ -26,7 +26,7 @@ def test_get_edge_data(test_nxcugraph): G.add_edge(0, 3) G.add_edge(0, 3) if test_nxcugraph: - G = nxcg.MultiGraph(G) + G = nxcg.CudaMultiGraph(G) default = object() assert G.get_edge_data(0, 0, default=default) is default assert G.get_edge_data("a", "b", default=default) is default @@ -60,7 +60,7 @@ def test_get_edge_data(test_nxcugraph): G = nx.MultiGraph() G.add_edge(0, 1) if test_nxcugraph: - G = nxcg.MultiGraph(G) + G = nxcg.CudaMultiGraph(G) assert G.get_edge_data(0, 1, default=default) == {0: {}} assert G.get_edge_data(0, 1, 0, default=default) == {} assert G.get_edge_data(0, 1, 1, default=default) is default diff --git a/python/nx-cugraph/nx_cugraph/tests/test_pagerank.py b/python/nx-cugraph/nx_cugraph/tests/test_pagerank.py index 0b437df2d..252f9e6bb 100644 --- a/python/nx-cugraph/nx_cugraph/tests/test_pagerank.py +++ b/python/nx-cugraph/nx_cugraph/tests/test_pagerank.py @@ -12,19 +12,23 @@ # limitations under the License. import networkx as nx import pandas as pd -from pytest import approx +import pytest def test_pagerank_multigraph(): """ - Ensures correct differences between pagerank results for Graphs - vs. MultiGraphs generated using from_pandas_edgelist() + Ensures correct pagerank for Graphs and MultiGraphs when using from_pandas_edgelist. + + PageRank for MultiGraph should give different result compared to Graph; when using + a Graph, the duplicate edges should be dropped. """ - df = pd.DataFrame({"source": [0, 1, 1, 1, 1, 1, 1, 2], - "target": [1, 2, 2, 2, 2, 2, 2, 3]}) + df = pd.DataFrame( + {"source": [0, 1, 1, 1, 1, 1, 1, 2], "target": [1, 2, 2, 2, 2, 2, 2, 3]} + ) expected_pr_for_G = nx.pagerank(nx.from_pandas_edgelist(df)) expected_pr_for_MultiG = nx.pagerank( - nx.from_pandas_edgelist(df, create_using=nx.MultiGraph)) + nx.from_pandas_edgelist(df, create_using=nx.MultiGraph) + ) G = nx.from_pandas_edgelist(df, backend="cugraph") actual_pr_for_G = nx.pagerank(G, backend="cugraph") @@ -32,5 +36,5 @@ def test_pagerank_multigraph(): MultiG = nx.from_pandas_edgelist(df, create_using=nx.MultiGraph, backend="cugraph") actual_pr_for_MultiG = nx.pagerank(MultiG, backend="cugraph") - assert actual_pr_for_G == approx(expected_pr_for_G) - assert actual_pr_for_MultiG == approx(expected_pr_for_MultiG) + assert actual_pr_for_G == pytest.approx(expected_pr_for_G) + assert actual_pr_for_MultiG == pytest.approx(expected_pr_for_MultiG) diff --git a/python/nx-cugraph/nx_cugraph/tests/testing_utils.py b/python/nx-cugraph/nx_cugraph/tests/testing_utils.py index 529a96efd..50836acf5 100644 --- a/python/nx-cugraph/nx_cugraph/tests/testing_utils.py +++ b/python/nx-cugraph/nx_cugraph/tests/testing_utils.py @@ -17,7 +17,7 @@ def assert_graphs_equal(Gnx, Gcg): assert isinstance(Gnx, nx.Graph) - assert isinstance(Gcg, nxcg.Graph) + assert isinstance(Gcg, nxcg.CudaGraph) assert (a := Gnx.number_of_nodes()) == (b := Gcg.number_of_nodes()), (a, b) assert (a := Gnx.number_of_edges()) == (b := Gcg.number_of_edges()), (a, b) assert (a := Gnx.is_directed()) == (b := Gcg.is_directed()), (a, b) diff --git a/python/nx-cugraph/nx_cugraph/utils/decorators.py b/python/nx-cugraph/nx_cugraph/utils/decorators.py index 3c5de4f29..16486996b 100644 --- a/python/nx-cugraph/nx_cugraph/utils/decorators.py +++ b/python/nx-cugraph/nx_cugraph/utils/decorators.py @@ -16,10 +16,14 @@ from textwrap import dedent import networkx as nx +from networkx import NetworkXError from networkx.utils.decorators import nodes_or_number, not_implemented_for +from nx_cugraph import _nxver from nx_cugraph.interface import BackendInterface +from .misc import _And_NotImplementedError + try: from networkx.utils.backends import _registered_algorithms except ModuleNotFoundError: @@ -44,6 +48,7 @@ class networkx_algorithm: version_added: str is_incomplete: bool is_different: bool + _fallback: bool _plc_names: set[str] | None def __new__( @@ -59,6 +64,7 @@ def __new__( version_added: str, # Required is_incomplete: bool = False, # See self.extra_doc for details if True is_different: bool = False, # See self.extra_doc for details if True + fallback: bool = False, # Change non-nx exceptions to NotImplementedError _plc: str | set[str] | None = None, # Hidden from user, may be removed someday ): if func is None: @@ -70,10 +76,11 @@ def __new__( version_added=version_added, is_incomplete=is_incomplete, is_different=is_different, + fallback=fallback, _plc=_plc, ) instance = object.__new__(cls) - if nodes_or_number is not None and nx.__version__[:3] > "3.2": + if nodes_or_number is not None and _nxver > (3, 2): func = nx.utils.decorators.nodes_or_number(nodes_or_number)(func) # update_wrapper sets __wrapped__, which will be used for the signature update_wrapper(instance, func) @@ -100,6 +107,7 @@ def __new__( instance.version_added = version_added instance.is_incomplete = is_incomplete instance.is_different = is_different + instance.fallback = fallback # The docstring on our function is added to the NetworkX docstring. instance.extra_doc = ( dedent(func.__doc__.lstrip("\n").rstrip()) if func.__doc__ else None @@ -113,7 +121,7 @@ def __new__( # Set methods so they are in __dict__ instance._can_run = instance._can_run instance._should_run = instance._should_run - if nodes_or_number is not None and nx.__version__[:3] <= "3.2": + if nodes_or_number is not None and _nxver <= (3, 2): instance = nx.utils.decorators.nodes_or_number(nodes_or_number)(instance) return instance @@ -136,7 +144,14 @@ def _should_run(self, func): self.should_run = func def __call__(self, /, *args, **kwargs): - return self.__wrapped__(*args, **kwargs) + if not self.fallback: + return self.__wrapped__(*args, **kwargs) + try: + return self.__wrapped__(*args, **kwargs) + except NetworkXError: + raise + except Exception as exc: + raise _And_NotImplementedError(exc) from exc def __reduce__(self): return _restore_networkx_dispatched, (self.name,) diff --git a/python/nx-cugraph/nx_cugraph/utils/misc.py b/python/nx-cugraph/nx_cugraph/utils/misc.py index 8526524f1..01c25dd59 100644 --- a/python/nx-cugraph/nx_cugraph/utils/misc.py +++ b/python/nx-cugraph/nx_cugraph/utils/misc.py @@ -194,7 +194,7 @@ def _get_int_dtype( def _get_float_dtype( - dtype: Dtype, *, graph: nxcg.Graph | None = None, weight: EdgeKey | None = None + dtype: Dtype, *, graph: nxcg.CudaGraph | None = None, weight: EdgeKey | None = None ): """Promote dtype to float32 or float64 as appropriate.""" if dtype is None: @@ -238,3 +238,37 @@ def _cp_iscopied_asarray(a, *args, orig_object=None, **kwargs): ): return False, arr return True, arr + + +class _And_NotImplementedError(NotImplementedError): + """Additionally make an exception a ``NotImplementedError``. + + For example: + + >>> try: + ... raise _And_NotImplementedError(KeyError("missing")) + ... except KeyError: + ... pass + + or + + >>> try: + ... raise _And_NotImplementedError(KeyError("missing")) + ... except NotImplementedError: + ... pass + + """ + + def __new__(cls, exc): + exc_type = type(exc) + if issubclass(exc_type, NotImplementedError): + new_type = exc_type + else: + new_type = type( + f"{exc_type.__name__}{cls.__name__}", + (exc_type, NotImplementedError), + {}, + ) + instance = NotImplementedError.__new__(new_type) + instance.__init__(*exc.args) + return instance diff --git a/python/nx-cugraph/pyproject.toml b/python/nx-cugraph/pyproject.toml index e7b4ea44d..98de089a9 100644 --- a/python/nx-cugraph/pyproject.toml +++ b/python/nx-cugraph/pyproject.toml @@ -40,7 +40,6 @@ dependencies = [ [project.optional-dependencies] test = [ - "packaging>=21", "pandas", "pytest", "pytest-benchmark", @@ -170,6 +169,7 @@ external = [ ] ignore = [ # Would be nice to fix these + "B905", # `zip()` without an explicit `strict=` parameter (Note: possible since py39 was dropped; we should do this!) "D100", # Missing docstring in public module "D101", # Missing docstring in public class "D102", # Missing docstring in public method @@ -215,6 +215,7 @@ ignore = [ "SIM105", # Use contextlib.suppress(...) instead of try-except-pass (Note: try-except-pass is much faster) "SIM108", # Use ternary operator ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) "TRY003", # Avoid specifying long messages outside the exception class (Note: why?) + "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` (Note: tuple is faster for now) # Ignored categories "C90", # mccabe (Too strict, but maybe we should make things less complex) @@ -241,6 +242,7 @@ ignore = [ # Allow assert, print, RNG, and no docstring "nx_cugraph/**/tests/*py" = ["S101", "S311", "T201", "D103", "D100"] "_nx_cugraph/__init__.py" = ["E501"] +"nx_cugraph/__init__.py" = ["E402"] # Allow module level import not at top of file "nx_cugraph/algorithms/**/*py" = ["D205", "D401"] # Allow flexible docstrings for algorithms "nx_cugraph/generators/**/*py" = ["D205", "D401"] # Allow flexible docstrings for generators "nx_cugraph/interface.py" = ["D401"] # Flexible docstrings diff --git a/python/nx-cugraph/run_nx_tests.sh b/python/nx-cugraph/run_nx_tests.sh index bceec53b7..5fb173cf9 100755 --- a/python/nx-cugraph/run_nx_tests.sh +++ b/python/nx-cugraph/run_nx_tests.sh @@ -18,6 +18,10 @@ # testing takes longer. Without it, tests will xfail when encountering a # function that we don't implement. # +# NX_CUGRAPH_USE_COMPAT_GRAPHS, {"True", "False"}, default is "True" +# Whether to use `nxcg.Graph` as the nx_cugraph backend graph. +# A Graph should be a compatible NetworkX graph, so fewer tests should fail. +# # Coverage of `nx_cugraph.algorithms` is reported and is a good sanity check # that algorithms run. From 782958eb0998c7d96b388ef823ba3d2615c40d61 Mon Sep 17 00:00:00 2001 From: Ralph Liu <137829296+nv-rliu@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:21:35 -0400 Subject: [PATCH 15/21] Fix `cit-patents` Dataset for `nx-cugraph` Benchmark (#4666) Replace `_` with `-` Authors: - Ralph Liu (https://github.com/nv-rliu) Approvers: - Rick Ratzel (https://github.com/rlratzel) URL: https://github.com/rapidsai/cugraph/pull/4666 --- benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh b/benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh index a1d32474e..3059e3d4b 100755 --- a/benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh +++ b/benchmarks/nx-cugraph/pytest-based/run-main-benchmarks.sh @@ -30,7 +30,7 @@ algos=" datasets=" netscience email_Eu_core - cit_patents + cit-patents hollywood soc-livejournal " From 71225961cbac25ed4d039ebdf6e30a68b3489349 Mon Sep 17 00:00:00 2001 From: Sebastian Berg Date: Mon, 30 Sep 2024 15:42:56 +0200 Subject: [PATCH 16/21] Remove NumPy <2 pin (#4615) This PR removes the NumPy<2 pin which is expected to work for RAPIDS projects once CuPy 13.3.0 is released (CuPy 13.2.0 had some issues preventing the use with NumPy 2). Authors: - Sebastian Berg (https://github.com/seberg) - https://github.com/jakirkham - Rick Ratzel (https://github.com/rlratzel) - Alex Barghi (https://github.com/alexbarghi-nv) - James Lamb (https://github.com/jameslamb) - Philip Hyunsu Cho (https://github.com/hcho3) Approvers: - Alex Barghi (https://github.com/alexbarghi-nv) - James Lamb (https://github.com/jameslamb) URL: https://github.com/rapidsai/cugraph/pull/4615 --- python/nx-cugraph/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/nx-cugraph/pyproject.toml b/python/nx-cugraph/pyproject.toml index 98de089a9..cf4b98afe 100644 --- a/python/nx-cugraph/pyproject.toml +++ b/python/nx-cugraph/pyproject.toml @@ -34,7 +34,7 @@ classifiers = [ dependencies = [ "cupy-cuda11x>=12.0.0", "networkx>=3.0", - "numpy>=1.23,<2.0a0", + "numpy>=1.23,<3.0a0", "pylibcugraph==24.10.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. From b9b218e7c2730a0b3e4c2112221f101ff1121b67 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Mon, 30 Sep 2024 14:28:42 -0500 Subject: [PATCH 17/21] Swtch traceback to `--native` in `cugraph` (#4663) In cudf we have observed a ~10% speed up of pytest suite execution by switching pytest traceback to `--native`: ``` currently: 102474 passed, 2117 skipped, 902 xfailed in 892.16s (0:14:52) --tb=short: 102474 passed, 2117 skipped, 902 xfailed in 898.99s (0:14:58) --tb=no: 102474 passed, 2117 skipped, 902 xfailed in 815.98s (0:13:35) --tb=native: 102474 passed, 2117 skipped, 902 xfailed in 820.92s (0:13:40) ``` This PR makes similar change to `cugraph` repo. xref: https://github.com/rapidsai/cudf/pull/16851 Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Brad Rees (https://github.com/BradReesWork) URL: https://github.com/rapidsai/cugraph/pull/4663 --- benchmarks/pytest.ini | 1 + python/nx-cugraph/nx_cugraph/tests/pytest.ini | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 python/nx-cugraph/nx_cugraph/tests/pytest.ini diff --git a/benchmarks/pytest.ini b/benchmarks/pytest.ini index fe7fc31b6..d692b78de 100644 --- a/benchmarks/pytest.ini +++ b/benchmarks/pytest.ini @@ -8,6 +8,7 @@ testpaths = addopts = --benchmark-columns="min, max, mean, stddev, outliers" + --tb=native markers = managedmem_on: RMM managed memory enabled diff --git a/python/nx-cugraph/nx_cugraph/tests/pytest.ini b/python/nx-cugraph/nx_cugraph/tests/pytest.ini new file mode 100644 index 000000000..7b0a9f29f --- /dev/null +++ b/python/nx-cugraph/nx_cugraph/tests/pytest.ini @@ -0,0 +1,4 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +[pytest] +addopts = --tb=native From e68669a4c923808b3ceb7cbc673cb5c898236038 Mon Sep 17 00:00:00 2001 From: Ralph Liu <137829296+nv-rliu@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:51:33 -0400 Subject: [PATCH 18/21] Add `nx-cugraph` Docs Pages (#4669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/rapidsai/graph_dl/issues/606 and [another issue] ## Proposed Changes In preparation for GA release, this PR adds a landing page for `nx-cugraph` in the cugraph API documentation site. The new pages can be viewed by clicking `nx-cugraph` in the navigation bar at the top of the page. ### New pages nx-cugraph └─ How it works └─ Supported Algorithms └─ Getting Started └─ Benchmarks └─ FAQ ## Notes for Reviewers - In order to build and test these docs, I modified the `build.sh` file to use `sphinx-autobuild`. ```bash 122 123 cd ${REPODIR}/docs/cugraph-docs 124 #make html 125 sphinx-autobuild source build/html 126 fi 127 ``` - For now, I believe the best way to view these changes is to clone the PR branch, then run `build.sh` in order to host the webserver locally.. --------- Co-authored-by: Don Acosta <97529984+acostadon@users.noreply.github.com> Co-authored-by: rlratzel Co-authored-by: Erik Welch --- docs/cugraph/source/_static/bc_benchmark.png | Bin 0 -> 139273 bytes docs/cugraph/source/_static/colab.png | Bin 0 -> 76512 bytes .../source/_static/nxcg-execution-diagram.jpg | Bin 0 -> 170702 bytes docs/cugraph/source/nx_cugraph/benchmarks.md | 28 ++ docs/cugraph/source/nx_cugraph/faqs.md | 5 + .../cugraph/source/nx_cugraph/how-it-works.md | 114 ++++++ docs/cugraph/source/nx_cugraph/index.rst | 49 ++- .../cugraph/source/nx_cugraph/installation.md | 50 +++ docs/cugraph/source/nx_cugraph/nx_cugraph.md | 57 +-- .../nx_cugraph/supported-algorithms.rst | 354 ++++++++++++++++++ python/nx-cugraph/_nx_cugraph/__init__.py | 2 +- 11 files changed, 599 insertions(+), 60 deletions(-) create mode 100644 docs/cugraph/source/_static/bc_benchmark.png create mode 100644 docs/cugraph/source/_static/colab.png create mode 100644 docs/cugraph/source/_static/nxcg-execution-diagram.jpg create mode 100644 docs/cugraph/source/nx_cugraph/benchmarks.md create mode 100644 docs/cugraph/source/nx_cugraph/faqs.md create mode 100644 docs/cugraph/source/nx_cugraph/how-it-works.md create mode 100644 docs/cugraph/source/nx_cugraph/installation.md create mode 100644 docs/cugraph/source/nx_cugraph/supported-algorithms.rst diff --git a/docs/cugraph/source/_static/bc_benchmark.png b/docs/cugraph/source/_static/bc_benchmark.png new file mode 100644 index 0000000000000000000000000000000000000000..9e385c97e996221f8698c324dcbafe24bf73ecc4 GIT binary patch literal 139273 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�{@}BJE*EU*tu*(hf9pJAgMSOj^ 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 XNkQE&=yoGrJcd0JTG$+?4JMuk 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 z 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

)e}NHDeyWt!@MDS$W$^tH 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 V2RcelJcSfPD{J2>ykQaX{{TGiEWQ8$ literal 0 HcmV?d00001 diff --git a/docs/cugraph/source/_static/nxcg-execution-diagram.jpg b/docs/cugraph/source/_static/nxcg-execution-diagram.jpg new file mode 100644 index 0000000000000000000000000000000000000000..48136289af9b46dfaf14bbec43fbce3cfbe3736b GIT binary patch literal 170702 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 z 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 + +![bench-image](../_static/bc_benchmark.png) + +

Results from running this Benchmark
+ + +## Reproducing Benchmarks + +Below are the steps to reproduce the results on your workstation. These are documented in this [README](https://github.com/rapidsai/cugraph/blob/HEAD/benchmarks/nx-cugraph/pytest-based). + +1. Clone the latest + +2. Follow the instructions to build an environment + +3. Activate the environment + +4. Install the latest `nx-cugraph` by following the [guide](installation.md) + +5. Follow the instructions written in the README here: `cugraph/benchmarks/nx-cugraph/pytest-based/` diff --git a/docs/cugraph/source/nx_cugraph/faqs.md b/docs/cugraph/source/nx_cugraph/faqs.md new file mode 100644 index 000000000..dee943d19 --- /dev/null +++ b/docs/cugraph/source/nx_cugraph/faqs.md @@ -0,0 +1,5 @@ +# FAQ + + > **1. Is `nx-cugraph` able to run across multiple GPUs?** + +nx-cugraph currently does not support multi-GPU. Multi-GPU support may be added to a future release of nx-cugraph, but consider [cugraph](https://docs.rapids.ai/api/cugraph/stable) for multi-GPU accelerated graph analytics in Python today. diff --git a/docs/cugraph/source/nx_cugraph/how-it-works.md b/docs/cugraph/source/nx_cugraph/how-it-works.md new file mode 100644 index 000000000..f9dc5af67 --- /dev/null +++ b/docs/cugraph/source/nx_cugraph/how-it-works.md @@ -0,0 +1,114 @@ +# How it Works + +NetworkX has the ability to **dispatch function calls to separately-installed third-party backends**. + +NetworkX backends let users experience improved performance and/or additional functionality without changing their NetworkX Python code. Examples include backends that provide algorithm acceleration using GPUs, parallel processing, graph database integration, and more. + +While NetworkX is a pure-Python implementation with minimal to no dependencies, backends may be written in other languages and require specialized hardware and/or OS support, additional software dependencies, or even separate services. Installation instructions vary based on the backend, and additional information can be found from the individual backend project pages listed in the NetworkX Backend Gallery. + + +![nxcg-execution-flow](../_static/nxcg-execution-diagram.jpg) + +## Enabling nx-cugraph + +NetworkX will use nx-cugraph as the graph analytics backend if any of the +following are used: + +### `NETWORKX_BACKEND_PRIORITY` environment variable. + +The `NETWORKX_BACKEND_PRIORITY` environment variable can be used to have NetworkX automatically dispatch to specified backends. This variable can be set to a single backend name, or a comma-separated list of backends ordered using the priority which NetworkX should try. If a NetworkX function is called that nx-cugraph supports, NetworkX will redirect the function call to nx-cugraph automatically, or fall back to the next backend in the list if provided, or run using the default NetworkX implementation. See [NetworkX Backends and Configs](https://networkx.org/documentation/stable/reference/backends.html). + +For example, this setting will have NetworkX use nx-cugraph for any function called by the script supported by nx-cugraph, and the default NetworkX implementation for all others. +``` +bash> NETWORKX_BACKEND_PRIORITY=cugraph python my_networkx_script.py +``` + +This example will have NetworkX use nx-cugraph for functions it supports, then try other_backend if nx-cugraph does not support them, and finally the default NetworkX implementation if not supported by either backend: +``` +bash> NETWORKX_BACKEND_PRIORITY="cugraph,other_backend" python my_networkx_script.py +``` + +### `backend=` keyword argument + +To explicitly specify a particular backend for an API, use the `backend=` +keyword argument. This argument takes precedence over the +`NETWORKX_BACKEND_PRIORITY` environment variable. This requires anyone +running code that uses the `backend=` keyword argument to have the specified +backend installed. + +Example: +```python +nx.betweenness_centrality(cit_patents_graph, k=k, backend="cugraph") +``` + +### Type-based dispatching + +NetworkX also supports automatically dispatching to backends associated with +specific graph types. Like the `backend=` keyword argument example above, this +requires the user to write code for a specific backend, and therefore requires +the backend to be installed, but has the advantage of ensuring a particular +behavior without the potential for runtime conversions. + +To use type-based dispatching with nx-cugraph, the user must import the backend +directly in their code to access the utilities provided to create a Graph +instance specifically for the nx-cugraph backend. + +Example: +```python +import networkx as nx +import nx_cugraph as nxcg + +G = nx.Graph() +... +nxcg_G = nxcg.from_networkx(G) # conversion happens once here +nx.betweenness_centrality(nxcg_G, k=1000) # nxcg Graph type causes cugraph backend + # to be used, no conversion necessary +``` + +## Command Line Example + +--- + +Create `bc_demo.ipy` and paste the code below. + +```python +import pandas as pd +import networkx as nx + +url = "https://data.rapids.ai/cugraph/datasets/cit-Patents.csv" +df = pd.read_csv(url, sep=" ", names=["src", "dst"], dtype="int32") +G = nx.from_pandas_edgelist(df, source="src", target="dst") + +%time result = nx.betweenness_centrality(G, k=10) +``` +Run the command: +``` +user@machine:/# ipython bc_demo.ipy +``` + +You will observe a run time of approximately 7 minutes...more or less depending on your CPU. + +Run the command again, this time specifying cugraph as the NetworkX backend. +``` +user@machine:/# NETWORKX_BACKEND_PRIORITY=cugraph ipython bc_demo.ipy +``` +This run will be much faster, typically around 20 seconds depending on your GPU. +``` +user@machine:/# NETWORKX_BACKEND_PRIORITY=cugraph ipython bc_demo.ipy +``` +There is also an option to cache the graph conversion to GPU. This can dramatically improve performance when running multiple algorithms on the same graph. Caching is enabled by default for NetworkX versions 3.4 and later, but if using an older version, set "NETWORKX_CACHE_CONVERTED_GRAPHS=True" +``` +NETWORKX_BACKEND_PRIORITY=cugraph NETWORKX_CACHE_CONVERTED_GRAPHS=True ipython bc_demo.ipy +``` + +When running Python interactively, the cugraph backend can be specified as an argument in the algorithm call. + +For example: +``` +nx.betweenness_centrality(cit_patents_graph, k=k, backend="cugraph") +``` + + +The latest list of algorithms supported by nx-cugraph can be found [here](https://github.com/rapidsai/cugraph/blob/HEAD/python/nx-cugraph/README.md#algorithms) or in the next section. + +--- diff --git a/docs/cugraph/source/nx_cugraph/index.rst b/docs/cugraph/source/nx_cugraph/index.rst index ef6f51601..110300c18 100644 --- a/docs/cugraph/source/nx_cugraph/index.rst +++ b/docs/cugraph/source/nx_cugraph/index.rst @@ -1,9 +1,48 @@ -=============================== -nxCugraph as a NetworkX Backend -=============================== +nx-cugraph +----------- +nx-cugraph is a `NetworkX backend `_ that provides **GPU acceleration** to many popular NetworkX algorithms. + +By simply `installing and enabling nx-cugraph `_, users can see significant speedup on workflows where performance is hindered by the default NetworkX implementation. With ``nx-cugraph``, users can have GPU-based, large-scale performance **without** changing their familiar and easy-to-use NetworkX code. + +.. code-block:: python + + import pandas as pd + import networkx as nx + + url = "https://data.rapids.ai/cugraph/datasets/cit-Patents.csv" + df = pd.read_csv(url, sep=" ", names=["src", "dst"], dtype="int32") + G = nx.from_pandas_edgelist(df, source="src", target="dst") + + %time result = nx.betweenness_centrality(G, k=10) + +.. figure:: ../_static/colab.png + :width: 200px + :target: https://nvda.ws/4drM4re + + Try it on Google Colab! + + ++------------------------------------------------------------------------------------------------------------------------+ +| **Zero Code Change Acceleration** | +| | +| Just ``nx.config.backend_priority=["cugraph"]`` in Jupyter, or set ``NETWORKX_BACKEND_PRIORITY=cugraph`` in the shell. | ++------------------------------------------------------------------------------------------------------------------------+ +| **Run the same code on CPU or GPU** | +| | +| Nothing changes, not even your `import` statements, when going from CPU to GPU. | ++------------------------------------------------------------------------------------------------------------------------+ + + +``nx-cugraph`` is now Generally Available (GA) as part of the ``RAPIDS`` package. See `RAPIDS +Quick Start `_ to get up-and-running with ``nx-cugraph``. .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + :caption: Contents: - nx_cugraph.md + how-it-works + supported-algorithms + installation + benchmarks + faqs diff --git a/docs/cugraph/source/nx_cugraph/installation.md b/docs/cugraph/source/nx_cugraph/installation.md new file mode 100644 index 000000000..8d221f16f --- /dev/null +++ b/docs/cugraph/source/nx_cugraph/installation.md @@ -0,0 +1,50 @@ +# Getting Started + +This guide describes how to install ``nx-cugraph`` and use it in your workflows. + + +## System Requirements + +`nx-cugraph` requires the following: + + - **Volta architecture or later NVIDIA GPU, with [compute capability](https://developer.nvidia.com/cuda-gpus) 7.0+** + - **[CUDA](https://docs.nvidia.com/cuda/index.html) 11.2, 11.4, 11.5, 11.8, 12.0, 12.2, or 12.5** + - **Python >= 3.10** + - **[NetworkX](https://networkx.org/documentation/stable/install.html#) >= 3.0 (version 3.2 or higher recommended)** + +More details about system requirements can be found in the [RAPIDS System Requirements Documentation](https://docs.rapids.ai/install#system-req). + +## Installing nx-cugraph + +Read the [RAPIDS Quick Start Guide](https://docs.rapids.ai/install) to learn more about installing all RAPIDS libraries. + +`nx-cugraph` can be installed using conda or pip. It is included in the RAPIDS metapackage, or can be installed separately. + +### Conda +**Nightly version** +```bash +conda install -c rapidsai-nightly -c conda-forge -c nvidia nx-cugraph +``` + +**Stable version** +```bash +conda install -c rapidsai -c conda-forge -c nvidia nx-cugraph +``` + +### pip +**Nightly version** +```bash +pip install nx-cugraph-cu11 --extra-index-url https://pypi.anaconda.org/rapidsai-wheels-nightly/simple +``` + +**Stable version** +```bash +pip install nx-cugraph-cu11 --extra-index-url https://pypi.nvidia.com +``` + +
+ +**Note:** + - The `pip install` examples above are for CUDA 11. To install for CUDA 12, replace `-cu11` with `-cu12` + +
diff --git a/docs/cugraph/source/nx_cugraph/nx_cugraph.md b/docs/cugraph/source/nx_cugraph/nx_cugraph.md index 75a30b0be..900362a6e 100644 --- a/docs/cugraph/source/nx_cugraph/nx_cugraph.md +++ b/docs/cugraph/source/nx_cugraph/nx_cugraph.md @@ -1,18 +1,10 @@ ### nx_cugraph -nx-cugraph is a [NetworkX -backend]() that provides GPU acceleration to many popular NetworkX algorithms. - -By simply [installing and enabling nx-cugraph](), users can see significant speedup on workflows where performance is hindered by the default NetworkX implementation. With nx-cugraph, users can have GPU-based, large-scale performance without changing their familiar and easy-to-use NetworkX code. - -Let's look at some examples of algorithm speedups comparing NetworkX with and without GPU acceleration using nx-cugraph. - -Each chart has three measurements. -* NX - default NetworkX, no GPU acceleration -* nx-cugraph - GPU-accelerated NetworkX using nx-cugraph. This involves an internal conversion/transfer of graph data from CPU to GPU memory -* nx-cugraph (preconvert) - GPU-accelerated NetworkX using nx-cugraph with the graph data pre-converted/transferred to GPU +`nx-cugraph` is a [networkX backend]() that accelerates many popular NetworkX functions using cuGraph and NVIDIA GPUs. +Users simply [install and enable nx-cugraph](installation.md) to experience GPU speedups. +Lets look at some examples of algorithm speedups comparing CPU based NetworkX to dispatched versions run on GPU with nx_cugraph. ![Ancestors](../images/ancestors.png) ![BFS Tree](../images/bfs_tree.png) @@ -22,46 +14,3 @@ Each chart has three measurements. ![Pagerank](../images/pagerank.png) ![Single Source Shortest Path](../images/sssp.png) ![Weakly Connected Components](../images/wcc.png) - -### Command line example -Open bc_demo.ipy and paste the code below. - -``` -import pandas as pd -import networkx as nx - -url = "https://data.rapids.ai/cugraph/datasets/cit-Patents.csv" -df = pd.read_csv(url, sep=" ", names=["src", "dst"], dtype="int32") -G = nx.from_pandas_edgelist(df, source="src", target="dst") - -%time result = nx.betweenness_centrality(G, k=10) -``` -Run the command: -``` -user@machine:/# ipython bc_demo.ipy -``` - -You will observe a run time of approximately 7 minutes...more or less depending on your cpu. - -Run the command again, this time specifying cugraph as the NetworkX backend. -``` -user@machine:/# NETWORKX_BACKEND_PRIORITY=cugraph ipython bc_demo.ipy -``` -This run will be much faster, typically around 20 seconds depending on your GPU. -``` -user@machine:/# NETWORKX_BACKEND_PRIORITY=cugraph ipython bc_demo.ipy -``` -There is also an option to cache the graph conversion to GPU. This can dramatically improve performance when running multiple algorithms on the same graph. -``` -NETWORKX_BACKEND_PRIORITY=cugraph NETWORKX_CACHE_CONVERTED_GRAPHS=True ipython bc_demo.ipy -``` - -When running Python interactively, the cugraph backend can be specified as an argument in the algorithm call. - -For example: -``` -nx.betweenness_centrality(cit_patents_graph, k=k, backend="cugraph") -``` - - -The latest list of algorithms supported by nx-cugraph can be found [here](https://github.com/rapidsai/cugraph/blob/main/python/nx-cugraph/README.md#algorithms). diff --git a/docs/cugraph/source/nx_cugraph/supported-algorithms.rst b/docs/cugraph/source/nx_cugraph/supported-algorithms.rst new file mode 100644 index 000000000..b21ef7bb6 --- /dev/null +++ b/docs/cugraph/source/nx_cugraph/supported-algorithms.rst @@ -0,0 +1,354 @@ +Supported Algorithms +===================== + +The nx-cugraph backend to NetworkX connects +`pylibcugraph <../../readme_pages/pylibcugraph.md>`_ (cuGraph's low-level Python +interface to its CUDA-based graph analytics library) and +`CuPy `_ (a GPU-accelerated array library) to NetworkX's +familiar and easy-to-use API. + +Below is the list of algorithms that are currently supported in nx-cugraph. + + +Algorithms +---------- + ++-----------------------------+ +| **Centrality** | ++=============================+ +| betweenness_centrality | ++-----------------------------+ +| edge_betweenness_centrality | ++-----------------------------+ +| degree_centrality | ++-----------------------------+ +| in_degree_centrality | ++-----------------------------+ +| out_degree_centrality | ++-----------------------------+ +| eigenvector_centrality | ++-----------------------------+ +| katz_centrality | ++-----------------------------+ + ++---------------------+ +| **Cluster** | ++=====================+ +| average_clustering | ++---------------------+ +| clustering | ++---------------------+ +| transitivity | ++---------------------+ +| triangles | ++---------------------+ + ++--------------------------+ +| **Community** | ++==========================+ +| louvain_communities | ++--------------------------+ + ++--------------------------+ +| **Bipartite** | ++==========================+ +| complete_bipartite_graph | ++--------------------------+ + ++------------------------------------+ +| **Components** | ++====================================+ +| connected_components | ++------------------------------------+ +| is_connected | ++------------------------------------+ +| node_connected_component | ++------------------------------------+ +| number_connected_components | ++------------------------------------+ +| weakly_connected | ++------------------------------------+ +| is_weakly_connected | ++------------------------------------+ +| number_weakly_connected_components | ++------------------------------------+ +| weakly_connected_components | ++------------------------------------+ + ++-------------+ +| **Core** | ++=============+ +| core_number | ++-------------+ +| k_truss | ++-------------+ + ++-------------+ +| **DAG** | ++=============+ +| ancestors | ++-------------+ +| descendants | ++-------------+ + ++--------------------+ +| **Isolate** | ++====================+ +| is_isolate | ++--------------------+ +| isolates | ++--------------------+ +| number_of_isolates | ++--------------------+ + ++-------------------+ +| **Link analysis** | ++===================+ +| hits | ++-------------------+ +| pagerank | ++-------------------+ + ++----------------+ +| **Operators** | ++================+ +| complement | ++----------------+ +| reverse | ++----------------+ + ++----------------------+ +| **Reciprocity** | ++======================+ +| overall_reciprocity | ++----------------------+ +| reciprocity | ++----------------------+ + ++---------------------------------------+ +| **Shortest Paths** | ++=======================================+ +| has_path | ++---------------------------------------+ +| shortest_path | ++---------------------------------------+ +| shortest_path_length | ++---------------------------------------+ +| all_pairs_shortest_path | ++---------------------------------------+ +| all_pairs_shortest_path_length | ++---------------------------------------+ +| bidirectional_shortest_path | ++---------------------------------------+ +| single_source_shortest_path | ++---------------------------------------+ +| single_source_shortest_path_length | ++---------------------------------------+ +| single_target_shortest_path | ++---------------------------------------+ +| single_target_shortest_path_length | ++---------------------------------------+ +| all_pairs_bellman_ford_path | ++---------------------------------------+ +| all_pairs_bellman_ford_path_length | ++---------------------------------------+ +| all_pairs_dijkstra | ++---------------------------------------+ +| all_pairs_dijkstra_path | ++---------------------------------------+ +| all_pairs_dijkstra_path_length | ++---------------------------------------+ +| bellman_ford_path | ++---------------------------------------+ +| bellman_ford_path_length | ++---------------------------------------+ +| dijkstra_path | ++---------------------------------------+ +| dijkstra_path_length | ++---------------------------------------+ +| single_source_bellman_ford | ++---------------------------------------+ +| single_source_bellman_ford_path | ++---------------------------------------+ +| single_source_bellman_ford_path_length| ++---------------------------------------+ +| single_source_dijkstra | ++---------------------------------------+ +| single_source_dijkstra_path | ++---------------------------------------+ +| single_source_dijkstra_path_length | ++---------------------------------------+ + ++---------------------------+ +| **Traversal** | ++===========================+ +| bfs_edges | ++---------------------------+ +| bfs_layers | ++---------------------------+ +| bfs_predecessors | ++---------------------------+ +| bfs_successors | ++---------------------------+ +| bfs_tree | ++---------------------------+ +| descendants_at_distance | ++---------------------------+ +| generic_bfs_edges | ++---------------------------+ + ++---------------------+ +| **Tree** | ++=====================+ +| is_arborescence | ++---------------------+ +| is_branching | ++---------------------+ +| is_forest | ++---------------------+ +| is_tree | ++---------------------+ + +Generators +------------ + ++-------------------------------+ +| **Classic** | ++===============================+ +| barbell_graph | ++-------------------------------+ +| circular_ladder_graph | ++-------------------------------+ +| complete_graph | ++-------------------------------+ +| complete_multipartite_graph | ++-------------------------------+ +| cycle_graph | ++-------------------------------+ +| empty_graph | ++-------------------------------+ +| ladder_graph | ++-------------------------------+ +| lollipop_graph | ++-------------------------------+ +| null_graph | ++-------------------------------+ +| path_graph | ++-------------------------------+ +| star_graph | ++-------------------------------+ +| tadpole_graph | ++-------------------------------+ +| trivial_graph | ++-------------------------------+ +| turan_graph | ++-------------------------------+ +| wheel_graph | ++-------------------------------+ + ++-----------------+ +| **Classic** | ++=================+ +| caveman_graph | ++-----------------+ + ++------------+ +| **Ego** | ++============+ +| ego_graph | ++------------+ + ++------------------------------+ +| **small** | ++==============================+ +| bull_graph | ++------------------------------+ +| chvatal_graph | ++------------------------------+ +| cubical_graph | ++------------------------------+ +| desargues_graph | ++------------------------------+ +| diamond_graph | ++------------------------------+ +| dodecahedral_graph | ++------------------------------+ +| frucht_graph | ++------------------------------+ +| heawood_graph | ++------------------------------+ +| house_graph | ++------------------------------+ +| house_x_graph | ++------------------------------+ +| icosahedral_graph | ++------------------------------+ +| krackhardt_kite_graph | ++------------------------------+ +| moebius_kantor_graph | ++------------------------------+ +| octahedral_graph | ++------------------------------+ +| pappus_graph | ++------------------------------+ +| petersen_graph | ++------------------------------+ +| sedgewick_maze_graph | ++------------------------------+ +| tetrahedral_graph | ++------------------------------+ +| truncated_cube_graph | ++------------------------------+ +| truncated_tetrahedron_graph | ++------------------------------+ +| tutte_graph | ++------------------------------+ + ++-------------------------------+ +| **Social** | ++===============================+ +| davis_southern_women_graph | ++-------------------------------+ +| florentine_families_graph | ++-------------------------------+ +| karate_club_graph | ++-------------------------------+ +| les_miserables_graph | ++-------------------------------+ + +Other +------- + ++-------------------------+ +| **Classes** | ++=========================+ +| is_negatively_weighted | ++-------------------------+ + ++----------------------+ +| **Convert** | ++======================+ +| from_dict_of_lists | ++----------------------+ +| to_dict_of_lists | ++----------------------+ + ++--------------------------+ +| **Convert Matrix** | ++==========================+ +| from_pandas_edgelist | ++--------------------------+ +| from_scipy_sparse_array | ++--------------------------+ + ++-----------------------------------+ +| **Relabel** | ++===================================+ +| convert_node_labels_to_integers | ++-----------------------------------+ +| relabel_nodes | ++-----------------------------------+ + + +To request nx-cugraph backend support for a NetworkX API that is not listed +above, visit the `cuGraph GitHub repo `_. diff --git a/python/nx-cugraph/_nx_cugraph/__init__.py b/python/nx-cugraph/_nx_cugraph/__init__.py index 428d266dd..a5e45979f 100644 --- a/python/nx-cugraph/_nx_cugraph/__init__.py +++ b/python/nx-cugraph/_nx_cugraph/__init__.py @@ -36,7 +36,7 @@ "backend_name": "cugraph", "project": "nx-cugraph", "package": "nx_cugraph", - "url": f"https://github.com/rapidsai/cugraph/tree/branch-{_version_major:0>2}.{_version_minor:0>2}/python/nx-cugraph", + "url": f"https://rapids.ai/nx-cugraph", "short_summary": "GPU-accelerated backend.", # "description": "TODO", "functions": { From f3002bae6e6099d57411f413b24c74c6f62f968d Mon Sep 17 00:00:00 2001 From: Ralph Liu <137829296+nv-rliu@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:51:54 -0400 Subject: [PATCH 19/21] Add `nx-cugraph` introduction notebook to repo (#4677) ## Proposed Changes This PR adds an introduction notebook to the `notebooks/demo` directory of the repository. Click the link to view the files directory in the dev branch. * [The introduction to `nx-cugraph` notebook](https://github.com/rapidsai/cugraph/blob/1a70b7ad588bcc1dc6f65af8fe07149ed8083ef0/notebooks/demo/accelerating_networkx.ipynb) --------- Co-authored-by: Erik Welch --- notebooks/demo/accelerating_networkx.ipynb | 614 +++++++++++++++++++++ 1 file changed, 614 insertions(+) create mode 100644 notebooks/demo/accelerating_networkx.ipynb diff --git a/notebooks/demo/accelerating_networkx.ipynb b/notebooks/demo/accelerating_networkx.ipynb new file mode 100644 index 000000000..1a6c6cfb3 --- /dev/null +++ b/notebooks/demo/accelerating_networkx.ipynb @@ -0,0 +1,614 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "R2cpVp2WdOsp" + }, + "source": [ + "# NetworkX - Easy Graph Analytics\n", + "\n", + "NetworkX is the most popular library for graph analytics available in Python, or quite possibly any language. To illustrate this, NetworkX was downloaded more than 71 million times in September of 2024 alone, which is roughly 71 times more than the next most popular graph analytics library! [*](https://en.wikipedia.org/wiki/NetworkX) NetworkX has earned this popularity from its very easy-to-use API, the wealth of documentation and examples available, the large (and friendly) community behind it, and its easy installation which requires nothing more than Python.\n", + "\n", + "However, NetworkX users are familiar with the tradeoff that comes with those benefits. The pure-Python implementation often results in poor performance when graph data starts to reach larger scales, limiting the usefulness of the library for many real-world problems.\n", + "\n", + "# Accelerated NetworkX - Easy (and fast!) Graph Analytics\n", + "\n", + "To address the performance problem, NetworkX 3.0 introduced a mechanism to dispatch algorithm calls to alternate implementations. The NetworkX Python API remains the same but NetworkX will use more capable algorithm implementations provided by one or more backends. This approach means users don't have to give up NetworkX -or even change their code- in order to take advantage of GPU performance." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xkg10FrNThrK" + }, + "source": [ + "# Let's Get the Environment Setup\n", + "This notebook will demonstrate NetworkX both with and without GPU acceleration provided by the `nx-cugraph` backend.\n", + "\n", + "`nx-cugraph` is available as a package installable using `pip`, `conda`, and [from source](https://github.com/rapidsai/nx-cugraph). Before importing `networkx`, lets install `nx-cugraph` so it can be registered as an available backend by NetworkX when needed. We'll use `pip` to install.\n", + "\n", + "NOTES:\n", + "* `nx-cugraph` requires a compatible NVIDIA GPU, NVIDIA CUDA and associated drivers, and a supported OS. Details about these and other installation prerequisites can be seen [here](https://docs.rapids.ai/install#system-req).\n", + "* The `nx-cugraph` package is currently hosted by NVIDIA and therefore the `--extra-index-url` option must be used.\n", + "* `nx-cugraph` is supported on specific 11.x and 12.x CUDA versions, and the major version number must be known in order to install the correct build (this is determined automatically when using `conda`).\n", + "\n", + "To find the CUDA major version on your system, run the following command:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NMFwzc1I95BS" + }, + "outputs": [], + "source": [ + "!nvcc --version" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i91Yj-yZ-nGS" + }, + "source": [ + "From the above output we can see we're using CUDA 12.x so we'll be installing `nx-cugraph-cu12`. If we were using CUDA 11.x, the package name would be `nx-cugraph-cu11`. We'll also be adding `https://pypi.nvidia.com` as an `--extra-index-url`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mYYN9EpnWphu" + }, + "outputs": [], + "source": [ + "!pip install nx-cugraph-cu12 --extra-index-url=https://pypi.nvidia.com" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0h1K-7tI_AZH" + }, + "source": [ + "Of course, we'll also be using `networkx`, which is already provided in the Colab environment. This notebook will be using features added in version 3.3, so we'll import it here to verify we have a compatible version." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YTV0ZTME2tV6" + }, + "outputs": [], + "source": [ + "import networkx as nx\n", + "nx.__version__" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UiZKOa3WC7be" + }, + "source": [ + "# Let's Start with Something Simple\n", + "\n", + "To begin, we'll compare NetworkX results without a backend to results of the same algorithm using the `nx-cugraph` backend on a small graph. `nx.karate_club_graph()` returns an instance of the famous example graph consisting of 34 nodes and 78 edges from Zachary's paper, described [here](https://en.wikipedia.org/wiki/Zachary%27s_karate_club)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3atL3tI0frYm" + }, + "source": [ + "## Betweenness Centrality\n", + "[Betweenness Centrality](https://en.wikipedia.org/wiki/Betweenness_centrality) is a graph algorithm that computes a centrality score for each node (`v`) based on how many of the shortest paths between pairs of nodes in the graph pass through `v`. A higher centrality score represents a node that \"connects\" other nodes in a network more than that of a node with a lower score.\n", + "\n", + "First, let's create a NetworkX Graph instance of the the Karate Club graph and inspect it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JSw7EZ46-kRu" + }, + "outputs": [], + "source": [ + "G = nx.karate_club_graph()\n", + "G.number_of_nodes(), G.number_of_edges()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_-E17u2gKgbC" + }, + "source": [ + "Next, let's run betweenness centrality and save the results. Because the Karate Club graph is so small, this should not take long." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qjxXXKJhKQ4s" + }, + "outputs": [], + "source": [ + "%%time\n", + "nx_bc_results = nx.betweenness_centrality(G)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ClrR3z9XMfLr" + }, + "source": [ + "Now, let's run the same algorithm on the same data using the `nx-cugraph` backend.\n", + "\n", + "There are several ways to instruct NetworkX to use a particular backend instead of the default implementation. Here, we will use the `config` API, which was added in NetworkX version 3.3.\n", + "\n", + "The following two lines set the backend to \"cugraph\" and enable graph conversion caching.\n", + "\n", + "Some notes:\n", + "* The standard convention for NetworkX backends is to name the package with a `nx-` prefix to denote that these are packages intended to be used with NetworkX, but the `nx-` prefix is not included when referring to them in NetworkX API calls. Here, `nx-cugraph` is the name of the backend package, and `\"cugraph\"` is the name NetworkX will use to refer to it.\n", + "* NetworkX can use multiple backends! `nx.config.backend_priority` is a list that can contain several backends, ordered based on priority. If a backend in the list cannot run a particular algorithm (either because it isn't supported in the backend, the algorithm doesn't support a particular option, or some other reason), NetworkX will try the next backend in the list. If no specified backend is able to run the algorithm, NetworkX will fall back to the default implementation.\n", + "* Many backends have their own data structures for representing an input graph, often optimized for that backend's implementation. Prior to running a backend algorithm, NetworkX will have the backend convert the standard NetworkX Graph instance to the backend-specific type. This conversion can be expensive, and rather than repeat it as part of each algorithm call, NetworkX can cache the conversion so it can be skipped on future calls if the graph doesn't change. This caching can save significant time and improve overall performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oFHwNqqsNsqS" + }, + "outputs": [], + "source": [ + "nx.config.backend_priority=[\"cugraph\"] # NETWORKX_BACKEND_PRIORITY=cugraph\n", + "nx.config.cache_converted_graphs=True # NETWORKX_CACHE_CONVERTED_GRAPHS=True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HrUeWRRQRzFP" + }, + "outputs": [], + "source": [ + "%%time\n", + "nxcg_bc_results = nx.betweenness_centrality(G)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "z1hxut3GTj5A" + }, + "source": [ + "You may have noticed that using the `nx-cugraph` backend resulted in a slightly slower execution time. This is not surprising when working with a graph this small, since the overhead of converting the graph for the first time and launching the algorithm kernel on the GPU is actually significantly more than the computation time itself. We'll see later that this overhead is negligible when compared to the time saved when running on a GPU for larger graphs.\n", + "\n", + "Since we've enabled graph conversion caching, we can see that if we re-run the same call the execution time is noticeably shorter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7a0XvpUOr9Ju" + }, + "outputs": [], + "source": [ + "%%time\n", + "nxcg_bc_results = nx.betweenness_centrality(G)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ppjE5J5RscOe" + }, + "source": [ + "Notice the warning above about using the cache. This will only be raised **once** per graph instance (it can also be easily disabled), but its purpose is to point out that the cache should not be used if the Graph object will have its attribute dictionary modified directly. In this case and many others, we won't be modifying the dictionaries directly. Instead, we will use APIs such as `nx.set_node_attributes` which properly clear the cache, so it's safe for us to use the cache. Because of that, we'll disable the warning so we don't see it on other graphs in this session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Namb5JLvwS-q" + }, + "outputs": [], + "source": [ + "import warnings\n", + "warnings.filterwarnings(\"ignore\", message=\"Using cached graph for 'cugraph' backend\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BzGAphcILFsT" + }, + "source": [ + "Smaller graphs are also easy to visualize with NetworkX's plotting utilities. The flexibility of NetworkX's `Graph` instances make it trivial to add the betweenness centrality scores back to the graph object as node attributes. This will allow us to use those values for the visualization.\n", + "\n", + "In this case, we'll create new attributes for each node called \"nx_bc\" for the default NetworkX results, and \"nxcg_bc\" for the nx-cugraph results. We'll use those values to assign the color for each node and plot two graphs side-by-side. This will make it easy to visually validate that the nodes with the higher centrality scores for both implementations match and do indeed appear to be more \"central\" to other nodes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1coV6ZfcUoqI" + }, + "outputs": [], + "source": [ + "nx.set_node_attributes(G, nx_bc_results, \"nx_bc\")\n", + "nx.set_node_attributes(G, nxcg_bc_results, \"nxcg_bc\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Sba2iYJgLoN2" + }, + "outputs": [], + "source": [ + "# Configure plot size and layout/position for each node\n", + "import matplotlib.pyplot as plt\n", + "plt.rcParams['figure.figsize'] = [12, 8]\n", + "pos = nx.spring_layout(G)\n", + "\n", + "# Assign colors for each set of betweenness centrality results\n", + "nx_colors = [G.nodes[n][\"nx_bc\"] for n in G.nodes()]\n", + "nxcg_colors = [G.nodes[n][\"nxcg_bc\"] for n in G.nodes()]\n", + "\n", + "# Plot the graph and color each node corresponding to NetworkX betweenness centrality values\n", + "plt.subplot(1, 2, 1)\n", + "nx.draw(G, pos=pos, with_labels=True, node_color=nx_colors)\n", + "\n", + "# Plot the graph and color each node corresponding to nx-cugraph betweenness centrality values\n", + "plt.subplot(1, 2, 2)\n", + "nx.draw(G, pos=pos, with_labels=True, node_color=nxcg_colors)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dJXH4Zn5VNSg" + }, + "source": [ + "As we can see, the same two nodes (`0` and `33`) are the two most central in both graphs, followed by `2`, `31`, and `32`.\n", + "\n", + "## PageRank\n", + "Another popular algorithm is [PageRank](https://en.wikipedia.org/wiki/PageRank). PageRank also assigns scores to each node, but these scores are based on analyzing links to each node to determine relative \"importance\" within the graph.\n", + "\n", + "Let's update the config to use the default NetworkX implementation and run `nx.pagerank`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9CdYNk62E1v_" + }, + "outputs": [], + "source": [ + "nx.config.backend_priority=[]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Jo39YxVmYolq" + }, + "outputs": [], + "source": [ + "%%time\n", + "nx_pr_results = nx.pagerank(G)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sV6dM8ToZDiC" + }, + "source": [ + "We could set `nx.config.backend_priority` again to list `\"cugraph\"` as the backend, but let's instead show how the `backend` kwarg can be used to override the priority list and force a specific backend to be used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oMSvQVGKY0rn" + }, + "outputs": [], + "source": [ + "%%time\n", + "nxcg_pr_results = nx.pagerank(G, backend=\"cugraph\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZGux_8xFZneI" + }, + "source": [ + "In this example, instead of plotting the graph to show that the results are identical, we can compare them directly using the saved values from both runs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "RcmtdFy4Zw7p" + }, + "outputs": [], + "source": [ + "sorted(nx_pr_results) == sorted(nxcg_pr_results)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mefjUEAnZ4pq" + }, + "source": [ + "# Working with Bigger Data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yLY-yl6PuNYo" + }, + "source": [ + "Now we'll look at a larger dataset from https://snap.stanford.edu/data/cit-Patents.html which contains citations across different U.S. patents granted from January 1, 1963 to December 30, 1999. The dataset represents 16.5M citations (edges) between 3.77M patents (nodes).\n", + "\n", + "This will demonstrate that data of this size starts to push the limits of the default pure-Python NetworkX implementation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lyYF0LbtFwjh" + }, + "outputs": [], + "source": [ + "# The locale encoding may have been modified from the plots above, reset here to run shell commands\n", + "import locale\n", + "locale.getpreferredencoding = lambda: \"UTF-8\"\n", + "!wget https://data.rapids.ai/cugraph/datasets/cit-Patents.csv # Skip if cit-Patents.csv already exists.\n", + "# !wget https://snap.stanford.edu/data/cit-Patents.txt.gz # Skip if cit-Patents.txt.gz already exists." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kjGINYphQSQ2" + }, + "outputs": [], + "source": [ + "%load_ext cudf.pandas\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iV4DieGZOalc" + }, + "outputs": [], + "source": [ + "%%time\n", + "df = pd.read_csv(\"cit-Patents.csv\",\n", + " sep=\" \",\n", + " names=[\"src\", \"dst\"],\n", + " dtype=\"int32\",\n", + ")\n", + "# df = pd.read_csv(\"cit-Patents.txt.gz\",\n", + "# compression=\"gzip\",\n", + "# skiprows=4,\n", + "# sep=\"\\t\",\n", + "# names=[\"src\", \"dst\"],\n", + "# dtype=\"int32\",\n", + "# )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PREA67u4eKat" + }, + "outputs": [], + "source": [ + "%%time\n", + "G = nx.from_pandas_edgelist(df, source=\"src\", target=\"dst\")\n", + "G.number_of_nodes(), G.number_of_edges()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NcsUxBqpu4zY" + }, + "source": [ + "By default, `nx.betweenness_centrality` will perform an all-pairs shortest path analysis when determining the centrality scores for each node. However, due to the much larger size of this graph, determining the shortest path for all pairs of nodes in the graph is not feasible. Instead, we'll use the parameter `k` to limit the number of shortest path computations used for determining the centrality scores, at the expense of accuracy. As we'll see when using a dataset this size with `nx.betweenness_centrality`, we have to limit `k` to `1` which is not practical but is sufficient here for demonstration purposes (since anything larger than `1` will result in many minutes of execution time)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gNDWbj3kAk3j" + }, + "outputs": [], + "source": [ + "%%time\n", + "bc_results = nx.betweenness_centrality(G, k=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NB8xmxMd1PlX" + }, + "source": [ + "Now we'll configure NetworkX to use the `nx-cugraph` backend (again, using the name convention that drops the package name's `nx-` prefix) and run the same call. Because this is a Graph that `nx-cugraph` hasn't seen before, the runtime will include the time to convert and cache a GPU-based graph." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xUYNG1xhvbWc" + }, + "outputs": [], + "source": [ + "nx.config.backend_priority = [\"cugraph\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cmK8ZuQGvfPo" + }, + "outputs": [], + "source": [ + "%%time\n", + "bc_results = nx.betweenness_centrality(G, k=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vdHb1YXP15TZ" + }, + "source": [ + "Let's run betweenness centrality again, now with a more useful number of samples by setting `k=100`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fKjIrzL-vrGS" + }, + "outputs": [], + "source": [ + "%%time\n", + "bc_results = nx.betweenness_centrality(G, k=100)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QeMcrAX2HZSM" + }, + "source": [ + "Let's also run pagerank on the same dataset to compare." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gR8ID6ekHgHt" + }, + "outputs": [], + "source": [ + "nx.config.backend_priority = []" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rTFuvX5wb_c1" + }, + "outputs": [], + "source": [ + "%%time\n", + "nx_pr_results = nx.pagerank(G)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8sJx9aeJV9hv" + }, + "outputs": [], + "source": [ + "%%time\n", + "nxcg_pr_results = nx.pagerank(G, backend=\"cugraph\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wGOVQ6ZyY4Ih" + }, + "outputs": [], + "source": [ + "sorted(nx_pr_results) == sorted(nxcg_pr_results)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "k2DfAaZaDIBj" + }, + "source": [ + "---\n", + "\n", + "Information on the U.S. Patent Citation Network dataset used in this notebook is as follows:\n", + "
Authors: Jure Leskovec and Andrej Krevl\n", + "
Title: SNAP Datasets, Stanford Large Network Dataset Collection\n", + "
URL: http://snap.stanford.edu/data\n", + "
Date: June 2014\n", + "
\n" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "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.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} From 6c1270eddb494b91594a33fa765487fbc7bea0ec Mon Sep 17 00:00:00 2001 From: Don Acosta <97529984+acostadon@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:19:20 -0400 Subject: [PATCH 20/21] Implementing some of the VDR feedback (#4674) Adding some content and navigation options per VDR, resolves https://github.com/rapidsai/graph_dl/issues/594 resolves https://github.com/rapidsai/graph_dl/issues/593 --------- Co-authored-by: rlratzel --- docs/cugraph/source/index.rst | 102 ++++++++++++++++++++------------ docs/cugraph/source/top_toc.rst | 13 ++++ 2 files changed, 76 insertions(+), 39 deletions(-) create mode 100644 docs/cugraph/source/top_toc.rst diff --git a/docs/cugraph/source/index.rst b/docs/cugraph/source/index.rst index 9ea9e4d65..259a36b8f 100644 --- a/docs/cugraph/source/index.rst +++ b/docs/cugraph/source/index.rst @@ -1,58 +1,82 @@ RAPIDS Graph documentation ========================== + .. image:: images/cugraph_logo_2.png :width: 600 -*Making graph analytics fast and easy regardless of scale* - - -.. list-table:: RAPIDS Graph covers a range of graph libraries and packages, that includes: - :widths: 25 25 25 - :header-rows: 1 - - * - Core - - GNN - - Extension - * - :abbr:`cugraph (Python wrapper with lots of convenience functions)` - - :abbr:`cugraph-ops (GNN aggregators and operators)` - - :abbr:`cugraph-service (Graph-as-a-service provides both Client and Server packages)` - * - :abbr:`pylibcugraph (light-weight Python wrapper with no guard rails)` - - :abbr:`cugraph-dgl (Accelerated extensions for use with the DGL framework)` - - - * - :abbr:`libcugraph (C++ API)` - - :abbr:`cugraph-pyg (Accelerated extensions for use with the PyG framework)` - - - * - :abbr:`libcugraph_etl (C++ renumbering function for strings)` - - :abbr:`wholegraph (Shared memory-based GPU-accelerated GNN training)` - - -.. -| ~~~~~~~~~~~~ Introduction ~~~~~~~~~~~~ cuGraph is a library of graph algorithms that seamlessly integrates into the RAPIDS data science ecosystem and allows the data scientist to easily call -graph algorithms using data stored in GPU DataFrames, NetworkX Graphs, or -even CuPy or SciPy sparse Matrices. +graph algorithms using data stored in GPU DataFrames, NetworkX Graphs, or even +CuPy or SciPy sparse Matrices. Our major integration effort with NetworkX +allows for **zero code change** GPU acceleration through the use of the +nx-cugraph backend. NetworkX and the nx-cugraph backend offer a seamless +transition to GPU accelerated graph analytics for NetworkX users with access to +a supported GPU. + +Getting started with cuGraph + +Required hardware/software for cuGraph and `RAPIDS `_ + * NVIDIA GPU, Volta architecture or later, with `compute capability 7.0+`_ + * CUDA 11.2-11.8, 12.0-12.5 + * Python version 3.10, 3.11, or 3.12 + * NetworkX version 3.0 or newer in order to use use the nx-cuGraph backend. NetworkX version 3.4 or newer is recommended. (`see below <#cugraph-using-networkx-code>`). + +Installation +The latest RAPIDS System Requirements documentation is located `here `_. + +This includes several ways to set up cuGraph + +* From Unix + + * `Conda `_ + * `Docker `_ + * `pip `_ + + +**Note: Windows use of RAPIDS depends on prior installation of** `WSL2 `_. + +* From Windows -Note: We are redoing all of our documents, please be patient as we update -the docs and links + * `Conda `_ + * `Docker `_ + * `pip `_ -| + +cuGraph Using NetworkX Code + +cuGraph is now available as a NetworkX backend using `nx-cugraph `_. +nx-cugraph offers NetworkX users a **zero code change** option to accelerate +their existing NetworkX code using an NVIDIA GPU and cuGraph. + + + Cugraph API Example + + .. code-block:: python + + import cugraph + import cudf + + # Create an instance of the popular Zachary Karate Club graph + from cugraph.datasets import karate + G = karate.get_graph() + + # Call cugraph.degree_centrality + vertex_bc = cugraph.degree_centrality(G) + +There are several resources containing cuGraph examples, `the cuGraph notebook repository `_ +has many examples of loading graph data and running algorithms in Jupyter notebooks. +The `cuGraph test code _` contain python scripts setting up and calling cuGraph algorithms. +A simple example of `testing the degree centrality algorithm `_ +is a good place to start. Some of these show `multi-GPU tests/examples `_ with larger data sets as well. .. toctree:: :maxdepth: 2 - :caption: Contents: - - basics/index - nx_cugraph/index - installation/index - tutorials/index - graph_support/index - wholegraph/index - references/index - api_docs/index + + top_toc Indices and tables ================== diff --git a/docs/cugraph/source/top_toc.rst b/docs/cugraph/source/top_toc.rst new file mode 100644 index 000000000..8e31e70ca --- /dev/null +++ b/docs/cugraph/source/top_toc.rst @@ -0,0 +1,13 @@ +.. toctree:: + :maxdepth: 2 + :caption: cuGraph documentation Contents: + :name: top_toc + + basics/index + nx_cugraph/index + installation/index + tutorials/index + graph_support/index + wholegraph/index + references/index + api_docs/index From 563fc3ed2f498a1d4382e760702d976c7d757d84 Mon Sep 17 00:00:00 2001 From: Ralph Liu Date: Thu, 3 Oct 2024 11:47:01 -0700 Subject: [PATCH 21/21] Remove unrelated docs --- .../installation/getting_wholegraph.md | 48 ----- .../wholegraph/installation/source_build.md | 186 ------------------ 2 files changed, 234 deletions(-) delete mode 100644 docs/cugraph/source/wholegraph/installation/getting_wholegraph.md delete mode 100644 docs/cugraph/source/wholegraph/installation/source_build.md diff --git a/docs/cugraph/source/wholegraph/installation/getting_wholegraph.md b/docs/cugraph/source/wholegraph/installation/getting_wholegraph.md deleted file mode 100644 index 80c666d65..000000000 --- a/docs/cugraph/source/wholegraph/installation/getting_wholegraph.md +++ /dev/null @@ -1,48 +0,0 @@ - -# Getting the WholeGraph Packages - -Start by reading the [RAPIDS Instalation guide](https://docs.rapids.ai/install) -and checkout the [RAPIDS install selector](https://rapids.ai/start.html) for a pick list of install options. - - -There are 4 ways to get WholeGraph packages: -1. [Quick start with Docker Repo](#docker) -2. [Conda Installation](#conda) -3. [Pip Installation](#pip) -4. [Build from Source](./source_build.md) - - -
- -## Docker -The RAPIDS Docker containers (as of Release 23.10) contain all RAPIDS packages, including WholeGraph, as well as all required supporting packages. To download a container, please see the [Docker Repository](https://hub.docker.com/r/rapidsai/rapidsai/), choosing a tag based on the NVIDIA CUDA version you’re running. This provides a ready to run Docker container with example notebooks and data, showcasing how you can utilize all of the RAPIDS libraries. - -
- - -## Conda -It is easy to install WholeGraph using conda. You can get a minimal conda installation with [miniforge](https://github.com/conda-forge/miniforge). - -WholeGraph conda packages - * libwholegraph - * pylibwholegraph - -Replace the package name in the example below to the one you want to install. - - -Install and update WholeGraph using the conda command: - -```bash -conda install -c rapidsai -c conda-forge -c nvidia wholegraph cudatoolkit=11.8 -``` - -
- -## PIP -wholegraph, and all of RAPIDS, is available via pip. - -``` -pip install wholegraph-cu11 --extra-index-url=https://pypi.nvidia.com -``` - -
diff --git a/docs/cugraph/source/wholegraph/installation/source_build.md b/docs/cugraph/source/wholegraph/installation/source_build.md deleted file mode 100644 index 7213cbfb0..000000000 --- a/docs/cugraph/source/wholegraph/installation/source_build.md +++ /dev/null @@ -1,186 +0,0 @@ -# Building from Source - -The following instructions are for users wishing to build wholegraph from source code. These instructions are tested on supported distributions of Linux,CUDA, -and Python - See [RAPIDS Getting Started](https://rapids.ai/start.html) for a list of supported environments. -Other operating systems _might be_ compatible, but are not currently tested. - -The wholegraph package includes both a C/C++ CUDA portion and a python portion. Both libraries need to be installed in order for cuGraph to operate correctly. -The C/C++ CUDA library is `libwholegraph` and the python library is `pylibwholegraph`. - -## Prerequisites - -__Compiler__: -* `gcc` version 11.0+ -* `nvcc` version 11.0+ -* `cmake` version 3.26.4+ - -__CUDA__: -* CUDA 11.8+ -* Volta architecture or better - -You can obtain CUDA from [https://developer.nvidia.com/cuda-downloads](https://developer.nvidia.com/cuda-downloads). - -__Other Packages__: -* ninja -* nccl -* cython -* setuputils3 -* scikit-learn -* scikit-build-core -* nanobind>=0.2.0 - -## Building wholegraph -To install wholegraph from source, ensure the dependencies are met. - -### Clone Repo and Configure Conda Environment -__GIT clone a version of the repository__ - - ```bash - # Set the location to wholegraph in an environment variable WHOLEGRAPH_HOME - export WHOLEGRAPH_HOME=$(pwd)/wholegraph - - # Download the wholegraph repo - if you have a forked version, use that path here instead - git clone https://github.com/rapidsai/wholegraph.git $WHOLEGRAPH_HOME - - cd $WHOLEGRAPH_HOME - ``` - -__Create the conda development environment__ - -```bash -# create the conda environment (assuming in base `wholegraph` directory) - -# for CUDA 11.x -conda env create --name wholegraph_dev --file conda/environments/all_cuda-118_arch-x86_64.yaml - -# activate the environment -conda activate wholegraph_dev - -# to deactivate an environment -conda deactivate -``` - - - The environment can be updated as development includes/changes the dependencies. To do so, run: - - -```bash - -# Where XXX is the CUDA version -conda env update --name wholegraph_dev --file conda/environments/all_cuda-XXX_arch-x86_64.yaml - -conda activate wholegraph_dev -``` - - -### Build and Install Using the `build.sh` Script -Using the `build.sh` script make compiling and installing wholegraph a -breeze. To build and install, simply do: - -```bash -$ cd $WHOLEGRAPH_HOME -$ ./build.sh clean -$ ./build.sh libwholegraph -$ ./build.sh pylibwholegraph -``` - -There are several other options available on the build script for advanced users. -`build.sh` options: -```bash -build.sh [ ...] [ ...] - where is: - clean - remove all existing build artifacts and configuration (start over). - uninstall - uninstall libwholegraph and pylibwholegraph from a prior build/install (see also -n) - libwholegraph - build the libwholegraph C++ library. - pylibwholegraph - build the pylibwholegraph Python package. - tests - build the C++ (OPG) tests. - benchmarks - build benchmarks. - docs - build the docs - and is: - -v - verbose build mode - -g - build for debug - -n - no install step - --allgpuarch - build for all supported GPU architectures - --cmake-args=\\\"\\\" - add arbitrary CMake arguments to any cmake call - --compile-cmd - only output compile commands (invoke CMake without build) - --clean - clean an individual target (note: to do a complete rebuild, use the clean target described above) - -h | --h[elp] - print this text - - default action (no args) is to build and install 'libwholegraph' then 'pylibwholegraph' targets - -examples: -$ ./build.sh clean # remove prior build artifacts (start over) -$ ./build.sh - -# make parallelism options can also be defined: Example build jobs using 4 threads (make -j4) -$ PARALLEL_LEVEL=4 ./build.sh libwholegraph - -Note that the libraries will be installed to the location set in `$PREFIX` if set (i.e. `export PREFIX=/install/path`), otherwise to `$CONDA_PREFIX`. -``` - - -## Building each section independently -### Build and Install the C++/CUDA `libwholegraph` Library -CMake depends on the `nvcc` executable being on your path or defined in `$CUDACXX`. - -This project uses cmake for building the C/C++ library. To configure cmake, run: - - ```bash - # Set the location to wholegraph in an environment variable WHOLEGRAPH_HOME - export WHOLEGRAPH_HOME=$(pwd)/wholegraph - - cd $WHOLEGRAPH_HOME - cd cpp # enter cpp directory - mkdir build # create build directory - cd build # enter the build directory - cmake .. -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX - - # now build the code - make -j # "-j" starts multiple threads - make install # install the libraries - ``` -The default installation locations are `$CMAKE_INSTALL_PREFIX/lib` and `$CMAKE_INSTALL_PREFIX/include/wholegraph` respectively. - -### Building and installing the Python package - -Build and Install the Python packages to your Python path: - -```bash -cd $WHOLEGRAPH_HOME -cd python -cd pylibwholegraph -python setup.py build_ext --inplace -python setup.py install # install pylibwholegraph -``` - -## Run tests - -Run either the C++ or the Python tests with datasets - - - **Python tests with datasets** - - ```bash - cd $WHOLEGRAPH_HOME - cd python - pytest - ``` - - - **C++ stand alone tests** - - From the build directory : - - ```bash - # Run the tests - cd $WHOLEGRAPH_HOME - cd cpp/build - gtests/PARALLEL_UTILS_TESTS # this is an executable file - ``` - - -Note: This conda installation only applies to Linux and Python versions 3.10, 3.11, and 3.12. - -## Creating documentation - -Python API documentation can be generated from _./docs/wholegraph directory_. Or through using "./build.sh docs" - -## Attribution -Portions adopted from https://github.com/pytorch/pytorch/blob/master/CONTRIBUTING.md