10 Technical guidelines

This section contains some more in-depth technical guidelines for Fusion API for Python, not strictly necessary for basic use of MOSEK.

10.1 Limitations

Fusion imposes some limitations on certain aspects of a model to ensure easier portability:

  • Constraints and variables belong to a single model, and cannot as such be used (e.g. stacked) with objects from other models.

  • Most objects forming a Fusion model are immutable.

The limits on the model size in Fusion are as follows:

  • The maximum number of variable elements is \(2^{31}-1\).

  • The maximum size of a dimension is \(2^{31}-1\).

  • The maximum number of structural nonzeros in any single expression object is \(2^{31}-1\).

  • The total size of an item (the product of dimensions) is limited to \(2^{63}-1\).

10.2 Memory management and garbage collection

Users who experience memory leaks using Fusion, especially:

  • memory usage not decreasing after the solver terminates,

  • memory usage increasing when solving a sequence of problems,

should make sure that the Model objects are properly garbage collected. Since each Model object links to a MOSEK task resource in a linked library, it is sometimes the case that the garbage collector is unable to reclaim it automatically. This means that substantial amounts of memory may be leaked. For this reason it is very important to make sure that the Model object is disposed of manually when it is not used any more. The necessary cleanup is performed by the method Model.dispose.

The Model supports the Context Manager protocol, so it will be destroyed properly when used in the construction:

with Model() as M:
    # Work with the model here
    pass;

One can also write

try:
    M = Model()
    # Work with the model here
finally:
    M.dispose()

This construction assures that the Model.dispose method is called when the object goes out of scope, even if an exception occurred. If this approach cannot be used, e.g. if the Model object is returned by a factory function, one should explicitly call the Model.dispose method when the object is no longer used.

Furthermore, if the Model class is extended, it is necessary to dispose of the superclass if the initialization of the derived subclass fails. One can use a construction such as:

class MyModel(Model): 
    def __init__(self): 
        finished = False 
        try: 
            Model.__init__(self) 
            # other initialization
            finished = True 
        finally: 
            if not finished: 
                self.dispose()

10.3 Names

All elements of an optimization problem in MOSEK (objective, constraints, variables, etc.) can be given names. Assigning meaningful names to variables and constraints makes it much easier to understand and debug optimization problems dumped to a file. On the other hand, note that assigning names can substantially increase setup time, so it should be avoided in time-critical applications.

The Model object’s, variables’ and constraints’ constructors provide versions with a string name as an optional parameter.

Names introduced in Fusion are transformed into names in the underlying low-level optimization task, which in turn can be saved to a file. In particular:

  • a scalar variable with name var becomes a variable with name var[],

  • a one- or more-dimensional variable with name var becomes a sequence of scalar variables with names var[0], var[1], etc. or var[0,0], var[0,1], etc., depending on the shape,

  • the same applies to constraints,

  • a new variable with name 1.0 may be added.

These are the guidelines. No guarantees are made for the exact form of this transformation.

The user can override the default numbering scheme by providing a list of string labels for some or all axes. For example the following code

    itemNames = ["ITEM1", "ITEM2", "ITEM3"]
    slotNames = ["SLOT1", "SLOT2"]

    x = M.variable("price", [3,2], Domain.unbounded()
                                         .withNamesOnAxis(itemNames,0)
                                         .withNamesOnAxis(slotNames,1))

will lead to the individual entries of variable price being named as price[ITEM1,SLOT1], price[ITEM1,SLOT2] and so on instead of price[0,0], price[0,1] etc.

Note that file formats impose various restrictions on names, so not all resulting names can be written verbatim to each type of file, and when writing to a file further transformations and character substitutions can be applied, resulting in poor readability. This is particularly true for LP files, so saving Fusion problems in LP format is discouraged. The PTF format is recommended instead. See Sec. 15 (Supported File Formats).

10.4 Multithreading

Thread safety

Sharing a Model object between threads is safe, as long as it is not accessed from more than one thread at a time. Multiple Model objects can be used in parallel without any problems.

Parallelization

The interior-point and mixed-integer optimizers in MOSEK are parallelized. By default MOSEK will automatically select the number of threads. However, the maximum number of threads allowed can be changed by setting the parameter numThreads and related parameters. This should never exceed the number of cores.

The speed-up obtained when using multiple threads is highly problem and hardware dependent. We recommend experimenting with various thread numbers to determine the optimal settings. For small problems using multiple threads may be counter-productive because of the associated overhead. Note also that not all parts of the algorithm can be parallelized, so there are times when CPU utilization is only 1 even if more cores are available.

Determinism

By default the optimizer is run-to-run deterministic, which means that it will return the same answer each time it is run on the same machine with the same input, the same parameter settings (including number of threads) and no time limits.

Setting the number of threads

The number of threads the optimizer uses can be changed with the parameter numThreads.

The parameter numThreads affects only the optimizer. It may be the case that numpy is consuming more threads. In most cases this can be limited by setting the environment variable MKL_NUM_THREADS. See the numpy documentation for more details.

10.5 Efficiency

The following guidelines can help keep the code as efficient as possible.

Decide between sparse and dense matrices

Deciding whether a matrix should be stored in dense or sparse format is not always trivial. First, there are storage considerations. An \(n\times m\) matrix with \(l\) non zero entries, requires

  • \(\approx n\cdot m\) storage space in dense format,

  • \(\approx 3\cdot l\) storage space in sparse (triplet) format.

Therefore if \(l \ll n\cdot m\), then the sparse format has smaller memory requirements. Especially for very sparse density matrices it will also yield much faster expression transformations. Also, this is the format used ultimately by the underlying optimizer task. However, there are borderline cases in which these advantages may vanish due to overhead spent creating the triplet representation.

Sparsity is a key feature of many optimization models and often occurs naturally. For instance, linear constraints arising from networks or multi-period planning are typically sparse. Fusion does not detect sparsity but leaves to the user the responsibility of choosing the most appropriate storage format.

Reduce the number of Fusion calls and level of nesting

A possible source of performance degradation is an excessive use of nested expressions resulting in a large number of Fusion calls with small model updates, where instead the model could be updated in larger chunks at once. In general, loop-free code and reduction of expression nesting are likely to be more efficient. For example the expression

\[\begin{split}\sum_{i=1}^n A_i x_i\\ x_i \in \real^k, A_i \in \real^{k\times k},\end{split}\]

could be implemented in a loop as

    ee = Expr.constTerm(k, 0.)
    for i in range(n):
      ee = Expr.add( ee, Expr.mul(A[i],x[i]) )

A better way is to store the intermediate expressions for \(A_i x_i\) and sum all of them in one step:

    ee = Expr.add( [ Expr.mul(AA,xx) for (AA,xx) in zip(A,x)] )

Fusion design naturally promotes this sort of vectorized implementations. See Sec. 6.8 (Vectorization) for more examples.

Parametrize relevant parts of the model

If you intend to reoptimize the same model with changing input data, use a parametrized model and modify it between optimizations by resetting parameter values, see Sec. 6.6 (Parameters). This way the model is constructed only once, and only a few coefficients need to be recomputed each time.

Keep a healthy balance and parametrize only the part of the model you in fact intend to change. For example, using parameters in place of all constants appearing in the model would be an overkill with an adverse effect on efficiency since all coefficients in the problem would still have to be recomputed each time.

Do not fetch the whole solution if not necessary

Fetching a solution from a shaped variable produces a flat array of values. This means that some reshaping has to take place and that the user gets all values even if they are potentially interested only in some of them. In this case, it is better to create a slice variable holding the relevant elements and fetch the solution for this subset. See Sec. 6.7 (Stacking and views). Fetching the full solution may cause an exception due to memory exhaustion or platform-dependent constraints on array sizes.

Remove names

Variables, constraints and the objective function can be constructed with user-assigned names. While this feature is very useful for debugging and improves the readability of both the code and of problems dumped to files, it also introduces quite some overhead: Fusion must check and make sure that names are unique. For optimal performance it is therefore recommended to not specify names at all.

10.6 The license system

MOSEK is a commercial product that always needs a valid license to work. MOSEK uses a third party license manager to implement license checking. The number of license tokens provided determines the number of optimizations that can be run simultaneously.

By default a license token remains checked out from the first optimization until the end of the MOSEK session, i.e.

  • a license token is checked out when the method Model.solve is called the first time, and

  • the token is returned when the process exits.

Starting the optimization when no license tokens are available will result in an error.

Default behaviour of the license system can be changed in several ways:

  • Setting the parameter cacheLicense to "off" will force MOSEK to return the license token immediately after the optimization completed.

  • Setting the license wait flag with Model.putlicensewait or with the parameter licenseWait will force MOSEK to wait until a license token becomes available instead of throwing an exception.

  • The default path to the license file can be changed with Model.putlicensepath.

10.7 Deployment

When redistributing a Python application using the MOSEK Fusion API for Python 10.2.8, the following shared libraries from the MOSEK bin folder are required:

  • Linux : libmosek64, libmosekxx, libtbb,

  • Windows : mosek64, mosekxx, tbb, svml_dispmd,

  • OSX : libmosek64, libmosekxx, libtbb.

Furthermore, the folder python/3/mosek must be in the PYTHONPATH.

By default the MOSEK Python API will look for the binary libraries in the MOSEK module directory, i.e. the directory containing __init__.py. Alternatively, if the binary libraries reside in another directory, the application can pre-load the mosekxx library from another location before mosek is imported, e.g. like this

import ctypes ; ctypes.CDLL('my/path/to/mosekxx.dll')