Skip to content

Commit ef59f7c

Browse files
authored
refactor!: Make QuadprogProjector dependencies optional (#683)
* `quadprog`, `qpsolvers`, and `numpy` are moved from core to optional dependencies * New `quadprog_projector` extra groups them together, enabling `UPGrad` and `DualProj` via `QuadprogProjector` * `numpy` is also added to the `cagrad` and `nash_mtl` extras (and their `_REQUIRED_DEPS` lists), and to the `plot` dev dependency group * `_WithOptionalDeps` is moved from `torchjd.aggregation._mixins` to `torchjd._mixins` so it can be reused by `QuadprogProjector` in `torchjd._linalg` * The standard install command in the README is updated to `pip install "torchjd[quadprog_projector]"` so users following the README have `UPGrad` and `DualProj` available out of the box * Add changelog entry * Update many tests to skip them / change aggregator depending on aggregator availability
1 parent ddf3f0b commit ef59f7c

22 files changed

Lines changed: 245 additions & 123 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ changelog does not include internal changes that do not affect the user.
88

99
## [Unreleased]
1010

11+
### Changed
12+
13+
- **BREAKING**: Removed `numpy`, `quadprog` and `qpsolvers` from the main dependencies of `torchjd`,
14+
(which now only has `torch` as its main dependency). This makes the base version of `torchjd`
15+
(installed with `pip install torchjd`) much lighter, but it means that users of `UPGrad` and
16+
`DualProj` now have to install the new optional dependency group `quadprog_projector` explicitly
17+
(with e.g. `pip install "torchjd[quadprog_projector]"`).
18+
1119
## [0.11.0] - 2026-05-18
1220

1321
### Changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,11 @@ size). In addition to $\mathcal A_{\text{UPGrad}}$, TorchJD supports
5555
<!-- start installation -->
5656
TorchJD can be installed directly with pip:
5757
```bash
58-
pip install torchjd
58+
pip install "torchjd[quadprog_projector]"
5959
```
6060
<!-- end installation -->
61-
Some aggregators may have additional dependencies. Please refer to the
61+
This includes the dependencies required by UPGrad and DualProj. Some other aggregators may have
62+
additional dependencies. Please refer to the
6263
[installation documentation](https://torchjd.org/stable/installation) for them.
6364

6465
## Usage

docs/source/installation.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77

88
Note that `torchjd` requires Python 3.10, 3.11, 3.12, 3.13 or 3.14 and `torch>=2.0`.
99

10-
Some aggregators (CAGrad and Nash-MTL) have additional dependencies that are not included by default
11-
when installing `torchjd`. To install them, you can use:
12-
```
13-
pip install "torchjd[cagrad]"
14-
```
15-
```
16-
pip install "torchjd[nash_mtl]"
17-
```
10+
Some aggregators have additional dependencies that are not included by default when installing
11+
`torchjd`. The following table lists the optional dependency groups and the aggregators they enable:
12+
13+
Group | Classes | Dependencies | Install command |
14+
|-----|---------|--------------|-----------------|
15+
| `quadprog_projector` | {class}`~torchjd.linalg.QuadprogProjector` (used in {class}`~torchjd.aggregation.UPGrad` and {class}`~torchjd.aggregation.DualProj`) | [numpy](https://github.com/numpy/numpy), [quadprog](https://github.com/quadprog/quadprog), [qpsolvers](https://github.com/qpsolvers/qpsolvers) | `pip install "torchjd[quadprog_projector]"` |
16+
| `cagrad` | {class}`~torchjd.aggregation.CAGrad` | [numpy](https://github.com/numpy/numpy), [cvxpy](https://github.com/cvxpy/cvxpy/) | `pip install "torchjd[cagrad]"` |
17+
| `nash_mtl` | {class}`~torchjd.aggregation.NashMTL` | [numpy](https://github.com/numpy/numpy), [cvxpy](https://github.com/cvxpy/cvxpy/), [ecos](https://github.com/embotech/ecos) | `pip install "torchjd[nash_mtl]"` |
1818

1919
To install `torchjd` with all of its optional dependencies, you can also use:
2020
```

pyproject.toml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ authors = [
1515
requires-python = ">=3.10"
1616
dependencies = [
1717
"torch>=2.3.0", # Problems before 2.4.0, especially with autogram.
18-
"quadprog>=0.1.9, != 0.1.10", # Doesn't work before 0.1.9, 0.1.10 is yanked
19-
"numpy>=1.21.2", # Does not work before 1.21. No python 3.10 wheel before 1.21.2.
20-
"qpsolvers>=1.0.1", # Does not work before 1.0.1
2118
]
2219
classifiers = [
2320
"Development Status :: 4 - Beta",
@@ -93,11 +90,13 @@ test = [
9390
]
9491

9592
plot = [
93+
"numpy>=1.21.2", # Does not work before 1.21. No python 3.10 wheel before 1.21.2.
9694
"plotly[kaleido]>=5.19.0", # Recent version to avoid problems, could be relaxed
9795
"dash>=2.16.0", # Recent version to avoid problems, could be relaxed
9896
"matplotlib>=3.10.0", # Recent version to avoid problems, could be relaxed
9997
]
100-
# Dependency group allowing to easily resolve version of the core dependencies to the lower bound.
98+
# Dependency group allowing to easily resolve version of the recommended dependencies to the lower
99+
# bound.
101100
lower_bounds = [
102101
"torch==2.3.0",
103102
"numpy==1.21.2",
@@ -106,14 +105,24 @@ lower_bounds = [
106105
]
107106

108107
[project.optional-dependencies]
108+
quadprog_projector = [
109+
"numpy>=1.21.2", # Does not work before 1.21. No python 3.10 wheel before 1.21.2.
110+
"quadprog>=0.1.9, != 0.1.10", # Doesn't work before 0.1.9, 0.1.10 is yanked
111+
"qpsolvers>=1.0.1", # Does not work before 1.0.1
112+
]
109113
nash_mtl = [
114+
"numpy>=1.21.2", # Does not work before 1.21. No python 3.10 wheel before 1.21.2.
110115
"cvxpy>=1.3.0", # Could be relaxed
111116
"ecos>=2.0.14", # Does not work before 2.0.14
112117
]
113118
cagrad = [
119+
"numpy>=1.21.2", # Does not work before 1.21. No python 3.10 wheel before 1.21.2.
114120
"cvxpy>=1.3.0", # No Clarabel solver before 1.3.0
115121
]
116122
full = [
123+
"numpy>=1.21.2", # Does not work before 1.21. No python 3.10 wheel before 1.21.2.
124+
"quadprog>=0.1.9, != 0.1.10", # Doesn't work before 0.1.9, 0.1.10 is yanked
125+
"qpsolvers>=1.0.1", # Does not work before 1.0.1
117126
"cvxpy>=1.3.0", # No Clarabel solver before 1.3.0
118127
"ecos>=2.0.14", # Does not work before 2.0.14
119128
]

src/torchjd/_linalg/_dual_cone.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import contextlib
12
from abc import ABC, abstractmethod
23

3-
import numpy as np
44
import torch
5-
from qpsolvers import solve_qp
65
from torch import Tensor
76

7+
from torchjd._mixins import _WithOptionalDeps
8+
9+
with contextlib.suppress(ImportError):
10+
import numpy as np
11+
from qpsolvers import solve_qp
12+
813
from ._gramian import normalize, regularize
914
from ._matrix import PSDMatrix
1015

@@ -49,7 +54,7 @@ def projector_or_default(projector: DualConeProjector | None) -> DualConeProject
4954
return projector
5055

5156

52-
class QuadprogProjector(DualConeProjector):
57+
class QuadprogProjector(_WithOptionalDeps, DualConeProjector):
5358
r"""
5459
Solves the quadratic program defined in :meth:`DualConeProjector.__call__` using the
5560
`quadprog <https://github.com/quadprog/quadprog>`_ QP solver.
@@ -61,12 +66,16 @@ class QuadprogProjector(DualConeProjector):
6166
ensures that it is positive definite.
6267
"""
6368

69+
_REQUIRED_DEPS = ["numpy", "qpsolvers", "quadprog"]
70+
_INSTALL_HINT = 'Install them with: pip install "torchjd[quadprog_projector]"'
71+
6472
def __init__(
6573
self,
6674
*,
6775
norm_eps: float = 0.0001,
6876
reg_eps: float = 0.0001,
6977
) -> None:
78+
super().__init__()
7079
self._norm_eps = norm_eps
7180
self._reg_eps = reg_eps
7281

src/torchjd/_mixins.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from importlib.util import find_spec
2+
from typing import Any
3+
4+
5+
class _WithOptionalDeps:
6+
"""
7+
Mixin that raises :class:`ImportError` at instantiation time if required optional dependencies
8+
are not installed.
9+
10+
Subclasses must define :attr:`_REQUIRED_DEPS` (list of package names to check via
11+
:func:`importlib.util.find_spec`) and :attr:`_INSTALL_HINT` (appended to the error message).
12+
13+
.. warning::
14+
This mixin must appear **first** in the inheritance list so that its :meth:`__init__`
15+
runs before any base class that uses the optional dependencies.
16+
"""
17+
18+
_REQUIRED_DEPS: list[str]
19+
_INSTALL_HINT: str
20+
21+
def __init__(self, *args: Any, **kwargs: Any) -> None:
22+
missing = [name for name in self._REQUIRED_DEPS if find_spec(name) is None]
23+
if len(missing) != 0:
24+
raise ImportError(
25+
f"{self.__class__.__name__} requires {missing} to be installed. "
26+
f"{self._INSTALL_HINT}"
27+
)
28+
super().__init__(*args, **kwargs)

src/torchjd/aggregation/_cagrad.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
import contextlib
22
from typing import cast
33

4-
import numpy as np
54
import torch
65
from torch import Tensor
76

87
from torchjd._linalg import normalize
8+
from torchjd._mixins import _WithOptionalDeps
99
from torchjd.linalg import PSDMatrix
1010

1111
from ._aggregator_bases import GramianWeightedAggregator
12-
from ._mixins import _NonDifferentiable, _WithOptionalDeps
12+
from ._mixins import _NonDifferentiable
1313
from ._weighting_bases import _GramianWeighting
1414

1515
with contextlib.suppress(ImportError):
1616
import cvxpy as cp
17+
import numpy as np
1718

1819

1920
# Non-differentiable: the cvxpy solver operates on numpy arrays, breaking the autograd graph.
2021
class CAGradWeighting(_WithOptionalDeps, _NonDifferentiable, _GramianWeighting):
21-
_REQUIRED_DEPS = ["cvxpy", "clarabel"]
22+
_REQUIRED_DEPS = ["numpy", "cvxpy", "clarabel"]
2223
_INSTALL_HINT = 'Install them with: pip install "torchjd[cagrad]"'
2324
"""
2425
:class:`~torchjd.aggregation.Weighting` [:class:`~torchjd.linalg.PSDMatrix`]

src/torchjd/aggregation/_mixins.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,10 @@
11
from abc import ABC, abstractmethod
2-
from importlib.util import find_spec
32
from typing import Any
43

54
import torch
65
from torch import nn
76

87

9-
class _WithOptionalDeps:
10-
"""
11-
Mixin that raises :class:`ImportError` at instantiation time if required optional dependencies
12-
are not installed.
13-
14-
Subclasses must define :attr:`_REQUIRED_DEPS` (list of package names to check via
15-
:func:`importlib.util.find_spec`) and :attr:`_INSTALL_HINT` (appended to the error message).
16-
17-
.. warning::
18-
This mixin must appear **first** in the inheritance list so that its :meth:`__init__`
19-
runs before any base class that uses the optional dependencies.
20-
"""
21-
22-
_REQUIRED_DEPS: list[str]
23-
_INSTALL_HINT: str
24-
25-
def __init__(self, *args: Any, **kwargs: Any) -> None:
26-
missing = [name for name in self._REQUIRED_DEPS if find_spec(name) is None]
27-
if len(missing) != 0:
28-
raise ImportError(
29-
f"{self.__class__.__name__} requires {missing} to be installed. "
30-
f"{self._INSTALL_HINT}"
31-
)
32-
super().__init__(*args, **kwargs)
33-
34-
358
class Stateful(ABC):
369
"""Mixin adding a reset method."""
3710

src/torchjd/aggregation/_nash_mtl.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,24 @@
55

66
import contextlib
77

8-
import numpy as np
98
import torch
109
from torch import Tensor
1110

12-
from torchjd.aggregation._mixins import Stateful, _NonDifferentiable, _WithOptionalDeps
11+
from torchjd._mixins import _WithOptionalDeps
12+
from torchjd.aggregation._mixins import Stateful, _NonDifferentiable
1313

1414
from ._aggregator_bases import WeightedAggregator
1515
from ._weighting_bases import _MatrixWeighting
1616

1717
with contextlib.suppress(ImportError):
1818
import cvxpy as cp
19+
import numpy as np
1920
from cvxpy import Expression, SolverError
2021

2122

2223
# Non-differentiable: the cvxpy solver operates on numpy arrays, breaking the autograd graph.
2324
class _NashMTLWeighting(_WithOptionalDeps, _NonDifferentiable, Stateful, _MatrixWeighting):
24-
_REQUIRED_DEPS = ["cvxpy", "ecos"]
25+
_REQUIRED_DEPS = ["numpy", "cvxpy", "ecos"]
2526
_INSTALL_HINT = 'Install them with: pip install "torchjd[nash_mtl]"'
2627
"""
2728
:class:`~torchjd.aggregation._mixins.Stateful`

tests/unit/aggregation/test_cagrad.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import pytest
1+
from utils.optional_deps import skip_if_deps_not_installed
22

3-
pytest.importorskip("cvxpy")
4-
pytest.importorskip("clarabel")
3+
from torchjd.aggregation import CAGradWeighting
4+
5+
skip_if_deps_not_installed(CAGradWeighting)
56

67
from contextlib import nullcontext as does_not_raise
78

@@ -11,7 +12,6 @@
1112
from utils.tensors import ones_
1213

1314
from torchjd.aggregation import CAGrad
14-
from torchjd.aggregation._cagrad import CAGradWeighting
1515

1616
from ._asserts import assert_expected_structure, assert_non_conflicting, assert_non_differentiable
1717
from ._inputs import scaled_matrices, typical_matrices

0 commit comments

Comments
 (0)