usearch 2.25.1 → 2.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +2 -2
  2. package/c/lib.cpp +28 -13
  3. package/cpp/test.cpp +99 -0
  4. package/include/usearch/index.hpp +338 -57
  5. package/include/usearch/index_dense.hpp +155 -50
  6. package/include/usearch/index_plugins.hpp +94 -35
  7. package/javascript/lib.cpp +4 -2
  8. package/numkong/CITATION.cff +1 -1
  9. package/numkong/Cargo.toml +1 -1
  10. package/numkong/bench/bench_cross_amx.cpp +12 -0
  11. package/numkong/bench/bench_mesh.cpp +12 -0
  12. package/numkong/bench/bench_spatial.cpp +0 -3
  13. package/numkong/binding.gyp +18 -0
  14. package/numkong/build.rs +1 -0
  15. package/numkong/c/dispatch_e5m2.c +23 -3
  16. package/numkong/include/numkong/capabilities.h +1 -1
  17. package/numkong/include/numkong/cast/README.md +3 -0
  18. package/numkong/include/numkong/cast/haswell.h +28 -64
  19. package/numkong/include/numkong/cast/serial.h +17 -0
  20. package/numkong/include/numkong/cast/skylake.h +67 -52
  21. package/numkong/include/numkong/cast.h +1 -0
  22. package/numkong/include/numkong/dot/README.md +1 -0
  23. package/numkong/include/numkong/dot/haswell.h +92 -13
  24. package/numkong/include/numkong/dot/serial.h +15 -0
  25. package/numkong/include/numkong/dot/skylake.h +61 -14
  26. package/numkong/include/numkong/dots/README.md +2 -0
  27. package/numkong/include/numkong/dots/graniteamx.h +434 -0
  28. package/numkong/include/numkong/dots/haswell.h +28 -28
  29. package/numkong/include/numkong/dots/sapphireamx.h +1 -1
  30. package/numkong/include/numkong/dots/serial.h +23 -8
  31. package/numkong/include/numkong/dots/skylake.h +28 -23
  32. package/numkong/include/numkong/dots.h +12 -0
  33. package/numkong/include/numkong/each/serial.h +18 -1
  34. package/numkong/include/numkong/geospatial/serial.h +14 -3
  35. package/numkong/include/numkong/maxsim/serial.h +15 -0
  36. package/numkong/include/numkong/mesh/README.md +50 -44
  37. package/numkong/include/numkong/mesh/genoa.h +462 -0
  38. package/numkong/include/numkong/mesh/haswell.h +806 -933
  39. package/numkong/include/numkong/mesh/neon.h +871 -943
  40. package/numkong/include/numkong/mesh/neonbfdot.h +382 -522
  41. package/numkong/include/numkong/mesh/neonfhm.h +676 -0
  42. package/numkong/include/numkong/mesh/rvv.h +404 -319
  43. package/numkong/include/numkong/mesh/serial.h +204 -162
  44. package/numkong/include/numkong/mesh/skylake.h +1029 -1585
  45. package/numkong/include/numkong/mesh/v128relaxed.h +403 -377
  46. package/numkong/include/numkong/mesh.h +38 -0
  47. package/numkong/include/numkong/reduce/serial.h +15 -1
  48. package/numkong/include/numkong/sparse/serial.h +17 -2
  49. package/numkong/include/numkong/spatial/genoa.h +0 -68
  50. package/numkong/include/numkong/spatial/haswell.h +98 -56
  51. package/numkong/include/numkong/spatial/serial.h +15 -0
  52. package/numkong/include/numkong/spatial/skylake.h +114 -54
  53. package/numkong/include/numkong/spatial.h +0 -12
  54. package/numkong/include/numkong/spatials/graniteamx.h +128 -0
  55. package/numkong/include/numkong/spatials/serial.h +18 -1
  56. package/numkong/include/numkong/spatials/skylake.h +2 -2
  57. package/numkong/include/numkong/spatials.h +17 -0
  58. package/numkong/include/numkong/tensor.hpp +107 -23
  59. package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
  60. package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
  61. package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
  62. package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
  63. package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
  64. package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
  65. package/numkong/javascript/numkong.c +3 -2
  66. package/numkong/package.json +7 -7
  67. package/numkong/test/test.cu +841 -0
  68. package/numkong/test/test_cross_amx.cpp +13 -0
  69. package/numkong/test/test_dlpack.py +492 -0
  70. package/numkong/test/test_mesh.cpp +12 -0
  71. package/numkong/test/test_spatial.cpp +0 -3
  72. package/numkong/test/test_tensor.cpp +87 -27
  73. package/package.json +1 -1
  74. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  75. package/prebuilds/linux-arm64/usearch.node +0 -0
  76. package/prebuilds/linux-x64/usearch.node +0 -0
@@ -46,5 +46,18 @@ void test_cross_amx() {
46
46
  nk_dots_pack_f16_graniteamx, nk_euclideans_packed_f16_graniteamx);
47
47
  check("euclideans_symmetric_f16_graniteamx", test_euclideans_symmetric<f16_t>,
48
48
  nk_euclideans_symmetric_f16_graniteamx);
49
+
50
+ check("dots_packed_e5m2_graniteamx", test_dots_packed<e5m2_t>, nk_dots_packed_size_e5m2_graniteamx,
51
+ nk_dots_pack_e5m2_graniteamx, nk_dots_packed_e5m2_graniteamx);
52
+ check("dots_symmetric_e5m2_graniteamx", test_dots_symmetric<e5m2_t>, nk_dots_symmetric_e5m2_graniteamx);
53
+
54
+ check("angulars_packed_e5m2_graniteamx", test_angulars_packed<e5m2_t>, nk_dots_packed_size_e5m2_graniteamx,
55
+ nk_dots_pack_e5m2_graniteamx, nk_angulars_packed_e5m2_graniteamx);
56
+ check("angulars_symmetric_e5m2_graniteamx", test_angulars_symmetric<e5m2_t>, nk_angulars_symmetric_e5m2_graniteamx);
57
+
58
+ check("euclideans_packed_e5m2_graniteamx", test_euclideans_packed<e5m2_t>, nk_dots_packed_size_e5m2_graniteamx,
59
+ nk_dots_pack_e5m2_graniteamx, nk_euclideans_packed_e5m2_graniteamx);
60
+ check("euclideans_symmetric_e5m2_graniteamx", test_euclideans_symmetric<e5m2_t>,
61
+ nk_euclideans_symmetric_e5m2_graniteamx);
49
62
  #endif
50
63
  }
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env python3
2
+ """DLPack zero-copy interop tests for NumKong.
3
+
4
+ Covers the Python Array API DLPack protocol implemented in ``python/dlpack_interop.{c,h}`` and
5
+ ``python/dlpack_abi.h``. Tests are organized into per-framework sections; every test gates its dependencies
6
+ through ``pytest.importorskip`` so the suite runs against any subset of installed frameworks (NumPy itself is
7
+ treated as optional — only the protocol-contract tests at the bottom of this file are framework-free, and they
8
+ build their tensors with NumKong's own constructors).
9
+
10
+ Frameworks exercised:
11
+
12
+ - NumPy — CPU round-trips for f32 / f64 / f16 / i* / u*.
13
+ - PyTorch — CPU round-trips, plus bf16, float8_e4m3fn, float8_e5m2 dtype-fidelity tests.
14
+ - JAX — CPU round-trips for f32 / f64 / bf16.
15
+ - TensorFlow — CPU round-trips for f32 / f64 / bf16 via ``tf.experimental.dlpack``.
16
+ - CuPy — three guarded paths covering the importer's device-acceptance contract:
17
+ (a) plain ``cudaMalloc`` (kDLCUDA) → must be REJECTED with a clear ValueError;
18
+ (b) ``cudaMallocManaged`` unified memory (kDLCUDAManaged) → must be ACCEPTED;
19
+ (c) ``cudaMallocHost`` pinned host memory (kDLCUDAHost) → must be ACCEPTED.
20
+ - PyArrow — Arrow Array → NumKong (one-way; PyArrow is producer-only in the protocol).
21
+ - ONNX Runtime — ``OrtValue`` ↔ NumKong via the ``to_dlpack`` / ``ortvalue_from_numpy`` helpers.
22
+ - MLX — Apple Silicon's unified-memory framework. Tests skip on Linux (no ``mlx.core`` wheel) and
23
+ run on macOS contributor machines / Apple Silicon CI.
24
+
25
+ The importer accepts every DLPack ``device_type`` whose pointer is dereferenceable from host code
26
+ (kDLCPU / CUDAHost / ROCMHost / CUDAManaged / OneAPI / Metal). Pure device memory (kDLCUDA, kDLROCM,
27
+ kDLOpenCL, kDLVulkan, kDLWebGPU, kDLHexagon, kDLMAIA, kDLTrn, kDLVPI, kDLExtDev) is rejected.
28
+ """
29
+
30
+ import pytest
31
+
32
+ import numkong as nk
33
+
34
+
35
+ # region Helpers
36
+
37
+
38
+ _INTEGER_DTYPES = ["int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64"]
39
+ _PLAIN_FLOAT_DTYPES = ["float16", "float32", "float64"]
40
+ _PLAIN_NUMERIC_DTYPES = _PLAIN_FLOAT_DTYPES + _INTEGER_DTYPES
41
+
42
+ # Per-framework dtype matrices. Each entry is a `(framework_attr_name, nk_dtype_string)` pair so a single
43
+ # parametrized test can drive both directions: `getattr(framework_module, attr)` resolves the producer-side
44
+ # dtype, and the `nk_dtype_string` is what we expect back on the NumKong-side after import.
45
+ #
46
+ # We include the "weird GPU formats" (bf16, fp8 e4m3fn, fp8 e5m2, fp6 e2m3, fp6 e3m2) wherever the framework
47
+ # supports them natively, so each framework's coverage matches what dtype-fidelity DLPack actually buys.
48
+
49
+ # NumPy core has no bf16/fp8/fp6 — those would need ml_dtypes. Skip exotics here; they're tested via PyTorch/MLX.
50
+ _NUMPY_DTYPES = [(name, name) for name in _PLAIN_NUMERIC_DTYPES]
51
+
52
+ # PyTorch ships native bf16 + float8_e4m3fn + float8_e5m2 since 2.1+; full coverage here is the marquee case.
53
+ _TORCH_DTYPES = (
54
+ [(name, name) for name in _PLAIN_NUMERIC_DTYPES]
55
+ + [("bfloat16", "bfloat16"), ("float8_e4m3fn", "e4m3"), ("float8_e5m2", "e5m2")]
56
+ )
57
+
58
+ # JAX has native bf16 (always) and the integer / float subset; no fp8/fp6 yet.
59
+ _JAX_DTYPES = [
60
+ ("float16", "float16"), ("float32", "float32"), ("float64", "float64"),
61
+ ("int8", "int8"), ("int16", "int16"), ("int32", "int32"), ("int64", "int64"),
62
+ ("uint8", "uint8"), ("uint16", "uint16"), ("uint32", "uint32"), ("uint64", "uint64"),
63
+ ("bfloat16", "bfloat16"),
64
+ ]
65
+
66
+ # TensorFlow: native bf16 + float16/32/64 + integers. No public fp8 dtype that maps cleanly via DLPack today.
67
+ _TF_DTYPES = [
68
+ ("float16", "float16"), ("float32", "float32"), ("float64", "float64"),
69
+ ("int8", "int8"), ("int16", "int16"), ("int32", "int32"), ("int64", "int64"),
70
+ ("uint8", "uint8"), ("uint16", "uint16"), ("uint32", "uint32"), ("uint64", "uint64"),
71
+ ("bfloat16", "bfloat16"),
72
+ ]
73
+
74
+ # PyArrow exposes float / signed-int / unsigned-int builders by name. No bf16/fp8/fp6.
75
+ _PYARROW_DTYPES = [
76
+ ("float16", "float16"), ("float32", "float32"), ("float64", "float64"),
77
+ ("int8", "int8"), ("int16", "int16"), ("int32", "int32"), ("int64", "int64"),
78
+ ("uint8", "uint8"), ("uint16", "uint16"), ("uint32", "uint32"), ("uint64", "uint64"),
79
+ ]
80
+
81
+ # MLX ships native bf16 on Apple Silicon, plus float16/32/integers. No fp8 yet (mlx.core 0.x).
82
+ _MLX_DTYPES = [
83
+ ("float16", "float16"), ("float32", "float32"),
84
+ ("int8", "int8"), ("int16", "int16"), ("int32", "int32"), ("int64", "int64"),
85
+ ("uint8", "uint8"), ("uint16", "uint16"), ("uint32", "uint32"), ("uint64", "uint64"),
86
+ ("bfloat16", "bfloat16"),
87
+ ]
88
+
89
+
90
+ def _assert_round_trip_through(producer_array, expected_dtype_str: str, expected_shape: tuple) -> None:
91
+ """Common pattern: framework-owned array → NumKong view → assertions on dtype/shape."""
92
+ imported = nk.from_dlpack(producer_array)
93
+ assert imported.shape == expected_shape, f"{imported.shape} != {expected_shape}"
94
+ assert imported.dtype == expected_dtype_str, f"{imported.dtype} != {expected_dtype_str}"
95
+
96
+
97
+ # endregion Helpers
98
+
99
+
100
+ # region NumPy round-trip
101
+
102
+
103
+ @pytest.mark.parametrize("np_dtype, nk_dtype", _NUMPY_DTYPES)
104
+ def test_numpy_export(np_dtype, nk_dtype):
105
+ """A NumKong tensor must be consumable zero-copy by `np.from_dlpack`."""
106
+ np = pytest.importorskip("numpy")
107
+ source = np.arange(24, dtype=np_dtype).reshape(4, 6)
108
+ tensor = nk.Tensor(source)
109
+ arr = np.from_dlpack(tensor)
110
+ assert arr.shape == (4, 6)
111
+ assert str(arr.dtype) == np_dtype
112
+ assert tensor.dtype == nk_dtype
113
+ np.testing.assert_array_equal(arr, source)
114
+ arr[0, 0] = 99
115
+ assert np.asarray(tensor)[0, 0] == 99, "mutation through NumPy view not visible in NumKong tensor"
116
+
117
+
118
+ @pytest.mark.parametrize("np_dtype, nk_dtype", _NUMPY_DTYPES)
119
+ def test_numpy_import(np_dtype, nk_dtype):
120
+ """A NumPy array must be consumable zero-copy by `nk.from_dlpack`."""
121
+ np = pytest.importorskip("numpy")
122
+ source = np.arange(20, dtype=np_dtype)
123
+ tensor = nk.from_dlpack(source)
124
+ assert tensor.shape == (20,)
125
+ assert tensor.dtype == nk_dtype
126
+ np.testing.assert_array_equal(np.asarray(tensor), source)
127
+
128
+
129
+ def test_numpy_import_strided():
130
+ """A non-contiguous (transposed) NumPy source preserves strides on import."""
131
+ np = pytest.importorskip("numpy")
132
+ source = np.arange(24, dtype=np.float32).reshape(4, 6)
133
+ tensor = nk.from_dlpack(source.T)
134
+ assert tensor.shape == (6, 4)
135
+ np.testing.assert_array_equal(np.asarray(tensor), source.T)
136
+
137
+
138
+ # endregion NumPy round-trip
139
+
140
+
141
+ # region PyTorch round-trip
142
+
143
+
144
+ @pytest.mark.parametrize("torch_dtype_attr, nk_dtype", _TORCH_DTYPES)
145
+ def test_torch_import(torch_dtype_attr, nk_dtype):
146
+ """Every torch-supported dtype (including bf16, fp8 e4m3fn, fp8 e5m2) imports zero-copy as NumKong dtype."""
147
+ torch = pytest.importorskip("torch")
148
+ torch_dtype = getattr(torch, torch_dtype_attr)
149
+ # `torch.zeros` works for fp8 (where `torch.arange` is unsupported); the test only checks dtype + round-trip.
150
+ src = torch.zeros(4, 6, dtype=torch_dtype)
151
+ nk_tensor = nk.from_dlpack(src)
152
+ assert nk_tensor.shape == (4, 6)
153
+ assert nk_tensor.dtype == nk_dtype
154
+ pt_back = torch.from_dlpack(nk_tensor)
155
+ assert pt_back.dtype == torch_dtype
156
+ assert tuple(pt_back.shape) == (4, 6)
157
+
158
+
159
+ @pytest.mark.parametrize("torch_dtype_attr, nk_dtype", _TORCH_DTYPES)
160
+ def test_torch_export(torch_dtype_attr, nk_dtype):
161
+ """Every torch-supported dtype is consumable by `torch.from_dlpack` after a NumKong round-trip."""
162
+ torch = pytest.importorskip("torch")
163
+ torch_dtype = getattr(torch, torch_dtype_attr)
164
+ # Build the source on the torch side (avoids needing ml_dtypes for fp8/bf16 numpy paths).
165
+ src = torch.zeros(4, 6, dtype=torch_dtype)
166
+ nk_tensor = nk.from_dlpack(src) # NumKong view sharing torch's buffer
167
+ pt_view = torch.from_dlpack(nk_tensor) # back to torch via NumKong's exporter
168
+ assert pt_view.dtype == torch_dtype
169
+ assert tuple(pt_view.shape) == (4, 6)
170
+
171
+
172
+ def test_torch_export_mutation_visibility():
173
+ """Writes through one view are visible through the other (proves zero-copy, not a value comparison)."""
174
+ np = pytest.importorskip("numpy")
175
+ torch = pytest.importorskip("torch")
176
+ source = np.arange(24, dtype=np.float32).reshape(4, 6)
177
+ tensor = nk.Tensor(source)
178
+ pt = torch.from_dlpack(tensor)
179
+ pt[0, 0] = 99
180
+ assert np.asarray(tensor)[0, 0] == 99, "mutation through PyTorch view not visible in NumKong tensor"
181
+
182
+
183
+ def test_torch_strided_import():
184
+ """A transposed PyTorch tensor preserves strides when imported."""
185
+ np = pytest.importorskip("numpy")
186
+ torch = pytest.importorskip("torch")
187
+ src = torch.arange(24, dtype=torch.float32).reshape(4, 6).T
188
+ nk_tensor = nk.from_dlpack(src)
189
+ assert nk_tensor.shape == (6, 4)
190
+ np.testing.assert_array_equal(np.asarray(nk_tensor), src.numpy())
191
+
192
+
193
+ # endregion PyTorch round-trip
194
+
195
+
196
+ # region JAX round-trip
197
+
198
+
199
+ @pytest.mark.parametrize("jax_dtype_attr, nk_dtype", _JAX_DTYPES)
200
+ def test_jax_export(jax_dtype_attr, nk_dtype):
201
+ """NumKong → JAX zero-copy across every JAX-supported dtype incl. bf16."""
202
+ np = pytest.importorskip("numpy")
203
+ jax = pytest.importorskip("jax")
204
+ # JAX disables float64 by default and silently downcasts to float32 unless x64 is enabled.
205
+ if jax_dtype_attr in ("float64", "int64", "uint64"):
206
+ jax.config.update("jax_enable_x64", True)
207
+ jnp = jax.numpy
208
+ np_dtype = jax_dtype_attr if jax_dtype_attr != "bfloat16" else "float32"
209
+ source = np.arange(20, dtype=np_dtype)
210
+ tensor = nk.Tensor(source) if jax_dtype_attr != "bfloat16" else nk.Tensor(source).astype("bfloat16")
211
+ jax_array = jnp.from_dlpack(tensor)
212
+ assert jax_array.shape == (20,)
213
+ assert str(jax_array.dtype) == jax_dtype_attr
214
+ assert tensor.dtype == nk_dtype
215
+
216
+
217
+ @pytest.mark.parametrize("jax_dtype_attr, nk_dtype", _JAX_DTYPES)
218
+ def test_jax_import(jax_dtype_attr, nk_dtype):
219
+ """JAX → NumKong zero-copy across every JAX-supported dtype incl. bf16."""
220
+ jax = pytest.importorskip("jax")
221
+ if jax_dtype_attr in ("float64", "int64", "uint64"):
222
+ jax.config.update("jax_enable_x64", True)
223
+ jnp = jax.numpy
224
+ jax_dtype = getattr(jnp, jax_dtype_attr)
225
+ # `jnp.zeros` works for every supported dtype incl. bf16 / int / uint.
226
+ src = jnp.zeros(20, dtype=jax_dtype)
227
+ nk_tensor = nk.from_dlpack(src)
228
+ assert nk_tensor.shape == (20,)
229
+ assert nk_tensor.dtype == nk_dtype
230
+
231
+
232
+ # endregion JAX round-trip
233
+
234
+
235
+ # region TensorFlow round-trip
236
+
237
+
238
+ @pytest.mark.parametrize("tf_dtype_attr, nk_dtype", _TF_DTYPES)
239
+ def test_tensorflow_export(tf_dtype_attr, nk_dtype):
240
+ """NumKong → TensorFlow zero-copy across every TF-supported dtype incl. bf16."""
241
+ np = pytest.importorskip("numpy")
242
+ tf = pytest.importorskip("tensorflow")
243
+ np_dtype = tf_dtype_attr if tf_dtype_attr != "bfloat16" else "float32"
244
+ source = np.arange(20, dtype=np_dtype)
245
+ tensor = nk.Tensor(source) if tf_dtype_attr != "bfloat16" else nk.Tensor(source).astype("bfloat16")
246
+ assert tensor.dtype == nk_dtype
247
+ tf_tensor = tf.experimental.dlpack.from_dlpack(tensor.__dlpack__())
248
+ assert tuple(tf_tensor.shape) == (20,)
249
+ assert tf_tensor.dtype == getattr(tf, tf_dtype_attr)
250
+
251
+
252
+ @pytest.mark.parametrize("tf_dtype_attr, nk_dtype", _TF_DTYPES)
253
+ def test_tensorflow_import(tf_dtype_attr, nk_dtype):
254
+ """TensorFlow → NumKong zero-copy across every TF-supported dtype incl. bf16."""
255
+ np = pytest.importorskip("numpy")
256
+ tf = pytest.importorskip("tensorflow")
257
+ np_dtype = tf_dtype_attr if tf_dtype_attr != "bfloat16" else "float32"
258
+ src_np = np.arange(20, dtype=np_dtype)
259
+ tf_tensor = tf.constant(src_np)
260
+ if tf_dtype_attr == "bfloat16":
261
+ tf_tensor = tf.cast(tf_tensor, tf.bfloat16)
262
+ capsule = tf.experimental.dlpack.to_dlpack(tf_tensor)
263
+ nk_tensor = nk.from_dlpack(capsule)
264
+ assert nk_tensor.shape == (20,)
265
+ assert nk_tensor.dtype == nk_dtype
266
+
267
+
268
+ # endregion TensorFlow round-trip
269
+
270
+
271
+ # region CuPy round-trip (GPU only — skipped on CPU-only venvs)
272
+
273
+
274
+ def _cupy_or_skip():
275
+ """Common CuPy + GPU + allocate guard. Returns the cupy module on success."""
276
+ cupy = pytest.importorskip("cupy")
277
+ try:
278
+ device_count = cupy.cuda.runtime.getDeviceCount()
279
+ except Exception as exc:
280
+ pytest.skip(f"CuPy installed but no CUDA runtime visible ({exc})")
281
+ if device_count == 0:
282
+ pytest.skip("CuPy installed but no CUDA devices visible")
283
+ # Smoke-test that CuPy can actually JIT a kernel on this GPU. cupy-cuda12x wheels are built for
284
+ # specific compute capabilities and CUDA runtimes; mismatches raise CUDA_ERROR_INVALID_IMAGE.
285
+ try:
286
+ cupy.arange(4, dtype=cupy.float32) + 1
287
+ except Exception as exc:
288
+ pytest.skip(f"CuPy/GPU mismatch: cannot allocate ({exc})")
289
+ return cupy
290
+
291
+
292
+ def test_cupy_device_memory_is_rejected():
293
+ """Plain ``cudaMalloc`` GPU memory (kDLCUDA) must be REJECTED — reading it on the host would fault."""
294
+ cupy = _cupy_or_skip()
295
+ src = cupy.arange(16, dtype=cupy.float32)
296
+ with pytest.raises(ValueError, match="CPU-accessible"):
297
+ nk.from_dlpack(src)
298
+
299
+
300
+ def test_cupy_managed_memory_accepted():
301
+ """``cudaMallocManaged`` unified memory (kDLCUDAManaged) is host-readable and must be ACCEPTED."""
302
+ cupy = _cupy_or_skip()
303
+ previous_allocator = cupy.cuda.get_allocator()
304
+ cupy.cuda.set_allocator(cupy.cuda.malloc_managed)
305
+ try:
306
+ src = cupy.arange(16, dtype=cupy.float32)
307
+ nk_tensor = nk.from_dlpack(src)
308
+ assert nk_tensor.shape == (16,)
309
+ assert nk_tensor.dtype == "float32"
310
+ finally:
311
+ cupy.cuda.set_allocator(previous_allocator)
312
+
313
+
314
+ def test_cupy_pinned_host_memory_accepted():
315
+ """``cudaMallocHost`` pinned host memory (kDLCUDAHost) is just CPU memory and must be ACCEPTED."""
316
+ cupy = _cupy_or_skip()
317
+ # CuPy doesn't expose pinned memory as a regular ndarray that implements __dlpack__. The closest
318
+ # path is alloc_pinned_memory + a numpy view. If the producer-side wrapper isn't available,
319
+ # skip — the C-side acceptance still lands.
320
+ try:
321
+ nbytes = 16 * 4
322
+ pinned = cupy.cuda.alloc_pinned_memory(nbytes)
323
+ np = pytest.importorskip("numpy")
324
+ host_view = np.frombuffer(pinned, dtype=np.float32, count=16)
325
+ host_view[:] = np.arange(16, dtype=np.float32)
326
+ # This DLPack capsule will report kDLCPU (numpy's view), not kDLCUDAHost. The test still
327
+ # serves as a sanity check that pinned memory exchanges work; an explicit kDLCUDAHost test
328
+ # requires a producer that surfaces the device code, which CuPy currently does not.
329
+ nk_tensor = nk.from_dlpack(host_view)
330
+ assert nk_tensor.shape == (16,)
331
+ assert float(np.asarray(nk_tensor).sum()) == 16 * 15 / 2
332
+ except (AttributeError, RuntimeError) as exc:
333
+ pytest.skip(f"CuPy pinned-memory DLPack producer unavailable ({exc})")
334
+
335
+
336
+ # endregion CuPy round-trip
337
+
338
+
339
+ # region MLX round-trip (Apple Silicon — unified memory)
340
+
341
+
342
+ @pytest.mark.parametrize("mlx_dtype_attr, nk_dtype", _MLX_DTYPES)
343
+ def test_mlx_import(mlx_dtype_attr, nk_dtype):
344
+ """MLX → NumKong zero-copy across every MLX-supported dtype incl. bf16."""
345
+ mx = pytest.importorskip("mlx.core")
346
+ mlx_dtype = getattr(mx, mlx_dtype_attr)
347
+ src = mx.zeros((4, 5), dtype=mlx_dtype)
348
+ nk_tensor = nk.from_dlpack(src)
349
+ assert nk_tensor.shape == (4, 5)
350
+ assert nk_tensor.dtype == nk_dtype
351
+
352
+
353
+ @pytest.mark.parametrize("mlx_dtype_attr, nk_dtype", _MLX_DTYPES)
354
+ def test_mlx_export(mlx_dtype_attr, nk_dtype):
355
+ """NumKong → MLX zero-copy across every MLX-supported dtype incl. bf16."""
356
+ mx = pytest.importorskip("mlx.core")
357
+ if nk_dtype == "bfloat16":
358
+ # bf16 isn't constructible via nk.zeros directly across all paths; stage via float32 + astype.
359
+ src = nk.zeros(20, dtype="float32").astype("bfloat16")
360
+ else:
361
+ src = nk.zeros(20, dtype=nk_dtype)
362
+ mx_array = mx.from_dlpack(src)
363
+ assert mx_array.shape == (20,)
364
+ assert mx_array.dtype == getattr(mx, mlx_dtype_attr)
365
+
366
+
367
+ # endregion MLX round-trip
368
+
369
+
370
+ # region PyArrow → NumKong (PyArrow is producer-only in the protocol)
371
+
372
+
373
+ @pytest.mark.parametrize("pa_dtype_attr, nk_dtype", _PYARROW_DTYPES)
374
+ def test_pyarrow_import(pa_dtype_attr, nk_dtype):
375
+ """PyArrow Array → NumKong via the PyArrow `__dlpack__` producer side."""
376
+ np = pytest.importorskip("numpy")
377
+ pa = pytest.importorskip("pyarrow")
378
+ pa_type = getattr(pa, pa_dtype_attr)()
379
+ if not hasattr(pa.array([1], type=pa_type), "__dlpack__"):
380
+ pytest.skip("PyArrow build lacks DLPack support (pre-15.0.0)")
381
+ arrow_array = pa.array(np.arange(20, dtype=pa_dtype_attr))
382
+ nk_tensor = nk.from_dlpack(arrow_array)
383
+ assert nk_tensor.shape == (20,)
384
+ assert nk_tensor.dtype == nk_dtype
385
+ np.testing.assert_array_equal(np.asarray(nk_tensor), np.arange(20, dtype=pa_dtype_attr))
386
+
387
+
388
+ # endregion PyArrow
389
+
390
+
391
+ # region ONNX Runtime round-trip
392
+
393
+
394
+ def test_onnxruntime_roundtrip():
395
+ """`OrtValue` ↔ NumKong via DLPack. ORT is a producer/consumer in inference builds."""
396
+ np = pytest.importorskip("numpy")
397
+ ort = pytest.importorskip("onnxruntime")
398
+ src_np = np.arange(40, dtype=np.float32).reshape(5, 8)
399
+ ort_value = ort.OrtValue.ortvalue_from_numpy(src_np)
400
+ if not hasattr(ort_value, "to_dlpack"):
401
+ pytest.skip("onnxruntime build lacks DLPack helpers (microsoft/onnxruntime#23110 not present)")
402
+ nk_tensor = nk.from_dlpack(ort_value.to_dlpack())
403
+ assert nk_tensor.shape == (5, 8)
404
+ np.testing.assert_array_equal(np.asarray(nk_tensor), src_np)
405
+
406
+
407
+ # endregion ONNX Runtime
408
+
409
+
410
+ # region Protocol contract (no framework dependency — uses only NumKong's own constructors)
411
+
412
+
413
+ def test_dlpack_device_is_cpu():
414
+ """`__dlpack_device__()` always returns `(kDLCPU=1, 0)`."""
415
+ tensor = nk.zeros(4, dtype="float32")
416
+ assert tensor.__dlpack_device__() == (1, 0)
417
+
418
+
419
+ def test_capsule_name_legacy_default():
420
+ """The default `__dlpack__()` returns a legacy `dltensor` capsule."""
421
+ tensor = nk.zeros(4, dtype="float32")
422
+ capsule = tensor.__dlpack__()
423
+ assert '"dltensor"' in repr(capsule), repr(capsule)
424
+
425
+
426
+ def test_capsule_name_versioned_when_requested():
427
+ """`max_version=(1, 0)` opts into the versioned capsule (`dltensor_versioned`)."""
428
+ tensor = nk.zeros(4, dtype="float32")
429
+ capsule = tensor.__dlpack__(max_version=(1, 0))
430
+ assert '"dltensor_versioned"' in repr(capsule), repr(capsule)
431
+
432
+
433
+ def test_export_rejects_non_cpu_device():
434
+ """Asking the exporter for a non-CPU `dl_device` raises immediately."""
435
+ tensor = nk.zeros(4, dtype="float32")
436
+ with pytest.raises((BufferError, TypeError, ValueError)):
437
+ tensor.__dlpack__(dl_device=(2, 0)) # kDLCUDA = 2
438
+
439
+
440
+ def test_export_rejects_copy_true():
441
+ """`copy=True` raises — we are zero-copy by construction."""
442
+ tensor = nk.zeros(4, dtype="float32")
443
+ with pytest.raises((BufferError, ValueError)):
444
+ tensor.__dlpack__(copy=True)
445
+
446
+
447
+ @pytest.mark.parametrize("dtype_str", ["e2m3", "e3m2"])
448
+ def test_fp6_uses_versioned_with_padded_flag(dtype_str):
449
+ """FP6 (e2m3 / e3m2) cannot be expressed in legacy v0; auto-upgrades to v1."""
450
+ fp6_tensor = nk.zeros((4, 4), dtype=dtype_str)
451
+ capsule = fp6_tensor.__dlpack__() # no max_version — still produced versioned for FP6
452
+ assert '"dltensor_versioned"' in repr(capsule), f"{dtype_str}: {capsule!r}"
453
+
454
+
455
+ @pytest.mark.parametrize("dtype_str", ["e2m3", "e3m2"])
456
+ def test_fp6_roundtrip_preserves_dtype(dtype_str):
457
+ """FP6 export + import preserves dtype and the underlying byte length."""
458
+ source = nk.zeros((3, 8), dtype=dtype_str)
459
+ imported = nk.from_dlpack(source)
460
+ assert imported.dtype == dtype_str
461
+ assert imported.shape == (3, 8)
462
+ assert imported.nbytes == source.nbytes
463
+
464
+
465
+ def test_import_rejects_object_without_dlpack_method():
466
+ """A bare object without `__dlpack__` raises TypeError, not a segfault."""
467
+ with pytest.raises(TypeError):
468
+ nk.from_dlpack(object())
469
+
470
+
471
+ def test_ownership_outlives_source_tensor():
472
+ """The capsule pins the source memory after the producing Tensor goes out of scope."""
473
+ tensor = nk.iota(100, dtype="float32")
474
+ expected_data_ptr = tensor.data_ptr
475
+ capsule = tensor.__dlpack__()
476
+ del tensor # source tensor goes out of scope; capsule still holds a refcount
477
+ imported = nk.from_dlpack(capsule)
478
+ assert imported.shape == (100,)
479
+ # If the producer's memory had been freed, this read would be a use-after-free; passing means the capsule
480
+ # kept it alive.
481
+ assert imported.data_ptr == expected_data_ptr
482
+
483
+
484
+ def test_capsule_renamed_to_used_after_import():
485
+ """After consumption the capsule is renamed to `used_dltensor*` per spec."""
486
+ tensor = nk.iota(8, dtype="float32")
487
+ capsule = tensor.__dlpack__()
488
+ nk.from_dlpack(capsule)
489
+ assert '"used_dltensor"' in repr(capsule), repr(capsule)
490
+
491
+
492
+ # endregion Protocol contract
@@ -137,6 +137,12 @@ void test_mesh() {
137
137
  check("umeyama_bf16_neonbfdot", test_umeyama<bf16_t>, nk_umeyama_bf16_neonbfdot);
138
138
  #endif // NK_TARGET_NEONBFDOT
139
139
 
140
+ #if NK_TARGET_NEONFHM
141
+ check("rmsd_f16_neonfhm", test_rmsd<f16_t>, nk_rmsd_f16_neonfhm);
142
+ check("kabsch_f16_neonfhm", test_kabsch<f16_t>, nk_kabsch_f16_neonfhm);
143
+ check("umeyama_f16_neonfhm", test_umeyama<f16_t>, nk_umeyama_f16_neonfhm);
144
+ #endif // NK_TARGET_NEONFHM
145
+
140
146
  #if NK_TARGET_HASWELL
141
147
  check("rmsd_f64_haswell", test_rmsd<f64_t>, nk_rmsd_f64_haswell);
142
148
  check("rmsd_f32_haswell", test_rmsd<f32_t>, nk_rmsd_f32_haswell);
@@ -167,6 +173,12 @@ void test_mesh() {
167
173
  check("umeyama_bf16_skylake", test_umeyama<bf16_t>, nk_umeyama_bf16_skylake);
168
174
  #endif // NK_TARGET_SKYLAKE
169
175
 
176
+ #if NK_TARGET_GENOA
177
+ check("rmsd_bf16_genoa", test_rmsd<bf16_t>, nk_rmsd_bf16_genoa);
178
+ check("kabsch_bf16_genoa", test_kabsch<bf16_t>, nk_kabsch_bf16_genoa);
179
+ check("umeyama_bf16_genoa", test_umeyama<bf16_t>, nk_umeyama_bf16_genoa);
180
+ #endif // NK_TARGET_GENOA
181
+
170
182
  #if NK_TARGET_RVV
171
183
  check("rmsd_f64_rvv", test_rmsd<f64_t>, nk_rmsd_f64_rvv);
172
184
  check("rmsd_f32_rvv", test_rmsd<f32_t>, nk_rmsd_f32_rvv);
@@ -312,9 +312,6 @@ void test_spatial() {
312
312
  check("angular_bf16_genoa", test_angular<bf16_t>, nk_angular_bf16_genoa);
313
313
  check("sqeuclidean_bf16_genoa", test_sqeuclidean<bf16_t>, nk_sqeuclidean_bf16_genoa);
314
314
  check("euclidean_bf16_genoa", test_euclidean<bf16_t>, nk_euclidean_bf16_genoa);
315
- check("angular_e5m2_genoa", test_angular<e5m2_t>, nk_angular_e5m2_genoa);
316
- check("sqeuclidean_e5m2_genoa", test_sqeuclidean<e5m2_t>, nk_sqeuclidean_e5m2_genoa);
317
- check("euclidean_e5m2_genoa", test_euclidean<e5m2_t>, nk_euclidean_e5m2_genoa);
318
315
  #endif // NK_TARGET_GENOA
319
316
 
320
317
  #if NK_TARGET_DIAMOND