feat(esopull): runtime body sync for EsoPull streaming mode

- New esopull_sync.cu: DecodeCellsToPhysical + EncodePhysicalToCells
  (compact-list mode, ddf_shifting-aware, encode applies collision).
- sync_bodies() now branches for double_buffer vs esopull: decode
  backing layout to physical DDF on GPU -> host patch -> collide +
  encode back to backing layout. No temp_gpu, no full-grid copy.
- 4 new integration tests covering esopull add/remove/cycle/roundtrip.
- ddf_shifting + esopull + sync_bodies jointly verified (1300 steps
  stable after add/remove).
- Bump version to 0.5.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank14f
2026-06-21 22:31:02 +08:00
co-authored by Cursor
parent 04c2bc75ea
commit 00b957f904
10 changed files with 343 additions and 75 deletions
+116 -35
View File
@@ -1,14 +1,17 @@
"""sync_bodies pipeline without DDF patch — recompile, esopull guard, step count preservation.
"""sync_bodies pipeline — recompile, esopull runtime body sync, step count preservation.
Requires GPU."""
import unittest
import tempfile
import os
import json
import numpy as np
import pycuda.driver as cuda
import pycuda.autoinit
from CelerisLab.simulation import Simulation
from CelerisLab.simulation import Simulation, _scatter_to_field, _gather_from_field
# Use a small grid for fast compilation and test execution
@@ -16,7 +19,7 @@ NX, NY = 128, 64
class TestSyncBodiesSkeleton(unittest.TestCase):
"""Test sync_bodies() with real GPU -- skeleton without DDF patch."""
"""Test sync_bodies() with real GPU -- double_buffer path."""
def _make_sim(self) -> Simulation:
"""Create a Simulation with a small double_buffer D2Q9 grid."""
@@ -125,40 +128,118 @@ class TestSyncBodiesSkeleton(unittest.TestCase):
self.assertEqual(sim.bodies.count, 1)
sim.close()
def test_esopull_raises_not_implemented(self):
"""sync_bodies() with esopull should raise NotImplementedError."""
# Create a sim with esopull streaming
from CelerisLab.config import load_lbm_config
cfg = load_lbm_config()
cfg.streaming = "esopull"
# We need to build the sim manually to override streaming
sim = Simulation.__new__(Simulation)
sim._stream = None
sim.lbm_cfg = cfg
from CelerisLab.config import BodyConfig
sim.body_cfg = BodyConfig()
from CelerisLab.cuda.context import CudaContext
sim.ctx = CudaContext(0)
from CelerisLab.cuda import compiler_v2 as compiler
arch = sim._resolve_compile_arch = lambda: sim.ctx.sm_arch
arch_val = CudaContext(0).sm_arch
compiler.generate_config(cfg, n_objects=0)
ptx_path = compiler.compile_kernel(arch=arch_val)
module = compiler.load_module(ptx_path)
sim._ptx_path = ptx_path
sim._module = module
from CelerisLab.lbm.field import LBMField
sim.field = LBMField(cfg, module)
from CelerisLab.lbm.stepper import LBMStepper
sim.stepper = LBMStepper(sim.field, module, cfg)
from CelerisLab.body.manager import ObjectManager
sim.bodies = ObjectManager(
cfg.nx, cfg.ny, cfg.nz, cfg.nq, cfg)
sim._initialized = True
class TestSyncBodiesEsoPull(unittest.TestCase):
"""Test sync_bodies() with esopull streaming."""
def _make_esopull_sim(self, nx=128, ny=64) -> Simulation:
"""Create a Simulation with a small esopull D2Q9 grid."""
cfg = {
"grid": {"lattice_model": "D2Q9", "nx": nx, "ny": ny, "nz": 1},
"physics": {"data_type": "FP32", "viscosity": 0.05,
"velocity": 0.03, "rho": 1.0},
"method": {
"collision": "SRT", "streaming": "esopull",
"store_precision": "FP32", "ddf_shifting": False,
"les": {"enabled": False, "cs": 0.16, "closed_form": True},
"trt": {"magic_param": 0.1875},
"inlet": {"profile": "parabolic", "scheme": "zou_he_local"},
"outlet": {"mode": "neq_extrap", "backflow_clamp": True,
"blend_alpha": 0.7},
"y_wall_bc": "bounce_back",
"omega_guard": {"min": 0.01, "max": 1.99},
},
"cuda": {"threads_per_block": 256, "compute_capability": "auto"},
}
tmpd = tempfile.mkdtemp(prefix="esopull_sync_")
lbm_path = os.path.join(tmpd, "config_lbm.json")
with open(lbm_path, "w") as f:
json.dump(cfg, f)
return Simulation(lbm_config_path=lbm_path)
def test_esopull_add_body(self):
"""EsoPull: add body, sync, run, read finite force."""
sim = self._make_esopull_sim()
sim.initialize()
sim.run(100)
sim.add_body("circle", center=(NX // 4, NY // 2), radius=8)
with self.assertRaises(NotImplementedError):
sim.sync_bodies()
sim.sync_bodies()
self.assertEqual(sim.bodies.count, 1)
sim.run(100)
force = sim.read_force(0)
self.assertTrue(np.all(np.isfinite(force)),
f"EsoPull sync: finite force expected, got {force}")
sim.close()
def test_esopull_remove_body(self):
"""EsoPull: add body → run → remove → sync → run."""
sim = self._make_esopull_sim()
sim.add_body("circle", center=(NX // 4, NY // 2), radius=8)
sim.initialize()
sim.run(100)
sim.remove_body(0)
sim.sync_bodies()
self.assertEqual(sim.bodies.count, 0)
sim.run(100)
macro = sim.get_macroscopic()
self.assertTrue(np.all(np.isfinite(macro["ux"])),
"Macroscopic ux should be finite after esopull sync")
sim.close()
def test_esopull_add_remove_add(self):
"""EsoPull: add → remove → add cycle with finite checks."""
sim = self._make_esopull_sim()
sim.initialize()
sim.run(100)
# Add
sim.add_body("circle", center=(NX // 4, NY // 2), radius=8)
sim.sync_bodies()
sim.run(100)
# Remove
sim.remove_body(0)
sim.sync_bodies()
self.assertEqual(sim.bodies.count, 0)
sim.run(100)
# Add again
sim.add_body("circle", center=(NX // 2, NY // 2), radius=6)
sim.sync_bodies()
self.assertEqual(sim.bodies.count, 1)
sim.run(100)
force = sim.read_force(0)
self.assertTrue(np.all(np.isfinite(force)),
f"Finite force after esopull add-remove-add: {force}")
sim.close()
def test_esopull_roundtrip_fullgrid(self):
"""EsoPull: decode/encode roundtrip on full grid (no patch)."""
sim = self._make_esopull_sim()
sim.initialize()
sim.run(100)
nx, ny, nq = sim.field.nx, sim.field.ny, sim.field.nq
all_cells = np.arange(nx * ny, dtype=np.uint32)
step = sim.stepper.step_count
stream = sim.stream
s_gpu, s_host, idx_gpu = sim.field.esopull_sync_decode(
all_cells, step, stream)
_scatter_to_field(sim.field, s_host, all_cells)
_gather_from_field(sim.field, s_host, all_cells)
sim.field.esopull_sync_encode(
s_gpu, s_host, idx_gpu, len(all_cells), step, stream)
stream.synchronize()
sim.run(100)
macro = sim.get_macroscopic()
self.assertTrue(np.all(np.isfinite(macro["ux"])),
"ux finite after full-grid roundtrip")
sim.close()