Batch and multi-GPU execution

Run many FDTD variants as one batch, spread independent simulations over GPUs, and reduce array scene gradients across devices.

This page covers the execution layer above a single Simulation.run(). Every name here is imported from witwin.maxwell (for example mw.BatchExecution, mw.run_many). You need it when you run many related FDTD simulations and want to reuse GPU state between them, or spread independent simulations over several GPUs.

There are three paths:

PathEntry pointUse it for
BatchSimulation.batch(...)Variants of one common FDTD scene, solved together in native CUDA cohorts on one or more GPUs. Experimental.
Ensemblerun_many(...) with MultiGPUExecutionIndependent Simulation objects, one per leased GPU, returned in submission order.
Array gradientensemble_scene_gradient_vjp(...)Per-port array column forwards run over the ensemble pool, then one deterministic gradient reduction.

To split one large simulation across GPUs, use FDTDParallelConfig on Simulation.fdtd(..., parallel=...) instead; see Simulation. A simulation with a parallel config cannot enter a batch or run_many.

Batch is experimental

Simulation.batch and every Batch* type are an experimental execution tier in 0.5.1. Batch supports a subset of ordinary FDTD (listed under Supported batch scenes) and rejects everything else with an explicit error. The API may change.

Batch lifecycle#

A batch keeps the ordinary Scene -> Simulation -> Result model. Each variant still produces its own Result.

  1. Declare

    Call Simulation.batch(...). It returns a BatchSimulation and only validates the declaration: types, names, shapes and the device policy. It does not touch CUDA.

  2. Plan (optional)

    Call batch.plan() for a dry BatchPlanReport. With grouping="strict" it raises ValueError naming the first field where a variant differs from variant 0 (for example frequencies, dt, boundary, grid). The report is provisional: resolved is always False.

  3. Prepare

    Call batch.prepare(). It returns a PreparedBatch: it checks the supported feature subset, creates the CUDA stream and memory budget, compiles the first cohort and fixes the cohort size. Use it as a context manager so it closes.

  4. Run or iterate

    Call prepared.run() for a BatchResult, or prepared.iter_results() to receive one BatchItem per variant as it finishes. A prepared batch can run again, optionally with new BatchParameters of the same schema.

  5. Use results and backward

    Each result owns its tensors and stays valid after the slot is reused or closed. If the batch inputs require gradients and autograd is enabled, the monitor outputs carry a graph: call torch.autograd.grad or .backward() on a loss built from them, even after close().

batch.run() runs prepare, run and close in one call. batch.iter_results() prepares a batch that the iterator owns and closes when the iterator closes.

This example is trimmed from tests/api/public/test_batch_execution.py and tests/fdtd/cuda/test_batch_runtime.py. It solves three source amplitudes of one scene, two variants at a time.

import torch
import witwin.maxwell as mw
 
scene = mw.Scene(domain=mw.Domain(bounds=((-0.2, 0.2),) * 3), grid=mw.GridSpec.uniform(0.025),
                 boundary=mw.BoundarySpec.pml(num_layers=3), device="cuda:0")
scene.add_source(mw.PointDipole(name="drive", position=(-0.05, 0.0, 0.0), polarization="Ez",
                                source_time=mw.GaussianPulse(frequency=1e9, fwidth=1e9)))
scene.add_monitor(mw.PointMonitor(name="probe", position=(0.1, 0.0, 0.0), fields=("Ez",)))
sim = mw.Simulation.fdtd(scene, frequencies=(1e9,), run_time=mw.TimeConfig(time_steps=80, dt=2e-11))
 
scales = mw.BatchParameters(source_scales={"drive": torch.tensor([1.0, 2.0, 3.0], device="cuda:0")})
batch = mw.Simulation.batch(sim, parameters=scales,
                            execution=mw.BatchExecution(devices=("cuda:0",), batch_size=2))
print(batch.plan().to_markdown())
with batch.prepare() as prepared:
    results = prepared.run()
    results.results()  # raises the first DistributedFailure, if any
    ez = results.stack(lambda r: r.raw_monitor("probe")["components"]["Ez"])

For gradients, give the batch a trainable input and build a loss from the results (from tests/gradients/test_batch_adjoint.py):

density = torch.full((3, 2, 2, 2), 0.4, device="cuda:0", requires_grad=True)
batch = mw.Simulation.batch(sim, parameters=mw.BatchParameters(region_densities={"rho": density}),
                            execution=mw.BatchExecution(devices=("cuda:0",), batch_size=2))
results = batch.run()
results.results()
loss = sum(r.raw_monitor("probe")["components"]["Ez"].abs().square().sum() for r in results)
(grad,) = torch.autograd.grad(loss, density)

Here sim.scene must hold a MaterialRegion named "rho" whose density has shape (2, 2, 2).

Supported batch scenes#

prepare() (and therefore run() and iter_results()) checks every simulation and raises for anything outside this subset:

RequirementError if violated
A CUDA device is available.RuntimeError
Method is FDTD and config.parallel is None.ValueError (at declaration)
Sources are all PointDipole.NotImplementedError
Monitors are all PointMonitor, PlaneMonitor or FluxMonitor.NotImplementedError
The scene has no ports, thin_wires, networks, lumped_elements or circuits, and the simulation has no port excitations.NotImplementedError
BatchOutput.materials is False and BatchOutput.fields has no H components.NotImplementedError
enable_plot is off.ValueError
With trainable inputs and autograd enabled, shutoff is 0.ValueError

Reverse execution uses native per-lane checkpoint adjoints. Supported trainable inputs are material densities, dielectric geometry and named source scales.

Simulation.batch#

Simulation.batch(simulations, *, execution, parameters=None, variant_ids=None, variation=None, outputs=None) -> BatchSimulation

Static method that declares a batch. It is the same as calling BatchSimulation(...) with the same arguments; see BatchSimulation for each argument.

Returns BatchSimulation.

BatchSimulation#

class BatchSimulation(simulations, *, execution, parameters=None, variant_ids=None, variation=None, outputs=None)

An inspectable batch submission. Construction validates the declaration and does not initialize CUDA.

There are two input forms:

  • Template: one Simulation plus parameters. The variant count is parameters.variant_count; variant i is the template with slice i of every parameter tensor applied.
  • List: a nonempty sequence of Simulation objects and no parameters. Each simulation is one variant.
simulationsSimulation | Sequence[Simulation]required

A template simulation, or a nonempty sequence of simulations. Every simulation must use FDTD and have no parallel config.

executionBatchExecutionrequired

Device and capacity policy.

parametersBatchParameters | Nonedefault None

Per-variant tensor inputs. Required with a template; not allowed with a list.

variant_idsSequence[str] | Nonedefault None

Unique, nonempty string IDs, one per variant. Default: "0", "1", and so on.

variationBatchVariation | Nonedefault None

Names of the scene objects that change between variants. With a template, its material_regions and sources must match the parameter targets exactly and structures must be empty.

outputsBatchOutput | Nonedefault None

What each result keeps. None means BatchOutput().

Raises TypeError for a wrong argument type. ValueError when a named region, structure, source, monitor or port does not identify exactly one object, when a template parameter's spatial shape differs from the region density, when a simulation sets full_field_dft but outputs.fields is empty, or when outputs.result_device="cpu" is combined with trainable inputs. NotImplementedError when a source_scales target is a port-generated source.

Attributes: execution, outputs, parameters, variation, template (the template simulation or None), simulations (tuple), variant_ids (tuple of str).

batch = mw.Simulation.batch([sim_a, sim_b, sim_c],
                            execution=mw.BatchExecution(devices=("cuda:0",), grouping="compatible"))
report = batch.plan()

BatchSimulation.scene#

BatchSimulation.scene(index) -> Scene

Returns a cloned Scene for variant index. For a template batch the clone carries slice index of each region_densities tensor (gradients still flow back to the batch tensor) and a source amplitude multiplied by source_scales[name][index]. The original scene is not changed.

Raises TypeError if index is not an int; IndexError if it is out of range.

BatchSimulation.plan#

BatchSimulation.plan() -> BatchPlanReport

Builds a dry, provisional plan. It never prepares a scene, allocates solver memory or queries the GPU. Variants are grouped by a digest of their declarations (domain, grid, boundary, symmetry, frequencies, config, sources, monitors, ports, excitations and output plan). Material and structure contents are not compared at this stage. Amplitudes of sources named in parameters.source_scales or variation.sources are ignored when comparing.

Raises ValueError with grouping="strict" when a variant differs from variant 0; the message names the first differing field.

BatchSimulation.prepare#

BatchSimulation.prepare() -> PreparedBatch

Runs plan() and builds a PreparedBatch. With execution.tune=True it first measures candidate policies with complete forward runs and returns a context built from the selected policy.

BatchSimulation.run#

BatchSimulation.run() -> BatchResult

Prepares, runs every variant once and closes the prepared batch.

BatchSimulation.iter_results#

BatchSimulation.iter_results(*, order='completion') -> BatchResultIterator

Prepares a batch and returns an iterator that owns it; closing the iterator also closes the prepared batch.

orderstrdefault 'completion'

'completion' yields variants group by group as cohorts finish. 'input' yields them in input index order.

Raises ValueError for another order value.

BatchSimulation length#

len(batch) -> int

Number of variants.

BatchExecution#

class BatchExecution(*, devices, batch_size='auto', max_batch_size=32, memory_limit_bytes=None, reserve_bytes=536870912, grouping='strict', scheduling='throughput', slots_per_device=1, graph='auto', graph_steps='auto', tune=False, fail_fast=True)

Frozen, keyword-only device and capacity policy. Construction validates values but does not initialize CUDA.

devicestuple[str, ...]required

Ordered, unique CUDA devices with an explicit index, such as ("cuda:0", "cuda:1"). "cpu" and bare "cuda" are rejected.

batch_sizeint | Literal['auto']default 'auto'

Variants solved together in one cohort (resident lanes per slot). An integer must be at least 1 and not exceed max_batch_size. 'auto' measures one prepared lane and picks the largest size that fits the budget, halving the size after a CUDA out-of-memory error.

max_batch_sizeintdefault 32

Upper bound for batch_size, at least 1.

memory_limit_bytesint | Mapping[str, int] | Nonedefault None

Byte cap on the private CUDA allocation budget. An int applies to every device; a mapping must name every execution device exactly once. The effective limit is the smaller of this value and free memory minus reserve_bytes.

reserve_bytesintdefault 536870912

Bytes of free device memory (512 MiB by default) left outside the budget. Must be at least 0.

groupingLiteral['strict', 'compatible']default 'strict'

'strict' requires every variant to match variant 0 at plan time. 'compatible' splits variants into groups of matching declarations.

schedulingLiteral['static', 'throughput']default 'throughput'

How cohorts are assigned to workers when there is more than one slot. 'static' assigns them in round-robin worker order; 'throughput' gives the next cohort to whichever worker asks first.

slots_per_deviceint | Literal['auto']default 1

Persistent field slots per device: 1 or 2. 'auto' requires tune=True.

graphLiteral['auto', 'required', 'off']default 'auto'

CUDA Graph capture of the time-step loop. 'auto' tries to capture and falls back to eager stepping if capture fails. 'required' raises if capture fails. 'off' always steps eagerly. The simulations' own cuda_graph flags are not used.

graph_stepsint | Literal['auto']default 'auto'

Time steps per captured chunk, at least 1. 'auto' uses 32 (tuning tries 16, 32 and 64).

tunebooldefault False

If True, prepare() measures candidate batch sizes, slot counts and graph chunk sizes with complete forward runs and keeps the fastest. Tuning measures forward throughput only, not backward.

fail_fastbooldefault True

If True, the first failed cohort cancels the variants that have not started. Fatal CUDA errors (illegal memory access, device-side assert, unspecified launch failure) always cancel.

Raises TypeError or ValueError for invalid values.

execution = mw.BatchExecution(devices=("cuda:0", "cuda:1"), batch_size=4, slots_per_device=2,
                              memory_limit_bytes=8 * 1024**3)

BatchParameters#

class BatchParameters(*, region_densities=None, source_scales=None)

Frozen, keyword-only per-variant tensor inputs for a template batch. Every tensor has a leading variant dimension, and all tensors must share the same leading size. Tensors are kept as given (no copy), so views and requires_grad survive.

region_densitiesMapping[str, torch.Tensor] | Nonedefault None

Maps a MaterialRegion name to a real floating-point tensor of shape (V, nx, ny, nz), where (nx, ny, nz) is the region's density shape.

source_scalesMapping[str, torch.Tensor] | Nonedefault None

Maps an explicit scene source name to a real floating-point tensor of shape (V,). Variant i multiplies the source's source_time.amplitude by entry i.

Tensors must be strided, real floating point, on CPU or CUDA, and have no empty dimension. At least one tensor is required.

Raises TypeError or ValueError for a wrong dtype, rank, device, empty input or mismatched leading size.

BatchParameters.variant_count#

BatchParameters.variant_count -> int

Shared leading size V. Set at construction.

BatchParameters.requires_grad#

BatchParameters.requires_grad -> bool

True if any tensor requires gradients.

BatchParameters.schema#

BatchParameters.schema -> tuple

Tuple of (kind, name, shape, dtype) entries. PreparedBatch.run(parameters=...) requires new parameters with the same schema.

BatchVariation#

class BatchVariation(*, material_regions=(), structures=None, sources=())

Frozen declaration of which named scene objects change between variants. For a list batch, named material regions and structures let the runtime reuse compiled materials outside the changed region.

material_regionstuple[str, ...]default ()

Unique names of MaterialRegion objects that differ between variants.

structuresMapping[str, GeometryBase] | Nonedefault None

Maps a structure name to a geometry that bounds where that structure may change.

sourcestuple[str, ...]default ()

Unique names of sources whose amplitude differs between variants. Their amplitudes are ignored by plan() comparison.

Raises TypeError if a name list is a string or a structures value is not a geometry; ValueError for empty or duplicate names.

BatchOutput#

class BatchOutput(*, monitors='all', ports='all', fields=(), materials=False, result_device='local', max_pending_results=2)

Frozen selection of what each variant's result keeps and where it lives.

monitorstuple[str, ...] | Literal['all']default 'all'

Monitor names to keep, or 'all'.

portstuple[str, ...] | Literal['all']default 'all'

Port names to keep, or 'all'. Batch execution currently rejects scenes with ports.

fieldstuple[str, ...]default ()

Full-field spectra to keep, from Ex, Ey, Ez, Hx, Hy, Hz. Empty means no full-field DFT. Only electric components run today; H components raise at prepare.

materialsbooldefault False

Export materials. Must stay False; True raises at prepare.

result_devicestrdefault 'local'

'local' keeps each result on the GPU that solved it. 'cpu' copies results to host memory (not allowed with trainable inputs). A CUDA device string must be one of the execution devices when there is more than one worker.

max_pending_resultsintdefault 2

Completed results that may wait in the delivery queue during multi-worker iteration. At least 1.

BatchPlanReport#

class BatchPlanReport(_encoded)

Immutable, JSON-backed planning snapshot. You get it from BatchSimulation.plan() or PreparedBatch.report; you do not build it yourself. Each top-level key is also readable as an attribute (report.groups, report.variant_count), and each read returns an independent copy.

Keys from plan():

KeyMeaning
schema_versionAlways 1.
resolvedAlways False: compiled equality and memory admission are not proven.
variant_count, variant_idsBatch size and IDs.
groupsList of groups with group_id, variant_indices, signature_digest, capability_status, output_plan and unresolved_reasons.
devicesOne entry per group and device with batch size, slots, graph steps and reserved headroom.
warnings, unsupported_featuresText notes and features not implemented yet.
configuration_digestSHA-256 over declarations, execution policy, IDs, variation and parameter schema.

PreparedBatch.report adds execution_tier, the chosen batch_size, graph, slots_per_device, memory (budget statistics) and capacity_retries. A multi-worker report adds total_capacity and per-worker reports under workers.

BatchPlanReport.to_dict#

BatchPlanReport.to_dict() -> dict

Returns a new dict decoded from the stored JSON.

BatchPlanReport.to_markdown#

BatchPlanReport.to_markdown() -> str

Returns a short Markdown summary: variant count, a group table and the warnings.

BatchPlanReport.from_dict#

BatchPlanReport.from_dict(payload) -> BatchPlanReport

Class method that encodes a JSON-serializable dict. NaN values are rejected.

PreparedBatch#

class PreparedBatch(batch)

A reusable CUDA context for one BatchSimulation. Create it with BatchSimulation.prepare(). With more than one device, or slots_per_device=2, you get a subclass that runs one worker per slot on separate threads; the public methods are the same.

Construction compiles the first cohort, measures capacity when batch_size='auto', and builds report. Only one run() or iter_results() may be active at a time. Do not mutate the input Scene objects while the prepared batch is open.

Attributes: batch, report (BatchPlanReport), capacity (resolved cohort size; summed over workers in the multi-worker case), device (first execution device).

Raises RuntimeError without CUDA; NotImplementedError or ValueError for unsupported scenes (see Supported batch scenes).

PreparedBatch.run#

PreparedBatch.run(*, parameters=None) -> BatchResult

Runs every variant and returns the results in input order. Failures are stored as entries, not raised.

parametersBatchParameters | Nonedefault None

New template inputs with the same schema as the original. None reuses batch.parameters (and reads their current values, so in-place updates take effect).

Raises RuntimeError if the batch is closed or another run or iteration is active; ValueError if parameters change the schema.

with batch.prepare() as prepared:
    first = prepared.run()
    second = prepared.run(parameters=mw.BatchParameters(source_scales={"drive": new_scales}))

PreparedBatch.iter_results#

PreparedBatch.iter_results(*, parameters=None, order='completion') -> BatchResultIterator

Starts a demand-driven run. With one slot, cohorts are solved as you pull items; with several workers, at most outputs.max_pending_results finished results wait in the queue.

parametersBatchParameters | Nonedefault None

Same as for run().

orderstrdefault 'completion'

'completion' or 'input'.

Raises as for run(), plus ValueError for another order.

PreparedBatch.request_cancel#

PreparedBatch.request_cancel() -> None

Asks the active run to stop. Variants not yet finished are reported with FailureKind.CANCELLED. Safe to call from another thread.

PreparedBatch.close#

PreparedBatch.close() -> None

Releases slots, caches, graphs and the memory budget. Results already returned stay valid. Calling it twice is a no-op. with batch.prepare() as prepared: calls it on exit.

Raises RuntimeError while an iterator is still open.

PreparedBatch.records#

PreparedBatch.records -> tuple[ExecutionRecord, ...]

Records of the latest run, in input order.

BatchResultIterator#

class BatchResultIterator

Iterator returned by iter_results(). It yields one BatchItem per variant and keeps no unbounded queue of finished results. Use it in a with block or call close() when you stop early; variants not yet delivered are then recorded as cancelled.

with prepared.iter_results(order="input") as items:
    for item in items:
        item.raise_for_error()
        save(item.variant_id, item.result)

BatchResultIterator.close#

BatchResultIterator.close() -> None

Stops the run and releases the prepared batch for the next run. If the iterator came from BatchSimulation.iter_results(), the prepared batch is closed too.

Raises RuntimeError if called while next() is running in another thread; call PreparedBatch.request_cancel() first.

BatchResultIterator.records#

BatchResultIterator.records -> tuple[ExecutionRecord, ...]

Records so far; after close(), one record per variant.

BatchItem#

class BatchItem(index, variant_id, result, failure, record)

Frozen record of one completed, failed or cancelled variant. Exactly one of result and failure is set.

FieldTypeMeaning
indexintInput index.
variant_idstrVariant ID.
resultResult or NoneThe variant's result.
failureDistributedFailure or NoneThe failure, if any.
recordExecutionRecordDevice, status and wall time.

BatchItem.raise_for_error#

BatchItem.raise_for_error() -> None

Raises failure if it is set.

BatchResult#

class BatchResult(entries, records, variant_ids)

A ResultSequence of ordinary Result objects with stable variant IDs. You get it from run(). Each result's metadata holds batch_index and variant_id, and its solver_stats hold batch keys such as batch_execution_tier. Results keep no solver or prepared scene. Indexing or iterating raises a failed variant's DistributedFailure; use entries for the non-raising view.

Attribute: variant_ids (tuple of str).

BatchResult.stack#

BatchResult.stack(selector, *, device=None) -> torch.Tensor

Applies selector to each result and stacks the tensors along a new first axis.

selectorCallable[[Result], torch.Tensor]required

Function that picks one tensor from a result.

devicestr | torch.device | Nonedefault None

Device to move tensors to before stacking. Required when results live on different devices.

Raises the first DistributedFailure; TypeError if the selector returns a non-tensor; ValueError for an empty batch, mismatched shapes or dtypes, or mixed devices without device.

BatchResult.save#

BatchResult.save(directory) -> None

Writes result-<index>.pt for each completed variant and a manifest.json with IDs, devices, statuses, timings and failures.

Raises FileExistsError if the directory is not empty.

BatchResult.load#

BatchResult.load(directory, *, scenes, map_location='cpu') -> BatchResult

Class method that reads a directory written by save().

directorystr | Pathrequired

Directory holding manifest.json.

scenesSequence[Scene]required

One scene per saved variant, in order.

map_locationstrdefault 'cpu'

Device to load tensors onto.

Raises ValueError for an unknown manifest version, non-contiguous indices or a wrong number of scenes.

result.save(tmp_path)
loaded = mw.BatchResult.load(tmp_path, scenes=[s.scene for s in sims])

MultiGPUExecution#

class MultiGPUExecution(devices, max_concurrency, placement='memory_aware', fail_fast=False, per_device_concurrency=1)

Frozen descriptor for running independent simulations over several GPUs with run_many. Construction validates structure only; CUDA availability and memory are checked when the run starts.

devicestuple[str, ...]required

Ordered, unique CUDA devices with an explicit index. One device is allowed.

max_concurrencyintrequired

Maximum tasks in flight, at least 1.

placementstrdefault 'memory_aware'

'memory_aware' estimates each simulation's memory, skips devices whose free-memory snapshot cannot hold it, and fails a task with FailureKind.CAPACITY before it runs if no device can. 'round_robin' makes no estimate and leases the first free device.

fail_fastbooldefault False

If True, the first failure cancels tasks that have not started. Tasks already running finish.

per_device_concurrencyintdefault 1

Maximum tasks on one device at once, at least 1.

Raises TypeError or ValueError for invalid values.

MultiGPUExecution.ensemble#

MultiGPUExecution.ensemble(*, devices, max_concurrency=None, placement='memory_aware', fail_fast=False, per_device_concurrency=1) -> MultiGPUExecution

Class method and the usual constructor. max_concurrency=None becomes len(devices) * per_device_concurrency.

execution = mw.MultiGPUExecution.ensemble(devices=("cuda:0", "cuda:1"))

MultiGPUExecution.build_pool#

MultiGPUExecution.build_pool(*, require_cuda=True) -> DevicePool

Builds the device pool that run_many uses. With require_cuda=True it snapshots each device's free and total memory. You do not need to call it yourself.

run_many#

run_many(simulations, *, execution) -> ResultSequence

Runs independent simulations concurrently over the devices in execution and returns their outcomes in submission order: entry i belongs to simulations[i]. Each task leases one device, runs Simulation.run() with its scene placed on that device, and releases the device. The caller's scene object is not changed. When more than one task can run at a time, CUDA Graph capture is suspended for the whole run.

simulationsIterable[Simulation]required

Simulations to run. Each must use a declarative Scene (not a SceneModule), have no trainable parameters, have no parallel config, and own a Scene object no other task shares.

executionMultiGPUExecutionrequired

Device policy.

Returns ResultSequence of Result or DistributedFailure entries.

Raises TypeError if execution is not a MultiGPUExecution or an item is not a Simulation; ValueError for a SceneModule input, trainable parameters, a parallel config or a shared Scene. Task failures are not raised here; they are stored in the sequence.

execution = mw.MultiGPUExecution.ensemble(devices=("cuda:0", "cuda:1"), max_concurrency=2)
seq = mw.run_many([sim_a, sim_b], execution=execution)
if not seq.failed:
    peaks = [float(r.fields["EZ"].abs().max()) for r in seq]

ResultSequence#

class ResultSequence(entries, records)

Ordered container of task outcomes. Indexing and iteration return the Result and raise the slot's DistributedFailure for a failed task, so a failure is never silently skipped. len(seq) is the number of tasks.

ResultSequence.entries#

ResultSequence.entries -> tuple

Non-raising view: each entry is a Result or a DistributedFailure.

ResultSequence.records#

ResultSequence.records -> tuple[ExecutionRecord, ...]

One record per task, in submission order.

ResultSequence.failures#

ResultSequence.failures -> tuple[DistributedFailure, ...]

All failures in order.

ResultSequence.failed#

ResultSequence.failed -> bool

True if any task failed or was cancelled.

ResultSequence.results#

ResultSequence.results() -> tuple

Returns every result in order, or raises the first failure.

ExecutionRecord#

class ExecutionRecord(index, device, status, estimated_bytes=None, wall_time_s=None, device_time_s=None, failure=None)

Frozen bookkeeping for one task or batch variant.

indexintrequired

Submission index.

devicestr | Nonerequired

Device the task ran on, or None if it never leased one.

statusstrrequired

"completed", "failed" or "cancelled".

estimated_bytesint | Nonedefault None

Memory estimate used for placement, in bytes.

wall_time_sfloat | Nonedefault None

Wall time in seconds.

device_time_sfloat | Nonedefault None

GPU time in seconds, measured with CUDA events by run_many when available.

failureDistributedFailure | Nonedefault None

The failure for a failed or cancelled task.

The timing fields are measurements only. Comparing them as a speedup needs exclusive use of the GPUs.

ExecutionRecord.completed#

ExecutionRecord.completed -> bool

True when status == "completed".

DistributedFailure#

class DistributedFailure(*, index, kind, device=None, exception=None, message=None)

Exception describing one failed task. It keeps the original exception as exception and as __cause__.

indexintrequired

Submission index.

kindFailureKind | strrequired

Failure category.

devicestr | Nonedefault None

Leased device, or None.

exceptionBaseException | Nonedefault None

Original exception.

messagestr | Nonedefault None

Detail text. Default: the exception's type and message.

Attributes: index, kind, device, exception, detail.

try:
    results[0]
except mw.DistributedFailure as failure:
    print(failure.index, failure.kind, failure.device, failure.detail)

FailureKind#

class FailureKind(str, Enum)

Failure category. str(kind) returns the value.

MemberValueMeaning
CAPACITY"capacity"run_many: the memory estimate fits no device, so the task never ran. Batch: a CUDA out-of-memory error.
RUNTIME"runtime"The task raised while preparing or running.
CANCELLED"cancelled"The task did not run because of fail_fast, request_cancel() or an early iterator close.

AggregatedSceneGradient#

class AggregatedSceneGradient(gradient, reduction_order, reduction_device, column_devices, port_names)

Frozen result of an array scene-gradient reduction.

FieldTypeMeaning
gradienttorch.Tensor or a tuple of tensorsSummed gradient on reduction_device. A bare tensor when each column has one parameter, otherwise a tuple.
reduction_ordertuple[int, ...]Order in which column gradients were summed.
reduction_devicestrDevice holding gradient.
column_devicestuple[str, ...]Device each column ran on.
port_namestuple[str, ...]Port order of the columns, from the basis.

aggregate_scene_gradient_vjp#

aggregate_scene_gradient_vjp(basis, *, columns, parameters, weights, objective=None, field_cotangents=None, reduction_order=None, reduction_device=None) -> AggregatedSceneGradient

Reduces per-column vector-Jacobian products (VJPs) for an array objective. It combines the columns into E = sum_n w_n e_n on the reduction device, takes the cotangent of the objective with respect to E, seeds column n with conj(w_n) times that cotangent, back-propagates each column on its own device, and sums the gradients in a fixed order. The result does not depend on which GPU ran which column.

basisArrayBasisDatarequired

Array basis that sets the port count N, frequencies F, angular grid, dtype and default device. See Antennas and arrays.

columnsSequence[tuple[torch.Tensor, torch.Tensor]]required

One (e_theta, e_phi) pair per port. Each tensor is complex, has the basis dtype and shape [F, T, P], carries an autograd graph, and both tensors of a pair share a device.

parametersSequence[torch.Tensor | tuple[torch.Tensor, ...]]required

One entry per port: the trainable leaf or leaves that column's graph differentiates. All entries share the same structure, shapes and dtypes; devices may differ.

weightstorch.Tensor | BeamWeightsrequired

Complex incident power-wave weights of shape [N] or [F, N], with the basis dtype, on the reduction device.

objectiveCallable | Nonedefault None

Function objective(e_theta, e_phi) on the combined field that returns a real scalar tensor.

field_cotangentstuple[torch.Tensor, torch.Tensor] | Nonedefault None

Precomputed cotangents for the combined (e_theta, e_phi), each of shape [F, T, P]. Give exactly one of objective and field_cotangents.

reduction_orderSequence[int] | Nonedefault None

Permutation of range(N) for the summation. Default: port order.

reduction_devicetorch.device | str | Nonedefault None

Device for the combine and the sum. Default: the basis device.

Raises ValueError if both or neither of objective and field_cotangents are given, if counts or shapes do not match, if columns carry no autograd graph, or if no column depends on the parameters. TypeError for wrong dtypes or a non-scalar objective.

ensemble_scene_gradient_vjp#

ensemble_scene_gradient_vjp(basis, *, column_forward, weights, execution, objective=None, field_cotangents=None, reduction_order=None, reduction_device=None) -> AggregatedSceneGradient

Runs each array column's forward as an independent task over the execution device pool (round-robin placement, fail-fast), then calls aggregate_scene_gradient_vjp. The backward and the sum run on the calling thread after every device has synchronized. Use this instead of run_many, which rejects trainable simulations.

basisArrayBasisDatarequired

Array basis, as for aggregate_scene_gradient_vjp.

column_forwardCallable[[int, torch.device], tuple]required

Function column_forward(index, device) that runs column index on device under autograd and returns (e_theta, e_phi, parameters).

weightstorch.Tensor | BeamWeightsrequired

Beam weights, as for aggregate_scene_gradient_vjp.

executionMultiGPUExecutionrequired

Device pool and max_concurrency. Its placement and fail_fast are not used.

objectiveCallable | Nonedefault None

As for aggregate_scene_gradient_vjp.

field_cotangentstuple[torch.Tensor, torch.Tensor] | Nonedefault None

As for aggregate_scene_gradient_vjp.

reduction_orderSequence[int] | Nonedefault None

As for aggregate_scene_gradient_vjp.

reduction_devicetorch.device | str | Nonedefault None

As for aggregate_scene_gradient_vjp.

Raises TypeError if execution is not a MultiGPUExecution; RuntimeError if a column forward fails; otherwise as for aggregate_scene_gradient_vjp.

grad = mw.ensemble_scene_gradient_vjp(
    basis, column_forward=column_forward, weights=weights,
    execution=mw.MultiGPUExecution.ensemble(devices=("cuda:0", "cuda:1")),
    objective=objective, reduction_device="cuda:0",
)
print(grad.gradient, grad.column_devices)