perf(solvers): name strings and getLp/passModel round trip dominate to_highspy (and gurobi) build time
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
- issue の種類
- リファクタリング
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- numpy, python
調査の方向性
Start in linopy/solvers.py at Highs._build_solver_model and in linopy/io.py at get_printers_scalar, to_highspy, and to_gurobipy. Run the supplied repro.py with uv run python repro.py to establish the naming and conversion timings. Done means reducing the name-related build overhead while preserving the expected solver model names and behavior for both direct APIs.
索引モデルが issue の本文から書いたものです。
説明
[!NOTE]
The following content was generated by AI.
Describe the feature you'd like to see
Once the sparse/CSR path makes building the matrices cheap, setting variable/constraint names is the largest single cost of the direct solver APIs. Found while working on #974 (sparse/CSR plan #972); measured on feat/csr-boundaries @ 0cf4a200.
Model with 200,000 vars, 218,000 cons and 596,000 nnz (frozen constraints), best of 5 runs:
| step | time |
|---|---|
to_highspy(m) (names on, the default) |
117–121 ms |
to_highspy(m, set_names=False) |
47–49 ms |
of which print_variables + print_constraints (build Python list[str]) |
28–29 ms |
of which h.getLp() + assigning lp.col_names_ / lp.row_names_ (list to std::vector<string>) |
13–14 ms |
of which h.passModel(lp) round trip |
11–12 ms |
to_gurobipy(m) vs to_gurobipy(m, set_names=False) |
566 ms vs 467 ms (+~100 ms) |
So names make to_highspy about 2.4x slower. In Highs._build_solver_model (linopy/solvers.py) the names are added after the model is built: lp = h.getLp() copies the whole LP out, names are set, and h.passModel(lp) copies it back in. That round trip copies the matrix twice only to attach names. get_printers_scalar (linopy/io.py) already builds the strings with polars ("x" + pl.Series(labels).cast(pl.String)), but .to_list() still makes one Python str per label, and every solver binding then converts them again. Gurobi (addMVar(name=...), setAttr("ConstrName", ...)) pays a similar ~100 ms.
Suggestions
- Avoid the getLp/passModel round trip in
to_highspy: build ahighspy.HighsLponce, with the matrix, bounds and names, and callpassModela single time. Or set the names without copying the LP out. - Make names cheaper or lazy: the default
x{label}/c{label}names carry no information beyond the index. The solution is already mapped back by position, so the names could default off for the direct APIs, or be set only when needed (e.g. writing a file from the solver object,explicit_coordinate_names=True). - If names stay on by default, build them in one vectorised pass (numpy
char/StringDTypeor polars) and pass them in the form each binding converts fastest, instead of going throughlist[str]twice.
Minimal reproducible example
Needs the freeze=True sparse path from #974 for the frozen constraints; the name cost is the same with dense constraints. Run with uv run python repro.py.
import time
import numpy as np, pandas as pd, xarray as xr
import linopy
from linopy import Model
from linopy.io import get_printers_scalar
linopy.options["semantics"] = "v1"
m = Model()
gen = pd.Index(range(2000), name="gen"); snap = pd.Index(range(100), name="snapshot")
rng = np.random.default_rng(0)
bus = xr.DataArray(rng.integers(0, 200, 2000), coords=[gen], name="bus")
p = m.add_variables(0, 10, coords=[gen, snap], name="p")
demand = xr.DataArray(rng.uniform(1, 5, (200, 100)), coords=[pd.Index(range(200), name="bus"), snap])
m.add_constraints((1.0 * p).groupby(bus).sum(sparse=True) == demand, name="balance", freeze=True)
m.add_constraints(p.diff("snapshot") <= 3, name="ramp", freeze=True)
m.add_objective((xr.DataArray(rng.uniform(1, 5, 2000), coords=[gen]) * p).sum())
M = m.matrices
print(f"vars={len(M.vlabels):,} cons={len(M.clabels):,} nnz={M.A.nnz:,}")
def best(f, n=5):
ts = []
for _ in range(n):
t = time.perf_counter(); f(); ts.append(time.perf_counter() - t)
return min(ts) * 1e3
print(f"to_highspy(set_names=True) {best(lambda: linopy.io.to_highspy(m)):6.1f} ms")
print(f"to_highspy(set_names=False) {best(lambda: linopy.io.to_highspy(m, set_names=False)):6.1f} ms")
pv, pc = get_printers_scalar(m)
print(f" name strings {best(lambda: (pv(M.vlabels), pc(M.clabels))):6.1f} ms")
h = linopy.io.to_highspy(m, set_names=False)
cn, rn = pv(M.vlabels), pc(M.clabels)
def assign():
lp = h.getLp(); lp.col_names_ = cn; lp.row_names_ = rn
print(f" getLp + assign names {best(assign):6.1f} ms")
print(f" passModel(getLp()) trip {best(lambda: h.passModel(h.getLp())):6.1f} ms")
print(f"to_gurobipy names/no names {best(lambda: linopy.io.to_gurobipy(m), 3):6.1f} / "
f"{best(lambda: linopy.io.to_gurobipy(m, set_names=False), 3):6.1f} ms")
Output (HiGHS 1.15.1, gurobipy restricted license, Python 3.13)
vars=200,000 cons=218,000 nnz=596,000
to_highspy(set_names=True) 117.2 ms
to_highspy(set_names=False) 47.4 ms
name strings 27.7 ms
getLp + assign names 13.3 ms
passModel(getLp()) trip 11.2 ms
to_gurobipy names/no names 566.4 / 466.6 ms
The HiGHS banner lines are left out. The name strings, name assignment and round trip add up to about 52 ms of the about 70 ms difference; the rest is allocation/GC overhead from the extra Python strings.
- 主要言語
- Python
- スター
- 257
- フォーク
- 87
- 平均マージ
- 21時間 29分
- マージ済み PR(30日)
- 42
環境構築
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
PyPSA/linopy のほかの issue
-
bug solver interface
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
メンテナーはふだん 1 日以内に返信
-
performance sparse
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
メンテナーはふだん 1 日以内に返信
-
documentation sparse
難易度 3/5 1〜2日 初心者へのやさしさ 68/100
メンテナーはふだん 1 日以内に返信
-
Make the sparse path observable and controllable (.is_sparse, densify warning, per-call sparse=)オープンenhancement sparse
難易度 5/5 1週間以上 初心者へのやさしさ 35/100
メンテナーはふだん 1 日以内に返信
-
enhancement model formulation
難易度 1/5 1時間未満 初心者へのやさしさ 35/100
メンテナーはふだん 1 日以内に返信
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
-
bug
難易度 1/5 1時間未満 初心者へのやさしさ 88/100
qgis/QGIS-Plugins-Website#459 ·
-
bug severity:medium
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 2 日以内に返信
-
bot-found bug priority: P3
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
madenvel/KalinkaPlayer#179 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
ls1intum/edutelligence#1098 ·
メンテナーはふだん 1 日以内に返信