Acados takes ages for compiling many interpolants

Hi :wave:

I want to use a lot of interpolants in my model. However, using acados, compiling the MPC takes forever for a lot of interpolants. Using dompc for setting up the MPC only takes a fraction of the time.

Below you find an example code where you can see the difference. In my actual example I want to use 215 interpolants and 120 repititions. Repitition in my example means how often I want to evaluate the same interpolant at a different point.
Any idea how I could speed up the compilation or reduce the generated code? My first idea would be that, for the repititions, similar code could be reused during code generation. I don’t know if this is done.

import uuid
from time import perf_counter

import casadi as cd
import numpy as np
from acados_template import AcadosModel, AcadosOcp, AcadosOcpSolver
from do_mpc.controller import MPC
from do_mpc.model import Model

rng = np.random.default_rng(seed=42)


def create_interpolant() -> cd.Function:
    xgrid = np.linspace(-5, 5, 12)
    ygrid = np.linspace(-4, 4, 10)
    X, Y = np.meshgrid(xgrid, ygrid, indexing='ij')
    R = np.sqrt(5 * X**2 + Y**2) + rng.random(1)
    data = np.sin(R) / R
    data_flat = data.ravel(order='F')
    name = 'interpolant_' + str(uuid.uuid4()).replace('-', '_')
    lut = cd.interpolant(name, 'bspline', [xgrid, ygrid], data_flat)
    return lut


def export_simple_acados_model(n_interpolants: int = 2,
                               n_repititions: int = 1) -> AcadosModel:
    model_name = 'minimal_example' + str(uuid.uuid4()).replace('-', '_')
    # set up states & controls
    x = cd.MX.sym('x')
    u = cd.MX.sym('u')
    # xdot
    x_dot = cd.MX.sym('x_dot')
    # dynamics
    f_expl = 0
    for _ in range(n_interpolants):
        interpolant = create_interpolant()
        for index in range(n_repititions):
            f_expl = f_expl + interpolant(cd.vertcat(u, index))
    f_impl = x_dot - f_expl

    model = AcadosModel()

    model.f_impl_expr = f_impl
    model.f_expl_expr = f_expl
    model.x = x
    model.xdot = x_dot
    model.u = u
    # set model_name
    model.name = model_name

    return model


def export_simple_dompc_model(n_interpolants: int = 2,
                              n_repititions: int = 1) -> AcadosModel:
    # set up states & controls
    model = Model('continuous', 'MX')
    u = model.set_variable('_u', 'u')
    x = model.set_variable('_x', 'x')
    # dynamics
    f_expl = 0
    for _ in range(n_interpolants):
        interpolant = create_interpolant()
        for index in range(n_repititions):
            f_expl = f_expl + interpolant(cd.vertcat(u, index / n_repititions))
    model.set_rhs('x', f_expl)
    model.setup()
    return model


def test_with_interpolants(n_interpolants: int = 2,
                           n_repititions: int = 1) -> None:
    # create ocp object to formulate the OCP
    dompc_time = create_dompc_mpc(n_interpolants, n_repititions)
    acados_time = create_acados_mpc(n_interpolants, n_repititions)
    print(f'{dompc_time=}')
    print(f'{acados_time=}')


def create_dompc_mpc(n_interpolants: int = 2, n_repititions: int = 1) -> float:
    model = export_simple_dompc_model(n_interpolants, n_repititions)
    start = perf_counter()
    mpc = MPC(model)
    lterm = model.x['x']**2 + model.u['u']**2
    mterm = model.x['x']**2
    mpc.set_objective(mterm, lterm)
    dompc_options = {
        'n_horizon': 2,
        't_step': 1,
        'n_robust': 0,
    }
    mpc.set_param(**dompc_options)
    mpc.setup()
    end = perf_counter()
    elapsed_time = end - start
    return elapsed_time


def create_acados_mpc(n_interpolants: int = 2, n_repititions: int = 1) -> float:
    ocp = AcadosOcp()

    model = export_simple_acados_model(n_interpolants, n_repititions)
    ocp.model = model

    ocp.dims.N = 20
    ocp.cost.cost_type = 'EXTERNAL'
    ocp.cost.cost_type_e = 'EXTERNAL'
    ocp.model.cost_expr_ext_cost = model.x**2 + model.u**2
    ocp.model.cost_expr_ext_cost_e = model.x**2
    ocp.solver_options.tf = 1.0

    start = perf_counter()
    ocp_solver = AcadosOcpSolver(ocp, json_file='acados_ocp.json')
    end = perf_counter()
    elapsed_time = end - start
    return elapsed_time


if __name__ == '__main__':
    test_with_interpolants(n_interpolants=200, n_repititions=5)

Thanks,
David

Hi Jonathan,

apparently this is not a problem with the interpolants. I tried to substitute the interpolants with Gaussian bells but got the same problem. Apparently, it has something to do with the number of operations.

This is apparently a known problem with the Casadi code generation:
See https://groups.google.com/g/casadi-users/c/FNqBF6ilFgc?pli=1.
There is actually also a master thesis, which tries to resolve the problem by implementing a better code generation in JModelica.org (see Large-scale dynamic optimization
using code generation and parallel
computing
).

As you suggested I could resolve the problem by using the -O0 flag for compiling.
Altough, I think this is not the nicest solution. Maybe, there should be a hint on the acados website, that there may be problems with large-scale systems.

Thanks,
David

Hi Jonathan,

I just found out that the problem is only solved by the flag for our smaller problem. For the actual big problem it does not compile even after two hours. So maybe Acados is not suitable for large scale problems. What do you think?

Thanks,
David

Hi Jonathan,

just the file cost_y_0_hess.c has 108 MB. Should this file be generated at all when using the GAUSS_NEWTON approximation? Is the jacobian not enough for that?

Thanks,
David

Ok especially the model files are gigantic:

grafik

Partly with more than 5,000,000 lines of code.

But half of it is just comments.

Is it possible to suppress these comments?
I saw that there is a verbose option in the code generator class https://casadi.sourceforge.net/api/internal/d1/da2/classcasadi_1_1CodeGenerator.html#a3b93d5e12b3a0ec2128527bf3067af6c

Thanks,
David

Hi David,

No, it is not necessary to generate this file in case of GAUSS_NEWTON.
You can add an if in the python interface, such that it is only generated if necessary.

The problem is a combination of a model with huge repetitive computations, and that fact that they are in a way that CasADi code generation can not reuse it but unrolls the full computational graph, especially also for the derivatives.

I don’t think the comments are the big issue here.

Ultimately, I think it is a limitation of CasADi C code generation.
Maybe there are other frameworks that can generate C code for functions and their derivatives better [for your application].

I guess you have the following options:

  • look into other C code generating tool. Would be interesting how e.g. sympy deals with such complex expressions
  • simplify the problem formulation in a way that the expressions generated are less complex
  • solve the problem somehow with another framework that doesnt require generating the code for the model

Best,
Jonathan