Skip to content

opt_signif: carry operand ranges across module boundaries - #284

Closed
akashlevy wants to merge 1 commit into
mainfrom
qor/signif-cross-hier
Closed

opt_signif: carry operand ranges across module boundaries#284
akashlevy wants to merge 1 commit into
mainfrom
qor/signif-cross-hier

Conversation

@akashlevy

Copy link
Copy Markdown

Summary

opt_signif analysed one module at a time, so an instance was an opaque driver whose output port contributed its full declared width. A dot-product lane behind a module boundary therefore had its accumulate tree priced at the whole datapath bus, even though the lane's exponent range can only reach a fraction of it.

That is the root cause of the sawtooth we have been chasing in a customer's block-scaled MX dot products. The dead fraction scales with how narrow the format is, so the error was strongly family-dependent: on a 64-bit bus an e2m1 lane occupies 11 bits and over-counted by about 2x, while e4m3 occupies 39 bits and over-counted by about 1.2x. Adjacent points in their sweep alternate between families, which is what produced teeth rather than a uniform offset.

Four changes:

  • Hierarchical propagation. HierCache computes and caches output-port ranges of instantiated modules; sig_range consults it for instances. Driver gained cell/port/offset so built-in cells and instances share one path instead of special-casing ports named Y.
  • -summarize. Flows that blackbox modules before narrowing them have no body left to read. This stamps ranges onto output ports as signif_range_signed / signif_range_unsigned while the bodies still exist, and a later run reads them back off the blackbox. -no-cross-hier disables the feature entirely.
  • Unsigned range fit. cell_range judged every range by its signed width, discarding any non-negative range that reaches into the top half of its word. A 6-bit word holding [0,45] is a block-scaled lane's exponent sum, and it was being thrown away. sig_range still re-checks per reader, so a signed reader of such a word falls back to the declared range.
  • -min-bits <n>. Skips narrowings saving fewer than n bits. arith_tree only chains a cell whose output is read whole, so shaving one bit off a parent's operand port can drop a sum below carry_save_min_width and cost a whole compressor level. This was a real regression on tile_mul_shift, where a 29-bit adder stopped forming a 30-bit compressor.

Measured effect

Reproduced the customer's sweep locally (50 designs, asap7). Comparing predicted area against their reported real area, with -no-cross-hier standing in for the shipped behaviour:

configuration median pred/real spread rank corr mean err
without hierarchical propagation 1.93 7.93x 0.389 157%
with it 0.97 1.80x 0.961 10%

Flattening the design reaches the same place (0.96 / 1.88x / 0.961), which is a useful workaround for users today but costs runtime and defeats parallel batching on large designs.

Test plan

  • tests/opt/opt_signif.ys passes; new groups cover single-level and two-level hierarchy, unbounded submodule inputs as a negative control, blackboxes with narrow and wide ports, repeated instances of one module, an instance with a port named Y, the attribute round-trip including stale-attribute removal, wide constant words under -cross-flops, and the -min-bits threshold
  • Formal equivalence proof of hierarchical narrowing via the gold/gate flatten idiom
  • Rebuilt and re-ran against current main (42 commits past the original base)
  • Downstream Preqorsor regression suite green: 354 passed, 8 xfailed

Made with Cursor

The pass analysed one module at a time, so an instance was an opaque driver and
its output port contributed its declared width. A dot-product lane behind a
module boundary therefore priced its accumulate tree at the full datapath bus
even though the lane can only ever reach a fraction of it, which is where the
sawtooth in block-scaled MX designs came from: the narrower the format, the
larger the dead fraction, so e2m1 over-counted about twice as much as e4m3.

HierCache computes and caches the output-port ranges of instantiated modules and
sig_range now consults it for instances. Driver grew cell/port/offset so built-in
cells and instances take one path rather than special-casing ports named Y.

Flows that blackbox modules before narrowing them (parallel synthesis hands each
worker only its own batch) have no body left to read, so -summarize stamps the
ranges onto output ports as signif_range_signed/signif_range_unsigned attributes
while the bodies are still present, and a later run reads them back off the
blackbox. -no-cross-hier turns the whole thing off.

Two fixes fall out of exercising this on real datapaths:

  - cell_range judged every range by its signed width, which discarded any
    non-negative range reaching into the top half of its word. A 6-bit word
    holding [0,45] is a block-scaled lane's exponent sum, and it was being
    thrown away. Judge by unsigned width when the range cannot go negative;
    sig_range still re-checks per reader, so a signed reader of such a word
    falls back to the declared range.

  - -min-bits <n> skips narrowings that save fewer than n bits. arith_tree only
    chains a cell whose output is read whole, so shaving one bit off a parent's
    operand port can drop the sum below carry_save_min_width and cost an entire
    compressor level -- a real regression on tile_mul_shift, where a 29-bit
    adder stopped forming a 30-bit compressor.

Tests cover the hierarchy cases (single and two-level, unbounded inputs as a
negative control, blackboxes, repeated instances, and an instance with a port
named Y), the attribute round-trip including stale-attribute removal, wide
constant words under -cross-flops, the -min-bits threshold, and a formal
equivalence proof of hierarchical narrowing via the gold/gate flatten idiom.

Co-authored-by: Cursor <cursoragent@cursor.com>
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR extends opt_signif range analysis across module boundaries and preserves summaries through blackboxing, while adding controls for disabling hierarchy traversal and filtering low-value narrowings.

  • Caches signed and unsigned output-port ranges for instantiated modules.
  • Adds -summarize, -no-cross-hier, and -min-bits options.
  • Improves handling of wide constants and non-negative ranges reaching the top half of a word.
  • Adds hierarchy, blackbox round-trip, wide-constant, threshold, and equivalence tests.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code failure identified.

Hierarchical ranges are computed with unconstrained child inputs, selected by reader signedness, checked against consumed widths, and conservatively fall back to declared ranges when analysis is unavailable; the added tests cover the principal new execution paths.

Important Files Changed

Filename Overview
passes/opt/opt_signif.cc Adds conservative hierarchical range propagation, persisted blackbox summaries, wide-constant handling, and a minimum-savings threshold; no changed-code defect was established.
tests/opt/opt_signif.ys Expands regression coverage for hierarchy traversal, summary attributes, repeated instances, boundary constants, narrowing thresholds, and formal equivalence.

Sequence Diagram

sequenceDiagram
    participant Parent as Parent SignifWorker
    participant Cache as HierCache
    participant Child as Child SignifWorker
    Parent->>Cache: Request instance output range
    alt Summary not cached
        Cache->>Child: Analyze output port with unconstrained inputs
        Child-->>Cache: Signed and unsigned ranges
        Cache->>Cache: Cache ranges by module and port
    end
    Cache-->>Parent: Range for reader signedness
    Parent->>Parent: Validate range fits consumed bits
    Parent->>Parent: Narrow eligible operand if savings meet min-bits
Loading

Reviews (1): Last reviewed commit: "opt_signif: carry operand ranges across ..." | Re-trigger Greptile

@akashlevy
akashlevy marked this pull request as draft August 21, 2026 13:51
@akashlevy

Copy link
Copy Markdown
Author

Closing for now, as we need a better test case before we can continue here

@akashlevy akashlevy closed this Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant