Support custom slime chunk patterns in CUDA finder

This commit is contained in:
Yaossg 2026-10-16 23:51:12 +08:00
parent dd5fab9540
commit 7952207735
10 changed files with 2302 additions and 34 deletions

View file

@ -1,6 +1,6 @@
# 史莱姆区块搜索与 CUDA 基准
当前可直接使用的入口是 **[bench/slime-bench](bench/README.md)**:指定中心区块、曼哈顿半径和实心 `n×n` 图案,测量全部有效种子的普通候选分支搜索时间,支持单张或多张 NVIDIA GPU。
当前可直接使用的入口是 **[bench/slime-bench](bench/README.md)**:指定中心区块、曼哈顿半径和自定义图案(或实心 `n×n`),测量全部有效种子的普通候选分支搜索时间,支持单张或多张 NVIDIA GPU。
```sh
make bench
@ -8,6 +8,8 @@ make bench
--position-batch 128 --devices all --repeat 3
```
自定义图案用 `--pattern` 或 `--pattern-file`,支持史莱姆、非史莱姆和忽略格;见 [6×6 苦力怕示例](bench/README.md#自定义图案6x6-苦力怕)。
H100 编译:`make -C bench -B CUDA_ARCH=90`。需要 CUDA Toolkit 和 C++17 编译器。普通分支不包含拒绝重抽补扫,不能将它的结果声明为严格完整目录;参数和计时口径见 [CLI 使用说明](bench/README.md)。
## 目录导航

View file

@ -1,6 +1,6 @@
# slime-bench:普通分支全种子搜索基准
指定中心区块、曼哈顿半径和实心 `n×n` 图案,搜索范围内每个锚点的全部有效种子,输出 JSON 结果及计时。支持 NVIDIA CUDA 单卡、多卡。
指定中心区块、曼哈顿半径和自定义图案(或实心 `n×n`),搜索范围内每个锚点的全部有效种子,输出 JSON 结果及计时。支持 NVIDIA CUDA 单卡、多卡。
## 本地编译与运行
@ -16,6 +16,37 @@ mkdir -p artifacts
默认编译 `sm_86` 并附带 PTX,适合本机 RTX 3060 Laptop。换架构时用 `-B` 强制重建;仅改变 Make 变量不会使已有文件自动过期。
## 自定义图案:6×6 苦力怕
`--pattern` 接受 `/` 分隔的行,或用 `--pattern-file` 读取每行一排的文本文件。`#` 必须是史莱姆区块,`.` 必须不是,`?` 不限制。支持非正方形,宽、高分别为 1~64,至少含一个 `#`;不能与 `--size` 同时指定。不自动旋转或镜像。
内置示例 [patterns/creeper-6x6.txt](patterns/creeper-6x6.txt) 将脸部像素设为史莱姆:
```text
##..##
##..##
..##..
.####.
.####.
.#..#.
```
```sh
./bench/bin/slime-bench --radius 512 \
--pattern-file bench/patterns/creeper-6x6.txt \
--position-batch 128 --max-results 100 --output artifacts/creeper.json
# 也可直接输入;必须使用引号保护 #、? 等字符
./bench/bin/slime-bench --radius 512 \
--pattern '##..##/##..##/..##../.####./.####./.#..#.' --max-results 100
```
目标是上面的正色模板:眼睛、鼻子和嘴巴为史莱姆区块,背景必须不是。已检查原点半径 2048、中心 `(51069,419124)` 半径 1024、32 个分散区域各半径 256、另外 128 个分散区域各半径 512;还筛选了 65,536 个坐标,选出低位候选最多的 64 个坐标类,检查这些类在世界边界内的全部 104,538 个实际位置。累计 82,047,228 次锚点检查仍未命中(不同路线可能重叠,见[搜索记录](results/creeper-6x6-search.json))。这不证明正色不存在;不能用反色图案作为该目标的搜索结果。
每行从左到右沿 `+X`,从上到下沿 `+Z`。返回坐标仍是矩形左上角,即使该格是 `.` 或 `?`。将 `.` 改为 `?` 可只要求轮廓、允许背景出现额外史莱姆区块。JSON 包含原始 `pattern` 行、宽高及两类约束数;自定义输入的 `square_size` 为 `null`。
筛选和区间求解只使用 `#` 格,随后精确核对所有 `.` 格;`?` 不参与计算。稀疏图案的候选可能很多,搜索时间不能直接套用实心方阵的基准。搜索口径仍为普通分支,不包含拒绝重抽补扫。
## H100 服务器
将仓库内的 `bench/` 和 `include/` 一起复制到服务器,保持相对路径;CLI 依赖 `include/slime/` 中的共用头文件。
@ -53,7 +84,7 @@ abs(anchor_x - x) + abs(anchor_z - z) <= radius
```
- `x,z,radius` 的单位都是区块,不是方块。
- 锚点是图案最小 X、最小 Z 的角;图案沿 `+X,+Z` 延伸。
- 锚点是图案最小 X、最小 Z 的角;图案沿 `+X,+Z` 延伸。自定义图案的锚点是外接矩形的角。
- 半径限制锚点,图案允许超出菱形范围。
- 不自动按世界边界裁剪;锚点和整块图案须在 int32 坐标范围内,否则报错。
- `--size` 支持 1~6,默认 5。小尺寸匹配种子极多,全种子计数也可能耗时很久,不能用 1×1 推断 5×5 性能。
@ -108,7 +139,7 @@ make -C bench test # 无需 GPU:范围、输入、可变尺寸数学对
make -C bench test-gpu # 需要 GPU:实际搜索与文件输出验证
```
验证内容包括:半径 0~64 的完整菱形枚举,较大半径抽样,1~6 中多个尺寸的组 mask 对照,已知 24 个命中,小根批分拆一致性,4×4 的全部 422 个示例经独立 Java 算法验证,6×6 的已知种子子集对照,以及半径 2 批量结果与全部 13 个位置分别搜索的完整列表一致。
验证内容包括:自定义图案解析和边界、稀疏图案高位枚举对照、带忽略格及非史莱姆格的 GPU 结果与独立 Java 算法筛选对照,以及半径 0~64 的完整菱形枚举,较大半径抽样,1~6 中多个尺寸的组 mask 对照,已知 24 个命中,小根批分拆一致性,4×4 的全部 422 个示例经独立 Java 算法验证,6×6 的已知种子子集对照,以及半径 2 批量结果与全部 13 个位置分别搜索的完整列表一致。
本机实测:RTX 3060 Laptop,中心 `(0,0)`,半径 512,5×5,`--position-batch 128 --repeat 3`。525,313 个锚点,三轮均有 368,935 个低位根任务、0 个命中,扫描墙钟中位数约 **3.990 秒**。原始 JSON 在 [results/local-r512-n5.json](results/local-r512-n5.json)。另有半径 64 的快速环境验证记录;其批大小为默认 32,与上述正式样例不同。

View file

@ -0,0 +1,6 @@
##..##
##..##
..##..
.####.
.####.
.#..#.

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,25 @@ using Clock=std::chrono::steady_clock;
struct Coord{int32_t x,z;};
struct Job{uint32_t position,root;};
struct Match{uint64_t seed;int32_t x,z;};
struct Config{int32_t x=0,z=0;uint64_t radius=0;int size=5,positionBatch=32,jobBatch=2048,repeats=1;uint64_t maxResults=0;size_t cacheClasses=4096;std::string devices="0",output;bool dry=false,list=false;};
struct PatternCell{int x,z;uint8_t requirement;};
struct Pattern{int width=0,height=0,positive=0;std::vector<std::string> rows;std::vector<PatternCell> cells;};
Pattern parsePattern(std::string text){
std::replace(text.begin(),text.end(),'/','\n');
Pattern p;std::stringstream input(text);std::string row;
while(std::getline(input,row)){if(!row.empty()&&row.back()=='\r')row.pop_back();if(row.empty())throw std::runtime_error("empty pattern row");p.rows.push_back(row);}
if(p.rows.empty())throw std::runtime_error("empty pattern");
p.width=p.rows[0].size();p.height=p.rows.size();
if(p.width>64||p.height>64)throw std::runtime_error("pattern dimensions must be 1..64");
for(auto& r:p.rows)if(r.size()!=size_t(p.width))throw std::runtime_error("pattern rows must have equal width");
for(char wanted:{'#','.'})for(int z=0;z<p.height;++z)for(int x=0;x<p.width;++x){char ch=p.rows[z][x];
if(ch!='#'&&ch!='.'&&ch!='?')throw std::runtime_error("pattern characters must be #, . or ?");
if(ch==wanted)p.cells.push_back({x,z,uint8_t(ch=='#'?sr::kSlime:sr::kNonSlime)});
if(wanted=='#'&&ch=='#')++p.positive;
}
if(!p.positive)throw std::runtime_error("pattern requires at least one # cell");
return p;
}
struct Config{Pattern pattern;int32_t x=0,z=0;uint64_t radius=0;int size=5,positionBatch=32,jobBatch=2048,repeats=1;uint64_t maxResults=0;size_t cacheClasses=4096;std::string devices="0",output;bool dry=false,list=false;};
uint64_t number(const std::string& s){size_t used=0;if(s.empty()||s[0]=='-')throw std::runtime_error("expected nonnegative integer: "+s);auto n=std::stoull(s,&used);if(used!=s.size())throw std::runtime_error("invalid integer: "+s);return n;}
int32_t signed32(const std::string&s){size_t used=0;auto n=std::stoll(s,&used);if(used!=s.size()||n<INT32_MIN||n>INT32_MAX)throw std::runtime_error("invalid int32 coordinate");return int32_t(n);}
uint64_t anchors(uint64_t r){return 1+2*r*(r+1);}
@ -38,15 +56,20 @@ Coord coordinate(const Config& c,uint64_t index){
}
uint64_t classKey(Coord p){return (uint64_t(uint32_t(p.x)&65535)<<17)|(uint32_t(p.z)&131071);}
void terms(Coord p,int n,sr::CellTerm* out){uint64_t anchor=sr::coordinateTerm(p.x,p.z);for(int z=0;z<n;++z)for(int x=0;x<n;++x)out[z*n+x]={(sr::coordinateTerm(p.x+x,p.z+z)-anchor)&sr::kMask48,sr::kSlime};}
__global__ void rootsKernel(const sr::CellTerm* cells,uint32_t positions,int count,Job* out,unsigned long long* total){
void terms(Coord p,const Pattern& pattern,sr::CellTerm* out){
// The grouped filter needs its first positive term to be zero.
auto first=pattern.cells.front();uint64_t anchor=sr::coordinateTerm(p.x+first.x,p.z+first.z);size_t i=0;
for(auto cell:pattern.cells)out[i++]={(sr::coordinateTerm(p.x+cell.x,p.z+cell.z)-anchor)&sr::kMask48,cell.requirement};
}
__global__ void rootsKernel(const sr::CellTerm* cells,uint32_t positions,int count,int positive,Job* out,unsigned long long* total){
uint64_t t=uint64_t(blockIdx.x)*blockDim.x+threadIdx.x;if(t>=(uint64_t(positions)<<18))return;
uint32_t pos=t>>18,root=t&262143;const auto* p=cells+uint64_t(pos)*count;
for(int i=0;i<count;++i)if((((root+uint32_t(p[i].term))^uint32_t(sr::kCombinedXor))*uint32_t(sr::kJavaMultiplier)+11U)&(1U<<17))return;
for(int i=0;i<positive;++i)if((((root+uint32_t(p[i].term))^uint32_t(sr::kCombinedXor))*uint32_t(sr::kJavaMultiplier)+11U)&(1U<<17))return;
out[atomicAdd(total,1ULL)]={pos,root};
}
__global__ void prepareKernel(const sr::CellTerm* cells,const Job* jobs,uint32_t n,int count,uint32_t* prepared){
__global__ void prepareKernel(const sr::CellTerm* cells,const Job* jobs,uint32_t n,int count,int positive,uint32_t* prepared){
uint32_t k=blockIdx.x*blockDim.x+threadIdx.x;if(k>=n)return;const auto job=jobs[k];
for(int i=0;i<count;++i){uint64_t a=job.root+cells[uint64_t(job.position)*count+i].term;
for(int i=0;i<positive;++i){uint64_t a=job.root+cells[uint64_t(job.position)*count+i].term;
prepared[(uint64_t(k)*count+i)*2]=a>>18;
prepared[(uint64_t(k)*count+i)*2+1]=((((a^sr::kCombinedXor)&262143)*sr::kJavaMultiplier+11)>>17);
}
@ -61,22 +84,22 @@ __host__ __device__ uint32_t groupMask(const uint32_t*p,uint32_t base,int count)
return mask;
}
struct Emit{
Coord p;Match* out;uint64_t capacity;unsigned long long* total;
__device__ void operator()(uint64_t seed){uint64_t slot=atomicAdd(total,1ULL);if(slot<capacity)out[slot]={(seed-sr::coordinateTerm(p.x,p.z))&sr::kMask48,p.x,p.z};}
Coord p;uint64_t originTerm;const sr::CellTerm* negative;int negativeCount;Match* out;uint64_t capacity;unsigned long long* total;
__device__ void operator()(uint64_t seed){if(!sr::verifyPattern(seed,negative,negativeCount))return;uint64_t slot=atomicAdd(total,1ULL);if(slot<capacity)out[slot]={(seed-originTerm)&sr::kMask48,p.x,p.z};}
};
__global__ void searchKernel(const sr::CellTerm*cells,const Coord*coords,const Job*jobs,uint32_t n,int count,const uint32_t*prepared,Match*out,uint64_t capacity,unsigned long long*total){
__global__ void searchKernel(const sr::CellTerm*cells,const Coord*coords,const Job*jobs,uint32_t n,int count,int positive,int originX,int originZ,const uint32_t*prepared,Match*out,uint64_t capacity,unsigned long long*total){
uint64_t index=uint64_t(blockIdx.x)*blockDim.x+threadIdx.x;if(index>=(uint64_t(n)<<13))return;
uint32_t g=index&8191,k=index>>13,base=((g>>4)<<8)+(g&15);const uint32_t* p=prepared+uint64_t(k)*count*2;
uint32_t mask=groupMask(p,base,count);
Job job=jobs[k];Emit emit{coords[job.position],out,capacity,total};
while(mask){uint32_t j=__ffs(mask)-1;mask&=mask-1;fs::solveIntervals(job.root+(uint64_t(base+16*j)<<18),cells+uint64_t(job.position)*count,count,emit);}
uint32_t mask=groupMask(p,base,positive);
Job job=jobs[k];Coord origin=coords[job.position];Emit emit{origin,sr::coordinateTerm(origin.x+originX,origin.z+originZ),cells+uint64_t(job.position)*count+positive,count-positive,out,capacity,total};
while(mask){uint32_t j=__ffs(mask)-1;mask&=mask-1;fs::solveIntervals(job.root+(uint64_t(base+16*j)<<18),cells+uint64_t(job.position)*count,positive,emit);}
}
template<class T>struct Buffer{T* p=nullptr;explicit Buffer(size_t n){if(n)CUDA(cudaMalloc(&p,n*sizeof(T)));}~Buffer(){if(p)cudaFree(p);}Buffer(const Buffer&)=delete;};
struct Metrics{int device;std::string name;uint64_t positions=0,classes=0,cacheHits=0,jobs=0,matches=0;double setupMs=0,scanMs=0,rootMs=0,searchMs=0;Clock::time_point start,end;std::vector<Match> sample;};
struct Shared{std::atomic<uint64_t> next{0},done{0};std::atomic<int> finished{0};int ready=0,workerCount=0;std::condition_variable cv;std::atomic<bool> cancel{false};std::mutex mutex;std::string error;};
void worker(const Config& c,int device,Shared& shared,Metrics& result){try{
auto setup=Clock::now();CUDA(cudaSetDevice(device));cudaDeviceProp prop{};CUDA(cudaGetDeviceProperties(&prop,device));result.device=device;result.name=prop.name;
int count=c.size*c.size;size_t rootsCapacity=size_t(c.positionBatch)*(1U<<17);
int count=c.pattern.cells.size(),positive=c.pattern.positive;size_t rootsCapacity=size_t(c.positionBatch)*(1U<<17);
Buffer<sr::CellTerm> dcells(size_t(c.positionBatch)*count);Buffer<Coord>dcoords(c.positionBatch);Buffer<Job>droots(rootsCapacity),djobs(c.jobBatch);
Buffer<uint32_t>prep(size_t(c.jobBatch)*count*2);Buffer<unsigned long long>total(1);Buffer<Match>matches(c.maxResults);
std::map<uint64_t,std::vector<uint32_t>> cache;std::deque<uint64_t> fifo;size_t cachedRoots=0;
@ -95,9 +118,9 @@ void worker(const Config& c,int device,Shared& shared,Metrics& result){try{
if(it!=cache.end()){lists[key]=it->second;++result.cacheHits;}else{lists[key]={};missing.push_back(p);missingKeys.push_back(key);}
}
if(!missing.empty()){
auto start=Clock::now();std::vector<sr::CellTerm> cells(missing.size()*count);for(size_t i=0;i<missing.size();++i)terms(missing[i],c.size,cells.data()+i*count);
auto start=Clock::now();std::vector<sr::CellTerm> cells(missing.size()*count);for(size_t i=0;i<missing.size();++i)terms(missing[i],c.pattern,cells.data()+i*count);
CUDA(cudaMemcpy(dcells.p,cells.data(),cells.size()*sizeof(sr::CellTerm),cudaMemcpyHostToDevice));CUDA(cudaMemset(total.p,0,sizeof(*total.p)));
rootsKernel<<<unsigned((uint64_t(missing.size())<<18)/256),256>>>(dcells.p,missing.size(),count,droots.p,total.p);CUDA(cudaGetLastError());
rootsKernel<<<unsigned((uint64_t(missing.size())<<18)/256),256>>>(dcells.p,missing.size(),count,positive,droots.p,total.p);CUDA(cudaGetLastError());
unsigned long long found;CUDA(cudaMemcpy(&found,total.p,sizeof(found),cudaMemcpyDeviceToHost));if(found>rootsCapacity)throw std::runtime_error("root capacity invariant failed");
std::vector<Job> roots(found);if(found)CUDA(cudaMemcpy(roots.data(),droots.p,found*sizeof(Job),cudaMemcpyDeviceToHost));
for(auto job:roots)lists[missingKeys[job.position]].push_back(job.root);
@ -112,12 +135,12 @@ void worker(const Config& c,int device,Shared& shared,Metrics& result){try{
std::vector<Coord> active;std::vector<sr::CellTerm> cells;std::vector<Job> jobs;jobs.reserve(c.jobBatch);
for(uint32_t i=0;i<num;++i)if(!lists.at(keys[i]).empty())active.push_back(coords[i]);
if(!active.empty()){
cells.resize(active.size()*count);for(size_t i=0;i<active.size();++i)terms(active[i],c.size,cells.data()+i*count);
cells.resize(active.size()*count);for(size_t i=0;i<active.size();++i)terms(active[i],c.pattern,cells.data()+i*count);
CUDA(cudaMemcpy(dcells.p,cells.data(),cells.size()*sizeof(sr::CellTerm),cudaMemcpyHostToDevice));CUDA(cudaMemcpy(dcoords.p,active.data(),active.size()*sizeof(Coord),cudaMemcpyHostToDevice));
auto search=[&](){if(jobs.empty()||shared.cancel)return;auto start=Clock::now();
CUDA(cudaMemcpy(djobs.p,jobs.data(),jobs.size()*sizeof(Job),cudaMemcpyHostToDevice));CUDA(cudaMemset(total.p,0,sizeof(*total.p)));
prepareKernel<<<unsigned((jobs.size()+127)/128),128>>>(dcells.p,djobs.p,jobs.size(),count,prep.p);
searchKernel<<<unsigned((uint64_t(jobs.size())<<13)/256),256>>>(dcells.p,dcoords.p,djobs.p,jobs.size(),count,prep.p,matches.p,c.maxResults,total.p);CUDA(cudaGetLastError());
prepareKernel<<<unsigned((jobs.size()+127)/128),128>>>(dcells.p,djobs.p,jobs.size(),count,positive,prep.p);
searchKernel<<<unsigned((uint64_t(jobs.size())<<13)/256),256>>>(dcells.p,dcoords.p,djobs.p,jobs.size(),count,positive,c.pattern.cells.front().x,c.pattern.cells.front().z,prep.p,matches.p,c.maxResults,total.p);CUDA(cudaGetLastError());
unsigned long long found;CUDA(cudaMemcpy(&found,total.p,sizeof(found),cudaMemcpyDeviceToHost));if(found>UINT64_MAX-result.matches)throw std::runtime_error("match count exceeds uint64");result.matches+=found;result.jobs+=jobs.size();
size_t take=std::min<uint64_t>(found,c.maxResults-result.sample.size());if(take){size_t old=result.sample.size();result.sample.resize(old+take);CUDA(cudaMemcpy(result.sample.data()+old,matches.p,take*sizeof(Match),cudaMemcpyDeviceToHost));}
result.searchMs+=std::chrono::duration<double,std::milli>(Clock::now()-start).count();jobs.clear();};
@ -130,12 +153,15 @@ void worker(const Config& c,int device,Shared& shared,Metrics& result){try{
}catch(const std::exception& e){shared.cancel=true;std::lock_guard<std::mutex> lock(shared.mutex);shared.error="GPU "+std::to_string(device)+": "+e.what();shared.cv.notify_all();}}
std::string escape(const std::string&s){std::ostringstream o;for(unsigned char ch:s){if(ch=='"'||ch=='\\')o<<'\\'<<char(ch);else if(ch<32)o<<"\\u"<<std::hex<<std::setw(4)<<std::setfill('0')<<int(ch)<<std::dec;else o<<char(ch);}return o.str();}
void help(){std::cout<<R"(slime-bench --radius R [options]
Search all effective 48-bit seeds for solid n x n slime chunk squares.
Search effective 48-bit seeds for slime chunk patterns.
Ordinary candidate branch only; rejection-tail supplement is NOT included.
--x X --z Z Center chunk coordinates (default 0,0)
--radius R Manhattan radius in chunks (required, >=0)
--size N Square side, 1..6 (default 5)
--size N Solid square side, 1..6 (default 5)
--pattern TEXT Rows separated by /; # slime, . non-slime, ? ignored
--pattern-file FILE Same pattern, one row per line (dimensions 1..64)
Requires at least one #; exclusive with --size
--devices 0,1,... CUDA device indices, or all (default 0)
--repeat N Complete passes; fresh root cache each pass (default 1)
--position-batch N Anchors per scheduled batch, 1..256 (default 32)
@ -153,20 +179,28 @@ this diamond. Coordinates are not clipped to Minecraft's world border.
Small squares can have enormous numbers of matching seeds and take very long.
)";}
int main(int argc,char**argv){try{
Config c;bool radiusSet=false;for(int i=1;i<argc;++i){std::string a=argv[i];if(a=="--help"||a=="-h"){help();return 0;}if(a=="--dry-run"){c.dry=true;continue;}if(a=="--list-devices"){c.list=true;continue;}
Config c;bool radiusSet=false,sizeSet=false;std::string patternText;bool patternSet=false;for(int i=1;i<argc;++i){std::string a=argv[i];if(a=="--help"||a=="-h"){help();return 0;}if(a=="--dry-run"){c.dry=true;continue;}if(a=="--list-devices"){c.list=true;continue;}
if(i+1>=argc)throw std::runtime_error("missing value for "+a);std::string v=argv[++i];
auto bounded=[&](uint64_t lo,uint64_t hi){auto n=number(v);if(n<lo||n>hi)throw std::runtime_error("out of range: "+a);return n;};
if(a=="--x")c.x=signed32(v);else if(a=="--z")c.z=signed32(v);else if(a=="--radius"){c.radius=bounded(0,INT32_MAX);radiusSet=true;}
else if(a=="--size")c.size=bounded(1,6);else if(a=="--devices")c.devices=v;else if(a=="--repeat")c.repeats=bounded(1,1000);
else if(a=="--size"){c.size=bounded(1,6);sizeSet=true;}
else if(a=="--pattern"||a=="--pattern-file"){if(patternSet)throw std::runtime_error("specify only one pattern");patternSet=true;
if(a=="--pattern")patternText=v;else{std::ifstream file(v);if(!file)throw std::runtime_error("cannot read pattern: "+v);std::ostringstream text;text<<file.rdbuf();patternText=text.str();}}else if(a=="--devices")c.devices=v;else if(a=="--repeat")c.repeats=bounded(1,1000);
else if(a=="--position-batch")c.positionBatch=bounded(1,256);else if(a=="--job-batch")c.jobBatch=bounded(1,65536);
else if(a=="--cache-classes")c.cacheClasses=bounded(0,1000000);else if(a=="--max-results")c.maxResults=bounded(0,1000000);
else if(a=="--output")c.output=v;else throw std::runtime_error("unknown option: "+a);
}
if(c.list){int n;CUDA(cudaGetDeviceCount(&n));for(int i=0;i<n;++i){cudaDeviceProp p{};CUDA(cudaGetDeviceProperties(&p,i));std::cout<<i<<"\t"<<p.name<<"\tsm_"<<p.major<<p.minor<<"\t"<<p.totalGlobalMem/(1ULL<<20)<<" MiB\n";}return 0;}
if(!radiusSet)throw std::runtime_error("--radius is required");
for(int64_t center:{int64_t(c.x),int64_t(c.z)})if(center-int64_t(c.radius)<INT32_MIN||center+int64_t(c.radius)+c.size-1>INT32_MAX)throw std::runtime_error("coordinates including square must fit int32");
if(patternSet&&sizeSet)throw std::runtime_error("--size and pattern options are mutually exclusive");
if(!patternSet){for(int z=0;z<c.size;++z){if(z)patternText+='/';patternText+=std::string(c.size,'#');}}
c.pattern=parsePattern(patternText);
for(auto axis:{std::make_pair(c.x,c.pattern.width),std::make_pair(c.z,c.pattern.height)})
if(int64_t(axis.first)-int64_t(c.radius)<INT32_MIN||int64_t(axis.first)+int64_t(c.radius)+axis.second-1>INT32_MAX)throw std::runtime_error("coordinates including pattern must fit int32");
if(!c.output.empty()&&std::ifstream(c.output).good())throw std::runtime_error("output already exists: "+c.output);
std::ostringstream report;report<<std::setprecision(12)<<"{\"schema_version\":1,\"mode\":\"ordinary_all_effective_seeds\",\"rejection_tail_included\":false,\"center\":["<<c.x<<","<<c.z<<"],\"radius_chunks\":"<<c.radius<<",\"square_size\":"<<c.size<<",\"anchors\":"<<anchors(c.radius)<<",\"effective_seed_bits\":48,\"dry_run\":"<<(c.dry?"true":"false");
std::ostringstream report;report<<std::setprecision(12)<<"{\"schema_version\":1,\"mode\":\"ordinary_all_effective_seeds\",\"rejection_tail_included\":false,\"center\":["<<c.x<<","<<c.z<<"],\"radius_chunks\":"<<c.radius<<",\"square_size\":"<<(patternSet?"null":std::to_string(c.size))<<",\"anchors\":"<<anchors(c.radius)<<",\"effective_seed_bits\":48,\"dry_run\":"<<(c.dry?"true":"false");
report<<",\"pattern_width\":"<<c.pattern.width<<",\"pattern_height\":"<<c.pattern.height<<",\"slime_cells\":"<<c.pattern.positive<<",\"non_slime_cells\":"<<c.pattern.cells.size()-c.pattern.positive<<",\"pattern\":[";
for(size_t i=0;i<c.pattern.rows.size();++i){if(i)report<<",";report<<"\""<<c.pattern.rows[i]<<"\"";}report<<"]";
if(c.dry){auto first=coordinate(c,0),last=coordinate(c,anchors(c.radius)-1);report<<",\"first_anchor\":["<<first.x<<","<<first.z<<"],\"last_anchor\":["<<last.x<<","<<last.z<<"]}";}
else{
int available;CUDA(cudaGetDeviceCount(&available));std::vector<int> devices;if(c.devices=="all"){for(int i=0;i<available;++i)devices.push_back(i);}else{

View file

@ -26,8 +26,31 @@ def main():
assert r['anchors']==1+2*radius*(radius+1)
assert r['first_anchor']==[-123,456-radius] and r['last_anchor']==[-123,456+radius]
for bad in [[],['--radius','-1'],['--radius','1','--size','0'],['--radius','1','--size','7'],['--radius','abc'],['--radius','0','--x','2147483647'],['--radius','1','--repeat','0'],['--radius','1','--position-batch','0']]:run(*bad,'--dry-run',ok=False)
summary={'cli_scope_and_errors':True}
pattern='?##./#..?/..#?'
r=run('--radius',0,'--pattern',pattern,'--dry-run')
assert r['pattern']==pattern.split('/') and r['square_size'] is None
assert (r['pattern_width'],r['pattern_height'],r['slime_cells'],r['non_slime_cells'])==(4,3,4,5)
for bad_pattern in ['', '.', '??', '#/##', '#x', '#//.', '#/'+'#'*65]:
run('--radius',0,'--pattern',bad_pattern,'--dry-run',ok=False)
run('--radius',0,'--size',5,'--pattern','#','--dry-run',ok=False)
run('--radius',0,'--pattern','#','--pattern','#','--dry-run',ok=False)
run('--radius',0,'--pattern-file','/nonexistent-pattern','--dry-run',ok=False)
run('--radius',0,'--x',2147483647,'--pattern','?#','--dry-run',ok=False)
r=run('--radius',0,'--x',2147483647,'--pattern','#/#','--dry-run')
with tempfile.TemporaryDirectory() as tmp:
path=Path(tmp)/'pattern.txt';path.write_bytes(b'?##.\r\n#..?\r\n..#?\r\n')
assert run('--radius',0,'--pattern-file',path,'--dry-run')['pattern']==pattern.split('/')
summary={'cli_scope_and_errors':True,'pattern_inputs':True}
if args.gpu:
# A mixed-mask regression fixture; this is not the target creeper pattern.
fixture=json.loads((HERE/'testdata/mixed-mask-regression.json').read_text())
face=run('--x',fixture['chunk_x'],'--z',fixture['chunk_z'],'--radius',0,
'--pattern','/'.join(fixture['pattern']),'--max-results',1000)
hits=face['rounds'][0]
assert not hits['sample_truncated'] and any(s==fixture['seed48'] for s,x,z in hits['sample'])
for seed,x,z in hits['sample']:
assert all(slime(seed,x+dx,z+dz)==(ch=='#') for dz,row in enumerate(fixture['pattern']) for dx,ch in enumerate(row))
summary['mixed_mask_verified_matches']=hits['matches']
common=['--x',51069,'--z',419124]
reference=run(*common,'--radius',0,'--size',5,'--repeat',2,'--max-results',100)
expected={61692151016558,15959335054446}
@ -45,6 +68,36 @@ def main():
wanted={t for t in samples[0] if all(slime(t[0],t[1]+dx,t[2]+dz) for dx in range(n) for dz in range(n))}
assert set(map(tuple,r['rounds'][0]['sample']))==wanted
summary[f'size_{n}']=r['rounds'][0]['matches']
# The same 5x5 positive constraints, shifted inside a non-square mask.
# Empty anchor exercises normalization independently of the first #.
rows=['?'*7]+['?'+'#'*5+'.' for _ in range(5)]+['?.?????']
pattern='/'.join(rows)
masked=run('--x',51068,'--z',419123,'--radius',0,'--pattern',pattern,'--max-results',100)
wanted={(seed,51068,419123) for seed,x,z in samples[0]
if all(ch=='?' or slime(seed,51068+dx,419123+dz)==(ch=='#')
for dz,row in enumerate(rows) for dx,ch in enumerate(row))}
assert wanted and set(map(tuple,masked['rounds'][0]['sample']))==wanted
assert masked['rounds'][0]['matches']==len(wanted)
# Identical positive subset, but ignored rather than forbidden background.
ignored=pattern.replace('.', '?')
loose=run('--x',51068,'--z',419123,'--radius',0,'--pattern',ignored,'--max-results',100)
assert {s for s,x,z in loose['rounds'][0]['sample']}=={s for s,x,z in samples[0]}
# Force a forbidden cell to reject at least one known positive candidate.
seed=next(iter(samples[0]))[0]
dx,dz=next((dx,dz) for dz in range(16) for dx in range(16)
if not (1<=dx<=5 and 1<=dz<=5) and slime(seed,51068+dx,419123+dz))
rejection=[list('?'*16) for _ in range(16)]
for z in range(1,6):
for x in range(1,6):rejection[z][x]='#'
rejection[dz][dx]='.'
rejected=run('--x',51068,'--z',419123,'--radius',0,'--pattern',
'/'.join(''.join(row) for row in rejection),'--max-results',100)
expected_seeds={s for s,x,z in samples[0] if not slime(s,51068+dx,419123+dz)}
assert len(expected_seeds)<24
assert {s for s,x,z in rejected['rounds'][0]['sample']}==expected_seeds
assert rejected['rounds'][0]['matches']==len(expected_seeds)
summary['mixed_pattern_vs_independent_filter']=len(wanted)
summary['forbidden_cell_excludes_candidates']=True
diamond=run(*common,'--radius',2,'--size',5,'--max-results',1000,'--position-batch',3)
split=[]
for dz in range(-2,3):

View file

@ -2,9 +2,17 @@
#include "slime_bench.cu"
#undef main
#include <random>
struct HostEmit{
const sr::CellTerm* cells;int count;std::set<uint64_t>* result;
__host__ __device__ void operator()(uint64_t seed){
#ifndef __CUDA_ARCH__
if(sr::verifyPattern(seed,cells,count))result->insert(seed);
#endif
}
};
void check(bool ok,const char*msg){if(!ok)throw std::runtime_error(msg);}
int main(){
uint64_t points=0,groups=0;std::mt19937_64 rng(123456);
uint64_t points=0,groups=0,mixed=0;std::mt19937_64 rng(123456);
for(int r=0;r<=64;++r){Config c;c.x=-123;c.z=98;c.radius=r;std::set<std::pair<int32_t,int32_t>> actual;
for(uint64_t i=0;i<anchors(r);++i){auto p=coordinate(c,i);actual.emplace(p.x,p.z);}
check(actual.size()==anchors(r),"duplicate anchor");
@ -21,5 +29,36 @@ int main(){
uint32_t expected=0;for(int j=0;j<16;++j)if(fs::differenceFilter(root+(uint64_t(base+16*j)<<18),cells.data(),count))expected|=1U<<j;
check(groupMask(prepared.data(),base,count)==expected,"variable-size group mask mismatch");++groups;
}
std::cout<<"{\"diamond_points\":"<<points<<",\"variable_size_groups\":"<<groups<<",\"passed\":true}\n";
// Compare arbitrary sparse layouts and forbidden cells against all high bits.
for(int t=0;t<128;++t){
Coord coord{int32_t(rng()),int32_t(rng())};
// Keep offsets away from overflow even for signed boundary samples.
coord.x/=2;coord.z/=2;
uint64_t world=rng()&sr::kMask48;
std::string text;
for(int z=0;z<5;++z){if(z)text+='/';for(int x=0;x<7;++x)
text+=(x==0&&z==0)||rng()%4==0?'?':sr::exactIsSlimeFromTerm(world,sr::coordinateTerm(coord.x+x,coord.z+z))?'#':'.';}
if(text.find('#')==std::string::npos)continue;
auto pattern=parsePattern(text);int count=pattern.cells.size();
std::vector<sr::CellTerm> cells(count);terms(coord,pattern,cells.data());
uint64_t normalized=(world+sr::coordinateTerm(coord.x+pattern.cells.front().x,coord.z+pattern.cells.front().z))&sr::kMask48;
check(sr::verifyPattern(normalized,cells.data(),count),"normalized pattern mismatch");
uint64_t low=normalized&((1ULL<<35)-1);std::set<uint64_t> actual,expected;
HostEmit emit{cells.data()+pattern.positive,count-pattern.positive,&actual};
fs::solveIntervals(low,cells.data(),pattern.positive,emit);
for(uint64_t high=0;high<8192;++high){uint64_t seed=low+(high<<35);
// Ordinary branch requires a successful first draw at positive cells.
bool ordinary=true;for(int i=0;i<pattern.positive;++i)
if((sr::nextStateFromTerm(seed,cells[i].term)>>17)%10!=0)ordinary=false;
if(ordinary&&sr::verifyPattern(seed,cells.data(),count))expected.insert(seed);
}
check(actual==expected,"mixed pattern high-bit solver mismatch");++mixed;
uint32_t root=low&262143,base=uint32_t(low>>18)&~240U;
std::vector<uint32_t> prepared(count*2);
for(int i=0;i<pattern.positive;++i){uint64_t a=root+cells[i].term;
prepared[2*i]=a>>18;prepared[2*i+1]=((((a^sr::kCombinedXor)&262143)*sr::kJavaMultiplier+11)>>17);}
uint32_t wanted=0;for(int j=0;j<16;++j)if(fs::differenceFilter(root+(uint64_t(base+16*j)<<18),cells.data(),pattern.positive))wanted|=1U<<j;
check(groupMask(prepared.data(),base,pattern.positive)==wanted,"sparse pattern group mask mismatch");
}
std::cout<<"{\"diamond_points\":"<<points<<",\"variable_size_groups\":"<<groups<<",\"mixed_pattern_slices\":"<<mixed<<",\"passed\":true}\n";
}

View file

@ -0,0 +1,26 @@
{
"pattern": [
"..##..",
"..##..",
"##..##",
"#....#",
"#....#",
"#.##.#"
],
"seed48": 100568620033461,
"chunk_x": -137,
"chunk_z": -53,
"independent_verification": true,
"search": {
"center": [
0,
0
],
"radius_chunks": 256,
"anchors": 131585,
"matches": 1248,
"scan_wall_ms": 2968.438239,
"gpu": "NVIDIA GeForce RTX 3060 Laptop GPU",
"rejection_tail_included": false
}
}

View file

@ -1,8 +1,8 @@
# 共用算法头文件
- `slime/solver_core.hpp`:Java 整数回绕、48 位随机数、精确史莱姆判定、低位约束和原高位求解器。
- `slime/upper_interval.hpp`:输出差必要条件与高位区间求交,用于实心图案的普通候选分支。
- `slime/upper_interval.hpp`:输出差必要条件与高位区间求交,用于正约束(可稀疏图案)的普通候选分支;CLI 在发出结果前核对非史莱姆约束。
CLI 直接依赖这里。`research/slime_solver_core.hpp` 和原实验目录的 `interval_solver.hpp` 仅转发包含,以便已有研究程序、教学代码和历史复现命令继续工作。
本次只迁移实现位置,保留现有命名空间及算法语义。精确验证支持 Java 重试,不代表普通候选生成已经包含拒绝尾部补扫。
保留现有命名空间及算法语义。精确验证支持 Java 重试,不代表普通候选生成已经包含拒绝尾部补扫。

View file

@ -13,8 +13,9 @@
namespace full_search_experiment {
namespace sr = slime_research;
// Solid patterns only. Each surviving residue has a single interval.
// Keeping just the anchor's two residues makes intersection constant-space.
// Positive (slime) constraints only; positions may be sparse.
// Each surviving residue has a single interval. Keeping the first cell's
// two residues makes intersection constant-space. Check negative cells on emit.
template<class Emit>
FS_HD FS_INLINE void solveIntervals(uint64_t low, const sr::CellTerm* cells,
int count, Emit& emit) {