From 59921a01816ce1845dc7d15df34627eedf9d1e03 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 03:52:42 +0000 Subject: [PATCH] Blocked fine: fix INNER assertion, skip empty halo uploads, explicit fixture - GPU test: after hop 2 some INNER_GHOST populations are NaN (pulled from OUTER poisoned on hop 1; 236 slots in pinned CPUREF); assert ACTIVE finite and OUTER NaN, keep the full dense-vs-blocked raw value/NaN-mask comparison. - A 1x1 non-periodic fine layout has zero halo pairs: skip the halo table uploads (CUDA rejects mem_alloc(0)); _fine_halo already skips launch. CPU guard test with driver doubles. - Drop the class-level fine_layout default; the followup fake backend now sets fine_layout = None explicitly. Co-Authored-By: Claude Opus 5.5 --- src/CelerisLab/lbm/refinement_stepper.py | 7 ++-- .../multigrid/test_refinement_blocked_fine.py | 33 +++++++++++++++++-- .../multigrid/test_refinement_followup.py | 1 + 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/CelerisLab/lbm/refinement_stepper.py b/src/CelerisLab/lbm/refinement_stepper.py index 56aabda..f63eb87 100644 --- a/src/CelerisLab/lbm/refinement_stepper.py +++ b/src/CelerisLab/lbm/refinement_stepper.py @@ -72,8 +72,6 @@ class FixedPatchStepper: only, ACTIVE collide); halos are gathered before each fine hop. Public arrays keep the dense (9, ny, nx) fine shape; only the allocation differs. """ - fine_layout = None # dense fine storage unless fine_block is given - def __init__(self, cfg, bounds, *, context, coarse_boundary="periodic", fine_objects=(), fine_block=None): if HALO != 4: @@ -169,8 +167,9 @@ class FixedPatchStepper: else: self._children = self._upload(tables[1]) self._kind = self._upload(tables[2]) - self._halo_dst = self._upload(layout.halo_dst) - self._halo_src = self._upload(layout.halo_src) + if len(layout.halo_dst): # a 1x1 non-periodic layout has no pairs + self._halo_dst = self._upload(layout.halo_dst) + self._halo_src = self._upload(layout.halo_src) module = self._levels[1].module self._explode = module.get_function("BlockExplode") self._halo_copy = module.get_function("BlockHaloCopy") diff --git a/tests/validation/multigrid/test_refinement_blocked_fine.py b/tests/validation/multigrid/test_refinement_blocked_fine.py index 9525ff9..6d14e29 100644 --- a/tests/validation/multigrid/test_refinement_blocked_fine.py +++ b/tests/validation/multigrid/test_refinement_blocked_fine.py @@ -5,8 +5,9 @@ periodic and slot_reservoir modes: dense fine storage vs 1x1, 2x2 and 4x2 fine blocks give bit-identical masked downloads AND bit-identical raw coarse and unpacked owner-core fine arrays, including patch-ghost values/NaN masks. """ +from contextlib import nullcontext import unittest -from unittest.mock import Mock +from unittest.mock import Mock, patch import numpy as np import pycuda.driver as cuda @@ -14,6 +15,7 @@ import pycuda.driver as cuda from CelerisLab.body.geometry.circle import CircleGeometry from CelerisLab.body.objects import SimObject from CelerisLab.config import LBMConfig +from CelerisLab.cuda import compiler_v2 as compiler from CelerisLab.lbm.blocks import BlockLayout2D from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY from CelerisLab.lbm.refinement import ACTIVE, INNER_GHOST, OUTER_GHOST, FixedPatch2D @@ -67,6 +69,31 @@ class BlockedFineCPU(unittest.TestCase): with self.subTest(kw=kw), self.assertRaises(ValueError): FixedPatchStepper(config("periodic"), BOUNDS, context=self.forbidden_context(), **kw) + def test_single_block_layout_allocates_no_empty_halo_tables(self): + # A 1x1 non-periodic fine layout has zero halo pairs; CUDA rejects mem_alloc(0). + context = Mock(activate=nullcontext, sm_arch="sm_70") + module = Mock() + module.get_global.return_value = (Mock(), 20) + sizes = [] + + def alloc(size): + if size <= 0: + raise AssertionError("zero-byte device allocation") + sizes.append(size) + return Mock() + with patch.object(cuda, "Stream"), patch.object(compiler, "generate_config"), \ + patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \ + patch.object(compiler, "load_module", return_value=module), \ + patch.object(cuda, "mem_alloc", side_effect=alloc), patch.object(cuda, "memcpy_htod"): + backend = FixedPatchStepper(config("periodic"), BOUNDS, context=context, + fine_block=FINE_BLOCKS[0]) + self.assertEqual(len(backend.fine_layout.halo_dst), 0) + self.assertIsNone(backend._halo_dst) + backend._fine_halo(Mock()) # no launch without pairs + module.get_function.return_value.assert_not_called() + backend.close() + self.assertTrue(sizes) + def test_tables_map_back_to_dense_slots_in_order(self): patch = FixedPatch2D.from_config(config("periodic"), BOUNDS) fy, fx = patch.fine_shape @@ -114,7 +141,9 @@ class BlockedFineGPU(unittest.TestCase): (dense_public, dense_raw), blocked = results[0], results[1:] kind = patch.fine_kind self.assertTrue(np.isnan(dense_raw[1][:, kind == OUTER_GHOST]).all(), mode) - self.assertTrue(np.isfinite(dense_raw[1][:, kind != OUTER_GHOST]).all(), mode) + # INNER populations may be NaN after hop 2 (pulled from poisoned OUTER); + # their exact pattern is pinned by the dense-vs-blocked comparison below. + self.assertTrue(np.isfinite(dense_raw[1][:, kind == ACTIVE]).all(), mode) for fine_block, (public, raw_arrays) in zip(FINE_BLOCKS, blocked): msg = f"{mode} fine_block={fine_block}" for a, b in zip(public + tuple(raw_arrays), dense_public + tuple(dense_raw)): diff --git a/tests/validation/multigrid/test_refinement_followup.py b/tests/validation/multigrid/test_refinement_followup.py index 889bd4b..9d65166 100644 --- a/tests/validation/multigrid/test_refinement_followup.py +++ b/tests/validation/multigrid/test_refinement_followup.py @@ -106,6 +106,7 @@ class FaultGuardsCPU(unittest.TestCase): def fake_backend(self): backend = PeriodicPatchStepper.__new__(PeriodicPatchStepper) + backend.fine_layout = None # dense fine storage backend.ctx = Mock(activate=nullcontext) backend._lock = RLock() backend._status = "ready"