File size: 12,011 Bytes
bfdbd33 | 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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | // physarum-cpu: one step of a Physarum (slime-mould) transport-network sim.
// Synchronous update: all agents sense the start-of-step field along three
// sensors, steer up-gradient and move (parallel over agents, each agent's jitter
// from an index-seeded RNG); deposits are then applied in a serial index-order
// pass, and the field is diffused (separable 3-tap) and decayed. On a cadence
// the agents are counting-sorted into tile order so that consecutive (and
// same-thread) agents sense a small hot window instead of scattering three
// gathers across the whole field -- the gathers dominate and otherwise miss.
// Read-only sensing + ordered deposit + serial sort make the step deterministic
// for any thread count. Agent trig/arithmetic and the diffuse stencil vectorise
// on NEON (aarch64); a portable scalar path covers everything else.
#include <torch/torch.h>
#include <ATen/Parallel.h>
#include <cstdint>
#include <cstring>
#include <cmath>
#include <thread>
#include <vector>
static const int TILE = 32; // sort granularity (px); ~8-step drift stays inside
static const int RESORT = 8; // re-sort every RESORT steps (amortises the sort)
namespace {
// Fan a range [0,total) across at::get_num_threads() worker threads, each given
// a contiguous chunk; f(begin,end) must touch only its own slice. std::thread
// (not OpenMP) so it parallelises under the stock kernel-builder compile, which
// exposes no way to pass -fopenmp to a CPU kernel. Falls back to serial for a
// single thread or a small range.
template<class F>
inline void parallel_chunks(int total, F f){
int T = at::get_num_threads(); if (T < 1) T = 1;
if (T == 1 || total < 4096){ f(0, total); return; }
if (T > total) T = total;
int chunk = (total + T - 1) / T;
std::vector<std::thread> pool; pool.reserve(T - 1);
for (int t=1; t<T; t++){
int b=t*chunk, e=b+chunk>total?total:b+chunk;
if (b>=e) break;
pool.emplace_back([&,b,e]{ f(b,e); });
}
f(0, chunk>total?total:chunk); // calling thread runs chunk 0
for (auto& th : pool) th.join();
}
inline uint32_t xr(uint32_t &s){ s ^= s<<13; s ^= s>>17; s ^= s<<5; return s; }
inline float frand(uint32_t &s){ return (xr(s) >> 8) * (1.0f/16777216.0f); }
// per-agent, per-step independent seed: the step's `seed` argument already
// encodes base_seed+t, so hashing it with the agent index gives a stream that
// varies per agent and per step yet does not depend on iteration order.
inline uint32_t agent_seed(uint32_t base, int i){
uint32_t s = (uint32_t)i*2654435761u ^ base*2246822519u;
s ^= s>>15; s *= 2246822519u; s ^= s>>13; s *= 3266489917u; s ^= s>>16;
return s ? s : 1u;
}
inline float fsin(float x){ // parabola approx, |err|<1e-3
float k = x * 0.15915494f;
k = x - 6.2831853f * (float)((int)(k + (k < 0 ? -0.5f : 0.5f)));
float y = 1.27323954f*k - 0.405284735f*k*(k<0?-k:k);
return 0.225f*(y*(y<0?-y:y) - y) + y;
}
inline float fcos(float x){ return fsin(x + 1.5707963f); }
inline float samp(const float* t, float x, float y, int W, int H){
int xi = ((int)x % W + W) % W, yi = ((int)y % H + H) % H;
return t[(size_t)yi*W + xi];
}
void agents(const float* t, float* ax, float* ay, float* ah, int W, int H, int N,
float sd, float sa, float turn, float speed, uint32_t base);
void deposit(float* t, const float* ax, const float* ay, int W, int H, int N, float dep);
void diffuse(float* t, float* tmp, int W, int H, float decay);
#if defined(__ARM_NEON)
#include <arm_neon.h>
inline float32x4_t vsin(float32x4_t x){
float32x4_t k = vrndnq_f32(vmulq_n_f32(x, 0.15915494f));
x = vmlsq_n_f32(x, k, 6.2831853f);
float32x4_t ax = vabsq_f32(x);
float32x4_t y = vsubq_f32(vmulq_n_f32(x,1.27323954f),
vmulq_n_f32(vmulq_f32(x,ax),0.405284735f));
float32x4_t ay = vabsq_f32(y);
return vmlaq_n_f32(y, vsubq_f32(vmulq_f32(y,ay), y), 0.225f);
}
inline float32x4_t vcos(float32x4_t x){ return vsin(vaddq_f32(x, vdupq_n_f32(1.5707963f))); }
// sense + steer + move for the agent quads [q0,q1); reads the field only, writes
// ax/ay/ah. No field writes, so any set of quads runs on any thread.
inline void move_quads(const float* t, float* ax, float* ay, float* ah,
int q0, int q1, int W, int H, float sd, float sa,
float turn, float speed, uint32_t base){
float32x4_t vsd=vdupq_n_f32(sd), vsp=vdupq_n_f32(speed), vsa=vdupq_n_f32(sa);
float32x4_t vW=vdupq_n_f32((float)W), vH=vdupq_n_f32((float)H);
float32x4_t iW=vdupq_n_f32(1.0f/W), iH=vdupq_n_f32(1.0f/H);
for (int q=q0; q<q1; q++){
int i=q*4;
float32x4_t x=vld1q_f32(ax+i), y=vld1q_f32(ay+i), h=vld1q_f32(ah+i);
float32x4_t hl=vsubq_f32(h,vsa), hr=vaddq_f32(h,vsa);
float cx[4],cy[4],lx[4],ly[4],rx[4],ry[4];
vst1q_f32(cx, vmlaq_f32(x, vcos(h), vsd)); vst1q_f32(cy, vmlaq_f32(y, vsin(h), vsd));
vst1q_f32(lx, vmlaq_f32(x, vcos(hl),vsd)); vst1q_f32(ly, vmlaq_f32(y, vsin(hl),vsd));
vst1q_f32(rx, vmlaq_f32(x, vcos(hr),vsd)); vst1q_f32(ry, vmlaq_f32(y, vsin(hr),vsd));
float FC[4],FL[4],FR[4];
for (int l=0;l<4;l++){ FC[l]=samp(t,cx[l],cy[l],W,H);
FL[l]=samp(t,lx[l],ly[l],W,H); FR[l]=samp(t,rx[l],ry[l],W,H); }
float32x4_t fc=vld1q_f32(FC), fl=vld1q_f32(FL), fr=vld1q_f32(FR);
float jit[4]; for(int l=0;l<4;l++){ uint32_t s=agent_seed(base,i+l); jit[l]=0.5f+0.5f*frand(s); }
float32x4_t vj=vld1q_f32(jit), vturn=vmulq_n_f32(vj,turn);
uint32x4_t straight=vandq_u32(vcgtq_f32(fc,fl), vcgtq_f32(fc,fr));
float32x4_t dh=vbslq_f32(vcgtq_f32(fl,fr), vnegq_f32(vturn),
vbslq_f32(vcgtq_f32(fr,fl), vturn,
vmulq_n_f32(vsubq_f32(vj,vdupq_n_f32(0.5f)), 2.0f*turn)));
dh=vbslq_f32(straight, vdupq_n_f32(0.0f), dh);
h=vaddq_f32(h,dh);
x=vmlaq_f32(x, vcos(h), vsp); y=vmlaq_f32(y, vsin(h), vsp);
x=vsubq_f32(x, vmulq_f32(vW, vrndmq_f32(vmulq_f32(x,iW))));
y=vsubq_f32(y, vmulq_f32(vH, vrndmq_f32(vmulq_f32(y,iH))));
vst1q_f32(ax+i,x); vst1q_f32(ay+i,y); vst1q_f32(ah+i,h);
}
}
void agents(const float* t, float* ax, float* ay, float* ah, int W, int H, int N,
float sd, float sa, float turn, float speed, uint32_t base){
int nq = N/4;
parallel_chunks(nq, [&](int q0, int q1){
move_quads(t, ax, ay, ah, q0, q1, W, H, sd, sa, turn, speed, base);
});
for (int i=nq*4; i<N; i++){ // scalar tail, same sync rule
float x=ax[i], y=ay[i], h=ah[i];
float fc=samp(t,x+fcos(h)*sd,y+fsin(h)*sd,W,H);
float fl=samp(t,x+fcos(h-sa)*sd,y+fsin(h-sa)*sd,W,H);
float fr=samp(t,x+fcos(h+sa)*sd,y+fsin(h+sa)*sd,W,H);
uint32_t s=agent_seed(base,i); float j=0.5f+0.5f*frand(s);
if(fc>fl&&fc>fr){} else if(fl>fr) h-=turn*j; else if(fr>fl) h+=turn*j; else h+=(j-0.5f)*2.0f*turn;
x=fmodf(x+fcos(h)*speed+W,W); y=fmodf(y+fsin(h)*speed+H,H);
ax[i]=x; ay[i]=y; ah[i]=h;
}
}
void diffuse(float* t, float* tmp, int W, int H, float dec){
for (int y=0;y<H;y++){ const float* r=t+(size_t)y*W; float* o=tmp+(size_t)y*W;
o[0]=0.25f*r[W-1]+0.5f*r[0]+0.25f*r[1]; int x=1;
for (; x+4<=W-1; x+=4){ float32x4_t L=vld1q_f32(r+x-1),C=vld1q_f32(r+x),R=vld1q_f32(r+x+1);
vst1q_f32(o+x, vmlaq_n_f32(vmlaq_n_f32(vmulq_n_f32(C,0.5f),L,0.25f),R,0.25f)); }
for (; x<W-1; x++) o[x]=0.25f*r[x-1]+0.5f*r[x]+0.25f*r[x+1];
o[W-1]=0.25f*r[W-2]+0.5f*r[W-1]+0.25f*r[0]; }
float32x4_t vd=vdupq_n_f32(dec);
for (int y=0;y<H;y++){ int yu=y?y-1:H-1, yd=y+1<H?y+1:0;
const float* U=tmp+(size_t)yu*W,*C=tmp+(size_t)y*W,*D=tmp+(size_t)yd*W;
float* o=t+(size_t)y*W; int x=0;
for (; x+4<=W; x+=4){ float32x4_t s=vmlaq_n_f32(vmlaq_n_f32(vmulq_n_f32(vld1q_f32(C+x),0.5f),
vld1q_f32(U+x),0.25f), vld1q_f32(D+x),0.25f); vst1q_f32(o+x, vmulq_f32(s,vd)); }
for (; x<W; x++) o[x]=(0.25f*U[x]+0.5f*C[x]+0.25f*D[x])*dec; }
}
#else // ---------- portable scalar ----------
void agents(const float* t, float* ax, float* ay, float* ah, int W, int H, int N,
float sd, float sa, float turn, float speed, uint32_t base){
parallel_chunks(N, [&](int i0, int i1){
for (int i=i0;i<i1;i++){
float x=ax[i], y=ay[i], h=ah[i];
float fc=samp(t,x+fcos(h)*sd,y+fsin(h)*sd,W,H);
float fl=samp(t,x+fcos(h-sa)*sd,y+fsin(h-sa)*sd,W,H);
float fr=samp(t,x+fcos(h+sa)*sd,y+fsin(h+sa)*sd,W,H);
uint32_t s=agent_seed(base,i); float j=0.5f+0.5f*frand(s);
if(fc>fl&&fc>fr){} else if(fl>fr) h-=turn*j; else if(fr>fl) h+=turn*j; else h+=(j-0.5f)*2.0f*turn;
x=fmodf(x+fcos(h)*speed+W,W); y=fmodf(y+fsin(h)*speed+H,H);
ax[i]=x; ay[i]=y; ah[i]=h;
}
});
}
void diffuse(float* t, float* tmp, int W, int H, float dec){
for (int y=0;y<H;y++){ const float* r=t+(size_t)y*W; float* o=tmp+(size_t)y*W;
for (int x=0;x<W;x++){ int xl=x?x-1:W-1, xr=x+1<W?x+1:0;
o[x]=0.25f*r[xl]+0.5f*r[x]+0.25f*r[xr]; } }
for (int y=0;y<H;y++){ int yu=y?y-1:H-1, yd=y+1<H?y+1:0;
const float* U=tmp+(size_t)yu*W,*C=tmp+(size_t)y*W,*D=tmp+(size_t)yd*W;
float* o=t+(size_t)y*W;
for (int x=0;x<W;x++) o[x]=(0.25f*U[x]+0.5f*C[x]+0.25f*D[x])*dec; }
}
#endif
// deposit is a serial index-order scatter so the field sum is identical for any
// thread count; positions are already wrapped into range by the move.
void deposit(float* t, const float* ax, const float* ay, int W, int H, int N, float dep){
for (int i=0;i<N;i++){ int xi=(int)ax[i], yi=(int)ay[i];
if(xi>=0&&xi<W&&yi>=0&&yi<H) t[(size_t)yi*W+xi]+=dep; }
}
// counting-sort agents into row-major tile order (serial, deterministic). Tile
// coords are clamped because the position wrap can round to exactly x==W.
void bin_sort(float* ax, float* ay, float* ah, int W, int H, int N, int tile){
int TW=(W+tile-1)/tile, TH=(H+tile-1)/tile, nt=TW*TH;
std::vector<int> off(nt+1, 0), cur(nt);
auto tid=[&](int i){ int xi=(int)ax[i]; xi=xi<0?0:(xi>=W?W-1:xi);
int yi=(int)ay[i]; yi=yi<0?0:(yi>=H?H-1:yi);
return (yi/tile)*TW + xi/tile; };
for (int i=0;i<N;i++) off[tid(i)+1]++;
for (int t=1;t<=nt;t++) off[t]+=off[t-1];
for (int t=0;t<nt;t++) cur[t]=off[t];
std::vector<float> sx(N), sy(N), sh(N);
for (int i=0;i<N;i++){ int p=cur[tid(i)]++; sx[p]=ax[i]; sy[p]=ay[i]; sh[p]=ah[i]; }
memcpy(ax, sx.data(), (size_t)N*4);
memcpy(ay, sy.data(), (size_t)N*4);
memcpy(ah, sh.data(), (size_t)N*4);
}
} // namespace
void physarum_step(torch::Tensor trail, torch::Tensor tmp,
torch::Tensor ax, torch::Tensor ay, torch::Tensor ah,
int64_t seed, double sense_dist, double sense_ang,
double turn, double speed, double deposit_, double decay){
TORCH_CHECK(trail.dtype()==torch::kFloat32 && trail.is_contiguous(), "trail must be f32 contiguous");
TORCH_CHECK(trail.dim()==2 && tmp.sizes()==trail.sizes(), "trail/tmp must be [H,W]");
int H=(int)trail.size(0), W=(int)trail.size(1), N=(int)ax.size(0);
uint32_t base = (uint32_t)(seed ^ 0x9e3779b9u); if(!base) base=1;
float* t = trail.data_ptr<float>();
// seed increments per step (base_seed + t), so this re-sorts every RESORT steps
if ((uint64_t)seed % (uint64_t)RESORT == 0)
bin_sort(ax.data_ptr<float>(), ay.data_ptr<float>(), ah.data_ptr<float>(), W,H,N, TILE);
agents(t, ax.data_ptr<float>(), ay.data_ptr<float>(), ah.data_ptr<float>(),
W,H,N, (float)sense_dist,(float)sense_ang,(float)turn,(float)speed, base);
deposit(t, ax.data_ptr<float>(), ay.data_ptr<float>(), W,H,N, (float)deposit_);
diffuse(t, tmp.data_ptr<float>(), W,H,(float)decay);
}
|