Skip to content

ENH: make matmul into a ufunc - #12219

Merged
eric-wieser merged 3 commits into
numpy:masterfrom
mattip:matmul-as-ufunc2
Dec 4, 2018
Merged

eric-wieser merged 3 commits into
numpy:masterfrom
mattip:matmul-as-ufunc2

Conversation

@mattip

@mattip mattip commented Oct 19, 2018 •

Copy link
Copy Markdown
Member

Replaces #11133.
Uses the extended generalized ufunc signature (n?,k),(k,m?)->(n?,m?), a custom PyUFunc_MatmulTypeResolver function, and new inner loop functions to implement matmul as a true ufunc.

Still known todo:

  • PR MAINT: refactor ufunc iter operand flags handling #11580 is needed to pass the failing test. The TypeResolver checks overlap. If the output and input overlap (via an out kwarg), the ufunc->op_flag will be set to use writeback semantics. On master, this currently does not work since all the flags are overridden.
  • The docstring processing needs refactoring, but I wanted to change it in two commits: the first to modify it for the new behavior, then to move it from core/_add_newdocs.py to ufunc_docstrings
  • The TypeResolver should already choose the proper vector-vector, vector-matrix, or matrix-matrix inner loop based on shape (maybe, needs benchmarking to see which is faster) Leave this for a future PR
  • Make sure the code handles non-contiguous inputs and output
  • Make sure the conversions from npy_intp to blas int do not overflow
  • General performance improvements to make sure the new code is no slower than the old code, which went through einsum.

Edit: update after commit 207fd8b3

@mattip mattip added 01 - Enhancement 56 - Needs Release Note. Needs an entry in doc/release/upcoming_changes labels Oct 19, 2018
@mattip mattip added this to the 1.16.0 release milestone Nov 4, 2018
@charris

charris commented Nov 6, 2018

Copy link
Copy Markdown
Member

It would be good to be able to deal with stacked vectors as well as stacked matrices. I started with four functions: vecvec, matvec, vecmat, matmat. Now that doesn't give matmul as a ufunc, but it does allow defining it using all ufuncs, which should be just a good. One could also just implement matmat, and do everything else by adding, removing a dimension, so that would also go through a ufunc. In all those versions, matmul should not be covered by __array_function__, it is just to supply a function for the @ operator for Python < 3.5.

@mhvk, @shoyer Thoughts?

@shoyer

shoyer commented Nov 6, 2018

Copy link
Copy Markdown
Member

If I recall correctly, the agreement on the mailing list was to make np.matmul a real ufunc (with optional dimensions), and use it for implementing @. This will let us complete the table of operators and ufuncs from NEP 13.

We should think about how single gufunc interacts with axis/axes arguments -- do the selected axes determine which function gets dispatched? If so, np.matmul(x, y, axes=[(-1,), (-1,)]) might be enough for specifying vector-vector multiplication.

For the sake of clarity it still might be better to have alternative gufuncs like vecvec and matvec. But this should not change the np.matmul gufunc signature with (m?,n),(n,p?)->(m?,p?), which we really want so we can implement __matmul__/@ consistently with other arithmetic operators.

@mattip

mattip commented Nov 7, 2018

Copy link
Copy Markdown
Member Author

Here is the asv benchmark comparison run from commit 3703c095 to 97df928 (master just before this commit)

Note that one of the benchmarks does not run. This is due to the following code segfaulting. Now debugging this (only fails for matmul ufunc, add for instance passes

a = np.arange(240000.0).reshape(400, 600);
b = np.arange(600); 
np.matmul(a, b)

@mhvk

mhvk commented Nov 7, 2018

Copy link
Copy Markdown
Contributor

@shoyer - that is an intriguing suggestion, to use axes to indicate that one wants the 1-D version (as it, that doesn't work, but it might be possible to make it work...).

Do agree that having matmul override is important; we put the whole ? in the signature for that to become possible...

@mattip

mattip commented Nov 7, 2018

Copy link
Copy Markdown
Member Author

valgrind says it is cblas_dgemv (dscal_k_HASWELL) writing past the data in libopenblasp-r0.2.20.so. Maybe the loop code is wrong? This is in matvec for double dtype.

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tried finding your bug, and found one mistake, but I think it is unrelated. Could it be that you should use is2_n? (see in-line comment) Or is perhaps the assumption of contiguous data broken?

Comment thread numpy/core/src/umath/matmul.c.src Resolved
Comment thread numpy/core/src/umath/matmul.c.src Resolved
Comment thread numpy/core/src/umath/matmul.c.src Resolved
Comment thread numpy/core/src/umath/matmul.c.src Resolved
@mattip

mattip commented Nov 7, 2018

Copy link
Copy Markdown
Member Author

I found the segfault, but in doing so discovered that my cblas routines do not work with non-contiguous data.

@mattip

mattip commented Nov 7, 2018

Copy link
Copy Markdown
Member Author

All the benchmarks run to completion, here are the latest results. There are a few regressions, and a few slight improvements

@mhvk

mhvk commented Nov 7, 2018

Copy link
Copy Markdown
Contributor

Only the first seems worth checking in a bit more detail. That's a large regression for a fairly common case (A * AT)

@mattip

mattip commented Nov 7, 2018 •

Copy link
Copy Markdown
Member Author

Commit cafcfa4dc improved the A @ A.T benchmark, but einsum is still faster, there is no significant difference.

Edit: misinterpreted the results

@charris

charris commented Nov 7, 2018

Copy link
Copy Markdown
Member

If I recall correctly, the agreement on the mailing list was to make np.matmul a real ufunc (with optional dimensions), and use it for implementing @.

One of the problems is that there are a number of optimizations that can be made that do not fit nicely into the ufunc scheme, ufuncs are too low level. For instance, a stack_of_matrices X single_matrix can be optimized if the stacked matrices are reshaped so that it is simply matrix X matrix, same with stack of vectors. My own preference would be to implement a single matmat ufunc and implement @ using it by jiggling the dimensions.

@mattip

mattip commented Nov 8, 2018

Copy link
Copy Markdown
Member Author

a stack_of_matrices X single_matrix can be optimized if the stacked matrices are reshaped so that it is simply matrix X matrix

Isn't that essentially the difference between dot and matmul?

@mattip

mattip commented Nov 8, 2018

Copy link
Copy Markdown
Member Author

@charris I opened #12348 to discuss further enhancements. Or are you suggesting to close this PR?

@charris

charris commented Nov 8, 2018

Copy link
Copy Markdown
Member

@mattip I still need to make a careful review, before writing out my thoughts.

@mattip mattip changed the title WIP, ENH: make matmul into a ufunc ENH: make matmul into a ufunc Nov 9, 2018
@mattip

mattip commented Nov 9, 2018

Copy link
Copy Markdown
Member Author

I think matmul as a ufunc is ready for review.

The loops cast M, N, and P from npy_intp (8 bytes) to int (4 bytes) in the cblas matrixmatrix routines, but did so in the past as well, and dot also suffers from this casting, so I am leaving that for a future issue/PR.

Speed improvements could be made, but the code seems to be as fast as the versions on master, so this too should be dealt with in subsequent work.

Comment thread numpy/core/src/umath/matmul.c.src Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should there be some sort of error handling here or should these be dropped? Just asking as the comment says this shouldn't be run yet the branch is here.

Also same question for the branch below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd use an assert here too, and also move the default definitions of trans1 and lda here (from lines 111 and 118).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

assert in C will abort in debug and be ignored in release builds. There is no error handling in inner loops. I added a check before this call to ensure the input is contiguous.

Also moved the definitions.

This comment was marked as outdated.

Comment thread numpy/core/src/umath/scalarmath.c.src Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this worth it vs just letting python emit the error?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I tend to agree to just let python error.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The error for np.arange(10) @ 3 then becomes "ValueError: matmul: Input operand 0 does not have enough dimensions (has 0, gufunc core with signature (n?,k),(k,m?)->(n?,m?) requires 1)"

Will the typical user understand what went wrong?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

3 @ 3 errors with "TypeError: unsupported operand type(s) for @: 'int' and 'int'"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The TypeError would definitely be fine. For the other, it does support keeping the error; on the other hand, really, when something goes wrong, how hard is it to do np.matmul?...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

np.matmul(np.arange(10), 3) will also reach this call, and will error.

In commit 98c8ac4cd221 I removed the call to PyErr_SetString, we can revert it if desired.

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@mattip - I went through with a reasonably fine-toothed comb, and found some little bits, but nothing too major. The main question I have is whether if cblas is used, it is reasonable to copy the whole input array (a possibly very large stack of matrices) rather than copy matrix by matrix. I must say my tendency would be to just not use cblas for non-contiguous data. Anyway, that perhaps can be decided after - since I think you currently just reproduce what was done before.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

dot(a,b) -> matmul(x1, x2)? Though I think it is also fine to rewrite the sentence to

it must be C-contiguous and have both the right type and the right dtype```

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

adopted

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe "but a subsequent call to matmul will..." (I'd parenthesize that, since it is somewhat of an aside).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

reworded

Comment thread numpy/core/src/umath/matmul.c.src Resolved
Comment thread numpy/core/src/umath/matmul.c.src Resolved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why the strange indentation?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

Comment thread numpy/core/tests/test_multiarray.py Resolved
Comment thread numpy/core/tests/test_multiarray.py Resolved
@mattip

mattip commented Nov 13, 2018

Copy link
Copy Markdown
Member Author

@mhvk I would like to rebase and squash this since it has conflicts. Since that will make review more difficult, should I wait for more comments?

@shoyer

shoyer commented Nov 13, 2018 via email

Copy link
Copy Markdown
Member

Comment thread numpy/core/src/umath/matmul.c.src Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are you sure these ns and ms aren't swapped?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

the assert was failing in the debug CI test, fixing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm wrong, these ms and ns are correct.

What's wrong is that the asserts on i1 should be within the if or if-else

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I rewrote the macro instead

Comment thread numpy/core/src/umath/matmul.c.src Resolved
Comment thread numpy/core/src/umath/matmul.c.src Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps rename to blasable_matrix_vector, and vector_blasable_matrix above?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will do if there are more fixes

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

done

Comment thread numpy/core/src/umath/matmul.c.src Resolved
Comment thread numpy/core/tests/test_multiarray.py Resolved

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can be removed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not strictly true, when you consider the 1d overloads. Maybe fine as is anyway.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

leaving this for now

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think you want double backticks here, since single backticks tries to make a reference.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

added

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For a separate doc cleanup PR: This would be a lot clearer if the dimensions were all different. (2, 3, 4) and (2, 4, 5) would show the behavior nicely.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For a separate doc cleanup PR: spaces around commas for PEP8 - let's not train new users into going against PEP8 if we can avoid it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

easy enough to clean up now

Comment thread numpy/core/src/umath/matmul.c.src Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not a fan of this macro. I think I'd prefer a form that takes all the parameters, with a little more explanation. Also, may as well just use an inline function.

Perhaps something like:

/* -1 to be conservative, in case blas internally uses a for loop with an inclusive upper bound */
#define BLAS_MAXSIZE (NPY_MAX_INT - 1)

/**
 * Return true if matrix with strides (s1, s2) and dimensions (n1, n2) can be used with blas
 */
static inline npy_bool
is_corder_blasable_2d(npy_intp s1, npy_intp s2, npy_intp n1, npy_intp n2, npy_intp itemsize) {
    npy_intp step1;
    if (!(s2 == itemsize)) {
        /* second axis must be contiguous */
        return NPY_FALSE;
    }
    if (!(s1 % itemsize == 0)) {
        /* strides in the first axis must count whole elements */
        return NPY_FALSE;
    }
    step1 = s1 / itemsize;
    if (!(n1 <= step1) {
        /* number of elements must not cause slices of the last axis to overlap */
        return NPY_FALSE;
    }
    /* todo: can n1 or n2 be 0? */
    if (!(n1 <= BLAS_MAXSIZE  && n2 <= BLAS_MAXSIZE && step1 <= BLAS_MAXSIZE) {
        /* dimensions must be representable in BLAS */
        return NPY_FALSE;
    }
    return NPY_TRUE;
}
static inline npy_bool
is_forder_blasable_2d(npy_intp s1, npy_intp s2, npy_intp n1, npy_intp n2, npy_intp itemsize) {
    /* just transpose the array */
    return is_corder_blasable_2d(s2, s1, n2, n1, itemsize);
}

static inline npy_bool
is_blasable_1d(npy_intp s, npy_intp n, npy_intp itemsize) {
    npy_intp step;
    if (!(s1 % itemsize == 0)) {
        /* strides must count whole elements */
        return NPY_FALSE;
    }
    /* todo: can INCX be 0 or negative? */
    /* todo: can N be 0? */
    if (!(n <= BLAS_MAXSIZE && step <= BLAS_MAXSIZE) {
        /* dimensions must fit in an int */
        return NPY_FALSE;
    }
    return NPY_TRUE;
}

Yes, this is far more verbose, but I'd hope they'd be optimized to the same code, and this gives far more places for comments.

It's not clear to me whether c / f order is the right way to name these function, or whether is_forder_blasable is more useful than explicitly calling is_corder_blasable with the arguments in the other order.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

adopted a modified version of this

Comment thread numpy/core/src/umath/matmul.c.src Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why the 2 here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

Comment thread numpy/core/src/umath/matmul.c.src Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

could omit the == 1 after you remove the 2 above

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

Comment thread numpy/core/src/umath/ufunc_object.c Resolved
Comment thread numpy/core/src/umath/matmul.c.src Outdated

@eric-wieser eric-wieser Dec 1, 2018 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think these three lines are correct, which is why I was pushing for a distinction between "is supported by blas" and "is supported by blas if I transpose it".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ok, will extend the function

Comment thread numpy/core/src/umath/matmul.c.src Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not true - it needn't be a multiple, it just needs to exceed the length of the fast axis

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

Comment thread numpy/core/src/umath/matmul.c.src Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should move into the if above.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

Comment thread numpy/core/tests/test_multiarray.py Resolved
@mattip
mattip force-pushed the matmul-as-ufunc2 branch 2 times, most recently from 962342f to ae9bb7b Compare December 1, 2018 23:42
Comment thread numpy/core/src/umath/matmul.c.src Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this still be here?

Comment thread numpy/core/src/umath/matmul.c.src Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Probably safer to use BLAS_MAX_SIZE here

if (op[i] == NULL) {
op[i] = operands[i];
Py_INCREF(op[i]);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These lines are unrelated cleanup, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes

Comment thread numpy/core/src/umath/ufunc_object.c Resolved

@eric-wieser eric-wieser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks ready now - just one comment left about documenting the need for the non-default ufunc flags, which you explained well in person.

@mattip

mattip commented Dec 3, 2018

Copy link
Copy Markdown
Member Author

Fixed comment on non-default flags, ready for merge

@eric-wieser
eric-wieser merged commit 82428bb into numpy:master Dec 4, 2018
@eric-wieser

Copy link
Copy Markdown
Member

Thanks for the persistence, @mattip!

@shoyer

shoyer commented Dec 4, 2018

Copy link
Copy Markdown
Member

Woohoo, thank you @mattip!

@mhvk

mhvk commented Dec 4, 2018

Copy link
Copy Markdown
Contributor

Yay!! Thanks, @mattip!

@jakirkham

Copy link
Copy Markdown
Contributor

Thanks so much for working on this, @mattip! 😄 🎉

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.

7 participants