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 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-15 03:52:42 +00:00
1 parent dc5850c198
commit 59921a0181
3 files changed
+35 -6

No files matched your search

+3 -4
View File
@@ -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")
@@ -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)):
@@ -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"