KernelBench cuda · H100
Grid + MinGRU SPS Claude Opus 5
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.
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