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

No inlining let-bound global vars with clock types #2846

Merged
merged 1 commit into from
Dec 3, 2024
Merged

Conversation

christiaanb
Copy link
Member

The global vars are usually backed by a clock generator that are not work-free.

In addition, when these global vars are recursively defined, they can mess up the post-normalization flattening stage which then violates certain invariants of the netlist generation stage. This then causes the netlist generation stage to generate bad Verilog names.

Fixes #2845

Still TODO:

  • Write a changelog entry (see changelog/README.md)
  • Check copyright notices are up to date in edited files

Copy link
Member

@DigitalBrains1 DigitalBrains1 left a comment

Choose a reason for hiding this comment

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

I think we should backport this to 1.8?

Furthermore, it wouldn't surprise me if the reproducer in clash-testsuite that is added here no longer reproduces the issue once issue #2570 is truly fixed. This is just something I realised and wanted to point out.

Copy link
Member

@leonschoorl leonschoorl left a comment

Choose a reason for hiding this comment

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

Are you aware that bindConstantVar already checks for (vars with) local ids?

test _ (i,stripTicks -> e) = case isLocalVar e of
-- Don't inline `let x = x in x`, it throws us in an infinite loop
True -> return (i `notElemFreeVars` e)

I think that means that your (isLocalId v) will be False, except possibly when we recurse inside isWorkFreeIsh.

Comment on lines +158 to +160
-- Only local variables with a clock type are work-free. When it is a global
-- variable, it is probably backed by a clock generator, which is definitely
-- not work-free.
Copy link
Member

Choose a reason for hiding this comment

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

The local variable distinction feel arbitrary to me.

Given:

clk = clockGen

clkGlobal = clk

f =
 let
   clkLocal = clk
   clkA = clkLocal
   clkB = clkGlobal
 in [...]

As I understand it, you're saying here it is ok for bindConstantVar to replace clkA inside of [...] with clkLocal, but not clkB with clkGlobal.
Is that what you're saying?
And if so, why?
They seem equivalent to me.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that's what I'm saying. Inlining clkA everywhere will not make the circuit f any larger (perform work). Inlining clkB everywhere will make the circuit f larger, because it will have duplicated calls to what will ultimately be clockGen.

--
-- Inlining let-bindings referencing a global variable with a clock type
-- can sometimes lead to the post-normalization flattening stage to generate
-- code that violates the invariants of the netlist generation stage.
Copy link
Member

Choose a reason for hiding this comment

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

Do you remember which invariants?
And are these invariants actually documented anywhere?

Copy link
Member Author

Choose a reason for hiding this comment

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

We cannot have let-expressions appear in the argument position of an application.

@christiaanb
Copy link
Member Author

Are you aware that bindConstantVar already checks for (vars with) local ids?

test _ (i,stripTicks -> e) = case isLocalVar e of
-- Don't inline `let x = x in x`, it throws us in an infinite loop
True -> return (i `notElemFreeVars` e)

I think that means that your (isLocalId v) will be False, except possibly when we recurse inside isWorkFreeIsh.

I am aware. But prior to this commit/PR, the compiler would inline let-bindings binding global variables with a clock type, which is bad because it duplicates work/increases the size of the circuit..

Do not inline let-bound recursive calls
@christiaanb
Copy link
Member Author

I think @leonschoorl comments correctly identify a missed CSE oppertunity in that the current implementation will, for:

clk = clockGen

clkGlobal = clk

f =
 let
   clkLocal = clk
   clkA = clkLocal
   clkB = clkGlobal
 in [...]

not transform f such that ultimately there is only one application/call of the clockGen primitive. While it is most likely desirable in increase sharing in this case, that has never been a guarantee/must of the Clash compiler. That's because CSE is not always desirable as it increases fanout. Decreasing sharing should however be considered a bug, which this PR actually fixes.

That being said, once this PR is merged, we should probably open an "enhancement" issue that Clash is missing a desirable CSE opportunity.

@DigitalBrains1
Copy link
Member

I'd like to call your attention to the point that there can ever only be one clock primitive in one domain.¹ Two clock primitives might generate the same frequency, but they are probably not perfectly phase-aligned, making them separate clock domains. So with constructions like clkGlobal = clk, you have to wonder: is this ever actually going to occur in a valid design? Or is this already user error since they are now putting things in the same domain even though they are not synchronised to the same clock? If I want stuff to be in the same clock domain, I pass them all the exact same bind of the clock variable, not clk to one and clkGlobal to another.

¹ The other way around is fine: you could have one clock primitive which outputs multiple clocks.

@christiaanb christiaanb merged commit 47b8fa7 into master Dec 3, 2024
13 checks passed
@christiaanb christiaanb deleted the fix_2845 branch December 3, 2024 12:06
@DigitalBrains1
Copy link
Member

DigitalBrains1 commented Dec 7, 2024

Mergify was not opening a backport PR so I tried to delete and re-add the label. Didn't work; but I remembered you can look at Actions for Mergify stuff. Mergify reports:

error: commit 6f9b7300da535bae01da67eeac99dd82f2cac965 is a merge but no -m option was given.
fatal: cherry-pick failed

and boy something has gone wrong with this PR. Just admire this nice shape:

$ git log --graph --oneline --no-show-signature origin/master
* 8a827e1e2 (origin/master) GitLab CI: Use Stackage Docker image (#2805)
*   47b8fa7c8 Merge pull request #2846 from clash-lang/fix_2845
|\  
| *   6f9b7300d Merge pull request #2844 from clash-lang/fix_2839
| |\  
* | | 8cded4cbe Move block RAM reset value into the `ClearOnReset` constructor of `ResetStrategy`, (#2849)
* | |   25a7bc18f Merge pull request #2844 from clash-lang/fix_2839
|\ \ \  
| |/ /  
|/| /   
| |/    
| * 00722413f Do not inline let-bound recursive calls
* | f13b8534e Add `Distributive` and `Representable` instances to `Vec n` (#2829)
|/  
* 10f26ff16 (gronau/master) Reduce `splitAt` to undefined in illegal contexts (#2837 

I don't know whatever happened here, but yeah, I don't blame Mergify not being able to backport this... D-:

I think PR #2846 was at one point based on PR #2844, and then rebased incorrectly somehow.

@DigitalBrains1
Copy link
Member

Commit b13b96f still looks fine, but then it got force-pushed to 6f9b730 and that is a merge commit with contents. It merges master and the commit from #2844, but its tree contains the changes of this PR instead of just being the combination of its parent commits.

@christiaanb
Copy link
Member Author

christiaanb commented Dec 7, 2024

I have no idea what happened here. My regular way of working is:

  • git pull —rebase origin/master
    And then after solving conflicts do a
  • git commit
  • git rebase —continue
  • git push -f
    I guess I should have done a
  • git fetch origin
    Before the rebase?

@DigitalBrains1
Copy link
Member

DigitalBrains1 commented Dec 7, 2024

pull should already include a fetch. So you shouldn't need to do fetch yourself before a pull.

I also don't know what went wrong, and we actually lost your commit message of this PR in the process. I'm currently handcrafting a backport PR.

@DigitalBrains1
Copy link
Member

The intended commit message of this PR is identical to the PR cover letter.

DigitalBrains1 pushed a commit that referenced this pull request Dec 7, 2024
The global vars are usually backed by a clock generator that
are not work-free.

In addition, when these global vars are recursively defined,
they can mess up the post-normalization flattening stage which
then violates certain invariants of the netlist generation stage.
This then causes the netlist generation stage to generate bad
Verilog names.

Fixes #2845

(The PR on master was malformed somehow, with the correct contents but
missing the commit message above and an incorrect Git structure. This
backport to 1.8 is reconstructed by hand.)
DigitalBrains1 added a commit that referenced this pull request Dec 7, 2024
The global vars are usually backed by a clock generator that
are not work-free.

In addition, when these global vars are recursively defined,
they can mess up the post-normalization flattening stage which
then violates certain invariants of the netlist generation stage.
This then causes the netlist generation stage to generate bad
Verilog names.

Fixes #2845

(The PR on master was malformed somehow, with the correct contents but
missing the commit message above and an incorrect Git structure. This
backport to 1.8 is reconstructed by hand.)

Co-authored-by: Christiaan Baaij <[email protected]>
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.

Clash generates invalid verilog names
4 participants