Repository navigation
Benchmarking - #410
Benchmarking#410
Conversation
# Conflicts: # pyproject.toml
…n rust in python 3.14
- asv-benchmarks.yml → benchmark-readiness.yml - asv-openmp-pr.yml → asv-openmp-pr-comparison.yml - asv-openmp-publish.yml → asv-benchmark-publish.yml - asv-openmp-comment.yml → benchmark-pr-comment.yml (new, workflow_run) Update all internal path references. benchmark-pr-comment.yml posts the ASV comparison as a PR comment via workflow_run, which runs in the base repo context and has pull-requests write access for both fork-internal and cross-fork PRs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LSchueler
left a comment
There was a problem hiding this comment.
Thanks for your great work! I think once you fixed the requested changes, this is ready to be merged.
| { | ||
| "version": 1, | ||
| "project": "GSTools", | ||
| "project_url": "https://github.com/jeilealr/GSTools", |
There was a problem hiding this comment.
Check all the URLs, they are pointing to your fork, not the original repo
There was a problem hiding this comment.
All jeilealr references are gone. Important note on this topic. There are still two things we need for the benchmarking to work. Unfortunately, I cannot do this myself due to permissions. The two missing steps are: 1) GeoStat-Framework/GSTools-Benchmarks must exist with a main branch and Pages enabled (creating an empty README.md file in main branch and pushing the changes, and then to enable Pages in repo: Settings → Pages → source "Deploy from a branch", branch main, folder / (root)). And 2) BENCHMARKS_REPO_TOKEN secret with push rights must be added to GeoStat-Framework/GSTools (on GeoStat-Framework/GSTools: Settings → Secrets and variables → Actions → New repository secret → name BENCHMARKS_REPO_TOKEN → paste the token).
There was a problem hiding this comment.
@LSchueler This will be something left to do that I cannot due to permissions
There was a problem hiding this comment.
This file is huge! I think splitting it into something like a parser and a renderer would make it more readable. I don't know a lot about html etc., but can you outsource the render_html stuff into some kind of template?
There was a problem hiding this comment.
I think you were refering in this comment to file plot_case_backend_comparison.py. I moved the content of the html into a templates folder to have the python script much more clearer. I
There was a problem hiding this comment.
I think it would be helpful to add some tests to check if ASV introduces breaking changes (API or result format) in the future.
There was a problem hiding this comment.
Yes, I agree. I've added tests/test_asv_integration.py, which runs the installed ASV on one fast case and checks the result-JSON layout, asv compare output, asv publish, and that both config files load (runs in source_check, ~7 s).
| def time_variogram_estimate(self, data, backend, case, threads): | ||
| """Run one variogram estimation case.""" | ||
| pos, field, bins = data[case] | ||
| kwargs = {} | ||
| if case == "sampled_5000_to_1500": | ||
| kwargs = {"sampling_size": 1500, "sampling_seed": 20220504} | ||
| if case == "sampled_15000_to_4500": | ||
| kwargs = {"sampling_size": 4500, "sampling_seed": 20220505} | ||
| with gstools_backend(_use_core(backend), threads): | ||
| gs.vario_estimate( | ||
| pos, | ||
| field, | ||
| bins, | ||
| mesh_type="unstructured", | ||
| return_counts=True, | ||
| **kwargs, | ||
| ) | ||
|
|
||
| def peakmem_variogram_estimate(self, data, backend, case, threads): | ||
| """Measure peak memory for variogram estimation case.""" | ||
| pos, field, bins = data[case] | ||
| kwargs = {} | ||
| if case == "sampled_5000_to_1500": | ||
| kwargs = {"sampling_size": 1500, "sampling_seed": 20220504} | ||
| if case == "sampled_15000_to_4500": | ||
| kwargs = {"sampling_size": 4500, "sampling_seed": 20220505} | ||
| with gstools_backend(_use_core(backend), threads): | ||
| gs.vario_estimate( | ||
| pos, | ||
| field, | ||
| bins, | ||
| mesh_type="unstructured", | ||
| return_counts=True, | ||
| **kwargs, | ||
| ) |
There was a problem hiding this comment.
I think you should write an internal function like _run_variogram which gets called from both peakmem_variogram_estimate and time_variogram_estimate. The same goes for kriging
There was a problem hiding this comment.
Good point. Now _run_variogram, run_krige, and run_field for the random-field class; each time*/peakmem* pair now delegates to its helper. ASV discovers the same six benchmarks with identical parameters, so existing results stay comparable and we reduced 46 lines :)
There was a problem hiding this comment.
Do you think this is necessary once your work on the benchmarks is ready? In case it is, can you include the checks into the other actions in order to reduce the CI runtimes?
|
|
||
| [tool.hatch.version.raw-options] | ||
| local_scheme = "no-local-version" | ||
| tag_regex = "^v?(?P<version>[0-9]+\\.[0-9]+.*)$" |
There was a problem hiding this comment.
Must have been there from some time and didn't remove it, I just checked and this is not needed so I removed it.
| f"**Base:** `{base}` → **Head:** `{head}`{report_link}\n\n" | ||
| f"{badge}\n\n" | ||
| "<details>\n<summary>Full ASV comparison</summary>\n\n" | ||
| f"```\n{comparison}\n```\n\n" |
There was a problem hiding this comment.
Secure this against content injection.
There was a problem hiding this comment.
Done. The comparison text can no longer close its code fence, SHAs are validated as hex, and the report link is only kept if it is an artifact URL of this repository. The workflow_run comment job now takes only the PR number and comparison text from the untrusted artifact, verifies the number against the run's head SHA, and resolves commits and artifact URL from the API. Tests added in tests/test_render_benchmark_comment.py.
Added in fc2698b as a workaround for a fork CI install failure that can no longer be reproduced. The default setuptools_scm tag regex handles the project's vX.Y.Z tags. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Hi @LSchueler Thank you for having a look at the repo, I addressed all the comments and check that everything is good to go. The only last step I left it in a comment above (first push into benchmark repo and creating BENCHMARKS_REPO_TOKEN) |
There was a problem hiding this comment.
Thanks! I think this PR is nearly finished. We don't need the comparison anymore, as we have dropped Cython-support. But for future Rust-developments, this will become very handy.
One more thing besides the other requests:
Mann-Witney U test needs the samples, which are discarded, right? Do we really need that test? If yes, do not discard the samples.
| markers = [change_marker(line) for line in comparison.splitlines()] | ||
| regressed = markers.count("+") | ||
| improved = markers.count("-") | ||
| note = "Mann-Whitney U · 5% threshold" | ||
| if regressed: | ||
| badge = f"⚠️ {regressed} benchmark(s) regressed · {note}" | ||
| elif improved: | ||
| badge = f"✅ {improved} benchmark(s) improved, none regressed · {note}" | ||
| else: | ||
| badge = f"✅ No significant changes detected · {note}" |
There was a problem hiding this comment.
This ignores the failure markers, which can result in "No significant changes detected"
There was a problem hiding this comment.
You are totally right about this, now it should be recognise and flag properly
| "number": 1, | ||
| "repeat": 20 |
There was a problem hiding this comment.
I think these are ignored by asv, they should be benchmark attributes, not configurations.
There was a problem hiding this comment.
Yes, you were right. I re-ran it and it was ignored. I created a small class in the benchmark_two_point_statistic called _TwoPointBenchmark that will get this number and repeat correctly and then it is pass to each class. I think we can keep it like that unless you want more flexibility when editing these numbers
| "number": 1, | ||
| "repeat": 20 |
There was a problem hiding this comment.
I think these are ignored by asv, they should be benchmark attributes, not configurations
| case_values = [ | ||
| item | ||
| for item in combo_values | ||
| if item not in BACKENDS and not item.startswith(THREAD_PREFIX) | ||
| ] | ||
| threads = next( | ||
| (item for item in combo_values if item.startswith(THREAD_PREFIX)), | ||
| parsed_threads, | ||
| ) | ||
| rows.append( | ||
| { | ||
| "backend": backend, | ||
| "benchmark": parsed_benchmark, | ||
| "case": "/".join(case_values) if case_values else parsed_case, | ||
| "threads": threads, |
There was a problem hiding this comment.
The thread # are part of the case strings, not part of the threads strings
| const groups = new Map(); | ||
| for (const row of data) { | ||
| const key = `${row.backend}|${row.case}|${row.threads}`; | ||
| if (!groups.has(key)) groups.set(key, []); | ||
| groups.get(key).push(row); | ||
| } | ||
|
|
||
| let svg = `<svg viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img">`; | ||
| svg += `<text x="${margin.left}" y="26" class="title">${escapeHtml(chartSubtitle(data))}</text>`; | ||
| svg += `<text x="${margin.left}" y="45" class="axis-label">${unit.label} (${unit.unit}), lower is better</text>`; | ||
| for (let i = 0; i <= 5; i++) { | ||
| const value = (maxValue / 5) * i; | ||
| const yy = y(value); | ||
| svg += `<line x1="${margin.left}" x2="${width - margin.right}" y1="${yy}" y2="${yy}" class="grid" />`; | ||
| svg += `<text x="${margin.left - 10}" y="${yy + 4}" text-anchor="end" class="tick">${formatNumber(value)}</text>`; | ||
| } | ||
| xKeys.forEach(xKey => { | ||
| const xx = x(xKey); | ||
| const row = rows.find(item => rowXKey(item) === xKey); | ||
| svg += `<text x="${xx}" y="${height - 56}" text-anchor="middle" class="tick" transform="rotate(-25 ${xx} ${height - 56})">${escapeHtml(xKey)}</text>`; | ||
| if (row && row.date_label !== "-") { | ||
| svg += `<text x="${xx}" y="${height - 30}" text-anchor="middle" class="tick">${escapeHtml(row.date_label)}</text>`; | ||
| } | ||
| }); | ||
|
|
||
| Array.from(groups.entries()).forEach(([key, group], index) => { | ||
| const color = linePalette[index % linePalette.length]; | ||
| const byXKey = new Map(group.map(row => [rowXKey(row), row])); |
There was a problem hiding this comment.
You have the info on the environment where the benchmarks run, but they are ignored in the graphs. This means that results from completely different machines are treated the same, leading to wrong graphs. Either add environment selection to graphs or reject from different environments
There was a problem hiding this comment.
I added a Machine filter. The data already carried per-row machine/OS/CPU info, but yes it graph ignored it, with this machine selector as single choice the results from different machines will never merge :). That was a good catch thanks
| PR_HEAD_SHA=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \ | ||
| --jq '.head.sha') | ||
| if [[ "$PR_HEAD_SHA" != "$HEAD_SHA" ]]; then | ||
| echo "::warning::PR #$PR_NUMBER head does not match the" \ | ||
| "benchmarked commit $HEAD_SHA; skipping comment." | ||
| exit 0 | ||
| fi | ||
| BASE_SHA=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \ | ||
| --jq '.base.sha') |
There was a problem hiding this comment.
This looks like it could run into problems, if the main branch gets updated
There was a problem hiding this comment.
AGain. good catch. solved :)
|
One last question that is no clear to me. should I then drop now all the benchmarking Cython related scripts or should we do that for the next release? (I think for the next one will be better, but I wonder) |
LSchueler
left a comment
There was a problem hiding this comment.
Thanks for the edits!
I found a few last(?) things that have to change.
Drop LAST_CHAT.md, AGENTS.md, direct_sampling_mps_guide.md
Write Benchmarks for Current DS-MPS Implementation
Summary
This PR adds a reproducible benchmarking setup for GSTools using
Airspeed Velocity plus a small complementary
cProfilehelper.The goal is to establish a measurement baseline before optimizing or extending
the DS-MPS implementation. The benchmarks focus on representative GSTools
workflows rather than isolated micro-functions:
The suite can compare the current Cython fallback backend with the Rust backend
from
gstools_core, and it can report both runtime and peak-memory results.What Changed
asv.conf.json.benchmarks/benchmark_backends.py.gstools-cythoninstallation inside ASVasv.macos-openmp.conf.json.benchmarks/README.md.benchmarkoptional dependency extra.Most of the detailed usage instructions, benchmark case explanations, and
interpretation notes are intentionally kept in
benchmarks/README.mdto avoidduplicating the guide in this PR description.
Why ASV and cProfile?
ASV is used for repeatable benchmark results across commits. This makes it
useful for detecting performance regressions and tracking how workflows change
over time. It is also the standard benchmarking tool used by several scientific
Python projects, including NumPy, SciPy, pandas, scikit-image, and Astropy.
cProfileis used for a different purpose: it profiles one selected workflow inthe current checkout and shows where runtime is spent internally. In this PR,
the cProfile helper imports the same benchmark classes used by ASV, so the
profiling cases stay aligned with the benchmark suite.
Initial Observations
The initial benchmark runs show that Rust is generally faster for larger
variogram and kriging workloads. For very small cases, Rust can be similar or
slightly slower because fixed overhead dominates. With one thread, Rust is not
always faster for every SRF case, so the backend comparison is workflow- and
problem-size-dependent.
The cProfile results suggest that the slowest parts of the measured workflows
are mostly inside compiled backend/numerical routines. That means Python-level
cleanup is unlikely to be the main optimization path for these cases. The next
useful optimization work should focus on algorithmic cost reduction and backend
implementation details.
Follow-Up Tasks
share one generalized OpenMP setup.
current migration work is ready to benchmark.