optora.core.convergence¶
Convergence tracking for iterative loops without per-iteration host syncs.
ConvergenceStatus
¶
Convergence diagnostics of a finished solve, materialized on demand.
A loop produces its convergence flag and iteration count on the
iterate's device, so reporting them as a Python bool and int costs
two synchronizations per solve: exactly the cost ConvergenceTracker
removes from the loop, paid again once the loop is over. Under nesting
that becomes the dominant cost, since every outer iteration of an
optora.dro.MinimaxSolver runs an inner dual solve whose diagnostics
nothing ever reads.
ConvergenceStatus therefore keeps the diagnostics as the tensors the
loop computed and converts them on first access, caching each
conversion so repeated reads cost one synchronization at most. A solve
whose diagnostics are never read never synchronizes for them.
converged
cached
property
¶
Whether the loop's convergence criterion was met, read on the host.
num_iterations
cached
property
¶
Number of iterations the loop performed, read on the host.
__init__(converged, num_iterations)
¶
Hold a finished loop's convergence state on its device.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
converged
|
Tensor
|
Zero-dimensional boolean tensor recording whether the loop's residual fell below its tolerance. |
required |
num_iterations
|
Tensor
|
Zero-dimensional integer tensor holding the number of iterations performed before convergence. |
required |
ConvergenceDiagnostics
¶
Host-side view of the convergence diagnostics of a solver result.
Mixed into every solver result so result.converged and
result.num_iterations read as plain Python values while the solve
itself stays asynchronous: status holds them as device tensors until
one of these properties is read.
Attributes:
| Name | Type | Description |
|---|---|---|
status |
ConvergenceStatus
|
Device-side convergence diagnostics of the solve, converted to host values only when read. |
ConvergenceTracker
¶
Convergence state of an iterative loop, held on the iterate's device.
A textbook iterative method tests residual < tol with a Python if,
which forces a device-to-host copy of the residual on every iteration.
Optora composes such loops (an optora.dro ambiguity set's inner dual
solve runs inside a MinimaxSolver outer solve, and a Wasserstein
ambiguity set additionally runs Sinkhorn iterations), so a single solve
would issue thousands of accelerator stalls.
ConvergenceTracker keeps the convergence flag and the iteration count
as tensors on the iterate's device, updates them with ordinary
elementwise kernels, and copies them to the host at most once every
check_interval iterations. Callers must freeze their iterates with
torch.where(tracker.converged, ...), which makes the iterations that
run between two host reads exact no-ops: the returned solution is
identical to one produced by testing convergence every iteration,
whatever check_interval is. Larger values trade those redundant
frozen iterations for fewer synchronizations.
That trade is only favourable when an iteration is cheap relative to a
synchronization, which holds for the inner loops this was written for
(a few elementwise kernels per iteration against a host read costing
microseconds). Raise check_interval only in that regime. Loops whose
single iteration is expensive — notably an outer solve whose objective
is an optora.dro worst-case expectation, where one frozen iteration
is an entire inner dual solve — should use check_interval=1 and pay
the synchronization instead.
Attributes:
| Name | Type | Description |
|---|---|---|
converged |
Zero-dimensional boolean tensor, |
|
check_interval |
Number of iterations between host reads of
|
__init__(tol, check_interval, reference)
¶
Initialize the tracker on the device of reference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tol
|
float
|
Positive tolerance the residual is compared against. Kept as a Python float: comparing a tensor against a Python scalar passes it to the comparison kernel directly, whereas materializing it as a tensor would copy it to the device on every solve. |
required |
check_interval
|
int
|
Number of iterations between host reads of the convergence flag. |
required |
reference
|
Tensor
|
Iterate whose device the convergence state is held on. |
required |
start(residual)
¶
Record the residual of the starting iterate, before any iteration.
A loop that evaluates its residual at the current iterate and steps with what it learned there tests the starting iterate once before iterating. That test is not an iteration, so it latches convergence without advancing the iteration count: a loop that starts at its own solution reports zero iterations.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
residual
|
Tensor
|
Zero-dimensional nonnegative convergence residual at the starting iterate. |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
The updated |
Tensor
|
|
update(residual)
¶
Record one iteration's residual without synchronizing.
Iterations performed after convergence do not advance the iteration count, so the reported count matches what a loop that exited the moment the residual dropped below the tolerance would report.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
residual
|
Tensor
|
Zero-dimensional nonnegative convergence residual, for example a gradient norm or the change in a dual potential. |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
The updated |
Tensor
|
|
should_stop(iteration)
¶
Check whether the loop may exit, synchronizing at most periodically.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
iteration
|
int
|
Zero-based index of the iteration that just ran. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
converged, |
status()
¶
Hand the loop's final diagnostics over without synchronizing.
Returns:
| Type | Description |
|---|---|
ConvergenceStatus
|
A |
ConvergenceStatus
|
number of iterations performed before convergence, still as |
ConvergenceStatus
|
device tensors. Converting them to host values is deferred to |
ConvergenceStatus
|
whoever reads them, so an inner solve nobody inspects costs no |
ConvergenceStatus
|
synchronization at all. |
validate_check_interval(check_interval)
¶
Validate a ConvergenceTracker checkpoint interval.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
check_interval
|
int
|
Number of iterations between host reads of the convergence flag. |
required |
Returns:
| Type | Description |
|---|---|
int
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |