Skip to content

[firtool] Only run verifiers at required points in the pipeline#9559

Open
youngar wants to merge 1 commit intollvm:mainfrom
youngar:firtool-verifiers
Open

[firtool] Only run verifiers at required points in the pipeline#9559
youngar wants to merge 1 commit intollvm:mainfrom
youngar:firtool-verifiers

Conversation

@youngar
Copy link
Member

@youngar youngar commented Jan 30, 2026

The MLIR PassManager defaults to verifyPasses=true, which means verification runs after every pass by default. This changes the default behaviour in firtool and explicitly disables the built-in pass manager verification when not in "--verify=all" mode, allowing verification to run only when required by a pass.

With this change, the three verification modes work as follows:

  • --verify=all: Uses MLIR's built-in pass manager verification (runs mlir::verify() after every pass, with optimization to skip when analyses are preserved)

  • --verify=default: Disables built-in verification, but runs custom verifier passes at strategic points in the pipeline where verification is required to catch user errors

  • --verify=none: Disables all verification (both built-in and custom)

The current pass pipeline is at the bottom of the description, annotated with where I have inserted verifier runs. I would like for people to review the pass list to see if anything requires verification and does not currently have it. VerifyInnerRefNamespace and VerifyObjectFields are listed as "guarded", which means they are enabled at only the All setting. I am not sure what VerifyClockedAssertLikePass is, and but it should probably be grouped up with the aforementioned passes.

One change is that the FIRRTL parser no longer runs the verifiers after parsing - it is now up to the caller to verify the IR after parsing. circt-translate --import-firrtl will still run IR verifiers after importing.

Preliminary measurement on the effect of this change shows that 40% reduction in run time, but a negligible reduction in maximum rss.

  FIR Parser
    Parse annotations
    Parse modules
@ Yes - initial verification
    CheckRecursiveInstantiation
    CheckLayers
    LowerOpenAggs
    ResolvePaths
    LowerFIRRTLAnnotations
@ Yes - LowerAnnotations has some unsafe annotation lowering
    LowerIntmodules
      LowerIntrinsics
@ Maybe - LowerIntrinsics is questionable - outside opinions needed
    SpecializeOption
    LowerSignatures
    InjectDUTHierarchy
      CSE
      PassiveWires
      DropName
      LowerCHIRRTLPass
      LowerMatches
    InferWidths
    MemToRegOfVec
    InferResets
@ Yes - InferResets relies on verifiers running to check FART results
    DropConst
    Dedup
      FlattenMemory
    LowerFIRRTLTypes
      ExpandWhens
      SFCCompat
    CheckCombLoops
    SpecializeLayers
    Inliner
      LayerMerge
      RandomizeRegisterInit
      Canonicalizer
      InferReadWrite
    LowerMemory
    IMConstProp
    AddSeqMemPorts
  CreateSiFiveMetadata
    ExtractInstances
@ Maybe - Is this one fine, or do we need verifiers?
    SymbolDCE
  InnerSymbolDCE
      EliminateWires
      Canonicalizer
      RegisterOptimizer
    IMConstProp
      Canonicalizer
  IMDeadCodeElim
      MergeConnections
      Vectorization
    LayerSink
    LowerXMR
    LowerLayers
      Canonicalizer
    AssignOutputDirs
    GrandCentral
    BlackBoxReader
    ResolveTraces
    LowerDPI
    LowerDomains
    LowerClasses
    VerifyObjectFields
@ Guard
    Lint
@ TODO: add flags for linting
  LowerFIRRTLToHW
    CSE
    Canonicalizer
  VerifyInnerRefNamespace
@ Guard
  VerifyObjectFields
@ Guard
    VerifyClockedAssertLikePass
@ What is this?
  DumpIRPass
    StripContractsPass
  LowerTestsPass
  LowerSymbolicValuesPass
  ExternalizeClockGate
  LowerSimToSV
  LowerSeqToSV
    LowerVerifToSV
  HWMemSimImpl
    CSE
    Canonicalizer
    CSE
    HWCleanup
  VerifyInnerRefNamespace
@ Guard
  VerifyObjectFields
@ Guard
    VerifyClockedAssertLikePass
@ What is this?
    HWLegalizeModules
    PrettifyVerilog
  StripDebugInfoWithPred
  HWExportModuleHierarchy
  VerifyInnerRefNamespace
@ Guard
  VerifyObjectFields
@ Guard
  ExportSplitVerilog
    HWLowerInstanceChoices
      PrepareForEmission
  FinalizeIR
  FreezePaths
  DumpIRPass

The MLIR PassManager defaults to verifyPasses=true, which means
verification runs after every pass by default. This change explicitly
disables the built-in pass manager verification when not in "all" mode,
allowing verification to run only when required.

With this change, the three verification modes work as follows:

- `--verify=all`: Uses MLIR's built-in pass manager verification
  (runs mlir::verify() after every pass, with optimization to skip
  when analyses are preserved)

- `--verify=default`: Disables built-in verification, but runs custom
  verifier passes at strategic points in the pipeline where verification
  is required to catch user errors

- `--verify=none`: Disables all verification (both built-in and custom)
@youngar youngar added the FIRRTL Involving the `firrtl` dialect label Jan 30, 2026
@seldridge
Copy link
Member

Note: most of my concerns are about the use of this option as opposed to the existence of the option itself. I like the option existing. 👍

I'm a little spooked by this. When I suggested running with verify-each=none in the past we decided against that as we do want the IR verified to be correct all the time. There is also a safety concern here for internal use as running without verification could introduce bugs. This at least deserves some thought.

How much do we save do we save by doing this, at each level of unsafe verification?

I would rather put effort into making the verifiers fast and/or changing IR structures to make verification easier. Have you looked into any performance issues in existing verifiers?

@darthscsi, WDYT?

@youngar
Copy link
Member Author

youngar commented Feb 3, 2026

I would rather put effort into making the verifiers fast and/or changing IR structures to make verification easier. Have you looked into any performance issues in existing verifiers?

Based on my profiling, I'm skeptical that we can significantly reduce the runtime overhead here. The bulk of the cost comes from fundamental operations—specifically, walking the ilist for every operation and verifying dominance—rather than dialect-specific logic. Short of simply having less IR or fewer passes, it seems these structural costs are unavoidable with the current design. We can take a look at a profile together if you want to see if you can spot some areas for improvement.

@uenoku
Copy link
Member

uenoku commented Feb 5, 2026

I must admit that I always add verify-each=0 internally to get instant speed up when I know it works fine :) I believe verifier is fine on core dialect (including OM dialect) so curios how much we can reduce runtime if we moved FIRRTL class to something more dataflow-ish.

@seldridge
Copy link
Member

specifically, walking the ilist for every operation and verifying dominance

Is this due to certain large modules or is it just due to IR size generally, specifically region size? I had thought we were seeing lots of problems with very large single-region OM-related classes which was the primary driver in the slow dominance computation.

Some thoughts:

  • Are there ways that we could speed up the dominance computation for large regions?
  • Is our IR representation fundamentally wrong in that we need to have such large regions?
  • Should we be moving to multi-region / multi-block (w/ isolated from above)?
  • Can we change the IR in other ways to not require such massive regions?

The firtool stance of trying hard to not turn off the verifier is pretty old 1. I originally wanted to turn off verifiers as it is an immediate speed boost. However, we've been able to hold this line basically since project inception in all situations except in drag racing comparisons to the SFC (which had no verification except in dedicated passes, like what this PR is enabling). MLIR's general stance is verifiers are supposed to be fast (based on only verifying local information 2) and I'd like to exhaust all efforts to keep the verifiers fast. Though, I recognize we're hitting MLIR limits due to our IR size.

@youngar
Copy link
Member Author

youngar commented Feb 5, 2026

I think the question of whether to turn off verifiers in production is orthogonal to the question of how we can make verifiers faster. Similar to how we disable asserts in production, it is not normal to ship with verifiers permanently enabled.

It seems like you are hoping that we can reduce verifier time to the point where the answer does not matter. While that is a good goal, perhaps we can decouple it from this change.

Can we merge this PR, but with the default changed to keep running verifiers? That way, we maintain the current safety profile for general users. Then, for @uenoku's use case, he can explicitly use this flag to improve speed, which is slightly safer than disabling all verifier runs entirely.

We can then move the discussion on how to speed up verification (and the specific dominance/region issues) to a new issue.

@seldridge
Copy link
Member

How about this: let's at least add upstream's -mlir-very-unsafe-disable-verifier-on-parsing (llvm/llvm-project#116287). This w/ -verify-each=false gives you the fastest behavior, like the proposed -verify=none does in this PR. That should be uncontroversial, though, extremely unsafe. The fact that that combination or the proposed -verify=none can get illegal FIRRTL to parse is almost always dangerous except for development flows.

What may also be less controversial is to put markAllAnalysesPreserved() at additional points where the pass mutation is guaranteed to not require verification or where we have knowledge that the changes introduced are exempt from verification. E.g., a pass like DropName is purely dropping attributes that are never verified (I think?) and by construction cannot create invalid IR. Possibly the same thing with StripDebugInfoWithPred. There might be other places. However, that's not going to get to a 40% savings like this patch.

it is not normal to ship with verifiers permanently enabled.

I don't think this is correct for either our flows, MLIR, or other MLIR-derived projects. Upstream guidance says verifiers must be fast, there was prior commentary about preferences to keep them enabled, and the default setting for -verify-each is true in both mlir-opt as well as IREE's main entry point. (Notably opt does default to false, though, so there is an alternative opinion from a older compiler infrastructure.) I didn't do a full survey and there may be other opinions. I wish there was clearer guidance from upstream here.

When it was discussed internally to change the default for our flows in the past (2022 was when I wanted to make the change to -verify-each=false), the conclusion was that verifiers should be made fast and we wanted to keep verifiers on. We should have a discussion on if things have changed since then.

It seems like you are hoping that we can reduce verifier time to the point where the answer does not matter. While that is a good goal, perhaps we can decouple it from this change.

That is the dream! It was also the conclusion the last time this kind of thing was brought up.

Then, for @uenoku's use case, he can explicitly use this flag to improve speed, which is slightly safer than disabling all verifier runs entirely.

More generally: Is this for developers and would they use it over -verify-each=false? Is this for users and are we saying they should use a different option from the default? If we ship the option, we'd hope somebody would use it, but both categories seem like they'd either want one of the existing options.

This one would be good to have a meeting about and an addition to the rationale with the summary of the conclusions about our stance here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FIRRTL Involving the `firrtl` dialect

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants