rigidformer / src /utils /cuda_knn.py
multimodalart's picture
multimodalart HF Staff
RigidFormer demo
632d805 verified
Raw History Blame Contribute Delete
26.7 kB
import torch
import torch.nn.functional as F
from typing import List, Tuple, Optional
# CUDA is mandatory for this module; there is intentionally no CPU fallback.
assert torch.cuda.is_available(), "cuda_knn requires a CUDA-capable device."
def compute_nearest_distance_to_list_cuda(
tensor_a: torch.Tensor,
tensor_list: List[torch.Tensor],
batch_mode: bool = False,
return_vectors: bool = False,
return_tensor_indices: bool = True
) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
"""
CUDA-accelerated parallel computation of nearest distances from query
points to a list of reference point sets. All tensors must reside on the
same device.
Args:
tensor_a: Query tensor
- If batch_mode=False: shape [N, D]
- If batch_mode=True: shape [B, N, D]
tensor_list: List of reference tensors
- If batch_mode=False: [M_i, D] each
- If batch_mode=True: [B, M_i, D] each
batch_mode: Whether to use batch processing
return_vectors: If True, also return vectors from query to nearest points
return_tensor_indices: If False, skip the per-list-entry loop that maps the nearest point to
the index of its list entry (returned as None); distances and vectors are unaffected
Returns:
min_distances: Minimum distances [N] or [B, N]
min_indices: Tensor list indices [N] or [B, N]
vectors: (optional) Vectors from query to nearest points [N, D] or [B, N, D]
vectors = nearest_point - query_point
"""
# All tensors must reside on the same device
device = tensor_a.device
for i, t in enumerate(tensor_list):
if t.device != device:
raise RuntimeError(f"All tensors must be on the same device: tensor_a is on {device}, tensor_list[{i}] is on {t.device}")
if len(tensor_list) == 0:
raise ValueError("tensor_list cannot be empty")
device = tensor_a.device
original_batch_mode = batch_mode
if not batch_mode:
# Add batch dimension for unified processing
tensor_a = tensor_a.unsqueeze(0) # [1, N, D]
tensor_list = [t.unsqueeze(0) for t in tensor_list] # Each becomes [1, M_i, D]
B, N, D = tensor_a.shape
# Strategy 1: Concatenate all references for single parallel computation
ref_sizes = [ref.shape[1] for ref in tensor_list]
all_refs = torch.cat(tensor_list, dim=1) # [B, total_M, D]
# Single CUDA-parallel distance computation
dist_matrix = torch.cdist(tensor_a, all_refs, p=2.0) # [B, N, total_M]
# Find minimum distances and indices in parallel
min_dists, min_idx = dist_matrix.min(dim=2) # Both [B, N]
# Convert concatenated indices to tensor list indices
min_tensor_indices = None
if return_tensor_indices:
min_tensor_indices = torch.zeros_like(min_idx, dtype=torch.long)
cumsum = 0
for i, size in enumerate(ref_sizes):
mask = (min_idx >= cumsum) & (min_idx < cumsum + size)
min_tensor_indices[mask] = i
cumsum += size
# Compute vectors if requested
vectors = None
if return_vectors:
# Get the nearest points using advanced indexing
batch_indices = torch.arange(B, device=device).view(B, 1).expand(B, N)
nearest_points = all_refs[batch_indices, min_idx] # [B, N, D]
# Vector from query to nearest point
vectors = nearest_points - tensor_a # [B, N, D]
if not original_batch_mode:
min_dists = min_dists.squeeze(0)
if min_tensor_indices is not None:
min_tensor_indices = min_tensor_indices.squeeze(0)
if vectors is not None:
vectors = vectors.squeeze(0)
if return_vectors:
return min_dists, min_tensor_indices, vectors
else:
return min_dists, min_tensor_indices
def compute_knn_all_objects_vectorized(
positions: torch.Tensor,
valid_vtx_mask: torch.Tensor,
FAR: float = 1e6,
valid_obj_mask: torch.Tensor = None,
num_groups: int = 1,
active_slots: Optional[List[int]] = None
) -> torch.Tensor:
"""
Efficient batch-mode KNN for multi-object collision detection.
Args:
positions: (B, Nobj, Nv, 3) - vertex positions for all objects
valid_vtx_mask: (B, Nobj, Nv) - True for valid vertices
FAR: Distance offset for invalid vertices (default 1e6)
valid_obj_mask: (B, Nobj) - True for valid objects
num_groups: Number of groups to split objects into for GPU-parallel processing.
num_groups=1: per-object loop (Nobj iterations)
num_groups>1: GPU-parallel within each group (num_groups iterations)
Example: Nobj=564, num_groups=6 -> 94 objects processed in parallel per group
active_slots: Optional list of object slots that hold a valid object in at least one batch element
(per-object path only). Slots outside it are padding in every sample and their output
is zeroed either way, so they are not searched; valid objects still search every other slot.
Returns:
dist_vectors: (B, Nobj, Nv, 3) - vector from each vertex to nearest point on OTHER objects
"""
B, Nobj, Nv, D = positions.shape
device = positions.device
if Nobj < 2:
raise RuntimeError("compute_knn_all_objects_vectorized requires at least 2 objects")
if valid_obj_mask is None:
valid_obj_mask = valid_vtx_mask.any(dim=2) # (B, Nobj)
dist_vectors = torch.zeros(B, Nobj, Nv, D, device=device)
# Pre-compute masked positions (FAR offset for invalid vertices)
masked_positions = positions.clone()
vtx_mask_3d = valid_vtx_mask.unsqueeze(-1) # (B, Nobj, Nv, 1)
masked_positions = torch.where(vtx_mask_3d, masked_positions, masked_positions + FAR)
# ========== GPU-PARALLEL GROUPED PROCESSING ==========
if num_groups > 1 and Nobj > num_groups:
group_size = (Nobj + num_groups - 1) // num_groups
# All reference points (all objects, masked): (B, Nobj*Nv, 3)
all_refs_flat = masked_positions.reshape(B, Nobj * Nv, D)
for group_idx in range(num_groups):
g_start = group_idx * group_size
g_end = min(g_start + group_size, Nobj)
G = g_end - g_start # Objects in this group
# Query points for this group: (B, G, Nv, 3) -> (B, G*Nv, 3)
group_queries = positions[:, g_start:g_end].reshape(B, G * Nv, D)
# Compute full distance matrix: (B, G*Nv, Nobj*Nv)
dist_matrix = torch.cdist(group_queries, all_refs_flat, p=2.0)
# Create self-exclusion mask: for each query vertex, mask out vertices from same object
# Query vertex q belongs to object (g_start + q // Nv)
# Ref vertex r belongs to object (r // Nv)
query_obj_idx = torch.arange(G, device=device).unsqueeze(1).expand(G, Nv).reshape(-1) + g_start # (G*Nv,)
ref_obj_idx = torch.arange(Nobj, device=device).unsqueeze(1).expand(Nobj, Nv).reshape(-1) # (Nobj*Nv,)
# Self mask: (G*Nv, Nobj*Nv), True where query and ref belong to same object
self_mask = query_obj_idx.unsqueeze(1) == ref_obj_idx.unsqueeze(0) # (G*Nv, Nobj*Nv)
# Apply FAR to self-distances so they're not selected as minimum
dist_matrix = dist_matrix + self_mask.float() * FAR # (B, G*Nv, Nobj*Nv)
# Find minimum distance and corresponding ref index
min_dists, min_indices = dist_matrix.min(dim=2) # (B, G*Nv)
# Get nearest points using advanced indexing
batch_idx = torch.arange(B, device=device).view(B, 1).expand(B, G * Nv)
nearest_points = all_refs_flat[batch_idx, min_indices] # (B, G*Nv, 3)
# Compute vectors from query to nearest
vectors = nearest_points - group_queries # (B, G*Nv, 3)
# Reshape back and store: (B, G*Nv, 3) -> (B, G, Nv, 3)
dist_vectors[:, g_start:g_end] = vectors.reshape(B, G, Nv, D)
else:
# ========== PER-OBJECT PROCESSING ==========
# The references of object i are all other slots in slot order: the flattened (B, Nobj*Nv, D)
# tensor without object i's block, the same tensor as concatenating the other slots one by one.
refs_flat = masked_positions.reshape(B, Nobj * Nv, D)
for i in (range(Nobj) if active_slots is None else active_slots):
body_pos = positions[:, i] # (B, Nv, 3)
ref_list = [refs_flat[:, :i * Nv], refs_flat[:, (i + 1) * Nv:]]
_, _, body_dist_vec = compute_nearest_distance_to_list_cuda(
body_pos, ref_list, batch_mode=True, return_vectors=True, return_tensor_indices=False
)
dist_vectors[:, i] = body_dist_vec
# Zero out invalid query vertices
vtx_mask_expanded = valid_vtx_mask.unsqueeze(-1)
dist_vectors = torch.where(vtx_mask_expanded, dist_vectors, torch.zeros_like(dist_vectors))
# Zero out invalid objects
obj_mask_expanded = valid_obj_mask.unsqueeze(-1).unsqueeze(-1)
dist_vectors = torch.where(obj_mask_expanded, dist_vectors, torch.zeros_like(dist_vectors))
return dist_vectors
def knn_pack(valid_vtx_mask: torch.Tensor, valid_obj_mask: Optional[torch.Tensor] = None) -> dict:
"""Packed layout of every sample's valid vertices for compute_knn_packed.
The masks do not change over a batch or a rollout, so this is computed once (one host sync for the packed
width) and reused at every step. valid_vtx_mask (B, Nobj, Nv); valid_obj_mask (B, Nobj) or None.
Packed order = slot order, then vertex order: the order in which the per-object path lists candidates."""
B, Nobj, Nv = valid_vtx_mask.shape
vm = valid_vtx_mask.bool()
if valid_obj_mask is not None:
vm = vm & valid_obj_mask.bool().unsqueeze(-1)
n_obj = vm.any(dim=2).sum(dim=1)
if bool((n_obj < 2).any()):
raise ValueError("compute_knn_packed: every sample needs at least two objects with valid vertices")
flat = vm.reshape(B, Nobj * Nv)
counts = flat.sum(dim=1)
n = int(counts.max())
order = torch.argsort((~flat).to(torch.int8), dim=1, stable=True)[:, :n] # (B, n) valid first
valid = torch.arange(n, device=flat.device).unsqueeze(0) < counts.unsqueeze(1) # (B, n)
obj = torch.where(valid, torch.div(order, Nv, rounding_mode='floor'), torch.full_like(order, -1))
return {'order': order, 'valid': valid, 'obj': obj, 'shape': (B, Nobj, Nv)}
def compute_knn_packed(positions: torch.Tensor, pack: dict, block_elems: int = 1 << 27) -> torch.Tensor:
"""Vector from every valid vertex to the nearest valid vertex of any OTHER object, over the packed valid
vertices only (padded vertices and padded object slots never enter the distance matrix).
Same candidates in the same order and the same first-minimum tie-breaking as compute_knn_all_objects_vectorized,
and the squared distance is cdist's own construction ([-2x, |x|^2, 1] . [y, 1, |y|^2]) without the clamp and
square root over the whole matrix (monotone, so the argmin is unchanged). Results agree with the per-object
path up to the floating-point rounding of the distance values (a near-tie may pick a different, equally near
vertex). positions (B, Nobj, Nv, D); pack from knn_pack; returns (B, Nobj, Nv, D), zero at invalid entries."""
B, Nobj, Nv = pack['shape']
D = positions.shape[-1]
order, valid, obj = pack['order'], pack['valid'], pack['obj']
n = order.shape[1]
with torch.autocast(device_type='cuda', enabled=False):
flat = positions.float().reshape(B, Nobj * Nv, D)
P = torch.gather(flat, 1, order.unsqueeze(-1).expand(B, n, D)) # (B, n, D)
sq = P.pow(2).sum(dim=-1, keepdim=True)
ones = torch.ones_like(sq)
Xq = torch.cat([P.mul(-2), sq, ones], dim=-1) # (B, n, D+2)
Yr = torch.cat([P, ones, sq], dim=-1).transpose(1, 2) # (B, D+2, n)
rows = max(1, min(n, block_elems // max(1, B * n)))
idx = torch.empty(B, n, dtype=torch.long, device=P.device)
for s0 in range(0, n, rows):
s1 = min(n, s0 + rows)
d2 = torch.bmm(Xq[:, s0:s1], Yr) # (B, rows, n)
d2.masked_fill_((obj[:, s0:s1, None] == obj[:, None, :]) | ~valid[:, None, :], float('inf'))
idx[:, s0:s1] = d2.argmin(dim=2)
nearest = torch.gather(P, 1, idx.unsqueeze(-1).expand(B, n, D))
vec = torch.where(valid.unsqueeze(-1), nearest - P, torch.zeros((), device=P.device))
out = torch.zeros(B, Nobj * Nv, D, device=P.device, dtype=torch.float32)
out.scatter_(1, order.unsqueeze(-1).expand(B, n, D), vec)
return out.reshape(B, Nobj, Nv, D).to(positions.dtype)
def compute_nearest_distance_to_list_cuda_chunked(
tensor_a: torch.Tensor,
tensor_list: List[torch.Tensor],
chunk_size: int = 10000
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Memory-efficient CUDA version using chunked processing for large tensors.
Processes distance computation in chunks to avoid OOM on large datasets.
Args:
tensor_a: Query tensor on CUDA, shape [N, D]
tensor_list: List of reference tensors on CUDA, each [M_i, D]
chunk_size: Maximum number of reference points to process at once
Returns:
min_distances: shape [N]
min_tensor_indices: shape [N]
"""
if not tensor_a.is_cuda:
raise RuntimeError("tensor_a must be on CUDA; CPU execution is not supported.")
for i, t in enumerate(tensor_list):
if not t.is_cuda:
raise RuntimeError(f"tensor_list[{i}] must be on CUDA; CPU execution is not supported.")
N, D = tensor_a.shape
device = tensor_a.device
# Initialize results
global_min_dists = torch.full((N,), float('inf'), device=device)
global_tensor_idx = torch.zeros(N, dtype=torch.long, device=device)
for tensor_idx, ref_tensor in enumerate(tensor_list):
M = ref_tensor.shape[0]
# Process this reference tensor in chunks
chunk_min_dists = torch.full((N,), float('inf'), device=device)
for start_idx in range(0, M, chunk_size):
end_idx = min(start_idx + chunk_size, M)
ref_chunk = ref_tensor[start_idx:end_idx]
# Compute distances for this chunk
dist_matrix = torch.cdist(tensor_a, ref_chunk, p=2.0) # [N, chunk_size]
chunk_dists, _ = dist_matrix.min(dim=1) # [N]
# Update chunk minimums
chunk_min_dists = torch.minimum(chunk_min_dists, chunk_dists)
# Update global minimums
update_mask = chunk_min_dists < global_min_dists
global_min_dists[update_mask] = chunk_min_dists[update_mask]
global_tensor_idx[update_mask] = tensor_idx
return global_min_dists, global_tensor_idx
def compute_nearest_distance_to_list_cuda_optimized(
tensor_a: torch.Tensor,
tensor_list: List[torch.Tensor],
max_memory_mb: float = 1000
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Adaptive CUDA implementation that chooses strategy based on memory constraints.
Args:
tensor_a: Query tensor on CUDA, shape [N, D]
tensor_list: List of reference tensors on CUDA
max_memory_mb: Maximum memory to use in MB
Returns:
min_distances: shape [N]
min_tensor_indices: shape [N]
"""
if not tensor_a.is_cuda:
raise RuntimeError("All tensors must be on CUDA; CPU execution is not supported.")
N, D = tensor_a.shape
# Estimate memory requirement
total_ref_points = sum(t.shape[0] for t in tensor_list)
memory_needed_mb = (N * total_ref_points * 4) / (1024 * 1024) # float32 = 4 bytes
if memory_needed_mb <= max_memory_mb:
# Use fast concatenated version
return compute_nearest_distance_to_list_cuda(tensor_a, tensor_list, batch_mode=False)
else:
# Use memory-efficient chunked version
chunk_size = int(max_memory_mb * 1024 * 1024 / (N * 4))
return compute_nearest_distance_to_list_cuda_chunked(tensor_a, tensor_list, chunk_size)
# Self-tests
if __name__ == "__main__":
print("=" * 60)
print("Testing CUDA-accelerated nearest distance computation")
print("=" * 60)
# Create test data ON CUDA
tensor_a = torch.rand(1000, 3).cuda() # 1000 query points
tensor_list = [
torch.rand(500, 3).cuda(), # 500 reference points
torch.rand(800, 3).cuda(), # 800 reference points
torch.rand(1200, 3).cuda(), # 1200 reference points
]
print("\nTest 1: Basic CUDA version")
try:
min_dists, min_indices = compute_nearest_distance_to_list_cuda(
tensor_a, tensor_list, batch_mode=False
)
print(f"Success. Min distances shape: {min_dists.shape}")
print(f" First 5 distances: {min_dists[:5]}")
print(f" Tensor indices: {min_indices[:5]}")
except Exception as e:
print(f"Error: {e}")
print("\nTest 1b: With vector output")
try:
min_dists, min_indices, vectors = compute_nearest_distance_to_list_cuda(
tensor_a, tensor_list, batch_mode=False, return_vectors=True
)
print(f"Success. Vectors shape: {vectors.shape}")
print(f" First vector: {vectors[0]}")
# Verify vector length matches distance
vec_norm = vectors[0].norm()
print(f" Vector norm: {vec_norm:.6f}, Distance: {min_dists[0]:.6f}")
print(f" Match: {torch.allclose(vec_norm, min_dists[0], rtol=1e-5)}")
except Exception as e:
print(f"Error: {e}")
print("\nTest 2: Batch mode")
tensor_a_batch = torch.rand(4, 100, 3).cuda() # Batch of 4
tensor_list_batch = [
torch.rand(4, 50, 3).cuda(),
torch.rand(4, 80, 3).cuda(),
]
try:
min_dists_batch, min_indices_batch = compute_nearest_distance_to_list_cuda(
tensor_a_batch, tensor_list_batch, batch_mode=True
)
print(f"Success. Batch distances shape: {min_dists_batch.shape}")
except Exception as e:
print(f"Error: {e}")
print("\nTest 3: Chunked version for large tensors")
try:
min_dists_chunked, min_indices_chunked = compute_nearest_distance_to_list_cuda_chunked(
tensor_a, tensor_list, chunk_size=100
)
print(f"Success. Chunked distances shape: {min_dists_chunked.shape}")
# Verify chunked gives same result as full
diff = (min_dists - min_dists_chunked).abs().max()
print(f" Difference from full version: {diff:.6f}")
except Exception as e:
print(f"Error: {e}")
print("\nTest 4: Testing with CPU tensor (should fail)")
tensor_cpu = torch.rand(10, 3) # CPU tensor
try:
min_dists, min_indices = compute_nearest_distance_to_list_cuda(
tensor_cpu, tensor_list, batch_mode=False
)
print("FAILED: expected an error for the CPU tensor")
except RuntimeError as e:
print(f"Correctly rejected CPU tensor: {e}")
print("\nTest 5: Performance comparison")
import time
# Large test case
large_query = torch.rand(5000, 3).cuda()
large_refs = [torch.rand(2000, 3).cuda() for _ in range(5)]
# Time the optimized version
start = time.time()
result = compute_nearest_distance_to_list_cuda_optimized(large_query, large_refs)
cuda_time = time.time() - start
print(f"Processed 5000 queries against 10000 references in {cuda_time:.3f}s")
print(f" Throughput: {5000 * 10000 / cuda_time / 1e6:.1f} million distance computations/second")
print("\n" + "=" * 60)
print("Test 6: Vectorized multi-object KNN (correctness verification)")
print("=" * 60)
# Create test data: B batches, Nobj objects, Nv vertices each
B, Nobj, Nv = 4, 5, 100
FAR = 1e6
positions = torch.rand(B, Nobj, Nv, 3).cuda()
# Create a valid mask (80% of vertices are valid)
valid_mask = torch.rand(B, Nobj, Nv).cuda() > 0.2
print(f" Test setup: B={B}, Nobj={Nobj}, Nv={Nv}")
print(f" Valid vertices per object: {valid_mask.float().mean(dim=(0,2)) * Nv}")
# Method 1: loop-based reference implementation
print("\n Method 1: Loop-based KNN...")
dist_vectors_loop = torch.zeros(B, Nobj, Nv, 3, device=positions.device)
start = time.time()
for i in range(Nobj):
body_pos = positions[:, i] # (B, Nv, 3)
ref_list = []
for j in range(Nobj):
if j == i:
continue
other_pos = positions[:, j].clone()
other_vmask = valid_mask[:, j].unsqueeze(-1) # (B, Nv, 1)
other_pos = torch.where(other_vmask, other_pos, other_pos + FAR)
ref_list.append(other_pos)
_, _, body_dist_vec = compute_nearest_distance_to_list_cuda(
body_pos, ref_list, batch_mode=True, return_vectors=True
)
dist_vectors_loop[:, i] = body_dist_vec
loop_time = time.time() - start
print(f" Loop-based completed in {loop_time*1000:.2f}ms")
# Method 2: Vectorized approach
print("\n Method 2: Vectorized KNN...")
start = time.time()
dist_vectors_vec = compute_knn_all_objects_vectorized(positions, valid_mask, FAR)
vec_time = time.time() - start
print(f" Vectorized completed in {vec_time*1000:.2f}ms")
# Compare results
print("\n Comparing results...")
# Only compare valid query vertices (invalid queries may have different results)
valid_query_mask = valid_mask.unsqueeze(-1).expand_as(dist_vectors_loop)
diff = (dist_vectors_loop - dist_vectors_vec).abs()
diff_valid = diff[valid_query_mask]
max_diff = diff_valid.max().item() if diff_valid.numel() > 0 else 0
mean_diff = diff_valid.mean().item() if diff_valid.numel() > 0 else 0
print(f" Max difference (valid vertices): {max_diff:.6e}")
print(f" Mean difference (valid vertices): {mean_diff:.6e}")
if max_diff < 1e-5:
print(f" PASSED: Results match within tolerance (1e-5)")
else:
print(f" FAILED: Results differ by more than tolerance")
# Debug: find where they differ
diff_mask = diff > 1e-5
num_diff = diff_mask.sum().item()
print(f" Number of differing elements: {num_diff}")
# Speedup
speedup = loop_time / vec_time if vec_time > 0 else float('inf')
print(f"\n Speedup: {speedup:.2f}x")
# Test with larger data
print("\n Testing with larger data (B=8, Nobj=10, Nv=500)...")
B2, Nobj2, Nv2 = 8, 10, 500
positions2 = torch.rand(B2, Nobj2, Nv2, 3).cuda()
valid_mask2 = torch.rand(B2, Nobj2, Nv2).cuda() > 0.1
# Warmup
_ = compute_knn_all_objects_vectorized(positions2, valid_mask2, FAR)
torch.cuda.synchronize()
start = time.time()
_ = compute_knn_all_objects_vectorized(positions2, valid_mask2, FAR)
torch.cuda.synchronize()
vec_time2 = time.time() - start
print(f" Vectorized (large): {vec_time2*1000:.2f}ms")
print(f" Throughput: {B2 * Nobj2 * Nv2 / vec_time2 / 1e6:.2f}M vertices/sec")
print("\n" + "=" * 60)
print("Test 7: Invalid vertex/object masking verification")
print("=" * 60)
B, Nobj, Nv = 2, 4, 50
positions = torch.rand(B, Nobj, Nv, 3).cuda()
# Create mask with some fully invalid objects and some invalid vertices
valid_vtx_mask = torch.ones(B, Nobj, Nv, dtype=torch.bool, device='cuda')
valid_obj_mask = torch.ones(B, Nobj, dtype=torch.bool, device='cuda')
# Make object 2 fully invalid (no valid vertices)
valid_vtx_mask[:, 2, :] = False
valid_obj_mask[:, 2] = False
# Make some vertices invalid in valid objects
valid_vtx_mask[0, 0, 10:20] = False # batch 0, object 0, vertices 10-19 invalid
valid_vtx_mask[1, 1, 30:40] = False # batch 1, object 1, vertices 30-39 invalid
print(f" Setup: B={B}, Nobj={Nobj}, Nv={Nv}")
print(f" Object 2 is fully invalid (all vertices masked)")
print(f" Some vertices in valid objects are also masked")
dist_vectors = compute_knn_all_objects_vectorized(
positions, valid_vtx_mask, FAR, valid_obj_mask
)
# Check 1: Invalid object (object 2) should have ALL zero distance vectors
obj2_dist = dist_vectors[:, 2, :, :] # (B, Nv, 3)
obj2_is_zero = (obj2_dist.abs() < 1e-10).all()
print(f"\n Check 1: Invalid object (obj 2) has zero dist_vec: {obj2_is_zero.item()}")
if obj2_is_zero:
print(" PASSED: Invalid object has all zeros")
else:
print(f" FAILED: Non-zero values found: max={obj2_dist.abs().max().item():.6e}")
# Check 2: Invalid vertices in valid objects should have zero distance vectors
invalid_vtx_dist_0 = dist_vectors[0, 0, 10:20, :] # batch 0, obj 0, vtx 10-19
invalid_vtx_dist_1 = dist_vectors[1, 1, 30:40, :] # batch 1, obj 1, vtx 30-39
vtx0_is_zero = (invalid_vtx_dist_0.abs() < 1e-10).all()
vtx1_is_zero = (invalid_vtx_dist_1.abs() < 1e-10).all()
print(f"\n Check 2: Invalid vertices have zero dist_vec:")
print(f" batch 0, obj 0, vtx 10-19: {vtx0_is_zero.item()}")
print(f" batch 1, obj 1, vtx 30-39: {vtx1_is_zero.item()}")
if vtx0_is_zero and vtx1_is_zero:
print(" PASSED: Invalid vertices have all zeros")
else:
print(f" FAILED: Non-zero values found")
# Check 3: Valid vertices in valid objects should have NON-zero distance vectors
# (at least most of them, since they should find nearest neighbors on other objects)
valid_vtx_dist_0 = dist_vectors[0, 0, :10, :] # batch 0, obj 0, vtx 0-9 (valid)
valid_vtx_dist_1 = dist_vectors[0, 1, :30, :] # batch 0, obj 1, vtx 0-29 (valid)
valid_has_nonzero_0 = (valid_vtx_dist_0.abs() > 1e-10).any()
valid_has_nonzero_1 = (valid_vtx_dist_1.abs() > 1e-10).any()
print(f"\n Check 3: Valid vertices have NON-zero dist_vec (found neighbors):")
print(f" batch 0, obj 0, vtx 0-9: {valid_has_nonzero_0.item()}")
print(f" batch 0, obj 1, vtx 0-29: {valid_has_nonzero_1.item()}")
if valid_has_nonzero_0 and valid_has_nonzero_1:
print(" PASSED: Valid vertices have non-zero distances")
else:
print(" FAILED: Valid vertices should find neighbors on other objects")
print("\n" + "=" * 60)
print("All tests completed! CUDA acceleration is working.")