Skip to content

Commit eafe2fc

Browse files
oharboeclaude
andcommitted
flow/bazel: synth-stage stub LEFs so a parent's synth doesn't block on macro PD
Add a synth_odb.tcl path that emits a port-only stub LEF and a write_timing_model Liberty for the synthesized design, plus a matching .odb-side path in load.tcl that re-reads ADDITIONAL_LEFS so lefin can augment stub masters with real geometry at floorplan time. Both paths are gated on env vars (ORFS_WRITE_SYNTH_STUBS, ORFS_AUGMENT_STUB_MASTERS) and are no-ops when unset. Companion bazel-orfs patches are vendored under orfs-bazel-patches/ and applied via MODULE.bazel's bazel-orfs git_override: they set the env vars, declare stub artifacts in OrfsInfo, and substitute each <name>_generate_abstract macro dep with <name>_synth for the synth Bazel action only - so the parent's synth depends on each child's synth target instead of the child's full PD flow. Motivation: make gui_synth on a parent of a large hierarchical bazel-orfs design. Today the parent's synthesis blocks on every child macro completing floorplan/place/cts (hours on big designs) even though the GUI only needs the synthesized netlist. With this change a parent's synth starts as soon as each child's synth_odb finishes. The primary win is engineer iteration time on gui_synth; CI/automation wait time is a secondary nice-to-have. Status: * Synth-only paths (gui_synth, synth reports, synth-stage WNS) produce bit-identical results to the unpatched flow. * Full PD against stub-master ODBs is currently broken: lefin updates dbMaster width/height in place, but each dbInst's bbox was cached from the 1x1 stub dims and OpenROAD has no public API to refresh it. ORFS_AUGMENT_STUB_MASTERS is therefore documented as experimental until OpenROAD adds a bbox-refresh API. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7b05472 commit eafe2fc

7 files changed

Lines changed: 296 additions & 0 deletions

File tree

MODULE.bazel

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,25 @@ BAZEL_ORFS_REMOTE = "https://github.com/The-OpenROAD-Project/bazel-orfs.git"
4444
git_override(
4545
module_name = "bazel-orfs",
4646
commit = BAZEL_ORFS_COMMIT,
47+
patch_strip = 1,
48+
patches = [
49+
# Parent synth depends only on each macro's _synth target
50+
# (not _generate_abstract). Companion to ORFS-side changes
51+
# in flow/scripts/{synth_odb.tcl,load.tcl} — synth_odb.tcl
52+
# emits port-only stub LEF + Liberty (gated on
53+
# ORFS_WRITE_SYNTH_STUBS=1) that the parent's synth_odb
54+
# consumes in place of waiting for the macro's
55+
# floorplan/place/cts. Floorplan onwards still pulls real
56+
# LEFs via deps on _generate_abstract. The primary motivation
57+
# is `make gui_synth` turnaround for engineers iterating on
58+
# RTL: today a parent's synthesis blocks on every child
59+
# macro's full PD flow, which can be hours on a large
60+
# hierarchical design; with these patches it blocks only on
61+
# each child's synthesis. To upstream into bazel-orfs.
62+
"//orfs-bazel-patches:synth-stubs-env.patch",
63+
"//orfs-bazel-patches:synth-stubs-rules.patch",
64+
"//orfs-bazel-patches:synth-stubs-flow.patch",
65+
],
4766
remote = BAZEL_ORFS_REMOTE,
4867
)
4968

flow/scripts/load.tcl

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,78 @@ proc load_design { design_file sdc_file } {
2727
log_cmd link_design {*}[hier_options] $::env(DESIGN_NAME)
2828
} elseif { $ext == ".odb" } {
2929
log_cmd read_db {*}[hier_options] $::env(RESULTS_DIR)/$design_file
30+
# When the parent's synth_odb stage was run against stub macro
31+
# LEFs (port-only, no geometry — see synth_odb.tcl's
32+
# ORFS_WRITE_SYNTH_STUBS path), 1_synth.odb carries those stub
33+
# masters as 1×1 frozen blocks. Read the real ADDITIONAL_LEFS
34+
# here so lefin augments them in place: width/height/class/site
35+
# get updated to the real LEF values, and pin geometry is
36+
# attached to the existing dbMTerms (whose directions came
37+
# from the stub).
38+
#
39+
# Per-LEF gating: only re-read LEFs whose primary MACRO is
40+
# currently a 1×1 stub. ADDITIONAL_LEFS at floorplan may also
41+
# contain non-stub macros (e.g. behavioral memories whose real
42+
# LEF was already read at synth time and is fully baked into
43+
# 1_synth.odb). Re-reading those would duplicate pin geometry
44+
# (lefin's pin handler leaves existing dbMTerms in place but
45+
# still adds the new geometry boxes). 1×1 is a unique stub
46+
# marker — synth_odb.tcl always writes SIZE 1 BY 1 in stub LEFs
47+
# and real macros are orders of magnitude larger.
48+
#
49+
# NOTE: this path is currently experimental. lefin updates the
50+
# dbMaster width/height in place, but each dbInst's cached
51+
# bbox was initialised at create time from the 1×1 stub master
52+
# dims and OpenROAD has no public API to refresh that bbox. As
53+
# a result, floorplan/place still see macros as 1×1 boxes and
54+
# CTS fails with EST-0092. A future fix in OpenROAD (e.g. a
55+
# dbInst::resetBox() or dbInst::setMaster() that reinitialises
56+
# the cached bbox from the master) is needed to unlock the
57+
# full PD flow against synth-stage stub masters.
58+
if { [info exists ::env(ORFS_AUGMENT_STUB_MASTERS)] \
59+
&& $::env(ORFS_AUGMENT_STUB_MASTERS) == "1" \
60+
&& [env_var_exists_and_non_empty ADDITIONAL_LEFS] } {
61+
set stub_master_names {}
62+
foreach lib [$db getLibs] {
63+
foreach master [$lib getMasters] {
64+
if { [$master getWidth] == 1 && [$master getHeight] == 1 } {
65+
lappend stub_master_names [$master getName]
66+
}
67+
}
68+
}
69+
foreach lef $::env(ADDITIONAL_LEFS) {
70+
set fh [open $lef r]
71+
set is_stub_lef 0
72+
while { [gets $fh line] >= 0 } {
73+
if { [regexp {^\s*MACRO\s+([A-Za-z_][A-Za-z0-9_]*)} $line - mname] } {
74+
if { [lsearch -exact $stub_master_names $mname] >= 0 } {
75+
set is_stub_lef 1
76+
}
77+
break
78+
}
79+
}
80+
close $fh
81+
if { $is_stub_lef } {
82+
read_lef $lef
83+
}
84+
}
85+
86+
# Re-set each dbInst's location to itself so OpenROAD has a
87+
# chance to recompute the bbox from the (now-augmented) master
88+
# dims. This is the only public refresh trigger today; if/when
89+
# OpenROAD adds an explicit bbox refresh API, switch to that.
90+
set block [ord::get_db_block]
91+
if { $block != "NULL" } {
92+
foreach inst [$block getInsts] {
93+
set master [$inst getMaster]
94+
set mname [$master getName]
95+
if { [lsearch -exact $stub_master_names $mname] >= 0 } {
96+
set loc [$inst getLocation]
97+
$inst setLocation [lindex $loc 0] [lindex $loc 1]
98+
}
99+
}
100+
}
101+
}
30102
} else {
31103
error "Unrecognized input file $design_file"
32104
}

flow/scripts/synth_odb.tcl

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,66 @@ orfs_write_db $::env(RESULTS_DIR)/1_synth.odb
3131
# which are read in here and a canonicalized version is written
3232
# out by OpenSTA that has no dependencies.
3333
orfs_write_sdc $::env(RESULTS_DIR)/1_synth.sdc
34+
35+
# Emit port-only stub LEF + Liberty for the synthesized design so a
36+
# parent that instantiates this design as a macro can complete its
37+
# synth_odb stage without waiting on our floorplan/place/cts to
38+
# produce real LEF and post-place Liberty. The stub LEF carries only
39+
# port names + directions (no geometry); the Liberty is generated by
40+
# OpenSTA's write_timing_model and carries arc delays computed from
41+
# the loaded netlist with ideal clocks. Together they are enough to
42+
# satisfy read_lef + read_liberty + read_verilog + dbInst::create at
43+
# the parent's synth_odb, and enough for the parent's synth-time STA
44+
# to compute path delays through macro arcs.
45+
if { [info exists ::env(ORFS_WRITE_SYNTH_STUBS)] && $::env(ORFS_WRITE_SYNTH_STUBS) == "1" } {
46+
set design $::env(DESIGN_NAME)
47+
set block [ord::get_db_block]
48+
49+
# Per-design filename so OpenROAD's lefin doesn't reject the second
50+
# stub LEF in a parent's synth_odb with `ODB-0229 Error: library
51+
# (1_synth) already exists` — lefin derives the library name from
52+
# the LEF file's basename, and a parent reading multiple sibling
53+
# stubs would otherwise see colliding "1_synth" libraries.
54+
set lef_file $::env(RESULTS_DIR)/${design}_1_synth.lef
55+
set f [open $lef_file w]
56+
puts $f "VERSION 5.7 ;"
57+
puts $f "BUSBITCHARS \"\[\]\" ;"
58+
puts $f "DIVIDERCHAR \"/\" ;"
59+
puts $f ""
60+
puts $f "MACRO $design"
61+
puts $f " CLASS BLOCK ;"
62+
puts $f " ORIGIN 0 0 ;"
63+
puts $f " SIZE 1 BY 1 ;"
64+
puts $f " SYMMETRY X Y ;"
65+
foreach bterm [$block getBTerms] {
66+
set pname [$bterm getName]
67+
set io [$bterm getIoType]
68+
if { $io eq "FEEDTHRU" } {
69+
continue
70+
}
71+
puts $f " PIN $pname"
72+
puts $f " DIRECTION $io ;"
73+
puts $f " USE SIGNAL ;"
74+
puts $f " END $pname"
75+
}
76+
puts $f "END $design"
77+
puts $f ""
78+
puts $f "END LIBRARY"
79+
close $f
80+
81+
# Emit a real-timing Liberty via OpenSTA's write_timing_model with
82+
# ideal clocks. The synthesized netlist is already loaded; PDK
83+
# Liberty is loaded too. write_timing_model characterises the
84+
# design as a Liberty cell with arc delays derived from the
85+
# current STA setup. Without parasitics, the arcs are optimistic
86+
# compared to post-place .lib but include cell delays through
87+
# the macro — enough to keep a parent's synth-stage WNS within
88+
# noise of the legacy (post-place-lib) flow.
89+
set lib_file $::env(RESULTS_DIR)/${design}_1_synth.lib
90+
set_clock_latency -source 0 [all_clocks]
91+
if { [env_var_exists_and_non_empty CORNERS] } {
92+
log_cmd write_timing_model -scene [lindex $::env(CORNERS) 0] $lib_file
93+
} else {
94+
log_cmd write_timing_model $lib_file
95+
}
96+
}

orfs-bazel-patches/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exports_files(glob(["*.patch"]))
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
private/environment.bzl: ORFS_WRITE_SYNTH_STUBS + ORFS_AUGMENT_STUB_MASTERS env
2+
3+
--- a/private/environment.bzl
4+
+++ b/private/environment.bzl
5+
@@ -56,6 +56,24 @@
6+
"KLAYOUT_CMD": _executable_path(_klayout_attr(ctx)),
7+
"OPENROAD_EXE": _executable_path(ctx.attr.openroad),
8+
"OPENSTA_EXE": _executable_path(ctx.attr.opensta),
9+
+ # See flow/scripts/synth_odb.tcl and flow/scripts/load.tcl in
10+
+ # OpenROAD-flow-scripts:
11+
+ # - WRITE_SYNTH_STUBS=1 tells synth_odb.tcl to emit port-only
12+
+ # stub <design>_1_synth.lef + .lib for this design so parents
13+
+ # can consume them at synth time without waiting on this
14+
+ # macro's floorplan/place/cts.
15+
+ # - AUGMENT_STUB_MASTERS=1 tells load.tcl's .odb branch to
16+
+ # read_lef ADDITIONAL_LEFS after read_db so lefin augments
17+
+ # stub masters (1×1, no geometry) baked into 1_synth.odb
18+
+ # with the real macro LEF at floorplan and later stages.
19+
+ # Both env vars are no-ops on ORFS revisions that don't have
20+
+ # the matching synth-stub-masters changes in load.tcl /
21+
+ # synth_odb.tcl. AUGMENT_STUB_MASTERS is currently experimental
22+
+ # — it requires an OpenROAD bbox-refresh API for the full PD
23+
+ # flow to work end-to-end; until then it's only useful with
24+
+ # synth-only targets such as gui_synth.
25+
+ "ORFS_WRITE_SYNTH_STUBS": "1",
26+
+ "ORFS_AUGMENT_STUB_MASTERS": "1",
27+
} | orfs_environment(ctx)
28+
29+
def yosys_environment(ctx):
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
private/flow.bzl: substitute macros->_synth for synth deps in _orfs_pass
2+
3+
--- a/private/flow.bzl
4+
+++ b/private/flow.bzl
5+
@@ -38,6 +38,29 @@
6+
kwargs.pop("yosys", None)
7+
return kwargs
8+
9+
+_GENERATE_ABSTRACT_SUFFIX = "_generate_abstract"
10+
+
11+
+def _synth_macros(macros):
12+
+ """Substitute each `<name>_generate_abstract` macro label with its
13+
+ `<name>_synth` sibling so the parent's synth Bazel action depends
14+
+ only on each child macro's synth target, not the macro's full
15+
+ PD flow.
16+
+
17+
+ The substituted target's OrfsInfo carries a port-only stub LEF
18+
+ (1_synth.lef) and stub Liberty (1_synth.lib) emitted by
19+
+ synth_odb.tcl under ORFS_WRITE_SYNTH_STUBS=1. Labels that
20+
+ don't follow the `_generate_abstract` convention are passed
21+
+ through unchanged (e.g. behavioral macros without a synth flow).
22+
+ """
23+
+ out = []
24+
+ for m in macros:
25+
+ s = str(m)
26+
+ if s.endswith(_GENERATE_ABSTRACT_SUFFIX):
27+
+ out.append(s[:-len(_GENERATE_ABSTRACT_SUFFIX)] + "_synth")
28+
+ else:
29+
+ out.append(m)
30+
+ return out
31+
+
32+
def _merge_extra_arguments(a, b):
33+
"""Merge two {stage: [label, ...]} dicts, concatenating per-stage lists."""
34+
merged = dict(a)
35+
@@ -518,7 +541,7 @@
36+
stage_arguments = stage_arguments,
37+
arguments = arguments,
38+
sources = sources,
39+
- deps = macros,
40+
+ deps = _synth_macros(macros),
41+
kept_macros = kept_macros if kept_macros != None else {},
42+
kept_macros_enabled = kept_macros != None,
43+
module_top = top,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
private/rules.bzl: SYNTH_STUB_OUTPUTS + OrfsInfo.lef/lib_pre_layout for synth
2+
3+
--- a/private/rules.bzl
4+
+++ b/private/rules.bzl
5+
@@ -744,8 +744,21 @@
6+
7+
CANON_OUTPUT = "1_1_yosys_canonicalize.rtlil"
8+
SYNTH_OUTPUTS = ["1_2_yosys.v", "1_2_yosys.sdc", "1_synth.sdc", "mem.json"]
9+
+# Synth-stage stub artifacts: port-only LEF + Liberty emitted by
10+
+# synth_odb.tcl (when ORFS_WRITE_SYNTH_STUBS=1). Parents instantiating
11+
+# this design as a macro consume them at synth_odb time so they don't
12+
+# have to wait for our floorplan/place/cts to produce real LEF and
13+
+# post-place Liberty. Augmented in place by lefin when the real LEF
14+
+# is read at the parent's floorplan stage (load.tcl's
15+
+# ORFS_AUGMENT_STUB_MASTERS path). Filename is per-design to avoid
16+
+# lefin's `library (1_synth) already exists` error when a parent
17+
+# reads multiple sibling stubs (lefin derives library name from LEF
18+
+# basename).
19+
SYNTH_REPORTS = ["synth_stat.txt", "synth_mocked_memories.txt"]
20+
21+
+def synth_stub_outputs(module_top):
22+
+ return ["{}_1_synth.lef".format(module_top), "{}_1_synth.lib".format(module_top)]
23+
+
24+
def _yosys_parallel_synth(ctx, config, canon_output, synth_outputs, synth_logs, synth_reports, num_partitions, save_odb, all_arguments = {}):
25+
"""Parallel synthesis: keep → kept-json → N partitions → merge.
26+
27+
@@ -1123,7 +1136,10 @@
28+
deps_inputs(ctx),
29+
],
30+
),
31+
- outputs = [synth_outputs["1_synth.odb"], synth_outputs["1_synth.sdc"]],
32+
+ outputs = [
33+
+ synth_outputs["1_synth.odb"],
34+
+ synth_outputs["1_synth.sdc"],
35+
+ ] + [synth_outputs[n] for n in synth_stub_outputs(ctx.attr.module_top)],
36+
tools = yosys_and_flow_tools,
37+
)
38+
39+
@@ -1199,8 +1215,9 @@
40+
41+
synth_logs = declare_artifacts(ctx, "logs", ["1_2_yosys.log", "1_2_yosys_metrics.log"] + (["1_synth.log"] if save_odb else []))
42+
43+
+ stub_outputs = synth_stub_outputs(ctx.attr.module_top) if save_odb else []
44+
synth_outputs = {}
45+
- for output in SYNTH_OUTPUTS + (["1_synth.odb"] if save_odb else []):
46+
+ for output in SYNTH_OUTPUTS + (["1_synth.odb"] + stub_outputs if save_odb else []):
47+
synth_outputs[output] = declare_artifact(ctx, "results", output)
48+
49+
synth_reports = declare_artifacts(ctx, "reports", SYNTH_REPORTS)
50+
@@ -1415,9 +1432,17 @@
51+
variant = ctx.attr.variant,
52+
odb = synth_outputs.get("1_synth.odb"),
53+
gds = None,
54+
- lef = None,
55+
+ # Port-only stub LEF + Liberty written by synth_odb.tcl
56+
+ # under ORFS_WRITE_SYNTH_STUBS=1. A parent that targets
57+
+ # this rule (instead of <name>_generate_abstract) for its
58+
+ # synth-stage deps consumes these stubs and schedules
59+
+ # without waiting for our floorplan/place/cts. lefin
60+
+ # augments stub masters with full geometry when the real
61+
+ # macro LEF is read at the parent's floorplan stage
62+
+ # (load.tcl's ORFS_AUGMENT_STUB_MASTERS path).
63+
+ lef = synth_outputs.get("{}_1_synth.lef".format(ctx.attr.module_top)),
64+
lib = None,
65+
- lib_pre_layout = None,
66+
+ lib_pre_layout = synth_outputs.get("{}_1_synth.lib".format(ctx.attr.module_top)),
67+
additional_gds = depset(
68+
[dep[OrfsInfo].gds for dep in ctx.attr.deps if dep[OrfsInfo].gds],
69+
),

0 commit comments

Comments
 (0)