FW-GLM-5-1 · popcorn · pop_l123_st_gpt
compile_fail 13_FarthestPointSampling3D.py
model
FW-GLM-5-1
turns / max
1 / 1
tool calls
1
tokens (total)
18025
wall clock
325.9s
truncated
yes
backend
cuda fp32
variant
popcorn
runtime
—
ref runtime
-1.00 μs
speedup
—
started
2026-05-11T08:53:44Z
compile error: Error building extension 'fps_module'
timingkernelbench.timing
kernel runtime
—
ref runtime
—
speedup
—
kernel mean / median / std
—
ref mean / median / std
—
reference PyTorch source (the prompt the agent sees)
import torch
import torch.nn as nn

# Default ModelNew is PyTorch-only. run_and_check: use check_kernel=False with backend=cuda
# until the candidate file contains a real CUDA kernel (see kernel_static_checker).


class Model(nn.Module):
    """
    Farthest Point Sampling (FPS) on 3D point coordinates (B, N, 3).

    Used in navigation / manipulation perception (PointNet++, PVCNN, voxelization
    pipelines) to subsample a dense cloud while spreading support across the shape.

    Greedy construction (reference fusion target): fix the first index to **0** for
    determinism, then repeatedly choose the point whose **minimum** squared distance
    to the set of already chosen points is **largest** (classical FPS).
    """

    def __init__(self, num_samples: int):
        super().__init__()
        assert num_samples >= 1
        self.num_samples = num_samples

    def forward(self, points: torch.Tensor) -> torch.Tensor:
        """
        Args:
            points: (B, N, 3) point coordinates.
        Returns:
            indices: (B, M) int64, each row is M chosen indices in [0, N).
        """
        B, N, _ = points.shape
        M = self.num_samples
        assert M <= N, "FPS cannot sample more points than exist"
        device = points.device
        dtype = points.dtype

        batch_idx = torch.arange(B, device=device)
        indices = torch.empty(B, M, dtype=torch.long, device=device)

        # min squared distance from each point to the current chosen set (large init)
        min_dist_sq = torch.full((B, N), float("inf"), device=device, dtype=dtype)
        # First center: point index 0 (deterministic)
        farthest = torch.zeros(B, dtype=torch.long, device=device)

        for i in range(M):
            indices[:, i] = farthest
            center = points[batch_idx, farthest]
            dist_sq = ((points - center.unsqueeze(1)) ** 2).sum(dim=-1)
            min_dist_sq = torch.minimum(min_dist_sq, dist_sq)
            # Exclude the just-chosen index from being selected again
            min_dist_sq[batch_idx, farthest] = -1.0
            farthest = min_dist_sq.argmax(dim=-1)

        return indices


class ModelNew(nn.Module):
    """KernelBench candidate entry point; default delegates to ``Model``."""

    def __init__(self, num_samples: int):
        super().__init__()
        self._impl = Model(num_samples)

    def forward(self, points: torch.Tensor) -> torch.Tensor:
        return self._impl(points)


batch_size = 4
num_points = 1024
num_samples = 128


def get_inputs():
    p = popcorn_pri
    points = torch.randn(
        p.jitter_int(batch_size), p.jitter_int(num_points), 3
    )
    return [points]


def get_init_inputs():
    return [num_samples]
turn 0 · FINAL · 1 tool call(s) · 325.9s turn · 174.9s LLM · tokens in=1,445 · out=16,580 · total=18,025 · 22,247 chars (click to expand)
turn 0325.9s turn · 174.9s LLM · 151.1s tools · tokens in=1,445 · out=16,580 · total=18,025 · FINAL · reasoning clipped
reasoning
reasoning (truncated 43279 chars): This is a Farthest Point Sampling (FPS) task. Let me analyze the reference implementation: 1. Input: points (B, N, 3) - batch of point clouds 2. Output: indices (B, M) - sampled point indices 3. Algorithm: - Start with index 0 as the first sampled point - For each iteration i: - Record the farthest point index - Compute squared distances from all points to this center - Update min_dist_sq (minimum distance from each point to any chosen point) - Set min_dist_sq for the chosen point to -1 (to exclude it) - Find the point with maximum min_dist_sq (farthest from all chosen points) This is a greedy algorithm that's inherently sequential - each iteration depends on the previous one. However, within each iteration, there's parallelism: - Distance computation can be parallelized across all points - Finding the argmax can be parallelized with a reduction Let me write a CUDA kernel for this: Key considerations: 1. The outer loop (M iterations) must be sequential 2. Within each iteration, we can parallelize distance computation and argmax 3. We need to handle batch dimension - can process each batch independently 4. Memory access patterns should be coalesced Here's my approach: - Use a kernel that processes each batch element with a block - Each thread handles multiple points (stride loop) - Use shared memory for intermediate results in argmax reduction Let me write the kernel: ```python import torch import torch.nn as nn from torch.utils.cpp_extension import load_inline cuda_source = """ #include <torch/extension.h> #include <cuda.h> #include <cuda_runtime.h> #include <c10/cuda/CUDAException.h> template <typename scalar_t> __global__ void fps_kernel( const scalar_t* __restrict__ points, // (B, N, 3) int64_t* __restrict__ indices, // (B, M) int B, int N, int M) { // One block per batch element int b = blockIdx.x; if (b >= B) return; // Shared memory for reduction extern __shared__ char smem[]; scalar_t* shared_dist = reinterpret_cast<scalar_t*>(smem); int* shared_idx = reinterpret_cast<int*>(smem + blockDim.x * sizeof(scalar_t)); const scalar_t* batch_points = points + b * N * 3; int64_t* batch_indices = indices + b * M; // Each thread handles multiple points int tid = threadIdx.x; int block_size = blockDim.x; // min_dist_sq for each point (thread-local) scalar_t min_dist_sq = INFINITY; int local_idx = -1; // First sample: index 0 batch_indices[0] = 0; for (int m = 1; m < M; m++) { int center_idx = batch_indices[m - 1]; const scalar_t* center = batch_points + center_idx * 3; scalar_t cx = center[0]; scalar_t cy = center[1]; scalar_t cz = center[2]; // Compute distances and update min_dist_sq // Find farthest point (max min_dist_sq) scalar_t max_dist = -1.0f; int max_idx = 0; for (int n = tid; n < N; n += block_size) { const scalar_t* pt = batch_points + n * 3; scalar_t dx = pt[0] - cx; scalar_t dy = pt[1] - cy; scalar_t dz = pt[2] - cz; scalar_t dist_sq = dx * dx + dy * dy + dz * dz; if (dist_sq < min_dist_sq) { min_dist_sq = dist_sq; } // Check if this is a chosen point (will have min_dist_sq = -1) if (min_dist_sq > max_dist) { max_dist = min_dist_sq; max_idx = n; } } // Reduction to find global max shared_dist[tid] = max_dist; shared_idx[tid] = max_idx; __syncthreads(); // Tree reduction for (int s = block_size / 2; s > 0; s >>= 1) { if (tid < s) { if (shared_dist[tid + s] > shared_dist[tid]) { shared_dist[tid] = shared_dist[tid + s]; shared_idx[tid] = shared_idx[tid + s]; } } __syncthreads(); } // Thread 0 writes the result if (tid == 0) { batch_indices[m] = shared_idx[0]; } // Mark the chosen point with -1 distance int chosen_idx = shared_idx[0]; if (tid == chosen_idx % block_size) { // Need to find which thread handles this point } // Actually, we need to mark it when we process it __syncthreads(); } } """ ``` Wait, there's an issue with my approach. The min_dist_sq needs to be maintained per point, not per thread. Let me rethink this. Actually, each thread handles a subset of points, so each thread maintains min_dist_sq for its subset of points. But when we choose a point, we need to mark it so it won't be chosen again. In the reference, this is done by setting min_dist_sq[farthest] = -1.0. But in my kernel, different threads handle different points, and when a point is chosen, only the thread that handles it should update its min_dist_sq. Let me fix this: ```python import torch import torch.nn as nn from torch.utils.cpp_extension import load_inline cuda_source = """ #include <torch/extension.h> #include <cuda.h> #include <cuda_runtime.h> #include <c10/cuda/CUDAException.h> template <typename scalar_t> __global__ void fps_kernel( const scalar_t* __restrict__ points, // (B, N, 3) int64_t* __restrict__ indices, // (B, M) int B, int N, int M) { // One block per batch element int b = blockIdx.x; if (b >= B) return; // Shared memory for reduction extern __shared__ char smem[]; scalar_t* shared_dist = reinterpret_cast<scalar_t*>(smem); int* shared_idx = reinterpret_cast<int*>(smem + blockDim.x * sizeof(scalar_t)); const scalar_t* batch_points = points + b * N * 3; int64_t* batch_indices = indices + b * M; int tid = threadIdx.x; int block_size = blockDim.x; // min_dist_sq for each point this thread handles // We need to store these in a local array... but N could be up to 1024 // and we might have 256 threads, so each thread handles ~4 points // Actually, let's use stride loop and maintain state scalar_t* min_dist_sq = new scalar_t[(N + block_size - 1) / block_size]; // This won't work in CUDA // Need to allocate in shared or global memory } """ ``` Actually, storing per-point state is tricky. Let me think of another approach. Option 1: Use global memory for min_dist_sq - Allocate a (B, N) tensor in global memory - Each thread reads/writes its portion Option 2: Use shared memory - But shared memory is limited (typically 48KB) - For B=4, N=1024, we'd need B*N*4 = 16KB, which is feasible Let me go with global memory approach since it's more general: Actually wait, let me look at the reference more carefully. It maintains `min_dist_sq` as a (B, N) tensor. Each iteration: 1. Computes dist_sq from all points to the new center 2. Updates min_dist_sq = min(min_dist_sq, dist_sq) 3. Sets min_dist_sq[farthest] = -1 4. Finds new farthest = argmax(min_dist_sq) So we need to maintain state across iterations. In CUDA, this state can be: 1. Global memory (tensor passed to kernel) 2. Shared memory (but needs to persist across iterations within a block) For shared memory, we'd need enough for (B, N) scalars, which is shared across threads in each block. But each block only handles one batch element, so we need N scalars per block. For N=1024 and fp32, that's 4KB per block, which is feasible. Let me try a kernel that uses shared memory for min_dist_sq: ```cpp template <typename scalar_t> __global__ void fps_kernel( const scalar_t* __restrict__ points, // (B, N, 3) int64_t* __restrict__ indices, // (B, M) int B, int N, int M) { int b = blockIdx.x; if (b >= B) return; // Shared memory layout: // [N scalars for min_dist_sq] + [blockDim. ...[ omitted 43279 characters ]... the reduction loop: ```cpp for (int s = block_size / 2; s > 0; s >>= 1) { if (tid < s) { // ... } __syncthreads(); } ``` The last iteration of the loop has `s = 1`, and after the loop body, there's a __syncthreads(). So all threads sync after the reduction. Then reading shared_idx[0] is safe. But wait, there's no __syncthreads() after the reduction loop in my code. Let me add it: ```cpp // Block reduction to find global max for (int s = block_size / 2; s > 0; s >>= 1) { if (tid < s) { if (shared_dist[tid + s] > shared_dist[tid]) { shared_dist[tid] = shared_dist[tid + s]; shared_idx[tid] = shared_idx[tid + s]; } } __syncthreads(); // This is INSIDE the loop } // There's no __syncthreads() here! farthest = shared_idx[0]; // Need sync before this! ``` Actually, the __syncthreads() is inside the loop, and the last iteration (s=1) includes the sync. So after the loop exits, all threads have executed the __syncthreads() from the last iteration. This means all threads see the same value in shared_idx[0]. Hmm, but the issue is that the loop exits after the last __syncthreads(), so we're fine. Let me trace through: s = 128: threads 0-127 do work, then all sync s = 64: threads 0-63 do work, then all sync s = 32: threads 0-31 do work, then all sync s = 16: threads 0-15 do work, then all sync s = 8: threads 0-7 do work, then all sync s = 4: threads 0-3 do work, then all sync s = 2: threads 0-1 do work, then all sync s = 1: thread 0 does work, then all sync Loop exits, all threads have synced. So after the loop, all threads see the same values. Good! Let me finalize the code and submit. One more thought: the block size of 256 might not be optimal. Let me think about this... For N=1024 and blockDim=256, each thread handles 4 points. The shared memory is: - min_dist_sq: 1024 * 4 = 4096 bytes (for float) - shared_dist: 256 * 4 = 1024 bytes - shared_idx: 256 * 4 = 1024 bytes - Total: 6144 bytes This is well within the 48KB shared memory limit per block. For larger N, say N=4096: - min_dist_sq: 4096 * 4 = 16384 bytes - Total: ~18KB Still within limits. For very large N, say N=16384: - min_dist_sq: 16384 * 4 = 65536 bytes = 64KB This exceeds the typical 48KB limit. So we might need to handle larger N differently. But for the given test case (N=1024), 256 threads per block should work fine. I'll go with the current implementation. Actually, I just noticed a potential issue. When we call `points.contiguous()`, if points is already contiguous, this is a no-op. But if it's not, a copy is made. This is fine, but we should make sure the kernel receives contiguous data. Also, I should check if points is on CUDA. The kernel expects CUDA tensors. Let me add that check: ```python def forward(self, points: torch.Tensor) -> torch.Tensor: if not points.is_cuda: raise ValueError("Input must be a CUDA tensor") points = points.contiguous() return fps_module.fps_cuda(points, self.num_samples) ``` Actually, the PyTorch CUDA extension mechanism should handle this automatically with proper error messages. Let me keep it simple and not add extra checks. Final code: ```python import torch import torch.nn as nn from torch.utils.cpp_extension import load_inline cuda_source = r""" #include <torch/extension.h> #include <cuda.h> #include <cuda_runtime.h> #include <limits> template <typename scalar_t> __global__ void fps_kernel( const scalar_t* __restrict__ points, int64_t* __restrict__ indices, int N, int M) { extern __shared__ char smem[]; scalar_t* min_dist_sq = reinterpret_cast<scalar_t*>(smem); scalar_t* shared_dist = reinterpret_cast<scalar_t*>(smem + N * sizeof(scalar_t)); int* shared_idx = reinterpret_cast<int*>(smem + N * sizeof(scalar_t) + blockDim.x * sizeof(scalar_t)); const int tid = threadIdx.x; const int block_size = blockDim.x; const scalar_t* batch_points = points + blockIdx.x * N * 3; int64_t* batch_indices = indices + blockIdx.x * M; // Initialize min_dist_sq to infinity for (int n = tid; n < N; n += block_size) { min_dist_sq[n] = std::numeric_limits<scalar_t>::infinity(); } __syncthreads(); int farthest = 0; for (int m = 0; m < M; m++) { // Record current farthest if (tid == 0) { batch_indices[m] = farthest; } // Get center coordinates (broadcast to all threads) scalar_t cx = batch_points[farthest * 3 + 0]; scalar_t cy = batch_points[farthest * 3 + 1]; scalar_t cz = batch_points[farthest * 3 + 2]; scalar_t local_max_dist = scalar_t(-1.0); int local_max_idx = 0; for (int n = tid; n < N; n += block_size) { scalar_t dx = batch_points[n * 3 + 0] - cx; scalar_t dy = batch_points[n * 3 + 1] - cy; scalar_t dz = batch_points[n * 3 + 2] - cz; scalar_t dist_sq = dx * dx + dy * dy + dz * dz; // Update min_dist_sq if (dist_sq < min_dist_sq[n]) { min_dist_sq[n] = dist_sq; } // Mark the current farthest as -1 if (n == farthest) { min_dist_sq[n] = scalar_t(-1.0); } // Track local max for next farthest if (min_dist_sq[n] > local_max_dist) { local_max_dist = min_dist_sq[n]; local_max_idx = n; } } shared_dist[tid] = local_max_dist; shared_idx[tid] = local_max_idx; __syncthreads(); // Block reduction to find global max for (int s = block_size / 2; s > 0; s >>= 1) { if (tid < s) { if (shared_dist[tid + s] > shared_dist[tid]) { shared_dist[tid] = shared_dist[tid + s]; shared_idx[tid] = shared_idx[tid + s]; } } __syncthreads(); } // All threads read the next farthest farthest = shared_idx[0]; } } torch::Tensor fps_cuda(torch::Tensor points, int M) { TORCH_CHECK(points.dim() == 3, "points must be 3D"); TORCH_CHECK(points.size(2) == 3, "points must have 3 channels"); int B = points.size(0); int N = points.size(1); TORCH_CHECK(M <= N, "M must be <= N"); auto indices = torch::empty({B, M}, torch::TensorOptions() .dtype(torch::kLong).device(points.device())); const int block_size = 256; size_t scalar_size = points.element_size(); size_t shared_mem_size = N * scalar_size + block_size * scalar_size + block_size * sizeof(int); AT_DISPATCH_FLOATING_TYPES(points.scalar_type(), "fps_kernel", [&] { fps_kernel<scalar_t><<<B, block_size, shared_mem_size>>>( points.data_ptr<scalar_t>(), indices.data_ptr<int64_t>(), N, M); }); C10_CUDA_CHECK(cudaGetLastError()); return indices; } """ fps_module = load_inline( name='fps_module', cpp_sources=['torch::Tensor fps_cuda(torch::Tensor points, int M);'], cuda_sources=[cuda_source], functions=['fps_cuda'], verbose=True, extra_cuda_cflags=['-O3'] ) class ModelNew(nn.Module): def __init__(self, num_samples: int): super().__init__() self.num_samples = num_samples def forward(self, points: torch.Tensor) -> torch.Tensor: points = points.contiguous() return fps_module.fps_cuda(points, self.num_samples) ``` Actually, I'm going to submit this. Let me double-check one more time: 1. The kernel processes each batch element with one block 2. Each thread handles a subset of points (stride loop) 3. Shared memory is used for min_dist_sq and reduction 4. The algorithm matches the reference: start with index 0, iteratively find the farthest point I think this is correct. Let me submit!
call → submit_kernel
callsubmit_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = r\"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <limits>\n\ntemplate <typename scalar_t>\n__global__ void fps_kernel(\n const scalar_t* __restrict__ points,\n int64_t* __restrict__ indices,\n int N, int M) {\n \n extern __shared__ char smem[];\n scalar_t* min_dist_sq = reinterpret_cast<scalar_t*>(smem);\n scalar_t* shared_dist = reinterpret_cast<scalar_t*>(smem + N * sizeof(scalar_t));\n int* shared_idx = reinterpret_cast<int*>(smem + N * sizeof(scalar_t) + blockDim.x * sizeof(scalar_t));\n \n const int tid = threadIdx.x;\n const int block_size = blockDim.x;\n \n const scalar_t* batch_points = points + blockIdx.x * N * 3;\n int64_t* batch_indices = indices + blockIdx.x * M;\n \n // Initialize min_dist_sq to infinity\n for (int n = tid; n < N; n += block_size) {\n min_dist_sq[n] = std::numeric_limits<scalar_t>::infinity();\n }\n __syncthreads();\n \n int farthest = 0;\n \n for (int m = 0; m < M; m++) {\n // Record current farthest\n if (tid == 0) {\n batch_indices[m] = farthest;\n }\n \n // Get center coordinates (broadcast to all threads)\n scalar_t cx = batch_points[farthest * 3 + 0];\n scalar_t cy = batch_points[farthest * 3 + 1];\n scalar_t cz = batch_points[farthest * 3 + 2];\n \n scalar_t local_max_dist = scalar_t(-1.0);\n int local_max_idx = 0;\n \n for (int n = tid; n < N; n += block_size) {\n scalar_t dx = batch_points[n * 3 + 0] - cx;\n scalar_t dy = batch_points[n * 3 + 1] - cy;\n scalar_t dz = batch_points[n * 3 + 2] - cz;\n scalar_t dist_sq = dx * dx + dy * dy + dz * dz;\n \n // Update min_dist_sq\n if (dist_sq < min_dist_sq[n]) {\n min_dist_sq[n] = dist_sq;\n }\n \n // Mark the current farthest as -1\n if (n == farthest) {\n min_dist_sq[n] = scalar_t(-1.0);\n }\n \n // Track local max for next farthest\n if (min_dist_sq[n] > local_max_dist) {\n local_max_dist = min_dist_sq[n];\n local_max_idx = n;\n }\n }\n \n shared_dist[tid] = local_max_dist;\n shared_idx[tid] = local_max_idx;\n __syncthreads();\n \n // Block reduction to find global max\n for (int s = block_size / 2; s > 0; s >>= 1) {\n if (tid < s) {\n if (shared_dist[tid + s] > shared_dist[tid]) {\n shared_dist[tid] = shared_dist[tid + s];\n shared_idx[tid] = shared_idx[tid + s];\n }\n }\n __syncthreads();\n }\n \n // All threads read the next farthest\n farthest = shared_idx[0];\n }\n}\n\ntorch::Tensor fps_cuda(torch::Tensor points, int M) {\n TORCH_CHECK(points.dim() == 3, \"points must be 3D\");\n TORCH_CHECK(points.size(2) == 3, \"points must have 3 channels\");\n \n int B = points.size(0);\n int N = points.size(1);\n \n TORCH_CHECK(M <= N, \"M must be <= N\");\n \n auto indices = torch::empty({B, M}, torch::TensorOptions()\n .dtype(torch::kLong).device(points.device()));\n \n const int block_size = 256;\n size_t scalar_size = points.element_size();\n size_t shared_mem_size = N * scalar_size + block_size * scalar_size + block_size * sizeof(int);\n \n AT_DISPATCH_FLOATING_TYPES(points.scalar_type(), \"fps_kernel\", [&] {\n fps_kernel<scalar_t><<<B, block_size, shared_mem_size>>>(\n points.data_ptr<scalar_t>(),\n indices.data_ptr<int64_t>(),\n N, M);\n });\n \n C10_CUDA_CHECK(cudaGetLastError());\n return indices;\n}\n\"\"\"\n\nfps_module = load_inline(\n name='fps_module',\n cpp_sources=['torch::Tensor fps_cuda(torch::Tensor points, int M);'],\n cuda_sources=[cuda_source],\n functions=['fps_cuda'],\n verbose=True,\n extra_cuda_cflags=['-O3']\n)\n\nclass ModelNew(nn.Module):\n def __init__(self, num_samples: int):\n super().__init__()\n self.num_samples = num_samples\n\n def forward(self, points: torch.Tensor) -> torch.Tensor:\n points = points.contiguous()\n return fps_module.fps_cuda(points, self.num_samples)" }
result ← submit_kernel (fail)
failsubmit_kernel submit_kernel FAILED: kernel did not compile. Error building extension 'fps_module'
final submitted kernel (137 lines, 4,405 chars) — full source, untruncated
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline

cuda_source = r"""
#include <torch/extension.h>
#include <cuda.h>
#include <cuda_runtime.h>
#include <limits>

template <typename scalar_t>
__global__ void fps_kernel(
    const scalar_t* __restrict__ points,
    int64_t* __restrict__ indices,
    int N, int M) {
    
    extern __shared__ char smem[];
    scalar_t* min_dist_sq = reinterpret_cast<scalar_t*>(smem);
    scalar_t* shared_dist = reinterpret_cast<scalar_t*>(smem + N * sizeof(scalar_t));
    int* shared_idx = reinterpret_cast<int*>(smem + N * sizeof(scalar_t) + blockDim.x * sizeof(scalar_t));
    
    const int tid = threadIdx.x;
    const int block_size = blockDim.x;
    
    const scalar_t* batch_points = points + blockIdx.x * N * 3;
    int64_t* batch_indices = indices + blockIdx.x * M;
    
    // Initialize min_dist_sq to infinity
    for (int n = tid; n < N; n += block_size) {
        min_dist_sq[n] = std::numeric_limits<scalar_t>::infinity();
    }
    __syncthreads();
    
    int farthest = 0;
    
    for (int m = 0; m < M; m++) {
        // Record current farthest
        if (tid == 0) {
            batch_indices[m] = farthest;
        }
        
        // Get center coordinates (broadcast to all threads)
        scalar_t cx = batch_points[farthest * 3 + 0];
        scalar_t cy = batch_points[farthest * 3 + 1];
        scalar_t cz = batch_points[farthest * 3 + 2];
        
        scalar_t local_max_dist = scalar_t(-1.0);
        int local_max_idx = 0;
        
        for (int n = tid; n < N; n += block_size) {
            scalar_t dx = batch_points[n * 3 + 0] - cx;
            scalar_t dy = batch_points[n * 3 + 1] - cy;
            scalar_t dz = batch_points[n * 3 + 2] - cz;
            scalar_t dist_sq = dx * dx + dy * dy + dz * dz;
            
            // Update min_dist_sq
            if (dist_sq < min_dist_sq[n]) {
                min_dist_sq[n] = dist_sq;
            }
            
            // Mark the current farthest as -1
            if (n == farthest) {
                min_dist_sq[n] = scalar_t(-1.0);
            }
            
            // Track local max for next farthest
            if (min_dist_sq[n] > local_max_dist) {
                local_max_dist = min_dist_sq[n];
                local_max_idx = n;
            }
        }
        
        shared_dist[tid] = local_max_dist;
        shared_idx[tid] = local_max_idx;
        __syncthreads();
        
        // Block reduction to find global max
        for (int s = block_size / 2; s > 0; s >>= 1) {
            if (tid < s) {
                if (shared_dist[tid + s] > shared_dist[tid]) {
                    shared_dist[tid] = shared_dist[tid + s];
                    shared_idx[tid] = shared_idx[tid + s];
                }
            }
            __syncthreads();
        }
        
        // All threads read the next farthest
        farthest = shared_idx[0];
    }
}

torch::Tensor fps_cuda(torch::Tensor points, int M) {
    TORCH_CHECK(points.dim() == 3, "points must be 3D");
    TORCH_CHECK(points.size(2) == 3, "points must have 3 channels");
    
    int B = points.size(0);
    int N = points.size(1);
    
    TORCH_CHECK(M <= N, "M must be <= N");
    
    auto indices = torch::empty({B, M}, torch::TensorOptions()
        .dtype(torch::kLong).device(points.device()));
    
    const int block_size = 256;
    size_t scalar_size = points.element_size();
    size_t shared_mem_size = N * scalar_size + block_size * scalar_size + block_size * sizeof(int);
    
    AT_DISPATCH_FLOATING_TYPES(points.scalar_type(), "fps_kernel", [&] {
        fps_kernel<scalar_t><<<B, block_size, shared_mem_size>>>(
            points.data_ptr<scalar_t>(),
            indices.data_ptr<int64_t>(),
            N, M);
    });
    
    C10_CUDA_CHECK(cudaGetLastError());
    return indices;
}
"""

fps_module = load_inline(
    name='fps_module',
    cpp_sources=['torch::Tensor fps_cuda(torch::Tensor points, int M);'],
    cuda_sources=[cuda_source],
    functions=['fps_cuda'],
    verbose=True,
    extra_cuda_cflags=['-O3']
)

class ModelNew(nn.Module):
    def __init__(self, num_samples: int):
        super().__init__()
        self.num_samples = num_samples

    def forward(self, points: torch.Tensor) -> torch.Tensor:
        points = points.contiguous()
        return fps_module.fps_cuda(points, self.num_samples)