Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nx-cugraph: Updates nxcg.Graph classes for API-compatibility with NetworkX Graph classes, needed for zero code change graph generators #4629

Merged
merged 12 commits into from
Sep 24, 2024
Merged
3 changes: 2 additions & 1 deletion ci/run_nx_cugraph_pytests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ set -euo pipefail
# Support invoking run_nx_cugraph_pytests.sh outside the script directory
cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../python/nx-cugraph/nx_cugraph

pytest --capture=no --cache-clear --benchmark-disable "$@" tests
NX_CUGRAPH_USE_COMPAT_GRAPHS=False pytest --capture=no --cache-clear --benchmark-disable "$@" tests
NX_CUGRAPH_USE_COMPAT_GRAPHS=True pytest --capture=no --cache-clear --benchmark-disable "$@" tests
Comment on lines +9 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should minimize the public API surface area and expose only the "compat" graph. We're not getting backwards compatibility by exposing both classes anyway, so we can expose GPUGraph (which I'm assuming is the non-compat graph) to users later if needed.
Or...do we get additional coverage by testing both compat and non-compat separately? I'd still prefer to start small and expand the API/our support burden as needed, but I can appreciate if it's used to get additional test coverage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is used to get addition test coverage. We can change the environment variable to something like _NX_CUGRAPH_USE_COMPAT_GRAPHS and use it as an internal implementation detail.

2 changes: 1 addition & 1 deletion ci/test_python.sh
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ echo "nx-cugraph coverage from networkx tests: $_coverage"
echo $_coverage | awk '{ if ($NF == "0.0%") exit 1 }'
# Ensure all algorithms were called by comparing covered lines to function lines.
# Run our tests again (they're fast enough) to add their coverage, then create coverage.json
pytest \
NX_CUGRAPH_USE_COMPAT_GRAPHS=False pytest \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is False? It reads as if we're not getting test coverage from the NX suite for the "compat" Graph, which seems bad, but maybe it's necessary for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should work for both True and False, and it would be great to be able to test both options. False (not "compat" graph) matches how we were testing before.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be great to be able to test both options

Why can't we test both? If we have to pick one, shouldn't we pick compat since (I'm assuming) it will be used much more?

--pyargs nx_cugraph \
--config-file=../pyproject.toml \
--cov-config=../pyproject.toml \
Expand Down
1 change: 1 addition & 0 deletions ci/test_wheel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ else
DASK_DISTRIBUTED__SCHEDULER__WORKER_TTL="1000s" \
DASK_DISTRIBUTED__COMM__TIMEOUTS__CONNECT="1000s" \
DASK_CUDA_WAIT_WORKERS_MIN_TIMEOUT="1000s" \
NX_CUGRAPH_USE_COMPAT_GRAPHS=False \
python -m pytest \
-v \
--import-mode=append \
Expand Down
1 change: 0 additions & 1 deletion conda/environments/all_cuda-118_arch-x86_64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ dependencies:
- nvcc_linux-64=11.8
- ogb
- openmpi
- packaging>=21
- pandas
- pre-commit
- pydantic
Expand Down
1 change: 0 additions & 1 deletion conda/environments/all_cuda-125_arch-x86_64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ dependencies:
- numpydoc
- ogb
- openmpi
- packaging>=21
- pandas
- pre-commit
- pydantic
Expand Down
1 change: 0 additions & 1 deletion dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,6 @@ dependencies:
common:
- output_types: [conda, pyproject]
packages:
- packaging>=21
# not needed by nx-cugraph tests, but is required for running networkx tests
- pytest-mpl
cugraph_dgl_dev:
Expand Down
17 changes: 15 additions & 2 deletions python/nx-cugraph/_nx_cugraph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

$ python _nx_cugraph/__init__.py
"""
import os

from _nx_cugraph._version import __version__

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
16 changes: 8 additions & 8 deletions python/nx-cugraph/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
14 changes: 11 additions & 3 deletions python/nx-cugraph/nx_cugraph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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})"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)."
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion python/nx-cugraph/nx_cugraph/algorithms/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 10 additions & 2 deletions python/nx-cugraph/nx_cugraph/algorithms/operators/unary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
)


Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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"
)
Expand Down
Loading
Loading