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 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
+ ```
@@ -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"]