dbSta: store the timing constraints in the .odb (write_db -sdc) - #11260
Open
oharboe wants to merge 10 commits into
Open
dbSta: store the timing constraints in the .odb (write_db -sdc)#11260oharboe wants to merge 10 commits into
oharboe wants to merge 10 commits into
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request enables storing and restoring timing constraints (SDC) directly within the OpenDB (.odb) database file, making it self-describing and removing the need for a separate .sdc file. Feedback on the changes suggests using the secure utl::ScopedTemporaryFile utility instead of a custom temporary file class to prevent insecure temporary file creation, and ensuring that SDC constraints are only restored if the database is successfully loaded without throwing an exception.
Collaborator
Author
|
out of sight, out of mind for now |
.odb already subsumes the netlist, tech, placement, routing and even dont_touch. Timing constraints are the conspicuous omission, so a flow has to carry a matching .sdc next to every .odb and then work out which .sdc goes with which .odb. That inference is a guess, and the only new freedom it buys anyone is the freedom to load the wrong constraints. write_db now stores the constraints in the block, and read_db -sdc replays them, so an .odb can describe itself. The payload is a dbStringProperty, opaque to odb, so no odb -> sta dependency is created and an older binary still reads the file. Replay is opt-in, so read_db behaves exactly as before unless asked otherwise. write_sdc is untouched and remains the way to export constraints for inspection or for a standalone OpenSTA bug report. Verified on the largest asap7 designs: write_db, then read_db -sdc, then write_sdc reproduces the original constraints byte for byte (swerv_wrapper, 89615 lines; mock-cpu, 94253 lines). Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
write_db stores the timing constraints as a native record of the Sdc
instead of write_sdc text, and read_db -sdc restores it without going
through Tcl: every pin, instance and net is an odb object id, so
restoring is a table lookup per object and the Sta constraint makers
are called directly.
Why: the text form costs exactly what read_sdc costs. A generated .sdc
names every register clock pin once per constraint over
[all_registers], and every [get_ports {x}] walks all ports of the
design, so a flow pays a quadratic-in-ports, linear-in-registers name
resolution at every stage boundary. The native form pays none of it,
and keeps full float precision where the text rounds to four digits.
The Sdc has no single accessor for everything it holds, but write_sdc
does enumerate all of it; its output is therefore the completeness
oracle: if every command in the text is one the native encoder covers,
the native form is stored, otherwise the text is stored and replayed
as before. Nothing is ever dropped: whatever write_sdc can say, the
.odb carries. Left to the text fallback for now: generated clocks,
clock uncertainty and propagation in pin form (not enumerable from the
Sdc), derating, disables, the drive/load environment, design rules on
ports and cells, and commands carrying a -comment.
Both payloads are dbStringProperty values on the block. That type has
been serialized by name and type tag since 2021
(odb/src/db/dbProperty.cpp), so an OpenROAD that predates this reads
the new .odb unchanged and ignores the properties.
ord::sdc_in_db_kind reports which form the current block carries:
native, text or none. Tests: sdc_in_db1 shows the native record;
sdc_in_db4 proves byte-identical write_sdc after a cross-process
restore of asap7-style constraints (set_max_delay and group_path over
all_inputs/all_registers, false path, multicycle, clock groups,
latency, uncertainty, propagated clock, design fanout and transition
limits); sdc_in_db5 does the same through the text fallback.
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
read_db -sdc returns 1 when constraints were found and restored and 0 when the .odb carries none, and write_db stores nothing for a linked design that has no constraints. A flow can therefore call read_db -sdc first and fall back to read_sdc only when it has to, which is the contract ORFS is written against, and an .odb written before this change, or by a flow that never read an .sdc, is not mistaken for a constrained one. Without -sdc, read_db is unchanged. sdc_in_db6 covers the unconstrained case; the man page documents the return value and ord::sdc_in_db_kind. Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
… the .sdc scratch file Two review points on the stored-constraints change. read_db -sdc now restores inside the same try block that reads the database, so a read that fails (ORD-54) never reaches the restore; the logger error is noreturn, but the structure now says so on its own. sdc_in_db7 reads an empty file with -sdc and shows the error and no restore. The scratch file write_db uses for the write_sdc text is created with mkstemp instead of a pid-plus-pointer name. utl::ScopedTemporaryFile was considered and does not fit: it exposes only a FILE* (its path is private and hard-wired under /tmp, sta::writeSdc needs a path) and it logs an INFO line on every construction, which write_db must not do. utl has no path-yielding temporary-file helper, so this keeps a local one and says why. Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
The Sdc keeps input/output delays, exceptions and case-analysis pins in pointer-ordered containers, so the record could depend on where the heap put things. The encoder now walks every set in a canonical order: pins, instances and nets by odb object id (PinSet and friends already are), clocks by definition index (what write_sdc uses, so the restored clock order matches), port delays by pin id then clock, exceptions with write_sdc's own ExceptionPathLess, case-analysis and logic values by pin id. sdc_in_db8 proves it: after one read_sdc, two write_db in the same process and a third from a fresh process that restored the constraints with read_db -sdc give byte-identical .odb files, not just identical records, so restoring and writing again is a fixpoint. Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
write_db no longer runs write_sdc to find out whether the native form covers the Sdc. On an 800k-register design that oracle cost 14.9 s per write_db, and ORFS writes the database about fifteen times per flow. Coverage is now decided by asking the Sdc directly, mirroring WriteSdc::writeTiming/writeEnvironment/writeDesignRules/writeVariables category by category through public accessors: generated clocks and comments, clock slew limits, inter-clock uncertainty over the clock pairs, disables, operating conditions, wire load, net resistances, voltages, max area and power, the propagate-all-clocks and preset/clear variables, port loads, input drives and port limits over the top-level ports, and global derating through a top-level pin. The categories the Sdc keeps only in per-object maps with no enumerator -- pin clock uncertainty, propagated clock pins, data checks, pin capacitance limits, latch borrow limits, min pulse widths, net loads and voltages, instance and net derating, clock senses -- are asked object by object in one pass over the leaf pins; each is a hash lookup in a map that is almost always empty. write_sdc now runs only when the text fallback is actually taken. Not observable from the public Sdc API: a clock sense set with set_clock_sense -positive or -negative (only -stop_propagation is queryable, via clkStopPropagation). That construct alone is not detected and would be lost from the native form. On the 50k-register fixture write_db goes from 0.51 s to 0.10 s; the .odb written after a restore is still byte-identical to the original. sdc_in_db9 shows a generated clock and a port load, both detected from the Sdc, still taking the text form and round-tripping intact. Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Buildifier's unsorted-dict-items and tclfmt's brace spacing plus the 100-column limit in sdc_in_db8.tcl, all flagged by CI. Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
read_db never returned anything, and giving it a 1/0 result for the -sdc flag was an API change nobody asked for. Drop it: OpenRoad::readDb and ord::read_db_cmd are void again and the read_db proc returns nothing. With -sdc, read_db restores the constraints the .odb carries and does nothing if it carries none; a flow that wants to know which case it is in asks ord::sdc_in_db_kind (native, text or none) afterwards and reads an .sdc instead when it has to. The rest of the contract stands: a linked design with no constraints stores nothing, and without -sdc read_db is unchanged. The round-trip tests print the kind after the restore instead of the dropped return value. Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
The sdc_in_db tests recorded whatever happened to be printed -- the
native record with its object ids, write_sdc dumps, liberty warnings,
info lines -- so any unrelated change to those would have failed them
without saying anything about stored constraints. Each test now asserts
the properties it exists to verify through the Tcl, odb and STA APIs
with `check`, fails on the spot if one is violated, and ends with
exit_summary; the .ok files shrink to the two verdict lines.
What each test asserts:
1 write_db stores the native form and only it, headed and with one
clock record per clock
2 read_db -sdc restores clocks, periods and port delays from the
.odb alone
3 read_db without -sdc leaves the stored constraints unrestored
4 native round trip across processes: write_sdc before == after
5 text fallback (derate) round trip: before == after
6 an unconstrained design stores nothing, restores nothing
7 a failed read_db -sdc errors (ORD-0054) and restores nothing
8 the record and the .odb are byte-identical across two same-process
writes and a fresh-process restore-and-write
9 a construct detected from the Sdc (generated clock, port load)
takes the text form and round-trips intact
The fixture moves from liberty1/reg1 to a small Nangate45 netlist,
sdc_in_db.v, because every Nangate45 liberty cell has a LEF master and
linking prints nothing to record; sdc_in_db_common.tcl holds the shared
helpers and quiets the LEF and restore info lines. Nothing in the tests
or their goldens depends on paths, ids, or process output beyond the
verdicts.
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
…nked read_db The opt-in moves from the reader to the producer. `write_db -sdc` stores the current constraints in the block; plain `read_db` restores whatever the block carries whenever the design is linked with liberty, and leaves the record alone otherwise. `read_db -sdc` is gone. Why the producer: a reader has nothing to remember, so every script that reads an .odb gets the constraints for free. An unmodified flow never passes -sdc, writes .odb files without constraints and behaves exactly as before. An odb-only session -- no liberty, no STA, the GUI_TIMING=0 kind of read -- never pays for a restore because there is nothing to restore into, and it carries an existing record through unchanged, so odb-only round trips do not lose the constraints. `write_db` without -sdc on a linked design drops any record the block carried: the caller chose not to store, and a record written before an edit could be stale. Stale-record guard: the native record now lists the odb objects it refers to (bterms, iterms, module iterms, instances, nets) in canonical order and carries a 64-bit FNV-1a digest of their full names. Restoring recomputes the digest over the same ids in the block first and errors out (STA-3012, "the timing constraints stored in the database do not match the design") instead of applying constraints to the wrong objects. On the 50k-register fixture the digest costs about 40 ms on write_db (0.095 s -> 0.13 s) and about 10 ms on the restore (0.38 s -> 0.39 s, under 3% of it); an .odb written with -sdc after a restore is still byte-identical to the original. Tests, all intent-based: 1..9 write with -sdc and read plainly; 3 now proves the odb-only round trip preserves the record; 10 proves that write_db without -sdc on a linked design drops it; 11 builds the stale case odb-only (read without liberty, rename a referenced instance, write) and asserts the linked reader's STA-3012. Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Store the timing constraints in the .odb
write_db -sdcstores the design's timing constraints in the block.read_dbrestores whatever the block carries, as soon as the design it reads is linked
against a liberty; an
.odbwithout constraints, or a read without a liberty,restores nothing and says nothing.
ord::sdc_in_db_kindreports what theloaded block carries,
native,textornone, so a flow can decide whetherto read an
.sdcinstead. The opt-in is on the producer; readers need remembernothing, and no existing command changes its result.
Why
.odbalready subsumes the netlist, the tech, placement, routing, PDN, scan,power domains and
dont_touch. Timing constraints are the conspicuousomission, so every flow carries a matching
.sdcnext to every.odband hasto work out which goes with which. In ORFS that is a glob of
results/, adictionary sort and "the greatest
.sdcat or below this.odb's stem". Withthe constraints in the database the artifact is self-describing: any build
system, and the C++ and Python APIs, get a constrained design from one file.
read_sdcandwrite_sdcare untouched. They are the human interface,forgiving on the way in and canonical on the way out, and a flow still reads
the human-written
.sdconce. What changes is that constraints no longer haveto travel between tools and stages as text.
How
The constraints are stored as a native, id-based record in a block string
property (
sta.sdc.native): pins as dbBTerm/dbITerm ids, instances and netsas ids, clocks by name, floats as hex floats, every set in a canonical order.
Restoring it is a linear pass that calls the Sta constraint makers directly:
no Tcl, no name lookup, no pattern match. The record covers clocks,
propagated clocks, latency, insertion, uncertainty, input/output delays,
false/min/max/multicycle path exceptions and group_path with from/through/to
sets, case analysis, clock groups and the design-level fanout/transition/
capacitance limits. An Sdc with a construct outside that set (generated
clocks, derating, environment, ...) is stored as
write_sdctext (sta.sdc)and replayed through Tcl, so whatever
write_sdccan say the.odbcan carry.Both payloads are opaque to odb: no odb -> sta dependency, and an OpenROAD that
predates this reads the file unchanged and ignores the properties.
The record is guarded against going stale. Its header carries a digest over
the full names of every object it references;
read_dbrecomputes it fromthe block before applying anything and fails with
STA-3012if the design nolonger matches, rather than restoring onto the wrong pins.
write_dbwithout-sdcon a linked design drops any stored record (the caller chose not tostore, and edits may have invalidated it); an odb-only session, with no
liberty, carries the record through untouched. The digest costs about 40 ms
on
write_db -sdcand 10 ms on restore for a 50k-register design.Guarantees (src/dbSta/test/sdc_in_db1..11, intent-based per #11281)
write_sdcafterread_dbis byte-identical towrite_sdcbeforewrite_db -sdc, on asap7-style constraints (threeset_max_delayand fourgroup_pathoverall_inputs/all_registers, false path, multicycle,clock groups, latency, uncertainty, propagated clock,
set_max_fanout,case analysis) and on a derated design that takes the text fallback.
write_db -sdcof the same design, and awrite_db -sdcafter arestore in a fresh process, produce byte-identical
.odbfiles. Restorethen write is a fixpoint.
.odbwithout constraints restores nothing, silently;sdc_in_db_kindsays
none. Restore runs only after a completeread_db.write_dbwithout-sdcon a linked design drops the record; an odb-onlyround trip (no liberty) preserves it; a record whose design was edited
underneath it is rejected with
STA-3012.the spot; the
.okfiles hold two verdict lines each.Forwards compatibility
Nothing existing changes behaviour, and a flow adopts this by choosing to:
write_dbwithout-sdcandread_dbon an.odbwithout constraintsare what they were. A flow that does not opt in produces the same files and
runs the same way as today.
.odbwritten with-sdcunchanged: the constraints are string properties it ignores.
read_dbrestores only when the design is linked with a liberty, so anodb-only read (a GUI opened without timing, a DEF or LEF utility) never pays
for constraints it cannot use, with no flag to remember.
ord::sdc_in_db_kindexists) and rununchanged on an older binary. The ORFS shape is a variable that adds
-sdcto every
write_db, withload_designreading the.sdcalongside onlywhen
sdc_in_db_kindreportsnone; once this OpenROAD is the floor, thefallback, the intermediate
.sdcfiles and their Makefile plumbing retire.Numbers
Measured in bazel-orfs (The-OpenROAD-Project/bazel-orfs#947, a throwaway
harness), same machine, OpenROAD master 5d6f167 with and without this change,
identical synthesis outputs; the restore path measured is the one
read_dbruns, under an earlier
read_db -sdcspelling. The synthetic designs are N flops of pipeline behind a 64-bit buswith the stock ORFS asap7 platform constraints, whose
-from [all_inputs] -to [all_registers]set_max_delay and group_path commands makewrite_sdcname every register clock pin seven times. nangate45/bp_be is the ORFS CI
design with the most ports, as the real-design control.
Load time (
read_db+read_sdcbefore,read_dbwith restore after)Peak memory (OpenSTA
memory_usageat process end: before =read_db,read_sdc,write_sdc,write_db; after =read_dbwith restore,write_sdc)Disk per flow (ORFS writes 20
.odbfiles per run; today also 9.sdcfiles, after only the 2 exports; synth-stage sizes stand in for every stage)write_dbtime (the record is encoded on every write)Whether the native record can carry an Sdc is decided by inspecting the Sdc
through its public accessors (mirroring
WriteSdccategory by category), sowrite_dbwrites and scans no text. The.odbgrows by the native record, afraction of the
.sdcit replaces. The one construct not observable throughthe public Sdc API is
set_clock_sense -positive|-negative(only-stop_propagationis queryable); it is called out in the code, and a smallaccessor in OpenSTA would close it.
Reference: sizes (synth stage)
Reference: step times
Consumer
The-OpenROAD-Project/bazel-orfs#947 is the measurement harness and carries an
earlier spelling of this change together with the ORFS adoption in two steps
(first
load_designrestores from the.odbwhen it can and reads the.sdcotherwise; then the intermediate
.sdcfiles, their Makefile plumbing andfind_sdc_filego). The ORFS patch itself will follow this PR.