Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ load("@rules_shell//shell:sh_binary.bzl", "sh_binary")

exports_files([
"bump.py",
"compute_floorplan_shape.tcl",
"compute_slack_margin.tcl",
"config_mk_parser.py",
"deploy.tpl",
"html_timing_report.tcl",
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,17 @@ invalidate the synthesis cache.
DSE by scripting `bazel build` invocations with different `--//pkg:flag=value`
arguments and parsing PPA metrics from the build outputs.

**Computed arguments — a precursor to DSE.** Before sweeping a parameter, it is
often cheaper to *compute* a defensible value from the synthesised netlist or
prior-stage ODB. bazel-orfs ships two ready-to-use Tcl scripts at the repository
root for this — `compute_floorplan_shape.tcl` (emits `CORE_UTILIZATION` /
`CORE_MARGIN`) and `compute_slack_margin.tcl` (emits
`SETUP_/HOLD_SLACK_MARGIN`) — both invoked through the existing `orfs_arguments`
rule. Computed arguments and AutoTuner compose: compute the seed, then let an
external optimizer explore the local neighbourhood. See
[docs/orfs_arguments.md](docs/orfs_arguments.md#a-way-out-of-parameter-guess-pray-stare-at-logs-hell)
for the longer write-up and `gallery/smoketest/BUILD.bazel` for a worked example.

### Examples

<!-- Add links to DSE example repos or PRs here -->
Expand Down
122 changes: 122 additions & 0 deletions compute_floorplan_shape.tcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
source $::env(SCRIPTS_DIR)/load.tcl

# Floorplan-shape heuristic: emits CORE_UTILIZATION + CORE_MARGIN for the
# floorplan stage, computed from synth-stage area data. These two knobs
# together determine the die geometry the placer + repair_design will see;
# co-locating them keeps "how the floorplan is sized" in one place instead
# of split between a heuristic (utilization) and a static constant (margin).
#
# Used via orfs_arguments(...). Constants are read from environment with
# the values below as defaults; override per-design via the `arguments`
# attr on orfs_arguments.

load_design 1_synth.odb 1_synth.sdc

proc env_or_default { name default } {
if { [info exists ::env($name)] && $::env($name) ne "" } {
return $::env($name)
}
return $default
}

set block [ord::get_db_block]

# Sum standard-cell area (CORE* masters) and macro / fixed area separately.
# repair_design only adds std cells (buffers, resized gates), so the
# headroom budget is computed against std-cell area, not macro area.
set std_cell_area_dbu 0
set macro_area_dbu 0
foreach inst [$block getInsts] {
set master [$inst getMaster]
set type [$master getType]
set width [$master getWidth]
set height [$master getHeight]
set area [expr { wide($width) * wide($height) }]
if { [string match "CORE*" $type] } {
set std_cell_area_dbu [expr { $std_cell_area_dbu + $area }]
} else {
set macro_area_dbu [expr { $macro_area_dbu + $area }]
}
}

# === CORE_UTILIZATION =====================================================
#
# Pick a core area such that AFTER repair_design has grown the std-cell
# budget by REPAIR_GROWTH_FACTOR (typical 30-50%), the resulting placement
# density is at most TARGET_POST_REPAIR_DENSITY. Macro area is fixed and
# just consumed from the core budget.
#
# util = (std + macro) × target_density
# ─────────────────────────────────
# (std × growth + macro)
#
# Defaults are calibrated against a representative ASIC flow where a
# static CORE_UTILIZATION of ~20% converged at ~22% post-repair density
# (a working configuration with a substantial std-cell count and small
# macro fraction). With the default REPAIR_GROWTH_FACTOR=1.40 and
# TARGET_POST_REPAIR_DENSITY=0.225, the formula reproduces ~20%
# utilization for that point. Override per-design as needed.
set REPAIR_GROWTH_FACTOR [env_or_default REPAIR_GROWTH_FACTOR 1.40]
set TARGET_POST_REPAIR_DENSITY [env_or_default TARGET_POST_REPAIR_DENSITY 0.225]
set CORE_UTILIZATION_FLOOR_PCT [env_or_default CORE_UTILIZATION_FLOOR_PCT 5.0]
set CORE_UTILIZATION_CEILING_PCT [env_or_default CORE_UTILIZATION_CEILING_PCT 50.0]

set total_initial_area [expr { double($std_cell_area_dbu + $macro_area_dbu) }]
set total_post_repair [expr { double($std_cell_area_dbu) * $REPAIR_GROWTH_FACTOR + double($macro_area_dbu) }]

if { $total_post_repair <= 0 } {
set util 20.0
set util_frac 0.20
puts "compute_floorplan_shape: WARNING zero post-repair area, falling back CORE_UTILIZATION=$util"
} else {
set util_frac [expr { $total_initial_area * $TARGET_POST_REPAIR_DENSITY / $total_post_repair }]
# Clamp to a sane range. The floor is roughly where the placer can still
# legalize cells; the ceiling is back in the too-tight-for-repair regime.
set floor [expr { $CORE_UTILIZATION_FLOOR_PCT / 100.0 }]
set ceil [expr { $CORE_UTILIZATION_CEILING_PCT / 100.0 }]
if { $util_frac < $floor } { set util_frac $floor }
if { $util_frac > $ceil } { set util_frac $ceil }
set util [format "%.1f" [expr { $util_frac * 100.0 }]]
}

# === CORE_MARGIN ==========================================================
#
# Distance from the core boundary to the die boundary, in microns. Has to
# fit the PDN ring stack and IO routing tracks. The default 2 µm value
# is a known-safe floor for small/medium designs; it scales mildly with
# die linear dimension so larger designs get more margin headroom for
# ring routing.
#
# Estimated die linear dimension comes from the cells we need to fit and
# the target utilization just picked: die_area_um² ≈ total_initial_um² /
# util_frac; die_linear ≈ sqrt(die_area). The default die-fraction (0.5%)
# is conservative — for a 4 mm² die (die_linear ≈ 2000 µm) it produces
# ≈10 µm; smaller dies stay at the floor.
#
# This is not a deep heuristic — a real one would parse PDN_TCL to extract
# actual ring widths. The shape (max with a floor) is what's important:
# safe for tested designs and grows for designs that get bigger.
set CORE_MARGIN_FLOOR_UM [env_or_default CORE_MARGIN_FLOOR_UM 2.0]
set CORE_MARGIN_DIE_FRACTION [env_or_default CORE_MARGIN_DIE_FRACTION 0.005]

# Convert dbu² to µm². Get the actual ratio from the technology rather
# than hardcoding (varies between PDKs).
set dbu_per_micron [[ord::get_db_tech] getDbUnitsPerMicron]
set total_initial_area_um2 [expr { $total_initial_area / (double($dbu_per_micron) ** 2) }]

if { $util_frac > 0.0 } {
set die_area_um2 [expr { $total_initial_area_um2 / $util_frac }]
} else {
set die_area_um2 0.0
}
set die_linear_um [expr { sqrt($die_area_um2) }]
set core_margin_raw [expr { $CORE_MARGIN_DIE_FRACTION * $die_linear_um }]
set core_margin [format "%.0f" [expr { $core_margin_raw < $CORE_MARGIN_FLOOR_UM ? $CORE_MARGIN_FLOOR_UM : $core_margin_raw }]]

puts "compute_floorplan_shape: std_cell=$std_cell_area_dbu dbu², macro=$macro_area_dbu dbu²"
puts "compute_floorplan_shape: CORE_UTILIZATION=$util (target post-repair density $TARGET_POST_REPAIR_DENSITY, growth $REPAIR_GROWTH_FACTOR)"
puts "compute_floorplan_shape: CORE_MARGIN=$core_margin µm (die_linear ≈ [format %.0f $die_linear_um] µm, fraction $CORE_MARGIN_DIE_FRACTION, floor $CORE_MARGIN_FLOOR_UM)"

set f [open $::env(OUTPUT) w]
puts $f "{\"CORE_UTILIZATION\": \"$util\", \"CORE_MARGIN\": \"$core_margin\"}"
close $f
89 changes: 89 additions & 0 deletions compute_slack_margin.tcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
source $::env(SCRIPTS_DIR)/load.tcl

# Slack-margin heuristic: emits SETUP_SLACK_MARGIN + HOLD_SLACK_MARGIN
# from the previous stage's worst slack, plus a Δ safety budget so the
# next stage's repair_timing has a defined target instead of "fix to ≥ 0".
#
# One TCL handles every stage of the chain (synth→floorplan, place→cts,
# cts→grt, ...). Each invocation (via orfs_arguments) runs in the
# previous stage's RESULTS_DIR, so the canonical ODB is just the latest
# file present. Search from late to early — whichever exists wins. The
# script is identical regardless of which downstream stage will consume
# the JSON; the destination is only encoded in the orfs_arguments
# target name.

set candidates {
{4_cts.odb 4_cts.sdc}
{3_place.odb 3_place.sdc}
{2_floorplan.odb 2_floorplan.sdc}
{1_synth.odb 1_synth.sdc}
}
foreach pair $candidates {
set odb_file [lindex $pair 0]
set sdc_file [lindex $pair 1]
if { [file exists $::env(RESULTS_DIR)/$odb_file] } {
load_design $odb_file $sdc_file
break
}
}

# Worst slacks in the user's current time unit (round-trip safe with
# repair_timing's -setup_margin / -hold_margin via sta::time_ui_sta).
set setup_wns [sta::time_sta_ui [sta::worst_slack_cmd "max"]]
set hold_whs [sta::time_sta_ui [sta::worst_slack_cmd "min"]]

# Sentinel handling: OpenSTA returns ~1e30 when no path exists (e.g. an
# unconstrained pre-CTS design has no real hold path). Fall back to
# generous defaults that trigger no useful repair work.
proc finite_or_default { v default } {
if { ![string is double -strict $v] || abs($v) > 1.0e20 } {
return $default
}
return $v
}
set setup_wns [finite_or_default $setup_wns -12000]
set hold_whs [finite_or_default $hold_whs -1000]

# Margin formula: min(slack, 0) - Δ.
#
# Two cases:
# - Pre-stage slack ≤ 0 (real violation): margin = slack - Δ. repair_timing
# sees current state already meets margin (slack ≥ slack - Δ), no work.
# - Pre-stage slack > 0 (no current violation, e.g. synth often has positive
# WHS): margin = 0 - Δ = -Δ. Acts as a generous floor that lets the next
# stage absorb real violations emerging from layout (e.g. CTS adding
# hold violations from real clock latency) without triggering futile
# repair.
#
# Δ is the "safety margin" — how much degradation we allow from pre-stage
# state. Δ=0 is a trap: if the previous stage had positive slack but the
# next stage introduces violations, margin = 0 means "fix to ≥ 0," the
# futile-tight regime which has been observed to cause hundreds of repair
# passes and eventual ODB-1200 crashes. Δ=1000 ps is data-validated:
# end-to-end runs have landed hold repair within a fraction of a ps of
# the -1000 margin, indicating Δ was tight enough to be useful but loose
# enough to converge.

proc clamp_at_zero { v } { return [expr {$v < 0 ? $v : 0}] }
set setup_wns [clamp_at_zero $setup_wns]
set hold_whs [clamp_at_zero $hold_whs]

proc env_or_default { name default } {
if { [info exists ::env($name)] && $::env($name) ne "" } {
return $::env($name)
}
return $default
}

set DELTA_SETUP [env_or_default DELTA_SETUP_PS 1000]
set DELTA_HOLD [env_or_default DELTA_HOLD_PS 1000]

set setup_margin [expr {$setup_wns - $DELTA_SETUP}]
set hold_margin [expr {$hold_whs - $DELTA_HOLD}]

puts "compute_slack_margin: previous-stage WNS=$setup_wns, WHS=$hold_whs (user time units, clamped at zero)"
puts "compute_slack_margin: SETUP_SLACK_MARGIN=$setup_margin, HOLD_SLACK_MARGIN=$hold_margin (ΔSETUP=$DELTA_SETUP ps, ΔHOLD=$DELTA_HOLD ps)"

set f [open $::env(OUTPUT) w]
puts $f "{\"SETUP_SLACK_MARGIN\": \"$setup_margin\", \"HOLD_SLACK_MARGIN\": \"$hold_margin\"}"
close $f
Loading
Loading