Skip to content

Commit

Permalink
[FIRRTL][NFC] Optimization Modes Rationale (#3959)
Browse files Browse the repository at this point in the history
Add information to the FIRRTL Rationale document about optimization
modes/name preservation modes.  Include discussion of how things
currently work, examples of what this means, and a short discussion of
alternatives considered.

Co-authored-by: Mike Urbach <[email protected]>
Signed-off-by: Schuyler Eldridge <[email protected]>
  • Loading branch information
seldridge and mikeurbach authored Sep 21, 2022
1 parent f83d57c commit 11b6314
Showing 1 changed file with 189 additions and 3 deletions.
192 changes: 189 additions & 3 deletions docs/Dialects/FIRRTL/RationaleFIRRTL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ used and transformed. This must specify which entities with names in Chisel
generate predictable output. Since names serve multiple purposes in a design,
for example, debugging, test-bench attachment, hooks for physical layout, etc,
we must balance multiple needs. This section describes the base semantics,
which are conservative and aimed at enabling debugging. _It is expected that
the compiler provide flags to relax these rules to produce more
synthesis-friendly or production-ready output_.
which are conservative and aimed at enabling debugging. The CIRCT
implementation of a FIRRTL compiler provides options to change the name
preservation behavior to produce more debuggable or more optimized output.

Modules shall use the name given in Chisel, unless they conflict with a Verilog
reserved word, not withstanding de-duplication or relevant annotations on the
Expand Down Expand Up @@ -142,6 +142,192 @@ by Chisel typically look like `_T_12`, and names generated by the SFC look like
array attribute `annotations` containing the attribute `{class =
"firrtl.transforms.DontTouchAnnotation}`.

Chisel-generated temporaries will not be discarded in compilation modes which
preserve all names.

### Name Preservation Modes

Name preservation modes, compiler options that produce different name
preservation behavior, was implemented as a compromise between two divergent and
seemingly irreconcilable goals:

1. A FIRRTL to Verilog compiler should apply heavy optimizations to improve its
own performance (early optimizations produce smaller IR which means later
passes need to do less work) and to improve the performance of tools
consuming output Verilog, e.g., Verilog simulator compilation and run time.

2. Chisel users (design and verification engineers) want to see a one-to-one
correspondence between what they write in Chisel and the Verilog that a
FIRRTL compiler produces to enable debuggability.

These two goals are viewed as irreconcilable because certain increases to
optimizations (1) necessarily detract from debuggability (2).

Currently CIRCT's FIRRTL compiler, `firtool`, provides two optimization modes,
debug and release, as well as finer grained options with lower-level flags:

1. `-O=release` (or `-preserve-values=none`) may delete any component as part of
an optimization.
2. `-O=debug` (or `-preserve-values=named`) keeps components with names that do
not begin with a leading underscore.
3. `-preserve-values=all`, which has no exposed `-O` option, keeps all
components.

As an example of these modes consider the following FIRRTL circuit:

``` firrtl
circuit Foo:
module Foo:
input a: UInt<1>
output b: UInt<1>
node named = a
node _unnamed = named
b <= _unnamed
```

When compiled with `-O=release` (or `--preserve-values=none`), no intermediary
nodes/wires are preserved because CIRCT inlines the usages of `a` into the
assignment to `b`:

``` verilog
module Foo(
input a,
output b);
assign b = a;
endmodule
```

When compiled with `-O=debug` (or `-preserve-values=named`), the `_unnamed` node
is removed, but the `named` node is preserved:

``` verilog
module Foo(
input a,
output b);
wire named = a;
assign b = named;
endmodule
```

When compiled with `-preserve-values=all` this produces the following Verilog
that preserves all nodes, regardless of name:

``` verilog
module Foo(
input a,
output b);
wire named = a;
wire _unnamed = named;
assign b = _unnamed;
endmodule
```

Design teams are expected to use `-O=debug` debuggabilty. Verification teams
and downstream tools are expected to use/consume `-O=release`.

This split of two different Verilog outputs initially created reproducibility
problems that CIRCT has attempted to solve with a guarantee of stable
randomization. Consider a situation where a verification team trips an
assertion failure using a release build with a particular seed. Because release
Verilog is highly optimized and difficult to debug, they want to switch to a
debug build. If the release build seed does not reproduce the failure in debug
mode, the verification team needs to search for a failure. This proved to be a
drag on Chisel users. Towards alleviating this, CIRCT will now guarantee that
registers in debug or release mode will be randomized to the same value for the
same seed.

It is important to note that the debug/release split was born out of our
inability to reconcile the goals at the top of this section. Discussion in the
subsequent section involves approaches to unify these two approaches.

#### Alternative Approaches to Name Preservation Modes and Historical Background

The following alternatives were implemented or considered instead of the
debug/release solution.

First, we created dead wire taps _with symbols_ for all "named" things in the
original FIRRTL design. We would then try to use these dead wire taps in place
of unnamed things when possible. This simple solution produced much more
readable Verilog. However, this also had a number of problems. Namely, leaving
in dead wire taps would result in situations where ports that downstream users
were expecting to be removed were not. E.g., a module with dead wire taps would
result in more ports at a physical design boundary. Additionally, leaving in
dead wire taps may introduce coverage holes for verification teams. We
attempted to remove dead wire taps when possible. However, this was problematic
as we had given them symbols which indicates that they may have external readers
(e.g., from a manually written testbench) and was intended to indicate that
later passes could never remove these. We considered using an alternative to a
symbol, but this was rejected due to its highly special-cased nature---it was
forcing us to communicate a Chisel expectation/semantic all the way to HW/SV
dialects.

These drawbacks are unfortunate because they stem from learned expectations of
how the Scala-based FIRRTL Compiler worked. A negative view of this is that
some level of optimization was required for a learned definition of correctness.
If CIRCT was the first FIRRTL compiler, we may have been able to circumvent
these problems with alternative means that included modifications to Chisel.

Second, we considered having CIRCT create "debug modules" that included all
named signals in the design. An instance of this debug module would then be
instantiated, via a SystemVerilog `bind` statement, inside the original module.
This was an early suggestion. However, a concern of users of any "debug module"
is that the debug module would not show usages of the named signals. E.g., the
example circuit shown above would compile to something like:


``` verilog
module Foo_debug(
input _0;
wire named = _0;
endmodule
bind Foo Foo_debug Foo_debug (
._0(a)
);
module Foo(
input a,
output b);
assign b = a;
endmodule
```

The main concern is that while users can see the value of `named` in a waveform,
they cannot trace back its usage in the computation of port `b` in module `Foo`.
This approach also suffers from the issues of the first approach of leaving in
ports and dead logic (that is only used when a debug instance is bound in).

This approach may be revisited in the future as it provides benefits of unifying
debug and release builds into a single release build with run-time debugging
information that can be bound in. Additionally, use of FIRRTL `RefType`s that
lower to Verilog cross-module references (XMRs) may alleviate some of the issues
above.

Third, a single build that always preserved names was considered. At the time,
this introduced long Verilog compilation and simulation times. We were not able
to discern an optimization design point which balanced the needs of
debuggability with compilation and simulation performance. This does not mean
that such a point does _not_ exist, only that we were not able to find it. Such
a design point may exist and should be investigated.

Since all these efforts happened, other work has occurred which may make
reviving these efforts a fruitful endeavor. FIRRTL now has `RefType`s which are
operations which lower to Verilog cross-module references (XMRs). This may
provide a mechanism to implement the "bound debug instance" approach above
without perturbing port optimizations. Reliance on symbols to encode
optimization blocking behavior has been largely rolled back. A
`DontTouchAnnotation` is now encoded as an annotation as opposed to a symbol. A
new inter-module dead code elimination (IMDCE) pass was implemented which
handles port removal. The approaches above, or new approaches, may be able to
build a better name preservation approach, but with certain optimizations
enabled.

## Symbols and Inner Symbols

Expand Down

0 comments on commit 11b6314

Please sign in to comment.