Skip to content

Solver parameters passed to project are never recorded on the ProjectBlock #5172

Description

@sghelichkhani

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions