diff --git a/tests/validation/multigrid/test_refinement_followup.py b/tests/validation/multigrid/test_refinement_followup.py index 6907ab1..fbe1b6d 100644 --- a/tests/validation/multigrid/test_refinement_followup.py +++ b/tests/validation/multigrid/test_refinement_followup.py @@ -17,6 +17,7 @@ import pycuda.driver as cuda import CelerisLab from CelerisLab.lbm import refinement_stepper as implementation +from CelerisLab.lbm.blocks import BlockLayout2D from test_refinement_gpu import (ROOT, POP_ATOL, MOMENT_ATOL, ref, postcollision, totals, LBMConfig, CudaContext, compiler, PeriodicPatchStepper) @@ -83,6 +84,21 @@ def scalar_interval(coarse, fine, bounds, slot=None): return result, current +def whole_tiles(cfg, bounds): + """One whole-level tile per level: (ny, nx) at the root, the fine window below.""" + return [(cfg.ny, cfg.nx), implementation.FixedPatch2D.from_config(cfg, bounds).fine_shape] + + +def owner_arrays(backend): + """Raw owner storage of every level as dense (9, ny, nx), before public masking.""" + arrays = [] + for level in backend._levels: + blocked = np.empty(level.layout.blocked_shape, np.float32) + cuda.memcpy_dtoh(blocked, level.current) + arrays.append(level.layout.to_dense(blocked)) + return arrays + + class FaultGuardsCPU(unittest.TestCase): def test_boundary_mode_and_margin_contract(self): cfg = LBMConfig(nx=32, ny=24, viscosity=.1, velocity=0, wall_normal_velocity=.001, @@ -95,7 +111,7 @@ class FaultGuardsCPU(unittest.TestCase): with self.subTest(mode=mode, bounds=bounds): with self.assertRaises(ValueError): implementation.FixedPatchStepper(cfg, context=context, coarse_boundary=mode, - refinements=[bounds]) + refinements=[bounds], level_blocks=[(1, 1)] * 2) context.activate.assert_not_called() # Both exact channel limits and the original periodic two-cell margin # must reach context activation, with no allocation or device required. @@ -104,18 +120,19 @@ class FaultGuardsCPU(unittest.TestCase): with self.subTest(mode=mode, bounds=bounds): with self.assertRaisesRegex(RuntimeError, "validated before CUDA"): implementation.FixedPatchStepper(cfg, context=context, coarse_boundary=mode, - refinements=[bounds]) + refinements=[bounds], level_blocks=[(1, 1)] * 2) def fake_backend(self): backend = PeriodicPatchStepper.__new__(PeriodicPatchStepper) - backend._layouts = [None, None] # dense storage on both levels + layout = BlockLayout2D.build(1, 1, 1, 1, periodic=(False, False)) + backend._layouts = [layout, layout] # blocked storage on both levels backend._ddf_shifting = False # unshifted on every level, as _check_storage expects backend._level_cfgs = [LBMConfig(nx=16, ny=16, viscosity=.1)] * 2 # ddf_shifting off backend.ctx = Mock(activate=nullcontext) backend._lock = RLock() backend._status = "ready" backend._stream = Mock() - backend._levels = [Mock(shape=(9, 1, 1), layout=None), Mock(shape=(9, 1, 1), layout=None)] + backend._levels = [Mock(shape=(9, 1, 1), layout=layout), Mock(shape=(9, 1, 1), layout=layout)] backend._allocations = [Mock(), Mock()] return backend @@ -158,7 +175,8 @@ class FaultGuardsCPU(unittest.TestCase): context = Mock(activate=nullcontext, sm_arch="sm_70") with patch.object(implementation, "HALO", 2): with self.assertRaisesRegex(RuntimeError, "four-cell"): - PeriodicPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)]) + PeriodicPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)], + level_blocks=[(16, 16), (24, 24)]) p = implementation.FixedPatch2D.from_config(cfg, (4, 4, 12, 12)) from CelerisLab.lbm import hierarchy bad = patch.object(implementation.FixedPatch2D, "from_config", @@ -169,10 +187,12 @@ class FaultGuardsCPU(unittest.TestCase): with patch.object(cuda, "Stream"), patch.object(cuda, "mem_get_info", return_value=(1 << 40, 1 << 40)), bad, patch.object(cuda, "mem_alloc") as alloc, \ patch.object(cuda, "memcpy_htod") as upload: with self.assertRaisesRegex(RuntimeError, "config chain is inconsistent"): - PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds]) + PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds], + level_blocks=[(16, 16), (24, 24)]) with patch.object(hierarchy, "_check_chain"), \ self.assertRaisesRegex(RuntimeError, "omega/tau"): - PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds]) + PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds], + level_blocks=[(16, 16), (24, 24)]) alloc.assert_not_called() upload.assert_not_called() module = Mock() @@ -182,7 +202,8 @@ class FaultGuardsCPU(unittest.TestCase): patch.object(compiler, "load_module", return_value=module), \ patch.object(cuda, "memcpy_htod") as upload: with self.assertRaisesRegex(RuntimeError, "layout mismatch"): - PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds]) + PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds], + level_blocks=[(16, 16), (24, 24)]) upload.assert_not_called() @@ -250,23 +271,28 @@ class NormalBuildGPU(unittest.TestCase): parts = line.split() if len(parts) == 3 and parts[0] == "#define": definitions[parts[1]] = parts[2] - generated.append((metadata["config"], definitions)) + generated.append((metadata["config"], definitions, build)) return build max_error = max_bc_error = 0.0 with CudaContext(0) as context: with patch.object(compiler, "generate_config", side_effect=record_config): backend = implementation.FixedPatchStepper(cfg, context=context, refinements=[bounds], - coarse_boundary="slot_reservoir") + coarse_boundary="slot_reservoir", + level_blocks=whole_tiles(cfg, bounds)) with backend: self.assertEqual(len(generated), 2) - fine_config, fine_defines = generated[1] - self.assertEqual((fine_config["nx"], fine_config["ny"]), (fx, fy)) + fine_config, fine_defines, _ = generated[1] + # Blocked kernels see one tile window including the 1-cell halo. + self.assertEqual((fine_config["nx"], fine_config["ny"]), (fx+2, fy+2)) for name in ("Y_WALL_BC", "TOP_WALL_BC", "WALL_SLOT_WIDTH", "WALL_NORMAL_VELOCITY", "TOP_WALL_NORMAL_VELOCITY"): self.assertEqual(float(fine_defines[name].removesuffix("f")), 0, name) - self.assertEqual(int(generated[0][1]["Y_WALL_BC"]), 2) - self.assertEqual(int(generated[0][1]["TOP_WALL_BC"]), 1) + # The root's physical slot/reservoir walls come from BLOCK_ROOT_BC=1 + # (module constant d_block_wall); the fine level has none. + root_meta = json.loads((Path(generated[0][2])/".celeris-build").read_text()) + fine_meta = json.loads((Path(generated[1][2])/".celeris-build").read_text()) + self.assertEqual((root_meta["block_root_bc"], fine_meta["block_root_bc"]), (1, 0)) # Independently derive all target links, including the eight # whose coarse sources are uncovered, from physical midpoints. targets, corners = set(), set() @@ -286,9 +312,7 @@ class NormalBuildGPU(unittest.TestCase): expected = scalar_interval(*expected, bounds, slot=(amplitude, width)) backend.step() self.assertEqual(backend.step_count, step) - raw = [np.empty(a.shape, np.float32) for a in initial] - for level, array in zip(backend._levels, raw): - cuda.memcpy_dtoh(array, level.current) + raw = owner_arrays(backend) # Read raw storage before masking: active NaNs cannot be # hidden by the public download's non-leaf cleanup. self.assertTrue(np.isfinite(raw[0][:, coarse_leaf]).all()) @@ -327,7 +351,7 @@ class NormalBuildGPU(unittest.TestCase): max_pop = max_drift = max_moment = 0.0 with CudaContext(0) as context, PeriodicPatchStepper( LBMConfig(nx=16, ny=16, viscosity=.1), context=context, - refinements=[(4, 4, 12, 12)]) as backend: + refinements=[(4, 4, 12, 12)], level_blocks=[(16, 16), (24, 24)]) as backend: backend.initialize(*postcollision(state, spec, maps)) before = totals(backend.download(), maps) for step in range(1, 9): @@ -358,17 +382,17 @@ class NormalBuildGPU(unittest.TestCase): fine_mask[4:-4, 4:-4] = True initial[1][:, ~fine_mask] = np.nan expected = scalar_interval(*[a.astype(np.float64) for a in initial], bounds) - with CudaContext(0) as context, PeriodicPatchStepper(cfg, context=context, refinements=[bounds]) as backend: + with CudaContext(0) as context, PeriodicPatchStepper(cfg, context=context, refinements=[bounds], + level_blocks=[(13, 17), (14, 16)]) as backend: self.assertEqual(backend.patch.fine_shape, (14, 16)) p = backend.patch - allocated = 2 * 9 * (17*13+16*14) * 4 + sum(a.nbytes for a in - (p.ghost_fine, p.ghost_coarse, p.coalesce_coarse, p.coalesce_fine)) + allocated = 2 * 4 * sum(int(np.prod(level.layout.blocked_shape)) for level in backend._levels) \ + + sum(a.nbytes for a in (p.ghost_fine, p.ghost_coarse, p.coalesce_coarse, p.coalesce_fine)) self.assertLess(allocated, 128*1024) backend.initialize(*initial) backend.step() coarse, fine = backend.download() - raw_fine = np.empty((9, 14, 16), np.float32) - cuda.memcpy_dtoh(raw_fine, backend._levels[1].current) + raw_fine = owner_arrays(backend)[1] max_error = 0.0 for got, want in zip((coarse, raw_fine), expected): np.testing.assert_array_equal(np.isnan(got), np.isnan(want))