test(D1): exchange, Jet and fine-surface checks run on all-blocked storage
- test_exchange_window and test_raw_exchange_gpu fine(): dense root becomes a (12, 8) blocked root (fine(None) stays only for the dense reference test that D2 retires); - jets: G1/G3 compare blocked layouts against the FINE_BLOCKS[0] reference (blocked root); the dense run is a separate leg D2 removes; G2 legacy actuation vs Jet on the blocked reference; CPU fixtures use a blocked root; - test_refinement_blocked_fine.raw reads every level through its layout; - test_fine_surface_gpu: new test_fine_surface_blocked_matches_uniform_four_hops (fine ROI after each of four hops vs the uniform 48^2 solver with the same tolerances, blowing/suction mass sign from raw exchange, zero actuation == passive bit for bit); the cross-interface donor rejection passes tiles. The dense hook/ledger/audit probes in test_fine_surface_four_hops and the dense SoA fault tests stay until D2 retires them by name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
452eaf32de
commit
104f74632f
5 files changed
+96
-31
No files matched your search
@@ -109,7 +109,7 @@ class ExchangeWindowGPU(unittest.TestCase):
|
|||||||
from test_raw_exchange_gpu import BOUNDS as RB, CFG, circle as rc, jet, state
|
from test_raw_exchange_gpu import BOUNDS as RB, CFG, circle as rc, jet, state
|
||||||
from CelerisLab.cuda.context import CudaContext
|
from CelerisLab.cuda.context import CudaContext
|
||||||
with CudaContext(0) as ctx:
|
with CudaContext(0) as ctx:
|
||||||
kw = dict(context=ctx, refinements=[RB], level_blocks=[None, (16, 20)],
|
kw = dict(context=ctx, refinements=[RB], level_blocks=[(12, 8), (16, 20)],
|
||||||
level_objects=[(), (rc(),)], level_jets=[(), (jet(),)],
|
level_objects=[(), (rc(),)], level_jets=[(), (jet(),)],
|
||||||
observe_raw_exchange=True)
|
observe_raw_exchange=True)
|
||||||
with FixedPatchStepper(CFG, observe_window_exchange=True, **kw) as windowed, \
|
with FixedPatchStepper(CFG, observe_window_exchange=True, **kw) as windowed, \
|
||||||
|
|||||||
@@ -279,6 +279,61 @@ def test_fine_surface_four_hops(speed):
|
|||||||
np.testing.assert_array_equal(got, wanted)
|
np.testing.assert_array_equal(got, wanted)
|
||||||
|
|
||||||
|
|
||||||
|
def run_blocked_patch(speed):
|
||||||
|
"""All-blocked run (whole-level tiles) of the same case; fine ROI after each fine hop."""
|
||||||
|
cfg = LBMConfig(nx=24, ny=24, viscosity=.05, velocity=0, rho=1)
|
||||||
|
source = object_spec(speed)
|
||||||
|
with CudaContext(0) as context:
|
||||||
|
with implementation.FixedPatchStepper(cfg, context=context, refinements=[BOUNDS],
|
||||||
|
level_blocks=[(24, 24), (40, 40)],
|
||||||
|
level_objects=[(), (source,)],
|
||||||
|
observe_raw_exchange=True) as backend:
|
||||||
|
c, f = backend._levels
|
||||||
|
flags = backend.fine_boundary.flags.reshape(40, 40)
|
||||||
|
active = np.zeros((40, 40), bool)
|
||||||
|
active[4:-4, 4:-4] = True
|
||||||
|
active &= ((flags & FLUID) != 0) & ((flags & SOLID) == 0)
|
||||||
|
yy, xx = np.mgrid[:40, :40]
|
||||||
|
roi = active & ((xx-20)**2+(yy-20)**2 <= 9**2)
|
||||||
|
backend.initialize(rest((24, 24)), rest((40, 40)))
|
||||||
|
traces, original = [], f.kernel
|
||||||
|
|
||||||
|
def fine(*args, **kwargs):
|
||||||
|
original(*args, **kwargs)
|
||||||
|
backend._stream.synchronize()
|
||||||
|
advanced = f.layout.to_dense(read(args[1], f.layout.blocked_shape))
|
||||||
|
assert np.isfinite(advanced[:, active]).all()
|
||||||
|
assert np.isnan(advanced[:, (flags & SOLID) != 0]).all()
|
||||||
|
traces.append(advanced[:, roi].copy())
|
||||||
|
|
||||||
|
with patch.object(f, "kernel", fine):
|
||||||
|
backend.step(2)
|
||||||
|
assert len(traces) == 4 and backend.step_count == 2
|
||||||
|
mass = backend.read_raw_exchange(0)["total"]["mass_into_fluid"]
|
||||||
|
return traces, roi, backend.download(), float(mass)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("speed", [0., .002, -.002])
|
||||||
|
def test_fine_surface_blocked_matches_uniform_four_hops(speed):
|
||||||
|
"""Blocked storage: fine ROI after each of four hops against the uniform 48^2 solver
|
||||||
|
(same tolerances as the dense test); blowing/suction drives mass with its sign;
|
||||||
|
zero actuation equals the passive circle bit for bit."""
|
||||||
|
actual, roi, final, mass = run_blocked_patch(speed)
|
||||||
|
reference = uniform_reference(speed, roi)
|
||||||
|
for got, wanted in zip(actual, reference):
|
||||||
|
np.testing.assert_allclose(got, wanted, atol=3e-6, rtol=0)
|
||||||
|
np.testing.assert_allclose(MOMENTS@got.astype(float), MOMENTS@wanted.astype(float),
|
||||||
|
atol=2e-5, rtol=0)
|
||||||
|
if speed:
|
||||||
|
assert speed*mass > 0
|
||||||
|
else:
|
||||||
|
passive, passive_roi, passive_final, _ = run_blocked_patch(None)
|
||||||
|
np.testing.assert_array_equal(passive_roi, roi)
|
||||||
|
for got, wanted in zip(actual+list(final), passive+list(passive_final)):
|
||||||
|
np.testing.assert_array_equal(got, wanted)
|
||||||
|
print(f"blocked speed={speed} four hops vs uniform PASS mass_into_fluid={mass:+.9g}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("missing", ["surface_velocity_gpu", "link_exchange_gpu"])
|
@pytest.mark.parametrize("missing", ["surface_velocity_gpu", "link_exchange_gpu"])
|
||||||
def test_missing_surface_device_buffer_rejects_before_launch(missing):
|
def test_missing_surface_device_buffer_rejects_before_launch(missing):
|
||||||
backend = implementation.FixedPatchStepper.__new__(implementation.FixedPatchStepper)
|
backend = implementation.FixedPatchStepper.__new__(implementation.FixedPatchStepper)
|
||||||
@@ -328,6 +383,7 @@ def test_cross_interface_donor_rejects_before_cuda():
|
|||||||
with patch.object(cuda, "mem_alloc") as allocate:
|
with patch.object(cuda, "mem_alloc") as allocate:
|
||||||
with pytest.raises(ValueError, match="donor crosses"):
|
with pytest.raises(ValueError, match="donor crosses"):
|
||||||
implementation.FixedPatchStepper(cfg, context=context, refinements=[BOUNDS],
|
implementation.FixedPatchStepper(cfg, context=context, refinements=[BOUNDS],
|
||||||
|
level_blocks=[(24, 24), (40, 40)],
|
||||||
level_objects=[(), (source,)])
|
level_objects=[(), (source,)])
|
||||||
context.activate.assert_not_called()
|
context.activate.assert_not_called()
|
||||||
allocate.assert_not_called()
|
allocate.assert_not_called()
|
||||||
@@ -92,8 +92,9 @@ def census(run, index, buffer):
|
|||||||
|
|
||||||
|
|
||||||
def fine(block):
|
def fine(block):
|
||||||
"""Two-level circle+jet construction on BOUNDS; block None keeps the dense fine level."""
|
"""Two-level circle+jet construction on BOUNDS, all-blocked with a (12, 8) root tile;
|
||||||
return dict(refinements=[BOUNDS], level_blocks=[None, block],
|
block None is the dense route (dense reference test only, retired with the dense route)."""
|
||||||
|
return dict(refinements=[BOUNDS], level_blocks=[None if block is None else (12, 8), block],
|
||||||
level_objects=[(), (circle(),)], level_jets=[(), (jet(),)])
|
level_objects=[(), (circle(),)], level_jets=[(), (jet(),)])
|
||||||
|
|
||||||
class RawExchangeGPU(unittest.TestCase):
|
class RawExchangeGPU(unittest.TestCase):
|
||||||
|
|||||||
@@ -88,15 +88,13 @@ def seam_cells(layout, mapped):
|
|||||||
|
|
||||||
|
|
||||||
def raw(backend):
|
def raw(backend):
|
||||||
"""Unmasked device arrays; blocked fine storage unpacked from owner cores."""
|
"""Unmasked device arrays of every level; blocked storage unpacked from owner cores."""
|
||||||
coarse, fine = backend._levels
|
out = []
|
||||||
out = [np.empty(coarse.shape, np.float32)]
|
for level in backend._levels:
|
||||||
out.append(np.empty(fine.shape if backend.fine_layout is None
|
array = np.empty(level.shape if level.layout is None else level.layout.blocked_shape,
|
||||||
else backend.fine_layout.blocked_shape, np.float32))
|
np.float32)
|
||||||
cuda.memcpy_dtoh(out[0], coarse.current)
|
cuda.memcpy_dtoh(array, level.current)
|
||||||
cuda.memcpy_dtoh(out[1], fine.current)
|
out.append(array if level.layout is None else level.layout.to_dense(array))
|
||||||
if backend.fine_layout is not None:
|
|
||||||
out[1] = backend.fine_layout.to_dense(out[1])
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
Jets use the same coarse cell-edge frame as fine_objects. The equivalent jet
|
Jets use the same coarse cell-edge frame as fine_objects. The equivalent jet
|
||||||
of Astra's actuated fixture is SurfaceActuation(0.7, 0.85, U).as_jet((16, 12),
|
of Astra's actuated fixture is SurfaceActuation(0.7, 0.85, U).as_jet((16, 12),
|
||||||
2.15). Frozen GPU gates (8 coarse steps, both coarse boundary modes):
|
2.15). Frozen GPU gates (8 coarse steps, both coarse boundary modes):
|
||||||
G1 jet run: dense vs 1x1/2x2/4x2 fine blocks bit-identical (values + NaN masks);
|
G1 jet run: 1x1/2x2/4x2 fine blocks (blocked (12, 8) root) bit-identical to the
|
||||||
G2 jet vs legacy actuation (dense): max |dG| over finite fine ACTIVE fluid
|
FINE_BLOCKS[0] reference (values + NaN masks); dense leg kept until dense retirement;
|
||||||
|
G2 jet vs legacy actuation (blocked reference): max |dG| over finite fine ACTIVE fluid
|
||||||
populations <= 1e-6 (velocity rows differ only by FP32 rounding; reported);
|
populations <= 1e-6 (velocity rows differ only by FP32 rounding; reported);
|
||||||
G3 amplitude change after 4 steps (+U -> -U), 4 more steps: dense vs blocked
|
G3 amplitude change after 4 steps (+U -> -U), 4 more steps: blocked layouts
|
||||||
bit-identical and the run differs from the unchanged run.
|
bit-identical to the FINE_BLOCKS[0] reference (dense leg until retirement) and the
|
||||||
|
run differs from the unchanged run.
|
||||||
"""
|
"""
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
import unittest
|
import unittest
|
||||||
@@ -26,6 +28,11 @@ from test_refinement_blocked_fine import (BOUNDS, FINE_BLOCKS, NX, NY, STEPS, ac
|
|||||||
SPEED = .002
|
SPEED = .002
|
||||||
|
|
||||||
|
|
||||||
|
def root_and(fine_block):
|
||||||
|
"""All-blocked levels with a (12, 8) root tile; fine_block None is the dense route."""
|
||||||
|
return [None if fine_block is None else (12, 8), fine_block]
|
||||||
|
|
||||||
|
|
||||||
def jet(speed=SPEED):
|
def jet(speed=SPEED):
|
||||||
return actuation(speed).as_jet((16.0, 12.0), 2.15)
|
return actuation(speed).as_jet((16.0, 12.0), 2.15)
|
||||||
|
|
||||||
@@ -42,7 +49,7 @@ def mocked(objects, jets, fine_block):
|
|||||||
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
|
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
|
||||||
patch.object(cuda, "memcpy_htod", htod):
|
patch.object(cuda, "memcpy_htod", htod):
|
||||||
backend = FixedPatchStepper(config("periodic"), context=context, refinements=[BOUNDS],
|
backend = FixedPatchStepper(config("periodic"), context=context, refinements=[BOUNDS],
|
||||||
level_blocks=[None, fine_block], level_objects=[(), tuple(objects)],
|
level_blocks=root_and(fine_block), level_objects=[(), tuple(objects)],
|
||||||
level_jets=[(), tuple(jets)])
|
level_jets=[(), tuple(jets)])
|
||||||
return backend, htod
|
return backend, htod
|
||||||
|
|
||||||
@@ -161,7 +168,7 @@ def run(mode, fine_block, *, objects, jets, switch=None):
|
|||||||
patch_ = FixedPatch2D.from_config(cfg, BOUNDS)
|
patch_ = FixedPatch2D.from_config(cfg, BOUNDS)
|
||||||
with CudaContext(0) as context, FixedPatchStepper(
|
with CudaContext(0) as context, FixedPatchStepper(
|
||||||
cfg, context=context, coarse_boundary=mode, refinements=[BOUNDS],
|
cfg, context=context, coarse_boundary=mode, refinements=[BOUNDS],
|
||||||
level_blocks=[None, fine_block], level_objects=[(), tuple(objects)],
|
level_blocks=root_and(fine_block), level_objects=[(), tuple(objects)],
|
||||||
level_jets=[(), tuple(jets)]) as backend:
|
level_jets=[(), tuple(jets)]) as backend:
|
||||||
backend.initialize(state(NY, NX, 1), state(*patch_.fine_shape, 2))
|
backend.initialize(state(NY, NX, 1), state(*patch_.fine_shape, 2))
|
||||||
if switch is None:
|
if switch is None:
|
||||||
@@ -179,29 +186,32 @@ class JetStepperGPU(unittest.TestCase):
|
|||||||
patch_, boundary = fine_circle(mode)
|
patch_, boundary = fine_circle(mode)
|
||||||
fluid = (patch_.fine_kind == ACTIVE) & (
|
fluid = (patch_.fine_kind == ACTIVE) & (
|
||||||
(boundary.flags.reshape(patch_.fine_shape) & SOLID) == 0)
|
(boundary.flags.reshape(patch_.fine_shape) & SOLID) == 0)
|
||||||
dense = run(mode, None, objects=[circle()], jets=[jet()])
|
reference = run(mode, FINE_BLOCKS[0], objects=[circle()], jets=[jet()])
|
||||||
self.assertTrue(np.isfinite(dense[1][1][:, fluid]).all(), mode)
|
self.assertTrue(np.isfinite(reference[1][1][:, fluid]).all(), mode)
|
||||||
for fine_block in FINE_BLOCKS:
|
for fine_block in FINE_BLOCKS[1:]:
|
||||||
public, raw_arrays = run(mode, fine_block, objects=[circle()], jets=[jet()])
|
public, raw_arrays = run(mode, fine_block, objects=[circle()], jets=[jet()])
|
||||||
for a, b in zip(public + tuple(raw_arrays), dense[0] + tuple(dense[1])):
|
for a, b in zip(public + tuple(raw_arrays), reference[0] + tuple(reference[1])):
|
||||||
np.testing.assert_array_equal(a, b, err_msg=f"G1 {mode} {fine_block}")
|
np.testing.assert_array_equal(a, b, err_msg=f"G1 {mode} {fine_block}")
|
||||||
legacy = run(mode, None, objects=[circle(actuation(SPEED))], jets=())
|
dense = run(mode, None, objects=[circle()], jets=[jet()]) # dense leg (D2 retires)
|
||||||
diff = np.abs(dense[1][1][:, fluid] - legacy[1][1][:, fluid]).max()
|
for a, b in zip(dense[0] + tuple(dense[1]), reference[0] + tuple(reference[1])):
|
||||||
|
np.testing.assert_array_equal(a, b, err_msg=f"G1 {mode} dense")
|
||||||
|
legacy = run(mode, FINE_BLOCKS[0], objects=[circle(actuation(SPEED))], jets=())
|
||||||
|
diff = np.abs(reference[1][1][:, fluid] - legacy[1][1][:, fluid]).max()
|
||||||
print(f"G2 {mode}: jet vs legacy fine max |dG| = {diff:.3e}", flush=True)
|
print(f"G2 {mode}: jet vs legacy fine max |dG| = {diff:.3e}", flush=True)
|
||||||
self.assertLessEqual(diff, 1e-6, mode)
|
self.assertLessEqual(diff, 1e-6, mode)
|
||||||
|
|
||||||
def test_g3_amplitude_switch_layouts_identical(self):
|
def test_g3_amplitude_switch_layouts_identical(self):
|
||||||
for mode in ("periodic", "slot_reservoir"):
|
for mode in ("periodic", "slot_reservoir"):
|
||||||
dense = run(mode, None, objects=[circle()], jets=[jet()], switch=-SPEED)
|
reference = run(mode, FINE_BLOCKS[0], objects=[circle()], jets=[jet()], switch=-SPEED)
|
||||||
steady = run(mode, None, objects=[circle()], jets=[jet()])
|
steady = run(mode, FINE_BLOCKS[0], objects=[circle()], jets=[jet()])
|
||||||
self.assertFalse(np.array_equal(dense[1][1], steady[1][1], equal_nan=True), mode)
|
self.assertFalse(np.array_equal(reference[1][1], steady[1][1], equal_nan=True), mode)
|
||||||
for fine_block in FINE_BLOCKS:
|
for fine_block in (*FINE_BLOCKS[1:], None): # None: dense leg (D2 retires)
|
||||||
public, raw_arrays = run(mode, fine_block, objects=[circle()], jets=[jet()],
|
public, raw_arrays = run(mode, fine_block, objects=[circle()], jets=[jet()],
|
||||||
switch=-SPEED)
|
switch=-SPEED)
|
||||||
for a, b in zip(public + tuple(raw_arrays), dense[0] + tuple(dense[1])):
|
for a, b in zip(public + tuple(raw_arrays), reference[0] + tuple(reference[1])):
|
||||||
np.testing.assert_array_equal(a, b, err_msg=f"G3 {mode} {fine_block}")
|
np.testing.assert_array_equal(a, b, err_msg=f"G3 {mode} {fine_block}")
|
||||||
print(f"G3 {mode}: amplitude switch dense vs {len(FINE_BLOCKS)} layouts identical",
|
print(f"G3 {mode}: amplitude switch, {len(FINE_BLOCKS) - 1} layouts + dense "
|
||||||
flush=True)
|
f"identical to {FINE_BLOCKS[0]}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in new issue
Block a user