Support tagged shifted storage in blocked host transfers
This commit is contained in:
1 parent
fbe73779c1
commit
b811b643e7
7 files changed
+310
-25
No files matched your search
@@ -21,6 +21,7 @@ import pycuda.driver as cuda
|
|||||||
from ..cuda import compiler_v2 as compiler
|
from ..cuda import compiler_v2 as compiler
|
||||||
from .blocks import NQ, BlockLayout2D, blocked_link_tables
|
from .blocks import NQ, BlockLayout2D, blocked_link_tables
|
||||||
from .descriptors import D2Q9_EX, D2Q9_EY, FLUID, SOLID
|
from .descriptors import D2Q9_EX, D2Q9_EY, FLUID, SOLID
|
||||||
|
from .population_storage import _StoredPopulations, _encode_physical, _decode_stored
|
||||||
|
|
||||||
MAX_GRID_DIM = 65535 # CUDA gridDim.y/z limit (rows, blocks)
|
MAX_GRID_DIM = 65535 # CUDA gridDim.y/z limit (rows, blocks)
|
||||||
|
|
||||||
@@ -85,10 +86,10 @@ class PeriodicBlockStepper:
|
|||||||
checked.validate()
|
checked.validate()
|
||||||
if (not checked.is_d2q9 or checked.collision != "SRT"
|
if (not checked.is_d2q9 or checked.collision != "SRT"
|
||||||
or checked.streaming != "double_buffer" or checked.store_precision != "FP32"
|
or checked.streaming != "double_buffer" or checked.store_precision != "FP32"
|
||||||
or checked.ddf_shifting or checked.les_enabled
|
or checked.les_enabled
|
||||||
or (checked.ny, checked.nx) != layout.shape):
|
or (checked.ny, checked.nx) != layout.shape):
|
||||||
raise ValueError("PeriodicBlockStepper requires matching D2Q9/SRT/FP32/"
|
raise ValueError("PeriodicBlockStepper requires matching D2Q9/SRT/FP32/"
|
||||||
"unshifted/double_buffer config without LES")
|
"double_buffer config without LES")
|
||||||
ly, lx = layout.local_shape
|
ly, lx = layout.local_shape
|
||||||
pairs = len(layout.halo_dst)
|
pairs = len(layout.halo_dst)
|
||||||
tpb = checked.threads_per_block
|
tpb = checked.threads_per_block
|
||||||
@@ -99,6 +100,7 @@ class PeriodicBlockStepper:
|
|||||||
links = 0 if self.boundary is None else len(self.boundary["cell"])
|
links = 0 if self.boundary is None else len(self.boundary["cell"])
|
||||||
if links >= 2**31:
|
if links >= 2**31:
|
||||||
raise ValueError("cut links exceed launch limits")
|
raise ValueError("cut links exceed launch limits")
|
||||||
|
self._ddf_shifting = bool(checked.ddf_shifting)
|
||||||
self.layout = layout
|
self.layout = layout
|
||||||
self.ctx = context
|
self.ctx = context
|
||||||
self._links = links
|
self._links = links
|
||||||
@@ -177,14 +179,20 @@ class PeriodicBlockStepper:
|
|||||||
"""Upload dense G_0; the spare buffer starts NaN so stale halos cannot hide."""
|
"""Upload dense G_0; the spare buffer starts NaN so stale halos cannot hide."""
|
||||||
with self._lock, self.ctx.activate():
|
with self._lock, self.ctx.activate():
|
||||||
self._require("new", "ready")
|
self._require("new", "ready")
|
||||||
dense = np.array(g, dtype=np.float32, order="C", copy=True)
|
dense = np.array(g, dtype=np.float64 if self._ddf_shifting else np.float32,
|
||||||
|
order="C", copy=True)
|
||||||
if dense.shape != (NQ, *self.layout.shape):
|
if dense.shape != (NQ, *self.layout.shape):
|
||||||
raise ValueError("Expected a Q-major (9, ny, nx) array")
|
raise ValueError("Expected a Q-major (9, ny, nx) array")
|
||||||
fluid = np.ones(self.layout.shape, bool) if self.boundary is None else ~self.boundary["solid"]
|
fluid = np.ones(self.layout.shape, bool) if self.boundary is None else ~self.boundary["solid"]
|
||||||
if not np.isfinite(dense[:, fluid]).all() or (dense[:, fluid].sum(axis=0) <= 0).any():
|
if not np.isfinite(dense[:, fluid]).all() or (dense[:, fluid].sum(axis=0) <= 0).any():
|
||||||
raise ValueError("Fluid populations must be finite with positive density")
|
raise ValueError("Fluid populations must be finite with positive density")
|
||||||
dense[:, ~fluid] = np.nan # Solid cores are deliberately invalid.
|
dense[:, ~fluid] = np.nan # Solid cores are deliberately invalid.
|
||||||
blocked = self.layout.to_blocked(dense)
|
encoded = _encode_physical(dense, self._ddf_shifting)
|
||||||
|
values = encoded[:, fluid]
|
||||||
|
if (not np.isfinite(values).all()
|
||||||
|
or (values.sum(axis=0, dtype=np.float64) + int(self._ddf_shifting) <= 0).any()):
|
||||||
|
raise ValueError("Stored fluid populations must be finite with positive density")
|
||||||
|
blocked = self.layout.to_blocked(encoded)
|
||||||
spare = np.full_like(blocked, np.nan)
|
spare = np.full_like(blocked, np.nan)
|
||||||
try:
|
try:
|
||||||
cuda.memcpy_htod(self._current, blocked)
|
cuda.memcpy_htod(self._current, blocked)
|
||||||
@@ -225,7 +233,12 @@ class PeriodicBlockStepper:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def download(self):
|
def download(self):
|
||||||
"""Return dense (9, ny, nx) FP32 core populations; no host cache."""
|
"""Return physical core populations (FP64 when shifted); no host cache."""
|
||||||
|
snapshot = self._download_raw()
|
||||||
|
return _decode_stored(snapshot.values[0], snapshot.shifted)
|
||||||
|
|
||||||
|
def _download_raw(self):
|
||||||
|
"""Return one tagged dense FP32 population array without conversion."""
|
||||||
with self._lock, self.ctx.activate():
|
with self._lock, self.ctx.activate():
|
||||||
self._require("ready")
|
self._require("ready")
|
||||||
blocked = np.empty(self.layout.blocked_shape, np.float32)
|
blocked = np.empty(self.layout.blocked_shape, np.float32)
|
||||||
@@ -234,7 +247,7 @@ class PeriodicBlockStepper:
|
|||||||
except BaseException:
|
except BaseException:
|
||||||
self._status = "failed"
|
self._status = "failed"
|
||||||
raise
|
raise
|
||||||
return self.layout.to_dense(blocked)
|
return _StoredPopulations((self.layout.to_dense(blocked),), self._ddf_shifting)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Release owned resources; retain them for retry if context activation fails."""
|
"""Release owned resources; retain them for retry if context activation fails."""
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""Private D2Q9 FP32 storage boundary for the opt-in blocked steppers."""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Unit-density reference weights in FP64; sum(w)=1 matches rho=1+sum(g).
|
||||||
|
_WEIGHTS = np.array([4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36],
|
||||||
|
dtype=np.float64)[:, None, None]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, eq=False)
|
||||||
|
class _StoredPopulations:
|
||||||
|
values: tuple
|
||||||
|
shifted: bool
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_physical(values, shifted):
|
||||||
|
if not shifted:
|
||||||
|
return np.array(values, dtype=np.float32, order="C", copy=True)
|
||||||
|
return np.ascontiguousarray(np.asarray(values, dtype=np.float64) - _WEIGHTS,
|
||||||
|
dtype=np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_stored(values, shifted):
|
||||||
|
return values.astype(np.float64) + _WEIGHTS if shifted else values
|
||||||
@@ -25,6 +25,7 @@ from .refinement_tables import (blocked_base_tables, blocked_fine_tables, # noq
|
|||||||
BLOCKED_BOUNDARY_DEVICE_COLUMNS)
|
BLOCKED_BOUNDARY_DEVICE_COLUMNS)
|
||||||
from .descriptors import SOLID
|
from .descriptors import SOLID
|
||||||
from .raw_exchange import summarize_exchange
|
from .raw_exchange import summarize_exchange
|
||||||
|
from .population_storage import _StoredPopulations, _encode_physical, _decode_stored
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -238,6 +239,9 @@ class FixedPatchStepper:
|
|||||||
level_blocks = [None if b is None else tuple(b) for b in level_blocks]
|
level_blocks = [None if b is None else tuple(b) for b in level_blocks]
|
||||||
_check_levels(refinements, level_blocks, level_objects, level_jets)
|
_check_levels(refinements, level_blocks, level_objects, level_jets)
|
||||||
depth = len(refinements) + 1
|
depth = len(refinements) + 1
|
||||||
|
self._ddf_shifting = bool(cfg.ddf_shifting)
|
||||||
|
if self._ddf_shifting and any(b is None for b in level_blocks):
|
||||||
|
raise ValueError("shifted populations require all-blocked levels; dense/mixed is unsupported")
|
||||||
self._wall_aware = coarse_boundary == "slot_reservoir"
|
self._wall_aware = coarse_boundary == "slot_reservoir"
|
||||||
if self._wall_aware and (cfg.y_wall_bc != "on_site_velocity" or cfg.top_wall_bc != "reservoir"
|
if self._wall_aware and (cfg.y_wall_bc != "on_site_velocity" or cfg.top_wall_bc != "reservoir"
|
||||||
or cfg.velocity != 0 or not 2 <= cfg.wall_slot_width < cfg.nx):
|
or cfg.velocity != 0 or not 2 <= cfg.wall_slot_width < cfg.nx):
|
||||||
@@ -469,23 +473,54 @@ class FixedPatchStepper:
|
|||||||
for array, leaf in zip(arrays, self._leaf):
|
for array, leaf in zip(arrays, self._leaf):
|
||||||
array[:, ~leaf] = np.nan
|
array[:, ~leaf] = np.nan
|
||||||
|
|
||||||
def initialize(self, *level_g, coarse_g=None, fine_g=None, child_g=None):
|
def _check_storage(self):
|
||||||
"""Upload an explicit G_0, one Q-major array per level; non-leaves are discarded.
|
if any(bool(c.ddf_shifting) != self._ddf_shifting for c in self._level_cfgs):
|
||||||
|
raise ValueError("mixed population representations are unsupported")
|
||||||
|
if self._ddf_shifting and any(layout is None for layout in self._layouts):
|
||||||
|
raise ValueError("shifted populations require all-blocked levels")
|
||||||
|
|
||||||
Pass the level arrays positionally (L0 first), or use the original
|
def initialize(self, *level_g, coarse_g=None, fine_g=None, child_g=None):
|
||||||
two/three-level names coarse_g, fine_g[, child_g]; mixing is rejected.
|
"""Upload physical postcollision G, encoding before any FP32 rounding.
|
||||||
|
|
||||||
|
Shifted input should be physical FP64 to retain small perturbations.
|
||||||
|
Public initialization resets the step count; non-leaves are discarded.
|
||||||
"""
|
"""
|
||||||
given = _initial_levels(level_g, coarse_g, fine_g, child_g)
|
given = _initial_levels(level_g, coarse_g, fine_g, child_g)
|
||||||
with self._lock, self.ctx.activate():
|
with self._lock, self.ctx.activate():
|
||||||
self._require("new", "ready")
|
self._require("new", "ready")
|
||||||
|
self._check_storage()
|
||||||
if len(given) != len(self._levels):
|
if len(given) != len(self._levels):
|
||||||
raise ValueError("Expected one Q-major array per level")
|
raise ValueError("Expected one Q-major array per level")
|
||||||
arrays = [np.array(a, dtype=np.float32, order="C", copy=True) for a in given]
|
dtype = np.float64 if self._ddf_shifting else np.float32
|
||||||
|
arrays = [np.array(a, dtype=dtype, order="C", copy=True) for a in given]
|
||||||
if any(a.shape != level.shape for a, level in zip(arrays, self._levels)):
|
if any(a.shape != level.shape for a, level in zip(arrays, self._levels)):
|
||||||
raise ValueError("Expected Q-major (9,ny,nx) arrays for every level")
|
raise ValueError("Expected Q-major (9,ny,nx) arrays for every level")
|
||||||
for array, mask in zip(arrays, self._leaf):
|
for array, mask in zip(arrays, self._leaf):
|
||||||
if not np.isfinite(array[:, mask]).all() or (array[:, mask].sum(axis=0) <= 0).any():
|
if not np.isfinite(array[:, mask]).all() or (array[:, mask].sum(axis=0) <= 0).any():
|
||||||
raise ValueError("Leaf populations must be finite with positive density")
|
raise ValueError("Leaf populations must be finite with positive density")
|
||||||
|
encoded = tuple(_encode_physical(a, self._ddf_shifting) for a in arrays)
|
||||||
|
self._initialize_raw(_StoredPopulations(encoded, self._ddf_shifting))
|
||||||
|
|
||||||
|
def _initialize_raw(self, snapshot):
|
||||||
|
"""Upload an internally tagged same-representation state without a codec."""
|
||||||
|
with self._lock, self.ctx.activate():
|
||||||
|
self._require("new", "ready")
|
||||||
|
self._check_storage()
|
||||||
|
if (not isinstance(snapshot, _StoredPopulations)
|
||||||
|
or type(snapshot.shifted) is not bool or snapshot.shifted != self._ddf_shifting):
|
||||||
|
raise ValueError("stored population representation mismatch")
|
||||||
|
if len(snapshot.values) != len(self._levels):
|
||||||
|
raise ValueError("Expected one stored array per level")
|
||||||
|
if any(not isinstance(a, np.ndarray) or a.dtype != np.float32 for a in snapshot.values):
|
||||||
|
raise ValueError("stored populations must be FP32")
|
||||||
|
arrays = [np.array(a, order="C", copy=True) for a in snapshot.values]
|
||||||
|
if any(a.shape != level.shape for a, level in zip(arrays, self._levels)):
|
||||||
|
raise ValueError("Expected Q-major (9,ny,nx) arrays for every level")
|
||||||
|
for array, mask in zip(arrays, self._leaf):
|
||||||
|
values = array[:, mask]
|
||||||
|
if (not np.isfinite(values).all()
|
||||||
|
or (values.sum(axis=0, dtype=np.float64) + int(self._ddf_shifting) <= 0).any()):
|
||||||
|
raise ValueError("Leaf populations must be finite with positive density")
|
||||||
self._mask_nonleaves(*arrays)
|
self._mask_nonleaves(*arrays)
|
||||||
arrays = [a if level.layout is None else level.layout.to_blocked(a)
|
arrays = [a if level.layout is None else level.layout.to_blocked(a)
|
||||||
for a, level in zip(arrays, self._levels)]
|
for a, level in zip(arrays, self._levels)]
|
||||||
@@ -518,7 +553,7 @@ class FixedPatchStepper:
|
|||||||
construction/upload errors never invalidate the old instance; failure
|
construction/upload errors never invalidate the old instance; failure
|
||||||
to synchronize/read its device state does, as with step/download.
|
to synchronize/read its device state does, as with step/download.
|
||||||
"""
|
"""
|
||||||
from .synchronized_remap import (RemapTopology, remap_synchronized,
|
from .synchronized_remap import (RemapTopology, remap_synchronized, _remap_stored,
|
||||||
same_physical_boundary, boundary_velocity)
|
same_physical_boundary, boundary_velocity)
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -558,14 +593,16 @@ class FixedPatchStepper:
|
|||||||
with self.ctx.activate():
|
with self.ctx.activate():
|
||||||
try:
|
try:
|
||||||
self._stream.synchronize()
|
self._stream.synchronize()
|
||||||
source = self.download()
|
source = self._download_raw() if self._ddf_shifting else self.download()
|
||||||
except BaseException:
|
except BaseException:
|
||||||
self._status = "failed"
|
self._status = "failed"
|
||||||
raise
|
raise
|
||||||
count = self._step_count
|
count = self._step_count
|
||||||
transferred = remap_synchronized(
|
transfer = _remap_stored if self._ddf_shifting else remap_synchronized
|
||||||
|
options = dict(shifted=self._ddf_shifting) if self._ddf_shifting else {}
|
||||||
|
transferred = transfer(
|
||||||
old, new, source, base_count=count, level_steps=(count, 2*count, 4*count),
|
old, new, source, base_count=count, level_steps=(count, 2*count, 4*count),
|
||||||
phase="postcollision_g")
|
phase="postcollision_g", **options)
|
||||||
candidate = type(self)(
|
candidate = type(self)(
|
||||||
cfg, pairs[0].bounds, context=self.ctx, base_block=base_block,
|
cfg, pairs[0].bounds, context=self.ctx, base_block=base_block,
|
||||||
fine_block=fine_block, child_block=child_block,
|
fine_block=fine_block, child_block=child_block,
|
||||||
@@ -573,7 +610,10 @@ class FixedPatchStepper:
|
|||||||
child_objects=child_objects, child_jets=jets,
|
child_objects=child_objects, child_jets=jets,
|
||||||
observe_raw_exchange=self._observe_raw_exchange)
|
observe_raw_exchange=self._observe_raw_exchange)
|
||||||
try:
|
try:
|
||||||
candidate.initialize(*transferred.g)
|
if self._ddf_shifting:
|
||||||
|
candidate._initialize_raw(_StoredPopulations(transferred.g, transferred.shifted))
|
||||||
|
else:
|
||||||
|
candidate.initialize(*transferred.g)
|
||||||
candidate._step_count = count
|
candidate._step_count = count
|
||||||
except BaseException:
|
except BaseException:
|
||||||
try:
|
try:
|
||||||
@@ -772,9 +812,15 @@ class FixedPatchStepper:
|
|||||||
reference_center=self._exchange_centers[level][body_id])
|
reference_center=self._exchange_centers[level][body_id])
|
||||||
|
|
||||||
def download(self):
|
def download(self):
|
||||||
"""Return synchronized G arrays (one per level), NaN on all non-leaves; no host cache."""
|
"""Return physical G (FP64 for shifted storage), NaN on non-leaves."""
|
||||||
|
snapshot = self._download_raw()
|
||||||
|
return tuple(_decode_stored(a, snapshot.shifted) for a in snapshot.values)
|
||||||
|
|
||||||
|
def _download_raw(self):
|
||||||
|
"""Current owned populations with a representation tag; no add/subtract w."""
|
||||||
with self._lock, self.ctx.activate():
|
with self._lock, self.ctx.activate():
|
||||||
self._require("ready")
|
self._require("ready")
|
||||||
|
self._check_storage()
|
||||||
arrays = [np.empty(level.shape if level.layout is None else level.layout.blocked_shape,
|
arrays = [np.empty(level.shape if level.layout is None else level.layout.blocked_shape,
|
||||||
np.float32) for level in self._levels]
|
np.float32) for level in self._levels]
|
||||||
try:
|
try:
|
||||||
@@ -786,7 +832,7 @@ class FixedPatchStepper:
|
|||||||
arrays = [a if level.layout is None else level.layout.to_dense(a)
|
arrays = [a if level.layout is None else level.layout.to_dense(a)
|
||||||
for a, level in zip(arrays, self._levels)]
|
for a, level in zip(arrays, self._levels)]
|
||||||
self._mask_nonleaves(*arrays)
|
self._mask_nonleaves(*arrays)
|
||||||
return tuple(arrays)
|
return _StoredPopulations(tuple(arrays), self._ddf_shifting)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Release owned resources; retain them for retry if context activation fails."""
|
"""Release owned resources; retain them for retry if context activation fails."""
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from .refinement import (ACTIVE, FixedPatch2D, FixedPatchSet2D, FineBoundary2D,
|
|||||||
nest_patch_set, prepare_fine_boundary)
|
nest_patch_set, prepare_fine_boundary)
|
||||||
from .descriptors import SOLID
|
from .descriptors import SOLID
|
||||||
from ..body.jets import surface_link_velocity
|
from ..body.jets import surface_link_velocity
|
||||||
|
from .population_storage import _StoredPopulations
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, eq=False)
|
@dataclass(frozen=True, eq=False)
|
||||||
@@ -118,6 +119,7 @@ class RemapCandidate:
|
|||||||
phase: str
|
phase: str
|
||||||
origin: tuple
|
origin: tuple
|
||||||
spacing: float
|
spacing: float
|
||||||
|
shifted: bool = False
|
||||||
|
|
||||||
|
|
||||||
def _same_parent(a, b):
|
def _same_parent(a, b):
|
||||||
@@ -126,7 +128,7 @@ def _same_parent(a, b):
|
|||||||
and a.tau_coarse == b.tau_coarse and a.tau_fine == b.tau_fine)
|
and a.tau_coarse == b.tau_coarse and a.tau_fine == b.tau_fine)
|
||||||
|
|
||||||
|
|
||||||
def _read_leaves(topology, arrays, layouts):
|
def _read_leaves(topology, arrays, layouts, *, shifted=False):
|
||||||
"""Unpack only owned cores; nonleaf values are not evidence of validity."""
|
"""Unpack only owned cores; nonleaf values are not evidence of validity."""
|
||||||
if len(arrays) != 3 or len(layouts) != 3:
|
if len(arrays) != 3 or len(layouts) != 3:
|
||||||
raise ValueError("expected three G arrays and three layouts")
|
raise ValueError("expected three G arrays and three layouts")
|
||||||
@@ -140,8 +142,10 @@ def _read_leaves(topology, arrays, layouts):
|
|||||||
a = layout.to_dense(a)
|
a = layout.to_dense(a)
|
||||||
if a.shape != (9, *leaf.shape):
|
if a.shape != (9, *leaf.shape):
|
||||||
raise ValueError("G must have Q-major (9, ny, nx) shape")
|
raise ValueError("G must have Q-major (9, ny, nx) shape")
|
||||||
|
if shifted and a.dtype != np.float32:
|
||||||
|
raise ValueError("shifted stored populations must be FP32")
|
||||||
values = a[:, leaf]
|
values = a[:, leaf]
|
||||||
if not np.isfinite(values).all() or (values.sum(axis=0, dtype=np.float64) <= 0).any():
|
if not np.isfinite(values).all() or (values.sum(axis=0, dtype=np.float64) + int(shifted) <= 0).any():
|
||||||
raise ValueError("required leaf donors must be finite with positive density")
|
raise ValueError("required leaf donors must be finite with positive density")
|
||||||
result.append(a)
|
result.append(a)
|
||||||
if len({a.dtype for a in result}) != 1:
|
if len({a.dtype for a in result}) != 1:
|
||||||
@@ -166,6 +170,20 @@ def remap_synchronized(old, new, old_g, *, base_count, level_steps, phase,
|
|||||||
the source dtype. No equilibrium reconstruction or nonequilibrium scaling.
|
the source dtype. No equilibrium reconstruction or nonequilibrium scaling.
|
||||||
All destination nonleaves are NaN. Inputs and their write flags are intact.
|
All destination nonleaves are NaN. Inputs and their write flags are intact.
|
||||||
"""
|
"""
|
||||||
|
return _remap(old, new, old_g, base_count=base_count, level_steps=level_steps,
|
||||||
|
phase=phase, origin=origin, spacing=spacing, layouts=layouts)
|
||||||
|
|
||||||
|
|
||||||
|
def _remap_stored(old, new, snapshot, *, shifted, **kwargs):
|
||||||
|
"""Private same-representation transfer; public remap remains physical."""
|
||||||
|
if (not isinstance(snapshot, _StoredPopulations) or type(shifted) is not bool
|
||||||
|
or type(snapshot.shifted) is not bool or snapshot.shifted != shifted):
|
||||||
|
raise ValueError("stored population representation mismatch")
|
||||||
|
return _remap(old, new, snapshot.values, shifted=shifted, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _remap(old, new, old_g, *, base_count, level_steps, phase,
|
||||||
|
origin=(0., 0.), spacing=1., layouts=None, shifted=False):
|
||||||
if (not isinstance(old, RemapTopology) or not isinstance(new, RemapTopology)
|
if (not isinstance(old, RemapTopology) or not isinstance(new, RemapTopology)
|
||||||
or not _same_parent(old.parent, new.parent)
|
or not _same_parent(old.parent, new.parent)
|
||||||
or old.child.tau_fine != new.child.tau_fine):
|
or old.child.tau_fine != new.child.tau_fine):
|
||||||
@@ -189,7 +207,7 @@ def remap_synchronized(old, new, old_g, *, base_count, level_steps, phase,
|
|||||||
if (frame.shape != (2,) or not np.isfinite(frame).all()
|
if (frame.shape != (2,) or not np.isfinite(frame).all()
|
||||||
or not np.isscalar(spacing) or not np.isfinite(spacing) or spacing <= 0):
|
or not np.isscalar(spacing) or not np.isfinite(spacing) or spacing <= 0):
|
||||||
raise ValueError("frame needs a finite (x,y) origin and positive spacing")
|
raise ValueError("frame needs a finite (x,y) origin and positive spacing")
|
||||||
source = _read_leaves(old, old_g, (None,)*3 if layouts is None else layouts)
|
source = _read_leaves(old, old_g, (None,)*3 if layouts is None else layouts, shifted=shifted)
|
||||||
candidate = [np.full((9, *mask.shape), np.nan, dtype=source[0].dtype)
|
candidate = [np.full((9, *mask.shape), np.nan, dtype=source[0].dtype)
|
||||||
for mask in new.leaf_masks]
|
for mask in new.leaf_masks]
|
||||||
for level, (before, after) in enumerate(zip(old.leaf_masks, new.leaf_masks)):
|
for level, (before, after) in enumerate(zip(old.leaf_masks, new.leaf_masks)):
|
||||||
@@ -210,6 +228,6 @@ def remap_synchronized(old, new, old_g, *, base_count, level_steps, phase,
|
|||||||
# Scale first to avoid an overflowing intermediate sum for finite FP64 G.
|
# Scale first to avoid an overflowing intermediate sum for finite FP64 G.
|
||||||
mean = sum(source[2][:, cy, cx].astype(np.float64) * .25 for cy, cx in children)
|
mean = sum(source[2][:, cy, cx].astype(np.float64) * .25 for cy, cx in children)
|
||||||
candidate[1][:, y, x] = mean
|
candidate[1][:, y, x] = mean
|
||||||
_read_leaves(new, candidate, (None,)*3)
|
_read_leaves(new, candidate, (None,)*3, shifted=shifted)
|
||||||
return RemapCandidate(new, tuple(candidate), int(base_count), tuple(map(int, level_steps)),
|
return RemapCandidate(new, tuple(candidate), int(base_count), tuple(map(int, level_steps)),
|
||||||
phase, tuple(map(float, frame)), float(spacing))
|
phase, tuple(map(float, frame)), float(spacing), shifted)
|
||||||
@@ -143,7 +143,7 @@ class FixedPatchTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_unsupported_backend_and_fine_relaxation_rejected(self):
|
def test_unsupported_backend_and_fine_relaxation_rejected(self):
|
||||||
for kwargs in ({"collision": "TRT"}, {"collision": "MRT"}, {"streaming": "esopull"},
|
for kwargs in ({"collision": "TRT"}, {"collision": "MRT"}, {"streaming": "esopull"},
|
||||||
{"ddf_shifting": True}, {"les_enabled": True},
|
{"les_enabled": True},
|
||||||
{"store_precision": "FP16S"}, {"lattice_model": "D3Q19"},
|
{"store_precision": "FP16S"}, {"lattice_model": "D3Q19"},
|
||||||
{"data_type": "FP64"}, {"omega_min": 1.0}):
|
{"data_type": "FP64"}, {"omega_min": 1.0}):
|
||||||
cfg = config(**kwargs)
|
cfg = config(**kwargs)
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
"""CPU host contracts; mocked CUDA executes real lifecycle, not numerical kernels."""
|
||||||
|
import ast
|
||||||
|
import types
|
||||||
|
from pathlib import Path
|
||||||
|
from contextlib import nullcontext
|
||||||
|
from dataclasses import replace
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
import numpy as np
|
||||||
|
from CelerisLab.lbm.population_storage import (_StoredPopulations, _WEIGHTS,
|
||||||
|
_encode_physical, _decode_stored)
|
||||||
|
from CelerisLab.lbm.synchronized_remap import _remap_stored, remap_synchronized
|
||||||
|
from tests.unit.test_synchronized_remap import topology, A, B
|
||||||
|
from tests.unit import test_refinement_three_level_multipatch as backend
|
||||||
|
|
||||||
|
|
||||||
|
def shifted_fields(mesh):
|
||||||
|
rng = np.random.default_rng(129)
|
||||||
|
arrays = []
|
||||||
|
for leaf in mesh.leaf_masks:
|
||||||
|
a = rng.normal(0, 1e-7, (9, *leaf.shape)).astype(np.float32)
|
||||||
|
a[:, ~leaf] = np.nan
|
||||||
|
y, x = np.nonzero(leaf)
|
||||||
|
a[0, y[:6], x[:6]] = np.array([0., -0., 2**-80, -2**-80, 2**-140, -2**-140], np.float32)
|
||||||
|
arrays.append(a)
|
||||||
|
return tuple(arrays)
|
||||||
|
|
||||||
|
|
||||||
|
def remap(old, new, arrays, shifted=True):
|
||||||
|
return _remap_stored(old, new, _StoredPopulations(arrays, shifted), shifted=True,
|
||||||
|
base_count=7, level_steps=(7, 14, 28), phase='postcollision_g')
|
||||||
|
|
||||||
|
|
||||||
|
def ready():
|
||||||
|
with patch.object(backend, 'CFG', replace(backend.CFG, ddf_shifting=True)):
|
||||||
|
run, module, allocations = backend.mock_backend(children=A)
|
||||||
|
storage = {}
|
||||||
|
module.cuda.memcpy_htod.side_effect = lambda p, a: storage.__setitem__(id(p), np.array(a, copy=True))
|
||||||
|
module.cuda.memcpy_dtoh.side_effect = lambda a, p: np.copyto(a, storage[id(p)])
|
||||||
|
run._initialize_raw(_StoredPopulations(shifted_fields(topology(A)), True))
|
||||||
|
run._step_count = 7
|
||||||
|
return run, module, allocations, storage
|
||||||
|
|
||||||
|
|
||||||
|
class ShiftedStorage(unittest.TestCase):
|
||||||
|
def test_codec_preserves_sub_fp32_physical_perturbation(self):
|
||||||
|
physical = np.broadcast_to(_WEIGHTS, (9, 2, 2)).copy()
|
||||||
|
physical[1] += 2**-35
|
||||||
|
encoded = _encode_physical(physical, True)
|
||||||
|
self.assertTrue((encoded[1] == np.float32(2**-35)).all())
|
||||||
|
np.testing.assert_array_equal(_decode_stored(encoded, True), physical)
|
||||||
|
self.assertEqual(_decode_stored(encoded, True).dtype, np.float64)
|
||||||
|
np.testing.assert_array_equal(_encode_physical(_WEIGHTS, True), np.zeros((9,1,1), np.float32))
|
||||||
|
self.assertEqual(_encode_physical(physical, False).dtype, np.float32)
|
||||||
|
|
||||||
|
def test_actual_fill_loop_bitwise_copy_and_single_rounding_restriction(self):
|
||||||
|
old, new = topology(A), topology(B)
|
||||||
|
arrays = shifted_fields(old)
|
||||||
|
saved = [a.tobytes() for a in arrays]
|
||||||
|
out = remap(old, new, arrays)
|
||||||
|
self.assertTrue(out.shifted)
|
||||||
|
for a, b, om, nm, before in zip(arrays, out.g, old.leaf_masks, new.leaf_masks, saved):
|
||||||
|
self.assertEqual(a[:, om & nm].tobytes(), b[:, om & nm].tobytes())
|
||||||
|
self.assertTrue(np.isnan(b[:, ~nm]).all())
|
||||||
|
self.assertEqual(a.tobytes(), before)
|
||||||
|
self.assertFalse(np.shares_memory(a,b))
|
||||||
|
y,x = np.nonzero(new.leaf_masks[2] & ~old.leaf_masks[2])
|
||||||
|
self.assertEqual(out.g[2][:,y,x].tobytes(), arrays[1][:,y//2,x//2].tobytes())
|
||||||
|
y,x = np.nonzero(new.leaf_masks[1] & ~old.leaf_masks[1])
|
||||||
|
mean = sum(arrays[2][:,2*y+dy,2*x+dx].astype(np.float64)*.25
|
||||||
|
for dy in (0,1) for dx in (0,1))
|
||||||
|
self.assertEqual(out.g[1][:,y,x].tobytes(), mean.astype(np.float32).tobytes())
|
||||||
|
def integral(mesh, values):
|
||||||
|
return sum(a[:,m].sum(axis=1,dtype=np.float64)/4**l
|
||||||
|
for l,(a,m) in enumerate(zip(values,mesh.leaf_masks)))
|
||||||
|
cast_error = abs(mean.astype(np.float32).astype(np.float64)-mean).sum(axis=1)/4
|
||||||
|
self.assertTrue(np.all(abs(integral(old, arrays)-integral(new,out.g)) <= cast_error+2e-18))
|
||||||
|
|
||||||
|
def test_shifted_density_and_tag_validation(self):
|
||||||
|
old,new = topology(A),topology(B)
|
||||||
|
a = tuple(np.where(m[None], np.float32(-.001), np.nan).repeat(9,axis=0) for m in old.leaf_masks)
|
||||||
|
remap(old,new,a)
|
||||||
|
with self.assertRaisesRegex(ValueError,'representation'): remap(old,new,a,False)
|
||||||
|
with self.assertRaisesRegex(ValueError,'FP32'): remap(old,new,tuple(x.astype(np.float64) for x in a))
|
||||||
|
a[0][:,old.leaf_masks[0]] = -1
|
||||||
|
with self.assertRaisesRegex(ValueError,'positive density'): remap(old,new,a)
|
||||||
|
rest = tuple(np.where(m[None],np.float32(0),np.nan).repeat(9,axis=0) for m in old.leaf_masks)
|
||||||
|
remap(old,new,rest)
|
||||||
|
with self.assertRaisesRegex(ValueError,'positive density'):
|
||||||
|
remap_synchronized(old,new,rest,base_count=7,level_steps=(7,14,28),phase='postcollision_g')
|
||||||
|
|
||||||
|
def test_runtime_remap_bypasses_public_codec_and_preserves_both_buffers(self):
|
||||||
|
run,module,allocations,storage = ready()
|
||||||
|
clone = None
|
||||||
|
try:
|
||||||
|
before = run._download_raw()
|
||||||
|
expected = remap(topology(A),topology(B),before.values)
|
||||||
|
with patch.object(run,'download',side_effect=AssertionError('public decode used')):
|
||||||
|
clone = run.remap_child(B)
|
||||||
|
self.assertEqual((run.step_count,clone.step_count),(7,7))
|
||||||
|
self.assertFalse(clone._exchange_valid)
|
||||||
|
for level, want, got in zip(clone._levels,expected.g,clone._download_raw().values):
|
||||||
|
self.assertEqual(want.tobytes(),got.tobytes())
|
||||||
|
for pointer in (level.current,level.spare):
|
||||||
|
np.testing.assert_array_equal(storage[id(pointer)],level.layout.to_blocked(want))
|
||||||
|
for a,b in zip(before.values,run._download_raw().values): self.assertEqual(a.tobytes(),b.tobytes())
|
||||||
|
for a in clone.download(): self.assertEqual(a.dtype,np.float64)
|
||||||
|
finally:
|
||||||
|
if clone is not None: clone.close()
|
||||||
|
run.close()
|
||||||
|
|
||||||
|
def test_runtime_public_encode_and_raw_mismatch_fail_before_upload(self):
|
||||||
|
run,module,_,_ = ready()
|
||||||
|
try:
|
||||||
|
physical = tuple(np.broadcast_to(_WEIGHTS, level.shape).copy() for level in run._levels)
|
||||||
|
for a in physical: a[1] += 2**-35
|
||||||
|
run.initialize(*physical)
|
||||||
|
for a,mask in zip(run._download_raw().values,run._leaf):
|
||||||
|
self.assertTrue((a[1,mask] == np.float32(2**-35)).all())
|
||||||
|
self.assertEqual(run.step_count,0)
|
||||||
|
module.cuda.memcpy_htod.reset_mock()
|
||||||
|
with self.assertRaisesRegex(ValueError,'representation'):
|
||||||
|
run._initialize_raw(_StoredPopulations(run._download_raw().values,False))
|
||||||
|
module.cuda.memcpy_htod.assert_not_called()
|
||||||
|
run._level_cfgs[0] = replace(run._level_cfgs[0],ddf_shifting=False)
|
||||||
|
with self.assertRaisesRegex(ValueError,'mixed'): run._download_raw()
|
||||||
|
finally: run.close()
|
||||||
|
|
||||||
|
def test_dense_or_mixed_shifted_rejected_before_compilation(self):
|
||||||
|
module=backend.host_module()
|
||||||
|
cfg=replace(backend.CFG,ddf_shifting=True)
|
||||||
|
for kwargs in ({},dict(base_block=(4,4)),dict(fine_block=(4,4))):
|
||||||
|
with self.assertRaisesRegex(ValueError,'all-blocked|base_block requires fine_block'):
|
||||||
|
module.FixedPatchStepper(cfg,(4,4,20,28),context=None,**kwargs)
|
||||||
|
module.compiler.compile_kernel.assert_not_called()
|
||||||
|
|
||||||
|
def test_uniform_block_codec_and_raw_snapshot(self):
|
||||||
|
path = Path(backend.r.__file__).with_name('block_stepper.py')
|
||||||
|
tree = ast.parse(path.read_text())
|
||||||
|
tree.body = [n for n in tree.body if not (
|
||||||
|
isinstance(n, ast.Import) and any(a.name == 'pycuda.driver' for a in n.names)
|
||||||
|
or isinstance(n, ast.ImportFrom) and n.module == 'cuda')]
|
||||||
|
module = types.ModuleType('CelerisLab.lbm._test_block_host')
|
||||||
|
module.__package__ = 'CelerisLab.lbm'
|
||||||
|
cuda, compiler = Mock(), Mock()
|
||||||
|
module.__dict__.update(cuda=cuda,compiler=compiler)
|
||||||
|
exec(compile(tree,str(path),'exec'),module.__dict__)
|
||||||
|
cuda.mem_get_info.return_value=(10**9,10**9)
|
||||||
|
cuda.mem_alloc.side_effect=lambda size: Mock()
|
||||||
|
gpu=Mock();gpu.get_global.return_value=(Mock(),20)
|
||||||
|
compiler.load_module.return_value=gpu
|
||||||
|
context=Mock(activate=nullcontext,sm_arch='sm_70')
|
||||||
|
layout=module.BlockLayout2D.build(16,16,4,4)
|
||||||
|
cfg=replace(backend.CFG,nx=16,ny=16,ddf_shifting=True)
|
||||||
|
run=module.PeriodicBlockStepper(cfg,layout,context=context)
|
||||||
|
storage={}
|
||||||
|
cuda.memcpy_htod.side_effect=lambda p,a: storage.__setitem__(id(p),np.array(a,copy=True))
|
||||||
|
cuda.memcpy_dtoh.side_effect=lambda a,p: np.copyto(a,storage[id(p)])
|
||||||
|
try:
|
||||||
|
physical=np.broadcast_to(_WEIGHTS,(9,16,16)).copy();physical[1]+=2**-35
|
||||||
|
run.initialize(physical)
|
||||||
|
raw=run._download_raw()
|
||||||
|
self.assertTrue(raw.shifted)
|
||||||
|
self.assertEqual(len(raw.values),1)
|
||||||
|
self.assertTrue((raw.values[0][1]==np.float32(2**-35)).all())
|
||||||
|
np.testing.assert_array_equal(run.download(),physical)
|
||||||
|
finally: run.close()
|
||||||
|
|
||||||
|
def test_passive_shifted_body_raw_remap_preserves_support(self):
|
||||||
|
from tests.unit.test_pinned_body_remap import mesh, body, A as rects, B as moved
|
||||||
|
old,new=mesh(rects,(body(),)),mesh(moved,(body(),))
|
||||||
|
result=remap(old,new,shifted_fields(old))
|
||||||
|
self.assertTrue(result.shifted)
|
||||||
|
self.assertTrue(np.isnan(result.g[2][:,~new.leaf_masks[2]]).all())
|
||||||
|
changed=mesh(moved,(body(center=(12.1,14.)),))
|
||||||
|
with self.assertRaisesRegex(ValueError,'boundary'):
|
||||||
|
remap(old,changed,shifted_fields(old))
|
||||||
|
|
||||||
|
def test_fp64_physical_roundtrip_cannot_replace_raw(self):
|
||||||
|
tiny=np.full((9,1,1),np.float32(2**-80))
|
||||||
|
self.assertFalse(np.array_equal(tiny,_encode_physical(_decode_stored(tiny,True),True)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=='__main__': unittest.main()
|
||||||
@@ -179,7 +179,7 @@ class SynchronizedRemapTests(unittest.TestCase):
|
|||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
topology(rectangles)
|
topology(rectangles)
|
||||||
parent = self.old.parent
|
parent = self.old.parent
|
||||||
for changes in (dict(lattice_model='D3Q19'), dict(ddf_shifting=True),
|
for changes in (dict(lattice_model='D3Q19'),
|
||||||
dict(collision='TRT'), dict(les_enabled=True), dict(nz=2)):
|
dict(collision='TRT'), dict(les_enabled=True), dict(nz=2)):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
RemapTopology.from_config(replace(CFG, **changes), parent, A)
|
RemapTopology.from_config(replace(CFG, **changes), parent, A)
|
||||||
|
|||||||
Reference in new issue
Block a user