Skip to content

SEM domain decomposition

sweep.sem.dd splits one model into x strips across GPUs on the impl='c' backend, the SEM mirror of the FD ModelParallel: a built PropSEM carries the configuration, global coordinates go in and gather_record assembles the record. Usage and scope are in Domain decomposition.

from sweep.sem.dd import SEMModelParallel, StripTopology

ddp = SEMModelParallel(prop, StripTopology(px=world, rank=rank))
rec = ddp(wavelet, src_node, rec_nodes, [vp])
full = ddp.gather_record(rec)

sweep.sem.dd.SEMModelParallel

SEMModelParallel(prop, topo: StripTopology)

DD wrapper over the impl='c' SEM acoustic C-PML propagator.

prop is a built :class:~sweep.sem.prop_base.PropSEM used as the CONFIG CARRIER (mirroring FD ModelParallel(prop, mesh)); the compute runs on the stepped CUDA entry points regardless of prop's impl. Scope: AcousticPotentialSEM2D, ElasticSEM2DDeformed, AcousticElasticSEM2D, AcousticSEM3D and AcousticSEM3DDeformed -- all C-PML, one shot per call, records at global-node receivers. The equation whitelist is exact (is, not issubclass) on purpose: each family needs its own stepped entry points and consts builder, so a near-miss must fail loudly rather than fall through to a sibling's path.

The two 3-D acoustic families share _parts3d and the same stepped kernels -- the deformed one only swaps which geometry the strip consts carry (per element-node w_J plus the nine inverse-Jacobian metrics, instead of the flat mesh's per-thread w_jac and 1/2h scalars). That works because the strip mesh is rebuilt for its ibool TOPOLOGY only and every geometric quantity is sliced from the global build -- the same rule that keeps the C-PML profile intact across a cut.

for_equation classmethod

for_equation(eq, dt, nt, topo=None)

A wrapper over a BUILT 3-D acoustic equation, for callers that hold one and no PropSEM (the low-level propagator): the config the strips need is read off the equation, and there is no model to resolve, so drive it with :meth:run. One strip unless topo says otherwise.

forward

forward(wavelet, source, receivers, models)

One shot; source/receivers are GLOBAL node indices of the row-major global grid. Returns (nt, nrec) -- fully assembled in single-process mode, own-columns-only under dist.

The gradient memory is the propagator's own. memory= BoundarySaving() stores each strip's C-PML band ring instead of its save-all tape; the two memory levers compose because both strips give a shared cut the same band membership (see :func:_band_nodes). Its storage_dtype picks the ring's storage precision -- 'fp32', 'bf16' (2x) or 'int8' (4x, one amax scale per field and step). The propagator's bs_store_dtype_elem overrides it for the ELEMENT rings, which the elastic and fluid-solid paths carry; on the elastic path they are 82% of the ring memory and quantise ~70x better than the node rings, so storage_dtype='fp32', bs_store_dtype_elem='int8' is the setting that actually pays there. Refused on paths that ring nodes only.

run

run(
    eq_g,
    wavelet,
    source,
    receivers,
    memory=None,
    bs_store_dtype=None,
    bs_store_dtype_elem=None,
)

One shot on a BUILT equation eq_g -- what :meth:forward does after resolving the model, and how the low-level :class:~sweep.sem.prop_c.SEMCudaPropagator (which holds a built equation and no PropSEM) reaches the strip driver.

source is a global node id, a :class:_Stamps, or a tuple of (n_global,) patterns in source-field order; receivers global node ids or a :class:_Stamps. Returns (nt, nrec) ((nt, nrec, ncomp) on the elastic and fluid-solid paths).

Refuse -> normalise -> build the strips -> check the cuts -> step -> gather, one method each, in that order: the refusals have to precede every allocation, and the cut check has to precede the first step.

gather_record

gather_record(record)

Assemble the global record on rank 0 (dist mode); identity in single-process mode. Mirrors FD ModelParallel.gather_record.

sweep.sem.dd.StripTopology

StripTopology(px: int, rank: int = 0)

1-D x-strip rank grid (the py == 1 row of FD's MeshTopology).

px strips over the element columns; z is never split. Pure arithmetic -- no torch.distributed calls.

rank class-attribute

rank = 0

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.

int('0b100', base=0) 4

local_elems

local_elems(nex: int) -> tuple[int, int]

Element-column range [ex0, ex1) (uniform tiles, the FD v1 rule: pad the domain to a multiple of px yourself).