Design¶
This page explains how a solve runs and why it is fast. It matters when you profile a problem or work on the package.
Programs and callbacks¶
Ipopt is a C++ library with a C interface. A user of that interface gives it five callback functions, and Ipopt calls them with the current point. Each callback of xipopt runs one program:
- The callback copies Ipopt's array
xinto an input buffer that is 64-byte aligned. - It calls
executefrom cyxla, which passes the buffers to the PJRT plugin. - It writes the program's output into the array that Ipopt provides.
The input buffers are aliased. On the CPU plugin a PJRT buffer can share the memory of an existing array, so filling the buffer is a plain copy and the plugin sees the new values at the next execution. The outputs are read from the plugin's memory in place. A callback allocates nothing and does not hold the GIL, and the solve itself runs with the GIL released.
The programs read the variables x and the parameters p. The Hessian program also
reads the objective factor and the multipliers, which Ipopt supplies with every call of
eval_h.
Where the time goes¶
A callback costs microseconds for a small problem, and its time grows with the
size of the programs. For problems of a few hundred variables the callbacks dominate a
solve, and a Python callback of a jitted function costs far more per call than the
program itself. Beyond that the factorization of the linear system takes over, so the
choice of linear solver and the sparsity of the derivatives matter more than the
callbacks. Solution.statistics shows how the time splits for a given problem.
Failures inside a callback¶
A callback never raises. If the plugin reports an error, the callback returns false,
which makes Ipopt treat the point as one where it cannot evaluate and shorten its step.
The message goes to the error buffer of cyxla. solve raises it as a RuntimeError
when the solve ends without success, and ignores it when Ipopt recovered.
Lowering and compiling¶
compile traces the functions on flat vectors of variables and parameters, inside
jax.enable_x64. It builds the derivative programs with
asdex or with dense JAX transformations, lowers all
five programs to StableHLO with cyxla.lower for the CPU, and compiles them with
cyxla.compile_program. The result is a NativeProblem, a Cython object that owns the
buffers and the executables.
The disk cache stores the lowered programs, the sparsity structure and, through cyxla, the compiled executables.
Libraries in the wheel¶
Ipopt calls BLAS and LAPACK through libblastrampoline, a library that forwards them to a
backend. When the package imports, it forwards the OpenBLAS that it links to
libblastrampoline. The wheels bundle all of these libraries next to the extension
module, and their run paths point there, so they never depend on a system copy. On
Windows the package adds the directory to the search path of DLLs before the extension
loads. SPRAL needs OpenMP cancellation, so the package sets OMP_CANCELLATION to TRUE
before the OpenMP runtime reads its environment.
Sharing a process¶
One process loads one PJRT plugin, and cyxla owns it. Libraries such as JAX-CIP that also use cyxla therefore share the client with xipopt.