pyepo

repository·main·Indexed 20 days ago

https://github.com/khalil-research/pyepo

A PyTorch/JAX-based End-to-End Predict-then-Optimize tool for training machine learning models to optimize downstream decision quality. It supports symbolic modeling of Linear Programs (LP) and Mixed-Integer Programs (MIP) via pyepo.dsl and integrates with solvers such as Gurobi, COPT, Pyomo, OR-Tools, and the GPU-accelerated MPAX. The library provides a wide array of loss functions, including SPO+, PG, DPO, PFYL, and CaVE for binary linear programs.

Tokens
16.9K
Snippets
58
Records
72
Agent score
71%

What's inside pyepo

  1. Overview of Data and Datasets in PyEPO

    main

    PyEPO provides two main components for handling data in Predict-then-Optimize tasks:

    1. Synthetic Data Generators (pyepo.data): Used to generate synthetic data samples for testing and training.
    2. optDataset family: A set of classes used to wrap data samples into a format suitable for training and evaluation.

    To use these with specific problem models, refer to the built-in problem models documentation. For a practical walkthrough, see the 02 Optimization Dataset notebook.

  2. Overview of PyEPO

    main

    PyEPO is an open-source Python library designed for modeling and solving predict-then-optimize problems with linear objective functions. It allows you to embed optimization models (built with solvers like Gurobi, COPT, Pyomo, OR-Tools, or MPAX) directly into artificial neural networks for end-to-end training.

    PyEPO provides implementations of various loss functions as PyTorch autograd modules and offers a mirroring JAX frontend for training in JAX/Flax environments.

  3. Key Features of PyEPO

    main

    PyEPO provides several advanced capabilities for end-to-end optimization training:

    • Extensive Loss Implementations: Supports SPO+, PG, DPO (additive and multiplicative), PFYL (additive and multiplicative), I-MLE, AI-MLE, L2-regularized RFWO/RFYL, DBB, NID, CaVE, NCE, CMAP, and LTR.
    • Symbolic Modeling: Use pyepo.dsl to define a Linear Program (LP), Mixed-Integer Program (MIP), or supported fixed-quadratic objective once, then compile it to any backend.
    • Framework Support: Includes a JAX frontend (pyepo.func.jax) to train losses using jax.grad in JAX/Flax.
    • Performance Optimizations: Supports parallel computing for solvers, solution caching to accelerate training, and kNN robust loss for improved decision quality.
  4. What is PyEPO and how does it work?

    main

    PyEPO is a Python library for predict-then-optimize tasks. It is designed for scenarios where a model predicts objective coefficients for an optimization problem with a fixed feasible region. Instead of training on standard prediction error (like MSE), PyEPO trains the predictor against downstream decision quality (regret).

    Key components:

    • Modeling: Uses pyepo.dsl to define Linear Programs (LPs), Mixed-Integer Programs (MIPs), and fixed-quadratic objectives symbolically. These models are then compiled into an optModel.
    • Solvers: Supports multiple backends including Gurobi, COPT, Pyomo, Google OR-Tools, and MPAX (for GPU-accelerated batch solving).
    • Training Frontends: Exposes optimization layers to both PyTorch and JAX/Flax.
  5. Overview of PyEPO loss families

    main

    PyEPO provides several families of end-to-end gradient surrogates for training predictors:

    FamilyDescription
    Surrogate lossesConvex upper bounds on regret (SPO+) or finite-difference directional gradients (PG).
    Perturbed methodsMonte Carlo gradients over random cost perturbations (DPO, PFYL, I-MLE, AI-MLE).
    Regularized methodsL2-regularized Frank-Wolfe over the convex hull (RFWO, RFYL).
    Black-box methodsSurrogate backward rules for discrete solvers (DBB, NID).
    Cone-aligned estimationProjects predicted cost onto binding-constraint normals at the true optimum. Specifically for binary linear programs (CaVE).
    Contrastive methodsMargins against a cached pool of non-optimal solutions (NCE, CMAP).
    Learning to rankRanks the true optimum highest among a pool (pointwise, pairwise, or listwise LTR).
  6. Handle randomized losses with RNG keys in jax.jit

    main

    Solution-returning modules like DPO (which use randomized/perturbed losses) require an explicit key= argument when used inside a jax.jit decorated function. If you call a randomized loss inside jax.jit without providing a key, it will raise an error rather than silently freezing a single noise draw.

    When using jax.jit, the key becomes a traced argument that must be split in each iteration of your training loop.

    from pyepo.func.jax import DPO
    
    dpo = DPO(optmodel, n_samples=10, sigma=0.5)
    
    def loss_fn(p, k):
        # 'key=k' is required for jitted randomized losses
        we = dpo(predmodel.apply(p, xj), key=k) 
        return jnp.mean((we - wj) ** 2)
    
    step = jax.jit(jax.grad(loss_fn))
    
    key = jax.random.PRNGKey(0)
    for epoch in range(10):
        key, subkey = jax.random.split(key)
        grads = step(params, subkey)
        updates, opt_state = optimizer.update(grads, opt_state)
        params = optax.apply_updates(params, updates)
  7. GPU-Accelerated Solving with MPAX

    main

    PyEPO integrates with MPAX, a JAX-based mathematical programming solver that uses the PDHG algorithm for GPU acceleration. Using MPAX provides three main advantages for end-to-end training:

    1. GPU-native solving: The first-order PDHG method runs efficiently on the GPU.
    2. Batch solving: Entire mini-batches can be solved simultaneously via vectorization.
    3. Reduced overhead: Both the neural network and the solver reside on the GPU, eliminating the data transfer bottleneck between CPU and GPU.
  8. Use CaVE for Binary Linear Programs

    main

    For end-to-end learning on binary linear programs (such as TSP, CVRP, or knapsack problems), PyEPO provides CaVE.

    Instead of performing a per-step Integer Linear Program (ILP) solve, CaVE uses a cone-alignment projection onto the binding-constraint normals at the true optimum. This is backed by an interior-point QP solver (Clarabel). This approach is significantly faster than SPO+ at scale because the cone projection is computationally cheaper than per-instance ILP solving.

  9. How the Solution Pool works in PyEPO

    main

    The Solution Pool is a mechanism used in end-to-end predict-then-optimize training to speed up the process by approximating the feasible region. Instead of solving the full optimization problem (e.g., a linear or integer program) for every training instance, PyEPO stores previously computed optimal solutions in a pool $S$.

    When the pool is active, PyEPO selects the best solution from the cached pool $S$ based on the predicted cost (minimizing the objective for minimization problems or maximizing for maximization) instead of calling a solver. This acts as an inner approximation of the feasible region.

    Key components of the algorithm:

    • Predictor weights ($\omega$): The parameters being trained.
    • Predicted cost ($\hat{c}$): The output of the model $m(\omega, x)$.
    • Solution Pool ($S$): A set of previously solved optimal solutions.
    • solve_ratio ($p_{\text{solve}}$): The probability of choosing to solve the actual optimization problem versus selecting from the pool.
    • Cost Transform ($t(\cdot)$): An optional transformation applied to predicted costs.
  10. Use the JAX frontend for training

    main

    The pyepo.func.jax module provides JAX-compatible versions of the training methods (SPOPlus, DPO, PFY). These losses use jax.custom_vjp to handle the gradient through the optimization model.

    Importing:

    • PyTorch: from pyepo.func import SPOPlus, DPO, PFY
    • JAX: from pyepo.func.jax import SPOPlus, DPO, PFY

    Solver Backends:

    • MPAX: Solved natively. The PDHG solve is JAX-traceable, allowing the entire training step to be accelerated with jax.jit.
    • Non-MPAX (GurobiPy, COPT, Pyomo, OR-Tools): These are reached via jax.pure_callback, wrapping the CPU solver. While the training step can still be wrapped in jax.jit, the solver itself will run on the CPU.
    import jax
    import jax.numpy as jnp
    import optax
    from flax import linen as nn
    import pyepo
    from pyepo.data.dataset import optDataset
    from pyepo.func.jax import SPOPlus
    
    # Example setup for a 5x5 grid shortest path
    grid = (5, 5)
    optmodel = pyepo.model.shortestPathModel(grid)
    
    # Synthetic data generation
    x, c = pyepo.data.shortestpath.genData(
        num_data=1000, num_features=5, grid=grid, deg=4, noise_width=0.5, seed=135,
    )
    
    ds = optDataset(optmodel, x, c)
    xj = jnp.asarray(x, jnp.float32)
    cj, wj, zj = (jnp.asarray(a, jnp.float32) for a in (ds.costs, ds.sols, ds.objs))
    
    # Predictor and Loss
    predmodel = nn.Dense(optmodel.num_cost)
    params = predmodel.init(jax.random.PRNGKey(0), xj[:1])
    spo = SPOPlus(optmodel, reduction="mean")
    optimizer = optax.adam(1e-2)
    opt_state = optimizer.init(params)
    
    # End-to-end training loop
    for epoch in range(10):
        grads = jax.grad(lambda p: spo(predmodel.apply(p, xj), cj, wj, zj))(params)
        updates, opt_state = optimizer.update(grads, opt_state)
        params = optax.apply_updates(params, updates)