Skip to content

User Guide

torchbvh expects CUDA float32 tensors, D in {2, 3}, and k in {4, 8, 16}. Callers provide finite inputs. Public APIs accept non-contiguous PyTorch views and may make layout-only contiguous copies internally for CUDA kernels. Gradients do not flow through BVH construction, FPS, or discrete neighbor selection.

Single-Sample k-NN

Use BVH(points) for one point cloud. Distances are squared Euclidean distances sorted ascending with their matching indices.

import torch
import torchbvh as tb

points = torch.randn(4096, 3, device="cuda")                 # (N, D) float32
query_pts = points                                           # (N, D) float32

bvh = tb.BVH(points)
idx, dist_sq = bvh.knn(query_pts, k=8)                       # (N, 8) int64, (N, 8) float32

The class owns the native handle. Use the context-manager form when lifetime should be scoped to a block.

with tb.BVH(points) as bvh:                                  # points: (N, D)
    idx, dist_sq = bvh.knn(query_pts, k=8)                   # (N, 8), (N, 8)

Fixed-Size Batched k-NN

For equal-size batches, pass (B, N, D) points. Query points use the same batch dimension and return local per-sample indices.

points = torch.randn(4, 4096, 3, device="cuda")              # (B, N, D) float32
query_pts = points                                           # (B, N, D) float32

bvh = tb.BVH(points)
idx, dist_sq = bvh.knn(query_pts, k=8)                       # (B, N, 8), (B, N, 8)

Ragged k-NN

For variable-size samples, pack source points as (total_N, D) and provide 1-D int64 start offsets. Ragged handles support k-NN only.

points = torch.randn(9000, 3, device="cuda")                 # (total_N, D) float32
batch_offsets = torch.tensor([0, 2000, 5500, 9000],           # (B + 1,) int64
                             device="cuda", dtype=torch.int64)
query_pts = torch.randn(7200, 3, device="cuda")              # (total_M, D) float32
query_offsets = torch.tensor([0, 1800, 4200, 7200],           # (B + 1,) int64
                             device="cuda", dtype=torch.int64)

bvh = tb.BVH(points, batch_offsets=batch_offsets)
idx, dist_sq = bvh.knn(query_pts, k=8, query_offsets=query_offsets)  # (total_M, 8), (total_M, 8)

bvh.interpolate(...) raises TypeError for ragged handles. Build a single-sample or fixed-size batched BVH for MLS interpolation.

MLS Interpolation

Use MLS when features live on source points and query positions may be displaced. Gradients flow to features and live displaced_pts.

points = torch.randn(4096, 3, device="cuda")                         # (N, D) float32
displaced_pts = points + 0.01 * torch.randn_like(points)              # (N, D) float32
displaced_pts.requires_grad_(True)
features = torch.randn(4096, 16, device="cuda", requires_grad=True)   # (N, Ch) float32

bvh = tb.BVH(points)
interpolated = bvh.interpolate(displaced_pts, features, k=8)          # (N, Ch) float32

The procedural alias is useful in low-level pipelines.

interpolated = tb.mls_interpolate(points, displaced_pts, features, k=8) # (N, Ch) float32

return_grad=True returns the spatial MLS field gradient. It is data returned by the operator, not PyTorch autograd metadata.

interpolated, field_gradient = bvh.interpolate(
    displaced_pts, features, k=8, return_grad=True
)                                                                    # (N, Ch), (N, D, Ch)

The same calls support fixed-size batches with (B, N, D) geometry and (B, N, Ch) features.

Displaced-Query Workflow

For multihead displaced queries, build one BVH per sample over pos and reuse it across all H heads. The helper owns that shape choreography.

pos = torch.randn(2, 4096, 3, device="cuda")                    # (B, N, D) float32
rho = torch.randn(2, 4096, 4, 3, device="cuda")                 # (B, N, H, D) float32
displaced_queries = pos[:, :, None, :] + rho                    # (B, N, H, D) float32
values = torch.randn(2, 4096, 4, 32, device="cuda")             # (B, N, H, Ch) float32

idx, dist_sq = tb.query_displaced_knn(
    pos, displaced_queries, k=8, return_positions=False
)                                                               # (B, N, H, 8), (B, N, H, 8)
gathered = tb.gather_neighbor_values(values, idx)               # (B, N, H, 8, Ch)
out = tb.interpolate_displaced(pos, displaced_queries, values, k=8) # (B, N, H, Ch)

Gradients flow to values only through gather_neighbor_values(...) and interpolate_displaced(...). Indices, distances, source positions, and displaced query geometry are discrete query inputs for these helpers.

FPS Downsampling Geometry

fps(points, target_tokens) selects farthest-point anchors for (N, D) or (B, N, D) inputs and returns geometry plus assignment metadata.

points = torch.randn(4, 4096, 3, device="cuda")                 # (B, N, D) float32
result = tb.fps(points, target_tokens=1024)

result.indices                     # (B, M) int64, selected source indices
result.points                      # (B, M, D) float32, selected anchor points
result.nearest_anchor              # (B, N) int32, assignment to selection-order anchor
result.nearest_anchor_dist_sq      # (B, N) float32, squared distance to assigned anchor
result.anchor_radius               # (B, M) float32, max assigned squared distance
result.anchor_counts               # (B, M) int32, number of points assigned per anchor
result.coarse_order                # (B, M) int64, anchors ordered by BVH leaf position

For a single sample, remove the leading B dimension: indices is (M,), points is (M, D), and assignment metadata is (N,) or (M,).

FPS is non-differentiable. Pass the assignment metadata to your own PyTorch pooling or unpooling logic.

API Choice

Prefer BVH for new code. It owns handle lifetime and dispatches across single-sample, fixed-size batched, and ragged k-NN workflows.

Use unified procedural functions such as build_bvh, query_knn, and mls_interpolate when a low-level pipeline needs explicit handle control.

Per-variant aliases such as build_bvh_batched and query_knn_batched remain stable for compatibility and shape-specific integrations, but they are not the recommended starting point.