Adjoint differentiation
Applicable version · ArcQML 0.1.0
On this page
Forward definition
For gates in execution order, ArcQML run evolves the initial state gate by gate, then evaluates the Hamiltonian expectation. The state after gate k and the final objective are defined as:
If the initial state or any Circuit Parameter requires gradients and the current thread records them, run saves the final state, the Hamiltonian applied to that state, bound operations, and parameter-slot mappings, and creates a custom F64 Tensor autograd node. Otherwise it executes only the forward pass without saving adjoint context.
Single-state adjoint backward formula
Let the outer real scalar loss be L, the single-state expectation be f, and the scalar upstream gradient be g = ∂L/∂f. To avoid counting the upstream weight twice, the formulas below use an unscaled adjoint state:
Applying conjugate-transposed gates in reverse order recovers each gate input state. When parameter p occurs in several gates, its gradient sums their contributions:
The implementation may multiply g into the adjoint buffer at the start of the backward pass; it must then not multiply by g again. If the initial state requires gradients, ArcQML uses the complex-gradient convention ∂L/∂Re(ψ₀) + i ∂L/∂Im(ψ₀):
This is twice the conjugate Wirtinger derivative and should not be confused with the derivative without the factor of 2.
Parameter gradients are scalar Tensor objects with the corresponding Parameter dtype, F32 or F64. Only the final state and bound-gate information are needed, not the complete state at every layer.
Batch upstream gradients and VJPs
Batch run produces a [B] vector, not a scalar, so it cannot directly use argument-free backward(). Usually reduce it to a scalar with a classical loss such as mse_loss or binary cross entropy with logits. When the outer graph passes grad_output, the batch adjoint node requires an F64 Tensor with shape [B] and multiplies each upstream gradient into the corresponding row of adjoint_state.
Batch mode therefore computes a vector-Jacobian product (VJP): the external classical network or loss determines upstream weights for sample outputs, and the quantum adjoint propagates them to shared Circuit parameters and differentiable initial states.
no_grad and training-graph memory
Wrapping run in no_grad() or Python with arcqml.no_grad(): sets records_gradients to false, retains no adjoint context, and returns an ordinary forward Tensor. Use this mode for validation, inference, benchmarking, and read-only state analysis. If backpropagation is needed, do not first convert predictions to f64 or NumPy arrays, which leaves the Tensor graph.
Rust must bind the scope guard; calling no_grad(); alone immediately restores recording at the end of the statement:
{
let _guard = no_grad();
let prediction = simulator.run(&circuit, &observable)?;
// Read prediction in this scope.
}
Gradient recording is thread-local. In Python, create a fresh context each time with with arcqml.no_grad():; do not cache or reenter the same context object.