Skip to content

Add per-object origin tracking (vOrigins) to Gia_Man_t#487

Open
robtaylor wants to merge 5 commits intoberkeley-abc:masterfrom
robtaylor:origin-tracking-clean
Open

Add per-object origin tracking (vOrigins) to Gia_Man_t#487
robtaylor wants to merge 5 commits intoberkeley-abc:masterfrom
robtaylor:origin-tracking-clean

Conversation

@robtaylor
Copy link
Copy Markdown

@robtaylor robtaylor commented Feb 28, 2026

Summary

This adds lightweight per-object origin tracking to Gia_Man_t, enabling the Yosys abc9 flow to preserve \src (source-location) attributes on LUT cells through ABC's optimization passes.

Motivation

We are working on preserving \src (source-location) attributes through the Yosys abc9 synthesis flow. The approach uses the XAIGER "y" extension to pass an object-to-source mapping through ABC — Yosys writes the mapping on &read, ABC preserves it during optimization, and Yosys reads it back on &write to annotate the resulting LUT cells.

We initially prototyped a &verify -y approach that uses combinational equivalence checking to reconstruct the mapping after optimization, but this only achieves 17-57% coverage on non-trivial designs and doesn't work for sequential circuits. This PR takes a different approach — propagating origins during optimization — which achieves ~100% coverage.

Approach

Add a single Vec_Int_t *vOrigins vector to Gia_Man_t that maps each object to its "origin" input-side object ID:

  • Initialized from the "y" extension on &read (AIGER reader)
  • Propagated through all dup/conversion/optimization passes and all major ABC9 engines
  • Written to "y" extension on &write (AIGER writer)
  • Heuristic: when creating AND/XOR/MUX via structural hashing, inherit the origin with the lowest valid ID from the parents

Three propagation helpers handle different transformation patterns:

  • Gia_ManOriginsDup() — for standard dup operations that use the Value field for old→new mapping
  • Gia_ManOriginsDupVec() — for functions using Vec_Int_t copy vectors (e.g., Jf/Lf mappers)
  • Gia_ManOriginsAfterRoundTrip() — for GIA→AIG→GIA round-trips (&dc2, &dch, &synch2) where per-object data is lost; recovers origins via CI/CO correspondence + top-down fanin propagation

Engine Coverage

Origins are propagated through all major ABC9 engines:

Engine Function(s) Propagation method
&dc2 Gia_ManCompress2 Round-trip recovery
&dch Gia_ManPerformDch Round-trip recovery
&if IF mapper + SopBalance/DsdBalance iCopy correspondence
&syn2 Jf/Lf mappers + sub-operations OriginsDupVec
&synch2 Jf/Lf mappers + AigSynch2Choices OriginsDupVec + round-trip recovery
&sweep FraigReduceGia, DupWithBoxes OriginsDup
&scorr Gia_ManCorrReduce OriginsDup
&mfs Gia_ManInsertMfs Custom via vMfs2Old/vMfs2Gia
&nf Nf_ManDeriveMapping No changes needed (returns same GIA)

Performance

Benchmarked on picorv32 (4-LUT mapping via abc9):

Metric Without origins With origins Overhead
Wall-clock time ~3.0s ~3.02s ~0.6%
Peak memory ~48MB ~49.5MB ~3%
LUT count identical identical 0%

The overhead is negligible — approximately 4 bytes per GIA object for the vOrigins vector, with O(1) propagation per node creation.

Coverage

With this change, \src retention on LUT cells after abc9 mapping improves from 17-57% to 100% on all tested designs (simple combinational, Amaranth-style, and larger multi-output designs with 54 LUTs).

Files changed

File Change
src/aig/gia/gia.h Add vOrigins field + inline accessors + helper declarations
src/aig/gia/giaMan.c Free vOrigins in Gia_ManStop
src/aig/gia/giaAiger.c Read/write vOrigins via "y" extension
src/aig/gia/giaDup.c Gia_ManOriginsDup + Gia_ManOriginsDupVec + Gia_ManOriginsAfterRoundTrip; instrument all Gia_ManDup* variants
src/aig/gia/giaHash.c Propagate origins in Gia_ManHashAnd, Gia_ManHashXorReal, Gia_ManHashMuxReal, Gia_ManRehash
src/aig/gia/giaAig.c Recover origins after AIG round-trips in Gia_ManCompress2 (&dc2) and Gia_ManPerformDch (&dch)
src/aig/gia/giaIf.c Propagate through IF mapper via iCopy + SopBalance/DsdBalance
src/aig/gia/giaJf.c Propagate in Jf_ManDeriveGia and Jf_ManDeriveMappingGia via OriginsDupVec
src/aig/gia/giaLf.c Propagate in Lf_ManDeriveMappingCoarse and Lf_ManDeriveMappingGia
src/aig/gia/giaMfs.c Propagate in Gia_ManInsertMfs via MFS ID correspondence
src/aig/gia/giaScript.c Round-trip recovery in Gia_ManAigSynch2Choices
src/aig/gia/giaSweep.c Propagate in Gia_ManFraigReduceGia and Gia_ManDupWithBoxes
src/aig/gia/giaEquiv.c Propagate in Gia_ManEquivReduce and Gia_ManEquivToChoices
src/aig/gia/giaMuxes.c Propagate in Gia_ManDupMuxes/Gia_ManDupNoMuxes
src/aig/gia/giaTim.c Propagate in Gia_ManDupNormalize/Gia_ManDupUnnormalize/Gia_ManDupCollapse
src/aig/gia/giaBalAig.c Propagate in balance operations
src/opt/dau/dauGia.c Propagate in Dsm_ManDeriveGia
src/proof/cec/cecCorr.c Propagate in Gia_ManCorrReduce

Context

This is part of work on YosysHQ/yosys#5712 to improve \src attribute retention through the abc9 flow.

cc @alanminko @Ravenslofty — would appreciate your thoughts on this approach.

robtaylor added a commit to robtaylor/yosys that referenced this pull request Feb 28, 2026
ABC now propagates origin mappings natively through optimization
passes via vOrigins (berkeley-abc/abc#487), so we no longer need
to run &verify -y and rewrite the output to reconstruct the mapping.

Keep &verify for correctness checking but without -y flag.

Co-developed-by: Claude Code v2.1.44 (claude-opus-4-6)
@robtaylor
Copy link
Copy Markdown
Author

Adding @alanminko's response here from email for my context:

It looks like your implementation is quite clean and has low resource usage. Currently it supports basic AIG manipulations (copy, balancing, etc). The next step is to propagate it into the engines, which manipulate XAIGs (&dc2, &if, &nf, &mfs, &syn2, &dch, &synch2, &sweep, &scorr, etc). It appears to be a lot of work but perhaps it can be quickly done using AI agents.

Glad this method works for mapping RTL code into the resulting AIG nodes (as well gates and LUTs after mapping). It also does not have the overhead of equivalence checking, required by the previous approach.

I'm now working on adding propagation into the engines, as advised.

@robtaylor
Copy link
Copy Markdown
Author

Following up on Alan's feedback — this new commit propagates vOrigins through all the major ABC9 engines:

Engines now instrumented:

Engine Function(s) Propagation method
&dc2 Gia_ManCompress2 Round-trip recovery (already in first commit)
&dch Gia_ManPerformDch Round-trip recovery (already in first commit)
&if Gia_ManPerformSopBalance, Gia_ManPerformDsdBalance iCopy correspondence
&syn2 Jf/Lf mappers + sub-operations OriginsDupVec (for vCopies-based mappers)
&synch2 Jf/Lf mappers + Gia_ManAigSynch2Choices OriginsDupVec + round-trip recovery
&sweep Gia_ManFraigReduceGia, Gia_ManDupWithBoxes OriginsDup (Value field)
&scorr Gia_ManCorrReduce OriginsDup (Value field)
&mfs Gia_ManInsertMfs Custom via vMfs2Old/vMfs2Gia
&nf Nf_ManDeriveMapping No changes needed (returns same GIA)

New helper added: Gia_ManOriginsDupVec() — like Gia_ManOriginsDup() but for functions that use a Vec_Int_t copy vector (e.g., Jf/Lf mappers) instead of the Value field.

Supporting functions also instrumented: Gia_ManDupCollapse (giaTim.c), Gia_ManEquivToChoices (giaEquiv.c).

Testing: Origins survive through all individual engines and chained pipelines (&dc2; &synch2; &if; &mfs). The change adds 86 lines across 11 files with no new allocations on the hot path.

@Shadlock0133
Copy link
Copy Markdown

can we not with the ai slop?

@robtaylor
Copy link
Copy Markdown
Author

can we not with the ai slop?

would you like to actually check the code first? Already discussed with @alanminko as this is the sort of change where AI helps.

@robtaylor
Copy link
Copy Markdown
Author

Performance Benchmark

Benchmarked origin propagation overhead on i10.aig (2675 ANDs, 257 inputs, 224 outputs) — same circuit with and without origins loaded, 20 iterations each:

Pipeline No Origins With Origins Overhead
&dc2 68.6±0.9ms 70.3±1.0ms +2.4%
&syn2 25.0±0.7ms 24.5±0.5ms -1.8%
&synch2 139.8±0.9ms 139.9±1.4ms +0.1%
&dch 143.6±1.4ms 143.8±1.7ms +0.1%
&scorr 17.2±0.6ms 17.4±0.8ms +1.4%
&sweep 26.1±1.1ms 26.3±0.6ms +0.8%
&if -K 6 39.5±0.9ms 39.4±0.6ms -0.2%
&dc2; &syn2; &if 89.2±1.6ms 88.1±1.1ms -1.2%
&dc2; &synch2; &if; &mfs 390.7±1.6ms 393.3±3.6ms +0.7%

All overhead is within measurement noise (~1%). The full abc9-style pipeline (&dc2; &synch2; &if -K 6; &mfs) shows +0.7% overhead — essentially free.

Methodology: Same circuit loaded with (&r -s file_with_y.aig) and without (&r -s file_no_y.aig) the "y" extension. The only difference is whether vOrigins is allocated and propagated. macOS/ARM64, 20 iterations averaged.

@akashlevy
Copy link
Copy Markdown

@robtaylor Does this support multiple origins per object? Or just one?

@alanminko
Copy link
Copy Markdown
Contributor

Thank you for keeping me in the loop. One thing observed is that following comment is not correct in all cases: "&nf -> No changes needed (returns same GIA)". Please note that all technology mappers (&if, &lf, &jf, &nf), when applied to an AIG with structural choices, derived by &dch or &synch2, return a different AIG after mapping. Of course it is possible to not use structural choices, but this leads to area/delay degradation on most of the designs.

@robtaylor
Copy link
Copy Markdown
Author

@akashlevy Single origin per object — each GIA object maps to one ancestor in the original AIG. This keeps the tracking lightweight (one int per object, zero allocations on the hot path).

Multi-source attribution is better handled at the Yosys layer: when mapping LUTs back to RTL cells, a single LUT may cover multiple original AIG objects. Yosys already has infrastructure for merging \src attributes (e.g., \src "foo.v:10|bar.v:20"), so it can union the \src values of all original objects that a LUT's origin chain touches. ABC just needs to provide the single best origin per object as an anchor for that lookup.

@robtaylor
Copy link
Copy Markdown
Author

@alanminko Thank you for the feedback. You're right that all technology mappers can return a different AIG when structural choices are present.

Looking at the current code, I believe &nf is actually covered but my PR comment was misleading — here's why:

  • &jf and &lf have two paths: a "same GIA" path (Jf_ManDeriveMapping, Lf_ManDeriveMapping) and a "new GIA" path (Jf_ManDeriveGia, Jf_ManDeriveMappingGia, Lf_ManDeriveMappingGia). The "new GIA" paths are the ones that needed Gia_ManOriginsDupVec — and they have it.
  • &nf's Nf_ManDeriveMapping always returns p->pGia (same pointer) with vCellMapping attached, so vOrigins survives on the same object. The timing/boxes path goes through Gia_ManDupUnnormalize/Gia_ManDupNormalize, both of which already call Gia_ManOriginsDup.
  • When choices are present, Nf_ManPerformMappingInt sets fCoarsen = 0 (line 2714), so the coarsening path that creates a different GIA via Gia_ManDupMuxes is skipped.

So I believe the current code is correct, but my summary table was wrong to say "No changes needed (returns same GIA)" without explaining why — it implied I hadn't considered the choices case.

That said, would you prefer a defensive Gia_ManOriginsDup call in Nf_ManPerformMapping for robustness (in case a "new GIA" derive path is added to &nf in the future)? Or have I misunderstood the concern — is there a code path where &nf with choices does produce a different GIA that I'm not seeing?

@robtaylor
Copy link
Copy Markdown
Author

@akashlevy To follow up — the Yosys-side patch that consumes the origin tracking now handles multi-source attribution: for each mapped cell, it collects \src from both the output object's origin and each input object's origin, merging unique values with | (Yosys's multi-source convention).

See the Yosys patch at: https://github.com/robtaylor/yosys/tree/src-retention-y-ext (specifically frontends/aiger/aigerparse.cc and frontends/aiger2/xaiger.cc).

@alanminko
Copy link
Copy Markdown
Contributor

@robtaylor When one of above mappers is called on AIG with choices derived by &dch or &synch2, it produces a new AIG, annotated with the mapping. It would be good if the origin propagation handled this case.

@robtaylor
Copy link
Copy Markdown
Author

@alanminko Thank you for the clarification. I've traced the code paths for all mappers with structural choices:

Mappers that create a new GIA (origin propagation already added):

  • &jf with fPureAigJf_ManDeriveGia() — has Gia_ManOriginsDupVec (giaJf.c:1514)
  • &jf with fCutMinJf_ManDeriveMappingGia() — has Gia_ManOriginsDupVec (giaJf.c:1657)
  • &lf with fCutMinLf_ManDeriveMappingGia() — has Gia_ManOriginsDupVec (giaLf.c:1907)
  • &ifGia_ManFromIfLogic() / Gia_ManFromIfAig() — has origin propagation via iCopy (giaIf.c)

Mapper that modifies in-place (vOrigins survives on same object):

  • &nfNf_ManDeriveMapping() returns p->pGia with vCellMapping attached; choices are freed via ABC_FREE(pNew->pReprs/pNexts) but object IDs are unchanged

Is there a specific scenario where &nf (or another mapper) with choices produces a genuinely new GIA object that I'm missing? I want to make sure I'm not overlooking a code path.

@robtaylor
Copy link
Copy Markdown
Author

@alanminko Thank you for pressing on this — I was being too narrow in my earlier analysis. Let me restate more carefully.

For &nf, there are three code paths that can produce a new GIA:

  1. Coarsening (fCoarsen=1): Gia_ManDupMuxesGia_ManCleanupGia_ManDupMarked — all three have Gia_ManOriginsDup calls (giaMuxes.c:143, giaScl.c→giaDup.c:1702)
  2. Timing/boxes path: Gia_ManDupUnnormalize and Gia_ManDupNormalize — both have Gia_ManOriginsDup (giaTim.c:424, giaTim.c:189)
  3. In-place mapping (choices present, fCoarsen=0): Nf_ManDeriveMapping returns same GIA — vOrigins survives unchanged

More broadly, the Dup functions used across all mappers (DupMuxes, DupMarked, DupNormalize, DupUnnormalize) all propagate origins. And the "new GIA" derive functions in &jf/&lf (Jf_ManDeriveGia, Jf_ManDeriveMappingGia, Lf_ManDeriveMappingGia) have Gia_ManOriginsDupVec calls.

Does this cover the cases you had in mind, or is there a path I'm still missing?

@robtaylor
Copy link
Copy Markdown
Author

Following up — I wrote a static analysis to find all Gia_Man_t* functions that create a new GIA (via Gia_ManStart) from an existing one but don't call Gia_ManOriginsDup or Gia_ManOriginsDupVec. It found 6 gaps in abc9-reachable functions, now fixed:

Function File Used by
Gia_ManDupFromBarBufs giaScript.c &synch2 with boxes
Gia_ManDupToBarBufs giaScript.c &synch2 with boxes
Gia_ManDupUnshuffleInputs giaTim.c timing CI reordering
Gia_ManDupMoveLast giaTim.c timing CI manipulation
Gia_ManDupHashMapping giaIf.c &if -y hash mapping mode
Gia_ManDupUnhashMapping giaIf.c &if unhash mapping mode

All 6 use the Value field for old→new object mapping, so adding Gia_ManOriginsDup(pNew, p) before return pNew is the correct fix. The remaining 167 "missing" functions are not in the abc9 pipeline (miters, verification, framing, etc).

Coverage summary: 23 Gia_Man_t* Dup-style functions now propagate origins, covering all paths reachable from the abc9 engines.

@robtaylor
Copy link
Copy Markdown
Author

Latest patch set (6211d3c): Added Gia_ManOriginsDup to 6 additional Dup functions found by static analysis.

The analysis script (gist) finds all Gia_Man_t* functions that:

  1. Take a Gia_Man_t* parameter (source GIA)
  2. Call Gia_ManStart (create a new GIA)
  3. Do not call Gia_ManOriginsDup or Gia_ManOriginsDupVec

It flags 173 functions total, but most are outside the abc9 pipeline (miters, verification, framing, etc). The second script cross-references against abc9 engine entry points to find the 6 that matter. All are now fixed.

Full commit summary:

Commit Description
e261ac1 Core vOrigins infrastructure (Gia_Man_t field, AIGER "y" read/write, hash propagation)
ace3e2f Propagation through abc9 engines (&dc2, &dch, &if, &jf, &lf, &nf, &mfs, &syn2, &synch2, &sweep, &scorr)
a213928 Bounds checks for grown GIA objects
8ccd01a Defensive bounds check in Gia_ObjSetOrigin, propagation in DupWithAttributes
6211d3c 6 additional Dup functions found by static analysis (BarBufs, timing, hash mapping)

@robtaylor robtaylor force-pushed the origin-tracking-clean branch from 6211d3c to 06cc387 Compare March 9, 2026 18:29
@akashlevy
Copy link
Copy Markdown

akashlevy commented Mar 9, 2026

@akashlevy Single origin per object — each GIA object maps to one ancestor in the original AIG. This keeps the tracking lightweight (one int per object, zero allocations on the hot path).

Multi-source attribution is better handled at the Yosys layer: when mapping LUTs back to RTL cells, a single LUT may cover multiple original AIG objects. Yosys already has infrastructure for merging \src attributes (e.g., \src "foo.v:10|bar.v:20"), so it can union the \src values of all original objects that a LUT's origin chain touches. ABC just needs to provide the single best origin per object as an anchor for that lookup.

Single origin per object implies that the src attribution is lossy. When nodes are merged in abc, you are then forcing one origin only among multiple possibilities. I think this solution is strictly worse than what we had implemented, where we allow for up to N src attributes. Yes, multi-src attribution is more complicated, but it's higher fidelity, and if done correctly, it's only a factor of N more overhead (and any user can feel free to set N=1). Your point about handling multi-src attribution in Yosys is orthogonal to this, because the information loss happens in abc.

@alanminko
Copy link
Copy Markdown
Contributor

@robtaylor The AIG duplication/reconstruction paths you mentioned are the one I had in mind, so if they are supported, the codes should be fine for handling XAIGs while propagating the origin markers.

@robtaylor
Copy link
Copy Markdown
Author

robtaylor commented Mar 9, 2026 via email

@akashlevy
Copy link
Copy Markdown

Ah, it’s one origin per input object. Check the Yosys patch for more details.

On Mon, 9 Mar 2026 at 18:40, Akash Levy @.> wrote: akashlevy left a comment (berkeley-abc/abc#487) <#487 (comment)> @akashlevy https://github.com/akashlevy Single origin per object — each GIA object maps to one ancestor in the original AIG. This keeps the tracking lightweight (one int per object, zero allocations on the hot path). Multi-source attribution is better handled at the Yosys layer: when mapping LUTs back to RTL cells, a single LUT may cover multiple original AIG objects. Yosys already has infrastructure for merging \src attributes (e.g., \src "foo.v:10|bar.v:20"), so it can union the \src values of all original objects that a LUT's origin chain touches. ABC just needs to provide the single best origin per object as an anchor for that lookup. Single origin per object implies that the src attribution is lossy. When nodes are merged in abc, you are then forcing one origin only among multiple possibilities. I think this solution is strictly worse than what we had implemented, where we allow for up to N src attributes. Yes, it's more complicated, but it's higher fidelity. Your point about handling multi-src attribution in Yosys is irrelevant, because the information loss happens in abc. — Reply to this email directly, view it on GitHub <#487 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAB4O4FEPR5VSFPK37MUW2L4P4FYVAVCNFSM6AAAAACWCKO2UKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DAMRVHEZTQNZQGU . You are receiving this because you were mentioned.Message ID: @.>

Ah got it! So there can still be multiple src attrs on the output objects as merge operations happen?

@robtaylor
Copy link
Copy Markdown
Author

Ah, it’s one origin per input object. Check the Yosys patch for more details.

On Mon, 9 Mar 2026 at 18:40, Akash Levy @.> wrote: akashlevy left a comment (berkeley-abc/abc#487) <#487 (comment)> @akashlevy https://github.com/akashlevy Single origin per object — each GIA object maps to one ancestor in the original AIG. This keeps the tracking lightweight (one int per object, zero allocations on the hot path). Multi-source attribution is better handled at the Yosys layer: when mapping LUTs back to RTL cells, a single LUT may cover multiple original AIG objects. Yosys already has infrastructure for merging \src attributes (e.g., \src "foo.v:10|bar.v:20"), so it can union the \src values of all original objects that a LUT's origin chain touches. ABC just needs to provide the single best origin per object as an anchor for that lookup. Single origin per object implies that the src attribution is lossy. When nodes are merged in abc, you are then forcing one origin only among multiple possibilities. I think this solution is strictly worse than what we had implemented, where we allow for up to N src attributes. Yes, it's more complicated, but it's higher fidelity. Your point about handling multi-src attribution in Yosys is irrelevant, because the information loss happens in abc. — Reply to this email directly, view it on GitHub <#487 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAB4O4FEPR5VSFPK37MUW2L4P4FYVAVCNFSM6AAAAACWCKO2UKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DAMRVHEZTQNZQGU . You are receiving this because you were mentioned.Message ID: _@**.**_>

Ah got it! So there can still be multiple src attrs on the output objects as merge operations happen?

yes indeed, thats the point =)

@robtaylor
Copy link
Copy Markdown
Author

To clarify my earlier reply — let me be more precise about what's happening.

You're right that single origin per object is lossy within ABC. There are two loss points:

  1. Structural sharing: when hashing finds an existing equivalent node, the new node's origin is discarded (the existing node keeps its origin)
  2. New nodes: AND(a, b) inherits min(origin(a), origin(b)) — the other input's origin is dropped

The Yosys-side multi-source collection partially compensates: for each mapped LUT, it collects \src from the output object's origin AND each input object's origin (up to K+1 anchors for a K-input LUT). This recovers attribution from the cut boundary, but interior nodes of the LUT cone are not individually tracked.

So in practice: the coverage is good for typical RTL (structurally identical sub-expressions from different source locations are uncommon), but it is not lossless. An N-origins-per-object approach would be higher fidelity at the cost of N× memory and significantly more complex propagation through all the engine paths. The current design is a pragmatic starting point — if real-world usage shows the loss matters, the origin field could be extended.

@akashlevy
Copy link
Copy Markdown

akashlevy commented Mar 9, 2026

To clarify my earlier reply — let me be more precise about what's happening.

You're right that single origin per object is lossy within ABC. There are two loss points:

  1. Structural sharing: when hashing finds an existing equivalent node, the new node's origin is discarded (the existing node keeps its origin)
  2. New nodes: AND(a, b) inherits min(origin(a), origin(b)) — the other input's origin is dropped

The Yosys-side multi-source collection partially compensates: for each mapped LUT, it collects \src from the output object's origin AND each input object's origin (up to K+1 anchors for a K-input LUT). This recovers attribution from the cut boundary, but interior nodes of the LUT cone are not individually tracked.

So in practice: the coverage is good for typical RTL (structurally identical sub-expressions from different source locations are uncommon), but it is not lossless. An N-origins-per-object approach would be higher fidelity at the cost of N× memory and significantly more complex propagation through all the engine paths. The current design is a pragmatic starting point — if real-world usage shows the loss matters, the origin field could be extended.

Got it. I suppose it depends on the application you are going for. Having one src attribute is obviously better than having none.

But an issue that can happen (we've seen happen) is "aliasing" of many final nodes to a single origin node. In reality, these final nodes have a large diversity of origins, which is evident when multi-src attribution is kept.

For our application (giving users an understanding of power/perf/area bottlenecks), it's not ideal to say, "this is the only line of RTL that led to this bottleneck" when in reality, it was multiple.

@robtaylor
Copy link
Copy Markdown
Author

@alanminko @akashlevy Thinking about this further — there's a middle ground between single-origin and N-origins-per-object that could address the fidelity concern without the memory overhead.

The origin loss happens at two specific points in structural hashing (giaHash.c):

  1. Hash hit: an equivalent node already exists, so the new node's origin is silently discarded
  2. New node: AND(a,b) picks min(origin(a), origin(b)), dropping the other

Since these merge events are relatively rare compared to total node count, we could keep a global merge log — a flat vector of (node_id, discarded_origin) pairs in Gia_Man_t:

Vec_Int_t * vOriginsMerged;  // append-only pairs: (node_id, alt_origin)

This is sparse and append-only — no per-object overhead, just a small side table for the rare merge events. Propagation through Dup functions would remap node IDs the same way Gia_ManOriginsDup does (via the Value field), and the list stays small.

For XAIGER transport, the merge log could be a new extension (e.g., "z") carrying (object_id, alt_origin) pairs alongside the existing "y" extension. Yosys would then include those additional \src values when mapping LUTs back to RTL.

The main subtlety is multi-pass optimization: a node merged in pass 1 might get further merged in pass 2, creating chains. These would need transitive resolution through the origin mapping at write time.

Alan — would something like this be acceptable as an addition to vOrigins, or would it add too much complexity to the core data structures?

@akashlevy
Copy link
Copy Markdown

akashlevy commented Mar 9, 2026

This proposed solution sounds like it would achieve multi-src attribution properly, just in a slightly different way than we had done in our fork. No need to have per-node arrays until the end. Seems like it would work well.

I will say that the merge events are not as rare as you might think for a typical design. Transitive resolution would be necessary in quite a few real-world cases.

@robtaylor
Copy link
Copy Markdown
Author

This proposed solution sounds like it would achieve multi-src attribution properly, just in a slightly different way than we had done in our fork. No need to have per-node arrays until the end. Seems like it would work well.

I will say that the merge events are not as rare as you might think for a typical design. Transitive resolution would be necessary in quite a few real-world cases.

ah, but is it rare with respect to the total number of nodes?

@akashlevy
Copy link
Copy Markdown

This proposed solution sounds like it would achieve multi-src attribution properly, just in a slightly different way than we had done in our fork. No need to have per-node arrays until the end. Seems like it would work well.
I will say that the merge events are not as rare as you might think for a typical design. Transitive resolution would be necessary in quite a few real-world cases.

ah, but is it rare with respect to the total number of nodes?

No, it's very common. We see about 4-5 src attributes per node on average, when we retain thru merge.

@akashlevy
Copy link
Copy Markdown

Hi @robtaylor, unfortunately the infra is embedded into our commercial tool so we can't share a working version without spending a lot of time sanitizing... can you try to replicate the infra on your end?

@robtaylor
Copy link
Copy Markdown
Author

Thanks @akashlevy , i can try...

@robtaylor robtaylor force-pushed the origin-tracking-clean branch from 06cc387 to cb4e59a Compare March 26, 2026 23:58
@robtaylor
Copy link
Copy Markdown
Author

Given the input from @akashlevy and @AdvaySingh1, I've updated this PR with the approach discussed previously to support multiple vOrigins. The single-origin-per-object scheme has been replaced with a union-based small-buffer optimization that stores up to 4 origins inline (16 bytes per object) and overflows to a geometrically-doubling heap array for high-fanout nodes. This captures merge events from structural hashing and optimization passes that previously lost origin information. The AIGER "y" extension uses a sentinel-based variable-length format that is backward compatible with the old single-origin format. The PR has been rebased onto current master as a clean 4-commit stack with no upstream dependencies.

@alanminko how does this seem to you?

Note: the last commit (cb4e59a&origins and &origins_id commands) adds debugging/inspection commands for origin tracking. These are useful for development and testing but could be dropped from the PR if the additional abc.c surface area is considered too heavy.

Cost when origin tracking is NOT active

When vOrigins is NULL (the normal abc9 flow without Yosys origin data):

  • Struct overhead: +8 bytes per Gia_Man_t (one NULL pointer)
  • Runtime overhead: one if (!p->vOrigins) NULL check at each propagation site — branch-predicted, not measurable
  • Measured: identical runtime with and without the patch (0% overhead)

Cost when origin tracking IS active

Design AND nodes Runtime without Runtime with Overhead vOrigins memory
picorv32 4,395 0.25s 0.26s ~4% 85 KB
picosoc 12,416 1.10s 1.13s ~3% 452 KB
aes_core 29,339 7.17s 7.31s ~2% 1.3 MB

Memory breakdown per object:

  • Fixed: 16 bytes/object (4-slot inline array in Vec_Int_t backing store)
  • Overflow: only nodes with >4 origins allocate heap arrays (geometric doubling)
  • In practice, 85-95% of nodes stay inline (≤4 origins); overflow is ~5-19% of origin-bearing nodes

Propagation coverage

Origins are propagated through all major ABC9 engines:

Category Commands / Operations
Optimization &scorr, &sweep, &fraig, &dc2, &dch, &syn2, &synch2, &b, &dam
Mapping &if, &jf, &lf, &mfs, &sopbalance, &dsdbalance
Structural &muxes, &nomuxes, &equiv, &collapse
Infrastructure Structural hashing (And/Xor/Mux hit+miss), all Gia_ManDup* variants, timing normalize/unnormalize, AIGER &r/&w round-trip, DSD decomposition, GIA→AIG→GIA round-trip recovery

Origin growth through the abc9 flow

Test flow: &read <xaiger>; &origins_id; &scorr; &sweep; &dc2; &dch -f -r; &if -v; &mfs

Design Init After &sweep After &dc2 After &if After &mfs Max/node Overflow nodes
picorv32 1.00x 1.07x 3.44x 4.48x 4.49x 297 321
picosoc 1.00x 1.08x 15.18x 16.09x 16.02x 865 1,316
aes_core 1.00x 1.06x 17.09x 23.80x 23.72x 1,728 5,520

AIGER write/read round-trip preserves origins exactly for all benchmarks.

Reproducing

The origin-tracking-test-baseline branch preserves the full test environment including upstream commits not yet on master (PRs #471, #480-#488, plus unreleased commits fa5029da9, 3dd086feb, and others). These are not required by the origin tracking code — the clean PR branch has no upstream dependencies.

# Generate XAIGER via Yosys (ECP5 target)
yosys -p "read_verilog <design>.v; synth_ecp5 -abc9 -top <module> -run :map_luts; \
  abc9 -exe ./abc -lut 4 -nocleanup -D 0 -script \"+&read {D};&ps\""

# Run ABC with origin tracking
./abc -c "read_lut <tmpdir>/lutdefs.txt; read_box <tmpdir>/input.box; \
  &read <tmpdir>/input.xaig; &origins_id; &scorr; &sweep; &origins; \
  &dc2; &origins; &dch -f -r; &origins; &if -v; &origins; &mfs; &origins"

@akashlevy
Copy link
Copy Markdown

akashlevy commented Mar 27, 2026

@robtaylor this is awesome. One suggestion is to have a (user-specified) limit on the number of attributes per node. We found that in some cases it can blow up to a very large number of attributes per node and cause a large slowdown when copying from node to node.

Should be a 1-2 line patch I think.

@robtaylor robtaylor force-pushed the origin-tracking-clean branch 2 times, most recently from 99c2e73 to 4496674 Compare March 27, 2026 01:50
@robtaylor
Copy link
Copy Markdown
Author

robtaylor commented Mar 27, 2026 via email

@akashlevy
Copy link
Copy Markdown

@AdvaySingh1 which example had the runtime/memory blowup without a limit on # of src attrs?

@robtaylor
Copy link
Copy Markdown
Author

@akashlevy Thanks for the feedback. I've added a per-node origin cap in commit d8734f9dc.

Implementation: nOriginsMax field on Gia_Man_t, checked in the overflow branch of Gia_ObjAddOrigin before the dedup scan. When a node reaches the limit, further origins are silently dropped. Cost when disabled (default): zero — the check is p->nOriginsMax > 0 && count >= p->nOriginsMax, which short-circuits on the first comparison.

Set via &origins_id -M <num> (0 = unlimited, default). In the Yosys flow, the cap can be passed through the ABC script.

Synthetic worst case (many outputs sharing a deep common AND chain):

Config Time Total origins Max/node
No cap (n=10000 outputs) 5.70s 1,010,000 10,000
-M 256 0.79s 45,344 256
-M 64 0.79s 26,336 64

Normal designs (i10.aig, picorv32, aes_core) are unaffected — their max origins/node are well under any reasonable cap.

The blowup comes from OriginsAfterRoundTrip's top-down propagation: each node in a shared fanin cone accumulates the union of all ancestor CO driver origins. The O(n) dedup scan in AddOrigin makes this O(n²) total. The cap bounds the dedup scan to O(cap) per node.

@AdvaySingh1 could you test with one of the designs that was hitting the blowup? A cap of 256 should be a good starting point — it preserves meaningful origin information while bounding the pathological case.

@akashlevy
Copy link
Copy Markdown

@AdvaySingh1, can you test out Rob's implementation of node retention across our internal suite and see if you get the same behavior as with our code?

@robtaylor robtaylor force-pushed the origin-tracking-clean branch from d8734f9 to 92b0ee5 Compare April 2, 2026 16:44
robtaylor added a commit to robtaylor/yosys that referenced this pull request Apr 2, 2026
ABC now propagates origin mappings natively through optimization
passes via vOrigins (berkeley-abc/abc#487), so we no longer need
to run &verify -y and rewrite the output to reconstruct the mapping.

Keep &verify for correctness checking but without -y flag.

Co-developed-by: Claude Code v2.1.44 (claude-opus-4-6)
@robtaylor
Copy link
Copy Markdown
Author

@akashlevy @AdvaySingh1 The Yosys side is ready for testing on my src-retention-y-ext branch. It reads back ABC's multi-origin "y" extension and merges \src attributes onto LUT cells.

The origin cap is now wired end-to-end:

# Set cap in Yosys (passed through to ABC automatically)
scratchpad -set abc9.origins_max 256

# Then run abc9 as normal
synth_ecp5 -abc9 ...

This injects &origins -M 256 into the ABC script after &read, capping the per-node origin count before any optimization passes run. The cap can also be set directly in ABC via &origins -M 256.

@akashlevy
Copy link
Copy Markdown

Thank you, @robtaylor, Advay will be testing this some time soon, probably next week. Appreciate the effort to upstream this!

Add lightweight origin tracking that propagates source-location
provenance through ABC's optimization passes. This enables the
Yosys abc9 flow to preserve \src attributes on LUT cells after
technology mapping, achieving ~100% coverage on tested designs
with negligible overhead (~0.6% time, ~3% memory on picorv32).

Changes:
- Add Vec_Int_t *vOrigins field to Gia_Man_t with inline accessors
- Read/write origins via AIGER "y" extension (sentinel -1 for unmapped)
- Propagate through all Gia_ManDup* variants via Gia_ManOriginsDup()
- Propagate through structural hashing (AND/XOR/MUX) in giaHash.c
- Recover origins after GIA→AIG→GIA round-trips (&dc2, &dch) via
  Gia_ManOriginsAfterRoundTrip() using CO driver + top-down propagation
- Propagate through IF mapper using iCopy correspondence
- Instrument giaEquiv, giaMuxes, giaTim, giaBalAig, dauGia

Co-developed-by: Claude Code v2.1.44 (claude-opus-4-6)
Extend origin tracking to cover the complete abc9 optimization
pipeline. Add Gia_ManOriginsDupVec for functions that use Vec_Int_t
copy vectors instead of the Value field.

Engines instrumented:
- &dc2, &dch: Round-trip recovery (giaAig.c)
- &if: iCopy-based propagation in SopBalance/DsdBalance (giaIf.c),
  plus DupHashMapping/DupUnhashMapping for timing paths
- &jf, &lf: OriginsDupVec in Jf_ManDeriveGia/Lf_ManDeriveMappingGia
- &syn2, &synch2: Sub-operations (Jf/Lf mappers) + round-trip
  recovery in Gia_ManAigSynch2Choices, DupFromBarBufs/DupToBarBufs
  for box designs (giaScript.c)
- &sweep: Gia_ManFraigReduceGia, Gia_ManDupWithBoxes (giaSweep.c)
- &scorr: Gia_ManCorrReduce (cecCorr.c)
- &mfs: Custom propagation via vMfs2Old/vMfs2Gia (giaMfs.c)
- Supporting: Gia_ManDupCollapse/DupNormalize/DupUnnormalize (giaTim.c),
  DupUnshuffleInputs/DupMoveLast (giaTim.c),
  EquivToChoices (giaEquiv.c), DupMuxes (giaMuxes.c),
  BalanceInt (giaBalAig.c), DauMergePart (dauGia.c),
  DupWithAttributes (giaDup.c)

Add bounds checks in Gia_ObjOrigin and Gia_ObjSetOrigin for GIAs
that grow after vOrigins is allocated (e.g., AreaBalance adding nodes).

Coverage verified by static analysis finding all Gia_Man_t* functions
that call Gia_ManStart without Gia_ManOriginsDup — 23 functions
covered, remaining 167 are outside the abc9 pipeline.

Co-developed-by: Claude Code v2.1.58 (claude-opus-4-6)
@robtaylor robtaylor force-pushed the origin-tracking-clean branch from dd225ea to bf2c7a3 Compare April 2, 2026 22:47
@AdvaySingh1
Copy link
Copy Markdown

This a great! Currently working on testing this!

Replace single-origin-per-object vOrigins with a union-based scheme
that stores up to GIA_ORIGINS_INLINE (default 4) origins inline, with
overflow to a heap-allocated array for outliers. This captures merge
events from structural hashing and optimization that previously lost
origin information.

Data structure (Gia_OriginsEntry_t union, 16 bytes on 64-bit):
- Inline mode: 4 inline int slots (-1 = unused)
- Overflow mode: sentinel (INT_MIN) + count + heap pointer
- Stride = sizeof(entry)/sizeof(int) = 4 ints per object

Key changes:
- gia.h: Union type, stride constants, inline accessors
  (Gia_ObjOriginsNum, Gia_ObjOriginsGet, Gia_ObjForEachOrigin),
  updated Gia_ObjOrigin/Gia_ObjSetOrigin for backward compat
- giaDup.c: Gia_ObjAddOrigin (dedup + promote), Gia_ObjUnionOrigins,
  Gia_ManOriginsFreeOverflows; updated OriginsDup/DupVec/AfterRoundTrip
  for strided multi-origin propagation
- giaHash.c: Union all input origins on hash miss AND hash hit (captures
  structural sharing from different source paths)
- giaAiger.c: Variable-length "y" extension format [count, lit0, lit1,...]
  with backward-compatible reader for old single-origin format
- giaIf.c, giaMfs.c: Mapper propagation via Gia_ObjUnionOrigins
- giaMan.c: Free overflow arrays before vOrigins cleanup

Co-developed-by: Claude Code v2.1.58 (claude-opus-4-6)

Harden multi-origin vOrigins: leak guards, geometric growth, format sentinel

- Add free-before-alloc guards in all OriginsDup functions to prevent
  memory leaks when vOrigins is already populated (e.g. Gia_ManDup
  followed by Gia_ManDupWithAttributes)
- Replace O(n²) realloc-by-1 with geometric doubling in overflow
  arrays (8-slot initial capacity, double at powers of 2)
- Add sentinel int -2 to AIGER "y" extension writer/reader for
  unambiguous new-vs-old format detection (fixes edge case where
  nInts == nAigObjs with all-zero origins)
- Extract Gia_ManOriginsDupIf() helper to deduplicate 3 identical
  14-line IF mapper origin propagation blocks
- Replace Gia_ManOriginsGrow push-loop with Vec_IntFillExtra
- Add vNodes NULL guard in AIGER reader
- Remove orphaned duplicate comment block

Co-developed-by: Claude Code v2.1.58 (claude-opus-4-6)

Simplify vOrigins: extract OriginsReset, fix free(), hoist iteration count

Code review cleanup:
- Extract Gia_ManOriginsReset() helper to replace 5 identical
  free-overflows + free-vec leak-guard blocks
- Use ABC_FREE() instead of bare free() in Gia_ObjSetOrigin
  for allocator consistency
- Hoist Gia_ObjOriginsNum() call out of Gia_ObjForEachOrigin
  macro loop condition (callers declare _nOrig)
- Add compile-time assertion that GIA_ORIGINS_INLINE is large
  enough to cover the overflow header on the target platform

Co-developed-by: Claude Code v2.1.83 (claude-opus-4-6)
- &origins: prints multi-origin statistics (entry count, total origins,
  average/max per node, overflow count, histogram). Origins are
  populated either from XAIGER "y" extension (normal abc9 flow) or
  by &origins_id (testing).
- &origins_id: sets identity origins (each AND node -> itself) for
  testing origin propagation in standalone ABC sessions. Not needed
  in the normal abc9 flow where origins come from Yosys via XAIGER.

Co-developed-by: Claude Code v2.1.58 (claude-opus-4-6)
Shared fanin cones can accumulate origins from all CO drivers,
causing O(n²) dedup cost in Gia_ObjAddOrigin. Add nOriginsMax
field to Gia_Man_t that short-circuits overflow-mode AddOrigin
when the node has reached the limit. Cap is set via &origins_id -M.

Pathological case (10K outputs, shared 50-deep cone):
  No cap:   5.70s, 1.01M total origins, max 10000/node
  -M 256:   0.79s, 45K total origins, max 256/node (7.2x faster)
Normal designs (i10.aig, max 165/node): identical results.

Co-developed-by: Claude Code v2.1.89 (claude-opus-4-6)
@robtaylor robtaylor force-pushed the origin-tracking-clean branch from 43bc82f to 4f03583 Compare April 3, 2026 01:08
@robtaylor
Copy link
Copy Markdown
Author

Fixes now pushed for Windows/MSVC

@AdvaySingh1
Copy link
Copy Markdown

Hi @robtaylor, is there support for the &get command as well? It looks like this approach currently only works with &read.

Would it be possible to add support for the read_blif flow through the abc.cc pass? This seems fairly straightforward—likely just a small change to &get to ensure it preserves the original source information.

Also, could you comment on the accuracy of the round-trip logic? Since the origin data is stored in Gia_Man_t, how is that information preserved across conversions to other representations? What guarantees are there that node origins remain accurate throughout these transformations?

@robtaylor
Copy link
Copy Markdown
Author

robtaylor commented Apr 7, 2026

Hi @AdvaySingh1 , thanks for the questions.

&get / read_blif flow:

This PR targets the abc9 flow, where origins are carried via the XAIGER "y" extension (&r → optimize → &w). The &get path (used by the old abc pass via read_blif) doesn't have a mechanism to carry per-object origin data from Yosys — BLIF doesn't encode it, so there's nothing for &get to preserve. Supporting the old flow would require Yosys-side changes to emit origin data in BLIF (or another sideband), which is a separate piece of work.

For context, this addresses a limitation that was considered fundamental back in 2018 (YosysHQ/yosys#533) — Claire stated that "there is inherently NO WAY to pass information attached to cells through ABC." The vOrigins infrastructure provides exactly that mechanism for the abc9 flow.

For the read_blif / eBLIF flow, the cleanest path forward would likely be migrating it to use XAIGER transport (like abc9 does), which would get origin tracking for free. Alternatively, .attr src annotations could be added to BLIF write/read on both the Yosys and ABC sides, with &get/&put bridging to vOrigins — but that's a multi-component change across both projects. Either way, it builds on top of the vOrigins infrastructure in this PR rather than requiring changes to it.

Round-trip accuracy:

Most engines (&sweep, &scorr, &syn2, &if, &jf, &lf, &nf, &mfs) use direct GIA→GIA duplication with exact object-level mapping — origins are lossless through these.

The exception is &dc2, &dch, and &synch2, which internally convert GIA → Aig_Man_t → GIA. Since Aig_Man_t has no origin field, origins are lost during the round-trip and recovered afterward using Gia_ManOriginsAfterRoundTrip. The recovery works by:

  1. Mapping CIs 1:1 by index (exact)
  2. Mapping CO drivers 1:1 by output index (exact)
  3. Propagating top-down from CO drivers through fanin cones using Gia_ObjUnionOrigins

With multi-origin support, step 3 accumulates origins from all fanout paths, so interior nodes get the union of all their transitive CO driver origins. This gives good coverage in practice, but it is approximate — an origin that exists only on an interior AND node in the pre-optimization GIA with no corresponding CO anchor won't be recovered.

Making the round-trip fully lossless would mean either adding origin tracking to Aig_Man_t itself (and propagating through the Dar rewriting internals), or building an explicit old↔new object mapping across the optimization. Both are non-trivial changes to core AIG infrastructure, so I think that's best addressed as a follow-up rather than blocking this PR.

Do you have a specific design or recipe where you've seen the round-trip recovery produce incorrect or missing attributions? That would help prioritize and target the improvement.

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.

5 participants