Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 09eb560

Browse files
authoredApr 23, 2021
Merge pull request #9 from eriknw/cython_utils
Add Cython utils needed for zero-copy import and export.
2 parents 620bf5d + 225fb81 commit 09eb560

File tree

14 files changed

+2707
-29
lines changed

14 files changed

+2707
-29
lines changed
 

‎.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
suitesparse_graphblas/_version.py export-subst

‎LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
same "printed page" as the copyright notice for easier
187187
identification within third-party archives.
188188

189-
Copyright [yyyy] [name of copyright owner]
189+
Copyright 2021 Anaconda Inc., Graphegon, and contributors
190190

191191
Licensed under the Apache License, Version 2.0 (the "License");
192192
you may not use this file except in compliance with the License.

‎MANIFEST.in

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
include setup.py
2+
include README.md
3+
include LICENSE
4+
include suitesparse_graphblas/*.pxd
5+
include suitesparse_graphblas/*.pyx
6+
include suitesparse_graphblas/*.h
7+
include versioneer.py
8+
include suitesparse_graphblas/_version.py

‎pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[build-system]
2+
requires = ["setuptools", "wheel", "numpy>=1.15"]
3+
4+
[tool.black]
5+
line-length = 100

‎setup.cfg

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[aliases]
2+
test=pytest
3+
4+
[flake8]
5+
max-line-length = 100
6+
exclude =
7+
versioneer.py,
8+
ignore =
9+
E203, # whitespace before ':'
10+
E231, # Multiple spaces around ","
11+
W503, # line break before binary operator
12+
13+
[coverage:run]
14+
source = suitesparse_graphblas
15+
plugins = Cython.Coverage
16+
omit =
17+
suitesparse_graphblas/_version.py
18+
19+
[coverage:report]
20+
# Regexes for lines to exclude from consideration
21+
exclude_lines =
22+
pragma: no cover
23+
24+
raise AssertionError
25+
raise NotImplementedError
26+
27+
[versioneer]
28+
VCS = git
29+
style = pep440
30+
versionfile_source = suitesparse_graphblas/_version.py
31+
versionfile_build = suitesparse_graphblas/_version.py
32+
tag_prefix=
33+
parentdir_prefix=suitesparse_graphblas-
34+
35+
[tool:pytest]
36+
testpaths = suitesparse_graphblas/tests
37+

‎setup.py

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,69 @@
1-
from setuptools import setup, find_packages
1+
from setuptools import setup, find_packages, Extension
2+
from glob import glob
3+
4+
try:
5+
from Cython.Build import cythonize
6+
from Cython.Compiler.Options import get_directive_defaults
7+
8+
use_cython = True
9+
except ImportError:
10+
use_cython = False
11+
import numpy as np
12+
import os
13+
import sys
14+
import versioneer
15+
16+
define_macros = [("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")]
17+
18+
if use_cython:
19+
suffix = ".pyx"
20+
directive_defaults = get_directive_defaults()
21+
directive_defaults["binding"] = True
22+
directive_defaults["language_level"] = 3
23+
if os.environ.get("CYTHON_COVERAGE"):
24+
directive_defaults["linetrace"] = True
25+
define_macros.append(("CYTHON_TRACE_NOGIL", "1"))
26+
else:
27+
suffix = ".c"
28+
29+
include_dirs = [np.get_include(), os.path.join(sys.prefix, "include")]
30+
ext_modules = [
31+
Extension(
32+
name[: -len(suffix)].replace("/", ".").replace("\\", "."),
33+
[name],
34+
include_dirs=include_dirs,
35+
define_macros=define_macros,
36+
)
37+
for name in glob(f"suitesparse_graphblas/**/*{suffix}", recursive=True)
38+
]
39+
if use_cython:
40+
ext_modules = cythonize(ext_modules, include_path=include_dirs)
41+
42+
with open("README.md") as f:
43+
long_description = f.read()
44+
45+
package_data = {"suitesparse_graphblas": ["*.pyx", "*.pxd", "*.h"]}
46+
if sys.platform == "win32":
47+
package_data["suitesparse_graphblas"].append("*.dll")
248

349
setup(
4-
name='suitesparse-graphblas',
5-
version='4.0.3',
6-
description='SuiteSparse:GraphBLAS Python bindings.',
50+
name="suitesparse-graphblas",
51+
version=versioneer.get_version(),
52+
cmdclass=versioneer.get_cmdclass(),
53+
description="SuiteSparse:GraphBLAS Python bindings.",
54+
long_description=long_description,
55+
long_description_content_type="text/markdown",
756
packages=find_packages(),
8-
author='Michel Pelletier, James Kitchen, Erik Welch',
57+
author="Michel Pelletier, James Kitchen, Erik Welch",
58+
author_email="michel@graphegon.com,jim22k@gmail.com,erik.n.welch@gmail.com",
59+
url="https://github.com/GraphBLAS/python-suitesparse-graphblas",
60+
ext_modules=ext_modules,
961
cffi_modules=["suitesparse_graphblas/build.py:ffibuilder"],
10-
install_requires=["cffi>=1.0.0"],
62+
python_requires=">=3.7",
63+
install_requires=["cffi>=1.0.0", "numpy>=1.15"],
1164
setup_requires=["cffi>=1.0.0", "pytest-runner"],
1265
tests_require=["pytest"],
66+
license="Apache License 2.0",
67+
package_data=package_data,
68+
include_package_data=True,
1369
)
14-

‎suitesparse_graphblas/__init__.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,44 @@
1-
from ._graphblas import ffi, lib
1+
from ._graphblas import ffi, lib # noqa
2+
from . import utils
3+
from ._version import get_versions
4+
5+
6+
def is_initialized():
7+
"""Is GraphBLAS initialized via GrB_init or GxB_init?"""
8+
return lib.GxB_Global_Option_get(lib.GxB_MODE, ffi.new("GrB_Mode*")) != lib.GrB_PANIC
9+
10+
11+
def initialize(*, blocking=False, memory_manager="numpy"):
12+
"""Initialize GraphBLAS via GrB_init or GxB_init.
13+
14+
This must be called before any other GraphBLAS functions are called.
15+
A RuntimeError will be raised if called more than once.
16+
17+
Parameters
18+
----------
19+
blocking : bool, optional
20+
Whether to call init with GrB_BLOCKING or GrB_NONBLOCKING.
21+
Default is False.
22+
memory_manager : {'numpy', 'c'}, optional
23+
Choose which malloc/free functions to use. 'numpy' uses numpy's
24+
allocators, which makes it safe to perform zero-copy to and from numpy,
25+
and allows Python to track memory usage via tracemalloc (if enabled).
26+
'c' uses the default allocators. Default is 'numpy'.
27+
28+
The global variable `suitesparse_graphblas.is_initialized` indicates whether
29+
GraphBLAS has been initialized.
30+
"""
31+
if is_initialized():
32+
raise RuntimeError("GraphBLAS is already initialized! Unable to initialize again.")
33+
blocking = lib.GrB_BLOCKING if blocking else lib.GrB_NONBLOCKING
34+
memory_manager = memory_manager.lower()
35+
if memory_manager == "numpy":
36+
utils.call_gxb_init(ffi, lib, blocking)
37+
elif memory_manager == "c":
38+
lib.GrB_init(blocking)
39+
else:
40+
raise ValueError(f'memory_manager argument must be "numpy" or "c"; got: {memory_manager!r}')
41+
42+
43+
__version__ = get_versions()["version"]
44+
del get_versions

‎suitesparse_graphblas/_version.py

Lines changed: 548 additions & 0 deletions
Large diffs are not rendered by default.

‎suitesparse_graphblas/create_headers.py

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -380,9 +380,7 @@ def get_groups(ast):
380380
seen.update(vals)
381381
groups["GrB typedef"] = sorted(vals, key=sort_key)
382382

383-
missing_typedefs = {
384-
x for x in lines if "typedef" in x and "GB" in x and "(" not in x
385-
} - seen
383+
missing_typedefs = {x for x in lines if "typedef" in x and "GB" in x and "(" not in x} - seen
386384
assert not missing_typedefs
387385
assert all(x.endswith(";") for x in seen) # sanity check
388386

@@ -393,20 +391,14 @@ def get_groups(ast):
393391
vals = {x for x in enums if "} GrB" in x}
394392
for val in vals:
395393
seen.update(val.splitlines())
396-
groups["GrB typedef enums"] = sorted(
397-
vals, key=lambda x: sort_key(x.rsplit("}", 1)[-1])
398-
)
394+
groups["GrB typedef enums"] = sorted(vals, key=lambda x: sort_key(x.rsplit("}", 1)[-1]))
399395

400396
vals = {x for x in enums if "} GxB" in x}
401397
for val in vals:
402398
seen.update(val.splitlines())
403-
groups["GxB typedef enums"] = sorted(
404-
vals, key=lambda x: sort_key(x.rsplit("}", 1)[-1])
405-
)
399+
groups["GxB typedef enums"] = sorted(vals, key=lambda x: sort_key(x.rsplit("}", 1)[-1]))
406400

407-
missing_enums = (
408-
set(enums) - set(groups["GrB typedef enums"]) - set(groups["GxB typedef enums"])
409-
)
401+
missing_enums = set(enums) - set(groups["GrB typedef enums"]) - set(groups["GxB typedef enums"])
410402
assert not missing_enums
411403

412404
vals = {x for x in lines if "typedef" in x and "GxB" in x} - seen
@@ -757,9 +749,7 @@ def main():
757749

758750
# Run it through the preprocessor
759751
print(f"Step 2: run preprocessor to create {processed_h}")
760-
include = os.path.join(
761-
os.path.dirname(pycparser.__file__), "utils", "fake_libc_include"
762-
)
752+
include = os.path.join(os.path.dirname(pycparser.__file__), "utils", "fake_libc_include")
763753
command = (
764754
f"gcc -nostdinc -E -I{include} {graphblas_h} "
765755
f"| sed 's/ complex / _Complex /g' > {processed_h}"
@@ -804,15 +794,12 @@ def main():
804794
unknown_defines = defines - DEFINES - CHAR_DEFINES - IGNORE_DEFINES
805795
if unknown_defines:
806796
raise ValueError(
807-
f"Unknown #define values found in {graphblas_h}: "
808-
+ ", ".join(sorted(unknown_defines))
797+
f"Unknown #define values found in {graphblas_h}: " + ", ".join(sorted(unknown_defines))
809798
)
810799
print("Success!", "\N{ROCKET}")
811800
if args.show_skipped:
812801
print()
813-
print(
814-
f"Showing lines from {processed_h} that were skipped when creating {final_h}:"
815-
)
802+
print(f"Showing lines from {processed_h} that were skipped when creating {final_h}:")
816803
print("-" * 80)
817804
for line in sorted(groups["not seen"], key=sort_key):
818805
print(line)

‎suitesparse_graphblas/tests/__init__.py

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from suitesparse_graphblas import lib, ffi
22

3+
34
def test_matrix_existence():
45
assert hasattr(lib, "GrB_Matrix_new")

‎suitesparse_graphblas/utils.pxd

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from numpy cimport ndarray
2+
from libc.stdint cimport uint64_t
3+
4+
cdef extern from "numpy/arrayobject.h" nogil:
5+
# These aren't public (i.e., "extern"), but other projects use them too
6+
void *PyDataMem_NEW(size_t)
7+
void *PyDataMem_NEW_ZEROED(size_t, size_t)
8+
void *PyDataMem_RENEW(void *, size_t)
9+
void PyDataMem_FREE(void *)
10+
# These are available in newer Cython versions
11+
void PyArray_ENABLEFLAGS(ndarray, int flags)
12+
void PyArray_CLEARFLAGS(ndarray, int flags)
13+
14+
ctypedef enum GrB_Mode:
15+
GrB_NONBLOCKING
16+
GrB_BLOCKING
17+
18+
ctypedef uint64_t (*GxB_init)(
19+
GrB_Mode,
20+
void *(*user_malloc_function)(size_t),
21+
void *(*user_calloc_function)(size_t, size_t),
22+
void *(*user_realloc_function)(void *, size_t),
23+
void (*user_free_function)(void *),
24+
bint, # user_malloc_is_thread_safe
25+
)
26+
27+
cpdef int call_gxb_init(ffi, lib, int mode)
28+
29+
cpdef ndarray claim_buffer(ffi, cdata, size_t size, dtype)
30+
31+
cpdef ndarray claim_buffer_2d(ffi, cdata, size_t cdata_size, size_t nrows, size_t ncols, dtype, bint is_c_order)
32+
33+
cpdef unclaim_buffer(ndarray array)
34+

‎suitesparse_graphblas/utils.pyx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import numpy as np
2+
from numpy cimport (
3+
import_array, ndarray, npy_intp,
4+
PyArray_SimpleNewFromData, PyArray_New,
5+
NPY_ARRAY_OWNDATA, NPY_ARRAY_WRITEABLE, NPY_ARRAY_F_CONTIGUOUS,
6+
)
7+
from libc.stdint cimport uintptr_t
8+
9+
import_array()
10+
11+
cpdef int call_gxb_init(ffi, lib, int mode):
12+
# We need to call `GxB_init`, but we didn't compile Cython against GraphBLAS. So, we get it from cffi.
13+
# Step 1: ffi.addressof(lib, "GxB_init")
14+
# Return type: cffi.cdata object of a function pointer. Can't cast to int.
15+
# Step 2: ffi.cast("uintptr_t", ...)
16+
# Return type: cffi.cdata object of a uintptr_t type, an unsigned pointer. Can cast to int.
17+
# Step 3: int(...)
18+
# Return type: int. The physical address of the function.
19+
# Step 4: <uintptr_t>(...)
20+
# Return type: uintptr_t in Cython. Cast Python int to Cython integer for pointers.
21+
# Step 5: <GsB_init>(...)
22+
# Return: function pointer in Cython!
23+
24+
cdef GxB_init func = <GxB_init><uintptr_t>int(ffi.cast("uintptr_t", ffi.addressof(lib, "GxB_init")))
25+
return func(<GrB_Mode>mode, PyDataMem_NEW, PyDataMem_NEW_ZEROED, PyDataMem_RENEW, PyDataMem_FREE, True)
26+
27+
28+
cpdef ndarray claim_buffer(ffi, cdata, size_t size, dtype):
29+
cdef:
30+
npy_intp dims = size
31+
uintptr_t ptr = int(ffi.cast("uintptr_t", cdata))
32+
ndarray array = PyArray_SimpleNewFromData(1, &dims, dtype.num, <void*>ptr)
33+
PyArray_ENABLEFLAGS(array, NPY_ARRAY_OWNDATA)
34+
return array
35+
36+
37+
cpdef ndarray claim_buffer_2d(ffi, cdata, size_t cdata_size, size_t nrows, size_t ncols, dtype, bint is_c_order):
38+
cdef:
39+
size_t size = nrows * ncols
40+
ndarray array
41+
uintptr_t ptr
42+
npy_intp dims[2]
43+
if cdata_size == size:
44+
ptr = int(ffi.cast("uintptr_t", cdata))
45+
dims[0] = nrows
46+
dims[1] = ncols
47+
if is_c_order:
48+
array = PyArray_SimpleNewFromData(2, dims, dtype.num, <void*>ptr)
49+
else:
50+
array = PyArray_New(
51+
ndarray, 2, dims, dtype.num, NULL, <void*>ptr, -1,
52+
NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_WRITEABLE, <object>NULL
53+
)
54+
PyArray_ENABLEFLAGS(array, NPY_ARRAY_OWNDATA)
55+
elif cdata_size > size: # pragma: no cover
56+
array = claim_buffer(ffi, cdata, cdata_size, dtype)
57+
if is_c_order:
58+
array = array[:size].reshape((nrows, ncols))
59+
else:
60+
array = array[:size].reshape((ncols, nrows)).T
61+
else: # pragma: no cover
62+
raise ValueError(
63+
f"Buffer size too small: {cdata_size}. "
64+
f"Unable to create matrix of size {nrows}x{ncols} = {size}"
65+
)
66+
return array
67+
68+
69+
cpdef unclaim_buffer(ndarray array):
70+
PyArray_CLEARFLAGS(array, NPY_ARRAY_OWNDATA | NPY_ARRAY_WRITEABLE)

‎versioneer.py

Lines changed: 1889 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.