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.
- package/README.md +2 -2
- package/c/lib.cpp +28 -13
- package/cpp/test.cpp +99 -0
- package/include/usearch/index.hpp +338 -57
- package/include/usearch/index_dense.hpp +155 -50
- package/include/usearch/index_plugins.hpp +94 -35
- 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 +1 -1
- 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
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @brief CUDA interop test: tensor memcpy round-trips + fp6/fp8 cast conformance.
|
|
3
|
+
* @file test/test.cu
|
|
4
|
+
* @author Ash Vardanian
|
|
5
|
+
* @date April 15, 2026
|
|
6
|
+
*
|
|
7
|
+
* The first half drives cudaMemcpy, cudaMemcpy2D (pitched), and cudaMemcpy3D round-trips through
|
|
8
|
+
* nk::tensor_view and runs a trivial add-one kernel to prove device-side readability. The second half
|
|
9
|
+
* does an exhaustive bit-exact comparison between nk_cast on the CPU and CUDA's __nv_cvt_* intrinsics
|
|
10
|
+
* on the GPU, covering every fp32, fp16, and bf16 input against every e4m3, e5m2, e3m2, and e2m3 variant.
|
|
11
|
+
*
|
|
12
|
+
* The test builds and runs on any Turing-or-newer GPU. CUDA's __nv_cvt_* converters fall back to software
|
|
13
|
+
* emulation below SM_89 for fp8 and below SM_100 for fp6, and PTX JIT forward-compiles to newer hardware,
|
|
14
|
+
* so a single binary works from Turing through Blackwell. Override CMAKE_CUDA_ARCHITECTURES at configure
|
|
15
|
+
* time to target a specific compute capability or to build a fat binary.
|
|
16
|
+
*/
|
|
17
|
+
#include <cinttypes>
|
|
18
|
+
#include <cstdint>
|
|
19
|
+
#include <cstdio>
|
|
20
|
+
#include <cstdlib>
|
|
21
|
+
#include <cstring>
|
|
22
|
+
#include <random>
|
|
23
|
+
#include <vector>
|
|
24
|
+
|
|
25
|
+
#if !defined(_WIN32)
|
|
26
|
+
#include <unistd.h>
|
|
27
|
+
#endif
|
|
28
|
+
|
|
29
|
+
#include <cuda_runtime.h>
|
|
30
|
+
#include <cuda_bf16.h>
|
|
31
|
+
#include <cuda_fp16.h>
|
|
32
|
+
#include <cuda_fp8.h>
|
|
33
|
+
#include <cuda_fp6.h>
|
|
34
|
+
|
|
35
|
+
#include <numkong/numkong.h>
|
|
36
|
+
#include <numkong/tensor.hpp>
|
|
37
|
+
|
|
38
|
+
namespace nk = ashvardanian::numkong;
|
|
39
|
+
|
|
40
|
+
using nk::bf16_t;
|
|
41
|
+
using nk::e2m3_t;
|
|
42
|
+
using nk::e3m2_t;
|
|
43
|
+
using nk::e4m3_t;
|
|
44
|
+
using nk::e5m2_t;
|
|
45
|
+
using nk::f16_t;
|
|
46
|
+
using nk::f32_t;
|
|
47
|
+
|
|
48
|
+
#pragma region Output Helpers
|
|
49
|
+
|
|
50
|
+
/** @brief Detect whether stdout supports ANSI colors (kept in sync with test/test.cpp:59-70). */
|
|
51
|
+
static bool colors_enabled_() {
|
|
52
|
+
static bool const result = [] {
|
|
53
|
+
if (std::getenv("NO_COLOR")) return false;
|
|
54
|
+
if (std::getenv("FORCE_COLOR")) return true;
|
|
55
|
+
#if !defined(_WIN32)
|
|
56
|
+
return isatty(fileno(stdout)) != 0;
|
|
57
|
+
#else
|
|
58
|
+
return false;
|
|
59
|
+
#endif
|
|
60
|
+
}();
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @brief Print ● (filled) for pass, ○ (hollow) for fail. Colored when stdout is a TTY. */
|
|
65
|
+
static void print_indicator_(bool on) {
|
|
66
|
+
if (on) std::printf(colors_enabled_() ? "\033[32m\xe2\x97\x8f\033[0m" : "\xe2\x97\x8f");
|
|
67
|
+
else std::printf(colors_enabled_() ? "\033[31m\xe2\x97\x8b\033[0m" : "\xe2\x97\x8b");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#pragma endregion
|
|
71
|
+
|
|
72
|
+
#pragma region CUDA Error Handling
|
|
73
|
+
|
|
74
|
+
/** @brief Return true on success; on failure, print a diagnostic and return false. */
|
|
75
|
+
static bool cuda_check_(cudaError_t error, char const *expression, char const *file, int line) {
|
|
76
|
+
if (error == cudaSuccess) return true;
|
|
77
|
+
std::fprintf(stderr, "CUDA error %s at %s:%d: %s\n", expression, file, line, cudaGetErrorString(error));
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @brief Early-return assertion: on failure prints a diagnostic and returns
|
|
83
|
+
* `false` from the enclosing function. Every `test_*_` / `sweep_*_` here returns bool.
|
|
84
|
+
*/
|
|
85
|
+
#define nk_cuda_assert_(expression) \
|
|
86
|
+
do { \
|
|
87
|
+
if (!cuda_check_((expression), #expression, __FILE__, __LINE__)) return false; \
|
|
88
|
+
} while (0)
|
|
89
|
+
|
|
90
|
+
#pragma endregion
|
|
91
|
+
|
|
92
|
+
#pragma region Tensor Round-Trip Tests
|
|
93
|
+
|
|
94
|
+
/** @brief 1D contiguous round-trip via cudaMemcpy; bit-exact compare. */
|
|
95
|
+
static bool test_memcpy_1d_roundtrip_() {
|
|
96
|
+
constexpr std::size_t count = 1 << 14;
|
|
97
|
+
auto host_source = nk::tensor<float>::try_zeros({count});
|
|
98
|
+
auto host_destination = nk::tensor<float>::try_zeros({count});
|
|
99
|
+
if (host_source.empty() || host_destination.empty()) return false;
|
|
100
|
+
|
|
101
|
+
std::mt19937 generator(42);
|
|
102
|
+
std::uniform_real_distribution<float> distribution(-100.0f, 100.0f);
|
|
103
|
+
for (std::size_t i = 0; i < count; ++i) host_source[i] = distribution(generator);
|
|
104
|
+
|
|
105
|
+
float *device_pointer = nullptr;
|
|
106
|
+
nk_cuda_assert_(cudaMalloc(&device_pointer, count * sizeof(float)));
|
|
107
|
+
nk_cuda_assert_(cudaMemcpy(device_pointer, host_source.data(), count * sizeof(float), cudaMemcpyHostToDevice));
|
|
108
|
+
nk_cuda_assert_(cudaMemcpy(host_destination.data(), device_pointer, count * sizeof(float), cudaMemcpyDeviceToHost));
|
|
109
|
+
nk_cuda_assert_(cudaFree(device_pointer));
|
|
110
|
+
|
|
111
|
+
for (std::size_t i = 0; i < count; ++i)
|
|
112
|
+
if (host_source[i] != host_destination[i]) return false;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @brief 2D round-trip with padded rows: logical `columns` elements per row,
|
|
118
|
+
* but rows are padded so `stride_bytes(0) > columns·sizeof(T)`. That stride
|
|
119
|
+
* drops straight into `cudaMemcpy2D`'s host pitch.
|
|
120
|
+
*/
|
|
121
|
+
static bool test_memcpy_2d_padded_rows_() {
|
|
122
|
+
constexpr std::size_t rows = 32;
|
|
123
|
+
constexpr std::size_t columns = 80;
|
|
124
|
+
constexpr std::size_t row_elements_padded = 128;
|
|
125
|
+
|
|
126
|
+
auto host_source = nk::tensor<float>::try_zeros({rows, row_elements_padded});
|
|
127
|
+
auto host_destination = nk::tensor<float>::try_zeros({rows, row_elements_padded});
|
|
128
|
+
if (host_source.empty() || host_destination.empty()) return false;
|
|
129
|
+
|
|
130
|
+
std::mt19937 generator(7);
|
|
131
|
+
std::uniform_real_distribution<float> distribution(-1.0f, 1.0f);
|
|
132
|
+
for (std::size_t row = 0; row < rows; ++row)
|
|
133
|
+
for (std::size_t column = 0; column < columns; ++column) host_source(row, column) = distribution(generator);
|
|
134
|
+
|
|
135
|
+
std::size_t const host_pitch = static_cast<std::size_t>(host_source.stride_bytes(0));
|
|
136
|
+
|
|
137
|
+
float *device_pointer = nullptr;
|
|
138
|
+
std::size_t device_pitch = 0;
|
|
139
|
+
nk_cuda_assert_(cudaMallocPitch(&device_pointer, &device_pitch, columns * sizeof(float), rows));
|
|
140
|
+
nk_cuda_assert_(cudaMemcpy2D(device_pointer, device_pitch, host_source.data(), host_pitch, columns * sizeof(float),
|
|
141
|
+
rows, cudaMemcpyHostToDevice));
|
|
142
|
+
nk_cuda_assert_(cudaMemcpy2D(host_destination.data(), host_pitch, device_pointer, device_pitch,
|
|
143
|
+
columns * sizeof(float), rows, cudaMemcpyDeviceToHost));
|
|
144
|
+
nk_cuda_assert_(cudaFree(device_pointer));
|
|
145
|
+
|
|
146
|
+
for (std::size_t row = 0; row < rows; ++row)
|
|
147
|
+
for (std::size_t column = 0; column < columns; ++column)
|
|
148
|
+
if (host_source(row, column) != host_destination(row, column)) return false;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @brief 3D batched round-trip via cudaMemcpy3D + cudaPitchedPtr. */
|
|
153
|
+
static bool test_memcpy_3d_batched_() {
|
|
154
|
+
constexpr std::size_t batches = 4, rows = 8, columns = 16;
|
|
155
|
+
auto host_source = nk::tensor<float>::try_zeros({batches, rows, columns});
|
|
156
|
+
auto host_destination = nk::tensor<float>::try_zeros({batches, rows, columns});
|
|
157
|
+
if (host_source.empty() || host_destination.empty()) return false;
|
|
158
|
+
|
|
159
|
+
std::mt19937 generator(13);
|
|
160
|
+
std::uniform_real_distribution<float> distribution(-5.0f, 5.0f);
|
|
161
|
+
for (std::size_t batch = 0; batch < batches; ++batch)
|
|
162
|
+
for (std::size_t row = 0; row < rows; ++row)
|
|
163
|
+
for (std::size_t column = 0; column < columns; ++column)
|
|
164
|
+
host_source(batch, row, column) = distribution(generator);
|
|
165
|
+
|
|
166
|
+
cudaPitchedPtr device_pitched;
|
|
167
|
+
cudaExtent const extent = make_cudaExtent(columns * sizeof(float), rows, batches);
|
|
168
|
+
nk_cuda_assert_(cudaMalloc3D(&device_pitched, extent));
|
|
169
|
+
|
|
170
|
+
// Tightly-packed host: stride_bytes(1) = row pitch, stride_bytes(0) = slice pitch.
|
|
171
|
+
cudaMemcpy3DParms host_to_device {};
|
|
172
|
+
host_to_device.srcPtr = make_cudaPitchedPtr(host_source.data(), columns * sizeof(float), columns, rows);
|
|
173
|
+
host_to_device.dstPtr = device_pitched;
|
|
174
|
+
host_to_device.extent = extent;
|
|
175
|
+
host_to_device.kind = cudaMemcpyHostToDevice;
|
|
176
|
+
nk_cuda_assert_(cudaMemcpy3D(&host_to_device));
|
|
177
|
+
|
|
178
|
+
cudaMemcpy3DParms device_to_host {};
|
|
179
|
+
device_to_host.srcPtr = device_pitched;
|
|
180
|
+
device_to_host.dstPtr = make_cudaPitchedPtr(host_destination.data(), columns * sizeof(float), columns, rows);
|
|
181
|
+
device_to_host.extent = extent;
|
|
182
|
+
device_to_host.kind = cudaMemcpyDeviceToHost;
|
|
183
|
+
nk_cuda_assert_(cudaMemcpy3D(&device_to_host));
|
|
184
|
+
nk_cuda_assert_(cudaFree(device_pitched.ptr));
|
|
185
|
+
|
|
186
|
+
for (std::size_t batch = 0; batch < batches; ++batch)
|
|
187
|
+
for (std::size_t row = 0; row < rows; ++row)
|
|
188
|
+
for (std::size_t column = 0; column < columns; ++column)
|
|
189
|
+
if (host_source(batch, row, column) != host_destination(batch, row, column)) return false;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @brief 2D sub-rectangle extraction from a parent tensor via cudaMemcpy2D.
|
|
195
|
+
* The sub-view keeps the parent row pitch; only extents shrink.
|
|
196
|
+
*/
|
|
197
|
+
static bool test_memcpy_2d_subview_() {
|
|
198
|
+
constexpr std::size_t parent_rows = 16, parent_columns = 32;
|
|
199
|
+
constexpr std::size_t row_start = 4, column_start = 8;
|
|
200
|
+
constexpr std::size_t sub_rows = 8, sub_columns = 16;
|
|
201
|
+
|
|
202
|
+
auto parent = nk::tensor<float>::try_zeros({parent_rows, parent_columns});
|
|
203
|
+
if (parent.empty()) return false;
|
|
204
|
+
for (std::size_t row = 0; row < parent_rows; ++row)
|
|
205
|
+
for (std::size_t column = 0; column < parent_columns; ++column)
|
|
206
|
+
parent(row, column) = static_cast<float>(row * parent_columns + column);
|
|
207
|
+
|
|
208
|
+
float const *sub_source_pointer = &parent(row_start, column_start);
|
|
209
|
+
std::size_t const parent_row_pitch = static_cast<std::size_t>(parent.stride_bytes(0));
|
|
210
|
+
|
|
211
|
+
auto host_back = nk::tensor<float>::try_zeros({sub_rows, sub_columns});
|
|
212
|
+
if (host_back.empty()) return false;
|
|
213
|
+
|
|
214
|
+
float *device_pointer = nullptr;
|
|
215
|
+
std::size_t device_pitch = 0;
|
|
216
|
+
nk_cuda_assert_(cudaMallocPitch(&device_pointer, &device_pitch, sub_columns * sizeof(float), sub_rows));
|
|
217
|
+
nk_cuda_assert_(cudaMemcpy2D(device_pointer, device_pitch, sub_source_pointer, parent_row_pitch,
|
|
218
|
+
sub_columns * sizeof(float), sub_rows, cudaMemcpyHostToDevice));
|
|
219
|
+
nk_cuda_assert_(cudaMemcpy2D(host_back.data(), host_back.stride_bytes(0), device_pointer, device_pitch,
|
|
220
|
+
sub_columns * sizeof(float), sub_rows, cudaMemcpyDeviceToHost));
|
|
221
|
+
nk_cuda_assert_(cudaFree(device_pointer));
|
|
222
|
+
|
|
223
|
+
for (std::size_t row = 0; row < sub_rows; ++row)
|
|
224
|
+
for (std::size_t column = 0; column < sub_columns; ++column)
|
|
225
|
+
if (host_back(row, column) != parent(row_start + row, column_start + column)) return false;
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @brief Trivial add-1.0 kernel used to prove device-side data is readable/writable. */
|
|
230
|
+
__global__ void add_one_kernel_(float *data, std::size_t rows, std::size_t columns, std::size_t pitch_bytes) {
|
|
231
|
+
std::size_t row = blockIdx.y * blockDim.y + threadIdx.y;
|
|
232
|
+
std::size_t column = blockIdx.x * blockDim.x + threadIdx.x;
|
|
233
|
+
if (row >= rows || column >= columns) return;
|
|
234
|
+
float *row_pointer = reinterpret_cast<float *>(reinterpret_cast<char *>(data) + row * pitch_bytes);
|
|
235
|
+
row_pointer[column] += 1.0f;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @brief Upload, add-one on device, download, verify every element incremented by 1. */
|
|
239
|
+
static bool test_device_kernel_usability_() {
|
|
240
|
+
constexpr std::size_t rows = 24, columns = 48;
|
|
241
|
+
auto host_source = nk::tensor<float>::try_zeros({rows, columns});
|
|
242
|
+
auto host_destination = nk::tensor<float>::try_zeros({rows, columns});
|
|
243
|
+
if (host_source.empty() || host_destination.empty()) return false;
|
|
244
|
+
for (std::size_t row = 0; row < rows; ++row)
|
|
245
|
+
for (std::size_t column = 0; column < columns; ++column)
|
|
246
|
+
host_source(row, column) = static_cast<float>((row * columns + column) % 17) * 0.25f;
|
|
247
|
+
|
|
248
|
+
float *device_pointer = nullptr;
|
|
249
|
+
std::size_t device_pitch = 0;
|
|
250
|
+
nk_cuda_assert_(cudaMallocPitch(&device_pointer, &device_pitch, columns * sizeof(float), rows));
|
|
251
|
+
nk_cuda_assert_(cudaMemcpy2D(device_pointer, device_pitch, host_source.data(), host_source.stride_bytes(0),
|
|
252
|
+
columns * sizeof(float), rows, cudaMemcpyHostToDevice));
|
|
253
|
+
|
|
254
|
+
dim3 block(16, 8);
|
|
255
|
+
dim3 grid((columns + block.x - 1) / block.x, (rows + block.y - 1) / block.y);
|
|
256
|
+
add_one_kernel_<<<grid, block>>>(device_pointer, rows, columns, device_pitch);
|
|
257
|
+
nk_cuda_assert_(cudaGetLastError());
|
|
258
|
+
nk_cuda_assert_(cudaDeviceSynchronize());
|
|
259
|
+
|
|
260
|
+
nk_cuda_assert_(cudaMemcpy2D(host_destination.data(), host_destination.stride_bytes(0), device_pointer,
|
|
261
|
+
device_pitch, columns * sizeof(float), rows, cudaMemcpyDeviceToHost));
|
|
262
|
+
nk_cuda_assert_(cudaFree(device_pointer));
|
|
263
|
+
|
|
264
|
+
for (std::size_t row = 0; row < rows; ++row)
|
|
265
|
+
for (std::size_t column = 0; column < columns; ++column)
|
|
266
|
+
if (host_destination(row, column) != host_source(row, column) + 1.0f) return false;
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#pragma endregion
|
|
271
|
+
|
|
272
|
+
#pragma region FP8 / FP6 Conversion Kernels
|
|
273
|
+
|
|
274
|
+
/** @brief Batch fp32 → fp8 conversion via __nv_cvt_float_to_fp8. */
|
|
275
|
+
__global__ void fp32_to_fp8_kernel_(float const *source, unsigned char *destination, std::size_t count,
|
|
276
|
+
__nv_fp8_interpretation_t interpretation, __nv_saturation_t saturate) {
|
|
277
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
278
|
+
if (index >= count) return;
|
|
279
|
+
destination[index] = __nv_cvt_float_to_fp8(source[index], saturate, interpretation);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** @brief Batch fp16 → fp8 conversion via __nv_cvt_halfraw_to_fp8. */
|
|
283
|
+
__global__ void fp16_to_fp8_kernel_(__half const *source, unsigned char *destination, std::size_t count,
|
|
284
|
+
__nv_fp8_interpretation_t interpretation, __nv_saturation_t saturate) {
|
|
285
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
286
|
+
if (index >= count) return;
|
|
287
|
+
__half_raw raw;
|
|
288
|
+
std::memcpy(&raw, &source[index], sizeof raw);
|
|
289
|
+
destination[index] = __nv_cvt_halfraw_to_fp8(raw, saturate, interpretation);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** @brief Batch bf16 → fp8 conversion via __nv_cvt_bfloat16raw_to_fp8. */
|
|
293
|
+
__global__ void bf16_to_fp8_kernel_(__nv_bfloat16 const *source, unsigned char *destination, std::size_t count,
|
|
294
|
+
__nv_fp8_interpretation_t interpretation, __nv_saturation_t saturate) {
|
|
295
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
296
|
+
if (index >= count) return;
|
|
297
|
+
__nv_bfloat16_raw raw;
|
|
298
|
+
std::memcpy(&raw, &source[index], sizeof raw);
|
|
299
|
+
destination[index] = __nv_cvt_bfloat16raw_to_fp8(raw, saturate, interpretation);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** @brief Batch fp8 → fp32 conversion (routed via halfraw; CUDA has no direct fp8→fp32). */
|
|
303
|
+
__global__ void fp8_to_fp32_kernel_(unsigned char const *source, float *destination, std::size_t count,
|
|
304
|
+
__nv_fp8_interpretation_t interpretation) {
|
|
305
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
306
|
+
if (index >= count) return;
|
|
307
|
+
__half_raw half_raw = __nv_cvt_fp8_to_halfraw(source[index], interpretation);
|
|
308
|
+
__half half_value;
|
|
309
|
+
std::memcpy(&half_value, &half_raw, sizeof half_value);
|
|
310
|
+
destination[index] = __half2float(half_value);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** @brief Batch fp8 → fp16 conversion via __nv_cvt_fp8_to_halfraw. */
|
|
314
|
+
__global__ void fp8_to_fp16_kernel_(unsigned char const *source, __half *destination, std::size_t count,
|
|
315
|
+
__nv_fp8_interpretation_t interpretation) {
|
|
316
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
317
|
+
if (index >= count) return;
|
|
318
|
+
__half_raw half_raw = __nv_cvt_fp8_to_halfraw(source[index], interpretation);
|
|
319
|
+
std::memcpy(&destination[index], &half_raw, sizeof half_raw);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** @brief Batch fp8 → bf16 conversion (CUDA has no direct path; routes fp8→halfraw→fp32→bf16). */
|
|
323
|
+
__global__ void fp8_to_bf16_kernel_(unsigned char const *source, __nv_bfloat16 *destination, std::size_t count,
|
|
324
|
+
__nv_fp8_interpretation_t interpretation) {
|
|
325
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
326
|
+
if (index >= count) return;
|
|
327
|
+
__half_raw half_raw = __nv_cvt_fp8_to_halfraw(source[index], interpretation);
|
|
328
|
+
__half half_value;
|
|
329
|
+
std::memcpy(&half_value, &half_raw, sizeof half_value);
|
|
330
|
+
destination[index] = __float2bfloat16(__half2float(half_value));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** @brief Batch fp32 → fp6 conversion via __nv_cvt_float_to_fp6. */
|
|
334
|
+
__global__ void fp32_to_fp6_kernel_(float const *source, unsigned char *destination, std::size_t count,
|
|
335
|
+
__nv_fp6_interpretation_t interpretation) {
|
|
336
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
337
|
+
if (index >= count) return;
|
|
338
|
+
destination[index] = __nv_cvt_float_to_fp6(source[index], interpretation, cudaRoundNearest);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** @brief Batch fp16 → fp6 conversion via __nv_cvt_halfraw_to_fp6. */
|
|
342
|
+
__global__ void fp16_to_fp6_kernel_(__half const *source, unsigned char *destination, std::size_t count,
|
|
343
|
+
__nv_fp6_interpretation_t interpretation) {
|
|
344
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
345
|
+
if (index >= count) return;
|
|
346
|
+
__half_raw raw;
|
|
347
|
+
std::memcpy(&raw, &source[index], sizeof raw);
|
|
348
|
+
destination[index] = __nv_cvt_halfraw_to_fp6(raw, interpretation, cudaRoundNearest);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** @brief Batch bf16 → fp6 conversion via __nv_cvt_bfloat16raw_to_fp6. */
|
|
352
|
+
__global__ void bf16_to_fp6_kernel_(__nv_bfloat16 const *source, unsigned char *destination, std::size_t count,
|
|
353
|
+
__nv_fp6_interpretation_t interpretation) {
|
|
354
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
355
|
+
if (index >= count) return;
|
|
356
|
+
__nv_bfloat16_raw raw;
|
|
357
|
+
std::memcpy(&raw, &source[index], sizeof raw);
|
|
358
|
+
destination[index] = __nv_cvt_bfloat16raw_to_fp6(raw, interpretation, cudaRoundNearest);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** @brief Batch fp6 → fp32 conversion (routed via halfraw). */
|
|
362
|
+
__global__ void fp6_to_fp32_kernel_(unsigned char const *source, float *destination, std::size_t count,
|
|
363
|
+
__nv_fp6_interpretation_t interpretation) {
|
|
364
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
365
|
+
if (index >= count) return;
|
|
366
|
+
__half_raw half_raw = __nv_cvt_fp6_to_halfraw(source[index], interpretation);
|
|
367
|
+
__half half_value;
|
|
368
|
+
std::memcpy(&half_value, &half_raw, sizeof half_value);
|
|
369
|
+
destination[index] = __half2float(half_value);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** @brief Batch fp6 → fp16 conversion via __nv_cvt_fp6_to_halfraw. */
|
|
373
|
+
__global__ void fp6_to_fp16_kernel_(unsigned char const *source, __half *destination, std::size_t count,
|
|
374
|
+
__nv_fp6_interpretation_t interpretation) {
|
|
375
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
376
|
+
if (index >= count) return;
|
|
377
|
+
__half_raw half_raw = __nv_cvt_fp6_to_halfraw(source[index], interpretation);
|
|
378
|
+
std::memcpy(&destination[index], &half_raw, sizeof half_raw);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** @brief Batch fp6 → bf16 conversion (routes fp6→halfraw→fp32→bf16). */
|
|
382
|
+
__global__ void fp6_to_bf16_kernel_(unsigned char const *source, __nv_bfloat16 *destination, std::size_t count,
|
|
383
|
+
__nv_fp6_interpretation_t interpretation) {
|
|
384
|
+
std::size_t index = static_cast<std::size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
|
|
385
|
+
if (index >= count) return;
|
|
386
|
+
__half_raw half_raw = __nv_cvt_fp6_to_halfraw(source[index], interpretation);
|
|
387
|
+
__half half_value;
|
|
388
|
+
std::memcpy(&half_value, &half_raw, sizeof half_value);
|
|
389
|
+
destination[index] = __float2bfloat16(__half2float(half_value));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
#pragma endregion
|
|
393
|
+
|
|
394
|
+
#pragma region NaN Bit-Pattern Predicates
|
|
395
|
+
|
|
396
|
+
/** @brief True iff the 32-bit IEEE-754 pattern encodes any NaN. */
|
|
397
|
+
static bool bits_fp32_nan_(std::uint32_t bits) noexcept {
|
|
398
|
+
return (bits & 0x7F800000u) == 0x7F800000u && (bits & 0x007FFFFFu) != 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** @brief True iff the 16-bit IEEE-754 half pattern encodes any NaN. */
|
|
402
|
+
static bool bits_fp16_nan_(std::uint16_t bits) noexcept { return (bits & 0x7C00u) == 0x7C00u && (bits & 0x03FFu) != 0; }
|
|
403
|
+
|
|
404
|
+
/** @brief True iff the 16-bit bfloat16 pattern encodes any NaN. */
|
|
405
|
+
static bool bits_bf16_nan_(std::uint16_t bits) noexcept { return (bits & 0x7F80u) == 0x7F80u && (bits & 0x007Fu) != 0; }
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @brief NaN-tolerant bit equality: any NaN encoding matches any other NaN
|
|
409
|
+
* of the same width, but the "is a NaN" vs "is a number" bit still matters.
|
|
410
|
+
*/
|
|
411
|
+
static bool f32_bits_equal_tolerant_nan_(float left, float right) {
|
|
412
|
+
std::uint32_t left_bits, right_bits;
|
|
413
|
+
std::memcpy(&left_bits, &left, sizeof left_bits);
|
|
414
|
+
std::memcpy(&right_bits, &right, sizeof right_bits);
|
|
415
|
+
if (bits_fp32_nan_(left_bits) && bits_fp32_nan_(right_bits)) return true;
|
|
416
|
+
return left_bits == right_bits;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** @brief NaN-tolerant bit equality for 16-bit half. */
|
|
420
|
+
static bool f16_bits_equal_tolerant_nan_(__half left, __half right) {
|
|
421
|
+
std::uint16_t left_bits, right_bits;
|
|
422
|
+
std::memcpy(&left_bits, &left, sizeof left_bits);
|
|
423
|
+
std::memcpy(&right_bits, &right, sizeof right_bits);
|
|
424
|
+
if (bits_fp16_nan_(left_bits) && bits_fp16_nan_(right_bits)) return true;
|
|
425
|
+
return left_bits == right_bits;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** @brief NaN-tolerant bit equality for 16-bit bfloat16. */
|
|
429
|
+
static bool bf16_bits_equal_tolerant_nan_(__nv_bfloat16 left, __nv_bfloat16 right) {
|
|
430
|
+
std::uint16_t left_bits, right_bits;
|
|
431
|
+
std::memcpy(&left_bits, &left, sizeof left_bits);
|
|
432
|
+
std::memcpy(&right_bits, &right, sizeof right_bits);
|
|
433
|
+
if (bits_bf16_nan_(left_bits) && bits_bf16_nan_(right_bits)) return true;
|
|
434
|
+
return left_bits == right_bits;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** @brief Masked byte equality; fp6 cares only about the low 6 bits. */
|
|
438
|
+
static bool bytes_equal_masked_(unsigned char left, unsigned char right, unsigned mask) noexcept {
|
|
439
|
+
return (left & mask) == (right & mask);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** @brief Print a single mismatch line to stderr for diagnostics. */
|
|
443
|
+
static void report_mismatch_(char const *label, std::uint64_t source_bits, unsigned char cuda_output,
|
|
444
|
+
unsigned char numkong_output, std::uint64_t index) {
|
|
445
|
+
std::fprintf(stderr, " [%s] input #%" PRIu64 " raw=0x%" PRIx64 " → CUDA=0x%02x NumKong=0x%02x\n", label, index,
|
|
446
|
+
source_bits, cuda_output, numkong_output);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#pragma endregion
|
|
450
|
+
|
|
451
|
+
#pragma region Conversion Sweeps
|
|
452
|
+
|
|
453
|
+
// Batch size: 1M elements per launch keeps device buffers under 16 MB even
|
|
454
|
+
// for 16-byte element types.
|
|
455
|
+
constexpr std::size_t batch_size_ = 1u << 20;
|
|
456
|
+
|
|
457
|
+
/** @brief Upload a batch, launch the kernel, download the result. */
|
|
458
|
+
template <typename source_type_, typename destination_type_, typename kernel_type_>
|
|
459
|
+
static bool run_kernel_batch_(source_type_ const *host_source, destination_type_ *host_destination, std::size_t count,
|
|
460
|
+
kernel_type_ kernel) {
|
|
461
|
+
source_type_ *device_source = nullptr;
|
|
462
|
+
destination_type_ *device_destination = nullptr;
|
|
463
|
+
nk_cuda_assert_(cudaMalloc(&device_source, count * sizeof(source_type_)));
|
|
464
|
+
nk_cuda_assert_(cudaMalloc(&device_destination, count * sizeof(destination_type_)));
|
|
465
|
+
nk_cuda_assert_(cudaMemcpy(device_source, host_source, count * sizeof(source_type_), cudaMemcpyHostToDevice));
|
|
466
|
+
dim3 block(256);
|
|
467
|
+
dim3 grid(static_cast<unsigned>((count + block.x - 1) / block.x));
|
|
468
|
+
kernel(device_source, device_destination, count, grid, block);
|
|
469
|
+
nk_cuda_assert_(cudaGetLastError());
|
|
470
|
+
nk_cuda_assert_(cudaDeviceSynchronize());
|
|
471
|
+
nk_cuda_assert_(
|
|
472
|
+
cudaMemcpy(host_destination, device_destination, count * sizeof(destination_type_), cudaMemcpyDeviceToHost));
|
|
473
|
+
nk_cuda_assert_(cudaFree(device_source));
|
|
474
|
+
nk_cuda_assert_(cudaFree(device_destination));
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* @brief Exhaustive fp32 → fp8 sweep (2^32 inputs) in 1M-element batches.
|
|
480
|
+
*
|
|
481
|
+
* NumKong uses a single rounding mode (RTNE) and one overflow policy per
|
|
482
|
+
* target type: E4M3 saturates (no Inf encoding), E5M2 produces ±Inf. The
|
|
483
|
+
* CUDA `saturate` dial is derived here from `interpretation` rather than
|
|
484
|
+
* plumbed through, so the test exposes only NumKong's fixed mode.
|
|
485
|
+
*/
|
|
486
|
+
template <typename launcher_type_>
|
|
487
|
+
static bool sweep_fp32_to_fp8_(char const *label, nk_dtype_t destination_dtype,
|
|
488
|
+
__nv_fp8_interpretation_t interpretation, launcher_type_ launcher) {
|
|
489
|
+
__nv_saturation_t const saturate = (interpretation == __NV_E4M3) ? __NV_SATFINITE : __NV_NOSAT;
|
|
490
|
+
std::vector<float> host_source(batch_size_);
|
|
491
|
+
std::vector<unsigned char> host_cuda(batch_size_);
|
|
492
|
+
std::vector<unsigned char> host_numkong(batch_size_);
|
|
493
|
+
std::size_t mismatches = 0;
|
|
494
|
+
constexpr std::uint64_t total = 1ull << 32;
|
|
495
|
+
for (std::uint64_t base = 0; base < total; base += batch_size_) {
|
|
496
|
+
std::size_t this_batch = static_cast<std::size_t>(std::min<std::uint64_t>(batch_size_, total - base));
|
|
497
|
+
for (std::size_t i = 0; i < this_batch; ++i) {
|
|
498
|
+
std::uint32_t bits = static_cast<std::uint32_t>(base + i);
|
|
499
|
+
std::memcpy(&host_source[i], &bits, sizeof(float));
|
|
500
|
+
}
|
|
501
|
+
auto kernel = [&](float const *device_source, unsigned char *device_destination, std::size_t n, dim3 grid,
|
|
502
|
+
dim3 block) {
|
|
503
|
+
launcher(device_source, device_destination, n, interpretation, saturate, grid, block);
|
|
504
|
+
};
|
|
505
|
+
if (!run_kernel_batch_(host_source.data(), host_cuda.data(), this_batch, kernel)) return false;
|
|
506
|
+
nk_cast_serial(host_source.data(), nk_f32_k, this_batch, host_numkong.data(), destination_dtype);
|
|
507
|
+
for (std::size_t i = 0; i < this_batch; ++i) {
|
|
508
|
+
std::uint32_t source_bits;
|
|
509
|
+
std::memcpy(&source_bits, &host_source[i], sizeof(float));
|
|
510
|
+
if (bits_fp32_nan_(source_bits)) continue;
|
|
511
|
+
if (!bytes_equal_masked_(host_cuda[i], host_numkong[i], 0xFF)) {
|
|
512
|
+
if (mismatches < 4) report_mismatch_(label, source_bits, host_cuda[i], host_numkong[i], base + i);
|
|
513
|
+
++mismatches;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (mismatches > 0) std::fprintf(stderr, " [%s] total mismatches: %zu\n", label, mismatches);
|
|
518
|
+
return mismatches == 0;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** @brief Exhaustive fp32 → fp6 sweep; only low 6 bits of each output byte matter. */
|
|
522
|
+
template <typename launcher_type_>
|
|
523
|
+
static bool sweep_fp32_to_fp6_(char const *label, nk_dtype_t destination_dtype,
|
|
524
|
+
__nv_fp6_interpretation_t interpretation, launcher_type_ launcher) {
|
|
525
|
+
std::vector<float> host_source(batch_size_);
|
|
526
|
+
std::vector<unsigned char> host_cuda(batch_size_);
|
|
527
|
+
std::vector<unsigned char> host_numkong(batch_size_);
|
|
528
|
+
std::size_t mismatches = 0;
|
|
529
|
+
constexpr std::uint64_t total = 1ull << 32;
|
|
530
|
+
for (std::uint64_t base = 0; base < total; base += batch_size_) {
|
|
531
|
+
std::size_t this_batch = static_cast<std::size_t>(std::min<std::uint64_t>(batch_size_, total - base));
|
|
532
|
+
for (std::size_t i = 0; i < this_batch; ++i) {
|
|
533
|
+
std::uint32_t bits = static_cast<std::uint32_t>(base + i);
|
|
534
|
+
std::memcpy(&host_source[i], &bits, sizeof(float));
|
|
535
|
+
}
|
|
536
|
+
auto kernel = [&](float const *device_source, unsigned char *device_destination, std::size_t n, dim3 grid,
|
|
537
|
+
dim3 block) { launcher(device_source, device_destination, n, interpretation, grid, block); };
|
|
538
|
+
if (!run_kernel_batch_(host_source.data(), host_cuda.data(), this_batch, kernel)) return false;
|
|
539
|
+
nk_cast_serial(host_source.data(), nk_f32_k, this_batch, host_numkong.data(), destination_dtype);
|
|
540
|
+
for (std::size_t i = 0; i < this_batch; ++i) {
|
|
541
|
+
std::uint32_t source_bits;
|
|
542
|
+
std::memcpy(&source_bits, &host_source[i], sizeof(float));
|
|
543
|
+
if (bits_fp32_nan_(source_bits)) continue;
|
|
544
|
+
if (!bytes_equal_masked_(host_cuda[i], host_numkong[i], 0x3F)) {
|
|
545
|
+
if (mismatches < 4) report_mismatch_(label, source_bits, host_cuda[i], host_numkong[i], base + i);
|
|
546
|
+
++mismatches;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (mismatches > 0) std::fprintf(stderr, " [%s] total mismatches: %zu\n", label, mismatches);
|
|
551
|
+
return mismatches == 0;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** @brief Exhaustive 16-bit (fp16 or bf16) → fp{6,8} sweep over all 2^16 inputs. */
|
|
555
|
+
template <typename source_type_, typename launcher_type_>
|
|
556
|
+
static bool sweep_16bit_to_8bit_(char const *label, nk_dtype_t source_dtype, nk_dtype_t destination_dtype,
|
|
557
|
+
bool (*is_nan_fn)(std::uint16_t), unsigned byte_mask, launcher_type_ launcher) {
|
|
558
|
+
constexpr std::size_t count = 1u << 16;
|
|
559
|
+
std::vector<source_type_> host_source(count);
|
|
560
|
+
std::vector<unsigned char> host_cuda(count);
|
|
561
|
+
std::vector<unsigned char> host_numkong(count);
|
|
562
|
+
for (std::size_t i = 0; i < count; ++i) {
|
|
563
|
+
std::uint16_t bits = static_cast<std::uint16_t>(i);
|
|
564
|
+
std::memcpy(&host_source[i], &bits, sizeof(source_type_));
|
|
565
|
+
}
|
|
566
|
+
if (!run_kernel_batch_(host_source.data(), host_cuda.data(), count, launcher)) return false;
|
|
567
|
+
nk_cast_serial(host_source.data(), source_dtype, count, host_numkong.data(), destination_dtype);
|
|
568
|
+
std::size_t mismatches = 0;
|
|
569
|
+
for (std::size_t i = 0; i < count; ++i) {
|
|
570
|
+
std::uint16_t source_bits;
|
|
571
|
+
std::memcpy(&source_bits, &host_source[i], sizeof(source_type_));
|
|
572
|
+
if (is_nan_fn(source_bits)) continue;
|
|
573
|
+
if (!bytes_equal_masked_(host_cuda[i], host_numkong[i], byte_mask)) {
|
|
574
|
+
if (mismatches < 4) report_mismatch_(label, source_bits, host_cuda[i], host_numkong[i], i);
|
|
575
|
+
++mismatches;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (mismatches > 0) std::fprintf(stderr, " [%s] total mismatches: %zu\n", label, mismatches);
|
|
579
|
+
return mismatches == 0;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* @brief Reverse-direction exhaustive sweep: 2^8 (fp8) or 2^6 (fp6) inputs,
|
|
584
|
+
* comparing the wider output via a caller-supplied bit-equality predicate.
|
|
585
|
+
*/
|
|
586
|
+
template <typename destination_type_, typename launcher_type_, typename equals_type_>
|
|
587
|
+
static bool sweep_small_to_wide_(char const *label, nk_dtype_t source_dtype, nk_dtype_t destination_dtype,
|
|
588
|
+
std::size_t domain_size, launcher_type_ launcher, equals_type_ equals) {
|
|
589
|
+
std::vector<unsigned char> host_source(domain_size);
|
|
590
|
+
std::vector<destination_type_> host_cuda(domain_size);
|
|
591
|
+
std::vector<destination_type_> host_numkong(domain_size);
|
|
592
|
+
for (std::size_t i = 0; i < domain_size; ++i) host_source[i] = static_cast<unsigned char>(i);
|
|
593
|
+
if (!run_kernel_batch_(host_source.data(), host_cuda.data(), domain_size, launcher)) return false;
|
|
594
|
+
nk_cast_serial(host_source.data(), source_dtype, domain_size, host_numkong.data(), destination_dtype);
|
|
595
|
+
std::size_t mismatches = 0;
|
|
596
|
+
for (std::size_t i = 0; i < domain_size; ++i) {
|
|
597
|
+
if (!equals(host_cuda[i], host_numkong[i])) {
|
|
598
|
+
if (mismatches < 4) std::fprintf(stderr, " [%s] input 0x%02zx mismatch\n", label, i);
|
|
599
|
+
++mismatches;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (mismatches > 0) std::fprintf(stderr, " [%s] total mismatches: %zu\n", label, mismatches);
|
|
603
|
+
return mismatches == 0;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
#pragma endregion
|
|
607
|
+
|
|
608
|
+
int main() {
|
|
609
|
+
int device = 0;
|
|
610
|
+
if (cudaSetDevice(device) != cudaSuccess) {
|
|
611
|
+
std::fprintf(stderr, "No CUDA device available\n");
|
|
612
|
+
return 1;
|
|
613
|
+
}
|
|
614
|
+
cudaDeviceProp properties {};
|
|
615
|
+
cudaGetDeviceProperties(&properties, device);
|
|
616
|
+
|
|
617
|
+
// Hardware intrinsics: fp8 from SM_89 (Ada), fp6 from SM_100 (Blackwell).
|
|
618
|
+
// Below those thresholds `__nv_cvt_*` falls back to software emulation;
|
|
619
|
+
// the test still produces correct results either way.
|
|
620
|
+
bool const fp8_native = properties.major > 8 || (properties.major == 8 && properties.minor >= 9);
|
|
621
|
+
bool const fp6_native = properties.major >= 10;
|
|
622
|
+
std::printf("NumKong CUDA Interop Test\n");
|
|
623
|
+
std::printf(" GPU: %s (SM %d.%d)\n", properties.name, properties.major, properties.minor);
|
|
624
|
+
std::printf(" FP8 path: ");
|
|
625
|
+
print_indicator_(fp8_native);
|
|
626
|
+
std::printf(" %s\n", fp8_native ? "native (Ada+)" : "software emulation");
|
|
627
|
+
std::printf(" FP6 path: ");
|
|
628
|
+
print_indicator_(fp6_native);
|
|
629
|
+
std::printf(" %s\n", fp6_native ? "native (Blackwell+)" : "software emulation");
|
|
630
|
+
std::printf("\n");
|
|
631
|
+
|
|
632
|
+
int passed = 0, failed = 0;
|
|
633
|
+
auto run_ = [&](char const *label, bool ok) {
|
|
634
|
+
std::printf(" %-44s ", label);
|
|
635
|
+
print_indicator_(ok);
|
|
636
|
+
std::printf("\n");
|
|
637
|
+
(ok ? passed : failed) += 1;
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
std::printf("Tensor memcpy round-trips\n");
|
|
641
|
+
run_("1D round-trip (cudaMemcpy)", test_memcpy_1d_roundtrip_());
|
|
642
|
+
run_("2D round-trip, padded rows (cudaMemcpy2D)", test_memcpy_2d_padded_rows_());
|
|
643
|
+
run_("2D sub-view extraction (cudaMemcpy2D)", test_memcpy_2d_subview_());
|
|
644
|
+
run_("3D batched round-trip (cudaMemcpy3D)", test_memcpy_3d_batched_());
|
|
645
|
+
run_("device kernel usability (add-one)", test_device_kernel_usability_());
|
|
646
|
+
|
|
647
|
+
std::printf("\nFP8 conformance: nk_cast vs __nv_cvt_* (bit-exact)\n");
|
|
648
|
+
run_("fp32 → e4m3 (exhaustive 2^32)",
|
|
649
|
+
sweep_fp32_to_fp8_( //
|
|
650
|
+
"fp32 → e4m3", nk_e4m3_k, __NV_E4M3,
|
|
651
|
+
[](float const *device_source, unsigned char *device_destination, std::size_t count,
|
|
652
|
+
__nv_fp8_interpretation_t interpretation, __nv_saturation_t saturate, dim3 grid, dim3 block) {
|
|
653
|
+
fp32_to_fp8_kernel_<<<grid, block>>>(device_source, device_destination, count, interpretation,
|
|
654
|
+
saturate);
|
|
655
|
+
}));
|
|
656
|
+
run_("fp32 → e5m2 (exhaustive 2^32)",
|
|
657
|
+
sweep_fp32_to_fp8_( //
|
|
658
|
+
"fp32 → e5m2", nk_e5m2_k, __NV_E5M2,
|
|
659
|
+
[](float const *device_source, unsigned char *device_destination, std::size_t count,
|
|
660
|
+
__nv_fp8_interpretation_t interpretation, __nv_saturation_t saturate, dim3 grid, dim3 block) {
|
|
661
|
+
fp32_to_fp8_kernel_<<<grid, block>>>(device_source, device_destination, count, interpretation,
|
|
662
|
+
saturate);
|
|
663
|
+
}));
|
|
664
|
+
run_("fp16 → e4m3 (exhaustive 2^16)",
|
|
665
|
+
sweep_16bit_to_8bit_<__half>( //
|
|
666
|
+
"fp16 → e4m3", nk_f16_k, nk_e4m3_k, bits_fp16_nan_, 0xFF,
|
|
667
|
+
[](__half const *device_source, unsigned char *device_destination, std::size_t count, dim3 grid,
|
|
668
|
+
dim3 block) {
|
|
669
|
+
fp16_to_fp8_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E4M3,
|
|
670
|
+
__NV_SATFINITE);
|
|
671
|
+
}));
|
|
672
|
+
run_("fp16 → e5m2 (exhaustive 2^16)",
|
|
673
|
+
sweep_16bit_to_8bit_<__half>( //
|
|
674
|
+
"fp16 → e5m2", nk_f16_k, nk_e5m2_k, bits_fp16_nan_, 0xFF,
|
|
675
|
+
[](__half const *device_source, unsigned char *device_destination, std::size_t count, dim3 grid,
|
|
676
|
+
dim3 block) {
|
|
677
|
+
fp16_to_fp8_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E5M2, __NV_NOSAT);
|
|
678
|
+
}));
|
|
679
|
+
run_("bf16 → e4m3 (exhaustive 2^16)",
|
|
680
|
+
sweep_16bit_to_8bit_<__nv_bfloat16>( //
|
|
681
|
+
"bf16 → e4m3", nk_bf16_k, nk_e4m3_k, bits_bf16_nan_, 0xFF,
|
|
682
|
+
[](__nv_bfloat16 const *device_source, unsigned char *device_destination, std::size_t count, dim3 grid,
|
|
683
|
+
dim3 block) {
|
|
684
|
+
bf16_to_fp8_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E4M3,
|
|
685
|
+
__NV_SATFINITE);
|
|
686
|
+
}));
|
|
687
|
+
run_("bf16 → e5m2 (exhaustive 2^16)",
|
|
688
|
+
sweep_16bit_to_8bit_<__nv_bfloat16>( //
|
|
689
|
+
"bf16 → e5m2", nk_bf16_k, nk_e5m2_k, bits_bf16_nan_, 0xFF,
|
|
690
|
+
[](__nv_bfloat16 const *device_source, unsigned char *device_destination, std::size_t count, dim3 grid,
|
|
691
|
+
dim3 block) {
|
|
692
|
+
bf16_to_fp8_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E5M2, __NV_NOSAT);
|
|
693
|
+
}));
|
|
694
|
+
|
|
695
|
+
std::printf("\nFP8 reverse: fp{32,16,bf16} ← e{4m3,5m2} (bit-exact)\n");
|
|
696
|
+
run_("e4m3 → fp32 (exhaustive 2^8)",
|
|
697
|
+
sweep_small_to_wide_<float>( //
|
|
698
|
+
"e4m3 → fp32", nk_e4m3_k, nk_f32_k, 256u,
|
|
699
|
+
[](unsigned char const *device_source, float *device_destination, std::size_t count, dim3 grid,
|
|
700
|
+
dim3 block) {
|
|
701
|
+
fp8_to_fp32_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E4M3);
|
|
702
|
+
},
|
|
703
|
+
f32_bits_equal_tolerant_nan_));
|
|
704
|
+
run_("e5m2 → fp32 (exhaustive 2^8)",
|
|
705
|
+
sweep_small_to_wide_<float>( //
|
|
706
|
+
"e5m2 → fp32", nk_e5m2_k, nk_f32_k, 256u,
|
|
707
|
+
[](unsigned char const *device_source, float *device_destination, std::size_t count, dim3 grid,
|
|
708
|
+
dim3 block) {
|
|
709
|
+
fp8_to_fp32_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E5M2);
|
|
710
|
+
},
|
|
711
|
+
f32_bits_equal_tolerant_nan_));
|
|
712
|
+
run_("e4m3 → fp16 (exhaustive 2^8)",
|
|
713
|
+
sweep_small_to_wide_<__half>( //
|
|
714
|
+
"e4m3 → fp16", nk_e4m3_k, nk_f16_k, 256u,
|
|
715
|
+
[](unsigned char const *device_source, __half *device_destination, std::size_t count, dim3 grid,
|
|
716
|
+
dim3 block) {
|
|
717
|
+
fp8_to_fp16_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E4M3);
|
|
718
|
+
},
|
|
719
|
+
f16_bits_equal_tolerant_nan_));
|
|
720
|
+
run_("e5m2 → fp16 (exhaustive 2^8)",
|
|
721
|
+
sweep_small_to_wide_<__half>( //
|
|
722
|
+
"e5m2 → fp16", nk_e5m2_k, nk_f16_k, 256u,
|
|
723
|
+
[](unsigned char const *device_source, __half *device_destination, std::size_t count, dim3 grid,
|
|
724
|
+
dim3 block) {
|
|
725
|
+
fp8_to_fp16_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E5M2);
|
|
726
|
+
},
|
|
727
|
+
f16_bits_equal_tolerant_nan_));
|
|
728
|
+
run_("e4m3 → bf16 (exhaustive 2^8)",
|
|
729
|
+
sweep_small_to_wide_<__nv_bfloat16>( //
|
|
730
|
+
"e4m3 → bf16", nk_e4m3_k, nk_bf16_k, 256u,
|
|
731
|
+
[](unsigned char const *device_source, __nv_bfloat16 *device_destination, std::size_t count, dim3 grid,
|
|
732
|
+
dim3 block) {
|
|
733
|
+
fp8_to_bf16_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E4M3);
|
|
734
|
+
},
|
|
735
|
+
bf16_bits_equal_tolerant_nan_));
|
|
736
|
+
run_("e5m2 → bf16 (exhaustive 2^8)",
|
|
737
|
+
sweep_small_to_wide_<__nv_bfloat16>( //
|
|
738
|
+
"e5m2 → bf16", nk_e5m2_k, nk_bf16_k, 256u,
|
|
739
|
+
[](unsigned char const *device_source, __nv_bfloat16 *device_destination, std::size_t count, dim3 grid,
|
|
740
|
+
dim3 block) {
|
|
741
|
+
fp8_to_bf16_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E5M2);
|
|
742
|
+
},
|
|
743
|
+
bf16_bits_equal_tolerant_nan_));
|
|
744
|
+
|
|
745
|
+
std::printf("\nFP6 conformance: nk_cast vs __nv_cvt_* (bit-exact)\n");
|
|
746
|
+
run_("fp32 → e3m2 (exhaustive 2^32)",
|
|
747
|
+
sweep_fp32_to_fp6_( //
|
|
748
|
+
"fp32 → e3m2", nk_e3m2_k, __NV_E3M2,
|
|
749
|
+
[](float const *device_source, unsigned char *device_destination, std::size_t count,
|
|
750
|
+
__nv_fp6_interpretation_t interpretation, dim3 grid, dim3 block) {
|
|
751
|
+
fp32_to_fp6_kernel_<<<grid, block>>>(device_source, device_destination, count, interpretation);
|
|
752
|
+
}));
|
|
753
|
+
run_("fp32 → e2m3 (exhaustive 2^32)",
|
|
754
|
+
sweep_fp32_to_fp6_( //
|
|
755
|
+
"fp32 → e2m3", nk_e2m3_k, __NV_E2M3,
|
|
756
|
+
[](float const *device_source, unsigned char *device_destination, std::size_t count,
|
|
757
|
+
__nv_fp6_interpretation_t interpretation, dim3 grid, dim3 block) {
|
|
758
|
+
fp32_to_fp6_kernel_<<<grid, block>>>(device_source, device_destination, count, interpretation);
|
|
759
|
+
}));
|
|
760
|
+
run_("fp16 → e3m2 (exhaustive 2^16)",
|
|
761
|
+
sweep_16bit_to_8bit_<__half>( //
|
|
762
|
+
"fp16 → e3m2", nk_f16_k, nk_e3m2_k, bits_fp16_nan_, 0x3F,
|
|
763
|
+
[](__half const *device_source, unsigned char *device_destination, std::size_t count, dim3 grid,
|
|
764
|
+
dim3 block) {
|
|
765
|
+
fp16_to_fp6_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E3M2);
|
|
766
|
+
}));
|
|
767
|
+
run_("fp16 → e2m3 (exhaustive 2^16)",
|
|
768
|
+
sweep_16bit_to_8bit_<__half>( //
|
|
769
|
+
"fp16 → e2m3", nk_f16_k, nk_e2m3_k, bits_fp16_nan_, 0x3F,
|
|
770
|
+
[](__half const *device_source, unsigned char *device_destination, std::size_t count, dim3 grid,
|
|
771
|
+
dim3 block) {
|
|
772
|
+
fp16_to_fp6_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E2M3);
|
|
773
|
+
}));
|
|
774
|
+
run_("bf16 → e3m2 (exhaustive 2^16)",
|
|
775
|
+
sweep_16bit_to_8bit_<__nv_bfloat16>( //
|
|
776
|
+
"bf16 → e3m2", nk_bf16_k, nk_e3m2_k, bits_bf16_nan_, 0x3F,
|
|
777
|
+
[](__nv_bfloat16 const *device_source, unsigned char *device_destination, std::size_t count, dim3 grid,
|
|
778
|
+
dim3 block) {
|
|
779
|
+
bf16_to_fp6_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E3M2);
|
|
780
|
+
}));
|
|
781
|
+
run_("bf16 → e2m3 (exhaustive 2^16)",
|
|
782
|
+
sweep_16bit_to_8bit_<__nv_bfloat16>( //
|
|
783
|
+
"bf16 → e2m3", nk_bf16_k, nk_e2m3_k, bits_bf16_nan_, 0x3F,
|
|
784
|
+
[](__nv_bfloat16 const *device_source, unsigned char *device_destination, std::size_t count, dim3 grid,
|
|
785
|
+
dim3 block) {
|
|
786
|
+
bf16_to_fp6_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E2M3);
|
|
787
|
+
}));
|
|
788
|
+
|
|
789
|
+
std::printf("\nFP6 reverse: fp{32,16,bf16} ← e{3m2,2m3} (bit-exact)\n");
|
|
790
|
+
run_("e3m2 → fp32 (exhaustive 2^6)",
|
|
791
|
+
sweep_small_to_wide_<float>( //
|
|
792
|
+
"e3m2 → fp32", nk_e3m2_k, nk_f32_k, 64u,
|
|
793
|
+
[](unsigned char const *device_source, float *device_destination, std::size_t count, dim3 grid,
|
|
794
|
+
dim3 block) {
|
|
795
|
+
fp6_to_fp32_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E3M2);
|
|
796
|
+
},
|
|
797
|
+
f32_bits_equal_tolerant_nan_));
|
|
798
|
+
run_("e2m3 → fp32 (exhaustive 2^6)",
|
|
799
|
+
sweep_small_to_wide_<float>( //
|
|
800
|
+
"e2m3 → fp32", nk_e2m3_k, nk_f32_k, 64u,
|
|
801
|
+
[](unsigned char const *device_source, float *device_destination, std::size_t count, dim3 grid,
|
|
802
|
+
dim3 block) {
|
|
803
|
+
fp6_to_fp32_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E2M3);
|
|
804
|
+
},
|
|
805
|
+
f32_bits_equal_tolerant_nan_));
|
|
806
|
+
run_("e3m2 → fp16 (exhaustive 2^6)",
|
|
807
|
+
sweep_small_to_wide_<__half>( //
|
|
808
|
+
"e3m2 → fp16", nk_e3m2_k, nk_f16_k, 64u,
|
|
809
|
+
[](unsigned char const *device_source, __half *device_destination, std::size_t count, dim3 grid,
|
|
810
|
+
dim3 block) {
|
|
811
|
+
fp6_to_fp16_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E3M2);
|
|
812
|
+
},
|
|
813
|
+
f16_bits_equal_tolerant_nan_));
|
|
814
|
+
run_("e2m3 → fp16 (exhaustive 2^6)",
|
|
815
|
+
sweep_small_to_wide_<__half>( //
|
|
816
|
+
"e2m3 → fp16", nk_e2m3_k, nk_f16_k, 64u,
|
|
817
|
+
[](unsigned char const *device_source, __half *device_destination, std::size_t count, dim3 grid,
|
|
818
|
+
dim3 block) {
|
|
819
|
+
fp6_to_fp16_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E2M3);
|
|
820
|
+
},
|
|
821
|
+
f16_bits_equal_tolerant_nan_));
|
|
822
|
+
run_("e3m2 → bf16 (exhaustive 2^6)",
|
|
823
|
+
sweep_small_to_wide_<__nv_bfloat16>( //
|
|
824
|
+
"e3m2 → bf16", nk_e3m2_k, nk_bf16_k, 64u,
|
|
825
|
+
[](unsigned char const *device_source, __nv_bfloat16 *device_destination, std::size_t count, dim3 grid,
|
|
826
|
+
dim3 block) {
|
|
827
|
+
fp6_to_bf16_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E3M2);
|
|
828
|
+
},
|
|
829
|
+
bf16_bits_equal_tolerant_nan_));
|
|
830
|
+
run_("e2m3 → bf16 (exhaustive 2^6)",
|
|
831
|
+
sweep_small_to_wide_<__nv_bfloat16>( //
|
|
832
|
+
"e2m3 → bf16", nk_e2m3_k, nk_bf16_k, 64u,
|
|
833
|
+
[](unsigned char const *device_source, __nv_bfloat16 *device_destination, std::size_t count, dim3 grid,
|
|
834
|
+
dim3 block) {
|
|
835
|
+
fp6_to_bf16_kernel_<<<grid, block>>>(device_source, device_destination, count, __NV_E2M3);
|
|
836
|
+
},
|
|
837
|
+
bf16_bits_equal_tolerant_nan_));
|
|
838
|
+
|
|
839
|
+
std::printf("\n%d passed, %d failed\n", passed, failed);
|
|
840
|
+
return failed == 0 ? 0 : 1;
|
|
841
|
+
}
|