Skip to content

ENH: Support the dot family for user-dtypes - #31574

Open
SwayamInSync wants to merge 27 commits into
numpy:mainfrom
SwayamInSync:enh/dot-family-user-dtypes-30793
Open

SwayamInSync wants to merge 27 commits into
numpy:mainfrom
SwayamInSync:enh/dot-family-user-dtypes-30793

Conversation

@SwayamInSync

Copy link
Copy Markdown
Member

PR summary

This PR adds the dot and correlate fallback methods for the user-dtypes. It uses the multiply and matmul operations to delegate the operations as per the input shape.

closes #30793

AI Disclosure

None

@SwayamInSync SwayamInSync changed the title ENH: support the dot family for new-style user DTypes ENH: Support the dot family for user-dtypes Jun 6, 2026
Comment thread numpy/_core/numeric.py Outdated
n = a.shape[axis_a]
m = math.prod(a.shape[ax] for ax in notin_a)
p = math.prod(b.shape[ax] for ax in notin_b)
at = ascontiguousarray(a.transpose(notin_a + [axis_a]).reshape(m, n))

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.dot must make out= as C-style array, so I think doing ascontiguousarray might be OK?
Ideally the matmul/BLAS should handle any layout (for perfomance cases we can revert back to let dtype's BLAS handle this)

If we revert then, this will require new release from numpy-quaddtype (PR numpy/numpy-quaddtype#108 is already landed)

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.

By default, any ufunc produces a C-contiguous array, so I don't think you'd need to do anything.

@SwayamInSync

Copy link
Copy Markdown
Member Author

Test failures seems independent from this PR

@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.

Some comments in-line. Functionally, the one about complex conjugation for correlate is the most important, but the others should help simplify the implementation. In particular, please check carefully whether all those ascontiguousarray calls are in fact necessary (by having tests with non-contigous arrays).

Comment thread numpy/_core/numeric.py Outdated
res = np.zeros(res_shape, dtype=res_dtype)
elif a.ndim == 1 and b.ndim == 1:
# 1-D x 1-D: inner product
res = np.matmul(ascontiguousarray(a), ascontiguousarray(b))

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.

Here, you could just do return np.matmul(a, b, out=out) - by default, output is contiguous.

Comment thread numpy/_core/numeric.py Outdated
n = a.shape[axis_a]
m = math.prod(a.shape[ax] for ax in notin_a)
p = math.prod(b.shape[ax] for ax in notin_b)
at = ascontiguousarray(a.transpose(notin_a + [axis_a]).reshape(m, n))

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.

By default, any ufunc produces a C-contiguous array, so I don't think you'd need to do anything.

Comment thread numpy/_core/numeric.py Outdated
p = math.prod(b.shape[ax] for ax in notin_b)
at = ascontiguousarray(a.transpose(notin_a + [axis_a]).reshape(m, n))
bt = ascontiguousarray(b.transpose([axis_b] + notin_b).reshape(n, p))
res = np.matmul(at, bt)

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.

Are you sure the detailed handling of the axes is necessary? Remember that with np.matmul, like with all gufuncs, you have the option of explicitly passing in the axes that are contracted; see https://numpy.org/doc/stable/reference/ufuncs.html#optional-keyword-arguments

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.

Unfortunately, for ndim > 2 dot has a funny behavior that the result shape is a.shape + b.shape minus the reduction axes.
I.e. it doesn't actually do a proper broadcasting/stacked operation, it is the same as tensordot(..., axes=[-1, -2]).

However, tensordot actually does this precise dance already. So we could consider being a bit annoying and just rejecting it for this path (we should deprecate it on the other path I guess) with a note of "please use tensordot instead".

There is another good argument for this, the code has:

#if defined(HAVE_CBLAS)
    if (PyArray_NDIM(ap1) <= 2 && PyArray_NDIM(ap2) <= 2 &&

so we don't even use BLAS for this case, which is ridiculous and unless arrays are small users are better of to use the clearer tensordot() anyway.

(If we do that this might be simple enough to just move it to C.)

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.

fallback's _dot_contract is the tensordot-style reshape=>matmul, faster than legacy N-D dot, so rejecting it isn't a perf win, it's purely about not carrying the deprecation-worthy N-D dot semantics. I'm fine either way.

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 sure I am following precisely, but I am leaning to just reject it with a "use tensordot" message. Let's ping @mhvk, though.
(That would mean we should aim for a deprecation as a tendency, I guess.)

Comment thread numpy/_core/numeric.py Outdated
``_pyarray_correlate`` already handles empty checks, complex conjugation
and the ``len(a) < len(v)`` swap/reverse, so ``a`` is the longer operand.
"""
a = ascontiguousarray(np.asarray(a).ravel())

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.

Did you check all these ascontiguousarray calls are in fact necessary? I rather doubt it, since matmul produces a contiguous array.

Comment thread numpy/_core/numeric.py Outdated
pad = zeros(n2 - 1, dtype=res_dtype)
apad = concatenate([pad, a, pad])
win = apad[arange(n1 + n2 - 1)[:, None] + arange(n2)]
full = np.matmul(win, v)

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 think you need vecdot here to ensure complex numbers are properly conjugated (do add at test! also for dot, where complex conjugation is not done [incorrectly, but the fallback needs to mimic that mistake...])

@SwayamInSync

Copy link
Copy Markdown
Member Author

By default, any ufunc produces a C-contiguous array, so I don't think you'd need to do anything.

@mhvk the element-wise ones does not, they preserve the order. So the case for (0-D x n-D) (just a multiply) will not go to C-contig order.

For BLAS ops yes it will always, I kept this earlier because in quaddtype the PR hasn't merged yet and in case any dtype did not conform with this.
But I agree, this should be a bug from dtype side then. For the above discussed case, I will keep and remove as in-general call.

Comment thread numpy/_core/tests/test_multiarray.py Outdated
ref = np.dot(a, b)
got = _dot_fallback(a, b)
assert_array_equal(got, ref)
assert got.shape == np.shape(ref)

@mhvk mhvk Jun 6, 2026 •

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.

You can get the shape check by setting strict=True in assert_array_equal.

Also, I'm actually not 100% sure we should care about contiguity, but if we do, we should test it to ensure we don't regress, by adding something like assert got.strides == ref.strides (same in other tests).

@SwayamInSync

SwayamInSync commented Jun 8, 2026 •

Copy link
Copy Markdown
Member Author

In quaddtype the vecdot and non-C-layout input support is not merged yet (The PRs are there)
So those tests will fail here.

I will try to fast-forward the work there to get the support merge nicely, and maybe should we add quaddtype's test dependecy to direct fetch and build from repo? @seberg

@SwayamInSync

Copy link
Copy Markdown
Member Author

Also I tried, axes= only selects the contracted core dims, but matmul still broadcasts the remaining dims whereas np.dot takes their outer product (a-batch then b-batch). Concretely:
dot((2,3,4),(6,4,5)) => (2,3,6,5), but matmul(..., axes=...) raises ValueError trying to broadcast (2,3) against (6,) and dot((2,3),(4,3,5)) => (2,4,5) while matmul gives (4,2,5) (wrong, no error).
So the transpose/reshape into a single 2-D matmul is needed to express the outer-product contraction

@seberg

seberg commented Jun 8, 2026

Copy link
Copy Markdown
Member

I will try to fast-forward the work there to get the support merge nicely, and maybe should we add quaddtype's test dependecy to direct fetch and build from repo?

I already have one or two tests with quaddtype, but I am not sure it is installed yet. The problem is that that even importing quaddtype can break some other tests. So you need a bit of a tedious skipping for the quaddtype version (without actually importing it).

But, I think that's fine... we can factor that out into a little private helper. (I think you should be able to find that hack if you search, quickly.)

@SwayamInSync

Copy link
Copy Markdown
Member Author

But, I think that's fine... we can factor that out into a little private helper. (I think you should be able to find that hack if you search, quickly.)

Done, added in this PR as it wasn't that disturbing the context

@SwayamInSync

Copy link
Copy Markdown
Member Author

This is ready for 2nd round of review.
Will be happy to discuss design choices or if I missed some case.

except PackageNotFoundError:
pytest.skip("numpy_quaddtype is not installed")
if _pep440.Version(installed) < _pep440.Version(MIN_VERSION):
pytest.skip(f"numpy_quaddtype >= {MIN_VERSION} is required")

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.

Did you mean to put MIN_VERSION to 1.1 since I think we are at 1.0 (and we need to reject it).
I guess we can make it customizable if needed. The current min-version is vital because even importing is wrong, but after 1.1 is out that isn't the case anymore.

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.

Yeah that also works, just weird habit of having even jumps for sub-versions :)

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 think this comment still needs to be addressed...

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 guess if the next release is 1.2 then it doesn't matter. Although, I don't really see why numpy-quaddtype should use a different version scheme (distinguising odd from even versions) than NumPy.

Comment thread numpy/_core/numeric.py Outdated
n1, n2 = a.size, v.size
pad = zeros(n2 - 1, dtype=np.result_type(a.dtype, v.dtype))
apad = concatenate([pad, a, pad])
win = apad[arange(n1 + n2 - 1)[:, None] + arange(n2)]

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 a sliding_window_view I think. I think matmul will be OK with core dimensions that have self overlap (if not, we could pass axes to make it a non-core dimension). Although that is of course not BLAS compatible (for our code, I think it might even go back into einsum).

Otherwise, you would bloat memory usage like crazy here. (I suspect we can do this, but I am slightly unsure and i tmay make sense to split the PR into two, since I think dot is a bit clearer.)

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 am too concerned about the memory bloat here + sliding_window_view user-dtypes don't support.

Right to split this, I will do another PR for correlate and with the current and we can make further changes there

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.

sliding_window_view user-dtypes don't support.

Ah, that is true and annoying :/. Could possibly not go via __array_interface__ there to make it work, but I am not sure that is better than trying a fix for __array_interface__ (heck even adding a dtype field).

@ngoldbaum

Copy link
Copy Markdown
Member

We talked a little bit about this at the triage meeting and I'm in favor of not implementing the weird behavior of np.dot.

@SwayamInSync

Copy link
Copy Markdown
Member Author

I see, so I think we are set to deprecate the last case with warning "use tensordot"? Then we also need to do this for legacy path too right.

@ngoldbaum

Copy link
Copy Markdown
Member

Then we also need to do this for legacy path too right.

Probably but it doesn't need to happen here.

Address review feedback: do not reproduce np.dot's non-broadcasting
outer-product semantics for stacked arrays (ndim > 2) in the user-dtype
fallback; raise a ValueError pointing users to numpy.tensordot instead.
The unambiguous N-D . 1-D reduction is kept.

Also stop forcing C order in the 0-D scalar*array branch so the fallback
reproduces np.dot's memory layout exactly (matches the got.strides ==
ref.strides invariant), and add a release note.
With the stacked (ndim>2) case now rejected, np.dot coincides exactly
with matmul for every remaining case (matrix/vector products, the
N-D . 1-D reduction and empty contractions). Remove the _dot_contract
transpose/reshape helper (which only existed to express the rejected
outer-product semantics) and call matmul/multiply directly, keeping the
_dot_check_out validation since those ufuncs are more permissive with
out= than np.dot.
It was only used once; fold the out= validation directly into the
fallback and drop the now-redundant comments.
@SwayamInSync

SwayamInSync commented Jul 15, 2026 •

Copy link
Copy Markdown
Member Author

Apologies for the delay, the weird behaviour of np.dot with a.ndim > 2 or b.ndim > 2 is gone and errors to use tensordot.
We might note to apply this for the builtin dtypes too in a follow-up.

…test

np.dot routes ndim<=2 float/double/cfloat/cdouble through CBLAS, which
returns a C-contiguous result even for the 0-D scalar*array case; without
CBLAS it uses an order-preserving elementwise multiply.  A user DType never
hits the CBLAS path, so the fallback (and the legacy path it mimics) uses
the order-preserving multiply.  Assert only the stable property -- that a
matrix product via matmul is C-contiguous -- instead of comparing strides
against the BLAS-dependent np.dot oracle for the scalar case.
@seberg

seberg commented Jul 29, 2026

Copy link
Copy Markdown
Member

IMO, at this point, going through Python adds as much complexity as it resolves, but if others don't mind...

@SwayamInSync

Copy link
Copy Markdown
Member Author

IMO, at this point, going through Python adds as much complexity as it resolves, but if others don't mind...

Make sense, I will wait for the final thoughts then.

@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.

I had another look and think it is fine, though I somewhat agree with @seberg that the python overrides are sufficiently short one could consider just doing it in C. On the other hand, it is easier for people to understand where things go wrong if they can just look at the python code...

Anyway, I'm fine either way, just don't forget to update MIN_VERSION!

except PackageNotFoundError:
pytest.skip("numpy_quaddtype is not installed")
if _pep440.Version(installed) < _pep440.Version(MIN_VERSION):
pytest.skip(f"numpy_quaddtype >= {MIN_VERSION} is required")

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 think this comment still needs to be addressed...

@SwayamInSync

Copy link
Copy Markdown
Member Author

@mhvk @seberg updated

  • MIN_VERSION is 1.1
  • moved the importorskip_quaddtype imports to the top of the test files.

On the C vs Python thing: I tried porting both fallbacks to C to see how it actually looks. The catch is the tests. Nothing in numpy's own test suite can reach the fallback through np.dot:

  • builtin dtypes that have a dotfunc never hit it
  • the ones without (datetime64, S, U, V) have no matmul loop either, so only the error path
  • rational has a dotfunc
  • _ScaledFloatTestDType has multiply but no matmul, and adding one through sfloat_add_wrapping_loop segfaults, that helper doesn't handle gufuncs

So either way I need a private helper for the tests to call, in Python it is numpy._core.numeric._dot_fallback, in C it would be two entries in the multiarray method table. That is why I am not fully convinced the port buys much, plus np.ndim/np.ravel are also needs to be put as a C function, and the test hook stays in both cases.
After #31992 it is also not a clean win on the module state side, Python version needs the two NPY_RUNTIME_IMPORTS_FIELDS entries and the C one would need F(vecdot) in NPY_N_OPS_FIELDS, so one field list either way. I have the C version ready and passing, will be happy to push in case.

Conflict in numpy/_core/src/common/npy_import.h: numpygh-32544 now generates
the members of npy_runtime_imports_struct from NPY_RUNTIME_IMPORTS_FIELDS
via NPY_DECLARE_PYOBJECT_FIELDS, so the explicit member list is gone.
Resolved by taking main's generated declaration and dropping the two
explicit `_dot_fallback`/`_vdot_fallback` members this branch had added;
they are now generated from the F() entries already present in
module_state_fields.h.
`dot` has always handled a 0-D operand with `multiply`, for every dtype,
after casting both operands to their common dtype.  Diverting DTypes
without a dotfunc into the fallback skipped that cast, which exposed
`multiply`'s heterogeneous loops where `dot` never allowed them:

    np.dot(np.array('ab'), 3, out=np.empty((), 'U6'))

returned 'ababab' instead of raising, and the same for bytes.  Move the
dotfunc check below the existing 0-D block so those operands stay on
dot's own code path, and let the fallback handle only the matmul case.

Checked against a build with the fallback compiled out over 8752
dot/vdot/inner cases (dtype x shape grids, mixed dtypes, out=,
subclasses, Fortran and strided layouts, empty arrays): no case where
one succeeds and the other raises, and no differing results.
@SwayamInSync

Copy link
Copy Markdown
Member Author

merge conflict and found a regression on stringdtype (since we prevented the casting to common type).
Will fix and update here

@SwayamInSync

Copy link
Copy Markdown
Member Author

Pushed: main merged (conflict cleared) and the regression fixed.

The dotfunc check sat above dot's 0-D block, so DTypes without a dotfunc skipped the cast to the common dtype and reached multiply's string×int loop

np.dot(np.array('ab'), 3, out=np.empty((), 'U6')) 
#  returned `'ababab' instead of raising. 

Moved it below the 0-D block (e45f588), so 0-D stays on dot's own path.

That left the multiply branch in _dot_fallback unreachable, so it's dropped (a5145db), the fallback is matmul-only now.
Behaviour is identical to 2.5.3 for every dtype without a dotfunc, so this only adds behaviour where dot used to raise.

@SwayamInSync

SwayamInSync commented Sep 16, 2026 •

Copy link
Copy Markdown
Member Author

An old bug on main is exposed by the new tests added. In PyArray_MatrixProduct2, the 0-D branch returns without releasing typec:
For float64 the descriptor is a static builtin, so the leak never showed. test_dot_scalar_operands_bypass_fallback calls np.dot with 0-D string and m8[s] operands, and those descriptors are newly allocated each call, so they leak for real.

I reproduced it locally: np.dot(td, td) on a 0-D m8[s] array leaked one descriptor reference per call (1000 leaked after 1000 calls).

Fixed it in 631a826

Other CI failure is unrelated

@ngoldbaum ngoldbaum 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.

I asked an AI model to look this over and it spotted some corner cases and one issue with the tests, see below. I also have a suggestion for the release note, although it's already getting pretty long so if you can come up with a one-sentence version that's even better.


from numpy._utils import _pep440

MIN_VERSION = "1.1"

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 guess we have a bit of a chicken-egg problem in that this release doesn't exist yet. Is there any chance you can make the test work (with xfails maybe?) against 1.0 so can be pretty confident it works? Right now this whole test is dead code.

@SwayamInSync SwayamInSync Sep 20, 2026 •

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.

Right now v1.0 of numpy-quaddtype has the bug of registering broad promoter/loops and importing it can interfere with other unrelated tests (those issues are fixed upstream and planned to be out in v1.1) so technically we cannot even import it in the current released version.

So this guard is technically saving us from multiple issues (indeed a bad practice) hence I will make sure that next release of quaddtype rightly solves such problems

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.

OK, then what do you think about deleting these dead tests in this PR and then re-adding them after you do a numpy-quaddtype release and bump the pin 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.

Yeah good to me.

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 dunno, I honestly prefer putting this in with the dead tests (that work and were tested locally), then with a bunch of unit-tests for the _dot_fallback().

(I don't like such unit-tests if it's avoidable. For one they don't age well if we delete _dot_fallback() and then the person who does that work has to now do the double work of also fixing all tests.)

Of course that relies on actually getting that quaddtype release out very soon (and this bug feels like almost enough of a reason to do so?)

Comment thread numpy/_core/numeric.py Outdated
"use 'numpy.tensordot' instead."
)
try:
return np.matmul(a, b, out=out)

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 specify order="C"? Otherwise stuff like this happens:

import numpy as np
from numpy._core.numeric import _dot_fallback

a = np.ones((2, 3, 4, 5)).swapaxes(0, 1)
b = np.ones((5, 6))

expected = np.dot(a, b)
actual = _dot_fallback(a, b)

print(expected.flags.c_contiguous, actual.flags.c_contiguous)
# True False

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.

Matmul and the ufunc machinery have different out compatibility rules. I'm not sure whether this is worth worrying about, but there's also this incosistency:

import numpy as np
from numpy._core.numeric import _dot_fallback

a, b = np.ones((2, 3)), np.ones((3, 4))
outputs = {
    "extra batch": np.empty((5, 2, 4)),
    "Fortran": np.empty((2, 4), order="F"),
    "float32": np.empty((2, 4), dtype=np.float32),
    "strided": np.empty((2, 8))[:, ::2],
}

for label, out in outputs.items():
    for fn in (np.dot, _dot_fallback):
        try:
            result = fn(a, b, out=out)
            print(label, fn.__name__, "accepted", result.shape, result.dtype)
        except ValueError:
            print(label, fn.__name__, "ValueError")

On this PR I get:

extra batch dot ValueError
extra batch _dot_fallback accepted (5, 2, 4) float64
Fortran dot ValueError
Fortran _dot_fallback accepted (2, 4) float64
float32 dot ValueError
float32 _dot_fallback accepted (2, 4) float32
strided dot ValueError
strided _dot_fallback accepted (2, 4) float64

e.g. the "real" dot implementation rejects all these out shapes, dtypes, C/F order, and strides

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, the fallback now passes order="C".
dot c_contiguous True | fallback c_contiguous True

and regarding dot's out= contract, earlier silently depends on the dtype. The fallback now validates out itself (ndim, C-contiguity, dtype) and reuses dot's own error message, so all four of your cases match:

extra batch  dot=ValueError  fallback=ValueError
Fortran      dot=ValueError  fallback=ValueError
float32      dot=ValueError  fallback=ValueError
strided      dot=ValueError  fallback=ValueError

I checked this with other possible permutation/combinations and all matches with the usual dot behaviour

`numpy.vecdot` for `numpy.vdot`, so the operations work as long as the DType
implements those.

The stacked-array behaviour of `numpy.dot`, which for ``a.ndim >= 2`` and

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.

It's not just np.dot though. Lots of other code paths go through MatrixProduct2. Maybe you could say "behaviour of numpy.dot, numpy.inner, numpy.tensordot, and other code paths like numpy.cov that rely on dot products"

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/numeric.py
Comment on lines +1246 to +1247
if (out.ndim != expected_ndim or not out.flags.c_contiguous
or out.dtype != np.result_type(a, b)):

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 still isn't quite right: it should also flag an incorrect output shape with the same number of dimensions and an un-aligned but C-contiguous output.

Rather than duplicating the rules in Python, can you instead extract the output validation from new_array_for_sum into a private C helper that this function can call?

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.

Do you have an opinion on this?

I feel like we either give up and pass out as allocated and checked by dot(). Or we don't worry about any of this and just use the gufunc machinery as is (as it is the "gold standard" and what dot could transition to as well).

Here, we actually check that out.dtype is identical to the result_type, but we don't enforce it in the ufunc when out=None, where it isn't strictly guaranteed. At that point we are almost at casting="no" into the ufunc, but also that I would prefer not to.

(I feel like all of this is churn for fairly weird corner cases -- yes, I don't like the divergence much either, but...)

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.

FWIW, when it comes to the order="C" that seems reasonable, because it is also a reasonable idea if we want to rewire things (unfortunately!).

In general, I don't want us to start to guarantee order, but maybe dot is special enough (to at least not rush changing it).

@ngoldbaum ngoldbaum added this to the 2.6.0 Release milestone Sep 21, 2026
@ngoldbaum ngoldbaum added the triage review Issue/PR to be discussed at the next triage meeting label Sep 21, 2026
@ngoldbaum

Copy link
Copy Markdown
Member

I think this is getting very close! I added a milestone and the triage review label to hopefully attract one more careful code review pass from a maintainer.

@seberg seberg 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.

I think it's very close, but now that we have all of those additional check, I am honestly a bit confused.
I don't care a whole lot if the new path is slightly more relaxed here (heck, I could ignore the order="C" part).

About the tests... Serious question: Can we maybe just get that release out? The fix for this issue alone feels like it may well warrent it? :) (no idea how much work it is, but also Python 3.15 support, etc. is upcoming so it would be timely.)

Comment thread numpy/_core/numeric.py
Comment on lines +1246 to +1247
if (out.ndim != expected_ndim or not out.flags.c_contiguous
or out.dtype != np.result_type(a, b)):

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.

Do you have an opinion on this?

I feel like we either give up and pass out as allocated and checked by dot(). Or we don't worry about any of this and just use the gufunc machinery as is (as it is the "gold standard" and what dot could transition to as well).

Here, we actually check that out.dtype is identical to the result_type, but we don't enforce it in the ufunc when out=None, where it isn't strictly guaranteed. At that point we are almost at casting="no" into the ufunc, but also that I would prefer not to.

(I feel like all of this is churn for fairly weird corner cases -- yes, I don't like the divergence much either, but...)


from numpy._utils import _pep440

MIN_VERSION = "1.1"

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 dunno, I honestly prefer putting this in with the dead tests (that work and were tested locally), then with a bunch of unit-tests for the _dot_fallback().

(I don't like such unit-tests if it's avoidable. For one they don't age well if we delete _dot_fallback() and then the person who does that work has to now do the double work of also fixing all tests.)

Of course that relies on actually getting that quaddtype release out very soon (and this bug feels like almost enough of a reason to do so?)

Comment thread numpy/_core/numeric.py
Comment on lines +1246 to +1247
if (out.ndim != expected_ndim or not out.flags.c_contiguous
or out.dtype != np.result_type(a, b)):

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.

FWIW, when it comes to the order="C" that seems reasonable, because it is also a reasonable idea if we want to rewire things (unfortunately!).

In general, I don't want us to start to guarantee order, but maybe dot is special enough (to at least not rush changing it).

@SwayamInSync

Copy link
Copy Markdown
Member Author

Hi sorry for the delay here, I will come back to it quick after doing the v1.1 release from the quaddtype side, releasing all the blockers for this and other PRs (won't take long)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

01 - Enhancement triage review Issue/PR to be discussed at the next triage meeting

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ENH: Make functions in dot family and related to support user-defined data types

5 participants