kernelbench.com

KernelBench cuda · RTX PRO 6000

Grid + MinGRU SPS Kimi K3 (256k)

17.4%geomean peak fraction across shapes

manually audited: clean

Genuine fused per-step raw-CUDA kernel: one step_kernel launch per env step (obs build -> 4x256 encoder -> three 256-wide MinGRU layers with cp.async double-buffered weight/activation streaming -> action head -> first-max argmax -> clamped move -> reward -> batch-global LCG respawn), driven over the whole horizon from a single C++ host call. All weights are streamed live from the model tensors on every call; fresh state/reward/logit buffers are allocated per run. No memoization, no caller/stack sniffing, no env-var backdoors or dormant dummy-weight variants (unlike the 1m sibling run), no grader mutation, no forbidden frameworks. Official score 0.1417 SPS geomean (17.98M/30.06M/15.36M/24.59M steps/s over the four shapes); a quiet-GPU re-benchmark reproduced 0.174, consistent with less contention at record time, so the arithmetic is sound and the audit question was authenticity.

harnesskinetic-claudeagent session2h 3mtotal wall3h 17mcheck38mbenchmark36moutput tokensgpu-lock wait1h 54mgpu-lock held15mregimethroughput

Per-shape vs governing ceilingeach shape graded against whichever binds — bf16 compute or HBM bandwidth

No per-shape benchmark data archived for this run.

Kernel source (redacted)
"""Fused CUDA grid-foraging + 3xMinGRU(h=256) SPS solution (v2, cp.async pipeline).

Strategy
--------
- policy_forward / env_step: exact PyTorch mirrors of the reference semantics
  (bit-identical ops, used by check.py comparisons).
- run(): one fused CUDA kernel per env-step (obs -> enc -> 3 MinGRU layers ->
  logits -> argmax -> env move -> reward/respawn bookkeeping), launched back to
  back from a single C++ host call over the whole horizon.

Kernel organization (per step, per block of 256 threads):
  * block owns E envs (E in {16,32,64} chosen by num_envs; KC in {8,16,32})
  * gate weights pre-transposed (host, once per run) to k-major so slices
    stream into smem as 16B cp.async copies, double-buffered H ring + rolling
    3-buffer W ring; one commit group of {H, W_zh, W_zg, W_zp} per K-chunk,
    wait_prior(1) + a single barrier per chunk.
  * thread accumulates all 48 gate values (3 groups x 4 envs x 4 j) for its
    tile, then the MinGRU nonlinearity is applied in registers (no barrier).
  * activations are stored transposed [k][e] so H global traffic is v4-clean.
  * rng/food respawn uses the reference LCG; the "advance rng iff any env hit"
    rule rides a per-step global flag slot (kernel boundary = grid barrier)
    plus a per-env hit byte consumed by the next step's prologue.
"""
from __future__ import annotations

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.cpp_extension import load_inline

OP_TYPE = "grid_mingru_sps"
SUPPORTED_PRECISIONS = ["fp32"]
HARDWARE_REQUIRED = ["RTX_PRO_6000"]

BOARD = 11
OBS_DIM = 4
HIDDEN = 256
GRU_LAYERS = 3
NUM_ACTIONS = 4
GRU_OUT = 3 * HIDDEN

_CUDA_SRC = r"""
#include <torch/extension.h>
#include <ATen/cuda/CUDAContext.h>
#include <cuda_runtime.h>
#include <cuda_pipeline_primitives.h>

#define DEVI __device__ __forceinline__

constexpr unsigned long long LCG_A    = 6364136223846793005ULL;
constexpr unsigned long long LCG_MASK = 0x7fffffffffffffffULL;

DEVI float sigmoid_(float x) { return __fdividef(1.0f, 1.0f + __expf(-x)); }
DEVI float tanh_(float x)    { float e = __expf(-2.0f * x); return __fdividef(1.0f - e, 1.0f + e); }

DEVI void cp16(void* smem, const void* gmem) {
    __pipeline_memcpy_async(smem, gmem, 16);
}
DEVI void cpz16(void* smem) {
    // pipeline-friendly zero fill (no traffic): plain v4 store is fine
    *reinterpret_cast<float4*>(smem) = make_float4(0.f, 0.f, 0.f, 0.f);
}

// ---------------------------------------------------------------------------
// One fused env+policy step. Block handles E envs with 256 threads.
// h is stored transposed: hX[k][e], k in [0,256), e in [0,n).
// wGruT[l][k][row], row in [0,768), k-major.
// ---------------------------------------------------------------------------
template <int E, int KC>
__global__ void __launch_bounds__(256, 2) step_kernel(
    const float* __restrict__ wEnc,   // (256,4)
    const float* __restrict__ bEnc,   // (256,)
    const float* __restrict__ wGruT,  // (3,256,768)
    const float* __restrict__ wA,     // (4,256)
    const float* __restrict__ bA,     // (4,)
    float* __restrict__ hA,           // (256,n) ping
    float* __restrict__ hB,           // (256,n) pong
    float* __restrict__ state,        // (n,3,256) in-place
    float* __restrict__ agent,        // (n,2)
    float* __restrict__ food,         // (n,2)
    long long* __restrict__ rng,      // (n,)
    float* __restrict__ rewards,      // (n,)
    unsigned char* __restrict__ hitf, // (n,)
    float* __restrict__ logitsOut,    // (n,4)
    const int* __restrict__ flagPrev,
    int* __restrict__ flagNext,
    int n)
{
    constexpr int EQ    = E / 4;          // env quads
    constexpr int NC    = 4096 / E;       // rows per jb chunk (16/32/64 -> 256/128/64)
    constexpr int NJB   = 256 / NC;       // jb chunks per gate group
    constexpr int HPAD  = E + 4;
    constexpr int WPAD  = NC + 4;
    constexpr int NK    = 256 / KC;       // k chunks
    constexpr int HSL   = KC * HPAD;      // H slice floats
    constexpr int WSL   = KC * WPAD;      // W slice floats

    extern __shared__ float smem[];
    float* obs_s  = smem;                  // [4*E]  (view as [E][4])
    float* logit_s= obs_s + E * 4;         // [4*E]  reused at end for [4][E] logits
    float* Hb[2]; float* Wb[2];            // parity slabs; Wb slab holds 3 gate slices
    float* p = obs_s + E * 4 + 4 * E;
    Hb[0] = p;              p += HSL;
    Hb[1] = p;              p += HSL;
    Wb[0] = p;              p += 3 * WSL;
    Wb[1] = p;

    const int tid = threadIdx.x;
    const int e0 = blockIdx.x * E;
    const bool fullTile = (e0 + E <= n);

    // ---------------- prologue: rng/food respawn + obs ----------------------
    if (tid < E) {
        int e = e0 + tid;
        if (e < n) {
            float2 ag = reinterpret_cast<const float2*>(agent)[e];
            float2 fd = reinterpret_cast<const float2*>(food)[e];
            int pend = *flagPrev;
            if (pend) {
                unsigned long long r = (unsigned long long)rng[e];
                r = (r * LCG_A + 1ULL) & LCG_MASK;
                float fx = (float)(long long)(r % 11ULL);
                r = (r * LCG_A + 1ULL) & LCG_MASK;
                float fy = (float)(long long)(r % 11ULL);
                rng[e] = (long long)r;
                if (hitf[e]) {
                    fd.x = fx; fd.y = fy;
                    reinterpret_cast<float2*>(food)[e] = fd;
                }
            }
            hitf[e] = 0;
            obs_s[tid * 4 + 0] = (fd.x - ag.x) / 11.0f;
            obs_s[tid * 4 + 1] = (fd.y - ag.y) / 11.0f;
            obs_s[tid * 4 + 2] = ag.x / 10.0f;
            obs_s[tid * 4 + 3] = ag.y / 10.0f;
        }
    }
    __syncthreads();

    // ---------------- encoder: hA[j][e] = obs(e) . wEnc[j] + bEnc[j] --------
    // hA is hT layout: hA[j * n + e].
    {
        // thread handles (env = v/EJ?, jquad) with v over E*(256/4)
        for (int v = tid; v < E * 64; v += 256) {
            int ee = v >> 6;               // env local
            int j4 = (v & 63) << 2;        // j quad
            int e = e0 + ee;
            if (e < n) {
                float4 o = *reinterpret_cast<const float4*>(&obs_s[ee * 4]);
                const float* w0 = &wEnc[(j4 + 0) * 4];
                const float* w1 = &wEnc[(j4 + 1) * 4];
                const float* w2 = &wEnc[(j4 + 2) * 4];
                const float* w3 = &wEnc[(j4 + 3) * 4];
                float x0 = bEnc[j4+0] + o.x*w0[0] + o.y*w0[1] + o.z*w0[2] + o.w*w0[3];
                float x1 = bEnc[j4+1] + o.x*w1[0] + o.y*w1[1] + o.z*w1[2] + o.w*w1[3];
                float x2 = bEnc[j4+2] + o.x*w2[0] + o.y*w2[1] + o.z*w2[2] + o.w*w2[3];
                float x3 = bEnc[j4+3] + o.x*w3[0] + o.y*w3[1] + o.z*w3[2] + o.w*w3[3];
                hA[(long)(j4+0) * n + e] = x0;
                hA[(long)(j4+1) * n + e] = x1;
                hA[(long)(j4+2) * n + e] = x2;
                hA[(long)(j4+3) * n + e] = x3;
            }
        }
    }
    __syncthreads();

    // ---------------- 3 MinGRU layers ---------------------------------------
    const float* hIn  = hA;   // [k][e]
    float*       hOut = hB;
    const int eq = tid % EQ;            // env quad index (4 consecutive envs)
    const int rg = tid / EQ;            // row quad index within chunk

    // stage helpers --------------------------------------------------------
    auto stageH = [&](int kt, const float* src /* hT base */, int sw /*parity*/)
    {
        // H slice for k-chunk kt of hIn: rows k = kt*KC + [0,KC), envs e0..e0+E
        // dst layout: [kk][e], kk in [0,KC)
        float* dst = Hb[sw];
        int total4 = KC * (E / 4);
        if (fullTile) {
            for (int idx = tid; idx < total4; idx += 256) {
                int kk = idx / (E / 4);
                int qq = idx % (E / 4);
                const float* srcp = src + (long)(kt * KC + kk) * n + e0 + qq * 4;
                cp16(&dst[kk * HPAD + qq * 4], srcp);
            }
        } else {
            for (int idx = tid; idx < total4; idx += 256) {
                int kk = idx / (E / 4);
                int qq = idx % (E / 4);
                int e = e0 + qq * 4;
                if (e + 3 < n) {
                    const float* srcp = src + (long)(kt * KC + kk) * n + e;
                    cp16(&dst[kk * HPAD + qq * 4], srcp);
                } else {
                    float t0=0,t1=0,t2=0,t3=0;
                    if (e + 0 < n) t0 = src[(long)(kt * KC + kk) * n + e + 0];
                    if (e + 1 < n) t1 = src[(long)(kt * KC + kk) * n + e + 1];
                    if (e + 2 < n) t2 = src[(long)(kt * KC + kk) * n + e + 2];
                    if (e + 3 < n) t3 = src[(long)(kt * KC + kk) * n + e + 3];
                    *reinterpret_cast<float4*>(&dst[kk * HPAD + qq * 4]) = make_float4(t0,t1,t2,t3);
                }
            }
        }
    };
    auto stageW = [&](int l, int g, int jb, int kt, int sw)
    {
        // W slice rows [g*256 + jb*NC, +NC), k = kt*KC + [0,KC)
        // src: wGruT[l][k][row]; dst: slab slot g, [kk][row]
        float* dst = Wb[sw] + g * WSL;
        const float* srcBase = wGruT + ((long)l * 256 + kt * KC) * 768 + (g * 256 + jb * NC);
        int total4 = KC * (NC / 4);
        for (int idx = tid; idx < total4; idx += 256) {
            int kk = idx / (NC / 4);
            int qq = idx % (NC / 4);
            cp16(&dst[kk * WPAD + qq * 4], srcBase + (long)kk * 768 + qq * 4);
        }
    };
    // commit a k-chunk group: {H(kt), W(0..2, kt)} into parity slab kc&1
    auto commitK = [&](int l, int jb, int kt)
    {
        int kc = jb * NK + kt;             // global k-chunk counter (per layer)
        int sw = kc & 1;
        stageH(kt, hIn, sw);
        stageW(l, 0, jb, kt, sw);
        stageW(l, 1, jb, kt, sw);
        stageW(l, 2, jb, kt, sw);
        __pipeline_commit();
    };

    for (int l = 0; l < 3; ++l) {
        // reset pipeline at layer start (hIn just produced by eltwise / enc)
        commitK(l, 0, 0);
        for (int jb = 0; jb < NJB; ++jb) {
            float acc[48];
            #pragma unroll
            for (int i = 0; i < 48; ++i) acc[i] = 0.0f;

            for (int kt = 0; kt < NK; ++kt) {
                int kc = jb * NK + kt;
                // wait for this chunk's copies (issued one iteration earlier),
                // barrier, THEN prefetch the next chunk (so no warp can be
                // writing buffers another warp still reads).
                __pipeline_wait_prior(0);
                __syncthreads();
                {
                    int jbn = jb, ktn = kt + 1;
                    if (ktn >= NK) { jbn = jb + 1; ktn = 0; }
                    if (jbn < NJB) commitK(l, jbn, ktn);
                    // (no else: an empty group is not needed with wait_prior(0))
                }
                const float* Hs = Hb[kc & 1];
                const float* Ws0 = Wb[kc & 1];
                #pragma unroll
                for (int g = 0; g < 3; ++g) {
                    const float* Ws = Ws0 + g * WSL;
                    #pragma unroll
                    for (int kk = 0; kk < KC; ++kk) {
                        float4 av = *reinterpret_cast<const float4*>(&Hs[kk * HPAD + eq * 4]);
                        float4 bv = *reinterpret_cast<const float4*>(&Ws[kk * WPAD + rg * 4]);
                        acc[g*16+ 0] += av.x * bv.x; acc[g*16+ 1] += av.x * bv.y; acc[g*16+ 2] += av.x * bv.z; acc[g*16+ 3] += av.x * bv.w;
                        acc[g*16+ 4] += av.y * bv.x; acc[g*16+ 5] += av.y * bv.y; acc[g*16+ 6] += av.y * bv.z; acc[g*16+ 7] += av.y * bv.w;
                        acc[g*16+ 8] += av.z * bv.x; acc[g*16+ 9] += av.z * bv.y; acc[g*16+10] += av.z * bv.z; acc[g*16+11] += av.z * bv.w;
                        acc[g*16+12] += av.w * bv.x; acc[g*16+13] += av.w * bv.y; acc[g*16+14] += av.w * bv.z; acc[g*16+15] += av.w * bv.w;
                    }
                }
            }

            // ---- MinGRU merge for this chunk (registers only) --------------
            int j0 = jb * NC + rg * 4;
            #pragma unroll
            for (int ee = 0; ee < 4; ++ee) {
                int e = e0 + eq * 4 + ee;
                if (e >= n) continue;
                float4 st  = *reinterpret_cast<const float4*>(&state[((long)e * 3 + l) * 256 + j0]);
                float stx[4] = {st.x, st.y, st.z, st.w};
                float outv[4], hnv[4];
                #pragma unroll
                for (int jj = 0; jj < 4; ++jj) {
                    float hold  = hIn[(long)(j0 + jj) * n + e];
                    float sg    = sigmoid_(acc[16 + ee * 4 + jj]);
                    float th    = tanh_(acc[0 + ee * 4 + jj]);
                    float sp    = sigmoid_(acc[32 + ee * 4 + jj]);
                    float o     = stx[jj] + sg * (th - stx[jj]);
                    outv[jj]    = o;
                    hnv[jj]     = fmaf(sp, o, (1.0f - sp) * hold);
                }
                *reinterpret_cast<float4*>(&state[((long)e * 3 + l) * 256 + j0]) =
                    make_float4(outv[0], outv[1], outv[2], outv[3]);
                #pragma unroll
                for (int jj = 0; jj < 4; ++jj)
                    hOut[(long)(j0 + jj) * n + e] = hnv[jj];
            }
        }
        const float* tmp = hIn; hIn = hOut; hOut = const_cast<float*>(tmp);
        __pipeline_wait_prior(0);
        __syncthreads();
    }

    // ---------------- heads: logits[e][a] = h[e].wA[a] + bA[a] --------------
    // thread = (ee = tid>>2, a = tid&3); hIn is [k][e]
    {
        int a  = tid & 3;
        int ee = tid >> 2;               // 0..63
        int e  = e0 + ee;
        if (a < 4 && ee < E && e < n) {
            float acc0 = 0.f;
            const float* wa = wA + a * 256;
            #pragma unroll 8
            for (int k = 0; k < 256; ++k) acc0 += hIn[(long)k * n + e] * wa[k];
            logit_s[a * E + ee] = acc0 + bA[a];
        }
        __syncthreads();
    }

    // ---------------- argmax + env step -------------------------------------
    if (tid < E) {
        int e = e0 + tid;
        if (e < n) {
            float l0 = logit_s[0 * E + tid], l1 = logit_s[1 * E + tid];
            float l2 = logit_s[2 * E + tid], l3 = logit_s[3 * E + tid];
            *reinterpret_cast<float4*>(&logitsOut[(long)e * 4]) = make_float4(l0, l1, l2, l3);
            int act = 0; float best = l0;
            if (l1 > best) { best = l1; act = 1; }
            if (l2 > best) { best = l2; act = 2; }
            if (l3 > best) { best = l3; act = 3; }
            float2 ag = reinterpret_cast<const float2*>(agent)[e];
            float2 fd = reinterpret_cast<const float2*>(food)[e];
            float nx = ag.x + ((act == 3) ? 1.0f : ((act == 2) ? -1.0f : 0.0f));
            float ny = ag.y + ((act == 1) ? 1.0f : ((act == 0) ? -1.0f : 0.0f));
            nx = fminf(fmaxf(nx, 0.0f), 10.0f);
            ny = fminf(fmaxf(ny, 0.0f), 10.0f);
            reinterpret_cast<float2*>(agent)[e] = make_float2(nx, ny);
            bool hit = (nx == fd.x) && (ny == fd.y);
            if (hit) {
                rewards[e] += 1.0f;
                hitf[e] = 1;
                atomicMax(flagNext, 1);
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Host side
// ---------------------------------------------------------------------------
static int pick_E(long n) {
    if (n >= 20480) return 64;  // >= 320 blocks
    if (n >= 5000)  return 32;  // >= 157 blocks
    return 16;
}

struct Args {
    const float *wEnc, *bEnc, *wGruT, *wA, *bA;
    float *hA, *hB, *state, *agent, *food;
    long long* rng;
    float* rewards;
    unsigned char* hitf;
    float* logits;
    const int* flags;
};

template <int E, int KC>
static void launch_all(dim3 grid, int smemBytes, cudaStream_t stream,
                       const Args& a, int horizon, int n) {
    for (int t = 0; t < horizon; ++t) {
        step_kernel<E, KC><<<grid, 256, smemBytes, stream>>>(
            a.wEnc, a.bEnc, a.wGruT, a.wA, a.bA, a.hA, a.hB, a.state, a.agent,
            a.food, a.rng, a.rewards, a.hitf, a.logits,
            a.flags + t, const_cast<int*>(a.flags) + t + 1, n);
    }
}

void rollout(
    torch::Tensor wEnc, torch::Tensor bEnc, torch::Tensor wGruT,
    torch::Tensor wA, torch::Tensor bA,
    torch::Tensor hA, torch::Tensor hB, torch::Tensor state,
    torch::Tensor agent, torch::Tensor food, torch::Tensor rng,
    torch::Tensor rewards, torch::Tensor hitf, torch::Tensor logits,
    torch::Tensor flags, long horizon, long n)
{
    cudaStream_t stream = at::cuda::getCurrentCUDAStream();
    Args a;
    a.wEnc = wEnc.data_ptr<float>();
    a.bEnc = bEnc.data_ptr<float>();
    a.wGruT = wGruT.data_ptr<float>();
    a.wA = wA.data_ptr<float>();
    a.bA = bA.data_ptr<float>();
    a.hA = hA.data_ptr<float>();
    a.hB = hB.data_ptr<float>();
    a.state = state.data_ptr<float>();
    a.agent = agent.data_ptr<float>();
    a.food = food.data_ptr<float>();
    a.rng = (long long*)rng.data_ptr<long>();
    a.rewards = rewards.data_ptr<float>();
    a.hitf = (unsigned char*)hitf.data_ptr<uint8_t>();
    a.logits = logits.data_ptr<float>();
    a.flags = flags.data_ptr<int>();

    int E = pick_E(n);
    dim3 grid((unsigned)((n + E - 1) / E));
    int NCv = 4096 / E;
    int KCv = (E == 64) ? 16 : (E == 32 ? 8 : 4);
    int smemBytes = 4 * E * 4 * 2
                  + 2 * KCv * (E + 4) * 4
                  + 2 * 3 * KCv * (NCv + 4) * 4;
    switch (E) {
        case 64: launch_all<64, 16>(grid, smemBytes, stream, a, (int)horizon, (int)n); break;
        case 32: launch_all<32, 8>(grid, smemBytes, stream, a, (int)horizon, (int)n); break;
        default: launch_all<16, 4>(grid, smemBytes, stream, a, (int)horizon, (int)n); break;
    }
}
"""

_CPP_SRC = r"""
#include <torch/extension.h>
void rollout(
    torch::Tensor wEnc, torch::Tensor bEnc, torch::Tensor wGruT,
    torch::Tensor wA, torch::Tensor bA,
    torch::Tensor hA, torch::Tensor hB, torch::Tensor state,
    torch::Tensor agent, torch::Tensor food, torch::Tensor rng,
    torch::Tensor rewards, torch::Tensor hitf, torch::Tensor logits,
    torch::Tensor flags, long horizon, long n);
"""

_ext = None


def _get_ext():
    global _ext
    if _ext is None:
        _ext = load_inline(
            name="grid_mingru_sps_v2",
            cpp_sources=[_CPP_SRC],
            cuda_sources=[_CUDA_SRC],
            functions=["rollout"],
            extra_cuda_cflags=[
                "-O3",
                "--generate-code=arch=compute_120,code=sm_120",
                "-lineinfo",
            ],
            verbose=False,
        )
    return _ext


def _mingru_g(x: torch.Tensor) -> torch.Tensor:
    return torch.tanh(x)


class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.w_enc = nn.Parameter(torch.empty(HIDDEN, OBS_DIM))
        self.b_enc = nn.Parameter(torch.zeros(HIDDEN))
        self.w_gru = nn.Parameter(torch.empty(GRU_LAYERS, GRU_OUT, HIDDEN))
        self.w_a = nn.Parameter(torch.empty(NUM_ACTIONS, HIDDEN))
        self.b_a = nn.Parameter(torch.zeros(NUM_ACTIONS))
        self.w_v = nn.Parameter(torch.empty(1, HIDDEN))
        self.b_v = nn.Parameter(torch.zeros(1))
        self.reset_parameters(0)

    def reset_parameters(self, seed: int = 0) -> None:
        g = torch.Generator(device="cpu")
        g.manual_seed(seed)
        for p in self.parameters():
            tmp = torch.empty(p.shape, dtype=p.dtype, device="cpu")
            tmp.normal_(0.0, 0.02, generator=g)
            p.data.copy_(tmp)

    def forward(self, obs: torch.Tensor, state: torch.Tensor):
        return policy_forward(self, obs, state)


def policy_forward(model: Model, obs: torch.Tensor, state: torch.Tensor):
    """obs (N,4), state (N,L,H) -> logits (N,4), new_state (N,L,H), value (N,).

    Exact mirror of the reference math (used for correctness comparison).
    """
    h = F.linear(obs, model.w_enc, model.b_enc)
    new_states = []
    for layer in range(GRU_LAYERS):
        st = state[:, layer, :]
        gates = F.linear(h, model.w_gru[layer])
        zh, zg, zp = gates.split(HIDDEN, dim=-1)
        out = st + torch.sigmoid(zg) * (_mingru_g(zh) - st)
        p = torch.sigmoid(zp)
        h = p * out + (1.0 - p) * h
        new_states.append(out)
    new_state = torch.stack(new_states, dim=1)
    logits = F.linear(h, model.w_a, model.b_a)
    value = F.linear(h, model.w_v, model.b_v).squeeze(-1)
    return logits, new_state, value


def env_step(
    agent: torch.Tensor,
    food: torch.Tensor,
    actions: torch.Tensor,
    rng_state: torch.Tensor,
):
    """Exact mirror of the reference env step (deterministic LCG respawn)."""

    def _lcg_step(rng: torch.Tensor) -> torch.Tensor:
        return (rng * 6364136223846793005 + 1) & 0x7FFFFFFFFFFFFFFF

    delta = torch.zeros_like(agent)
    delta[:, 1] = torch.where(actions == 0, -torch.ones_like(delta[:, 1]), delta[:, 1])
    delta[:, 1] = torch.where(actions == 1, torch.ones_like(delta[:, 1]), delta[:, 1])
    delta[:, 0] = torch.where(actions == 2, -torch.ones_like(delta[:, 0]), delta[:, 0])
    delta[:, 0] = torch.where(actions == 3, torch.ones_like(delta[:, 0]), delta[:, 0])
    agent = (agent + delta).clamp(0, BOARD - 1)
    hit = (agent == food).all(dim=-1)
    reward = hit.float()
    rng_state = rng_state.clone()
    if hit.any():
        rng_state = _lcg_step(rng_state)
        fx = (rng_state % BOARD).to(agent.dtype)
        rng_state = _lcg_step(rng_state)
        fy = (rng_state % BOARD).to(agent.dtype)
        new_food = torch.stack([fx, fy], dim=-1)
        food = food.clone()
        food[hit] = new_food[hit]
    return agent, food, reward, rng_state


def _run_torch(model: Model, num_envs: int, horizon: int, seed: int) -> dict:
    """Reference-mirror fallback (CPU or non-multiple-of-4 num_envs)."""
    device = next(model.parameters()).device
    g = torch.Generator(device="cpu")
    g.manual_seed(seed)
    agent = torch.randint(0, BOARD, (num_envs, 2), generator=g).float().to(device)
    food = torch.randint(0, BOARD, (num_envs, 2), generator=g).float().to(device)
    rng_state = torch.arange(num_envs, device=device, dtype=torch.int64) + (seed * 10007)
    state = torch.zeros(num_envs, GRU_LAYERS, HIDDEN, device=device)
    rewards = torch.zeros(num_envs, device=device)
    last_logits = torch.zeros(num_envs, NUM_ACTIONS, device=device)

    with torch.no_grad():
        for _t in range(horizon):
            dx = (food[:, 0] - agent[:, 0]) / BOARD
            dy = (food[:, 1] - agent[:, 1]) / BOARD
            obs = torch.stack([dx, dy, agent[:, 0] / (BOARD - 1), agent[:, 1] / (BOARD - 1)], dim=-1)
            logits, state, _v = policy_forward(model, obs, state)
            last_logits = logits
            actions = torch.argmax(logits, dim=-1)
            agent, food, r, rng_state = env_step(agent, food, actions, rng_state)
            rewards = rewards + r

    return {
        "rewards": rewards.detach(),
        "positions": agent.detach().round().long(),
        "last_logits": last_logits.detach(),
        "state": state.detach(),
    }


def run(num_envs: int, horizon: int, seed: int, model: Model | None = None) -> dict:
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    if model is None:
        model = Model()
    model = model.to(device).eval()
    n = int(num_envs)
    h = int(horizon)
    if device.type != "cuda" or (n % 4) != 0:
        return _run_torch(model, n, h, seed)

    ext = _get_ext()

    g = torch.Generator(device="cpu")
    g.manual_seed(seed)
    agent = torch.randint(0, BOARD, (n, 2), generator=g).float().to(device)
    food = torch.randint(0, BOARD, (n, 2), generator=g).float().to(device)
    rng_state = torch.arange(n, device=device, dtype=torch.int64) + (seed * 10007)
    state = torch.zeros(n, GRU_LAYERS, HIDDEN, device=device)
    rewards = torch.zeros(n, device=device)
    logits = torch.empty(n, NUM_ACTIONS, device=device)
    hA = torch.empty(HIDDEN, n, device=device)   # transposed [k][e]
    hB = torch.empty(HIDDEN, n, device=device)
    hitf = torch.zeros(n, dtype=torch.uint8, device=device)
    flags = torch.zeros(h + 2, dtype=torch.int32, device=device)

    # k-major transpose of gate weights for smem-friendly streaming
    wGruT = model.w_gru.detach().transpose(1, 2).contiguous()

    ext.rollout(
        model.w_enc.detach().contiguous(),
        model.b_enc.detach().contiguous(),
        wGruT,
        model.w_a.detach().contiguous(),
        model.b_a.detach().contiguous(),
        hA, hB, state, agent, food, rng_state, rewards, hitf, logits, flags,
        h, n,
    )

    return {
        "rewards": rewards.detach(),
        "positions": agent.detach().round().long(),
        "last_logits": logits.detach(),
        "state": state.detach(),
    }


def get_init_inputs():
    return []


def get_inputs():
    return []

20260716_090648_kinetic-claude_kinetic-0715_04_grid_mingru_sps