Skip to content

Conversation

@rafaelha
Copy link
Contributor

This PR fixes several issues related to fixpoint rewrites:

  1. A subtle bug in Pass.fixpoint lead to rewrites being applied for max_iter iterations, even if they had already converged.
  2. Several compiler passes were incorrectly returning has_done_something=True, even if they had not done anything, leading to maxed out fixpoint loops.
  3. The CSE rewrite would prematurely exit, leading to more fixpoint iterations.

I checked that all tests in the kirin test suite lead to fixpoint rewrites that converge before reaching max_iter. It's still possible that there are more rewrite passes that need to be fixed. We should consider raising an error or at least a warning if a fixpoint pass does not converge, as this will immediately point us to any such issues in the future.

The default max_iter is set to 32. With this fix, many passes now iterate only once or twice instead of 32 times. So in many cases there is a 32x speedup. Some passes also contain a fixpoint within a fixpoint. They can now become 1000x faster.

I noticed these issues when running the following script

    @qasm2.extended  # O(n) scaling
    def main_fun():
        qreg = qasm2.qreg(1)
        for _ in range(n):
            qasm2.h(qreg[0])

    QASM2().emit(main_fun)  # ~O(n^1.5)

This script runs really slow for large n. But even more important, recording the duration of compilation and QASM2 emission, I found non-linear scaling ~ O(n^1.5). Any compiler pass must be linear in the program size (or at worst n log n).

image

With the fixes in this PR, the execution time of this script is significantly faster and the computational complexity is back to the expected O(n):
image

These issues also affect the runtime of @cduck's flair-to-squin pipeline - but I think there are additional slowdowns that I'm still looking into.

Since result was always recomputed from the join of result and result_, as soon as result_.has_done_something was set to True in the first loop iteration, result.has_done_something would never become false anymore.
This is important for fixed-point iterations because in that case, the fixed-point loop would just iterate until it reaches max iteration.
Before this fix, the following code would produce the following IR:

rule = rewrite.Fixpoint(
    rewrite.Walk(
        rewrite.Chain(
            ilist.rewrite.HintLen(),
            rewrite.ConstantFold(),
            rewrite.DeadCodeElimination(),
        )
    )
)

@basic
def len_func(xs: ilist.IList[int, Literal[3]]):
    return len(xs)

@basic
def len_func3(xs: ilist.IList[int, Any]):
    return len(xs)

rule.rewrite(len_func.code)
len_func.print()
func.func len_func(!py.IList[!py.int, Literal(3,int)]) -> !Any {
  ^0(%len_func_self, %xs):
  │  %0 = py.constant.constant 3 : !py.int
  │  %1 = py.constant.constant 3 : !py.int
  │  %2 = py.constant.constant 3 : !py.int
  │  %3 = py.constant.constant 3 : !py.int
  │  %4 = py.constant.constant 3 : !py.int
  │  %5 = py.constant.constant 3 : !py.int
  │  %6 = py.constant.constant 3 : !py.int
  │  %7 = py.constant.constant 3 : !py.int
  │  %8 = py.constant.constant 3 : !py.int
  │  %9 = py.constant.constant 3 : !py.int
  │ %10 = py.constant.constant 3 : !py.int
  │ %11 = py.constant.constant 3 : !py.int
  │ %12 = py.constant.constant 3 : !py.int
  │ %13 = py.constant.constant 3 : !py.int
  │ %14 = py.constant.constant 3 : !py.int
  │ %15 = py.constant.constant 3 : !py.int
  │ %16 = py.constant.constant 3 : !py.int
  │ %17 = py.constant.constant 3 : !py.int
  │ %18 = py.constant.constant 3 : !py.int
  │ %19 = py.constant.constant 3 : !py.int
  │ %20 = py.constant.constant 3 : !py.int
  │ %21 = py.constant.constant 3 : !py.int
  │ %22 = py.constant.constant 3 : !py.int
  │ %23 = py.constant.constant 3 : !py.int
  │ %24 = py.constant.constant 3 : !py.int
  │ %25 = py.constant.constant 3 : !py.int
  │ %26 = py.constant.constant 3 : !py.int
  │ %27 = py.constant.constant 3 : !py.int
  │ %28 = py.constant.constant 3 : !py.int
  │ %29 = py.constant.constant 3 : !py.int
  │ %30 = py.constant.constant 3 : !py.int
  │ %31 = py.len.len(value=%xs : !py.IList[!py.int, Literal(3,int)]) : !py.int
  │       func.return %0
} // func.func len_func
If the type-in for a pass was to always do something, this would lead to infinite fixpoint loops (or loops would reach max iterations)
@codecov
Copy link

codecov bot commented Oct 25, 2025

Codecov Report

❌ Patch coverage is 70.00000% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/kirin/dialects/ilist/rewrite/const.py 0.00% 8 Missing ⚠️
src/kirin/dialects/ilist/rewrite/hint_len.py 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions
Copy link
Contributor

github-actions bot commented Oct 25, 2025

PR Preview Action v1.6.2
Preview removed because the pull request was closed.
2025-10-28 15:02 UTC

@github-actions
Copy link
Contributor

github-actions bot commented Oct 25, 2025

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
11398 10160 89% 0% 🟢

New Files

No new covered files...

Modified Files

File Coverage Status
src/kirin/dialects/ilist/rewrite/const.py 60% 🟢
src/kirin/dialects/ilist/rewrite/hint_len.py 92% 🟢
src/kirin/dialects/ilist/rewrite/list.py 79% 🟢
src/kirin/passes/abc.py 100% 🟢
src/kirin/rewrite/apply_type.py 91% 🟢
src/kirin/rewrite/cse.py 89% 🟢
src/kirin/rewrite/fold.py 98% 🟢
src/kirin/rewrite/walk.py 93% 🟢
src/kirin/rewrite/wrap_const.py 100% 🟢
TOTAL 89% 🟢

updated for commit: 1c791a5 by action🐍

@rafaelha rafaelha force-pushed the rafaelha/fix-has-done-something-and-fixpoint-logic branch from e979178 to 1c791a5 Compare October 27, 2025 13:59
for _ in range(max_iter):
result_ = self.unsafe_run(mt)
result = result_.join(result)
if not result.has_done_something:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The _ is important. If the pass does something in the first iteration, result.has_done_something will always be true for all iterations.

@rafaelha rafaelha self-assigned this Oct 28, 2025
@Roger-luo Roger-luo merged commit 10b4f47 into main Oct 28, 2025
12 of 13 checks passed
@Roger-luo Roger-luo deleted the rafaelha/fix-has-done-something-and-fixpoint-logic branch October 28, 2025 15:02
Roger-luo pushed a commit that referenced this pull request Oct 28, 2025
This PR fixes several issues related to fixpoint rewrites:

1. A subtle bug in `Pass.fixpoint` lead to rewrites being applied for
`max_iter` iterations, even if they had already converged.
2. Several compiler passes were incorrectly returning
`has_done_something=True`, even if they had not done anything, leading
to maxed out fixpoint loops.
3. The CSE rewrite would prematurely exit, leading to more fixpoint
iterations.

I checked that all tests in the kirin test suite lead to fixpoint
rewrites that converge before reaching `max_iter`. It's still possible
that there are more rewrite passes that need to be fixed. We should
consider raising an error or at least a warning if a fixpoint pass does
not converge, as this will immediately point us to any such issues in
the future.

**The default `max_iter` is set to 32. With this fix, many passes now
iterate only once or twice instead of 32 times. So in many cases there
is a 32x speedup. Some passes also contain a fixpoint within a fixpoint.
They can now become 1000x faster.**

I noticed these issues when running the following script
```python
    @qasm2.extended  # O(n) scaling
    def main_fun():
        qreg = qasm2.qreg(1)
        for _ in range(n):
            qasm2.h(qreg[0])

    QASM2().emit(main_fun)  # ~O(n^1.5)
```

This script runs really slow for large `n`. But even more important,
recording the duration of compilation and QASM2 emission, I found
non-linear scaling ~ O(n^1.5). Any compiler pass must be linear in the
program size (or at worst n log n).

<img width="331" height="250" alt="image"
src="https://github.com/user-attachments/assets/c914db82-e2c6-4821-8a0e-b00ab19b432c"
/>

With the fixes in this PR, the execution time of this script is
significantly faster and the computational complexity is back to the
expected O(n):
<img width="331" height="245" alt="image"
src="https://github.com/user-attachments/assets/35480c95-29b5-4117-b796-c4cc891d1cb0"
/>

These issues also affect the runtime of @cduck's flair-to-squin pipeline
- but I think there are additional slowdowns that I'm still looking
into.
@rafaelha rafaelha changed the title Rafaelha/fix has_done_something and fixpoint logic Fix has_done_something and fixpoint logic Oct 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants