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:
parent
04c2bc75ea
commit
00b957f904
@ -195,7 +195,6 @@ kernel launches on a user-provided stream, then sync and read later.
|
|||||||
`sync_bodies()` applies all staged body edits (added via `add_body()` and removed via `remove_body()`) to a running simulation without full reinitialization. The GPU flow field is preserved; only the body-related topology is rebuilt.
|
`sync_bodies()` applies all staged body edits (added via `add_body()` and removed via `remove_body()`) to a running simulation without full reinitialization. The GPU flow field is preserved; only the body-related topology is rebuilt.
|
||||||
|
|
||||||
**Limitations:**
|
**Limitations:**
|
||||||
- Requires `streaming: "double_buffer"` (esopull raises `NotImplementedError`)
|
|
||||||
- Abrupt body introduction causes a transient; force readback is finite but may take 50+ steps to settle
|
- Abrupt body introduction causes a transient; force readback is finite but may take 50+ steps to settle
|
||||||
- Verified for `"circle"` type bodies; sensors and force_regions are also expected to work
|
- Verified for `"circle"` type bodies; sensors and force_regions are also expected to work
|
||||||
(they produce no curved links so the DDF patch is simpler)
|
(they produce no curved links so the DDF patch is simpler)
|
||||||
@ -365,6 +364,7 @@ Current verification scope:
|
|||||||
- 2D D2Q9 only (D3Q19 not yet implemented)
|
- 2D D2Q9 only (D3Q19 not yet implemented)
|
||||||
- MRT collision model (SRT/TRT expected to work but not explicitly validated)
|
- MRT collision model (SRT/TRT expected to work but not explicitly validated)
|
||||||
- Fixed and rotating cylinder benchmarks (Kan99b K2: bit-identical metrics)
|
- Fixed and rotating cylinder benchmarks (Kan99b K2: bit-identical metrics)
|
||||||
|
- Runtime body topology sync via ``sync_bodies()`` -- add and remove bodies at runtime
|
||||||
- `get_macroscopic()` uses GPU kernel for physically correct output
|
- `get_macroscopic()` uses GPU kernel for physically correct output
|
||||||
- `get_ddf()` returns backing-layout data (not physical DDF) in EsoPull mode
|
- `get_ddf()` returns backing-layout data (not physical DDF) in EsoPull mode
|
||||||
|
|
||||||
@ -389,13 +389,12 @@ Stores `f_i - w_i` instead of `f_i` to improve FP16 accuracy. Supported with th
|
|||||||
|-----------|-----------|-------|-------------|--------|
|
|-----------|-----------|-------|-------------|--------|
|
||||||
| MRT | double_buffer | zou_he_local | cylinder | Verified (K2 metrics match FP32) |
|
| MRT | double_buffer | zou_he_local | cylinder | Verified (K2 metrics match FP32) |
|
||||||
| MRT | double_buffer | regularized | cylinder | Under investigation -- use zou_he_local |
|
| MRT | double_buffer | regularized | cylinder | Under investigation -- use zou_he_local |
|
||||||
| MRT | esopull | any | any | Not yet verified |
|
| MRT | esopull | zou_he_local | cylinder | Verified (sync_bodies tested) |
|
||||||
| SRT | double_buffer | any | cylinder | Expected to work (f-feq style) |
|
| SRT | double_buffer | any | cylinder | Expected to work (f-feq style) |
|
||||||
|
|
||||||
**Known limitations (ddf_shifting):**
|
**Known limitations (ddf_shifting):**
|
||||||
- Verified configuration: **D2Q9 + MRT + double_buffer + zou_he_local** only
|
- Verified configuration: **D2Q9 + MRT + double_buffer/zou_he_local** and **D2Q9 + MRT + esopull/zou_he_local** (sync_bodies add, remove, stepping stable)
|
||||||
- `regularized` inlet with `ddf_shifting` is **known incompatible / unsolved** -- use `zou_he_local`
|
- `regularized` inlet with `ddf_shifting` is **known incompatible / unsolved** -- use `zou_he_local`
|
||||||
- `esopull + ddf_shifting` has not been jointly validated
|
|
||||||
- MRT shifts to physical space before collision, shifts back after (SRT/TRT are shift-invariant natively)
|
- MRT shifts to physical space before collision, shifts back after (SRT/TRT are shift-invariant natively)
|
||||||
- D3Q19 MRT shifting patch has a `compute_feq` inconsistency (not in scope for 2D-only)
|
- D3Q19 MRT shifting patch has a `compute_feq` inconsistency (not in scope for 2D-only)
|
||||||
- Host `upload_ddf()` path is asymmetric (repaired)
|
- Host `upload_ddf()` path is asymmetric (repaired)
|
||||||
|
|||||||
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "CelerisLab"
|
name = "CelerisLab"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
description = "GPU-accelerated Lattice Boltzmann Method (LBM) CFD solver using CUDA"
|
description = "GPU-accelerated Lattice Boltzmann Method (LBM) CFD solver using CUDA"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
|
|||||||
2
setup.py
2
setup.py
@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='CelerisLab',
|
name='CelerisLab',
|
||||||
version='0.4.0',
|
version='0.5.0',
|
||||||
author='Frank14f',
|
author='Frank14f',
|
||||||
description='GPU-accelerated Lattice Boltzmann Method (LBM) CFD solver using CUDA',
|
description='GPU-accelerated Lattice Boltzmann Method (LBM) CFD solver using CUDA',
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ Usage::
|
|||||||
force = sim.read_force(0)
|
force = sim.read_force(0)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.4.0"
|
__version__ = "0.5.0"
|
||||||
|
|
||||||
from . import common, cuda, lbm, body, config
|
from . import common, cuda, lbm, body, config
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ Python `config.py` 只负责读取和校验,不是配置位置。
|
|||||||
| 字段 | 类型 | 默认 | 允许值 | 说明 |
|
| 字段 | 类型 | 默认 | 允许值 | 说明 |
|
||||||
|------|------|------|--------|------|
|
|------|------|------|--------|------|
|
||||||
| `collision` | string | `"SRT"` | `SRT`, `TRT`, `MRT` | 碰撞算子 |
|
| `collision` | string | `"SRT"` | `SRT`, `TRT`, `MRT` | 碰撞算子 |
|
||||||
| `streaming` | string | `"double_buffer"` | `double_buffer`, `esopull` | 流传输方式。运行时 body 拓扑同步 (``sync_bodies()``) 仅支持 ``double_buffer`` |
|
| `streaming` | string | `"double_buffer"` | `double_buffer`, `esopull` | 流传输方式。运行时 body 拓扑同步 (``sync_bodies()``) 两种模式均支持 |
|
||||||
| `store_precision` | string | `"FP32"` | `FP32`, `FP16S`, `FP16C` | GPU 存储精度。当前运行时已实现 `FP32` 与 `FP16S`,`FP16C` 仍为保留选项 |
|
| `store_precision` | string | `"FP32"` | `FP32`, `FP16S`, `FP16C` | GPU 存储精度。当前运行时已实现 `FP32` 与 `FP16S`,`FP16C` 仍为保留选项 |
|
||||||
| `ddf_shifting` | bool | false | | 存储 f−w 而非 f,提升 FP16 精度 |
|
| `ddf_shifting` | bool | false | | 存储 f−w 而非 f,提升 FP16 精度 |
|
||||||
| `les.enabled` | bool | false | | LES Smagorinsky 子格模型 |
|
| `les.enabled` | bool | false | | LES Smagorinsky 子格模型 |
|
||||||
@ -93,8 +93,8 @@ step/one_step_*.cu → kernel 编排
|
|||||||
|
|
||||||
### 运行时 body 拓扑同步
|
### 运行时 body 拓扑同步
|
||||||
|
|
||||||
- 运行时增删 body(`add_body()` / `remove_body()` + `sync_bodies()`)仅支持 `streaming: "double_buffer"`。
|
- 运行时增删 body(`add_body()` / `remove_body()` + `sync_bodies()`)在 ``double_buffer`` 和 ``esopull`` 两种流模式下均支持。
|
||||||
- `esopull` 模式下调用 `sync_bodies()` 会抛出 `NotImplementedError`。
|
- 用法相同,无需特别的运行时检查。
|
||||||
|
|
||||||
### 力区域标记(Force region flag)
|
### 力区域标记(Force region flag)
|
||||||
|
|
||||||
|
|||||||
@ -407,6 +407,57 @@ class LBMField:
|
|||||||
# directly from Simulation.sync_bodies(). They are no longer methods on
|
# directly from Simulation.sync_bodies(). They are no longer methods on
|
||||||
# LBMField.
|
# LBMField.
|
||||||
|
|
||||||
|
# -- EsoPull sync decode/encode (compact list) --------------------------
|
||||||
|
def esopull_sync_decode(self, patch_indices, step_count, stream=None):
|
||||||
|
"""Decode a subset of cells from EsoPull backing to physical DDF.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
patch_indices: uint32 array of cell indices to decode.
|
||||||
|
step_count: Current step count (for EsoPull parity).
|
||||||
|
stream: Optional CUDA stream.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(scratch_gpu, scratch_host, idx_gpu): scratch buffer handles.
|
||||||
|
"""
|
||||||
|
n = len(patch_indices)
|
||||||
|
nq = self.nq
|
||||||
|
scratch_gpu = cuda.mem_alloc(n * nq * 4)
|
||||||
|
scratch_host = np.empty(n * nq, dtype=np.float32)
|
||||||
|
idx_gpu = cuda.mem_alloc(n * 4)
|
||||||
|
cuda.memcpy_htod(idx_gpu, np.asarray(patch_indices, dtype=np.uint32))
|
||||||
|
|
||||||
|
fn = self.module.get_function("DecodeCellsToPhysical")
|
||||||
|
tpb = self.cfg.threads_per_block
|
||||||
|
fn(self.ddf_gpu, scratch_gpu, idx_gpu, np.uint32(n),
|
||||||
|
np.uint64(step_count),
|
||||||
|
block=(tpb, 1, 1), grid=((n + tpb - 1) // tpb, 1, 1),
|
||||||
|
stream=stream)
|
||||||
|
|
||||||
|
cuda.memcpy_dtoh_async(scratch_host, scratch_gpu, stream)
|
||||||
|
stream.synchronize()
|
||||||
|
return scratch_gpu, scratch_host, idx_gpu
|
||||||
|
|
||||||
|
def esopull_sync_encode(self, scratch_gpu, scratch_host, idx_gpu,
|
||||||
|
n, step_count, stream=None):
|
||||||
|
"""Upload patched physical DDF, collide + encode to EsoPull layout.
|
||||||
|
|
||||||
|
The backing layout stores post-collision values. This method
|
||||||
|
applies collision to the patched physical DDF before encoding,
|
||||||
|
matching the store_f_esopull convention at parity *step_count*.
|
||||||
|
Frees scratch_gpu and idx_gpu after encoding.
|
||||||
|
"""
|
||||||
|
cuda.memcpy_htod_async(scratch_gpu, scratch_host, stream)
|
||||||
|
|
||||||
|
fn = self.module.get_function("EncodePhysicalToCells")
|
||||||
|
tpb = self.cfg.threads_per_block
|
||||||
|
fn(self.ddf_gpu, scratch_gpu, idx_gpu, np.uint32(n),
|
||||||
|
np.uint64(step_count),
|
||||||
|
block=(tpb, 1, 1), grid=((n + tpb - 1) // tpb, 1, 1),
|
||||||
|
stream=stream)
|
||||||
|
|
||||||
|
scratch_gpu.free()
|
||||||
|
idx_gpu.free()
|
||||||
|
|
||||||
def snapshot(self):
|
def snapshot(self):
|
||||||
self.download_ddf(force=True)
|
self.download_ddf(force=True)
|
||||||
self._ddf_snap = self.ddf.copy()
|
self._ddf_snap = self.ddf.copy()
|
||||||
|
|||||||
@ -69,5 +69,6 @@ extern "C"
|
|||||||
#include "step/one_step_esopull.cu"
|
#include "step/one_step_esopull.cu"
|
||||||
#include "step/aux_kernels.cu"
|
#include "step/aux_kernels.cu"
|
||||||
#include "step/esopull_macro.cu"
|
#include "step/esopull_macro.cu"
|
||||||
|
#include "step/esopull_sync.cu"
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
81
src/CelerisLab/lbm/kernels/step/esopull_sync.cu
Normal file
81
src/CelerisLab/lbm/kernels/step/esopull_sync.cu
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// CelerisLab -- step/esopull_sync.cu
|
||||||
|
// Compact-list EsoPull decode/encode for runtime body topology sync.
|
||||||
|
//
|
||||||
|
// Decodes a subset of cells from EsoPull backing layout to physical DDF,
|
||||||
|
// or encodes physical DDF back to EsoPull backing layout.
|
||||||
|
// Each thread handles one cell from a compact index list.
|
||||||
|
//
|
||||||
|
// Included by kernel_v2.cu. No standalone compilation.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#ifndef CELERIS_STEP_ESOPULL_SYNC_CU
|
||||||
|
#define CELERIS_STEP_ESOPULL_SYNC_CU
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DecodeCellsToPhysical: backing layout -> physical DDF (compact list)
|
||||||
|
//
|
||||||
|
// Uses ``t`` as the step count for EsoPull semantic parity decoding.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
__global__ void DecodeCellsToPhysical(
|
||||||
|
const fpxx* fi,
|
||||||
|
float* fi_phys,
|
||||||
|
const unsigned int* cell_idx,
|
||||||
|
unsigned int n_cells,
|
||||||
|
unsigned long t)
|
||||||
|
{
|
||||||
|
unsigned int tid = threadIdx.x + blockIdx.x * blockDim.x;
|
||||||
|
if (tid >= n_cells) return;
|
||||||
|
|
||||||
|
unsigned long k = (unsigned long)cell_idx[tid];
|
||||||
|
unsigned int x, y; coordinates(k, x, y);
|
||||||
|
unsigned long j[NQ]; compute_neighbors(x, y, j);
|
||||||
|
|
||||||
|
float f[NQ];
|
||||||
|
load_physical_node_esopull(k, fi, j, t, f);
|
||||||
|
|
||||||
|
// DDF-shifting: backing layout stores f-w, unshift to physical for host patch
|
||||||
|
#if USE_DDF_SHIFTING
|
||||||
|
#pragma unroll
|
||||||
|
for (int i = 0; i < NQ; i++)
|
||||||
|
f[i] += (float)d_w[i];
|
||||||
|
#endif
|
||||||
|
|
||||||
|
for (int i = 0; i < NQ; i++)
|
||||||
|
fi_phys[tid * NQ + i] = f[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// EncodePhysicalToCells: physical DDF -> backing layout (compact list)
|
||||||
|
//
|
||||||
|
// Reads physical (pre-collision) DDF from *fi_phys*, applies collision
|
||||||
|
// to produce post-collision DDF, then encodes into the EsoPull backing
|
||||||
|
// layout on *fi*. The backing layout stores post-collision values,
|
||||||
|
// matching what store_f_esopull would produce at step *t*.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
__global__ void EncodePhysicalToCells(
|
||||||
|
fpxx* fi,
|
||||||
|
const float* fi_phys,
|
||||||
|
const unsigned int* cell_idx,
|
||||||
|
unsigned int n_cells,
|
||||||
|
unsigned long t)
|
||||||
|
{
|
||||||
|
unsigned int tid = threadIdx.x + blockIdx.x * blockDim.x;
|
||||||
|
if (tid >= n_cells) return;
|
||||||
|
|
||||||
|
unsigned long k = (unsigned long)cell_idx[tid];
|
||||||
|
unsigned int x, y; coordinates(k, x, y);
|
||||||
|
unsigned long j[NQ]; compute_neighbors(x, y, j);
|
||||||
|
|
||||||
|
float f[NQ];
|
||||||
|
for (int i = 0; i < NQ; i++)
|
||||||
|
f[i] = fi_phys[tid * NQ + i];
|
||||||
|
|
||||||
|
// Collide: physical (pre-collision) -> post-collision
|
||||||
|
float rho_n, ux, uy;
|
||||||
|
compute_rho_u(f, rho_n, ux, uy);
|
||||||
|
collide_dispatch(f, rho_n, ux, uy);
|
||||||
|
|
||||||
|
store_physical_node_esopull(k, f, fi, j, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // CELERIS_STEP_ESOPULL_SYNC_CU
|
||||||
@ -391,17 +391,9 @@ class Simulation:
|
|||||||
def sync_bodies(self) -> None:
|
def sync_bodies(self) -> None:
|
||||||
"""Apply pending body edits (add/remove) to a running simulation.
|
"""Apply pending body edits (add/remove) to a running simulation.
|
||||||
|
|
||||||
This is the main entry point for runtime body topology changes.
|
Supports both ``double_buffer`` and ``esopull`` streaming modes.
|
||||||
It downloads the current DDF, rebuilds topology, recompiles the
|
In EsoPull mode, the backing layout is decoded to physical DDF on
|
||||||
kernel for the new object count, patches the DDF for geometry
|
GPU for the patched cells only, patched on host, then encoded back.
|
||||||
changes, and re-uploads everything to GPU.
|
|
||||||
|
|
||||||
Currently only supports ``double_buffer`` streaming mode.
|
|
||||||
``esopull`` mode raises ``NotImplementedError``.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If called before ``initialize()``.
|
|
||||||
NotImplementedError: If streaming mode is ``esopull``.
|
|
||||||
"""
|
"""
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
raise RuntimeError("Call initialize() before sync_bodies()")
|
raise RuntimeError("Call initialize() before sync_bodies()")
|
||||||
@ -413,28 +405,34 @@ class Simulation:
|
|||||||
if not self.bodies.has_pending_edit():
|
if not self.bodies.has_pending_edit():
|
||||||
return
|
return
|
||||||
|
|
||||||
# 3. Check streaming mode
|
is_esopull = (self.lbm_cfg.streaming == "esopull")
|
||||||
if self.lbm_cfg.streaming == "esopull":
|
|
||||||
raise NotImplementedError(
|
|
||||||
"Runtime body sync is not yet supported for esopull "
|
|
||||||
"streaming mode. Use double_buffer instead.")
|
|
||||||
|
|
||||||
# 4. Download current DDF to host (snapshot for patching)
|
# 3. Build sync plan (same for both modes)
|
||||||
|
plan = self.bodies.build_sync_plan(self.field)
|
||||||
|
|
||||||
|
# 4. Obtain physical DDF on host (mode-dependent path)
|
||||||
|
if is_esopull:
|
||||||
|
patch_set = _build_patch_set(
|
||||||
|
plan.added_solid_mask, plan.released_fluid_mask,
|
||||||
|
plan.curved_host[0], nx=self.field.nx, ny=self.field.ny)
|
||||||
|
scratch_gpu, scratch_host, idx_gpu = \
|
||||||
|
self.field.esopull_sync_decode(
|
||||||
|
patch_set, self.stepper.step_count, self.stream)
|
||||||
|
_scatter_to_field(self.field, scratch_host, patch_set)
|
||||||
|
else:
|
||||||
self.field.download_ddf(
|
self.field.download_ddf(
|
||||||
step_id=self.stepper.step_count, force=True)
|
step_id=self.stepper.step_count, force=True)
|
||||||
|
|
||||||
old_ddf = self.field.ddf.copy()
|
old_ddf = self.field.ddf.copy()
|
||||||
old_flags = self.field.flag.copy()
|
old_flags = self.field.flag.copy()
|
||||||
|
|
||||||
# 5. Build sync plan
|
# 5. Runtime recompile for new object count
|
||||||
plan = self.bodies.build_sync_plan(self.field)
|
|
||||||
|
|
||||||
# 6. Runtime recompile for new object count
|
|
||||||
self._recompile(plan.next_count)
|
self._recompile(plan.next_count)
|
||||||
|
|
||||||
# 7. Apply new topology data (flags, compact lists, params)
|
# 6. Apply new topology data (flags, compact lists, params)
|
||||||
self.bodies.apply_sync_plan(self.field, plan)
|
self.bodies.apply_sync_plan(self.field, plan)
|
||||||
|
|
||||||
# 8. DDF patch for geometry changes
|
# 7. DDF patch for geometry changes (works on physical DDF, mode-agnostic)
|
||||||
cl_fluid_idx = plan.curved_host[0]
|
cl_fluid_idx = plan.curved_host[0]
|
||||||
patch_ddf_for_body_sync(
|
patch_ddf_for_body_sync(
|
||||||
self.field,
|
self.field,
|
||||||
@ -442,6 +440,15 @@ class Simulation:
|
|||||||
plan.added_solid_mask, plan.released_fluid_mask,
|
plan.added_solid_mask, plan.released_fluid_mask,
|
||||||
curved_fluid_indices=cl_fluid_idx,
|
curved_fluid_indices=cl_fluid_idx,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 8. Upload patched DDF (mode-dependent path)
|
||||||
|
if is_esopull:
|
||||||
|
_gather_from_field(self.field, scratch_host, patch_set)
|
||||||
|
n = len(patch_set)
|
||||||
|
self.field.esopull_sync_encode(
|
||||||
|
scratch_gpu, scratch_host, idx_gpu,
|
||||||
|
n, self.stepper.step_count, self.stream)
|
||||||
|
else:
|
||||||
upload_patched_ddf(
|
upload_patched_ddf(
|
||||||
self.field,
|
self.field,
|
||||||
plan.added_solid_mask, plan.released_fluid_mask,
|
plan.added_solid_mask, plan.released_fluid_mask,
|
||||||
@ -649,3 +656,51 @@ class Simulation:
|
|||||||
"""Validate object-count and obs layout contracts before stepping."""
|
"""Validate object-count and obs layout contracts before stepping."""
|
||||||
self._assert_object_count_contract()
|
self._assert_object_count_contract()
|
||||||
self._assert_obs_layout_contract()
|
self._assert_obs_layout_contract()
|
||||||
|
|
||||||
|
|
||||||
|
# -- Module-level helpers -------------------------------------------------------
|
||||||
|
|
||||||
|
def _build_patch_set(added_solid_mask, released_fluid_mask, curved_fluid_idx,
|
||||||
|
nx=None, ny=None):
|
||||||
|
"""Return sorted uint32 array of cell indices that need DDF patching.
|
||||||
|
|
||||||
|
Includes a 1-cell halo because ``store_physical_node_esopull`` writes
|
||||||
|
to neighbor slots (D2Q9 directions 1,3,5,7). Neighbors must be
|
||||||
|
decoded/encoded together to avoid corrupting the backing layout.
|
||||||
|
"""
|
||||||
|
s = set()
|
||||||
|
s.update(np.where(added_solid_mask)[0].tolist())
|
||||||
|
s.update(np.where(released_fluid_mask)[0].tolist())
|
||||||
|
if curved_fluid_idx is not None:
|
||||||
|
s.update(curved_fluid_idx.tolist())
|
||||||
|
|
||||||
|
if nx is None or ny is None:
|
||||||
|
return np.array(sorted(s), dtype=np.uint32)
|
||||||
|
|
||||||
|
# Expand to include 1-cell halo (D2Q9 Moore neighbourhood)
|
||||||
|
expanded = set(s)
|
||||||
|
for idx in s:
|
||||||
|
x = idx % nx
|
||||||
|
y = idx // nx
|
||||||
|
for dx in (-1, 0, 1):
|
||||||
|
for dy in (-1, 0, 1):
|
||||||
|
if dx == 0 and dy == 0:
|
||||||
|
continue
|
||||||
|
xn, yn = x + dx, y + dy
|
||||||
|
if 0 <= xn < nx and 0 <= yn < ny:
|
||||||
|
expanded.add(xn + yn * nx)
|
||||||
|
return np.array(sorted(expanded), dtype=np.uint32)
|
||||||
|
|
||||||
|
|
||||||
|
def _scatter_to_field(field, scratch_host, patch_indices):
|
||||||
|
"""Copy decoded physical DDF from scratch buffer into field.ddf."""
|
||||||
|
nq = field.nq
|
||||||
|
for j, idx in enumerate(patch_indices):
|
||||||
|
field.ddf[idx * nq:(idx + 1) * nq] = scratch_host[j * nq:(j + 1) * nq]
|
||||||
|
|
||||||
|
|
||||||
|
def _gather_from_field(field, scratch_host, patch_indices):
|
||||||
|
"""Copy patched physical DDF from field.ddf back to scratch buffer."""
|
||||||
|
nq = field.nq
|
||||||
|
for j, idx in enumerate(patch_indices):
|
||||||
|
scratch_host[j * nq:(j + 1) * nq] = field.ddf[idx * nq:(idx + 1) * nq]
|
||||||
|
|||||||
@ -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."""
|
Requires GPU."""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pycuda.driver as cuda
|
import pycuda.driver as cuda
|
||||||
import pycuda.autoinit
|
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
|
# Use a small grid for fast compilation and test execution
|
||||||
@ -16,7 +19,7 @@ NX, NY = 128, 64
|
|||||||
|
|
||||||
|
|
||||||
class TestSyncBodiesSkeleton(unittest.TestCase):
|
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:
|
def _make_sim(self) -> Simulation:
|
||||||
"""Create a Simulation with a small double_buffer D2Q9 grid."""
|
"""Create a Simulation with a small double_buffer D2Q9 grid."""
|
||||||
@ -125,40 +128,118 @@ class TestSyncBodiesSkeleton(unittest.TestCase):
|
|||||||
self.assertEqual(sim.bodies.count, 1)
|
self.assertEqual(sim.bodies.count, 1)
|
||||||
sim.close()
|
sim.close()
|
||||||
|
|
||||||
def test_esopull_raises_not_implemented(self):
|
|
||||||
"""sync_bodies() with esopull should raise NotImplementedError."""
|
class TestSyncBodiesEsoPull(unittest.TestCase):
|
||||||
# Create a sim with esopull streaming
|
"""Test sync_bodies() with esopull streaming."""
|
||||||
from CelerisLab.config import load_lbm_config
|
|
||||||
cfg = load_lbm_config()
|
def _make_esopull_sim(self, nx=128, ny=64) -> Simulation:
|
||||||
cfg.streaming = "esopull"
|
"""Create a Simulation with a small esopull D2Q9 grid."""
|
||||||
# We need to build the sim manually to override streaming
|
cfg = {
|
||||||
sim = Simulation.__new__(Simulation)
|
"grid": {"lattice_model": "D2Q9", "nx": nx, "ny": ny, "nz": 1},
|
||||||
sim._stream = None
|
"physics": {"data_type": "FP32", "viscosity": 0.05,
|
||||||
sim.lbm_cfg = cfg
|
"velocity": 0.03, "rho": 1.0},
|
||||||
from CelerisLab.config import BodyConfig
|
"method": {
|
||||||
sim.body_cfg = BodyConfig()
|
"collision": "SRT", "streaming": "esopull",
|
||||||
from CelerisLab.cuda.context import CudaContext
|
"store_precision": "FP32", "ddf_shifting": False,
|
||||||
sim.ctx = CudaContext(0)
|
"les": {"enabled": False, "cs": 0.16, "closed_form": True},
|
||||||
from CelerisLab.cuda import compiler_v2 as compiler
|
"trt": {"magic_param": 0.1875},
|
||||||
arch = sim._resolve_compile_arch = lambda: sim.ctx.sm_arch
|
"inlet": {"profile": "parabolic", "scheme": "zou_he_local"},
|
||||||
arch_val = CudaContext(0).sm_arch
|
"outlet": {"mode": "neq_extrap", "backflow_clamp": True,
|
||||||
compiler.generate_config(cfg, n_objects=0)
|
"blend_alpha": 0.7},
|
||||||
ptx_path = compiler.compile_kernel(arch=arch_val)
|
"y_wall_bc": "bounce_back",
|
||||||
module = compiler.load_module(ptx_path)
|
"omega_guard": {"min": 0.01, "max": 1.99},
|
||||||
sim._ptx_path = ptx_path
|
},
|
||||||
sim._module = module
|
"cuda": {"threads_per_block": 256, "compute_capability": "auto"},
|
||||||
from CelerisLab.lbm.field import LBMField
|
}
|
||||||
sim.field = LBMField(cfg, module)
|
tmpd = tempfile.mkdtemp(prefix="esopull_sync_")
|
||||||
from CelerisLab.lbm.stepper import LBMStepper
|
lbm_path = os.path.join(tmpd, "config_lbm.json")
|
||||||
sim.stepper = LBMStepper(sim.field, module, cfg)
|
with open(lbm_path, "w") as f:
|
||||||
from CelerisLab.body.manager import ObjectManager
|
json.dump(cfg, f)
|
||||||
sim.bodies = ObjectManager(
|
return Simulation(lbm_config_path=lbm_path)
|
||||||
cfg.nx, cfg.ny, cfg.nz, cfg.nq, cfg)
|
|
||||||
sim._initialized = True
|
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)
|
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()
|
sim.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user