FW-GLM-5-1 · popcorn · pop_l123_st_gpt
correct 42_BatchedForwardKinematics.py
model
FW-GLM-5-1
turns / max
1 / 1
tool calls
1
tokens (total)
7334
wall clock
272.5s
truncated
yes
backend
cuda fp32
variant
popcorn
runtime
0.01 μs
ref runtime
0.02 μs
speedup
3.76x
started
2026-05-11T09:00:04Z
timingkernelbench.timing
kernel runtime
0.01 μs
ref runtime
0.02 μs
speedup
3.76x
kernel mean / median / std
0.01 / — / 2.20e-04 μs
ref mean / median / std
0.02 / — / 0.00 μs
numerical precisioneval_kernel_against_ref
max abs error
4.45e-07
mean abs error
3.93e-08
max rel error
1.33e-04
mean rel error
9.61e-07
gpu memoryextended_metrics.measure_memory
kernel peak
33.00 MB
ref peak
33.01 MB
kernel / ref
1.00x
kernel launches / fusiontorch.profiler
kernel launches
1
ref launches
14
fusion ratio (ref / kernel)
14.00x
total cuda time
3 μs
SOL (speed-of-light)nsight_hardware_counters
SOL score (0–1)
9.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.88 GB/s
dram util %
0.1%
fp32 tflops
—
fp32 util %
—
fp16 tflops
—
occupancy %
6.3%
bottleneck
unknown
dominant pipe %
—
arithmetic intensity
—
ridge point
20.00
L1 hit %
—
L2 hit %
99.2%
ld sectors / req
14.50
st sectors / req
—
regs / thread
32
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
short_scoreboard
1683.1%
long_scoreboard
392.6%
reference PyTorch source (the prompt the agent sees)
import math

import torch
import torch.nn as nn

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


class Model(nn.Module):
    """
    **Batched forward kinematics (FK)** for a **planar serial** revolute chain.

    Fixed link lengths ``L_j`` (buffer), joint angles ``θ_j`` (batch ``B``). For each
    link, the heading is ``Σ_{k≤j} θ_k``; joint positions are the cumulative sum of
    ``L_j [cos(heading), sin(heading)]``—fully **vectorized** over ``B`` and ``J``.

    Returns positions of the **end of each link** in the base frame ``(B, J, 2)``
    (last row is the end-effector in the plane). Typical fusion target for
    batched geometry in calibration, whole-body control, and VLA motion heads.
    """

    def __init__(self, num_joints: int, link_scale: float = 1.0):
        super().__init__()
        assert num_joints >= 1
        self.num_joints = num_joints
        lengths = torch.ones(num_joints, dtype=torch.float32) * float(link_scale)
        self.register_buffer("link_lengths", lengths)

    def forward(self, joint_angles: torch.Tensor) -> torch.Tensor:
        """
        Args:
            joint_angles: (B, J) radians, one revolute per link.
        Returns:
            positions: (B, J, 2) world-frame (x, y) at each link end; ``[..., -1, :]`` is EE.
        """
        B, J = joint_angles.shape
        assert J == self.num_joints

        heading = torch.cumsum(joint_angles, dim=-1)
        c = torch.cos(heading)
        s = torch.sin(heading)
        direction = torch.stack((c, s), dim=-1)
        displacements = direction * self.link_lengths.view(1, J, 1)
        return torch.cumsum(displacements, dim=1)


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

    def __init__(self, num_joints: int, link_scale: float = 1.0):
        super().__init__()
        self._impl = Model(num_joints, link_scale=link_scale)

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


batch_size = 32
num_joints = 7
link_scale = 0.15


def get_inputs():
    p = popcorn_pri
    return [torch.empty(p.jitter_int(batch_size), num_joints).uniform_(-math.pi, math.pi)]


def get_init_inputs():
    return [num_joints, link_scale]
turn 0 · FINAL · 1 tool call(s) · 272.5s turn · 59.0s LLM · tokens in=1,431 · out=5,903 · total=7,334 · 21,096 chars (click to expand)
turn 0272.5s turn · 59.0s LLM · 213.5s tools · tokens in=1,431 · out=5,903 · total=7,334 · FINAL · reasoning clipped
reasoning
reasoning (truncated 4386 chars): Let me analyze this task: 1. The model does forward kinematics for a planar serial revolute chain 2. Input: joint_angles (B, J) - batch of joint angles in radians 3. Output: positions (B, J, 2) - world-frame (x, y) at each link end The computation is: 1. heading = cumsum(joint_angles, dim=-1) - cumulative sum of angles along joints 2. c = cos(heading), s = sin(heading) 3. direction = stack((c, s), dim=-1) - shape (B, J, 2) 4. displacements = direction * link_lengths.view(1, J, 1) - shape (B, J, 2) 5. return cumsum(displacements, dim=1) - shape (B, J, 2) This is a sequential computation where each joint's position depends on all previous joints. However, we can parallelize across the batch dimension B. For optimal performance: - Use one thread block per batch element - Within each block, all threads work on computing the kinematics for that batch - We need two cumsum operations: one for headings, one for positions Let me write an optimized CUDA kernel: ```python import math import torch import torch.nn as nn from torch.utils.cpp_extension import load_inline # Compile the CUDA kernel cuda_source = """ #include <torch/extension.h> #include <cuda.h> #include <cuda_runtime.h> #include <math.h> __global__ void forward_kinematics_kernel( const float* __restrict__ joint_angles, const float* __restrict__ link_lengths, float* __restrict__ positions, int batch_size, int num_joints ) { int b = blockIdx.x; if (b >= batch_size) return; const float* angles = joint_angles + b * num_joints; float* pos = positions + b * num_joints * 2; float heading = 0.0f; float x = 0.0f, y = 0.0f; for (int j = 0; j < num_joints; j++) { heading += angles[j]; float c = cosf(heading); float s = sinf(heading); float L = link_lengths[j]; x += L * c; y += L * s; pos[j * 2] = x; pos[j * 2 + 1] = y; } } torch::Tensor forward_kinematics_cuda(torch::Tensor joint_angles, torch::Tensor link_lengths) { TORCH_CHECK(joint_angles.dim() == 2, "joint_angles must be 2D"); TORCH_CHECK(link_lengths.dim() == 1, "link_lengths must be 1D"); int batch_size = joint_angles.size(0); int num_joints = joint_angles.size(1); TORCH_CHECK(link_lengths.size(0) == num_joints, "link_lengths size mismatch"); auto positions = torch::empty({batch_size, num_joints, 2}, joint_angles.options()); forward_kinematics_kernel<<<batch_size, 1>>>( joint_angles.data_ptr<float>(), link_lengths.data_ptr<float>(), positions.data_ptr<float>(), batch_size, num_joints ); return positions; } """ cpp_source = "torch::Tensor forward_kinematics_cuda(torch::Tensor joint_angles, torch::Tensor link_lengths);" forward_kinematics_module = load_inline( name='forward_kinematics', cpp_sources=cpp_source, cuda_sources=cuda_source, functions=['forward_kinematics_cuda'], verbose=False ) class ModelNew(nn.Module): def __init__(self, num_joints: int, link_scale: float = 1.0): super().__init__() assert num_joints >= 1 self.num_joints = num_joints lengths = torch.ones(num_joints, dtype=torch.float32) * float(link_scale) self.register_buffer("link_lengths", lengths) def forward(self, joint_angles: torch.Tensor) -> torch.Tensor: return forward_kinematics_module.forward_kinematics_cuda(joint_angles, self.link_lengths) ``` This is a straightforward implementation. But let me think about optimization: 1. Each batch element is independent, so we can use one block per batch 2. The computation within each batch is sequential (cumsum dependencies) 3. num_joints is typically small (7 in the default), so the inner loop is short For such a small inner loop, using a single thread per batch element might actually be optimal. The overhead of shared memory and thread synchronization for such a small number of joints might outweigh the benefits. However, I could consider using warp-level parallelism within each batch. But with only 7 joints, it might not be worth it. Let me also consider using __ldg for read-only loads and __restrict__ for aliasing hints, which I already have. One potential optimization is to ensure the link_lengths are in constant memory or cached. Since link_lengths is a buffer and small, this should be handled well. Let me also use __sincosf for computing both sin and cos at once, which might be faster: ```cpp __sincosf(heading, &s, &c); ``` Actually, looking at NVIDIA documentation, __sincosf is a device function that computes both sin and cos at once and might be faster. Let me also use __ldg to load through the read-only cache: ```cpp float angle = __ldg(&angles[j]); float L = __ldg(&link_lengths[j]); ``` Actually, for joint_angles which is accessed sequentially and might not be cached optimally, using __ldg could help. But for link_lengths, since it's small and reused across all batches, it should already be in cache. Let me refine the kernel: ```cpp __global__ void forward_kinematics_kernel( const float* __restrict__ joint_angles, const float* __restrict__ link_lengths, float* __restrict__ positions, int batch_size, int num_joints ) { int b = blockIdx.x; if (b >= batch_size) return; const float* angles = joint_angles + b * num_joints; float* pos = positions + b * num_joints * 2; float heading = 0.0f; float x = 0.0f, y = 0.0f; for (int j = 0; j < num_joints; j++) { heading += __ldg(&angles[j]); float c, s; __sincosf(heading, &s, &c); float L = __ldg(&link_lengths[j]); x += L * c; y += L * s; pos[j * 2] = x; pos[j * 2 + 1] = y; } } ``` Wait, I should check if __sincosf returns cos first or sin first. Let me check: `void __sincosf(float x, float *sptr, float *cptr)` - so it's sin first, then cos. Let me also think about memory layout. The output tensor is (B, J, 2), so positions[b, j, 0] = x and positions[b, j, 1] = y. In memory, this is laid out as [x0, y0, x1, y1, x2, y2, ...] for each batch element. This is coalesced memory access pattern for writing, which is good. Now, let me think about launching configuration. With batch_size=32, we have 32 blocks, each with 1 thread. This is not great for occupancy. We could use multiple threads per block, but since the computation is inherently sequential (cumsum), we can't easily parallelize within a batch element. One option is to use shared memory to have multiple batch elements per block. But that complicates the indexing and might not provide significant benefits for small num_joints. Another option is to use a 1D grid and stride over batch elements within each thread: ```cpp __global__ void forward_kinematics_kernel( const float* __restrict__ joint_angles, const float* __restrict__ link_lengths, float* __restrict__ positions, int batch_size, int num_joints ) { int b = blockIdx.x * blockDim.x + threadIdx.x; while (b < batch_size) { const float* angles = joint_angles + b * num_joints; float* pos = positions + b * num_joints * 2; float heading = 0.0f; float x = 0.0f, y = 0.0f; for (int j = 0; j < num_joints; j++) { heading += __ldg(&angles[j]); float c, s; __sincosf(heading, &s, &c); float L = __ldg(&link_lengths[j]); x += L * c; y += L * s; pos[j * 2] = x; pos[j * 2 + 1] = y; } b += blockDim.x * gridDim.x; } } ``` This allows for better occupancy with larger block sizes. Let's use 256 threads per block. Actually, for batch_size=32 and num_joints=7, the total computation is quite small. The key is to ensure good memory access patterns and minimize kernel launch overhead. Let me write a final version: ```python import math im ...[ omitted 4386 characters ]... * num_joints * 2; float heading = 0.0f; float x = 0.0f, y = 0.0f; for (int j = 0; j < num_joints; ++j) { heading += angles[j]; float c, s; __sincosf(heading, &s, &c); float L = link_lengths[j]; x += L * c; y += L * s; pos[j * 2] = x; pos[j * 2 + 1] = y; } } } torch::Tensor forward_kinematics_cuda(torch::Tensor joint_angles, torch::Tensor link_lengths) { TORCH_CHECK(joint_angles.dim() == 2, "joint_angles must be 2D"); TORCH_CHECK(link_lengths.dim() == 1, "link_lengths must be 1D"); TORCH_CHECK(joint_angles.is_contiguous(), "joint_angles must be contiguous"); TORCH_CHECK(link_lengths.is_contiguous(), "link_lengths must be contiguous"); const int batch_size = joint_angles.size(0); const int num_joints = joint_angles.size(1); TORCH_CHECK(link_lengths.size(0) == num_joints, "link_lengths size mismatch"); auto positions = torch::empty({batch_size, num_joints, 2}, joint_angles.options()); const int block_size = 256; const int num_blocks = (batch_size + block_size - 1) / block_size; forward_kinematics_kernel<<<num_blocks, block_size>>>( joint_angles.data_ptr<float>(), link_lengths.data_ptr<float>(), positions.data_ptr<float>(), batch_size, num_joints ); return positions; } """ cpp_source = "torch::Tensor forward_kinematics_cuda(torch::Tensor joint_angles, torch::Tensor link_lengths);" forward_kinematics_module = load_inline( name='forward_kinematics_ext', cpp_sources=cpp_source, cuda_sources=cuda_source, functions=['forward_kinematics_cuda'], verbose=False, extra_cuda_cflags=['-O3'] ) class ModelNew(nn.Module): def __init__(self, num_joints: int, link_scale: float = 1.0): super().__init__() assert num_joints >= 1 self.num_joints = num_joints lengths = torch.ones(num_joints, dtype=torch.float32) * float(link_scale) self.register_buffer("link_lengths", lengths) def forward(self, joint_angles: torch.Tensor) -> torch.Tensor: return forward_kinematics_module.forward_kinematics_cuda( joint_angles.contiguous(), self.link_lengths ) ``` Hmm, but link_lengths is already contiguous since it's created as a 1D tensor. Let me not add the check and just ensure joint_angles is contiguous in the forward call. Actually, looking at the reference code, joint_angles is created as empty(B, J) and then uniform_, which should be contiguous. But to be safe, I'll call contiguous(). Let me also think about whether we need to check if the tensor is on CUDA. The reference implementation doesn't explicitly check, so I'll assume it's handled by PyTorch. One more optimization: we could use `#pragma unroll` for the inner loop if num_joints is small and known at compile time. But since it's a runtime parameter, we can't unroll it directly. However, since num_joints is typically small (like 7), the loop is already tight and should be efficient. Let me also consider using const __restrict__ more aggressively. The kernel signature already has these qualifiers. I think the kernel is reasonably optimized. Let me finalize and submit. Actually, I realize I should double-check the memory access pattern for positions. The tensor is (B, J, 2), so in memory, it's laid out as: - positions[b, 0, 0], positions[b, 0, 1], positions[b, 1, 0], positions[b, 1, 1], ... So `pos[j * 2]` and `pos[j * 2 + 1]` are correct for accessing positions[b, j, 0] and positions[b, j, 1] respectively. Let me verify: For batch element b, the starting pointer is `positions + b * num_joints * 2`. Then for joint j, the offset is `j * 2` for x and `j * 2 + 1` for y. This is correct. Now let me also make sure the kernel handles edge cases like empty batch or zero joints. Actually, the constructor asserts num_joints >= 1, so we don't need to worry about zero joints. For empty batch, the kernel will simply not execute any threads, which is fine. I think the implementation is correct. Let me submit it. Oh wait, I should also consider using the CUDAStreams. PyTorch uses CUDA streams for async execution. The kernel launch already respects the current CUDA stream, so we should be fine. One more thing: I should ensure the kernel doesn't throw exceptions. The TORCH_CHECK macros will raise exceptions on the CPU side, which is fine. Let me also add proper includes for the CUDA math functions: ```cpp #include <math.h> ``` Actually, `__sincosf` is defined in `device_functions.h` for some CUDA versions, but it should be available through `<cuda_runtime.h>` or implicitly included. Let me add it to be safe. Actually, looking at CUDA documentation, `__sincosf` is a device intrinsic and should be available without explicit includes when compiling with nvcc. Let me finalize: ```python import math import torch import torch.nn as nn from torch.utils.cpp_extension import load_inline cuda_source = """ #include <torch/extension.h> #include <cuda_runtime.h> __global__ void forward_kinematics_kernel( const float* __restrict__ joint_angles, const float* __restrict__ link_lengths, float* __restrict__ positions, int batch_size, int num_joints ) { const int tid = blockIdx.x * blockDim.x + threadIdx.x; const int stride = blockDim.x * gridDim.x; for (int b = tid; b < batch_size; b += stride) { const float* angles = joint_angles + b * num_joints; float* pos = positions + b * num_joints * 2; float heading = 0.0f; float x = 0.0f, y = 0.0f; for (int j = 0; j < num_joints; ++j) { heading += angles[j]; float c, s; __sincosf(heading, &s, &c); float L = link_lengths[j]; x += L * c; y += L * s; pos[j * 2] = x; pos[j * 2 + 1] = y; } } } torch::Tensor forward_kinematics_cuda(torch::Tensor joint_angles, torch::Tensor link_lengths) { TORCH_CHECK(joint_angles.dim() == 2, "joint_angles must be 2D"); TORCH_CHECK(link_lengths.dim() == 1, "link_lengths must be 1D"); TORCH_CHECK(joint_angles.is_contiguous(), "joint_angles must be contiguous"); TORCH_CHECK(link_lengths.is_contiguous(), "link_lengths must be contiguous"); const int batch_size = joint_angles.size(0); const int num_joints = joint_angles.size(1); TORCH_CHECK(link_lengths.size(0) == num_joints, "link_lengths size mismatch"); auto positions = torch::empty({batch_size, num_joints, 2}, joint_angles.options()); const int block_size = 256; const int num_blocks = (batch_size + block_size - 1) / block_size; forward_kinematics_kernel<<<num_blocks, block_size>>>( joint_angles.data_ptr<float>(), link_lengths.data_ptr<float>(), positions.data_ptr<float>(), batch_size, num_joints ); return positions; } """ cpp_source = "torch::Tensor forward_kinematics_cuda(torch::Tensor joint_angles, torch::Tensor link_lengths);" forward_kinematics_module = load_inline( name='forward_kinematics_ext', cpp_sources=cpp_source, cuda_sources=cuda_source, functions=['forward_kinematics_cuda'], verbose=False, extra_cuda_cflags=['-O3'] ) class ModelNew(nn.Module): def __init__(self, num_joints: int, link_scale: float = 1.0): super().__init__() assert num_joints >= 1 self.num_joints = num_joints lengths = torch.ones(num_joints, dtype=torch.float32) * float(link_scale) self.register_buffer("link_lengths", lengths) def forward(self, joint_angles: torch.Tensor) -> torch.Tensor: return forward_kinematics_module.forward_kinematics_cuda( joint_angles.contiguous(), self.link_lengths ) ``` This looks good. Let me submit it.
call → submit_kernel
callsubmit_kernel { "kernel_code": "import math\nimport torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n\n__global__ void forward_kinematics_kernel(\n const float* __restrict__ joint_angles,\n const float* __restrict__ link_lengths,\n float* __restrict__ positions,\n int batch_size,\n int num_joints\n) {\n const int tid = blockIdx.x * blockDim.x + threadIdx.x;\n const int stride = blockDim.x * gridDim.x;\n \n for (int b = tid; b < batch_size; b += stride) {\n const float* angles = joint_angles + b * num_joints;\n float* pos = positions + b * num_joints * 2;\n \n float heading = 0.0f;\n float x = 0.0f, y = 0.0f;\n \n for (int j = 0; j < num_joints; ++j) {\n heading += angles[j];\n float c, s;\n __sincosf(heading, &s, &c);\n float L = link_lengths[j];\n x += L * c;\n y += L * s;\n pos[j * 2] = x;\n pos[j * 2 + 1] = y;\n }\n }\n}\n\ntorch::Tensor forward_kinematics_cuda(torch::Tensor joint_angles, torch::Tensor link_lengths) {\n TORCH_CHECK(joint_angles.dim() == 2, \"joint_angles must be 2D\");\n TORCH_CHECK(link_lengths.dim() == 1, \"link_lengths must be 1D\");\n TORCH_CHECK(joint_angles.is_contiguous(), \"joint_angles must be contiguous\");\n TORCH_CHECK(link_lengths.is_contiguous(), \"link_lengths must be contiguous\");\n \n const int batch_size = joint_angles.size(0);\n const int num_joints = joint_angles.size(1);\n \n TORCH_CHECK(link_lengths.size(0) == num_joints, \"link_lengths size mismatch\");\n \n auto positions = torch::empty({batch_size, num_joints, 2}, joint_angles.options());\n \n const int block_size = 256;\n const int num_blocks = (batch_size + block_size - 1) / block_size;\n \n forward_kinematics_kernel<<<num_blocks, block_size>>>(\n joint_angles.data_ptr<float>(),\n link_lengths.data_ptr<float>(),\n positions.data_ptr<float>(),\n batch_size,\n num_joints\n );\n \n return positions;\n}\n\"\"\"\n\ncpp_source = \"torch::Tensor forward_kinematics_cuda(torch::Tensor joint_angles, torch::Tensor link_lengths);\"\n\nforward_kinematics_module = load_inline(\n name='forward_kinematics_ext',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['forward_kinematics_cuda'],\n verbose=False,\n extra_cuda_cflags=['-O3']\n)\n\nclass ModelNew(nn.Module):\n def __init__(self, num_joints: int, link_scale: float = 1.0):\n super().__init__()\n assert num_joints >= 1\n self.num_joints = num_joints\n lengths = torch.ones(num_joints, dtype=torch.float32) * float(link_scale)\n self.register_buffer(\"link_lengths\", lengths)\n\n def forward(self, joint_angles: torch.Tensor) -> torch.Tensor:\n return forward_kinematics_module.forward_kinematics_cuda(\n joint_angles.contiguous(), \n self.link_lengths\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=4.45e-07 mean_abs_err=3.93e-08 max_rel_err=1.33e-04 Memory: 33.0 MB peak (ref: 33.01 MB, ratio: 0.9998x) Kernel launches: 1 (ref: 14, fusion ratio: 14.0) SOL score: 0.0009
final submitted kernel (91 lines, 2,980 chars) — full source, untruncated
import math
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline

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

__global__ void forward_kinematics_kernel(
    const float* __restrict__ joint_angles,
    const float* __restrict__ link_lengths,
    float* __restrict__ positions,
    int batch_size,
    int num_joints
) {
    const int tid = blockIdx.x * blockDim.x + threadIdx.x;
    const int stride = blockDim.x * gridDim.x;
    
    for (int b = tid; b < batch_size; b += stride) {
        const float* angles = joint_angles + b * num_joints;
        float* pos = positions + b * num_joints * 2;
        
        float heading = 0.0f;
        float x = 0.0f, y = 0.0f;
        
        for (int j = 0; j < num_joints; ++j) {
            heading += angles[j];
            float c, s;
            __sincosf(heading, &s, &c);
            float L = link_lengths[j];
            x += L * c;
            y += L * s;
            pos[j * 2] = x;
            pos[j * 2 + 1] = y;
        }
    }
}

torch::Tensor forward_kinematics_cuda(torch::Tensor joint_angles, torch::Tensor link_lengths) {
    TORCH_CHECK(joint_angles.dim() == 2, "joint_angles must be 2D");
    TORCH_CHECK(link_lengths.dim() == 1, "link_lengths must be 1D");
    TORCH_CHECK(joint_angles.is_contiguous(), "joint_angles must be contiguous");
    TORCH_CHECK(link_lengths.is_contiguous(), "link_lengths must be contiguous");
    
    const int batch_size = joint_angles.size(0);
    const int num_joints = joint_angles.size(1);
    
    TORCH_CHECK(link_lengths.size(0) == num_joints, "link_lengths size mismatch");
    
    auto positions = torch::empty({batch_size, num_joints, 2}, joint_angles.options());
    
    const int block_size = 256;
    const int num_blocks = (batch_size + block_size - 1) / block_size;
    
    forward_kinematics_kernel<<<num_blocks, block_size>>>(
        joint_angles.data_ptr<float>(),
        link_lengths.data_ptr<float>(),
        positions.data_ptr<float>(),
        batch_size,
        num_joints
    );
    
    return positions;
}
"""

cpp_source = "torch::Tensor forward_kinematics_cuda(torch::Tensor joint_angles, torch::Tensor link_lengths);"

forward_kinematics_module = load_inline(
    name='forward_kinematics_ext',
    cpp_sources=cpp_source,
    cuda_sources=cuda_source,
    functions=['forward_kinematics_cuda'],
    verbose=False,
    extra_cuda_cflags=['-O3']
)

class ModelNew(nn.Module):
    def __init__(self, num_joints: int, link_scale: float = 1.0):
        super().__init__()
        assert num_joints >= 1
        self.num_joints = num_joints
        lengths = torch.ones(num_joints, dtype=torch.float32) * float(link_scale)
        self.register_buffer("link_lengths", lengths)

    def forward(self, joint_angles: torch.Tensor) -> torch.Tensor:
        return forward_kinematics_module.forward_kinematics_cuda(
            joint_angles.contiguous(), 
            self.link_lengths
        )