Skip to content

Commit 6f995e7

Browse files
PERF pre-allocate arrays in NARX _get_hc_ids/_get_jc_ids (#242)
* PERF replace quadratic np.vstack/np.append with pre-allocated arrays in NARX ID construction _get_hc_ids and _get_jc_ids grew arrays with np.vstack inside loops, causing O(n²) allocation. Two-pass approach: count rows first, pre-allocate exact size, fill in-place. 1.5x faster, 50% less RAM. * vectorize and comments * fix np astype * PERF avoid result copies in NARX _get_jc_ids/_get_hc_ids Use astype(copy=False) and free the boolean mask before allocating result arrays, so peak RAM no longer exceeds the previous incremental implementation at meaningful sizes (-15% to -27% peak for large term counts). Outputs are byte-identical; jac/hess tests pass. * CI re-trigger (OpenML 504 timeouts) * readability * scikit-learn<1.9 * pin pixi 0.70.0 * fix readthedocs --------- Co-authored-by: sikai zhang <matthew.szhang91@gmail.com>
1 parent c87b0fd commit 6f995e7

7 files changed

Lines changed: 65 additions & 82 deletions

File tree

‎.github/workflows/asv.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
3737
- uses: prefix-dev/setup-pixi@v0.9.6
3838
with:
39+
pixi-version: v0.70.0
3940
environments: dev
4041
cache: true
4142

@@ -102,6 +103,7 @@ jobs:
102103
103104
- uses: prefix-dev/setup-pixi@v0.9.6
104105
with:
106+
pixi-version: v0.70.0
105107
environments: dev
106108
cache: true
107109

‎.github/workflows/static.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ jobs:
1111
- uses: actions/checkout@v6
1212
- uses: prefix-dev/setup-pixi@v0.9.6
1313
with:
14+
pixi-version: v0.70.0
1415
environments: static
1516
cache: true
1617

‎.github/workflows/test.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
- uses: actions/checkout@v6
2626
- uses: prefix-dev/setup-pixi@v0.9.6
2727
with:
28+
pixi-version: v0.70.0
2829
environments: >-
2930
dev
3031
docs

‎.github/workflows/wheel.yml‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ jobs:
1010
- uses: actions/checkout@v6
1111
- uses: prefix-dev/setup-pixi@v0.9.6
1212
with:
13+
pixi-version: v0.70.0
1314
environments: dev
1415
cache: true
1516
- name: Re-install local
@@ -52,8 +53,6 @@ jobs:
5253
CIBW_ARCHS_LINUX: auto
5354
CIBW_ARCHS_MACOS: x86_64 arm64
5455
CIBW_ARCHS_WINDOWS: auto64
55-
# Include free-threaded support
56-
CIBW_ENABLE: cpython-freethreading
5756
CIBW_BUILD_FRONTEND: "build[uv]"
5857
- name: Upload package
5958
uses: actions/upload-artifact@v7

‎.readthedocs.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ build:
66
jobs:
77
create_environment:
88
- asdf plugin add pixi
9-
- asdf install pixi latest
10-
- asdf global pixi latest
9+
- asdf install pixi 0.70.0
10+
- asdf global pixi 0.70.0
1111
install:
1212
- pixi install -e docs
1313
build:

‎README.rst‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,6 @@ fastcan can be used for system identification.
108108
In particular, we provide a submodule `fastcan.narx` to build Nonlinear AutoRegressive eXogenous (NARX) models.
109109
For more information, check this `NARX model example <https://fastcan.readthedocs.io/en/latest/auto_examples/plot_narx.html>`_.
110110

111-
112-
Support Free-Threaded Wheels
113-
----------------------------
114-
fastcan has support for free-threaded (also known as nogil) CPython 3.13.
115-
For more information about free-threaded CPython, check `how to install a free-threaded CPython <https://py-free-threading.github.io/installing_cpython/>`_.
116-
117111
Support WASM Wheels
118112
-------------------
119113
fastcan is compiled to WebAssembly (WASM) wheels using `pyodide <https://github.com/pyodide/pyodide>`_.

‎fastcan/narx/_base.py‎

Lines changed: 58 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -718,59 +718,54 @@ def _get_hc_ids(
718718
d2ydx2[k, i] += Constant terms
719719
"""
720720
n_degree = jac_feat_ids.shape[1]
721-
hess_yyd_ids = np.zeros((0, 3), dtype=np.int32)
722-
hess_yd_ids = np.zeros((0, 2), dtype=np.int32)
723-
hess_coef_ids = np.zeros(0, dtype=np.int32)
724-
hess_feat_ids = np.zeros((0, n_degree), dtype=np.int32)
725-
hess_delay_ids = np.zeros((0, n_degree), dtype=np.int32)
726721

727722
if mode < 2:
728723
return (
729-
hess_yyd_ids,
730-
hess_yd_ids,
731-
hess_coef_ids,
732-
hess_feat_ids,
733-
hess_delay_ids,
724+
np.zeros((0, 3), dtype=np.int32), # hess_yyd_ids
725+
np.zeros((0, 2), dtype=np.int32), # hess_yd_ids
726+
np.zeros(0, dtype=np.int32), # hess_coef_ids
727+
np.zeros((0, n_degree), dtype=np.int32), # hess_feat_ids
728+
np.zeros((0, n_degree), dtype=np.int32), # hess_delay_ids
734729
)
735730

736-
for yyd_id, coef_id, feat_ids, delay_ids in zip(
737-
jac_yyd_ids, jac_coef_ids, jac_feat_ids, jac_delay_ids
738-
):
739-
# In jac, x * term will generate a term with coef 1.
740-
# In hess, it will be
741-
# d term / dx = 1 * d y_in * yi * yj
742-
hess_yyd_ids = np.vstack([hess_yyd_ids, yyd_id])
743-
hess_yd_ids = np.vstack([hess_yd_ids, [-1, -1]]) # empty
744-
# constant 1 handled in _update_hc
745-
hess_coef_ids = np.append(hess_coef_ids, coef_id)
746-
hess_feat_ids = np.vstack([hess_feat_ids, feat_ids])
747-
hess_delay_ids = np.vstack([hess_delay_ids, delay_ids])
748-
for var_id, (feat_id, delay_id) in enumerate(zip(feat_ids, delay_ids)):
749-
# d JC / dx = coef * d y_in * d yi * yj
750-
# hess_yyd_ids: y_out and y_in
751-
# hess_coef_ids: coef
752-
# hess_feat_ids: yj ..
753-
# hess_delay_ids: yj ..
754-
# feat_ids and delay_ids contain spaceholder -1 to keep poly_degree size
755-
# Skip input x and spaceholder -1
756-
if feat_id >= n_features_in and delay_id > 0:
757-
# when feat_id is output y, drop it from hess_feat_ids
758-
hess_yd_ids = np.vstack(
759-
[hess_yd_ids, [feat_id - n_features_in, delay_id]]
760-
)
761-
hess_yyd_ids = np.vstack([hess_yyd_ids, yyd_id])
762-
hess_coef_ids = np.append(hess_coef_ids, coef_id)
763-
hess_feat_ids = np.vstack([hess_feat_ids, feat_ids])
764-
hess_delay_ids = np.vstack([hess_delay_ids, delay_ids])
765-
hess_feat_ids[-1][var_id] = -1
766-
hess_delay_ids[-1][var_id] = -1
731+
# In jac, x * term will generate two parts: 1 * term + x * d term / dx
732+
# In hess, the part of a term with coef 1 will be
733+
# d term / dx = 1 * d y_in * yi * yj
734+
# constant 1 in hess_coef_ids handled in _update_hc
735+
# The second part will be
736+
# d JC / dx = coef * d y_in * d yi * yj
737+
# hess_yyd_ids: y_out and y_in
738+
# hess_coef_ids: coef
739+
# hess_feat_ids: yj ..
740+
# hess_delay_ids: yj ..
741+
# feat_ids and delay_ids contain spaceholder -1 to keep poly_degree size
742+
# Skip input x and spaceholder -1
743+
n_d_coefs = len(jac_yyd_ids)
744+
mask = (jac_feat_ids >= n_features_in) & (jac_delay_ids > 0)
745+
d_terms_ids, var_ids = np.nonzero(mask)
746+
747+
hess_yyd_ids = np.vstack([jac_yyd_ids, jac_yyd_ids[d_terms_ids]])
748+
yd_d_coefs = np.full((n_d_coefs, 2), -1, dtype=np.int32)
749+
yd_d_terms = np.column_stack(
750+
[jac_feat_ids[mask] - n_features_in, jac_delay_ids[mask]]
751+
)
752+
hess_yd_ids = np.vstack([yd_d_coefs, yd_d_terms])
753+
hess_coef_ids = np.concatenate([jac_coef_ids, jac_coef_ids[d_terms_ids]])
754+
755+
hess_feat_ids = np.vstack([jac_feat_ids, jac_feat_ids[d_terms_ids]])
756+
hess_delay_ids = np.vstack([jac_delay_ids, jac_delay_ids[d_terms_ids]])
757+
758+
row_indices = np.arange(n_d_coefs, len(hess_yyd_ids))
759+
# when feat_id is output y, drop it from hess_feat_ids
760+
hess_feat_ids[row_indices, var_ids] = -1
761+
hess_delay_ids[row_indices, var_ids] = -1
767762

768763
return (
769-
hess_yyd_ids.astype(np.int32),
770-
hess_yd_ids.astype(np.int32),
771-
hess_coef_ids.astype(np.int32),
772-
hess_feat_ids.astype(np.int32),
773-
hess_delay_ids.astype(np.int32),
764+
hess_yyd_ids.astype(np.int32, copy=False),
765+
hess_yd_ids.astype(np.int32, copy=False),
766+
hess_coef_ids.astype(np.int32, copy=False),
767+
hess_feat_ids.astype(np.int32, copy=False),
768+
hess_delay_ids.astype(np.int32, copy=False),
774769
)
775770

776771
@staticmethod
@@ -802,35 +797,26 @@ def _get_jc_ids(feat_ids, delay_ids, output_ids, n_features_in):
802797
axis-2 (j) input y: dy0(k-d)/dx, dy1(k-d)/dx, ..., dyn(k-d)/dx
803798
"""
804799

805-
n_degree = feat_ids.shape[1]
806-
jac_yyd_ids = np.zeros((0, 3), dtype=np.int32)
807-
jac_coef_ids = np.zeros(0, dtype=int)
808-
jac_feat_ids = np.zeros((0, n_degree), dtype=np.int32)
809-
jac_delay_ids = np.zeros((0, n_degree), dtype=np.int32)
800+
mask = (feat_ids >= n_features_in) & (delay_ids > 0)
801+
jac_coef_ids, var_ids = np.nonzero(mask)
802+
n_rows = jac_coef_ids.shape[0]
810803

811-
for coef_id, (term_feat_ids, term_delay_ids) in enumerate(
812-
zip(feat_ids, delay_ids)
813-
):
814-
out_y_id = output_ids[coef_id] # y(k, id), output
815-
for var_id, (feat_id, delay_id) in enumerate(
816-
zip(term_feat_ids, term_delay_ids)
817-
):
818-
if feat_id >= n_features_in and delay_id > 0:
819-
in_y_id = feat_id - n_features_in # y(k-d, id), input
820-
jac_yyd_ids = np.vstack(
821-
[jac_yyd_ids, [out_y_id, in_y_id, delay_id]]
822-
)
823-
jac_coef_ids = np.append(jac_coef_ids, coef_id)
824-
jac_feat_ids = np.vstack([jac_feat_ids, term_feat_ids])
825-
jac_delay_ids = np.vstack([jac_delay_ids, term_delay_ids])
826-
jac_feat_ids[-1][var_id] = -1
827-
jac_delay_ids[-1][var_id] = -1
804+
jac_yyd_ids = np.empty((n_rows, 3), dtype=np.int32)
805+
jac_yyd_ids[:, 0] = output_ids[jac_coef_ids] # y(k, id), output
806+
jac_yyd_ids[:, 1] = feat_ids[mask] - n_features_in # y(k-d, id), input
807+
jac_yyd_ids[:, 2] = delay_ids[mask]
808+
809+
jac_feat_ids = feat_ids[jac_coef_ids]
810+
jac_delay_ids = delay_ids[jac_coef_ids]
811+
row_indices = np.arange(n_rows)
812+
jac_feat_ids[row_indices, var_ids] = -1
813+
jac_delay_ids[row_indices, var_ids] = -1
828814

829815
return (
830-
jac_yyd_ids.astype(np.int32),
831-
jac_coef_ids.astype(np.int32),
832-
jac_feat_ids.astype(np.int32),
833-
jac_delay_ids.astype(np.int32),
816+
jac_yyd_ids.astype(np.int32, copy=False),
817+
jac_coef_ids.astype(np.int32, copy=False),
818+
jac_feat_ids.astype(np.int32, copy=False),
819+
jac_delay_ids.astype(np.int32, copy=False)
834820
)
835821

836822
@staticmethod

0 commit comments

Comments
 (0)