-
Notifications
You must be signed in to change notification settings - Fork 0
231 lines (217 loc) · 10.7 KB
/
Copy pathci.yml
File metadata and controls
231 lines (217 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
name: CI
on:
push:
branches: [main, develop]
pull_request:
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed]
permissions:
contents: read
concurrency:
# Keep one active CI run per PR while preserving independent push runs. A
# synchronized head cancels its queued/running predecessor instead of
# multiplying the hosted-runner backlog with stale evidence.
group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# Matrix legs report as "python (3.12)" / "python (3.14)". Branch protection
# still requires the exact check context name `python`, so a thin aggregate
# job below re-exports matrix success under that name.
python-matrix:
if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }}
name: python (${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.14"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
with:
python-version: ${{ matrix.python-version }}
# Rust toolchain is required so the maturin build compiles the primary
# numeric core (fast_mlsirm._core) during the editable install; the
# Rust<->NumPy parity gate then exercises the Rust path.
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30
with:
toolchain: 1.97.1
- run: python -m pip install --require-hashes -r requirements/ci.txt
- run: python -m pip install --no-deps --no-build-isolation -e .
- name: Verify Rust core is the resolved default backend
run: python -c "import fast_mlsirm._core as c; from fast_mlsirm.backend import resolve_backend; from fast_mlsirm import FitConfig; assert hasattr(c, 'neg_loglik_and_grad'); assert resolve_backend(FitConfig().backend) == 'rust'; print('rust primary OK')"
- run: pytest
python:
name: python
needs: python-matrix
if: ${{ always() && (github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed')) }}
runs-on: ubuntu-latest
steps:
- name: Require every CPython matrix leg
run: |
echo "python-matrix result=${{ needs.python-matrix.result }}"
test "${{ needs.python-matrix.result }}" = "success"
rust:
if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
with:
python-version: "3.12"
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30
with:
toolchain: 1.97.1
# Ordinary Rust tests include the deterministic equation and bounded
# recovery-contract sentinels. Exhaustive ignored studies run in the
# scheduled Statistical Studies workflow rather than on every PR.
- run: cargo test --workspace
- run: cargo test --manifest-path crates/fast-mlsirm-py/Cargo.toml
gpu-smoke:
if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }}
# One bounded explicit-GPU parity test remains on every PR. The full
# paper-design GPU recovery study runs in the scheduled/manual workflow.
runs-on: ubuntu-latest
timeout-minutes: 30
env:
WGPU_BACKEND: vulkan
XDG_RUNTIME_DIR: /tmp/fast-mlsirm-xdg-runtime
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
with:
python-version: "3.12"
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30
with:
toolchain: 1.97.1
- name: Install software Vulkan adapter
run: |
# The hosted runner's mirror+file registry can keep selecting a
# black-holed Azure mirror even after apt falls back for InRelease.
# Use a job-local source file pinned to canonical Ubuntu archives so
# package payloads do not re-enter that broken mirror selection path.
. /etc/os-release
test -n "${VERSION_CODENAME:-}"
cat > /tmp/fast-mlsirm-ubuntu.sources <<EOF
Types: deb
URIs: https://archive.ubuntu.com/ubuntu
Suites: ${VERSION_CODENAME} ${VERSION_CODENAME}-updates ${VERSION_CODENAME}-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Types: deb
URIs: https://security.ubuntu.com/ubuntu
Suites: ${VERSION_CODENAME}-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
EOF
timeout 120s sudo apt-get \
-o Dir::Etc::sourcelist=/tmp/fast-mlsirm-ubuntu.sources \
-o Dir::Etc::sourceparts=- \
-o Acquire::Retries=2 \
-o Acquire::http::Timeout=10 \
-o Acquire::https::Timeout=10 \
-o DPkg::Lock::Timeout=30 \
update
timeout 180s sudo apt-get \
-o Dir::Etc::sourcelist=/tmp/fast-mlsirm-ubuntu.sources \
-o Dir::Etc::sourceparts=- \
-o Acquire::Retries=2 \
-o Acquire::http::Timeout=10 \
-o Acquire::https::Timeout=10 \
-o DPkg::Lock::Timeout=30 \
install --yes mesa-vulkan-drivers vulkan-tools
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR"
LVP_ICD="$(find /usr/share/vulkan/icd.d -maxdepth 1 -type f -name '*lvp*.json' -print -quit)"
test -n "$LVP_ICD"
echo "VK_ICD_FILENAMES=$LVP_ICD" >> "$GITHUB_ENV"
- name: Prove Vulkan compute adapter availability
run: vulkaninfo --summary
- run: python -m pip install --require-hashes -r requirements/ci.txt
- run: python -m pip install --no-deps --no-build-isolation -e .
- name: Run explicit GPU parity without a skip
run: pytest tests/test_marginal_parity.py::test_marginal_gpu_agrees_with_cpu_loosely -q --junitxml=gpu-junit.xml
- name: Reject skipped GPU evidence
run: |
python - <<'PY'
from pathlib import Path
from xml.etree import ElementTree
root = ElementTree.parse(Path("gpu-junit.xml")).getroot()
suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite"))
tests = sum(int(suite.attrib.get("tests", "0")) for suite in suites)
skipped = sum(int(suite.attrib.get("skipped", "0")) for suite in suites)
if tests < 1:
raise SystemExit("GPU evidence did not execute a test")
if skipped != 0:
raise SystemExit(f"GPU evidence contained {skipped} skipped test(s)")
PY
fuzz:
if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }}
# Short, bounded coverage-guided fuzzing so PR cost stays low. The
# property-based Hypothesis tests already run in the `python` job (via the
# dev extra); this job adds the Atheris coverage-guided harnesses with a
# ~60s-per-target budget. Longer campaigns can be run nightly if desired.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
with:
python-version: "3.12"
# pip is version-pinned so the toolchain used to resolve the fuzz extras
# is reproducible (OpenSSF Scorecard pinned-dependencies hardening).
- run: python -m pip install --upgrade "pip==26.1.2"
# Editable install of the repo-under-test. pip does not support
# --require-hashes for editable/local installs, so the fuzz extras
# (atheris plus test-only hypothesis) are resolved from the pinned pip
# above rather than a hash-locked requirements file.
- run: python -m pip install -e '.[fuzz]'
- name: Fuzz load_factor_csv (CSV parser)
run: |
python fuzz/atheris/fuzz_load_factor_csv.py \
-max_total_time=60 -timeout=25 fuzz/corpus/load_factor_csv
- name: Fuzz render_diagnostics_report (JSON -> HTML)
run: |
python fuzz/atheris/fuzz_render_report.py \
-max_total_time=60 -timeout=25 fuzz/corpus/render_report
- name: Fuzz config validators
run: |
python fuzz/atheris/fuzz_config.py \
-max_total_time=60 -timeout=25 fuzz/corpus/config
package:
if: ${{ github.event_name != 'pull_request' || (!github.event.pull_request.draft && github.event.action != 'closed') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
with:
python-version: "3.12"
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30
with:
toolchain: 1.97.1
- run: python -m pip install --require-hashes -r requirements/package.txt
- run: python -m build --no-isolation
- run: python -m pip install --no-index --no-deps --force-reinstall dist/*.whl
- run: python -c "import fast_mlsirm, fast_mlsirm._core as core; print(fast_mlsirm.__version__, hasattr(core, 'neg_loglik_and_grad'))"
- name: Run release acceptance smoke
run: |
python scripts/release_acceptance.py --out release-acceptance --require-rust --persons 12 --dims 1 --items-per-dim 2 --latent-dim 1 --latent-dims 1,2 --folds 2 --max-iter 1 --n-restarts 1
python - <<'PY'
import json
with open("release-acceptance/acceptance_summary.json", encoding="utf-8") as fh:
summary = json.load(fh)
assert summary["status"] == "ok"
PY
- name: Run enterprise sales readiness gate
run: |
python scripts/sales_readiness.py \
--acceptance release-acceptance/acceptance_summary.json \
--dist dist \
--require-rust \
--require-20b-product \
--check-import \
--out release-acceptance/sales_readiness_manifest.json
python - <<'PY'
import json
with open("release-acceptance/sales_readiness_manifest.json", encoding="utf-8") as fh:
manifest = json.load(fh)
assert manifest["status"] == "ok"
PY