From aaf35c650510fcae330e92ef19503a58906c0e10 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 16 Oct 2026 23:28:09 -0700 Subject: [PATCH] Add opt-in shifted raw remap GPU lifecycle gate --- .../multigrid/test_shifted_remap_gpu.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 tests/validation/multigrid/test_shifted_remap_gpu.py diff --git a/tests/validation/multigrid/test_shifted_remap_gpu.py b/tests/validation/multigrid/test_shifted_remap_gpu.py new file mode 100644 index 0000000..a225194 --- /dev/null +++ b/tests/validation/multigrid/test_shifted_remap_gpu.py @@ -0,0 +1,113 @@ +"""Opt-in GPU raw shifted remap lifecycle gates; no precision/performance claim. + +Run only on an authorized GPU: + PYTHONPATH=src python tests/validation/multigrid/test_shifted_remap_gpu.py -v +Frozen gates: bitwise raw overlap/copy and identity continuation; moved-remap +matches the existing host fill loop exactly; finite owned leaves after stepping. +""" +from contextlib import ExitStack +import unittest +import numpy as np +import pycuda.driver as cuda +from CelerisLab.config import LBMConfig +from CelerisLab.cuda.context import CudaContext +from CelerisLab.lbm.population_storage import _StoredPopulations +from CelerisLab.lbm.refinement_stepper import FixedPatchStepper +from CelerisLab.lbm.synchronized_remap import RemapTopology, _remap_stored + +CFG=LBMConfig(nx=48,ny=32,viscosity=.1,ddf_shifting=True) +PARENTS=((4,4,20,28),(24,4,44,28)) +A=((16,16,32,40),) +B=((20,16,36,40),) + + +def make(ctx): + return FixedPatchStepper(CFG,PARENTS,context=ctx,base_block=(4,4), + fine_block=(4,4),child_block=(4,4), + child_rectangles_in_parent=A) + + +def initial(run): + # Non-unit density, nonzero velocity and nonequilibrium perturbations. + w=np.array([4/9,*([1/9]*4),*([1/36]*4)],np.float64) + cx=np.array([0,1,-1,0,0,1,-1,1,-1]);cy=np.array([0,0,0,1,-1,1,-1,-1,1]) + rho,u,v=.99,.013,-.007 + cu=3*(cx*u+cy*v) + eq=w*((rho-1)+rho*(cu+.5*cu*cu-1.5*(u*u+v*v))) + rng=np.random.default_rng(8473) + arrays=tuple((np.broadcast_to(eq[:,None,None],level.shape)+ + rng.normal(0,1e-7,level.shape)).astype(np.float32) for level in run._levels) + return _StoredPopulations(arrays,True) + + +class ShiftedRemapGPU(unittest.TestCase): + def exact(self,a,b,label): + self.assertEqual(a.shape,b.shape,label) + self.assertEqual(a.dtype,b.dtype,label) + self.assertEqual(a.tobytes(),b.tobytes(),label) + + def snapshot_equal(self,a,b,label): + self.assertTrue(a.shifted and b.shifted) + self.assertEqual(len(a.values),len(b.values)) + for x,y in zip(a.values,b.values):self.exact(x,y,label) + + def check_reset(self,run,wanted): + for level,want in zip(run._levels,wanted): + blocked=level.layout.to_blocked(want) + for pointer in (level.current,level.spare): + actual=np.empty(level.layout.blocked_shape,np.float32) + cuda.memcpy_dtoh(actual,pointer) + self.exact(actual,blocked,'fresh current and spare') + + def check_finite(self,run): + raw=run._download_raw() + self.assertTrue(raw.shifted) + for a,leaf in zip(raw.values,run._leaf): + self.assertTrue(np.isfinite(a[:,leaf]).all()) + self.assertTrue(np.isnan(a[:,~leaf]).all()) + self.assertTrue((1+a[:,leaf].sum(axis=0,dtype=np.float64)>0).all()) + + def test_identity_next_step_bitwise(self): + with CudaContext(0) as ctx,ExitStack() as owned: + old=owned.enter_context(make(ctx)) + old._initialize_raw(initial(old));old.step(2) + before=old._download_raw() + new=owned.enter_context(old.remap_child(A)) + self.check_reset(new,before.values) + self.snapshot_equal(old._download_raw(),before,'old unchanged') + self.snapshot_equal(new._download_raw(),before,'identity raw copy') + old.step();new.step() + self.assertEqual((old.step_count,new.step_count),(3,3)) + self.snapshot_equal(old._download_raw(),new._download_raw(),'identity next step') + self.check_finite(old);self.check_finite(new) + + def test_moved_remap_matches_raw_host_and_old_remains_usable(self): + with CudaContext(0) as ctx,ExitStack() as owned: + old=owned.enter_context(make(ctx)) + old._initialize_raw(initial(old));old.step(2) + before=old._download_raw();count=old.step_count + old_mesh=RemapTopology.from_config(CFG,old.patch,A) + new_mesh=RemapTopology.from_config(CFG,old.patch,B) + expected=_remap_stored(old_mesh,new_mesh,before,shifted=True, + base_count=count,level_steps=(count,2*count,4*count),phase='postcollision_g') + new=owned.enter_context(old.remap_child(B)) + self.check_reset(new,expected.g) + self.snapshot_equal(new._download_raw(),_StoredPopulations(expected.g,True),'host raw fill') + for a,b,om,nm in zip(before.values,new._download_raw().values,old_mesh.leaf_masks,new_mesh.leaf_masks): + self.exact(a[:,om&nm],b[:,om&nm],'physical same-level overlap') + self.snapshot_equal(old._download_raw(),before,'source unchanged') + old.step();new.step(2) + self.assertEqual((old.step_count,new.step_count),(3,4)) + self.check_finite(old);self.check_finite(new) + + def test_rejected_remap_does_not_mutate_source(self): + with CudaContext(0) as ctx,make(ctx) as run: + run._initialize_raw(initial(run));run.step(2) + before=run._download_raw() + with self.assertRaises(ValueError):run.remap_child([]) + self.snapshot_equal(run._download_raw(),before,'rejection preserves source') + self.assertEqual(run.step_count,2) + run.step();self.check_finite(run) + + +if __name__=='__main__':unittest.main()