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

MAINT: wrap sparsetools manually instead via SWIG #3440

Merged
merged 18 commits into from
Mar 15, 2014

Conversation

pv
Copy link
Member

@pv pv commented Mar 8, 2014

This PR changes sparsetools to be wrapped via custom code generation rather than via SWIG.

This reduces memory and time requirements for compiling sparsetools. The approach is scalable: the set of routines can be easily split into smaller compilation units if necessary. Code generation is done at build time, so there are also no huge autogenerated files checked in the VCS.

Reduction in memory requirements likely originates from tighter, manually written, Python argument parsing code, which is shared between all routines.

A convenience drawback is that the argument specs for each function needs to be typed in manually, rather than parsed directly from the C++ code. However, this is implemented so that errors in the argument list specs result to compilation errors rather than crashes at runtime.

I also took a look at Cython: fused types don't seem to play together with C++ templates at the moment. A similar code generation framework and call_thunk would be then needed also with Cython, and I think the present approach is less magical. (The other suggestion of replacing this with templated C also requires the type dispatch mechanism to be written, and would probably cause pain when rewriting the boolean and complex ops.)

Some statistics:

#
# Before (master @ 7c64f1969)
#
$ /usr/bin/time -v g++ -Wall -D__STDC_FORMAT_MACROS -O3 -c -o /dev/null -Isparsetools -I/usr/include/python2.7 sparsetools/bsr_wrap.cxx
    Elapsed (wall clock) time (h:mm:ss or m:ss): 1:49.53
    Maximum resident set size (kbytes): 1140424

$ /usr/bin/time -v g++ -Wall -D__STDC_FORMAT_MACROS -O3 -c -o /dev/null -Isparsetools -I/usr/include/python2.7 sparsetools/csr_wrap.cxx
    Elapsed (wall clock) time (h:mm:ss or m:ss): 1:14.86
    Maximum resident set size (kbytes): 1089156

$ sloccount sparsetools (counting autogenerated files, too)
Total Physical Source Lines of Code (SLOC)                = 440,250

#
# After
#
$ /usr/bin/time -v g++ -Wall -D__STDC_FORMAT_MACROS -O3 -c -o /dev/null -Isparsetools -I/usr/include/python2.7 sparsetools/bsr.cxx 
    Elapsed (wall clock) time (h:mm:ss or m:ss): 1:12.27
    Maximum resident set size (kbytes): 672080

$ /usr/bin/time -v g++ -Wall -D__STDC_FORMAT_MACROS -O3 -c -o /dev/null -Isparsetools -I/usr/include/python2.7 sparsetools/csr.cxx 
    Elapsed (wall clock) time (h:mm:ss or m:ss): 0:29.09
    Maximum resident set size (kbytes): 469900

$ sloccount sparsetools (counting autogenerated files, too)
Total Physical Source Lines of Code (SLOC)                = 10,302

@pv pv added PR labels Mar 8, 2014
@coveralls
Copy link

Coverage Status

Coverage remained the same when pulling be4d498 on pv:c-sparsetools into 7c64f19 on scipy:master.

@rgommers
Copy link
Member

rgommers commented Mar 9, 2014

Awesome work! Works for me on 32-bit linux, now testing the release builds.

@rgommers
Copy link
Member

rgommers commented Mar 9, 2014

Other observations besides compile time and memory usage:

  • test suite runs ~2% faster for me (consistent for several retries).
  • we now get one 29 Mb .so out instead of six .so's which together are ~20 Mb.

@rgommers
Copy link
Member

rgommers commented Mar 9, 2014

Doesn't work on OS X yet:

@pv
Copy link
Member Author

pv commented Mar 9, 2014

The OSX issue seemed to be some header ordering problem (Python.h must be included before C++ stdlib headers, for whatever reason). Fixed.

The .so sizes after stripping debug symbols are for me: before 9.9 MB, after 3.3MB. (Which reflects the SLOC reductions.)

@coveralls
Copy link

Coverage Status

Coverage remained the same when pulling e3e6b09 on pv:c-sparsetools into 7c64f19 on scipy:master.

@rgommers
Copy link
Member

rgommers commented Mar 9, 2014

Builds on both OS X versions now, so good to go and backport to 0.14.x I think. Also tested with scikit-learn, works fine.

@rgommers
Copy link
Member

rgommers commented Mar 9, 2014

Fixes gh-2944 by removing all the warnings generated by Clang due to SWIG.

@rgommers
Copy link
Member

rgommers commented Mar 9, 2014

And will stop code analysis tools/sites from reporting that Scipy is a C++ project:)

@rgommers
Copy link
Member

rgommers commented Mar 9, 2014

Minor issue, this doesn't seem to work on Windows with MinGW:

Many hundreds of these:

======================================================================
ERROR: test_base.TestDOK.test_asfptype
----------------------------------------------------------------------
Traceback (most recent call last):
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\nose-1.1.2-py3.2.egg\nose\case.py", line 198, in runTest
    self.test(*self.arg)
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\tests\test_base.py", line 944, in test_asfptype
    assert_equal(A.asfptype().dtype, np.dtype('float64'))
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\base.py", line 114, in asfptype
    return self.astype(fp_type)
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\base.py", line 102, in astype
    return self.tocsr().astype(t).asformat(self.format)
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\dok.py", line 467, in tocsr
    return self.tocoo().tocsr()
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\coo.py", line 360, in tocsr
    A.sum_duplicates()
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\compressed.py", line 952, in sum_duplicates
    if self.has_canonical_format:
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\base.py", line 499, in __getattr__
    raise AttributeError(attr + " not found")
AttributeError: has_canonical_format not found

Quite a few of these:

======================================================================
ERROR: test_base.Test64Bit.test_resiliency_random(<class 'test_base.TestLIL'>, 'test_iterator')
----------------------------------------------------------------------
Traceback (most recent call last):
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\nose-1.1.2-py3.2.egg\nose\case.py", line 198, in runTest
    self.test(*self.arg)
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\numpy\testing\decorators.py", line 147, in skipper_func
    return f(*args, **kwargs)
  File "<string>", line 2, in check
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\tests\test_base.py", line 105, in deco
    return func(*a, **kw)
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\tests\test_base.py", line 3890, in check
    instance = cls()
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\tests\test_base.py", line 157, in __init__
    self.datsp = self.spmatrix(self.dat)
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\lil.py", line 125, in __init__
    A = csr_matrix(A, dtype=dtype).tolil()
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\compressed.py", line 68, in __init__
    self._set_self(self.__class__(coo_matrix(arg1, dtype=dtype)))
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\compressed.py", line 31, in __init__
    arg1 = arg1.asformat(self.format)
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\base.py", line 213, in asformat
    return getattr(self,'to' + format)()
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\coo.py", line 357, in tocsr
    data)
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\sparsetools\coo.py", line 104, in coo_tocsr
    return _coo.coo_tocsr(*args)
NotImplementedError: Wrong number or type of arguments for overloaded function 'coo_tocsr'.
  Possible C/C++ prototypes are:
    coo_tocsr< int,npy_bool_wrapper >(int const,int const,int const,int const [],int const [],npy_bool_wrapper const [],int [],int [],npy_bool_wrapper [])
    coo_tocsr< int,signed char >(int const,int const,int const,int const [],int const [],signed char const [],int [],int [],signed char [])
    coo_tocsr< int,unsigned char >(int const,int const,int const,int const [],int const [],unsigned char const [],int [],int [],unsigned char [])
    coo_tocsr< int,short >(int const,int const,int const,int const [],int const [],short const [],int [],int [],short [])
    coo_tocsr< int,unsigned short >(int const,int const,int const,int const [],int const [],unsigned short const [],int [],int [],unsigned short [])
    coo_tocsr< int,int >(int const,int const,int const,int const [],int const [],int const [],int [],int [],int [])
    coo_tocsr< int,unsigned int >(int const,int const,int const,int const [],int const [],unsigned int const [],int [],int [],unsigned int [])
    coo_tocsr< int,long long >(int const,int const,int const,int const [],int const [],long long const [],int [],int [],long long [])
    coo_tocsr< int,unsigned long long >(int const,int const,int const,int const [],int const [],unsigned long long const [],int [],int [],unsigned long long [])
    coo_tocsr< int,float >(int const,int const,int const,int const [],int const [],float const [],int [],int [],float [])
    coo_tocsr< int,double >(int const,int const,int const,int const [],int const [],double const [],int [],int [],double [])
    coo_tocsr< int,long double >(int const,int const,int const,int const [],int const [],long double const [],int [],int [],long double [])
    coo_tocsr< int,npy_cfloat_wrapper >(int const,int const,int const,int const [],int const [],npy_cfloat_wrapper const [],int [],int [],npy_cfloat_wrapper [])
    coo_tocsr< int,npy_cdouble_wrapper >(int const,int const,int const,int const [],int const [],npy_cdouble_wrapper const [],int [],int [],npy_cdouble_wrapper [])
    coo_tocsr< int,npy_clongdouble_wrapper >(int const,int const,int const,int const [],int const [],npy_clongdouble_wrapper const [],int [],int [],npy_clongdouble_wrapper [])

And this one just once:

======================================================================
FAIL: test_base.TestCOONonCanonical.test_astype
----------------------------------------------------------------------
Traceback (most recent call last):
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\nose-1.1.2-py3.2.egg\nose\case.py", line 198, in runTest
    self.test(*self.arg)
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\scipy\sparse\tests\test_base.py", line 937, in test_astype
    assert_equal(S.astype(x).toarray(), D.astype(x))        # correct values
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\numpy\testing\utils.py", line 256, in assert_equal
    return assert_array_equal(actual, desired, err_msg, verbose)
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\numpy\testing\utils.py", line 686, in assert_array_equal
    verbose=verbose, header='Arrays are not equal')
  File "Z:\Users\rgommers\.wine\drive_c\Python32\lib\site-packages\numpy\testing\utils.py", line 618, in assert_array_compare
    raise AssertionError(msg)
AssertionError: 
Arrays are not equal

(mismatch 22.22222222222223%)
 x: array([[4294967297,          0,          0],
       [         0, 4294967303,          0],
       [         0,          0,          0]], dtype=uint64)
 y: array([[1, 0, 0],
       [0, 7, 0],
       [0, 0, 0]], dtype=uint64)

@pv
Copy link
Member Author

pv commented Mar 9, 2014

Maybe you have some left-over files from previous build? "Possible C/C++ prototypes are:" is a message from SWIG.

@rgommers
Copy link
Member

rgommers commented Mar 9, 2014

That's possible, I didn't manually clean out site-packages. I'll check why they are picked up, probably that can be prevented by some renaming. Otherwise a lot of people could run into this on upgrade.

@rgommers
Copy link
Member

rgommers commented Mar 9, 2014

The test_base.TestCOONonCanonical.test_astype failure is real. There's four of those, for COO, BSR, CSC and CSR. The rest is indeed due to old files left behind.

@rgommers
Copy link
Member

rgommers commented Mar 9, 2014

The issue was that there's now a sparsetools.pyd and also an older installed submodule sparsetools/. The latter is picked up first. Can you rename the new extension _sparsetools?

rgommers and others added 4 commits March 12, 2014 21:45
Installing over a previous Scipy installation causes problems, as
sparsetools is not a package any more. Therefore, change the name.

At the same time, mark the module for internal use only.
@pv
Copy link
Member Author

pv commented Mar 12, 2014

done, and rebased

@pv
Copy link
Member Author

pv commented Mar 12, 2014

Not sure what's up with the noncanonical astype failure. Does not occur on 32-bit linux.

@rgommers
Copy link
Member

Shall we merge this and figure that last one out after the beta? Would be good to get that out the door finally.

@pv
Copy link
Member Author

pv commented Mar 12, 2014

Would be better to resolve it first, as it probably indicates something is wrong.

@rgommers
Copy link
Member

OK. Probably won't be able to look at that before Saturday though.

@rgommers
Copy link
Member

It looks like the test is wrong. I have a bit of a hard time figuring out why the noncanonical tests are written the way they are, but what's going on is that this code:

def _same_sum_duplicate(data, *inds, **kwargs):
    """Duplicates entries to produce the same matrix"""
    ...
    data = data.repeat(2, axis=0)
    data[::2] *= 2
    data[1::2] *= -1

returns complex arrays with negative numbers for the failing test and those turn into large positions ones after astype(uint64) (also on Linux). Sparse matrices shouldn't have duplicate coordinates, so why is this tested this way? On Linux the toarray call happens to pick the positive complex values while under Wine it picks the negative ones.

If I insert the following debug prints the output should show what I described above:

class _NonCanonicalMixin(object):
    def spmatrix(self, D, **kwargs):
        """Replace D with a non-canonical equivalent"""
        construct = super(_NonCanonicalMixin, self).spmatrix
        M = construct(D, **kwargs)
        arg1 = self._arg1_for_noncanonical(M)
        if 'shape' not in kwargs:
            kwargs['shape'] = M.shape
        NC = construct(arg1, **kwargs)
        print('NC:  ', NC)
        print('NC type:  ', type(NC))
        print('NC astype:  ', NC.astype(np.uint64))
        print('NC astype.toarray:  ', NC.astype(np.uint64).toarray())
        assert_allclose(M.A, NC.A)
        return NC

Output for one failing test (the COO one) on Linux:

NC:     (0, 0)  (2+6j)
  (0, 0)        (-1-3j)
  (1, 1)        (14+0j)
  (1, 1)        (-7+0j)
NC type:   <class 'scipy.sparse.coo.coo_matrix'>
NC astype:     (0, 0)   2
  (0, 0)        18446744073709551615
  (1, 1)        14
  (1, 1)        18446744073709551609
NC astype.toarray:   [[1 0 0]
 [0 7 0]
 [0 0 0]]

And under Wine:

NC:     (0, 0)  (2+6j)
  (0, 0)        (-1-3j)
  (1, 1)        (14+0j)
  (1, 1)        (-7+0j)
NC type:   <class 'scipy.sparse.coo.coo_matrix'>
NC astype:     (0, 0)   2
  (0, 0)        4294967297
  (1, 1)        14
  (1, 1)        4294967303
NC astype.toarray:   [[4294967297        0        0]
 [       0 4294967303        0]
 [       0        0        0]]

@pv
Copy link
Member Author

pv commented Mar 15, 2014

Ok, in that case it's probably safe to just merge, and mark the test as knownfailure, similarly as several other noncanonical tests.

coo and csr/csc matrices can in principle be allowed to contain duplicate entries, as this is convenient for construction.
Some of the routines currently treat such inputs incorrectly, so @jnothman wrote some test pre-emptively to track the status.
It would be useful to check why the test is not testing what it should, but this seems to be unrelated to the de-swigification.

@rgommers
Copy link
Member

OK I'll do that and add some comment about the failure pointing to this discussion. Thanks.

@jnothman
Copy link
Contributor

Sparse matrices shouldn't have duplicate coordinates

The COO matrix documentation makes explicit that duplicates are permitted, with the claim that "Duplicate (i,j) entries are summed when converting to CSR or CSC."
Yet their public sum_duplicates method, and a few code paths specifically to handle the duplicate cases in CSR/CSC, it seemed like we should actually ensure they're working (and they were some times).

Clearly astype won't work safely in the duplicate case for certain casts, unless duplicates are first checked for and summed.

@rgommers
Copy link
Member

Ah, missed that in the docs. It's a bit incomplete, they should always be summed (like in toarray and not only during conversion to CSR/CSC).

Regarding the failures: I don't see what summing duplicates has to do with astype being safe for casts, either way complex to uint is never safe. The problem here is not astype, it's the test.

rgommers added a commit that referenced this pull request Mar 15, 2014
MAINT: wrap sparsetools manually instead via SWIG
@rgommers rgommers merged commit 5a626d2 into scipy:master Mar 15, 2014
@jnothman
Copy link
Contributor

The problem here is not astype, it's the test.

I haven't understood where astype is being called here anyway. The test ensures duplicates are treated properly by artificially creating duplicates: for a matrix with entries x, concatenate -x and 2*x. This should be an effective test for any signed type, as far as I can tell.

rgommers added a commit that referenced this pull request Mar 15, 2014
Issue was that negative numbers were produced that were then going
through astype().  Discussion on gh-3440.
@rgommers
Copy link
Member

Failures fixed by avoiding usage of negative numbers in 727fa40.

@rgommers
Copy link
Member

Thanks Pauli, this PR was massively helpful!

@rgommers
Copy link
Member

@jnothman but it was failing on uint64. For signed types it indeed works fine.

@rgommers
Copy link
Member

the test loops simply over supported_dtypes

rgommers added a commit that referenced this pull request Mar 15, 2014
Issue was that negative numbers were produced that were then going
through astype().  Discussion on gh-3440.
@rgommers
Copy link
Member

backported to 0.14.x, last commit is 90fbdd2.

@jnothman
Copy link
Contributor

but it was failing on uint64. For signed types it indeed works fine.

Hmm... but the unsigned int case should be handled (it tests np.issubdtype(data.dtype, np.unsignedinteger) and returns a non-duplicated version (although we could possibly put in zeros instead).

@rgommers
Copy link
Member

No, all that stuff is done in the self.spmatrix call at the beginning of test_astype, before the type casting. D is always complex, so that issubdtype check does nothing here. Then you get negative numbers in S, after which S.astype(np.uint64) fails.

Anyway, I fixed it already to not use negative numbers - those aren't necessary at all.

@jnothman
Copy link
Contributor

Well, your solution could still produce negative numbers if there are
explicit zeros. You've still got the case there that stops testing for
unsigned int types; if we assume tests create no explicit zeros, then it
should be possible to test uints here too. I used negative and doubled
numbers in part to make the errors more obvious, but of course as a test it
matters little.

On 16 March 2014 14:44, Ralf Gommers notifications@github.com wrote:

No, all that stuff is done in the self.spmatrix call at the beginning of
test_astype, before the type casting. D is always complex, so that
issubdtype check does nothing here. Then you get negative numbers in S,
after which S.astype(np.uint64) fails.

Anyway, I fixed it already to not use negative numbers - those aren't
necessary at all.

Reply to this email directly or view it on GitHubhttps://github.com//pull/3440#issuecomment-37747919
.

@rgommers
Copy link
Member

Yes, I did look at the test matrices to make sure that there were no negative numbers created for the choice I made. It's not the most robust fix if new tests are going to be added. My purpose was to get this PR in and get 0.14.0b1 out the door (building the binaries now). If you have a more future-proof solution then it's very welcome of course:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants