Skip to content

Commit a1777e7

Browse files
committed
final perf optimizations to sdf kernel
1 parent 657ee0b commit a1777e7

1 file changed

Lines changed: 70 additions & 33 deletions

File tree

physicsnemo/mesh/spatial/_sdf_triton.py

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)