OpenMDAO / OpenMDAO

OpenMDAO repository.
http://openmdao.org
Other
550 stars 251 forks source link

Distributed constraint values #1861

Closed markleader closed 3 years ago

markleader commented 3 years ago

Summary of Issue

Allow constraints to be distributed for parallel optimization.

Issue Type

Description

Allow driver.get_constraint_values to (optionally) be a distributed operation.

Mimic the API in get_design_var_values, add a get_remote argument to the get_constraint_values method in driver.

When get_remote=False, then the constraint values should not be gathered. When get_remote=True, then the constraint values will be gathered. (this should be the default)

NOTE: Default behavior should remain unchanged.

Example

Run the example with mpirun -np 2 python .

from pprint import pprint
import numpy as np
import openmdao.api as om
from mpi4py import MPI

from openmdao.utils.assert_utils import assert_near_equal

class DistribParaboloid(om.ExplicitComponent):

    def setup(self):
        self.options['distributed'] = True

        if self.comm.rank == 0:
            ndvs = 3
        else:
            ndvs = 2

        self.add_input('w', val=1.) # this will connect to a non-distributed IVC
        self.add_input('x', shape=ndvs) # this will connect to a distributed IVC

        self.add_output('y', shape=1) # all-gathered output, duplicated on all procs
        self.add_output('z', shape=ndvs) # distributed output
        self.declare_partials('y', 'x')
        self.declare_partials('y', 'w')
        self.declare_partials('z', 'x')

    def compute(self, inputs, outputs):
        x = inputs['x']
        local_y = np.sum((x-5)**2)
        y_g = np.zeros(self.comm.size)
        self.comm.Allgather(local_y, y_g)
        outputs['y'] = np.sum(y_g) + (inputs['w']-10)**2
        outputs['z'] = x**2

    def compute_partials(self, inputs, J):
        x = inputs['x']
        J['y', 'x'] = 2*(x-5)
        J['y', 'w'] = 2*(inputs['w']-10)
        J['z', 'x'] = np.diag(2*x)

if __name__ == "__main__":
    comm = MPI.COMM_WORLD

    p = om.Problem()
    d_ivc = p.model.add_subsystem('distrib_ivc',
                                   om.IndepVarComp(distributed=True),
                                   promotes=['*'])
    if comm.rank == 0:
        ndvs = 3
    else:
        ndvs = 2
    d_ivc.add_output('x', 2*np.ones(ndvs))

    ivc = p.model.add_subsystem('ivc',
                                om.IndepVarComp(distributed=False),
                                promotes=['*'])
    ivc.add_output('w', 2.0)
    p.model.add_subsystem('dp', DistribParaboloid(), promotes=['*'])

    p.model.add_design_var('x', lower=-100)
    p.model.add_objective('y')
    p.model.add_constraint('z', upper=10.0)

    p.setup()
    p.run_driver()

    dv_vals = p.driver.get_design_var_values(get_remote=False)

    # Check the values of the constraint array
    con_vals = p.driver.get_constraint_values(get_remote=False)
    assert_near_equal(con_vals['dp.z'], 4.0*np.ones(ndvs))
JustinSGray commented 3 years ago

Maybe we need to have a broader discussion about the correct responses for a distributed optimizer.

We have three quantities:

Design vars and constraints can either be local or global. I suppose in the general case of multi-objective problems it might be possible to have a distributed objective... but that seems weird to me. I think the objective(s) should be global always.

So that would imply that get_objective_values() should a) return the local copy of the global value b) broadcast the value from the root proc to all procs and return that We should choose one of these and go with it. There does not seem to be a need for optional selection of a/b here to me.

For get_constraint_values and get_design_var_values we need to distinguish what you want: a) local + global (may or may not need a broadcast from root, depending on how we decide to handle this) b) local + global + remote (requires an all gather) It will maybe be important for the user to know which constarints/dvs are local/remote/global... so that metadata needs to be available too.

gjkennedy commented 3 years ago

So in my mind the simplest case for a distributed optimization algorithm is this:

  1. the objective value and constraint values are global and the same on all processes
  2. the design variables are distributed with local values/associated meta data on each process

My view is that this should be the default, but that you could have options for a distributed constraint vector as well.

How the constraint values are broadcast or stored on each process isn't as much of an issue for the optimization algorithm itself, but is, of course, an issue for OpenMDAO.

The other issue here is the gradient/Jacobian, but in my view that should follow from the distribution of the design variables.

JustinSGray commented 3 years ago

The other issue here is the gradient/Jacobian, but in my view that should follow from the distribution of the design variables ... and constraints

I agree with that philosophy. But let me ask a few follow up questions:

1) if you have a distributed design vector, should OpenMDAO ever be expected to gather it all to one proc (i.e. should we let a non-distributed optimizer work in this situation)? I would think the answer is yes, even if just so we can run comparisons. You really don't want to have to re-structure the problem to switch optimizers. I know that some problems will be far too big to do the gather with... but I think OpenMDAO should still have the ability to do it.

2) same question, but with constraints.

3) same question, but with jacobian.

If the answer in all cases is "the driver should be able to query either the local + global, or the local+global+remote. Its up to the driver to do it right" then that gives us good bounds for the API. The other option is "The driver has to respect the structure that the model uses. So it can really only every have local+global".

Im not a fan of the latter, but Im open to someone attempting to change my mind. The "local+global" only would provide a simpler API and be a little less work... but I don't think the restriction is worth it. Im pretty confident that users will try to swap out to a non parallel driver and be frustrated when they get an error saying "fix your model. this won't work".

gjkennedy commented 3 years ago
  1. If the optimizer is efficient, it wouldn't have to gather everything to a single processor. Should OpenMDAO have this as an option? Probably yes. But I don't think that should be the default behavior.
  2. The constraints and objective are a bit different. OpenMDAO's target application is simulation-based optimization. If you're using a distributed/parallel code to perform a simulation and evaluate the objective and constraints, it's likely that they represent an output quantity of interest from the simulation which is often, but not always, a handful of scalar functions of interest. If you're using something like SAND or AAO, then the constraints may be fully distributed. But I think this is the exceptional use case, not a standard/typical use case.
  3. If the optimizer is efficient, it shouldn't collect the Jacobian, but again, that's going to depend on the optimizer so you probably want to have options.

I agree, I think the driver should have options to pick the data that it needs, with the associated penalties in computational time the further the desired problem structure deviates from the actual problem structure.

JustinSGray commented 3 years ago

@markleader is this till an open issue?

markleader commented 3 years ago

@JustinSGray No, we're good here - closed it.