usearch 2.25.1 → 2.26.0
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.
- package/README.md +2 -2
- package/c/lib.cpp +28 -13
- package/cpp/test.cpp +169 -0
- package/include/usearch/index.hpp +364 -58
- package/include/usearch/index_dense.hpp +155 -50
- package/include/usearch/index_plugins.hpp +156 -79
- package/javascript/lib.cpp +4 -2
- package/numkong/CITATION.cff +1 -1
- package/numkong/Cargo.toml +1 -1
- package/numkong/bench/bench_cross_amx.cpp +12 -0
- package/numkong/bench/bench_mesh.cpp +12 -0
- package/numkong/bench/bench_spatial.cpp +0 -3
- package/numkong/binding.gyp +18 -0
- package/numkong/build.rs +1 -0
- package/numkong/c/dispatch_e5m2.c +23 -3
- package/numkong/include/numkong/capabilities.h +1 -1
- package/numkong/include/numkong/cast/README.md +3 -0
- package/numkong/include/numkong/cast/haswell.h +28 -64
- package/numkong/include/numkong/cast/serial.h +17 -0
- package/numkong/include/numkong/cast/skylake.h +67 -52
- package/numkong/include/numkong/cast.h +1 -0
- package/numkong/include/numkong/dot/README.md +1 -0
- package/numkong/include/numkong/dot/haswell.h +92 -13
- package/numkong/include/numkong/dot/serial.h +15 -0
- package/numkong/include/numkong/dot/skylake.h +61 -14
- package/numkong/include/numkong/dots/README.md +2 -0
- package/numkong/include/numkong/dots/graniteamx.h +434 -0
- package/numkong/include/numkong/dots/haswell.h +28 -28
- package/numkong/include/numkong/dots/sapphireamx.h +1 -1
- package/numkong/include/numkong/dots/serial.h +23 -8
- package/numkong/include/numkong/dots/skylake.h +28 -23
- package/numkong/include/numkong/dots.h +12 -0
- package/numkong/include/numkong/each/serial.h +18 -1
- package/numkong/include/numkong/geospatial/serial.h +14 -3
- package/numkong/include/numkong/maxsim/serial.h +15 -0
- package/numkong/include/numkong/mesh/README.md +50 -44
- package/numkong/include/numkong/mesh/genoa.h +462 -0
- package/numkong/include/numkong/mesh/haswell.h +806 -933
- package/numkong/include/numkong/mesh/neon.h +871 -943
- package/numkong/include/numkong/mesh/neonbfdot.h +382 -522
- package/numkong/include/numkong/mesh/neonfhm.h +676 -0
- package/numkong/include/numkong/mesh/rvv.h +404 -319
- package/numkong/include/numkong/mesh/serial.h +204 -162
- package/numkong/include/numkong/mesh/skylake.h +1029 -1585
- package/numkong/include/numkong/mesh/v128relaxed.h +403 -377
- package/numkong/include/numkong/mesh.h +38 -0
- package/numkong/include/numkong/reduce/serial.h +15 -1
- package/numkong/include/numkong/sparse/serial.h +17 -2
- package/numkong/include/numkong/spatial/genoa.h +0 -68
- package/numkong/include/numkong/spatial/haswell.h +98 -56
- package/numkong/include/numkong/spatial/serial.h +15 -0
- package/numkong/include/numkong/spatial/skylake.h +114 -54
- package/numkong/include/numkong/spatial.h +0 -12
- package/numkong/include/numkong/spatials/graniteamx.h +128 -0
- package/numkong/include/numkong/spatials/serial.h +18 -1
- package/numkong/include/numkong/spatials/skylake.h +2 -2
- package/numkong/include/numkong/spatials.h +17 -0
- package/numkong/include/numkong/tensor.hpp +107 -23
- package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
- package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
- package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
- package/numkong/javascript/numkong.c +3 -2
- package/numkong/package.json +7 -7
- package/numkong/test/test.cu +841 -0
- package/numkong/test/test_cross_amx.cpp +13 -0
- package/numkong/test/test_dlpack.py +492 -0
- package/numkong/test/test_mesh.cpp +12 -0
- package/numkong/test/test_spatial.cpp +0 -3
- package/numkong/test/test_tensor.cpp +87 -27
- package/package.json +2 -2
- package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
- package/prebuilds/linux-arm64/usearch.node +0 -0
- package/prebuilds/linux-x64/usearch.node +0 -0
- package/prebuilds/win32-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
|