-
Notifications
You must be signed in to change notification settings - Fork 198
TSFC: cache the kernel by the repo state, not just the form signature #5346
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
Closed
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
f48aa22
tsfc: index the kernel cache by the code generator, not only the form
pbrubeck 500f681
tsfc: explain codegen_key()'s mtime choice and cross-process reuse
pbrubeck 61b72a0
tsfc: say caches are keyed, not indexed, by the form
pbrubeck 1483da1
tsfc: say caches key on the form, not by it
pbrubeck e746bea
tsfc: stop documenting the fixed bug in present tense
pbrubeck 7786f6c
tsfc: drop "caller" from the docstring, name what actually folds the …
pbrubeck 2bd86f0
tsfc: restructure the docstring as what, why, then how
pbrubeck b34cbed
tsfc: say "add", not "fold", in the caching.py docstring
pbrubeck 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
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
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 |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import importlib | ||
| import os | ||
| import time | ||
|
|
||
| import tsfc | ||
| import tsfc.caching | ||
| from tsfc.caching import codegen_key, stamp_source_tree | ||
|
|
||
|
|
||
| def test_stamp_source_tree_moves_when_a_file_moves(tmp_path): | ||
| (tmp_path / "a.py").write_text("x = 1\n") | ||
| stamp1 = stamp_source_tree(tmp_path) | ||
|
|
||
| # A file system timestamp can be coarser than a single Python statement, so | ||
| # force the mtime forward instead of relying on wall-clock time to pass. | ||
| a = tmp_path / "a.py" | ||
| os.utime(a, (a.stat().st_atime, a.stat().st_mtime + 1)) | ||
| stamp2 = stamp_source_tree(tmp_path) | ||
|
|
||
| assert stamp1 != stamp2 | ||
|
|
||
|
|
||
| def test_stamp_source_tree_is_stable(tmp_path): | ||
| (tmp_path / "a.py").write_text("x = 1\n") | ||
| (tmp_path / "b.py").write_text("y = 2\n") | ||
|
|
||
| assert stamp_source_tree(tmp_path) == stamp_source_tree(tmp_path) | ||
|
|
||
|
|
||
| def test_codegen_key_is_stable_between_calls(): | ||
| assert codegen_key() == codegen_key() | ||
|
|
||
|
|
||
| def test_codegen_key_reflects_toolchain_source_at_import_time(): | ||
| """This is the end-to-end property that the fix exists for. | ||
|
|
||
| A process that imports `tsfc.caching` after a file under `tsfc/` is edited | ||
| must get a different `codegen_key()`. A process that imported it before the | ||
| edit must not. `codegen_key()` is fixed at import time. It does not recompute | ||
| per compile. So this test simulates a fresh process with a reload, rather | ||
| than calling `codegen_key()` again in place. | ||
| """ | ||
| edited = tsfc.__file__ | ||
| original_mtime = os.stat(edited).st_mtime | ||
|
|
||
| key_before = codegen_key() | ||
| try: | ||
| os.utime(edited, (os.stat(edited).st_atime, original_mtime + 1)) | ||
| importlib.reload(tsfc.caching) | ||
| key_after = codegen_key() | ||
| assert key_before != key_after | ||
| finally: | ||
| os.utime(edited, (os.stat(edited).st_atime, original_mtime)) | ||
| importlib.reload(tsfc.caching) | ||
|
|
||
|
|
||
| def test_importing_tsfc_caching_is_cheap(): | ||
| """Guards against the mtime/size approach regressing to a content hash: the | ||
| one-time cost, paid when this module is imported, must stay small.""" | ||
| t0 = time.perf_counter() | ||
| importlib.reload(tsfc.caching) | ||
| elapsed = time.perf_counter() - t0 | ||
|
|
||
| assert elapsed < 1.0 |
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,88 @@ | ||
| """This module fingerprints the toolchain that generates code. | ||
|
|
||
| A kernel cache that adds this to its own key stays correct after an edit to the | ||
| toolchain that produced it. It does not stay keyed only on the form. | ||
|
|
||
| Firedrake's kernel caches add :func:`codegen_key` to their own key, alongside the form | ||
| that they key on. An edit to the toolchain then changes the key for every kernel | ||
| that it could have changed. | ||
|
|
||
| Every process computes this key once, at import, not on every compile. Two processes | ||
| that see the same toolchain files compute the same key. A later process then reuses | ||
| a kernel that an earlier process cached on disk. | ||
|
|
||
| :func:`stamp_source_tree` stats a file rather than reading it. It sees a file's path, | ||
| size, and the time that the file was last written, not the file's content. Reading | ||
| every file's content would catch more edits, but at a cost that this module cannot | ||
| pay on every import. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| import hashlib | ||
| import os | ||
| from importlib import import_module, metadata | ||
| from pathlib import Path | ||
| from typing import Hashable | ||
|
|
||
| #: Import names of the packages that TSFC's output depends on. | ||
| #: TSFC lowers through FInAT, FIAT and GEM, and generates code through UFL and loopy. | ||
| _TOOLCHAIN = ("tsfc", "finat", "FIAT", "gem", "ufl", "loopy") | ||
|
|
||
|
|
||
| def stamp_source_tree(root: os.PathLike) -> Hashable: | ||
| """Fingerprint every ``.py`` file under `root`. | ||
|
|
||
| Stats each file rather than reading it, so the cost is cheap enough to pay at | ||
| import time. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| root : os.PathLike | ||
| Directory to scan, recursively. | ||
|
|
||
| Returns | ||
| ------- | ||
| Hashable | ||
| A digest that changes when a file under `root` is added, removed, or has its | ||
| size or modification time change. | ||
| """ | ||
| root = Path(root) | ||
| entries = tuple(sorted( | ||
| (str(path.relative_to(root)), stat.st_size, stat.st_mtime_ns) | ||
| for path in root.rglob("*.py") | ||
| for stat in (path.stat(),) | ||
| )) | ||
| return hashlib.sha1(repr(entries).encode()).hexdigest() | ||
|
|
||
|
|
||
| def _is_editable(dist_name: str) -> bool: | ||
| dir_info = getattr(metadata.distribution(dist_name).origin, "dir_info", None) | ||
| return bool(getattr(dir_info, "editable", False)) | ||
|
|
||
|
|
||
| def _package_stamp(name: str, distributions: dict[str, list[str]]) -> Hashable: | ||
| module = import_module(name) | ||
| dist_names = distributions.get(name) | ||
| if dist_names and not _is_editable(dist_names[0]): | ||
| return metadata.version(dist_names[0]) | ||
| return stamp_source_tree(Path(module.__file__).resolve().parent) | ||
|
|
||
|
|
||
| # `packages_distributions()` scans every installed distribution's metadata, so it is | ||
| # called once here and shared, rather than once per package in `_TOOLCHAIN`. | ||
| _DISTRIBUTIONS = metadata.packages_distributions() | ||
| _CODEGEN_KEY: Hashable = tuple(_package_stamp(name, _DISTRIBUTIONS) for name in _TOOLCHAIN) | ||
|
|
||
|
|
||
| def codegen_key() -> Hashable: | ||
| """Fingerprint the toolchain that TSFC compiles through. | ||
|
|
||
| Two calls compare equal exactly when TSFC, FInAT, FIAT, GEM, UFL and loopy were all | ||
| unchanged at the time this module was imported. | ||
|
|
||
| Returns | ||
| ------- | ||
| Hashable | ||
| A value suitable for adding to a `cachetools` hash key. | ||
| """ | ||
| return _CODEGEN_KEY |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test
sys.prefixfor writability. If it is then use it. If it is not writable then fall back toPath.home()Put this in a separate PR. Possibly add support for
FIREDRAKE_CACHE_DIRat the same time.