PolyStep.jl

Stable Dev CI codecov Aqua QA arXiv DOI License: MIT

PolyStep.jl minimizes black-box functions that have no useful gradient. Examples are losses that are piecewise constant, losses that go through an argmax or a combinatorial solver, and outputs of integer-valued simulations. It is the Julia version of PolyStep (TMLR 2026).

Each step puts a randomly rotated polytope around the current point. It evaluates all vertices in one batched call and moves to a softmax-weighted average of the vertices. The vertices are at a finite distance, so a step can cross flat regions where the gradient is zero. The same seed gives bit-identical results on any number of threads.

Paper · Interactive visualization · Guide · Benchmarks

One particleFour particles, two minima
One particle probes an objective and moves toward its minimum through weighted barycentric steps.Four particles probe a two-basin objective and move toward its minima through weighted barycentric steps.

The animations come from the interactive visualization by Viet T. Nguyen.

Installation

using Pkg
Pkg.add("PolyStep")

Quick start

using PolyStep, Random

# batched objective: (d, N) candidate columns -> N costs.
# Piecewise constant, zero gradient almost everywhere, minimum 0.
f(X) = vec(sum(floor.(abs.(X)); dims = 1))

x0 = fill(5.5, 10)
es = minimize(f, 10; steps = 100, epsilon = 0.1, step_radius = 1.0, x0,
              rng = Xoshiro(0))
println((f0 = f(reshape(x0, :, 1))[1], best_f = es.best_f, evals = es.evals))
# (f0 = 50.0, best_f = 0.0, evals = 2001)

The radius must be larger than the flat regions. With step_radius = 0.5, all vertices cost 50, the weights are uniform, and the iterate stays at x0.

Ask/tell, with bounds and integrality repair:

cost(X) = vec(sum(abs2, X .- 3.4; dims = 1))   # best integer point: fill(3.0, 5)

es = PolyStepES(5; epsilon = 0.1, step_radius = 1.0, x0 = fill(9.0, 5),
                lb = 0.0, ub = 10.0, repair = X -> round.(X))
for _ in 1:100
    X = ask!(es)          # (dim, popsize) columns, clamped and repaired
    tell!(es, cost(X))
end
println((best_x = es.best_x, best_f = es.best_f))
# (best_x = [3.0, 3.0, 3.0, 3.0, 3.0], best_f = 0.7999999999999996)

minimize and PolyStepES accept entropic optimal transport and greedy solvers, box bounds, a repair function and a geometric radius_decay. minimize also takes a callback. PolyStepConfig with step! and solve! adds simplex and cube templates, epsilon schedules, momentum, an adaptive radius and biased rotations. It also has a quadratic model with Newton refinement and a trust region. HybridSubspace searches a subspace of the parameters of a model, layer by layer. PolyStepOptimizer connects the package to Optimization.jl. With LoopVectorization loaded, the Sinkhorn and KL kernels run faster, and PolyStep.cuda_objective evaluates an objective on a GPU. The guide explains tuning, bounds, solvers, threads and subspaces. The package needs Julia 1.10 or later.

When to use it

Use PolyStep when the loss goes through argmax routing, a combinatorial solver or an integer-valued simulation, and a batch of evaluations is cheap.

Use a different method in these cases:

  • One evaluation takes minutes: use Bayesian optimization.
  • The problem is smooth and small: CMA-ES is usually better, and Nelder-Mead solves example 02 in 106 evaluations.
  • A usable gradient or surrogate exists: in example 04, a Fenchel-Young loss needs approximately 200 times fewer solver calls.
  • You need convergence guarantees with integer variables: use NOMAD.

Examples

Each script prints a results table and asserts its claim. All methods get the same evaluation budget and a tuning grid of the same size. The tuning uses training data only, and the grids are in the script headers. "CMA-ES" is IPOP-CMA-ES with restarts (ipop_cma.jl).

julia --project=examples -e 'using Pkg; Pkg.instantiate()'
julia --project=examples examples/01_hard_decision_tree.jl
#ProblemResult
01Hard oblique decision tree, 0-1 loss, 20 checkerboards, 40k evalsMedian train/test accuracy: PolyStep 94.2/90.9%, IPOP-CMA-ES 87.7/83.3%, OpenAI-ES 85.4/80.8%, SPSA 84.6/80.4%. PolyStep has the higher test accuracy on 16-19 of 20 instances.
02(s,S) inventory policy, 2-D, Poisson demand, 5 seedsPolyStep, IPOP-CMA-ES and Nelder-Mead all reach the grid optimum. SPSA is worse at a 500-eval budget.
03Contextual districting through a capacitated-MST decoder, 8000 decoder calls, 5 seedsPolyStep 9.549, perturbed gradient + Adam 9.554, IPOP-CMA-ES 9.584. All are better than the true-cost decode (9.674) and within 0.4% of each other.
04Decision-focused subset selection (DecisionFocusedLearningBenchmarks), 625 weights, hard top-k, 3 seedsTest regret: PolyStep 2.27, Fenchel-Young 3.51, IPOP-CMA-ES 4.02 (untrained 6.51). PolyStep is lowest on every seed but uses approximately 200 times more solver calls than Fenchel-Young.
05DFL with predicted knapsack weights, 10 seeds, 12k evalsPolyStep has the lowest median train regret (0.229 vs CMA-ES 0.302). On test it is 4.74, CMA-ES 4.67 and SFGE 4.90; the differences are within seed noise.

We also ran the COCO bbob-mixint suite, where 80% of the variables are integers. In dimensions 5 and 10, PolyStep solves as many targets as the better CMA-ES, or more, up to 1000 * dim evaluations. With the full 10^4 * dim budget, it ties CMA-ES with margin, and pycma is better in dimension 10. The benchmarks page gives the details.

Maintainers

Citation

@article{le2026training,
  title   = {Training Non-Differentiable Networks via Optimal Transport},
  author  = {Le, An T.},
  journal = {Transactions on Machine Learning Research},
  year    = {2026},
  url     = {https://arxiv.org/abs/2605.01928}
}

See also CITATION.cff and the changelog.