When a project call is annotated, the resulting ProjectBlock does not record the solver parameters, so any replay or adjoint of the projection silently runs with the global firedrake defaults (preonly + lu, i.e. mumps) rather than the parameters the forward projection actually used. The same is true for the Projector's own defaults (cg with bjacobi/icc) and for constant_jacobian: none of the Projector configuration makes it onto the tape.
The cause is in firedrake/adjoint_utils/projection.py, where annotate_project passes only ProjectBlock.pop_kwargs(kwargs) to the block, and solver_parameters is not among the pop_kwargs_keys. Compare with annotate_solve in firedrake/adjoint_utils/solving.py, which does sb_kwargs.update(kwargs) so a plain solve keeps its parameters on the tape. The Function.project path in firedrake/adjoint_utils/function.py is affected even more directly, since it constructs the block with no kwargs at all. On top of that, the cg/icc defaults are applied inside Projector.__init__ after annotation has already happened, so even fixing the kwarg forwarding would not capture the default behaviour.
A small example. The projection is done with a deliberately unconverged solver so that the forward result is distinguishable from a direct solve; if the tape replayed with the recorded parameters, the replayed functional would match the taped one.
from firedrake import *
from firedrake.adjoint import *
mesh = UnitSquareMesh(4, 4)
W = FunctionSpace(mesh, "CG", 2)
V = FunctionSpace(mesh, "CG", 1)
x, y = SpatialCoordinate(mesh)
f = Function(W).interpolate(sin(2 * pi * x) * cos(2 * pi * y))
continue_annotation()
# Deliberately unconverged solve, so the result is distinguishable from
# the firedrake default (preonly + lu/mumps).
sp = {
"ksp_type": "richardson",
"pc_type": "none",
"ksp_max_it": 1,
"ksp_convergence_test": "skip",
}
u = project(f, V, solver_parameters=sp)
J = assemble(u**2 * dx)
pause_annotation()
block = get_working_tape().get_blocks()[0] # the ProjectBlock
print("solver_parameters passed to project:", sp)
print("block.forward_kwargs:", block.forward_kwargs)
print("block.adj_kwargs: ", block.adj_kwargs)
Jhat = ReducedFunctional(J, Control(f))
print("J recorded on tape:", float(J))
print("J replayed: ", Jhat(f))
which prints
solver_parameters passed to project: {'ksp_type': 'richardson', 'pc_type': 'none', 'ksp_max_it': 1, 'ksp_convergence_test': 'skip'}
block.forward_kwargs: {}
block.adj_kwargs: {}
J recorded on tape: 0.00017053972659324828
J replayed: 0.21837196945346216
The block has recorded no solver parameters at all, and the replayed functional differs from the taped one by three orders of magnitude because the replay solved the mass matrix exactly with mumps. Note that source and target spaces must differ in the example: projecting a function onto its own space shortcuts to an Assigner and no solve happens forward at all, while the tape still records a ProjectBlock that will solve on replay, which is arguably a second inconsistency.
Beyond reproducibility of the replay, the practical consequence is performance: every taped projection turns into a direct factorisation on replay and in the adjoint, which for large problems is dramatically more expensive than the cg/icc mass solve the user (or the Projector default) asked for.
When a
projectcall is annotated, the resultingProjectBlockdoes not record the solver parameters, so any replay or adjoint of the projection silently runs with the global firedrake defaults (preonly + lu, i.e. mumps) rather than the parameters the forward projection actually used. The same is true for the Projector's own defaults (cg with bjacobi/icc) and forconstant_jacobian: none of the Projector configuration makes it onto the tape.The cause is in
firedrake/adjoint_utils/projection.py, whereannotate_projectpasses onlyProjectBlock.pop_kwargs(kwargs)to the block, andsolver_parametersis not among thepop_kwargs_keys. Compare withannotate_solveinfiredrake/adjoint_utils/solving.py, which doessb_kwargs.update(kwargs)so a plainsolvekeeps its parameters on the tape. TheFunction.projectpath infiredrake/adjoint_utils/function.pyis affected even more directly, since it constructs the block with no kwargs at all. On top of that, the cg/icc defaults are applied insideProjector.__init__after annotation has already happened, so even fixing the kwarg forwarding would not capture the default behaviour.A small example. The projection is done with a deliberately unconverged solver so that the forward result is distinguishable from a direct solve; if the tape replayed with the recorded parameters, the replayed functional would match the taped one.
which prints
The block has recorded no solver parameters at all, and the replayed functional differs from the taped one by three orders of magnitude because the replay solved the mass matrix exactly with mumps. Note that source and target spaces must differ in the example: projecting a function onto its own space shortcuts to an
Assignerand no solve happens forward at all, while the tape still records aProjectBlockthat will solve on replay, which is arguably a second inconsistency.Beyond reproducibility of the replay, the practical consequence is performance: every taped projection turns into a direct factorisation on replay and in the adjoint, which for large problems is dramatically more expensive than the cg/icc mass solve the user (or the Projector default) asked for.