"""CUDA grid-foraging rollout with a globally synchronized MinGRU policy.""" import os from pathlib import Path import torch from torch import nn from torch.nn import functional as F from torch.utils.cpp_extension import load_inline _ROOT = Path(__file__).resolve().parent os.environ.setdefault('TORCH_CUDA_ARCH_LIST', '12.0') os.environ.setdefault('MAX_JOBS', '4') _CPP = r''' #include std::vector gru_cuda(at::Tensor,at::Tensor,at::Tensor,int); std::vector env_cuda(at::Tensor,at::Tensor,at::Tensor,at::Tensor); void rollout_cuda(at::Tensor,at::Tensor,at::Tensor,at::Tensor,at::Tensor,at::Tensor,at::Tensor,at::Tensor,at::Tensor,at::Tensor,at::Tensor,at::Tensor,at::Tensor,at::Tensor,at::Tensor,int); PYBIND11_MODULE(TORCH_EXTENSION_NAME,m) { m.def("gru", &gru_cuda); m.def("env", &env_cuda); m.def("rollout", &rollout_cuda); } ''' _CUDA = r''' #include #include #include #include #include #include #include #include #include namespace cg=cooperative_groups; using at::Tensor; __device__ __forceinline__ float sig(float x) { return 1.f/(1.f+expf(-x)); } __global__ void gru_kernel(const float* h,const float* gates,const float* state,float* hn,float* sn,int n,int layer) { int i=blockIdx.x*blockDim.x+threadIdx.x; if(i>=n*256) return; int e=i/256,c=i%256; float zh=gates[e*768+c],zg=gates[e*768+256+c],zp=gates[e*768+512+c]; float st=state[e*768+layer*256+c]; float o=st+sig(zg)*(tanhf(zh)-st),p=sig(zp); sn[e*256+c]=o; hn[i]=p*o+(1.f-p)*h[i]; } std::vector gru_cuda(Tensor h,Tensor gates,Tensor state,int layer) { c10::cuda::CUDAGuard guard(h.device()); auto hn=at::empty_like(h),sn=at::empty_like(h); gru_kernel<<<(h.numel()+255)/256,256,0,c10::cuda::getCurrentCUDAStream()>>>(h.data_ptr(),gates.data_ptr(),state.data_ptr(),hn.data_ptr(),sn.data_ptr(),h.size(0),layer); C10_CUDA_KERNEL_LAUNCH_CHECK(); return {hn,sn}; } __global__ void move_kernel(const float* a,const float* f,const int64_t* act,float* ao,unsigned char* hits,float* rew,int* any,int n) { int i=blockIdx.x*blockDim.x+threadIdx.x; if(i>=n)return; int ac=act[i]; float x=fminf(10.f,fmaxf(0.f,a[i*2]+(ac==3)-(ac==2))),y=fminf(10.f,fmaxf(0.f,a[i*2+1]+(ac==1)-(ac==0))); ao[i*2]=x;ao[i*2+1]=y; bool hit=(x==f[i*2] && y==f[i*2+1]);hits[i]=hit;rew[i]=hit; if(hit)atomicExch(any,1); } __device__ __forceinline__ unsigned long long lcg(unsigned long long x) { return (x*6364136223846793005ULL+1ULL)&0x7fffffffffffffffULL; } __global__ void respawn_kernel(const float* f,const unsigned long long* rng,const unsigned char* hits,const int* any,float* fo,unsigned long long* ro,int n) { int i=blockIdx.x*blockDim.x+threadIdx.x; if(i>=n)return; unsigned long long r=rng[i];float x=f[2*i],y=f[2*i+1]; if(*any){r=lcg(r);if(hits[i])x=r%11ULL;r=lcg(r);if(hits[i])y=r%11ULL;} fo[2*i]=x;fo[2*i+1]=y;ro[i]=r; } std::vector env_cuda(Tensor a,Tensor f,Tensor act,Tensor rng) { c10::cuda::CUDAGuard guard(a.device()); auto ao=at::empty_like(a),fo=at::empty_like(f),ro=at::empty_like(rng),rew=at::empty({a.size(0)},a.options()); auto hits=at::empty({a.size(0)},a.options().dtype(at::kByte)),any=at::zeros({1},a.options().dtype(at::kInt)); auto stream=c10::cuda::getCurrentCUDAStream();int n=a.size(0); move_kernel<<<(n+255)/256,256,0,stream>>>(a.data_ptr(),f.data_ptr(),act.data_ptr(),ao.data_ptr(),hits.data_ptr(),rew.data_ptr(),any.data_ptr(),n); respawn_kernel<<<(n+255)/256,256,0,stream>>>(f.data_ptr(),(const unsigned long long*)rng.data_ptr(),hits.data_ptr(),any.data_ptr(),fo.data_ptr(),(unsigned long long*)ro.data_ptr(),n); C10_CUDA_KERNEL_LAUNCH_CHECK();return {ao,fo,rew,ro}; } __device__ __forceinline__ void mma(float* c,const unsigned* a,const unsigned* b) { 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};" : "+f"(c[0]),"+f"(c[1]),"+f"(c[2]),"+f"(c[3]) : "r"(a[0]),"r"(a[1]),"r"(a[2]),"r"(a[3]),"r"(b[0]),"r"(b[1])); } // Each CTA evaluates 16 environments. The recurrent state is FP32, and the // matrix products use three FP16 tensor-core products with FP32 accumulation. // Splitting both operands into a high part and a rounding residual preserves // FP32 accuracy without rounding recurrent state or highway activations. __global__ __launch_bounds__(256) void rollout_kernel( const float* we,const float* be,const unsigned short* wh,const unsigned short* wl, const float* wa,const float* ba,const float* wv,const float* bv, int64_t* agent,int64_t* food,unsigned long long* rng,float* state,float* rewards,float* logits, unsigned char* hits,int* flags,int n,int horizon) { __shared__ float h[16*264]; __shared__ unsigned short ah[16*264],al[16*264]; int lane=threadIdx.x%32,warp=threadIdx.x/32; int row=lane/4,col=(lane%4)*2; auto grid=cg::this_grid(); for(int t=0;t0;d/=2) { #pragma unroll for(int a=0;a<4;a++)sums[a]+=__shfl_down_sync(0xffffffff,sums[a],d); } if(lane==0 && ebest){best=z;action=a;}} int64_t x=agent[e*2]+(action==3)-(action==2),y=agent[e*2+1]+(action==1)-(action==0); x=x<0?0:(x>10?10:x);y=y<0?0:(y>10?10:y); agent[e*2]=x;agent[e*2+1]=y; bool hit=x==food[e*2] && y==food[e*2+1];hits[e]=hit;rewards[e]+=hit; if(hit)atomicExch(flags+t,1); } } __syncthreads(); } grid.sync(); if(flags[t]) { for(int e=blockIdx.x*256+threadIdx.x;e(),*pbe=be.data_ptr(),*pwa=wa.data_ptr(),*pba=ba.data_ptr(),*pwv=wv.data_ptr(),*pbv=bv.data_ptr(); const unsigned short *pwh=(const unsigned short*)wh.data_ptr(),*pwl=(const unsigned short*)wl.data_ptr(); int64_t *pa=agent.data_ptr(),*pf=food.data_ptr(); auto pr=(unsigned long long*)rng.data_ptr();float *ps=state.data_ptr(),*prew=rewards.data_ptr(),*pl=logits.data_ptr(); auto ph=hits.data_ptr();auto pflags=flags.data_ptr(); void* args[]={&pwe,&pbe,&pwh,&pwl,&pwa,&pba,&pwv,&pbv,&pa,&pf,&pr,&ps,&prew,&pl,&ph,&pflags,&n,&horizon}; C10_CUDA_CHECK(cudaLaunchCooperativeKernel((void*)rollout_kernel,blocks,256,args,0,c10::cuda::getCurrentCUDAStream())); } ''' _EXT = None def _ext(): global _EXT if _EXT is None: build = _ROOT / '.cuda_build' build.mkdir(exist_ok=True) _EXT = load_inline(name='grid_rollout_cuda', cpp_sources=_CPP, cuda_sources=_CUDA, build_directory=str(build), extra_cflags=['-O3'], extra_cuda_cflags=['-O3', '--use_fast_math', '-lineinfo'], verbose=False) return _EXT class Model(nn.Module): def __init__(self): super().__init__() self.w_enc = nn.Parameter(torch.empty(256, 4)) self.b_enc = nn.Parameter(torch.zeros(256)) self.w_gru = nn.Parameter(torch.empty(3, 768, 256)) self.w_a = nn.Parameter(torch.empty(4, 256)) self.b_a = nn.Parameter(torch.zeros(4)) self.w_v = nn.Parameter(torch.empty(1, 256)) self.b_v = nn.Parameter(torch.zeros(1)) self.reset_parameters(0) def reset_parameters(self, seed=0): g = torch.Generator(device='cpu').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) self._packed = None def forward(self, obs, state): return policy_forward(self, obs, state) def _weights(self): key = (self.w_gru.data_ptr(), self.w_gru._version) cached = getattr(self, '_packed', None) if cached is None or cached[0] != key: wh = self.w_gru.detach().to(torch.float16) wl = (self.w_gru.detach() - wh.float()).to(torch.float16) wh = wh.reshape(3, 96, 8, 16, 2, 4, 2).permute(0, 1, 3, 2, 5, 4, 6).contiguous() wl = wl.reshape(3, 96, 8, 16, 2, 4, 2).permute(0, 1, 3, 2, 5, 4, 6).contiguous() self._packed = (key, wh, wl) return self._packed[1:] @torch.no_grad() def policy_forward(model, obs, state): obs, state = obs.contiguous(), state.contiguous() h = F.linear(obs, model.w_enc, model.b_enc) states = [] for layer in range(3): gates = F.linear(h, model.w_gru[layer]) h, st = _ext().gru(h, gates, state, layer) states.append(st) return F.linear(h, model.w_a, model.b_a), torch.stack(states, dim=1), F.linear(h, model.w_v, model.b_v).squeeze(-1) @torch.no_grad() def env_step(agent, food, actions, rng_state): if agent.shape[0] == 0: return agent.clone(), food.clone(), torch.empty(0, device=agent.device, dtype=torch.float32), rng_state.clone() a, f, reward, rng = _ext().env(agent.float().contiguous(), food.float().contiguous(), actions.long().contiguous(), rng_state.contiguous()) return a.to(agent.dtype), f.to(food.dtype), reward, rng @torch.no_grad() def run(num_envs: int, horizon: int, seed: int, model=None) -> dict: if model is None: model = Model() model = model.to('cuda:0').eval() ext = _ext() g = torch.Generator(device='cpu').manual_seed(seed) agent = torch.randint(0, 11, (num_envs, 2), generator=g).to('cuda:0') food = torch.randint(0, 11, (num_envs, 2), generator=g).to('cuda:0') rng = torch.arange(num_envs, device='cuda:0', dtype=torch.int64) + seed * 10007 state = torch.zeros(num_envs, 3, 256, device='cuda:0') rewards = torch.zeros(num_envs, device='cuda:0') logits = torch.zeros(num_envs, 4, device='cuda:0') flags = torch.zeros(horizon, device='cuda:0', dtype=torch.int32) wh, wl = model._weights() if num_envs and horizon: ext.rollout(model.w_enc, model.b_enc, wh, wl, model.w_a, model.b_a, model.w_v, model.b_v, agent, food, rng, state, rewards, logits, flags, horizon) return {'rewards': rewards, 'positions': agent, 'last_logits': logits, 'state': state}