Create and run a Julia solver benchmark

Create and run a Julia solver benchmark#

This example shows how to add a Julia solver in a simple benchmark using benchopt’s helpers to call Julia code from Python.

The benchmark objective is a simple minimization task:

\[\min_{\hat{X}} \; \mathrm{MSE}(X, \hat{X})\]

We define:

  • a Python Objective that evaluates MSE between X and X_hat;

  • a Python Dataset that generates a random matrix X;

  • two solvers:

    • Python-GD implemented in Python;

    • Julia-GD implemented in Julia and called through JuliaSolver.

At the end, we run the benchmark and display the comparison.

# Import example helpers to define the benchmark and
# programmatically call the CLI.
from benchopt.helpers.run_examples import ExampleBenchmark
from benchopt.helpers.run_examples import benchopt_cli
from benchopt.helpers.run_examples import EXAMPLES_ROOT

First, we define the initial Python benchmark, based on the benchmark examples/minimal_benchmark. It contains an objective.py file, a simulated dataset and a full python solver based on gradient descent.

benchmark = ExampleBenchmark(
    base="minimal_benchmark", name="julia_solver",
    ignore=["custom_plot.py", "example_config.yml"]
)
benchmark
            
from benchopt import BaseObjective
import numpy as np


class Objective(BaseObjective):
    # Name of the Objective function
    name = 'Quadratic'

    # The three methods below define the links between the Dataset,
    # the Objective and the Solver.
    def set_data(self, X):
        """Set the data from a Dataset to compute the objective.

        The argument are the keys of the dictionary returned by
        ``Dataset.get_data``.
        """
        self.X = X

    def get_objective(self):
        "Returns a dict passed to ``Solver.set_objective`` method."
        return dict(X=self.X)

    def evaluate_result(self, X_hat):
        """Compute the objective value(s) given the output of a solver.

        The arguments are the keys in the dictionary returned
        by ``Solver.get_result``.
        """
        return dict(value=np.linalg.norm(self.X - X_hat))

    def get_one_result(self):
        """Return one solution for which the objective can be evaluated.

        This function is mostly used for testing and debugging purposes.
        """
        return dict(X_hat=1)
from benchopt import BaseDataset

import numpy as np


class Dataset(BaseDataset):
    # Name of the Dataset, used to select it in the CLI
    name = 'simulated'

    # ``get_data()`` is the only method a dataset should implement.
    def get_data(self):
        """Load the data for this Dataset.

        Usually, the data are either loaded from disk as arrays or Tensors,
        or a dataset/dataloader object is used to allow the models to load
        the data in more flexible forms (e.g. with mini-batches).

        The dictionary's keys are the kwargs passed to ``Objective.set_data``.
        """
        return dict(X=np.random.randn(10, 2))
from benchopt import BaseSolver
import numpy as np


class Solver(BaseSolver):
    # Name of the Solver, used to select it in the CLI
    name = 'gd'

    # By default, benchopt will evaluate the result of a method after various
    # number of iterations. Setting the sampling_strategy controls how this is
    # done. Here, we use a callback function that is called at each iteration.
    sampling_strategy = 'callback'

    # Parameters of the method, that will be tested by the benchmark.
    # Each parameter ``param_name`` will be accessible as ``self.param_name``.
    parameters = {'lr': [1e-3, 1e-2]}

    # The three methods below define the necessary methods for the Solver, to
    # get the info from the Objective, to run the method and to return a
    # result that can be evaluated by the Objective.
    def set_objective(self, X):
        """Set the info from a Objective, to run the method.

        This method is also typically used to adapt the solver's parameters to
        the data (e.g. scaling) or to initialize the algorithm.

        The kwargs are the keys of the dictionary returned by
        ``Objective.get_objective``.
        """
        self.X = X
        self.X_hat = np.zeros_like(X)

    def run(self, cb):
        """Run the actual method to benchmark.

        Here, as we use a "callback", we need to call it at each iteration to
        evaluate the result as the procedure progresses.

        The callback implements a stopping mechanism, based on the number of
        iterations, the time and the evoluation of the performances.
        """
        while cb():
            self.X_hat = self.X_hat - self.lr * (self.X_hat - self.X)

    def get_result(self):
        """Format the output of the method to be evaluated in the Objective.

        Returns a dict which is passed to ``Objective.evaluate_result`` method.
        """
        return {'X_hat': self.X_hat}
#loaded from minimal_benchmark/config.yml
plot_configs:
  Subopt. (log):
    plot_kind: objective_curve
    scale: loglog
  Runtimes:
    plot_kind: bar_chart
"


We can now add solver in Julia with the same algorithm. To do this, we create a new file julia_gd.py that defines a solver based on the JuliaSolver class. This class provides helpers to call Julia code from Python, and to define the dependencies of the solver. The Julia code is defined in a separate file julia_gd.jl, that is loaded and called from the Python solver.

JULIA_SOLVER = EXAMPLES_ROOT / "language_solvers" / "julia_gd.py"
JULIA_SOLVER_PY = JULIA_SOLVER.read_text(encoding="utf-8")
JULIA_SOLVER_JL = JULIA_SOLVER.with_suffix(".jl").read_text(encoding="utf-8")

benchmark.update(
    solvers={"julia_gd.py": JULIA_SOLVER_PY, "julia_gd.jl": JULIA_SOLVER_JL},
)
            

We now update the following files:


from pathlib import Path

from benchopt.helpers.julia import JuliaSolver
from benchopt.helpers.julia import get_jl_interpreter


JULIA_SOLVER_FILE = str(Path(__file__).with_suffix('.jl'))


class Solver(JuliaSolver):
    name = "Julia-GD"
    sampling_strategy = "iteration"
    parameters = {"lr": [1e-3, 1e-2]}
    requirements = [
        "https://repo.prefix.dev/julia-forge::julia",
        "pip::julia",
    ]

    def set_objective(self, X):
        self.X = X
        jl = get_jl_interpreter()
        self.julia_gd = jl.include(JULIA_SOLVER_FILE)

    def warm_up(self):
        # Make sure we don't account for the Julia loading time in the
        # first iteration of the benchmark.
        self.julia_gd(self.X, self.lr, 20)

    def run(self, n_iter):
        # Here we cannot call a python callback, so we call iteratively
        # the solver with a growing number of iterations.
        self.X_hat = self.julia_gd(self.X, self.lr, n_iter)

    def get_result(self):
        return dict(X_hat=self.X_hat)
using Core


function gradient_descent(X, lr, n_iter)
    X_hat = zeros(size(X))
    for i  1:n_iter
        grad = X_hat - X
        X_hat -= lr * grad
    end

    return X_hat
end
"


In order to load the Julia interpreter, we use get_jl_interpreter. This function returns a Julia object from PyJulia, that can be used to interact with Julia. In particular, we can use the include method to load a Julia file and retrieve the functions defined in it as attributes of the returned object.

Note that the Julia solver cannot call a Python callback to report intermediate results, so we call iteratively the Julia solver with a growing number of iterations to be able to report the curve of the convergence.

To be able to run this benchmark, we need to install its dependencies. We can do this with benchopt install, using with the -s option which allow to select only this solver if multiple solvers are present. If Julia is not available in your environment, this command will use conda to install it.

benchopt_cli(f"install {benchmark.benchmark_dir} -s julia-gd")
$ benchopt install temp_benchmark__sdtdi20/julia_solver -s julia-gd
Installing 'julia_solver' requirements
# Install
Collecting packages:
- Quadratic: already available 
- Julia-GD: collected 
 done
Installing required packages for:
- Julia-GD
...Channels:
 - conda-forge
 - https://repo.prefix.dev/julia-forge
Platform: linux-64
Collecting package metadata (repodata.json): - \ | / - \ | / - \ | / done
Solving environment: \ | done

julia-1.12.5         | 165.2 MB  |            |   0%
perl-5.32.1          | 12.7 MB   |            |   0%

git-2.54.0           | 10.8 MB   |            |   0%


openblas-ilp64-0.3.3 | 5.7 MB    |            |   0%



libopenblas-ilp64-0. | 5.5 MB    |            |   0%




metis-5.1.0          | 3.7 MB    |            |   0%





p7zip-16.02          | 2.2 MB    |            |   0%






libcholmod-5.3.1     | 1.1 MB    |            |   0%







libgit2-1.9.4        | 1014 KB   |            |   0%








mpfr-4.2.2           | 713 KB    |            |   0%









gmp-6.3.0            | 449 KB    |            |   0%










libumfpack-6.3.5     | 424 KB    |            |   0%











libspqr-4.3.4        | 213 KB    |            |   0%












libklu-2.3.5         | 142 KB    |            |   0%













arpack-3.9.1         | 127 KB    |            |   0%














libcxsparse-4.4.1    | 118 KB    |            |   0%















libopenlibm4-0.8.1   | 102 KB    |            |   0%
















zlib-1.3.2           | 94 KB     |            |   0%

















libparu-1.0.0        | 91 KB     |            |   0%


















julia-1.12.5         | 165.2 MB  | 1          |   1% [A
perl-5.32.1          | 12.7 MB   | ##4        |  24%

git-2.54.0           | 10.8 MB   | ##9        |  30%


openblas-ilp64-0.3.3 | 5.7 MB    | #4         |  15%



julia-1.12.5         | 165.2 MB  | 5          |   5%
perl-5.32.1          | 12.7 MB   | ########4  |  84%

git-2.54.0           | 10.8 MB   | #########4 |  94%



libopenblas-ilp64-0. | 5.5 MB    | ########## | 100%




metis-5.1.0          | 3.7 MB    |            |   0%


openblas-ilp64-0.3.3 | 5.7 MB    | ########## | 100%


openblas-ilp64-0.3.3 | 5.7 MB    | ########## | 100%





julia-1.12.5         | 165.2 MB  | 8          |   8%





p7zip-16.02          | 2.2 MB    | ########## | 100%




metis-5.1.0          | 3.7 MB    | ########## | 100%




julia-1.12.5         | 165.2 MB  | #1         |  11%






libcholmod-5.3.1     | 1.1 MB    | 1          |   1%







libgit2-1.9.4        | 1014 KB   | 1          |   2%

git-2.54.0           | 10.8 MB   | ########## | 100%







libgit2-1.9.4        | 1014 KB   | ########## | 100%






libcholmod-5.3.1     | 1.1 MB    | ########## | 100%
perl-5.32.1          | 12.7 MB   | ########## | 100%








mpfr-4.2.2           | 713 KB    | 2          |   2%









gmp-6.3.0            | 449 KB    | 3          |   4%









gmp-6.3.0            | 449 KB    | ########## | 100%








mpfr-4.2.2           | 713 KB    | ########## | 100%










libumfpack-6.3.5     | 424 KB    | 3          |   4%











julia-1.12.5         | 165.2 MB  | #4         |  14%












libklu-2.3.5         | 142 KB    | #1         |  11%













arpack-3.9.1         | 127 KB    | #2         |  13%












libklu-2.3.5         | 142 KB    | ########## | 100%













arpack-3.9.1         | 127 KB    | ########## | 100%











libspqr-4.3.4        | 213 KB    | ########## | 100%










libumfpack-6.3.5     | 424 KB    | ########## | 100%














libcxsparse-4.4.1    | 118 KB    | #3         |  14%














libcxsparse-4.4.1    | 118 KB    | ########## | 100%
















zlib-1.3.2           | 94 KB     | #7         |  17%
















zlib-1.3.2           | 94 KB     | ########## | 100%

















libparu-1.0.0        | 91 KB     | #7         |  18%















libopenlibm4-0.8.1   | 102 KB    | #5         |  16%

















libparu-1.0.0        | 91 KB     | ########## | 100%















libopenlibm4-0.8.1   | 102 KB    | ########## | 100%


















 ... (more hidden) ...


















julia-1.12.5         | 165.2 MB  | ##5        |  25% [A



julia-1.12.5         | 165.2 MB  | ###4       |  34%




julia-1.12.5         | 165.2 MB  | ####1      |  42%


openblas-ilp64-0.3.3 | 5.7 MB    | ########## | 100%





p7zip-16.02          | 2.2 MB    | ########## | 100%





julia-1.12.5         | 165.2 MB  | ####8      |  49%







libgit2-1.9.4        | 1014 KB   | ########## | 100%







libgit2-1.9.4        | 1014 KB   | ########## | 100%






libcholmod-5.3.1     | 1.1 MB    | ########## | 100%






libcholmod-5.3.1     | 1.1 MB    | ########## | 100%









gmp-6.3.0            | 449 KB    | ########## | 100%









julia-1.12.5         | 165.2 MB  | #####5     |  56%








mpfr-4.2.2           | 713 KB    | ########## | 100%








mpfr-4.2.2           | 713 KB    | ########## | 100%












libklu-2.3.5         | 142 KB    | ########## | 100%












julia-1.12.5         | 165.2 MB  | ######2    |  63%













arpack-3.9.1         | 127 KB    | ########## | 100%













arpack-3.9.1         | 127 KB    | ########## | 100%











libspqr-4.3.4        | 213 KB    | ########## | 100%











libspqr-4.3.4        | 213 KB    | ########## | 100%










libumfpack-6.3.5     | 424 KB    | ########## | 100%










julia-1.12.5         | 165.2 MB  | ######9    |  69%














libcxsparse-4.4.1    | 118 KB    | ########## | 100%














libcxsparse-4.4.1    | 118 KB    | ########## | 100%
















zlib-1.3.2           | 94 KB     | ########## | 100%
















zlib-1.3.2           | 94 KB     | ########## | 100%

















libparu-1.0.0        | 91 KB     | ########## | 100%

















libparu-1.0.0        | 91 KB     | ########## | 100%















julia-1.12.5         | 165.2 MB  | #######5   |  76%















libopenlibm4-0.8.1   | 102 KB    | ########## | 100%


















 ... (more hidden) ...


















julia-1.12.5         | 165.2 MB  | #########7 |  97% [A

julia-1.12.5         | 165.2 MB  | ########## | 100%
julia-1.12.5         | 165.2 MB  | ########## | 100%


















                                                     A





































































































































































































































































 done
Preparing transaction: - \ done
Verifying transaction: / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ |
/ - done
Executing transaction: | / - \ | / - \ | / - \ | / - \ | / - \ | done
Installing pip dependencies: - \ | / - \ | / - \ | / - \ | / - \ | / - \ Ran
pip subprocess with arguments:
['/home/circleci/miniconda/envs/benchopt-docs/bin/python', '-m', 'pip',
'install', '-U', '-r', '/tmp/condaenv.cucyxon5.requirements.txt',
'--exists-action=b']
Pip subprocess output:
Collecting julia (from -r /tmp/condaenv.cucyxon5.requirements.txt (line 1))
  Downloading julia-0.6.2-py2.py3-none-any.whl.metadata (2.4 kB)
Downloading julia-0.6.2-py2.py3-none-any.whl (68 kB)
Installing collected packages: julia
Successfully installed julia-0.6.2

done
#
# To activate this environment, use
#
#     $ conda activate benchopt-docs
#
# To deactivate an active environment, use
#
#     $ conda deactivate

 done
- Checking installed packages...Failed to import Solver from
temp_benchmark__sdtdi20/julia_solver/solvers/julia_gd.py. Please fix the
following error to use this file with benchopt:
Traceback (most recent call last):
  File "/home/circleci/project/benchopt/utils/dynamic_modules.py", line 101,
in _load_class_from_module
    module = _get_module_from_file(module_filename, benchmark_dir)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/circleci/project/benchopt/utils/dynamic_modules.py", line 68, in
_get_module_from_file
    spec.loader.exec_module(module)
  File "<frozen importlib._bootstrap_external>", line 999, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File
"/home/circleci/project/examples/temp_benchmark__sdtdi20/julia_solver/solvers/
julia_gd.py", line 3, in <module>
    from benchopt.helpers.julia import JuliaSolver
  File "/home/circleci/project/benchopt/helpers/julia.py", line 8, in <module>
    import julia
ModuleNotFoundError: No module named 'julia'

/home/circleci/project/benchopt/benchmark.py:674: UserWarning: Some solvers
were not successfully installed, and will thus be ignored. Use 'export
BENCHOPT_RAISE_INSTALL_ERROR=true' to stop at any installation failure and
print the traceback.
  warnings.warn(
 done (missing deps: {'Julia-GD'})





Then, we can run the benchmark and show the comparison.

benchopt_cli(f"run {benchmark.benchmark_dir} -n 20 -r 4")
$ benchopt run temp_benchmark__sdtdi20/julia_solver -n 20 -r 4
Benchopt is running!
Benchopt called using profiling
Loading objective, datasets and solvers... done.
simulated
  |--Quadratic
No seed was specified. Selected global seed: 0
    |--gd[lr=0.001]: done (not enough run)
    |--gd[lr=0.01]: done (not enough run)
/home/circleci/miniconda/envs/benchopt-docs/lib/python3.12/site-packages/julia
/core.py:715: FutureWarning: Accessing `Julia().<name>` to obtain Julia
objects is deprecated.  Use `from julia import Main; Main.<name>` or `jl =
Julia(); jl.eval('<name>')`.
  warnings.warn(
    |--Julia-GD[lr=0.001]: done (not enough run)
    |--Julia-GD[lr=0.01]: done (not enough run)
Saving result in: 
temp_benchmark__sdtdi20/julia_solver/outputs/benchopt_run_2026-05-28_01h02m08.
parquet
Rendering benchmark results...
   Processing
temp_benchmark__sdtdi20/julia_solver/outputs/benchopt_run_2026-05-28_01h02m08.
parquet
done
Writing results to
temp_benchmark__sdtdi20/julia_solver/outputs/julia_solver_benchopt_run_2026-05
-28_01h02m08.html
Writing julia_solver index to
temp_benchmark__sdtdi20/julia_solver/outputs/julia_solver.html





Here, you see that the Julia solver is faster than the Python one. You also notice that the first iteration seems to take much longer than the other, hinting to a loading time for the solver.

Total running time of the script: (4 minutes 6.549 seconds)

Gallery generated by Sphinx-Gallery