Spaces:
Running on Zero
Running on Zero
Download src/utils/cuda_knn.py from hugging-apps/rigidformer: direct link, hf CLI and curl.
- Browser
- Download file 26.7 kB
-
https://huggingface.co/spaces/hugging-apps/rigidformer/resolve/main/src/utils/cuda_knn.py
- Command line
-
hf download hf://spaces/hugging-apps/rigidformer/src/utils/cuda_knn.py
-
curl -L -o cuda_knn.py https://huggingface.co/spaces/hugging-apps/rigidformer/resolve/main/src/utils/cuda_knn.py
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.") |