Skip to content

MAINT: Make most DTypes (not np.dtype) HeapTypes - #31364

Merged
ngoldbaum merged 10 commits into
numpy:mainfrom
seberg:heap-dtypes
Jun 23, 2026
Merged

ngoldbaum merged 10 commits into
numpy:mainfrom
seberg:heap-dtypes

Conversation

@seberg

@seberg seberg commented Apr 30, 2026 •

Copy link
Copy Markdown
Member

This makes all DTypes HeapTypes. I would mark them as immortal, but Python isn't making it easy without private API use so assume the (already enabled) deferred ref-counting is enough.

Because it is IMO nonsense to limit that to free-threaded, added a helper and use it throughout.

(Claude was used with guidance to execute what I wanted.)


Summary of changes:

  • Use PyType_FromMetaclass for all dynamically created dtypes:
    • We make sure to also make the immortal, and keep them immutable for now.
  • Simplify the abstract (and semi-concrete Python dtype) creation with a loop.
    • This should init from spec for the abstract ones at least ideally, but this works and is a big first step.
  • add_newdoc is changed to work with mutable heap-types where one can set __doc__. Making all of these immutable means it doesn't matter, but thought I'd just keep it.
    • The new branch is in theory a bit of an improvement as it stores the stripped version (since we have __doc__ available in the dict), but now with immutable it isn't actually used.
      So there is a potential small improvement here.
    • I can pull this out again, I don't think it matters as it's internal, but it may only be quite the right direction.
  • _PyArray_MapPyTypeToDType for the other dtypes is just moved out for now, didn't seem clearer (and the order didn't work out).
  • (StringDType isn't moved yet, but it is straight-forward. The dynamically created ones seemed more interesting.)

Overall, I think this is all very straight-forward actually. I thought there might be something tricky, but the trickiest thing was that all DTypes need to be moved if we move the abstract ones for subclassing.

@seberg

seberg commented Apr 30, 2026 •

Copy link
Copy Markdown
Member Author

@ngoldbaum free-threaded crash in `macOS tests / Accelerate - macos_x86_64 - 3.14t (pull_request). Presumably unrelated (let's see if it reproduces).

EDIT, no it's reliable. This happened before in #30375 except that there it didn't actually segfault. Here it starts to actually segfault.

Details
Fatal Python error: Segmentation fault

<Cannot show all threads while the GIL is disabled>
Stack (most recent call first):
  File "/Users/runner/work/numpy/numpy/build-install/usr/lib/python3.14t/site-packages/numpy/_core/tests/test_dtype.py", line 886 in test_tuple_recursion
  File "/Users/runner/work/numpy/numpy/build-install/usr/lib/python3.14t/site-packages/numpy/testing/_private/utils.py", line 2865 in wrapper
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/_pytest/python.py", line 166 in pytest_pyfunc_call
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_callers.py", line 121 in _multicall
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_manager.py", line 120 in _hookexec
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_hooks.py", line 512 in __call__
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/_pytest/python.py", line 1720 in runtest
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/_pytest/runner.py", line 179 in pytest_runtest_call
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_callers.py", line 121 in _multicall
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_manager.py", line 120 in _hookexec
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_hooks.py", line 512 in __call__
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/_pytest/runner.py", line 245 in <lambda>
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/_pytest/runner.py", line 353 in from_call
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/_pytest/runner.py", line 244 in call_and_report
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/_pytest/runner.py", line 137 in runtestprotocol
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/_pytest/runner.py", line 118 in pytest_runtest_protocol
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_callers.py", line 121 in _multicall
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_manager.py", line 120 in _hookexec
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_hooks.py", line 512 in __call__
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/xdist/remote.py", line 227 in run_one_test
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/xdist/remote.py", line 206 in pytest_runtestloop
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_callers.py", line 121 in _multicall
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_manager.py", line 120 in _hookexec
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_hooks.py", line 512 in __call__
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/_pytest/main.py", line 372 in _main
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/_pytest/main.py", line 318 in wrap_session
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/_pytest/main.py", line 365 in pytest_cmdline_main
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_callers.py", line 121 in _multicall
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_manager.py", line 120 in _hookexec
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/pluggy/_hooks.py", line 512 in __call__
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/xdist/remote.py", line 427 in <module>
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/execnet/gateway_base.py", line 1291 in executetask
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/execnet/gateway_base.py", line 341 in run
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/execnet/gateway_base.py", line 411 in _perform_spawn
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/execnet/gateway_base.py", line 389 in integrate_as_primary_thread
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/execnet/gateway_base.py", line 1273 in serve
  File "/Library/Frameworks/PythonT.framework/Versions/3.14/lib/python3.14t/site-packages/execnet/gateway_base.py", line 1806 in serve
  File "<string>", line 8 in <module>
  File "<string>", line 1 in <module>

Current thread's C stack trace (most recent call first):
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at _Py_DumpStack+0x42 [0x1080430a2]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at faulthandler_dump_c_stack+0x3f [0x10805913f]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at faulthandler_fatal_error+0x127 [0x108059027]
  Binary file "/usr/lib/system/libsystem_platform.dylib", at _sigtramp+0x1d [0x7ff81b97c31d]
  Binary file '<unknown>' [0x0]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at _Py_MergeZeroLocalRefcount+0x63 [0x107d9ce03]
  Binary file "/Users/runner/work/numpy/numpy/build-install/usr/lib/python3.14t/site-packages/numpy/_core/_multiarray_umath.cpython-314t-darwin.so", at arraydescr_dealloc+0x123 [0x108aeeed3]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at subtype_dealloc+0xbaf [0x107dddf7f]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at _Py_MergeZeroLocalRefcount+0x63 [0x107d9ce03]
  Binary file "/Users/runner/work/numpy/numpy/build-install/usr/lib/python3.14t/site-packages/numpy/_core/_multiarray_umath.cpython-314t-darwin.so", at arraydescr_dealloc+0x123 [0x108aeeed3]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at subtype_dealloc+0xbaf [0x107dddf7f]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at _Py_MergeZeroLocalRefcount+0x63 [0x107d9ce03]
  Binary file "/Users/runner/work/numpy/numpy/build-install/usr/lib/python3.14t/site-packages/numpy/_core/_multiarray_umath.cpython-314t-darwin.so", at arraydescr_dealloc+0x123 [0x108aeeed3]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at subtype_dealloc+0xbaf [0x107dddf7f]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at _Py_MergeZeroLocalRefcount+0x63 [0x107d9ce03]
  Binary file "/Users/runner/work/numpy/numpy/build-install/usr/lib/python3.14t/site-packages/numpy/_core/_multiarray_umath.cpython-314t-darwin.so", at arraydescr_dealloc+0x123 [0x108aeeed3]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at subtype_dealloc+0xbaf [0x107dddf7f]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at _Py_MergeZeroLocalRefcount+0x63 [0x107d9ce03]
  Binary file "/Users/runner/work/numpy/numpy/build-install/usr/lib/python3.14t/site-packages/numpy/_core/_multiarray_umath.cpython-314t-darwin.so", at arraydescr_dealloc+0x123 [0x108aeeed3]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at subtype_dealloc+0xbaf [0x107dddf7f]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at _Py_MergeZeroLocalRefcount+0x63 [0x107d9ce03]
  Binary file "/Users/runner/work/numpy/numpy/build-install/usr/lib/python3.14t/site-packages/numpy/_core/_multiarray_umath.cpython-314t-darwin.so", at arraydescr_dealloc+0x123 [0x108aeeed3]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at subtype_dealloc+0xbaf [0x107dddf7f]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at _Py_MergeZeroLocalRefcount+0x63 [0x107d9ce03]
  Binary file "/Users/runner/work/numpy/numpy/build-install/usr/lib/python3.14t/site-packages/numpy/_core/_multiarray_umath.cpython-314t-darwin.so", at arraydescr_dealloc+0x123 [0x108aeeed3]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at subtype_dealloc+0xbaf [0x107dddf7f]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at _Py_MergeZeroLocalRefcount+0x63 [0x107d9ce03]
  Binary file "/Users/runner/work/numpy/numpy/build-install/usr/lib/python3.14t/site-packages/numpy/_core/_multiarray_umath.cpython-314t-darwin.so", at arraydescr_dealloc+0x123 [0x108aeeed3]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at subtype_dealloc+0xbaf [0x107dddf7f]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at _Py_MergeZeroLocalRefcount+0x63 [0x107d9ce03]
  Binary file "/Users/runner/work/numpy/numpy/build-install/usr/lib/python3.14t/site-packages/numpy/_core/_multiarray_umath.cpython-314t-darwin.so", at arraydescr_dealloc+0x123 [0x108aeeed3]
  Binary file "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT", at subtype_dealloc+0xbaf [0x107dddf7f]
  <truncated rest of calls>

EDIT: My impression is that its OKish to skip the test, but after it's done and we have nightlies, we either re-open the Python issue or make a new one -- I would hope this is Python.

@seberg seberg changed the title MAINT,ENH: Make DTypes (except np.dtype) proper HeapTypes MAINT,ENH: Make most DTypes (not np.dtype) proper HeapTypes Apr 30, 2026
@seberg seberg changed the title MAINT,ENH: Make most DTypes (not np.dtype) proper HeapTypes MAINT: Make most DTypes (not np.dtype) proper HeapTypes Apr 30, 2026
@seberg seberg changed the title MAINT: Make most DTypes (not np.dtype) proper HeapTypes MAINT: Make most DTypes (not np.dtype) HeapTypes Apr 30, 2026
@mattip

mattip commented May 11, 2026

Copy link
Copy Markdown
Member

Any thoughts about benchmarks?

@mattip

mattip commented May 11, 2026

Copy link
Copy Markdown
Member

Very cool, BTW.

@seberg

seberg commented May 12, 2026

Copy link
Copy Markdown
Member Author

I would think it doesn't make a real difference. The main difference I could see would be related to them being implicitly immortal previously and now they are only explicitly immortal.
And I am not sure if the explicitly immortalizing them actually prevents them fully from taking part in cyclic GC handling.

I suppose there may be some other minor differences between heap and static types, but I don't think for the most part.

I ran some benchmarks (-t small -t Small -t scalar as I thought larger array tests really shouldn't notice this). Some are surprisingly different (and consistently so), although a test that runs for 300-400us doesn't seem like it should see heap-type changes... I think those ones randomly spread both ways.

Details
| Change   | Before [c36307a4] <main>   | After [c2bf62ca] <heap-dtypes>   |   Ratio | Benchmark (Parameter)                                                                           |
|----------|----------------------------|----------------------------------|---------|-------------------------------------------------------------------------------------------------|
| +        | 317±0μs                    | 461±0μs                          |    1.45 | bench_ma.MAFunctions1v.time_functions_1v('np', 'log', 'big')                                    |
| +        | 1.92±0μs                   | 2.56±0.01μs                      |    1.34 | bench_ufunc.CustomComparison.time_less_than_scalar1(<class 'numpy.bool'>)                       |
| +        | 36.2±0μs                   | 48.2±0μs                         |    1.33 | bench_ufunc.NDArrayGetItem.time_methods_getitem([0, -1], 'big')                                 |
| +        | 1.94±0μs                   | 2.46±0μs                         |    1.27 | bench_ma.MAMethod0v.time_methods_0v('conjugate', 'big')                                         |
| +        | 2.05±0μs                   | 2.59±0.01μs                      |    1.27 | bench_ufunc.CustomComparison.time_less_than_scalar2(<class 'numpy.uint8'>)                      |
| +        | 175±0μs                    | 219±0μs                          |    1.25 | bench_ma.MAFunctions1v.time_functions_1v('np.ma', 'sqrt', 'big')                                |
| +        | 37.1±0μs                   | 45.3±0μs                         |    1.22 | bench_ma.MAMethodSetItem.time_methods_setitem((0, 0), masked, 'big')                            |
| +        | 34.9±0μs                   | 41.3±0μs                         |    1.18 | bench_ma.MAMethodGetItem.time_methods_getitem((0, 0), 'big')                                    |
| +        | 32.4±0μs                   | 37.1±0μs                         |    1.15 | bench_ma.MAMethodSetItem.time_methods_setitem((-1, 0), masked, 'big')                           |
| +        | 2.07±0ms                   | 2.35±0ms                         |    1.14 | bench_ma.Corrcoef.time_corrcoef('large')                                                        |
| +        | 150±0μs                    | 171±0μs                          |    1.14 | bench_ma.MAFunctions1v.time_functions_1v('np', 'sin', 'big')                                    |
| +        | 324±0μs                    | 370±0μs                          |    1.14 | bench_ma.MAFunctions2v.time_functions_2v('np', 'divide', 'big')                                 |
| +        | 56.0±0μs                   | 63.9±0μs                         |    1.14 | bench_ma.MAMethodGetItem.time_methods_getitem(0, 'big')                                         |
| +        | 36.4±0μs                   | 41.0±0μs                         |    1.13 | bench_ufunc.NDArraySetItem.time_methods_setitem([0, -1], 'big')                                 |
| +        | 149±0.6ns                  | 166±5ns                          |    1.12 | bench_ufunc.NDArrayGetItem.time_methods_getitem((0, 0), 'small')                                |
| +        | 77.9±0.2ns                 | 87.2±10ns                        |    1.12 | bench_ufunc.NDArraySetItem.time_methods_setitem(0, 'small')                                     |
| +        | 169±2ns                    | 188±2ns                          |    1.11 | bench_ufunc.UFuncSmall.time_ufunc_numpy_scalar('sqrt')                                          |
| +        | 230±2ns                    | 253±7ns                          |    1.1  | bench_array_coercion.ArrayCoercionSmall.time_array_subok(1)                                     |
| +        | 150±0.4ns                  | 165±7ns                          |    1.1  | bench_ufunc.NDArrayGetItem.time_methods_getitem((-1, 0), 'small')                               |
| +        | 150±1ns                    | 166±2ns                          |    1.1  | bench_ufunc.UFuncSmall.time_ufunc_python_float('sqrt')                                          |
| +        | 208±0.5ns                  | 227±8ns                          |    1.09 | bench_array_coercion.ArrayCoercionSmall.time_array_dtype_not_kwargs(1)                          |
| +        | 25.2±0μs                   | 27.5±0μs                         |    1.09 | bench_ma.MAMethod0v.time_methods_0v('transpose', 'big')                                         |
| +        | 162±0.9ns                  | 177±6ns                          |    1.09 | bench_ufunc.NDArrayGetItem.time_methods_getitem(0, 'small')                                     |
| +        | 4.18±0.1ms                 | 4.57±0.03ms                      |    1.09 | bench_ufunc_strides.BinaryComplex.time_binary_scalar_in1(<ufunc 'multiply'>, 4, 2, 4, 'D')      |
| +        | 222±0.7ns                  | 241±5ns                          |    1.08 | bench_array_coercion.ArrayCoercionSmall.time_array_no_copy(1)                                   |
| +        | 172±1ns                    | 186±9ns                          |    1.08 | bench_ufunc.UFuncSmall.time_ufunc_numpy_scalar('cos')                                           |
| +        | 696±2ns                    | 742±9ns                          |    1.07 | bench_ma.Indexing.time_scalar(False, 1, 10)                                                     |
| +        | 714±6ns                    | 765±6ns                          |    1.07 | bench_ma.Indexing.time_scalar(False, 2, 1000)                                                   |
| +        | 273±2ns                    | 292±2ns                          |    1.07 | bench_scalar.ScalarMath.time_multiplication('int32')                                            |
| +        | 284±3ns                    | 303±3ns                          |    1.07 | bench_scalar.ScalarMath.time_multiplication('int64')                                            |
| +        | 339±5ns                    | 358±7ns                          |    1.06 | bench_array_coercion.ArrayCoercionSmall.time_array_all_kwargs(1)                                |
| +        | 247±3ns                    | 262±10ns                         |    1.06 | bench_array_coercion.ArrayCoercionSmall.time_array_dtype_not_kwargs(array([5]))                 |
| +        | 245±0.6ns                  | 259±5ns                          |    1.06 | bench_array_coercion.ArrayCoercionSmall.time_array_subok(np.int64(5))                           |
| +        | 337±0.8ns                  | 358±6ns                          |    1.06 | bench_array_coercion.ArrayCoercionSmall.time_asanyarray_dtype_order([1])                        |
| +        | 699±2ns                    | 737±7ns                          |    1.06 | bench_ma.Indexing.time_scalar(False, 1, 1000)                                                   |
| +        | 718±2ns                    | 761±10ns                         |    1.06 | bench_ma.Indexing.time_scalar(False, 2, 100)                                                    |
| +        | 402±2ns                    | 427±10ns                         |    1.06 | bench_scalar.ScalarMath.time_addition('float16')                                                |
| +        | 30.0±0.3μs                 | 31.7±0.1μs                       |    1.06 | bench_scalar.ScalarStr.time_str_repr('complex128')                                              |
| +        | 182±0.8ns                  | 193±10ns                         |    1.06 | bench_ufunc.UFuncSmall.time_ufunc_numpy_scalar('abs')                                           |
| +        | 219±1ns                    | 230±7ns                          |    1.05 | bench_array_coercion.ArrayCoercionSmall.time_array(array([5]))                                  |
| +        | 331±0.6ns                  | 349±10ns                         |    1.05 | bench_array_coercion.ArrayCoercionSmall.time_array_no_copy([1])                                 |
| +        | 238±2ns                    | 250±7ns                          |    1.05 | bench_array_coercion.ArrayCoercionSmall.time_ascontiguousarray(1)                               |
| +        | 699±2ns                    | 735±10ns                         |    1.05 | bench_ma.Indexing.time_scalar(False, 1, 100)                                                    |
| -        | 1.63±0μs                   | 1.55±0μs                         |    0.95 | bench_polynomial.Polynomial.time_polynomial_evaluation_scalar                                   |
| -        | 2.65±0.03μs                | 2.52±0.01μs                      |    0.95 | bench_scalar.ScalarMath.time_add_int32arr_and_other('int32')                                    |
| -        | 33.8±0.4μs                 | 32.1±0.1μs                       |    0.95 | bench_ufunc_strides.BinaryInt.time_binary_scalar_in0(<ufunc 'minimum'>, 1, 1, 1, 'l')           |
| -        | 40.9±0.5μs                 | 38.7±0.1μs                       |    0.95 | bench_ufunc_strides.BinaryInt.time_binary_scalar_in1(<ufunc 'maximum'>, 2, 2, 2, 'l')           |
| -        | 23.5±0.3μs                 | 22.3±0.8μs                       |    0.95 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in0(<ufunc 'bitwise_and'>, 1, 1, 1, 'b') |
| -        | 46.2±0.7μs                 | 43.8±2μs                         |    0.95 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in0(<ufunc 'multiply'>, 1, 1, 1, 'H')    |
| -        | 46.2±0.3μs                 | 43.8±1μs                         |    0.95 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in0(<ufunc 'subtract'>, 1, 1, 1, 'H')    |
| -        | 45.4±0.6μs                 | 43.0±1μs                         |    0.95 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in1(<ufunc 'bitwise_and'>, 1, 1, 1, 'H') |
| -        | 23.8±0.2μs                 | 22.5±1μs                         |    0.95 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in1(<ufunc 'multiply'>, 1, 1, 1, 'B')    |
| -        | 4.17±0μs                   | 3.93±0μs                         |    0.94 | bench_ufunc.NDArraySetItem.time_methods_setitem((0, 0), 'big')                                  |
| -        | 333±0.9ns                  | 314±8ns                          |    0.94 | bench_ufunc.UFuncSmall.time_ufunc_small_array_inplace('sqrt')                                   |
| -        | 46.7±0.4μs                 | 43.8±2μs                         |    0.94 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in0(<ufunc 'bitwise_and'>, 1, 1, 1, 'h') |
| -        | 46.6±0.3μs                 | 43.7±2μs                         |    0.94 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in0(<ufunc 'bitwise_xor'>, 1, 1, 1, 'h') |
| -        | 36.3±0.6μs                 | 34.0±0.9μs                       |    0.94 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in0(<ufunc 'logical_xor'>, 1, 1, 1, 'h') |
| -        | 24.0±0.07μs                | 22.4±0.8μs                       |    0.94 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in1(<ufunc 'bitwise_xor'>, 1, 1, 1, 'B') |
| -        | 2.00±0.01ms                | 1.86±0.01ms                      |    0.93 | bench_alloc_cache.SmallArrayCreation.time_empty_loop(256)                                       |
| -        | 6.72±0.05μs                | 6.25±0.01μs                      |    0.93 | bench_core.NumPyChar.time_add_big_list_small_string                                             |
| -        | 24.0±0.1μs                 | 22.3±0.6μs                       |    0.93 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in1(<ufunc 'bitwise_and'>, 1, 1, 1, 'B') |
| -        | 24.2±0.1μs                 | 22.4±0.9μs                       |    0.93 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in1(<ufunc 'bitwise_xor'>, 1, 1, 1, 'b') |
| -        | 46.8±1μs                   | 43.3±2μs                         |    0.93 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in1(<ufunc 'bitwise_xor'>, 1, 1, 1, 'h') |
| -        | 24.7±0.3μs                 | 22.9±1μs                         |    0.93 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in1(<ufunc 'subtract'>, 1, 1, 1, 'B')    |
| -        | 45.7±1μs                   | 42.6±1μs                         |    0.93 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in1(<ufunc 'subtract'>, 1, 1, 1, 'H')    |
| -        | 1.61±0.01ms                | 1.48±0ms                         |    0.92 | bench_alloc_cache.SmallArrayCreation.time_empty_loop(16)                                        |
| -        | 1.75±0.02ms                | 1.60±0.01ms                      |    0.91 | bench_alloc_cache.SmallArrayCreation.time_empty_loop(128)                                       |
| -        | 1.60±0.02ms                | 1.46±0.02ms                      |    0.91 | bench_alloc_cache.SmallArrayCreation.time_empty_loop(4)                                         |
| -        | 2.01±0.01ms                | 1.83±0.01ms                      |    0.91 | bench_alloc_cache.SmallArrayCreation.time_empty_loop(512)                                       |
| -        | 4.82±0.2ms                 | 4.39±0.02ms                      |    0.91 | bench_ufunc_strides.BinaryComplex.time_binary_scalar_in0(<ufunc 'add'>, 4, 4, 4, 'D')           |
| -        | 47.9±1μs                   | 43.5±2μs                         |    0.91 | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in1(<ufunc 'bitwise_and'>, 1, 1, 1, 'h') |
| -        | 1.60±0.01ms                | 1.44±0.01ms                      |    0.9  | bench_alloc_cache.SmallArrayCreation.time_empty_loop(64)                                        |
| -        | 274±0μs                    | 248±0μs                          |    0.9  | bench_ma.MAFunctions2v.time_functions_2v('np.ma', 'power', 'big')                               |
| -        | 16.8±0μs                   | 15.1±0μs                         |    0.9  | bench_ufunc.NDArraySetItem.time_methods_setitem(0, 'big')                                       |
| -        | 3.73±0.05ms                | 3.37±0.1ms                       |    0.9  | bench_ufunc_strides.BinaryComplex.time_binary_scalar_in0(<ufunc 'subtract'>, 1, 2, 4, 'D')      |
| -        | 24.9±0.4μs                 | 22.4±0.8μs                       |    0.9  | bench_ufunc_strides.BinaryIntContig.time_binary_scalar_in0(<ufunc 'bitwise_or'>, 1, 1, 1, 'b')  |
| -        | 1.62±0.02ms                | 1.44±0.01ms                      |    0.89 | bench_alloc_cache.SmallArrayCreation.time_empty_loop(127)                                       |
| -        | 228±0μs                    | 197±0μs                          |    0.86 | bench_ma.MAFunctions1v.time_functions_1v('np.ma', 'sin', 'big')                                 |
| -        | 166±0μs                    | 143±0μs                          |    0.86 | bench_ma.MAFunctions2v.time_functions_2v('np', 'multiply', 'big')                               |
| -        | 372±0μs                    | 318±0μs                          |    0.86 | bench_ma.MAFunctions2v.time_functions_2v('np.ma', 'divide', 'big')                              |
| -        | 48.2±0μs                   | 41.3±0μs                         |    0.86 | bench_ma.MAMethodSetItem.time_methods_setitem(0, 17, 'big')                                     |
| -        | 34.7±0μs                   | 29.4±0μs                         |    0.85 | bench_ma.MAMethod0v.time_methods_0v('ravel', 'big')                                             |
| -        | 48.6±0μs                   | 41.3±0μs                         |    0.85 | bench_ma.MAMethodSetItem.time_methods_setitem((0, 0), 17, 'big')                                |
| -        | 345±0μs                    | 291±0μs                          |    0.84 | bench_ma.MAFunctions1v.time_functions_1v('np', 'sqrt', 'big')                                   |
| -        | 191±0μs                    | 156±0μs                          |    0.82 | bench_ma.MAFunctions1v.time_functions_1v('np.ma', 'log', 'big')                                 |
| -        | 10.5±0μs                   | 7.80±0μs                         |    0.74 | bench_ufunc.NDArrayGetItem.time_methods_getitem((-1, 0), 'big')                                 |
| -        | 2.58±0.01μs                | 1.82±0μs                         |    0.71 | bench_ufunc.CustomComparison.time_less_than_scalar1(<class 'numpy.int8'>)                       |
| -        | 10.5±0μs                   | 7.11±0μs                         |    0.68 | bench_ufunc.NDArrayGetItem.time_methods_getitem(0, 'big')                                       |
| -        | 11.7±0μs                   | 7.68±0μs                         |    0.66 | bench_ufunc.NDArrayGetItem.time_methods_getitem((0, 0), 'big')                                  |

@mattip

mattip commented May 12, 2026

Copy link
Copy Markdown
Member

Seems something is off with a release note

/home/circleci/repo/doc/source/release/notes-towncrier.rst:341: WARNING: py:obj reference target not found: numpy.dtypes.register_dlpack_dtype
/home/circleci/repo/doc/source/release/notes-towncrier.rst:341: WARNING: py:obj reference target not found: numpy.dtypes.register_dlpack_dtype
/home/circleci/repo/doc/source/release/notes-towncrier.rst:341: WARNING: py:obj reference target not found: numpy.dtypes.register_dlpack_dtype

Some are surprisingly different (and consistently so),

I wonder if heap types are forcing a different memory layout and causing some cache misses. These are consistent over a few runs and recompilations?

@seberg

seberg commented May 12, 2026

Copy link
Copy Markdown
Member Author

Seems something is off with a release note

Yeah, I'll open a PR. I thought it was just a missing rebase first, but I think I truly forgot to add this function to the docs probably... And it didn't register because I thought there was a general CI failure that also hit the circle CI build.

@seberg

seberg commented May 12, 2026 •

Copy link
Copy Markdown
Member Author

I wonder if heap types are forcing a different memory layout and causing some cache misses. These are consistent over a few runs and recompilations?

I don't really think so, but of course it might change allocation patterns in the grand scheme? The allocation size didn't change (they were heap-type sized also before, we just didn't use the heaptype part of it) and in practice we also heap allocated them before as well...

The changes in those benchmarks are consistent yes, but I think we had +consistent random shifts in performance before in many PRs that are completely unrelated?

One other thing I could imagine (since I can't find gc.disable in asv) is that the pattern in which the GC kicks in changed and that makes a difference. Although I doubt that should make such a big difference...

@ngoldbaum

Copy link
Copy Markdown
Member

@kumaraditya303 can you try taking a look at this PR sometime in the next week if you have a chance?

@kumaraditya303

Copy link
Copy Markdown
Contributor

Converting a static type to heap type is technically backwards incompatible because if any third party code defines static type which subclasses from PyArray_ComplexAbstractDType etc then that wouldn't work. I am not sure if such usage is valid but I think it would be worth checking it.

@seberg

seberg commented May 15, 2026

Copy link
Copy Markdown
Member Author

Yeah which is why np.dtype itself cannot be converted :(. But it is only valid to subclass the abstract types in principle. In practice, I think we can get away with it now hopefully. (I.e. quaddtype doesn't and there are no big ones, and even if, it is unlikely they subclass).

@mattip

mattip commented May 16, 2026

Copy link
Copy Markdown
Member

Converting a static type to heap type is technically backwards incompatible because if any third party code defines static type which subclasses from PyArray_ComplexAbstractDType ...

Are you sure? Why wouldn't this work as long as PyArray_ComplexAbstractDType is fully constructed when the subclass calls PyType_Ready?

@kumaraditya303

Copy link
Copy Markdown
Contributor

@mattip

mattip commented May 16, 2026 •

Copy link
Copy Markdown
Member

ahh, I see, it is an actual check in CPython. But why is that? Is there a theoretical barrier? The basic_size and slots could all be made to handle this case, no?

Edit: the discussion in the issue that led to adding this check gave no rational for it. The opposite is true: Cython needed to flip the HEAP_TYPE flag when calling PyType_Ready on some types to avoid this check. There was a request to review this for CPython3.10 but it got lost.

@kumaraditya303

Copy link
Copy Markdown
Contributor

ahh, I see, it is an actual check in CPython. But why is that? Is there a theoretical barrier? The basic_size and slots could all be made to handle this case, no?

Static types are immutable whereas heap types are mutable, so if this is allowed then you can have a static type whose attribute change even though it is marked as immutable which is problematic. I think the specializing interpreter takes advantage of this and has performance benefit as well.

@seberg

seberg commented May 17, 2026

Copy link
Copy Markdown
Member Author

Static types are immutable whereas heap types are mutable,

Hmmm, but if this is the main reason then setting Py_TPFLAGS_IMMUTABLETYPE would be sufficient? Because that is of course no problem when the goal is to generally move from static to heaptypes.
I thought this was about ABI (i.e. smaller basicsize of the subclass), but as you said, maybe that is actually OK for for heap vs. static types.

@seberg

seberg commented Jun 3, 2026 •

Copy link
Copy Markdown
Member Author

Well, I just removed the immortalization since it seems hard and python devs feel that deferred refcounting should be enough and these types are already deferred ref-counted anyway.

EDIT: Maybe Python relaxes the immortalization logic, but I think we can follow up either way (I might also just make a PR there).

@jorenham

Copy link
Copy Markdown
Member

Can we now simplify that inspect.signature hack from #30169? (in a follow-up or something)

@seberg

seberg commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

Can we now simplify that inspect.signature hack from #30169? (in a follow-up or something)

Hmmmm, I stil made them immutable, so we still would have to add __signature__ explicitly or so, I suspect?
So... maybe, but I actually somewhat doubt it. Unless we had a clear moment of "oh this would work on a heap type".

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

Overall looks good. There are some errors (mostly in error paths) that Claude spotted, see comments below.

* Fill DTypeMeta information that varies between DTypes, any variable
* type information would need to be set before PyType_Ready().
*/
dtype_class->singleton = (PyArray_Descr *)descr;

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.

Pre-existing issue, but shouldn't this be an owned reference? The dealloc path does a DECREF and there's no corresponding INCREF:

Py_XDECREF(self->singleton);

Comment thread numpy/_core/src/multiarray/dtypemeta.c Resolved
Comment thread numpy/_core/src/multiarray/dtypemeta.c Resolved
Comment thread numpy/_core/src/multiarray/dtypemeta.c 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 mutates a type after creating it, which violates the IMMUTABLE flag that is set on the type. Now that you're creating every type with a spec, you can trivially add this as a special-case inside dtypemeta_wrap_legacy_descriptor when you create type_slots.

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 still getting mutated - was that intentional?

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.

ack, no just forgot to move it...

Comment thread numpy/_core/src/multiarray/dtypemeta.c Resolved
seberg added 7 commits June 17, 2026 18:05
This makes all DTypes HeapTypes.  For the builtin ones (and non-user
created ones) we also mark them all as immortal as it might help
a bit with refcounting on threaded execution.

Because it is IMO nonsense to limit that to free-threaded, added a
helper and use it throughout.

(Claude was used with guidance to execute what I wanted.)

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

Just one comment, I'm going to send a separate PR in for the singleton ownership issues, I think.

Comment thread numpy/_core/src/multiarray/dtypemeta.c 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 still getting mutated - was that intentional?

@kumaraditya303 kumaraditya303 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 read through the C changes and didn't find any issues. LGTM

@ngoldbaum

Copy link
Copy Markdown
Member

I opened seberg#53 with a couple fixes.

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

OK, I did one last round of review and I'm pretty sure all the C changes are OK now.

Sorry to have a couple last requests but I think the heap type inheritance issue might be worth pausing to think over? Up to you though, I'm hitting approve if you want to merge as-is and iterate on these issues.

IMO this needs a release note describing the nature of the change before hitting the merge button. The note should target extension module authors and should note that dtypes can now be dynamically allocated heap types and refcounting assumptions around static lifetimes might be invalidated.

I think the way we are setting a docstring on heap types is incorrect with this PR:

else if (PyObject_TypeCheck(obj, &PyType_Type)) {
/*
* We add it to both `tp_doc` and `__doc__` here. Note that in theory
* `tp_doc` extracts the signature line, but we currently do not use
* it. It may make sense to only add it as `__doc__` and
* `__text_signature__` to the dict in the future.
* The dictionary path is only necessary for heaptypes (currently not
* used) and metaclasses.
* If `__doc__` as stored in `tp_dict` is None, we assume this was
* filled in by `PyType_Ready()` and should also be replaced.
*/
PyTypeObject *new = (PyTypeObject *)obj;
_ADDDOC(new->tp_doc, new->tp_name);
if (new->tp_dict != NULL && PyDict_CheckExact(new->tp_dict) &&
PyDict_GetItemString(new->tp_dict, "__doc__") == Py_None) { // noqa: borrowed-ref - manual fix needed
/* Warning: Modifying `tp_dict` is not generally safe! */
if (PyDict_SetItemString(new->tp_dict, "__doc__", str) < 0) {
return NULL;
}
}

For a heap type that actually ends up garbage collected, tp_doc can be free'd, and us setting tp_doc manually there becomes incorrect.

Claude found that on a debug build of Python, both type("X", (np.dtype,), {}) and type("X", (type(np.dtype("float64")),), {}) segfault on this PR, with this stack trace:

Details
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
  * frame #0: 0x0000000000000000
    frame #1: 0x00000001001e5b88 python`type_new_get_bases + 532
    frame #2: 0x00000001001dab10 python`type_new + 424
    frame #3: 0x00000001001d9f30 python`type_call + 624
    frame #4: 0x00000001000ee020 python`_PyObject_MakeTpCall + 644
    frame #5: 0x00000001001dad4c python`type_vectorcall + 180
    frame #6: 0x00000001000eda68 python`_PyObject_VectorcallTstate + 316
    frame #7: 0x00000001000eec80 python`PyObject_Vectorcall + 60
    frame #8: 0x00000001002f9360 python`_TAIL_CALL_CALL + 2624
    frame #9: 0x00000001002e54dc python`_PyEval_EvalFrameDefault + 536
    frame #10: 0x00000001002e4fb4 python`_PyEval_EvalFrame + 60
    frame #11: 0x00000001002e4efc python`_PyEval_Vector + 552
    frame #12: 0x00000001002e4c7c python`PyEval_EvalCode + 228
    frame #13: 0x00000001003f3bf0 python`run_eval_code_obj + 212
    frame #14: 0x00000001003f3900 python`run_mod + 680
    frame #15: 0x00000001003f19f4 python`_PyRun_StringFlagsWithName + 228
    frame #16: 0x00000001003f184c python`_PyRun_SimpleStringFlagsWithName + 196
    frame #17: 0x0000000100434e18 python`pymain_run_command + 288
    frame #18: 0x00000001004345e0 python`pymain_run_python + 656
    frame #19: 0x0000000100434304 python`Py_RunMain + 36
    frame #20: 0x00000001004347d8 python`pymain_main + 100
    frame #21: 0x000000010043482c python`Py_BytesMain + 60
    frame #22: 0x0000000100004024 python`main + 36
    frame #23: 0x0000000187ac7e00 dyld`start + 6992

On a normal build of Python and on NumPy main with both builds, you get TypeError: Preliminary-API: Cannot subclass DType..

Claude suggests the following fix, which I can neither confirm no deny:

Suggested fix (needs a build to confirm — I reasoned it through CPython's
  type_new/type_call but didn't compile it): drop
  Py_TPFLAGS_DISALLOW_INSTANTIATION and restore tp_init = dtypemeta_init (the
  raising one). Leave tp_new inherited (== type_new), which still satisfies
  PyType_FromMetaclass's "tp_new must be NULL or type_new" restriction. Then:
  - type("X",(np.dtype,),{}) → type_new builds the candidate with
  metatype=_DTypeMeta (no NULL call), then type_call invokes _DTypeMeta.tp_init →
  clean TypeError.
  - type("X",(Float64DType,),{}) → Float64DType has no Py_TPFLAGS_BASETYPE → "not
  an acceptable base type" before tp_init is even reached.
  - Caveat for that fix: type_new now briefly builds a PyArray_DTypeMeta with
  dt_slots == NULL before tp_init rejects it, and it's GC-tracked in that window.
  dtypemeta_dealloc already tolerates NULL dt_slots (the ownership-fix commit),
  but dtypemeta_traverse (dtypemeta.c:511) does not —
  Py_VISIT(NPY_DT_SLOTS(type)->castingimpls) would deref NULL. Guard it: if
  (NPY_DT_SLOTS(type) != NULL).

@seberg

seberg commented Jun 22, 2026

Copy link
Copy Markdown
Member Author

On a normal build of Python and on NumPy main with both builds, you get TypeError: Preliminary-API: Cannot subclass DType..

Yes, but the analysis just bitterly wrong/omitting things. This is a Python bug and I'll open one: Python must raise an error when tp_new == NULL rather than thinking that Python instantiation is fine on this code path. NumPy cannot.

IMO this needs a release note describing the nature of the change before hitting the merge button. The note should target extension module authors and should note that dtypes can now be dynamically allocated heap types and refcounting assumptions around static lifetimes might be invalidated.

Sure, let me add a very brief note. Extension dtype authors won't notice this, IMO. It really is only user-DType authors who now indeed could get refcounting wrong when it previously may not have mattered (they actually work with the types, nobody else does).

@ngoldbaum

Copy link
Copy Markdown
Member

Thanks @seberg!

@ngoldbaum
ngoldbaum merged commit ed5ecac into numpy:main Jun 23, 2026
87 of 90 checks passed
@seberg
seberg deleted the heap-dtypes branch June 23, 2026 13:15
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.

6 participants