Skip to content
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

fixed-point iteration support #603

Open
wants to merge 91 commits into
base: master
Choose a base branch
from
Open

Conversation

carljm
Copy link
Contributor

@carljm carljm commented Oct 23, 2024

This PR removes the existing unwind-based cycle fallback support (a plus for WASM compatibility), and replaces it with support for fixpoint iteration of cycles.

To opt in to fixpoint iteration, provide two additional arguments to salsa::tracked on the definition of a tracked function: cycle_initial and cycle_fn. The former is a function which should provide a provisional starting value for fixpoint iteration on this query, and the latter is a function which has the opportunity, after each iteration that failed to converge, to decide whether to continue iterating or fallback to some fixed value. See the added test in cycle_fixpoint.rs for details.

Usability points that should be covered in the documentation:

  • With the old cycle fallback, it was sufficient to avoid panic for at least one query in a cycle to define a cycle fallback. With fixpoint iteration, to avoid cycle panics you must define cycle_fn and cycle_initial on every query that might end up as the "head" of a cycle (that is, queried for its value while it is already executing.)
  • It is entirely possible to define cycle_fn and cycle_initial so as to cause iteration to diverge and never terminate; it's up to the user to avoid this. Techniques to avoid this include a) ensuring that cycles will converge, by defining the initial value and the queries themselves monotonically (for example, in a type-inference scenario, the initial value is the bottom, or empty, type, and types will only widen, never narrow, as the cycle iterates -- thus the cycle must eventually converge to the top type, if nowhere else), and/or b) with a larger hammer, by ensuring that cycle_fn respects the iteration count it is given, and always halts iteration with a fallback value if the count reaches some "too large" value.
  • It's also entirely possible to define cycle_fn and cycle_initial such that memoized results can vary depending only on the order in which queries occur. Avoid this by minimizing the number of tracked functions that support fixpoint iteration and ensuring initial values and fallback values are consistent among tracked functions that may occur in a cycle together.
  • You can call Salsa queries from within your cycle_fn and cycle_initial queries, but if the query you call re-enters the same cycle, it could lead to unexpected behavior. Take care what queries you call inside cycle recovery functions.

This is an RFC pull request to get initial reviewer feedback on the design and implementation. Remaining TODO items:

  • add tests for more complex cycles:
    • nested (multiple head) cycles
    • cycles with multiple paths back to the same cycle head
  • add tests for cross-thread cycles
  • add tests that call queries in cycle recovery functions
  • test in red-knot and validate it works there
  • performance improvements
    • lazy creation of initial-value memo?
  • documentation

Copy link

netlify bot commented Oct 23, 2024

Deploy Preview for salsa-rs canceled.

Name Link
🔨 Latest commit aa41186
🔍 Latest deploy log https://app.netlify.com/sites/salsa-rs/deploys/67bf42d28f6a470009cd8601

Copy link

codspeed-hq bot commented Oct 23, 2024

CodSpeed Performance Report

Merging #603 will not alter performance

Comparing carljm:fixpoint (aa41186) with master (99be5d9)

Summary

✅ 11 untouched benchmarks
🆕 1 new benchmarks

Benchmarks breakdown

Benchmark BASE HEAD Change
🆕 converge_diverge N/A 172.6 µs N/A

@nikomatsakis
Copy link
Member

This is very cool! (Admittedly, I say this pre-review.)

Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great. I left a few comments where I struggled understanding the implementation or had smaller suggestions.

@carljm
Copy link
Contributor Author

carljm commented Oct 29, 2024

In writing more comprehensive tests for this, I realized that it needs some changes to correctly handle multi-revision scenarios; taking it to Draft mode until I get that fixed.

@carljm carljm marked this pull request as draft October 29, 2024 18:15
@carljm
Copy link
Contributor Author

carljm commented Oct 30, 2024

Ok, multiple-revision cases are now fixed, and we now populate the initial provisional value only lazily, in case a cycle is actually encountered, which should reduce the number of memos created by quite a lot.

Also added a bunch of tests, including multiple-revision cases and one test involving durability. Still need to add cross-thread cycle tests.

@carljm carljm marked this pull request as ready for review October 30, 2024 00:37
Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lazy creation of the initial value is a neat improvement. Nice for taking the time to work on it !

@MichaReiser
Copy link
Contributor

MichaReiser commented Oct 30, 2024

The benchmarks show a 4-5% regression. It seems that we're now resizing some hash maps more often. Are we reporting more tracked reads than before? Could you take a look what's causing it?

@carljm
Copy link
Contributor Author

carljm commented Nov 1, 2024

Initial experiments using this in the red-knot type checker are promising: astral-sh/ruff#14029

Not yet using it for loopy control flow in that PR, but there are cycles in the core type definitions of Python builtins and standard library, which we previously had a hacky fallback in place for using Salsa's previous cycle fallback support. Moving over to fixpoint iteration just worked, and fixed the type of a builtin impacted by the cycle.

On the downside, it is a performance regression. Need to do more work there.

@nikomatsakis nikomatsakis self-assigned this Feb 19, 2025
* master:
  ci: use `release-plz` for release
  internal: switch to `boxcar` from `append-only-vec`
  Deduplicate `Storage` and `StorageHandle` fields
  More test cases for `tracked_fn_return_ref`
  Emit better diagnostic for invalid tracked function return types
  `black_box` benchmark inputs and outputs
  Introduce `RefUnwindSafe` `StorageHandle`
  add guide of releasing
  add release workflow
Copy link
Member

@nikomatsakis nikomatsakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty good. I think we gotta get it landed and start testing it.

@carljm
Copy link
Contributor Author

carljm commented Feb 20, 2025

Oops, looks like we're hitting some GitHub CI job limits.

@davidbarsky
Copy link
Contributor

No objections to the design or approach of this, but I was wondering if we could get rust-lang/rust-analyzer#18964 landed first. For the corresponding PR to land in rust-analyzer, we'll need an Salsa 1.0 alpha-1 release out with—hopefully—the ability dump the contents of tracked functions (as described in here). Those two together should should take roughly a a week or so of work, at which point salsa 1.0 alpha-2 could contain this change?

@carljm
Copy link
Contributor Author

carljm commented Feb 20, 2025

I'm not in a big rush to merge this on my side, though the merge conflicts do get exhausting to deal with. I'll add some more tests and some documentation, and work on using it in red-knot in the mean time. We may learn some new things from that.

@davidbarsky
Copy link
Contributor

davidbarsky commented Feb 20, 2025

I'm not in a big rush to merge this on my side, though the merge conflicts do get exhausting to deal with. I'll add some more tests and some documentation, and work on using it in red-knot in the mean time. We may learn some new things from that.

Yeah, I'm familiar with the rebase pains. Hopefully you won't have too many conflicts with the remaining, in-flight changes, but in the event you do, I want to set a tolerably short upper bound for my "don't land!" request.

* master:
  Reduce method delegation duplication
  Automatically clear the cancellation flag when cancellation completes
  Allow trigger LRU eviction without increasing the current revision
  Simplify `Ingredient::reset_for_new_revision` setup
  Require mut Zalsa access for setting the lru limit
  Split off revision bumping from `zalsa_mut` access
* master:
  Remove some `ZalsaDatabase::zalsa` calls
  Remove outdated FIXME
  Replace `IngredientCache` lock with atomic primitive
@carljm
Copy link
Contributor Author

carljm commented Feb 22, 2025

The current code in this branch requires Rust 1.83, for Default implementations of hashset iterators. Are we OK with this, or do we need to stay compatible with older Rust versions? If so, how old?

Cargo.toml still says rust-version = 1.76.

Main branch doesn't compile with 1.76, it only compiles with 1.80 or newer.

The tests only actually pass with 1.84 or newer, because we have snapshotted compiler output in the compile_fail tests.

So we clearly don't have any CI set up to enforce our declared MSRV.

I put up #712 to fix this.

* master:
  update MSRV to 1.80 and test it in CI
@carljm
Copy link
Contributor Author

carljm commented Feb 22, 2025

Ok, I added the necessary boilerplate here to maintain compatibility with Rust 1.80 for now.

@Veykril
Copy link
Member

Veykril commented Feb 23, 2025

Stumbled upon this issue #193, might be good to check whether that is still relevant after this PR rewriting most of the cycle handling

* master:
  Fix book deployment take 2
  Disable jemalloc decay in benches
  Fix book deployment
  Cancel duplicate test workflow runs
  Track revisions for tracked fields only
  implement `Update` trait for `hashbrown::HashMap`
  Use `db.zalsa`
  created_at documentation
  Fix bad-hash with in-place update
  Tracked struct recycling and coarse grained dependencies
  Move `unwind_if_revision_cancelled` from `ZalsaLocal` to `Zalsa`
  Don't clone strings in benchmarks
  Skip book and benchmark CI runs for `merge_group`
  Tidy up Cargo.toml dependencies
  Enforce `unsafe_op_in_unsafe_fn`
  Prevent fragmention of table due to frequent `ZalsaLocal` reconstruction
@Veykril
Copy link
Member

Veykril commented Feb 25, 2025

Another related issue #9

* master:
  fix typo
  more correct bounds on `Send` and `Sync` implementation `DeletedEntries`
  replace `arc-swap` with manual `AtomicPtr`
  Remove unnecessary `current_revision` call from `setup_interned_struct`
  Add test for durability changes
  `#[inline(never)]` queries in benchmarks
  Remove some dynamically dispatched `Database::event` calls
  Don't load `verified_at` twice in `shallow_verify_memo`
  Lazy fetching
  Merge pull request salsa-rs#3 from Veykril/veykril/push-tynvvyqoltqx
  Add small supertype input benchmark
  Replace a `DashMap` with `RwLock` as writing is rare for it
  internal: address review comments
  Skip memo ingredient index mapping for non enum tracked functions
  Trade off a bit of memory for more speed in `MemoIngredientIndices`
  fix enums bug
  Introduce Salsa enums
@carljm carljm linked an issue Feb 26, 2025 that may be closed by this pull request
@carljm carljm changed the title [RFC] fixpoint iteration support fixed-point iteration support Feb 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

Fixed-point computations and other desirable cycles
5 participants