llvm / circt

Circuit IR Compilers and Tools
https://circt.org
Other
1.62k stars 281 forks source link

[FIRRTL] Debug Mode / Add a Mechanism to Preserve Names #2488

Closed seldridge closed 2 years ago

seldridge commented 2 years ago

tl;dr: this is a proposal for a "debug mode" which enables specific CIRCT signals to always show up in the output, but also to not block optimizations. This is entirely based off of ideas of and discussions with @fabianschuiki and @darthscsi.

Consider the following input FIRRTL circuit:

circuit Foo:
  module Foo:
    input a: UInt<8>
    input b: UInt<8>
    output c: UInt<8>
    output d: UInt<8>
    input x: UInt<1>

    node foo = not(x)
    node bar = xor(x, UInt<1>(1))

    c <= mux(foo, a, b)
    d <= mux(bar, a, b)

Currently, CIRCT produces the following Verilog:

module Foo( // Foo.fir:2:10
  input  [7:0] a, b,
  input        x,
  output [7:0] c, d);

  wire [7:0] _T = x ? b : a;    // Foo.fir:12:10
  assign c = _T;    // Foo.fir:2:10
  assign d = _T;    // Foo.fir:2:10
endmodule

A combination of canonicalization not(x) -> xor(x, 1) and mux(~cond, left, right) -> mux(cond, right, left), inlining, and CSE result in all internal names (foo and bar) being lost. A user who wrote the above code in Chisel may care deeply that foo and bar exist so they can debug their code.

Analogously, this is like the compiler optimizing away a printf in C. There's no way for the user to reconstruct foo or bar in their waveform. This makes for grumpy users.

The SFC, entirely coincidentally, preserves both foo and bar because it doesn't implement not(x) -> xor(x, 1) canonicalization. With the SFC you get:

module Foo(
  input  [7:0] a,
  input  [7:0] b,
  output [7:0] c,
  output [7:0] d,
  input        x
);
  wire  foo = ~x;
  wire  bar = x ^ 1'h1;
  assign c = foo ? a : b;
  assign d = bar ? a : b;
endmodule

Note that if foo and bar canonicalize in the SFC to the same thing (i.e., you use different expressions for which the SFC implemented those canonicalizations), you will wind up with only foo in the output.

This is enormously problematic because CIRCT then has diametrically opposed goals:

  1. Build a fast compiler with the best optimizations.
  2. Produce readable Verilog that users can debug.

Relatedly, it's unclear that we can get ourselves out of this with only better naming. E.g., what should _T be called here? It's just c_or_d, but that's not useful. What to do with foo and bar? They're totally gone and there's no good way to kludge them back in.

Both @fabianschuiki and @darthscsi have suggested great ideas in this space that I try to synthesize below. When we're confronted with a situation like this where no good name is possible we need a mechanism to not block optimizations while still preserving the original names. This can either be achieved with a new type of "dont touch" / symbol that guarantees that something will exist, but doesn't block optimizations. We could then produce the hypothetical:

module Foo( // Foo.fir:2:10
  input  [7:0] a, b,
  input        x,
  output [7:0] c, d);

  // Debug Info
  wire foo = ~x;
  wire bar = x ^ `1'h1;

  // Module body
  wire [7:0] _T = x ? b : a;    // Foo.fir:12:10
  assign c = _T;    // Foo.fir:2:10
  assign d = _T;    // Foo.fir:2:10
endmodule

This can then be further improved by extracting the debug logic to a bound instance like how FIRRTL dialect test code extraction or Grand Central works:

module Foo_DEBUG(
  input x;
  wire foo = ~x);
  wire bar = x ^ 1'h1;
endmodule;

module Foo( // Foo.fir:2:10
  input  [7:0] a, b,
  input        x,
  output [7:0] c, d);

  wire [7:0] _T = x ? b : a;    // Foo.fir:12:10
  assign c = _T;    // Foo.fir:2:10
  assign d = _T;    // Foo.fir:2:10
endmodule

For the above to have no effect on the final circuit Foo, it may be necessary to duplicate all logic (that affects named signals) from Foo into Foo_DEBUG so that the effect of this "debug mode" doesn't result in a different circuit Foo.

Note: This isn't saying that we shouldn't try to generate good names, just that we need some fallback debug mode when good names aren't possible.

This is related to #2470.

darthscsi commented 2 years ago

I think there is value in a debug-mode compile for firrtl. This would promote nodes to wires and don't touch all wires. That should be sufficient to ensure all names survive.

That said, we can also do a better job not dropping names. This means the canonicalization which removes names from nodes and then removes nodes needs to instead try to move the name from a node to the expression feeding it as a name attribute. This won't prevent optimizations, but will ensure that if an expression is spilled, then the name of the node it fed in the fir file will be used.

I am less thrilled than I used to be with the debug-view idea because it does interfere with optimization and leave dead-code in the production-path module.

seldridge commented 2 years ago

Fixed in: https://github.com/llvm/circt/pull/2676.