humancompatible-train 0.3.2__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {humancompatible_train-0.3.2/src/humancompatible_train.egg-info → humancompatible_train-0.4.0}/PKG-INFO +24 -5
  2. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/README.md +2 -2
  3. humancompatible_train-0.4.0/pyproject.toml +56 -0
  4. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/src/humancompatible/train/dual_optim/__init__.py +3 -0
  5. humancompatible_train-0.4.0/src/humancompatible/train/dual_optim/alm.py +333 -0
  6. humancompatible_train-0.4.0/src/humancompatible/train/dual_optim/base.py +502 -0
  7. humancompatible_train-0.4.0/src/humancompatible/train/dual_optim/ialm.py +248 -0
  8. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/src/humancompatible/train/dual_optim/moreau.py +11 -3
  9. humancompatible_train-0.4.0/src/humancompatible/train/dual_optim/nupi.py +231 -0
  10. humancompatible_train-0.4.0/src/humancompatible/train/dual_optim/pbm.py +505 -0
  11. humancompatible_train-0.4.0/src/humancompatible/train/dual_optim/ssg.py +171 -0
  12. humancompatible_train-0.4.0/src/humancompatible/train/fairness/utils/balanced_batch_sampler.py +142 -0
  13. humancompatible_train-0.4.0/src/humancompatible/train/sqp/__init__.py +1 -0
  14. humancompatible_train-0.4.0/src/humancompatible/train/sqp/ghost/__init__.py +28 -0
  15. humancompatible_train-0.4.0/src/humancompatible/train/sqp/ghost/ghost_sqp.py +103 -0
  16. humancompatible_train-0.4.0/src/humancompatible/train/sqp/ghost/mlmc.py +74 -0
  17. humancompatible_train-0.4.0/src/humancompatible/train/sqp/ghost/oracle.py +75 -0
  18. humancompatible_train-0.4.0/src/humancompatible/train/sqp/ghost/sampler.py +54 -0
  19. humancompatible_train-0.4.0/src/humancompatible/train/sqp/ghost/subproblems.py +75 -0
  20. humancompatible_train-0.4.0/src/humancompatible/train/sqp/nonopt/__init__.py +12 -0
  21. humancompatible_train-0.4.0/src/humancompatible/train/sqp/nonopt/direction.py +421 -0
  22. humancompatible_train-0.4.0/src/humancompatible/train/sqp/nonopt/inverse_hessian.py +276 -0
  23. humancompatible_train-0.4.0/src/humancompatible/train/sqp/nonopt/line_search.py +156 -0
  24. humancompatible_train-0.4.0/src/humancompatible/train/sqp/nonopt/optimizer.py +442 -0
  25. humancompatible_train-0.4.0/src/humancompatible/train/sqp/nonopt/point_set.py +79 -0
  26. humancompatible_train-0.4.0/src/humancompatible/train/sqp/nonopt/qp.py +111 -0
  27. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0/src/humancompatible_train.egg-info}/PKG-INFO +24 -5
  28. humancompatible_train-0.4.0/src/humancompatible_train.egg-info/SOURCES.txt +49 -0
  29. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/src/humancompatible_train.egg-info/requires.txt +9 -1
  30. humancompatible_train-0.4.0/tests/test_alm.py +454 -0
  31. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/tests/test_balanced_batch_sampler.py +108 -0
  32. humancompatible_train-0.4.0/tests/test_cooper_parity.py +159 -0
  33. humancompatible_train-0.4.0/tests/test_distributed.py +64 -0
  34. humancompatible_train-0.4.0/tests/test_dual_optimizer_base.py +331 -0
  35. humancompatible_train-0.4.0/tests/test_fairness_problems.py +212 -0
  36. humancompatible_train-0.4.0/tests/test_ghost.py +86 -0
  37. humancompatible_train-0.4.0/tests/test_golden.py +90 -0
  38. humancompatible_train-0.4.0/tests/test_ialm.py +95 -0
  39. humancompatible_train-0.4.0/tests/test_llm_gates.py +461 -0
  40. humancompatible_train-0.4.0/tests/test_nonopt.py +265 -0
  41. humancompatible_train-0.4.0/tests/test_nupi.py +146 -0
  42. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/tests/test_pbm.py +47 -47
  43. humancompatible_train-0.4.0/tests/test_ssg.py +143 -0
  44. humancompatible_train-0.3.2/pyproject.toml +0 -29
  45. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/algorithms/Algorithm.py +0 -25
  46. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/algorithms/__init__.py +0 -8
  47. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/algorithms/ghost.py +0 -254
  48. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/algorithms/optim_wrapper.py +0 -184
  49. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/algorithms/sgd.py +0 -114
  50. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/algorithms/ssl_alm.py +0 -317
  51. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/algorithms/switching_subgradient.py +0 -199
  52. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/algorithms/utils.py +0 -61
  53. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/constraints/__init__.py +0 -15
  54. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/constraints/constraint.py +0 -114
  55. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/constraints/constraint_fns.py +0 -251
  56. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/constraints/torch/__init__.py +0 -1
  57. humancompatible_train-0.3.2/src/humancompatible/train/benchmark/constraints/torch/constraints.py +0 -48
  58. humancompatible_train-0.3.2/src/humancompatible/train/dual_optim/alm.py +0 -369
  59. humancompatible_train-0.3.2/src/humancompatible/train/dual_optim/ialm.py +0 -330
  60. humancompatible_train-0.3.2/src/humancompatible/train/dual_optim/nupi.py +0 -357
  61. humancompatible_train-0.3.2/src/humancompatible/train/dual_optim/pbm.py +0 -503
  62. humancompatible_train-0.3.2/src/humancompatible/train/fairness/__init__.py +0 -0
  63. humancompatible_train-0.3.2/src/humancompatible/train/fairness/utils/balanced_batch_sampler.py +0 -126
  64. humancompatible_train-0.3.2/src/humancompatible/train/optim/PBM.py +0 -571
  65. humancompatible_train-0.3.2/src/humancompatible/train/optim/__init__.py +0 -5
  66. humancompatible_train-0.3.2/src/humancompatible/train/optim/barrier.py +0 -158
  67. humancompatible_train-0.3.2/src/humancompatible/train/optim/ssl_alm.py +0 -236
  68. humancompatible_train-0.3.2/src/humancompatible/train/optim/ssl_alm_adam.py +0 -347
  69. humancompatible_train-0.3.2/src/humancompatible/train/optim/ssl_alm_adam_moment.py +0 -315
  70. humancompatible_train-0.3.2/src/humancompatible/train/optim/ssw.py +0 -156
  71. humancompatible_train-0.3.2/src/humancompatible/train/optim/ssw_barrier.py +0 -236
  72. humancompatible_train-0.3.2/src/humancompatible_train.egg-info/SOURCES.txt +0 -44
  73. humancompatible_train-0.3.2/tests/test_alm.py +0 -90
  74. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/LICENCE.txt +0 -0
  75. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/setup.cfg +0 -0
  76. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/src/humancompatible/train/__init__.py +0 -0
  77. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/src/humancompatible/train/dual_optim/barrier.py +0 -0
  78. {humancompatible_train-0.3.2/src/humancompatible/train/benchmark → humancompatible_train-0.4.0/src/humancompatible/train/fairness}/__init__.py +0 -0
  79. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/src/humancompatible/train/fairness/utils/__init__.py +0 -0
  80. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/src/humancompatible_train.egg-info/dependency_links.txt +0 -0
  81. {humancompatible_train-0.3.2 → humancompatible_train-0.4.0}/src/humancompatible_train.egg-info/top_level.txt +0 -0
@@ -1,10 +1,23 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: humancompatible-train
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: PyTorch-based package for constrained training of neural networks
5
- Author: Gilles Bareilles, Adam Bosak, Jana Lepsova, Jakub Marecek
5
+ Author: Adam Bosak, Gilles Bareilles, Jana Lepsova, Jakub Marecek
6
6
  Author-email: Andrii Kliachkin <kliacand@fel.cvut.cz>
7
7
  Maintainer-email: Andrii Kliachkin <kliacand@fel.cvut.cz>
8
+ License-Expression: Apache-2.0
9
+ Project-URL: Homepage, https://github.com/humancompatible/train
10
+ Project-URL: Repository, https://github.com/humancompatible/train
11
+ Project-URL: Issues, https://github.com/humancompatible/train/issues
12
+ Project-URL: Documentation, https://humancompatible-train.readthedocs.io
13
+ Keywords: constrained-optimization,pytorch,fairness,lagrangian,machine-learning
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Operating System :: OS Independent
8
21
  Requires-Python: >=3.11
9
22
  Description-Content-Type: text/markdown
10
23
  License-File: LICENCE.txt
@@ -23,16 +36,22 @@ Requires-Dist: matplotlib; extra == "benchmark"
23
36
  Requires-Dist: pandas; extra == "benchmark"
24
37
  Requires-Dist: folktables; extra == "benchmark"
25
38
  Requires-Dist: pot; extra == "benchmark"
26
- Requires-Dist: hydra; extra == "benchmark"
39
+ Requires-Dist: hydra-core; extra == "benchmark"
27
40
  Requires-Dist: omegaconf; extra == "benchmark"
41
+ Provides-Extra: paper
42
+ Requires-Dist: hydra-core; extra == "paper"
43
+ Requires-Dist: omegaconf; extra == "paper"
44
+ Requires-Dist: hydra-submitit-launcher; extra == "paper"
28
45
  Provides-Extra: ghost
29
46
  Requires-Dist: qpsolvers; extra == "ghost"
30
47
  Requires-Dist: scipy; extra == "ghost"
48
+ Provides-Extra: compare
49
+ Requires-Dist: cooper-optim>=1.0.1; extra == "compare"
31
50
  Dynamic: license-file
32
51
 
33
52
  # humancompatible-train: a package for constrained machine learning
34
53
 
35
- [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Setup](https://github.com/humancompatible/train/actions/workflows/setup.yml/badge.svg)](https://github.com/humancompatible/train/actions/workflows/setup.yml)
54
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Setup](https://github.com/humancompatible/train/actions/workflows/setup.yml/badge.svg)](https://github.com/humancompatible/train/actions/workflows/setup.yml) [![docs](https://app.readthedocs.org/projects/humancompatible-train/badge/?version=latest)](https://humancompatible-train.readthedocs.io/en/latest/?badge=latest)
36
55
 
37
56
  The toolkit implements algorithms for constrained training of neural networks based on PyTorch, and inspired by PyTorch's API.
38
57
  <!-- , as well as a tool to compare stochastic-constrained stochastic optimization algorithms on a _fair learning_ task in the `experiments` folder. -->
@@ -61,7 +80,7 @@ The only dependencies of this package are `numpy` and `torch`.
61
80
 
62
81
  ## Using the toolkit
63
82
 
64
- The toolkit implements algorithms for constrained training of neural networks based on PyTorch.
83
+ The toolkit implements algorithms for constrained training of neural networks based on PyTorch. For the documentation, please visit [our Read the Docs page!](https://humancompatible-train.readthedocs.io?version=latest)
65
84
 
66
85
  The algorithms are intended for use in tandem with classic PyTorch optimizers, calculating the Lagrangian and keeping track of the dual variables.
67
86
 
@@ -1,6 +1,6 @@
1
1
  # humancompatible-train: a package for constrained machine learning
2
2
 
3
- [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Setup](https://github.com/humancompatible/train/actions/workflows/setup.yml/badge.svg)](https://github.com/humancompatible/train/actions/workflows/setup.yml)
3
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Setup](https://github.com/humancompatible/train/actions/workflows/setup.yml/badge.svg)](https://github.com/humancompatible/train/actions/workflows/setup.yml) [![docs](https://app.readthedocs.org/projects/humancompatible-train/badge/?version=latest)](https://humancompatible-train.readthedocs.io/en/latest/?badge=latest)
4
4
 
5
5
  The toolkit implements algorithms for constrained training of neural networks based on PyTorch, and inspired by PyTorch's API.
6
6
  <!-- , as well as a tool to compare stochastic-constrained stochastic optimization algorithms on a _fair learning_ task in the `experiments` folder. -->
@@ -29,7 +29,7 @@ The only dependencies of this package are `numpy` and `torch`.
29
29
 
30
30
  ## Using the toolkit
31
31
 
32
- The toolkit implements algorithms for constrained training of neural networks based on PyTorch.
32
+ The toolkit implements algorithms for constrained training of neural networks based on PyTorch. For the documentation, please visit [our Read the Docs page!](https://humancompatible-train.readthedocs.io?version=latest)
33
33
 
34
34
  The algorithms are intended for use in tandem with classic PyTorch optimizers, calculating the Lagrangian and keeping track of the dual variables.
35
35
 
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "humancompatible-train"
7
+ version = "0.4.0"
8
+ dependencies = [
9
+ "torch",
10
+ "numpy",
11
+ ]
12
+ requires-python = ">= 3.11"
13
+ authors = [
14
+ {name = "Andrii Kliachkin", email = "kliacand@fel.cvut.cz"},
15
+ {name = "Adam Bosak"},
16
+ {name = "Gilles Bareilles"},
17
+ {name = "Jana Lepsova"},
18
+ {name = "Jakub Marecek"},
19
+ ]
20
+ maintainers = [
21
+ {name = "Andrii Kliachkin", email = "kliacand@fel.cvut.cz"}
22
+ ]
23
+ description = "PyTorch-based package for constrained training of neural networks"
24
+ readme = "README.md"
25
+ license = "Apache-2.0"
26
+ keywords = ["constrained-optimization", "pytorch", "fairness", "lagrangian", "machine-learning"]
27
+ classifiers = [
28
+ "Development Status :: 3 - Alpha",
29
+ "Intended Audience :: Science/Research",
30
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Operating System :: OS Independent",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/humancompatible/train"
39
+ Repository = "https://github.com/humancompatible/train"
40
+ Issues = "https://github.com/humancompatible/train/issues"
41
+ Documentation = "https://humancompatible-train.readthedocs.io"
42
+
43
+ [project.optional-dependencies]
44
+ examples = ["ipykernel", "ipympl", "fairret", "folktables", "scikit-learn", "matplotlib"]
45
+ # `hydra-core` is the distribution this repo imports. `hydra` on PyPI is an unrelated 2016
46
+ # project, which is what the lockfile had been resolving to (uv.lock pinned Hydra 2.5).
47
+ benchmark = ["fairret", "matplotlib", "pandas", "folktables", "pot", "hydra-core", "omegaconf"]
48
+ # The hyperparameter sweeps in paper/tune.py. The submitit launcher is what `-m
49
+ # hydra/launcher=slurm` needs; the basic sweeper covers the local case with no plugin.
50
+ # hydra-optuna-sweeper is deliberately absent: its 1.2.0 release requires optuna<3.0.0.
51
+ paper = ["hydra-core", "omegaconf", "hydra-submitit-launcher"]
52
+ ghost = ["qpsolvers", "scipy"]
53
+ # Cross-validation against another library (paper/e0/e_cooper.py,
54
+ # tests/test_cooper_parity.py). Pinned to a floor because E0e's claims are about
55
+ # specific update orderings in Cooper's optimizer classes.
56
+ compare = ["cooper-optim>=1.0.1"]
@@ -1,4 +1,7 @@
1
+ from .base import DualOptimizer
1
2
  from .alm import ALM
2
3
  from .ialm import iALM
3
4
  from .pbm import PBM
5
+ from .nupi import nuPI
6
+ from .ssg import SSG
4
7
  from .moreau import MoreauEnvelope
@@ -0,0 +1,333 @@
1
+ import torch
2
+ import torch.distributed as dist
3
+ from typing import Any, Optional, Tuple
4
+ from torch import Tensor
5
+
6
+ from .base import DualOptimizer
7
+
8
+ # cite: Stochastic Smoothed Primal-Dual Algorithms for Nonconvex Optimization with Linear Inequality Constraints
9
+ # https://arxiv.org/pdf/2504.07607
10
+
11
+ AUGMENTATIONS = ("quadratic", "hpr")
12
+
13
+
14
+ class ALM(DualOptimizer):
15
+ def __init__(
16
+ self,
17
+ m: int = None,
18
+ lr: float = 0.01,
19
+ init_duals: float | Tensor = None,
20
+ penalty: float = 1.0,
21
+ *,
22
+ dual_range: Tuple[float, float] = (-100.0, 100.0),
23
+ momentum: float = 0.0,
24
+ dampening: Optional[float] = None,
25
+ is_ineq: bool = False,
26
+ restart: bool = False,
27
+ augmentation: str = "hpr",
28
+ device=None,
29
+ process_group: Optional[dist.ProcessGroup] = None,
30
+ ) -> None:
31
+
32
+ if augmentation not in AUGMENTATIONS:
33
+ raise ValueError(
34
+ f"Unknown augmentation: {augmentation!r}; expected one of "
35
+ f"{AUGMENTATIONS}"
36
+ )
37
+ if augmentation == "hpr" and penalty <= 0:
38
+ raise ValueError(
39
+ f"The 'hpr' augmentation divides by the penalty, so it requires "
40
+ f"penalty > 0; got {penalty}"
41
+ )
42
+
43
+ self.penalty = penalty
44
+ self.augmentation = augmentation
45
+ params, settings = self._make_group(
46
+ m, lr, momentum, dampening, init_duals, dual_range, is_ineq, restart, device
47
+ )
48
+ super().__init__(
49
+ [{"params": params, **settings}],
50
+ self._scalar_defaults(settings),
51
+ process_group=process_group,
52
+ )
53
+
54
+ @classmethod
55
+ def _make_group(
56
+ cls,
57
+ m: int = None,
58
+ lr: float = None,
59
+ momentum: float = None,
60
+ dampening: float = None,
61
+ init_duals: float | Tensor = None,
62
+ dual_range: Tuple[float, float] = None,
63
+ is_ineq: bool = None,
64
+ restart: bool = None,
65
+ device=None,
66
+ ):
67
+ if momentum is not None and (momentum < 0 or momentum > 1):
68
+ raise ValueError(f"momentum must be within [0,1]; got {momentum}")
69
+
70
+ # Default dampening to momentum (EMA) when unset and momentum > 0; else 0.
71
+ if dampening is None:
72
+ dampening = momentum if (momentum is not None and momentum > 0) else 0.0
73
+
74
+ if not isinstance(restart, bool):
75
+ raise ValueError(
76
+ f"Expected a Boolean value for restart, got {type(restart)}"
77
+ )
78
+
79
+ duals, settings = cls._base_group(
80
+ m, init_duals, dual_range, is_ineq, device
81
+ )
82
+ settings.update(
83
+ {
84
+ "lr": lr,
85
+ "momentum": momentum,
86
+ "dampening": dampening,
87
+ "momentum_buffer": torch.zeros_like(
88
+ duals.data, requires_grad=False, device=device
89
+ ),
90
+ "restart": restart,
91
+ }
92
+ )
93
+ return [duals], cls._drop_none(settings)
94
+
95
+ def add_constraint_group(
96
+ self,
97
+ m: int,
98
+ lr: float = None,
99
+ momentum: float = None,
100
+ dampening: Optional[float] = None,
101
+ init_duals: Tensor = None,
102
+ dual_range: tuple[float, float] = None,
103
+ is_ineq: bool = False,
104
+ restart: bool = False,
105
+ device = None,
106
+ *,
107
+ name: str = None,
108
+ bound: float = None,
109
+ ) -> None:
110
+ """
111
+ Allows to add a group of dual variables with separate initial values and learning rates.
112
+
113
+ :param m: Size of group (number of dual variables to add)
114
+ :type m: int
115
+ :param lr: Dual variable update rate.
116
+ :type lr: float
117
+ :param momentum: Momentum/Smoothing factor for dual variables. Equivalent to SGD momentum. Set to `0` to disable.
118
+ :type momentum: float
119
+ :param dampening: Dampening for momentum. Equivalent to SGD dampening. Set to `0` to disable.
120
+ :type dampening: float
121
+ :param init_duals: Initial values for the new dual variables. Defaults to the value set when creating the optimizer.
122
+ :type init_duals: Tensor
123
+ :param dual_range: After each dual update, the dual variables will be clamped to this range.
124
+ :type dual_range: Tuple[float, float]
125
+ :param is_ineq: Whether to treat the constraints as equality or inequality. If`True`, dual variables will be relaxed on strict satisfaction and lower-bounded by `max(dual_range[0], 0)`.
126
+ :type is_ineq: bool
127
+ :param restart: Whether to set the dual variables to zero immediately on strict satisfaction of corresponding constraints. Not recommended for stochastic constraints.
128
+ :type restart: bool
129
+ :param name: Name for this group, used when passing constraints as a mapping. Defaults to `group<k>`.
130
+ :type name: str
131
+ :param bound: Right-hand side of this group's constraints, if any. Only used by :meth:`violation`.
132
+ :type bound: float
133
+
134
+ .. note::
135
+ Parameters here will default to values set when initializing the dual optimizer.
136
+
137
+ """
138
+ params, settings = self._make_group(
139
+ m, lr, momentum, dampening, init_duals, dual_range, is_ineq, restart, device
140
+ )
141
+ if bound is not None:
142
+ settings["bound"] = bound
143
+ if name is not None:
144
+ settings["name"] = name
145
+ self.add_param_group({"params": params, **settings})
146
+
147
+ # ------------------------------------------------------------------ #
148
+ # hooks
149
+ # ------------------------------------------------------------------ #
150
+
151
+ def _ascent_direction(self, group: dict[str, Any], c: Tensor) -> Tensor:
152
+ """This group's dual ascent direction, i.e. the surrogate's gradient in the duals.
153
+ """
154
+ if self.augmentation == "quadratic" or not group.get("is_ineq"):
155
+ return c
156
+ duals = group["params"][0]
157
+ return (torch.clamp(duals + self.penalty * c, min=0.0) - duals) / self.penalty
158
+
159
+ def _dual_update(self, group: dict[str, Any], c: Tensor) -> None:
160
+ momentum = group.get("momentum", 0.0)
161
+ buffer = group["momentum_buffer"]
162
+
163
+ d = self._ascent_direction(group, c)
164
+
165
+ if momentum > 0:
166
+ _update_c_buffers(d, momentum, group.get("dampening", 0.0), buffer)
167
+
168
+ _update_duals(
169
+ group["params"][0],
170
+ buffer if momentum > 0 else d,
171
+ group["lr"],
172
+ group.get("restart"),
173
+ raw_constraints=c,
174
+ )
175
+
176
+ def _add_constraint_contributions(
177
+ self, lagrangian: Tensor, group: dict[str, Any], snapshot: Any, c: Tensor
178
+ ) -> None:
179
+ if self.augmentation == "quadratic":
180
+ lagrangian.add_(snapshot @ c)
181
+ return
182
+
183
+ rho = self.penalty
184
+ if group.get("is_ineq"):
185
+ with torch.no_grad():
186
+ lam_star = torch.clamp(snapshot + rho * c, min=0.0)
187
+ step = lam_star - snapshot
188
+ lagrangian.add_(lam_star @ c)
189
+ lagrangian.add_(step.dot(step), alpha=-1.0 / (2 * rho))
190
+ else:
191
+ lagrangian.add_(snapshot @ c)
192
+ lagrangian.add_(0.5 * rho * torch.dot(c, c))
193
+
194
+ def _add_global_terms(self, lagrangian: Tensor, constraints: Tensor) -> None:
195
+ # Under HPR the quadratic already sits inside each group's term.
196
+ if self.augmentation == "hpr" or self.penalty == 0:
197
+ return
198
+ # Violations only for inequality groups; see _penalty_constraints.
199
+ c = self._penalty_constraints(constraints)
200
+ lagrangian.add_(0.5 * self.penalty * torch.dot(c, c))
201
+
202
+ def _extra_state(self) -> dict[str, Any]:
203
+ return {"penalty": self.penalty, "augmentation": self.augmentation}
204
+
205
+ def _load_extra_state(self, state: dict[str, Any]) -> None:
206
+ self.penalty = state["penalty"]
207
+ # Checkpoints written before the augmentation option lack the key.
208
+ self.augmentation = state.get("augmentation", "quadratic")
209
+
210
+
211
+ def _update_c_buffers(
212
+ constraints: Tensor,
213
+ momentum: float,
214
+ dampening: float,
215
+ buffer: Tensor,
216
+ ) -> None:
217
+ """Update the constraint buffer with momentum."""
218
+ buffer.mul_(momentum).add_(constraints, alpha=1 - dampening)
219
+
220
+
221
+ def _update_duals(
222
+ duals: Tensor,
223
+ buffer: Tensor,
224
+ lr: float,
225
+ restart: bool,
226
+ raw_constraints: Tensor = None,
227
+ ) -> None:
228
+ """Update duals using the buffered constraint gradients."""
229
+ duals.add_(buffer, alpha=lr)
230
+ if restart:
231
+ # Use raw constraints (not the EMA buffer) to check satisfaction.
232
+ check = raw_constraints if raw_constraints is not None else buffer
233
+ duals.masked_fill_(check < 0, 0.0)
234
+
235
+
236
+
237
+ ALM.__doc__ = (
238
+
239
+ # \textbf{input}: \gamma \text{ (lr) }, \pmb{\lambda}_t \text{ (dual variables, created by method) }, \\
240
+ # \mathbf{c}(\theta) \text{ (constraints) }, f(\theta) \text{ (objective) }, \rho \text{ (penalty coefficient) } \\
241
+ r"""
242
+ A Dual Optimizer that works on the dual maximization tasks according to the (Augmented) Lagrangian rule. Creates and updates dual variables. Reference: https://doi.org/10.48550/arXiv.2504.07607
243
+
244
+ .. math::
245
+
246
+ \pmb{\lambda}_{t+1} & \leftarrow \pmb{\lambda}_t + \gamma \mathbf{c}_t(\theta_{t})
247
+
248
+ \mathcal{L}_{t+1} & \leftarrow f_t(\theta_{t}) + \pmb{\lambda}_{t+1}^T \mathbf{c}_t(\theta_{t}) + \frac{\rho}{2} \| \mathbf{c}_t(\theta_{t}) \|^2_2
249
+
250
+ For constraint groups registered with ``is_ineq=True`` the quadratic term acts
251
+ on the violation, :math:`\frac{\rho}{2} \| [\mathbf{c}_t(\theta_t)]_+ \|^2_2`,
252
+ since penalising the raw value of an inequality constraint would also penalise
253
+ being strictly feasible. The linear term and the dual update always use the raw
254
+ values.
255
+
256
+ Setting ``augmentation="hpr"`` switches to the Hestenes--Powell--Rockafellar
257
+ augmentation, in which the linear and quadratic terms are replaced by a single
258
+ expression per group,
259
+
260
+ .. math::
261
+
262
+ \mathcal{L}_{t+1} \leftarrow f_t(\theta_t) + \frac{1}{2\rho} \left[
263
+ \big\| \big[ \pmb{\lambda}_{t+1} + \rho \, \mathbf{c}_t(\theta_t) \big]_+ \big\|^2_2
264
+ - \| \pmb{\lambda}_{t+1} \|^2_2 \right]
265
+
266
+ \pmb{\lambda}_{t+1} \leftarrow \left( 1 - \tfrac{\gamma}{\rho} \right) \pmb{\lambda}_t
267
+ + \tfrac{\gamma}{\rho} \big[ \pmb{\lambda}_t + \rho \, \mathbf{c}_t(\theta_t) \big]_+
268
+
269
+ for inequality groups, the dual update again being gradient ascent on the
270
+ surrogate. Writing :math:`\sigma = \pmb{\lambda} + \rho \mathbf{c}` for the trial
271
+ multiplier, the whole difference from the default is where the clamp sits: the
272
+ weight the surrogate puts on :math:`\partial \mathbf{c} / \partial \theta` is
273
+ :math:`\max(\pmb{\lambda}, \sigma)` for ``"quadratic"`` but
274
+ :math:`\max(0, \sigma)` for ``"hpr"``. The default therefore never lets a
275
+ multiplier's pull on the primal step fall below :math:`\pmb{\lambda}` however
276
+ feasible the constraint is, whereas HPR switches that pull off entirely once
277
+ :math:`\sigma \le 0`. Three practical consequences:
278
+
279
+ * HPR is a **no-op for equality groups**, where
280
+ :math:`\frac{1}{2\rho}(\|\pmb{\mu} + \rho \mathbf{h}\|^2 - \|\pmb{\mu}\|^2)
281
+ = \pmb{\mu}^T \mathbf{h} + \frac{\rho}{2}\|\mathbf{h}\|^2` identically and the
282
+ dual update is unchanged.
283
+ * At ``lr == penalty`` both modes perform the *same* dual update
284
+ :math:`\pmb{\lambda}_{t+1} = [\pmb{\lambda}_t + \rho \mathbf{c}_t]_+` -- the
285
+ default reaches it via the non-negativity clamp -- so at that setting the choice
286
+ is purely a primal-side one. ``restart=True`` is also less necessary under HPR,
287
+ since an inactive multiplier stops influencing the primal step while still
288
+ nonzero.
289
+ * The clamp inside the dual update makes it a nonlinear function of the constraint
290
+ estimate, so with *stochastic* constraints HPR biases the multipliers upward
291
+ (Jensen), i.e. toward feasibility. The default's dual update is linear in the
292
+ estimate and carries no such bias.
293
+
294
+ :param m: Number of constraints (determines the number of dual variables to create)
295
+ :type m: int
296
+ :param lr: Dual variable update rate.
297
+ :type lr: float
298
+ :param init_duals: Initial values for the new dual variables. Defaults to 0 for all.
299
+ :type init_duals: float | Tensor
300
+ :param penalty: Augmented Lagrangian penalty parameter. Defaults to`1.`
301
+ :type penalty: float
302
+ :param dual_range: Safeguarding range for dual variables; they will be`clamp`-ed to this range.
303
+ :type dual_range: Tuple[float, float]
304
+ :param momentum: Momentum/Smoothing factor for dual variables. Equivalent to SGD momentum. Set to `0` to disable.
305
+ :type momentum: float
306
+ :param dampening: Dampening for momentum. Equivalent to SGD dampening. Set to `0` to disable.
307
+ :type dampening: float
308
+ :param is_ineq: Whether to treat the constraints as equality or inequality. If`True`, dual variables will be decreased on strict satisfaction and lower-bounded by `max(dual_range[0], 0)`.
309
+ :type is_ineq: bool
310
+ :param restart: Whether to set the dual variables to zero immediately on strict satisfaction of corresponding constraints. Not recommended for stochastic constraints.
311
+ :type restart: bool
312
+ :param ctol: Reserved for a constraint tolerance allowing tiny violations to account for noise. Accepted for API stability but **currently unused** by the dual update.
313
+ :type ctol: float
314
+ :param augmentation: Which augmentation to form, ``"quadratic"`` (default) or ``"hpr"``. The latter requires `penalty > 0` and, on inequality groups, replaces the linear-plus-quadratic terms by the Hestenes-Powell-Rockafellar expression above.
315
+ :type augmentation: str
316
+ :param process_group: Distributed process group for DDP. When set, constraint values are averaged across all workers via ``dist.all_reduce`` before each dual update, keeping dual variables consistent across replicas. Defaults to ``None`` (no synchronization).
317
+ :type process_group: dist.ProcessGroup, optional
318
+
319
+ .. note::
320
+ Constraint values may be passed to :meth:`forward` / :meth:`update` /
321
+ :meth:`forward_update` as a flat tensor, as one tensor per constraint
322
+ group, or as a mapping from group name to tensor. See
323
+ :meth:`~humancompatible.train.dual_optim.base.DualOptimizer._gather_constraints`.
324
+
325
+ .. note::
326
+ The HPR term also exists in this module as
327
+ :func:`~humancompatible.train.dual_optim.barrier.augmented_lagrangian`, which
328
+ :class:`~.pbm.PBM` reproduces with ``pbf="augmented_lagrangian"`` and
329
+ per-coordinate penalties :math:`p = \lambda / \rho`. That parametrisation is not
330
+ reused here because it divides by :math:`\lambda` and so breaks at
331
+ :math:`\lambda = 0`.
332
+ """
333
+ )