kernelbench.com

KernelBench cuda · H100

Grid + MinGRU SPS Claude Opus 5

88.3%geomean peak fraction across shapes

manually audited: clean

Fused env+policy rollout kernel (24KB py + 1448-line kernels.cu): grid-foraging env step fused with 3x MinGRU(256) via mma.sync.m16n8k16 inline PTX (plus wgmma m64 variant), weights pre-swizzled to fragment order, LCG env RNG bit-exact with deferred food/rng update using the kernel boundary as grid-wide reduction, MUFU transcendentals with documented error analysis (5e-7 logit err vs 1.1e-5 min top-2 gap -> argmax/rewards/positions bit-exact). _gate_weights keyed on (data_ptr, _version, device) -- _version bumps on in-place edits, so weight staleness is covered; graph plan key includes all five param addresses; seed enters outside the graph by value. No output caching. Sibling-run refs are ps//proc/lock-owner listings during the lock contention this cell suffered -- benign. Grader files Read-only, template_mutated false. REGRADE IS LOAD-BEARING: contended phase died at the 7200s GPU-lock wait (check_exit_code 124, never graded); the only valid grade is the sequential isolated re-grade (idle H100, 2026-07-27): correct=true, SPS 0.8828. result.json failure_reason=check_timeout is a leftover label from the contended phase; the regrade block is authoritative.

harnessor-opusagent session10h 1mtotal wall12h 1mcheck6sbenchmark2soutput tokensgpu-lock wait38mgpu-lock held1h 9mregimethroughput

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 env + 3x MinGRU(h=256) rollout for H100 (SM90).

The kernels live in ``kernels.cu`` (real CUDA C++ with inline ``mma.sync`` PTX).
They are compiled once with ``nvcc`` into a shared library and driven through
``ctypes`` with raw ``data_ptr()`` values, which keeps the build free of any
torch C++ / pybind11 headers (this image ships torch 2.7/cu128 headers without
bundled pybind11 next to a CUDA 13.0 nvcc).

Public API
----------
class Model                                  -- load_state_dict-compatible
policy_forward(model, obs, state)             -- fp32, reference-accurate
env_step(agent, food, actions, rng_state)     -- bit-exact
run(num_envs, horizon, seed, model=None)      -- fused greedy rollout
"""
from __future__ import annotations

import ctypes
import hashlib
import os
import subprocess
import sys
import time

import torch
import torch.nn as nn

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

BENV_MAX = 96  # largest envs-per-block the rollout kernel is compiled for

# Microseconds for one block-step of step_kernel<BENV>, measured on H100 PCIe
# with a single wave in flight (one block per SM -- the kernel asks for 256
# threads at ~170 registers, so only one block is ever resident).  The five
# points fit t(BENV) = 12.32 + 0.528*BENV to within 1.9%.  The 12.3 us intercept
# is the part that does not shrink with BENV -- the per-step latency chain that
# every block pays whatever it carries: 9 back-to-back k-loops whose mma issue
# cannot start until the previous layer's epilogue has published hc to shared,
# plus the 8 syncthreads that serialise them.  (It is NOT the gate-weight
# stream: forcing the k=0 B-tiles to stay L1-resident, which removes most of
# that traffic, only buys 4-7% at the BENVs actually used.)  So big BENV is much
# cheaper per env but packs into the SMs more coarsely.
_BENVS = (16, 32, 48, 64, 80, 96)
_T0, _T1 = 12.32, 0.528

# A candidate within this much of the cheapest is treated as a tie, and ties go
# to the smaller BENV.  The model above is calibrated at one wave, where each
# block's 3*BENV*256 floats of recurrent state stay resident in L2; at many
# waves that state streams from HBM, which costs more per block-step the bigger
# BENV is.  So the model systematically flatters large BENV on multi-wave
# shapes, and the margin absorbs that.  Measured, this picks the fastest BENV on
# all four sweep shapes: 4096->48, 16384->48, 65536->96, 8192->80.
_TIE = 1.08


def _model_cost(num_envs: int) -> dict:
    nsm = _nsm()
    cost = {}
    for b in _BENVS:
        waves = -(-((num_envs + b - 1) // b) // nsm)
        cost[b] = waves * (_T0 + _T1 * b)
    return cost


def _pick_benv(num_envs: int) -> int:
    """Choose envs-per-block to minimise the per-step makespan.

    ``ceil(num_envs / BENV)`` blocks run ``nsm`` at a time and the tail wave is
    as expensive as a full one, so the step costs ``waves * t(BENV)`` and the
    winner is whichever BENV lands closest above a wave boundary: 8192 envs is
    171 blocks at BENV=48 (1.5 waves, half a wave wasted) but 103 blocks at
    BENV=80, which is a single wave and 1.46x faster.
    """
    cost = _model_cost(num_envs)
    lo = min(cost.values())
    return min(b for b in _BENVS if cost[b] <= lo * _TIE)


# ---------------------------------------------------------------------------
# which kernel: mma.sync at some BENV, or the wgmma warpgroup kernel at 64
# ---------------------------------------------------------------------------
# The two kernels scale differently with wave count -- the wgmma one is fixed at
# BENV=64 (wgmma is m64-granular) and its per-block-step cost barely grows with
# occupancy, while the mma path's grows with BENV as the recurrent state spills
# out of L2.  A single analytic model cannot rank them across shapes: it gets
# 4096/16384/8192 right but says mma for 65536, where wgmma actually wins by
# 12.5%.  So the model only shortlists, and a short probe rollout decides.  Any
# config is numerically valid -- rewards and positions are exact for all of
# them and the two kernels' logits agree to 3.7e-9 -- so a timing-dependent
# choice cannot change what run() returns beyond that.
_PROBE_H = 4
_PROBE_REPS = 2
_TUNE: dict = {}


def _shortlist(num_envs: int) -> list:
    cost = _model_cost(num_envs)
    best = sorted(_BENVS, key=lambda b: cost[b])[:3]
    return [("mma", b) for b in best] + [("wg", 64)]

_HERE = os.path.dirname(os.path.abspath(__file__))
_CU = os.path.join(_HERE, "kernels.cu")


# ---------------------------------------------------------------------------
# build + load the CUDA shared library
# ---------------------------------------------------------------------------
def _find_nvcc() -> str:
    cand = []
    home = os.environ.get("CUDA_HOME") or os.environ.get("CUDA_PATH")
    if home:
        cand.append(os.path.join(home, "bin", "nvcc"))
    cand.append("/usr/local/cuda/bin/nvcc")
    for p in sorted(
        (d for d in os.listdir("/usr/local") if d.startswith("cuda-")), reverse=True
    ) if os.path.isdir("/usr/local") else []:
        cand.append(os.path.join("/usr/local", p, "bin", "nvcc"))
    cand.append("nvcc")
    for c in cand:
        if c == "nvcc":
            from shutil import which

            w = which("nvcc")
            if w:
                return w
        elif os.path.exists(c):
            return c
    raise RuntimeError("nvcc not found")


def _load_lib() -> ctypes.CDLL:
    src = open(_CU, "rb").read()
    tag = hashlib.sha256(src).hexdigest()[:16]
    so = os.path.join(_HERE, f"_gmk_{tag}.so")
    if not os.path.exists(so):
        nvcc = _find_nvcc()
        tmp = so + f".{os.getpid()}.tmp"
        # sm_90a (not sm_90) -- the rollout kernel uses Hopper wgmma, which
        # ptxas only accepts for the arch-specific target.
        cmd = [
            nvcc, "-O3", "-std=c++17",
            "-gencode", "arch=compute_90a,code=sm_90a",
            "-Xptxas", "-O3,-v",
            "-Xcompiler", "-fPIC", "-shared",
            "-lineinfo",
            _CU, "-o", tmp,
        ]
        try:
            subprocess.run(cmd, check=True, capture_output=True)
        except subprocess.CalledProcessError as e:  # pragma: no cover
            sys.stderr.write(e.stderr.decode(errors="ignore"))
            raise
        os.replace(tmp, so)
    lib = ctypes.CDLL(so)
    P = ctypes.c_void_p
    I = ctypes.c_int
    LL = ctypes.c_longlong
    lib.gm_prep_w.restype = I
    lib.gm_prep_w.argtypes = [P, P, P]
    lib.gm_prep_wb.restype = I
    lib.gm_prep_wb.argtypes = [P, P, P]
    lib.gm_setup.restype = I
    lib.gm_setup.argtypes = [ctypes.c_uint] + [P] * 5 + [LL, I, I, I, P]
    lib.gm_rollout.restype = I
    lib.gm_rollout.argtypes = [P] * 13 + [I, I, I, I, P]
    lib.gm_rollout_wg.restype = I
    lib.gm_rollout_wg.argtypes = [P] * 13 + [I, I, I, P]
    lib.gm_copyout.restype = I
    lib.gm_copyout.argtypes = [P] * 6 + [I, P]
    lib.gm_policy_forward.restype = I
    lib.gm_policy_forward.argtypes = [P] * 12 + [I, P]
    lib.gm_env_step.restype = I
    lib.gm_env_step.argtypes = [P] * 10 + [I, P]
    return lib


_LIB = _load_lib()


def _ck(rc: int, what: str) -> None:
    if rc != 0:
        raise RuntimeError(f"{what} failed: cuda error {rc}")


def _stream() -> int:
    return torch.cuda.current_stream().cuda_stream


# ``current_stream()`` builds a Python Stream object and costs 7.1 us; the raw
# accessor torch's own inductor backend uses is 0.12 us and honours a
# ``with torch.cuda.stream(...)`` context just the same.  A shape 0 run() is
# 1.2 ms, so the 7 us matters.
try:
    _raw_stream = torch._C._cuda_getCurrentRawStream
except AttributeError:  # pragma: no cover - older torch
    def _raw_stream(index: int = 0) -> int:
        return torch.cuda.current_stream().cuda_stream


_NSM = 0


def _nsm() -> int:
    global _NSM
    if _NSM == 0:
        _NSM = torch.cuda.get_device_properties(0).multi_processor_count
    return _NSM


# ---------------------------------------------------------------------------
# model
# ---------------------------------------------------------------------------
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)


# ---------------------------------------------------------------------------
# swizzled fp16 gate weights (cached on the model)
# ---------------------------------------------------------------------------
def _gate_weights(model: Model, kind: str = "mma") -> torch.Tensor:
    """Gate weights swizzled for one kernel's operand layout, cached on the model.

    ``mma`` wants m16n8k16 B-fragments in registers; ``wg`` wants 192x16 wgmma B
    tiles addressed by a shared-memory matrix descriptor, so they are different
    permutations of the same 1.18 MB and a process that probes both keeps both.
    """
    attr = "_gm_wsw" if kind == "mma" else "_gm_wb"
    wg = model.w_gru
    key = (wg.data_ptr(), wg._version, wg.device.index)
    cached = getattr(model, attr, None)
    if cached is not None and cached[0] == key:
        return cached[1]
    wgc = wg.detach().contiguous().float()
    n = GRU_LAYERS * 96 * 16 * 32 * 4
    prep = _LIB.gm_prep_w
    if kind != "mma":
        n *= 2  # 4 chunks x 16 ktiles x 192n x 16k halves per layer
        prep = _LIB.gm_prep_wb
    buf = torch.empty(n, dtype=torch.float16, device=wgc.device)
    _ck(prep(wgc.data_ptr(), buf.data_ptr(), _raw_stream(0)), f"gm_prep_{kind}")
    object.__setattr__(model, attr, (key, buf))
    return buf


# ---------------------------------------------------------------------------
# reusable device scratch, keyed by size
# ---------------------------------------------------------------------------
_SCRATCH: dict = {}
_SGEN = 0  # bumped on every (re)allocation -- invalidates cached raw pointers


def _scratch(name: str, n: int, dtype, device):
    global _SGEN
    key = (name, dtype, device.index)
    cur = _SCRATCH.get(key)
    if cur is None or cur.numel() < n:
        cur = torch.empty(max(n, 1), dtype=dtype, device=device)
        _SCRATCH[key] = cur
        _SGEN += 1
    return cur


# ---------------------------------------------------------------------------
# policy_forward
# ---------------------------------------------------------------------------
def policy_forward(model: Model, obs: torch.Tensor, state: torch.Tensor):
    dev = obs.device
    n = obs.shape[0]
    o = obs.detach().contiguous().float()
    s = state.detach().contiguous().float()
    logits = torch.empty(n, NUM_ACTIONS, device=dev, dtype=torch.float32)
    newstate = torch.empty(n, GRU_LAYERS, HIDDEN, device=dev, dtype=torch.float32)
    value = torch.empty(n, device=dev, dtype=torch.float32)
    _ck(
        _LIB.gm_policy_forward(
            o.data_ptr(), s.data_ptr(),
            model.w_enc.data_ptr(), model.b_enc.data_ptr(), model.w_gru.data_ptr(),
            model.w_a.data_ptr(), model.b_a.data_ptr(), model.w_v.data_ptr(),
            model.b_v.data_ptr(), logits.data_ptr(), newstate.data_ptr(), value.data_ptr(),
            n, _stream(),
        ),
        "gm_policy_forward",
    )
    return logits, newstate, value


# ---------------------------------------------------------------------------
# env_step
# ---------------------------------------------------------------------------
def env_step(agent: torch.Tensor, food: torch.Tensor, actions: torch.Tensor,
             rng_state: torch.Tensor):
    dev = agent.device
    n = agent.shape[0]
    ag = agent.detach().contiguous().float()
    fd = food.detach().contiguous().float()
    ac = actions.detach().contiguous().to(torch.int64)
    rg = rng_state.detach().contiguous().to(torch.int64)
    agout = torch.empty_like(ag)
    fdout = torch.empty_like(fd)
    rew = torch.empty(n, device=dev, dtype=torch.float32)
    rngout = torch.empty_like(rg)
    hits = torch.empty(n, device=dev, dtype=torch.uint8)
    anyflag = torch.zeros(1, device=dev, dtype=torch.int32)
    _ck(
        _LIB.gm_env_step(
            ag.data_ptr(), fd.data_ptr(), ac.data_ptr(), rg.data_ptr(),
            agout.data_ptr(), fdout.data_ptr(), rew.data_ptr(), rngout.data_ptr(),
            hits.data_ptr(), anyflag.data_ptr(), n, _stream(),
        ),
        "gm_env_step",
    )
    return agout, fdout, rew, rngout


# ---------------------------------------------------------------------------
# fused rollout
# ---------------------------------------------------------------------------
# A run() call is two ctypes calls wrapped in ~35 us of python, and at shape 0
# (4096/32, 1.2 ms) that is 3% of the score.  Almost all of it was pointer
# plumbing: ``model.w_enc`` goes through ``nn.Module.__getattr__`` (0.69 us) and
# ``.data_ptr()`` adds 0.17, so the 19 operands of the two calls cost ~16 us to
# re-derive every time even though only four of them ever change.  So freeze the
# two argument lists once per (model, cfg, num_envs) and rewrite just the four.
#
# The plan caches raw device addresses, so it is keyed on everything that can
# move one: ``_SGEN`` (any scratch reallocation), w_gru's version (in-place
# edits, i.e. ``load_state_dict``), and the address of all five parameters the
# rollout reads (a rebound parameter, or a model this path had to move onto the
# GPU).  Reading those through the live ``_parameters`` dict the plan holds
# costs 0.22 us each instead of the 0.86 us that ``model.w_enc.data_ptr()``
# costs through ``nn.Module.__getattr__``.  ``nh`` is not in the key -- the
# hitflags buffer is allocated with headroom and the plan carries its capacity,
# so a shorter horizon reuses the plan and a longer one grows the scratch and
# bumps _SGEN.
#
# On top of that the horizon loop itself is captured into a CUDA graph.  The
# loop is a C-side ``for t < horizon`` of ordinary launches, and back-to-back
# dependent launches on a stream cost ~1.1 us of hardware gap each -- 35 us of
# a 1.2 ms shape-0 call, and 71 us of a 3.7 ms shape-3 one, because the gap
# scales with the horizon and not with the work.  A graph pays that once at
# instantiation instead.  The catch is that a graph bakes its pointers while
# run() returns fresh tensors, so the captured rollout writes to persistent
# scratch and gm_copyout moves the three outputs afterwards in one launch.
# gm_setup stays outside the graph: its seed is a by-value kernel argument.
_S_SEED, _S_REW, _S_BASE, _S_NH = 0, 4, 6, 9
_R_REW, _R_LL, _R_POS, _R_HOR = 9, 10, 11, 15
_PNAMES = ("w_gru", "w_enc", "b_enc", "w_a", "b_a")
_GRAPH_MIN_H = 4  # below this the capture costs more than the gap it removes
_GRAPH_MAX = 8    # cache bound: a caller that sweeps horizons must not leak
_PROBING = False  # _pick_cfg times candidates on the plain path


def _build_plan(model, cfg, num_envs, nh, dev):
    if model.w_enc.device != dev:
        model.to(dev)  # nn.Module.to is in-place and returns self
    kind, benv = cfg
    npad = ((num_envs + benv - 1) // benv) * benv
    w = _gate_weights(model, kind)
    agent = _scratch("agent", npad * 2, torch.int32, dev)
    food = _scratch("food", npad * 2, torch.int32, dev)
    rng = _scratch("rng", npad, torch.int64, dev)
    statebuf = _scratch("state", GRU_LAYERS * npad * HIDDEN, torch.float32, dev)
    hitflags = _scratch("hits", max(nh, 64), torch.int32, dev)

    ap, fp, rp, hp = (agent.data_ptr(), food.data_ptr(), rng.data_ptr(),
                      hitflags.data_ptr())
    sa = [0, ap, fp, rp, 0, hp, 0, num_envs, npad, nh, 0]
    ra = [w.data_ptr(), model.w_enc.data_ptr(), model.b_enc.data_ptr(),
          model.w_a.data_ptr(), model.b_a.data_ptr(), statebuf.data_ptr(),
          ap, fp, rp, 0, 0, 0, hp, num_envs, npad // benv, 0]
    if kind == "mma":
        ra.append(benv)
        fn = _LIB.gm_rollout
    else:
        fn = _LIB.gm_rollout_wg
    ra.append(0)  # stream slot, always last

    # the graph's copy of the argument list, aimed at scratch instead of at
    # whatever tensors this particular call allocated
    orew = _scratch("orew", npad, torch.float32, dev)
    oll = _scratch("oll", npad * NUM_ACTIONS, torch.float32, dev)
    opos = _scratch("opos", npad * 2, torch.int64, dev)
    rag = list(ra)
    rag[_R_REW], rag[_R_LL], rag[_R_POS] = (orew.data_ptr(), oll.data_ptr(),
                                            opos.data_ptr())
    ca = [rag[_R_REW], rag[_R_LL], rag[_R_POS], 0, 0, 0, num_envs, 0]

    pd = getattr(model, "_parameters", None)
    if pd is not None and all(n in pd for n in _PNAMES):
        wgt = pd["w_gru"]
        key = (num_envs, cfg, _SGEN, wgt._version, wgt.data_ptr(),
               pd["w_enc"].data_ptr(), pd["b_enc"].data_ptr(),
               pd["w_a"].data_ptr(), pd["b_a"].data_ptr())
    else:  # not an nn.Module we recognise -- never take the cached path
        pd, key = {}, None
    # the graph cache hangs off the plan, so every event that invalidates a
    # cached pointer drops the graphs that baked it in the same breath
    plan = (key, hitflags.numel(), sa, ra, fn, pd, {}, rag, ca)
    object.__setattr__(model, "_gm_plan", plan)
    return plan


_WARMED = set()   # entry-point names that have had a non-captured launch
_UNTRIED = object()


def _capture(plan, horizon, nh):
    """Capture the horizon loop, or return None if the driver refuses.

    Capture records launches without running them, so it never reads the
    scratch -- but the *first* launch of a kernel also loads its module and sets
    its dynamic-smem attribute, and neither is legal mid-capture.  So an entry
    point gets one real setup+rollout before it is ever captured.
    """
    graphs = plan[6]
    if len(graphs) >= _GRAPH_MAX:
        graphs.clear()
    fn, rag = plan[4], plan[7]
    rag[_R_HOR] = horizon
    if fn.__name__ not in _WARMED:  # ctypes func pointers are not hashable
        sa = plan[2]
        sa[_S_SEED], sa[_S_REW], sa[_S_BASE] = 0, rag[_R_REW], 0
        sa[_S_NH] = nh
        sa[-1] = rag[-1] = _raw_stream(0)
        _ck(_LIB.gm_setup(*sa), "gm_setup(warm)")
        _ck(fn(*rag), "gm_rollout(warm)")
        torch.cuda.synchronize()
        _WARMED.add(fn.__name__)
    try:
        g = torch.cuda.CUDAGraph()
        with torch.cuda.graph(g):
            rag[-1] = _raw_stream(0)
            _ck(fn(*rag), "gm_rollout(capture)")
    except Exception:  # capture unsupported here -- fall back for good
        graphs[horizon] = None
        return None
    graphs[horizon] = g
    return g


_TPL = None  # zero-element f32/i64 templates; new_empty beats torch.empty by 1.6us


def _alloc(num_envs, dev):
    global _TPL
    tpl = _TPL
    if tpl is None:
        tpl = _TPL = (torch.empty(0, dtype=torch.float32, device=dev),
                      torch.empty(0, dtype=torch.int64, device=dev))
    return (tpl[0].new_empty(num_envs), tpl[1].new_empty((num_envs, 2)),
            tpl[0].new_empty((num_envs, NUM_ACTIONS)))


def _launch(cfg, model, num_envs, horizon, seed, dev, out=None):
    """One setup + horizon fused steps with the kernel named by ``cfg``."""
    nh = horizon if horizon > 0 else 1
    plan = model.__dict__.get("_gm_plan")
    if plan is None:
        plan = _build_plan(model, cfg, num_envs, nh, dev)
    else:
        pd = plan[5]
        wgt = pd.get("w_gru")  # empty dict == "this model is not cacheable"
        if wgt is None:
            plan = _build_plan(model, cfg, num_envs, nh, dev)
        elif nh > plan[1] or plan[0] != (
                num_envs, cfg, _SGEN, wgt._version, wgt.data_ptr(),
                pd["w_enc"].data_ptr(), pd["b_enc"].data_ptr(),
                pd["w_a"].data_ptr(), pd["b_a"].data_ptr()):
            plan = _build_plan(model, cfg, num_envs, nh, dev)
    sa, ra, fn = plan[2], plan[3], plan[4]

    graph = None
    if horizon >= _GRAPH_MIN_H and not _PROBING:
        graph = plan[6].get(horizon, _UNTRIED)
        if graph is _UNTRIED:
            graph = _capture(plan, horizon, nh)

    st = _raw_stream(0)
    # the rollout accumulates reward in place, so setup has to zero whichever
    # buffer this call's rollout is going to add into
    sa[_S_SEED] = seed & 0xFFFFFFFF
    sa[_S_BASE] = seed * 10007
    sa[_S_NH] = nh
    sa[-1] = st
    if graph is not None:
        # a captured rollout only ever touches the scratch its nodes were built
        # against, so setup does not need the caller's tensors: enqueue it first
        # and allocate while the GPU is already working.  That is 15 us off the
        # entry-to-first-enqueue path (25.0 -> 10.0) and ~9 us of wall on every
        # call, whatever the horizon.
        sa[_S_REW] = plan[7][_R_REW]
        _ck(_LIB.gm_setup(*sa), "gm_setup")
        if out is None:
            out = _alloc(num_envs, dev)
        rewards, positions, last_logits = out
        graph.replay()
        ca = plan[8]
        ca[3] = rewards.data_ptr()
        ca[4] = last_logits.data_ptr()
        ca[5] = positions.data_ptr()
        ca[-1] = st
        _ck(_LIB.gm_copyout(*ca), "gm_copyout")
        return out
    if out is None:
        out = _alloc(num_envs, dev)
    rewards, positions, last_logits = out
    sa[_S_REW] = rewards.data_ptr()
    _ck(_LIB.gm_setup(*sa), "gm_setup")
    ra[_R_REW] = rewards.data_ptr()
    ra[_R_LL] = last_logits.data_ptr()
    ra[_R_POS] = positions.data_ptr()
    ra[_R_HOR] = horizon
    ra[-1] = st
    _ck(fn(*ra), "gm_rollout")
    return out


def _pick_cfg(model, num_envs, seed, dev):
    """Shortlist by model, then time a short rollout of each and keep the best.

    Probing costs a few milliseconds once per distinct ``num_envs``; it lands in
    the first timed trial of a benchmark and is discarded by the median.
    """
    global _PROBING
    forced = os.environ.get("GM_BENV")
    if forced:
        return ("wg", 64) if forced == "wg" else ("mma", int(forced))
    if num_envs in _TUNE:
        return _TUNE[num_envs]
    cands = _shortlist(num_envs)
    best, best_t = cands[0], float("inf")
    out = _alloc(num_envs, dev)  # scribble buffer; the probe results are discarded
    # capturing a graph per candidate would cost more than the probe; the launch
    # gap a graph removes is per step and cfg-independent, so it cancels out of
    # the ranking anyway
    _PROBING = True
    try:
        for cfg in cands:
            _launch(cfg, model, num_envs, _PROBE_H, seed, dev, out)  # warm
            torch.cuda.synchronize()
            t = float("inf")
            for _ in range(_PROBE_REPS):
                t0 = time.perf_counter()
                _launch(cfg, model, num_envs, _PROBE_H, seed, dev, out)
                torch.cuda.synchronize()
                t = min(t, time.perf_counter() - t0)
            if t < best_t:
                best, best_t = cfg, t
    except RuntimeError:  # a candidate failed to launch -- keep the model's pick
        best = cands[0]
    finally:
        _PROBING = False
    _TUNE[num_envs] = best
    return best


_DEV = None


def run(num_envs: int, horizon: int, seed: int, model: Model | None = None) -> dict:
    global _DEV
    dev = _DEV
    if dev is None:
        dev = _DEV = torch.device("cuda:0")
    if model is None:
        model = Model()
    # The model's device is validated in _build_plan, which runs whenever the
    # cached w_gru address changes -- and moving a model changes it.  _launch
    # allocates the outputs itself so that on the graphed path they can be
    # created after setup is already in flight.
    cfg = _pick_cfg(model, num_envs, seed, dev)
    rewards, positions, last_logits = _launch(cfg, model, num_envs, horizon, seed, dev)
    return {"rewards": rewards, "positions": positions, "last_logits": last_logits}


# ==================================================================
# ===== sidecar: kernels.cu (63037 bytes, loaded by solution.py) =====
# ==================================================================

// Fused grid-foraging env + 3x MinGRU(h=256) rollout for H100 (SM90).
//
// Design notes
// ------------
// * One kernel launch per environment step.  The reference env_step advances the
//   LCG for *every* env iff any env hit food this step, so a grid-wide reduction
//   is needed between steps; the kernel boundary provides it for free.  The
//   food/rng update for step t-1 is *deferred* to the top of step t's kernel,
//   where the per-step `any_hit` flag written with atomicOr is already visible.
// * Gate matmuls (h[64x256] @ W[256x768]) run on the tensor cores with
//   mma.sync.aligned.m16n8k16.  h is the A operand (row = env) and lives in
//   shared memory pre-swizzled into exact A-fragment order; W is the B operand
//   and is streamed straight from L2 into registers, pre-swizzled into exact
//   B-fragment order (no shared staging, no ldmatrix).
// * The gate rows are permuted at prep time into per-hidden-group blocks
//   [zh(16) | zg(16) | zp(16)] so a warp's accumulator holds zh/zg/zp for the
//   same hidden indices.  That makes the MinGRU recurrence a pure register-local
//   epilogue and -- crucially -- the h_new values a thread produces land at
//   *exactly* the A-fragment slot that same lane will read next layer, so the
//   shared-memory write is one aligned 16B store per (m-tile, group).
// * h_old (needed for the highway term) never leaves registers.
// * fp16 inputs / fp32 accumulate for the gates.  Measured worst-case logit
//   error over the check seeds is 5e-7 vs a min top-2 logit gap of 1.1e-5, so
//   greedy argmax (and therefore positions/rewards) is bit-exact.  The
//   256->4 action head is done in fp32 because its output *is* the compared
//   quantity and it is only 0.17% of the flops.
#include <cuda_runtime.h>
#include <cuda_fp16.h>
#include <cstdint>

#define BOARD_N 11
#define HID_N   256
#define NLAY_N  3
#define NG_N    768
#define NACT_N  4

#define LCG_A 6364136223846793005ULL
#define LCG_M 0x7FFFFFFFFFFFFFFFULL

__device__ __forceinline__ unsigned long long lcg(unsigned long long r) {
  return (r * LCG_A + 1ULL) & LCG_M;
}
// Accurate versions, used by the reference-matching single-step kernels.
__device__ __forceinline__ float sigmoidf_(float x) { return 1.0f / (1.0f + expf(-x)); }

// Hardware-approximation versions for the fused rollout.  libm's expf/tanhf
// compile to out-of-line calls (~60 instr + BSSY/BSYNC per evaluation) and the
// MinGRU epilogue needs three of them per (env, hidden, layer) element, which
// dominates the kernel.  MUFU.EX2 + MUFU.RCP gives ~2e-6 relative error --
// 250x tighter than the fp16 gate matmul already in the critical path.
//   tanh(x) = 2*sigmoid(2x) - 1  keeps ~1e-7 absolute error near 0, unlike
//   tanh.approx.f32 (2^-11).
// Raw MUFU: __expf/__fdividef still emit range-check FSETP/FSEL pairs.
__device__ __forceinline__ float fex2(float x) {
  float r;
  asm("ex2.approx.ftz.f32 %0, %1;" : "=f"(r) : "f"(x));
  return r;
}
__device__ __forceinline__ float frcp(float x) {
  float r;
  asm("rcp.approx.ftz.f32 %0, %1;" : "=f"(r) : "f"(x));
  return r;
}
#define LOG2E_ 1.4426950408889634f
__device__ __forceinline__ float fsig(float x) { return frcp(1.0f + fex2(-LOG2E_ * x)); }
__device__ __forceinline__ float ftanh(float x) {
  return fmaf(2.0f, frcp(1.0f + fex2(-2.0f * LOG2E_ * x)), -1.0f);
}

// Epilogue transcendental ablation (GM_ABL_M): 0 = shipped (6 MUFU/unit),
// 1 = tanh.approx (3 MUFU/unit, ~2^-11 abs), 2 = no MUFU at all (results
// garbage; measures what the MUFU pipe contributes to the step time).
#ifndef GM_ABL_M
#define GM_ABL_M 0
#endif
#if GM_ABL_M == 0
#define ESIG(x) fsig(x)
#define ETANH(x) ftanh(x)
#elif GM_ABL_M == 1
__device__ __forceinline__ float ftanh_ap(float x) {
  float r;
  asm("tanh.approx.f32 %0, %1;" : "=f"(r) : "f"(x));
  return r;
}
#define ESIG(x) fmaf(0.5f, ftanh_ap(0.5f * (x)), 0.5f)
#define ETANH(x) ftanh_ap(x)
#else
#define ESIG(x) fmaf(0.2f, (x), 0.5f)
#define ETANH(x) fmaf(0.9f, (x), 0.01f)
#endif

__device__ __forceinline__ void mma16816(float (&d)[4], const uint32_t (&a)[4],
                                         const uint32_t (&b)[2]) {
  asm volatile(
      "mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32 "
      "{%0,%1,%2,%3}, {%4,%5,%6,%7}, {%8,%9}, {%0,%1,%2,%3};\n"
      : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3])
      : "r"(a[0]), "r"(a[1]), "r"(a[2]), "r"(a[3]), "r"(b[0]), "r"(b[1]));
}

// ---------------------------------------------------------------------------
// Weight pre-swizzle:  w_gru (3, 768, 256) fp32  ->  Wsw fp16
//   Wsw[layer][ntile(96)][kpair(8)][lane(32)][8]
// Two consecutive k-tiles share one 16 B slot per lane so the inner loop can
// fetch both B fragments with a single LDG.128 (halves the LDG issue count;
// the B stream costs ~18% of the k-loop at 64-bit granularity).
// permuted gate column p = 8*ntile + lane/4
//   group g = p/48, r = p%48, gate = r/16, hoff = r%16, hidden = 16*g + hoff
//   original row = gate*256 + hidden
// B-fragment: lane holds B[n = lane/4][k = 2*(lane%4) + (j&1) + 8*(j>>1)]
// ---------------------------------------------------------------------------
__global__ void prep_w_kernel(const float* __restrict__ wg, __half* __restrict__ out) {
  const int idx = blockIdx.x * blockDim.x + threadIdx.x;
  if (idx >= NLAY_N * 96 * 8 * 32) return;
  const int lane = idx & 31;
  const int kp = (idx >> 5) & 7;
  const int nt = (idx >> 8) % 96;
  const int layer = (idx >> 8) / 96;

  const int p = 8 * nt + (lane >> 2);
  const int g = p / 48, r = p - 48 * g;
  const int gate = r >> 4, hoff = r & 15;
  const int row = gate * HID_N + (16 * g + hoff);
  const float* w = wg + (size_t)layer * NG_N * HID_N + (size_t)row * HID_N;

  __half h8[8];
#pragma unroll
  for (int j = 0; j < 8; ++j) {
    const int kt = 2 * kp + (j >> 2);
    const int jj = j & 3;
    const int kk = 2 * (lane & 3) + (jj & 1) + 8 * (jj >> 1);
    h8[j] = __float2half_rn(w[16 * kt + kk]);
  }
  *(uint4*)(out + (size_t)idx * 8) = *(const uint4*)h8;
}

// Timing ablations (address-only; results are wrong, instruction mix identical).
//   GM_ABL_B=1  every k-pass re-reads k-pass 0's gate weights -> L1-resident,
//               so the global-load latency/bandwidth term disappears.
//   GM_ABL_A=1  every k-pass re-reads k-tile 0 of hA -> LDS stays but hits the
//               same 16 B per lane.
#ifndef GM_CUNROLL
#define GM_CUNROLL 1
#endif
#ifndef GM_ABL_B
#define GM_ABL_B 0
#endif
#ifndef GM_ABL_A
#define GM_ABL_A 0
#endif

// ---------------------------------------------------------------------------
// Fused rollout step.
//
// shared layout (BENV = 64):
//   hA   : BENV*HID halves, [mtile][ktile][lane][8]   (A-fragment order)
//   sw_a : NACT*HID floats  (action head weights)
//   sobs : BENV*4 floats
//   sag  : BENV*2 ints,  sfd : BENV*2 ints
//   slog : NWARP*BENV*NACT floats (per-warp partial logits)
//   slgt : BENV*NACT floats
// ---------------------------------------------------------------------------
// MinGRU highway epilogue for one 16-hidden chunk C of a warp's 32 columns.
//   out = st + sigmoid(zg) * (tanh(zh) - st)      -> new state
//   h   = sigmoid(zp) * out + (1 - sigmoid(zp)) * h
// hcur lives in shared, not registers: at BENV = 80/96 the NMT*16 floats of a
// register-resident hcur plus acc[NMT][6][4] blow past 255 and spill.  Every
// slot is private to the thread that owns it (same (mt, g, lane) swizzle as hA
// and the state buffer), so no barrier is needed around the read-modify-write.
#define HSLOT(mt, g) ((size_t)(((mt) * 16 + (g)) * 32 + lane) * 8)

template <int NMT>
__device__ __forceinline__ void mingru_epi(const float (&acc)[NMT][6][4],
                                           float* __restrict__ hcs, float* __restrict__ stl,
                                           int g, int lane, int t) {
#pragma unroll
  for (int mt = 0; mt < NMT; ++mt) {
    const size_t sb = HSLOT(mt, g);
    float st[8];
    if (t == 0) {
#pragma unroll
      for (int j = 0; j < 8; ++j) st[j] = 0.0f;
    } else {
      const float4 s0 = *(const float4*)(stl + sb);
      const float4 s1 = *(const float4*)(stl + sb + 4);
      st[0] = s0.x; st[1] = s0.y; st[2] = s0.z; st[3] = s0.w;
      st[4] = s1.x; st[5] = s1.y; st[6] = s1.z; st[7] = s1.w;
    }
    const float4 c0 = *(const float4*)(hcs + sb);
    const float4 c1 = *(const float4*)(hcs + sb + 4);
    float hc[8] = {c0.x, c0.y, c0.z, c0.w, c1.x, c1.y, c1.z, c1.w};
    float o[8];
#pragma unroll
    for (int j = 0; j < 8; ++j) {
      const int nh = (j >> 2);  // hidden offset >= 8 selects the second n-tile
      const int q = j & 3;
      const float zh = acc[mt][0 + nh][q];
      const float zg = acc[mt][2 + nh][q];
      const float zp = acc[mt][4 + nh][q];
      const float sg = ESIG(zg);
      const float ov = fmaf(sg, ETANH(zh) - st[j], st[j]);
      const float pp = ESIG(zp);
      o[j] = ov;
      hc[j] = fmaf(pp, ov, (1.0f - pp) * hc[j]);
    }
    *(float4*)(stl + sb) = make_float4(o[0], o[1], o[2], o[3]);
    *(float4*)(stl + sb + 4) = make_float4(o[4], o[5], o[6], o[7]);
    *(float4*)(hcs + sb) = make_float4(hc[0], hc[1], hc[2], hc[3]);
    *(float4*)(hcs + sb + 4) = make_float4(hc[4], hc[5], hc[6], hc[7]);
  }
}

template <int BENV>
__global__ __launch_bounds__(256, 1) void step_kernel(
    const __half* __restrict__ Wsw,      // [3][96][16][32][4]
    const float* __restrict__ w_enc,     // [256][4]
    const float* __restrict__ b_enc,     // [256]
    const float* __restrict__ w_a,       // [4][256]
    const float* __restrict__ b_a,       // [4]
    float* __restrict__ statebuf,        // [3][nblk][BENV*256]
    int* __restrict__ agentbuf,          // [nenv][2]
    int* __restrict__ foodbuf,           // [nenv][2]
    unsigned long long* __restrict__ rngbuf,  // [nenv]
    float* __restrict__ rewbuf,          // [nenv]
    float* __restrict__ lastlogits,      // [nenv][4]
    long long* __restrict__ posout,      // [nenv][2]
    int* __restrict__ hitflags,          // [horizon]
    int t, int nenv, int nblk, int is_last) {
  constexpr int NMT = BENV / 16;   // m-tiles (16 envs each)
  constexpr int NWARP = 8;

  const int tid = threadIdx.x;
  const int warp = tid >> 5;
  const int lane = tid & 31;
  const int blk = blockIdx.x;
  const int env0 = blk * BENV;

  extern __shared__ char smem_raw[];
  __half* hA = (__half*)smem_raw;
  float* hcs = (float*)(hA + BENV * HID_N);
  float* sw_a = hcs + BENV * HID_N;
  float* sobs = sw_a + NACT_N * HID_N;
  int* sag = (int*)(sobs + BENV * 4);
  int* sfd = sag + BENV * 2;
  float* slog = (float*)(sfd + BENV * 2);
  float* slgt = slog + NWARP * BENV * NACT_N;

  // ---- action-head weights into shared -------------------------------------
  for (int i = tid; i < NACT_N * HID_N; i += 256) sw_a[i] = w_a[i];

  // ---- env prologue: deferred food/rng update + obs ------------------------
  if (tid < BENV) {
    const int e = env0 + tid;
    const bool valid = (e < nenv);
    int ax = 0, ay = 0, fx = 0, fy = 0;
    if (valid) {
      ax = agentbuf[2 * e];
      ay = agentbuf[2 * e + 1];
      fx = foodbuf[2 * e];
      fy = foodbuf[2 * e + 1];
    }
    if (t > 0 && hitflags[t - 1]) {
      unsigned long long r = valid ? rngbuf[e] : 0ULL;
      r = lcg(r);
      const int nfx = (int)(r % (unsigned long long)BOARD_N);
      r = lcg(r);
      const int nfy = (int)(r % (unsigned long long)BOARD_N);
      if (ax == fx && ay == fy) {  // this env hit food last step
        fx = nfx;
        fy = nfy;
      }
      if (valid) {
        rngbuf[e] = r;
        foodbuf[2 * e] = fx;
        foodbuf[2 * e + 1] = fy;
      }
    }
    sag[2 * tid] = ax;
    sag[2 * tid + 1] = ay;
    sfd[2 * tid] = fx;
    sfd[2 * tid + 1] = fy;
    sobs[4 * tid + 0] = (float)(fx - ax) * (1.0f / (float)BOARD_N);
    sobs[4 * tid + 1] = (float)(fy - ay) * (1.0f / (float)BOARD_N);
    sobs[4 * tid + 2] = (float)ax * (1.0f / (float)(BOARD_N - 1));
    sobs[4 * tid + 3] = (float)ay * (1.0f / (float)(BOARD_N - 1));
  }
  __syncthreads();

  // ---- encoder: h = w_enc @ obs + b_enc, straight into fragment slots ------
  // slot (mt, c, j):  env = 16*mt + lane/4 + 8*((j>>1)&1)
  //                   hid = 16*(2*warp+c) + 2*(lane&3) + (j&1) + 8*(j>>2)
  {
    float bb[2][4], we[2][4][4];
#pragma unroll
    for (int c = 0; c < 2; ++c) {
      const int hbase = 16 * (2 * warp + c) + 2 * (lane & 3);
#pragma unroll
      for (int q = 0; q < 4; ++q) {  // q = (j&1) + 2*(j>>2)
        const int hid = hbase + (q & 1) + 8 * (q >> 1);
        bb[c][q] = b_enc[hid];
        const float4 wv = *(const float4*)(w_enc + 4 * hid);
        we[c][q][0] = wv.x;
        we[c][q][1] = wv.y;
        we[c][q][2] = wv.z;
        we[c][q][3] = wv.w;
      }
    }
#pragma unroll
    for (int mt = 0; mt < NMT; ++mt) {
      const int el = 16 * mt + (lane >> 2);
      const float4 o0 = *(const float4*)(sobs + 4 * el);
      const float4 o1 = *(const float4*)(sobs + 4 * (el + 8));
#pragma unroll
      for (int c = 0; c < 2; ++c) {
        float hv[8];
#pragma unroll
        for (int j = 0; j < 8; ++j) {
          const int q = (j & 1) + 2 * (j >> 2);
          const float4 ob = ((j >> 1) & 1) ? o1 : o0;
          float v = bb[c][q];
          v = fmaf(we[c][q][0], ob.x, v);
          v = fmaf(we[c][q][1], ob.y, v);
          v = fmaf(we[c][q][2], ob.z, v);
          v = fmaf(we[c][q][3], ob.w, v);
          hv[j] = v;
        }
        // publish straight into hA + hcs while the values are still in registers
        const size_t sb = HSLOT(mt, 2 * warp + c);
        __half2 pk[4];
#pragma unroll
        for (int q = 0; q < 4; ++q) pk[q] = __floats2half2_rn(hv[2 * q], hv[2 * q + 1]);
        *(uint4*)(hA + sb) = *(const uint4*)pk;
        *(float4*)(hcs + sb) = make_float4(hv[0], hv[1], hv[2], hv[3]);
        *(float4*)(hcs + sb + 4) = make_float4(hv[4], hv[5], hv[6], hv[7]);
      }
    }
  }
  __syncthreads();

  // ---- 3 MinGRU layers ----------------------------------------------------
  for (int layer = 0; layer < NLAY_N; ++layer) {
    float* stl = statebuf + (size_t)(layer * nblk + blk) * (BENV * HID_N);
#if GM_CUNROLL == 1
#pragma unroll 1
#else
#pragma unroll 2
#endif
    for (int c = 0; c < 2; ++c) {
      const int g = 2 * warp + c;
      float acc[NMT][6][4];
#pragma unroll
      for (int mt = 0; mt < NMT; ++mt)
#pragma unroll
        for (int nt = 0; nt < 6; ++nt)
#pragma unroll
          for (int q = 0; q < 4; ++q) acc[mt][nt][q] = 0.0f;

      const __half* Wg = Wsw + ((size_t)layer * 96 + 6 * g) * (8 * 32 * 8) + lane * 8;

      // unroll 2: 96 mma per iteration keeps the TC pipe fed across the load
      // batch without spilling (unroll 4 spills at BENV=64 and measures slower)
#pragma unroll 2
      for (int kp = 0; kp < 8; ++kp) {
        // one LDG.128 per n-tile covers k-tiles 2kp (x,y) and 2kp+1 (z,w)
        uint4 Bv[6];
#pragma unroll
        for (int nt = 0; nt < 6; ++nt)
          Bv[nt] = *(const uint4*)(Wg + (size_t)nt * 2048 + (GM_ABL_B ? 0 : kp * 256));
#pragma unroll
        for (int sh = 0; sh < 2; ++sh) {
          uint32_t A[NMT][4];
#pragma unroll
          for (int mt = 0; mt < NMT; ++mt) {
            const int akt = GM_ABL_A ? 0 : 2 * kp + sh;
            const uint4 v = *(const uint4*)(hA + ((mt * 16 + akt) * 32 + lane) * 8);
            A[mt][0] = v.x;
            A[mt][1] = v.y;
            A[mt][2] = v.z;
            A[mt][3] = v.w;
          }
          uint32_t B[6][2];
#pragma unroll
          for (int nt = 0; nt < 6; ++nt) {
            B[nt][0] = sh ? Bv[nt].z : Bv[nt].x;
            B[nt][1] = sh ? Bv[nt].w : Bv[nt].y;
          }
#pragma unroll
          for (int mt = 0; mt < NMT; ++mt)
#pragma unroll
            for (int nt = 0; nt < 6; ++nt) mma16816(acc[mt][nt], A[mt], B[nt]);
        }
      }

      // ---- MinGRU epilogue -------------------------------------------------
      mingru_epi<NMT>(acc, hcs, stl, g, lane, t);
    }
    __syncthreads();  // everybody done reading hA for this layer
#pragma unroll
    for (int mt = 0; mt < NMT; ++mt) {
#pragma unroll
      for (int c = 0; c < 2; ++c) {
        const size_t sb = HSLOT(mt, 2 * warp + c);
        const float4 h0 = *(const float4*)(hcs + sb);
        const float4 h1 = *(const float4*)(hcs + sb + 4);
        __half2 pk[4] = {__floats2half2_rn(h0.x, h0.y), __floats2half2_rn(h0.z, h0.w),
                         __floats2half2_rn(h1.x, h1.y), __floats2half2_rn(h1.z, h1.w)};
        *(uint4*)(hA + sb) = *(const uint4*)pk;
      }
    }
    __syncthreads();
  }

  // ---- action head in fp32 ------------------------------------------------
  {
    float part[NMT][2][NACT_N];
#pragma unroll
    for (int mt = 0; mt < NMT; ++mt)
#pragma unroll
      for (int e2 = 0; e2 < 2; ++e2)
#pragma unroll
        for (int a = 0; a < NACT_N; ++a) part[mt][e2][a] = 0.0f;

#pragma unroll
    for (int c = 0; c < 2; ++c) {
      const int hbase = 16 * (2 * warp + c) + 2 * (lane & 3);
#pragma unroll
      for (int j = 0; j < 8; ++j) {
        const int hid = hbase + (j & 1) + 8 * (j >> 2);
        const int e2 = (j >> 1) & 1;
        float wa[NACT_N];
#pragma unroll
        for (int a = 0; a < NACT_N; ++a) wa[a] = sw_a[a * HID_N + hid];
#pragma unroll
        for (int mt = 0; mt < NMT; ++mt) {
          const float hv = hcs[HSLOT(mt, 2 * warp + c) + j];
#pragma unroll
          for (int a = 0; a < NACT_N; ++a) part[mt][e2][a] = fmaf(wa[a], hv, part[mt][e2][a]);
        }
      }
    }
    // reduce over the 4 lanes that share (env) but hold different hidden
#pragma unroll
    for (int mt = 0; mt < NMT; ++mt)
#pragma unroll
      for (int e2 = 0; e2 < 2; ++e2)
#pragma unroll
        for (int a = 0; a < NACT_N; ++a) {
          float v = part[mt][e2][a];
          v += __shfl_xor_sync(0xffffffffu, v, 1);
          v += __shfl_xor_sync(0xffffffffu, v, 2);
          part[mt][e2][a] = v;
        }
    if ((lane & 3) == 0) {
#pragma unroll
      for (int mt = 0; mt < NMT; ++mt)
#pragma unroll
        for (int e2 = 0; e2 < 2; ++e2) {
          const int el = 16 * mt + (lane >> 2) + 8 * e2;
          float* dst = slog + (size_t)(warp * BENV + el) * NACT_N;
#pragma unroll
          for (int a = 0; a < NACT_N; ++a) dst[a] = part[mt][e2][a];
        }
    }
  }
  __syncthreads();
  {
    const int a = tid & 3;
    // 256 threads cover 64 envs per pass; BENV > 64 needs more than one pass
    for (int el = tid >> 2; el < BENV; el += 64) {
      float s = 0.0f;
#pragma unroll
      for (int w = 0; w < NWARP; ++w) s += slog[(size_t)(w * BENV + el) * NACT_N + a];
      slgt[el * NACT_N + a] = s + b_a[a];
    }
  }
  __syncthreads();

  // ---- greedy action + env transition ------------------------------------
  int myhit = 0;
  if (tid < BENV) {
    const int e = env0 + tid;
    const bool valid = (e < nenv);
    const float4 lg = *(const float4*)(slgt + tid * NACT_N);
    float best = lg.x;
    int act = 0;
    if (lg.y > best) { best = lg.y; act = 1; }
    if (lg.z > best) { best = lg.z; act = 2; }
    if (lg.w > best) { best = lg.w; act = 3; }

    int ax = sag[2 * tid], ay = sag[2 * tid + 1];
    if (act == 0) ay -= 1;
    else if (act == 1) ay += 1;
    else if (act == 2) ax -= 1;
    else ax += 1;
    ax = min(max(ax, 0), BOARD_N - 1);
    ay = min(max(ay, 0), BOARD_N - 1);
    const int hit = (ax == sfd[2 * tid] && ay == sfd[2 * tid + 1]) ? 1 : 0;
    if (valid) {
      agentbuf[2 * e] = ax;
      agentbuf[2 * e + 1] = ay;
      if (hit) rewbuf[e] += 1.0f;
      if (is_last) {
        *(float4*)(lastlogits + 4 * e) = lg;
        posout[2 * e] = (long long)ax;
        posout[2 * e + 1] = (long long)ay;
      }
      myhit = hit;
    }
  }
  const unsigned mask = __ballot_sync(0xffffffffu, myhit != 0);
  if (lane == 0 && mask) atomicOr(&hitflags[t], 1);
}

// ---------------------------------------------------------------------------
// SM90a warpgroup-MMA rollout step (BENV = 64).
//
// The mma.sync path above tops out at ~1800 FLOP/cycle/SM because HMMA.16816
// reads 10 registers per lane per instruction and starves on register-file
// bandwidth.  wgmma.mma_async reads B straight out of shared memory and
// measures 4085 FLOP/cycle/SM -- 99.7% of the SM90 fp16 peak -- so the gate
// matmul floor drops from 41.9k to 18.4k cycles per 64-env block step.
//
// Decomposition: 256 threads = 2 warpgroups; each warpgroup owns 384 of the 768
// permuted gate columns (2 chunks of N=192) for all 64 envs, and each of its 4
// warps owns one 16-env m-tile (the wgmma m64 A-fragment layout is exactly the
// mma.m16n8k16 one, tiled by warp), so the existing hA fragment layout, the
// state layout and the "h_new lands where the next layer reads it" swizzle all
// carry over unchanged.  B tiles stream global -> shared with cp.async through
// an 8-deep ring, 5 tiles in flight; the tile counter runs over all
// (layer, chunk, k-tile) triples so the pipeline never drains mid-step.
// ---------------------------------------------------------------------------
#define WG_NSTAGE 8
#define WG_PF 5
#define WG_TILE 3072  /* halves per (chunk, k-tile) B tile = 192*16 */

// B-operand matrix descriptor: no swizzle, k-major (trans_b = 0), core matrices
// of 8 n-rows x 16 B, LBO = 128 (k-block stride), SBO = 256 (n-block stride).
__device__ __forceinline__ uint64_t wg_desc(uint32_t sa) {
  uint64_t d = (uint64_t)((sa & 0x3FFFFu) >> 4);
  d |= (uint64_t)(128u >> 4) << 16;
  d |= (uint64_t)(256u >> 4) << 32;
  return d;
}
__device__ __forceinline__ void cp_async16(uint32_t dst, const void* src) {
  asm volatile("cp.async.cg.shared.global [%0], [%1], 16;\n" ::"r"(dst), "l"(src) : "memory");
}
__device__ __forceinline__ void cp_commit() { asm volatile("cp.async.commit_group;\n" ::: "memory"); }
template <int N>
__device__ __forceinline__ void cp_wait() {
  asm volatile("cp.async.wait_group %0;\n" ::"n"(N) : "memory");
}
// wgmma.fence orders prior register accesses by the warpgroup against the
// accumulator/A registers a following wgmma reads.  Emitting it explicitly is
// mandatory for performance: without it ptxas injects its own warpgroup.arrive
// *and* a full wgmma.wait_group 0 drain after every single wgmma, which
// serializes the async pipeline and destroys any epilogue overlap.
__device__ __forceinline__ void wg_fence() {
  asm volatile("wgmma.fence.sync.aligned;\n" ::: "memory");
}
// wgmma reads its shared operands through the *async* proxy, so ordinary STS
// writes to hA are not guaranteed visible to it by __syncthreads() alone --
// that only orders the generic proxy.  Without this fence the A tile a wgmma
// reads can be the previous layer's h; the resulting error is tiny (the
// highway keeps consecutive layers' h close) but real, and it made the kernel
// nondeterministic run to run.  fence.proxy.async.shared::cta is the generic
// -> async proxy release; it must come after the stores and before the
// __syncthreads() that publishes them.
__device__ __forceinline__ void wg_proxy_fence() {
  asm volatile("fence.proxy.async.shared::cta;\n" ::: "memory");
}
// CUTLASS' warpgroup_fence_operand: a zero-instruction tie on the accumulator
// registers so the compiler cannot sink non-wgmma accesses across the fence.
template <int N>
__device__ __forceinline__ void wg_tie(float* d) {
#pragma unroll
  for (int i = 0; i < N; ++i) asm volatile("" : "+f"(d[i]));
}
// Same tie for the 4 A-fragment registers.  wgmma reads A *asynchronously*, so
// the registers must stay untouched until the group retires; ptxas only sees a
// plain "r" input and would happily reuse them for the next k-tile's LDS
// destination.  Placing this tie after the wait that retires the group extends
// their live range far enough to stop that.
__device__ __forceinline__ void wg_tie4(uint32_t* a) {
#pragma unroll
  for (int i = 0; i < 4; ++i) asm volatile("" : "+r"(a[i]));
}

__device__ __forceinline__ void wg_commit() {
  asm volatile("wgmma.commit_group.sync.aligned;\n" ::: "memory");
}
template <int N>
__device__ __forceinline__ void wg_wait() {
  asm volatile("wgmma.wait_group.sync.aligned %0;\n" ::"n"(N) : "memory");
}
// One wgmma.mma_async m64n192k16: C[64][192] += A[64][16] @ B[16][192].
// A comes from registers (4 regs = the m16n8k16 A-fragment of the warp's own
// m-tile), B from shared through a 64-bit matrix descriptor.  SCALED=0 zeroes
// the accumulator instead of accumulating (used on the first k-tile).
template <int SCALED>
__device__ __forceinline__ void wgmma_n192(float* d, const uint32_t* a, uint64_t desc) {
  asm volatile(
      "wgmma.mma_async.sync.aligned.m64n192k16.f32.f16.f16 "
      "{%0,%1,%2,%3,%4,%5,%6,%7,%8,%9,%10,%11,%12,%13,%14,%15,%16,%17,%18,%19,%20,%21,%22,%23,%24,"
      "%25,%26,%27,%28,%29,%30,%31,%32,%33,%34,%35,%36,%37,%38,%39,%40,%41,%42,%43,%44,%45,%46,%47,"
      "%48,%49,%50,%51,%52,%53,%54,%55,%56,%57,%58,%59,%60,%61,%62,%63,%64,%65,%66,%67,%68,%69,%70,"
      "%71,%72,%73,%74,%75,%76,%77,%78,%79,%80,%81,%82,%83,%84,%85,%86,%87,%88,%89,%90,%91,%92,%93,"
      "%94,%95}, "
      "{%96,%97,%98,%99}, %100, %101, 1, 1, 0;\n"
      : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3]), "+f"(d[4]), "+f"(d[5]), "+f"(d[6]), "+f"(d[7]), "+f"(d[8]), "+f"(d[9]), "+f"(d[10]), "+f"(d[11]), "+f"(d[12]), "+f"(d[13]), "+f"(d[14]), "+f"(d[15]), "+f"(d[16]), "+f"(d[17]), "+f"(d[18]), "+f"(d[19]), "+f"(d[20]), "+f"(d[21]), "+f"(d[22]), "+f"(d[23]), "+f"(d[24]), "+f"(d[25]), "+f"(d[26]), "+f"(d[27]), "+f"(d[28]), "+f"(d[29]), "+f"(d[30]), "+f"(d[31]), "+f"(d[32]), "+f"(d[33]), "+f"(d[34]), "+f"(d[35]), "+f"(d[36]), "+f"(d[37]), "+f"(d[38]), "+f"(d[39]), "+f"(d[40]), "+f"(d[41]), "+f"(d[42]), "+f"(d[43]), "+f"(d[44]), "+f"(d[45]), "+f"(d[46]), "+f"(d[47]), "+f"(d[48]), "+f"(d[49]), "+f"(d[50]), "+f"(d[51]), "+f"(d[52]), "+f"(d[53]), "+f"(d[54]), "+f"(d[55]), "+f"(d[56]), "+f"(d[57]), "+f"(d[58]), "+f"(d[59]), "+f"(d[60]), "+f"(d[61]), "+f"(d[62]), "+f"(d[63]), "+f"(d[64]), "+f"(d[65]), "+f"(d[66]), "+f"(d[67]), "+f"(d[68]), "+f"(d[69]), "+f"(d[70]), "+f"(d[71]), "+f"(d[72]), "+f"(d[73]), "+f"(d[74]), "+f"(d[75]), "+f"(d[76]), "+f"(d[77]), "+f"(d[78]), "+f"(d[79]), "+f"(d[80]), "+f"(d[81]), "+f"(d[82]), "+f"(d[83]), "+f"(d[84]), "+f"(d[85]), "+f"(d[86]), "+f"(d[87]), "+f"(d[88]), "+f"(d[89]), "+f"(d[90]), "+f"(d[91]), "+f"(d[92]), "+f"(d[93]), "+f"(d[94]), "+f"(d[95])
      : "r"(a[0]), "r"(a[1]), "r"(a[2]), "r"(a[3]), "l"(desc), "n"(SCALED));
}

// Stage one 6 KB B tile (tau = layer*32 + chunk*16 + ktile, per warpgroup) into
// ring slot tau % WG_NSTAGE.  128 threads x 3 x 16 B, fully coalesced.
__device__ __forceinline__ void wg_issue(uint32_t sBw, const __half* __restrict__ Wb, int wgi,
                                         int tau, int wtid) {
  const __half* src = Wb + (size_t)(tau >> 5) * (4 * 16 * WG_TILE) +
                      (size_t)wgi * (2 * 16 * WG_TILE) + (size_t)(tau & 31) * WG_TILE + wtid * 8;
  const uint32_t dst = sBw + (uint32_t)((tau & (WG_NSTAGE - 1)) * (2 * WG_TILE) + wtid * 16);
  cp_async16(dst, src);
  cp_async16(dst + 2048, src + 1024);
  cp_async16(dst + 4096, src + 2048);
}

// MinGRU highway epilogue over the 4 hidden groups of one N=192 chunk.
template <int CC>
__device__ __forceinline__ void mingru_epi_wg(const float* __restrict__ acc,
                                              float (&hcur)[2][4][8], float* __restrict__ stl,
                                              int w, int ch, int lane, int t) {
#pragma unroll
  for (int gg = 0; gg < 4; ++gg) {
    const size_t sb = (size_t)((w * 16 + 4 * ch + gg) * 32 + lane) * 8;
    float st[8];
    if (t == 0) {
#pragma unroll
      for (int j = 0; j < 8; ++j) st[j] = 0.0f;
    } else {
      const float4 s0 = *(const float4*)(stl + sb);
      const float4 s1 = *(const float4*)(stl + sb + 4);
      st[0] = s0.x; st[1] = s0.y; st[2] = s0.z; st[3] = s0.w;
      st[4] = s1.x; st[5] = s1.y; st[6] = s1.z; st[7] = s1.w;
    }
    float o[8];
#pragma unroll
    for (int j = 0; j < 8; ++j) {
      const int nh = j >> 2, q = j & 3;
      const float zh = acc[4 * (6 * gg + 0 + nh) + q];
      const float zg = acc[4 * (6 * gg + 2 + nh) + q];
      const float zp = acc[4 * (6 * gg + 4 + nh) + q];
      const float sg = fsig(zg);
      const float ov = fmaf(sg, ftanh(zh) - st[j], st[j]);
      const float pp = fsig(zp);
      o[j] = ov;
      hcur[CC][gg][j] = fmaf(pp, ov, (1.0f - pp) * hcur[CC][gg][j]);
    }
    *(float4*)(stl + sb) = make_float4(o[0], o[1], o[2], o[3]);
    *(float4*)(stl + sb + 4) = make_float4(o[4], o[5], o[6], o[7]);
  }
}

template <int BENV>
__global__ __launch_bounds__(256, 1) void step_kernel_wg(
    const __half* __restrict__ Wb,       // [3][4][16][3072] wgmma B tiles
    const float* __restrict__ w_enc,     // [256][4]
    const float* __restrict__ b_enc,     // [256]
    const float* __restrict__ w_a,       // [4][256]
    const float* __restrict__ b_a,       // [4]
    float* __restrict__ statebuf,        // [3][nblk][BENV*256]
    int* __restrict__ agentbuf, int* __restrict__ foodbuf,
    unsigned long long* __restrict__ rngbuf, float* __restrict__ rewbuf,
    float* __restrict__ lastlogits, long long* __restrict__ posout, int* __restrict__ hitflags,
    int t, int nenv, int nblk, int is_last) {
  static_assert(BENV == 64, "wgmma path is m64-granular");
  const int tid = threadIdx.x;
  const int wgi = tid >> 7;       // warpgroup
  const int w = (tid >> 5) & 3;   // m-tile within the warpgroup
  const int lane = tid & 31;
  const int wtid = tid & 127;
  const int blk = blockIdx.x;
  const int env0 = blk * BENV;

  extern __shared__ char smem_raw[];
  __half* sB = (__half*)smem_raw;
  __half* hA = sB + 2 * WG_NSTAGE * WG_TILE;
  float* sw_a = (float*)(hA + BENV * HID_N);   // [hid][4], transposed
  float* sw_e = sw_a + NACT_N * HID_N;         // [hid][4]
  float* sb_e = sw_e + HID_N * 4;              // [hid]
  float* sobs = sb_e + HID_N;
  int* sag = (int*)(sobs + BENV * 4);
  int* sfd = sag + BENV * 2;
  float* slog = (float*)(sfd + BENV * 2);      // [2][BENV][NACT]
  float* slgt = slog + 2 * BENV * NACT_N;

  // ---- fill the B pipeline first: 5 tiles (30 KB) in flight across the env
  // prologue and the encoder, which is enough to cover the L2 latency.
  const uint32_t sBw = (uint32_t)__cvta_generic_to_shared(sB) + (uint32_t)(wgi * WG_NSTAGE * 2 * WG_TILE);
  const uint64_t dbase = wg_desc(sBw);
#pragma unroll
  for (int i = 0; i < WG_PF; ++i) {
    wg_issue(sBw, Wb, wgi, i, wtid);
    cp_commit();
  }

  for (int i = tid; i < NACT_N * HID_N; i += 256) sw_a[(i % HID_N) * NACT_N + i / HID_N] = w_a[i];
  for (int i = tid; i < HID_N * 4; i += 256) sw_e[i] = w_enc[i];
  for (int i = tid; i < HID_N; i += 256) sb_e[i] = b_enc[i];

  // ---- env prologue: deferred food/rng update + obs ------------------------
  if (tid < BENV) {
    const int e = env0 + tid;
    const bool valid = (e < nenv);
    int ax = 0, ay = 0, fx = 0, fy = 0;
    if (valid) {
      ax = agentbuf[2 * e];
      ay = agentbuf[2 * e + 1];
      fx = foodbuf[2 * e];
      fy = foodbuf[2 * e + 1];
    }
    if (t > 0 && hitflags[t - 1]) {
      unsigned long long r = valid ? rngbuf[e] : 0ULL;
      r = lcg(r);
      const int nfx = (int)(r % (unsigned long long)BOARD_N);
      r = lcg(r);
      const int nfy = (int)(r % (unsigned long long)BOARD_N);
      if (ax == fx && ay == fy) {
        fx = nfx;
        fy = nfy;
      }
      if (valid) {
        rngbuf[e] = r;
        foodbuf[2 * e] = fx;
        foodbuf[2 * e + 1] = fy;
      }
    }
    sag[2 * tid] = ax;
    sag[2 * tid + 1] = ay;
    sfd[2 * tid] = fx;
    sfd[2 * tid + 1] = fy;
    sobs[4 * tid + 0] = (float)(fx - ax) * (1.0f / (float)BOARD_N);
    sobs[4 * tid + 1] = (float)(fy - ay) * (1.0f / (float)BOARD_N);
    sobs[4 * tid + 2] = (float)ax * (1.0f / (float)(BOARD_N - 1));
    sobs[4 * tid + 3] = (float)ay * (1.0f / (float)(BOARD_N - 1));
  }
  __syncthreads();

  // ---- encoder straight into fragment slots -------------------------------
  //   slot (cc, gg, j): env = 16*w + lane/4 + 8*((j>>1)&1)
  //                     hid = 16*(4*(2*wgi+cc)+gg) + 2*(lane&3) + (j&1) + 8*(j>>2)
  float hcur[2][4][8];
  {
    const int el = 16 * w + (lane >> 2);
    const float4 o0 = *(const float4*)(sobs + 4 * el);
    const float4 o1 = *(const float4*)(sobs + 4 * (el + 8));
#pragma unroll
    for (int cc = 0; cc < 2; ++cc)
#pragma unroll
      for (int gg = 0; gg < 4; ++gg) {
        const int hbase = 16 * (4 * (2 * wgi + cc) + gg) + 2 * (lane & 3);
#pragma unroll
        for (int q = 0; q < 4; ++q) {
          const int hid = hbase + (q & 1) + 8 * (q >> 1);
          const float4 we = *(const float4*)(sw_e + 4 * hid);
          const float bb = sb_e[hid];
#pragma unroll
          for (int e2 = 0; e2 < 2; ++e2) {
            const float4 ob = e2 ? o1 : o0;
            float v = bb;
            v = fmaf(we.x, ob.x, v);
            v = fmaf(we.y, ob.y, v);
            v = fmaf(we.z, ob.z, v);
            v = fmaf(we.w, ob.w, v);
            hcur[cc][gg][(q & 1) + 2 * e2 + 4 * (q >> 1)] = v;
          }
        }
      }
  }
#pragma unroll
  for (int cc = 0; cc < 2; ++cc)
#pragma unroll
    for (int gg = 0; gg < 4; ++gg) {
      __half2 pk[4];
#pragma unroll
      for (int q = 0; q < 4; ++q)
        pk[q] = __floats2half2_rn(hcur[cc][gg][2 * q], hcur[cc][gg][2 * q + 1]);
      *(uint4*)(hA + ((w * 16 + 4 * (2 * wgi + cc) + gg) * 32 + lane) * 8) = *(const uint4*)pk;
    }
  __syncthreads();

  // ---- 3 MinGRU layers ----------------------------------------------------
// Async pipeline depths, overridable to bisect the >1-wave mismatch: GM_WGW is
// how many wgmma groups may stay in flight, GM_CPW how many cp.async groups.
#ifndef GM_WGW
#define GM_WGW (WG_NSTAGE - WG_PF - 1)
#endif
#ifndef GM_CPW
#define GM_CPW WG_PF
#endif
// GM_WGFIX picks how the A-register WAR hazard above is closed.  0 = as
// originally written (fast but racy past one wave: nondeterministic rewards and
// up to 1.6e-1 logit error at 65536 envs).  1 = spec-mandated wgmma.fence
// between the LDS that writes A and the wgmma that reads it.  2 = rotate over
// GM_WGW+1 A-register sets and tie each one until its group has retired.
// 3 = both.
//
// Measured at 65536 envs (wg path, Msps, bit-exactness vs the mma path):
//   0: 167.7  nondeterministic, NaN logits    1: 145.8  still nondeterministic
//   2: 151.2  exact, deterministic            3: 144.2  exact, fence costs 5%
// So the rotation is what actually closes it -- the fence alone does not, which
// makes sense: the fence orders the *access*, it does not stop ptxas from
// recycling the register.  2 is the default; 12.5% faster than the best mma
// config at this size.
#ifndef GM_WGFIX
#define GM_WGFIX 2
#endif
#define NASET (GM_WGW + 1)
#if GM_WGFIX & 1
#define WG_AFENCE() wg_fence()
#else
#define WG_AFENCE() ((void)0)
#endif

// SLOT must be a compile-time constant so aaR stays in registers.
#define WG_KSTEP_S(SD, KT, SLOT)                                                          \
  do {                                                                                    \
    wg_wait<GM_WGW>();                                                                     \
    if ((KT) >= NASET) wg_tie4(aaR[SLOT]);                                                 \
    if (tau + WG_PF < 96) wg_issue(sBw, Wb, wgi, tau + WG_PF, wtid);                        \
    cp_commit();                                                                           \
    cp_wait<GM_CPW>();                                                                     \
    const uint4 av = *(const uint4*)(hA + (size_t)(((w * 16 + (KT)) * 32 + lane) * 8));     \
    aaR[SLOT][0] = av.x;                                                                   \
    aaR[SLOT][1] = av.y;                                                                   \
    aaR[SLOT][2] = av.z;                                                                   \
    aaR[SLOT][3] = av.w;                                                                   \
    WG_AFENCE();                                                                           \
    wgmma_n192<SD>(acc, aaR[SLOT],                                                         \
                   dbase + (uint64_t)((tau & (WG_NSTAGE - 1)) * (2 * WG_TILE / 16)));       \
    wg_commit();                                                                            \
    ++tau;                                                                                  \
  } while (0)

#define WG_KSTEP(SD, KT)                                                                  \
  do {                                                                                    \
    wg_wait<GM_WGW>();                                                                     \
    if (tau + WG_PF < 96) wg_issue(sBw, Wb, wgi, tau + WG_PF, wtid);                        \
    cp_commit();                                                                           \
    cp_wait<GM_CPW>();                                                                     \
    const uint4 av = *(const uint4*)(hA + (size_t)(((w * 16 + (KT)) * 32 + lane) * 8));     \
    const uint32_t aa[4] = {av.x, av.y, av.z, av.w};                                        \
    WG_AFENCE();                                                                            \
    wgmma_n192<SD>(acc, aa, dbase + (uint64_t)((tau & (WG_NSTAGE - 1)) * (2 * WG_TILE / 16))); \
    wg_commit();                                                                            \
    ++tau;                                                                                  \
  } while (0)

  int tau = 0;
#if GM_WGFIX & 2
  uint32_t aaR[NASET][4];
#endif
  for (int layer = 0; layer < NLAY_N; ++layer) {
    float* stl = statebuf + (size_t)(layer * nblk + blk) * (BENV * HID_N);
#pragma unroll 1
    for (int cc = 0; cc < 2; ++cc) {
      float acc[96];
      // Each chunk starts with every group drained (wg_wait<0> below), so the
      // A-set rotation can restart from slot 0 regardless of tau's phase.
#if GM_WGFIX & 2
      static_assert(NASET == 3, "kt unroll below assumes 3 A-register sets");
      WG_KSTEP_S(0, 0, 0);
#pragma unroll
      for (int k3 = 0; k3 < 5; ++k3) {
        const int kt = 1 + 3 * k3;
        WG_KSTEP_S(1, kt + 0, 1);
        WG_KSTEP_S(1, kt + 1, 2);
        WG_KSTEP_S(1, kt + 2, 0);
      }
#else
      WG_KSTEP(0, 0);
      for (int kt = 1; kt < 16; ++kt) WG_KSTEP(1, kt);
#endif
      wg_wait<0>();
      if (cc == 0) mingru_epi_wg<0>(acc, hcur, stl, w, 2 * wgi + 0, lane, t);
      else mingru_epi_wg<1>(acc, hcur, stl, w, 2 * wgi + 1, lane, t);
    }
    __syncthreads();  // everybody done reading hA for this layer
#pragma unroll
    for (int cc = 0; cc < 2; ++cc)
#pragma unroll
      for (int gg = 0; gg < 4; ++gg) {
        __half2 pk[4];
#pragma unroll
        for (int q = 0; q < 4; ++q)
          pk[q] = __floats2half2_rn(hcur[cc][gg][2 * q], hcur[cc][gg][2 * q + 1]);
        *(uint4*)(hA + ((w * 16 + 4 * (2 * wgi + cc) + gg) * 32 + lane) * 8) = *(const uint4*)pk;
      }
    __syncthreads();
  }
#undef WG_KSTEP

  // ---- action head in fp32 ------------------------------------------------
  {
    float part[2][NACT_N];
#pragma unroll
    for (int e2 = 0; e2 < 2; ++e2)
#pragma unroll
      for (int a = 0; a < NACT_N; ++a) part[e2][a] = 0.0f;
#pragma unroll
    for (int cc = 0; cc < 2; ++cc)
#pragma unroll
      for (int gg = 0; gg < 4; ++gg) {
        const int hbase = 16 * (4 * (2 * wgi + cc) + gg) + 2 * (lane & 3);
#pragma unroll
        for (int j = 0; j < 8; ++j) {
          const int hid = hbase + (j & 1) + 8 * (j >> 2);
          const int e2 = (j >> 1) & 1;
          const float4 wa = *(const float4*)(sw_a + NACT_N * hid);
          const float hv = hcur[cc][gg][j];
          part[e2][0] = fmaf(wa.x, hv, part[e2][0]);
          part[e2][1] = fmaf(wa.y, hv, part[e2][1]);
          part[e2][2] = fmaf(wa.z, hv, part[e2][2]);
          part[e2][3] = fmaf(wa.w, hv, part[e2][3]);
        }
      }
#pragma unroll
    for (int e2 = 0; e2 < 2; ++e2)
#pragma unroll
      for (int a = 0; a < NACT_N; ++a) {
        float v = part[e2][a];
        v += __shfl_xor_sync(0xffffffffu, v, 1);
        v += __shfl_xor_sync(0xffffffffu, v, 2);
        part[e2][a] = v;
      }
    if ((lane & 3) == 0) {
#pragma unroll
      for (int e2 = 0; e2 < 2; ++e2) {
        const int el = 16 * w + (lane >> 2) + 8 * e2;
        float* dst = slog + (size_t)(wgi * BENV + el) * NACT_N;
#pragma unroll
        for (int a = 0; a < NACT_N; ++a) dst[a] = part[e2][a];
      }
    }
  }
  __syncthreads();
  {
    const int el = tid >> 2, a = tid & 3;
    if (el < BENV)
      slgt[el * NACT_N + a] = slog[(size_t)el * NACT_N + a] +
                              slog[(size_t)(BENV + el) * NACT_N + a] + b_a[a];
  }
  __syncthreads();

  // ---- greedy action + env transition ------------------------------------
  int myhit = 0;
  if (tid < BENV) {
    const int e = env0 + tid;
    const bool valid = (e < nenv);
    const float4 lg = *(const float4*)(slgt + tid * NACT_N);
    float best = lg.x;
    int act = 0;
    if (lg.y > best) { best = lg.y; act = 1; }
    if (lg.z > best) { best = lg.z; act = 2; }
    if (lg.w > best) { best = lg.w; act = 3; }

    int ax = sag[2 * tid], ay = sag[2 * tid + 1];
    if (act == 0) ay -= 1;
    else if (act == 1) ay += 1;
    else if (act == 2) ax -= 1;
    else ax += 1;
    ax = min(max(ax, 0), BOARD_N - 1);
    ay = min(max(ay, 0), BOARD_N - 1);
    const int hit = (ax == sfd[2 * tid] && ay == sfd[2 * tid + 1]) ? 1 : 0;
    if (valid) {
      agentbuf[2 * e] = ax;
      agentbuf[2 * e + 1] = ay;
      if (hit) rewbuf[e] += 1.0f;
      if (is_last) {
        *(float4*)(lastlogits + 4 * e) = lg;
        posout[2 * e] = (long long)ax;
        posout[2 * e + 1] = (long long)ay;
      }
      myhit = hit;
    }
  }
  const unsigned mask = __ballot_sync(0xffffffffu, myhit != 0);
  if (lane == 0 && mask) atomicOr(&hitflags[t], 1);
}

// Repack the gate weights into wgmma B tiles: Wb[layer][chunk][ktile] holds a
// 192(n) x 16(k) tile as 24 core matrices of 8 n-rows x 16 B, i.e.
// half offset = (n/8)*128 + (k/8)*64 + (n%8)*8 + (k%8).  n is the permuted gate
// column within the chunk (chunk = 192 columns = 4 [zh|zg|zp] hidden groups).
__global__ void prep_wb_kernel(const float* __restrict__ wg, __half* __restrict__ out) {
  int idx = blockIdx.x * blockDim.x + threadIdx.x;
  if (idx >= NLAY_N * 4 * 16 * 192 * 2) return;
  const int khi = idx & 1;
  int r = idx >> 1;
  const int n = r % 192;
  r /= 192;
  const int kt = r & 15;
  r >>= 4;
  const int ch = r & 3;
  const int layer = r >> 2;

  const int p = 192 * ch + n;
  const int g = p / 48, rr = p - 48 * g;
  const int row = (rr >> 4) * HID_N + (16 * g + (rr & 15));
  const float* src = wg + (size_t)layer * NG_N * HID_N + (size_t)row * HID_N + 16 * kt + 8 * khi;
  __half h8[8];
#pragma unroll
  for (int j = 0; j < 8; ++j) h8[j] = __float2half_rn(src[j]);
  __half* dst = out + ((size_t)(layer * 4 + ch) * 16 + kt) * WG_TILE + (n >> 3) * 128 + khi * 64 +
                (n & 7) * 8;
  *(uint4*)dst = *(const uint4*)h8;
}

// ---------------------------------------------------------------------------
// Reference-accurate single-step policy_forward (one block per env).
// ---------------------------------------------------------------------------
__global__ __launch_bounds__(256) void pf_kernel(
    const float* __restrict__ obs, const float* __restrict__ state,
    const float* __restrict__ w_enc, const float* __restrict__ b_enc,
    const float* __restrict__ w_gru, const float* __restrict__ w_a,
    const float* __restrict__ b_a, const float* __restrict__ w_v,
    const float* __restrict__ b_v, float* __restrict__ logits,
    float* __restrict__ newstate, float* __restrict__ value, int N) {
  const int e = blockIdx.x;
  if (e >= N) return;
  const int j = threadIdx.x;
  __shared__ float sh[HID_N];
  __shared__ float red[256];

  const float4 ob = *(const float4*)(obs + 4 * e);
  const float4 wv = *(const float4*)(w_enc + 4 * j);
  float h = b_enc[j];
  h = fmaf(wv.x, ob.x, h);
  h = fmaf(wv.y, ob.y, h);
  h = fmaf(wv.z, ob.z, h);
  h = fmaf(wv.w, ob.w, h);
  sh[j] = h;
  __syncthreads();

  for (int l = 0; l < NLAY_N; ++l) {
    const float* W = w_gru + (size_t)l * NG_N * HID_N;
    const float* wh = W + (size_t)j * HID_N;
    const float* wgt = W + (size_t)(HID_N + j) * HID_N;
    const float* wp = W + (size_t)(2 * HID_N + j) * HID_N;
    float ah[4] = {0, 0, 0, 0}, ag[4] = {0, 0, 0, 0}, ap[4] = {0, 0, 0, 0};
#pragma unroll 4
    for (int k = 0; k < HID_N; k += 4) {
      const float4 hv = *(const float4*)(sh + k);
      const float4 a4 = *(const float4*)(wh + k);
      const float4 b4 = *(const float4*)(wgt + k);
      const float4 c4 = *(const float4*)(wp + k);
      ah[0] = fmaf(a4.x, hv.x, ah[0]); ah[1] = fmaf(a4.y, hv.y, ah[1]);
      ah[2] = fmaf(a4.z, hv.z, ah[2]); ah[3] = fmaf(a4.w, hv.w, ah[3]);
      ag[0] = fmaf(b4.x, hv.x, ag[0]); ag[1] = fmaf(b4.y, hv.y, ag[1]);
      ag[2] = fmaf(b4.z, hv.z, ag[2]); ag[3] = fmaf(b4.w, hv.w, ag[3]);
      ap[0] = fmaf(c4.x, hv.x, ap[0]); ap[1] = fmaf(c4.y, hv.y, ap[1]);
      ap[2] = fmaf(c4.z, hv.z, ap[2]); ap[3] = fmaf(c4.w, hv.w, ap[3]);
    }
    const float zh = (ah[0] + ah[1]) + (ah[2] + ah[3]);
    const float zg = (ag[0] + ag[1]) + (ag[2] + ag[3]);
    const float zp = (ap[0] + ap[1]) + (ap[2] + ap[3]);
    const float st = state[(size_t)e * NLAY_N * HID_N + l * HID_N + j];
    const float sg = sigmoidf_(zg);
    const float out = fmaf(sg, tanhf(zh) - st, st);
    const float pp = sigmoidf_(zp);
    const float hn = fmaf(pp, out, (1.0f - pp) * sh[j]);
    newstate[(size_t)e * NLAY_N * HID_N + l * HID_N + j] = out;
    __syncthreads();
    sh[j] = hn;
    __syncthreads();
  }

  const float hf = sh[j];
  for (int a = 0; a < NACT_N; ++a) {
    red[j] = w_a[a * HID_N + j] * hf;
    __syncthreads();
    for (int s = 128; s > 0; s >>= 1) {
      if (j < s) red[j] += red[j + s];
      __syncthreads();
    }
    if (j == 0) logits[e * NACT_N + a] = red[0] + b_a[a];
    __syncthreads();
  }
  red[j] = w_v[j] * hf;
  __syncthreads();
  for (int s = 128; s > 0; s >>= 1) {
    if (j < s) red[j] += red[j + s];
    __syncthreads();
  }
  if (j == 0) value[e] = red[0] + b_v[0];
}

// ---------------------------------------------------------------------------
// Standalone env_step (float agent/food, int64 actions/rng), 2 launches so the
// grid-wide `hit.any()` is exact.
// ---------------------------------------------------------------------------
__global__ void es_k1(const float* __restrict__ ag, const float* __restrict__ fd,
                      const long long* __restrict__ act, float* __restrict__ agout,
                      float* __restrict__ rew, unsigned char* __restrict__ hits,
                      int* __restrict__ anyflag, int N) {
  const int i = blockIdx.x * blockDim.x + threadIdx.x;
  if (i >= N) return;
  float ax = ag[2 * i], ay = ag[2 * i + 1];
  const long long a = act[i];
  float dx = 0.0f, dy = 0.0f;
  if (a == 0) dy = -1.0f;
  else if (a == 1) dy = 1.0f;
  else if (a == 2) dx = -1.0f;
  else if (a == 3) dx = 1.0f;
  ax = fminf(fmaxf(ax + dx, 0.0f), (float)(BOARD_N - 1));
  ay = fminf(fmaxf(ay + dy, 0.0f), (float)(BOARD_N - 1));
  agout[2 * i] = ax;
  agout[2 * i + 1] = ay;
  const bool h = (ax == fd[2 * i]) && (ay == fd[2 * i + 1]);
  rew[i] = h ? 1.0f : 0.0f;
  hits[i] = h ? 1 : 0;
  if (h) atomicOr(anyflag, 1);
}

__global__ void es_k2(const float* __restrict__ fd, const long long* __restrict__ rngin,
                      const unsigned char* __restrict__ hits, const int* __restrict__ anyflag,
                      float* __restrict__ fdout, long long* __restrict__ rngout, int N) {
  const int i = blockIdx.x * blockDim.x + threadIdx.x;
  if (i >= N) return;
  unsigned long long r = (unsigned long long)rngin[i];
  float fx = fd[2 * i], fy = fd[2 * i + 1];
  if (*anyflag) {
    r = lcg(r);
    const float nx = (float)(r % (unsigned long long)BOARD_N);
    r = lcg(r);
    const float ny = (float)(r % (unsigned long long)BOARD_N);
    if (hits[i]) { fx = nx; fy = ny; }
  }
  fdout[2 * i] = fx;
  fdout[2 * i + 1] = fy;
  rngout[i] = (long long)r;
}

// ---------------------------------------------------------------------------
// run() setup.  reference.run() draws the initial agent/food with
//   torch.randint(0, 11, (n,2), generator=cpu_generator_seeded_with(seed))
// which is ATen's MT19937 (standard init_genrand + tempering) with
// value = next_uint32() % 11, consumed contiguously: 2n words for agent then
// 2n for food.  Doing that on the CPU costs 1.75 ms at n=65536 -- inside the
// timed region -- so it is replicated here on one SM instead (~2% of the
// rollout at the largest shape).
//
// The twist is in-place: new[i] = mt[(i+397)%624] ^ tw(old[i], old[i+1]) where
// the mt[] read is post-update for i >= 227.  Written as three 227-wide slices
//   new[i]     = old[i+397] ^ tw(old[i],     old[i+1])
//   new[i+227] = new[i]     ^ tw(old[i+227], old[i+228])
//   new[i+454] = new[i+227] ^ tw(old[i+454], old[i+455])   (i < 170)
// every carried operand is either OLD state or the *same thread's* previous
// result, so one thread owns words {i, i+227, i+454} and needs no barrier
// between the slices.  Double-buffering the state removes the write-after-read
// barrier too, leaving a single __syncthreads() per 624 words instead of six.
//
// The block is then warp-specialised, because measurement said the temper +
// %11 + store tail was 53% of the twist's cost while sitting on the critical
// path between the state update and the barrier.  Warps 0-7 do nothing but the
// twist; warps 8-12 temper and store the PREVIOUS iteration's 624 words out of
// the other half of the double buffer, so the output of iteration T overlaps
// the state chain of T+1 and costs only the barrier they already shared.  Four
// words per output thread (uint4 in, int4 out) beat one and two: 156 threads,
// a quarter of the shared loads and index math, and one 16B store where the
// group does not straddle the agent/food boundary.
//
// 0.406 -> 0.283 us per twist; the chain is 4*nenv/624 twists long and lands
// in the timed region, 179 -> 129 us of it at nenv = 65536.
// ---------------------------------------------------------------------------
#define MT_N 624
#define MT_M 397
#define MT_S 227  // MT_N - MT_M: slice width, and the live threads per twist
#define MT_NT 416  // 256 twist threads (227 live) + 160 output threads (156 live)

__device__ __forceinline__ unsigned int mt_tw(unsigned int u, unsigned int v) {
  const unsigned int x = (u & 0x80000000u) | (v & 0x7fffffffu);
  return (x >> 1) ^ ((x & 1u) ? 0x9908b0dfu : 0u);
}
__device__ __forceinline__ unsigned int mt_temper(unsigned int y) {
  y ^= y >> 11;
  y ^= (y << 7) & 0x9d2c5680u;
  y ^= (y << 15) & 0xefc60000u;
  y ^= y >> 18;
  return y;
}

// temper + %11 + store of one draw at stream position w
__device__ __forceinline__ void mt_put(unsigned int y, int w, int total, int b2,
                                       int* __restrict__ ag, int* __restrict__ fd) {
  if (w >= total) return;
  const int v = (int)(mt_temper(y) % 11u);
  if (w < b2) ag[w] = v; else fd[w - b2] = v;
}

__device__ __forceinline__ void mt_draw(unsigned int seed, int* __restrict__ ag,
                                        int* __restrict__ fd, int nenv) {
  __shared__ __align__(16) unsigned int mtb[2][MT_N];  // 16B for the uint4 reads
  const int i = threadIdx.x;
  if (i == 0) {
    unsigned int x = seed;
    mtb[0][0] = x;
    for (int j = 1; j < MT_N; ++j) {
      x = 1812433253u * (x ^ (x >> 30)) + (unsigned int)j;
      mtb[0][j] = x;
    }
  }
  __syncthreads();

  const int total = 4 * nenv, b2 = 2 * nenv;
  const int niter = (total + MT_N - 1) / MT_N;
  const bool tw = (i < 256);                            // warps 0-7: the twist
  const bool act = tw && (i < MT_S);
  const bool act3 = tw && (i < MT_N - 2 * MT_S);        // 170 threads own a third word
  const int oi = i - 256;                               // warps 8-12: the output
  int p = 0;
  // one extra trip: the output half trails the twist half by a full iteration
  for (int it = 0; it <= niter; ++it, p ^= 1) {
    const unsigned int* __restrict__ cur = mtb[p];
    unsigned int* __restrict__ nxt = mtb[p ^ 1];
    if (tw) {
      if (it < niter && act) {
        const unsigned int n0 = cur[i + MT_M] ^ mt_tw(cur[i], cur[i + 1]);
        const unsigned int n1 = n0 ^ mt_tw(cur[i + MT_S], cur[i + MT_S + 1]);
        nxt[i] = n0;
        nxt[i + MT_S] = n1;
        if (act3) {
          // word 623 folds in new[0], which this thread recomputes from old state
          // rather than waiting on thread 0 (the in-place algorithm reads it back).
          const unsigned int hi = cur[i + 2 * MT_S];
          const unsigned int lo = (i + 2 * MT_S + 1 < MT_N)
                                      ? cur[i + 2 * MT_S + 1]
                                      : (cur[MT_M] ^ mt_tw(cur[0], cur[1]));
          nxt[i + 2 * MT_S] = n1 ^ mt_tw(hi, lo);
        }
      }
    } else if (it > 0 && oi < MT_N / 4) {
      const int w = (it - 1) * MT_N + 4 * oi;
      const uint4 q = *(const uint4*)&cur[4 * oi];
      // the int4 store needs w (or w - b2) 16B-aligned.  w is a multiple of 4
      // because 624 is; b2 = 2*nenv only when nenv is even, so odd nenv takes
      // the scalar path on the food side.
      if (w + 3 < total && (w + 3 < b2 || (w >= b2 && (b2 & 3) == 0))) {
        int4 o;
        o.x = (int)(mt_temper(q.x) % 11u);
        o.y = (int)(mt_temper(q.y) % 11u);
        o.z = (int)(mt_temper(q.z) % 11u);
        o.w = (int)(mt_temper(q.w) % 11u);
        *(int4*)((w < b2) ? &ag[w] : &fd[w - b2]) = o;
      } else {
        mt_put(q.x, w, total, b2, ag, fd);
        mt_put(q.y, w + 1, total, b2, ag, fd);
        mt_put(q.z, w + 2, total, b2, ag, fd);
        mt_put(q.w, w + 3, total, b2, ag, fd);
      }
    }
    __syncthreads();
  }
}

// Block 0 runs the serial MT chain; the rest zero/fill in parallel with it (one
// launch instead of two, and the fill no longer waits on the chain).
__global__ __launch_bounds__(MT_NT) void setup_kernel(unsigned int seed, int* __restrict__ ag,
                                                   int* __restrict__ fd,
                                                   unsigned long long* __restrict__ rng,
                                                   float* __restrict__ rew,
                                                   int* __restrict__ hitflags, long long base,
                                                   int nenv, int npad, int horizon) {
  if (blockIdx.x == 0) {
    mt_draw(seed, ag, fd, nenv);
    return;
  }
  const int i = (blockIdx.x - 1) * blockDim.x + threadIdx.x;
  if (i < npad) {
    rng[i] = (i < nenv) ? (unsigned long long)(base + (long long)i) : 0ULL;
    if (i < nenv) {
      rew[i] = 0.0f;
    } else {
      ag[2 * i] = 0; ag[2 * i + 1] = 0;
      fd[2 * i] = 0; fd[2 * i + 1] = 0;
    }
  }
  if (i < horizon) hitflags[i] = 0;
}

// ---------------------------------------------------------------------------
// host entry points
// ---------------------------------------------------------------------------
#define CK(x) do { cudaError_t e_ = (x); if (e_ != cudaSuccess) return (int)e_; } while (0)

extern "C" int gm_prep_w(const void* wg, void* out, void* stream) {
  const int total = NLAY_N * 96 * 8 * 32;
  prep_w_kernel<<<(total + 255) / 256, 256, 0, (cudaStream_t)stream>>>(
      (const float*)wg, (__half*)out);
  return (int)cudaGetLastError();
}

extern "C" int gm_prep_wb(const void* wg, void* out, void* stream) {
  const int total = NLAY_N * 4 * 16 * 192 * 2;
  prep_wb_kernel<<<(total + 255) / 256, 256, 0, (cudaStream_t)stream>>>(
      (const float*)wg, (__half*)out);
  return (int)cudaGetLastError();
}

// A captured CUDA graph bakes its pointer arguments, but run() hands back fresh
// tensors every call, so the graphed rollout writes to persistent scratch and
// this copies the three outputs into the caller's tensors afterwards.  One
// launch, because the graph is worth ~1.1 us of launch gap per step and three
// torch copies would hand a third of that back at the shorter horizons.
__global__ void copyout_kernel(const float* __restrict__ rs, const float4* __restrict__ ls,
                               const longlong2* __restrict__ ps, float* __restrict__ rd,
                               float4* __restrict__ ld, longlong2* __restrict__ pd, int nenv) {
  const int i = blockIdx.x * blockDim.x + threadIdx.x;
  if (i >= nenv) return;
  rd[i] = rs[i];
  ld[i] = ls[i];
  pd[i] = ps[i];
}

extern "C" int gm_copyout(const void* rs, const void* ls, const void* ps, void* rd, void* ld,
                          void* pd, int nenv, void* stream) {
  copyout_kernel<<<(nenv + 255) / 256, 256, 0, (cudaStream_t)stream>>>(
      (const float*)rs, (const float4*)ls, (const longlong2*)ps, (float*)rd, (float4*)ld,
      (longlong2*)pd, nenv);
  return (int)cudaGetLastError();
}

// One call replaces reference.run()'s CPU randint + arange + zero_ prologue.
extern "C" int gm_setup(unsigned int seed, void* ag, void* fd, void* rng, void* rew,
                        void* hitflags, long long base, int nenv, int npad, int horizon,
                        void* stream) {
  cudaStream_t s = (cudaStream_t)stream;
  const int n = npad > horizon ? npad : horizon;
  setup_kernel<<<1 + (n + MT_NT - 1) / MT_NT, MT_NT, 0, s>>>(
      seed, (int*)ag, (int*)fd, (unsigned long long*)rng, (float*)rew, (int*)hitflags, base,
      nenv, npad, horizon);
  return (int)cudaGetLastError();
}

static constexpr int smem_bytes_wg() {
  return 2 * WG_NSTAGE * WG_TILE * 2 + 64 * HID_N * 2 + NACT_N * HID_N * 4 + HID_N * 4 * 4 +
         HID_N * 4 + 64 * 4 * 4 + 64 * 2 * 4 * 2 + 2 * 64 * NACT_N * 4 + 64 * NACT_N * 4;
}

static int launch_rollout_wg(const void* Wb, const void* w_enc, const void* b_enc,
                             const void* w_a, const void* b_a, void* statebuf, void* agent,
                             void* food, void* rng, void* rew, void* lastlogits, void* posout,
                             void* hitflags, int nenv, int nblk, int horizon, cudaStream_t s) {
  constexpr int SM = smem_bytes_wg();
  static bool inited = false;
  if (!inited) {
    CK(cudaFuncSetAttribute(step_kernel_wg<64>, cudaFuncAttributeMaxDynamicSharedMemorySize, SM));
    inited = true;
  }
  for (int t = 0; t < horizon; ++t) {
    step_kernel_wg<64><<<nblk, 256, SM, s>>>(
        (const __half*)Wb, (const float*)w_enc, (const float*)b_enc, (const float*)w_a,
        (const float*)b_a, (float*)statebuf, (int*)agent, (int*)food,
        (unsigned long long*)rng, (float*)rew, (float*)lastlogits, (long long*)posout,
        (int*)hitflags, t, nenv, nblk, (t == horizon - 1) ? 1 : 0);
  }
  return (int)cudaGetLastError();
}

extern "C" int gm_rollout_wg(const void* Wb, const void* w_enc, const void* b_enc,
                             const void* w_a, const void* b_a, void* statebuf, void* agent,
                             void* food, void* rng, void* rew, void* lastlogits, void* posout,
                             void* hitflags, int nenv, int nblk, int horizon, void* stream) {
  return launch_rollout_wg(Wb, w_enc, b_enc, w_a, b_a, statebuf, agent, food, rng, rew,
                           lastlogits, posout, hitflags, nenv, nblk, horizon,
                           (cudaStream_t)stream);
}

template <int BENV>
static constexpr int smem_bytes() {
  return BENV * HID_N * 2 + BENV * HID_N * 4 + NACT_N * HID_N * 4 + BENV * 4 * 4 +
         BENV * 2 * 4 * 2 + 8 * BENV * NACT_N * 4 + BENV * NACT_N * 4;
}

template <int BENV>
static int launch_rollout(const void* Wsw, const void* w_enc, const void* b_enc,
                          const void* w_a, const void* b_a, void* statebuf, void* agent,
                          void* food, void* rng, void* rew, void* lastlogits, void* posout,
                          void* hitflags, int nenv, int nblk, int horizon, cudaStream_t s) {
  constexpr int SM = smem_bytes<BENV>();
  static bool inited = false;
  if (!inited) {
    CK(cudaFuncSetAttribute(step_kernel<BENV>, cudaFuncAttributeMaxDynamicSharedMemorySize, SM));
    inited = true;
  }
  for (int t = 0; t < horizon; ++t) {
    step_kernel<BENV><<<nblk, 256, SM, s>>>(
        (const __half*)Wsw, (const float*)w_enc, (const float*)b_enc, (const float*)w_a,
        (const float*)b_a, (float*)statebuf, (int*)agent, (int*)food,
        (unsigned long long*)rng, (float*)rew, (float*)lastlogits, (long long*)posout,
        (int*)hitflags, t, nenv, nblk, (t == horizon - 1) ? 1 : 0);
  }
  return (int)cudaGetLastError();
}

extern "C" int gm_rollout(const void* Wsw, const void* w_enc, const void* b_enc,
                          const void* w_a, const void* b_a, void* statebuf, void* agent,
                          void* food, void* rng, void* rew, void* lastlogits, void* posout,
                          void* hitflags, int nenv, int nblk, int horizon, int benv,
                          void* stream) {
  cudaStream_t s = (cudaStream_t)stream;
#define GM_DISPATCH(B)                                                                        \
  case B:                                                                                     \
    return launch_rollout<B>(Wsw, w_enc, b_enc, w_a, b_a, statebuf, agent, food, rng, rew,     \
                             lastlogits, posout, hitflags, nenv, nblk, horizon, s)
  switch (benv) {
    GM_DISPATCH(16);
    GM_DISPATCH(32);
    GM_DISPATCH(48);
    GM_DISPATCH(80);
    GM_DISPATCH(96);
    default:
      GM_DISPATCH(64);
  }
#undef GM_DISPATCH
}

extern "C" int gm_policy_forward(const void* obs, const void* state, const void* w_enc,
                                 const void* b_enc, const void* w_gru, const void* w_a,
                                 const void* b_a, const void* w_v, const void* b_v,
                                 void* logits, void* newstate, void* value, int N,
                                 void* stream) {
  pf_kernel<<<N, 256, 0, (cudaStream_t)stream>>>(
      (const float*)obs, (const float*)state, (const float*)w_enc, (const float*)b_enc,
      (const float*)w_gru, (const float*)w_a, (const float*)b_a, (const float*)w_v,
      (const float*)b_v, (float*)logits, (float*)newstate, (float*)value, N);
  return (int)cudaGetLastError();
}

extern "C" int gm_env_step(const void* ag, const void* fd, const void* act, const void* rngin,
                           void* agout, void* fdout, void* rew, void* rngout, void* hits,
                           void* anyflag, int N, void* stream) {
  cudaStream_t s = (cudaStream_t)stream;
  const int nb = (N + 255) / 256;
  es_k1<<<nb, 256, 0, s>>>((const float*)ag, (const float*)fd, (const long long*)act,
                           (float*)agout, (float*)rew, (unsigned char*)hits, (int*)anyflag, N);
  es_k2<<<nb, 256, 0, s>>>((const float*)fd, (const long long*)rngin,
                           (const unsigned char*)hits, (const int*)anyflag, (float*)fdout,
                           (long long*)rngout, N);
  return (int)cudaGetLastError();
}

20260725_084321_or-opus_anthropic_claude-opus-5_04_grid_mingru_sps