@@ -80,14 +80,20 @@ def available() -> bool:
8080 _TINY_C = tl .constexpr (_TINY )
8181
8282 @triton .jit
83- def _node_dist_sq (qx , qy , qz , nmin_ptr , nmax_ptr , node , valid ):
84- """Squared distance from each query to its node's AABB (0 if inside)."""
85- minx = tl .load (nmin_ptr + node * 3 + 0 , mask = valid , other = 0.0 )
86- miny = tl .load (nmin_ptr + node * 3 + 1 , mask = valid , other = 0.0 )
87- minz = tl .load (nmin_ptr + node * 3 + 2 , mask = valid , other = 0.0 )
88- maxx = tl .load (nmax_ptr + node * 3 + 0 , mask = valid , other = 0.0 )
89- maxy = tl .load (nmax_ptr + node * 3 + 1 , mask = valid , other = 0.0 )
90- maxz = tl .load (nmax_ptr + node * 3 + 2 , mask = valid , other = 0.0 )
83+ def _node_dist_sq (qx , qy , qz , aabb_ptr , node , valid ):
84+ """Squared distance from each query to its node's AABB (0 if inside).
85+
86+ ``aabb_ptr`` packs each node's bounds contiguously as
87+ ``(n_nodes, 6)`` -- ``min(xyz)`` then ``max(xyz)`` -- so a node's six
88+ floats land in one ~32-byte segment, giving the per-lane gather better
89+ cache locality than two separate ``(n_nodes, 3)`` arrays.
90+ """
91+ minx = tl .load (aabb_ptr + node * 6 + 0 , mask = valid , other = 0.0 )
92+ miny = tl .load (aabb_ptr + node * 6 + 1 , mask = valid , other = 0.0 )
93+ minz = tl .load (aabb_ptr + node * 6 + 2 , mask = valid , other = 0.0 )
94+ maxx = tl .load (aabb_ptr + node * 6 + 3 , mask = valid , other = 0.0 )
95+ maxy = tl .load (aabb_ptr + node * 6 + 4 , mask = valid , other = 0.0 )
96+ maxz = tl .load (aabb_ptr + node * 6 + 5 , mask = valid , other = 0.0 )
9197 dx = tl .maximum (qx - maxx , 0.0 ) + tl .maximum (minx - qx , 0.0 )
9298 dy = tl .maximum (qy - maxy , 0.0 ) + tl .maximum (miny - qy , 0.0 )
9399 dz = tl .maximum (qz - maxz , 0.0 ) + tl .maximum (minz - qz , 0.0 )
@@ -211,15 +217,13 @@ def _closest_point_on_triangle(px, py, pz, ax, ay, az, bx, by, bz, cx, cy, cz):
211217 @triton .jit
212218 def _nearest_triangle_kernel (
213219 query_ptr , # (N, 3) f32
214- fv_ptr , # (n_faces, 9) f32 -- a(xyz), b(xyz), c(xyz)
215- nmin_ptr , # (n_nodes, 3) f32
216- nmax_ptr , # (n_nodes, 3) f32
220+ fv_ptr , # (n_faces, 9) f32 -- leaf-sorted: a(xyz), b(xyz), c(xyz)
221+ aabb_ptr , # (n_nodes, 6) f32 -- min(xyz), max(xyz)
217222 left_ptr , # (n_nodes,) i32
218223 right_ptr , # (n_nodes,) i32
219224 lstart_ptr , # (n_nodes,) i32
220225 lcount_ptr , # (n_nodes,) i32
221- order_ptr , # (n_cells,) i32
222- stack_ptr , # (N, STACK_SIZE) i32 scratch
226+ stack_ptr , # (STACK_SIZE, N) i32 scratch (depth-major: coalesced lanes)
223227 out_dist_ptr , # (N,) f32 best squared distance
224228 out_face_ptr , # (N,) i32 best face index
225229 out_pt_ptr , # (N, 3) f32 closest point
@@ -232,7 +236,7 @@ def _nearest_triangle_kernel(
232236 """One query per lane; bounded-stack near-first DFS for nearest triangle."""
233237 pid = tl .program_id (0 )
234238 # int64 index base: ``off`` feeds element-offset arithmetic (``off * 3``
235- # for queries, ``off * STACK_SIZE `` for the per-lane stack). At tens of
239+ # for queries, ``depth * N + off `` for the per-lane stack). At tens of
236240 # millions of queries the default int32 product silently overflows and
237241 # the kernel reads/writes wrong addresses, so widen before the multiply.
238242 off = pid .to (tl .int64 ) * BLOCK + tl .arange (0 , BLOCK ).to (tl .int64 )
@@ -248,9 +252,12 @@ def _nearest_triangle_kernel(
248252 bpy = qy
249253 bpz = qz
250254
251- # Seed each lane's stack with the root node (0) and size 1.
255+ # Seed each lane's stack with the root node (0) and size 1. The stack is
256+ # depth-major (``depth * N + off``): at a shared depth, adjacent lanes
257+ # map to adjacent addresses, so coherent pushes/pops coalesce. Root sits
258+ # at depth 0, i.e. element ``off``.
252259 sp = tl .where (m , 1 , 0 ).to (tl .int32 )
253- tl .store (stack_ptr + off * STACK_SIZE + 0 , tl .zeros ((BLOCK ,), tl .int32 ), mask = m )
260+ tl .store (stack_ptr + off , tl .zeros ((BLOCK ,), tl .int32 ), mask = m )
254261
255262 # Each node is pushed at most once per lane (one parent per node), so the
256263 # DFS pops a finite number of nodes and the loop is guaranteed to
@@ -259,11 +266,11 @@ def _nearest_triangle_kernel(
259266 while tl .sum (active .to (tl .int32 )) > 0 :
260267 # --- Pop the top node from every active lane.
261268 ptr = sp - 1
262- node = tl .load (stack_ptr + off * STACK_SIZE + ptr , mask = active , other = 0 )
269+ node = tl .load (stack_ptr + ptr . to ( tl . int64 ) * N + off , mask = active , other = 0 )
263270 sp = tl .where (active , ptr , sp )
264271
265272 # --- Prune: skip nodes that can no longer beat the running bound.
266- lower_sq = _node_dist_sq (qx , qy , qz , nmin_ptr , nmax_ptr , node , active )
273+ lower_sq = _node_dist_sq (qx , qy , qz , aabb_ptr , node , active )
267274 proceed = active & (lower_sq < best )
268275
269276 lcount = tl .load (lcount_ptr + node , mask = proceed , other = 0 )
@@ -274,7 +281,10 @@ def _nearest_triangle_kernel(
274281 lstart = tl .load (lstart_ptr + node , mask = is_leaf , other = 0 )
275282 for ci in tl .static_range (0 , MAX_LEAF ):
276283 cell_valid = is_leaf & (ci < lcount )
277- cell = tl .load (order_ptr + lstart + ci , mask = cell_valid , other = 0 )
284+ # ``fv`` is pre-sorted into leaf order, so the leaf position is
285+ # the triangle row directly -- no ``sorted_cell_order`` load. The
286+ # caller maps this leaf position back to the original face id.
287+ cell = lstart + ci
278288 ax = tl .load (fv_ptr + cell * 9 + 0 , mask = cell_valid , other = 0.0 )
279289 ay = tl .load (fv_ptr + cell * 9 + 1 , mask = cell_valid , other = 0.0 )
280290 az = tl .load (fv_ptr + cell * 9 + 2 , mask = cell_valid , other = 0.0 )
@@ -306,8 +316,8 @@ def _nearest_triangle_kernel(
306316 left_valid = is_internal & (left >= 0 )
307317 right_valid = is_internal & (right >= 0 )
308318
309- d_left = _node_dist_sq (qx , qy , qz , nmin_ptr , nmax_ptr , left , left_valid )
310- d_right = _node_dist_sq (qx , qy , qz , nmin_ptr , nmax_ptr , right , right_valid )
319+ d_left = _node_dist_sq (qx , qy , qz , aabb_ptr , left , left_valid )
320+ d_right = _node_dist_sq (qx , qy , qz , aabb_ptr , right , right_valid )
311321 inf = tl .full ((BLOCK ,), float ("inf" ), tl .float32 )
312322 d_left = tl .where (left_valid , d_left , inf )
313323 d_right = tl .where (right_valid , d_right , inf )
@@ -318,10 +328,22 @@ def _nearest_triangle_kernel(
318328 near_valid = tl .where (left_first , left_valid , right_valid )
319329 far_valid = tl .where (left_first , right_valid , left_valid )
320330
331+ # Prune at push time: a child whose AABB lower bound already exceeds
332+ # the running best cannot hold a closer triangle, so never push it.
333+ # ``d_left``/``d_right`` are reused here (already computed for the
334+ # near-first ordering), so this is effectively free and keeps
335+ # prunable subtrees out of the stack entirely. ``best`` only shrinks
336+ # later, so the pop-time prune above still catches nodes that become
337+ # prunable after they were pushed.
338+ d_near = tl .where (left_first , d_left , d_right )
339+ d_far = tl .where (left_first , d_right , d_left )
340+ near_valid = near_valid & (d_near < best )
341+ far_valid = far_valid & (d_far < best )
342+
321343 # Push the farther child first so it sits below the nearer child.
322- tl .store (stack_ptr + off * STACK_SIZE + sp , far , mask = far_valid )
344+ tl .store (stack_ptr + sp . to ( tl . int64 ) * N + off , far , mask = far_valid )
323345 sp = tl .where (far_valid , sp + 1 , sp )
324- tl .store (stack_ptr + off * STACK_SIZE + sp , near , mask = near_valid )
346+ tl .store (stack_ptr + sp . to ( tl . int64 ) * N + off , near , mask = near_valid )
325347 sp = tl .where (near_valid , sp + 1 , sp )
326348
327349 active = sp > 0
@@ -379,15 +401,27 @@ def nearest_triangle_triton(
379401 )
380402
381403 n_faces = face_vertices .shape [0 ]
382- fv = face_vertices .reshape (n_faces , 9 ).to (torch .float32 ).contiguous ()
383404 query_c = query .reshape (- 1 , 3 ).to (torch .float32 ).contiguous ()
384- nmin = bvh .node_aabb_min .to (torch .float32 ).contiguous ()
385- nmax = bvh .node_aabb_max .to (torch .float32 ).contiguous ()
405+ # Reorder the triangle payload into BVH leaf order: leaf position ``i`` holds
406+ # original face ``cell_order[i]``. Storing triangles this way makes a leaf's
407+ # cells contiguous and lets the kernel index them by leaf position
408+ # (``leaf_start + ci``) -- dropping the per-cell ``sorted_cell_order``
409+ # indirection load and coalescing the 9-float triangle gather for
410+ # warp-coherent lanes. The kernel records the leaf position as the winning
411+ # "face"; we map it back to the original face id before returning.
412+ cell_order = bvh .sorted_cell_order .to (torch .long )
413+ fv = face_vertices .reshape (n_faces , 9 ).to (torch .float32 )
414+ fv_sorted = fv [cell_order ].contiguous ()
415+ # Pack node bounds as (n_nodes, 6) = min(xyz) | max(xyz) so each node's six
416+ # floats are contiguous, improving the per-lane AABB gather's cache locality.
417+ node_aabb = torch .cat (
418+ [bvh .node_aabb_min .to (torch .float32 ), bvh .node_aabb_max .to (torch .float32 )],
419+ dim = 1 ,
420+ ).contiguous ()
386421 left = bvh .node_left_child .to (torch .int32 ).contiguous ()
387422 right = bvh .node_right_child .to (torch .int32 ).contiguous ()
388423 lstart = bvh .leaf_start .to (torch .int32 ).contiguous ()
389424 lcount = bvh .leaf_count .to (torch .int32 ).contiguous ()
390- cell_order = bvh .sorted_cell_order .to (torch .int32 ).contiguous ()
391425
392426 # Reorder queries along a Morton curve for warp coherence; unsorted at the
393427 # end. Outputs are written/allocated in sorted order, then scattered back.
@@ -403,7 +437,10 @@ def nearest_triangle_triton(
403437 # to the host (that readback stalled the prefetch stream).
404438 max_leaf = max (1 , leaf_size )
405439
406- stack = torch .empty (n_queries , _STACK_SIZE , dtype = torch .int32 , device = device )
440+ # Depth-major scratch: shape (STACK_SIZE, n_queries) so that, at a shared
441+ # DFS depth, adjacent lanes index adjacent memory and the push/pop traffic
442+ # coalesces. Indexed in the kernel as ``depth * N + off``.
443+ stack = torch .empty (_STACK_SIZE , n_queries , dtype = torch .int32 , device = device )
407444
408445 # ``BLOCK`` (and ``num_warps``) come from the autotuner; the grid must be a
409446 # meta-aware callable so it tracks the chosen block size.
@@ -412,14 +449,12 @@ def grid(meta):
412449
413450 _nearest_triangle_kernel [grid ](
414451 query_s ,
415- fv ,
416- nmin ,
417- nmax ,
452+ fv_sorted ,
453+ node_aabb ,
418454 left ,
419455 right ,
420456 lstart ,
421457 lcount ,
422- cell_order ,
423458 stack ,
424459 out_dist_s ,
425460 out_face_s ,
@@ -437,4 +472,6 @@ def grid(meta):
437472 best_face [perm ] = out_face_s
438473 best_point [perm ] = out_pt_s
439474
440- return best_dist_sq , best_face .long (), best_point
475+ # ``out_face_s`` holds BVH leaf positions (the kernel runs on leaf-sorted
476+ # triangles); map them back to original face ids.
477+ return best_dist_sq , cell_order [best_face .long ()], best_point
0 commit comments