CVXPYgen

repository·master·Indexed 41 days ago

https://github.com/cvxgrp/cvxpygen

A code generation tool that transforms parametrized CVXPY convex optimization problems (LP, QP, SOCP) into optimized, custom C/C++ solvers. It supports embedded applications via C/C++ interfaces and rapid prototyping via Python/CVXPY interfaces. Key features include support for differentiable QPs via cvxpylayers, explicit solutions for QPs using PDAQP, and integration with solvers such as OSQP, CLARABEL, and QOCOGEN.

Tokens
7.2K
Snippets
24
Records
35
Agent score
52%

What's inside CVXPYgen

  1. How explicit solutions for QPs work

    master

    For QPs where all parameters are vectors (no matrix parameters), the solution is a piecewise affine function of the parameters. CVXPYgen integrates with PDAQP to precompute this map offline, allowing for extremely fast online evaluation.

    Requirements & Best Practices:

    • Constraint Limits: To keep the number of affine pieces tractable, limit parameters with standard CVXPY constraints (e.g., use l <= p, p <= u where p is a parameter).
    • Solver Selection: Use solver='explicit' in generate_code.
    • Dual Variables: To compute dual variables, set solver_opts={'dual': True}.
    • Complexity Control: Use solver_opts to control 'max_floats' (default 1e6) and 'max_regions' (default 500).
    • Precision: Use 'fp16': True in solver_opts to store the solution in half precision.
    # Example of generating an explicit solver with constraints
    prob = cp.Problem(obj, constr + [l <= p, p <= u])
    cpg.generate_code(prob, code_dir='code_explicit', solver='explicit')
  2. How differentiable QPs work with CVXPYlayers

    master

    CVXPYgen can generate code to differentiate the parameter-solution map of Quadratic Programs (QPs). This is achieved by enabling gradient=True during code generation and using the generated forward and backward functions with cvxpylayers.

    This feature supports conic solvers like QOCOGEN or CLARABEL for QP problems.

    # 1. Generate code with gradient enabled
    cpg.generate_code(problem, code_dir='nonneg_ls_diff', gradient=True)
    
    # 2. Integrate with CVXPYlayers
    from nonneg_ls_diff.cpg_solver import forward, backward
    from cvxpylayers.torch import CvxpyLayer
    from cvxpylayers.interfaces import SolverInterface
    
    layer = CvxpyLayer(problem, parameters=[A, b], variables=[x],
                       solver=SolverInterface.from_codegen(forward, backward))
  3. Use the generated C interface

    master

    The generated C code stores parameters and variables as Fortran-order (column-major) flat arrays. To compile the example cpg_example.c using CMake:

    Unix:

    cd nonneg_ls/c/build
    cmake ..
    cmake --build . --target cpg_example
    ./cpg_example

    Windows:

    cd nonneg_ls\c\build
    cmake ..
    cmake --build . --target cpg_example --config release
    Release\cpg_example
    # Unix example
    cd nonneg_ls/c/build
    cmake ..
    cmake --build . --target cpg_example
    ./cpg_example
  4. Quick start: Generate a custom C solver from a CVXPY problem

    master

    To use CVXPYgen, follow these steps:

    1. Define a parametrized CVXPY problem using Disciplined Parametrized Programming (DPP). Important: You must set the name= argument on all cp.Parameter and cp.Variable objects. These names are used to identify the symbols in the generated code.
    2. Generate the code using cpg.generate_code(problem, code_dir='...').

    An HTML summary of the generated code and usage instructions will be written to {code_dir}/README.html.

    import cvxpy as cp
    from cvxpygen import cpg
    
    # define CVXPY problem
    m, n = 3, 2
    A = cp.Parameter((m, n), name='A')
    b = cp.Parameter(m, name='b')
    x = cp.Variable(n, name='x')
    problem = cp.Problem(cp.Minimize(cp.sum_squares(A @ x - b)), [x >= 0])
    
    # generate code
    cpg.generate_code(problem, code_dir='nonneg_ls')
  5. Use the generated CVXPY interface

    master

    You can register the generated Python module as a custom solver method within CVXPY. This allows you to solve the original problem using the high-performance generated code.

    Use problem.register_solve('METHOD_NAME', solve_function) and then call problem.solve(method='METHOD_NAME', updated_params=[...]).

    from nonneg_ls.cpg_solver import cpg_solve
    
    # Register the solver
    problem.register_solve('CPG', cpg_solve)
    
    # Solve the problem
    b.value = ...
    problem.solve(method='CPG', updated_params=['b'])
    
    # Access solution
    solution = x.value
  6. Understand parameter canonicalization with ParameterInfo and ParameterCanon

    master

    To bridge the gap between user-defined parameters and the canonical vector passed to the solver, CVXPYgen uses two primary structures:

    1. ParameterInfo: Contains metadata about user-defined parameters, including their names, shapes, sizes, and how they map to columns in the canonical parameter vector (id_to_col). It also tracks which parameters are writable.
    2. ParameterCanon: Represents the first affine form of the problem. It stores the canonicalized parameters (p_csc as sparse matrices) and tracks which parameters are outdated or require updates (p_id_to_changes).
  7. How the Generator orchestrates code generation

    master

    The Generator follows a structured pipeline to transform a high-level CVXPY problem into optimized C code:

    1. Solver Resolution: Determines the appropriate solver and whether 'explicit' mode is used.
    2. Canonicalization: Converts the CVXPY problem into a canonical form suitable for the chosen solver. If gradient=True and the solver is not OSQP (and not in explicit mode), it performs a two-stage gradient process.
    3. Presolving: If in 'explicit' mode, a presolver is run to optimize the canonical form.
    4. Folder Setup: Creates the necessary directory structure (e.g., c/src, c/include, cpp/src) inside the target code_dir.
    5. Solver Code Generation: Writes solver-specific C files.
    6. Canonical Code Generation: Writes the code that handles the problem's canonical structure.
    7. Compilation (Optional): If wrapper=True, it compiles the code into a Python module that can be imported directly.
  8. Retrieve primal and dual variables using VariableInfo

    master

    CVXPYgen uses specialized info classes to map canonical solver solutions back to user-defined variable names.

    • PrimalVariableInfo: Used to retrieve primal variables. It includes mapping for symbolic variables (name_to_sym) and handles reduced variable sets (reduced).
    • DualVariableInfo: Used to retrieve dual variables. It maps variable names to their vector representations (name_to_vec).

    Both inherit from VariableInfo, which provides metadata like name_to_shape, name_to_size, and name_to_indices.

  9. How two-stage gradient code generation works

    master

    When configuration.gradient_two_stage is enabled, CCodeWriter modifies the generation logic to support a two-stage gradient computation. This involves:

    1. Workspace Redirection: Temporarily redirecting solver workspace pointers so the main workspace points to the intermediate OSQP solution.
    2. Include Path Management: Adding OSQP include directories (osqp_code/, osqp_code/inc/private, osqp_code/inc/public) to the CMake context after solver include directories to prevent header shadowing.
    3. Gradient Workspace: Generating a specific cpg_gradient_workspace.h/c and cpg_osqp_grad_workspace.h/c.
    4. Function Generation: Generating specialized functions like cpg_retrieve_intermediate_primal() and cpg_retrieve_intermediate_dual() to extract solutions from the intermediate stage.
  10. Embed generated code in a C/C++ project

    master

    To integrate the generated solver into an existing C/C++ project, follow these steps:

    1. Copy the following files into a subdirectory <subdir> of your project:

      • {{ code_dir }}/c/CMakeLists.txt
      • {{ code_dir }}/c/src/*
      • {{ code_dir }}/c/include/*
      • {{ code_dir }}/c/solver_code/*
    2. In your project's top-level CMakeLists.txt, add:

    add_subdirectory(<subdir>)
    include_directories(... ${{ '{' }}{{ cmake_prefix }}_include)

    Note: ${{ '{' }}{{ cmake_prefix }}_include is the include directory exposed by the generated CMakeLists.txt. Add ${{ '{' }}{{ cmake_prefix }}_head and ${{ '{' }}{{ cmake_prefix }}_src to any targets that require the generated code.

  11. Use the CVXPY interface with generated code

    master

    To use the generated C solver within a CVXPY problem, register the custom cpg_solve method. This allows you to call prob.solve(method='CPG') to route the optimization through the high-performance generated C extension.

    Note: <param> and <var> are placeholders for your specific parameter and variable names.

    from {{ code_dir_python }}.cpg_solver import cpg_solve
    prob.register_solve('CPG', cpg_solve)
    
    <param>.value = ...
    prob.solve(method='CPG')
    print(<var>.value)