Repository navigation
Use new DType API with backcompat path and fix result_type() - #374
Conversation
de3a5a8 to
eff09d7
Compare
|
@hawkinsp just in case you have a quick thought here. I tried to rewrite things to just use the new API but keep it a "legacy" dtype to some degree, so that there should be no real regressions but at the same time it works fine all the way back to NumPy 2 (the only regression I noticed it that arrays print a bit less nice). However, this uses The alternative I can currently think off is to just allow |
|
I haven't looked yet, but
It's not the end of the world to have to build ml_dtypes per Python version: it's a small enough package. I had previously abandoned trying to use the limited dtype API for similar reasons (#195). And eventually when the oldest supported NumPy ages off our support matrix, we can switch. |
|
OK, cool, then I think I'll pursue this, we need a better way to transition a package like Long term for the stable API: I suspect the right thing will be to have a new DType creation function, that creates the full heap-type for you based on the spec. |
result_type()
result_type()result_type()
c67581e to
800f815
Compare
|
I made a big pass on this cleaning things up and hopefully fixing some issues (byte-swapping and an incorrect This should now actually be OK, although it does require NumPy 2.0+. With this being merged in NumPy and working out fine here, this is now actually ready. I'll note again the one little annoyance I have found for now (hopefully the only one), and that is that when printing arrays we now print Unlike gh-360 this doesn't make it obvious to just not set a character for example but it does ensure that old-style code paths are taken so that actual regressions are unlikely (while it seems it'll be a long whack-a-mole with gh-360). |
|
This also now fixes gh-301 which I suspect is the magic thing that might make my CuPy "test everything" attempt feasible (to the point I am considering if we should just hack that flag if this PR isn't so easy). (Nevermin, older NumPy will still not support it nicely of course. If we want to improve this without the flag, I think the solution might be to check if |
|
Sorry this had dropped off my radar of things to look at. I asked Gemini to review this and it pointed out some issues that I think may be real at least in part. I asked it to make test cases. WDYT? |
|
#360 is looking more plausible to merge, but I'm wondering if it makes sense to take some of the backward compat ideas from this PR. |
|
I'll have a look very soon. I think this is the necessary approach, but it is very possible that #360 is getting those promotion cases more correct. I think I basically left some existing over-promotions untouched here (but don't exactly recall). |
418cf95 to
3cbcc72
Compare
|
OK, asked my agent to adopt those and fixed with a short review (I should have another look through for sanity, although the tests look good). Added a few more smaller fixes, this probably needs an iteration, but it may work now. (Again about the other PR, it does more I am sure, but I think backwards compat mean we have to go this route, then basically continue the work there to use the new API as much as possible. That makes sense, but I wanted to not do everything in one go and the result-type is the most interesting fix, I think.) (I'll work on the failures, there are some more follow-ups here probably.) |
54c74b4 to
2127317
Compare
|
Remembered, this so just a gentle ping (actually I remembered more than this, that I need the character change in CuPy, because without that promotion isn't just weird but very broken because |
|
Thanks for the ping, I'll take a look. |
|
I haven't had time to stare deeply yet myself, but AI says this: Problem 1: 3+ Argument
|
| Expression (Custom Array First) | Result | Expression (Python Scalar First) | Result (Bug) |
|---|---|---|---|
np.result_type(np.zeros(1, f8_e4m3fn), 0) |
float8_e4m3fn |
np.result_type(0, np.zeros(1, f8_e4m3fn)) |
float64 (probed with uint8) |
np.result_type(np.zeros(1, bf16), 0.0) |
bfloat16 |
np.result_type(0.0, np.zeros(1, bf16)) |
float32 (probed with float16) |
np.result_type(np.zeros(1, f8_e4m3fn), 0.0) |
float8_e4m3fn |
np.result_type(0.0, np.zeros(1, f8_e4m3fn)) |
float16 (probed with float16) |
np.result_type(np.zeros(1, i4), 0) |
int4 |
np.result_type(0, np.zeros(1, i4)) |
int16 (probed with uint8) |
np.result_type(np.zeros(1, u4), 0) |
uint4 |
np.result_type(0, np.zeros(1, u4)) |
uint8 (probed with uint8) |
np.result_type(np.zeros(1, i4), 0.0) |
float64 |
np.result_type(0.0, np.zeros(1, i4)) |
float16 (probed with float16) |
np.result_type(np.zeros(1, i4), 0.0j) |
complex128 |
np.result_type(0.0j, np.zeros(1, i4)) |
complex64 (probed with complex64) |
np.result_type(np.zeros(1, bc32), 0) |
bcomplex32 |
np.result_type(0, np.zeros(1, bc32)) |
complex64 (probed with uint8) |
np.result_type(np.zeros(1, bc32), 0.0) |
bcomplex32 |
np.result_type(0.0, np.zeros(1, bc32)) |
complex64 (probed with float16) |
np.result_type(np.zeros(1, bc32), 0.0j) |
bcomplex32 |
np.result_type(0.0j, np.zeros(1, bc32)) |
complex64 (probed with complex64) |
Furthermore, for 3 arguments with arrays (e.g., np.result_type(1.0, np.zeros(1, dtype=np.float64), np.zeros(1, dtype=i4))), reduce_dtypes_to_most_knowledgeable compares low=0 (PyFloatDType) with high=2 (i4) via float_common_dtype(PyFloatDType, i4) -> float16 (so PyFloatDType is not swapped with i4), then swaps PyFloatDType with Float64DType, electing Float64DType as main_dtype, which then fails to promote i4 and raises DTypePromotionError.
Reproducing Test Case
@pytest.mark.parametrize(
"dtype, scalar, expected",
[
(bf16, 0, bf16),
(bf16, 0.0, bf16),
(f8_e4m3fn, 0, f8_e4m3fn),
(f8_e4m3fn, 0.0, f8_e4m3fn),
(f4, 0, f4),
(f4, 0.0, f4),
(i4, 0, i4),
(i4, 0.0, np.float64),
(i4, 0.0j, np.complex128),
(u4, 0, u4),
(u4, 0.0, np.float64),
(u4, 0.0j, np.complex128),
(bc32, 0, bc32),
(bc32, 0.0, bc32),
(bc32, 0.0j, bc32),
(c32, 0, c32),
(c32, 0.0, c32),
(c32, 0.0j, c32),
],
)
def test_python_scalars_with_arrays_commutative(dtype, scalar, expected):
arr = np.zeros(1, dtype=dtype)
assert np.result_type(arr, scalar) == np.dtype(expected)
assert np.result_type(scalar, arr) == np.dtype(expected)
@pytest.mark.parametrize("int_t", ALL_INTN)
@pytest.mark.parametrize(
"scalar, concrete, expected",
[
(1.0, np.float16, np.float16),
(1.0, np.float32, np.float32),
(1.0, np.float64, np.float64),
(1.0j, np.float32, np.complex64),
(1.0j, np.float64, np.complex128),
],
)
def test_weak_scalar_stays_weak_with_arrays(int_t, scalar, concrete, expected):
int_arr = np.zeros(1, dtype=int_t)
concrete_arr = np.zeros(1, dtype=concrete)
for args in itertools.permutations([int_arr, scalar, concrete_arr]):
assert np.result_type(*args) == np.dtype(expected), argsProblem 3: NPyIntN_CommonDType Promotes Custom Ints to Non-Numeric Built-In DTypes and New-Style DTypes (StringDType)
Root Cause
In ints.cc, NPyIntN_CommonDType has an unguarded check:
if (!PyTypeNum_ISUSERDEF(other->type_num)) {
Py_INCREF(other);
return other;
}Because PyTypeNum_ISUSERDEF(type) is ((type) >= 256) && ((type) < 512) (ndarraytypes.h), !PyTypeNum_ISUSERDEF(other->type_num) is true for:
np.dtypes.StringDType()(type_num == NPY_VSTRING == 2056) and any new-stylePyArrayDTypeMeta_SpecDType (type_num == -1):- Because
StringDTypeis non-parametric (NPY_DT_is_parametricis false),np.result_type(int4, np.dtypes.StringDType())succeeds and returnsStringDType()instead of raisingDTypePromotionError!
- Because
- Built-in non-numeric legacy DTypes (
NPY_STRING,NPY_UNICODE,NPY_VOID,NPY_DATETIME,NPY_TIMEDELTA):NPyIntN_CommonDTypereturnsStrDType/BytesDType/VoidDType/DateTime64DType/TimeDelta64DTypeinstead ofPy_NotImplemented.PyArray_ResultTypethen attemptsPyArray_CastDescrToDType(int4_descr, common_dtype), raising a genericTypeError: cannot cast dtype int4 to <class 'numpy.dtypes.StrDType'>instead ofnp.exceptions.DTypePromotionError.
Suggested Fix
Restrict the built-in branch in NPyIntN_CommonDType to legacy numeric DTypes:
if (NPY_DT_is_legacy(other) && other->type_num < NPY_NTYPES_LEGACY) {
if (other->type_num == NPY_BOOL) {
Py_INCREF(cls);
return cls;
}
if constexpr (std::numeric_limits<T>::is_signed) {
if (PyTypeNum_ISUNSIGNED(other->type_num)) {
return PyArray_CommonDType(&PyArray_ByteDType, other);
}
}
if (PyTypeNum_ISNUMBER(other->type_num)) {
Py_INCREF(other);
return other;
}
}Reproducing Test Case
@pytest.mark.parametrize(
"custom_t", ALL_CUSTOM_FLOATS + ALL_INTN + ALL_CUSTOM_COMPLEX
)
@pytest.mark.parametrize(
"incompat_t",
[
np.dtype("U8"),
np.dtype("S8"),
np.dtype("V8"),
np.dtype("M8[D]"),
np.dtype("m8[s]"),
np.dtypes.StringDType(),
],
)
def test_incompatible_dtypes_raise_promotion_error(custom_t, incompat_t):
with pytest.raises(np.exceptions.DTypePromotionError):
np.result_type(custom_t, incompat_t)
with pytest.raises(np.exceptions.DTypePromotionError):
np.result_type(incompat_t, custom_t)Problem 4: np.float16 (NPY_HALF) Non-Associativity and Missing PyArray_RegisterCanCast to NPY_HALF
Root Cause
- Non-associativity between custom floats that fit in
float16:- In
floats.cc,NPyCustomFloat_CommonDTypepromotes any custom floatTwhereCustomFloatSafeTo<T, half>()is true (f4,f6_e2m3,f6_e3m2,f8_e3m4,f8_e4m3,f8_e4m3fn,f8_e5m2) withnp.float16tonp.float16. - However, in
floats.h,CommonCustomFloatDTypeImplpromotes two incomparable custom floats (such as(f8_e4m3fn, f8_e5m2)or(f6_e2m3, f6_e3m2)) directly tonp.float32, even when both fit innp.float16(and even thoughbfloat16contains bothf8_e4m3fnandf8_e5m2!). - As a result:
np.result_type(np.result_type(f8_e4m3fn, np.float16), f8_e5m2)==np.result_type(np.float16, f8_e5m2)==dtype('float16'), whereasnp.result_type(np.result_type(f8_e4m3fn, f8_e5m2), np.float16)==np.result_type(np.float32, np.float16)==dtype('float32').
- In
- Missing
PyArray_RegisterCanCasttoNPY_HALF:- In
RegisterCustomFloatCasts,PyArray_RegisterCanCastis called forNPY_FLOAT,NPY_DOUBLE,NPY_LONGDOUBLE,NPY_CFLOAT,NPY_CDOUBLE, andNPY_CLONGDOUBLE, but never forNPY_HALF, even whenCustomFloatSafeTo<T, half>()is true. - Because custom floats have
kind == 'V', NumPy marks theT -> float16cast asNPY_UNSAFE_CASTING. - Consequently:
np.result_type(f8_e4m3fn, np.float16)isdtype('float16'), yetnp.can_cast(f8_e4m3fn, np.float16, casting='safe')andnp.can_cast(f8_e4m3fn, np.float16, casting='same_kind')areFalse.- Calling any ufunc or
np.concatenateon(f8_e4m3fn, np.float16)promotes the inputs tofloat16viadefault_ufunc_promoterand then fails casting validation (casting='same_kind') withUFuncTypeError: Cannot cast ufunc 'add' input 0 from dtype(float8_e4m3fn) to dtype('float16') with casting rule 'same_kind'.
- In
Suggested Fix
- In
RegisterCustomFloatCasts, registerNPY_HALFwhenCustomFloatSafeTo<T, half>()holds:if constexpr (CustomFloatSafeTo<T, half>()) { if (PyArray_RegisterCanCast(CustomFloatType<T>::npy_descr, NPY_HALF, NPY_NOSCALAR) < 0) { return false; } }
- Make
CommonCustomFloatDTypeImplandNPyCustomFloat_CommonDTypeconsistent on whether custom floats that fit infloat16(orbfloat16) promote tofloat16orfloat32.
Reproducing Test Case
@pytest.mark.parametrize(
"float_t",
[f4, f6_e2m3, f6_e3m2, f8_e3m4, f8_e4m3, f8_e4m3fn, f8_e5m2],
)
def test_custom_float_safe_to_float16_can_cast_and_ufunc(float_t):
assert np.result_type(float_t, np.float16) == np.dtype(np.float16)
assert np.can_cast(float_t, np.float16, casting="safe")
a = np.ones(2, dtype=float_t)
b = np.ones(2, dtype=np.float16)
assert np.add(a, b).dtype == np.dtype(np.float16)
assert np.concatenate([a, b]).dtype == np.dtype(np.float16)
def test_float8_with_float16_associativity():
ab_c = np.result_type(np.result_type(f8_e4m3fn, f8_e5m2), np.float16)
a_bc = np.result_type(f8_e4m3fn, np.result_type(f8_e5m2, np.float16))
assert ab_c == a_bcProblem 5: CommonDType Promotes Type Pairs That Have No Registered Cast (or Only unsafe Casts) in casts.cc
Root Cause
-
(custom_int, float8_e8m0fnu)has no cast registered at all (andfloat8_e8m0fnucannot represent0or negative numbers):-
NPyCustomFloat_CommonDTypeunconditionally returnsclsfor anyIsCustomIntDType(other). - However,
float8_e8m0fnuis an unsigned exponent-only format ($2^E$ , no zero, no negative numbers), andcasts.ccregisters no cast between any custom int (i1, i2, i4, u1, u2, u4) andfloat8_e8m0fnu. -
np.result_type(i4, f8_e8m0)returnsdtype(float8_e8m0fnu), butnp.can_cast(i4, f8_e8m0, casting='unsafe')isFalseandnp.zeros(1, dtype=i4).astype(f8_e8m0)raisesTypeError.
-
-
(int4 | uint4, float6_e2m3fn)has no cast registered at all (and other narrow floats cannot represent allint4/uint4values):-
casts.ccexplicitly omitsint4 <-> float6_e2m3fnanduint4 <-> float6_e2m3fnbecausefloat6_e2m3fnhas max value7.5(// int4 -> float6_e2m3fn is not safe and we only register safe casts). - Yet
np.result_type(i4, f6_e2m3)andnp.result_type(u4, f6_e2m3)returndtype(float6_e2m3fn), which cannot even be cast to withcasting='unsafe'. - Likewise,
float4_e2m1fn(max6.0,digits = 2) cannot representint4values-8, -7, 7oruint4values5, 7, 9..15, andfloat6_e3m2fn,float8_e5m2,float8_e5m2fnuz,float8_e3m4(digits = 3) cannot represent odduint4values9, 11, 13, 15.
-
-
(float8_e8m0fnu, bcomplex32)has no cast registered at all:- In
complex.cc,NPyCustomComplex_CommonDTypereturnsbcomplex32whenotherisfloat8_e8m0fnu(sinceCustomFloatSafeTo<float8_e8m0fnu, bfloat16>()is true). - However,
casts.cconly registers casts betweenfloat8_e8m0fnuandbfloat16(and registers that same cast twice on lines 109 and 110), never registering a cast betweenfloat8_e8m0fnuandbcomplex32(orcomplex64/float64). - Thus
np.result_type(f8_e8m0, bc32)returnsdtype(bcomplex32), whilenp.can_cast(f8_e8m0, bc32, casting='unsafe')isFalseandnp.ones(1, dtype=f8_e8m0).astype(bc32)raisesTypeError.
- In
-
Missing
PyArray_RegisterCanCastfor cross-custom-type casts breaks ufuncs andnp.concatenateunder defaultcasting='same_kind':- In
casts.cc,RegisterTwoWayCustomCastandRegisterOneWayCustomCastonly callPyArray_RegisterCastFuncand neverPyArray_RegisterCanCast. - Because all custom DTypes have
kind == 'V',PyArray_AddLegacyWrapping_CastingImplassignsNPY_UNSAFE_CASTINGto every cross-itemsize cast between custom types (e.g.f8_e4m3fn -> bf16,f4 -> bf16,i4 -> bf16,bf16 -> bc32). - Whenever
np.result_type(a, b)promotes two distinct custom DTypes to a wider custom DType,np.add(arr_a, arr_b)andnp.concatenate([arr_a, arr_b])fail under defaultcasting='same_kind'withTypeError: Cannot cast ... with casting rule 'same_kind'.
- In
Reproducing Test Case
ALL_CUSTOM_DTYPES = ALL_CUSTOM_FLOATS + ALL_INTN + ALL_CUSTOM_COMPLEX
@pytest.mark.parametrize(
"a, b", list(itertools.combinations(ALL_CUSTOM_DTYPES, 2))
)
def test_all_promoted_custom_pairs_can_cast_and_concatenate(a, b):
common = np.result_type(a, b)
assert np.can_cast(a, common, casting="safe"), (a, b, common)
assert np.can_cast(b, common, casting="safe"), (a, b, common)
arr_a = np.ones(1, dtype=a)
arr_b = np.ones(1, dtype=b)
assert np.concatenate([arr_a, arr_b]).dtype == common
assert np.add(arr_a, arr_b).dtype == commonProblem 6: NPyIntN_CommonDType Promotes Distinct Custom Ints to int16 Instead of int8, Breaking Associativity
Root Cause
In ints.cc, promoting any two distinct custom ints (int1, int2, int4, uint1, uint2, uint4) returns &PyArray_Int16DType (np.int16), even though:
- Every custom int has values in
$[-8, 15]$ , which fits innp.int8($[-128, 127]$ ). -
ints.ccregistersNPY_INT8as a safe cast target for all six custom int types. -
np.result_type(int_t, np.int8)returnsnp.int8for every custom intint_t(includinguint4).
Promoting (i2, i4) or (i4, u4) to np.int16 instead of np.int8 (or i4 for (i2, i4)) breaks associativity with np.int8 and bfloat16:
- With
np.int8:np.result_type(np.result_type(i2, np.int8), i4)==np.result_type(np.int8, i4)==dtype('int8')np.result_type(np.result_type(i2, i4), np.int8)==np.result_type(np.int16, np.int8)==dtype('int16')
- With
bfloat16:np.result_type(i2, np.result_type(i4, bf16))==np.result_type(i2, bf16)==dtype(bfloat16)np.result_type(np.result_type(i2, i4), bf16)==np.result_type(np.int16, bf16)==dtype('float64')
Reproducing Test Case
@pytest.mark.parametrize("a, b", list(itertools.combinations(ALL_INTN, 2)))
@pytest.mark.parametrize("c", [np.int8, bf16])
def test_custom_int_promotion_associativity(a, b, c):
ab_c = np.result_type(np.result_type(a, b), c)
a_bc = np.result_type(a, np.result_type(b, c))
abc = np.result_type(a, b, c)
assert ab_c == a_bc == abc, (a, b, c, ab_c, a_bc, abc)Problem 7: Leftover Safe-Cast Registration from int8/uint8 to Narrow Custom Floats, Duplicate Cast Registration, and Unchecked spec->slots
Root Cause
- Bogus
PyArray_RegisterCanCastfromint8/uint8to all custom floats infloats.cc:RegisterCustomFloatCastsunconditionally registersNPY_UBYTE(uint8) andNPY_BYTE(int8) as safely castable to everyCustomFloatType<T>.- Only
bfloat16(8 significand bits, signed, wide exponent range) can represent allint8anduint8values. Narrower floats (float4_e2m1fn,float6_*,float8_*) cannot represent allint8/uint8values (andfloat8_e8m0fnucannot represent0or negative numbers). - As a result,
np.can_cast(np.int8, f4, casting='safe')andnp.can_cast(np.uint8, f8_e8m0, casting='safe')returnTrue, contradictingnp.result_type(np.int8, f4) == np.dtype(np.float64).
- Duplicate cast registration in
casts.cc:RegisterTwoWayCustomCast<float8_e8m0fnu, bfloat16, float>()already registers both directions (float8_e8m0fnu -> bfloat16andbfloat16 -> float8_e8m0fnu), so line 110 (RegisterTwoWayCustomCast<bfloat16, float8_e8m0fnu, float>()) is a duplicate registration.
- Unchecked
spec->slotspointer dereference innpy_2_compat_new_dtypes.h:- Line 50 checks
if (spec->slots != NULL ...), but lines 61–66 unconditionally dereferenceslot = spec->slotsinwhile (slot->slot != 0 || slot->pfunc != NULL).
- Line 50 checks
Reproducing Test Case
@pytest.mark.parametrize(
"float_t", [t for t in ALL_CUSTOM_FLOATS if t is not bf16]
)
@pytest.mark.parametrize("int_t", [np.int8, np.uint8])
def test_int8_uint8_not_safe_to_narrow_custom_floats(float_t, int_t):
assert not np.can_cast(int_t, float_t, casting="safe")|
I ran our internal tests against this PR and this behavior change causes at least one test failure: Before: This one certainly seems like one that should be fixed: floats should win against a Python int scalar. AI suggests:Why it breaks: When evaluating arr_float8 + 1 (or arr_bfloat16 + 1.0), NumPy calls PyArray_CommonDType(PyLongDType, float8_e4m3fn) with the Python scalar's abstract DType first. Because ml_dtypes uses NPY_DT_legacy_descriptor_proto, NPY_DT_is_legacy(other) is true. In abstractdtypes.c, PyLongDType, PyFloatDType, and PyComplexDType assume any NPY_DT_is_legacy user dtype has no custom common_dtype for abstract scalars, so instead of returning Py_NotImplemented (which would delegate to ml_dtypes's common_dtype), they probe other->common_dtype(other, uint8 / float16 / complex64)—which promotes float8 + uint8 to float64/float16 and bfloat16 + float16 to float32. The fix: In NumPy (abstractdtypes.c): In int_common_dtype, float_common_dtype, and complex_common_dtype, inside else if (NPY_DT_is_legacy(other)), call NPY_DT_CALL_common_dtype(other, cls) first and return its result unless it is Py_NotImplemented, only falling back to the uint8/float16/complex64 probe when it returns Py_NotImplemented. In ml_dtypes (to work on already-released NumPy 2.0–2.4 without waiting for the NumPy fix): At module init in dtypes.cc, wrap the common_dtype slot (((PyArrayDTypeMeta_CommonDType**)dt.dt_slots)[NPY_DT_common_dtype - 1]) on PyArray_PyLongDType, PyArray_PyFloatDType, and PyArray_PyComplexDType so that when other is an ml_dtypes DType, it returns Py_NotImplemented. That forces PyArray_CommonDType to call other->common_dtype(other, cls) (your NPyCustomFloat_CommonDType / NPyCustomInt_CommonDType / NPyCustomComplex_CommonDType). One small bug in ints.cc: In NPyCustomInt_CommonDType, other == &PyArray_PyComplexDType currently returns NPY_DOUBLE instead of NPY_CDOUBLE. |
|
Grrr, yeah. Some of those earlier reports seemed a bit like the agent wanted to solve everything (rather than just improve things considerably), but not all and not the last thing. Yes, I had not adjusted all the safe-casting logic, I had considered it a follow-up, but yeah, maybe this is large enough to just pile on a bit. That said, due to the effect below, I don't think we should change it yet. Disregarding the AI suggestions (yes, NumPy should not use it's "legacy hack" on the abstract DTypes when downstream explicitly implement I designed this NumPy path explicitly to allow improving things while not breaking existing behavior, because fixing everything at once isn't viable. I have to check the notes on non-commutative promotion. Many of those seem like embarrassing misses in testing mostly; but there are some limitations on that when it comes to int/uint mixing e.g.. I have logic in NumPy that should make it work within some limitations, but let's see. |
|
OK... this grew again, there is a lot of agent work there, but I did manually polish the actual common-dtype operations quite a lot as well at some point along the way. The current state is that:
The safe casts are fixed up, but I did not attempt to fix the fact that float8 and float4 are marked as safe because they are both Another thing I ignored is that |
|
Thanks! I'll pull this in and run our tests as well. |
|
Hmmm, the nightly is failing because I tested with the NumPy fixes applied and the xfail is wrong on dev without those. If we want to push things, could make that either wait for the final release or add a probe. |
|
Happy either way. I know you probably want to get this in, up to you. |
|
From the point of view of our tests this seems ok. I'm tempted to land and iterate at this point. (There's a merge conflict but I have resolved it internally and can do it in the merge.) Is this good to go from your perspective? |
|
Thanks, I pushed a fix for the nightly failure (one of the PRs is merged already, which made it leaner as well).
Yeah, I think so. As I mentioned there are follow-ups that are good and I am sure we could squabble about details, but I think the direction is sound and the backwards compatibility changes not too bad here. (Of course the promotions are changed, so not a bug fix.) |
|
Would you mind squashing your commits? Thanks! |
8730b84 to
44565ce
Compare
Register dtypes through NumPy's new-style DType API while retaining legacy dtype compatibility. To do this the NumPy 2.5 compatibility headers are vendored (to not require NumPy 2.5+ for compilation). The major new feature is to use the common-dtype new-style method to fix many `np.result_type()` results, especially when mixing with Python scalars. (Some of these fixes need NumPy 2.6+ for full consistency) Related to this some safe-casting rules are fixed (but not all, this will require using new-style casts throughout). Additinoall, byte-swapping and complex-to-complex conversions are now fixed.
44565ce to
4603e1d
Compare
|
Yup, done! Got too used to squash merging (or not caring) ;). |
gh-360 is a little bit hard to actually pull of. In NumPy 2.5+ (backport vendorered to compile on older NumPy versions), it is now allowed to create a "new style DType" that still is a legacy dtype.
That means we inherit a few existing quirks, but it mostly means that
ml_dtypescan start using new API conveniently without much concern for backwards compatibility issues.The current state is:
result_type()now does a better job/the right thing (yes this adds a bunch of code).array(..., dtype=dtype('bfloat16'))rather than justbfloat16. (I am restoring that in NumPy for 2.5+, although we need a better solution).Next steps, unlocked things:
np.finfo()(given one backport PR from me in NumPy) for NumPy 2.5+.I agree that the long list of dtypes is a bit annoying for
CommonDType. After NumPy has a better DType hierarchy, we could allow creating your own baseclass there easily which would simplify this (although maybe not speed up in practice).(heavy use of claude to spit out code, but of course with absolute design micro-managing in many relevant parts -- but I'll need to go through once more myself).