fame-qp 0.2.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.
- fame_qp-0.2.0/LICENSE +21 -0
- fame_qp-0.2.0/PKG-INFO +196 -0
- fame_qp-0.2.0/README.md +158 -0
- fame_qp-0.2.0/README_pypi.md +176 -0
- fame_qp-0.2.0/fame_qp/__init__.py +29 -0
- fame_qp-0.2.0/fame_qp/calibration.py +371 -0
- fame_qp-0.2.0/fame_qp/kernels.py +391 -0
- fame_qp-0.2.0/fame_qp/mpc.py +163 -0
- fame_qp-0.2.0/fame_qp/operator.py +126 -0
- fame_qp-0.2.0/fame_qp/problem.py +171 -0
- fame_qp-0.2.0/fame_qp/scaling.py +89 -0
- fame_qp-0.2.0/fame_qp/solver.py +682 -0
- fame_qp-0.2.0/fame_qp/utils/__init__.py +16 -0
- fame_qp-0.2.0/fame_qp/utils/random_lmpc.py +155 -0
- fame_qp-0.2.0/fame_qp/utils/random_qp.py +45 -0
- fame_qp-0.2.0/fame_qp/warmstart.py +49 -0
- fame_qp-0.2.0/fame_qp.egg-info/PKG-INFO +196 -0
- fame_qp-0.2.0/fame_qp.egg-info/SOURCES.txt +23 -0
- fame_qp-0.2.0/fame_qp.egg-info/dependency_links.txt +1 -0
- fame_qp-0.2.0/fame_qp.egg-info/requires.txt +10 -0
- fame_qp-0.2.0/fame_qp.egg-info/top_level.txt +1 -0
- fame_qp-0.2.0/pyproject.toml +35 -0
- fame_qp-0.2.0/setup.cfg +4 -0
- fame_qp-0.2.0/tests/test_solver.py +305 -0
- fame_qp-0.2.0/tests/test_utils.py +51 -0
fame_qp-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Santosh Mohan Rajkumar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fame_qp-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fame-qp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: GPU-accelerated QP solver for real-time model predictive control
|
|
5
|
+
Author: Santosh Mohan Rajkumar, Debdipta Goswami, Stephanie Stockar
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/santoshrajkumar/fame-qp
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: torch>=2.1
|
|
13
|
+
Requires-Dist: triton
|
|
14
|
+
Provides-Extra: mpc
|
|
15
|
+
Requires-Dist: scipy; extra == "mpc"
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest; extra == "test"
|
|
18
|
+
Requires-Dist: scipy; extra == "test"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# fame_qp — GPU QP solver for MPC with automatic execution mode
|
|
22
|
+
|
|
23
|
+
Paper: [FAME-QP: A GPU-Accelerated Quadratic-Programming Solver for Real-Time Model Predictive Control](https://arxiv.org/abs/2611.13766) (arXiv:2611.13766)
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
min_z ½ zᵀPz + qᵀz s.t. l ≤ G z ≤ h (P ∈ S₊ⁿ, G ∈ ℝ^{m×n})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install fame-qp # requires Linux, a CUDA GPU, PyTorch and Triton
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from fame_qp import QP, Solver
|
|
39
|
+
qp = QP.create(P, q, G, l, h, device="cuda") # G=None means G = I
|
|
40
|
+
sol = Solver(qp, eps_abs=1e-4, eps_rel=1e-4) # calibrates / selects at setup
|
|
41
|
+
res = sol.solve()
|
|
42
|
+
res.z, res.s, res.lam, res.status, res.info["mode"], res.info["precision"]
|
|
43
|
+
|
|
44
|
+
sol.update(q=q2, l=l2, h=h2) # MPC loop: last iterate and rho carry over
|
|
45
|
+
sol.update_matrices(P=P2, G=G2) # same structure; bank re-inverted in place
|
|
46
|
+
sol.warm_start_shifted([(0, n_u, N)], # z = col(u_0..u_{N-1})
|
|
47
|
+
[(0, n_b, N), (n_b * N, n_u, N)]) # row groups, by stage
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Shifted warm start.** In receding-horizon control the next QP is the last one
|
|
51
|
+
moved one stage along the horizon, so the best starting point is the last
|
|
52
|
+
solution shifted by one stage (stage k+1 becomes stage k, the last stage is
|
|
53
|
+
held). `warm_start_shifted(z_segments, lam_segments)` does this for the primal
|
|
54
|
+
and the duals; a segment `(start, size, count)` is `count` stages of `size`
|
|
55
|
+
entries. Call it after `update` / `update_matrices`: the copy `s` is rebuilt
|
|
56
|
+
from the current `G`, `l`, `h`. `MPCController` uses it by default
|
|
57
|
+
(`shift=True`, segments from `LinearMPC.stage_segments()`).
|
|
58
|
+
|
|
59
|
+
LMPC (condensed, `G = [𝓑; I]`, optional LQR pre-stabilisation, sparse form for references):
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from fame_qp.mpc import LinearMPC
|
|
63
|
+
mpc = LinearMPC(A, B, Q, R, Q_N, N=40, u_lo=-1, u_hi=1, x_lo=-2, x_hi=2)
|
|
64
|
+
ctrl = mpc.controller(x_hat)
|
|
65
|
+
u0 = ctrl.step(x_hat)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Benchmark problems
|
|
69
|
+
|
|
70
|
+
`fame_qp.utils` generates the random problems used in the paper, seeded as in ReLU-QP:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from fame_qp.utils import random_qp, random_lmpc
|
|
74
|
+
|
|
75
|
+
qp = QP.create(**random_qp(n=400, seed=0)) # n/4 dense equalities, n/4 one-sided inequalities
|
|
76
|
+
|
|
77
|
+
prob = random_lmpc(n_u=10, N=40) # n_x = 3 n_u, LQR-preconditioned condensed QP
|
|
78
|
+
x = prob.initial_state() # paper's initial-state scale for n_u
|
|
79
|
+
sol = Solver(prob.qp(x), precision="float64", eps_abs=1e-3, eps_rel=1e-3)
|
|
80
|
+
u0 = prob.first_input(sol.solve().z.cpu().numpy(), x)
|
|
81
|
+
x = prob.step(x, u0)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Example
|
|
85
|
+
|
|
86
|
+
[`examples/demo.py`](https://github.com/santoshrajkumar/fame-qp/blob/main/examples/demo.py) solves a random QP with n = 1000, runs
|
|
87
|
+
closed-loop MPC on a random linear system, and runs the same MPC with A and B
|
|
88
|
+
changing at every step, using `update_matrices`:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python examples/demo.py
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Notation in the code
|
|
95
|
+
|
|
96
|
+
| Symbol | Code | Meaning |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| z, P, q, G, l, h | `z, P, q, G, l, h` | the QP above |
|
|
99
|
+
| s = G z | `s` | ADMM copy of the constrained quantities |
|
|
100
|
+
| λ | `lam` | Lagrange multiplier of l ≤ G z ≤ h (dual of s = G z); `lambda` is reserved in Python |
|
|
101
|
+
| ρ, σ, α | `rho, sigma, alpha` | penalty, regularisation, relaxation |
|
|
102
|
+
| K = P + σI + Gᵀdiag(ρ)G, M = K⁻¹ | | pre-inverted for a bank of ρ |
|
|
103
|
+
| diag(d), C | `d, C` | diagonal rows and dense rows of G (n_d = n or 0, m_c rows) |
|
|
104
|
+
| 𝒜, 𝓑, 𝒬, ℛ | `Acal, Bcal, Qcal, Rcal` | condensing matrices (`mpc.py`) |
|
|
105
|
+
|
|
106
|
+
The ADMM paragraph of the paper still uses x, z, A, u for z, s, G, h.
|
|
107
|
+
|
|
108
|
+
Structures, detected from G: **box** `G = diag(d)`; **split** `G = [C; diag(d)]`
|
|
109
|
+
or `[diag(d); C]`; **general** otherwise (`C = G`).
|
|
110
|
+
|
|
111
|
+
## Execution modes
|
|
112
|
+
|
|
113
|
+
| Mode | Role of the CUDA graph |
|
|
114
|
+
|---|---|
|
|
115
|
+
| **Persistent-kernel solve** (`persistent`) | Not used for looping: the whole solve is one kernel launch (box structure, tile within the register file). A one-node graph only cuts the launch cost; ρ adapts between launches of `persistent_budget` iterations |
|
|
116
|
+
| **Single-kernel iteration** (`single_kernel`) | Replays a block of `check_interval` iterations, one kernel each, then the on-device stopping test |
|
|
117
|
+
| **Two-kernel iteration** (`two_kernel`) | Replays a block of iterations, two dependent kernels each, then the on-device stopping test |
|
|
118
|
+
|
|
119
|
+
Operators (`operator.py`): single-kernel `W = S M Sᵀ`, `S = [I; C]`, size (n+m_c)²;
|
|
120
|
+
two-kernel `[M, M Cᵀ]` plus `C`, n(n+2m_c) values. They differ by exactly m_c²
|
|
121
|
+
values per iteration, against one kernel launch.
|
|
122
|
+
|
|
123
|
+
## Automatic selection (`mode="auto"`, default)
|
|
124
|
+
|
|
125
|
+
1. **Calibration, once per device and precision** (`calibration.py`, ~1 s with
|
|
126
|
+
Triton's compile cache warm): graph-node cost τ, host-read cost, streaming
|
|
127
|
+
time vs operator size (captures L2 vs DRAM), and for the persistent kernel
|
|
128
|
+
the registers/spills reported by the compiler per tile, giving the largest
|
|
129
|
+
spill-free tile on this GPU.
|
|
130
|
+
2. **Setup test, once per problem shape**: predict single vs two kernels from
|
|
131
|
+
the calibration, drop candidates > 1.5× the best prediction or whose bank
|
|
132
|
+
does not fit in memory, time the rest on placeholder operators of the real
|
|
133
|
+
shape (tiles tuned), keep the single kernel unless two kernels win by > 10 %.
|
|
134
|
+
3. **Persistent vs graph replay**: predicted solve time from the iteration count
|
|
135
|
+
of the ρ probe. If the persistent mode wins, its starting ρ is re-probed for
|
|
136
|
+
its schedule (ρ can only change between launches) and the comparison redone.
|
|
137
|
+
|
|
138
|
+
Force a mode with `Solver(qp, mode="persistent" | "single_kernel" | "two_kernel")`.
|
|
139
|
+
|
|
140
|
+
## Precision
|
|
141
|
+
|
|
142
|
+
`precision="float32"` (default), `"float64"`, or `"auto"` (optional, two stages):
|
|
143
|
+
|
|
144
|
+
1. κ(K) by power iterations on K and K⁻¹ (float64). `u₃₂κ ≤ 0.01ε` → float32;
|
|
145
|
+
`u₃₂κ ≥ ε` → float64; otherwise stage 2.
|
|
146
|
+
2. Probe in both precisions; drop one that does not reach ε; take the shorter
|
|
147
|
+
predicted solve, preferring float64 within `precision_margin` (20 %).
|
|
148
|
+
|
|
149
|
+
Mode is selected per precision (float64 changes the persistent limit and the
|
|
150
|
+
crossover). The bank is always inverted in float64.
|
|
151
|
+
|
|
152
|
+
## Penalty selection
|
|
153
|
+
|
|
154
|
+
Probe trials adapt ρ as the solve does and record when the test passes at
|
|
155
|
+
`probe_tighten`×, √`probe_tighten`× and 1× the tolerance; candidates are compared
|
|
156
|
+
at the tightest level some candidate reached. The bank is re-centred when the
|
|
157
|
+
best start is at an end. In the split structure γ = ρ_dense/ρ_diag is searched
|
|
158
|
+
at the probed ρ, then ρ is probed again.
|
|
159
|
+
|
|
160
|
+
## Cache
|
|
161
|
+
|
|
162
|
+
`$FAME_QP_CACHE`, else `$XDG_CACHE_HOME/fame_qp`, else `~/.cache/fame_qp`:
|
|
163
|
+
`calibration.json` (per device: GPU, compute capability, driver, CUDA, Triton,
|
|
164
|
+
package version) and `modes.json` (per device, precision and problem shape).
|
|
165
|
+
Read once per process at the first setup; never touched by `solve`. Atomic,
|
|
166
|
+
file-locked writes; in memory only if the directory is not writable.
|
|
167
|
+
`fame_qp.clear_cache()` forgets everything; `Settings(retune=True)` re-measures.
|
|
168
|
+
|
|
169
|
+
## Tests
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest -q tests # 48 tests
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Reproducing the paper
|
|
176
|
+
|
|
177
|
+
[`quick_demo.ipynb`](https://github.com/santoshrajkumar/fame-qp/blob/main/quick_demo.ipynb) regenerates the figures and tables of the
|
|
178
|
+
experimental results from the saved benchmark results, and runs short versions
|
|
179
|
+
of the experiments live on your GPU. The benchmark
|
|
180
|
+
scripts and the setup of the baselines are in [`experiments/`](https://github.com/santoshrajkumar/fame-qp/blob/main/experiments/README.md).
|
|
181
|
+
|
|
182
|
+
## Citation
|
|
183
|
+
|
|
184
|
+
If you use FAME-QP in your research, please cite:
|
|
185
|
+
|
|
186
|
+
```bibtex
|
|
187
|
+
@misc{rajkumar2026fameqp,
|
|
188
|
+
title = {{FAME-QP}: A {GPU}-Accelerated Quadratic-Programming Solver for Real-Time Model Predictive Control},
|
|
189
|
+
author = {Rajkumar, Santosh Mohan and Goswami, Debdipta and Stockar, Stephanie},
|
|
190
|
+
year = {2026},
|
|
191
|
+
eprint = {2611.13766},
|
|
192
|
+
archivePrefix = {arXiv},
|
|
193
|
+
primaryClass = {cs.RO},
|
|
194
|
+
url = {https://arxiv.org/abs/2611.13766}
|
|
195
|
+
}
|
|
196
|
+
```
|
fame_qp-0.2.0/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# fame_qp — GPU QP solver for MPC with automatic execution mode
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
min_z ½ zᵀPz + qᵀz s.t. l ≤ G z ≤ h (P ∈ S₊ⁿ, G ∈ ℝ^{m×n})
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e . # requires a CUDA GPU, PyTorch and Triton
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from fame_qp import QP, Solver
|
|
17
|
+
qp = QP.create(P, q, G, l, h, device="cuda") # G=None means G = I
|
|
18
|
+
sol = Solver(qp, eps_abs=1e-4, eps_rel=1e-4) # calibrates / selects at setup
|
|
19
|
+
res = sol.solve()
|
|
20
|
+
res.z, res.s, res.lam, res.status, res.info["mode"], res.info["precision"]
|
|
21
|
+
|
|
22
|
+
sol.update(q=q2, l=l2, h=h2) # MPC loop: last iterate and rho carry over
|
|
23
|
+
sol.update_matrices(P=P2, G=G2) # same structure; bank re-inverted in place
|
|
24
|
+
sol.warm_start_shifted([(0, n_u, N)], # z = col(u_0..u_{N-1})
|
|
25
|
+
[(0, n_b, N), (n_b * N, n_u, N)]) # row groups, by stage
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Shifted warm start.** In receding-horizon control the next QP is the last one
|
|
29
|
+
moved one stage along the horizon, so the best starting point is the last
|
|
30
|
+
solution shifted by one stage (stage k+1 becomes stage k, the last stage is
|
|
31
|
+
held). `warm_start_shifted(z_segments, lam_segments)` does this for the primal
|
|
32
|
+
and the duals; a segment `(start, size, count)` is `count` stages of `size`
|
|
33
|
+
entries. Call it after `update` / `update_matrices`: the copy `s` is rebuilt
|
|
34
|
+
from the current `G`, `l`, `h`. `MPCController` uses it by default
|
|
35
|
+
(`shift=True`, segments from `LinearMPC.stage_segments()`).
|
|
36
|
+
|
|
37
|
+
LMPC (condensed, `G = [𝓑; I]`, optional LQR pre-stabilisation, sparse form for references):
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from fame_qp.mpc import LinearMPC
|
|
41
|
+
mpc = LinearMPC(A, B, Q, R, Q_N, N=40, u_lo=-1, u_hi=1, x_lo=-2, x_hi=2)
|
|
42
|
+
ctrl = mpc.controller(x_hat)
|
|
43
|
+
u0 = ctrl.step(x_hat)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Benchmark problems
|
|
47
|
+
|
|
48
|
+
`fame_qp.utils` generates the random problems used in the paper, seeded as in ReLU-QP:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from fame_qp.utils import random_qp, random_lmpc
|
|
52
|
+
|
|
53
|
+
qp = QP.create(**random_qp(n=400, seed=0)) # n/4 dense equalities, n/4 one-sided inequalities
|
|
54
|
+
|
|
55
|
+
prob = random_lmpc(n_u=10, N=40) # n_x = 3 n_u, LQR-preconditioned condensed QP
|
|
56
|
+
x = prob.initial_state() # paper's initial-state scale for n_u
|
|
57
|
+
sol = Solver(prob.qp(x), precision="float64", eps_abs=1e-3, eps_rel=1e-3)
|
|
58
|
+
u0 = prob.first_input(sol.solve().z.cpu().numpy(), x)
|
|
59
|
+
x = prob.step(x, u0)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Example
|
|
63
|
+
|
|
64
|
+
[`examples/demo.py`](examples/demo.py) solves a random QP with n = 1000, runs
|
|
65
|
+
closed-loop MPC on a random linear system, and runs the same MPC with A and B
|
|
66
|
+
changing at every step, using `update_matrices`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python examples/demo.py
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Notation in the code
|
|
73
|
+
|
|
74
|
+
| Symbol | Code | Meaning |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| z, P, q, G, l, h | `z, P, q, G, l, h` | the QP above |
|
|
77
|
+
| s = G z | `s` | ADMM copy of the constrained quantities |
|
|
78
|
+
| λ | `lam` | Lagrange multiplier of l ≤ G z ≤ h (dual of s = G z); `lambda` is reserved in Python |
|
|
79
|
+
| ρ, σ, α | `rho, sigma, alpha` | penalty, regularisation, relaxation |
|
|
80
|
+
| K = P + σI + Gᵀdiag(ρ)G, M = K⁻¹ | | pre-inverted for a bank of ρ |
|
|
81
|
+
| diag(d), C | `d, C` | diagonal rows and dense rows of G (n_d = n or 0, m_c rows) |
|
|
82
|
+
| 𝒜, 𝓑, 𝒬, ℛ | `Acal, Bcal, Qcal, Rcal` | condensing matrices (`mpc.py`) |
|
|
83
|
+
|
|
84
|
+
The ADMM paragraph of the paper still uses x, z, A, u for z, s, G, h.
|
|
85
|
+
|
|
86
|
+
Structures, detected from G: **box** `G = diag(d)`; **split** `G = [C; diag(d)]`
|
|
87
|
+
or `[diag(d); C]`; **general** otherwise (`C = G`).
|
|
88
|
+
|
|
89
|
+
## Execution modes
|
|
90
|
+
|
|
91
|
+
| Mode | Role of the CUDA graph |
|
|
92
|
+
|---|---|
|
|
93
|
+
| **Persistent-kernel solve** (`persistent`) | Not used for looping: the whole solve is one kernel launch (box structure, tile within the register file). A one-node graph only cuts the launch cost; ρ adapts between launches of `persistent_budget` iterations |
|
|
94
|
+
| **Single-kernel iteration** (`single_kernel`) | Replays a block of `check_interval` iterations, one kernel each, then the on-device stopping test |
|
|
95
|
+
| **Two-kernel iteration** (`two_kernel`) | Replays a block of iterations, two dependent kernels each, then the on-device stopping test |
|
|
96
|
+
|
|
97
|
+
Operators (`operator.py`): single-kernel `W = S M Sᵀ`, `S = [I; C]`, size (n+m_c)²;
|
|
98
|
+
two-kernel `[M, M Cᵀ]` plus `C`, n(n+2m_c) values. They differ by exactly m_c²
|
|
99
|
+
values per iteration, against one kernel launch.
|
|
100
|
+
|
|
101
|
+
## Automatic selection (`mode="auto"`, default)
|
|
102
|
+
|
|
103
|
+
1. **Calibration, once per device and precision** (`calibration.py`, ~1 s with
|
|
104
|
+
Triton's compile cache warm): graph-node cost τ, host-read cost, streaming
|
|
105
|
+
time vs operator size (captures L2 vs DRAM), and for the persistent kernel
|
|
106
|
+
the registers/spills reported by the compiler per tile, giving the largest
|
|
107
|
+
spill-free tile on this GPU.
|
|
108
|
+
2. **Setup test, once per problem shape**: predict single vs two kernels from
|
|
109
|
+
the calibration, drop candidates > 1.5× the best prediction or whose bank
|
|
110
|
+
does not fit in memory, time the rest on placeholder operators of the real
|
|
111
|
+
shape (tiles tuned), keep the single kernel unless two kernels win by > 10 %.
|
|
112
|
+
3. **Persistent vs graph replay**: predicted solve time from the iteration count
|
|
113
|
+
of the ρ probe. If the persistent mode wins, its starting ρ is re-probed for
|
|
114
|
+
its schedule (ρ can only change between launches) and the comparison redone.
|
|
115
|
+
|
|
116
|
+
Force a mode with `Solver(qp, mode="persistent" | "single_kernel" | "two_kernel")`.
|
|
117
|
+
|
|
118
|
+
## Precision
|
|
119
|
+
|
|
120
|
+
`precision="float32"` (default), `"float64"`, or `"auto"` (optional, two stages):
|
|
121
|
+
|
|
122
|
+
1. κ(K) by power iterations on K and K⁻¹ (float64). `u₃₂κ ≤ 0.01ε` → float32;
|
|
123
|
+
`u₃₂κ ≥ ε` → float64; otherwise stage 2.
|
|
124
|
+
2. Probe in both precisions; drop one that does not reach ε; take the shorter
|
|
125
|
+
predicted solve, preferring float64 within `precision_margin` (20 %).
|
|
126
|
+
|
|
127
|
+
Mode is selected per precision (float64 changes the persistent limit and the
|
|
128
|
+
crossover). The bank is always inverted in float64.
|
|
129
|
+
|
|
130
|
+
## Penalty selection
|
|
131
|
+
|
|
132
|
+
Probe trials adapt ρ as the solve does and record when the test passes at
|
|
133
|
+
`probe_tighten`×, √`probe_tighten`× and 1× the tolerance; candidates are compared
|
|
134
|
+
at the tightest level some candidate reached. The bank is re-centred when the
|
|
135
|
+
best start is at an end. In the split structure γ = ρ_dense/ρ_diag is searched
|
|
136
|
+
at the probed ρ, then ρ is probed again.
|
|
137
|
+
|
|
138
|
+
## Cache
|
|
139
|
+
|
|
140
|
+
`$FAME_QP_CACHE`, else `$XDG_CACHE_HOME/fame_qp`, else `~/.cache/fame_qp`:
|
|
141
|
+
`calibration.json` (per device: GPU, compute capability, driver, CUDA, Triton,
|
|
142
|
+
package version) and `modes.json` (per device, precision and problem shape).
|
|
143
|
+
Read once per process at the first setup; never touched by `solve`. Atomic,
|
|
144
|
+
file-locked writes; in memory only if the directory is not writable.
|
|
145
|
+
`fame_qp.clear_cache()` forgets everything; `Settings(retune=True)` re-measures.
|
|
146
|
+
|
|
147
|
+
## Tests
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest -q tests # 48 tests
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Reproducing the paper
|
|
154
|
+
|
|
155
|
+
[`quick_demo.ipynb`](quick_demo.ipynb) regenerates the figures and tables of the
|
|
156
|
+
experimental results from the saved benchmark results, and runs short versions
|
|
157
|
+
of the experiments live on your GPU. The benchmark
|
|
158
|
+
scripts and the setup of the baselines are in [`experiments/`](experiments/README.md).
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# fame_qp — GPU QP solver for MPC with automatic execution mode
|
|
2
|
+
|
|
3
|
+
Paper: [FAME-QP: A GPU-Accelerated Quadratic-Programming Solver for Real-Time Model Predictive Control](https://arxiv.org/abs/2611.13766) (arXiv:2611.13766)
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
min_z ½ zᵀPz + qᵀz s.t. l ≤ G z ≤ h (P ∈ S₊ⁿ, G ∈ ℝ^{m×n})
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install fame-qp # requires Linux, a CUDA GPU, PyTorch and Triton
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from fame_qp import QP, Solver
|
|
19
|
+
qp = QP.create(P, q, G, l, h, device="cuda") # G=None means G = I
|
|
20
|
+
sol = Solver(qp, eps_abs=1e-4, eps_rel=1e-4) # calibrates / selects at setup
|
|
21
|
+
res = sol.solve()
|
|
22
|
+
res.z, res.s, res.lam, res.status, res.info["mode"], res.info["precision"]
|
|
23
|
+
|
|
24
|
+
sol.update(q=q2, l=l2, h=h2) # MPC loop: last iterate and rho carry over
|
|
25
|
+
sol.update_matrices(P=P2, G=G2) # same structure; bank re-inverted in place
|
|
26
|
+
sol.warm_start_shifted([(0, n_u, N)], # z = col(u_0..u_{N-1})
|
|
27
|
+
[(0, n_b, N), (n_b * N, n_u, N)]) # row groups, by stage
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Shifted warm start.** In receding-horizon control the next QP is the last one
|
|
31
|
+
moved one stage along the horizon, so the best starting point is the last
|
|
32
|
+
solution shifted by one stage (stage k+1 becomes stage k, the last stage is
|
|
33
|
+
held). `warm_start_shifted(z_segments, lam_segments)` does this for the primal
|
|
34
|
+
and the duals; a segment `(start, size, count)` is `count` stages of `size`
|
|
35
|
+
entries. Call it after `update` / `update_matrices`: the copy `s` is rebuilt
|
|
36
|
+
from the current `G`, `l`, `h`. `MPCController` uses it by default
|
|
37
|
+
(`shift=True`, segments from `LinearMPC.stage_segments()`).
|
|
38
|
+
|
|
39
|
+
LMPC (condensed, `G = [𝓑; I]`, optional LQR pre-stabilisation, sparse form for references):
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from fame_qp.mpc import LinearMPC
|
|
43
|
+
mpc = LinearMPC(A, B, Q, R, Q_N, N=40, u_lo=-1, u_hi=1, x_lo=-2, x_hi=2)
|
|
44
|
+
ctrl = mpc.controller(x_hat)
|
|
45
|
+
u0 = ctrl.step(x_hat)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Benchmark problems
|
|
49
|
+
|
|
50
|
+
`fame_qp.utils` generates the random problems used in the paper, seeded as in ReLU-QP:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from fame_qp.utils import random_qp, random_lmpc
|
|
54
|
+
|
|
55
|
+
qp = QP.create(**random_qp(n=400, seed=0)) # n/4 dense equalities, n/4 one-sided inequalities
|
|
56
|
+
|
|
57
|
+
prob = random_lmpc(n_u=10, N=40) # n_x = 3 n_u, LQR-preconditioned condensed QP
|
|
58
|
+
x = prob.initial_state() # paper's initial-state scale for n_u
|
|
59
|
+
sol = Solver(prob.qp(x), precision="float64", eps_abs=1e-3, eps_rel=1e-3)
|
|
60
|
+
u0 = prob.first_input(sol.solve().z.cpu().numpy(), x)
|
|
61
|
+
x = prob.step(x, u0)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Example
|
|
65
|
+
|
|
66
|
+
[`examples/demo.py`](https://github.com/santoshrajkumar/fame-qp/blob/main/examples/demo.py) solves a random QP with n = 1000, runs
|
|
67
|
+
closed-loop MPC on a random linear system, and runs the same MPC with A and B
|
|
68
|
+
changing at every step, using `update_matrices`:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python examples/demo.py
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Notation in the code
|
|
75
|
+
|
|
76
|
+
| Symbol | Code | Meaning |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| z, P, q, G, l, h | `z, P, q, G, l, h` | the QP above |
|
|
79
|
+
| s = G z | `s` | ADMM copy of the constrained quantities |
|
|
80
|
+
| λ | `lam` | Lagrange multiplier of l ≤ G z ≤ h (dual of s = G z); `lambda` is reserved in Python |
|
|
81
|
+
| ρ, σ, α | `rho, sigma, alpha` | penalty, regularisation, relaxation |
|
|
82
|
+
| K = P + σI + Gᵀdiag(ρ)G, M = K⁻¹ | | pre-inverted for a bank of ρ |
|
|
83
|
+
| diag(d), C | `d, C` | diagonal rows and dense rows of G (n_d = n or 0, m_c rows) |
|
|
84
|
+
| 𝒜, 𝓑, 𝒬, ℛ | `Acal, Bcal, Qcal, Rcal` | condensing matrices (`mpc.py`) |
|
|
85
|
+
|
|
86
|
+
The ADMM paragraph of the paper still uses x, z, A, u for z, s, G, h.
|
|
87
|
+
|
|
88
|
+
Structures, detected from G: **box** `G = diag(d)`; **split** `G = [C; diag(d)]`
|
|
89
|
+
or `[diag(d); C]`; **general** otherwise (`C = G`).
|
|
90
|
+
|
|
91
|
+
## Execution modes
|
|
92
|
+
|
|
93
|
+
| Mode | Role of the CUDA graph |
|
|
94
|
+
|---|---|
|
|
95
|
+
| **Persistent-kernel solve** (`persistent`) | Not used for looping: the whole solve is one kernel launch (box structure, tile within the register file). A one-node graph only cuts the launch cost; ρ adapts between launches of `persistent_budget` iterations |
|
|
96
|
+
| **Single-kernel iteration** (`single_kernel`) | Replays a block of `check_interval` iterations, one kernel each, then the on-device stopping test |
|
|
97
|
+
| **Two-kernel iteration** (`two_kernel`) | Replays a block of iterations, two dependent kernels each, then the on-device stopping test |
|
|
98
|
+
|
|
99
|
+
Operators (`operator.py`): single-kernel `W = S M Sᵀ`, `S = [I; C]`, size (n+m_c)²;
|
|
100
|
+
two-kernel `[M, M Cᵀ]` plus `C`, n(n+2m_c) values. They differ by exactly m_c²
|
|
101
|
+
values per iteration, against one kernel launch.
|
|
102
|
+
|
|
103
|
+
## Automatic selection (`mode="auto"`, default)
|
|
104
|
+
|
|
105
|
+
1. **Calibration, once per device and precision** (`calibration.py`, ~1 s with
|
|
106
|
+
Triton's compile cache warm): graph-node cost τ, host-read cost, streaming
|
|
107
|
+
time vs operator size (captures L2 vs DRAM), and for the persistent kernel
|
|
108
|
+
the registers/spills reported by the compiler per tile, giving the largest
|
|
109
|
+
spill-free tile on this GPU.
|
|
110
|
+
2. **Setup test, once per problem shape**: predict single vs two kernels from
|
|
111
|
+
the calibration, drop candidates > 1.5× the best prediction or whose bank
|
|
112
|
+
does not fit in memory, time the rest on placeholder operators of the real
|
|
113
|
+
shape (tiles tuned), keep the single kernel unless two kernels win by > 10 %.
|
|
114
|
+
3. **Persistent vs graph replay**: predicted solve time from the iteration count
|
|
115
|
+
of the ρ probe. If the persistent mode wins, its starting ρ is re-probed for
|
|
116
|
+
its schedule (ρ can only change between launches) and the comparison redone.
|
|
117
|
+
|
|
118
|
+
Force a mode with `Solver(qp, mode="persistent" | "single_kernel" | "two_kernel")`.
|
|
119
|
+
|
|
120
|
+
## Precision
|
|
121
|
+
|
|
122
|
+
`precision="float32"` (default), `"float64"`, or `"auto"` (optional, two stages):
|
|
123
|
+
|
|
124
|
+
1. κ(K) by power iterations on K and K⁻¹ (float64). `u₃₂κ ≤ 0.01ε` → float32;
|
|
125
|
+
`u₃₂κ ≥ ε` → float64; otherwise stage 2.
|
|
126
|
+
2. Probe in both precisions; drop one that does not reach ε; take the shorter
|
|
127
|
+
predicted solve, preferring float64 within `precision_margin` (20 %).
|
|
128
|
+
|
|
129
|
+
Mode is selected per precision (float64 changes the persistent limit and the
|
|
130
|
+
crossover). The bank is always inverted in float64.
|
|
131
|
+
|
|
132
|
+
## Penalty selection
|
|
133
|
+
|
|
134
|
+
Probe trials adapt ρ as the solve does and record when the test passes at
|
|
135
|
+
`probe_tighten`×, √`probe_tighten`× and 1× the tolerance; candidates are compared
|
|
136
|
+
at the tightest level some candidate reached. The bank is re-centred when the
|
|
137
|
+
best start is at an end. In the split structure γ = ρ_dense/ρ_diag is searched
|
|
138
|
+
at the probed ρ, then ρ is probed again.
|
|
139
|
+
|
|
140
|
+
## Cache
|
|
141
|
+
|
|
142
|
+
`$FAME_QP_CACHE`, else `$XDG_CACHE_HOME/fame_qp`, else `~/.cache/fame_qp`:
|
|
143
|
+
`calibration.json` (per device: GPU, compute capability, driver, CUDA, Triton,
|
|
144
|
+
package version) and `modes.json` (per device, precision and problem shape).
|
|
145
|
+
Read once per process at the first setup; never touched by `solve`. Atomic,
|
|
146
|
+
file-locked writes; in memory only if the directory is not writable.
|
|
147
|
+
`fame_qp.clear_cache()` forgets everything; `Settings(retune=True)` re-measures.
|
|
148
|
+
|
|
149
|
+
## Tests
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest -q tests # 48 tests
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Reproducing the paper
|
|
156
|
+
|
|
157
|
+
[`quick_demo.ipynb`](https://github.com/santoshrajkumar/fame-qp/blob/main/quick_demo.ipynb) regenerates the figures and tables of the
|
|
158
|
+
experimental results from the saved benchmark results, and runs short versions
|
|
159
|
+
of the experiments live on your GPU. The benchmark
|
|
160
|
+
scripts and the setup of the baselines are in [`experiments/`](https://github.com/santoshrajkumar/fame-qp/blob/main/experiments/README.md).
|
|
161
|
+
|
|
162
|
+
## Citation
|
|
163
|
+
|
|
164
|
+
If you use FAME-QP in your research, please cite:
|
|
165
|
+
|
|
166
|
+
```bibtex
|
|
167
|
+
@misc{rajkumar2026fameqp,
|
|
168
|
+
title = {{FAME-QP}: A {GPU}-Accelerated Quadratic-Programming Solver for Real-Time Model Predictive Control},
|
|
169
|
+
author = {Rajkumar, Santosh Mohan and Goswami, Debdipta and Stockar, Stephanie},
|
|
170
|
+
year = {2026},
|
|
171
|
+
eprint = {2611.13766},
|
|
172
|
+
archivePrefix = {arXiv},
|
|
173
|
+
primaryClass = {cs.RO},
|
|
174
|
+
url = {https://arxiv.org/abs/2611.13766}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""FAME-QP: fused ADMM with multimode execution for dense QPs on the GPU.
|
|
2
|
+
|
|
3
|
+
Solves
|
|
4
|
+
|
|
5
|
+
minimize 1/2 z'Pz + q'z
|
|
6
|
+
subject to l <= Gz <= h
|
|
7
|
+
|
|
8
|
+
Example
|
|
9
|
+
-------
|
|
10
|
+
>>> from fame_qp import QP, Solver
|
|
11
|
+
>>> qp = QP.create(P, q, G, l, h, device="cuda")
|
|
12
|
+
>>> res = Solver(qp, eps_abs=1e-4, eps_rel=1e-4).solve()
|
|
13
|
+
>>> res.z, res.lam, res.status, res.info["mode"]
|
|
14
|
+
|
|
15
|
+
Linear MPC in condensed form:
|
|
16
|
+
|
|
17
|
+
>>> from fame_qp.mpc import LinearMPC
|
|
18
|
+
>>> mpc = LinearMPC(A, B, Q, R, Q_N, N=30, u_lo=-1, u_hi=1)
|
|
19
|
+
>>> ctrl = mpc.controller(x_hat)
|
|
20
|
+
>>> u0 = ctrl.step(x_hat)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__version__ = "0.2.0"
|
|
24
|
+
|
|
25
|
+
from .problem import QP
|
|
26
|
+
from .solver import Result, Settings, Solver
|
|
27
|
+
from .calibration import clear_cache, get_calibration
|
|
28
|
+
|
|
29
|
+
__all__ = ["QP", "Solver", "Settings", "Result", "get_calibration", "clear_cache"]
|