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:
| Path | Entry point | Use it for |
|---|---|---|
| Batch | Simulation.batch(...) | Variants of one common FDTD scene, solved together in native CUDA cohorts on one or more GPUs. Experimental. |
| Ensemble | run_many(...) with MultiGPUExecution | Independent Simulation objects, one per leased GPU, returned in submission order. |
| Array gradient | ensemble_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.
Declare
Call
Simulation.batch(...). It returns aBatchSimulationand only validates the declaration: types, names, shapes and the device policy. It does not touch CUDA.Plan (optional)
Call
batch.plan()for a dryBatchPlanReport. Withgrouping="strict"it raisesValueErrornaming the first field where a variant differs from variant 0 (for examplefrequencies,dt,boundary,grid). The report is provisional:resolvedis alwaysFalse.Prepare
Call
batch.prepare(). It returns aPreparedBatch: 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.Run or iterate
Call
prepared.run()for aBatchResult, orprepared.iter_results()to receive oneBatchItemper variant as it finishes. A prepared batch can run again, optionally with newBatchParametersof the same schema.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.grador.backward()on a loss built from them, even afterclose().
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:
| Requirement | Error 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) -> BatchSimulationStatic 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
Simulationplusparameters. The variant count isparameters.variant_count; variantiis the template with sliceiof every parameter tensor applied. - List: a nonempty sequence of
Simulationobjects and noparameters. Each simulation is one variant.
A template simulation, or a nonempty sequence of simulations. Every simulation must use FDTD and have no parallel config.
Device and capacity policy.
NonePer-variant tensor inputs. Required with a template; not allowed with a list.
NoneUnique, nonempty string IDs, one per variant. Default: "0", "1", and so on.
NoneNames 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.
NoneWhat 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) -> SceneReturns 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() -> BatchPlanReportBuilds 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() -> PreparedBatchRuns 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() -> BatchResultPrepares, runs every variant once and closes the prepared batch.
BatchSimulation.iter_results#
BatchSimulation.iter_results(*, order='completion') -> BatchResultIteratorPrepares a batch and returns an iterator that owns it; closing the iterator also closes the prepared batch.
'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) -> intNumber 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.
Ordered, unique CUDA devices with an explicit index, such as ("cuda:0", "cuda:1"). "cpu" and bare "cuda" are rejected.
'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.
32Upper bound for batch_size, at least 1.
NoneByte 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.
536870912Bytes of free device memory (512 MiB by default) left outside the budget. Must be at least 0.
'strict''strict' requires every variant to match variant 0 at plan time. 'compatible' splits variants into groups of matching declarations.
'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.
1Persistent field slots per device: 1 or 2. 'auto' requires tune=True.
'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.
'auto'Time steps per captured chunk, at least 1. 'auto' uses 32 (tuning tries 16, 32 and 64).
FalseIf 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.
TrueIf 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.
NoneMaps 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.
NoneMaps 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 -> intShared leading size V. Set at construction.
BatchParameters.requires_grad#
BatchParameters.requires_grad -> boolTrue if any tensor requires gradients.
BatchParameters.schema#
BatchParameters.schema -> tupleTuple 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.
()Unique names of MaterialRegion objects that differ between variants.
NoneMaps a structure name to a geometry that bounds where that structure may change.
()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.
'all'Monitor names to keep, or 'all'.
'all'Port names to keep, or 'all'. Batch execution currently rejects scenes with ports.
()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.
FalseExport materials. Must stay False; True raises at prepare.
'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.
2Completed 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():
| Key | Meaning |
|---|---|
schema_version | Always 1. |
resolved | Always False: compiled equality and memory admission are not proven. |
variant_count, variant_ids | Batch size and IDs. |
groups | List of groups with group_id, variant_indices, signature_digest, capability_status, output_plan and unresolved_reasons. |
devices | One entry per group and device with batch size, slots, graph steps and reserved headroom. |
warnings, unsupported_features | Text notes and features not implemented yet. |
configuration_digest | SHA-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() -> dictReturns a new dict decoded from the stored JSON.
BatchPlanReport.to_markdown#
BatchPlanReport.to_markdown() -> strReturns a short Markdown summary: variant count, a group table and the warnings.
BatchPlanReport.from_dict#
BatchPlanReport.from_dict(payload) -> BatchPlanReportClass 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) -> BatchResultRuns every variant and returns the results in input order. Failures are stored as entries, not raised.
NoneNew 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') -> BatchResultIteratorStarts 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.
NoneSame as for run().
'completion''completion' or 'input'.
Raises as for run(), plus ValueError for another order.
PreparedBatch.request_cancel#
PreparedBatch.request_cancel() -> NoneAsks the active run to stop. Variants not yet finished are reported with FailureKind.CANCELLED. Safe to call from another thread.
PreparedBatch.close#
PreparedBatch.close() -> NoneReleases 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 BatchResultIteratorIterator 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() -> NoneStops 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.
| Field | Type | Meaning |
|---|---|---|
index | int | Input index. |
variant_id | str | Variant ID. |
result | Result or None | The variant's result. |
failure | DistributedFailure or None | The failure, if any. |
record | ExecutionRecord | Device, status and wall time. |
BatchItem.raise_for_error#
BatchItem.raise_for_error() -> NoneRaises 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.TensorApplies selector to each result and stacks the tensors along a new first axis.
Function that picks one tensor from a result.
NoneDevice 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) -> NoneWrites 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') -> BatchResultClass method that reads a directory written by save().
Directory holding manifest.json.
One scene per saved variant, in order.
'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.
Ordered, unique CUDA devices with an explicit index. One device is allowed.
Maximum tasks in flight, at least 1.
'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.
FalseIf True, the first failure cancels tasks that have not started. Tasks already running finish.
1Maximum 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) -> MultiGPUExecutionClass 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) -> DevicePoolBuilds 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) -> ResultSequenceRuns 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.
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.
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 -> tupleNon-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 -> boolTrue if any task failed or was cancelled.
ResultSequence.results#
ResultSequence.results() -> tupleReturns 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.
Submission index.
Device the task ran on, or None if it never leased one.
"completed", "failed" or "cancelled".
NoneMemory estimate used for placement, in bytes.
NoneWall time in seconds.
NoneGPU time in seconds, measured with CUDA events by run_many when available.
NoneThe 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 -> boolTrue 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__.
Submission index.
Failure category.
NoneLeased device, or None.
NoneOriginal exception.
NoneDetail 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.
| Member | Value | Meaning |
|---|---|---|
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.
| Field | Type | Meaning |
|---|---|---|
gradient | torch.Tensor or a tuple of tensors | Summed gradient on reduction_device. A bare tensor when each column has one parameter, otherwise a tuple. |
reduction_order | tuple[int, ...] | Order in which column gradients were summed. |
reduction_device | str | Device holding gradient. |
column_devices | tuple[str, ...] | Device each column ran on. |
port_names | tuple[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) -> AggregatedSceneGradientReduces 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.
Array basis that sets the port count N, frequencies F, angular grid, dtype and default device. See Antennas and arrays.
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.
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.
Complex incident power-wave weights of shape [N] or [F, N], with the basis dtype, on the reduction device.
NoneFunction objective(e_theta, e_phi) on the combined field that returns a real scalar tensor.
NonePrecomputed cotangents for the combined (e_theta, e_phi), each of shape [F, T, P]. Give exactly one of objective and field_cotangents.
NonePermutation of range(N) for the summation. Default: port order.
NoneDevice 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) -> AggregatedSceneGradientRuns 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.
Array basis, as for aggregate_scene_gradient_vjp.
Function column_forward(index, device) that runs column index on device under autograd and returns (e_theta, e_phi, parameters).
Beam weights, as for aggregate_scene_gradient_vjp.
Device pool and max_concurrency. Its placement and fail_fast are not used.
NoneAs for aggregate_scene_gradient_vjp.
NoneAs for aggregate_scene_gradient_vjp.
NoneAs for aggregate_scene_gradient_vjp.
NoneAs 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)