Kimi-K2.6 · popcorn · l12_default_gpt
correct 4_GJKRobotEnvironmentIntersection.py
model
Kimi-K2.6
turns / max
5 / 10
tool calls
5
tokens (total)
0
wall clock
—
truncated
no
backend
cuda fp32
variant
popcorn
runtime
0.01 μs
ref runtime
4.33 μs
speedup
679.75x
started
2026-05-02T02:32:22Z
timingkernelbench.timing
kernel runtime
0.01 μs
ref runtime
4.33 μs
speedup
679.75x
kernel mean / median / std
0.01 / — / 1.76e-04 μs
ref mean / median / std
4.33 / — / 1.38 μs
numerical precisioneval_kernel_against_ref
max abs error
0.00
mean abs error
0.00
max rel error
0.00
mean rel error
0.00
gpu memoryextended_metrics.measure_memory
kernel peak
0.00 MB
ref peak
0.01 MB
kernel / ref
0.24x
kernel launches / fusiontorch.profiler
kernel launches
1
ref launches
295
fusion ratio (ref / kernel)
295.00x
total cuda time
3 μs
SOL (speed-of-light)nsight_hardware_counters
SOL score (0–1)
7.000e-04
dram util %
0.1%
compute util %
0.0%
bottleneck
unknown
dominant pipe
None
arithmetic intensity
—
ridge point
20.00
roofline / occupancynsight_hardware_counters
dram bandwidth
2.42 GB/s
dram util %
0.1%
fp32 tflops
—
fp32 util %
—
fp16 tflops
—
occupancy %
6.4%
bottleneck
unknown
dominant pipe %
—
arithmetic intensity
—
ridge point
20.00
L1 hit %
—
L2 hit %
98.9%
ld sectors / req
8.80
st sectors / req
—
regs / thread
28
smem / block
1024 B
block size
256
peak bw
3350.00 GB/s
peak fp32 tflops
67.00
peak fp16 tflops
1979.00
warp stalls (top 5)nsight
long_scoreboard
208.1%
short_scoreboard
114.1%
flagssubmit_kernel guard
excessive speedup
flagged
reference PyTorch source (the prompt the agent sees)
"""
Robot–environment convex collision in **xy** (ground plane).

For an **axis-aligned robot footprint** vs. a **single convex obstacle triangle**, a
**separating-axis test (SAT)** gives the same **intersection predicate** as **GJK** in
2D, with a compact fully batched reference. A fused device kernel often implements
**GJK (+ EPA for penetration depth)** for arbitrary 3D polyhedra; this problem is
the canonical 2D footprint special case.
"""

import torch
import torch.nn as nn

# Default ModelNew is PyTorch-only. run_and_check: use check_kernel=False with backend=cuda


def _rect_corners_xy(robot_min: torch.Tensor, robot_max: torch.Tensor) -> torch.Tensor:
    """(B, 4, 2) corners of axis-aligned rectangles."""
    x0, x1 = robot_min[:, 0], robot_max[:, 0]
    y0, y1 = robot_min[:, 1], robot_max[:, 1]
    return torch.stack(
        (
            torch.stack((x0, y0), dim=-1),
            torch.stack((x1, y0), dim=-1),
            torch.stack((x1, y1), dim=-1),
            torch.stack((x0, y1), dim=-1),
        ),
        dim=1,
    )


def _project_min_max(points: torch.Tensor, axis: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
    """points (B, K, 2), axis (B, 2) or (2,) broadcast → (B,) min and max projections."""
    if axis.dim() == 1:
        axis = axis.view(1, 2).expand(points.shape[0], -1)
    proj = (points * axis.unsqueeze(1)).sum(dim=-1)
    return proj.min(dim=-1).values, proj.max(dim=-1).values


def _batched_sat_rect_triangle(
    robot_min: torch.Tensor,
    robot_max: torch.Tensor,
    env_triangle: torch.Tensor,
    eps: float,
) -> torch.Tensor:
    """
    Separating-axis test: AABB vs triangle in 2D, fully vectorized over batch.
    Returns (B,) float 1.0 if overlapping else 0.0.
    """
    B = robot_min.shape[0]
    corners = _rect_corners_xy(robot_min, robot_max)
    hit = torch.ones(B, device=robot_min.device, dtype=robot_min.dtype)

    # World axes (rectangle is AABB)
    for ax in (
        torch.tensor([1.0, 0.0], device=robot_min.device, dtype=robot_min.dtype),
        torch.tensor([0.0, 1.0], device=robot_min.device, dtype=robot_min.dtype),
    ):
        r0, r1 = _project_min_max(corners, ax)
        t0, t1 = _project_min_max(env_triangle, ax)
        separated = (r1 < t0 - eps) | (t1 < r0 - eps)
        hit = hit * (~separated).to(hit.dtype)

    # Triangle edge normals (per batch)
    for i in range(3):
        e = env_triangle[:, (i + 1) % 3] - env_triangle[:, i]
        n = torch.stack((-e[:, 1], e[:, 0]), dim=-1)
        n = n / (n.norm(dim=-1, keepdim=True) + 1e-8)
        r0, r1 = _project_min_max(corners, n)
        t0, t1 = _project_min_max(env_triangle, n)
        separated = (r1 < t0 - eps) | (t1 < r0 - eps)
        hit = hit * (~separated).to(hit.dtype)

    return hit


class Model(nn.Module):
    """
    **Batched robot footprint vs. environment triangle** intersection in **xy**.

    Robot: axis-aligned box ``[robot_min, robot_max]`` per batch element.
    Environment: one **triangle** ``(B, 3, 2)``.

    Reference: **vectorized SAT** (5 axis tests). Fused-kernel target: same
    predicate via **GJK/EPA** or specialized SAT SIMD for planning loops.
    """

    def __init__(self, eps: float = 1e-6):
        super().__init__()
        self.eps = eps

    def forward(
        self,
        robot_min: torch.Tensor,
        robot_max: torch.Tensor,
        env_triangle: torch.Tensor,
    ) -> torch.Tensor:
        """
        Args:
            robot_min: (B, 2), robot_max: (B, 2) with max >= min componentwise.
            env_triangle: (B, 3, 2) obstacle vertices in order (any consistent winding).
        Returns:
            (B, 1) float in ``{0.0, 1.0}`` — **1** if the sets intersect.
        """
        B = robot_min.shape[0]
        assert robot_max.shape == (B, 2) and env_triangle.shape == (B, 3, 2)
        h = _batched_sat_rect_triangle(robot_min, robot_max, env_triangle, self.eps)
        return h.unsqueeze(-1)


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

    def __init__(self, eps: float = 1e-6):
        super().__init__()
        self._impl = Model(eps=eps)

    def forward(
        self,
        robot_min: torch.Tensor,
        robot_max: torch.Tensor,
        env_triangle: torch.Tensor,
    ) -> torch.Tensor:
        return self._impl(robot_min, robot_max, env_triangle)


batch_size = 16


def get_inputs():
    bs = popcorn_pri.jitter_int(batch_size)
    robot_min = torch.empty(bs, 2).uniform_(-1.0, 0.5)
    robot_max = robot_min + torch.empty(bs, 2).uniform_(0.2, 0.8)
    env_triangle = torch.empty(bs, 3, 2).uniform_(-0.8, 0.8)
    return [robot_min, robot_max, env_triangle]


def get_init_inputs():
    return []
turn 0 · 1 tool call(s) · 67.1s turn · 67.1s LLM · 18,038 chars (click to expand)
turn 067.1s turn · 67.1s LLM · 0.0s tools
reasoning
We need to write a custom CUDA kernel that performs a batched SAT (Separating Axis Theorem) collision detection between axis-aligned rectangles (AABB) and triangles in 2D. The kernel should be faster than the fully vectorized PyTorch reference. Key aspects: - Inputs: robot_min (B,2), robot_max (B,2), env_triangle (B,3,2) - Output: (B,1) float in {0.0, 1.0} - Need to define a ModelNew class that loads a CUDA kernel via torch.utils.cpp_extension.load_inline. - Must pass correctness checks (atol=rtol=1e-4) and run faster than reference. Let's plan the kernel. For each batch element, the SAT tests 5 axes: 1. x-axis (1,0) 2. y-axis (0,1) 3. Normal to edge 0 of triangle (from v0 to v1) 4. Normal to edge 1 of triangle (from v1 to v2) 5. Normal to edge 2 of triangle (from v2 to v0) For each axis, project the 4 rectangle corners and 3 triangle vertices onto the axis, compute min and max, and check for overlap. Projection of a point (x,y) onto axis (ax,ay) is dot = x*ax + y*ay. AABB projection onto world axes is trivial (axis-aligned): - x-axis: rect_min = robot_min.x, rect_max = robot_max.x - y-axis: rect_min = robot_min.y, rect_max = robot_max.y For triangle edge normals: - Edge e_i = v_{i+1} - v_i - Normal n_i = (-e_i.y, e_i.x) - We can avoid normalizing n_i because SAT only cares about the sign/direction, and projection min/max scale linearly with axis length. However, comparing projections of two sets onto a non-unit vector works the same: if the intervals don't overlap on the unnormalized axis, they don't overlap on the normalized one. If they overlap on unnormalized, they overlap on normalized. So we can skip normalization to save sqrt/div. But need to be careful with eps scaling? The reference normalizes n and uses eps as an absolute threshold. Reference: ``` n = torch.stack((-e[:, 1], e[:, 0]), dim=-1) n = n / (n.norm(dim=-1, keepdim=True) + 1e-8) r0, r1 = _project_min_max(corners, n) t0, t1 = _project_min_max(env_triangle, n) separated = (r1 < t0 - eps) | (t1 < r0 - eps) ``` If we use unnormalized n, the threshold `eps` should be scaled by `||n||` to be equivalent? Let's see. Projection onto n: p' = p . n. Projection onto n_unit = n / |n|: p_unit = p' / |n|. Separation condition on n_unit: max_rect_unit < min_tri_unit - eps, or max_tri_unit < min_rect_unit - eps. Multiplying by |n|: max_rect < min_tri - eps*|n|, or max_tri < min_rect - eps*|n|. So if we use unnormalized n, we must use eps * |n| for the same condition. But wait, what if |n| is very small? The edges could be degenerate. In the reference, they add 1e-8 to the norm before dividing, so they effectively handle it. If we don't normalize, the eps threshold becomes very small for tiny edges. However, `eps = 1e-6` is already tiny. If the triangle is degenerate (zero area), the SAT should still work? For a degenerate triangle, the edge normals for zero-length edges are zero vectors. Projection onto zero vector gives zero for all points, so min=max=0, and the test would say they overlap on that axis, which is fine because a degenerate triangle might still intersect. Actually, if all edges are zero, it's a point. Overlap on zero axis is trivial, and other axes will determine intersection. So skipping normalization and using zero-length normals is okay as long as we don't divide by zero. But correctness: If we don't normalize, the separation condition is (r1 < t0 - eps) | (t1 < r0 - eps) with *unnormalized* projections. That's **not** equivalent to the reference unless we scale eps by |n|. Because the reference computes projections on *unit* normals. So to match exactly (within fp32 tolerance), we should either normalize or adjust eps. However, we don't need to match the reference operation-by-operation, just the final output within tolerance 1e-4. But the geometry is exact, and scaling eps by |n| is equivalent. Since |n| is the edge length. Actually, edge length can be up to ~1.6 (from -0.8 to 0.8). So scaling eps by length (say 1.6) yields eps' ~ 1.6e-6. The separation margin is tiny either way. Could we just skip normalization and use the same eps? Would that change results? Suppose we have max_rect=0, min_tri=1e-7, eps=1e-6. Normalized case: max_rect < min_tri - eps? 0 < 1e-7 - 1e-6 = -9e-7 → False. Unnormalized case with |n|=2: max_rect' = 0*2=0? Wait, projections scale: if n = 2 * n_unit, then p' = p . n = 2 * (p . n_unit). So max_rect' = 2 * max_rect_unit = 0. min_tri' = 2 * min_tri_unit = 2e-7. Condition: 0 < 2e-7 - 1e-6 = -8e-7 → False. Same result. In general, scaling the axis by a positive constant `s` scales all projections by `s`. Condition `s*max_rect < s*min_tri - eps` is equivalent to `max_rect < min_tri - eps/s`. **This is different from the reference condition** `max_rect < min_tri - eps` unless `s=1`. So if eps is small relative to the gap, it doesn't matter. But if the gap is on the order of eps, it could flip the result. Example: max_rect_unit = 0, min_tri_unit = 5e-7, eps = 1e-6, s = 0.1. Reference: 0 < 5e-7 - 1e-6 = -5e-7 → False (not separated). Unnormalized with same eps: 0 < 5e-8 - 1e-6 = -9.5e-7 → False. Same. Wait, max_rect' = s*max_rect_unit = 0. min_tri' = s*min_tri_unit = 5e-8. Condition: 0 < 5e-8 - 1e-6? False. It matches because both sides of inequality involve projections that are scaled, but the eps is *not* scaled. So the effective eps in unit space is `eps/s`. If s=10, effective eps = 1e-7. Then if max_rect_unit=0, min_tri_unit=5e-7. Reference: 0 < 5e-7 - 1e-6 → False. Unnormalized: 0 < 5e-6 - 1e-6 = 4e-6 → True! Result changes. So yes, normalizing matters for correctness if we keep eps constant. The reference normalizes. We should match it exactly or adjust eps. Adjusting eps is easy: compute `norm = sqrt(nx*nx + ny*ny)`, then use `eps * norm` as the threshold for unnormalized projections. If norm is zero, the axis is degenerate; we can skip it or set threshold to 0. Wait, but does that match exactly with fp32? Reference: - n = (-e.y, e.x) - n_norm = sqrt(nx^2 + ny^2) + 1e-8 - n_unit = n / n_norm - proj_rect = corners . n_unit - proj_tri = env_triangle . n_unit - separated = (max_rect < min_tri - eps) OR (max_tri < min_rect - eps) Our approach with unnormalized axis and scaled eps: - n = (-e.y, e.x) - n_norm = sqrt(nx^2 + ny^2) (maybe + 1e-8 for safety) - proj_rect_un = corners . n - proj_tri_un = env_triangle . n - scaled_eps = eps * n_norm - separated = (max_rect_un < min_tri_un - scaled_eps) OR ... Mathematically, `corners . n = (corners . n_unit) * n_norm`. So: max_rect_un < min_tri_un - scaled_eps <=> n_norm * max_rect_unit < n_norm * min_tri_unit - eps * n_norm <=> max_rect_unit < min_tri_unit - eps (if n_norm > 0) This is exactly equivalent. We just need to handle n_norm = 0. If n_norm is zero, we can just skip the axis or treat it as no separation (multiply hit by 1). So we can avoid the division per batch element and just compute `norm` to scale eps. This saves 3 divisions (and 3 sqrt + reciprocal) per batch. Actually, the reference does `n.norm(dim=-1)` which is sqrt. We also need sqrt for norm. But we avoid the division of each corner/triangle vertex projection by n_norm? Wait, the reference computes projections by multiplying corners (B,4,2) with n_unit (B,2), which is a batched matrix multiply. In the kernel, we'd compute dot products per point with n and compare. Avoiding division inside dot product means fewer FLOPs but same sqrt. However, we can also avoid sqrt by noticing that scaling eps by `norm` requires `norm`. We could compare squared projections? No, because the min/max are linear in norm. We could compare `max_rect_un * sign`? No, we need the actual `norm` to scale eps. Alternatively, we can normalize the axis in the kernel: `nx /= norm; ny /= norm;`. That's just 2 divisions per batch element per edge. Then we use original eps. The cost of 2 divs is tiny. And it exactly matches the reference's `n / (norm + 1e-8)`. The reference does this on CPU (PyTorch) but it's fused in the kernel. I think normalizing in the kernel is fine and simpler. Wait, the reference computes n, normalizes it, then does `_project_min_max(corners, n)` which computes dot products with the normalized n. `_project_min_max(env_triangle, n)` does the same. If we normalize in the kernel: for each edge compute nx, ny, norm = sqrt(nx^2+ny^2) + 1e-8f; nx/=norm; ny/=norm. Then compute dots. This is simple. Now, for x and y axes, the reference uses `torch.tensor([1.0, 0.0])` and `[0.0, 1.0]`. In the kernel, we can handle them specially to avoid computing dots for all 4 corners: since axis is (1,0), projection is just x. So rect_min = robot_min.x, rect_max = robot_max.x. Similarly for y-axis. This reduces FLOPs. For triangle edge normals, we have to compute the dot products of the 4 rectangle corners and 3 triangle vertices. 7 dot products per axis. With 3 edges, that's 21 dot products. Plus the 2 world axes (special cased). Total is cheap. Because B might be small (16?), launching a kernel with one thread per batch element is fine. But maybe batch size is larger during timing? The benchmark usually uses the provided batch_size (16) or maybe varies it. We should handle arbitrary B efficiently. Using one thread per batch element is perfectly fine for small B. For larger B, we could use more threads per batch to parallelize the reductions, but the per-batch work is tiny (a few dozen FLOPs), so one thread per element is likely optimal to avoid inter-thread synchronization overhead. Actually, with B up to maybe 10k or 100k? The task says `batch_size = 16`. But timing might use a different size? Let's check typical KernelBench... often they use the stated batch size or maybe scale it. The instructions say "replace the forward computation... run faster than the PyTorch reference." The timing is done by the evaluator with the given batch size or maybe larger. Actually, they might run with multiple sizes. But usually the given `batch_size` is what they use for correctness and timing. Let's assume B is at least 16, maybe up to 10^4. One thread per batch element is efficient for small to medium B. For very large B, it's also fine because it's embarrassingly parallel. The main cost in PyTorch reference is likely memory bandwidth: it creates intermediate tensors (corners of size (B,4,2), edges, normals, projections, etc.). A fused kernel avoids all those intermediate allocations and memory traffic. Even with one thread per batch, we should win big by fusing. So let's write a CUDA kernel where each thread handles one batch element. Algorithm per thread i: ``` float x0 = robot_min[i*2+0], y0 = robot_min[i*2+1]; float x1 = robot_max[i*2+0], y1 = robot_max[i*2+1]; // Triangle vertices float tx0 = env_tri[i*6+0], ty0 = env_tri[i*6+1]; float tx1 = env_tri[i*6+2], ty1 = env_tri[i*6+3]; float tx2 = env_tri[i*6+4], ty2 = env_tri[i*6+5]; float hit = 1.0f; // Axis 1: (1, 0) float r0 = x0, r1 = x1; float t_min = fminf(fminf(tx0, tx1), tx2); float t_max = fmaxf(fmaxf(tx0, tx1), tx2); if (r1 < t_min - eps || t_max < r0 - eps) hit = 0.0f; // Axis 2: (0, 1) if (hit != 0.0f) { r0 = y0; r1 = y1; t_min = fminf(fminf(ty0, ty1), ty2); t_max = fmaxf(fmaxf(ty0, ty1), ty2); if (r1 < t_min - eps || t_max < r0 - eps) hit = 0.0f; } // Edge 0: v1 - v0 float ex = tx1 - tx0; float ey = ty1 - ty0; float nx = -ey; float ny = ex; float norm = sqrtf(nx*nx + ny*ny) + 1e-8f; nx /= norm; ny /= norm; // Compute rect projection min/max onto (nx, ny) // Since rectangle may not be aligned with this axis, we need all 4 corners. // But we can compute min and max of dot product efficiently. // corners: (x0,y0), (x1,y0), (x1,y1), (x0,y1) // dot = x*nx + y*ny // Since x is either x0 or x1, y is either y0 or y1. // min_dot = (nx>0 ? x0 : x1)*nx + (ny>0 ? y0 : y1)*ny ??? Wait. // Actually, min of a*nx + b*ny where a in {x0,x1}, b in {y0,y1}. // Since x0 <= x1 and y0 <= y1: // If nx >= 0: choosing x0 gives smaller contribution from x. // If nx < 0: choosing x1 gives smaller contribution (since negative * larger x). // Same for ny. // So min_dot = (nx >= 0 ? x0 : x1) * nx + (ny >= 0 ? y0 : y1) * ny // max_dot = (nx >= 0 ? x1 : x0) * nx + (ny >= 0 ? y1 : y0) * ny // This avoids evaluating all 4 corners. // This is a standard technique for AABB projection onto an axis. // Let's verify: // dot = nx*x + ny*y. // To minimize, pick x in {x0, x1} that minimizes nx*x. If nx >= 0, x0. If nx < 0, x1. // Similarly for y. Since x and y are independent, min is separable. // This is correct. // For triangle: we have 3 vertices. Just compute 3 dot products and fminf/fmaxf. if (hit != 0.0f) { float rect_min = ((nx >= 0.0f) ? x0 : x1) * nx + ((ny >= 0.0f) ? y0 : y1) * ny; float rect_max = ((nx >= 0.0f) ? x1 : x0) * nx + ((ny >= 0.0f) ? y1 : y0) * ny; float p0 = tx0*nx + ty0*ny; float p1 = tx1*nx + ty1*ny; float p2 = tx2*nx + ty2*ny; float tri_min = fminf(fminf(p0, p1), p2); float tri_max = fmaxf(fmaxf(p0, p1), p2); if (rect_max < tri_min - eps || tri_max < rect_min - eps) hit = 0.0f; } // Repeat for edge 1 and 2. ... output[i] = hit; ``` Wait, we need to be careful with the condition `nx >= 0.0f`. In floating point, exactly zero doesn't matter: both branches give same contribution. So we can just use `nx >= 0.0f`. This approach avoids creating the corners tensor entirely and reduces arithmetic. It's exact compared to computing all 4 corners and taking min/max, but because the min/max over 4 independent points is separable as described, it's mathematically identical. The reference computes the 4 corners explicitly and then does min/max. Our method computes the same min/max without materializing corners. This is fine for correctness since it's mathematically equivalent. However, we need to be careful: the reference computes `_rect_corners_xy` which returns the 4 corners in order: (x0,y0), (x1,y0), (x1,y1), (x0,y1). Then `_project_min_max` computes dot products for all 4 and takes min/max. Since x and y contributions are independent, our separable min/max is identical. The only difference might be floating point rounding if we use `fma`? But our computation: `(nx >= 0 ? x0 : x1) * nx + ...`. This is exactly one of the 4 corner dot products. So it's identical to computing the min among the 4 dot products. So no issue. For the triangle edges, the reference computes `n = (-e[:, 1], e[:, 0])`, normalizes with `n_norm + 1e-8`, and then projects. We match this. One subtlety: The reference does `n = n / (n.norm(dim=-1, keepdim=True) + 1e-8)`. If norm is zero, n becomes 0/1e-8 = 0. Then projections are all zero. Then `separated = (r1 < t0 - eps) | (t1 < r0 - eps)`. Since r0=r1=0 and t0=t1=0, separated is false. So hit remains unchanged. In our kernel, if norm is zero, after adding `1e-8f`, norm becomes `1e-8f`. `nx` and `ny` are zero (since ex=ey=0), so division yields 0. Same behavior. Good. Potential issue: The reference processes all 5 axes even if already separated (since hit gets multiplied by zero). In our kernel, we early out if `hit == 0`. This is fine because the final output is the same. But we must ensure that the final output shape is (B, 1). The reference returns `h.unsqueeze(-1)`, so (B, 1). Our kernel should output a tensor of shape (B,) or (B,1). We can launch a kernel that writes to a (B,) tensor and then unsqueeze in Python, or write directly to a (B,1) tensor. Simpler: write to (B,) and `return out.unsqueeze(-1)`. Now, let's think about performance. The PyTorch implementation likely becomes memory-bound because it creates many temporaries. The fused kernel is compute-light but avoids all memory traffic except reading inputs and writing output. With B=16, the reference is very fast but kernel overhead might dominate. However, the evaluator might run with `torch.cuda.synchronize()` and average over many iterations. For such a small B, avoiding Python/PyTorch overhead by fusing into a single kernel call is still a big win, because the reference calls several PyTorch ops (stack, subtraction, norm, etc.), each with its own kernel launch and memory allocation. Our kernel launches once. Actually, `torch.utils.cpp_extension.load_inline` has compilation overhead on load, but runtime is just one kernel call. This should be much faster. Let's check GPU specs to see if we should use vectorized loads or special occupancy tuning. But likely not necessary for this small kernel. Let's call `get_gpu_specs`.
call → get_gpu_specs
callget_gpu_specs {}
result ← get_gpu_specs (ok)
okget_gpu_specs GPU specs for NVIDIA H100 80GB HBM3: total memory (runtime): 79.1 GB spec entry: H100 GPU Architecture: Hopper GPU Memory: 80GB Memory Bandwidth: 3.35 TB/s FP64 TFLOPS: 34 FP64 Tensor Core TFLOPS: 67 FP32 TFLOPS: 67 TF32 Tensor Core TFLOPS: 989 with sparsity BFLOAT16 Tensore Core TFLOPS: 1979 with sparsity FP16 Tensor Core TFLOPS: 1979 with sparsity FP8 Tensor Core TFLOPS: 3958 with sparsity INT8 Tensor Core TOPS: 3958 with sparsity Register File Size: 64K 32-bit registers per SM Maximum number of registers per thread: 255 Maximum number of thread blocks per SM: 32 Shared memory capacity per SM: 228 KB Maximum shared memory per thread block: 227 KB
turn 1 · 1 tool call(s) · 92.0s turn · 92.0s LLM · 47,617 chars (click to expand)
turn 192.0s turn · 92.0s LLM · 0.0s tools
reasoning
We need to write a custom CUDA kernel that replaces the forward computation of the given Model. Model computes batched SAT (separating axis theorem) for axis-aligned rectangles vs triangles in 2D. We need to implement ModelNew with a custom CUDA kernel, producing numerically equivalent outputs at fp32 precision, and runs faster than reference. Key aspects: - The reference is fully vectorized with some Python-level loops over 2 world axes and 3 triangle edge normals. For batch size 16, and presumably larger batch sizes in evaluation, a fused CUDA kernel should be able to get much lower latency by avoiding intermediate allocations and kernel launch overhead. - We need to use `torch.utils.cpp_extension.load_inline` to compile and bind CUDA source at module load time. - We need to be careful not to include our own `PYBIND11_MODULE` block; we list the host-side wrapper function names in `functions=[...]`. - The kernel should be a device kernel that for each batch element computes the SAT test. We can write a simple CUDA kernel where each thread handles one batch element, computing: For each of 5 axes: axis = ... project rectangle corners (4 points) onto axis: compute dot product, keep min/max. project triangle vertices (3 points) onto axis: compute dot product, keep min/max. if (rect_max < tri_min - eps) or (tri_max < rect_min - eps): no collision => return 0. If all axes pass, return 1. The world axes: x-axis (1,0) and y-axis (0,1). For these, rectangle projections are just x and y coordinates. Specifically, rect_min_proj = robot_min[:, axis_index], rect_max_proj = robot_max[:, axis_index]. Triangle projections: min and max of env_triangle[:, :, axis_index]. This is a simplification that we should exploit. For triangle edge normals: For each edge i from vertex i to (i+1)%3: e = v_next - v_i n = (-e.y, e.x) (perpendicular) length = sqrt(n.x^2 + n.y^2) if length < 1e-8: skip? But in reference, n = n / (norm + 1e-8), so axis is effectively zero if edge is degenerate. If axis is zero, all projections are zero, so the separation test will never separate. We can handle it similarly, or just skip if length is near zero. But to match numerically, we should replicate reference behavior. For normalized axis: nx /= length; ny /= length; Then compute dot products for 4 rectangle corners and 3 triangle vertices with this axis. Note: rectangle corners are axis-aligned, so dot product with arbitrary axis = nx * x + ny * y. We can precompute the 4 corners per batch element: (x0, y0), (x1, y0), (x1, y1), (x0, y1). However, we can compute min and max projection for the rectangle on a general axis as: proj = nx * x + ny * y For rectangle [x0,x1]x[y0,y1], the min and max of nx*x + ny*y over corners are: min = nx * (nx > 0 ? x0 : x1) + ny * (ny > 0 ? y0 : y1)? No, because it's linear, min over the box occurs at a corner determined by sign of nx and ny: If nx >= 0: x contribution is min at x0, max at x1. If nx < 0: x contribution is min at x1, max at x0. Similarly for ny. So: rect_min_proj = nx * (nx >= 0 ? x0 : x1) + ny * (ny >= 0 ? y0 : y1) rect_max_proj = nx * (nx >= 0 ? x1 : x0) + ny * (ny >= 0 ? y1 : y0) This avoids iterating corners. For the world axes (1,0) and (0,1): axis = (1,0): rect_min_proj = x0, rect_max_proj = x1. tri_min_proj = min(v0.x, v1.x, v2.x), tri_max_proj = max(...). axis = (0,1): rect_min_proj = y0, rect_max_proj = y1. tri_min_proj = min(v0.y, v1.y, v2.y), tri_max_proj = max(...). This is even simpler. For general axis (nx, ny): rect_min_proj = nx * (nx >= 0 ? x0 : x1) + ny * (ny >= 0 ? y0 : y1) rect_max_proj = nx * (nx >= 0 ? x1 : x0) + ny * (ny >= 0 ? y1 : y0) tri_min_proj = min(nx*v0.x + ny*v0.y, nx*v1.x + ny*v1.y, nx*v2.x + ny*v2.y) tri_max_proj = max(...). Wait, is this exactly equivalent to projecting the 4 corners? Yes, because the rectangle is axis-aligned, and the dot product is a linear function. The extrema over a convex polytope occur at vertices. So min over 4 corners is exactly as above. This reduces computation significantly. Let's verify with the reference. Reference does: r0, r1 = _project_min_max(corners, ax) where corners is (B,4,2) and ax is (B,2). _project_min_max does (points * axis.unsqueeze(1)).sum(dim=-1), then min/max over dim=1. So for each batch element, it computes 4 dot products and takes min/max. Our analytical min/max is mathematically identical, and should be numerically identical (same formulas, just computed directly). But we need to be careful with the sign check: `nx >= 0` vs `nx > 0` vs `nx < 0`. In the reference, for nx=0, x contribution is 0 regardless of x, so nx >= 0 or nx > 0 doesn't matter for the min/max of the x part alone, but combined with ny, we need to be consistent. Let's test: nx=0, ny=1. Then rect_min_proj should be y0. Formula: nx*(nx>=0?x0:x1) + ny*(ny>=0?y0:y1) = 0*x0 + 1*y0 = y0. Good. nx=0, ny=-1: rect_min_proj = 0*x0 + (-1)*( -1>=0? y0:y1) = -1*y1. But the actual min projection should be -y1 (since max y is y1, dot with -1 gives -y1). Yes, correct. So this is fine. However, we need to worry about numerical equivalence: the reference computes projections by summing products for all 4 corners, then min/max. Our direct formula is much faster and mathematically equivalent, but is it numerically equivalent within tolerance? It should be, because it's just a different way of computing the same expression. But let's consider edge cases with very large coordinates or infinities? The test data is uniform(-1,1) roughly, so fine. Actually, for the triangle edge normals, the reference normalizes the axis: `n = n / (n.norm(dim=-1, keepdim=True) + 1e-8)`. Then projects. Does normalizing affect the separation test? If axis is scaled by a positive factor, the min/max projections scale by the same factor, and the conditions `r1 < t0 - eps` and `t1 < r0 - eps` become `c*r1 < c*t0 - eps` which is NOT equivalent to `r1 < t0 - eps/c`. Wait, the eps is not scaled! However, the reference uses the same eps for all axes, including triangle edge normals. Since they normalize the axis to unit length, the projection values are in the same units as coordinates (e.g., meters). If we didn't normalize, the projection would be scaled by the edge length, and the eps threshold `1e-6` would be effectively `1e-6 / edge_length` in the unscaled space, which is different. So to match the reference numerically, we MUST also normalize the triangle edge normals before projecting. Good catch. So for the triangle edge normals: e = v_next - v_i n = (-e.y, e.x) len = sqrt(n.x^2 + n.y^2) if len > 0: nx = n.x / len; ny = n.y / len; else nx = n.x / 1e-8? Wait, reference does `n = n / (n.norm(dim=-1, keepdim=True) + 1e-8)`. So if len is 0, it divides by 1e-8. So nx = n.x * (1.0 / (len + 1e-8)). Actually, since n.x and n.y are 0 when len is 0, dividing by 1e-8 gives 0. So axis is (0,0). In that case, all projections are 0, and separation condition becomes `0 < 0 - eps` which is false. So we can implement: `inv_len = 1.0f / (len + 1e-8f); nx = n.x * inv_len; ny = n.y * inv_len;`. This matches. Wait, but if len is 0, n.x and n.y are 0, so nx and ny become 0. This is numerically exact. Let's think about the whole kernel: ```cuda __global__ void sat_rect_triangle_kernel( const float* robot_min, const float* robot_max, const float* env_triangle, float* out, int B, float eps) { int idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx >= B) return; float x0 = robot_min[idx * 2 + 0]; float y0 = robot_min[idx * 2 + 1]; float x1 = robot_max[idx * 2 + 0]; float y1 = robot_max[idx * 2 + 1]; // Triangle vertices float v0x = env_triangle[idx * 6 + 0]; float v0y = env_triangle[idx * 6 + 1]; float v1x = env_triangle[idx * 6 + 2]; float v1y = env_triangle[idx * 6 + 3]; float v2x = env_triangle[idx * 6 + 4]; float v2y = env_triangle[idx * 6 + 5]; int hit = 1; // Axis 1: (1, 0) { float r0 = x0; float r1 = x1; float t0 = fminf(fminf(v0x, v1x), v2x); float t1 = fmaxf(fmaxf(v0x, v1x), v2x); if ((r1 < t0 - eps) || (t1 < r0 - eps)) hit = 0; } // Axis 2: (0, 1) if (hit) { float r0 = y0; float r1 = y1; float t0 = fminf(fminf(v0y, v1y), v2y); float t1 = fmaxf(fmaxf(v0y, v1y), v2y); if ((r1 < t0 - eps) || (t1 < r0 - eps)) hit = 0; } // Triangle edges // Edge 0: v0 -> v1 if (hit) { float ex = v1x - v0x; float ey = v1y - v0y; float nx = -ey; float ny = ex; float len = sqrtf(nx*nx + ny*ny); float inv_len = 1.0f / (len + 1e-8f); nx *= inv_len; ny *= inv_len; // rect min/max float r0 = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1); float r1 = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0); float p0 = nx * v0x + ny * v0y; float p1 = nx * v1x + ny * v1y; float p2 = nx * v2x + ny * v2y; float t0 = fminf(fminf(p0, p1), p2); float t1 = fmaxf(fmaxf(p0, p1), p2); if ((r1 < t0 - eps) || (t1 < r0 - eps)) hit = 0; } // Edge 1: v1 -> v2 if (hit) { float ex = v2x - v1x; float ey = v2y - v1y; float nx = -ey; float ny = ex; float len = sqrtf(nx*nx + ny*ny); float inv_len = 1.0f / (len + 1e-8f); nx *= inv_len; ny *= inv_len; float r0 = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1); float r1 = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0); float p0 = nx * v0x + ny * v0y; float p1 = nx * v1x + ny * v1y; float p2 = nx * v2x + ny * v2y; float t0 = fminf(fminf(p0, p1), p2); float t1 = fmaxf(fmaxf(p0, p1), p2); if ((r1 < t0 - eps) || (t1 < r0 - eps)) hit = 0; } // Edge 2: v2 -> v0 if (hit) { float ex = v0x - v2x; float ey = v0y - v2y; float nx = -ey; float ny = ex; float len = sqrtf(nx*nx + ny*ny); float inv_len = 1.0f / (len + 1e-8f); nx *= inv_len; ny *= inv_len; float r0 = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1); float r1 = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0); float p0 = nx * v0x + ny * v0y; float p1 = nx * v1x + ny * v1y; float p2 = nx * v2x + ny * v2y; float t0 = fminf(fminf(p0, p1), p2); float t1 = fmaxf(fmaxf(p0, p1), p2); if ((r1 < t0 - eps) || (t1 < r0 - eps)) hit = 0; } out[idx] = hit ? 1.0f : 0.0f; } ``` Wait, but there's a subtlety: in the reference, for each axis that separates, `hit = hit * (~separated).to(hit.dtype)`. This means if any axis separates, hit becomes 0. It does not short-circuit per batch element? Actually, for each axis, it computes `separated` for all batch elements, and updates hit. The `&` or multiplication ensures that if any axis separates for a given batch element, hit becomes 0. So short-circuiting per batch element is fine. However, the reference computes all axes regardless, because it's vectorized and must compute the whole tensor. Our kernel can short-circuit per thread, which is an advantage. Is there any numerical difference from short-circuiting? No, because if hit becomes 0, staying 0 doesn't depend on later computations. So the result is the same. But we need to be careful about `nx >= 0.0f ? x0 : x1` in the rect min/max formula. The reference computes 4 dot products and takes min/max. Let's verify they are exactly equivalent. For a given nx, ny: The 4 corners projections are: c00 = nx*x0 + ny*y0 c10 = nx*x1 + ny*y0 c11 = nx*x1 + ny*y1 c01 = nx*x0 + ny*y1 Min = min(c00, c10, c11, c01). Since x0 <= x1 and y0 <= y1: If nx >= 0 and ny >= 0: min is c00 = nx*x0 + ny*y0. Matches formula. If nx >= 0 and ny < 0: min is c01 = nx*x0 + ny*y1. Matches formula. If nx < 0 and ny >= 0: min is c10 = nx*x1 + ny*y0. Matches formula. If nx < 0 and ny < 0: min is c11 = nx*x1 + ny*y1. Matches formula. Max = max(c00, c10, c11, c01). If nx >= 0 and ny >= 0: max is c11 = nx*x1 + ny*y1. Matches. If nx >= 0 and ny < 0: max is c10 = nx*x1 + ny*y0. Matches. If nx < 0 and ny >= 0: max is c01 = nx*x0 + ny*y1. Matches. If nx < 0 and ny < 0: max is c00 = nx*x0 + ny*y0. Matches. So yes, exactly equivalent. But what about `nx == 0`? If nx == 0 and ny >= 0: min = 0*x0 + ny*y0 = ny*y0. Actual min over corners: all have nx*x = 0, so min is ny*y0. Correct. If nx == 0 and ny < 0: min = 0*x0 + ny*y1 = ny*y1. Actual min: ny*y1 (since y1 > y0 and ny negative). Correct. So the formula is exact. One more thing: the reference's `separated` condition: `(r1 < t0 - eps) | (t1 < r0 - eps)`. In our kernel, we compute the same. Note that the reference uses `eps` as `self.eps`. In the reference, `eps=1e-6` by default. We should pass it through. Now, what about the host wrapper function? We need to write a function that takes Tensors and calls the kernel. ```cpp torch::Tensor sat_rect_triangle_cuda( torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps) { int B = robot_min.size(0); auto out = torch::empty({B, 1}, robot_min.options()); int threads = 256; int blocks = (B + threads - 1) / threads; sat_rect_triangle_kernel<<<blocks, threads>>>( robot_min.data_ptr<float>(), robot_max.data_ptr<float>(), env_triangle.data_ptr<float>(), out.data_ptr<float>(), B, eps); return out; } ``` Wait, the output shape should be (B, 1). Our kernel writes `out[idx]`. `out` is contiguous with shape (B, 1). The index is just `idx`. Since the stride for the first dimension is 1? Actually, for a (B,1) tensor, the elements are stored contiguously as B elements. So `out[idx]` is correct. But if the tensor is not contiguous, `.data_ptr<float>()` could be problematic. We should ensure the output is contiguous, which it is. For inputs, we should ideally check they are contiguous and on CUDA, or call `.contiguous()`. To be safe: robot_min = robot_min.contiguous(); robot_max = robot_max.contiguous(); env_triangle = env_triangle.contiguous(); Also check dtype is float. In the reference, dtype is fp32. We can enforce or cast. The problem says "numerically equivalent outputs at fp32 precision". The inputs might be fp32. Let's assume they are. We should check that robot_min is on CUDA. But the reference might run on CPU or CUDA? The evaluation says "backend=cuda". So inputs are CUDA tensors. We can add TORCH_CHECK. Let's write the Python file. Important: `load_inline` usage: ```python import torch from torch.utils.cpp_extension import load_inline cuda_source = """ #include <torch/extension.h> #include <cuda_runtime.h> __global__ void sat_rect_triangle_kernel(...) { ... } torch::Tensor sat_rect_triangle_cuda(torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps) { ... } """ cpp_sources = "" # Not needed if we define everything in cuda_source? Actually load_inline takes cuda_sources. Actually, torch.utils.cpp_extension.load_inline signature: load_inline(name, cpp_sources, cuda_sources=None, functions=None, ...) We can put everything in `cuda_sources`. But the host function needs to be compiled as C++ code that knows about torch. Usually we put the device kernel in `cuda_sources` and the host wrapper also in `cuda_sources`. Or we can put host wrapper in `cpp_sources` and device kernel in `cuda_sources`. But for `load_inline`, it's fine to put both in `cuda_sources` if we are using CUDA extension. Let's just put everything in `cuda_sources`. Wait, I recall that `load_inline` with `cuda_sources` compiles the CUDA source with nvcc. You can include torch headers in cuda_sources. Let's just do: ```python import torch from torch.utils.cpp_extension import load_inline _cuda_module = load_inline( name="sat_kernel", cpp_sources="", cuda_sources=''' #include <torch/extension.h> #include <cuda_runtime.h> __global__ void sat_rect_triangle_kernel(...) { ... } torch::Tensor sat_rect_triangle_cuda(torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps) { ... } ''', functions=['sat_rect_triangle_cuda'], extra_cuda_cflags=['-O3'], verbose=False, ) ``` Wait, `load_inline` might need `cpp_sources` to be a non-empty string or not? The signature is `load_inline(name, cpp_sources, cuda_sources=None, ...)`. If we pass empty string for `cpp_sources`, it might be okay. Alternatively, we can put the host wrapper in `cpp_sources` and the kernel in `cuda_sources`: ```python cpp_sources = ''' #include <torch/extension.h> torch::Tensor sat_rect_triangle_cuda(torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps); ''' cuda_sources = ''' #include <torch/extension.h> #include <cuda_runtime.h> __global__ void sat_rect_triangle_kernel(...) { ... } torch::Tensor sat_rect_triangle_cuda(torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps) { ... } ''' ``` Actually, for `load_inline`, the `cpp_sources` string is compiled as C++ source, and `cuda_sources` is compiled as CUDA source. They are linked together. If we declare the function in `cpp_sources` but define it in `cuda_sources`, that works. But simpler: I think we can just put everything in `cpp_sources` if we are using `load_inline` with `with_cuda=True` or if we provide `cuda_sources`. Actually, if we provide `cuda_sources`, the extension is built with CUDA. Let's check typical usage patterns. Typical usage: ```python module = load_inline( name="my_kernel", cpp_sources="", cuda_sources=cuda_code, functions=["my_function"], extra_cuda_cflags=["-O3"], verbose=True, ) ``` This is a common pattern. Let's do that. Let's write the full kernel code in Python. One thing: we need to make sure we handle `B` correctly. `env_triangle` has shape (B, 3, 2), so `env_triangle.size(0)` is B. `robot_min` is (B,2). In the kernel, indexing for env_triangle: if the tensor is contiguous, `env_triangle[idx * 6 + 0]` is correct. But what if the tensor has some other layout? We'll make it contiguous. Let's think about the fminf/fmaxf functions. These are CUDA device functions. For float, they are correct. Potential issue: when `nx` or `ny` is NaN? Not possible with given inputs. Another potential issue: `nx >= 0.0f` vs `nx > 0.0f`. As discussed, `>=` is correct. Let's double check with `nx == 0`. If `nx == 0`, then `r0`'s x-part should be `0 * x0 = 0`. With `nx >= 0.0f ? x0 : x1`, we get 0 * x0 = 0. With `nx > 0.0f ? x0 : x1`, we get 0 * x1 = 0. Same result. So it doesn't matter for the value, but it's fine. Wait, there is a subtle numerical issue. The reference for the triangle normal axes computes 4 dot products for each corner. For a corner (x_i, y_j), the dot product is `nx*x_i + ny*y_j`. The min over 4 corners is what we compute analytically. This is exact. But the reference computes `proj = (points * axis.unsqueeze(1)).sum(dim=-1)`. For each corner, it does `corner_x * nx + corner_y * ny`. This is the same formula. Then takes min. So they are identical. However, what about the `eps` threshold and floating point rounding? The reference computes `r1 < t0 - eps`. In our kernel, we compute the same. Since our `r1` and `r0` are analytically the same as the min/max of the 4 dot products, the result should be bit-identical or at least within a tiny rounding error. But let's consider if `nx*nx + ny*ny` might not be exactly 1.0 after normalization because of float rounding. The reference computes: `n = torch.stack((-e[:, 1], e[:, 0]), dim=-1)` `n_norm = n.norm(dim=-1, keepdim=True) + 1e-8` `n = n / n_norm` This is exactly the same as our `inv_len = 1.0f / (len + 1e-8f); nx *= inv_len; ny *= inv_len;`. So `nx`, `ny` should be identical (or very close). Then rect projections: reference computes 4 dot products. Our analytic formula computes the same min/max. So the values should match exactly (assuming same FMA behavior?). Actually, the reference computes `nx*x0 + ny*y0` etc. individually. Our formula computes `nx*x0 + ny*y0` for the case where nx>=0, ny>=0. It literally computes the same expression. So it should be identical. For other cases, it computes `nx*x1 + ny*y0` etc., which is also the same expression that the reference would compute for that corner. But is there a possibility that due to associativity/ordering, `fminf` over 4 values yields a slightly different min than explicit selection? The min of a set of floats is well-defined, but `fminf(a,b)` is equivalent to `min(a,b)`. Our analytic formula directly selects the corner that yields the minimum based on the sign of nx and ny. For linear functions over a box, the minimum is guaranteed to be at one of the corners we select. So the value we compute is exactly one of the 4 corner projections, which is exactly what the reference computes and then takes the min of. So no difference. Therefore, our kernel should be numerically equivalent. But wait, what if the batch dimension is large and we want to optimize further? For H100, we can use vectorized loads (float2) to reduce memory traffic. The problem is memory-bound because it's a small computation per element: read 2*2 + 3*2 = 10 floats, do some fmin/fmax and sqrt, write 1 float. That's about 44 bytes read + 4 bytes write = 48 bytes. Compute is negligible. So memory bandwidth is the limit. A naive kernel will be memory-bound. The reference does many intermediate operations creating tensors, which involves multiple kernel launches and memory reads/writes. The fused kernel reduces memory traffic dramatically. Even a simple kernel should be much faster. However, can we make the kernel even faster? We can use `float2` loads if the memory is aligned. `robot_min` is (B,2), so we can load as float2. Similarly `robot_max`. `env_triangle` is (B,3,2), so we can load 6 floats per element. Since 6 floats = 24 bytes, we can load as float4 + float2 or three float2s. Using `float2` can reduce instructions and improve memory throughput. Also, we should use `__ldg` (load through read-only cache) for input data. And we can unroll the axes manually. Another optimization: for world axes, we don't need to compute triangle min/max by comparing all three values if we use vectorized operations? But in the kernel, `fminf(fminf(v0x, v1x), v2x)` is just two instructions. Let's refine the kernel to use float2 for `robot_min` and `robot_max` and `env_triangle`. Actually, we need to be careful with alignment: `robot_min` is contiguous (B,2). The first element of row `idx` is at offset `idx*2`. A `float2` load requires 8-byte alignment. Since `idx*2*sizeof(float) = idx*8`, it is always 8-byte aligned. Great. `env_triangle` contiguous (B,3,2): offset `idx*6*sizeof(float) = idx*24`. Is that 8-byte aligned? Yes, for any `idx`, `idx*24` is divisible by 8. So we can load as float2 or float4. For `idx*24`, we can load two float4s (32 bytes) but we only need 24. Or three float2s (8*3=24 bytes). Let's just load three float2s. But we need to be careful: `float2` in CUDA is a struct with `x` and `y`. The layout matches the tensor's last dimension being 2. So we can do: ```cuda float2 rmin = reinterpret_cast<const float2*>(robot_min)[idx]; float2 rmax = reinterpret_cast<const float2*>(robot_max)[idx]; float x0 = rmin.x, y0 = rmin.y; float x1 = rmax.x, y1 = rmax.y; const float2* tri_ptr = reinterpret_cast<const float2*>(env_triangle); float2 v0 = tri_ptr[idx * 3 + 0]; float2 v1 = tri_ptr[idx * 3 + 1]; float2 v2 = tri_ptr[idx * 3 + 2]; ``` Wait, indexing: `env_triangle` is (B,3,2). The last dimension is 2. So `tri_ptr[idx*3 + i]` gives the i-th vertex of batch element idx. Yes. This is clean and efficient. However, `reinterpret_cast` on the data pointer requires that the pointer is aligned appropriately. Since `robot_min` is a `float*` from a contiguous tensor of shape (B,2), `robot_min` points to the first element. `robot_min[idx]` is offset `idx*2` floats, i.e., `idx*8` bytes. Casting to `float2*` and indexing by `idx` gives offset `idx*sizeof(float2) = idx*8` bytes. Perfect. Similarly for `env_triangle`: `env_triangle[idx*6 + j]` is offset `(idx*6 + j)*4` bytes. Casting to `float2*` and indexing by `idx*3 + i` gives offset `(idx*3 + i)*8 = idx*24 + i*8` bytes = `(idx*6 + i*2)*4` bytes. That matches `env_triangle[idx, i, 0]` being at float index `idx*6 + i*2`. Yes. So using `float2` loads is safe and efficient. Let's also consider storing the output. `out` is (B,1). We can just write `out[idx] = hit ? 1.0f : 0.0f;`. The output pointer is `float*`. We could cast to something, but it's just one float per thread. Should we use `__restrict__` for pointers? Yes. Should we use `-use_fast_math`? The sqrt is only 3 per thread, and `-use_fast_math` makes sqrt faster but slightly less accurate. Since we need numerical equivalence, and the reference uses `torch.norm` which maps to standard sqrt (not fast math), we should probably avoid `-use_fast_math` to be safe? Actually, CUDA's `sqrtf` without fast math is correctly rounded. With fast math, it's an approximation. The reference uses `n.norm(dim=-1, keepdim=True)`, which in PyTorch likely uses `sqrt` from libm or cublas, which is standard. So we should use standard `sqrtf` (from `<cmath>`) or `sqrtf` device function. If we pass `-use_fast_math`, `sqrtf` becomes `__fsqrt_rn`? Actually, with `-use_fast_math`, `sqrtf` is approximated. To be safe, we should not use `-use_fast_math`. But if we aren't using it, the compile will be fine. However, to squeeze more performance, we can manually call `__fsqrt_rn`? No, we want accuracy. Actually, the number of sqrtfs is tiny (3 per thread). The kernel is memory-bound, so the cost of sqrt is negligible. Avoid fast math. Let's think about block size. 256 threads is a good default. For very large B, we can use more. Let's also consider using grids with `cudaOccupancyMaxPotentialBlockSize`? Not needed. One more thing: `load_inline` may compile with default flags. We should add `-O3` in `extra_cuda_cflags` to ensure optimization. Also, we need to handle `eps` being a `float` correctly. Let's also check the tolerance: fp32 uses atol=rtol=1e-4. The outputs are 0.0 or 1.0. So as long as we return exactly 0.0 or 1.0 (within float rounding), we are fine. Let's write the code. Actually, there is one more subtlety: The reference `Model` has `eps: float = 1e-6`. The host wrapper should accept `float eps`. In ModelNew.forward, we will call `_cuda_module.sat_rect_triangle_cuda(robot_min, robot_max, env_triangle, self.eps)`. Let's draft the complete Python file. ```python import torch import torch.nn as nn from torch.utils.cpp_extension import load_inline _CUDA_MODULE = load_inline( name="sat_rect_triangle", cpp_sources="", cuda_sources=r""" #include <torch/extension.h> #include <cuda_runtime.h> __global__ void sat_rect_triangle_kernel( const float* __restrict__ robot_min, const float* __restrict__ robot_max, const float* __restrict__ env_triangle, float* __restrict__ out, int B, float eps) { int idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx >= B) return; // Load robot box float2 rmin = reinterpret_cast<const float2*>(robot_min)[idx]; float2 rmax = reinterpret_cast<const float2*>(robot_max)[idx]; float x0 = rmin.x; float y0 = rmin.y; float x1 = rmax.x; float y1 = rmax.y; // Load triangle vertices const float2* tri_ptr = reinterpret_cast<const float2*>(env_triangle); float2 v0 = tri_ptr[idx * 3 + 0]; float2 v1 = tri_ptr[idx * 3 + 1]; float2 v2 = tri_ptr[idx * 3 + 2]; int hit = 1; // Axis 1: x-axis (1, 0) { float r_min = x0; float r_max = x1; float t_min = fminf(fminf(v0.x, v1.x), v2.x); float t_max = fmaxf(fmaxf(v0.x, v1.x), v2.x); if ((r_max < t_min - eps) || (t_max < r_min - eps)) { hit = 0; } } // Axis 2: y-axis (0, 1) if (hit) { float r_min = y0; float r_max = y1; float t_min = fminf(fminf(v0.y, v1.y), v2.y); float t_max = fmaxf(fmaxf(v0.y, v1.y), v2.y); if ((r_max < t_min - eps) || (t_max < r_min - eps)) { hit = 0; } } // Triangle edge normals // Edge 0: v0 -> v1 if (hit) { float ex = v1.x - v0.x; float ey = v1.y - v0.y; float nx = -ey; float ny = ex; float len = sqrtf(nx * nx + ny * ny); float inv_len = 1.0f / (len + 1e-8f); nx *= inv_len; ny *= inv_len; float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1); float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0); float p0 = nx * v0.x + ny * v0.y; float p1 = nx * v1.x + ny * v1.y; float p2 = nx * v2.x + ny * v2.y; float t_min = fminf(fminf(p0, p1), p2); float t_max = fmaxf(fmaxf(p0, p1), p2); if ((r_max < t_min - eps) || (t_max < r_min - eps)) { hit = 0; } } // Edge 1: v1 -> v2 if (hit) { float ex = v2.x - v1.x; float ey = v2.y - v1.y; float nx = -ey; float ny = ex; float len = sqrtf(nx * nx + ny * ny); float inv_len = 1.0f / (len + 1e-8f); nx *= inv_len; ny *= inv_len; float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1); float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0); float p0 = nx * v0.x + ny * v0.y; float p1 = nx * v1.x + ny * v1.y; float p2 = nx * v2.x + ny * v2.y; float t_min = fminf(fminf(p0, p1), p2); float t_max = fmaxf(fmaxf(p0, p1), p2); if ((r_max < t_min - eps) || (t_max < r_min - eps)) { hit = 0; } } // Edge 2: v2 -> v0 if (hit) { float ex = v0.x - v2.x; float ey = v0.y - v2.y; float nx = -ey; float ny = ex; float len = sqrtf(nx * nx + ny * ny); float inv_len = 1.0f / (len + 1e-8f); nx *= inv_len; ny *= inv_len; float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1); float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0); float p0 = nx * v0.x + ny * v0.y; float p1 = nx * v1.x + ny * v1.y; float p2 = nx * v2.x + ny * v2.y; float t_min = fminf(fminf(p0, p1), p2); float t_max = fmaxf(fmaxf(p0, p1), p2); if ((r_max < t_min - eps) || (t_max < r_min - eps)) { hit = 0; } } out[idx] = hit ? 1.0f : 0.0f; } torch::Tensor sat_rect_triangle_cuda( torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps) { TORCH_CHECK(robot_min.is_cuda(), "robot_min must be a CUDA tensor"); TORCH_CHECK(robot_max.is_cuda(), "robot_max must be a CUDA tensor"); TORCH_CHECK(env_triangle.is_cuda(), "env_triangle must be a CUDA tensor"); TORCH_CHECK(robot_min.dim() == 2 && robot_min.size(1) == 2, "robot_min must be (B, 2)"); TORCH_CHECK(robot_max.dim() == 2 && robot_max.size(1) == 2, "robot_max must be (B, 2)"); TORCH_CHECK(env_triangle.dim() == 3 && env_triangle.size(1) == 3 && env_triangle.size(2) == 2, "env_triangle must be (B, 3, 2)"); int B = robot_min.size(0); TORCH_CHECK(robot_max.size(0) == B && env_triangle.size(0) == B, "Batch sizes must match"); robot_min = robot_min.contiguous(); robot_max = robot_max.contiguous(); env_triangle = env_triangle.contiguous(); auto out = torch::empty({B, 1}, robot_min.options()); int threads = 256; int blocks = (B + threads - 1) / threads; sat_rect_triangle_kernel<<<blocks, threads>>>( robot_min.data_ptr<float>(), robot_max.data_ptr<float>(), env_triangle.data_ptr<float>(), out.data_ptr<float>(), B, eps); return out; } """, functions=["sat_rect_triangle_cuda"], extra_cuda_cflags=["-O3"], verbose=False, ) class ModelNew(nn.Module): def __init__(self, eps: float = 1e-6): super().__init__() self.eps = eps def forward(self, robot_min, robot_max, env_triangle): return _CUDA_MODULE.sat_rect_triangle_cuda(robot_min, robot_max, env_triangle, self.eps) ``` Wait, but default `cpp_sources` is empty string. Does `load_inline` accept that? Looking at PyTorch docs: `load_inline(name, cpp_sources, cuda_sources=None, functions=None, extra_cflags=None, extra_cuda_cflags=None, extra_ldflags=None, extra_include_paths=None, build_directory=None, is_python_module=True, verbose=False, with_cuda=None, is_standalone=False, keep_intermediates=True)` where `cpp_sources` is a string containing the C++ source code. Actually, I'm not sure if `cpp_sources=""` is accepted. Some versions might require it to be non-empty if `cuda_sources` is provided? I think it's fine. But to be safe, we can include a dummy comment or a forward declaration in `cpp_sources`. But the docs say `cpp_sources` is required. Let's put a minimal C++ source: maybe just `#include <torch/extension.h>` or just empty? I think empty is okay, but let's put a comment. Actually, looking at examples online, `cpp_sources` is often the host-side code, and `cuda_sources` is the device code. If `cpp_sources` is empty, some versions may complain? I'll put a small amount: ```python cpp_sources=r""" #include <torch/extension.h> torch::Tensor sat_rect_triangle_cuda(torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps); """, ``` Wait, `load_inline` auto-generates bindings for functions listed in `functions`. The function definition is in `cuda_sources`. The declaration in `cpp_sources` ensures the C++ compiler knows about it. This is a standard pattern. Let's do this. Wait, but if we put the declaration in `cpp_sources`, does the auto-generated pybind11 binding call the function from the C++ side? Yes, the binding is generated in the C++ compilation unit. The `cuda_sources` is compiled separately and linked. So the function must be declared in the C++ compilation unit (or in a header included there). If we put the definition in `cuda_sources` and no declaration in `cpp_sources`, the C++ compiler won't see it when generating the bindings, leading to a compile error. Actually, `load_inline` generates bindings using the `functions` list. It generates code like: ```cpp m.def("sat_rect_triangle_cuda", &sat_rect_triangle_cuda, ...); ``` This generated code is in the C++ compilation unit. So it needs a declaration of `sat_rect_triangle_cuda`. Therefore, we should provide a declaration in `cpp_sources`. Let's verify: In PyTorch's `load_inline` implementation: - It creates a `main.cpp` that includes the `cpp_sources` and adds the pybind11 wrappers. - It creates a `cuda.cu` that includes `cuda_sources`. - They are linked together. So yes, we need a declaration in `cpp_sources` if the definition is in `cuda_sources`. Therefore: ```python cpp_sources=r""" #include <torch/extension.h> torch::Tensor sat_rect_triangle_cuda(torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps); """ ``` And `cuda_sources` contains the definition. Let's adjust. Also, we might want to handle cases where `B=0`. Our code handles it: blocks = 0? Actually `(0 + 255) / 256 = 0`. Launching a kernel with 0 blocks is valid in CUDA, but torch might not like it? CUDA supports `<<<0, threads>>>` as a no-op. But `blocks` computed as integer division: `(B + threads - 1) / threads`. If B=0, blocks = 0. That's fine. Another thing: we should ensure that `ModelNew` inherits from `nn.Module` and accepts the same constructor arguments as `Model`. Yes. Let's compile and test. Actually, I need to be careful about the `r` string prefix for the CUDA source. It might contain backslashes or something? No. But using `r"""` is fine. One more thing: the `extra_cuda_cflags=["-O3"]`. Should we add `-std=c++17`? PyTorch usually defaults to C++17 or C++14. We can rely on defaults. But adding `-O3` is good. Let's write the full source and call compile_kernel. Wait, I need to call compile_kernel first. But there's a catch: `compile_kernel` just compiles the kernel. It expects the full Python source. So I need to provide the string representing the entire Python file. Let me draft it precisely.
call → compile_kernel
callcompile_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\n# Load the fused SAT kernel at module import time.\n_CUDA_MODULE = load_inline(\n name=\"sat_rect_triangle\",\n cpp_sources=r\"\"\"\n#include <torch/extension.h>\ntorch::Tensor sat_rect_triangle_cuda(torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps);\n\"\"\",\n cuda_sources=r\"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n\n__global__ void sat_rect_triangle_kernel(\n const float* __restrict__ robot_min,\n const float* __restrict__ robot_max,\n const float* __restrict__ env_triangle,\n float* __restrict__ out,\n int B,\n float eps) {\n\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= B) return;\n\n // Load robot AABB as float2 (B,2) -> 8-byte aligned\n float2 rmin = reinterpret_cast<const float2*>(robot_min)[idx];\n float2 rmax = reinterpret_cast<const float2*>(robot_max)[idx];\n float x0 = rmin.x;\n float y0 = rmin.y;\n float x1 = rmax.x;\n float y1 = rmax.y;\n\n // Load triangle vertices as float2 (B,3,2) -> 24 bytes per element, 8-byte aligned\n const float2* tri_ptr = reinterpret_cast<const float2*>(env_triangle);\n float2 v0 = tri_ptr[idx * 3 + 0];\n float2 v1 = tri_ptr[idx * 3 + 1];\n float2 v2 = tri_ptr[idx * 3 + 2];\n\n int hit = 1;\n\n // Axis 1: world x-axis (1, 0)\n {\n float r_min = x0;\n float r_max = x1;\n float t_min = fminf(fminf(v0.x, v1.x), v2.x);\n float t_max = fmaxf(fmaxf(v0.x, v1.x), v2.x);\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Axis 2: world y-axis (0, 1)\n if (hit) {\n float r_min = y0;\n float r_max = y1;\n float t_min = fminf(fminf(v0.y, v1.y), v2.y);\n float t_max = fmaxf(fmaxf(v0.y, v1.y), v2.y);\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 0: v0 -> v1\n if (hit) {\n float ex = v1.x - v0.x;\n float ey = v1.y - v0.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 1: v1 -> v2\n if (hit) {\n float ex = v2.x - v1.x;\n float ey = v2.y - v1.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 2: v2 -> v0\n if (hit) {\n float ex = v0.x - v2.x;\n float ey = v0.y - v2.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n out[idx] = hit ? 1.0f : 0.0f;\n}\n\ntorch::Tensor sat_rect_triangle_cuda(\n torch::Tensor robot_min,\n torch::Tensor robot_max,\n torch::Tensor env_triangle,\n float eps) {\n\n TORCH_CHECK(robot_min.is_cuda(), \"robot_min must be a CUDA tensor\");\n TORCH_CHECK(robot_max.is_cuda(), \"robot_max must be a CUDA tensor\");\n TORCH_CHECK(env_triangle.is_cuda(), \"env_triangle must be a CUDA tensor\");\n TORCH_CHECK(robot_min.dim() == 2 && robot_min.size(1) == 2, \"robot_min must be (B,2)\");\n TORCH_CHECK(robot_max.dim() == 2 && robot_max.size(1) == 2, \"robot_max must be (B,2)\");\n TORCH_CHECK(env_triangle.dim() == 3 && env_triangle.size(1) == 3 && env_triangle.size(2) == 2, \"env_triangle must be (B,3,2)\");\n\n int B = robot_min.size(0);\n TORCH_CHECK(robot_max.size(0) == B && env_triangle.size(0) == B, \"Batch sizes must match\");\n\n robot_min = robot_min.contiguous();\n robot_max = robot_max.contiguous();\n env_triangle = env_triangle.contiguous();\n\n auto out = torch::empty({B, 1}, robot_min.options());\n\n int threads = 256;\n int blocks = (B + threads - 1) / threads;\n\n sat_rect_triangle_kernel<<<blocks, threads>>>(\n robot_min.data_ptr<float>(),\n robot_max.data_ptr<float>(),\n env_triangle.data_ptr<float>(),\n out.data_ptr<float>(),\n B,\n eps);\n\n return out;\n}\n\"\"\",\n functions=[\"sat_rect_triangle_cuda\"],\n extra_cuda_cflags=[\"-O3\"],\n verbose=False,\n)\n\n\nclass ModelNew(nn.Module):\n \"\"\"\n Fused CUDA kernel for batched axis-aligned rectangle vs convex triangle SAT.\n \"\"\"\n\n def __init__(self, eps: float = 1e-6):\n super().__init__()\n self.eps = eps\n\n def forward(\n self,\n robot_min: torch.Tensor,\n robot_max: torch.Tensor,\n env_triangle: torch.Tensor,\n ) -> torch.Tensor:\n return _CUDA_MODULE.sat_rect_triangle_cuda(robot_min, robot_max, env_triangle, self.eps)\n" }
result ← compile_kernel (ok)
okcompile_kernel compile_kernel PASSED: kernel compiled without errors.
turn 2 · 1 tool call(s) · 15.4s turn · 15.4s LLM · 7,660 chars (click to expand)
turn 215.4s turn · 15.4s LLM · 0.0s tools
reasoning
The kernel compiled successfully. Now let me run the correctness check.
call → run_correctness
callrun_correctness { "kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\n# Load the fused SAT kernel at module import time.\n_CUDA_MODULE = load_inline(\n name=\"sat_rect_triangle\",\n cpp_sources=r\"\"\"\n#include <torch/extension.h>\ntorch::Tensor sat_rect_triangle_cuda(torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps);\n\"\"\",\n cuda_sources=r\"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n\n__global__ void sat_rect_triangle_kernel(\n const float* __restrict__ robot_min,\n const float* __restrict__ robot_max,\n const float* __restrict__ env_triangle,\n float* __restrict__ out,\n int B,\n float eps) {\n\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= B) return;\n\n // Load robot AABB as float2 (B,2) -> 8-byte aligned\n float2 rmin = reinterpret_cast<const float2*>(robot_min)[idx];\n float2 rmax = reinterpret_cast<const float2*>(robot_max)[idx];\n float x0 = rmin.x;\n float y0 = rmin.y;\n float x1 = rmax.x;\n float y1 = rmax.y;\n\n // Load triangle vertices as float2 (B,3,2) -> 24 bytes per element, 8-byte aligned\n const float2* tri_ptr = reinterpret_cast<const float2*>(env_triangle);\n float2 v0 = tri_ptr[idx * 3 + 0];\n float2 v1 = tri_ptr[idx * 3 + 1];\n float2 v2 = tri_ptr[idx * 3 + 2];\n\n int hit = 1;\n\n // Axis 1: world x-axis (1, 0)\n {\n float r_min = x0;\n float r_max = x1;\n float t_min = fminf(fminf(v0.x, v1.x), v2.x);\n float t_max = fmaxf(fmaxf(v0.x, v1.x), v2.x);\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Axis 2: world y-axis (0, 1)\n if (hit) {\n float r_min = y0;\n float r_max = y1;\n float t_min = fminf(fminf(v0.y, v1.y), v2.y);\n float t_max = fmaxf(fmaxf(v0.y, v1.y), v2.y);\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 0: v0 -> v1\n if (hit) {\n float ex = v1.x - v0.x;\n float ey = v1.y - v0.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 1: v1 -> v2\n if (hit) {\n float ex = v2.x - v1.x;\n float ey = v2.y - v1.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 2: v2 -> v0\n if (hit) {\n float ex = v0.x - v2.x;\n float ey = v0.y - v2.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n out[idx] = hit ? 1.0f : 0.0f;\n}\n\ntorch::Tensor sat_rect_triangle_cuda(\n torch::Tensor robot_min,\n torch::Tensor robot_max,\n torch::Tensor env_triangle,\n float eps) {\n\n TORCH_CHECK(robot_min.is_cuda(), \"robot_min must be a CUDA tensor\");\n TORCH_CHECK(robot_max.is_cuda(), \"robot_max must be a CUDA tensor\");\n TORCH_CHECK(env_triangle.is_cuda(), \"env_triangle must be a CUDA tensor\");\n TORCH_CHECK(robot_min.dim() == 2 && robot_min.size(1) == 2, \"robot_min must be (B,2)\");\n TORCH_CHECK(robot_max.dim() == 2 && robot_max.size(1) == 2, \"robot_max must be (B,2)\");\n TORCH_CHECK(env_triangle.dim() == 3 && env_triangle.size(1) == 3 && env_triangle.size(2) == 2, \"env_triangle must be (B,3,2)\");\n\n int B = robot_min.size(0);\n TORCH_CHECK(robot_max.size(0) == B && env_triangle.size(0) == B, \"Batch sizes must match\");\n\n robot_min = robot_min.contiguous();\n robot_max = robot_max.contiguous();\n env_triangle = env_triangle.contiguous();\n\n auto out = torch::empty({B, 1}, robot_min.options());\n\n int threads = 256;\n int blocks = (B + threads - 1) / threads;\n\n sat_rect_triangle_kernel<<<blocks, threads>>>(\n robot_min.data_ptr<float>(),\n robot_max.data_ptr<float>(),\n env_triangle.data_ptr<float>(),\n out.data_ptr<float>(),\n B,\n eps);\n\n return out;\n}\n\"\"\",\n functions=[\"sat_rect_triangle_cuda\"],\n extra_cuda_cflags=[\"-O3\"],\n verbose=False,\n)\n\n\nclass ModelNew(nn.Module):\n \"\"\"\n Fused CUDA kernel for batched axis-aligned rectangle vs convex triangle SAT.\n \"\"\"\n\n def __init__(self, eps: float = 1e-6):\n super().__init__()\n self.eps = eps\n\n def forward(\n self,\n robot_min: torch.Tensor,\n robot_max: torch.Tensor,\n env_triangle: torch.Tensor,\n ) -> torch.Tensor:\n return _CUDA_MODULE.sat_rect_triangle_cuda(robot_min, robot_max, env_triangle, self.eps)\n" }
result ← run_correctness (ok)
okrun_correctness run_correctness PASSED: (5 / 5) trials all matched the reference. Numerical precision: max_abs_err=0.00e+00 mean_abs_err=0.00e+00 max_rel_err=0.00e+00
turn 3 · 1 tool call(s) · 45.7s turn · 45.7s LLM · 18,013 chars (click to expand)
turn 345.7s turn · 45.7s LLM · 0.0s tools
reasoning
The kernel compiles and is numerically correct. Let me think if I can optimize it further for the evaluation run. The issue is that with a fixed batch size of 16, we only launch 1 block with 256 threads, but only 16 threads do real work. This leaves a lot of parallelism on the table. However, the kernel is fundamentally doing very little work per thread. The latency-bound aspect means we might not see huge speedups over the PyTorch reference, but typically for small batch sizes, the reference overhead (multiple kernel launches, memory operations) is still significant. One optimization I can do is to unroll and avoid some branches for the first two axes. But the main issue is occupancy. Let me try two approaches: 1. Increase thread count per block (already at 256) - with B=16, only 1 block launches. 2. Process multiple elements per thread - this could help but B=16 is quite small. Actually, let me reconsider. The evaluation uses batch_size=16. With 256 threads per block, we get 1 block x 256 threads = 256 threads, of which only 16 do work. That's only 1 warp of actual work. GPU warp scheduling won't help much since there's not enough warps to hide latency. What if I use much fewer threads per block, like 128 or 64, but this still only gives us 1 block. A better approach: what if I make each thread process 8 or 16 elements? But B=16, so 1 thread could process all 16. But then we lose parallelism. With 16 threads processing 1 element each, we get 1 warp. Actually, let me think about this differently. We need to think about what makes the PyTorch reference slow. The reference does: 1. Stack corners: creates a (B,4,2) tensor - allocates memory 2. For each of 5 axes: projection via broadcast multiply, sum, min, max - multiple kernel launches per axis 3. Logical operations and multiplications So the reference probably launches ~20+ kernels. The fused kernel is a single kernel launch. Even with low occupancy, avoiding all the intermediate allocations and kernel launch overhead should be a big win. But there may be a subtle issue: with `batch_size = 16`, the problem might just be memory-bound with very little data. But let me check if there's anything else I can do. Wait, I should also check if `float2` loads can cause alignment issues. Since `robot_min` is (16,2) = 32 elements, each float2 load reads 8 bytes. The stride between rows is 2*4=8 bytes, so row-major (B,2) is perfectly contiguous and 8-byte aligned. Same for robot_max. For env_triangle, shape is (16,3,2) = 96 elements, last dim = 2 floats = 8 bytes, so each float2 is contiguous and 8-byte aligned. Good. One small optimization: instead of using `sqrtf` and division, I can check if the edge length is zero and skip normalization, since a zero-length edge means the triangle has duplicate vertices (degenerate). But `1e-8f` already handles this. Another optimization: use `fmaf` for some operations. But the compiler should do this. Actually, I notice there's a subtle issue with the rectangle min/max projection. In the reference, they do: ``` proj = (points * axis.unsqueeze(1)).sum(dim=-1) min, max = proj.min(), proj.max() ``` My optimization for Axes 1 and 2 (world axes) is correct because the axis-aligned box projects to exactly [x0, x1] and [y0, y1]. For triangle edge normals, I use: ``` r_min = nx * (nx >= 0 ? x0 : x1) + ny * (ny >= 0 ? y0 : y1) r_max = nx * (nx >= 0 ? x1 : x0) + ny * (ny >= 0 ? y1 : y0) ``` This is correct for axis-aligned boxes and essentially computes the support function. Each term picks the min or max corner depending on the sign of the normal component. Wait, is this equivalent to `min(nx*x0, nx*x1) + min(ny*y0, ny*y1)`? Yes, because `nx * (nx >= 0 ? x0 : x1)` = `min(nx*x0, nx*x1)` when x0 <= x1. And `nx * (nx >= 0 ? x1 : x0)` = `max(nx*x0, nx*x1)`. So the sum is correct. Actually, one concern: when nx is negative, `nx * (nx >= 0 ? x0 : x1)` with x1 is `nx * x1`. Since nx < 0 and x1 >= x0, `nx * x1 <= nx * x0`, so this is indeed the minimum. ✓ I think the kernel is correct. Let me try to see if I can make it faster by avoiding some of the branching and float2 usage, or by processing multiple batch elements per thread. Actually, let me try a different approach: process 4 batch elements per thread. With B=16, we'd need 4 threads (but our block is 256). Actually, let me think about this differently. What if I redesign the kernel as a single-warp kernel? Or use many blocks with 1 thread each, processing multiple elements serially? No, that's worse. Another idea: with B=16, we can completely unroll everything in shared memory. Launch 16 threads, each reads 1 batch element, computes the result, and stores it. We could even use cooperative groups or just a simple thread-per-element approach with a small block size. Let me try reducing block size from 256 to something smaller like 64 or 32. This won't change the number of active warps (still 1 warp of useful work), but it might reduce launch overhead slightly. Actually, on modern GPUs, launching a block of 256 vs 32 has negligible difference. What might help more is using vectorized loads more aggressively. Or maybe I can avoid the `if (hit)` early-out branches, which might serialize some warps. Wait, actually the early-out branches can be bad for warp divergence. Since every thread in the warp does the same branch (all hit or all not hit), there's no warp divergence within the warp. But across warps, some warps might early-out while others continue. This is actually fine - early-out warps can retire, freeing resources. Another thought: the `sqrtf` might be expensive. But the reference also computes norms via `.norm()`. Actually, in the reference, `n = n / (n.norm(dim=-1, keepdim=True) + 1e-8)` computes a norm per batch element, which is the same as my `sqrtf`. So this should be similar. Actually, let me reconsider what makes the reference slow. Looking at the reference more carefully: For world axes, the reference does: 1. `_project_min_max(corners, ax)`: (B,4,2) * (2,) -> sum -> min/max 2. `_project_min_max(env_triangle, ax)`: (B,3,2) * (2,) -> sum -> min/max 3. Logical operations `corners` is a (B,4,2) tensor created by stacking. So there are several memory operations here. My kernel fuses all of this into one kernel with no intermediate allocations. This should be much faster. But wait, with B=16, the data is tiny - robot_min is 32 floats, robot_max is 32 floats, env_triangle is 96 floats. Total input: 160 floats = 640 bytes. Output: 16 floats = 64 bytes. This is an extremely small computation. CUDA kernel launch overhead is typically ~5-10 microseconds. The PyTorch reference might spend a lot of time in Python overhead and intermediate allocations. The fused kernel has only one kernel launch but still has Python -> C++ -> CUDA overhead. Hmm, actually with such a small batch size, a custom kernel might not beat PyTorch if PyTorch's overhead is already well-optimized. PyTorch can fuse some operations together. Let me check what ech0 said earlier about the model details. Looking back: "model_id": "71", batch_size: 16. Let me run a static check and then submit. But actually, let me try one more optimization: making the kernel an `__launch_bounds__` optimized version and using a smaller block size. Actually, this probably won't matter. Another idea: maybe I should process the batch with a block size matching B exactly (16 threads), and use shared memory for nothing. But 16 threads per block would hurt occupancy. What about 128 threads with each processing multiple elements? Let me just submit and see what happens. But wait, I should also consider: what if the evaluation runs with larger batch sizes too? The description says batch_size = 16 but the actual evaluation might vary. Actually, I know that in KernelBench the actual evaluation might use larger batch sizes or multiple iterations. The timing typically runs the kernel many times in a loop. So the kernel should be efficient for arbitrary batch sizes. One thing I can optimize: the `if (idx >= B) return;` with a block size of 256 and B=16 means 240 threads immediately return, wasting a lot of the block. If I expect small batch sizes, I should make the block size small, like 64 or 32. But for larger batch sizes, 256 is good. Actually, the best practice for element-wise kernels is to use 256 or 512 threads per block. The GPU scheduler handles the extra threads fine. And for large B, having more threads per block improves occupancy. I think the current kernel is good enough. Let me static check and submit. Actually wait, let me reconsider one more time. The original reference computes: ```python n = torch.stack((-e[:, 1], e[:, 0]), dim=-1) n = n / (n.norm(dim=-1, keepdim=True) + 1e-8) ``` Notice that the reference computes `n.norm(dim=-1, keepdim=True)` which computes `sqrt(nx^2 + ny^2)` and then adds `1e-8`. My code does `1.0f / (len + 1e-8f)` which is identical. Then the reference computes projections by broadcasting multiplication with corners (B,4,2) and summing. My code computes the AABB support function directly. One concern: for the rectangle projections onto triangle edge normals, the reference computes: ```python proj = (points * axis.unsqueeze(1)).sum(dim=-1) min, max = proj.min(), proj.max() ``` With `corners` of shape (B,4,2) and `axis` of shape (B,2), the result is (B,4). My code computes: ```python r_min = nx * (nx >= 0 ? x0 : x1) + ny * (ny >= 0 ? y0 : y1) r_max = nx * (nx >= 0 ? x1 : x0) + ny * (ny >= 0 ? y1 : y0) ``` Is this equivalent? Yes, because for an axis-aligned rectangle, the support function in direction (nx, ny) is evaluated at one of the four corners. The minimum projection is at the corner where each coordinate is chosen based on the sign of the corresponding normal component. This is a well-known property of AABBs. So the logic is correct. Let me submit. Wait, actually I just realized there might be one edge case: if nx = 0 exactly, then `nx >= 0 ? x0 : x1` picks x0. But `nx * x0 = 0` regardless, and `nx * x1 = 0` too. So it doesn't matter which one is picked when nx = 0. Same for ny = 0. But what about floating point? `nx >= 0.0f` when nx is -0.0f? In IEEE 754, -0.0f >= 0.0f is true. And `nx * x0` with nx = -0.0f gives -0.0f, which is the same as 0.0f for comparisons. So it's fine. Alright, let me do the static check and submit.
call → static_check
callstatic_check { "kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\n# Load the fused SAT kernel at module import time.\n_CUDA_MODULE = load_inline(\n name=\"sat_rect_triangle\",\n cpp_sources=r\"\"\"\n#include <torch/extension.h>\ntorch::Tensor sat_rect_triangle_cuda(torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps);\n\"\"\",\n cuda_sources=r\"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n\n__global__ void sat_rect_triangle_kernel(\n const float* __restrict__ robot_min,\n const float* __restrict__ robot_max,\n const float* __restrict__ env_triangle,\n float* __restrict__ out,\n int B,\n float eps) {\n\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= B) return;\n\n // Load robot AABB as float2 (B,2) -> 8-byte aligned\n float2 rmin = reinterpret_cast<const float2*>(robot_min)[idx];\n float2 rmax = reinterpret_cast<const float2*>(robot_max)[idx];\n float x0 = rmin.x;\n float y0 = rmin.y;\n float x1 = rmax.x;\n float y1 = rmax.y;\n\n // Load triangle vertices as float2 (B,3,2) -> 24 bytes per element, 8-byte aligned\n const float2* tri_ptr = reinterpret_cast<const float2*>(env_triangle);\n float2 v0 = tri_ptr[idx * 3 + 0];\n float2 v1 = tri_ptr[idx * 3 + 1];\n float2 v2 = tri_ptr[idx * 3 + 2];\n\n int hit = 1;\n\n // Axis 1: world x-axis (1, 0)\n {\n float r_min = x0;\n float r_max = x1;\n float t_min = fminf(fminf(v0.x, v1.x), v2.x);\n float t_max = fmaxf(fmaxf(v0.x, v1.x), v2.x);\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Axis 2: world y-axis (0, 1)\n if (hit) {\n float r_min = y0;\n float r_max = y1;\n float t_min = fminf(fminf(v0.y, v1.y), v2.y);\n float t_max = fmaxf(fmaxf(v0.y, v1.y), v2.y);\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 0: v0 -> v1\n if (hit) {\n float ex = v1.x - v0.x;\n float ey = v1.y - v0.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 1: v1 -> v2\n if (hit) {\n float ex = v2.x - v1.x;\n float ey = v2.y - v1.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 2: v2 -> v0\n if (hit) {\n float ex = v0.x - v2.x;\n float ey = v0.y - v2.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n out[idx] = hit ? 1.0f : 0.0f;\n}\n\ntorch::Tensor sat_rect_triangle_cuda(\n torch::Tensor robot_min,\n torch::Tensor robot_max,\n torch::Tensor env_triangle,\n float eps) {\n\n TORCH_CHECK(robot_min.is_cuda(), \"robot_min must be a CUDA tensor\");\n TORCH_CHECK(robot_max.is_cuda(), \"robot_max must be a CUDA tensor\");\n TORCH_CHECK(env_triangle.is_cuda(), \"env_triangle must be a CUDA tensor\");\n TORCH_CHECK(robot_min.dim() == 2 && robot_min.size(1) == 2, \"robot_min must be (B,2)\");\n TORCH_CHECK(robot_max.dim() == 2 && robot_max.size(1) == 2, \"robot_max must be (B,2)\");\n TORCH_CHECK(env_triangle.dim() == 3 && env_triangle.size(1) == 3 && env_triangle.size(2) == 2, \"env_triangle must be (B,3,2)\");\n\n int B = robot_min.size(0);\n TORCH_CHECK(robot_max.size(0) == B && env_triangle.size(0) == B, \"Batch sizes must match\");\n\n robot_min = robot_min.contiguous();\n robot_max = robot_max.contiguous();\n env_triangle = env_triangle.contiguous();\n\n auto out = torch::empty({B, 1}, robot_min.options());\n\n int threads = 256;\n int blocks = (B + threads - 1) / threads;\n\n sat_rect_triangle_kernel<<<blocks, threads>>>(\n robot_min.data_ptr<float>(),\n robot_max.data_ptr<float>(),\n env_triangle.data_ptr<float>(),\n out.data_ptr<float>(),\n B,\n eps);\n\n return out;\n}\n\"\"\",\n functions=[\"sat_rect_triangle_cuda\"],\n extra_cuda_cflags=[\"-O3\"],\n verbose=False,\n)\n\n\nclass ModelNew(nn.Module):\n \"\"\"\n Fused CUDA kernel for batched axis-aligned rectangle vs convex triangle SAT.\n \"\"\"\n\n def __init__(self, eps: float = 1e-6):\n super().__init__()\n self.eps = eps\n\n def forward(\n self,\n robot_min: torch.Tensor,\n robot_max: torch.Tensor,\n env_triangle: torch.Tensor,\n ) -> torch.Tensor:\n return _CUDA_MODULE.sat_rect_triangle_cuda(robot_min, robot_max, env_triangle, self.eps)\n" }
result ← static_check (ok)
okstatic_check static_check PASSED: no violations or warnings detected.
turn 4 · FINAL · 1 tool call(s) · 10.7s turn · 10.7s LLM · 8,035 chars (click to expand)
turn 410.7s turn · 10.7s LLM · 0.0s tools · FINAL
reasoning
All checks have passed. Now I just need to submit the kernel. Given that correctness and compile passed, and static check passed, there's no reason not to submit it. I'll provide the final solution.
call → submit_kernel
callsubmit_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\n# Load the fused SAT kernel at module import time.\n_CUDA_MODULE = load_inline(\n name=\"sat_rect_triangle\",\n cpp_sources=r\"\"\"\n#include <torch/extension.h>\ntorch::Tensor sat_rect_triangle_cuda(torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps);\n\"\"\",\n cuda_sources=r\"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n\n__global__ void sat_rect_triangle_kernel(\n const float* __restrict__ robot_min,\n const float* __restrict__ robot_max,\n const float* __restrict__ env_triangle,\n float* __restrict__ out,\n int B,\n float eps) {\n\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= B) return;\n\n // Load robot AABB as float2 (B,2) -> 8-byte aligned\n float2 rmin = reinterpret_cast<const float2*>(robot_min)[idx];\n float2 rmax = reinterpret_cast<const float2*>(robot_max)[idx];\n float x0 = rmin.x;\n float y0 = rmin.y;\n float x1 = rmax.x;\n float y1 = rmax.y;\n\n // Load triangle vertices as float2 (B,3,2) -> 24 bytes per element, 8-byte aligned\n const float2* tri_ptr = reinterpret_cast<const float2*>(env_triangle);\n float2 v0 = tri_ptr[idx * 3 + 0];\n float2 v1 = tri_ptr[idx * 3 + 1];\n float2 v2 = tri_ptr[idx * 3 + 2];\n\n int hit = 1;\n\n // Axis 1: world x-axis (1, 0)\n {\n float r_min = x0;\n float r_max = x1;\n float t_min = fminf(fminf(v0.x, v1.x), v2.x);\n float t_max = fmaxf(fmaxf(v0.x, v1.x), v2.x);\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Axis 2: world y-axis (0, 1)\n if (hit) {\n float r_min = y0;\n float r_max = y1;\n float t_min = fminf(fminf(v0.y, v1.y), v2.y);\n float t_max = fmaxf(fmaxf(v0.y, v1.y), v2.y);\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 0: v0 -> v1\n if (hit) {\n float ex = v1.x - v0.x;\n float ey = v1.y - v0.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 1: v1 -> v2\n if (hit) {\n float ex = v2.x - v1.x;\n float ey = v2.y - v1.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n // Triangle edge 2: v2 -> v0\n if (hit) {\n float ex = v0.x - v2.x;\n float ey = v0.y - v2.y;\n float nx = -ey;\n float ny = ex;\n float len = sqrtf(nx * nx + ny * ny);\n float inv_len = 1.0f / (len + 1e-8f);\n nx *= inv_len;\n ny *= inv_len;\n\n float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);\n float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);\n\n float p0 = nx * v0.x + ny * v0.y;\n float p1 = nx * v1.x + ny * v1.y;\n float p2 = nx * v2.x + ny * v2.y;\n float t_min = fminf(fminf(p0, p1), p2);\n float t_max = fmaxf(fmaxf(p0, p1), p2);\n\n if ((r_max < t_min - eps) || (t_max < r_min - eps)) {\n hit = 0;\n }\n }\n\n out[idx] = hit ? 1.0f : 0.0f;\n}\n\ntorch::Tensor sat_rect_triangle_cuda(\n torch::Tensor robot_min,\n torch::Tensor robot_max,\n torch::Tensor env_triangle,\n float eps) {\n\n TORCH_CHECK(robot_min.is_cuda(), \"robot_min must be a CUDA tensor\");\n TORCH_CHECK(robot_max.is_cuda(), \"robot_max must be a CUDA tensor\");\n TORCH_CHECK(env_triangle.is_cuda(), \"env_triangle must be a CUDA tensor\");\n TORCH_CHECK(robot_min.dim() == 2 && robot_min.size(1) == 2, \"robot_min must be (B,2)\");\n TORCH_CHECK(robot_max.dim() == 2 && robot_max.size(1) == 2, \"robot_max must be (B,2)\");\n TORCH_CHECK(env_triangle.dim() == 3 && env_triangle.size(1) == 3 && env_triangle.size(2) == 2, \"env_triangle must be (B,3,2)\");\n\n int B = robot_min.size(0);\n TORCH_CHECK(robot_max.size(0) == B && env_triangle.size(0) == B, \"Batch sizes must match\");\n\n robot_min = robot_min.contiguous();\n robot_max = robot_max.contiguous();\n env_triangle = env_triangle.contiguous();\n\n auto out = torch::empty({B, 1}, robot_min.options());\n\n int threads = 256;\n int blocks = (B + threads - 1) / threads;\n\n sat_rect_triangle_kernel<<<blocks, threads>>>(\n robot_min.data_ptr<float>(),\n robot_max.data_ptr<float>(),\n env_triangle.data_ptr<float>(),\n out.data_ptr<float>(),\n B,\n eps);\n\n return out;\n}\n\"\"\",\n functions=[\"sat_rect_triangle_cuda\"],\n extra_cuda_cflags=[\"-O3\"],\n verbose=False,\n)\n\n\nclass ModelNew(nn.Module):\n \"\"\"\n Fused CUDA kernel for batched axis-aligned rectangle vs convex triangle SAT.\n \"\"\"\n\n def __init__(self, eps: float = 1e-6):\n super().__init__()\n self.eps = eps\n\n def forward(\n self,\n robot_min: torch.Tensor,\n robot_max: torch.Tensor,\n env_triangle: torch.Tensor,\n ) -> torch.Tensor:\n return _CUDA_MODULE.sat_rect_triangle_cuda(robot_min, robot_max, env_triangle, self.eps)\n" }
result ← submit_kernel (ok)
oksubmit_kernel submit_kernel PASSED: (5 / 5) correctness trials all passed. Kernel runtime: 0.01 μs Runtime stats: mean=0.01μs median=?μs std=0.00μs Numerical precision: max_abs_err=0.00e+00 mean_abs_err=0.00e+00 max_rel_err=0.00e+00 Kernel launches: 1 (ref: 295, fusion ratio: 295.0) SOL score: 0.0007 Flagged for excessive speedup — this submission may have been rejected by automated review.
final submitted kernel (201 lines, 6,229 chars) — full source, untruncated
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline

# Load the fused SAT kernel at module import time.
_CUDA_MODULE = load_inline(
    name="sat_rect_triangle",
    cpp_sources=r"""
#include <torch/extension.h>
torch::Tensor sat_rect_triangle_cuda(torch::Tensor robot_min, torch::Tensor robot_max, torch::Tensor env_triangle, float eps);
""",
    cuda_sources=r"""
#include <torch/extension.h>
#include <cuda_runtime.h>

__global__ void sat_rect_triangle_kernel(
    const float* __restrict__ robot_min,
    const float* __restrict__ robot_max,
    const float* __restrict__ env_triangle,
    float* __restrict__ out,
    int B,
    float eps) {

    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx >= B) return;

    // Load robot AABB as float2 (B,2) -> 8-byte aligned
    float2 rmin = reinterpret_cast<const float2*>(robot_min)[idx];
    float2 rmax = reinterpret_cast<const float2*>(robot_max)[idx];
    float x0 = rmin.x;
    float y0 = rmin.y;
    float x1 = rmax.x;
    float y1 = rmax.y;

    // Load triangle vertices as float2 (B,3,2) -> 24 bytes per element, 8-byte aligned
    const float2* tri_ptr = reinterpret_cast<const float2*>(env_triangle);
    float2 v0 = tri_ptr[idx * 3 + 0];
    float2 v1 = tri_ptr[idx * 3 + 1];
    float2 v2 = tri_ptr[idx * 3 + 2];

    int hit = 1;

    // Axis 1: world x-axis (1, 0)
    {
        float r_min = x0;
        float r_max = x1;
        float t_min = fminf(fminf(v0.x, v1.x), v2.x);
        float t_max = fmaxf(fmaxf(v0.x, v1.x), v2.x);
        if ((r_max < t_min - eps) || (t_max < r_min - eps)) {
            hit = 0;
        }
    }

    // Axis 2: world y-axis (0, 1)
    if (hit) {
        float r_min = y0;
        float r_max = y1;
        float t_min = fminf(fminf(v0.y, v1.y), v2.y);
        float t_max = fmaxf(fmaxf(v0.y, v1.y), v2.y);
        if ((r_max < t_min - eps) || (t_max < r_min - eps)) {
            hit = 0;
        }
    }

    // Triangle edge 0: v0 -> v1
    if (hit) {
        float ex = v1.x - v0.x;
        float ey = v1.y - v0.y;
        float nx = -ey;
        float ny =  ex;
        float len = sqrtf(nx * nx + ny * ny);
        float inv_len = 1.0f / (len + 1e-8f);
        nx *= inv_len;
        ny *= inv_len;

        float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);
        float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);

        float p0 = nx * v0.x + ny * v0.y;
        float p1 = nx * v1.x + ny * v1.y;
        float p2 = nx * v2.x + ny * v2.y;
        float t_min = fminf(fminf(p0, p1), p2);
        float t_max = fmaxf(fmaxf(p0, p1), p2);

        if ((r_max < t_min - eps) || (t_max < r_min - eps)) {
            hit = 0;
        }
    }

    // Triangle edge 1: v1 -> v2
    if (hit) {
        float ex = v2.x - v1.x;
        float ey = v2.y - v1.y;
        float nx = -ey;
        float ny =  ex;
        float len = sqrtf(nx * nx + ny * ny);
        float inv_len = 1.0f / (len + 1e-8f);
        nx *= inv_len;
        ny *= inv_len;

        float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);
        float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);

        float p0 = nx * v0.x + ny * v0.y;
        float p1 = nx * v1.x + ny * v1.y;
        float p2 = nx * v2.x + ny * v2.y;
        float t_min = fminf(fminf(p0, p1), p2);
        float t_max = fmaxf(fmaxf(p0, p1), p2);

        if ((r_max < t_min - eps) || (t_max < r_min - eps)) {
            hit = 0;
        }
    }

    // Triangle edge 2: v2 -> v0
    if (hit) {
        float ex = v0.x - v2.x;
        float ey = v0.y - v2.y;
        float nx = -ey;
        float ny =  ex;
        float len = sqrtf(nx * nx + ny * ny);
        float inv_len = 1.0f / (len + 1e-8f);
        nx *= inv_len;
        ny *= inv_len;

        float r_min = nx * (nx >= 0.0f ? x0 : x1) + ny * (ny >= 0.0f ? y0 : y1);
        float r_max = nx * (nx >= 0.0f ? x1 : x0) + ny * (ny >= 0.0f ? y1 : y0);

        float p0 = nx * v0.x + ny * v0.y;
        float p1 = nx * v1.x + ny * v1.y;
        float p2 = nx * v2.x + ny * v2.y;
        float t_min = fminf(fminf(p0, p1), p2);
        float t_max = fmaxf(fmaxf(p0, p1), p2);

        if ((r_max < t_min - eps) || (t_max < r_min - eps)) {
            hit = 0;
        }
    }

    out[idx] = hit ? 1.0f : 0.0f;
}

torch::Tensor sat_rect_triangle_cuda(
    torch::Tensor robot_min,
    torch::Tensor robot_max,
    torch::Tensor env_triangle,
    float eps) {

    TORCH_CHECK(robot_min.is_cuda(), "robot_min must be a CUDA tensor");
    TORCH_CHECK(robot_max.is_cuda(), "robot_max must be a CUDA tensor");
    TORCH_CHECK(env_triangle.is_cuda(), "env_triangle must be a CUDA tensor");
    TORCH_CHECK(robot_min.dim() == 2 && robot_min.size(1) == 2, "robot_min must be (B,2)");
    TORCH_CHECK(robot_max.dim() == 2 && robot_max.size(1) == 2, "robot_max must be (B,2)");
    TORCH_CHECK(env_triangle.dim() == 3 && env_triangle.size(1) == 3 && env_triangle.size(2) == 2, "env_triangle must be (B,3,2)");

    int B = robot_min.size(0);
    TORCH_CHECK(robot_max.size(0) == B && env_triangle.size(0) == B, "Batch sizes must match");

    robot_min = robot_min.contiguous();
    robot_max = robot_max.contiguous();
    env_triangle = env_triangle.contiguous();

    auto out = torch::empty({B, 1}, robot_min.options());

    int threads = 256;
    int blocks = (B + threads - 1) / threads;

    sat_rect_triangle_kernel<<<blocks, threads>>>(
        robot_min.data_ptr<float>(),
        robot_max.data_ptr<float>(),
        env_triangle.data_ptr<float>(),
        out.data_ptr<float>(),
        B,
        eps);

    return out;
}
""",
    functions=["sat_rect_triangle_cuda"],
    extra_cuda_cflags=["-O3"],
    verbose=False,
)


class ModelNew(nn.Module):
    """
    Fused CUDA kernel for batched axis-aligned rectangle vs convex triangle SAT.
    """

    def __init__(self, eps: float = 1e-6):
        super().__init__()
        self.eps = eps

    def forward(
        self,
        robot_min: torch.Tensor,
        robot_max: torch.Tensor,
        env_triangle: torch.Tensor,
    ) -> torch.Tensor:
        return _CUDA_MODULE.sat_rect_triangle_cuda(robot_min, robot_max, env_triangle, self.eps)