Open3D (C++ API)  0.20.0
Loading...
Searching...
No Matches
KnnSearchSYCLImpl.h
Go to the documentation of this file.
1// ----------------------------------------------------------------------------
2// - Open3D: www.open3d.org -
3// ----------------------------------------------------------------------------
4// Copyright (c) 2018-2026 www.open3d.org
5// SPDX-License-Identifier: MIT
6// ----------------------------------------------------------------------------
7
47
48#pragma once
49
50#include <algorithm>
51#include <limits>
52#include <oneapi/dpl/algorithm>
53#include <oneapi/dpl/execution>
54#include <sycl/sycl.hpp>
55#include <type_traits>
56
61
62namespace open3d {
63namespace core {
64namespace nns {
65
68
74inline void ChooseTileSize(int64_t num_queries,
75 int64_t num_points,
76 int64_t element_size,
77 int64_t tile_bytes,
78 int64_t& tile_queries,
79 int64_t& tile_points,
80 int64_t max_tile_queries = 128,
81 int64_t tile_points_alignment = 128) {
82 tile_queries = std::min<int64_t>(num_queries, max_tile_queries);
83 tile_queries = std::max<int64_t>(tile_queries, 1);
84 tile_points = std::max<int64_t>(tile_bytes / (tile_queries * element_size),
85 int64_t(256));
86 if (tile_points > tile_points_alignment) {
87 tile_points =
88 (tile_points / tile_points_alignment) * tile_points_alignment;
89 }
90 tile_points = std::min<int64_t>(tile_points, num_points);
91 tile_points = std::max<int64_t>(tile_points, 1);
92}
93
95
99
102template <typename T, typename TIndex, int K>
103inline void HeapifyDown(T* d, TIndex* idx, int root) {
104 while (true) {
105 int left = 2 * root + 1, right = 2 * root + 2, largest = root;
106 if (left < K && (d[left] > d[largest] ||
107 (d[left] == d[largest] && idx[left] > idx[largest])))
108 largest = left;
109 if (right < K && (d[right] > d[largest] || (d[right] == d[largest] &&
110 idx[right] > idx[largest])))
111 largest = right;
112 if (largest == root) break;
113 T td = d[root];
114 d[root] = d[largest];
115 d[largest] = td;
116 TIndex ti = idx[root];
117 idx[root] = idx[largest];
118 idx[largest] = ti;
119 root = largest;
120 }
121}
122
124template <typename T, typename TIndex, int K>
125inline void HeapSort(T* d, TIndex* idx) {
126 for (int end = K - 1; end > 0; --end) {
127 T td = d[0];
128 d[0] = d[end];
129 d[end] = td;
130 TIndex ti = idx[0];
131 idx[0] = idx[end];
132 idx[end] = ti;
133 int root = 0;
134 while (true) {
135 int left = 2 * root + 1, right = 2 * root + 2, largest = root;
136 if (left < end &&
137 (d[left] > d[largest] ||
138 (d[left] == d[largest] && idx[left] > idx[largest])))
139 largest = left;
140 if (right < end &&
141 (d[right] > d[largest] ||
142 (d[right] == d[largest] && idx[right] > idx[largest])))
143 largest = right;
144 if (largest == root) break;
145 T td2 = d[root];
146 d[root] = d[largest];
147 d[largest] = td2;
148 TIndex ti2 = idx[root];
149 idx[root] = idx[largest];
150 idx[largest] = ti2;
151 root = largest;
152 }
153 }
154}
155
157
160
186template <typename T, typename TIndex, int K>
187void UpdateTopKFromTile(sycl::queue& queue,
188 const T* neg2qp_ptr,
189 int64_t distance_stride,
190 const T* point_norms_ptr,
191 int64_t num_queries,
192 int64_t num_points,
193 TIndex point_offset,
194 T* best_dist_ptr,
195 TIndex* best_idx_ptr,
196 bool use_threshold,
197 T threshold) {
198 const size_t wg = core::sy::PreferredWorkGroupSize(queue.get_device());
199 const size_t global_size =
200 ((static_cast<size_t>(num_queries) + wg - 1) / wg) * wg;
201 queue.parallel_for(
202 sycl::nd_range<1>(sycl::range<1>(global_size), sycl::range<1>(wg)),
203 [=](sycl::nd_item<1> it) [[intel::kernel_args_restrict]] {
204 const int64_t q = it.get_global_id(0);
205 if (q >= num_queries) return;
206 const T* qrow = neg2qp_ptr + q * distance_stride;
207 T* qd = best_dist_ptr + q * K;
208 TIndex* qi = best_idx_ptr + q * K;
209
210 // Load running best into private registers (or scratch for
211 // large K).
212 T d[K];
213 TIndex idx[K];
214 for (int i = 0; i < K; ++i) {
215 d[i] = qd[i];
216 idx[i] = qi[i];
217 }
218
219 // Scan: fused |p|² add, heap insert.
220 // Note: partial_dist = −2qp + |p|² may be negative (|q|² not
221 // yet added). Do NOT clamp here; C1 clamping is applied in
222 // FinalizeTopK / GatherWithinThresholdQueries once |q|² is
223 // added back.
224 for (int64_t p = 0; p < num_points; ++p) {
225 const T dist = qrow[p] + point_norms_ptr[p];
226 if (use_threshold && dist > threshold) continue;
227 const TIndex gp = point_offset + static_cast<TIndex>(p);
228 // d[0] = heap root = current k-th worst; insert if better.
229 if (dist < d[0] || (dist == d[0] && gp < idx[0])) {
230 d[0] = dist;
231 idx[0] = gp;
232 HeapifyDown<T, TIndex, K>(d, idx, 0);
233 }
234 }
235
236 for (int i = 0; i < K; ++i) {
237 qd[i] = d[i];
238 qi[i] = idx[i];
239 }
240 });
241}
242
253template <typename T, typename TIndex, int K>
254void FinalizeTopK(sycl::queue& queue,
255 int64_t num_queries,
256 const T* running_dist_ptr,
257 const TIndex* running_idx_ptr,
258 T* out_dist_ptr,
259 TIndex* out_idx_ptr,
260 int64_t actual_k,
261 const T* query_norms_ptr) {
262 const size_t wg = core::sy::PreferredWorkGroupSize(queue.get_device());
263 const size_t global_size =
264 ((static_cast<size_t>(num_queries) + wg - 1) / wg) * wg;
265 queue.parallel_for(
266 sycl::nd_range<1>(sycl::range<1>(global_size), sycl::range<1>(wg)),
267 [=](sycl::nd_item<1> it) [[intel::kernel_args_restrict]] {
268 const int64_t q = it.get_global_id(0);
269 if (q >= num_queries) return;
270 T d[K];
271 TIndex idx[K];
272 for (int i = 0; i < K; ++i) {
273 d[i] = running_dist_ptr[q * K + i];
274 idx[i] = running_idx_ptr[q * K + i];
275 }
276 HeapSort<T, TIndex, K>(d, idx);
277
278 const T qnorm = query_norms_ptr ? query_norms_ptr[q] : T(0);
279 T* qout_d = out_dist_ptr + q * actual_k;
280 TIndex* qout_i = out_idx_ptr + q * actual_k;
281 for (int64_t i = 0; i < actual_k; ++i) {
282 T dist = d[i];
283 if (query_norms_ptr) dist = sycl::fmax(T(0), dist + qnorm);
284 qout_d[i] = dist;
285 qout_i[i] = idx[i];
286 }
287 });
288}
289
291
295
297inline int64_t KBucket(int64_t k) {
298 if (k <= 1) return 1;
299 if (k <= 2) return 2;
300 if (k <= 4) return 4;
301 if (k <= 8) return 8;
302 if (k <= 16) return 16;
303 if (k <= 32) return 32;
304 if (k <= 64) return 64;
305 if (k <= 128) return 128;
306 if (k <= 256) return 256;
307 return 512;
308}
309
311template <typename T, typename TIndex>
313 const T* neg2qp_ptr,
314 int64_t distance_stride,
315 const T* point_norms_ptr,
316 int64_t num_queries,
317 int64_t num_points,
318 int64_t k_bucket,
319 TIndex point_offset,
320 T* best_dist_ptr,
321 TIndex* best_idx_ptr,
322 bool use_threshold,
323 T threshold) {
324#define CALL_UPDATE(Kval) \
325 UpdateTopKFromTile<T, TIndex, Kval>( \
326 queue, neg2qp_ptr, distance_stride, point_norms_ptr, num_queries, \
327 num_points, point_offset, best_dist_ptr, best_idx_ptr, \
328 use_threshold, threshold)
329 if (k_bucket <= 1)
330 CALL_UPDATE(1);
331 else if (k_bucket <= 2)
332 CALL_UPDATE(2);
333 else if (k_bucket <= 4)
334 CALL_UPDATE(4);
335 else if (k_bucket <= 8)
336 CALL_UPDATE(8);
337 else if (k_bucket <= 16)
338 CALL_UPDATE(16);
339 else if (k_bucket <= 32)
340 CALL_UPDATE(32);
341 else if (k_bucket <= 64)
342 CALL_UPDATE(64);
343 else if (k_bucket <= 128)
344 CALL_UPDATE(128);
345 else if (k_bucket <= 256)
346 CALL_UPDATE(256);
347 else
348 CALL_UPDATE(512);
349#undef CALL_UPDATE
350}
351
353template <typename T, typename TIndex>
354void DispatchFinalizeTopK(sycl::queue& queue,
355 int64_t num_queries,
356 const T* running_dist_ptr,
357 const TIndex* running_idx_ptr,
358 T* out_dist_ptr,
359 TIndex* out_idx_ptr,
360 int64_t actual_k,
361 int64_t k_bucket,
362 const T* query_norms_ptr) {
363#define CALL_FINALIZE(Kval) \
364 FinalizeTopK<T, TIndex, Kval>(queue, num_queries, running_dist_ptr, \
365 running_idx_ptr, out_dist_ptr, out_idx_ptr, \
366 actual_k, query_norms_ptr)
367 if (k_bucket <= 1)
368 CALL_FINALIZE(1);
369 else if (k_bucket <= 2)
370 CALL_FINALIZE(2);
371 else if (k_bucket <= 4)
372 CALL_FINALIZE(4);
373 else if (k_bucket <= 8)
374 CALL_FINALIZE(8);
375 else if (k_bucket <= 16)
376 CALL_FINALIZE(16);
377 else if (k_bucket <= 32)
378 CALL_FINALIZE(32);
379 else if (k_bucket <= 64)
380 CALL_FINALIZE(64);
381 else if (k_bucket <= 128)
382 CALL_FINALIZE(128);
383 else if (k_bucket <= 256)
384 CALL_FINALIZE(256);
385 else
386 CALL_FINALIZE(512);
387#undef CALL_FINALIZE
388}
389
391
407
409constexpr int64_t kKnnDirectSubgroupSize = 16;
411constexpr int64_t kKnnDirectSubgroupsPerWG = 32;
413constexpr int64_t kKnnDirectTilePoints = 2048;
415constexpr int64_t kKnnDirectMaxDim = 8;
416
418template <typename T, typename TIndex, int NDIM, int K, int SG>
420
422template <typename T, typename TIndex, int NDIM, int K, int SG>
423void KnnDirect(sycl::queue& queue,
424 const T* points_ptr,
425 const T* queries_ptr,
426 int64_t num_points,
427 int64_t num_queries,
428 int64_t actual_k,
429 T* out_dist_ptr,
430 TIndex* out_idx_ptr,
431 int64_t subgroups_per_wg,
432 int64_t tile_points) {
433 if (num_points <= 0 || num_queries <= 0) return;
434
435 const int64_t wg_size = subgroups_per_wg * SG;
436 const int64_t num_wgs =
437 (num_queries + subgroups_per_wg - 1) / subgroups_per_wg;
438 const int64_t global_size = num_wgs * wg_size;
439 const int64_t tp = std::min<int64_t>(tile_points, num_points);
440 const int64_t num_tiles = (num_points + tp - 1) / tp;
441
442 queue.submit([&](sycl::handler& h) {
443 sycl::local_accessor<T, 1> slm(sycl::range<1>(2 * tp * NDIM), h);
445 sycl::nd_range<1>(sycl::range<1>(global_size),
446 sycl::range<1>(wg_size)),
447 [=](sycl::nd_item<1> it) [[sycl::reqd_sub_group_size(
448 SG)]] [[intel::kernel_args_restrict]] {
449 const auto sg = it.get_sub_group();
450 const int64_t lane = sg.get_local_id()[0];
451 const int64_t sg_id_in_wg = sg.get_group_id()[0];
452 const int64_t wg_id = it.get_group(0);
453 const int64_t local_lin = it.get_local_linear_id();
454 const int64_t local_range = it.get_local_range(0);
455
456 const int64_t query_idx =
457 wg_id * subgroups_per_wg + sg_id_in_wg;
458 const bool active_query = query_idx < num_queries;
459
460 // Load this sub-group's query once. Inactive sub-groups
461 // (tail of the last work-group) load row 0 so every
462 // lane in the work-group stays in lock-step for the
463 // shared SLM tile loads / barriers below.
464 T q[NDIM];
465 {
466 const int64_t qrow = active_query ? query_idx : 0;
467 for (int d = 0; d < NDIM; ++d) {
468 q[d] = queries_ptr[qrow * NDIM + d];
469 }
470 }
471
472 // Private ascending-sorted top-K, sentinel-filled.
473 T d[K];
474 TIndex idx[K];
475 for (int i = 0; i < K; ++i) {
476 d[i] = std::numeric_limits<T>::max();
477 idx[i] = TIndex(-1);
478 }
479
480 for (int64_t t = 0; t < num_tiles; ++t) {
481 const int64_t cur = t & 1;
482 const int64_t cur_start = t * tp;
483 const int64_t cur_n =
484 std::min<int64_t>(tp, num_points - cur_start);
485
486 if (t == 0) {
487 // Cooperative whole-work-group load of tile 0.
488 for (int64_t e = local_lin; e < cur_n * NDIM;
489 e += local_range) {
490 const int64_t p = e / NDIM, dd = e % NDIM;
491 slm[cur * tp * NDIM + e] =
492 points_ptr[(cur_start + p) * NDIM + dd];
493 }
494 sycl::group_barrier(it.get_group());
495 }
496
497 // Prefetch: cooperatively load the NEXT tile into
498 // the other SLM buffer before computing on the
499 // current one, so its global-memory loads are
500 // issued early and can overlap with this tile's
501 // compute below.
502 if (t + 1 < num_tiles) {
503 const int64_t nxt = 1 - cur;
504 const int64_t nxt_start = (t + 1) * tp;
505 const int64_t nxt_n = std::min<int64_t>(
506 tp, num_points - nxt_start);
507 for (int64_t e = local_lin; e < nxt_n * NDIM;
508 e += local_range) {
509 const int64_t p = e / NDIM, dd = e % NDIM;
510 slm[nxt * tp * NDIM + e] =
511 points_ptr[(nxt_start + p) * NDIM + dd];
512 }
513 }
514
515 if (active_query) {
516 for (int64_t p_local = lane; p_local < cur_n;
517 p_local += SG) {
518 T dist = T(0);
519 const int64_t base =
520 cur * tp * NDIM + p_local * NDIM;
521 for (int dd = 0; dd < NDIM; ++dd) {
522 const T diff = q[dd] - slm[base + dd];
523 dist += diff * diff;
524 }
525 const TIndex gp = static_cast<TIndex>(
526 cur_start + p_local);
527 if (dist < d[K - 1] ||
528 (dist == d[K - 1] && gp < idx[K - 1])) {
529 int pos = K - 1;
530 d[pos] = dist;
531 idx[pos] = gp;
532 while (pos > 0 &&
533 (d[pos - 1] > d[pos] ||
534 (d[pos - 1] == d[pos] &&
535 idx[pos - 1] > idx[pos]))) {
536 T td = d[pos - 1];
537 d[pos - 1] = d[pos];
538 d[pos] = td;
539 TIndex ti = idx[pos - 1];
540 idx[pos - 1] = idx[pos];
541 idx[pos] = ti;
542 --pos;
543 }
544 }
545 }
546 }
547
548 // Bottom barrier: (a) the next-tile load issued
549 // above must finish before the following iteration
550 // treats it as "current"; (b) every lane must be
551 // done reading the current buffer before it is
552 // overwritten two iterations from now.
553 sycl::group_barrier(it.get_group());
554 }
555
556 if (!active_query) return;
557
558 // Sub-group all-reduce merge: after log2(SG)
559 // shuffle/merge rounds every lane holds the identical
560 // final top-K for this query, entirely register
561 // resident.
562 for (int step = 1; step < SG; step <<= 1) {
563 const int64_t partner = lane ^ step;
564 T od[K];
565 TIndex oidx[K];
566 for (int i = 0; i < K; ++i) {
567 od[i] = sycl::select_from_group(sg, d[i], partner);
568 oidx[i] = sycl::select_from_group(sg, idx[i],
569 partner);
570 }
571 T md[K];
572 TIndex mi[K];
573 int a = 0, b = 0;
574 for (int o = 0; o < K; ++o) {
575 const bool take_a =
576 (b >= K) ||
577 (a < K &&
578 (d[a] < od[b] ||
579 (d[a] == od[b] && idx[a] <= oidx[b])));
580 if (take_a) {
581 md[o] = d[a];
582 mi[o] = idx[a];
583 ++a;
584 } else {
585 md[o] = od[b];
586 mi[o] = oidx[b];
587 ++b;
588 }
589 }
590 for (int o = 0; o < K; ++o) {
591 d[o] = md[o];
592 idx[o] = mi[o];
593 }
594 }
595
596 if (lane == 0) {
597 T* od = out_dist_ptr + query_idx * actual_k;
598 TIndex* oi = out_idx_ptr + query_idx * actual_k;
599 for (int64_t i = 0; i < actual_k; ++i) {
600 od[i] = sycl::fmax(T(0), d[i]); // C1
601 oi[i] = idx[i];
602 }
603 }
604 });
605 });
606}
607
612template <typename T, typename TIndex, int NDIM, int SG>
614 const T* points_ptr,
615 const T* queries_ptr,
616 int64_t num_points,
617 int64_t num_queries,
618 int64_t actual_k,
619 T* out_dist_ptr,
620 TIndex* out_idx_ptr,
621 int64_t subgroups_per_wg,
622 int64_t tile_points) {
623 const int64_t k_bucket = KBucket(actual_k);
624#define CALL_DIRECT(Kval) \
625 KnnDirect<T, TIndex, NDIM, Kval, SG>( \
626 queue, points_ptr, queries_ptr, num_points, num_queries, actual_k, \
627 out_dist_ptr, out_idx_ptr, subgroups_per_wg, tile_points)
628 if (k_bucket <= 1)
629 CALL_DIRECT(1);
630 else if (k_bucket <= 2)
631 CALL_DIRECT(2);
632 else if (k_bucket <= 4)
633 CALL_DIRECT(4);
634 else if (k_bucket <= 8)
635 CALL_DIRECT(8);
636 else if (k_bucket <= 16)
637 CALL_DIRECT(16);
638 else
639 CALL_DIRECT(32);
640#undef CALL_DIRECT
641}
642
645template <typename T, typename TIndex, int NDIM>
646void DispatchKnnDirectK(sycl::queue& queue,
647 const T* points_ptr,
648 const T* queries_ptr,
649 int64_t num_points,
650 int64_t num_queries,
651 int64_t actual_k,
652 T* out_dist_ptr,
653 TIndex* out_idx_ptr,
654 int64_t subgroups_per_wg,
655 int64_t tile_points) {
656 if constexpr (std::is_same_v<T, double>) {
657 const auto sg_sizes =
658 queue.get_device()
659 .get_info<sycl::info::device::sub_group_sizes>();
660 const bool supports_subgroup_8 =
661 std::find(sg_sizes.begin(), sg_sizes.end(), size_t(8)) !=
662 sg_sizes.end();
663 if (supports_subgroup_8) {
664 DispatchKnnDirectKForSG<T, TIndex, NDIM, 8>(
665 queue, points_ptr, queries_ptr, num_points, num_queries,
666 actual_k, out_dist_ptr, out_idx_ptr, subgroups_per_wg,
667 tile_points);
668 } else {
669 DispatchKnnDirectKForSG<T, TIndex, NDIM, 16>(
670 queue, points_ptr, queries_ptr, num_points, num_queries,
671 actual_k, out_dist_ptr, out_idx_ptr, subgroups_per_wg,
672 tile_points);
673 }
674 } else {
675 DispatchKnnDirectKForSG<T, TIndex, NDIM, 16>(
676 queue, points_ptr, queries_ptr, num_points, num_queries,
677 actual_k, out_dist_ptr, out_idx_ptr, subgroups_per_wg,
678 tile_points);
679 }
680}
681
687template <typename T, typename TIndex>
688void DispatchKnnDirect(sycl::queue& queue,
689 const T* points_ptr,
690 const T* queries_ptr,
691 int64_t dim,
692 int64_t num_points,
693 int64_t num_queries,
694 int64_t actual_k,
695 T* out_dist_ptr,
696 TIndex* out_idx_ptr,
697 int64_t subgroups_per_wg = kKnnDirectSubgroupsPerWG,
698 int64_t tile_points = kKnnDirectTilePoints) {
699 // kKnnDirectTilePoints is tuned for the common case (dim ≤ 3), where the
700 // resulting per-work-group SLM usage (2 * tile_points * dim * sizeof(T))
701 // is well inside typical device budgets. For larger `dim` (up to
702 // kKnnDirectMaxDim) or double precision, that same tile_points could
703 // exceed the device's actual local memory size, so clamp it down here
704 // using the real device limit (queried once, cheap) rather than baking a
705 // dim/dtype-specific constant into the caller.
706 {
707 const size_t local_mem_bytes =
708 queue.get_device()
709 .get_info<sycl::info::device::local_mem_size>();
710 // Leave 10% headroom for other local allocations / runtime overhead.
711 const int64_t max_tile_points_by_slm = static_cast<int64_t>(
712 (local_mem_bytes * 9 / 10) / (2 * dim * sizeof(T)));
713 tile_points = std::min(tile_points,
714 std::max<int64_t>(max_tile_points_by_slm, 1));
715 }
716 // kKnnDirectSubgroupsPerWG (32) is a value tuned for dim=3 on Intel Xe,
717 // giving wg_size = subgroups_per_wg * SG = 512 work-items at SG=16.
718 // Unlike tile_points above, this was never clamped against the device's
719 // actual max_work_group_size, so a device with a smaller limit than 512
720 // would hit an invalid kernel launch. This is not routed through the
721 // generic MaxWorkGroupSizeForSLM helper: that helper's SLM budget model
722 // assumes usage scales linearly per-work-item (slm_bytes_per_wi *
723 // wg_size), whereas this kernel's SLM usage is a fixed double-buffered
724 // tile (2 * tile_points * dim * sizeof(T), already clamped above)
725 // shared by the whole work-group and independent of subgroups_per_wg.
726 // Clamp conservatively against SG=16 (the wider of the two possible
727 // sub-group widths chosen later in DispatchKnnDirectK) so this is safe
728 // regardless of which SG the dtype ends up selecting.
729 {
730 const size_t max_wg_size =
731 queue.get_device()
732 .get_info<sycl::info::device::max_work_group_size>();
733 subgroups_per_wg = std::min(subgroups_per_wg,
734 static_cast<int64_t>(max_wg_size / 16));
735 subgroups_per_wg = std::max<int64_t>(subgroups_per_wg, 1);
736 }
737#define CALL_DIM(NDIMVAL) \
738 DispatchKnnDirectK<T, TIndex, NDIMVAL>( \
739 queue, points_ptr, queries_ptr, num_points, num_queries, actual_k, \
740 out_dist_ptr, out_idx_ptr, subgroups_per_wg, tile_points)
741 switch (dim) {
742 case 1:
743 CALL_DIM(1);
744 break;
745 case 2:
746 CALL_DIM(2);
747 break;
748 case 3:
749 CALL_DIM(3);
750 break;
751 case 4:
752 CALL_DIM(4);
753 break;
754 case 5:
755 CALL_DIM(5);
756 break;
757 case 6:
758 CALL_DIM(6);
759 break;
760 case 7:
761 CALL_DIM(7);
762 break;
763 case 8:
764 CALL_DIM(8);
765 break;
766 default:
767 utility::LogError("DispatchKnnDirect only supports dim 1 to {}.",
769 }
770#undef CALL_DIM
771}
772
774inline bool UseKnnDirect(int64_t dim, int64_t knn) {
775 return dim >= 1 && dim <= kKnnDirectMaxDim && knn <= kSYCLKnnSmallKMax;
776}
777
779
783
784namespace {
785
787template <typename T, typename TIndex, int K>
788inline void HeapifyDownActive(T* local_d,
789 TIndex* local_i,
790 int root,
791 int active_k) {
792 int i = root;
793 while (true) {
794 int left = 2 * i + 1, right = 2 * i + 2, largest = i;
795 if (left < active_k && (local_d[left] > local_d[largest] ||
796 (local_d[left] == local_d[largest] &&
797 local_i[left] > local_i[largest])))
798 largest = left;
799 if (right < active_k && (local_d[right] > local_d[largest] ||
800 (local_d[right] == local_d[largest] &&
801 local_i[right] > local_i[largest])))
802 largest = right;
803 if (largest == i) break;
804 T td = local_d[i];
805 local_d[i] = local_d[largest];
806 local_d[largest] = td;
807 TIndex ti = local_i[i];
808 local_i[i] = local_i[largest];
809 local_i[largest] = ti;
810 i = largest;
811 }
812}
813
815template <typename T, typename TIndex, int K>
816void SelectTopKQueriesHeap(sycl::queue& queue,
817 const T* distances_ptr,
818 int64_t distance_query_stride,
819 int64_t num_queries,
820 int64_t num_points,
821 int64_t knn,
822 TIndex index_offset,
823 TIndex* out_indices_ptr,
824 T* out_distances_ptr,
825 int64_t out_query_stride,
826 bool use_threshold,
827 const T* query_norms_ptr,
828 T radius_sq,
829 T scalar_threshold) {
830 const T inf = std::numeric_limits<T>::max();
831 const int64_t actual_knn = std::min(knn, num_points);
832
833 const size_t wg = core::sy::PreferredWorkGroupSize(queue.get_device());
834 const size_t global_size =
835 ((static_cast<size_t>(num_queries) + wg - 1) / wg) * wg;
836 queue.parallel_for(
837 sycl::nd_range<1>(sycl::range<1>(global_size), sycl::range<1>(wg)),
838 [=](sycl::nd_item<1> it) [[intel::kernel_args_restrict]] {
839 const int64_t q = it.get_global_id(0);
840 if (q >= num_queries) return;
841 const T* qd = distances_ptr + q * distance_query_stride;
842 TIndex* qout_i = out_indices_ptr + q * out_query_stride;
843 T* qout_d = out_distances_ptr + q * out_query_stride;
844
845 const T thr = (use_threshold && query_norms_ptr)
846 ? (radius_sq - query_norms_ptr[q])
847 : scalar_threshold;
848
849 T local_d[K];
850 TIndex local_i[K];
851 for (int k = 0; k < actual_knn; ++k) {
852 local_d[k] = inf;
853 local_i[k] = TIndex(-1);
854 }
855
856 for (TIndex p = 0; p < static_cast<TIndex>(num_points); ++p) {
857 const T dist = qd[p];
858 if (use_threshold && dist > thr) continue;
859 if (dist < local_d[0] ||
860 (dist == local_d[0] &&
861 index_offset + p < index_offset + local_i[0])) {
862 local_d[0] = dist;
863 local_i[0] = p;
864 HeapifyDownActive<T, TIndex, K>(
865 local_d, local_i, 0,
866 static_cast<int>(actual_knn));
867 }
868 }
869
870 for (int i = 1; i < actual_knn; ++i) {
871 T key_d = local_d[i];
872 TIndex key_i = local_i[i];
873 int j = i - 1;
874 while (j >= 0 &&
875 (local_d[j] > key_d ||
876 (local_d[j] == key_d && local_i[j] > key_i))) {
877 local_d[j + 1] = local_d[j];
878 local_i[j + 1] = local_i[j];
879 j--;
880 }
881 local_d[j + 1] = key_d;
882 local_i[j + 1] = key_i;
883 }
884
885 for (int64_t k = 0; k < knn; ++k) {
886 if (k >= actual_knn || local_i[k] == TIndex(-1)) {
887 qout_i[k] = TIndex(-1);
888 qout_d[k] = inf;
889 } else {
890 qout_i[k] = index_offset + local_i[k];
891 qout_d[k] = local_d[k];
892 }
893 }
894 });
895}
896
897} // namespace
898
901template <typename T, typename TIndex>
903 const T* distances_ptr,
904 int64_t distance_query_stride,
905 int64_t num_queries,
906 int64_t num_points,
907 int64_t knn,
908 int64_t k_bucket,
909 TIndex index_offset,
910 TIndex* out_indices_ptr,
911 T* out_distances_ptr,
912 int64_t out_query_stride,
913 bool use_threshold,
914 const T* query_norms_ptr,
915 T radius_sq,
916 T scalar_threshold) {
917#define CALL_SELECT(Kval) \
918 SelectTopKQueriesHeap<T, TIndex, Kval>( \
919 queue, distances_ptr, distance_query_stride, num_queries, \
920 num_points, knn, index_offset, out_indices_ptr, out_distances_ptr, \
921 out_query_stride, use_threshold, query_norms_ptr, radius_sq, \
922 scalar_threshold)
923 if (k_bucket <= 1)
924 CALL_SELECT(1);
925 else if (k_bucket <= 2)
926 CALL_SELECT(2);
927 else if (k_bucket <= 4)
928 CALL_SELECT(4);
929 else if (k_bucket <= 8)
930 CALL_SELECT(8);
931 else if (k_bucket <= 16)
932 CALL_SELECT(16);
933 else if (k_bucket <= 32)
934 CALL_SELECT(32);
935 else if (k_bucket <= 64)
936 CALL_SELECT(64);
937 else if (k_bucket <= 128)
938 CALL_SELECT(128);
939 else if (k_bucket <= 256)
940 CALL_SELECT(256);
941 else
942 CALL_SELECT(512);
943#undef CALL_SELECT
944}
945
954template <typename T, typename TIndex>
955void SelectTopKQueries(const Device& device,
956 const T* distances_ptr,
957 int64_t distance_query_stride,
958 int64_t num_queries,
959 int64_t num_points,
960 int64_t knn,
961 TIndex index_offset,
962 TIndex* scratch_indices_ptr,
963 int64_t scratch_query_stride,
964 TIndex* out_indices_ptr,
965 T* out_distances_ptr,
966 int64_t out_query_stride,
967 bool use_threshold = false,
968 const T* query_norms_ptr = nullptr,
969 T radius_sq = T(0),
970 T scalar_threshold = T(0)) {
971 if (num_queries == 0 || num_points == 0 || knn <= 0) return;
972
973 const T inf = std::numeric_limits<T>::max();
974 const int64_t actual_knn = std::min(knn, num_points);
975 sycl::queue queue = sy::GetQueue(device);
976
977 if (knn <= kSYCLKnnMidKMax) {
978 const int64_t k_bucket = KBucket(knn);
979 DispatchSelectTopKQueries<T, TIndex>(
980 queue, distances_ptr, distance_query_stride, num_queries,
981 num_points, knn, k_bucket, index_offset, out_indices_ptr,
982 out_distances_ptr, out_query_stride, use_threshold,
983 query_norms_ptr, radius_sq, scalar_threshold);
984 } else {
985 // oneDPL partial_sort fallback (P8: serial per query).
986 auto policy = oneapi::dpl::execution::make_device_policy(queue);
987 queue.parallel_for(
988 sycl::range<2>(num_queries, num_points),
989 [=](sycl::id<2> id) [[intel::kernel_args_restrict]] {
990 scratch_indices_ptr[id[0] * scratch_query_stride + id[1]] =
991 static_cast<TIndex>(id[1]);
992 });
993 queue.wait_and_throw();
994
995 for (int64_t qi = 0; qi < num_queries; ++qi) {
996 TIndex* q_scratch = scratch_indices_ptr + qi * scratch_query_stride;
997 const T* q_dist = distances_ptr + qi * distance_query_stride;
998 // C1's clamp is for the *reported* distance only (applied below,
999 // when writing qout_d). Clamping here in the comparator would
1000 // tie together every point whose true partial distance is
1001 // slightly negative from P2 cancellation (common for
1002 // widely-spread float32 data), corrupting the selected/sorted
1003 // *set* of neighbors -- not just their reported distance value.
1004 // Comparing the raw (unclamped) values preserves the true
1005 // relative order even when cancellation makes some values
1006 // slightly negative.
1007 std::partial_sort(policy, q_scratch, q_scratch + actual_knn,
1008 q_scratch + num_points,
1009 [q_dist](TIndex lhs, TIndex rhs) {
1010 const T ld = q_dist[lhs];
1011 const T rd = q_dist[rhs];
1012 if (ld < rd) return true;
1013 if (rd < ld) return false;
1014 return lhs < rhs; // C4
1015 });
1016 }
1017
1018 queue.parallel_for(
1019 sycl::range<2>(num_queries, knn),
1020 [=](sycl::id<2> id) [[intel::kernel_args_restrict]] {
1021 const int64_t qi = id[0], k = id[1];
1022 TIndex* qout_i = out_indices_ptr + qi * out_query_stride;
1023 T* qout_d = out_distances_ptr + qi * out_query_stride;
1024 if (k >= actual_knn) {
1025 qout_i[k] = TIndex(-1);
1026 qout_d[k] = inf;
1027 return;
1028 }
1029 const TIndex li =
1030 scratch_indices_ptr[qi * scratch_query_stride + k];
1031 // P2/C1: this is the *partial* distance (−2qp+|p|², |q|²
1032 // not yet added by the caller). Do not clamp ≥ 0 here --
1033 // the partial value can be legitimately very negative
1034 // (missing +|q|²), especially for widely-spread float32
1035 // data; clamping it here (before |q|² is added) ties
1036 // together every such point at exactly 0, corrupting the
1037 // reported distance for many neighbors at once. The
1038 // final clamp is applied once |q|² has been added (see
1039 // AddQueryNormsToDistances / FinalizeTopK's C1).
1040 const T dist =
1041 distances_ptr[qi * distance_query_stride + li];
1042 const T thr = (use_threshold && query_norms_ptr)
1043 ? (radius_sq - query_norms_ptr[qi])
1044 : scalar_threshold;
1045 if (use_threshold && dist > thr) {
1046 qout_i[k] = TIndex(-1);
1047 qout_d[k] = inf;
1048 return;
1049 }
1050 qout_i[k] = index_offset + li;
1051 qout_d[k] = dist;
1052 });
1053 }
1054}
1055
1058template <typename T, typename TIndex>
1059void MergeTopKQueries(const Device& device,
1060 const T* curr_dist_ptr,
1061 const TIndex* curr_idx_ptr,
1062 int64_t curr_stride,
1063 const T* cand_dist_ptr,
1064 const TIndex* cand_idx_ptr,
1065 int64_t cand_stride,
1066 int64_t num_queries,
1067 int64_t knn,
1068 TIndex* scratch_ptr,
1069 int64_t scratch_stride,
1070 TIndex* out_idx_ptr,
1071 T* out_dist_ptr,
1072 int64_t out_stride) {
1073 if (num_queries == 0 || knn <= 0) return;
1074 const T inf = std::numeric_limits<T>::max();
1075 sycl::queue queue = sy::GetQueue(device);
1076
1077 if (knn <= kSYCLKnnMidKMax) {
1078 queue.parallel_for(
1079 sycl::range<1>(num_queries),
1080 [=](sycl::id<1> id) [[intel::kernel_args_restrict]] {
1081 const int64_t q = id[0];
1082 const T* qcd = curr_dist_ptr + q * curr_stride;
1083 const TIndex* qci = curr_idx_ptr + q * curr_stride;
1084 const T* qad = cand_dist_ptr + q * cand_stride;
1085 const TIndex* qai = cand_idx_ptr + q * cand_stride;
1086 TIndex* qout_i = out_idx_ptr + q * out_stride;
1087 T* qout_d = out_dist_ptr + q * out_stride;
1088
1089 int64_t ic = 0, ia = 0;
1090 for (int64_t k = 0; k < knn; ++k) {
1091 const TIndex ci = (ic < knn) ? qci[ic] : TIndex(-1);
1092 const TIndex ai = (ia < knn) ? qai[ia] : TIndex(-1);
1093 if (ci < 0 && ai < 0) {
1094 qout_i[k] = TIndex(-1);
1095 qout_d[k] = inf;
1096 continue;
1097 }
1098 bool take_curr;
1099 if (ci < 0) {
1100 take_curr = false;
1101 } else if (ai < 0) {
1102 take_curr = true;
1103 } else {
1104 const T cd = qcd[ic], ad = qad[ia];
1105 if (cd < ad)
1106 take_curr = true;
1107 else if (ad < cd)
1108 take_curr = false;
1109 else
1110 take_curr = (ci < ai); // C4
1111 }
1112 if (take_curr) {
1113 qout_d[k] = qcd[ic];
1114 qout_i[k] = ci;
1115 ++ic;
1116 } else {
1117 qout_d[k] = qad[ia];
1118 qout_i[k] = ai;
1119 ++ia;
1120 }
1121 }
1122 });
1123 } else {
1124 // oneDPL merge sort fallback for large knn (P8).
1125 const int64_t combined = 2 * knn;
1126 auto policy = oneapi::dpl::execution::make_device_policy(queue);
1127 queue.parallel_for(sycl::range<2>(num_queries, combined),
1128 [=](sycl::id<2> id) [[intel::kernel_args_restrict]] {
1129 scratch_ptr[id[0] * scratch_stride + id[1]] =
1130 static_cast<TIndex>(id[1]);
1131 });
1132 queue.wait_and_throw();
1133
1134 for (int64_t qi = 0; qi < num_queries; ++qi) {
1135 TIndex* qs = scratch_ptr + qi * scratch_stride;
1136 const T* qcd = curr_dist_ptr + qi * curr_stride;
1137 const TIndex* qci = curr_idx_ptr + qi * curr_stride;
1138 const T* qad = cand_dist_ptr + qi * cand_stride;
1139 const TIndex* qai = cand_idx_ptr + qi * cand_stride;
1140 std::partial_sort(
1141 policy, qs, qs + knn, qs + combined,
1142 [qcd, qci, qad, qai, knn](TIndex lhs, TIndex rhs) {
1143 const bool lc = (lhs < knn), rc = (rhs < knn);
1144 const TIndex li = lc ? qci[lhs] : qai[lhs - knn];
1145 const TIndex ri = rc ? qci[rhs] : qai[rhs - knn];
1146 const T ld = lc ? qcd[lhs] : qad[lhs - knn];
1147 const T rd = rc ? qcd[rhs] : qad[rhs - knn];
1148 if ((li >= 0) != (ri >= 0)) return li >= 0;
1149 if (ld < rd) return true;
1150 if (rd < ld) return false;
1151 return li < ri; // C4
1152 });
1153 }
1154
1155 queue.parallel_for(
1156 sycl::range<2>(num_queries, knn),
1157 [=](sycl::id<2> id) [[intel::kernel_args_restrict]] {
1158 const int64_t qi = id[0], k = id[1];
1159 const TIndex src = scratch_ptr[qi * scratch_stride + k];
1160 const bool is_curr = (src < knn);
1161 const int64_t off = is_curr ? src : src - knn;
1162 const TIndex ii =
1163 is_curr ? curr_idx_ptr[qi * curr_stride + off]
1164 : cand_idx_ptr[qi * cand_stride + off];
1165 const T dd =
1166 is_curr ? curr_dist_ptr[qi * curr_stride + off]
1167 : cand_dist_ptr[qi * cand_stride + off];
1168 TIndex* qout_i = out_idx_ptr + qi * out_stride;
1169 T* qout_d = out_dist_ptr + qi * out_stride;
1170 if (ii < 0) {
1171 qout_i[k] = TIndex(-1);
1172 qout_d[k] = inf;
1173 } else {
1174 qout_i[k] = ii;
1175 qout_d[k] = dd;
1176 }
1177 });
1178 }
1179}
1180
1182
1185
1188template <typename T, typename TIndex>
1190 int64_t num_queries,
1191 int64_t knn,
1192 const TIndex* indices_ptr,
1193 T* distances_ptr,
1194 const T* query_norms_ptr) {
1195 sycl::queue queue = sy::GetQueue(device);
1196 queue.parallel_for(sycl::range<2>(num_queries, knn),
1197 [=](sycl::id<2> id) [[intel::kernel_args_restrict]] {
1198 const int64_t q = id[0], k = id[1];
1199 if (indices_ptr[q * knn + k] < 0) return;
1200 distances_ptr[q * knn + k] =
1201 sycl::fmax(T(0), distances_ptr[q * knn + k] +
1202 query_norms_ptr[q]);
1203 });
1204}
1205
1207
1208} // namespace nns
1209} // namespace core
1210} // namespace open3d
T dist
Definition FixedRadiusSearchSYCLImpl.h:167
#define CALL_SELECT(Kval)
#define CALL_FINALIZE(Kval)
#define CALL_DIM(NDIMVAL)
#define CALL_DIRECT(Kval)
#define CALL_UPDATE(Kval)
int knn
Definition PointCloudSmoothing.cpp:131
sycl::queue queue
Definition SYCLContext.cpp:88
SYCL device properties and (when built) queue manager.
double t
Definition SurfaceReconstructionPoisson.cpp:175
Definition Device.h:18
Named kernel tag for KnnDirect (SYCL kernel naming).
Definition KnnSearchSYCLImpl.h:419
Shared types and SYCL nearest-neighbor search tuning defaults.
constexpr int64_t kKnnDirectMaxDim
Maximum point dimension compiled for DispatchKnnDirect.
Definition KnnSearchSYCLImpl.h:415
void FinalizeTopK(sycl::queue &queue, int64_t num_queries, const T *running_dist_ptr, const TIndex *running_idx_ptr, T *out_dist_ptr, TIndex *out_idx_ptr, int64_t actual_k, const T *query_norms_ptr)
Definition KnnSearchSYCLImpl.h:254
void DispatchUpdateTopKFromTile(sycl::queue &queue, const T *neg2qp_ptr, int64_t distance_stride, const T *point_norms_ptr, int64_t num_queries, int64_t num_points, int64_t k_bucket, TIndex point_offset, T *best_dist_ptr, TIndex *best_idx_ptr, bool use_threshold, T threshold)
Instantiate UpdateTopKFromTile for the given k_bucket.
Definition KnnSearchSYCLImpl.h:312
void DispatchFinalizeTopK(sycl::queue &queue, int64_t num_queries, const T *running_dist_ptr, const TIndex *running_idx_ptr, T *out_dist_ptr, TIndex *out_idx_ptr, int64_t actual_k, int64_t k_bucket, const T *query_norms_ptr)
Instantiate FinalizeTopK for the given k_bucket.
Definition KnnSearchSYCLImpl.h:354
constexpr int64_t kKnnDirectSubgroupSize
Default sub-group width for the direct KNN kernel (float path).
Definition KnnSearchSYCLImpl.h:409
void DispatchKnnDirectKForSG(sycl::queue &queue, const T *points_ptr, const T *queries_ptr, int64_t num_points, int64_t num_queries, int64_t actual_k, T *out_dist_ptr, TIndex *out_idx_ptr, int64_t subgroups_per_wg, int64_t tile_points)
Definition KnnSearchSYCLImpl.h:613
void SelectTopKQueries(const Device &device, const T *distances_ptr, int64_t distance_query_stride, int64_t num_queries, int64_t num_points, int64_t knn, TIndex index_offset, TIndex *scratch_indices_ptr, int64_t scratch_query_stride, TIndex *out_indices_ptr, T *out_distances_ptr, int64_t out_query_stride, bool use_threshold=false, const T *query_norms_ptr=nullptr, T radius_sq=T(0), T scalar_threshold=T(0))
Definition KnnSearchSYCLImpl.h:955
void UpdateTopKFromTile(sycl::queue &queue, const T *neg2qp_ptr, int64_t distance_stride, const T *point_norms_ptr, int64_t num_queries, int64_t num_points, TIndex point_offset, T *best_dist_ptr, TIndex *best_idx_ptr, bool use_threshold, T threshold)
Definition KnnSearchSYCLImpl.h:187
void HeapifyDown(T *d, TIndex *idx, int root)
Definition KnnSearchSYCLImpl.h:103
void DispatchKnnDirectK(sycl::queue &queue, const T *points_ptr, const T *queries_ptr, int64_t num_points, int64_t num_queries, int64_t actual_k, T *out_dist_ptr, TIndex *out_idx_ptr, int64_t subgroups_per_wg, int64_t tile_points)
Definition KnnSearchSYCLImpl.h:646
constexpr int64_t kKnnDirectTilePoints
Default point tile size for SLM staging.
Definition KnnSearchSYCLImpl.h:413
void DispatchSelectTopKQueries(sycl::queue &queue, const T *distances_ptr, int64_t distance_query_stride, int64_t num_queries, int64_t num_points, int64_t knn, int64_t k_bucket, TIndex index_offset, TIndex *out_indices_ptr, T *out_distances_ptr, int64_t out_query_stride, bool use_threshold, const T *query_norms_ptr, T radius_sq, T scalar_threshold)
Definition KnnSearchSYCLImpl.h:902
void MergeTopKQueries(const Device &device, const T *curr_dist_ptr, const TIndex *curr_idx_ptr, int64_t curr_stride, const T *cand_dist_ptr, const TIndex *cand_idx_ptr, int64_t cand_stride, int64_t num_queries, int64_t knn, TIndex *scratch_ptr, int64_t scratch_stride, TIndex *out_idx_ptr, T *out_dist_ptr, int64_t out_stride)
Definition KnnSearchSYCLImpl.h:1059
void HeapSort(T *d, TIndex *idx)
Heap-sort a compile-time max-heap of size K into ascending order.
Definition KnnSearchSYCLImpl.h:125
void ChooseTileSize(int64_t num_queries, int64_t num_points, int64_t element_size, int64_t tile_bytes, int64_t &tile_queries, int64_t &tile_points, int64_t max_tile_queries=128, int64_t tile_points_alignment=128)
Definition KnnSearchSYCLImpl.h:74
void KnnDirect(sycl::queue &queue, const T *points_ptr, const T *queries_ptr, int64_t num_points, int64_t num_queries, int64_t actual_k, T *out_dist_ptr, TIndex *out_idx_ptr, int64_t subgroups_per_wg, int64_t tile_points)
Launch direct-distance KNN for fixed compile-time NDIM, K, and SG.
Definition KnnSearchSYCLImpl.h:423
void DispatchKnnDirect(sycl::queue &queue, const T *points_ptr, const T *queries_ptr, int64_t dim, int64_t num_points, int64_t num_queries, int64_t actual_k, T *out_dist_ptr, TIndex *out_idx_ptr, int64_t subgroups_per_wg=kKnnDirectSubgroupsPerWG, int64_t tile_points=kKnnDirectTilePoints)
Definition KnnSearchSYCLImpl.h:688
bool UseKnnDirect(int64_t dim, int64_t knn)
True if (dim, knn) qualifies for the direct-distance SYCL KNN path.
Definition KnnSearchSYCLImpl.h:774
int64_t KBucket(int64_t k)
Return the smallest dispatch-bucket value ≥ k.
Definition KnnSearchSYCLImpl.h:297
constexpr int64_t kKnnDirectSubgroupsPerWG
Default sub-groups per work-group (512 work-items at SG=16).
Definition KnnSearchSYCLImpl.h:411
void AddQueryNormsToDistances(const Device &device, int64_t num_queries, int64_t knn, const TIndex *indices_ptr, T *distances_ptr, const T *query_norms_ptr)
Definition KnnSearchSYCLImpl.h:1189
constexpr int64_t kSYCLKnnMidKMax
Definition NeighborSearchCommon.h:73
constexpr int64_t kSYCLKnnSmallKMax
Upper bound of k for the GRF-register heap path (eliminates scratch spill).
Definition NeighborSearchCommon.h:69
sycl::queue GetQueue(const Device &device)
Definition SYCLContext.cpp:183
Definition PinholeCameraIntrinsic.cpp:16