synth: single writer for 1_synth.sdc + single-process flow test that enforces it#4333
synth: single writer for 1_synth.sdc + single-process flow test that enforces it#4333oharboe wants to merge 16 commits into
Conversation
1_synth.sdc had two writers: yosys' synth.tcl copied the user's SDC_FILE into place (a leftover from before SDC canonicalization existed, "one day a more sophisticated synthesis..."), then synth_odb.tcl overwrote it in place with the OpenSTA-canonicalized version. cp preserves the source's mode bits, so when SDC_FILE is a read-only build input (bazel RBE), the copy is read-only and the canonicalization write fails. Locally sources are writable, so this only accidentally worked. Drop the copies from synth.tcl and synth_preamble.tcl: synth_odb.tcl/synth_syn.tcl are the sole writers of 1_synth.sdc. 1_2_yosys.sdc (copied by the do-yosys make recipe) remains the raw SDC the yosys stage hands to synth_odb.tcl. Also write 1_synth.odb/.sdc unconditionally in synth_odb.tcl and synth_syn.tcl instead of via the orfs_write_* wrappers: producing those files is the step's contract, and the WRITE_ODB_AND_SDC_EACH_STAGE=0 no-op silently left downstream stages consuming the stale, non-canonicalized copy. Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
There was a problem hiding this comment.
Code Review
This pull request refactors the synthesis scripts to ensure that 1_synth.sdc is solely written and canonicalized by OpenSTA using write_sdc -no_timestamp in synth_odb.tcl and synth_syn.tcl, rather than copying the original SDC file in synth.tcl and synth_preamble.tcl. It also replaces orfs_write_db and orfs_write_sdc with direct log_cmd write_db and log_cmd write_sdc calls. There are no review comments, so I have no feedback to provide.
|
@maliberty TL;DR orfs_write_db is not needed/useful for synthesis, because synthesis is the first stage. |
|
@codex review |
|
I don't follow your comment about WRITE_ODB_AND_SDC_EACH_STAGE. |
We can now create a single .tcl for all openroad stages reusing the scripts, WRITE_ODB_AND_SDC_EACH_STAGE is part of that mechanics. It has been a while, I forget the details. The code isn't complicated, just read it a few lines + you have a floorplan+place joined script that you refer to occasionally, so I'm pretty sure you're aware of the feature. |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
I am aware of the feature and it seems like this PR breaks it. I don't follow your justification for doing so. |
So this feature has no automated tests in CI? |
|
No as with a great many things in ORFS |
Well... if we care about a feature, we must have tests and tests are so much cheaper than they used to be. I'll see about wiring up a bazel driven test in ORFS. I know, I know... CI for |
sv-lang (via tools/OpenROAD) pulls in rules_pycross, whose toolchain extension fails with "Unknown Python version: 3.8" (transitively registered via or-tools -> pybind11_abseil; 3.8 was dropped from rules_python 2.0.0's MINOR_MAPPING). tools/OpenROAD/MODULE.bazel already carries the pycross.configure_environments workaround, but the extension configuration is only honored in the root module, so any bazel analysis in this repository fails without mirroring it here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
…CH_STAGE A sweep of flow/scripts for stage outputs produced outside the orfs_write_db/orfs_write_sdc helpers found four spots that break the single-process flow contract (WRITE_ODB_AND_SDC_EACH_STAGE=0 means stage scripts produce no .odb/.sdc files): - io_placement.tcl, fillcell.tcl and density_fill.tcl forward the input .odb with a raw exec cp on their skip paths. The cp is deliberate — much faster than re-serializing an unchanged database with write_db — so keep it, behind a new orfs_copy_db helper that is gated exactly like orfs_write_db. In a single-process flow the source file may not even exist. - write_ref_sdc.tcl (sourced unconditionally from global_route.tcl) wrote updated_clks.sdc with a raw write_sdc; use orfs_write_sdc. Not addressed here: detail_route.tcl exits the process in the SKIP_DETAILED_ROUTE branch, which would end a single-process flow early. Left for a follow-up since it produces no stray files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
…_STAGE Partially revert the previous commit: synth_odb.tcl and synth_syn.tcl go back to orfs_write_db/orfs_write_sdc. The single-writer property and the removal of the mode-bit-preserving SDC_FILE copies stand, but the writes must stay gated: WRITE_ODB_AND_SDC_EACH_STAGE=0 is the contract of the single-process flow (all stages in one OpenROAD invocation, sourcing the stage scripts back to back), where no stage script may produce .odb/.sdc files — the follow-up commit adds a flow.tcl plus a test that enforces exactly that, and the unconditional writes trip it. With the writes gated the bazel-orfs side needs no patch either: 1_synth.odb/.sdc are produced by the do-1_synth action, which runs with the default WRITE_ODB_AND_SDC_EACH_STAGE=1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
…sertion ORFS supports running all OpenROAD stages in a single OpenROAD process by sourcing the per-stage scripts into one Tcl script: load_design skips reloading when a design is already in memory, KEEP_VARS=1 keeps variables across stages, and WRITE_ODB_AND_SDC_EACH_STAGE=0 turns the per-stage .odb/.sdc writes into no-ops. floorplan_to_place.tcl uses this for floorplan through place; this PR's discussion noted the mechanism has no automated test coverage. Add scripts/flow.tcl, which runs the yosys netlist through the final report in one process with WRITE_ODB_AND_SDC_EACH_STAGE=0: - flow_source sources a stage script and errors if it wrote any .odb/.sdc file - the single-process contract that this PR's first version broke with unconditional writes in synth_odb.tcl (this test catches that). - flow_write_db/flow_write_sdc write out, from the top level, exactly the .odb/.sdc file set the stage-per-process make flow produces (substep writes plus the Makefile do-copy products). Cover it with bazel tests on asap7/gcd: an orfs_run executes the whole flow off the yosys synthesis outputs under FLOW_VARIANT=single, and per-stage sh_tests byte-compare the stage-boundary .odb/.sdc files (synth through final) against the regular per-stage flow targets, reusing tools/OpenROAD's check_same.sh. Equal bytes proves the single-process mode is equivalent to the per-stage flow, not merely that it runs to completion. bazelisk test //flow/designs/asap7/gcd:gcd_single_flow_test Known divergence, deliberately left failing so it gets root-caused rather than papered over: from grt on, the grt-stage repair_timing inserts one extra buffer when continuing from in-memory CTS state instead of reloading a byte-identical 4_cts.odb. SYNTH_USE_SYN is declared make-only (local_arguments): bazel-orfs's synth stage always runs the yosys flow and neither stages the Verilog sources nor sets VERILOG_FILES for the OpenROAD synthesis step, so the built-in synthesizer opt-in must not reach the bazel arguments (with it, bazelisk build :gcd_synth fails outright). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
The gcd_single_flow grt/route/final comparisons failed: the grt-stage repair_timing inserted one extra buffer when the flow continued from in-memory CTS state instead of reloading the byte-identical 4_cts.odb. Root cause, found by bisecting the single-process flow stage by stage and then diffing resistance-aware routing decisions: pins of a freshly created instance that are never connected — the dangling outputs of the dummy-load buffers (clkload*) CTS inserts to balance the clock tree — get no connectPinAfter call, so they never enter the OpenSTA search endpoint set and their required times are never seeded. Once the clock is propagated, such a pin reports slack = 0 - arrival (-195 ps on gcd) instead of +infinity. A process that reads the same design from disk seeds requireds for every endpoint in the full scan, so it sees +infinity. global_route -resistance_aware consumes net slacks to pick and order the resistance-aware net set, so the clock nets sorted differently and routing diverged before any repair ran; the repair_timing buffer delta was downstream fallout. Fix in Sta::makeInstanceAfter: register the new pin vertices as endpoint candidates (and initialize makePinVertices' return values, which are left unassigned for power/ground pins). Carried as patches/openroad/0001-sta-register-new-instance-pin-vertices-as- endpoints.patch, applied by fetching openroad via git_override pinned to the exact commit the tools/OpenROAD submodule records (with submodules, so the patch reaches src/sta); local_path_override does not accept patches. Revert to local_path_override when the fix lands upstream. With the patch, synth/floorplan/place/cts/grt are byte-identical between the single-process and per-stage flows. route and final still diverge from a second, independent cause (detail route behaves differently in-process than freshly loaded despite byte-identical 5_1_grt.odb inputs); their tests stay failing on purpose and the BUILD-file comment records the state. Ruled out along the way: global rand() state (cts/gpl do perturb it, but nothing in rsz/grt/est/dbSta consumes bare rand()), grt's shuffle seed (off by default), SDC write precision (gcd constraints are exact integers), rsz static move counters (report-only), odb unique-name counters (serialized). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
|
Root-caused and fixed the grt-stage single-process divergence (640e205). Root cause: the dummy-load buffers CTS inserts to balance the clock tree ( Fix: 5 lines in Usual suspects ruled out with evidence: global Test matrix now: synth / floorplan / place / cts / grt byte-identical ✅. route/final still diverge from a second, independent cause — detail route behaves differently in-process than freshly loaded despite byte-identical |
With the grt stage byte-identical, route and final still diverged. Root causes, found with the same fresh-vs-in-process experiment pair on byte-identical 5_1_grt.odb inputs: - drt's incremental design update (reused when pin_access already ran in the same process) appends recreated nets and instances to its lists while a fresh read follows odb iteration order, and track assignment iterates those lists: the wire assignment diverged before detail routing started. - drt's final database update emitted each net's vias in a history-derived order (the track-assignment path already canonicalizes; the final path did not). Both fixed by patches/openroad/0002-drt-make-incremental-design- updates-match-a-fresh-read.patch: reorder insts/nets to odb order in updateDesign, and order vias canonically in the final write. The routed layout is now identical between the flows (equal DEF for 5_route). The route/final tests stay red on the .odb byte comparison for two fully root-caused reasons recorded in the BUILD file: odb id reuse history (byte equality after further edits needs canonical-id serialization) and the 6_final.odb write point (the per-stage flow writes it before the RCX extraction reorders every dbWire; the single-process flow writes at end of script). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
|
Root-caused and fixed the route-stage divergence as well (87b6286). Root causes (fresh-vs-in-process experiment pair on byte-identical
Both fixed in Why route/final still fail the .odb byte comparison (both fully root-caused, recorded in the BUILD file):
Open question for the follow-up: pursue canonical-id odb serialization (real prize for bazel caching/determinism, but a substantial odb change), compare route/final on canonicalized content (e.g. DEF after orderWires) instead of odb bytes, or move |
|
@maliberty I need guidance on this one #4333 (comment) to make progress. Single invocation flow has issues to be fixed(we knew that...), the test now identifies the basic issues for asap7 gcd for the flow to be identical to split invocation. With a bit of guidance from you, I can ask Claude to complete the patches, then we can create PRs upstream and let the fixes trickle through then clean up here... |
| + if (vertex) | ||
| + search_->endpointInvalid(vertex); | ||
| + if (bidir_drvr_vertex) | ||
| + search_->endpointInvalid(bidir_drvr_vertex); |
There was a problem hiding this comment.
Yes. I reached out to James regarding upstreaming this fix. Parallelly I am trying to create a standalone testcase that can somehow reproduce this gap too.
This is wrong - the free lists are serialized and reloaded. |
…points Review feedback (maliberty) was right: odb free lists are serialized and reloaded, so the earlier explanation blaming id/freelist history for the remaining route-stage .odb difference was wrong. With byte-identical inputs both flows start from equivalent allocation state, which meant the databases had to contain genuinely different objects — and they did. Per-table stream accounting located the delta in the access point table: the per-stage flow's 5_route.odb contained 138 block-terminal access points where the single-process flow had 69. drt's updateDbAccessPoints appends bterm access points without destroying the previously written ones, so every process that reruns pin access on a design that already has them (each flow stage after pin_access first ran) duplicates them; the single-process flow's in-memory bookkeeping skips the unchanged terms and was the correct one. Fixed by patches/openroad/0003 (destroy previously written bterm access points before writing, in reverse creation order so recycled ids preserve the original assignment). The route stage is now byte-identical between the flows and its test passes; only the final stage remains, from the already-documented 6_final.odb write point vs RCX's orderWires re-encoding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
|
You're right, and thanks — the free lists are indeed serialized and reloaded ( Per-table stream accounting (the Fixed in |
The final stage was the last remaining divergence in the single-process flow test: the per-stage flow writes 6_final.odb in the middle of final_report.tcl (after global_connect, before deleteRoutingObstructions and the RCX extraction whose odb orderWires re-encodes every dbWire), while the single-process flow could only write at end of script and captured the re-encoded state. Split final_report.tcl into final_connect.tcl (metrics stage, load, PRE hook, set_propagated_clock, global_connect) and final_outputs.tcl (everything after the write); final_report.tcl now sources the first, performs the gated 6_final.odb write, and sources the second, so the per-stage flow is unchanged. flow.tcl sources the two parts and performs the write itself at the identical point. With this, all seven stage boundaries (synth, floorplan, place, cts, grt, route, final) are byte-identical between the single-process and stage-per-process flows: //flow/designs/asap7/gcd:gcd_single_flow_test 7/7 PASSED Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
|
7/7 — the whole flow is now byte-identical between the single-process and stage-per-process flows (4567701). The last stage, Final test matrix for Summary of what the single-process equivalence test flushed out of OpenROAD along the way, all carried as
|
|
@codex review |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a single-process flow variant for OpenROAD, allowing all stages from synthesis to final reporting to run within a single process. It includes several patches to OpenROAD to ensure byte-identical outputs between the single-process and per-stage flows, alongside test suites to verify this equivalence. The review feedback highlights two key issues: a strict weak ordering violation in the sorting comparator of the second OpenROAD patch that could lead to undefined behavior, and a recommendation to use file join in flow.tcl to avoid path mismatches caused by manual string concatenation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4567701474
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
flow_write_db/flow_write_sdc built paths with string concatenation; a trailing slash in RESULTS_DIR would produce dir//file, which never matches glob results in flow_source's lsearch -exact check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
detail_route.tcl exited on the skip path, which is fine when each stage is its own process (OPENROAD_CMD passes -exit anyway) but kills the whole single-process flow before 5_2_route.odb and every later stage is written. Skip via if/else instead of exit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
The fillConnFigs comparator returned false whenever either side was a non-via, making every non-via equivalent to every via while vias were still ordered among themselves — not a strict weak ordering, so std::list::sort was UB. Partition non-vias before vias instead (list::sort is stable, so non-vias keep their relative order); each connFig is encoded as its own path in the written wire, so the partitioning does not change the layout. Regenerates patches 0002 (the fix) and 0003 (hunk offsets shift). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
glibc 2.42 defines memchr as a function-like _Generic macro under C23 (implied by _GNU_SOURCE), and sed's vendored gnulib only #undefs it for _LIBC builds, so lib/memchr.c no longer parses against current host glibc headers and every build of @Sed (bazel-orfs -> abc -> readline -> ncurses -> sed) fails. Carry a one-hunk #undef patch via single_version_override until a fixed sed lands in BCR; 4.9.bcr.5 was tried and fails the same way under toolchains_llvm + host sysroot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
OpenROAD master merged the hermetic-llvm-toolchain work (#10812): a statically linked, zero-sysroot LLVM toolchain with no host compiler or /usr/include involved. Bump the pin from f4e5e40f to e8230044 and switch this repo from toolchains_llvm (host sysroot) to the same hermetic toolchain, mirroring the root-honored parts of tools/OpenROAD's MODULE.bazel/.bazelrc: - llvm 0.8.11 + register_toolchains(@llvm//toolchain:all), plus BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 and rules_python bootstrap_impl=script in .bazelrc. - gnulib tool version overrides (sed 4.9.bcr.5, gawk 5.3.2.bcr.7, m4 1.4.21.bcr.4, bison 3.8.2.bcr.7 + wrapper-header patch) and the tcl_lang crypt.h patch; single_version_override is root-honored only, so each is copied here with its patch files. - bazel/slang-compat placeholder: openroad's new_local_repository(name="slang") resolves its relative path against the root workspace, so ORFS-as-root must provide the directory. The zero-sysroot toolchain takes host glibc out of the build entirely, which supersedes the sed gnulib memchr patch for glibc >= 2.42 — remove it. The abc, boost.context and scip overrides openroad carries are not needed in this graph (full test suite builds without them). patches/openroad/0001..0003 are unchanged: no src/sta or src/drt changes between the old and new pin, and the src/sta submodule pin is identical. All seven gcd_single_flow byte-equality tests pass on the new base. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a single-process flow variant for OpenROAD, allowing all stages from synthesis to final report to run within a single OpenROAD process. It adds a test suite (gcd_single_flow_test) to verify that the single-process flow produces byte-identical outputs compared to the traditional per-stage flow. To support this, several Tcl scripts were refactored, including splitting final_report.tcl and introducing helper functions like orfs_copy_db. Additionally, the build configuration is updated to use hermetic LLVM (@llvm), and several single-version overrides and patches are introduced for dependencies like bison, tcl_lang, and openroad to resolve toolchain compatibility issues. There are no review comments, so no further feedback is provided.
|
@maliberty Anything else from me? |
The OpenROAD bump to master e8230044 plus patches/openroad/0001..0003 (the sta endpoint-seeding fix that changes -resistance_aware global routing, and the two drt ordering/access-point fixes) shift QoR deterministically across designs, not just the gcd single-flow test target. Three designs regressed past their committed rules-base.json limits in the pr-head CI run (build #7, 93b9992): - nangate45/mempool_group: cts setup tns - nangate45/tinyRocket: cts/globalroute/finish setup tns - sky130hd/microwatt: globalroute/finish setup tns, detailed route antenna diode count Regenerate the three rules files with genRuleFile.py --failing --tighten (equivalent to `make update_rules`) against the metadata the pr-head job produced, so the failing limits are relaxed with the standard margin and incidentally-improved metrics are tightened. checkMetadata.py passes for all three against that metadata. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
1_synth.sdchad two writers: yosys'synth.tclcopied the user'sSDC_FILEinto place, thensynth_odb.tcloverwrote it in place with the OpenSTA-canonicalized version.cppreserves the source's mode bits, so whenSDC_FILEis a read-only build input (bazel RBE), the copy is read-only and the canonicalization write fails — see OpenROAD #10818, which papered over this withchmod +w. On writable local checkouts it only accidentally worked.The first version of this PR made
synth_odb.tcl/synth_syn.tclwrite1_synth.odb/.sdcunconditionally, which — as the review pointed out — breaks the single-OpenROAD-process flow (WRITE_ODB_AND_SDC_EACH_STAGE=0, all stage scripts sourced into one process). That mode had no test coverage. This PR now keeps the single-writer fix, restores the gating, and adds the missing coverage; the review history is retained as follow-up commits:flow/scripts/flow.tcl: the whole flow (yosys netlist through the final report) as one Tcl script in a single OpenROAD process. WithWRITE_ODB_AND_SDC_EACH_STAGE=0the top level is the sole writer:flow_write_db/flow_write_sdcproduce exactly the .odb/.sdc file set of the stage-per-process make flow, andflow_sourceerrors if any stage script writes an .odb/.sdc itself — this assertion catches the unconditional writes from this PR's first version (verified: the flow fails withsynth_odb.tcl wrote .../1_synth.odbwhen they are reinstated).io_placement.tcl/fillcell.tcl/density_fill.tclforward the input .odb with a rawexec cpon their skip paths (kept — cp is much faster than re-serializing an unchanged DB, and Tcl'sfile copyhas second-resolution mtimes that break make's up-to-date checks — but now behind a gatedorfs_copy_dbhelper),write_ref_sdc.tclwroteupdated_clks.sdcungated, anddetail_route.tclexited on itsSKIP_DETAILED_ROUTEskip path, which would have killed a single process running the whole flow (the per-stage flow always passes-exit).bazelisk test //flow/designs/asap7/gcd:gcd_single_flow_testruns the single-process flow off the yosys outputs underFLOW_VARIANT=singleand byte-compares every stage boundary (synth, floorplan, place, cts, grt, route, final × .odb/.sdc) against the regular per-stage flow targets, reusing tools/OpenROAD'scheck_same.sh.All seven stage boundaries are byte-identical between the single-process and per-stage flows. Three OpenROAD fixes were needed, carried as
patches/openroad/on thegit_override(pinned to the exact commit thetools/OpenROADsubmodule records, with submodules, so a patch can reachsrc/sta;local_path_overrideaccepts no patches):clkload*) — a bogus −195 ps net slack steered-resistance_awareglobal routing in the process that inserted them, diverging grt (an extrarepair_timingbuffer). Upstreaming is in progress with the OpenSTA maintainers.detailed_routeafter earlier stages iterated nets/instances in incremental-update order rather than odb order, and emitted vias in routing-history order — a fresh process on a byte-identical database routed differently. Nets/insts are reordered to odb order after incremental updates and vias are written in a canonical order (per review: the sort comparator is a strict weak ordering — non-vias first keeping their relative order, then vias by cut layer and origin).The final-stage residue was flow structure, not OpenROAD: the per-stage flow writes
6_final.odbmid-final_report.tcl(before RCX extraction reorders every dbWire), sofinal_report.tclis split around that write andflow.tclwrites at the same point.OpenROAD is bumped to master
e8230044(hermetic-llvm toolchain merged), and this repo's bazel setup adopts the same zero-sysroot toolchain — no host compiler or/usr/includein the build, so host glibc changes can't break it. The root-honored parts of tools/OpenROAD's MODULE.bazel are mirrored (gnulib tool version overrides sed/gawk/m4/bison + the bison and tcl_lang patches,bazel/slang-compatplaceholder), as is the earlier rules_pycross Python 3.8 workaround (extension configuration is root-honored only).SYNTH_USE_SYNis declared make-only for gcd's bazel flow (bazel-orfs's synth doesn't stage Verilog sources for the OpenROAD synth step).With the writes gated, the bazel-orfs side needs no patch (bazel-orfs#754 anticipated this;
1_synth.odb/.sdccome from thedo-1_synthaction, which runs with the defaultWRITE_ODB_AND_SDC_EACH_STAGE=1).Supersedes #4338.
🤖 Generated with Claude Code