-
Notifications
You must be signed in to change notification settings - Fork 93
Add CI benchmark suite: latency + recall regression tracking #446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Sravan1011
wants to merge
24
commits into
usemoss:main
Choose a base branch
from
Sravan1011:feat/ci-benchmark-suite
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
be9d9f6
feat(moss-cli): add shell completions
Sravan1011 8e4937b
Potential fix for pull request finding
Sravan1011 d0b74b0
Merge branch 'usemoss:main' into main
Sravan1011 e3f3133
Merge branch 'usemoss:main' into main
Sravan1011 7f6ffc8
Merge branch 'usemoss:main' into main
Sravan1011 f72b6d5
Merge branch 'usemoss:main' into main
Sravan1011 81b15dd
Merge branch 'usemoss:main' into main
Sravan1011 be0503c
Merge branch 'usemoss:main' into main
Sravan1011 e021aec
Merge branch 'usemoss:main' into main
Sravan1011 f4732a1
Add CI benchmark suite for latency and recall regression tracking (#435)
Sravan1011 ded57ea
Address review feedback: harden generator, workflow, and docs
Sravan1011 196d2f8
Potential fix for pull request finding
Sravan1011 26ff7d4
Fix broken indent from suggested change; address Copilot findings
Sravan1011 16313b1
Fix ruff F541: remove f-prefix from placeholder-free strings
Sravan1011 ffd185b
fix: fail trusted CI on missing secrets; signature-derived benchmark …
Sravan1011 7df6fd3
fix: fail on missing required inputs; serialize runs; validate baseli…
Sravan1011 16b0343
ci: retry pip install on transient PyPI download failures
Sravan1011 1814b15
fix: guard --recreate against deleting non-benchmark indexes; fail on…
Sravan1011 85618ba
ci: retrigger after transient PyPI download failure in python-sdk-tes…
Sravan1011 3e6ac46
fix: explicit unarmed marker for the placeholder latency baseline
Sravan1011 062eebc
fix: regression guards fail in trusted CI when measurement data is mi…
Sravan1011 bf8f116
fix: guard recall@10 alongside recall@5 in the regression check
Sravan1011 ad59ba0
Address review: exercise index build path per change; write artifact …
Sravan1011 942ac8a
Address review: hash installed files not versions; fix loop/import or…
Sravan1011 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| name: Benchmark | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| # All benchmark runs share one Moss project and the deterministic | ||
| # benchmark-ci-<hash> index, so concurrent jobs could race on first index | ||
| # creation and contaminate each other's latency numbers with cross-job | ||
| # load. Serialize globally; don't cancel a run that is already measuring. | ||
| concurrency: | ||
| group: moss-benchmark | ||
| cancel-in-progress: false | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
| workflow_dispatch: | ||
| inputs: | ||
| update_baseline: | ||
| description: 'Update baseline.json with current results' | ||
| type: boolean | ||
| default: false | ||
|
|
||
| jobs: | ||
| benchmark: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.12' | ||
|
|
||
| - name: Install dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| # PyPI downloads occasionally break mid-stream on hosted runners | ||
| # (IncompleteRead / ProtocolError), and pip does not resume a | ||
| # partial wheel download — retry the whole install a few times. | ||
| for attempt in 1 2 3; do | ||
| if pip install -r benchmarks/ci/requirements.txt; then | ||
| exit 0 | ||
| fi | ||
| echo "pip install failed (attempt ${attempt}/3) — retrying in 15s" | ||
| sleep 15 | ||
| done | ||
| echo "pip install failed after 3 attempts" | ||
| exit 1 | ||
|
|
||
| - name: Run benchmark suite | ||
| env: | ||
| MOSS_PROJECT_ID: ${{ secrets.MOSS_PROJECT_ID }} | ||
| MOSS_PROJECT_KEY: ${{ secrets.MOSS_PROJECT_KEY }} | ||
| # Fork PRs, and same-repo Dependabot PRs (which GitHub also denies | ||
| # normal secrets), cannot read repository secrets, so missing | ||
| # credentials are expected there and the suite may skip. On other | ||
| # trusted runs (push to main, same-repo human PRs, manual | ||
| # dispatch) missing secrets make the suite FAIL instead of | ||
| # passing as a green no-op. | ||
| ALLOW_BENCHMARK_SKIP: ${{ (github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.actor == 'dependabot[bot]')) && '1' || '0' }} | ||
| run: | | ||
| # Baseline-update runs skip the regression comparison: comparing | ||
| # against the baseline being replaced would fail the run (and skip | ||
| # the copy step) exactly when an intentional change moved the numbers. | ||
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && \ | ||
| [ "${{ github.event.inputs.update_baseline }}" = "true" ]; then | ||
| pytest benchmarks/ci/ -v \ | ||
| --benchmark-output=benchmark_results.json | ||
| else | ||
| pytest benchmarks/ci/ -v \ | ||
| --benchmark-output=benchmark_results.json \ | ||
| --baseline-file=benchmarks/ci/baseline.json \ | ||
| --latency-threshold=0.20 \ | ||
| --recall-threshold=0.05 | ||
| fi | ||
|
|
||
| - name: Upload results artifact | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: benchmark-results-${{ github.sha }} | ||
| path: benchmark_results.json | ||
| retention-days: 90 | ||
| if: always() | ||
|
|
||
| - name: Update baseline (manual trigger only) | ||
| if: >- | ||
| github.event_name == 'workflow_dispatch' && | ||
| github.event.inputs.update_baseline == 'true' | ||
| run: | | ||
| echo "Copying benchmark_results.json → benchmarks/ci/baseline.json" | ||
| cp benchmark_results.json benchmarks/ci/baseline.json | ||
| echo "New baseline (runner copy only — NOT committed):" | ||
| cat benchmarks/ci/baseline.json | ||
| echo "" | ||
| echo "To persist: download the benchmark-results-${{ github.sha }} artifact," | ||
| echo "copy it to benchmarks/ci/baseline.json, and commit." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| .env | ||
| __pycache__/ | ||
| embedding_server/__pycache__/ | ||
| ci/__pycache__/ | ||
| benchmark_results.json |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| # CI Benchmark Suite — Latency & Recall Regression Guard | ||
|
|
||
| Automated benchmark harness that runs on every push/PR to `main` and catches | ||
| performance regressions before they ship. | ||
|
|
||
| ## What it measures | ||
|
|
||
| | Metric | Description | | ||
| |--------|-------------| | ||
| | **P50 / P95 / P99 latency** | End-to-end query latency (embedding + search) in ms | | ||
| | **Mean / Stdev** | Average and standard deviation of latency | | ||
| | **Recall@5** | Fraction of ground-truth top-5 docs returned in top 5 | | ||
| | **Recall@10** | Fraction of ground-truth top-10 docs returned in top 10 | | ||
|
|
||
| All measurements use the Moss built-in embedding model (`moss-minilm`) with a | ||
| 1,000-document subset of the benchmark corpus for CI speed. | ||
|
|
||
| ## Quick start | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| Set `MOSS_PROJECT_ID` and `MOSS_PROJECT_KEY` in your environment (or in a | ||
| `.env` file). | ||
|
|
||
| ### Run locally | ||
|
|
||
| ```bash | ||
| # Install dependencies | ||
| pip install -r benchmarks/ci/requirements.txt | ||
|
|
||
| # Run the full suite | ||
| pytest benchmarks/ci/ -v \ | ||
| --benchmark-output=benchmark_results.json \ | ||
| --baseline-file=benchmarks/ci/baseline.json | ||
|
|
||
| # Skip regression checks (no baseline comparison) | ||
| pytest benchmarks/ci/ -v --benchmark-output=benchmark_results.json | ||
| ``` | ||
|
|
||
| ### Regenerate ground truth | ||
|
|
||
| Run this when the index data or model changes: | ||
|
|
||
| ```bash | ||
| python benchmarks/ci/generate_ground_truth.py | ||
| ``` | ||
|
|
||
| This queries Moss with `top_k=50` for each benchmark query and writes the | ||
| expected document IDs to `ground_truth.json`. Commit the updated file. | ||
| After a corpus or model change, pass `--recreate` so the index is rebuilt | ||
| from the current corpus before the ground truth is captured. | ||
|
|
||
| `--recreate` deletion is guarded: only the derived `benchmark-ci-<hash>` | ||
| index is deleted without confirmation. If `MOSS_INDEX_NAME` overrides the | ||
| name, deletion additionally requires `--force`, and indexes outside the | ||
| `benchmark-ci-*` namespace are never deleted — so a stray environment | ||
| variable pointing at a shared or production index cannot be destroyed. | ||
|
|
||
| > **Note**: the ground truth is a *ranking-stability reference* generated by | ||
| > Moss itself at a known-good commit — not an independent relevance judgment. | ||
| > The recall gate detects changes in retrieval behavior; an intentional | ||
| > relevance improvement will trip it and should be accompanied by a | ||
| > regenerated ground truth in the same PR. | ||
|
|
||
| ## How regression detection works | ||
|
|
||
| The harness compares the current run's metrics against `baseline.json`: | ||
|
|
||
| - **Latency**: Fails if P95 increases by more than the threshold (default 20%) | ||
| - **Recall**: Fails if Recall@5 **or** Recall@10 drops by more than the | ||
| threshold (default 5pp) | ||
|
|
||
| The latency guard arms itself from the baseline file: | ||
|
|
||
| - The checked-in placeholder declares `"latency_guard": "unarmed"` — the | ||
| latency test skips with a loud message while recall remains guarded, so | ||
| trusted runs are not red before the first baseline exists. | ||
| - To arm the guard, download the `benchmark-results-<sha>` artifact from a | ||
| trusted CI run and commit it as `baseline.json`. Artifacts carry no | ||
| `latency_guard` flag, so committing one arms the guard automatically. | ||
| - A zero p95 **without** the unarmed flag fails the run as a misconfigured | ||
| baseline — the guard can never be silently inactive by accident. | ||
|
|
||
| Thresholds are configurable via CLI flags: | ||
|
|
||
| ```bash | ||
| # --latency-threshold: max fractional P95 increase (0.15 = 15%) | ||
| # --recall-threshold: max absolute recall@5 drop (0.03 = 3pp) | ||
| pytest benchmarks/ci/ -v \ | ||
| --latency-threshold=0.15 \ | ||
| --recall-threshold=0.03 | ||
| ``` | ||
|
|
||
| ## Updating the baseline | ||
|
|
||
| After a legitimate performance change (e.g., model upgrade, index config | ||
| change), update the baseline: | ||
|
|
||
| 1. Trigger the `Benchmark` workflow manually with `update_baseline=true`. | ||
| This runs the suite **without** the regression comparison (so an | ||
| intentional change can't fail its own baseline run) and produces a fresh | ||
| `benchmark-results-<sha>` artifact. It does **not** commit anything — | ||
| the runner copy is discarded when the job ends. | ||
| 2. Download that artifact, copy it to `benchmarks/ci/baseline.json`, and | ||
| commit. Baselines should always come from CI runners — latency measured | ||
| on other hardware is not comparable. | ||
|
|
||
| ## Index naming and staleness protection | ||
|
|
||
| The benchmark index name is derived from two content hashes — | ||
| `benchmark-ci-<data_sig>-<build_fp>`: | ||
|
|
||
| - **data signature** covers the model id, `DOC_COUNT`, and the exact corpus | ||
| slice being indexed; | ||
| - **build fingerprint** covers the installed SDK/bindings versions and the | ||
| Python SDK source tree, i.e. the code that builds the index. | ||
|
|
||
| If any of those inputs change, the name changes and the index is rebuilt | ||
| from the current corpus by the current code — so a stale remote index can | ||
| never be silently benchmarked, and a PR that changes `create_index`, | ||
| document serialization, or the index/model build path always exercises that | ||
| path (an embedding change then surfaces as a recall regression instead of | ||
| passing against old embeddings). Set `MOSS_INDEX_NAME` to override the | ||
| derived name (this bypasses the staleness protection for the index itself; | ||
| the compatibility checks below still apply). | ||
|
|
||
| Superseded indexes accumulate in the Moss project as signatures change; | ||
| clean them up occasionally with | ||
| `python benchmarks/ci/generate_ground_truth.py --prune` (only | ||
| `benchmark-ci-*` indexes are ever deleted). | ||
|
|
||
| Three compatibility checks keep every comparison honest, and each **fails** | ||
| (never skips) on mismatch: | ||
|
|
||
| - `ground_truth.json` embeds the corpus/model **signature** — recall is not | ||
| evaluated against ground truth generated from different inputs. | ||
| - `ground_truth.json` also embeds a **query-set hash** — adding, removing, | ||
| or editing `QUERIES` without regenerating trips it. | ||
| - `baseline.json`'s config (signature, query-set hash, doc count, rounds, | ||
| top_k) must match the current run's config — the regression guard refuses | ||
| to compare against a baseline captured under different benchmark inputs. | ||
|
|
||
| Runs are serialized via a GitHub Actions `concurrency` group (without | ||
| cancelling in-progress runs): all jobs share one Moss project, so parallel | ||
| runs could race on index creation and contaminate each other's latency. | ||
|
|
||
| ## Credentials policy | ||
|
|
||
| - **Trusted CI runs** (push to `main`, same-repo PRs, manual dispatch): | ||
| missing `MOSS_PROJECT_ID` / `MOSS_PROJECT_KEY` **fails** the suite — a | ||
| misconfigured secret must not turn the workflow into a green no-op. | ||
| - **Fork PRs**: secrets are not available to forks, so the workflow sets | ||
| `ALLOW_BENCHMARK_SKIP=1` and the suite skips cleanly. | ||
| - **Local runs** (no `CI` env var): missing credentials skip the suite. | ||
|
|
||
| ## CI integration | ||
|
|
||
| The benchmark runs as a GitHub Actions job (`.github/workflows/benchmark.yml`). | ||
| Results are uploaded as artifacts named `benchmark-results-<sha>` and are | ||
| available for download from the Actions tab. | ||
|
|
||
| ## File overview | ||
|
|
||
| | File | Purpose | | ||
| |------|---------| | ||
| | `test_bench_ci_moss.py` | Main test module (latency, recall, regression guard) | | ||
| | `bench_queries.py` | Shared query set + index config (used by tests and generator) | | ||
| | `conftest.py` | Pytest CLI flags | | ||
| | `generate_ground_truth.py` | One-time ground truth generator | | ||
| | `ground_truth.json` | Pre-computed expected results per query | | ||
| | `baseline.json` | Performance baseline for regression checks | | ||
| | `requirements.txt` | Python dependencies | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "commit": "f4732a1", | ||
| "timestamp": "2026-07-20T05:35:00+00:00", | ||
| "latency_guard": "unarmed", | ||
| "_note": "Recall values are measured (hardware-independent) so the recall guard is active. Latency values are zero and 'latency_guard: unarmed' marks this file as the explicit placeholder: the latency regression test skips (loudly) instead of failing every trusted run. To arm the latency guard, run the Benchmark workflow on a trusted ref, download the benchmark-results-<sha> artifact, and commit it as this file \u2014 artifacts carry no latency_guard flag, so committing one arms the guard automatically. A zero p95 WITHOUT the unarmed flag fails the run as a misconfigured baseline. Latency baselines must come from CI runners; numbers from other hardware are not comparable.", | ||
| "latency_ms": { | ||
| "p50": 0, | ||
| "p95": 0, | ||
| "p99": 0, | ||
| "mean": 0, | ||
| "stdev": 0, | ||
| "count": 0 | ||
| }, | ||
| "recall": { | ||
| "recall_at_5": 1.0, | ||
| "recall_at_10": 1.0, | ||
| "queries_evaluated": 15 | ||
| }, | ||
| "config": { | ||
| "doc_count": 1000, | ||
| "query_rounds": 20, | ||
| "warmup_rounds": 3, | ||
| "top_k_latency": 5, | ||
| "signature": "73d5e83176f7", | ||
| "query_set_hash": "70d932a5a939", | ||
| "query_count": 15 | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.