diff --git a/tests/validation/multigrid/test_refinement_gpu.py b/tests/validation/multigrid/test_refinement_gpu.py index bafa6d3..ef092bc 100644 --- a/tests/validation/multigrid/test_refinement_gpu.py +++ b/tests/validation/multigrid/test_refinement_gpu.py @@ -52,6 +52,17 @@ def totals(arrays, maps): return np.array([populations.sum(), populations @ ref.CX, populations @ ref.CY]) +def level_storage(level): + """Raw device storage of one level as dense (9, ny, nx), dense or blocked.""" + if level.layout is None: + raw = np.empty(level.shape, np.float32) + cuda.memcpy_dtoh(raw, level.current) + return raw + blocked = np.empty(level.layout.blocked_shape, np.float32) + cuda.memcpy_dtoh(blocked, level.current) + return level.layout.to_dense(blocked) + + def global_array(module, name, shape): out = np.empty(shape, dtype=np.uint32) ptr, size = module.get_global(name) @@ -82,7 +93,7 @@ class CoupledGPU(unittest.TestCase): expected = postcollision(next_state, spec, maps) arrivals = (next_state.coarse_f, next_state.fine_f) - cases = (("dense_unshifted", False, {}), + cases = (("blocked_unshifted", False, dict(level_blocks=[(16, 16), (24, 24)])), ("blocked_shifted", True, dict(level_blocks=[(4, 4), (6, 6)]))) with CudaContext(0) as context: @@ -148,7 +159,8 @@ class CoupledGPU(unittest.TestCase): maps = ref.build_maps(spec) with CudaContext(0) as context: with PeriodicPatchStepper(LBMConfig(nx=16, ny=16, viscosity=.1), - context=context, refinements=[(4, 4, 12, 12)]) as backend: + context=context, refinements=[(4, 4, 12, 12)], + level_blocks=[(16, 16), (24, 24)]) as backend: backend.initialize(*postcollision(ref.make_state(spec), spec, maps)) coarse, fine = backend._levels @@ -164,12 +176,94 @@ class CoupledGPU(unittest.TestCase): for operation in (backend.download, backend.step): with self.assertRaisesRegex(RuntimeError, "failed"): operation() - raw = np.empty(fine.shape, np.float32) - cuda.memcpy_dtoh(raw, fine.current) + raw = level_storage(fine) self.assertTrue(np.isfinite(raw.ravel()[backend.patch.coalesce_fine]).all()) self.assertFalse(context.is_closed) print("partial failure: two fine hops completed, no coarse step published; cleanup PASS", flush=True) + def eight_steps(self, backend, spec, maps, audit): + """Rest and seeded nonequilibrium, 8 steps against the scalar reference. + + audit=True also checks the dense CELERIS_REFINEMENT_AUDIT ownership counters. + """ + p = backend.patch + for case in ("rest", "nonequilibrium_seed1"): + with self.subTest(case=case): + state = ref.make_state(spec, velocity=(0, 0) if case == "rest" else (.03, -.02)) + if case != "rest": + rng = np.random.default_rng(1) + state = ref.State(*(a * (1 + 1e-3 * rng.standard_normal(a.shape)) + for a in (state.coarse_f, state.fine_f))) + initial = postcollision(state, spec, maps) + backend.initialize(*initial) + if audit: # dense audit build only + for level in backend._levels: + for name in ("collision_counts", "explosion_counts", "incoming_counts"): + ptr, size = level.module.get_global(name) + cuda.memset_d8(ptr, 0, size) + before = totals(backend.download(), maps) + max_error = max_drift = max_moment_error = max_corner = 0.0 + for step in range(1, 9): + state, diagnostic = ref.advance_coarse_once(spec, maps, state) + backend.step() + actual = backend.download() + expected = postcollision(state, spec, maps) + self.assertEqual(backend.step_count, step) + p.check_synchronization(step, state.tick, phase="postcollision_g") + for got, want in zip(actual, expected): + np.testing.assert_array_equal(np.isnan(got), np.isnan(want)) + error = float(np.nanmax(np.abs(got - want))) + self.assertLessEqual(error, POP_ATOL) + max_error = max(max_error, error) + drift = float(np.max(np.abs(totals(actual, maps) - before))) + moment_error = float(np.max(np.abs(totals(actual, maps) - totals(expected, maps)))) + self.assertLessEqual(drift, MOMENT_ATOL) + self.assertLessEqual(moment_error, MOMENT_ATOL) + max_drift, max_moment_error = max(max_drift, drift), max(max_moment_error, moment_error) + c, f = backend._levels + if audit: + np.testing.assert_array_equal(global_array(c.module, "collision_counts", (16, 16)), + step * maps.coarse_active) + np.testing.assert_array_equal(global_array(f.module, "collision_counts", (24, 24)), + 2 * step * maps.fine_active) + np.testing.assert_array_equal(global_array(f.module, "explosion_counts", (24, 24)), + step * ~maps.fine_active) + owners = np.zeros((9, 16, 16), np.uint32) + owners.ravel()[p.coalesce_coarse] = step + np.testing.assert_array_equal(global_array(c.module, "incoming_counts", owners.shape), owners) + raw = level_storage(f) + np.testing.assert_array_equal(np.isfinite(raw), diagnostic["hop2_valid"]) + self.assertTrue(np.isfinite(raw.ravel()[p.coalesce_fine]).all()) + self.assertEqual(len(diagnostic["corner_packets"]), 8) + for packet in diagnostic["corner_packets"].values(): + sx, sy = packet["source_coarse_xy"] + self.assertTrue(maps.coarse_active[sy, sx]) + values = [raw[packet["direction"], y, x] + for y, x in packet["children_fine_yx"]] + error = float(np.max(np.abs(np.array(values) - packet["packets"]))) + self.assertLessEqual(error, POP_ATOL) + max_corner = max(max_corner, error) + print(f"{case}: steps=8 max_population={max_error:.9g} " + f"max_leaf_drift={max_drift:.9g} max_leaf_error={max_moment_error:.9g} " + f"max_corner_packet={max_corner:.9g}; ownership/counts PASS", flush=True) + + def test_bounded_equivalence_blocked(self): + """The same eight-step reference and launch-failure checks on whole-level tiles.""" + spec = ref.make_spec(collision="srt") + maps = ref.build_maps(spec) + cfg = LBMConfig(nx=16, ny=16, viscosity=.1) + with CudaContext(0) as context: + with PeriodicPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)], + level_blocks=[(16, 16), (24, 24)]) as backend: + self.eight_steps(backend, spec, maps, audit=False) + with patch.object(backend._pairs[0], "explode", side_effect=RuntimeError("injected launch failure")): + with self.assertRaisesRegex(RuntimeError, "injected"): + backend.step() + for operation in (backend.download, backend.step): + with self.assertRaisesRegex(RuntimeError, "failed"): + operation() + self.assertFalse(context.is_closed) + def test_bounded_equivalence_and_ownership(self): self.assertEqual(Path(CelerisLab.__file__).resolve(), ROOT / "src/CelerisLab/__init__.py") self.assertEqual(Path(compiler._KERNEL_DIR).resolve(), ROOT / "src/CelerisLab/lbm/kernels") @@ -198,64 +292,7 @@ class CoupledGPU(unittest.TestCase): for level in backend._levels) self.assertLess(device_bytes + audit_bytes, 128 * 1024) print(f"device_arrays={device_bytes} audit_arrays={audit_bytes} bytes", flush=True) - for case in ("rest", "nonequilibrium_seed1"): - with self.subTest(case=case): - state = ref.make_state(spec, velocity=(0, 0) if case == "rest" else (.03, -.02)) - if case != "rest": - rng = np.random.default_rng(1) - state = ref.State(*(a * (1 + 1e-3 * rng.standard_normal(a.shape)) - for a in (state.coarse_f, state.fine_f))) - initial = postcollision(state, spec, maps) - backend.initialize(*initial) - for level in backend._levels: - for name in ("collision_counts", "explosion_counts", "incoming_counts"): - ptr, size = level.module.get_global(name) - cuda.memset_d8(ptr, 0, size) - before = totals(backend.download(), maps) - max_error = max_drift = max_moment_error = max_corner = 0.0 - for step in range(1, 9): - state, diagnostic = ref.advance_coarse_once(spec, maps, state) - backend.step() - actual = backend.download() - expected = postcollision(state, spec, maps) - self.assertEqual(backend.step_count, step) - p.check_synchronization(step, state.tick, phase="postcollision_g") - for got, want in zip(actual, expected): - np.testing.assert_array_equal(np.isnan(got), np.isnan(want)) - error = float(np.nanmax(np.abs(got - want))) - self.assertLessEqual(error, POP_ATOL) - max_error = max(max_error, error) - drift = float(np.max(np.abs(totals(actual, maps) - before))) - moment_error = float(np.max(np.abs(totals(actual, maps) - totals(expected, maps)))) - self.assertLessEqual(drift, MOMENT_ATOL) - self.assertLessEqual(moment_error, MOMENT_ATOL) - max_drift, max_moment_error = max(max_drift, drift), max(max_moment_error, moment_error) - c, f = backend._levels - np.testing.assert_array_equal(global_array(c.module, "collision_counts", (16, 16)), - step * maps.coarse_active) - np.testing.assert_array_equal(global_array(f.module, "collision_counts", (24, 24)), - 2 * step * maps.fine_active) - np.testing.assert_array_equal(global_array(f.module, "explosion_counts", (24, 24)), - step * ~maps.fine_active) - owners = np.zeros((9, 16, 16), np.uint32) - owners.ravel()[p.coalesce_coarse] = step - np.testing.assert_array_equal(global_array(c.module, "incoming_counts", owners.shape), owners) - raw = np.empty(f.shape, np.float32) - cuda.memcpy_dtoh(raw, f.current) - np.testing.assert_array_equal(np.isfinite(raw), diagnostic["hop2_valid"]) - self.assertTrue(np.isfinite(raw.ravel()[p.coalesce_fine]).all()) - self.assertEqual(len(diagnostic["corner_packets"]), 8) - for packet in diagnostic["corner_packets"].values(): - sx, sy = packet["source_coarse_xy"] - self.assertTrue(maps.coarse_active[sy, sx]) - values = [raw[packet["direction"], y, x] - for y, x in packet["children_fine_yx"]] - error = float(np.max(np.abs(np.array(values) - packet["packets"]))) - self.assertLessEqual(error, POP_ATOL) - max_corner = max(max_corner, error) - print(f"{case}: steps=8 max_population={max_error:.9g} " - f"max_leaf_drift={max_drift:.9g} max_leaf_error={max_moment_error:.9g} " - f"max_corner_packet={max_corner:.9g}; ownership/counts PASS", flush=True) + self.eight_steps(backend, spec, maps, audit=True) # Fail before any additional time step; no stale synchronized export. with patch.object(backend._pairs[0], "explode", side_effect=RuntimeError("injected launch failure")):