cuwave 0.7.0__tar.gz → 0.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {cuwave-0.7.0 → cuwave-0.7.2}/PKG-INFO +1 -1
  2. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/anisotropic.py +110 -2
  3. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/boundary.py +13 -1
  4. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/decomposition.py +3 -1
  5. cuwave-0.7.2/cuwave/elastic.py +792 -0
  6. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/kernels/anisotropic.cu +192 -21
  7. cuwave-0.7.2/cuwave/kernels/common.cuh +520 -0
  8. cuwave-0.7.2/cuwave/kernels/elastic.cu +1151 -0
  9. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/kernels/elastic_sensitivity.cu +30 -37
  10. cuwave-0.7.2/cuwave/kernels/maxwell.cu +712 -0
  11. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/kernels/maxwell_sensitivity.cu +20 -8
  12. cuwave-0.7.2/cuwave/kernels/scalar.cu +1374 -0
  13. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/kernels/scalar_sensitivity.cu +190 -152
  14. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/maxwell.py +278 -18
  15. cuwave-0.7.2/cuwave/scalar.py +946 -0
  16. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/sensitivity.py +25 -4
  17. cuwave-0.7.2/cuwave/stencils.py +453 -0
  18. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/wave.py +246 -8
  19. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave.egg-info/PKG-INFO +1 -1
  20. {cuwave-0.7.0 → cuwave-0.7.2}/pyproject.toml +1 -1
  21. cuwave-0.7.0/cuwave/elastic.py +0 -387
  22. cuwave-0.7.0/cuwave/kernels/common.cuh +0 -264
  23. cuwave-0.7.0/cuwave/kernels/elastic.cu +0 -667
  24. cuwave-0.7.0/cuwave/kernels/maxwell.cu +0 -367
  25. cuwave-0.7.0/cuwave/kernels/scalar.cu +0 -402
  26. cuwave-0.7.0/cuwave/scalar.py +0 -428
  27. cuwave-0.7.0/cuwave/stencils.py +0 -80
  28. {cuwave-0.7.0 → cuwave-0.7.2}/LICENSE +0 -0
  29. {cuwave-0.7.0 → cuwave-0.7.2}/README.md +0 -0
  30. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/__init__.py +0 -0
  31. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/evals.py +0 -0
  32. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/geometry.py +0 -0
  33. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/kernels/__init__.py +0 -0
  34. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/kernels/anisotropic_sensitivity.cu +0 -0
  35. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/nn.py +0 -0
  36. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/optimization.py +0 -0
  37. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/postprocessing.py +0 -0
  38. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/regularization.py +0 -0
  39. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/signals.py +0 -0
  40. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave/utils.py +0 -0
  41. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave.egg-info/SOURCES.txt +0 -0
  42. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave.egg-info/dependency_links.txt +0 -0
  43. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave.egg-info/requires.txt +0 -0
  44. {cuwave-0.7.0 → cuwave-0.7.2}/cuwave.egg-info/top_level.txt +0 -0
  45. {cuwave-0.7.0 → cuwave-0.7.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuwave
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: GPU finite-difference wave solver with differentiable adjoints
5
5
  Author-email: Leon Herrmann <herrmann.leon@pm.me>
6
6
  License-Expression: MIT
@@ -13,7 +13,9 @@ general anisotropy.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import hashlib
16
17
  import itertools
18
+ import math
17
19
  from collections.abc import Callable
18
20
  from dataclasses import dataclass
19
21
  from pathlib import Path
@@ -28,15 +30,29 @@ from .elastic import voigt
28
30
  from .wave import (
29
31
  PAIRS,
30
32
  PML_THREADS,
33
+ Blocked,
34
+ LaunchSlots,
31
35
  Simulation,
36
+ Source,
32
37
  apply_cell_weights,
33
38
  axis_geometry,
39
+ blocked_launches,
40
+ compile_kernels,
41
+ covering_hits,
42
+ flatten_indices,
34
43
  grid_block,
44
+ owner_hits,
35
45
  slab_grid,
46
+ tile_counts,
47
+ tile_lists,
36
48
  )
37
49
 
38
50
  KERNEL_PATH = Path(__file__).parent / "kernels" / "anisotropic.cu"
39
51
  SENSITIVITY_PATH = Path(__file__).parent / "kernels" / "anisotropic_sensitivity.cu"
52
+ # candidates (tile, thread rows, steps) of the 2D blocked step: the first with the fewest waves
53
+ BLOCKED = (((32, 32), 32, 6), ((32, 32), 32, 4), ((32, 64), 16, 6))
54
+ BLOCKED_WAVES = 1 # the waves of tiles the blocked step pays to
55
+ LARGE_BLOCKED = ((32, 128), 8, 4) # for fields past twice L2
40
56
 
41
57
 
42
58
  # -------------------------------------- helpers --------------------------------------
@@ -169,6 +185,13 @@ class AnisotropicElasticWave(Simulation):
169
185
  """One displacement component per axis."""
170
186
  return self.ndim
171
187
 
188
+ @property
189
+ def compile_flags(self) -> tuple[str, ...]:
190
+ """The shared flags and `-DSTENCIL_KEY`, so modules alike but for the table never share `STENCIL`."""
191
+ table = np.append(np.asarray(self.dx, dtype=float), self.stiffness_matrix())
192
+ key = hashlib.sha256(table.tobytes()).hexdigest()[:16]
193
+ return super().compile_flags + (f"-DSTENCIL_KEY={key}",)
194
+
172
195
  def __post_init__(self) -> None:
173
196
  """Validate the material, the 2D plane assumption and the order, then derive the grid."""
174
197
  super().__post_init__()
@@ -259,8 +282,8 @@ class AnisotropicElasticWave(Simulation):
259
282
  return mat
260
283
 
261
284
  def step_kernel_args(self, mat: dict) -> tuple:
262
- """Material, stencil table and component stride for the step kernel."""
263
- args = (mat["minv"], mat["cell"], mat["stencil"])
285
+ """Material and component stride for the step kernel, whose table sits in its constant memory."""
286
+ args = (mat["minv"], mat["cell"])
264
287
  if self.damping is not None:
265
288
  args += (mat["damping"], self.dtype(self.dt))
266
289
  return args + (np.int32(self.comp_stride),)
@@ -287,6 +310,89 @@ class AnisotropicElasticWave(Simulation):
287
310
  passes = ("pml_cell", "pml_force")
288
311
  return self._define_pml(kernels, mat, passes)
289
312
 
313
+ def define_blocked_step(
314
+ self, mat: dict, source: Source, sensors: cpt.NDArray[cp.int32] | None
315
+ ) -> Blocked | None:
316
+ """The 2D step advancing several steps a launch, excitation and record included, else None.
317
+
318
+ Each thread block loads a tile of both components with a halo of a node a step,
319
+ gathers the cells and steps the tile in shared memory and writes back its core:
320
+ bitwise the step, the excitation and the record launched one by one. Taken where
321
+ launches bound the step (the candidate of `BLOCKED` with the fewest waves of
322
+ tiles, while those stay within `BLOCKED_WAVES`) or DRAM does (`LARGE_BLOCKED`,
323
+ fields past twice L2); else None, as in 1D and 3D, in float64, with a pml, a
324
+ source or a sensor off the interior, or two sources or sensors on one point.
325
+ """
326
+ if self.ndim != 2 or self.precision != "float32" or self.pml is not None:
327
+ return None
328
+ if min(self.Nx) < 4:
329
+ return None
330
+
331
+ # launches bound a grid of a wave of tiles, DRAM one whose fields pass L2
332
+ device = cp.cuda.Device().attributes
333
+ sms = device["MultiProcessorCount"]
334
+ waves = [
335
+ -(-math.prod(tile_counts(self.Nx, b[0], b[2])) // sms) for b in BLOCKED
336
+ ]
337
+ fields = (3 * self.ndim + 2) * math.prod(map(int, self.Nx)) * 4
338
+ if min(waves) <= BLOCKED_WAVES:
339
+ tile, rows, steps = BLOCKED[waves.index(min(waves))]
340
+ elif fields > 2 * device["L2CacheSize"]:
341
+ tile, rows, steps = LARGE_BLOCKED
342
+ else:
343
+ return None
344
+ core = [t - 2 * steps for t in tile]
345
+ cores = tile_counts(self.Nx, tile, steps)
346
+
347
+ # sources and sensors in the interior, none twice
348
+ sources = flatten_indices(self, source.position)
349
+ targets = flatten_indices(self, sensors) if sensors is not None else sources[:0]
350
+ upper = np.asarray(self.Nx)[:, None] - 2
351
+ points, layers = [], []
352
+ for index in (sources, targets):
353
+ flat = index.get()
354
+ layer, node = np.divmod(flat, self.comp_stride)
355
+ point = np.stack(np.divmod(node, self.Nx_padded[1]))
356
+ # in the interior, each point once, the index fitting an entry's half
357
+ if not ((point >= 1) & (point <= upper)).all():
358
+ return None
359
+ if len(flat) >= 2**16 or len(np.unique(flat)) != len(flat):
360
+ return None
361
+ points.append(point)
362
+ layers.append(layer)
363
+
364
+ # a tile's sources wherever its nodes reach, its sensors where its core does
365
+ hits = [
366
+ covering_hits(points[0], tile, core, steps, cores),
367
+ owner_hits(points[1], points[1], core),
368
+ ]
369
+ lists = []
370
+ for hit, layer in zip(hits, layers):
371
+ lists += tile_lists(hit, tile, core, steps, cores, layer)
372
+
373
+ # the kernel, its stencil table copied in, u twice and the cell field
374
+ flags = (f"-DSTEPS={steps}", f"-DTILE0={tile[0]}", f"-DTILE1={tile[1]}")
375
+ kernels = compile_kernels(self, flags=(*flags, f"-DROWS={rows}"))
376
+ table = kernels.get_global("STENCIL")
377
+ cp.ndarray(mat["stencil"].shape, self.dtype, table)[...] = mat["stencil"]
378
+ kernel = kernels.get_function("blocked_kernel")
379
+ shared = 5 * (tile[0] + 2) * (tile[1] + 2) * 4
380
+ if shared > device["MaxSharedMemoryPerBlockOptin"]:
381
+ return None
382
+ kernel.max_dynamic_shared_size_bytes = shared
383
+
384
+ # the arguments, the pointers and the per-call values filled in per launch
385
+ weights = self.excitation_weights(mat, sources)
386
+ args = [None, None, None, None, mat["minv"], mat["cell"]]
387
+ if self.damping is not None:
388
+ args += [mat["damping"], self.dtype(self.dt)]
389
+ args += [None, weights, np.int32(len(sources)), None, np.int32(len(targets))]
390
+ args += [*lists, None]
391
+ slots = LaunchSlots(len(args) - 10, len(args) - 7, len(args) - 1, None)
392
+ args += [np.int32(self.comp_stride), *axis_geometry(self, self.step_factors())]
393
+ launches = [(kernel, tuple(cores[::-1]), None)]
394
+ return blocked_launches(steps, launches, (tile[1], rows), shared, args, slots)
395
+
290
396
  def define_adjoint_step(
291
397
  self, kernels: cp.RawModule, sens_kernels: cp.RawModule, mat: dict
292
398
  ) -> Callable:
@@ -296,6 +402,8 @@ class AnisotropicElasticWave(Simulation):
296
402
 
297
403
  def _define_pml(self, kernels: cp.RawModule, mat: dict, passes: tuple) -> Callable:
298
404
  """The cell step, then each pass over every lined axis, all axes in one memory."""
405
+ table = kernels.get_global("STENCIL")
406
+ cp.ndarray(mat["stencil"].shape, self.dtype, table)[...] = mat["stencil"]
299
407
  fd_step = super().define_step(kernels, mat)
300
408
  if self.pml is None:
301
409
  return fd_step
@@ -83,7 +83,7 @@ def faces_with(sim: Simulation, condition: BoundaryCondition) -> list[int]:
83
83
  2 * d + side
84
84
  for d, pair in enumerate(sim.boundary)
85
85
  for side, marker in enumerate(pair)
86
- if marker is condition
86
+ if marker == condition # a copied or unpickled condition is still the same
87
87
  ]
88
88
 
89
89
 
@@ -92,6 +92,18 @@ def face_mask(sim: Simulation, condition: BoundaryCondition) -> np.int32:
92
92
  return np.int32(sum(1 << f for f in faces_with(sim, condition)))
93
93
 
94
94
 
95
+ def require_off_held_walls(sim: Simulation, position: cpt.NDArray[cp.int32]) -> None:
96
+ """Raise if a source at `position` sits on the wall node of a `Dirichlet` face, which the step holds at zero."""
97
+ rows = position[-sim.ndim :]
98
+ for face in faces_with(sim, Dirichlet):
99
+ d, side = divmod(face, 2)
100
+ wall = sim.Nx[d] - 2 if side else 1
101
+ if bool(cp.any(rows[d] == wall)):
102
+ raise ValueError(
103
+ f"a source on the wall node {wall} of Dirichlet face {face}, held at zero"
104
+ )
105
+
106
+
95
107
  def canonical_boundary(
96
108
  boundary: BoundaryCondition | Sequence | None,
97
109
  ndim: int,
@@ -20,7 +20,7 @@ import cupy as cp
20
20
  import cupy.typing as cpt
21
21
  import numpy as np
22
22
 
23
- from .boundary import _canonical_faces, pml_widths
23
+ from .boundary import _canonical_faces, pml_widths, require_off_held_walls
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from .wave import Simulation
@@ -295,6 +295,8 @@ def decomposable(function: Callable) -> Callable:
295
295
  def run(*args, **kwargs):
296
296
  bound = signature.bind(*args, **kwargs)
297
297
  sim = bound.arguments["sim"]
298
+ # checked whole, since a subdomain's walls at the cuts are halo rows
299
+ require_off_held_walls(sim, bound.arguments["source"].position)
298
300
  if sim.devices is None:
299
301
  return function(*args, **kwargs)
300
302
  if bound.arguments.get("record_every") is not None: