usearch 2.26.1 → 2.26.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 (146) hide show
  1. package/README.md +1 -1
  2. package/include/usearch/index.hpp +5 -1
  3. package/numkong/CITATION.cff +13 -9
  4. package/numkong/CONTRIBUTING.md +29 -24
  5. package/numkong/Cargo.toml +1 -1
  6. package/numkong/README.md +21 -7
  7. package/numkong/binding.gyp +47 -31
  8. package/numkong/build.rs +15 -5
  9. package/numkong/c/dispatch.h +0 -1
  10. package/numkong/c/parallel.c +139 -0
  11. package/numkong/c/parallel.h +47 -0
  12. package/numkong/cmake/toolchain-wasi.cmake +39 -5
  13. package/numkong/include/README.md +24 -17
  14. package/numkong/include/numkong/attention/sapphireamx.h +6 -6
  15. package/numkong/include/numkong/attention/sme.h +2 -2
  16. package/numkong/include/numkong/capabilities.h +2 -2
  17. package/numkong/include/numkong/cast/README.md +9 -7
  18. package/numkong/include/numkong/cast/loongsonasx.h +48 -40
  19. package/numkong/include/numkong/cast/serial.h +89 -11
  20. package/numkong/include/numkong/cast.h +3 -3
  21. package/numkong/include/numkong/curved/README.md +10 -10
  22. package/numkong/include/numkong/curved/neon.h +1 -0
  23. package/numkong/include/numkong/curved/neonbfdot.h +1 -0
  24. package/numkong/include/numkong/curved/smef64.h +1 -0
  25. package/numkong/include/numkong/dot/README.md +18 -18
  26. package/numkong/include/numkong/dot/alder.h +8 -8
  27. package/numkong/include/numkong/dot/haswell.h +8 -8
  28. package/numkong/include/numkong/dot/icelake.h +12 -12
  29. package/numkong/include/numkong/dot/loongsonasx.h +6 -6
  30. package/numkong/include/numkong/dot/neon.h +2 -1
  31. package/numkong/include/numkong/dot/neonbfdot.h +1 -0
  32. package/numkong/include/numkong/dot/neonfhm.h +1 -0
  33. package/numkong/include/numkong/dot/neonsdot.h +1 -0
  34. package/numkong/include/numkong/dot/powervsx.h +3 -3
  35. package/numkong/include/numkong/dot/serial.h +19 -6
  36. package/numkong/include/numkong/dot/skylake.h +4 -4
  37. package/numkong/include/numkong/dot/svehalf.h +1 -1
  38. package/numkong/include/numkong/dot/v128relaxed.h +24 -23
  39. package/numkong/include/numkong/dots/README.md +26 -26
  40. package/numkong/include/numkong/dots/genoa.h +3 -1
  41. package/numkong/include/numkong/dots/icelake.h +1 -0
  42. package/numkong/include/numkong/dots/powervsx.h +1 -1
  43. package/numkong/include/numkong/dots/serial.h +32 -33
  44. package/numkong/include/numkong/dots/sierra.h +4 -3
  45. package/numkong/include/numkong/each/README.md +5 -3
  46. package/numkong/include/numkong/each/sapphire.h +2 -0
  47. package/numkong/include/numkong/each/skylake.h +1 -0
  48. package/numkong/include/numkong/geospatial/README.md +6 -6
  49. package/numkong/include/numkong/geospatial/v128relaxed.h +1 -0
  50. package/numkong/include/numkong/maxsim/README.md +5 -5
  51. package/numkong/include/numkong/maxsim/genoa.h +4 -0
  52. package/numkong/include/numkong/maxsim/icelake.h +1 -0
  53. package/numkong/include/numkong/maxsim/serial.h +22 -9
  54. package/numkong/include/numkong/maxsim/sme.h +3 -2
  55. package/numkong/include/numkong/mesh/README.md +7 -4
  56. package/numkong/include/numkong/mesh/genoa.h +2 -2
  57. package/numkong/include/numkong/mesh/haswell.h +8 -8
  58. package/numkong/include/numkong/mesh/neon.h +6 -6
  59. package/numkong/include/numkong/mesh/neonbfdot.h +3 -2
  60. package/numkong/include/numkong/mesh/neonfhm.h +3 -2
  61. package/numkong/include/numkong/mesh/rvv.h +4 -4
  62. package/numkong/include/numkong/mesh/serial.h +17 -17
  63. package/numkong/include/numkong/mesh/skylake.h +4 -4
  64. package/numkong/include/numkong/mesh/v128relaxed.h +4 -4
  65. package/numkong/include/numkong/mesh.hpp +2 -2
  66. package/numkong/include/numkong/numkong.h +1 -1
  67. package/numkong/include/numkong/numkong.hpp +1 -1
  68. package/numkong/include/numkong/probability/README.md +17 -15
  69. package/numkong/include/numkong/probability/skylake.h +1 -1
  70. package/numkong/include/numkong/reduce/README.md +27 -21
  71. package/numkong/include/numkong/reduce/genoa.h +2 -1
  72. package/numkong/include/numkong/reduce/icelake.h +1 -0
  73. package/numkong/include/numkong/reduce/neon.h +1 -1
  74. package/numkong/include/numkong/reduce/serial.h +9 -9
  75. package/numkong/include/numkong/reduce/skylake.h +4 -4
  76. package/numkong/include/numkong/reduce.hpp +38 -0
  77. package/numkong/include/numkong/scalar/README.md +29 -28
  78. package/numkong/include/numkong/set/README.md +17 -14
  79. package/numkong/include/numkong/set/haswell.h +15 -16
  80. package/numkong/include/numkong/set/icelake.h +15 -16
  81. package/numkong/include/numkong/set/loongsonasx.h +12 -10
  82. package/numkong/include/numkong/set/neon.h +15 -19
  83. package/numkong/include/numkong/set/powervsx.h +15 -21
  84. package/numkong/include/numkong/set/serial.h +19 -20
  85. package/numkong/include/numkong/set/v128relaxed.h +10 -8
  86. package/numkong/include/numkong/sets/icelake.h +1 -0
  87. package/numkong/include/numkong/sparse/README.md +4 -4
  88. package/numkong/include/numkong/sparse/icelake.h +1 -0
  89. package/numkong/include/numkong/sparse/neon.h +1 -0
  90. package/numkong/include/numkong/sparse/serial.h +10 -3
  91. package/numkong/include/numkong/sparse/turin.h +1 -0
  92. package/numkong/include/numkong/spatial/README.md +25 -24
  93. package/numkong/include/numkong/spatial/alder.h +2 -1
  94. package/numkong/include/numkong/spatial/haswell.h +48 -40
  95. package/numkong/include/numkong/spatial/icelake.h +2 -1
  96. package/numkong/include/numkong/spatial/neon.h +54 -46
  97. package/numkong/include/numkong/spatial/neonbfdot.h +1 -1
  98. package/numkong/include/numkong/spatial/powervsx.h +54 -46
  99. package/numkong/include/numkong/spatial/serial.h +56 -46
  100. package/numkong/include/numkong/spatial/sierra.h +4 -3
  101. package/numkong/include/numkong/spatial/skylake.h +27 -22
  102. package/numkong/include/numkong/spatial/sve.h +6 -6
  103. package/numkong/include/numkong/spatial/svebfdot.h +1 -1
  104. package/numkong/include/numkong/spatial/svehalf.h +1 -1
  105. package/numkong/include/numkong/spatial/v128relaxed.h +36 -30
  106. package/numkong/include/numkong/spatials/README.md +15 -15
  107. package/numkong/include/numkong/spatials/genoa.h +3 -0
  108. package/numkong/include/numkong/spatials/graniteamx.h +2 -0
  109. package/numkong/include/numkong/spatials/icelake.h +1 -0
  110. package/numkong/include/numkong/spatials/sierra.h +1 -0
  111. package/numkong/include/numkong/tensor.hpp +236 -123
  112. package/numkong/include/numkong/trigonometry/README.md +4 -4
  113. package/numkong/include/numkong/trigonometry/serial.h +6 -6
  114. package/numkong/include/numkong/trigonometry/v128relaxed.h +1 -0
  115. package/numkong/include/numkong/types.hpp +46 -16
  116. package/numkong/include/numkong/vector.hpp +116 -23
  117. package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
  118. package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
  119. package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
  120. package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
  121. package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
  122. package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
  123. package/numkong/javascript/README.md +18 -16
  124. package/numkong/javascript/numkong.c +70 -40
  125. package/numkong/package.json +8 -8
  126. package/numkong/rust/README.md +19 -9
  127. package/numkong/rust/cast.rs +52 -1
  128. package/numkong/rust/each.rs +480 -7
  129. package/numkong/rust/matrix.rs +877 -216
  130. package/numkong/rust/numkong.rs +12 -8
  131. package/numkong/rust/reduce.rs +433 -5
  132. package/numkong/rust/tensor.rs +450 -842
  133. package/numkong/rust/vector.rs +520 -101
  134. package/numkong/swift/README.md +5 -3
  135. package/numkong/test/README.md +4 -4
  136. package/numkong/test/test-wasi.mjs +17 -0
  137. package/numkong/test/test_each.cpp +30 -1
  138. package/numkong/test/test_each.py +74 -1
  139. package/numkong/test/test_reduce.cpp +51 -0
  140. package/numkong/test/test_tensor.cpp +2 -63
  141. package/numkong/test/test_tensor.py +93 -0
  142. package/package.json +1 -1
  143. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  144. package/prebuilds/linux-arm64/usearch.node +0 -0
  145. package/prebuilds/linux-x64/usearch.node +0 -0
  146. package/prebuilds/win32-x64/usearch.node +0 -0
@@ -38,10 +38,23 @@ set(CMAKE_RANLIB "${WASI_SDK_PATH}/bin/llvm-ranlib${WASI_TOOL_SUFFIX}")
38
38
  set(CMAKE_SYSROOT "${WASI_SDK_PATH}/share/wasi-sysroot")
39
39
  set(CMAKE_FIND_ROOT_PATH "${WASI_SDK_PATH}")
40
40
 
41
+ # Cross-runtime portable mode: build single-threaded with self-contained memory so the SAME `.wasm`
42
+ # runs under node, wasmtime, AND wasmer (used by the cross-runtime kernel validation). Default OFF keeps
43
+ # the threaded, host-imported-memory build that the full nk_test / nk_bench suites rely on.
44
+ if (NK_WASI_PORTABLE)
45
+ set(NK_WASI_TARGET_ "wasm32-wasip1")
46
+ set(NK_WASI_THREADS_ "")
47
+ set(NK_WASI_MEMORY_ "")
48
+ else ()
49
+ set(NK_WASI_TARGET_ "wasm32-wasip1-threads")
50
+ set(NK_WASI_THREADS_ "-pthread")
51
+ set(NK_WASI_MEMORY_ "-Wl,--import-memory -Wl,--export-memory -Wl,--shared-memory -Wl,--max-memory=2147483648")
52
+ endif ()
53
+
41
54
  # WASM SIMD flags (same as Emscripten for consistency).
42
55
  set(WASM_SIMD_FLAGS "-msimd128 -mrelaxed-simd")
43
- set(CMAKE_C_FLAGS_INIT "${WASM_SIMD_FLAGS} --target=wasm32-wasip1-threads -pthread")
44
- set(CMAKE_CXX_FLAGS_INIT "${WASM_SIMD_FLAGS} --target=wasm32-wasip1-threads -pthread -fno-exceptions")
56
+ set(CMAKE_C_FLAGS_INIT "${WASM_SIMD_FLAGS} --target=${NK_WASI_TARGET_} ${NK_WASI_THREADS_}")
57
+ set(CMAKE_CXX_FLAGS_INIT "${WASM_SIMD_FLAGS} --target=${NK_WASI_TARGET_} ${NK_WASI_THREADS_} -fno-exceptions")
45
58
 
46
59
  # Optimization flags.
47
60
  set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")
@@ -49,15 +62,18 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
49
62
  set(CMAKE_C_FLAGS_DEBUG "-O0 -g")
50
63
  set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g")
51
64
 
52
- # Linker flags for WASI.
65
+ # Linker flags for WASI (the imported/shared-memory group is empty in NK_WASI_PORTABLE mode).
53
66
  set(CMAKE_EXE_LINKER_FLAGS_INIT
54
67
  "-Wl,--allow-undefined \
55
- -Wl,--import-memory -Wl,--export-memory \
56
- -Wl,--shared-memory -Wl,--max-memory=2147483648 \
68
+ ${NK_WASI_MEMORY_} \
57
69
  -Wl,--export=main \
58
70
  -Wl,--export=_start"
59
71
  )
60
72
 
73
+ # `wasm-ld` resolves `__global_base`, `__data_end`, and `__heap_base` only in a main module, so a `-shared`
74
+ # WASI build cannot link. Seeded rather than forced, so `-D NK_BUILD_SHARED=ON` still overrides.
75
+ set(NK_BUILD_SHARED OFF CACHE BOOL "Compile a dynamic library")
76
+
61
77
  # Do not look for programs in build-host directories.
62
78
  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
63
79
  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
@@ -75,3 +91,21 @@ endif ()
75
91
 
76
92
  message(STATUS "NumKong WASI: SIMD128 and Relaxed SIMD enabled")
77
93
  message(STATUS "NumKong WASI: Toolchain at ${WASI_SDK_PATH}")
94
+
95
+ # Runtime that CTest invokes on each cross binary, so `ctest` runs the WASI tests cross-engine without a
96
+ # wrapper script. wasmtime and wasmer execute a `.wasm` command directly; node uses the small WASI runner
97
+ # in test/test-wasi.mjs. Relaxed-SIMD lowers differently per engine, so testing more than one matters.
98
+ set(NK_WASM_RUNTIME "wasmtime" CACHE STRING "WASM runtime CTest uses for WASI tests (wasmtime, wasmer, or node)")
99
+ if (NK_WASM_RUNTIME STREQUAL "wasmer")
100
+ find_program(NK_WASMER_EXE_ wasmer PATHS "$ENV{HOME}/.cargo/bin" "$ENV{HOME}/.wasmer/bin")
101
+ set(CMAKE_CROSSCOMPILING_EMULATOR "${NK_WASMER_EXE_};run")
102
+ elseif (NK_WASM_RUNTIME STREQUAL "node")
103
+ find_program(NK_NODE_EXE_ node)
104
+ set(CMAKE_CROSSCOMPILING_EMULATOR
105
+ "${NK_NODE_EXE_};--experimental-wasi-unstable-preview1;${CMAKE_CURRENT_LIST_DIR}/../test/test-wasi.mjs"
106
+ )
107
+ else ()
108
+ find_program(NK_WASMTIME_EXE_ wasmtime PATHS "$ENV{HOME}/.wasmtime/bin")
109
+ set(CMAKE_CROSSCOMPILING_EMULATOR "${NK_WASMTIME_EXE_}")
110
+ endif ()
111
+ message(STATUS "NumKong WASI: CTest runtime = ${NK_WASM_RUNTIME}")
@@ -1,7 +1,7 @@
1
1
  # NumKong for C and C++
2
2
 
3
3
  NumKong's native SDK is the reference surface for the project.
4
- The plain C ABI exposes every kernel family directly: dot products, dense distances, binary metrics, probability divergences, geospatial solvers, curved-space kernels, sparse intersections, mesh alignment, packed matrix multiplication, symmetric self-similarity, and late-interaction scoring.
4
+ The plain C ABI exposes every kernel family directly: dot products, dense distances, binary metrics, probability divergences, geospatial solvers, curved-space kernels, sparse intersections, mesh alignment, packed matrix multiplication, symmetric self-similarity, late-interaction scoring, elementwise arithmetic, reductions, type conversions, scalar math, and trigonometry.
5
5
  The ABI is stable, versioned, and callable from any language that can load a shared library.
6
6
  There is no runtime overhead: no hidden thread pool, no implicit allocation, no garbage collector interaction.
7
7
  The C++ layer stays thin, typed, allocator-aware, and close enough to inline through, adding type-level result promotion and owning containers without hiding the dispatch model or the mixed-precision policy.
@@ -43,7 +43,7 @@ Packing handles internal layout itself and does not require caller-side alignmen
43
43
 
44
44
  ## Ecosystem Comparison
45
45
 
46
- | Feature | NumKong | [OpenBLAS](https://github.com/OpenMathLib/OpenBLAS) | [Eigen](https://gitlab.com/libeigen/eigen) |
46
+ | Feature | NumKong | [OpenBLAS][openblas] | [Eigen][eigen] |
47
47
  | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
48
48
  | Operation families | dots, distances, binary, probability, geospatial, curved, mesh, sparse, MaxSim, elementwise, reductions, cast, trig | dense linear algebra only | dense LA, some reductions and elementwise |
49
49
  | Precision | Sub-byte to Float64 dtypes; automatic widening per scalar type; Kahan-compensated summation; 0 ULP Float32/Float64 where applicable | Float32, Float64 only; same-type in/out; no compensated summation | Float16/BFloat16 partial; no Float8 or sub-byte; manual casts; no compensated summation |
@@ -52,6 +52,8 @@ Packing handles internal layout itself and does not require caller-side alignmen
52
52
  | Symmetric kernels, SYRK-like | skips duplicate pairs, up to 2x speedup for self-distance | `SSYRK`/`DSYRK` for rank-k updates | `.selfadjointView` for rank-k updates |
53
53
  | Memory model | Caller-owned buffers; C++ adds `tensor<T,A>` with per-container allocators | Caller-managed buffers; no container abstraction | Lazy expression templates avoid most temporaries; `aligned_allocator` provided |
54
54
 
55
+ [openblas]: https://github.com/OpenMathLib/OpenBLAS
56
+ [eigen]: https://gitlab.com/libeigen/eigen
55
57
 
56
58
 
57
59
  ## Installation
@@ -168,7 +170,7 @@ The higher-level templates use `result_type_ = typename in_type_::dot_result_t`
168
170
  The fast typed overloads are constrained so that overriding the result type away from the native policy can disable the specialized path and fall back to the more generic one.
169
171
 
170
172
  When `__cpp_lib_format >= 202110L` for the C++23 `<format>` header support, all NumKong scalar types provide `std::formatter` specializations with similar format specs to the traditional `float`.
171
- For the BFloat16 type, the output for `nk::f16_t::from_f32(3.14f)` will look like:
173
+ For the Float16 type, the output for `nk::f16_t::from_f32(3.14f)` will look like:
172
174
 
173
175
  | Format spec | Output example | Description |
174
176
  | ----------- | -------------------- | -------------------------------------- |
@@ -189,10 +191,10 @@ They include real, complex, packed-binary, mini-float, and mixed-precision forms
189
191
  ```c
190
192
  nk_f32c_t a[384];
191
193
  nk_f32c_t b[384];
192
- nk_f32_t out[2] = {0, 0};
194
+ nk_f64c_t out = {0, 0}; // widened f32c → f64c output
193
195
 
194
- nk_dot_f32c(a, b, 384, out); // complex inner product
195
- nk_vdot_f32c(a, b, 384, out); // conjugated variant, like numpy.vdot
196
+ nk_dot_f32c(a, b, 384, &out); // complex inner product
197
+ nk_vdot_f32c(a, b, 384, &out); // conjugated variant, like numpy.vdot
196
198
  ```
197
199
 
198
200
  For quantized retrieval pipelines, the storage format often matters more than the nominal math family.
@@ -231,15 +233,18 @@ nk_hamming_u1(a, b, 128 * 8, &hamming);
231
233
  nk_jaccard_u1(a, b, 128 * 8, &jaccard);
232
234
  ```
233
235
 
234
- Integer set Jaccard works on sorted arrays of integer identifiers.
236
+ `nk_jaccard_u32` is a dense word-wise kernel rather than a sorted-set operation.
237
+ It walks two arrays of `n` 32-bit words position by position and returns `1 - matches / n`.
235
238
 
236
239
  ```c
237
- nk_u32_t set_a[] = {1, 3, 5, 7, 9}, set_b[] = {3, 5, 8, 9, 10};
238
- nk_f32_t jaccard_sets = 0;
239
- nk_jaccard_u32(set_a, set_b, 5, &jaccard_sets); // |A ∩ B| / |A ∪ B|
240
- assert(jaccard_sets > 0.0f && jaccard_sets < 1.0f && "|A ∩ B| / |A ∪ B| should be in (0, 1)");
240
+ nk_u32_t a[] = {1, 3, 5, 7, 9}, b[] = {1, 3, 5, 8, 10};
241
+ nk_f32_t jaccard_words = 0;
242
+ nk_jaccard_u32(a, b, 5, &jaccard_words); // 1 - matches / n
243
+ assert(jaccard_words > 0.0f && jaccard_words < 1.0f && "3 of 5 words match");
241
244
  ```
242
245
 
246
+ Sorted arrays of integer identifiers are handled by the sparse kernels instead, described below.
247
+
243
248
  ## Probability Metrics
244
249
 
245
250
  Probability kernels target divergences directly instead of making you rebuild them from scalar loops.
@@ -351,8 +356,8 @@ You can also use a more traditional syntax with member functions, also leveragin
351
356
  Similar to NumPy, but statically typed:
352
357
 
353
358
  ```cpp
354
- auto first_column = t[all, 1, slice]; // strided column view → {2, 5, 8}
355
- auto minimum_index = nk::argmin(first_column); // index of the minimum in the second column
359
+ auto second_column = t[all, 1, slice]; // strided column view → {2, 5, 8}
360
+ auto minimum_index = nk::argmin(second_column); // index of the minimum in the second column
356
361
  ```
357
362
 
358
363
  The view types are conceptually close to `std::mdspan` from C++23.
@@ -498,10 +503,11 @@ The native API covers `rmsd`, `kabsch`, and `umeyama`.
498
503
  nk_f32_t source[] = {0, 0, 0, 1, 0, 0, 0, 1, 0};
499
504
  nk_f32_t target[] = {0, 0, 0, 2, 0, 0, 0, 2, 0};
500
505
  nk_f32_t a_centroid[3], b_centroid[3], rotation[9];
501
- nk_f32_t scale = 0, rmsd = 0;
506
+ nk_f32_t scale = 0;
507
+ nk_f64_t rmsd = 0; // widened f32 → f64 output
502
508
 
503
509
  nk_umeyama_f32(source, target, 3, a_centroid, b_centroid, rotation, &scale, &rmsd);
504
- assert(rmsd < 1e-6f && "umeyama should recover exact alignment");
510
+ assert(rmsd < 1e-6 && "umeyama should recover exact alignment");
505
511
  assert(scale > 1.99f && scale < 2.01f && "umeyama should recover 2x scale");
506
512
  ```
507
513
 
@@ -592,13 +598,14 @@ The main user-facing CMake options are:
592
598
  - `NK_BUILD_SHARED` builds a shared library, ON by default for standalone builds and OFF when included as a subdirectory.
593
599
  - `NK_BUILD_TEST` and `NK_BUILD_BENCH` enable precision tests and benchmarks respectively, both OFF by default.
594
600
  - `NK_DYNAMIC_DISPATCH=1` compiles all backends into one binary and selects at runtime via `nk_capabilities()`, recommended for shipping one binary across CPU generations.
601
+ It is a preprocessor definition rather than a CMake option, so pass it through the compiler flags.
595
602
  - `NK_COMPARE_TO_BLAS` and `NK_COMPARE_TO_MKL` link benchmarks against a system BLAS or Intel MKL, each accepting `AUTO`, `ON`, or `OFF` with `AUTO` as the default.
596
603
 
597
- The build enforces C99 for the C layer and C++23 for the C++ layer.
604
+ The build enforces C99 for the C layer and C++20 for the C++ layer.
598
605
 
599
606
  ```sh
600
607
  cmake -B build -D CMAKE_BUILD_TYPE=Release -D NK_BUILD_TEST=ON
601
- cmake -B build -D NK_DYNAMIC_DISPATCH=1 -D NK_BUILD_BENCH=ON -D NK_COMPARE_TO_MKL=ON
608
+ cmake -B build -D CMAKE_C_FLAGS="-DNK_DYNAMIC_DISPATCH=1" -D NK_BUILD_BENCH=ON -D NK_COMPARE_TO_MKL=ON
602
609
  ```
603
610
 
604
611
  ## Cross-Compilation
@@ -117,7 +117,7 @@ typedef struct {
117
117
  * 2. Polynomial approximation: exp(r) ≈ 1 + r + r²/2 + ... (degree 6)
118
118
  * 3. Reconstruction: exp(x) = 2ⁿ × exp(r)
119
119
  *
120
- * @param x Input vector (16 floats)
120
+ * @param x_f32x16 Input vector (16 floats)
121
121
  * @return exp(x) for each element
122
122
  */
123
123
  NK_INTERNAL __m512 nk_exp_ps_avx512_(__m512 x_f32x16) {
@@ -169,7 +169,7 @@ NK_INTERNAL __m512 nk_exp_ps_avx512_(__m512 x_f32x16) {
169
169
  *
170
170
  * Performance: ~12-15 cycles for 16 floats (vs ~18-22 for degree-6)
171
171
  *
172
- * @param x Input vector (16 floats)
172
+ * @param x_f32x16 Input vector (16 floats)
173
173
  * @return exp(x) approximation
174
174
  */
175
175
  NK_INTERNAL __m512 nk_exp_ps_fast_avx512_(__m512 x_f32x16) {
@@ -475,10 +475,10 @@ NK_INTERNAL void nk_attention_softmax_update_(nk_attention_softmax_row_state_t *
475
475
  * When processing a new KV block with larger scores, previous O accumulator
476
476
  * needs rescaling: O = O × exp(oldₘₐₓ - newₘₐₓ)
477
477
  *
478
- * @param output Output accumulator [16][head_dim] in F32
479
- * @param head_dim Head dimension
480
- * @param old_max Previous running max per row (16 values)
481
- * @param new_max New running max per row (16 values)
478
+ * @param output Output accumulator [16][head_dim] in F32
479
+ * @param head_dim Head dimension
480
+ * @param old_max_f32x16 Previous running max per row (16 values)
481
+ * @param new_max_f32x16 New running max per row (16 values)
482
482
  */
483
483
  NK_INTERNAL void nk_attention_rescale_output_(nk_f32_t *output, nk_size_t head_dim, __m512 old_max_f32x16,
484
484
  __m512 new_max_f32x16) {
@@ -162,8 +162,8 @@ typedef struct {
162
162
  * Uses Cody-Waite range reduction + Horner polynomial (degree 4).
163
163
  * Accuracy: ~0.1% relative error, acceptable for softmax normalization.
164
164
  *
165
- * @param pg Active predicate
166
- * @param x Input vector
165
+ * @param predicate_b32x Active predicate
166
+ * @param x_f32x Input vector
167
167
  * @return exp(x) approximation
168
168
  */
169
169
  NK_INTERNAL svfloat32_t nk_exp_f32_sve_(svbool_t predicate_b32x, svfloat32_t x_f32x) __arm_streaming {
@@ -95,8 +95,8 @@
95
95
  #include "numkong/types.h" // `nk_u64_t`, `NK_DEFINED_LINUX_`
96
96
 
97
97
  #define NK_VERSION_MAJOR 7
98
- #define NK_VERSION_MINOR 6
99
- #define NK_VERSION_PATCH 0
98
+ #define NK_VERSION_MINOR 8
99
+ #define NK_VERSION_PATCH 1
100
100
 
101
101
  /**
102
102
  * @brief Removes compile-time dispatching, and replaces it with runtime dispatching.
@@ -1,7 +1,8 @@
1
1
  # Type Conversions in NumKong
2
2
 
3
- NumKong implements bidirectional type conversions between all supported numeric formats through Float32 as a hub type.
4
- Conversions cover IEEE 754 floats (Float16, Float32, Float64), brain float (BFloat16), Float8 formats (e4m3, e5m2, e2m3, e3m2), and integers (Int8–Int64, UInt8–UInt64, packed i4x2/u4x2).
3
+ NumKong implements bidirectional type conversions between all supported numeric formats through a wider hub type: `u64` when both sides are unsigned integers, `i64` when both sides are integers and at least one is signed, and `f64c` for everything else.
4
+ The SIMD backends use a narrower Float32 hub for the subset of types they cover.
5
+ Conversions cover IEEE 754 floats (Float16, Float32, Float64), brain float (BFloat16), Float8 formats (e4m3, e5m2, e2m3, e3m2), and integers (Int8–Int64, UInt8–UInt64, packed i4x2/u4x2, packed u1x8).
5
6
  All conversions use round-to-nearest-even (RNE) for narrowing and exact widening where the target format has sufficient range and precision.
6
7
 
7
8
  BFloat16 relates to Float32 by truncation with rounding:
@@ -77,16 +78,17 @@ Packed sub-byte conversions:
77
78
 
78
79
  ### Lookup Tables for Mini-Floats
79
80
 
80
- `nk_e4m3_to_f32_serial`, `nk_e5m2_to_f32_serial`, `nk_e2m3_to_f32_serial`, `nk_e3m2_to_f32_serial` use 256-entry precomputed lookup tables — each 8-bit input indexes directly into a Float32 result array.
81
- The reverse direction (`nk_f32_to_e4m3_serial`) uses clamping + rounding: clamp to format range, multiply by scale, round-to-nearest, cast to UInt8.
82
- SIMD backends (`nk_cast_haswell`, `nk_cast_skylake`) use `VPGATHERDD` to perform 8 or 16 simultaneous table lookups from the same 256-entry table.
83
- AVX-512 gathers on Skylake achieve ~3cy throughput per 16-element lookup vs ~8cy on Haswell for 8-element gathers.
81
+ `nk_e5m2_to_f32_serial` indexes a 128-entry table with the 7-bit magnitude, and `nk_e2m3_to_f32_serial` and `nk_e3m2_to_f32_serial` index 32-entry tables with their 5-bit magnitudes, OR-ing the sign bit back in afterwards.
82
+ `nk_e4m3_to_f32_serial` needs no table: the exponent and mantissa fields map onto Float32 with a bias shift of 120, and only the subnormal and the single NaN encoding are special-cased.
83
+ The reverse direction (`nk_f32_to_e4m3_serial`) is bit manipulation with RNE rounding: NaN and infinity are mapped explicitly, subnormals below 2⁻⁶ are rounded through a `× 512` scale, and normals get their 23-bit mantissa rounded down to 3 bits.
84
+ The SIMD backends do not gather from these tables.
85
+ On x86 the Float8 paths ride the F16C converters described below, and NEON looks up Float16 high bytes with `VQTBL4` over a 128-byte table in `nk_e4m3x16_to_f16x8x2_neon_` and its siblings.
84
86
 
85
87
  ### BFloat16 as Truncated Float32
86
88
 
87
89
  `nk_bf16_to_f32_serial` zero-extends by left-shifting 16 bits — exact, no rounding error, single-cycle on all platforms.
88
90
  `nk_f32_to_bf16_serial` right-shifts with round-to-nearest-even: adds a rounding bias of `0x7FFF + ((bits >> 16) & 1)` before truncating, matching the IEEE 754 RNE tie-breaking rule.
89
- NEON backend uses `vreinterpretq_u16_u8` + `vzip` for zero-extension; Haswell uses `VPSLLD` / `VPSRLD` shifts.
91
+ NEON backend uses `vmovl_u16` + `vshlq_n_u32` for zero-extension; Haswell uses `VPSLLD` / `VPSRLD` shifts.
90
92
 
91
93
  ### F16C Hardware Conversion
92
94
 
@@ -139,106 +139,114 @@ NK_INTERNAL __m128 nk_sqrt_f32x4_loongsonasx_(__m128 x_f32x4) {
139
139
  }
140
140
 
141
141
  /** @brief Angular from_dot: computes 1 − dot × rsqrt(query_sumsq × target_sumsq) for 4 pairs (LSX 128-bit f32). */
142
- NK_INTERNAL void nk_angular_through_f32_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_f32_t query_sumsq,
143
- nk_b128_vec_t target_sumsqs, nk_b128_vec_t *results) {
144
- __m128 dots_f32x4 = dots.xmm_ps;
142
+ NK_INTERNAL void nk_angular_through_f32_from_dot_loongsonasx_(nk_b128_vec_t const *dots_vec, nk_f32_t query_sumsq,
143
+ nk_b128_vec_t const *target_sumsqs_vec,
144
+ nk_b128_vec_t *result_vec) {
145
+ __m128 dots_f32x4 = dots_vec->xmm_ps;
145
146
  __m128 query_sumsq_f32x4 = nk_xvreplgr2vr_s_128_(query_sumsq);
146
- __m128 products_f32x4 = __lsx_vfmul_s(query_sumsq_f32x4, target_sumsqs.xmm_ps);
147
+ __m128 products_f32x4 = __lsx_vfmul_s(query_sumsq_f32x4, target_sumsqs_vec->xmm_ps);
147
148
  __m128 rsqrt_f32x4 = __lsx_vfrsqrt_s(products_f32x4);
148
149
  __m128 normalized_f32x4 = __lsx_vfmul_s(dots_f32x4, rsqrt_f32x4);
149
150
  __m128 one_f32x4 = nk_xvreplgr2vr_s_128_(1.0f);
150
151
  __m128 angular_f32x4 = __lsx_vfsub_s(one_f32x4, normalized_f32x4);
151
152
  __m128 zero_f32x4 = (__m128)__lsx_vreplgr2vr_w(0);
152
- results->xmm_ps = __lsx_vfmax_s(angular_f32x4, zero_f32x4);
153
+ result_vec->xmm_ps = __lsx_vfmax_s(angular_f32x4, zero_f32x4);
153
154
  }
154
155
 
155
156
  /** @brief Euclidean from_dot: computes √(query_sumsq + target_sumsq − 2 × dot) for 4 pairs (LSX 128-bit f32). */
156
- NK_INTERNAL void nk_euclidean_through_f32_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_f32_t query_sumsq,
157
- nk_b128_vec_t target_sumsqs, nk_b128_vec_t *results) {
158
- __m128 dots_f32x4 = dots.xmm_ps;
157
+ NK_INTERNAL void nk_euclidean_through_f32_from_dot_loongsonasx_(nk_b128_vec_t const *dots_vec, nk_f32_t query_sumsq,
158
+ nk_b128_vec_t const *target_sumsqs_vec,
159
+ nk_b128_vec_t *result_vec) {
160
+ __m128 dots_f32x4 = dots_vec->xmm_ps;
159
161
  __m128 query_sumsq_f32x4 = nk_xvreplgr2vr_s_128_(query_sumsq);
160
- __m128 sum_sq_f32x4 = __lsx_vfadd_s(query_sumsq_f32x4, target_sumsqs.xmm_ps);
162
+ __m128 sum_sq_f32x4 = __lsx_vfadd_s(query_sumsq_f32x4, target_sumsqs_vec->xmm_ps);
161
163
  __m128 two_f32x4 = nk_xvreplgr2vr_s_128_(2.0f);
162
164
  // dist_sq = sum_sq − 2 × dots = -(2 × dots − sum_sq)
163
165
  __m128 dist_sq_f32x4 = __lsx_vfnmsub_s(two_f32x4, dots_f32x4, sum_sq_f32x4);
164
- results->xmm_ps = nk_sqrt_f32x4_loongsonasx_(dist_sq_f32x4);
166
+ result_vec->xmm_ps = nk_sqrt_f32x4_loongsonasx_(dist_sq_f32x4);
165
167
  }
166
168
 
167
169
  /** @brief Angular from_dot for native f64: 1 − dot / √(query_sumsq × target_sumsq) for 4 pairs (LASX 256-bit). */
168
- NK_INTERNAL void nk_angular_through_f64_from_dot_loongsonasx_(nk_b256_vec_t dots, nk_f64_t query_sumsq,
169
- nk_b256_vec_t target_sumsqs, nk_b256_vec_t *results) {
170
- __m256d dots_f64x4 = dots.ymm_pd;
170
+ NK_INTERNAL void nk_angular_through_f64_from_dot_loongsonasx_(nk_b256_vec_t const *dots_vec, nk_f64_t query_sumsq,
171
+ nk_b256_vec_t const *target_sumsqs_vec,
172
+ nk_b256_vec_t *result_vec) {
173
+ __m256d dots_f64x4 = dots_vec->ymm_pd;
171
174
  __m256d query_sumsq_f64x4 = nk_xvfreplgr2vr_d_(query_sumsq);
172
- __m256d products_f64x4 = __lasx_xvfmul_d(query_sumsq_f64x4, target_sumsqs.ymm_pd);
175
+ __m256d products_f64x4 = __lasx_xvfmul_d(query_sumsq_f64x4, target_sumsqs_vec->ymm_pd);
173
176
  __m256d sqrt_products_f64x4 = __lasx_xvfsqrt_d(products_f64x4);
174
177
  __m256d normalized_f64x4 = __lasx_xvfdiv_d(dots_f64x4, sqrt_products_f64x4);
175
178
  __m256d one_f64x4 = nk_xvfreplgr2vr_d_(1.0);
176
179
  __m256d angular_f64x4 = __lasx_xvfsub_d(one_f64x4, normalized_f64x4);
177
180
  __m256d zero_f64x4 = (__m256d)__lasx_xvreplgr2vr_d(0);
178
- results->ymm_pd = __lasx_xvfmax_d(angular_f64x4, zero_f64x4);
181
+ result_vec->ymm_pd = __lasx_xvfmax_d(angular_f64x4, zero_f64x4);
179
182
  }
180
183
 
181
184
  /** @brief Euclidean from_dot for native f64: √(query_sumsq + target_sumsq − 2 × dot) for 4 pairs (LASX 256-bit). */
182
- NK_INTERNAL void nk_euclidean_through_f64_from_dot_loongsonasx_(nk_b256_vec_t dots, nk_f64_t query_sumsq,
183
- nk_b256_vec_t target_sumsqs, nk_b256_vec_t *results) {
184
- __m256d dots_f64x4 = dots.ymm_pd;
185
+ NK_INTERNAL void nk_euclidean_through_f64_from_dot_loongsonasx_(nk_b256_vec_t const *dots_vec, nk_f64_t query_sumsq,
186
+ nk_b256_vec_t const *target_sumsqs_vec,
187
+ nk_b256_vec_t *result_vec) {
188
+ __m256d dots_f64x4 = dots_vec->ymm_pd;
185
189
  __m256d query_sumsq_f64x4 = nk_xvfreplgr2vr_d_(query_sumsq);
186
- __m256d sum_sq_f64x4 = __lasx_xvfadd_d(query_sumsq_f64x4, target_sumsqs.ymm_pd);
190
+ __m256d sum_sq_f64x4 = __lasx_xvfadd_d(query_sumsq_f64x4, target_sumsqs_vec->ymm_pd);
187
191
  __m256d two_f64x4 = nk_xvfreplgr2vr_d_(2.0);
188
192
  // dist_sq = sum_sq − 2 × dots = -(2 × dots − sum_sq)
189
193
  __m256d dist_sq_f64x4 = __lasx_xvfnmsub_d(two_f64x4, dots_f64x4, sum_sq_f64x4);
190
194
  __m256d zero_f64x4 = (__m256d)__lasx_xvreplgr2vr_d(0);
191
- results->ymm_pd = __lasx_xvfsqrt_d(__lasx_xvfmax_d(dist_sq_f64x4, zero_f64x4));
195
+ result_vec->ymm_pd = __lasx_xvfsqrt_d(__lasx_xvfmax_d(dist_sq_f64x4, zero_f64x4));
192
196
  }
193
197
 
194
198
  /** @brief Angular from_dot for i32 accumulators: cast i32 → f32, rsqrt+NR, clamp. 4 pairs (LSX 128-bit). */
195
- NK_INTERNAL void nk_angular_through_i32_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_i32_t query_sumsq,
196
- nk_b128_vec_t target_sumsqs, nk_b128_vec_t *results) {
197
- __m128 dots_f32x4 = __lsx_vffint_s_w(dots.xmm);
199
+ NK_INTERNAL void nk_angular_through_i32_from_dot_loongsonasx_(nk_b128_vec_t const *dots_vec, nk_i32_t query_sumsq,
200
+ nk_b128_vec_t const *target_sumsqs_vec,
201
+ nk_b128_vec_t *result_vec) {
202
+ __m128 dots_f32x4 = __lsx_vffint_s_w(dots_vec->xmm);
198
203
  __m128 query_sumsq_f32x4 = nk_xvreplgr2vr_s_128_((nk_f32_t)query_sumsq);
199
- __m128 products_f32x4 = __lsx_vfmul_s(query_sumsq_f32x4, __lsx_vffint_s_w(target_sumsqs.xmm));
204
+ __m128 products_f32x4 = __lsx_vfmul_s(query_sumsq_f32x4, __lsx_vffint_s_w(target_sumsqs_vec->xmm));
200
205
  __m128 rsqrt_f32x4 = __lsx_vfrsqrt_s(products_f32x4);
201
206
  __m128 normalized_f32x4 = __lsx_vfmul_s(dots_f32x4, rsqrt_f32x4);
202
207
  __m128 one_f32x4 = nk_xvreplgr2vr_s_128_(1.0f);
203
208
  __m128 angular_f32x4 = __lsx_vfsub_s(one_f32x4, normalized_f32x4);
204
209
  __m128 zero_f32x4 = (__m128)__lsx_vreplgr2vr_w(0);
205
- results->xmm_ps = __lsx_vfmax_s(angular_f32x4, zero_f32x4);
210
+ result_vec->xmm_ps = __lsx_vfmax_s(angular_f32x4, zero_f32x4);
206
211
  }
207
212
 
208
213
  /** @brief Euclidean from_dot for i32 accumulators: cast i32 → f32, then √(a² + b² − 2ab). 4 pairs (LSX 128-bit). */
209
- NK_INTERNAL void nk_euclidean_through_i32_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_i32_t query_sumsq,
210
- nk_b128_vec_t target_sumsqs, nk_b128_vec_t *results) {
211
- __m128 dots_f32x4 = __lsx_vffint_s_w(dots.xmm);
214
+ NK_INTERNAL void nk_euclidean_through_i32_from_dot_loongsonasx_(nk_b128_vec_t const *dots_vec, nk_i32_t query_sumsq,
215
+ nk_b128_vec_t const *target_sumsqs_vec,
216
+ nk_b128_vec_t *result_vec) {
217
+ __m128 dots_f32x4 = __lsx_vffint_s_w(dots_vec->xmm);
212
218
  __m128 query_sumsq_f32x4 = nk_xvreplgr2vr_s_128_((nk_f32_t)query_sumsq);
213
- __m128 sum_sq_f32x4 = __lsx_vfadd_s(query_sumsq_f32x4, __lsx_vffint_s_w(target_sumsqs.xmm));
219
+ __m128 sum_sq_f32x4 = __lsx_vfadd_s(query_sumsq_f32x4, __lsx_vffint_s_w(target_sumsqs_vec->xmm));
214
220
  __m128 two_f32x4 = nk_xvreplgr2vr_s_128_(2.0f);
215
221
  __m128 dist_sq_f32x4 = __lsx_vfnmsub_s(two_f32x4, dots_f32x4, sum_sq_f32x4);
216
- results->xmm_ps = nk_sqrt_f32x4_loongsonasx_(dist_sq_f32x4);
222
+ result_vec->xmm_ps = nk_sqrt_f32x4_loongsonasx_(dist_sq_f32x4);
217
223
  }
218
224
 
219
225
  /** @brief Angular from_dot for u32 accumulators: cast u32 → f32, rsqrt+NR, clamp. 4 pairs (LSX 128-bit). */
220
- NK_INTERNAL void nk_angular_through_u32_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_u32_t query_sumsq,
221
- nk_b128_vec_t target_sumsqs, nk_b128_vec_t *results) {
222
- __m128 dots_f32x4 = __lsx_vffint_s_w(dots.xmm);
226
+ NK_INTERNAL void nk_angular_through_u32_from_dot_loongsonasx_(nk_b128_vec_t const *dots_vec, nk_u32_t query_sumsq,
227
+ nk_b128_vec_t const *target_sumsqs_vec,
228
+ nk_b128_vec_t *result_vec) {
229
+ __m128 dots_f32x4 = __lsx_vffint_s_w(dots_vec->xmm);
223
230
  __m128 query_sumsq_f32x4 = nk_xvreplgr2vr_s_128_((nk_f32_t)query_sumsq);
224
- __m128 products_f32x4 = __lsx_vfmul_s(query_sumsq_f32x4, __lsx_vffint_s_w(target_sumsqs.xmm));
231
+ __m128 products_f32x4 = __lsx_vfmul_s(query_sumsq_f32x4, __lsx_vffint_s_w(target_sumsqs_vec->xmm));
225
232
  __m128 rsqrt_f32x4 = __lsx_vfrsqrt_s(products_f32x4);
226
233
  __m128 normalized_f32x4 = __lsx_vfmul_s(dots_f32x4, rsqrt_f32x4);
227
234
  __m128 one_f32x4 = nk_xvreplgr2vr_s_128_(1.0f);
228
235
  __m128 angular_f32x4 = __lsx_vfsub_s(one_f32x4, normalized_f32x4);
229
236
  __m128 zero_f32x4 = (__m128)__lsx_vreplgr2vr_w(0);
230
- results->xmm_ps = __lsx_vfmax_s(angular_f32x4, zero_f32x4);
237
+ result_vec->xmm_ps = __lsx_vfmax_s(angular_f32x4, zero_f32x4);
231
238
  }
232
239
 
233
240
  /** @brief Euclidean from_dot for u32 accumulators: cast u32 → f32, then √(a² + b² − 2ab). 4 pairs (LSX 128-bit). */
234
- NK_INTERNAL void nk_euclidean_through_u32_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_u32_t query_sumsq,
235
- nk_b128_vec_t target_sumsqs, nk_b128_vec_t *results) {
236
- __m128 dots_f32x4 = __lsx_vffint_s_w(dots.xmm);
241
+ NK_INTERNAL void nk_euclidean_through_u32_from_dot_loongsonasx_(nk_b128_vec_t const *dots_vec, nk_u32_t query_sumsq,
242
+ nk_b128_vec_t const *target_sumsqs_vec,
243
+ nk_b128_vec_t *result_vec) {
244
+ __m128 dots_f32x4 = __lsx_vffint_s_w(dots_vec->xmm);
237
245
  __m128 query_sumsq_f32x4 = nk_xvreplgr2vr_s_128_((nk_f32_t)query_sumsq);
238
- __m128 sum_sq_f32x4 = __lsx_vfadd_s(query_sumsq_f32x4, __lsx_vffint_s_w(target_sumsqs.xmm));
246
+ __m128 sum_sq_f32x4 = __lsx_vfadd_s(query_sumsq_f32x4, __lsx_vffint_s_w(target_sumsqs_vec->xmm));
239
247
  __m128 two_f32x4 = nk_xvreplgr2vr_s_128_(2.0f);
240
248
  __m128 dist_sq_f32x4 = __lsx_vfnmsub_s(two_f32x4, dots_f32x4, sum_sq_f32x4);
241
- results->xmm_ps = nk_sqrt_f32x4_loongsonasx_(dist_sq_f32x4);
249
+ result_vec->xmm_ps = nk_sqrt_f32x4_loongsonasx_(dist_sq_f32x4);
242
250
  }
243
251
 
244
252
  #pragma endregion Vectorized From Dot Helpers
@@ -13,17 +13,6 @@
13
13
  extern "C" {
14
14
  #endif
15
15
 
16
- /* Keep the serial conversions below actually scalar, regardless of build type.
17
- * Without this, -O3 + LTO can vectorize or clone the serial kernels under AVX-512
18
- * callers in dispatch_*.c, which wastes binary and breaks the nk_*_serial-as-scalar-oracle
19
- * contract. See dots/serial.h. */
20
- #if defined(__clang__)
21
- #pragma clang attribute push(__attribute__((noinline)), apply_to = function)
22
- #elif defined(__GNUC__)
23
- #pragma GCC push_options
24
- #pragma GCC optimize("no-tree-vectorize", "no-tree-slp-vectorize", "no-ipa-cp-clone", "no-inline")
25
- #endif
26
-
27
16
  #pragma region Type Punned Loads and Stores
28
17
 
29
18
  /** @brief Type-agnostic 32-bit full load (scalar). */
@@ -563,6 +552,17 @@ NK_INTERNAL void nk_strided_load_b8x16_serial_(void const *src, nk_size_t stride
563
552
 
564
553
  #pragma endregion Type Punned Loads and Stores
565
554
 
555
+ /* Keep the serial conversions below actually scalar, regardless of build type.
556
+ * Without this, -O3 + LTO can vectorize or clone the serial kernels under AVX-512
557
+ * callers in dispatch_*.c, which wastes binary and breaks the nk_*_serial-as-scalar-oracle
558
+ * contract. See dots/serial.h. */
559
+ #if defined(__clang__)
560
+ #pragma clang attribute push(__attribute__((noinline)), apply_to = function)
561
+ #elif defined(__GNUC__)
562
+ #pragma GCC push_options
563
+ #pragma GCC optimize("no-tree-vectorize", "no-tree-slp-vectorize", "no-ipa-cp-clone", "no-inline")
564
+ #endif
565
+
566
566
  /**
567
567
  * @brief Expands an `f16` (IEEE-754 16-bit) to a `float`.
568
568
  *
@@ -622,6 +622,12 @@ NK_PUBLIC void nk_f16_to_f32_serial(nk_f16_t const *src, nk_f32_t *dest) {
622
622
  #endif
623
623
  }
624
624
 
625
+ #if defined(__clang__)
626
+ #pragma clang attribute pop
627
+ #elif defined(__GNUC__)
628
+ #pragma GCC pop_options
629
+ #endif
630
+
625
631
  /** @brief Load 4 × f16 from memory and upcast them to 4 × f32. */
626
632
  NK_INTERNAL void nk_load_f16x4_to_f32x4_serial_(void const *src, nk_b128_vec_t *dst) {
627
633
  nk_f16_t const *scalars = (nk_f16_t const *)src;
@@ -644,6 +650,13 @@ NK_INTERNAL void nk_partial_load_f16x4_to_f32x4_serial_(nk_f16_t const *src, nk_
644
650
  }
645
651
  }
646
652
 
653
+ #if defined(__clang__)
654
+ #pragma clang attribute push(__attribute__((noinline)), apply_to = function)
655
+ #elif defined(__GNUC__)
656
+ #pragma GCC push_options
657
+ #pragma GCC optimize("no-tree-vectorize", "no-tree-slp-vectorize", "no-ipa-cp-clone", "no-inline")
658
+ #endif
659
+
647
660
  /**
648
661
  * @brief Compresses a `float` to an `f16` (IEEE-754 16-bit).
649
662
  *
@@ -946,6 +959,12 @@ NK_PUBLIC void nk_f32_to_e4m3_serial(nk_f32_t const *src, nk_e4m3_t *dest) {
946
959
  *dest = (nk_e4m3_t)(sign | (exp_field << 3) | mant_field);
947
960
  }
948
961
 
962
+ #if defined(__clang__)
963
+ #pragma clang attribute pop
964
+ #elif defined(__GNUC__)
965
+ #pragma GCC pop_options
966
+ #endif
967
+
949
968
  /**
950
969
  * @brief Convert FP8 E4M3 to IEEE 754 half-precision float.
951
970
  *
@@ -1039,6 +1058,13 @@ NK_INTERNAL void nk_e5m2_to_f32_manual_(nk_e5m2_t const *src, nk_f32_t *dest) {
1039
1058
  *dest = conv.f;
1040
1059
  }
1041
1060
 
1061
+ #if defined(__clang__)
1062
+ #pragma clang attribute push(__attribute__((noinline)), apply_to = function)
1063
+ #elif defined(__GNUC__)
1064
+ #pragma GCC push_options
1065
+ #pragma GCC optimize("no-tree-vectorize", "no-tree-slp-vectorize", "no-ipa-cp-clone", "no-inline")
1066
+ #endif
1067
+
1042
1068
  NK_PUBLIC void nk_e5m2_to_f32_serial(nk_e5m2_t const *src, nk_f32_t *dest) {
1043
1069
  static nk_u32_t const lut[128] = {
1044
1070
  0x00000000, 0x37800000, 0x38000000, 0x38400000, // exp=0 sub
@@ -1181,6 +1207,12 @@ NK_PUBLIC void nk_f32_to_e5m2_serial(nk_f32_t const *src, nk_e5m2_t *dest) {
1181
1207
  *dest = (nk_e5m2_t)(sign | (exp_field << 2) | mant_field);
1182
1208
  }
1183
1209
 
1210
+ #if defined(__clang__)
1211
+ #pragma clang attribute pop
1212
+ #elif defined(__GNUC__)
1213
+ #pragma GCC pop_options
1214
+ #endif
1215
+
1184
1216
  /**
1185
1217
  * @brief Convert FP8 E5M2 to IEEE 754 half-precision float.
1186
1218
  *
@@ -1315,6 +1347,13 @@ NK_INTERNAL void nk_e2m3_to_f32_manual_(nk_e2m3_t const *src, nk_f32_t *dest) {
1315
1347
  *dest = conv.f;
1316
1348
  }
1317
1349
 
1350
+ #if defined(__clang__)
1351
+ #pragma clang attribute push(__attribute__((noinline)), apply_to = function)
1352
+ #elif defined(__GNUC__)
1353
+ #pragma GCC push_options
1354
+ #pragma GCC optimize("no-tree-vectorize", "no-tree-slp-vectorize", "no-ipa-cp-clone", "no-inline")
1355
+ #endif
1356
+
1318
1357
  NK_PUBLIC void nk_e2m3_to_f32_serial(nk_e2m3_t const *src, nk_f32_t *dest) {
1319
1358
  static nk_u32_t const lut[32] = {
1320
1359
  0x00000000, 0x3E000000, 0x3E800000, 0x3EC00000, 0x3F000000, 0x3F200000, 0x3F400000, 0x3F600000, // exp=0 sub
@@ -1415,6 +1454,12 @@ NK_PUBLIC void nk_f32_to_e2m3_serial(nk_f32_t const *src, nk_e2m3_t *dest) {
1415
1454
  *dest = (nk_e2m3_t)(sign | (exp_field << 3) | mant_field);
1416
1455
  }
1417
1456
 
1457
+ #if defined(__clang__)
1458
+ #pragma clang attribute pop
1459
+ #elif defined(__GNUC__)
1460
+ #pragma GCC pop_options
1461
+ #endif
1462
+
1418
1463
  /**
1419
1464
  * @brief Convert FP6 E3M2FN to IEEE 754 single-precision float.
1420
1465
  *
@@ -1452,6 +1497,13 @@ NK_INTERNAL void nk_e3m2_to_f32_manual_(nk_e3m2_t const *src, nk_f32_t *dest) {
1452
1497
  *dest = conv.f;
1453
1498
  }
1454
1499
 
1500
+ #if defined(__clang__)
1501
+ #pragma clang attribute push(__attribute__((noinline)), apply_to = function)
1502
+ #elif defined(__GNUC__)
1503
+ #pragma GCC push_options
1504
+ #pragma GCC optimize("no-tree-vectorize", "no-tree-slp-vectorize", "no-ipa-cp-clone", "no-inline")
1505
+ #endif
1506
+
1455
1507
  NK_PUBLIC void nk_e3m2_to_f32_serial(nk_e3m2_t const *src, nk_f32_t *dest) {
1456
1508
  static nk_u32_t const lut[32] = {
1457
1509
  0x00000000, 0x3D800000, 0x3E000000, 0x3E400000, // exp=0 sub
@@ -1557,6 +1609,12 @@ NK_PUBLIC void nk_f32_to_e3m2_serial(nk_f32_t const *src, nk_e3m2_t *dest) {
1557
1609
  *dest = (nk_e3m2_t)(sign | (exp_field << 2) | mant_field);
1558
1610
  }
1559
1611
 
1612
+ #if defined(__clang__)
1613
+ #pragma clang attribute pop
1614
+ #elif defined(__GNUC__)
1615
+ #pragma GCC pop_options
1616
+ #endif
1617
+
1560
1618
  NK_INTERNAL void nk_f16_to_f64_serial(nk_f16_t const *x, nk_f64_t *y) {
1561
1619
  nk_f32_t f32;
1562
1620
  nk_f16_to_f32_serial(x, &f32);
@@ -1731,6 +1789,13 @@ NK_INTERNAL void nk_u64_to_bf16_serial(nk_u64_t const *x, nk_bf16_t *y) {
1731
1789
  nk_f32_to_bf16_serial(&f32, y);
1732
1790
  }
1733
1791
 
1792
+ #if defined(__clang__)
1793
+ #pragma clang attribute push(__attribute__((noinline)), apply_to = function)
1794
+ #elif defined(__GNUC__)
1795
+ #pragma GCC push_options
1796
+ #pragma GCC optimize("no-tree-vectorize", "no-tree-slp-vectorize", "no-ipa-cp-clone", "no-inline")
1797
+ #endif
1798
+
1734
1799
  /** @brief Convert a pair of i4 (4-bit signed integer, -8 to 7) nibbles into signed integers. */
1735
1800
  NK_PUBLIC void nk_i4x2_to_i8x2_serial(nk_i4x2_t const *src, nk_i8_t *dest) {
1736
1801
  nk_u8_t byte = *(nk_u8_t const *)src;
@@ -1776,6 +1841,12 @@ typedef union NK_MAY_ALIAS_ nk_scalar_buffer_t {
1776
1841
  nk_u8_t u8;
1777
1842
  } nk_scalar_buffer_t;
1778
1843
 
1844
+ #if defined(__clang__)
1845
+ #pragma clang attribute pop
1846
+ #elif defined(__GNUC__)
1847
+ #pragma GCC pop_options
1848
+ #endif
1849
+
1779
1850
  /** @brief Reads a typed scalar from @p buf and writes the widened f64c into @p result.
1780
1851
  * Real types set `.imag = 0`. Safe when @p result aliases @p buf (in-place conversion).
1781
1852
  * @return 1 on success, 0 for unsupported types (sub-byte, unknown). */
@@ -2247,6 +2318,13 @@ NK_INTERNAL int nk_scalar_buffer_from_f64(nk_f64_t const *value, nk_scalar_buffe
2247
2318
 
2248
2319
  #pragma region Public API
2249
2320
 
2321
+ #if defined(__clang__)
2322
+ #pragma clang attribute push(__attribute__((noinline)), apply_to = function)
2323
+ #elif defined(__GNUC__)
2324
+ #pragma GCC push_options
2325
+ #pragma GCC optimize("no-tree-vectorize", "no-tree-slp-vectorize", "no-ipa-cp-clone", "no-inline")
2326
+ #endif
2327
+
2250
2328
  NK_PUBLIC void nk_cast_serial(void const *from, nk_dtype_t from_type, nk_size_t n, void *to, nk_dtype_t to_type) {
2251
2329
  if (from_type == to_type) {
2252
2330
  nk_size_t size_bits = nk_dtype_bits(from_type);