From ef64398ca183408fd75fe32a13c808515b83a3ad Mon Sep 17 00:00:00 2001 From: Jaehyun Kim Date: Fri, 16 Oct 2026 23:51:56 -0700 Subject: [PATCH 1/3] rsz: Model side paths in the Mt delay estimator What: the Mt delay estimator models the fanin side loads, the opposite output transition, the latch D-side driver and the flip-flop setup and hold change of a cell swap, and SizeUpMt and VtSwapMt charge them. Why: scoring a swap on the selected path alone let a scored gain turn into a loss after the timing update. How: side models in DelayEstimator and SequentialTimingEstimator, input cap budgets that workers can check without STA, and ArcDelayState renamed to EstimatorContext. The side models are off by default. Setup repair profiler review follow-ups: the profiler constructor takes a RepairSetupProfiler::Mode instead of an int, CRIT_VT_SWAP samples WNS and TNS before its sweep, and the Bazel TestResizerMt target drops the --gtest_filter that skipped the profiler and fixture tests. Signed-off-by: Jaehyun Kim --- src/rsz/BUILD | 6 +- src/rsz/include/rsz/Resizer.hh | 21 + src/rsz/src/CMakeLists.txt | 1 + src/rsz/src/DelayEstimator.cc | 681 +++++++++++++++-- src/rsz/src/DelayEstimator.hh | 200 ++++- src/rsz/src/DelayEstimatorReporter.cc | 34 +- src/rsz/src/DelayEstimatorReporter.hh | 7 +- src/rsz/src/OptimizationPolicyConfig.hh | 24 + src/rsz/src/Optimizer.cc | 4 +- src/rsz/src/OptimizerTypes.cc | 21 +- src/rsz/src/OptimizerTypes.hh | 104 ++- src/rsz/src/RepairSetupProfiler.cc | 33 +- src/rsz/src/RepairSetupProfiler.hh | 4 +- src/rsz/src/RepairTargetCollector.cc | 43 +- src/rsz/src/RepairTargetCollector.hh | 19 + src/rsz/src/Resizer.cc | 91 +++ src/rsz/src/SequentialTimingEstimator.cc | 765 ++++++++++++++++++++ src/rsz/src/SequentialTimingEstimator.hh | 53 ++ src/rsz/src/move/MoveCandidate.cc | 35 + src/rsz/src/move/MoveCandidate.hh | 8 + src/rsz/src/move/MoveGenerator.cc | 9 + src/rsz/src/move/MoveGenerator.hh | 6 + src/rsz/src/move/SizeUpMtCandidate.cc | 34 +- src/rsz/src/move/SizeUpMtCandidate.hh | 12 +- src/rsz/src/move/SizeUpMtGenerator.cc | 85 ++- src/rsz/src/move/SizeUpMtGenerator.hh | 38 +- src/rsz/src/move/VtSwapMtCandidate.cc | 59 +- src/rsz/src/move/VtSwapMtCandidate.hh | 15 +- src/rsz/src/move/VtSwapMtGenerator.cc | 48 +- src/rsz/src/move/VtSwapMtGenerator.hh | 7 +- src/rsz/src/policy/OptimizationPolicy.cc | 75 +- src/rsz/src/policy/OptimizationPolicy.hh | 10 +- src/rsz/src/policy/SetupCritVtSwapPolicy.cc | 10 + src/rsz/test/BUILD | 3 +- src/rsz/test/cpp/CMakeLists.txt | 2 + src/rsz/test/cpp/TestCellSwapMt.cc | 94 +++ src/rsz/test/cpp/TestDelayEstimatorMt.cc | 723 ++++++++++++++++++ src/rsz/test/cpp/TestRepairSetupProfiler.cc | 19 +- src/rsz/test/cpp/TestResizerMt.cc | 143 +++- src/rsz/test/cpp/TestResizerMtFixture.hh | 199 +++++ 40 files changed, 3466 insertions(+), 279 deletions(-) create mode 100644 src/rsz/src/SequentialTimingEstimator.cc create mode 100644 src/rsz/src/SequentialTimingEstimator.hh create mode 100644 src/rsz/test/cpp/TestCellSwapMt.cc create mode 100644 src/rsz/test/cpp/TestDelayEstimatorMt.cc diff --git a/src/rsz/BUILD b/src/rsz/BUILD index c8a297dca61..53a26295aa9 100644 --- a/src/rsz/BUILD +++ b/src/rsz/BUILD @@ -45,6 +45,7 @@ cc_library( "src/RepairSetupProfiler.cc", "src/RepairTargetCollector.cc", "src/Resizer.cc", + "src/SequentialTimingEstimator.cc", "src/SwapArithModules.hh", "src/move/BufferCandidate.cc", "src/move/BufferCandidate.hh", @@ -77,9 +78,7 @@ cc_library( "src/move/SizeUpMatchGenerator.cc", "src/move/SizeUpMatchGenerator.hh", "src/move/SizeUpMtCandidate.cc", - "src/move/SizeUpMtCandidate.hh", "src/move/SizeUpMtGenerator.cc", - "src/move/SizeUpMtGenerator.hh", "src/move/SplitLoadCandidate.cc", "src/move/SplitLoadCandidate.hh", "src/move/SplitLoadGenerator.cc", @@ -133,9 +132,12 @@ cc_library( "src/RepairSetupProfiler.hh", "src/RepairTargetCollector.hh", "src/ResizerObserver.hh", + "src/SequentialTimingEstimator.hh", "src/move/MoveCandidate.hh", "src/move/MoveGenerator.hh", "src/move/SizeUpMatchGenerator.hh", + "src/move/SizeUpMtCandidate.hh", + "src/move/SizeUpMtGenerator.hh", "src/move/VtSwapMtCandidate.hh", "src/policy/SetupLegacyBase.hh", "src/policy/SetupLegacyPolicy.hh", diff --git a/src/rsz/include/rsz/Resizer.hh b/src/rsz/include/rsz/Resizer.hh index c27bbab8ca1..8ff4a77ac53 100644 --- a/src/rsz/include/rsz/Resizer.hh +++ b/src/rsz/include/rsz/Resizer.hh @@ -897,6 +897,27 @@ class Resizer : public sta::dbStaState, public sta::dbNetworkObserver const sta::LibertyCell* replacement); bool checkMaxCapOK(const sta::Pin* drvr_pin, float cap_delta); + // Fanin max-cap state of one instance, captured once. It lets + // replacementFitsInputCapBudgets() screen many cells without new STA checks, + // with the same decision as replacementPreservesMaxCap(). + struct DriverCapLimit + { + float cap{0.0f}; + float max_cap{0.0f}; + bool negative_slack{false}; + }; + + struct InputCapBudget + { + const sta::LibertyPort* port{nullptr}; + float current_cap{0.0f}; + std::vector driver_limits; + }; + + std::vector inputCapBudgets(sta::Instance* inst); + bool replacementFitsInputCapBudgets( + const std::vector& budgets, + const sta::LibertyCell* replacement) const; bool estimateInputSlewImpact( sta::Instance* instance, const sta::Scene* scene, diff --git a/src/rsz/src/CMakeLists.txt b/src/rsz/src/CMakeLists.txt index 78a4c57841f..ae74a671cd8 100644 --- a/src/rsz/src/CMakeLists.txt +++ b/src/rsz/src/CMakeLists.txt @@ -29,6 +29,7 @@ add_library(rsz_lib OptimizerTypes.cc DelayEstimator.cc DelayEstimatorReporter.cc + SequentialTimingEstimator.cc LRSubproblem.cc policy/SetupCritVtSwapPolicy.cc policy/SetupDirectionalPolicy.cc diff --git a/src/rsz/src/DelayEstimator.cc b/src/rsz/src/DelayEstimator.cc index 6b90c22ca4d..c277e950160 100644 --- a/src/rsz/src/DelayEstimator.cc +++ b/src/rsz/src/DelayEstimator.cc @@ -5,15 +5,19 @@ #include #include +#include #include #include #include +#include +#include #include #include #include #include #include "OptimizerTypes.hh" +#include "RepairSetupProfiler.hh" #include "db_sta/dbSta.hh" #include "rsz/Resizer.hh" #include "sta/ArcDelayCalc.hh" @@ -27,11 +31,15 @@ #include "sta/Parasitics.hh" #include "sta/Path.hh" #include "sta/PathExpanded.hh" +#include "sta/PortDirection.hh" #include "sta/Scene.hh" #include "sta/Sdc.hh" +#include "sta/SearchClass.hh" +#include "sta/Sta.hh" #include "sta/StaState.hh" #include "sta/TableModel.hh" #include "sta/TimingArc.hh" +#include "sta/TimingRole.hh" #include "utl/Logger.h" namespace rsz { @@ -41,6 +49,12 @@ namespace { constexpr float kSlewMatchAbsTolerance = 1.0e-15f; constexpr float kSlewMatchRelTolerance = 1.0e-6f; constexpr float kMinSlewRatioDenominator = 1.0e-15f; +// Sibling gate delay growth per unit of input slew growth, and the slew growth +// per unit of driver delay growth used when no slew model is available. +constexpr float kSideSlewDelayFactor = 0.3f; +// Estimates side-input slew growth from driver delay growth when the side net +// has no selected-arc slew model. +constexpr float kSideSlewPerDelay = 1.4f; void setFailReason(FailReason* fail_reason, const FailReason reason) { @@ -834,10 +848,13 @@ float maxTargetInputCapDelta(Resizer& resizer, const DelayStageState& target_stage) { // Stage arcs use scene/corner cells for timing lookup, while Resizer - // replacement queries require the canonical link cell mapped to OpenDB. + // replacement queries require the canonical link cell mapped to OpenDB. The + // driver instance already carries that link cell. sta::LibertyCell* current_scene_cell = target_stage.arc.currentCell(); + sta::Network* network = resizer.network(); + sta::Instance* driver_inst = network->instance(target_stage.driver_pin); sta::LibertyCell* current_link_cell - = resizer.network()->findLibertyCell(current_scene_cell->name()); + = driver_inst != nullptr ? network->libertyCell(driver_inst) : nullptr; if (current_link_cell == nullptr) { debugPrint(resizer.logger(), utl::RSZ, @@ -888,7 +905,14 @@ float maxTargetInputCapDelta(Resizer& resizer, return std::max(max_input_cap - current_input_cap, 0.0f); } -void prepareFaninNeighborSlewBias(Resizer& resizer, ArcDelayState& context) +uint32_t floatBits(const float value) +{ + return std::bit_cast(value); +} + +void prepareFaninNeighborSlewBias(Resizer& resizer, + EstimatorContext& context, + DelayEstimatorCache* cache) { // STA slew bias is applied only to the immediate fanin driver stage. if (context.target_stage_index <= 0) { @@ -897,7 +921,9 @@ void prepareFaninNeighborSlewBias(Resizer& resizer, ArcDelayState& context) const DelayStageState& target_stage = context.target(); const float max_input_cap_delta - = maxTargetInputCapDelta(resizer, target_stage); + = cache != nullptr + ? cache->maxEquivalentInputCapIncrease(resizer, target_stage) + : maxTargetInputCapDelta(resizer, target_stage); if (max_input_cap_delta <= 0.0f) { logSlewBiasSkip( resizer, target_stage, "no target input capacitance growth"); @@ -906,10 +932,116 @@ void prepareFaninNeighborSlewBias(Resizer& resizer, ArcDelayState& context) DelayStageState& fanin_stage = context.path_stages[context.target_stage_index - 1]; + fanin_stage.sta_slew_bias - = buildSlewBiasModel(resizer, fanin_stage, max_input_cap_delta); + = cache != nullptr + ? cache->getOrSampleSlewBias( + resizer, fanin_stage, max_input_cap_delta) + : buildSlewBiasModel(resizer, fanin_stage, max_input_cap_delta); +} + +} // namespace + +void DelayEstimatorCache::clear() +{ + *this = DelayEstimatorCache{}; +} + +// The equivalent-cell envelope does not change within a policy run. +float DelayEstimatorCache::maxEquivalentInputCapIncrease( + Resizer& resizer, + const DelayStageState& target_stage) +{ + const sta::LibertyPort* input_port = target_stage.arc.inputPort(); + std::unordered_map::iterator cached + = max_equiv_input_cap_increase_by_port.find(input_port); + if (cached == max_equiv_input_cap_increase_by_port.end()) { + cached = max_equiv_input_cap_increase_by_port + .emplace(input_port, + maxTargetInputCapDelta(resizer, target_stage)) + .first; + } + + return cached->second; +} + +// Every input read by buildSlewBiasModel(): the stage identity and slews, the +// sampled load range, and the live reduced Pi model of the driver net. A +// missing Pi model disables memoization because sampling would fail anyway. +std::optional +DelayEstimatorCache::makeSlewBiasModelKey(Resizer& resizer, + const DelayStageState& stage, + const float max_load_delta) +{ + sta::Parasitics* parasitics = stage.arc.scene->parasitics(stage.arc.min_max); + sta::ArcDelayCalc* arc_delay_calc = resizer.staState()->arcDelayCalc(); + if (parasitics == nullptr || arc_delay_calc == nullptr + || stage.driver_pin == nullptr) { + return std::nullopt; + } + + sta::Parasitic* parasitic + = arc_delay_calc->findParasitic(stage.driver_pin, + stage.arc.outputRiseFall(), + stage.arc.scene, + stage.arc.min_max); + if (parasitic == nullptr || !parasitics->isPiModel(parasitic)) { + return std::nullopt; + } + + float c2 = 0.0f; + float rpi = 0.0f; + float c1 = 0.0f; + parasitics->piModel(parasitic, c2, rpi, c1); + + SlewBiasModelKey key{ + .driver_pin = stage.driver_pin, + .ref_arc = stage.arc.ref_arc, + .scene = stage.arc.scene, + .min_max = stage.arc.min_max, + .current_cell = stage.arc.currentCell(), + .input_slew_bits = floatBits(stage.input_slew), + .load_cap_bits = floatBits(stage.load_cap), + .max_load_delta_bits = floatBits(max_load_delta), + .pi_c2_bits = floatBits(c2), + .pi_r_bits = floatBits(rpi), + .pi_c1_bits = floatBits(c1), + .merge_arcs = {}, + }; + key.merge_arcs.reserve(stage.output_slew_merge_arcs.size()); + for (const OutputSlewMergeArc& merge_arc : stage.output_slew_merge_arcs) { + key.merge_arcs.push_back( + {merge_arc.ref_arc, floatBits(merge_arc.input_slew)}); + } + + return key; +} + +SlewBiasModel DelayEstimatorCache::getOrSampleSlewBias( + Resizer& resizer, + const DelayStageState& stage, + const float max_load_delta) +{ + std::optional key + = makeSlewBiasModelKey(resizer, stage, max_load_delta); + if (key.has_value()) { + const SlewBiasModelMap::const_iterator cached = slew_bias_models.find(*key); + if (cached != slew_bias_models.end()) { + RepairSetupProfiler::event("slew_bias_model_hits"); + return cached->second; + } + } + + SlewBiasModel model = buildSlewBiasModel(resizer, stage, max_load_delta); + if (key.has_value()) { + RepairSetupProfiler::event("slew_bias_model_misses"); + slew_bias_models.emplace(std::move(*key), model); + } + return model; } +namespace { + // Estimate the post-ECO driver output slew for the selected transition. When // an STA slew-bias model is prepared for this stage, correct only the stable // table-worst arc. Otherwise use the existing merged table-delta calibration. @@ -1114,18 +1246,18 @@ std::optional buildDelayStageStateFromPath( fail_reason); } -// Build an ArcDelayState that always contains the target stage; with +// Build an EstimatorContext that always contains the target stage; with // delay_levels > 0, additionally captures up to N valid fanin/fanout // stages. target_stage_index points to the target stage in path_stages. -ArcDelayState collectPathStages(const Resizer& resizer, - const sta::PathExpanded& expanded, - const int target_path_index, - const sta::Scene* scene, - const sta::MinMax* min_max, - const int delay_levels, - const DelayStageState& target_stage) +EstimatorContext collectPathStages(const Resizer& resizer, + const sta::PathExpanded& expanded, + const int target_path_index, + const sta::Scene* scene, + const sta::MinMax* min_max, + const int delay_levels, + const DelayStageState& target_stage) { - ArcDelayState context; + EstimatorContext context; context.delay_estimation_levels = delay_levels; if (delay_levels == 0) { context.path_stages = {target_stage}; @@ -1161,6 +1293,8 @@ ArcDelayState collectPathStages(const Resizer& resizer, context.path_stages.push_back(target_stage); int found_fanout_stages = 0; + sta::Instance* target_inst + = resizer.network()->instance(target_stage.driver_pin); for (int path_index = target_path_index + 1; path_index < path_count && found_fanout_stages < delay_levels; ++path_index) { @@ -1170,6 +1304,12 @@ ArcDelayState collectPathStages(const Resizer& resizer, if (!stage.has_value()) { continue; } + + sta::Instance* fanout_inst = resizer.network()->instance(stage->driver_pin); + if (fanout_inst != nullptr && target_inst != nullptr + && fanout_inst == target_inst) { + continue; + } context.path_stages.push_back(*stage); ++found_fanout_stages; } @@ -1179,6 +1319,118 @@ ArcDelayState collectPathStages(const Resizer& resizer, return context; } +bool latchDataPathMatches(const Resizer& resizer, + const sta::Path* q_path, + const sta::Path* d_path, + const DelayStageState& target_stage) +{ + const SelectedArc& target_arc = target_stage.arc; + + // Only the gated, selected D->Q path may bridge a latch boundary. + const sta::StaState* sta = resizer.staState(); + const sta::Pin* d_pin = resizer.network()->findPin( + resizer.network()->instance(target_stage.driver_pin), + target_arc.inputPort()->name()); + return d_path != nullptr && sta::Path::equal(q_path->prevPath(), d_path, sta) + && q_path->pin(sta) == target_stage.driver_pin + && d_path->pin(sta) == d_pin + && q_path->transition(sta) == target_arc.outputRiseFall() + && d_path->transition(sta) == target_arc.inputRiseFall() + && q_path->scene(sta) == target_arc.scene + && d_path->scene(sta) == target_arc.scene + && q_path->minMax(sta) == target_arc.min_max + && d_path->minMax(sta) == target_arc.min_max + && d_path->pathAnalysisPtIndex(sta) + == q_path->pathAnalysisPtIndex(sta); +} + +bool includeLatchFaninStages(const Resizer& resizer, + const sta::PathExpanded& expanded, + EstimatorContext& context, + FailReason* fail_reason) +{ + const DelayStageState& target_stage = context.target(); + const SelectedArc& target_arc = target_stage.arc; + if (!target_arc.ref_arc->role()->isLatchDtoQ() + || target_stage.path_index != static_cast(expanded.startIndex())) { + return true; + } + + const sta::StaState* sta = resizer.staState(); + const sta::Instance* target_inst + = resizer.network()->instance(target_stage.driver_pin); + const sta::Path* q_path = expanded.path(target_stage.path_index); + const sta::Path* d_path = latchDataPath(expanded); + if (!latchDataPathMatches(resizer, q_path, d_path, target_stage)) { + setFailReason(fail_reason, FailReason::kMissingPrevTimingArc); + return false; + } + + // Expand this data segment only; do not walk earlier launch clocks/latches. + const sta::PathExpanded data_expanded(d_path, sta); + const int driver_index = static_cast(data_expanded.size()) - 2; + const sta::Path* driver_path = d_path->prevPath(); + const sta::Edge* wire_edge = d_path->prevEdge(sta); + if (driver_index < static_cast(data_expanded.startIndex()) + || driver_path == nullptr || wire_edge == nullptr || !wire_edge->isWire() + || !sta::Path::equal(data_expanded.path(driver_index), driver_path, sta) + || wire_edge->from(resizer.graph()) != driver_path->vertex(sta) + || wire_edge->to(resizer.graph()) != d_path->vertex(sta) + || resizer.network()->instance(driver_path->pin(sta)) == target_inst + || driver_path->transition(sta) != target_arc.inputRiseFall() + || driver_path->scene(sta) != target_arc.scene + || driver_path->minMax(sta) != target_arc.min_max + || driver_path->pathAnalysisPtIndex(sta) + != d_path->pathAnalysisPtIndex(sta)) { + setFailReason(fail_reason, FailReason::kMissingPrevTimingArc); + return false; + } + + const std::optional driver_stage + = buildDelayStageStateFromPath(resizer, + data_expanded, + driver_index, + target_arc.scene, + target_arc.min_max, + fail_reason); + if (!driver_stage.has_value()) { + return false; + } + + const int additional_levels + = std::max(context.delay_estimation_levels, 1) - 1; + EstimatorContext fanin = collectPathStages(resizer, + data_expanded, + driver_index, + target_arc.scene, + target_arc.min_max, + additional_levels, + *driver_stage); + for (DelayStageState& stage : fanin.path_stages) { + // A feedback segment must not reintroduce the resized physical driver. + const sta::Path* path = data_expanded.path(stage.path_index); + if (resizer.network()->instance(stage.driver_pin) == target_inst + || path->scene(sta) != target_arc.scene + || path->minMax(sta) != target_arc.min_max + || path->pathAnalysisPtIndex(sta) != d_path->pathAnalysisPtIndex(sta) + || path->transition(sta) != stage.arc.outputRiseFall()) { + setFailReason(fail_reason, FailReason::kMissingPrevTimingArc); + return false; + } + + // These stages have no index in the primary expanded path. + stage.path_index = -1; + } + + context.path_stages.insert(context.path_stages.begin(), + fanin.path_stages.begin(), + fanin.path_stages.end()); + context.target_stage_index += static_cast(fanin.path_stages.size()); + context.current_total_delay += fanin.current_total_delay; + + return true; +} + DelayEstimate makeEstimate(const float current_delay, const float candidate_delay) { @@ -1190,6 +1442,308 @@ DelayEstimate makeEstimate(const float current_delay, : FailReason::kEstimateNonImproving}; } +// Record each input net of the target: the driver's delay per load and the +// worst slack of each load pin. The selected-arc net skips the target pin, +// which the window already scores with the exact fanin delta. +void collectFaninSideLoads(Resizer& resizer, + sta::Instance* target_inst, + const sta::MinMax* min_max, + const FaninSideLoadOptions& options, + EstimatorContext& context) +{ + if (min_max == nullptr) { + return; + } + + sta::dbSta* sta = resizer.sta(); + sta::Network* network = resizer.network(); + sta::Graph* graph = sta->graph(); + const sta::Pin* selected_input_pin + = network->findPin(target_inst, context.target().arc.inputPort()); + const sta::Pin* selected_fanin_pin + = context.target_stage_index >= 1 + ? context.path_stages[context.target_stage_index - 1].driver_pin + : nullptr; + + size_t load_count = 0; + std::unique_ptr input_iter( + network->pinIterator(target_inst)); + while (input_iter->hasNext()) { + const sta::Pin* input_pin = input_iter->next(); + sta::LibertyPort* input_port = network->libertyPort(input_pin); + if (input_port == nullptr || !network->direction(input_pin)->isInput() + || sta->isClock(input_pin, sta->cmdMode())) { + continue; + } + + sta::Net* net = network->net(input_pin); + if (net == nullptr) { + continue; + } + + FaninSideNet side_net; + side_net.input_port = input_port; + side_net.input_cap = input_port->capacitance(); + side_net.selected_arc = input_pin == selected_input_pin + && selected_fanin_pin != nullptr + && network->net(selected_fanin_pin) == net; + if (!side_net.selected_arc) { + // Drive resistance is the Liberty delay-versus-load slope of the driver. + const sta::PinSet* drivers = network->drivers(net); + const sta::LibertyPort* driver_port + = drivers != nullptr && !drivers->empty() + ? network->libertyPort(*drivers->begin()) + : nullptr; + if (driver_port == nullptr) { + continue; + } + + side_net.delay_per_cap = driver_port->driveResistance(); + if (side_net.delay_per_cap <= 0.0f) { + continue; + } + } + + std::unique_ptr pin_iter( + network->connectedPinIterator(net)); + while (pin_iter->hasNext()) { + const sta::Pin* pin = pin_iter->next(); + if ((side_net.selected_arc && pin == input_pin) + || !network->isLoad(pin)) { + continue; + } + + sta::Vertex* vertex = graph->pinLoadVertex(pin); + if (vertex == nullptr) { + continue; + } + + const sta::Slack slack = sta->slack(vertex, min_max); + if (sta::delayInf(slack, sta)) { + continue; + } + + side_net.loads.push_back( + {.pin = pin, + .slack = sta::delayAsFloat(slack), + .weight = sideLoadWeight( + pin, options.critical_load_pins, options.off_path_weight)}); + } + + if (side_net.loads.empty()) { + continue; + } + + load_count += side_net.loads.size(); + context.fanin_side_nets.push_back(std::move(side_net)); + } + + RepairSetupProfiler::event("fanin_side_loads_collected", load_count); +} + +// Record the target cell arcs that drive the opposite output transition +// through the selected input, with their current table-model delays, and the +// worst slack of that transition at the driver. +void collectOppositeRfArcs(Resizer& resizer, EstimatorContext& context) +{ + DelayStageState& target = context.path_stages[context.target_stage_index]; + sta::Graph* graph = resizer.graph(); + sta::Vertex* driver_vertex = graph->pinDrvrVertex(target.driver_pin); + if (driver_vertex == nullptr) { + return; + } + + const sta::RiseFall* output_rf = target.arc.outputRiseFall(); + const sta::RiseFall* opposite_rf = output_rf->opposite(); + const std::string& input_name = target.arc.inputPort()->name(); + const std::string& output_name = target.arc.outputPort()->name(); + sta::LibertyCell* current_cell = target.arc.currentCell(); + + sta::VertexInEdgeIterator edge_iter(driver_vertex, graph); + while (edge_iter.hasNext()) { + sta::Edge* edge = edge_iter.next(); + const sta::TimingArcSet* arc_set = edge->timingArcSet(); + if (arc_set->role()->isTimingCheck() || arc_set->to()->name() != output_name + || arc_set->from()->name() != input_name) { + continue; + } + + for (const sta::TimingArc* graph_arc : arc_set->arcs()) { + const sta::RiseFall* input_rf = graph_arc->fromEdge()->asRiseFall(); + if (input_rf == nullptr + || graph_arc->toEdge()->asRiseFall() != opposite_rf) { + continue; + } + + const sta::TimingArc* ref_arc + = findCellTimingArcLike(graph_arc, current_cell); + if (ref_arc == nullptr) { + continue; + } + + const float input_slew + = resizer.staState()->graphDelayCalc()->edgeFromSlew( + edge->from(graph), + input_rf, + edge, + target.arc.scene, + target.arc.min_max); + + float delay = 0.0f; + float output_slew = 0.0f; + if (!gateDelayAndSlewFromTableModel(target.arc.pvt, + ref_arc, + input_slew, + target.load_cap, + delay, + output_slew)) { + continue; + } + + target.opposite_rf_arcs.push_back({.ref_arc = ref_arc, + .input_slew = input_slew, + .current_model_delay = delay}); + } + } + + if (target.opposite_rf_arcs.empty()) { + return; + } + + const sta::Slack slack + = resizer.sta()->slack(driver_vertex, opposite_rf, target.arc.min_max); + if (!sta::delayInf(slack, resizer.sta())) { + context.opposite_rf_slack = sta::delayAsFloat(slack); + } + + RepairSetupProfiler::event("dual_rf_arcs_prepared", + target.opposite_rf_arcs.size()); +} + +// Charge the opposite output transition of the target like a side path: its +// delay growth costs negative-region slack, and pushing it below the global +// floor rejects the candidate. Only arcs the candidate cell also has count. +void applyDualRfCheck(const EstimatorContext& context, + const sta::LibertyCell* candidate_cell, + DelayEstimate& estimate) +{ + const DelayStageState& target = context.target(); + if (target.opposite_rf_arcs.empty()) { + return; + } + + float worst_delta = -std::numeric_limits::infinity(); + for (const OppositeRfArc& opposite : target.opposite_rf_arcs) { + float delay = 0.0f; + float output_slew = 0.0f; + if (!lookupArcDelayAndSlewForArc(target.arc.scene, + target.arc.min_max, + target.arc.pvt, + opposite.ref_arc, + opposite.input_slew, + target.load_cap, + candidate_cell, + delay, + output_slew, + ArcMatchMode::kRelaxedCandidate)) { + continue; + } + + worst_delta = std::max(worst_delta, delay - opposite.current_model_delay); + } + + if (!std::isfinite(worst_delta)) { + return; + } + + if (worst_delta <= 0.0f || !std::isfinite(context.opposite_rf_slack)) { + return; + } + + const float slack_after = context.opposite_rf_slack - worst_delta; + estimate.side_loss + += oppositeRfSideLoss(context.opposite_rf_slack, worst_delta); + ++estimate.side_paths_evaluated; + if (slack_after < context.sideSlackFloor(estimate.arrival_impr)) { + estimate.legal = false; + estimate.reason = FailReason::kDualRfNonImproving; + } +} + +// Adds the negative-region loss of side_paths side loads, weighted by +// fanin_side_loss_weight, and rejects the candidate when the worst of them +// falls below the side slack floor. The fanin side loads and the loads a +// split moves share it. +void chargeSideLoss(const EstimatorContext& context, + const int side_paths, + const float loss, + const float worst_slack_after, + DelayEstimate& estimate) +{ + // Accumulate: the dual rise/fall check may already have charged a path. + estimate.side_paths_evaluated += side_paths; + estimate.side_loss += loss * context.fanin_side_loss_weight; + if (worst_slack_after < context.sideSlackFloor(estimate.arrival_impr)) { + estimate.legal = false; + estimate.reason = FailReason::kSideSlackFloorViolation; + } +} + +// Charge the fanin drivers' delay growth to the recorded side paths. The loss +// counts only below zero slack, as an estimate of the TNS change. A side path +// pushed below the side slack floor rejects the candidate. +void applyFaninSideLoss(const EstimatorContext& context, + const sta::LibertyCell* candidate_scene_cell, + const float selected_fanin_delay_delta, + const float selected_fanin_slew_delta, + DelayEstimate& estimate) +{ + if (context.fanin_side_nets.empty()) { + return; + } + + float loss = 0.0f; + float worst_slack_after = std::numeric_limits::infinity(); + int side_paths = 0; + for (const FaninSideNet& side_net : context.fanin_side_nets) { + // A slower fanin driver also slows the slew into each side gate, adding + // kSideSlewDelayFactor of the slew growth to its delay. Nets off the + // selected arc have no slew model and take kSideSlewPerDelay instead. + float delay_delta = selected_fanin_delay_delta; + float slew_delta = selected_fanin_slew_delta; + if (!side_net.selected_arc) { + const sta::LibertyPort* candidate_port + = candidate_scene_cell->findLibertyPort(side_net.input_port->name()); + if (candidate_port == nullptr) { + continue; + } + + delay_delta = side_net.delay_per_cap + * (candidate_port->capacitance() - side_net.input_cap); + slew_delta = kSideSlewPerDelay * delay_delta; + } + + if (delay_delta <= 0.0f) { + continue; + } + + delay_delta += kSideSlewDelayFactor * std::max(slew_delta, 0.0f); + for (const SideLoad& side : side_net.loads) { + const float slack_after = side.slack - delay_delta; + loss += side.weight + * (std::min(side.slack, 0.0f) - std::min(slack_after, 0.0f)); + worst_slack_after = std::min(worst_slack_after, slack_after); + ++side_paths; + } + } + + if (side_paths == 0) { + return; + } + + chargeSideLoss(context, side_paths, loss, worst_slack_after, estimate); +} + // Score a candidate over a path-local fanin/fanout window using cached delay // state. When `trace` is non-null, populates one StageEvaluation per // evaluated stage so diagnostic callers can present per-stage detail. @@ -1200,7 +1754,7 @@ DelayEstimate makeEstimate(const float current_delay, // interconnect (RC) slew degradation between a driver's output pin and the next // cell's input pin. This remains a speed/accuracy trade-off for candidate // ranking. -DelayEstimate estimateWindow(const ArcDelayState& context, +DelayEstimate estimateWindow(const EstimatorContext& context, const sta::LibertyCell* candidate_cell, std::vector* trace = nullptr) { @@ -1228,6 +1782,8 @@ DelayEstimate estimateWindow(const ArcDelayState& context, const float current_total_delay = context.current_total_delay; float candidate_total_delay = 0.0f; + float fanin_delay_delta = 0.0f; + float fanin_slew_delta = 0.0f; float propagated_driver_output_slew = 0.0f; bool has_propagated_driver_output_slew = false; @@ -1308,6 +1864,10 @@ DelayEstimate estimateWindow(const ArcDelayState& context, arc_mode, arc_relaxed_out); has_propagated_driver_output_slew = true; + if (is_fanin_neighbor) { + fanin_delay_delta = stage_delay - stage.current_delay; + fanin_slew_delta = propagated_driver_output_slew - stage.current_slew; + } if (trace != nullptr) { trace->push_back({.path_index = stage.path_index, @@ -1320,7 +1880,18 @@ DelayEstimate estimateWindow(const ArcDelayState& context, } } - return makeEstimate(current_total_delay, candidate_total_delay); + DelayEstimate estimate + = makeEstimate(current_total_delay, candidate_total_delay); + if (estimate.legal) { + applyDualRfCheck(context, candidate_scene_cell, estimate); + } + + applyFaninSideLoss(context, + candidate_scene_cell, + fanin_delay_delta, + fanin_slew_delta, + estimate); + return estimate; } } // namespace @@ -1336,12 +1907,16 @@ bool DelayEstimator::findArcDelay(const SelectedArc& arc, arc, input_slew, load_cap, cell, delay, output_slew); } -std::optional DelayEstimator::buildContext( +std::optional DelayEstimator::buildContext( Resizer& resizer, const Target& target, const int delay_levels, FailReason* fail_reason, - const bool use_sta_slew_bias) + const bool use_sta_slew_bias, + const bool include_latch_fanin, + DelayEstimatorCache* cache, + const FaninSideLoadOptions* fanin_side_loads, + const bool dual_rf_eval) { // Capture one active path arc and its electrical state so candidates can be // scored without touching shared timing state. Higher delay levels expand @@ -1370,10 +1945,14 @@ std::optional DelayEstimator::buildContext( min_max, delay_levels, fail_reason, - use_sta_slew_bias); + use_sta_slew_bias, + include_latch_fanin, + cache, + fanin_side_loads, + dual_rf_eval); } -std::optional DelayEstimator::buildContext( +std::optional DelayEstimator::buildContext( Resizer& resizer, sta::Instance* inst, sta::Pin* driver_pin, @@ -1383,7 +1962,11 @@ std::optional DelayEstimator::buildContext( const sta::MinMax* min_max, const int delay_levels, FailReason* fail_reason, - const bool use_sta_slew_bias) + const bool use_sta_slew_bias, + const bool include_latch_fanin, + DelayEstimatorCache* cache, + const FaninSideLoadOptions* fanin_side_loads, + const bool dual_rf_eval) { // The target stage must be valid; surrounding stages are best-effort because // path endpoints and non-cell arcs are expected at window boundaries. @@ -1401,20 +1984,44 @@ std::optional DelayEstimator::buildContext( } const int normalized_delay_levels = delay_levels > 0 ? delay_levels : 0; - ArcDelayState context = collectPathStages(resizer, - expanded, - path_index, - scene, - min_max, - normalized_delay_levels, - *target_stage); + EstimatorContext context = collectPathStages(resizer, + expanded, + path_index, + scene, + min_max, + normalized_delay_levels, + *target_stage); + if (include_latch_fanin + && !includeLatchFaninStages(resizer, expanded, context, fail_reason)) { + return std::nullopt; + } if (use_sta_slew_bias) { - prepareFaninNeighborSlewBias(resizer, context); + const ProfileScope profile("prepare_slew_bias"); + prepareFaninNeighborSlewBias(resizer, context, cache); + } + + if (fanin_side_loads != nullptr) { + const ProfileScope profile("prepare_fanin_side_loads"); + collectFaninSideLoads(resizer, inst, min_max, *fanin_side_loads, context); + } + + if (dual_rf_eval) { + const ProfileScope profile("prepare_dual_rf"); + collectOppositeRfArcs(resizer, context); } return context; } -DelayEstimate DelayEstimator::estimate(const ArcDelayState& context, +float oppositeRfSideLoss(const float opposite_slack, const float delay_delta) +{ + if (!std::isfinite(opposite_slack) || delay_delta <= 0.0f) { + return 0.0f; + } + return std::min(opposite_slack, 0.0f) + - std::min(opposite_slack - delay_delta, 0.0f); +} + +DelayEstimate DelayEstimator::estimate(const EstimatorContext& context, const sta::LibertyCell* candidate_cell, std::vector* trace) { @@ -1426,18 +2033,4 @@ DelayEstimate DelayEstimator::estimate(const ArcDelayState& context, return estimateWindow(context, candidate_cell, trace); } -DelayEstimate DelayEstimator::estimate(const SelectedArc& arc, - const float input_slew, - const float load_cap, - const float current_delay, - const sta::LibertyCell* candidate_cell) -{ - float candidate_delay = 0.0f; - if (!findArcDelay( - arc, input_slew, load_cap, candidate_cell, candidate_delay)) { - return {.legal = false, .reason = FailReason::kMissingCandidateTimingArc}; - } - return makeEstimate(current_delay, candidate_delay); -} - } // namespace rsz diff --git a/src/rsz/src/DelayEstimator.hh b/src/rsz/src/DelayEstimator.hh index 2c066fc6727..88b3b168339 100644 --- a/src/rsz/src/DelayEstimator.hh +++ b/src/rsz/src/DelayEstimator.hh @@ -3,10 +3,15 @@ #pragma once +#include +#include #include +#include +#include #include #include "OptimizerTypes.hh" +#include "boost/functional/hash.hpp" namespace sta { class Instance; @@ -18,24 +23,53 @@ class Pin; class Pvt; class RiseFall; class Scene; +class TimingArc; } // namespace sta namespace rsz { class Resizer; struct Target; -// Per-stage trace entry produced by the trace overload of -// DelayEstimator::estimate. Captures the inputs and outputs of the -// table-model lookup that was applied (with the candidate at the target -// stage) so diagnostic tools can present per-stage detail without -// re-implementing the estimator's stage walk. +// Negative-region slack lost by the opposite output transition when its delay +// grows by delay_delta; zero when the transition stays non-critical. +float oppositeRfSideLoss(float opposite_slack, float delay_delta); + +// Weights of the fanin side loads in buildContext(). Loads in +// critical_load_pins, the worst paths of all violating endpoints, keep weight +// 1 and the others take off_path_weight. A null set keeps weight 1 for all. +struct FaninSideLoadOptions +{ + const std::unordered_set* critical_load_pins{nullptr}; + float off_path_weight{0.0f}; +}; + +// The TNS weight of a side load: 1 on the worst path of a violating endpoint, +// off_path_weight elsewhere. A null set puts every load on a worst path. +inline float sideLoadWeight( + const sta::Pin* pin, + const std::unordered_set* critical_load_pins, + const float off_path_weight) +{ + return critical_load_pins == nullptr || critical_load_pins->contains(pin) + ? 1.0f + : off_path_weight; +} + +// Per-stage trace entry that the estimate functions (estimate(), +// estimateInsertion() and the buffer-tree estimates) produce when given a +// trace. Captures the inputs and outputs of the table-model lookup that was +// applied (with the candidate at the target stage) so diagnostic tools can +// present per-stage detail without re-implementing the estimator's stage +// walk. struct StageEvaluation { int path_index{-1}; const sta::LibertyCell* cell{nullptr}; float input_slew{0.0f}; float load_cap{0.0f}; - float stage_delay{0.0f}; // Calibrated cell delay, not wire delay. + // Cell delay, not wire delay, except for the wire and via entries of a + // buffer tree, which have no cell and carry their wire delay here. + float stage_delay{0.0f}; float output_slew{0.0f}; // True when the target candidate lookup used relaxed (port-name+RF) // matching because no exact conditional/default arc was found. @@ -44,17 +78,112 @@ struct StageEvaluation // === Delay estimator API ==================================================== -// Stateless helper for building timing-arc contexts and comparing candidate -// cell delays. The default buildContext() path only reads STA/OpenDB state. -// The optional STA slew-bias path temporarily mutates and restores STA's -// reduced Pi model during main-thread prepare. +// Reuses delay-estimator preparation results across targets in one policy run. +class DelayEstimatorCache +{ + public: + // Discards results from the previous policy run. + void clear(); + + // Computes or reuses the maximum cap increase for a target input port. + float maxEquivalentInputCapIncrease(Resizer& resizer, + const DelayStageState& target_stage); + + // Reuses a slew-bias model or samples live STA on a main-thread miss. + SlewBiasModel getOrSampleSlewBias(Resizer& resizer, + const DelayStageState& stage, + float max_load_delta); + + private: + // Identifies one fanin-driver slew-bias sample by its electrical state. + struct SlewBiasModelKey + { + // One arc contributing to the driver's merged output slew. + struct MergeArc + { + // Liberty arc contributing to the merged output slew. + const sta::TimingArc* ref_arc{nullptr}; + // Bitwise input slew used for this arc. + uint32_t input_slew_bits{0}; + + // Compares the arc and its exact input-slew bits. + bool operator==(const MergeArc&) const = default; + }; + + // Output pin of the fanin driver being sampled. + const sta::Pin* driver_pin{nullptr}; + // Selected timing arc of that driver. + const sta::TimingArc* ref_arc{nullptr}; + // STA scene containing the selected arc. + const sta::Scene* scene{nullptr}; + // Analysis sense used for the timing sample. + const sta::MinMax* min_max{nullptr}; + // Current Liberty cell of the fanin driver in this scene. + const sta::LibertyCell* current_cell{nullptr}; + // Bitwise input slew at the fanin driver. + uint32_t input_slew_bits{0}; + // Bitwise current load capacitance at the fanin driver. + uint32_t load_cap_bits{0}; + // Bitwise maximum load increase covered by the sample. + uint32_t max_load_delta_bits{0}; + // Bitwise c2 capacitance of the reduced Pi parasitic. + uint32_t pi_c2_bits{0}; + // Bitwise resistance of the reduced Pi parasitic. + uint32_t pi_r_bits{0}; + // Bitwise c1 capacitance of the reduced Pi parasitic. + uint32_t pi_c1_bits{0}; + // Additional arcs that affect the driver's merged output slew. + std::vector merge_arcs; + + // Compares every fixed field and the merge-arc sequence. + bool operator==(const SlewBiasModelKey&) const = default; + + // Hashes the same fields used by equality for Boost's cache lookup. + friend size_t hash_value(const SlewBiasModelKey& key) + { + size_t seed = 0; + boost::hash_combine(seed, key.driver_pin); + boost::hash_combine(seed, key.ref_arc); + boost::hash_combine(seed, key.scene); + boost::hash_combine(seed, key.min_max); + boost::hash_combine(seed, key.current_cell); + boost::hash_combine(seed, key.input_slew_bits); + boost::hash_combine(seed, key.load_cap_bits); + boost::hash_combine(seed, key.max_load_delta_bits); + boost::hash_combine(seed, key.pi_c2_bits); + boost::hash_combine(seed, key.pi_r_bits); + boost::hash_combine(seed, key.pi_c1_bits); + boost::hash_combine(seed, key.merge_arcs.size()); + for (const MergeArc& arc : key.merge_arcs) { + boost::hash_combine(seed, arc.ref_arc); + boost::hash_combine(seed, arc.input_slew_bits); + } + return seed; + } + }; + + // Captures the driver and parasitic state read by the slew-bias sampler. + static std::optional makeSlewBiasModelKey( + Resizer& resizer, + const DelayStageState& stage, + float max_load_delta); + + // Largest equivalent-cell input-cap increase per scene input port. + std::unordered_map + max_equiv_input_cap_increase_by_port; + // Reuses sampled slew-bias models for identical driver states. + using SlewBiasModelMap = std::unordered_map>; + SlewBiasModelMap slew_bias_models; +}; + +// Stateless delay model of the MT moves. buildContext() reads STA on the main +// thread into an EstimatorContext. The estimate functions read only that +// snapshot and Liberty tables, so they are safe on worker threads. // -// All methods are static. buildContext() queries STA (main-thread only) to -// capture arc, slew, and cap into a Target::arc_delay. estimate() -// evaluates the Liberty table model for a candidate cell given that context -// and is thread-safe (reads only Liberty data structures, no STA graph -// mutation). This split lets MT generators call estimate() freely from worker -// threads as long as the ArcDelayState was prepared on the main thread first. +// With use_sta_slew_bias, buildContext() samples by changing STA's reduced Pi +// model and restores it afterwards. class DelayEstimator { public: @@ -64,20 +193,24 @@ class DelayEstimator float load_cap, const sta::LibertyCell* cell, float& delay); - // === Context construction ================================================= - // delay_levels=0 captures the target stage only; N>0 also captures up to N - // valid fanin and N valid fanout stages from the selected path. - static std::optional buildContext(Resizer& resizer, - const Target& target, - int delay_levels, - FailReason* fail_reason - = nullptr, - bool use_sta_slew_bias - = false); + // The window holds delay_levels stages before and after the target stage. + // With include_latch_fanin, a latch at the path start also needs its D-side + // driver, stitched in with path_index -1, or the context fails. + // fanin_side_loads records the fanin nets' other loads to charge their loss. + static std::optional buildContext( + Resizer& resizer, + const Target& target, + int delay_levels, + FailReason* fail_reason = nullptr, + bool use_sta_slew_bias = false, + bool include_latch_fanin = false, + DelayEstimatorCache* cache = nullptr, + const FaninSideLoadOptions* fanin_side_loads = nullptr, + bool dual_rf_eval = false); // Lower-level overload used by tests and policy prepare code. - static std::optional buildContext( + static std::optional buildContext( Resizer& resizer, sta::Instance* inst, sta::Pin* driver_pin, @@ -87,20 +220,19 @@ class DelayEstimator const sta::MinMax* min_max, int delay_levels, FailReason* fail_reason = nullptr, - bool use_sta_slew_bias = false); + bool use_sta_slew_bias = false, + bool include_latch_fanin = false, + DelayEstimatorCache* cache = nullptr, + const FaninSideLoadOptions* fanin_side_loads = nullptr, + bool dual_rf_eval = false); // === Delay estimation ===================================================== // When `trace` is non-null it is cleared and one StageEvaluation per // evaluated stage is appended; pass nullptr (the default) for normal // candidate ranking. - static DelayEstimate estimate(const ArcDelayState& context, + static DelayEstimate estimate(const EstimatorContext& context, const sta::LibertyCell* candidate_cell, std::vector* trace = nullptr); - static DelayEstimate estimate(const SelectedArc& arc, - float input_slew, - float load_cap, - float current_delay, - const sta::LibertyCell* candidate_cell); }; } // namespace rsz diff --git a/src/rsz/src/DelayEstimatorReporter.cc b/src/rsz/src/DelayEstimatorReporter.cc index 948acd20649..ac30b626b3b 100644 --- a/src/rsz/src/DelayEstimatorReporter.cc +++ b/src/rsz/src/DelayEstimatorReporter.cc @@ -698,7 +698,7 @@ DelayEstimatorReporter::buildLegacyProfile(const Target& target, // Per-stage rows are best-effort. If the context for input_slew capture // is unavailable, return the legal/fail_reason already computed above // and skip the optional stage tables instead of overwriting the result. - const std::optional context + const std::optional context = contextForTarget(target, 0, nullptr); if (!context.has_value()) { return profile; @@ -762,7 +762,7 @@ DelayEstimatorReporter::buildDelayEstimatorProfile( EstimatorProfile profile; FailReason fail_reason = FailReason::kNone; - const std::optional context + const std::optional context = contextForTarget(target, delay_levels, &fail_reason); if (!context.has_value()) { profile.fail_reason = fail_reason; @@ -841,7 +841,7 @@ DelayEstimatorReporter::captureFixedStages(const Target& target, { std::vector fixed_stages; FailReason fail_reason = FailReason::kNone; - const std::optional context + const std::optional context = contextForTarget(target, delay_levels, &fail_reason); if (!context.has_value()) { return fixed_stages; @@ -1015,14 +1015,14 @@ DelayEstimatorReporter::buildGoldenStageRow(const FixedStage& fixed_stage) const // === Path / context utilities ============================================= -std::optional DelayEstimatorReporter::contextForTarget( +std::optional DelayEstimatorReporter::contextForTarget( const Target& target, const int delay_levels, FailReason* fail_reason) const { - if (target.arc_delay.has_value() - && target.arc_delay->delay_estimation_levels == delay_levels) { - return target.arc_delay; + if (target.estimator_context.has_value() + && target.estimator_context->delay_estimation_levels == delay_levels) { + return target.estimator_context; } return DelayEstimator::buildContext( resizer_, target, delay_levels, fail_reason, kStaSlewBias); @@ -1050,16 +1050,16 @@ void DelayEstimatorReporter::prepareTargetContext( } FailReason ignored_reason = FailReason::kNone; - target.arc_delay = DelayEstimator::buildContext(resizer_, - inst, - driver_pin, - expanded, - path_index, - scene, - min_max, - delay_levels, - &ignored_reason, - kStaSlewBias); + target.estimator_context = DelayEstimator::buildContext(resizer_, + inst, + driver_pin, + expanded, + path_index, + scene, + min_max, + delay_levels, + &ignored_reason, + kStaSlewBias); } float DelayEstimatorReporter::pathWireDelay(const sta::PathExpanded& expanded, diff --git a/src/rsz/src/DelayEstimatorReporter.hh b/src/rsz/src/DelayEstimatorReporter.hh index c9324d9bc19..fec59663db4 100644 --- a/src/rsz/src/DelayEstimatorReporter.hh +++ b/src/rsz/src/DelayEstimatorReporter.hh @@ -178,9 +178,10 @@ class DelayEstimatorReporter int target_index) const; // === Path / context utilities ============================================ - std::optional contextForTarget(const Target& target, - int delay_levels, - FailReason* fail_reason) const; + std::optional contextForTarget( + const Target& target, + int delay_levels, + FailReason* fail_reason) const; void prepareTargetContext(Target& target, sta::Instance* inst, sta::Pin* driver_pin, diff --git a/src/rsz/src/OptimizationPolicyConfig.hh b/src/rsz/src/OptimizationPolicyConfig.hh index 213c54f19e3..448634d349f 100644 --- a/src/rsz/src/OptimizationPolicyConfig.hh +++ b/src/rsz/src/OptimizationPolicyConfig.hh @@ -19,6 +19,30 @@ struct OptimizationPolicyConfig // Correct the table slew of the fanin driver stage with STA slews sampled // at three loads. The accuracy report uses the same setting. bool sta_slew_bias{true}; + + // Charge the slower fanin driver of a larger input pin to the other loads + // of the fanin net. + bool fanin_side_loss{false}; + float fanin_side_loss_weight{3.0f}; + // Weight of fanin loads off every violating worst path. + float fanin_side_offpath_weight{0.0f}; + // A side path may fall below the worst slack by this fraction of the + // candidate's own gain. + float side_floor_margin{1.0f}; + // Also score the opposite output transition of a resized cell. + bool dual_rf_eval{false}; + + // --- VtSwapMt and SizeUpMt ------------------------------------------------ + + // Drop replacements that do not drive at least as strong on rise and fall. + bool dual_rf_drive_guard{true}; + + // Weight of the SizeUpMt score; VtSwapMt keeps 1. + float size_up_mt_score_scale{0.75f}; + + // Largest drive-resistance ratio (current / replacement) of a size-up; + // zero means no bound. + float size_up_mt_max_drive_ratio{0.0f}; }; } // namespace rsz diff --git a/src/rsz/src/Optimizer.cc b/src/rsz/src/Optimizer.cc index 34d6fe9d9e0..d74a609fab5 100644 --- a/src/rsz/src/Optimizer.cc +++ b/src/rsz/src/Optimizer.cc @@ -144,7 +144,9 @@ std::unique_ptr Optimizer::makePolicyForPhase( bool Optimizer::run() { RepairSetupProfiler profile( - resizer_, config_.profile_mode, config_.profile_dir); + resizer_, + static_cast(config_.profile_mode), + config_.profile_dir); const ProfileScope optimizer_scope("optimizer_overhead"); profile.runConfig(config_); diff --git a/src/rsz/src/OptimizerTypes.cc b/src/rsz/src/OptimizerTypes.cc index 3bfaf6a3f95..4ba8f179cb9 100644 --- a/src/rsz/src/OptimizerTypes.cc +++ b/src/rsz/src/OptimizerTypes.cc @@ -17,6 +17,7 @@ #include "sta/PathExpanded.hh" #include "sta/TimingArc.hh" #include "sta/TimingRole.hh" +#include "sta/Transition.hh" namespace rsz { @@ -56,6 +57,10 @@ const char* failReasonName(const FailReason reason) return "missing_candidate_port"; case FailReason::kMissingCandidateTimingArc: return "missing_candidate_timing_arc"; + case FailReason::kSideSlackFloorViolation: + return "side_slack_floor_violation"; + case FailReason::kDualRfNonImproving: + return "dual_rf_non_improving"; } return "unknown_fail_reason"; } @@ -197,6 +202,16 @@ const sta::TimingArc* findMatchingTimingArc(const sta::TimingArc* reference, reference, candidate, ArcMatchMode::kExact, nullptr); } +bool driveResNoHigherOnBothRf(const sta::LibertyPort* current, + const sta::LibertyPort* candidate) +{ + const sta::MinMax* min_max = sta::MinMax::max(); + return candidate->driveResistance(sta::RiseFall::rise(), min_max) + <= current->driveResistance(sta::RiseFall::rise(), min_max) + && candidate->driveResistance(sta::RiseFall::fall(), min_max) + <= current->driveResistance(sta::RiseFall::fall(), min_max); +} + const sta::Path* latchDataPath(const sta::PathExpanded& expanded) { const sta::Path* d_path = nullptr; @@ -284,13 +299,13 @@ Target makePathDriverTarget(const sta::Path* endpoint_path, bool Target::isPrepared(const PrepareCacheMask mask) const { - if ((mask & ~kArcDelayStateCache) != 0) { + if ((mask & ~kEstimatorContextCache) != 0) { return false; } - if ((mask & kArcDelayStateCache) == 0) { + if ((mask & kEstimatorContextCache) == 0) { return true; } - return arc_delay.has_value(); + return estimator_context.has_value(); } const sta::Pin* Target::endpointPin(const Resizer& resizer) const diff --git a/src/rsz/src/OptimizerTypes.hh b/src/rsz/src/OptimizerTypes.hh index e345d32f130..f56b6acbfab 100644 --- a/src/rsz/src/OptimizerTypes.hh +++ b/src/rsz/src/OptimizerTypes.hh @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,7 @@ class Vertex; namespace rsz { class MoveCandidate; class Resizer; +class SequentialTimingEstimator; } // namespace rsz namespace rsz { @@ -71,7 +73,13 @@ enum class FailReason : uint8_t // Candidate timing context. kMissingCandidatePort, - kMissingCandidateTimingArc + kMissingCandidateTimingArc, + + // A modeled side path pushed below the prepared global worst slack. + kSideSlackFloorViolation, + + // The opposite output transition of the target would fall below the floor. + kDualRfNonImproving }; // === Fail-reason labels ===================================================== @@ -107,12 +115,23 @@ const sta::TimingArc* findMatchingTimingArc(const sta::TimingArc* reference, ArcMatchMode match_mode, ArcMatchType* match_type); +// True when candidate drive resistance is at most the current value on both +// rise and fall. +bool driveResNoHigherOnBothRf(const sta::LibertyPort* current, + const sta::LibertyPort* candidate); + // === Target preparation flags ============================================== using PrepareCacheMask = uint32_t; inline constexpr PrepareCacheMask kNoPrepareCache = 0; -inline constexpr PrepareCacheMask kArcDelayStateCache = 1u << 0; +inline constexpr PrepareCacheMask kEstimatorContextCache = 1u << 0; + +// Generation inputs: one Mt generator's live STA reads, prepared on the main +// thread so its generate() can run on a worker. Without them, the cell-swap +// generators read live STA. +inline constexpr PrepareCacheMask kInputCapBudgetCache = 1u << 2; +inline constexpr PrepareCacheMask kGenerationInputCaches = kInputCapBudgetCache; // === Prepared timing data =================================================== @@ -183,6 +202,16 @@ struct SlewBiasModel std::array samples; }; +// A target cell arc that drives the opposite output transition through the +// selected input. Workers use it to charge a candidate that speeds up one +// transition but slows the other. +struct OppositeRfArc +{ + const sta::TimingArc* ref_arc{nullptr}; + float input_slew{0.0f}; + float current_model_delay{0.0f}; +}; + // Snapshot of one timing stage's electrical state needed for table-model delay // estimation. MT policies prepare this on the main thread and then worker // threads read it from Target without touching STA analysis state. @@ -198,19 +227,58 @@ struct DelayStageState float current_slew{0.0f}; // STA graph merged slew baseline std::vector output_slew_merge_arcs; SlewBiasModel sta_slew_bias; + // Target stage only: the opposite-transition arcs, empty when dual_rf_eval + // is off. + std::vector opposite_rf_arcs; int path_index{-1}; }; +// One load pin whose slack pays for the delay a candidate adds to it: a load +// on a target fanin net, or a load a split moves to a new driver. weight +// scales the charged loss: a pin on a violating worst path moves its endpoint +// one for one, other pins only lose slack locally. +struct SideLoad +{ + const sta::Pin* pin{nullptr}; + float slack{0.0f}; + float weight{1.0f}; +}; + +// One input net of the target instance. A new cell changes every input pin +// capacitance, so each fanin driver slows its other loads. The selected-arc +// net takes the window's exact delta, the others the drive resistance. +struct FaninSideNet +{ + const sta::LibertyPort* input_port{nullptr}; + float input_cap{0.0f}; + float delay_per_cap{0.0f}; + bool selected_arc{false}; + std::vector loads; +}; + // Prepared delay-estimator context for a target stage and its optional // N-level fanin/fanout timing window. path_stages always contains at least // one entry (the target stage) after a successful buildContext; consumers // access electrical state via target() rather than duplicated top-level // fields. -struct ArcDelayState +struct EstimatorContext { std::vector path_stages; int target_stage_index{0}; int delay_estimation_levels{0}; + + // The target's fanin nets and their other loads, empty when fanin_side_loss + // is off. + std::vector fanin_side_nets; + // The worst slack at prepare time. A side path may fall below it by at most + // side_floor_margin times the candidate's gain, or the checkpoint would fail. + float side_slack_floor{-std::numeric_limits::infinity()}; + float side_floor_margin{1.0f}; + // Weight of the summed side loss before it is subtracted from the gain. + float fanin_side_loss_weight{1.0f}; + // Worst slack of the opposite output transition at the target driver; the + // dual rise/fall check treats that transition as one more side path. + float opposite_rf_slack{std::numeric_limits::infinity()}; // Cached sum of stage.current_delay across path_stages; populated once at // build time so per-candidate scoring does not re-walk the window. float current_total_delay{0.0f}; @@ -219,6 +287,11 @@ struct ArcDelayState { return path_stages[target_stage_index]; } + // The lowest slack a side path may fall to for a candidate that gains gain. + float sideSlackFloor(const float gain) const + { + return side_slack_floor - side_floor_margin * std::max(gain, 0.0f); + } }; // Raw delay-estimator result before a candidate converts it into a @@ -228,6 +301,10 @@ struct DelayEstimate bool legal{false}; float candidate_delay{0.0f}; float arrival_impr{0.0f}; + int side_paths_evaluated{0}; + // The weighted slack loss of the side paths in the negative-slack region, + // zero when none. + float side_loss{0.0f}; FailReason reason{FailReason::kNone}; }; @@ -285,7 +362,13 @@ struct Target const sta::MinMax* min_max{nullptr}; // === Prepared data for MT generation/estimation ========================== - std::optional arc_delay; + std::optional estimator_context; + std::shared_ptr sequential_timing_estimator; + + // === Generation inputs (kGenerationInputCaches) =========================== + // Fanin max-cap headroom of the target instance (the cell-swap + // generators); the STA capacitance check behind it is main-thread only. + std::optional> input_cap_budgets; // === Field validation ==================================================== bool canBePathDriver() const; @@ -361,8 +444,19 @@ struct Estimate float score{0.0f}; // Unscaled arrival_impr in seconds; absent if estimation did not complete. std::optional raw_gain{}; - // Non-owning static label from failReasonName(), "min_score", or "max_cap". + // Non-owning static label: failReasonName() or a candidate's own literal + // such as "side_loss_non_improving" or "min_score". std::string_view reason{}; + // A reject by a modelled side cost alone while the own path gains. The WNS + // tail may admit it and let the batch STA judge the side cost. The moves + // set it for their side-loss, sequential-check and score-floor rejects; + // only SizeUpMt sets it for an opposite-transition reject. + bool tail_admissible{false}; + // Opposite-side constraint loss stays separate from primary timing gain. + std::optional sequential_penalty{}; + std::optional sequential_side_loss{}; + // Estimated side-path slack loss charged against the local gain. + std::optional side_loss{}; }; // Result of applying one candidate ECO (MoveCandidate::apply()). The diff --git a/src/rsz/src/RepairSetupProfiler.cc b/src/rsz/src/RepairSetupProfiler.cc index 7d5ad5f4518..755e9e21497 100644 --- a/src/rsz/src/RepairSetupProfiler.cc +++ b/src/rsz/src/RepairSetupProfiler.cc @@ -53,11 +53,10 @@ constexpr std::array kCoveredPhases } // namespace RepairSetupProfiler::RepairSetupProfiler(Resizer& resizer, - const int mode, + const Mode mode, const std::string& directory) - : resizer_(resizer) + : resizer_(resizer), mode_(mode) { - mode_ = static_cast(std::clamp(mode, 0, 2)); if (mode_ == Mode::kOff) { return; } @@ -333,6 +332,17 @@ void RepairSetupProfiler::moveEstimated(MoveCandidate& candidate, event(estimate.legal ? "estimate_legal" : "estimate_rejected", 1, moveName(candidate.type())); + if (estimate.sequential_penalty.has_value()) { + event("sequential_estimated", 1, moveName(candidate.type())); + if (*estimate.sequential_penalty > 0.0f) { + event("sequential_penalized", 1, moveName(candidate.type())); + } + } + + if (estimate.side_loss.has_value() && *estimate.side_loss > 0.0f) { + event("side_loss_penalized", 1, moveName(candidate.type())); + } + if (!estimate.reason.empty()) { event("estimate_reason:" + std::string(estimate.reason), 1, @@ -495,6 +505,23 @@ void RepairSetupProfiler::beginApply(MoveCandidate& candidate) profiler.observation("estimate", {{"raw_gain_s", *info.estimate.raw_gain}}); } + + if (info.estimate.sequential_penalty.has_value()) { + profiler.observation( + "estimate", + {{"sequential_penalty_s", *info.estimate.sequential_penalty}}); + } + + if (info.estimate.sequential_side_loss.has_value()) { + profiler.observation( + "estimate", + {{"sequential_side_loss_s", *info.estimate.sequential_side_loss}}); + } + + if (info.estimate.side_loss.has_value()) { + profiler.observation("estimate", + {{"side_loss_s", *info.estimate.side_loss}}); + } } } diff --git a/src/rsz/src/RepairSetupProfiler.hh b/src/rsz/src/RepairSetupProfiler.hh index 57114397449..bd650103e99 100644 --- a/src/rsz/src/RepairSetupProfiler.hh +++ b/src/rsz/src/RepairSetupProfiler.hh @@ -52,9 +52,9 @@ class RepairSetupProfiler : public odb::dbBlockCallBackObj // === Run lifetime and active context ==================================== - // mode 0 disables every output; see OptimizerRunConfig::profile_mode. + // Mode::kOff disables every output; see OptimizerRunConfig::profile_mode. explicit RepairSetupProfiler(Resizer& resizer, - int mode = 0, + Mode mode = Mode::kOff, const std::string& directory = ""); ~RepairSetupProfiler() override; diff --git a/src/rsz/src/RepairTargetCollector.cc b/src/rsz/src/RepairTargetCollector.cc index e4389c18bce..4ab780d758c 100644 --- a/src/rsz/src/RepairTargetCollector.cc +++ b/src/rsz/src/RepairTargetCollector.cc @@ -861,6 +861,7 @@ void RepairTargetCollector::collectViolatingEndpoints() { const ProfileScope profile("endpoint_scan"); violating_endpoints_.clear(); + crit_path_load_pins_.clear(); if (restrictedToPathGroup()) { // One query for the whole group, already carrying each endpoint's slack @@ -880,6 +881,35 @@ void RepairTargetCollector::collectViolatingEndpoints() // Preserve equal-slack STA endpoint order for legacy QoR parity. std::ranges::stable_sort(violating_endpoints_, slackLess); + if (record_crit_path_load_pins_) { + // Side loss weights a load fully only on a violating endpoint's worst path. + // Record those pins for every violating endpoint, beyond the current chunk. + const ProfileScope crit_profile("scan_crit_paths"); + for (const PinSlack& endpoint_data : violating_endpoints_) { + sta::Vertex* endpoint = graph_->pinLoadVertex(endpoint_data.first); + sta::Path* path + = endpoint != nullptr ? findWorstSlackPath(endpoint) : nullptr; + if (path == nullptr) { + continue; + } + + sta::PathExpanded expanded(path, sta_); + const int path_count = static_cast(expanded.size()); + for (int index = static_cast(expanded.startIndex()); + index < path_count; + ++index) { + const sta::Path* point = expanded.path(index); + sta::Vertex* vertex = point->vertex(sta_); + if (vertex != nullptr && !vertex->isDriver(network_)) { + crit_path_load_pins_.insert(point->pin(sta_)); + } + } + } + + RepairSetupProfiler::event("scan_crit_path_load_pins", + crit_path_load_pins_.size()); + } + debugPrint(logger_, RSZ, "violator_collector", @@ -1058,8 +1088,17 @@ void RepairTargetCollector::collectExpandedPathDriverTargets( const sta::Path* driver_path = expanded.path(index); sta::Vertex* vertex = driver_path->vertex(sta_); sta::Pin* pin = driver_path->pin(sta_); - if (vertex == nullptr || pin == nullptr || !vertex->isDriver(network_) - || network_->isTopLevelPort(pin)) { + if (vertex == nullptr || pin == nullptr) { + continue; + } + + if (!vertex->isDriver(network_)) { + // Load pins on this worst path carry the endpoint slack one-for-one. + crit_path_load_pins_.insert(pin); + continue; + } + + if (network_->isTopLevelPort(pin)) { continue; } diff --git a/src/rsz/src/RepairTargetCollector.hh b/src/rsz/src/RepairTargetCollector.hh index 4b9837f0bfb..8dfd28e3c0d 100644 --- a/src/rsz/src/RepairTargetCollector.hh +++ b/src/rsz/src/RepairTargetCollector.hh @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -180,6 +181,15 @@ class RepairTargetCollector return violating_endpoints_; } + const std::unordered_set& critPathLoadPins() const + { + return crit_path_load_pins_; + } + void setRecordCritPathLoadPins(const bool record) + { + record_crit_path_load_pins_ = record; + } + // Public startpoint collection for STARTPOINT_FANOUT Phase void collectViolatingStartpoints(); const vector>& @@ -382,6 +392,15 @@ class RepairTargetCollector std::map pin_data_; vector> violating_endpoints_; vector> violating_startpoints_; + // Load pins that take the full side-loss weight; cleared at each scan. With + // record_crit_path_load_pins_, the scan adds the main worst path of every + // violating endpoint. Target collection adds each path it expands, latch + // D-side segments included, so the set also grows with the endpoints + // evaluated since the scan. + mutable std::unordered_set crit_path_load_pins_; + // True makes the scan walk the worst path of every violating endpoint so + // crit_path_load_pins_ covers endpoints beyond the current target chunk. + bool record_crit_path_load_pins_{false}; // === Endpoint iteration state ============================================ // Endpoint pass tracking diff --git a/src/rsz/src/Resizer.cc b/src/rsz/src/Resizer.cc index 39fecf0304e..e4cb3346e5f 100644 --- a/src/rsz/src/Resizer.cc +++ b/src/rsz/src/Resizer.cc @@ -7515,6 +7515,97 @@ bool Resizer::replacementPreservesMaxCap(sta::Instance* inst, return true; } +std::vector Resizer::inputCapBudgets( + sta::Instance* inst) +{ + std::vector budgets; + sta::LibertyCell* current_cell = network_->libertyCell(inst); + if (current_cell == nullptr) { + return budgets; + } + + // One STA capacitance check per fanin driver, shared by every replacement. + std::unique_ptr pin_iter( + network_->pinIterator(inst)); + while (pin_iter->hasNext()) { + sta::Pin* pin = pin_iter->next(); + if (!network_->direction(pin)->isAnyInput()) { + continue; + } + + sta::PinSet* drivers = network_->drivers(pin); + sta::LibertyPort* port = network_->libertyPort(pin); + if (drivers == nullptr || port == nullptr) { + continue; + } + + InputCapBudget budget; + budget.port = port; + budget.current_cap = inputPinCapacitance(network_, pin, current_cell, max_); + budget.driver_limits.reserve(drivers->size()); + for (const sta::Pin* driver_pin : *drivers) { + float cap; + float max_cap; + float cap_slack; + const sta::Scene* corner; + const sta::RiseFall* tr; + sta_->checkCapacitance(driver_pin, + sta_->scenes(), + max_, + cap, + max_cap, + cap_slack, + tr, + corner); + if (max_cap > 0.0 && corner != nullptr) { + budget.driver_limits.push_back({.cap = cap, + .max_cap = max_cap, + .negative_slack = cap_slack < 0.0}); + } + } + + budgets.push_back(std::move(budget)); + } + + return budgets; +} + +bool Resizer::replacementFitsInputCapBudgets( + const std::vector& budgets, + const sta::LibertyCell* replacement) const +{ + if (replacement == nullptr) { + return false; + } + + for (const InputCapBudget& budget : budgets) { + const sta::LibertyPort* cell_port + = replacement->findLibertyPort(budget.port->name()); + + float new_cap = 0.0f; + if (cell_port != nullptr) { + for (const sta::RiseFall* rf : sta::RiseFall::range()) { + new_cap = std::max(new_cap, cell_port->capacitance(rf, max_)); + } + } + + const float cap_delta = new_cap - budget.current_cap; + if (cap_delta <= 0.0f) { + continue; + } + + for (const DriverCapLimit& limit : budget.driver_limits) { + // Same acceptance rule as checkMaxCapOK(). + const float driver_cap = limit.cap + cap_delta; + if (driver_cap > (limit.negative_slack ? limit.cap : limit.max_cap)) { + return false; + } + } + } + + return true; +} + bool Resizer::estimateInputSlewImpact( sta::Instance* instance, const sta::Scene* scene, diff --git a/src/rsz/src/SequentialTimingEstimator.cc b/src/rsz/src/SequentialTimingEstimator.cc new file mode 100644 index 00000000000..4aa7a7e297a --- /dev/null +++ b/src/rsz/src/SequentialTimingEstimator.cc @@ -0,0 +1,765 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#include "SequentialTimingEstimator.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DelayEstimator.hh" +#include "OptimizerTypes.hh" +#include "RepairSetupProfiler.hh" +#include "db_sta/dbSta.hh" +#include "rsz/Resizer.hh" +#include "sta/ClkNetwork.hh" +#include "sta/Delay.hh" +#include "sta/Graph.hh" +#include "sta/GraphDelayCalc.hh" +#include "sta/Liberty.hh" +#include "sta/MinMax.hh" +#include "sta/Mode.hh" +#include "sta/Network.hh" +#include "sta/Path.hh" +#include "sta/PathEnd.hh" +#include "sta/PathExpanded.hh" +#include "sta/PocvMode.hh" +#include "sta/Scene.hh" +#include "sta/Sdc.hh" +#include "sta/Sequential.hh" +#include "sta/TimingArc.hh" +#include "sta/TimingModel.hh" +#include "sta/TimingRole.hh" +#include "sta/VisitPathEnds.hh" + +namespace rsz { +namespace { + +constexpr double kSideSlackWeightCutoff = 10.0e-12; +constexpr double kSideSlackViolationTolerance = 0.001e-12; + +struct PreparedInputTiming +{ + const sta::MinMax* min_max{nullptr}; + std::optional driver_pvt; + std::optional driver; + std::vector driver_gate_models; + std::vector candidate_connected_input_caps; + float current_connected_input_cap{0.0f}; + float slew{0.0f}; + float delay_derate{1.0f}; + bool ideal_clock{false}; +}; + +struct PreparedSetupHoldCheck +{ + const sta::MinMax* min_max{nullptr}; + std::optional pvt; + PreparedInputTiming data; + PreparedInputTiming clock; + std::optional clock_check_slew_input; + std::vector candidate_check_models; + float related_out_cap{0.0f}; + float baseline_check_margin{0.0f}; + float check_derate{1.0f}; + double baseline_slack{0.0}; + bool setup{false}; +}; + +std::optional copyPvt(const sta::Pvt* pvt) +{ + if (pvt == nullptr) { + return std::nullopt; + } + return *pvt; +} + +const sta::Pvt* pvtValue(const std::optional& pvt) +{ + return pvt ? &*pvt : nullptr; +} + +bool isFlipFlop(const sta::LibertyCell* cell) +{ + return cell != nullptr && !cell->sequentials().empty() + && std::ranges::all_of(cell->sequentials(), + [](const sta::Sequential& sequential) { + return sequential.isRegister(); + }); +} + +const sta::TimingArc* matchingArc(const sta::TimingArc* reference, + const sta::LibertyCell* cell) +{ + const sta::LibertyPort* from + = cell->findLibertyPort(reference->from()->name()); + const sta::LibertyPort* to = cell->findLibertyPort(reference->to()->name()); + if (from == nullptr || to == nullptr) { + return nullptr; + } + + for (const sta::TimingArcSet* set : cell->timingArcSets(from, to)) { + const sta::TimingArc* arc = findMatchingTimingArc(reference, set); + if (arc != nullptr) { + const sta::LibertyPort* related = reference->set()->relatedOut(); + const sta::LibertyPort* candidate_related = set->relatedOut(); + if ((related == nullptr) != (candidate_related == nullptr) + || (related != nullptr + && related->name() != candidate_related->name())) { + continue; + } + return arc; + } + } + + return nullptr; +} + +float pinCapacitance(sta::LibertyCell* cell, + std::string_view port_name, + const sta::Scene* scene, + const sta::RiseFall* rf, + const sta::MinMax* min_max, + const sta::Pvt* pvt) +{ + const float missing = std::numeric_limits::quiet_NaN(); + const sta::LibertyCell* scene_cell = cell->sceneCell(scene, min_max); + const sta::LibertyPort* port = scene_cell == nullptr + ? nullptr + : scene_cell->findLibertyPort(port_name); + if (port == nullptr) { + return missing; + } + + // The unscaled lookup tells whether the port has a capacitance at all. + bool exists = false; + float unscaled_cap = 0.0f; + port->capacitance(rf, min_max, unscaled_cap, exists); + + // The scaled-port overload can insert into a Liberty map: prepare only. + const float cap = port->capacitance( + rf, min_max, scene->sdc()->operatingConditions(min_max), pvt); + return (exists || cap > 0.0f) && std::isfinite(cap) && cap >= 0.0f ? cap + : missing; +} + +bool tableDelay(const sta::GateTimingModel* model, + const sta::Pvt* pvt, + float slew, + float cap, + float& delay, + float& output_slew) +{ + if (model == nullptr) { + return false; + } + + model->gateDelay(pvt, slew, cap, delay, output_slew); + return std::isfinite(delay) && std::isfinite(output_slew) + && output_slew >= 0.0f; +} + +bool prepareInputCaps(Resizer& resizer, + sta::Instance* target_inst, + const sta::Path* path, + const sta::LibertyCellSeq& candidates, + PreparedInputTiming& context) +{ + const sta::StaState* sta = resizer.staState(); + const sta::Scene* scene = path->scene(sta); + const sta::RiseFall* rf = path->transition(sta); + const sta::Pvt* pvt + = resizer.sta()->pvt(target_inst, context.min_max, scene->sdc()); + + // Include every replaced input sharing this net, not just the selected D/CK. + sta::Network* network = resizer.network(); + std::unique_ptr pins( + network->connectedPinIterator(path->pin(sta))); + + int drivers = 0; + bool found_load_port = false; + while (pins->hasNext()) { + const sta::Pin* pin = pins->next(); + drivers += network->isDriver(pin); + if (network->instance(pin) != target_inst || !network->isLoad(pin)) { + continue; + } + + const sta::LibertyPort* port = network->libertyPort(pin); + found_load_port = true; + + context.current_connected_input_cap += pinCapacitance( + port->libertyCell(), port->name(), scene, rf, context.min_max, pvt); + for (size_t index = 0; index < candidates.size(); ++index) { + context.candidate_connected_input_caps[index] += pinCapacitance( + candidates[index], port->name(), scene, rf, context.min_max, pvt); + } + } + + return drivers == 1 && found_load_port + && std::isfinite(context.current_connected_input_cap); +} + +// The gate driving the input on path: its table models at the current load +// and its delay derate, so estimate() can replay a load change. +bool prepareInputDriver(Resizer& resizer, + const sta::Path* path, + const sta::Path* driver_path, + const sta::MinMax* min_max, + PreparedInputTiming& context) +{ + const sta::StaState* sta = resizer.staState(); + const sta::Scene* scene = path->scene(sta); + sta::Pin* driver_pin = driver_path->pin(sta); + sta::Instance* driver_inst = resizer.network()->instance(driver_pin); + sta::PathExpanded expanded(driver_path, sta); + const std::optional prepared + = DelayEstimator::buildContext(resizer, + driver_inst, + driver_pin, + expanded, + expanded.size() - 1, + scene, + context.min_max, + 0); + if (!prepared) { + return false; + } + + context.driver = prepared->target(); + DelayStageState& stage = *context.driver; + context.driver_pvt = copyPvt(stage.arc.pvt); + stage.load_cap = sta->graphDelayCalc()->loadCap( + driver_pin, path->transition(sta), scene, min_max); + context.driver_gate_models.push_back( + stage.arc.ref_arc->gateModel(scene, min_max)); + if (!tableDelay(context.driver_gate_models.front(), + pvtValue(context.driver_pvt), + stage.input_slew, + stage.load_cap, + stage.current_model_delay, + stage.current_model_slew)) { + return false; + } + + for (OutputSlewMergeArc& arc : stage.output_slew_merge_arcs) { + const sta::GateTimingModel* model = arc.ref_arc->gateModel(scene, min_max); + float delay = 0.0f; + if (!tableDelay(model, + pvtValue(context.driver_pvt), + arc.input_slew, + stage.load_cap, + delay, + arc.current_model_slew)) { + return false; + } + + context.driver_gate_models.push_back(model); + if (min_max->compare(arc.current_model_slew, stage.current_model_slew)) { + stage.current_model_slew = arc.current_model_slew; + } + } + + context.delay_derate = scene->sdc()->timingDerateInstance( + driver_path->prevPath()->pin(sta), + sta::TimingDerateCellType::cell_delay, + driver_path->isClock(sta) ? sta::PathClkOrData::clk + : sta::PathClkOrData::data, + stage.arc.inputRiseFall(), + context.min_max); + + // Leave only immutable Liberty handles and numerical state in the prepared + // timing. + stage.arc.scene = nullptr; + stage.arc.pvt = nullptr; + stage.driver_pin = nullptr; + + return std::isfinite(stage.load_cap) && std::isfinite(context.delay_derate); +} + +bool prepareInput(Resizer& resizer, + sta::Instance* target_inst, + const sta::Path* path, + float check_slew, + const sta::MinMax* min_max, + const sta::LibertyCellSeq& candidates, + PreparedInputTiming& context) +{ + const sta::StaState* sta = resizer.staState(); + const sta::Scene* scene = path->scene(sta); + context.min_max = min_max; + context.slew = check_slew; + context.candidate_connected_input_caps.resize(candidates.size(), 0.0f); + if (!std::isfinite(check_slew) || check_slew < 0.0f) { + return false; + } + + context.ideal_clock + = path->isClock(sta) + && scene->mode()->clkNetwork()->isIdealClock(path->pin(sta)); + if (context.ideal_clock) { + return true; + } + + if (!prepareInputCaps(resizer, target_inst, path, candidates, context)) { + return false; + } + + // A primary input without a gate model is usable only for unchanged load. + const sta::Path* driver_path = path->prevPath(); + if (driver_path == nullptr || path->prevArc(sta) == nullptr + || !path->prevArc(sta)->role()->isWire()) { + return true; + } + + sta::Instance* driver_inst + = resizer.network()->instance(driver_path->pin(sta)); + if (driver_inst == target_inst) { + return false; + } + + if (driver_path->prevArc(sta) == nullptr) { + return true; + } + + return prepareInputDriver(resizer, path, driver_path, min_max, context); +} + +bool checkMargin(const sta::CheckTimingModel* model, + const PreparedSetupHoldCheck& context, + float data_slew, + float clock_slew, + float& margin) +{ + if (model == nullptr) { + return false; + } + + // Liberty check arcs run from related clock to constrained data, in that + // order. + margin = sta::delayAsFloat(model->checkDelay(pvtValue(context.pvt), + clock_slew, + data_slew, + context.related_out_cap, + context.min_max, + sta::PocvMode::scalar)); + return std::isfinite(margin); +} + +bool prepareSetupHoldCheck(Resizer& resizer, + sta::Instance* inst, + const sta::PathEnd* end, + const sta::LibertyCellSeq& candidates, + PreparedSetupHoldCheck& context) +{ + const sta::StaState* sta = resizer.staState(); + const sta::Path* data_path = end->path(); + const sta::Path* clock_path = end->targetClkPath(); + const sta::Scene* scene = data_path->scene(sta); + context.min_max = data_path->minMax(sta); + const sta::LibertyCell* cell + = resizer.network()->libertyCell(inst)->sceneCell(scene, context.min_max); + if (cell == nullptr || clock_path == nullptr) { + return false; + } + + const sta::TimingArc* check_arc = matchingArc(end->checkArc(), cell); + if (check_arc == nullptr) { + return false; + } + + context.pvt + = copyPvt(resizer.sta()->pvt(inst, context.min_max, scene->sdc())); + context.setup = check_arc->role() == sta::TimingRole::setup(); + context.baseline_slack = sta::delayAsFloat(end->slack(sta)); + context.check_derate = scene->sdc()->timingDerateInstance( + clock_path->pin(sta), + sta::TimingDerateCellType::cell_check, + sta::PathClkOrData::data, + data_path->transition(sta), + context.min_max); + for (sta::LibertyCell* candidate : candidates) { + const sta::LibertyCell* candidate_cell + = candidate->sceneCell(scene, context.min_max); + const sta::TimingArc* arc = candidate_cell == nullptr + ? nullptr + : matchingArc(check_arc, candidate_cell); + context.candidate_check_models.push_back( + arc == nullptr ? nullptr : arc->checkModel(scene, context.min_max)); + } + + const sta::ClkNetwork* clock_network = scene->mode()->clkNetwork(); + const sta::RiseFall* clock_rf = check_arc->fromEdge()->asRiseFall(); + const float clock_slew + = clock_network->isIdealClock(clock_path->pin(sta)) + ? clock_network->idealClkSlew( + clock_path->pin(sta), + clock_rf, + scene->checkClkSlewMinMax(context.min_max)) + : sta::delayAsFloat(resizer.graph()->slew( + clock_path->vertex(sta), + clock_rf, + scene->checkClkSlewIndex(context.min_max))); + const float data_slew = sta::delayAsFloat(data_path->slew(sta)); + const sta::MinMax* clock_min_max = clock_path->minMax(sta); + const sta::MinMax* waveform_min_max + = scene->checkClkSlewMinMax(context.min_max); + if (!prepareInput(resizer, + inst, + data_path, + data_slew, + context.min_max, + candidates, + context.data) + || !prepareInput(resizer, + inst, + clock_path, + clock_slew, + clock_min_max, + candidates, + context.clock)) { + return false; + } + + // The check-slew analysis reuses the fixed clock topology, not its arrival + // sense. Its delay delta never replaces the capture-arrival delta. + if (waveform_min_max != clock_min_max && !context.clock.ideal_clock) { + context.clock_check_slew_input.emplace(); + if (!prepareInput(resizer, + inst, + clock_path, + clock_slew, + waveform_min_max, + candidates, + *context.clock_check_slew_input)) { + return false; + } + } + + const sta::LibertyPort* related_out = check_arc->set()->relatedOut(); + if (related_out != nullptr) { + const sta::Pin* pin = resizer.network()->findPin(inst, related_out->name()); + if (pin == nullptr) { + return false; + } + + context.related_out_cap = sta->graphDelayCalc()->loadCap( + pin, data_path->transition(sta), scene, context.min_max); + } + + return std::isfinite(context.baseline_slack) + && std::isfinite(context.check_derate) + && std::isfinite(context.related_out_cap) + && checkMargin(end->checkArc()->checkModel(scene, context.min_max), + context, + data_slew, + clock_slew, + context.baseline_check_margin); +} + +class SetupHoldCheckCollector : public sta::PathEndVisitor +{ + public: + SetupHoldCheckCollector(Resizer& resizer, + sta::Instance* inst, + const sta::LibertyCellSeq& candidates, + std::vector& checks, + std::string_view& failure_reason) + : resizer_(resizer), + inst_(inst), + candidates_(candidates), + checks_(checks), + failure_reason_(failure_reason) + { + } + + sta::PathEndVisitor* copy() const override + { + return new SetupHoldCheckCollector(*this); + } + + void visit(sta::PathEnd* end) override + { + if (!failure_reason_.empty() || !end->isCheck()) { + return; + } + + const sta::TimingRole* role = end->checkArc()->role(); + if (role != sta::TimingRole::setup() && role != sta::TimingRole::hold()) { + return; + } + + PreparedSetupHoldCheck check; + if (!prepareSetupHoldCheck(resizer_, inst_, end, candidates_, check)) { + failure_reason_ = "sequential_missing_check_context"; + return; + } + + checks_.push_back(std::move(check)); + } + + private: + Resizer& resizer_; + sta::Instance* inst_; + const sta::LibertyCellSeq& candidates_; + std::vector& checks_; + std::string_view& failure_reason_; +}; + +bool estimateInputTimingForCap(const PreparedInputTiming& context, + float candidate_connected_input_cap, + double& arrival_delta, + float& slew) +{ + arrival_delta = 0.0; + slew = context.slew; + if (context.ideal_clock) { + return true; + } + + if (!std::isfinite(candidate_connected_input_cap)) { + return false; + } + + const float delta_cap + = candidate_connected_input_cap - context.current_connected_input_cap; + if (delta_cap == 0.0f) { + return true; + } + + if (!context.driver) { + return false; + } + + const DelayStageState& driver = *context.driver; + const float load_cap = driver.load_cap + delta_cap; + float delay = 0.0f; + float model_slew = 0.0f; + if (!std::isfinite(load_cap) || load_cap < 0.0f + || !tableDelay(context.driver_gate_models.front(), + pvtValue(context.driver_pvt), + driver.input_slew, + load_cap, + delay, + model_slew)) { + return false; + } + + for (size_t index = 0; index < driver.output_slew_merge_arcs.size(); + ++index) { + const OutputSlewMergeArc& arc = driver.output_slew_merge_arcs[index]; + float merge_delay = 0.0f; + float merge_slew = 0.0f; + if (!tableDelay(context.driver_gate_models[index + 1], + pvtValue(context.driver_pvt), + arc.input_slew, + load_cap, + merge_delay, + merge_slew)) { + return false; + } + + if (context.min_max->compare(merge_slew, model_slew)) { + model_slew = merge_slew; + } + } + + // Preserve the measured wire contribution; only the driver table delta moves. + arrival_delta = (static_cast(delay) - driver.current_model_delay) + * context.delay_derate; + slew = std::max(0.0f, context.slew + model_slew - driver.current_model_slew); + return std::isfinite(arrival_delta) && std::isfinite(slew); +} + +} // namespace + +struct SequentialTimingEstimator::PreparedFlipFlopTiming +{ + sta::LibertyCellSeq candidates; + std::vector checks; + std::string_view prepare_failure_reason; +}; + +SequentialTimingEstimator::SequentialTimingEstimator( + std::shared_ptr prepared_timing) + : prepared_timing_(std::move(prepared_timing)) +{ +} + +std::shared_ptr +SequentialTimingEstimator::prepare(Resizer& resizer, const Target& target) +{ + sta::Instance* inst = target.inst(resizer); + sta::LibertyCell* cell + = inst != nullptr ? resizer.network()->libertyCell(inst) : nullptr; + if (!isFlipFlop(cell)) { + return nullptr; + } + + const ProfileScope profile("prepare_sequential"); + std::shared_ptr prepared_timing + = std::make_shared(); + const sta::LibertyPort* output + = resizer.network()->libertyPort(target.resolvedPin(resizer)); + + bool has_clock_to_q = false; + for (const sta::TimingArcSet* set : cell->timingArcSetsTo(output)) { + has_clock_to_q |= set->role() == sta::TimingRole::regClkToQ(); + } + + if (!has_clock_to_q || resizer.sta()->pocvMode() != sta::PocvMode::scalar) { + prepared_timing->prepare_failure_reason + = "sequential_unsupported_target_context"; + } else { + std::set candidates{cell}; + for (sta::LibertyCell* candidate : resizer.getSwappableCells(cell)) { + if (isFlipFlop(candidate)) { + candidates.insert(candidate); + } + } + + for (sta::LibertyCell* candidate : resizer.getVTEquivCells(cell)) { + if (isFlipFlop(candidate)) { + candidates.insert(candidate); + } + } + + prepared_timing->candidates.assign(candidates.begin(), candidates.end()); + + // Liberty roles identify constrained inputs, including scan-data inputs. + std::set data_ports; + for (const sta::TimingArcSet* set : cell->timingArcSets()) { + if (set->role() == sta::TimingRole::setup() + || set->role() == sta::TimingRole::hold()) { + data_ports.insert(set->to()); + } + } + + SetupHoldCheckCollector visitor(resizer, + inst, + prepared_timing->candidates, + prepared_timing->checks, + prepared_timing->prepare_failure_reason); + + sta::VisitPathEnds visit_ends(resizer.staState()); + for (const sta::LibertyPort* port : data_ports) { + const sta::Pin* pin = resizer.network()->findPin(inst, port->name()); + sta::Vertex* vertex + = pin == nullptr ? nullptr : resizer.graph()->pinLoadVertex(pin); + if (vertex == nullptr) { + prepared_timing->prepare_failure_reason = "sequential_missing_data_pin"; + break; + } + + visit_ends.visitPathEnds(vertex, &visitor); + if (!prepared_timing->prepare_failure_reason.empty()) { + break; + } + } + + if (data_ports.empty() && prepared_timing->prepare_failure_reason.empty()) { + prepared_timing->prepare_failure_reason + = "sequential_missing_check_context"; + } + } + + RepairSetupProfiler::event("sequential_active_checks", + prepared_timing->checks.size()); + RepairSetupProfiler::event("sequential_candidate_cells_prepared", + prepared_timing->candidates.size()); + return std::shared_ptr( + new SequentialTimingEstimator(std::move(prepared_timing))); +} + +SequentialTimingEstimator::Result SequentialTimingEstimator::estimate( + const sta::LibertyCell* candidate_cell) const +{ + if (!prepared_timing_->prepare_failure_reason.empty()) { + return {.reason = prepared_timing_->prepare_failure_reason}; + } + + const sta::LibertyCellSeq::const_iterator candidate + = std::ranges::find(prepared_timing_->candidates, candidate_cell); + if (candidate == prepared_timing_->candidates.end()) { + return {.reason = "sequential_missing_candidate_cell"}; + } + + const size_t index = candidate - prepared_timing_->candidates.begin(); + Result result{.legal = true, .reason = "sequential_estimate_legal"}; + for (const PreparedSetupHoldCheck& check : prepared_timing_->checks) { + double data_delta = 0.0; + double clock_delta = 0.0; + float data_slew = 0.0f; + float clock_slew = 0.0f; + float candidate_check_margin = 0.0f; + if (!estimateInputTimingForCap( + check.data, + check.data.candidate_connected_input_caps[index], + data_delta, + data_slew) + || !estimateInputTimingForCap( + check.clock, + check.clock.candidate_connected_input_caps[index], + clock_delta, + clock_slew)) { + return {.reason = "sequential_missing_candidate_model"}; + } + + if (check.clock_check_slew_input) { + double unused_arrival_delta = 0.0; + const PreparedInputTiming& clock_check_slew_input + = *check.clock_check_slew_input; + if (!estimateInputTimingForCap( + clock_check_slew_input, + clock_check_slew_input.candidate_connected_input_caps[index], + unused_arrival_delta, + clock_slew)) { + return {.reason = "sequential_missing_candidate_model"}; + } + } + + if (!checkMargin(check.candidate_check_models[index], + check, + data_slew, + clock_slew, + candidate_check_margin)) { + return {.reason = "sequential_missing_candidate_model"}; + } + + const double margin_delta = (static_cast(candidate_check_margin) + - check.baseline_check_margin) + * check.check_derate; + // Setup = clock - data - setup; hold = data - clock - hold. + // Signed Liberty constraints (including negative hold) keep the same signs. + const double slack_delta + = (check.setup ? clock_delta - data_delta : data_delta - clock_delta) + - margin_delta; + const double after = check.baseline_slack + slack_delta; + const double loss = std::max(0.0, -slack_delta); + const double weight = std::clamp( + 1.0 - std::min(check.baseline_slack, after) / kSideSlackWeightCutoff, + 0.0, + 1.0); + result.weighted_side_slack_loss = std::max( + result.weighted_side_slack_loss, static_cast(weight * loss)); + result.max_side_slack_loss + = std::max(result.max_side_slack_loss, static_cast(loss)); + if (after + < std::min(0.0, check.baseline_slack) - kSideSlackViolationTolerance) { + result.legal = false; + result.reason = check.setup ? "sequential_setup_violation" + : "sequential_hold_violation"; + } + } + + return result; +} + +} // namespace rsz diff --git a/src/rsz/src/SequentialTimingEstimator.hh b/src/rsz/src/SequentialTimingEstimator.hh new file mode 100644 index 00000000000..e2a4e4c4b37 --- /dev/null +++ b/src/rsz/src/SequentialTimingEstimator.hh @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#pragma once + +#include +#include + +namespace sta { +class LibertyCell; +} + +namespace rsz { +class Resizer; +struct Target; + +// Estimates setup/hold impact at a flip-flop's data and clock inputs when +// scoring a Q-path size or VT swap. Uses local load and Liberty-check deltas +// against current STA timing; post-ECO STA determines the actual result. +class SequentialTimingEstimator +{ + public: + struct Result + { + bool legal{false}; + // Maximum criticality-weighted side slack loss, in seconds; reduces gain. + float weighted_side_slack_loss{0.0f}; + // Maximum unweighted side slack loss, in seconds; for diagnostics. + float max_side_slack_loss{0.0f}; + // Static storage, so it outlives the estimator. + std::string_view reason; + }; + + // Capture the current flip-flop's setup/hold timing and equivalent-cell + // models for scoring Q-path size/VT swaps. Main thread only; non-flip-flop + // targets return null, and missing timing context makes estimates unscorable. + static std::shared_ptr prepare( + Resizer& resizer, + const Target& target); + + // Estimate a candidate flip-flop swap's effect on input-side setup and hold + // checks. Return legality and the side slack losses used to score the Q-path + // move; reject candidates that create or worsen a timing violation. + Result estimate(const sta::LibertyCell* candidate_cell) const; + + private: + struct PreparedFlipFlopTiming; + explicit SequentialTimingEstimator( + std::shared_ptr prepared_timing); + const std::shared_ptr prepared_timing_; +}; + +} // namespace rsz diff --git a/src/rsz/src/move/MoveCandidate.cc b/src/rsz/src/move/MoveCandidate.cc index c9308c820cd..96cf876e97c 100644 --- a/src/rsz/src/move/MoveCandidate.cc +++ b/src/rsz/src/move/MoveCandidate.cc @@ -4,6 +4,7 @@ #include "MoveCandidate.hh" #include "OptimizerTypes.hh" +#include "SequentialTimingEstimator.hh" #include "rsz/Resizer.hh" namespace rsz { @@ -22,4 +23,38 @@ MoveResult MoveCandidate::rejectedMove() const }; } +void MoveCandidate::chargeCellSwapSideCosts(const DelayEstimate& delay_est, + const sta::LibertyCell* replacement, + const float score_scale, + Estimate& estimate) const +{ + float adjusted_gain = delay_est.arrival_impr; + if (target().sequential_timing_estimator != nullptr) { + const SequentialTimingEstimator::Result side + = target().sequential_timing_estimator->estimate(replacement); + estimate.sequential_penalty = side.weighted_side_slack_loss; + estimate.sequential_side_loss = side.max_side_slack_loss; + adjusted_gain -= *estimate.sequential_penalty; + estimate.score = adjusted_gain * score_scale; + if (!side.legal || adjusted_gain <= 0.0f) { + estimate.legal = false; + estimate.reason = side.legal ? "sequential_non_improving" : side.reason; + estimate.tail_admissible = side.legal && delay_est.arrival_impr > 0.0f; + return; + } + } + + // Sibling paths on the fanin net pay for the larger input capacitance. + if (delay_est.side_paths_evaluated > 0) { + estimate.side_loss = delay_est.side_loss; + adjusted_gain -= delay_est.side_loss; + estimate.score = adjusted_gain * score_scale; + if (adjusted_gain <= 0.0f) { + estimate.legal = false; + estimate.reason = "side_loss_non_improving"; + estimate.tail_admissible = delay_est.arrival_impr > 0.0f; + } + } +} + } // namespace rsz diff --git a/src/rsz/src/move/MoveCandidate.hh b/src/rsz/src/move/MoveCandidate.hh index 6d9dda85a95..98eccba316e 100644 --- a/src/rsz/src/move/MoveCandidate.hh +++ b/src/rsz/src/move/MoveCandidate.hh @@ -40,6 +40,14 @@ class MoveCandidate protected: MoveCandidate(Resizer& resizer, const Target& target); MoveResult rejectedMove() const; + // The side costs of swapping the target cell for `replacement`, charged + // against the own-path gain of delay_est: the flip-flop input-side checks, + // then the fanin side paths. Updates estimate's penalties and score + // (adjusted gain times score_scale) and rejects it once the gain is gone. + void chargeCellSwapSideCosts(const DelayEstimate& delay_est, + const sta::LibertyCell* replacement, + float score_scale, + Estimate& estimate) const; // === Candidate identity =================================================== Resizer& resizer_; diff --git a/src/rsz/src/move/MoveGenerator.cc b/src/rsz/src/move/MoveGenerator.cc index 32b63e6d988..1feedd3e212 100644 --- a/src/rsz/src/move/MoveGenerator.cc +++ b/src/rsz/src/move/MoveGenerator.cc @@ -51,4 +51,13 @@ bool MoveGenerator::weakerCellFirst(const sta::LibertyCell* lhs, > std::tie(rhs_drive_resistance, rhs_intrinsic); } +std::vector MoveGenerator::inputCapBudgets( + const Target& target) const +{ + if (target.input_cap_budgets.has_value()) { + return *target.input_cap_budgets; + } + return resizer_.inputCapBudgets(target.inst(resizer_)); +} + } // namespace rsz diff --git a/src/rsz/src/move/MoveGenerator.hh b/src/rsz/src/move/MoveGenerator.hh index 3d3bcae5933..9facafa44d8 100644 --- a/src/rsz/src/move/MoveGenerator.hh +++ b/src/rsz/src/move/MoveGenerator.hh @@ -109,6 +109,12 @@ class MoveGenerator const std::string& drvr_port_name, int lib_ap_index) const; + // === Shared fanin max-cap helper ========================================= + // The cell-swap generators' fanin max-cap headroom: the prepared + // kInputCapBudgetCache, else a fresh main-thread capture. + std::vector inputCapBudgets( + const Target& target) const; + // === Shared generator dependencies ======================================= Resizer& resizer_; MoveCommitter& committer_; diff --git a/src/rsz/src/move/SizeUpMtCandidate.cc b/src/rsz/src/move/SizeUpMtCandidate.cc index 8cd1178da1a..c3d777b82b6 100644 --- a/src/rsz/src/move/SizeUpMtCandidate.cc +++ b/src/rsz/src/move/SizeUpMtCandidate.cc @@ -3,8 +3,8 @@ #include "SizeUpMtCandidate.hh" +#include #include -#include #include "DelayEstimator.hh" #include "MoveCandidate.hh" @@ -21,16 +21,14 @@ using utl::RSZ; SizeUpMtCandidate::SizeUpMtCandidate(Resizer& resizer, const Target& target, - sta::Pin* drvr_pin, sta::Instance* inst, sta::LibertyCell* replacement, - const ArcDelayState& arc_delay) + float score_scale) : MoveCandidate(resizer, target), - drvr_pin_(drvr_pin), inst_(inst), current_cell_(resizer.network()->libertyCell(inst)), replacement_(replacement), - arc_delay_(arc_delay) + score_scale_(score_scale) { } @@ -38,9 +36,27 @@ Estimate SizeUpMtCandidate::estimate() { // Score one size-up replacement from immutable inputs prepared before the // worker pool runs candidate estimation. - const DelayEstimate delay_est - = DelayEstimator::estimate(arc_delay_, replacement_); - return {.legal = delay_est.legal, .score = delay_est.arrival_impr}; + const DelayEstimate delay_est = DelayEstimator::estimate( + target().estimator_context.value(), replacement_); + Estimate estimate{.legal = delay_est.legal, + .raw_gain = delay_est.arrival_impr, + .reason = failReasonName(delay_est.reason)}; + estimate.score = delay_est.arrival_impr * score_scale_; + if (!delay_est.legal) { + // The opposite-transition reject carries the target gain in its score. + return { + .legal = false, + .score = estimate.score, + .raw_gain = delay_est.reason == FailReason::kEstimateNonImproving + ? std::optional(delay_est.arrival_impr) + : std::nullopt, + .reason = failReasonName(delay_est.reason), + .tail_admissible = delay_est.reason == FailReason::kDualRfNonImproving + && estimate.score > 0.0f}; + } + + chargeCellSwapSideCosts(delay_est, replacement_, score_scale_, estimate); + return estimate; } MoveResult SizeUpMtCandidate::apply() @@ -102,7 +118,7 @@ MoveResult SizeUpMtCandidate::apply() std::string SizeUpMtCandidate::logName() const { - return drvr_pin_ != nullptr ? resizer_.network()->pathName(drvr_pin_) : ""; + return resizer_.network()->pathName(target().driver_pin); } } // namespace rsz diff --git a/src/rsz/src/move/SizeUpMtCandidate.hh b/src/rsz/src/move/SizeUpMtCandidate.hh index b038de87e03..4371e6031b5 100644 --- a/src/rsz/src/move/SizeUpMtCandidate.hh +++ b/src/rsz/src/move/SizeUpMtCandidate.hh @@ -4,6 +4,7 @@ #pragma once #include +#include #include "DelayEstimator.hh" #include "MoveCandidate.hh" @@ -18,10 +19,10 @@ class Instance; namespace rsz { -// MT-safe size-up candidate scored from a prepared ArcDelayState. +// MT-safe size-up candidate scored from a prepared EstimatorContext. // // Unlike the legacy SizeUpCandidate which queries STA on the fly, this -// variant receives a state of the timing arc (ArcDelayState) built on +// variant receives a state of the timing arc (EstimatorContext) built on // the main thread and stored in Target. estimate() evaluates the Liberty table // model purely from that snapshot (thread-safe; no STA mutation), so MT // policies can run it on worker threads. apply() still @@ -30,12 +31,12 @@ class SizeUpMtCandidate : public MoveCandidate { public: // === Construction ========================================================= + // Scores from target.estimator_context, which must be prepared. SizeUpMtCandidate(Resizer& resizer, const Target& target, - sta::Pin* drvr_pin, sta::Instance* inst, sta::LibertyCell* replacement, - const ArcDelayState& arc_delay); + float score_scale); // === MoveCandidate API ==================================================== Estimate estimate() override; @@ -46,11 +47,10 @@ class SizeUpMtCandidate : public MoveCandidate std::string logName() const; // === Candidate state ====================================================== - sta::Pin* drvr_pin_{nullptr}; sta::Instance* inst_{nullptr}; sta::LibertyCell* current_cell_{nullptr}; sta::LibertyCell* replacement_{nullptr}; - ArcDelayState arc_delay_; + const float score_scale_; }; } // namespace rsz diff --git a/src/rsz/src/move/SizeUpMtGenerator.cc b/src/rsz/src/move/SizeUpMtGenerator.cc index a954acf0390..5f3959d7091 100644 --- a/src/rsz/src/move/SizeUpMtGenerator.cc +++ b/src/rsz/src/move/SizeUpMtGenerator.cc @@ -4,12 +4,14 @@ #include "SizeUpMtGenerator.hh" #include +#include #include #include #include "MoveCandidate.hh" #include "MoveGenerator.hh" #include "OptimizerTypes.hh" +#include "RepairSetupProfiler.hh" #include "SizeUpMtCandidate.hh" #include "db_sta/dbNetwork.hh" #include "odb/db.h" @@ -31,65 +33,92 @@ bool SizeUpMtGenerator::isApplicable(const Target& target) const sta::Instance* target_inst = target.inst(resizer_); return MoveGenerator::isApplicable(target) && target_inst != nullptr && !resizer_.dontTouch(target_inst) && !resizer_.isFixed(target_inst) - && target.isPrepared(kArcDelayStateCache); + && target.isPrepared(kEstimatorContextCache); } std::vector> SizeUpMtGenerator::generate( const Target& target) { std::vector> candidates; - if (!isApplicable(target)) { - return candidates; - } - sta::Instance* target_inst = target.inst(resizer_); - const ArcDelayState& arc_delay = target.arc_delay.value(); - const SelectedArc& target_arc = arc_delay.target().arc; - const std::vector replacements = findSizeUpOptions( + const EstimatorContext& estimator_context = target.estimator_context.value(); + const SelectedArc& target_arc = estimator_context.target().arc; + const float score_scale = policy_config_.size_up_mt_score_scale; + const SizeUpOptions& options = findSizeUpOptions( target_arc.outputPort(), target_arc.scene, target_arc.min_max); + if (options.dual_rf_rejects > 0) { + RepairSetupProfiler::event("dual_rf_drive_guard_rejects", + options.dual_rf_rejects, + moveName(MoveType::kSizeUp)); + } + + const std::vector& replacements = options.cells; + + // Fanin max-cap headroom is instance-level, captured once per target. + const std::vector budgets = inputCapBudgets(target); candidates.reserve(replacements.size()); for (sta::LibertyCell* replacement : replacements) { - if (!resizer_.replacementPreservesMaxCap(target_inst, replacement)) { + if (!resizer_.replacementFitsInputCapBudgets(budgets, replacement)) { + RepairSetupProfiler::event( + "max_cap_rejects", 1, moveName(MoveType::kSizeUp)); continue; } - candidates.push_back(std::make_unique(resizer_, - target, - target.driver_pin, - target_inst, - replacement, - arc_delay)); + candidates.push_back(std::make_unique( + resizer_, target, target_inst, replacement, score_scale)); } return candidates; } -std::vector SizeUpMtGenerator::findSizeUpOptions( +const SizeUpMtGenerator::SizeUpOptions& SizeUpMtGenerator::findSizeUpOptions( const sta::LibertyPort* drvr_port, const sta::Scene* scene, const sta::MinMax* min_max) const { - std::vector replacements; + const SizeUpOptionKey key{drvr_port, scene, min_max}; + const std::lock_guard lock(size_up_options_mutex_); + std::map::iterator cached + = size_up_options_.find(key); + if (cached == size_up_options_.end()) { + cached = size_up_options_ + .emplace(key, buildSizeUpOptions(drvr_port, scene, min_max)) + .first; + } + + return cached->second; +} + +SizeUpMtGenerator::SizeUpOptions SizeUpMtGenerator::buildSizeUpOptions( + const sta::LibertyPort* drvr_port, + const sta::Scene* scene, + const sta::MinMax* min_max) const +{ + SizeUpOptions options; + std::vector& replacements = options.cells; if (drvr_port == nullptr || scene == nullptr || min_max == nullptr) { - return replacements; + return options; } const int lib_ap = scene->libertyIndex(min_max); sta::LibertyCell* cell = drvr_port->libertyCell(); sta::LibertyCellSeq swappable_cells = resizer_.getSwappableCells(cell); if (swappable_cells.empty()) { - return replacements; + return options; } const std::string& drvr_port_name = drvr_port->name(); const sta::LibertyPort* scene_drvr_port = drvr_port->scenePort(lib_ap); if (scene_drvr_port == nullptr) { - return replacements; + return options; } const float drive_r = scene_drvr_port->driveResistance(); - // Keep every equivalent cell that does not weaken drive resistance and let - // candidate::estimate require a strict delay improvement. + // A cell more than size_up_mt_max_drive_ratio times stronger grows the input + // capacitance by about the same factor, so the jump is bounded. + const float max_ratio = policy_config_.size_up_mt_max_drive_ratio; + const bool limit_ratio = max_ratio > 0.0f && drive_r > 0.0f; + const float min_drive_r = limit_ratio ? drive_r / max_ratio : 0.0f; odb::dbMaster* current_master = resizer_.dbNetwork()->staToDb(cell); for (sta::LibertyCell* swappable : swappable_cells) { if (swappable == cell) { @@ -115,12 +144,20 @@ std::vector SizeUpMtGenerator::findSizeUpOptions( if (swappable_drvr == nullptr) { continue; } + if (policy_config_.dual_rf_drive_guard + && !driveResNoHigherOnBothRf(scene_drvr_port, swappable_drvr)) { + ++options.dual_rf_rejects; + continue; + } + // Keep every equivalent cell that does not weaken drive resistance and let + // candidate::estimate require a strict delay improvement. const float swappable_drive_r = swappable_drvr->driveResistance(); - if (swappable_drive_r <= drive_r) { + if (swappable_drive_r <= drive_r + && (!limit_ratio || swappable_drive_r >= min_drive_r)) { replacements.push_back(swappable); } } - return replacements; + return options; } } // namespace rsz diff --git a/src/rsz/src/move/SizeUpMtGenerator.hh b/src/rsz/src/move/SizeUpMtGenerator.hh index 28db2e57ae5..b3cbce6e8c6 100644 --- a/src/rsz/src/move/SizeUpMtGenerator.hh +++ b/src/rsz/src/move/SizeUpMtGenerator.hh @@ -3,7 +3,10 @@ #pragma once +#include #include +#include +#include #include #include "MoveGenerator.hh" @@ -22,11 +25,11 @@ class Instance; namespace rsz { // MT-safe generator that enumerates same-VT cells that do not weaken output -// drive for one target using a prepared ArcDelayState. +// drive for one target using a prepared EstimatorContext. // -// prepareRequirements() requests kArcDelayState so worker threads can read the -// prepared timing snapshot without touching shared STA state. Produces multiple -// SizeUpMtCandidate instances so +// prepareRequirements() asks for kEstimatorContextCache and +// kInputCapBudgetCache, so generate() runs on worker threads without live +// STA. Produces multiple SizeUpMtCandidate instances so // the policy can evaluate them in parallel and pick the best. class SizeUpMtGenerator : public MoveGenerator { @@ -38,7 +41,7 @@ class SizeUpMtGenerator : public MoveGenerator MoveType type() const override { return MoveType::kSizeUp; } PrepareCacheMask prepareRequirements() const override { - return kArcDelayStateCache; + return kEstimatorContextCache | kInputCapBudgetCache; } bool isApplicable(const Target& target) const override; std::vector> generate( @@ -46,10 +49,27 @@ class SizeUpMtGenerator : public MoveGenerator private: // === Candidate-cell selection ============================================ - std::vector findSizeUpOptions( - const sta::LibertyPort* drvr_port, - const sta::Scene* scene, - const sta::MinMax* min_max) const; + // Replacement cells for one driver port and analysis view. The library and + // dont_use set are fixed for a run, so workers share a cache under a mutex. + // A cache hit replays the dual-RF rejects to the profiler. + struct SizeUpOptions + { + std::vector cells; + int dual_rf_rejects{0}; + }; + + using SizeUpOptionKey = std:: + tuple; + + const SizeUpOptions& findSizeUpOptions(const sta::LibertyPort* drvr_port, + const sta::Scene* scene, + const sta::MinMax* min_max) const; + SizeUpOptions buildSizeUpOptions(const sta::LibertyPort* drvr_port, + const sta::Scene* scene, + const sta::MinMax* min_max) const; + + mutable std::map size_up_options_; + mutable std::mutex size_up_options_mutex_; }; } // namespace rsz diff --git a/src/rsz/src/move/VtSwapMtCandidate.cc b/src/rsz/src/move/VtSwapMtCandidate.cc index 7cbf57304d6..6e1a4ff750c 100644 --- a/src/rsz/src/move/VtSwapMtCandidate.cc +++ b/src/rsz/src/move/VtSwapMtCandidate.cc @@ -3,7 +3,9 @@ #include "VtSwapMtCandidate.hh" +#include #include +#include #include #include "DelayEstimator.hh" @@ -21,14 +23,14 @@ using utl::RSZ; namespace { -Estimate makeRejectedEstimate(const float score = 0.0f) +Estimate makeRejectedEstimate(const std::string_view reason, + const std::optional raw_gain + = std::nullopt) { - return {.legal = false, .score = score}; -} - -Estimate makeAcceptedEstimate(const float score) -{ - return {.legal = true, .score = score}; + return {.legal = false, + .score = raw_gain.value_or(0.0f), + .raw_gain = raw_gain, + .reason = reason}; } const char* applyLogFormat(const bool accepted) @@ -41,39 +43,39 @@ const char* applyLogFormat(const bool accepted) VtSwapMtCandidate::VtSwapMtCandidate(Resizer& resizer, const Target& target, - sta::Pin* driver_pin, sta::Instance* inst, - sta::LibertyCell* current_cell, - sta::LibertyCell* candidate_cell, - const ArcDelayState& arc_delay) + sta::LibertyCell* candidate_cell) : MoveCandidate(resizer, target), - driver_pin_(driver_pin), inst_(inst), - current_cell_(current_cell), - candidate_cell_(candidate_cell), - arc_delay_(arc_delay) + current_cell_(resizer.network()->libertyCell(inst)), + candidate_cell_(candidate_cell) { } Estimate VtSwapMtCandidate::estimate() { // Reuse the pre-built local delay context to score the candidate without - // mutating the design. - if (!resizer_.replacementPreservesMaxCap(inst_, candidate_cell_)) { - return makeRejectedEstimate(); - } - - const DelayEstimate delay_est - = DelayEstimator::estimate(arc_delay_, candidate_cell_); + // mutating the design. The generator already applied the max-cap check. + const DelayEstimate delay_est = DelayEstimator::estimate( + target().estimator_context.value(), candidate_cell_); if (!delay_est.legal) { // Preserve the computed score for non-improving swaps so policy ranking can // still compare rejected candidates consistently. if (delay_est.reason == FailReason::kEstimateNonImproving) { - return makeRejectedEstimate(delay_est.arrival_impr); + return makeRejectedEstimate(failReasonName(delay_est.reason), + delay_est.arrival_impr); } - return makeRejectedEstimate(); + return makeRejectedEstimate(failReasonName(delay_est.reason)); } - return makeAcceptedEstimate(delay_est.arrival_impr); + + Estimate estimate{.legal = true, + .raw_gain = delay_est.arrival_impr, + .reason = failReasonName(FailReason::kEstimateLegal)}; + estimate.score = delay_est.arrival_impr; + + // VtSwapMt keeps score scale 1; the side costs come off the raw gain. + chargeCellSwapSideCosts(delay_est, candidate_cell_, 1.0f, estimate); + return estimate; } MoveResult VtSwapMtCandidate::apply() @@ -86,7 +88,7 @@ MoveResult VtSwapMtCandidate::apply() "opt_moves", 1, applyLogFormat(accepted), - getDrvPinName(), + resizer_.network()->pathName(target().driver_pin), current_cell_->name(), candidate_cell_->name()); return { @@ -98,9 +100,4 @@ MoveResult VtSwapMtCandidate::apply() }; } -std::string VtSwapMtCandidate::getDrvPinName() const -{ - return resizer_.network()->pathName(driver_pin_); -} - } // namespace rsz diff --git a/src/rsz/src/move/VtSwapMtCandidate.hh b/src/rsz/src/move/VtSwapMtCandidate.hh index 774960f5d6c..06b1ba67d5c 100644 --- a/src/rsz/src/move/VtSwapMtCandidate.hh +++ b/src/rsz/src/move/VtSwapMtCandidate.hh @@ -4,6 +4,7 @@ #pragma once #include +#include #include "DelayEstimator.hh" #include "MoveCandidate.hh" @@ -17,10 +18,10 @@ class LibertyCell; namespace rsz { -// MT-safe VT-swap candidate scored from a prepared ArcDelayState. +// MT-safe VT-swap candidate scored from a prepared EstimatorContext. // // Same concept as VtSwapCandidate but estimate() reads from a prepared -// ArcDelayState (built on the main thread and stored on Target) and +// EstimatorContext (built on the main thread and stored on Target) and // evaluates the Liberty table model locally, making it thread-safe for // parallel evaluation on worker threads. apply() still runs on the main // thread via Resizer::replaceCell. @@ -28,13 +29,11 @@ class VtSwapMtCandidate : public MoveCandidate { public: // === Construction ========================================================= + // Scores from target.estimator_context, which must be prepared. VtSwapMtCandidate(Resizer& resizer, const Target& target, - sta::Pin* driver_pin, sta::Instance* inst, - sta::LibertyCell* current_cell, - sta::LibertyCell* candidate_cell, - const ArcDelayState& arc_delay); + sta::LibertyCell* candidate_cell); // === MoveCandidate API ==================================================== Estimate estimate() override; @@ -42,14 +41,10 @@ class VtSwapMtCandidate : public MoveCandidate MoveType type() const override { return MoveType::kVtSwap; } private: - std::string getDrvPinName() const; - // === Candidate state ====================================================== - sta::Pin* driver_pin_{nullptr}; sta::Instance* inst_{nullptr}; sta::LibertyCell* current_cell_{nullptr}; sta::LibertyCell* candidate_cell_{nullptr}; - ArcDelayState arc_delay_; }; } // namespace rsz diff --git a/src/rsz/src/move/VtSwapMtGenerator.cc b/src/rsz/src/move/VtSwapMtGenerator.cc index 9d07b5e8b6b..a8cdaddf3f0 100644 --- a/src/rsz/src/move/VtSwapMtGenerator.cc +++ b/src/rsz/src/move/VtSwapMtGenerator.cc @@ -3,13 +3,13 @@ #include "VtSwapMtGenerator.hh" -#include #include #include #include "MoveCandidate.hh" #include "MoveGenerator.hh" #include "OptimizerTypes.hh" +#include "RepairSetupProfiler.hh" #include "VtSwapMtCandidate.hh" #include "db_sta/dbNetwork.hh" #include "rsz/Resizer.hh" @@ -28,24 +28,46 @@ std::vector> VtSwapMtGenerator::generate( const Target& target) { std::vector> candidates; - if (!target.isPrepared(kArcDelayStateCache)) { + if (!target.isPrepared(kEstimatorContextCache)) { return candidates; } - const ArcDelayState& arc_delay = target.arc_delay.value(); - sta::LibertyCell* current_cell = arc_delay.target().arc.currentCell(); + const EstimatorContext& estimator_context = target.estimator_context.value(); + const SelectedArc& target_arc = estimator_context.target().arc; + sta::LibertyCell* current_cell = estimator_context.target().arc.currentCell(); const std::vector candidate_cells = selectCandidateCells(current_cell); + + // Fanin max-cap headroom is instance-level, captured once per target. + const std::vector budgets = inputCapBudgets(target); candidates.reserve(candidate_cells.size()); for (sta::LibertyCell* candidate_cell : candidate_cells) { - candidates.push_back( - std::make_unique(resizer_, - target, - target.driver_pin, - target.inst(resizer_), - current_cell, - candidate_cell, - arc_delay)); + if (policy_config_.dual_rf_drive_guard) { + sta::LibertyCell* candidate_scene + = candidate_cell->sceneCell(target_arc.scene, target_arc.min_max); + sta::LibertyPort* candidate_output + = candidate_scene != nullptr ? candidate_scene->findLibertyPort( + target_arc.outputPort()->name()) + : nullptr; + if (candidate_output != nullptr + && !driveResNoHigherOnBothRf(target_arc.outputPort(), + candidate_output)) { + RepairSetupProfiler::event( + "dual_rf_drive_guard_rejects", 1, moveName(MoveType::kVtSwap)); + continue; + } + } + + // The same decision as replacementPreservesMaxCap(), from the captured + // headroom. + if (!resizer_.replacementFitsInputCapBudgets(budgets, candidate_cell)) { + RepairSetupProfiler::event( + "max_cap_rejects", 1, moveName(MoveType::kVtSwap)); + continue; + } + + candidates.push_back(std::make_unique( + resizer_, target, target.inst(resizer_), candidate_cell)); } return candidates; } @@ -55,7 +77,7 @@ bool VtSwapMtGenerator::isApplicable(const Target& target) const // Screen out targets that cannot legally change VT in the current library // set. if (!MoveGenerator::isApplicable(target) - || !target.isPrepared(kArcDelayStateCache)) { + || !target.isPrepared(kEstimatorContextCache)) { return false; } diff --git a/src/rsz/src/move/VtSwapMtGenerator.hh b/src/rsz/src/move/VtSwapMtGenerator.hh index 0869b603766..c9787ac8658 100644 --- a/src/rsz/src/move/VtSwapMtGenerator.hh +++ b/src/rsz/src/move/VtSwapMtGenerator.hh @@ -20,8 +20,9 @@ namespace rsz { // MT-safe generator that enumerates VT-equivalent cells and produces // multiple VtSwapMtCandidate instances for parallel evaluation. // -// prepareRequirements() requests kArcDelayState so worker threads can read the -// prepared timing snapshot without touching shared STA state. +// prepareRequirements() asks for kEstimatorContextCache and +// kInputCapBudgetCache, so generate() runs on worker threads without live +// STA. class VtSwapMtGenerator : public MoveGenerator { public: @@ -32,7 +33,7 @@ class VtSwapMtGenerator : public MoveGenerator MoveType type() const override { return MoveType::kVtSwap; } PrepareCacheMask prepareRequirements() const override { - return kArcDelayStateCache; + return kEstimatorContextCache | kInputCapBudgetCache; } bool isApplicable(const Target& target) const override; std::vector> generate( diff --git a/src/rsz/src/policy/OptimizationPolicy.cc b/src/rsz/src/policy/OptimizationPolicy.cc index 76aace85370..105da6a050b 100644 --- a/src/rsz/src/policy/OptimizationPolicy.cc +++ b/src/rsz/src/policy/OptimizationPolicy.cc @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include "RepairSetupProfiler.hh" #include "RepairTargetCollector.hh" #include "RerouteGenerator.hh" +#include "SequentialTimingEstimator.hh" #include "SizeDownFanoutGenerator.hh" #include "SizeUpGenerator.hh" #include "SizeUpMatchGenerator.hh" @@ -92,6 +94,7 @@ bool OptimizationPolicy::start() estimate_parasitics_ = resizer_.estimateParasitics(); max_ = resizer_.maxAnalysisMode(); resetRun(); + estimator_cache_ = DelayEstimatorCache{}; // Start every phase with the progress table's area cache invalid. The // cache is keyed on MoveCommitter::netlistEdits(), which only counts // edits made through the committer; GlobalSizingPolicy replaces cells @@ -283,7 +286,7 @@ PrepareCacheMask OptimizationPolicy::accumulatePrepareRequirements() const } prepare_mask |= generator->prepareRequirements(); } - return prepare_mask; + return prepare_mask & ~kGenerationInputCaches; } MoveGenerator* OptimizationPolicy::findGenerator(const MoveType type) const @@ -338,24 +341,74 @@ bool OptimizationPolicy::generatorEnabled(const MoveType) const void OptimizationPolicy::prepareTarget(Target& target, const PrepareCacheMask mask) const { - if ((mask & kArcDelayStateCache) != 0) { - prepareArcDelayState(target); + if ((mask & kEstimatorContextCache) != 0) { + prepareEstimatorContext(target); + } + + // Generation inputs: each Mt generator's live STA reads, so that its + // generate() runs on a worker thread. + if ((mask & kGenerationInputCaches) == 0) { + return; + } + if ((mask & kInputCapBudgetCache) != 0) { + prepareInputCapBudgets(target); } } -void OptimizationPolicy::prepareArcDelayState(Target& target) const +void OptimizationPolicy::prepareEstimatorContext(Target& target) const { - if (target.isPrepared(kArcDelayStateCache) || !target.canBePathDriver()) { + if (target.isPrepared(kEstimatorContextCache) || !target.canBePathDriver()) { return; } FailReason fail_reason = FailReason::kNone; - target.arc_delay - = DelayEstimator::buildContext(resizer_, - target, - policy_config_.delay_estimation_levels, - &fail_reason, - policy_config_.sta_slew_bias); + const FaninSideLoadOptions fanin_side_loads{ + .critical_load_pins = &target_collector_->critPathLoadPins(), + .off_path_weight = policy_config_.fanin_side_offpath_weight}; + { + const ProfileScope profile("prepare_context"); + target.estimator_context = DelayEstimator::buildContext( + resizer_, + target, + policy_config_.delay_estimation_levels, + &fail_reason, + policy_config_.sta_slew_bias, + /* include_latch_fanin */ true, + &estimator_cache_, + policy_config_.fanin_side_loss ? &fanin_side_loads : nullptr, + policy_config_.dual_rf_eval); + } + + if ((policy_config_.fanin_side_loss || policy_config_.dual_rf_eval) + && target.estimator_context.has_value()) { + // Side paths may not be pushed below the current global worst slack. + target.estimator_context->side_slack_floor + = sta::delayAsFloat(sta_->worstSlack(max_)); + target.estimator_context->side_floor_margin + = policy_config_.side_floor_margin; + target.estimator_context->fanin_side_loss_weight + = policy_config_.fanin_side_loss_weight; + } + + if (target.estimator_context.has_value()) { + target.sequential_timing_estimator + = SequentialTimingEstimator::prepare(resizer_, target); + if (target.sequential_timing_estimator != nullptr) { + RepairSetupProfiler::event("sequential_targets_prepared"); + } + } + + if (!target.estimator_context.has_value() && RepairSetupProfiler::enabled()) { + RepairSetupProfiler::event(std::string("prepare_reject_") + + failReasonName(fail_reason)); + } +} + +void OptimizationPolicy::prepareInputCapBudgets(Target& target) const +{ + if (!target.input_cap_budgets.has_value()) { + target.input_cap_budgets = resizer_.inputCapBudgets(target.inst(resizer_)); + } } void OptimizationPolicy::prewarmTargetLibertyCaches( diff --git a/src/rsz/src/policy/OptimizationPolicy.hh b/src/rsz/src/policy/OptimizationPolicy.hh index 0019ba1783d..539b23ead37 100644 --- a/src/rsz/src/policy/OptimizationPolicy.hh +++ b/src/rsz/src/policy/OptimizationPolicy.hh @@ -6,6 +6,7 @@ #include #include +#include "DelayEstimator.hh" #include "OptimizerTypes.hh" #include "ProgressReporter.hh" #include "rsz/Resizer.hh" @@ -89,6 +90,8 @@ class OptimizationPolicy virtual void buildMoveGenerators(const std::vector& move_types, const GeneratorContext& context); + // The enabled generators' target-level caches; the generation inputs are + // left to a policy that prepares them per generator. PrepareCacheMask accumulatePrepareRequirements() const; MoveGenerator* findGenerator(MoveType type) const; @@ -106,7 +109,9 @@ class OptimizationPolicy void prepareTarget(Target& target, PrepareCacheMask mask) const; // Build the immutable arc-delay snapshot consumed by MT-safe candidates. - void prepareArcDelayState(Target& target) const; + void prepareEstimatorContext(Target& target) const; + // Capture the target instance's fanin max-cap headroom. + void prepareInputCapBudgets(Target& target) const; // Prewarm Liberty-cell caches used by MT generator candidate selection. void prewarmTargetLibertyCaches(const std::vector& targets, @@ -162,6 +167,9 @@ class OptimizationPolicy GeneratorVector move_generators_; std::vector move_sequence_; std::unique_ptr thread_pool_; + // Main-thread caches; library configuration stays fixed until the next + // start(). + mutable DelayEstimatorCache estimator_cache_; bool converged_{false}; bool result_{false}; bool is_experimental{false}; diff --git a/src/rsz/src/policy/SetupCritVtSwapPolicy.cc b/src/rsz/src/policy/SetupCritVtSwapPolicy.cc index 5591ebe040c..f9584c194df 100644 --- a/src/rsz/src/policy/SetupCritVtSwapPolicy.cc +++ b/src/rsz/src/policy/SetupCritVtSwapPolicy.cc @@ -55,6 +55,16 @@ void SetupCritVtSwapPolicy::iterate() return; } + // The timing the sweep starts from, which no earlier phase may have sampled. + if (RepairSetupProfiler::enabled()) { + const ProfileScope collection("qor_collection"); + sta::Slack wns; + sta::Vertex* worst_vertex; + target_collector_->worstSlack(wns, worst_vertex); + RepairSetupProfiler::qor( + "phase_before", wns, target_collector_->totalNegativeSlack()); + } + const ProfileIteration iteration("crit_instance_sweep"); committer_.capturePrePhaseSlack(); if (swapVTCritCells(num_viols)) { diff --git a/src/rsz/test/BUILD b/src/rsz/test/BUILD index e561c9e5b99..20a0dbd0092 100644 --- a/src/rsz/test/BUILD +++ b/src/rsz/test/BUILD @@ -608,11 +608,12 @@ cc_test( cc_test( name = "TestResizerMt", srcs = [ + "cpp/TestCellSwapMt.cc", + "cpp/TestDelayEstimatorMt.cc", "cpp/TestRepairSetupProfiler.cc", "cpp/TestResizerMt.cc", "cpp/TestResizerMtFixture.hh", ], - args = ["--gtest_filter=TestResizerMtThreadPool.*"], data = [ "Nangate45/Nangate45.lef", "Nangate45/Nangate45_lvt.lef", diff --git a/src/rsz/test/cpp/CMakeLists.txt b/src/rsz/test/cpp/CMakeLists.txt index 6bf111c748b..e360667a2d5 100644 --- a/src/rsz/test/cpp/CMakeLists.txt +++ b/src/rsz/test/cpp/CMakeLists.txt @@ -175,6 +175,8 @@ add_dependencies(build_and_test TestNestedJournal ) add_executable(TestResizerMt + TestCellSwapMt.cc + TestDelayEstimatorMt.cc TestRepairSetupProfiler.cc TestResizerMt.cc ) diff --git a/src/rsz/test/cpp/TestCellSwapMt.cc b/src/rsz/test/cpp/TestCellSwapMt.cc new file mode 100644 index 00000000000..dfe8749cd1c --- /dev/null +++ b/src/rsz/test/cpp/TestCellSwapMt.cc @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +// VtSwapMt and SizeUpMt moves. + +#include "TestResizerMtFixture.hh" + +namespace rsz { + +TEST_F(TestResizerMt, SizeUpMtCandidateAppliesScoreScale) +{ + Resizer& resizer = resizer_; + resizer.runRepairSetupPreamble(); + Target target = makeTarget("out0", "target", "Z"); + + FailReason fail_reason = FailReason::kNone; + const std::optional estimator_context + = DelayEstimator::buildContext(resizer, target, 0, &fail_reason); + ASSERT_TRUE(estimator_context.has_value()) << failReasonName(fail_reason); + if (!estimator_context.has_value()) { + return; + } + target.estimator_context = estimator_context; + + sta::Instance* target_inst = target.inst(resizer); + ASSERT_NE(target_inst, nullptr); + + sta::LibertyCell* replacement_cell + = sta_->network()->findLibertyCell("BUF_X16"); + ASSERT_NE(replacement_cell, nullptr); + + const DelayEstimate raw_estimate + = DelayEstimator::estimate(estimator_context.value(), replacement_cell); + ASSERT_TRUE(raw_estimate.legal); + + SizeUpMtCandidate default_scale_candidate( + resizer, target, target_inst, replacement_cell, 1.0f); + const Estimate default_scale = default_scale_candidate.estimate(); + EXPECT_EQ(default_scale.legal, raw_estimate.legal); + EXPECT_FLOAT_EQ(default_scale.score, raw_estimate.arrival_impr); + + SizeUpMtCandidate half_scale_candidate( + resizer, target, target_inst, replacement_cell, 0.5f); + const Estimate half_scale = half_scale_candidate.estimate(); + EXPECT_EQ(half_scale.legal, raw_estimate.legal); + EXPECT_FLOAT_EQ(half_scale.score, raw_estimate.arrival_impr * 0.5f); +} + +TEST_F(TestResizerMt, DualRfDriveGuardRequiresBothTransitions) +{ + // A stronger buffer dominates on both transitions, a weaker one on neither, + // and a port dominates itself. + sta::LibertyPort* weak + = sta_->network()->findLibertyCell("BUF_X1")->findLibertyPort("Z"); + sta::LibertyPort* strong + = sta_->network()->findLibertyCell("BUF_X16")->findLibertyPort("Z"); + EXPECT_TRUE(driveResNoHigherOnBothRf(weak, strong)); + EXPECT_FALSE(driveResNoHigherOnBothRf(strong, weak)); + EXPECT_TRUE(driveResNoHigherOnBothRf(weak, weak)); +} + +TEST_F(TestResizerMt, SizeUpMtGeneratorCachesLibraryOptionsPerDriverPort) +{ + resizer_.runRepairSetupPreamble(); + MoveCommitter committer(resizer_); + OptimizerRunConfig run_config; + OptimizationPolicyConfig policy_config; + SizeUpMtGenerator generator( + makeGeneratorContext(resizer_, committer, run_config, policy_config)); + + Target target = makeTarget("out0", "target", "Z"); + target.estimator_context = DelayEstimator::buildContext(resizer_, target, 0); + ASSERT_TRUE(target.estimator_context.has_value()); + const SelectedArc& arc = target.estimator_context->target().arc; + + const SizeUpMtGenerator::SizeUpOptions& first + = generator.findSizeUpOptions(arc.outputPort(), arc.scene, arc.min_max); + const SizeUpMtGenerator::SizeUpOptions& second + = generator.findSizeUpOptions(arc.outputPort(), arc.scene, arc.min_max); + EXPECT_EQ(&first, &second); + EXPECT_EQ(generator.size_up_options_.size(), 1u); + EXPECT_FALSE(first.cells.empty()); + EXPECT_EQ( + first.cells, + generator.buildSizeUpOptions(arc.outputPort(), arc.scene, arc.min_max) + .cells); + + // Generation consumes the cached options without changing the candidates. + const CandidateVector candidates = generator.generate(target); + EXPECT_EQ(candidates.size(), first.cells.size()); + EXPECT_EQ(generator.size_up_options_.size(), 1u); +} + +} // namespace rsz diff --git a/src/rsz/test/cpp/TestDelayEstimatorMt.cc b/src/rsz/test/cpp/TestDelayEstimatorMt.cc new file mode 100644 index 00000000000..7a8b7103abc --- /dev/null +++ b/src/rsz/test/cpp/TestDelayEstimatorMt.cc @@ -0,0 +1,723 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +// Delay estimator: fanin side loads, sequential timing and the opposite +// transition. + +#include "TestResizerMtFixture.hh" + +namespace rsz { + +TEST_F(TestResizerMt, FaninSideLoadsChargeSiblingSlackLoss) +{ + // out2 is the only negative endpoint; load1/load2 are the fanin-net + // siblings of the load0 target input. + prepareUniqueViolatingEndpoint(); + const Target target = makeTarget("out0", "load0", "Z"); + + FailReason fail_reason = FailReason::kNone; + const std::optional plain + = DelayEstimator::buildContext(resizer_, target, 1, &fail_reason); + ASSERT_TRUE(plain.has_value()) << failReasonName(fail_reason); + EXPECT_TRUE(plain->fanin_side_nets.empty()); + + // Only load2/A sits on a violating worst path; load1/A is off-path. + sta::Instance* load1 = db_network_->dbToSta(block_->findInst("load1")); + sta::Instance* load2 = db_network_->dbToSta(block_->findInst("load2")); + const sta::Pin* load1_pin = db_network_->findPin(load1, "A"); + const sta::Pin* load2_pin = db_network_->findPin(load2, "A"); + const std::unordered_set critical_pins{load2_pin}; + const FaninSideLoadOptions options{.critical_load_pins = &critical_pins, + .off_path_weight = 0.25f}; + std::optional context = DelayEstimator::buildContext( + resizer_, target, 1, &fail_reason, false, false, nullptr, &options); + ASSERT_TRUE(context.has_value()) << failReasonName(fail_reason); + ASSERT_EQ(context->target_stage_index, 1); + + // A buffer has one input: the selected-arc net with two sibling loads. + ASSERT_EQ(context->fanin_side_nets.size(), 1u); + const FaninSideNet& fanin_net = context->fanin_side_nets.front(); + EXPECT_TRUE(fanin_net.selected_arc); + EXPECT_EQ(fanin_net.input_port->name(), std::string("A")); + ASSERT_EQ(fanin_net.loads.size(), 2u); + + sta::Vertex* load1_input = sta_->graph()->pinLoadVertex(load1_pin); + sta::Vertex* load2_input = sta_->graph()->pinLoadVertex(load2_pin); + const float offpath_slack + = sta::delayAsFloat(sta_->slack(load1_input, resizer_.maxAnalysisMode())); + const float sibling_slack + = sta::delayAsFloat(sta_->slack(load2_input, resizer_.maxAnalysisMode())); + EXPECT_LT(sibling_slack, 0.0f); + EXPECT_GT(offpath_slack, 0.0f); + float critical_weight = -1.0f; + float offpath_weight = -1.0f; + for (const SideLoad& side : fanin_net.loads) { + if (side.slack == sibling_slack) { + critical_weight = side.weight; + } else if (side.slack == offpath_slack) { + offpath_weight = side.weight; + } + } + + EXPECT_FLOAT_EQ(critical_weight, 1.0f); + EXPECT_FLOAT_EQ(offpath_weight, 0.25f); + + sta::LibertyCell* bigger = sta_->network()->findLibertyCell("BUF_X32"); + ASSERT_NE(bigger, nullptr); + const DelayEstimate baseline = DelayEstimator::estimate(*plain, bigger); + EXPECT_EQ(baseline.side_paths_evaluated, 0); + EXPECT_FLOAT_EQ(baseline.side_loss, 0.0f); + + // The window gain is unchanged. The side loss, reported apart, is the fanin + // delay growth charged to the negative sibling only. + const DelayEstimate charged = DelayEstimator::estimate(*context, bigger); + EXPECT_EQ(charged.side_paths_evaluated, 2); + EXPECT_FLOAT_EQ(charged.arrival_impr, baseline.arrival_impr); + EXPECT_GT(charged.side_loss, 0.0f); + EXPECT_EQ(charged.legal, baseline.legal); + + // The weight scales the charged loss linearly. + context->fanin_side_loss_weight = 2.0f; + EXPECT_FLOAT_EQ(DelayEstimator::estimate(*context, bigger).side_loss, + 2.0f * charged.side_loss); + + // A sibling at the worst slack may lose at most the margin. With no margin, + // the candidate is rejected. + context->side_slack_floor = sibling_slack; + context->side_floor_margin = 0.0f; + const DelayEstimate rejected = DelayEstimator::estimate(*context, bigger); + EXPECT_FALSE(rejected.legal); + EXPECT_EQ(rejected.reason, FailReason::kSideSlackFloorViolation); + + // A margin covering the whole drop keeps the candidate legal. + context->side_floor_margin + = charged.side_loss / std::max(charged.arrival_impr, 1e-15f) + 1.0f; + EXPECT_EQ(DelayEstimator::estimate(*context, bigger).legal, baseline.legal); + context->side_floor_margin = 1.0f; + + // A smaller replacement lowers the fanin load and charges nothing. + sta::LibertyCell* smaller = sta_->network()->findLibertyCell("BUF_X4"); + ASSERT_NE(smaller, nullptr); + const DelayEstimate relieved = DelayEstimator::estimate(*context, smaller); + EXPECT_EQ(relieved.side_paths_evaluated, 0); + EXPECT_FLOAT_EQ(relieved.side_loss, 0.0f); + + // Without a critical pin set every sibling keeps full weight. + const FaninSideLoadOptions all_critical{}; + const std::optional unweighted + = DelayEstimator::buildContext(resizer_, + target, + 1, + &fail_reason, + false, + false, + nullptr, + &all_critical); + ASSERT_TRUE(unweighted.has_value()) << failReasonName(fail_reason); + ASSERT_EQ(unweighted->fanin_side_nets.size(), 1u); + for (const SideLoad& side : unweighted->fanin_side_nets.front().loads) { + EXPECT_FLOAT_EQ(side.weight, 1.0f); + } +} + +TEST_F(TestResizerMt, FaninSideLoadsCoverEveryTargetInput) +{ + // path_target is a NAND2: A1 is the selected arc (path_pre1 net), A2 comes + // from path_side_buf whose only load is the target itself. + resizer_.runRepairSetupPreamble(); + const Target target = makeTarget("path_out", "path_target", "ZN"); + FailReason fail_reason = FailReason::kNone; + const FaninSideLoadOptions options{}; + const std::optional context = DelayEstimator::buildContext( + resizer_, target, 1, &fail_reason, false, false, nullptr, &options); + ASSERT_TRUE(context.has_value()) << failReasonName(fail_reason); + ASSERT_EQ(context->target_stage_index, 1); + + // The selected-arc net has no sibling, so only the A2 net is recorded. + ASSERT_EQ(context->fanin_side_nets.size(), 1u); + const FaninSideNet& side_net = context->fanin_side_nets.front(); + EXPECT_FALSE(side_net.selected_arc); + EXPECT_EQ(side_net.input_port->name(), std::string("A2")); + EXPECT_GT(side_net.delay_per_cap, 0.0f); + ASSERT_EQ(side_net.loads.size(), 1u); + + // A larger NAND2 raises A2 capacitance, so the off-arc fanin driver slows: + // with the floor at the side load's slack, the candidate is rejected. + sta::LibertyCell* bigger = sta_->network()->findLibertyCell("NAND2_X4"); + ASSERT_NE(bigger, nullptr); + EstimatorContext floored = *context; + floored.side_slack_floor = side_net.loads.front().slack; + floored.side_floor_margin = 0.0f; + const DelayEstimate charged = DelayEstimator::estimate(floored, bigger); + EXPECT_EQ(charged.side_paths_evaluated, 1); + EXPECT_FALSE(charged.legal); + EXPECT_EQ(charged.reason, FailReason::kSideSlackFloorViolation); +} + +TEST_F(TestResizerMt, SequentialTimingOffPreservesCombinationalWindow) +{ + resizer_.runRepairSetupPreamble(); + const Target target = makeTarget("path_out", "path_target", "ZN"); + const sta::PathExpanded expanded(target.endpoint_path, sta_.get()); + for (const int levels : {0, 1, 2}) { + SCOPED_TRACE(levels); + const std::optional baseline + = DelayEstimator::buildContext(resizer_, target, levels); + const std::optional disabled + = DelayEstimator::buildContext( + resizer_, target, levels, nullptr, false, false); + const std::optional enabled + = DelayEstimator::buildContext( + resizer_, target, levels, nullptr, false, true); + const std::optional lower_default + = DelayEstimator::buildContext(resizer_, + target.inst(resizer_), + target.driver_pin, + expanded, + target.path_index, + target.scene, + target.minMax(resizer_), + levels); + const std::optional lower_disabled + = DelayEstimator::buildContext(resizer_, + target.inst(resizer_), + target.driver_pin, + expanded, + target.path_index, + target.scene, + target.minMax(resizer_), + levels, + nullptr, + false, + false); + + ASSERT_TRUE(baseline.has_value()); + ASSERT_TRUE(disabled.has_value()); + ASSERT_TRUE(enabled.has_value()); + ASSERT_TRUE(lower_default.has_value()); + ASSERT_TRUE(lower_disabled.has_value()); + expectSameDelayContext(*baseline, *disabled); + expectSameDelayContext(*baseline, *enabled); + expectSameDelayContext(*baseline, *lower_default); + expectSameDelayContext(*baseline, *lower_disabled); + sta::LibertyCell* candidate = db_network_->findLibertyCell("NAND2_X1_L"); + const DelayEstimate before = DelayEstimator::estimate(*baseline, candidate); + const DelayEstimate after = DelayEstimator::estimate(*disabled, candidate); + EXPECT_EQ(before.legal, after.legal); + EXPECT_EQ(before.reason, after.reason); + EXPECT_FLOAT_EQ(before.candidate_delay, after.candidate_delay); + EXPECT_FLOAT_EQ(before.arrival_impr, after.arrival_impr); + } +} + +TEST_F(TestResizerMtSequential, LatchWindowIncludesPhysicalDataFanin) +{ + prepareSequential("DLH_X1", "G"); + ASSERT_FALSE(HasFatalFailure()); + sta::Vertex* endpoint = findEndpointVertex("out0"); + ASSERT_NE(endpoint, nullptr); + sta::Pin* data_driver = db_network_->findPin("target/Z"); + sta::Pin* clock_driver = db_network_->findPin("dq_clock_driver/Z"); + odb::dbInst* latch = block_->findInst("dq_seq"); + ASSERT_EQ(latch->findITerm("D")->getNet(), + block_->findInst("target")->findITerm("Z")->getNet()); + ASSERT_NE(latch->findITerm("D")->getNet(), latch->findITerm("G")->getNet()); + sta::LibertyCell* candidate = db_network_->findLibertyCell("DLH_X2"); + ASSERT_NE(candidate, nullptr); + + // The worst slack paths read required times. + sta_->findRequireds(); + for (const sta::RiseFall* rf : sta::RiseFall::range()) { + SCOPED_TRACE(rf->index()); + const sta::Path* path + = sta_->vertexWorstSlackPath(endpoint, rf, sta::MinMax::max()); + ASSERT_NE(path, nullptr); + const sta::PathExpanded expanded(path, sta_.get()); + const sta::Path* data_path = latchDataPath(expanded); + ASSERT_NE(data_path, nullptr); + ASSERT_NE(data_path->prevPath(), nullptr); + ASSERT_EQ(data_path->prevPath()->pin(sta_.get()), data_driver); + const Target target = makePathDriverTarget(path, + expanded, + expanded.startIndex(), + path->slack(sta_.get()), + resizer_); + ASSERT_EQ(target.driver_pin, db_network_->findPin("dq_seq/Q")); + + for (const int levels : {0, 1, 2}) { + SCOPED_TRACE(levels); + const std::optional baseline + = DelayEstimator::buildContext(resizer_, target, levels); + const std::optional disabled + = DelayEstimator::buildContext( + resizer_, target, levels, nullptr, false, false); + + FailReason reason = FailReason::kNone; + const std::optional enabled + = DelayEstimator::buildContext( + resizer_, target, levels, &reason, false, true); + const std::optional lower_enabled + = DelayEstimator::buildContext(resizer_, + target.inst(resizer_), + target.driver_pin, + expanded, + target.path_index, + target.scene, + sta::MinMax::max(), + levels, + nullptr, + false, + true); + + ASSERT_TRUE(baseline.has_value()); + ASSERT_TRUE(disabled.has_value()); + ASSERT_TRUE(enabled.has_value()) << failReasonName(reason); + ASSERT_TRUE(lower_enabled.has_value()); + expectSameDelayContext(*baseline, *disabled); + expectSameDelayContext(*enabled, *lower_enabled); + EXPECT_EQ(disabled->target_stage_index, 0); + ASSERT_EQ(enabled->target_stage_index, 1); + ASSERT_EQ(enabled->path_stages.size(), disabled->path_stages.size() + 1); + const DelayStageState& fanin = enabled->path_stages.front(); + const DelayStageState& sequential = enabled->target(); + EXPECT_EQ(fanin.driver_pin, data_driver); + EXPECT_EQ(sequential.arc.inputPort()->name(), "D"); + EXPECT_EQ(fanin.arc.outputRiseFall(), sequential.arc.inputRiseFall()); + EXPECT_EQ(sequential.arc.outputRiseFall(), rf); + int data_driver_count = 0; + float total_delay = 0.0f; + for (const DelayStageState& stage : enabled->path_stages) { + data_driver_count += stage.driver_pin == data_driver; + EXPECT_NE(stage.driver_pin, clock_driver); + EXPECT_EQ(stage.arc.scene, target.scene); + EXPECT_EQ(stage.arc.min_max, sta::MinMax::max()); + total_delay += stage.current_delay; + } + + EXPECT_EQ(data_driver_count, 1); + EXPECT_FLOAT_EQ(enabled->current_total_delay, total_delay); + + const float input_cap_delta + = candidate->sceneCell(target.scene, sta::MinMax::max()) + ->findLibertyPort("D") + ->capacitance() + - sequential.arc.inputPort()->capacitance(); + ASSERT_GT(input_cap_delta, 0.0f); + std::vector trace; + const DelayEstimate charged + = DelayEstimator::estimate(*enabled, candidate, &trace); + const DelayEstimate uncharged + = DelayEstimator::estimate(*disabled, candidate); + + ASSERT_EQ(trace.size(), enabled->path_stages.size()); + EXPECT_FLOAT_EQ(trace.front().load_cap, fanin.load_cap + input_cap_delta); + EXPECT_GT(trace.front().stage_delay, fanin.current_delay); + EXPECT_LT(charged.arrival_impr, uncharged.arrival_impr); + } + } +} + +TEST_F(TestResizerMtSequential, LatchContextBuildAndEstimatePreserveLiveTiming) +{ + prepareSequential("DLH_X1", "G"); + ASSERT_FALSE(HasFatalFailure()); + const Target target = makeTarget("out0", "dq_seq", "Q"); + odb::dbInst* latch = block_->findInst("dq_seq"); + odb::dbMaster* master = latch->getMaster(); + odb::dbNet* data_net = latch->findITerm("D")->getNet(); + odb::dbNet* output_net = latch->findITerm("Q")->getNet(); + const sta::Slack slack = target.endpoint_path->slack(sta_.get()); + const sta::Arrival arrival = target.endpoint_path->arrival(); + const float load_cap = sta_->graphDelayCalc()->loadCap( + target.driver_pin, target.scene, sta::MinMax::max()); + const std::optional context + = DelayEstimator::buildContext(resizer_, target, 1, nullptr, false, true); + ASSERT_TRUE(context.has_value()); + const EstimatorContext captured = *context; + sta::LibertyCell* candidate = db_network_->findLibertyCell("DLH_X2"); + const DelayEstimate first = DelayEstimator::estimate(*context, candidate); + for (int repeat = 0; repeat < 3; ++repeat) { + const DelayEstimate repeated + = DelayEstimator::estimate(*context, candidate); + EXPECT_EQ(repeated.legal, first.legal); + EXPECT_FLOAT_EQ(repeated.arrival_impr, first.arrival_impr); + } + + expectSameDelayContext(*context, captured); + EXPECT_EQ(latch->getMaster(), master); + EXPECT_EQ(latch->findITerm("D")->getNet(), data_net); + EXPECT_EQ(latch->findITerm("Q")->getNet(), output_net); + EXPECT_FLOAT_EQ(target.endpoint_path->slack(sta_.get()), slack); + EXPECT_FLOAT_EQ(target.endpoint_path->arrival(), arrival); + EXPECT_FLOAT_EQ(sta_->graphDelayCalc()->loadCap( + target.driver_pin, target.scene, sta::MinMax::max()), + load_cap); + + EXPECT_NO_THROW(sta_->updateTiming(true)); + const Target refreshed = makeTarget("out0", "dq_seq", "Q"); + EXPECT_NEAR(refreshed.endpoint_path->slack(sta_.get()), slack, 1e-15); + EXPECT_NEAR(refreshed.endpoint_path->arrival(), arrival, 1e-15); + EXPECT_FLOAT_EQ(DelayEstimator::estimate(*context, candidate).arrival_impr, + first.arrival_impr); +} + +TEST_F(TestResizerMtSequential, FlipFlopSetupPenaltyUsesCapturedDataMargin) +{ + prepareFlipFlop("DFF_X1"); + ASSERT_FALSE(HasFatalFailure()); + sta::LibertyCell* current = db_network_->findLibertyCell("DFF_X1"); + sta::LibertyCell* candidate = db_network_->findLibertyCell("DFF_X2"); + ASSERT_NE(candidate, nullptr); + ASSERT_LT(candidate->findLibertyPort("D")->capacitance(), + current->findLibertyPort("D")->capacitance()); + + odb::dbInst* flip_flop = block_->findInst("dq_seq"); + odb::dbMaster* original = flip_flop->getMaster(); + odb::dbNet* data_net = flip_flop->findITerm("D")->getNet(); + odb::dbNet* clock_net = flip_flop->findITerm("CK")->getNet(); + sta::Vertex* data_vertex + = sta_->graph()->pinLoadVertex(db_network_->findPin("dq_seq/D")); + + setDataSlack(5e-12, sta::MinMax::max()); + ASSERT_FALSE(HasFatalFailure()); + Target target = makeTarget("out0", "dq_seq", "Q"); + const float setup_before = sta_->slack(data_vertex, sta::MinMax::max()); + const float hold_before = sta_->slack(data_vertex, sta::MinMax::min()); + const float data_arrival = sta_->arrival(db_network_->findPin("dq_seq/D"), + sta::RiseFallBoth::riseFall(), + sta::MinMax::max()); + + const std::shared_ptr near + = SequentialTimingEstimator::prepare(resizer_, target); + ASSERT_NE(near, nullptr); + const SequentialTimingEstimator::Result identity = near->estimate(current); + ASSERT_TRUE(identity.legal) << identity.reason; + EXPECT_FLOAT_EQ(identity.weighted_side_slack_loss, 0.0f); + EXPECT_FLOAT_EQ(identity.max_side_slack_loss, 0.0f); + const SequentialTimingEstimator::Result risk = near->estimate(candidate); + ASSERT_TRUE(risk.legal) << risk.reason; + EXPECT_GT(risk.weighted_side_slack_loss, 0.0f); + EXPECT_GT(risk.max_side_slack_loss, 0.0f); + EXPECT_LT(risk.weighted_side_slack_loss, risk.max_side_slack_loss); + EXPECT_EQ(flip_flop->getMaster(), original); + EXPECT_EQ(flip_flop->findITerm("D")->getNet(), data_net); + EXPECT_EQ(flip_flop->findITerm("CK")->getNet(), clock_net); + EXPECT_FLOAT_EQ(sta_->slack(data_vertex, sta::MinMax::max()), setup_before); + EXPECT_FLOAT_EQ(sta_->slack(data_vertex, sta::MinMax::min()), hold_before); + EXPECT_FLOAT_EQ(sta_->arrival(db_network_->findPin("dq_seq/D"), + sta::RiseFallBoth::riseFall(), + sta::MinMax::max()), + data_arrival); + + setDataSlack(100e-12, sta::MinMax::max()); + ASSERT_FALSE(HasFatalFailure()); + target = makeTarget("out0", "dq_seq", "Q"); + const std::shared_ptr ample + = SequentialTimingEstimator::prepare(resizer_, target); + ASSERT_NE(ample, nullptr); + const SequentialTimingEstimator::Result safe = ample->estimate(candidate); + ASSERT_TRUE(safe.legal) << safe.reason; + EXPECT_FLOAT_EQ(safe.weighted_side_slack_loss, 0.0f); + EXPECT_FLOAT_EQ(safe.max_side_slack_loss, risk.max_side_slack_loss); + const SequentialTimingEstimator::Result cached = near->estimate(candidate); + EXPECT_EQ(cached.legal, risk.legal); + EXPECT_FLOAT_EQ(cached.weighted_side_slack_loss, + risk.weighted_side_slack_loss); + EXPECT_FLOAT_EQ(cached.max_side_slack_loss, risk.max_side_slack_loss); + EXPECT_EQ(cached.reason, risk.reason); + std::vector parallel_results(4); + std::vector workers; + for (size_t index = 0; index < parallel_results.size(); ++index) { + workers.emplace_back( + [&, index] { parallel_results[index] = near->estimate(candidate); }); + } + + for (std::thread& worker : workers) { + worker.join(); + } + + for (const SequentialTimingEstimator::Result& result : parallel_results) { + EXPECT_EQ(result.legal, risk.legal); + EXPECT_FLOAT_EQ(result.weighted_side_slack_loss, + risk.weighted_side_slack_loss); + EXPECT_FLOAT_EQ(result.max_side_slack_loss, risk.max_side_slack_loss); + EXPECT_EQ(result.reason, risk.reason); + } + + setDataSlack(0.0f, sta::MinMax::max()); + ASSERT_FALSE(HasFatalFailure()); + target = makeTarget("out0", "dq_seq", "Q"); + const std::shared_ptr critical + = SequentialTimingEstimator::prepare(resizer_, target); + ASSERT_NE(critical, nullptr); + const SequentialTimingEstimator::Result violation + = critical->estimate(candidate); + EXPECT_FALSE(violation.legal); + EXPECT_EQ(violation.reason, "sequential_setup_violation"); + EXPECT_GT(violation.weighted_side_slack_loss, 0.0f); + EXPECT_FLOAT_EQ(violation.weighted_side_slack_loss, + violation.max_side_slack_loss); + + ASSERT_TRUE(resizer_.replaceCell(target.inst(resizer_), candidate, false)); + resizer_.updateParasiticsAndTiming(); + data_vertex = sta_->graph()->pinLoadVertex(db_network_->findPin("dq_seq/D")); + EXPECT_LT(sta_->slack(data_vertex, sta::MinMax::max()), 0.0f); + EXPECT_FLOAT_EQ(near->estimate(candidate).weighted_side_slack_loss, + risk.weighted_side_slack_loss); +} + +TEST_F(TestResizerMtSequential, FlipFlopHoldCheckLossHasCorrectSign) +{ + prepareFlipFlop("DFF_X2"); + ASSERT_FALSE(HasFatalFailure()); + sta::LibertyCell* current = db_network_->findLibertyCell("DFF_X2"); + sta::LibertyCell* candidate = db_network_->findLibertyCell("DFF_X1"); + ASSERT_NE(candidate, nullptr); + ASSERT_GT(candidate->findLibertyPort("D")->capacitance(), + current->findLibertyPort("D")->capacitance()); + + setDataSlack(0.0f, sta::MinMax::min()); + ASSERT_FALSE(HasFatalFailure()); + const Target target = makeTarget("out0", "dq_seq", "Q"); + const std::shared_ptr estimator + = SequentialTimingEstimator::prepare(resizer_, target); + ASSERT_NE(estimator, nullptr); + const SequentialTimingEstimator::Result identity + = estimator->estimate(current); + ASSERT_TRUE(identity.legal) << identity.reason; + EXPECT_FLOAT_EQ(identity.weighted_side_slack_loss, 0.0f); + const SequentialTimingEstimator::Result result + = estimator->estimate(candidate); + EXPECT_FALSE(result.legal); + EXPECT_EQ(result.reason, "sequential_hold_violation"); + EXPECT_GT(result.weighted_side_slack_loss, 0.0f); + EXPECT_LE(result.weighted_side_slack_loss, result.max_side_slack_loss); + + ASSERT_TRUE(resizer_.replaceCell(target.inst(resizer_), candidate, false)); + resizer_.updateParasiticsAndTiming(); + sta::Vertex* data_vertex + = sta_->graph()->pinLoadVertex(db_network_->findPin("dq_seq/D")); + EXPECT_LT(sta_->slack(data_vertex, sta::MinMax::min()), 0.0f); + EXPECT_GT(sta_->slack(data_vertex, sta::MinMax::max()), 0.0f); +} + +TEST_F(TestResizerMtSequential, FlipFlopEstimatorExcludesLatchAndCombinational) +{ + prepareSequential("DLH_X1", "G"); + ASSERT_FALSE(HasFatalFailure()); + const Target latch = makeTarget("out0", "dq_seq", "Q"); + const Target combinational = makeTarget("path_out", "path_target", "ZN"); + EXPECT_EQ(SequentialTimingEstimator::prepare(resizer_, latch), nullptr); + EXPECT_EQ(SequentialTimingEstimator::prepare(resizer_, combinational), + nullptr); +} + +TEST_F(TestResizerMtSequential, FlipFlopPreparationCountsCandidateCells) +{ + prepareFlipFlop("DFF_X1"); + ASSERT_FALSE(HasFatalFailure()); + ProfileTestDirectory directory; + { + RepairSetupProfiler profiler(resizer_, + RepairSetupProfiler::Mode::kTiming, + directory.root().string()); + profiler.phase("PARALLEL_EVAL", 1); + const Target target = makeTarget("out0", "dq_seq", "Q"); + ASSERT_NE(SequentialTimingEstimator::prepare(resizer_, target), nullptr); + profiler.complete(true); + } + + const std::string stats + = readProfileFile(directory.root() / "run_1/stats.csv"); + EXPECT_NE(stats.find("\"sequential_candidate_cells_prepared\""), + std::string::npos); + EXPECT_EQ(stats.find("\"sequential_cells_snapshotted\""), std::string::npos); +} + +TEST_F(TestResizerMtSequential, FlipFlopPropagatedClockUsesCheckSlewPoint) +{ + prepareFlipFlop("DFF_X1"); + ASSERT_FALSE(HasFatalFailure()); + sta_->setAnalysisType(sta::AnalysisType::single, sta_->cmdMode()->sdc()); + sta_->updateTiming(true); + sta::LibertyCell* current = db_network_->findLibertyCell("DFF_X1"); + Target target = makeTarget("out0", "dq_seq", "Q"); + const std::shared_ptr ideal + = SequentialTimingEstimator::prepare(resizer_, target); + ASSERT_NE(ideal, nullptr); + ASSERT_TRUE(ideal->estimate(current).legal); + + sta_->setPropagatedClock(sta_->cmdMode()->sdc()->findClock("clk"), + sta_->cmdMode()); + + sta_->updateTiming(true); + target = makeTarget("out0", "dq_seq", "Q"); + ASSERT_NE(target.scene->checkClkSlewMinMax(sta::MinMax::max()), + target.minMax(resizer_)); + + sta::Vertex* clock_vertex + = sta_->graph()->pinLoadVertex(db_network_->findPin("dq_seq/CK")); + ASSERT_GT( + sta_->graph()->slew(clock_vertex, + sta::RiseFall::rise(), + target.scene->checkClkSlewIndex(sta::MinMax::max())), + 0.0f); + const std::shared_ptr propagated + = SequentialTimingEstimator::prepare(resizer_, target); + ASSERT_NE(propagated, nullptr); + const SequentialTimingEstimator::Result identity + = propagated->estimate(current); + EXPECT_TRUE(identity.legal) << identity.reason; + EXPECT_FLOAT_EQ(identity.weighted_side_slack_loss, 0.0f); + EXPECT_FLOAT_EQ(identity.max_side_slack_loss, 0.0f); +} + +TEST_F(TestResizerMtSequential, FlipFlopCandidateUsesActiveScaledCheckModel) +{ + prepareFlipFlop("DFF_X1"); + ASSERT_FALSE(HasFatalFailure()); + sta::LibertyCell* current = db_network_->findLibertyCell("DFF_X1"); + sta::LibertyCell* candidate = db_network_->findLibertyCell("DFF_X1_L"); + sta::LibertyCell* scaled = db_network_->findLibertyCell("DFF_X2_L"); + copyFlipFlopChecks(current, candidate); + copyFlipFlopChecks(current, scaled, 4e-12f); + ASSERT_FALSE(HasFatalFailure()); + for (const sta::RiseFall* rf : sta::RiseFall::range()) { + for (const sta::MinMax* min_max : sta::MinMax::range()) { + candidate->findLibertyPort("D")->setCapacitance( + rf, min_max, current->findLibertyPort("D")->capacitance(rf, min_max)); + } + } + + setDataSlack(2e-12f, sta::MinMax::max()); + ASSERT_FALSE(HasFatalFailure()); + sta::OperatingConditions* op_cond + = current->libertyLibrary()->defaultOperatingConditions(); + ASSERT_NE(op_cond, nullptr); + sta_->setOperatingConditions( + op_cond, sta::MinMaxAll::all(), sta_->cmdMode()->sdc()); + + sta_->updateTiming(true); + const Target target = makeTarget("out0", "dq_seq", "Q"); + const std::shared_ptr nominal + = SequentialTimingEstimator::prepare(resizer_, target); + ASSERT_NE(nominal, nullptr); + ASSERT_TRUE(nominal->estimate(candidate).legal); + EXPECT_FLOAT_EQ(nominal->estimate(candidate).weighted_side_slack_loss, 0.0f); + int scaled_checks = 0; + for (sta::TimingArcSet* set : candidate->timingArcSets()) { + if (set->role() != sta::TimingRole::setup()) { + continue; + } + + for (sta::TimingArc* arc : set->arcs()) { + for (sta::TimingArcSet* scaled_set : scaled->timingArcSets()) { + const sta::TimingArc* scaled_arc + = findMatchingTimingArc(arc, scaled_set); + if (scaled_arc != nullptr) { + scaled_arc->model()->setIsScaled(true); + arc->addScaledModel(op_cond, scaled_arc->model()); + ASSERT_NE(arc->checkModel(target.scene, sta::MinMax::max()), + arc->model()); + ++scaled_checks; + } + } + } + } + + ASSERT_EQ(scaled_checks, 2); + const std::shared_ptr estimator + = SequentialTimingEstimator::prepare(resizer_, target); + ASSERT_NE(estimator, nullptr); + const SequentialTimingEstimator::Result result + = estimator->estimate(candidate); + EXPECT_FALSE(result.legal); + EXPECT_EQ(result.reason, "sequential_setup_violation"); + EXPECT_NEAR(result.weighted_side_slack_loss, 4e-12, 1e-15); + EXPECT_TRUE(nominal->estimate(candidate).legal); + ASSERT_TRUE(resizer_.replaceCell(target.inst(resizer_), candidate, false)); + resizer_.updateParasiticsAndTiming(); + sta::Vertex* data_vertex + = sta_->graph()->pinLoadVertex(db_network_->findPin("dq_seq/D")); + EXPECT_NEAR(sta_->slack(data_vertex, sta::MinMax::max()), -2e-12, 1e-15); +} + +TEST_F(TestResizerMtSequential, + FlipFlopCandidateUsesActiveScaledDataCapacitance) +{ + prepareFlipFlop("DFF_X1"); + ASSERT_FALSE(HasFatalFailure()); + sta::LibertyCell* current = db_network_->findLibertyCell("DFF_X1"); + sta::LibertyCell* candidate = db_network_->findLibertyCell("DFF_X1_L"); + sta::LibertyCell* scaled = db_network_->findLibertyCell("DFF_X2_L"); + copyFlipFlopChecks(current, candidate); + ASSERT_FALSE(HasFatalFailure()); + for (const sta::RiseFall* rf : sta::RiseFall::range()) { + for (const sta::MinMax* min_max : sta::MinMax::range()) { + const float cap = current->findLibertyPort("D")->capacitance(rf, min_max); + candidate->findLibertyPort("D")->setCapacitance(rf, min_max, cap); + scaled->findLibertyPort("D")->setCapacitance(rf, min_max, cap * 1.05f); + } + } + + sta::OperatingConditions* op_cond + = current->libertyLibrary()->defaultOperatingConditions(); + ASSERT_NE(op_cond, nullptr); + sta_->setOperatingConditions( + op_cond, sta::MinMaxAll::all(), sta_->cmdMode()->sdc()); + + setDataSlack(5e-12f, sta::MinMax::max()); + ASSERT_FALSE(HasFatalFailure()); + const Target target = makeTarget("out0", "dq_seq", "Q"); + const std::shared_ptr nominal + = SequentialTimingEstimator::prepare(resizer_, target); + ASSERT_NE(nominal, nullptr); + ASSERT_TRUE(nominal->estimate(candidate).legal); + EXPECT_FLOAT_EQ(nominal->estimate(candidate).weighted_side_slack_loss, 0.0f); + candidate->findLibertyPort("D")->addScaledPort(op_cond, + scaled->findLibertyPort("D")); + + const std::shared_ptr estimator + = SequentialTimingEstimator::prepare(resizer_, target); + ASSERT_NE(estimator, nullptr); + const SequentialTimingEstimator::Result result + = estimator->estimate(candidate); + EXPECT_TRUE(result.legal) << result.reason; + EXPECT_GT(result.weighted_side_slack_loss, 0.0f); + EXPECT_GT(result.max_side_slack_loss, result.weighted_side_slack_loss); + EXPECT_FLOAT_EQ(nominal->estimate(candidate).weighted_side_slack_loss, 0.0f); + ASSERT_TRUE(resizer_.replaceCell(target.inst(resizer_), candidate, false)); + resizer_.updateParasiticsAndTiming(); + const sta::Pin* data_pin = db_network_->findPin("dq_seq/D"); + EXPECT_FLOAT_EQ( + sta_->cmdMode()->sdc()->pinCapacitance( + data_pin, sta::RiseFall::rise(), target.scene, sta::MinMax::max()), + scaled->findLibertyPort("D")->capacitance(sta::RiseFall::rise(), + sta::MinMax::max())); + + // Scaled-only fixture edits bypass STA's nominal-cap replacement test. + ep_.estimateWireParasitics(); + sta_->delaysInvalid(); + sta_->findRequireds(); + sta::Vertex* data_vertex + = sta_->graph()->pinLoadVertex(db_network_->findPin("dq_seq/D")); + EXPECT_LT(sta_->slack(data_vertex, sta::MinMax::max()), 5e-12f); + EXPECT_GT(sta_->slack(data_vertex, sta::MinMax::max()), 0.0f); +} + +TEST(TestResizerMtThreadPool, OppositeRfSideLossChargesNegativeRegionOnly) +{ + // Violating opposite transition: every picosecond of growth is lost. + EXPECT_FLOAT_EQ(oppositeRfSideLoss(-10e-12f, 3e-12f), 3e-12f); + + // Positive slack absorbs part of the growth. + EXPECT_FLOAT_EQ(oppositeRfSideLoss(2e-12f, 3e-12f), 1e-12f); + + // Enough slack, a faster opposite transition, or no timing: no loss. + EXPECT_FLOAT_EQ(oppositeRfSideLoss(5e-12f, 3e-12f), 0.0f); + EXPECT_FLOAT_EQ(oppositeRfSideLoss(-10e-12f, -1e-12f), 0.0f); + EXPECT_FLOAT_EQ( + oppositeRfSideLoss(std::numeric_limits::infinity(), 3e-12f), 0.0f); +} + +} // namespace rsz diff --git a/src/rsz/test/cpp/TestRepairSetupProfiler.cc b/src/rsz/test/cpp/TestRepairSetupProfiler.cc index 9f9906ef3f5..03234f4f62a 100644 --- a/src/rsz/test/cpp/TestRepairSetupProfiler.cc +++ b/src/rsz/test/cpp/TestRepairSetupProfiler.cc @@ -14,7 +14,8 @@ TEST_F(TestResizerMt, RepairSetupProfilerOffProducesNoOutput) { testing::internal::CaptureStdout(); EXPECT_NO_THROW({ - RepairSetupProfiler profiler(resizer_, 0, directory.root().string()); + RepairSetupProfiler profiler( + resizer_, RepairSetupProfiler::Mode::kOff, directory.root().string()); EXPECT_FALSE(RepairSetupProfiler::enabled()); const ProfileScope scope("prepare"); const ProfileIteration iteration("test_pass"); @@ -38,7 +39,9 @@ TEST_F(TestResizerMt, RepairSetupProfilerConsecutiveCallsKeepFreshFiles) std::vector first_outputs; for (int run = 1; run <= 2; ++run) { { - RepairSetupProfiler profiler(resizer_, 1, directory.root().string()); + RepairSetupProfiler profiler(resizer_, + RepairSetupProfiler::Mode::kTiming, + directory.root().string()); EXPECT_TRUE(RepairSetupProfiler::enabled()); profiler.phase("LEGACY", 1); const ProfileIteration iteration("test_pass"); @@ -82,7 +85,9 @@ TEST_F(TestResizerMt, RepairSetupProfilerEventSummaryAndTrace) { ProfileTestDirectory directory; - for (const int mode : {1, 2}) { + for (const RepairSetupProfiler::Mode mode : + {RepairSetupProfiler::Mode::kTiming, + RepairSetupProfiler::Mode::kEvents}) { { RepairSetupProfiler profiler(resizer_, mode, directory.root().string()); profiler.phase("LEGACY", 1); @@ -97,7 +102,7 @@ TEST_F(TestResizerMt, RepairSetupProfilerEventSummaryAndTrace) } const std::filesystem::path run_dir - = directory.root() / ("run_" + std::to_string(mode)); + = directory.root() / ("run_" + std::to_string(static_cast(mode))); const std::string stats = readProfileFile(run_dir / "stats.csv"); const std::string events = readProfileFile(run_dir / "events.csv"); EXPECT_NE(stats.find(",\"count\",\"iterations\",\"\",1,1,1\n"), @@ -108,7 +113,7 @@ TEST_F(TestResizerMt, RepairSetupProfilerEventSummaryAndTrace) std::string::npos); EXPECT_EQ(stats.find("journal_undo_begin"), std::string::npos); EXPECT_EQ(events.find("retries"), std::string::npos); - if (mode == 1) { + if (mode == RepairSetupProfiler::Mode::kTiming) { EXPECT_EQ(events.find("\"retry\""), std::string::npos); EXPECT_EQ(events.find("\"journal_undo_begin\""), std::string::npos); } else { @@ -131,7 +136,9 @@ TEST_F(TestResizerMt, RepairSetupProfilerNestedCommitThenOuterUndo) StubBufferMoveCandidate rejected(resizer_, target, 1, false); { - RepairSetupProfiler profiler(resizer_, 2, directory.root().string()); + RepairSetupProfiler profiler(resizer_, + RepairSetupProfiler::Mode::kEvents, + directory.root().string()); profiler.phase("LEGACY", 1); const ProfileIteration iteration("legacy_pass"); const auto commit = [&](MoveCandidate& candidate) { diff --git a/src/rsz/test/cpp/TestResizerMt.cc b/src/rsz/test/cpp/TestResizerMt.cc index 03394a1ddee..a9c628e4442 100644 --- a/src/rsz/test/cpp/TestResizerMt.cc +++ b/src/rsz/test/cpp/TestResizerMt.cc @@ -103,7 +103,7 @@ static std::string capacitanceSignature(sta::dbSta* sta, const sta::Pin* pin) return oss.str(); } -TEST_F(TestResizerMt, BuildArcDelayStateAndEstimateLvtCandidate) +TEST_F(TestResizerMt, BuildEstimatorContextAndEstimateLvtCandidate) { Resizer& resizer = resizer_; resizer.runRepairSetupPreamble(); @@ -116,15 +116,16 @@ TEST_F(TestResizerMt, BuildArcDelayStateAndEstimateLvtCandidate) if (!context.has_value()) { return; } - const ArcDelayState& arc_delay = context.value(); - const DelayStageState& target_stage = arc_delay.target(); + + const EstimatorContext& estimator_context = context.value(); + const DelayStageState& target_stage = estimator_context.target(); ASSERT_NE(target_stage.arc.inputPort(), nullptr); ASSERT_NE(target_stage.arc.outputPort(), nullptr); EXPECT_EQ(target_stage.arc.inputPort()->name(), "A"); EXPECT_EQ(target_stage.arc.outputPort()->name(), "Z"); - ASSERT_EQ(arc_delay.path_stages.size(), 1); - EXPECT_EQ(arc_delay.target_stage_index, 0); - EXPECT_EQ(arc_delay.delay_estimation_levels, 0); + ASSERT_EQ(estimator_context.path_stages.size(), 1); + EXPECT_EQ(estimator_context.target_stage_index, 0); + EXPECT_EQ(estimator_context.delay_estimation_levels, 0); EXPECT_GT(target_stage.load_cap, 0.0f); EXPECT_GT(target_stage.current_delay, 0.0f); @@ -133,34 +134,34 @@ TEST_F(TestResizerMt, BuildArcDelayStateAndEstimateLvtCandidate) ASSERT_NE(candidate_cell, nullptr); const DelayEstimate estimate - = DelayEstimator::estimate(arc_delay, candidate_cell); + = DelayEstimator::estimate(estimator_context, candidate_cell); EXPECT_TRUE(estimate.legal); EXPECT_GT(estimate.arrival_impr, 0.0f); EXPECT_EQ(estimate.reason, FailReason::kEstimateLegal); } -TEST_F(TestResizerMt, BuildArcDelayStateWithOneLevelPathWindow) +TEST_F(TestResizerMt, BuildEstimatorContextWithOneLevelPathWindow) { Resizer& resizer = resizer_; resizer.runRepairSetupPreamble(); const Target target = makeTarget("path_out", "path_target", "ZN"); FailReason fail_reason = FailReason::kNone; - const std::optional context + const std::optional context = DelayEstimator::buildContext(resizer, target, 1, &fail_reason); ASSERT_TRUE(context.has_value()) << failReasonName(fail_reason); if (!context.has_value()) { return; } - const ArcDelayState& arc_delay = context.value(); - EXPECT_EQ(arc_delay.delay_estimation_levels, 1); - ASSERT_EQ(arc_delay.path_stages.size(), 3); - ASSERT_EQ(arc_delay.target_stage_index, 1); - EXPECT_EQ(arc_delay.path_stages[0].arc.outputPort()->name(), "Z"); - EXPECT_EQ(arc_delay.path_stages[1].arc.outputPort()->name(), "ZN"); - EXPECT_EQ(arc_delay.path_stages[2].arc.outputPort()->name(), "Z"); - for (const DelayStageState& stage : arc_delay.path_stages) { + const EstimatorContext& estimator_context = context.value(); + EXPECT_EQ(estimator_context.delay_estimation_levels, 1); + ASSERT_EQ(estimator_context.path_stages.size(), 3); + ASSERT_EQ(estimator_context.target_stage_index, 1); + EXPECT_EQ(estimator_context.path_stages[0].arc.outputPort()->name(), "Z"); + EXPECT_EQ(estimator_context.path_stages[1].arc.outputPort()->name(), "ZN"); + EXPECT_EQ(estimator_context.path_stages[2].arc.outputPort()->name(), "Z"); + for (const DelayStageState& stage : estimator_context.path_stages) { EXPECT_GT(stage.current_delay, 0.0f); EXPECT_GT(stage.current_slew, 0.0f); } @@ -170,7 +171,7 @@ TEST_F(TestResizerMt, BuildArcDelayStateWithOneLevelPathWindow) ASSERT_NE(candidate_cell, nullptr); const DelayEstimate estimate - = DelayEstimator::estimate(arc_delay, candidate_cell); + = DelayEstimator::estimate(estimator_context, candidate_cell); EXPECT_TRUE(estimate.legal); EXPECT_EQ(estimate.reason, FailReason::kEstimateLegal); } @@ -182,7 +183,7 @@ TEST_F(TestResizerMt, SlewBiasContextBuildLeavesTimingUpdateUsable) const Target target = makeTarget("path_out", "path_target", "ZN"); FailReason fail_reason = FailReason::kNone; - const std::optional context + const std::optional context = DelayEstimator::buildContext(resizer, target, 1, &fail_reason, true); ASSERT_TRUE(context.has_value()) << failReasonName(fail_reason); @@ -200,6 +201,73 @@ TEST_F(TestResizerMt, SlewBiasContextBuildLeavesTimingUpdateUsable) EXPECT_EQ(estimate.reason, FailReason::kEstimateLegal); } +TEST_F(TestResizerMt, SlewBiasCacheReusesSamplingUntilClear) +{ + resizer_.runRepairSetupPreamble(); + sta::Scene* scene = sta_->cmdScene(); + for (const char* name : {"path_pre1/Z", "path_side_buf/Z"}) { + const sta::Pin* driver = sta_->network()->findPin(name); + ASSERT_NE(driver, nullptr); + for (const sta::RiseFall* rf : sta::RiseFall::range()) { + float pin_cap = 0.0f; + float sdc_wire_cap = 0.0f; + float fanout = 0.0f; + bool has_net_load = false; + sta_->graphDelayCalc()->netCaps(driver, + rf, + scene, + sta::MinMax::max(), + pin_cap, + sdc_wire_cap, + fanout, + has_net_load); + scene->parasitics(sta::MinMax::max()) + ->makePiElmore(driver, rf, sta::MinMax::max(), 1e-15f, 1.0f, pin_cap); + } + } + sta_->updateTiming(true); + const Target target = makeTarget("path_out", "path_target", "ZN"); + const std::optional context + = DelayEstimator::buildContext(resizer_, target, 1); + ASSERT_TRUE(context.has_value()); + ASSERT_GT(context->target_stage_index, 0); + + DelayEstimatorCache cache; + const float max_load_delta + = cache.maxEquivalentInputCapIncrease(resizer_, context->target()); + ASSERT_GT(max_load_delta, 0.0f); + const DelayStageState& fanin_stage + = context->path_stages[context->target_stage_index - 1]; + + ProfileTestDirectory directory; + { + RepairSetupProfiler profiler(resizer_, + RepairSetupProfiler::Mode::kTiming, + directory.root().string()); + profiler.phase("PARALLEL_EVAL", 1); + const ProfileIteration iteration("test_pass"); + const SlewBiasModel first + = cache.getOrSampleSlewBias(resizer_, fanin_stage, max_load_delta); + const SlewBiasModel cached + = cache.getOrSampleSlewBias(resizer_, fanin_stage, max_load_delta); + EXPECT_EQ(cached.valid, first.valid); + cache.clear(); + const SlewBiasModel after_clear + = cache.getOrSampleSlewBias(resizer_, fanin_stage, max_load_delta); + EXPECT_EQ(after_clear.valid, first.valid); + profiler.complete(true); + } + + const std::string stats + = readProfileFile(directory.root() / "run_1/stats.csv"); + EXPECT_NE(stats.find(",\"count\",\"slew_bias_model_hits\",\"\",1,1,1\n"), + std::string::npos) + << stats; + EXPECT_NE(stats.find(",\"count\",\"slew_bias_model_misses\",\"\",2,2,2\n"), + std::string::npos) + << stats; +} + TEST_F(TestResizerMt, RejectCandidateWithMismatchedOutputPort) { Resizer& resizer = resizer_; @@ -212,19 +280,20 @@ TEST_F(TestResizerMt, RejectCandidateWithMismatchedOutputPort) if (!context.has_value()) { return; } - const ArcDelayState& arc_delay = context.value(); + + const EstimatorContext& estimator_context = context.value(); sta::LibertyCell* candidate_cell = sta_->network()->findLibertyCell("NAND2_X1"); ASSERT_NE(candidate_cell, nullptr); const DelayEstimate estimate - = DelayEstimator::estimate(arc_delay, candidate_cell); + = DelayEstimator::estimate(estimator_context, candidate_cell); EXPECT_FALSE(estimate.legal); EXPECT_EQ(estimate.reason, FailReason::kMissingCandidatePort); } -TEST_F(TestResizerMt, BuildArcDelayStateCapturesOutputSlewMergeArcs) +TEST_F(TestResizerMt, BuildEstimatorContextCapturesOutputSlewMergeArcs) { Resizer& resizer = resizer_; resizer.runRepairSetupPreamble(); @@ -236,14 +305,15 @@ TEST_F(TestResizerMt, BuildArcDelayStateCapturesOutputSlewMergeArcs) if (!context.has_value()) { return; } - const ArcDelayState& arc_delay = context.value(); - const SelectedArc& target_arc = arc_delay.target().arc; + + const EstimatorContext& estimator_context = context.value(); + const SelectedArc& target_arc = estimator_context.target().arc; ASSERT_NE(target_arc.inputPort(), nullptr); ASSERT_NE(target_arc.outputPort(), nullptr); EXPECT_EQ(target_arc.inputPort()->name(), "A1"); EXPECT_EQ(target_arc.outputPort()->name(), "ZN"); - EXPECT_GE(arc_delay.target().output_slew_merge_arcs.size(), 1); - EXPECT_GT(arc_delay.target().current_model_slew, 0.0f); + EXPECT_GE(estimator_context.target().output_slew_merge_arcs.size(), 1); + EXPECT_GT(estimator_context.target().current_model_slew, 0.0f); sta::Vertex* driver_vertex = sta_->graph()->pinDrvrVertex(target.driver_pin); ASSERT_NE(driver_vertex, nullptr); @@ -251,14 +321,14 @@ TEST_F(TestResizerMt, BuildArcDelayStateCapturesOutputSlewMergeArcs) = target_arc.scene->dcalcAnalysisPtIndex(target_arc.min_max); const float graph_slew = sta::delayAsFloat(sta_->graph()->slew( driver_vertex, target_arc.outputRiseFall(), dcalc_ap)); - EXPECT_FLOAT_EQ(arc_delay.target().current_slew, graph_slew); + EXPECT_FLOAT_EQ(estimator_context.target().current_slew, graph_slew); sta::LibertyCell* candidate_cell = sta_->network()->findLibertyCell("NAND2_X1_L"); ASSERT_NE(candidate_cell, nullptr); const DelayEstimate estimate - = DelayEstimator::estimate(arc_delay, candidate_cell); + = DelayEstimator::estimate(estimator_context, candidate_cell); EXPECT_TRUE(estimate.legal); EXPECT_EQ(estimate.reason, FailReason::kEstimateLegal); } @@ -267,15 +337,16 @@ TEST_F(TestResizerMt, MtCandidateEstimateUsesSelectedArcAndCachedState) { Resizer& resizer = resizer_; resizer.runRepairSetupPreamble(); - const Target target = makeTarget("out0", "target", "Z"); + Target target = makeTarget("out0", "target", "Z"); FailReason fail_reason = FailReason::kNone; - const std::optional arc_delay + const std::optional estimator_context = DelayEstimator::buildContext(resizer, target, 0, &fail_reason); - ASSERT_TRUE(arc_delay.has_value()) << failReasonName(fail_reason); - if (!arc_delay.has_value()) { + ASSERT_TRUE(estimator_context.has_value()) << failReasonName(fail_reason); + if (!estimator_context.has_value()) { return; } + target.estimator_context = estimator_context; sta::Instance* target_inst = target.inst(resizer); sta::LibertyCell* current_cell = sta_->network()->libertyCell(target_inst); @@ -285,13 +356,7 @@ TEST_F(TestResizerMt, MtCandidateEstimateUsesSelectedArcAndCachedState) = sta_->network()->findLibertyCell("BUF_X1_L"); ASSERT_NE(candidate_cell, nullptr); - VtSwapMtCandidate candidate(resizer, - target, - target.driver_pin, - target_inst, - current_cell, - candidate_cell, - arc_delay.value()); + VtSwapMtCandidate candidate(resizer, target, target_inst, candidate_cell); const Estimate estimate = candidate.estimate(); ASSERT_TRUE(estimate.legal); diff --git a/src/rsz/test/cpp/TestResizerMtFixture.hh b/src/rsz/test/cpp/TestResizerMtFixture.hh index a1ddcea2f27..d60e34cf4e3 100644 --- a/src/rsz/test/cpp/TestResizerMtFixture.hh +++ b/src/rsz/test/cpp/TestResizerMtFixture.hh @@ -32,8 +32,14 @@ #include // Expose scaled-library fixture hooks without changing OpenSTA production APIs. +#define protected public +#include "sta/Liberty.hh" +#include "sta/TimingArc.hh" +#undef protected + #include "DelayEstimator.hh" #include "RepairSetupProfiler.hh" +#include "SequentialTimingEstimator.hh" #include "rsz/Resizer.hh" #define private public #define protected public @@ -48,6 +54,10 @@ #define private public #include "SizeUpMatchGenerator.hh" #undef private +#include "SizeUpMtCandidate.hh" +#define private public +#include "SizeUpMtGenerator.hh" +#undef private #include "VtSwapMtCandidate.hh" #include "db_sta/dbNetwork.hh" #include "db_sta/dbSta.hh" @@ -93,6 +103,27 @@ class TestResizerMt : public tst::IntegratedFixture } } + // Make the unchanged sibling load the unique negative-slack endpoint. + void prepareUniqueViolatingEndpoint() + { + resizer_.runRepairSetupPreamble(); + sta::Sdc* sdc = sta_->cmdMode()->sdc(); + sta_->setOutputDelay(db_network_->findPin("out2"), + sta::RiseFallBoth::riseFall(), + sdc->findClock("clk"), + sta::RiseFall::rise(), + nullptr, + false, + false, + sta::MinMaxAll::all(), + false, + 1e-9, + sdc); + + resizer_.updateParasiticsAndTiming(); + ASSERT_LT(sta_->worstSlack(resizer_.maxAnalysisMode()), 0.0f); + } + void SetUp() override { readVerilogAndSetup("TestResizerMt_DelayEstimator.v"); @@ -188,6 +219,174 @@ class TestResizerMt : public tst::IntegratedFixture } }; +class TestResizerMtSequential : public TestResizerMt +{ + protected: + void SetUp() override + { + readVerilogAndSetup("TestResizerMt_DelayEstimator.v", true, false); + } + + void prepareSequential(const char* cell_name, const char* clock_port) + { + odb::dbMaster* master = db_->findMaster(cell_name); + ASSERT_NE(master, nullptr); + odb::dbInst* sequential = odb::dbInst::create(block_, master, "dq_seq"); + odb::dbInst* clock_driver = odb::dbInst::create( + block_, db_->findMaster("BUF_X1"), "dq_clock_driver"); + odb::dbNet* q_net = odb::dbNet::create(block_, "dq_q"); + odb::dbNet* clock_net = odb::dbNet::create(block_, "dq_clock"); + + // Keep the actual D driver distinct from the propagated clock driver. + sequential->findITerm("D")->connect(block_->findNet("n1")); + sequential->findITerm("Q")->connect(q_net); + sequential->findITerm(clock_port)->connect(clock_net); + clock_driver->findITerm("A")->connect(block_->findNet("clk")); + clock_driver->findITerm("Z")->connect(clock_net); + for (const char* name : {"load0", "load1", "load2"}) { + block_->findInst(name)->findITerm("A")->connect(q_net); + } + + int x = 1000; + for (odb::dbInst* inst : block_->getInsts()) { + inst->setLocation(x, 1000); + inst->setPlacementStatus(odb::dbPlacementStatus::PLACED); + x += 2000; + } + + sta_->setPropagatedClock(sta_->cmdMode()->sdc()->findClock("clk"), + sta_->cmdMode()); + + setDataInputDelay(0.10e-9, sta::MinMaxAll::all()); + ep_.estimateWireParasitics(); + resizer_.runRepairSetupPreamble(); + sta_->updateTiming(true); + } + + void setDataInputDelay(float delay, const sta::MinMaxAll* min_max) + { + sta::Sdc* sdc = sta_->cmdMode()->sdc(); + sta_->setInputDelay(db_network_->findPin("in"), + sta::RiseFallBoth::riseFall(), + sdc->findClock("clk"), + sta::RiseFall::rise(), + nullptr, + false, + false, + min_max, + false, + delay, + sdc); + } + + void prepareFlipFlop(const char* cell_name) + { + prepareSequential(cell_name, "CK"); + ASSERT_FALSE(HasFatalFailure()); + sta_->removePropagatedClock(sta_->cmdMode()->sdc()->findClock("clk"), + sta_->cmdMode()); + + sta_->updateTiming(true); + } + + void setDataSlack(float slack, const sta::MinMax* min_max) + { + setDataInputDelay(0.10e-9, min_max->asMinMaxAll()); + sta_->updateTiming(true); + sta::Vertex* data_vertex + = sta_->graph()->pinLoadVertex(db_network_->findPin("dq_seq/D")); + const float current_slack = sta_->slack(data_vertex, min_max); + const float adjustment = min_max == sta::MinMax::max() + ? current_slack - slack + : slack - current_slack; + setDataInputDelay(0.10e-9 + adjustment, min_max->asMinMaxAll()); + sta_->updateTiming(true); + ASSERT_NEAR(sta_->slack(data_vertex, min_max), slack, 1e-15); + } + + void copyFlipFlopChecks(sta::LibertyCell* source, + sta::LibertyCell* destination, + const float setup_shift = 0.0f) + { + int copied = 0; + for (const sta::TimingArcSet* source_set : source->timingArcSets()) { + if (source_set->role() != sta::TimingRole::setup() + && source_set->role() != sta::TimingRole::hold()) { + continue; + } + + for (const sta::TimingArc* source_arc : source_set->arcs()) { + const sta::CheckTableModel* source_model + = dynamic_cast(source_arc->model()); + ASSERT_NE(source_model, nullptr); + for (sta::TimingArcSet* destination_set : + destination->timingArcSets()) { + const sta::TimingArc* destination_arc + = findMatchingTimingArc(source_arc, destination_set); + if (destination_arc == nullptr) { + continue; + } + + const sta::CheckTableModel* destination_model + = dynamic_cast( + destination_arc->model()); + ASSERT_NE(destination_model, nullptr); + sta::Table* table = destination_model->checkModel()->table().get(); + + *table = sta::Table(*source_model->checkModel()->table()); + ASSERT_EQ(table->order(), 2); + if (source_set->role() == sta::TimingRole::setup()) { + for (sta::FloatSeq& row : *table->values3()) { + for (float& value : row) { + value += setup_shift; + } + } + } + ++copied; + } + } + } + + ASSERT_EQ(copied, 4); + } +}; + +inline void expectSameDelayContext(const EstimatorContext& lhs, + const EstimatorContext& rhs) +{ + EXPECT_EQ(lhs.target_stage_index, rhs.target_stage_index); + EXPECT_EQ(lhs.delay_estimation_levels, rhs.delay_estimation_levels); + EXPECT_FLOAT_EQ(lhs.current_total_delay, rhs.current_total_delay); + ASSERT_EQ(lhs.path_stages.size(), rhs.path_stages.size()); + for (size_t index = 0; index < lhs.path_stages.size(); ++index) { + SCOPED_TRACE(index); + const DelayStageState& left = lhs.path_stages[index]; + const DelayStageState& right = rhs.path_stages[index]; + EXPECT_EQ(left.driver_pin, right.driver_pin); + EXPECT_EQ(left.path_index, right.path_index); + EXPECT_EQ(left.arc.ref_arc, right.arc.ref_arc); + EXPECT_EQ(left.arc.scene, right.arc.scene); + EXPECT_EQ(left.arc.min_max, right.arc.min_max); + EXPECT_EQ(left.arc.pvt, right.arc.pvt); + EXPECT_FLOAT_EQ(left.input_slew, right.input_slew); + EXPECT_FLOAT_EQ(left.load_cap, right.load_cap); + EXPECT_FLOAT_EQ(left.current_model_delay, right.current_model_delay); + EXPECT_FLOAT_EQ(left.current_delay, right.current_delay); + EXPECT_FLOAT_EQ(left.current_model_slew, right.current_model_slew); + EXPECT_FLOAT_EQ(left.current_slew, right.current_slew); + ASSERT_EQ(left.output_slew_merge_arcs.size(), + right.output_slew_merge_arcs.size()); + for (size_t arc = 0; arc < left.output_slew_merge_arcs.size(); ++arc) { + EXPECT_EQ(left.output_slew_merge_arcs[arc].ref_arc, + right.output_slew_merge_arcs[arc].ref_arc); + EXPECT_FLOAT_EQ(left.output_slew_merge_arcs[arc].input_slew, + right.output_slew_merge_arcs[arc].input_slew); + EXPECT_FLOAT_EQ(left.output_slew_merge_arcs[arc].current_model_slew, + right.output_slew_merge_arcs[arc].current_model_slew); + } + } +} + class StubBufferMoveCandidate : public MoveCandidate { public: From 38357a8a5c52a668d141adc9be2e805e9c983e80 Mon Sep 17 00:00:00 2001 From: Jaehyun Kim Date: Fri, 16 Oct 2026 23:51:58 -0700 Subject: [PATCH 2/3] rsz: Add the RebufferMt buffer-tree move What: RebufferMt, an Mt buffering move that plans a beam of Rebuffer trees per driver and scores each with the buffer-tree estimator. Why: buffering needs the same scored, comparable candidates as the cell swaps before a batch policy can choose among them. How: makeRebufferTreePlans() and applyRebufferTreePlan() in Rebuffer, estimateBufferTreeWithSidePaths() in DelayEstimator, and current required times after the endpoint scan. No policy runs RebufferMt yet; PARALLEL_EVAL will. LEGACY is unchanged. Signed-off-by: Jaehyun Kim --- src/rsz/BUILD | 10 +- src/rsz/include/rsz/Resizer.hh | 14 +- src/rsz/src/CMakeLists.txt | 3 + src/rsz/src/DelayEstimator.cc | 145 ++- src/rsz/src/DelayEstimator.hh | 59 +- src/rsz/src/DelayEstimatorBufferTree.cc | 609 +++++++++++ src/rsz/src/DelayEstimatorInternal.hh | 51 + src/rsz/src/DelayEstimatorReporter.hh | 22 +- src/rsz/src/MoveCommitter.cc | 1 - src/rsz/src/OptimizationPolicyConfig.hh | 18 +- src/rsz/src/OptimizerTypes.cc | 6 + src/rsz/src/OptimizerTypes.hh | 21 + src/rsz/src/Rebuffer.cc | 1235 +++++++++++++++++++--- src/rsz/src/Rebuffer.hh | 268 ++++- src/rsz/src/RepairSetupProfiler.hh | 5 + src/rsz/src/RepairTargetCollector.cc | 7 + src/rsz/src/RepairTargetCollector.hh | 2 +- src/rsz/src/SequentialTimingEstimator.cc | 3 + src/rsz/src/SequentialTimingEstimator.hh | 7 +- src/rsz/src/move/MoveCandidate.hh | 2 +- src/rsz/src/move/MoveGenerator.cc | 9 - src/rsz/src/move/MoveGenerator.hh | 12 +- src/rsz/src/move/RebufferMtCandidate.cc | 121 +++ src/rsz/src/move/RebufferMtCandidate.hh | 51 + src/rsz/src/move/RebufferMtGenerator.cc | 135 +++ src/rsz/src/move/RebufferMtGenerator.hh | 52 + src/rsz/src/move/SizeUpGenerator.cc | 8 - src/rsz/src/move/SizeUpMtGenerator.cc | 2 +- src/rsz/src/move/SizeUpMtGenerator.hh | 9 +- src/rsz/src/move/VtSwapMtGenerator.cc | 2 +- src/rsz/src/move/VtSwapMtGenerator.hh | 9 +- src/rsz/src/policy/OptimizationPolicy.cc | 29 +- src/rsz/src/policy/OptimizationPolicy.hh | 3 +- src/rsz/test/cpp/TestCellSwapMt.cc | 4 +- src/rsz/test/cpp/TestResizerMtFixture.hh | 5 + 35 files changed, 2697 insertions(+), 242 deletions(-) create mode 100644 src/rsz/src/DelayEstimatorBufferTree.cc create mode 100644 src/rsz/src/DelayEstimatorInternal.hh create mode 100644 src/rsz/src/move/RebufferMtCandidate.cc create mode 100644 src/rsz/src/move/RebufferMtCandidate.hh create mode 100644 src/rsz/src/move/RebufferMtGenerator.cc create mode 100644 src/rsz/src/move/RebufferMtGenerator.hh diff --git a/src/rsz/BUILD b/src/rsz/BUILD index 53a26295aa9..372d176527d 100644 --- a/src/rsz/BUILD +++ b/src/rsz/BUILD @@ -19,6 +19,8 @@ cc_library( "src/ConcreteSwapArithModules.cc", "src/ConcreteSwapArithModules.hh", "src/DelayEstimator.cc", + "src/DelayEstimatorBufferTree.cc", + "src/DelayEstimatorInternal.hh", "src/DelayEstimatorReporter.cc", "src/DelayEstimatorReporter.hh", "src/LRSubproblem.cc", @@ -35,7 +37,6 @@ cc_library( "src/PreChecks.cc", "src/PreChecks.hh", "src/Rebuffer.cc", - "src/Rebuffer.hh", "src/RecoverPower.cc", "src/RecoverPower.hh", "src/RepairDesign.cc", @@ -50,7 +51,6 @@ cc_library( "src/move/BufferCandidate.cc", "src/move/BufferCandidate.hh", "src/move/BufferGenerator.cc", - "src/move/BufferGenerator.hh", "src/move/BufferToInvertersCandidate.cc", "src/move/BufferToInvertersCandidate.hh", "src/move/BufferToInvertersGenerator.cc", @@ -61,6 +61,9 @@ cc_library( "src/move/CloneGenerator.hh", "src/move/MoveCandidate.cc", "src/move/MoveGenerator.cc", + "src/move/RebufferMtCandidate.cc", + "src/move/RebufferMtCandidate.hh", + "src/move/RebufferMtGenerator.cc", "src/move/RerouteCandidate.cc", "src/move/RerouteCandidate.hh", "src/move/RerouteGenerator.cc", @@ -128,13 +131,16 @@ cc_library( "src/MoveCommitter.hh", "src/MoveTracker.hh", "src/OptimizerTypes.hh", + "src/Rebuffer.hh", "src/RepairSetupContext.hh", "src/RepairSetupProfiler.hh", "src/RepairTargetCollector.hh", "src/ResizerObserver.hh", "src/SequentialTimingEstimator.hh", + "src/move/BufferGenerator.hh", "src/move/MoveCandidate.hh", "src/move/MoveGenerator.hh", + "src/move/RebufferMtGenerator.hh", "src/move/SizeUpMatchGenerator.hh", "src/move/SizeUpMtCandidate.hh", "src/move/SizeUpMtGenerator.hh", diff --git a/src/rsz/include/rsz/Resizer.hh b/src/rsz/include/rsz/Resizer.hh index 8ff4a77ac53..0bd52770f5d 100644 --- a/src/rsz/include/rsz/Resizer.hh +++ b/src/rsz/include/rsz/Resizer.hh @@ -187,6 +187,7 @@ struct LibraryAnalysisData class OdbCallBack; +// Owns timing-driven netlist repair and its OpenDB/STA services. class Resizer : public sta::dbStaState, public sta::dbNetworkObserver { public: @@ -897,24 +898,31 @@ class Resizer : public sta::dbStaState, public sta::dbNetworkObserver const sta::LibertyCell* replacement); bool checkMaxCapOK(const sta::Pin* drvr_pin, float cap_delta); - // Fanin max-cap state of one instance, captured once. It lets - // replacementFitsInputCapBudgets() screen many cells without new STA checks, - // with the same decision as replacementPreservesMaxCap(). + // One driver's current load, cap limit, and violation state. struct DriverCapLimit { + // Total load currently driven on this net. float cap{0.0f}; + // Effective max-cap limit for this driver. float max_cap{0.0f}; + // True when the driver's current max-cap check has negative slack. bool negative_slack{false}; }; + // Cached fanin max-cap headroom for one input pin of a candidate cell. struct InputCapBudget { + // Current cell's Liberty input port matched to candidate ports. const sta::LibertyPort* port{nullptr}; + // Input capacitance of that current port. float current_cap{0.0f}; + // Limits for all drivers that STA associates with the input net. std::vector driver_limits; }; + // Capture fanin max-cap state once before screening equivalent cells. std::vector inputCapBudgets(sta::Instance* inst); + // Test a replacement using captured headroom without new STA queries. bool replacementFitsInputCapBudgets( const std::vector& budgets, const sta::LibertyCell* replacement) const; diff --git a/src/rsz/src/CMakeLists.txt b/src/rsz/src/CMakeLists.txt index ae74a671cd8..c14dc86d252 100644 --- a/src/rsz/src/CMakeLists.txt +++ b/src/rsz/src/CMakeLists.txt @@ -28,6 +28,7 @@ add_library(rsz_lib Optimizer.cc OptimizerTypes.cc DelayEstimator.cc + DelayEstimatorBufferTree.cc DelayEstimatorReporter.cc SequentialTimingEstimator.cc LRSubproblem.cc @@ -52,6 +53,8 @@ add_library(rsz_lib move/CloneGenerator.cc move/MoveCandidate.cc move/MoveGenerator.cc + move/RebufferMtCandidate.cc + move/RebufferMtGenerator.cc move/RerouteCandidate.cc move/RerouteGenerator.cc move/SizeDownFanoutCandidate.cc diff --git a/src/rsz/src/DelayEstimator.cc b/src/rsz/src/DelayEstimator.cc index c277e950160..46399718b8c 100644 --- a/src/rsz/src/DelayEstimator.cc +++ b/src/rsz/src/DelayEstimator.cc @@ -16,6 +16,7 @@ #include #include +#include "DelayEstimatorInternal.hh" #include "OptimizerTypes.hh" #include "RepairSetupProfiler.hh" #include "db_sta/dbSta.hh" @@ -44,13 +45,18 @@ namespace rsz { +using estimator_detail::estimateOutputSlew; +using estimator_detail::estimateReceiverInputSlew; +using estimator_detail::kMinSlewRatioDenominator; +using estimator_detail::lookupArcDelayAndSlew; +using estimator_detail::lookupBufferDelayAndSlew; +using estimator_detail::makeEstimate; + namespace { constexpr float kSlewMatchAbsTolerance = 1.0e-15f; constexpr float kSlewMatchRelTolerance = 1.0e-6f; -constexpr float kMinSlewRatioDenominator = 1.0e-15f; -// Sibling gate delay growth per unit of input slew growth, and the slew growth -// per unit of driver delay growth used when no slew model is available. +// Converts positive side-input slew growth into sibling gate delay growth. constexpr float kSideSlewDelayFactor = 0.3f; // Estimates side-input slew growth from driver delay growth when the side net // has no selected-arc slew model. @@ -362,14 +368,19 @@ bool lookupArcDelayAndSlewForArc(const sta::Scene* scene, return found; } +} // namespace + +// Shared with DelayEstimatorBufferTree.cc. +namespace estimator_detail { + bool lookupArcDelayAndSlew(const SelectedArc& arc, const float input_slew, const float load_cap, const sta::LibertyCell* cell, float& delay, float& output_slew, - ArcMatchMode match_mode = ArcMatchMode::kExact, - bool* match_relaxed_out = nullptr) + ArcMatchMode match_mode, + bool* match_relaxed_out) { return lookupArcDelayAndSlewForArc(arc.scene, arc.min_max, @@ -384,6 +395,77 @@ bool lookupArcDelayAndSlew(const SelectedArc& arc, match_relaxed_out); } +bool lookupBufferDelayAndSlew(const DelayStageState& target_stage, + const sta::LibertyCell* buffer_cell, + const float input_slew, + const float load_cap, + float& delay, + float& output_slew) +{ + delay = 0.0f; + output_slew = 0.0f; + if (buffer_cell == nullptr) { + return false; + } + + sta::LibertyCell* scene_cell + = const_cast(buffer_cell) + ->sceneCell(target_stage.arc.scene, target_stage.arc.min_max); + if (scene_cell == nullptr) { + return false; + } + + sta::LibertyPort* buffer_input = nullptr; + sta::LibertyPort* buffer_output = nullptr; + scene_cell->bufferPorts(buffer_input, buffer_output); + if (buffer_input == nullptr || buffer_output == nullptr) { + return false; + } + + const sta::TimingArcSetSeq& arc_sets + = scene_cell->timingArcSets(buffer_input, buffer_output); + + bool found_arc = false; + for (const sta::TimingArcSet* arc_set : arc_sets) { + if (arc_set->role()->isTimingCheck()) { + continue; + } + + for (const sta::TimingArc* arc : arc_set->arcs()) { + const sta::RiseFall* input_rf = arc->fromEdge()->asRiseFall(); + const sta::RiseFall* output_rf = arc->toEdge()->asRiseFall(); + if (input_rf != target_stage.arc.outputRiseFall() + || output_rf != target_stage.arc.outputRiseFall()) { + continue; + } + + float candidate_delay = 0.0f; + float candidate_slew = 0.0f; + if (!gateDelayAndSlewFromTableModel(target_stage.arc.pvt, + arc, + input_slew, + load_cap, + candidate_delay, + candidate_slew)) { + continue; + } + + if (!found_arc + || target_stage.arc.min_max->compare(candidate_delay, delay)) { + delay = candidate_delay; + output_slew = candidate_slew; + found_arc = true; + } + } + } + + return found_arc; +} + +} // namespace estimator_detail + +namespace { + const sta::TimingArc* findCellTimingArcLike(const sta::TimingArc* graph_arc, sta::LibertyCell* scene_cell) { @@ -1040,7 +1122,8 @@ SlewBiasModel DelayEstimatorCache::getOrSampleSlewBias( return model; } -namespace { +// Shared with DelayEstimatorBufferTree.cc. +namespace estimator_detail { // Estimate the post-ECO driver output slew for the selected transition. When // an STA slew-bias model is prepared for this stage, correct only the stable @@ -1053,8 +1136,8 @@ float estimateOutputSlew(const DelayStageState& stage, const float path_input_slew, const float load_cap, const float selected_output_slew, - ArcMatchMode merge_arc_mode = ArcMatchMode::kExact, - bool* slew_relaxed_out = nullptr) + ArcMatchMode merge_arc_mode, + bool* slew_relaxed_out) { if (canUseSlewBias(stage, cell, path_input_slew)) { float table_delay = 0.0f; @@ -1137,6 +1220,41 @@ float estimateReceiverInputSlew(const DelayStageState& driver_stage, / current_driver_output_slew; } +float wireElmoreDelay(const WireRC& wire, const float load_cap) +{ + if (wire.res <= 0.0f || wire.cap < 0.0f || load_cap < 0.0f) { + return 0.0f; + } + return wire.res * (load_cap + 0.5f * wire.cap); +} + +} // namespace estimator_detail + +namespace { + +float currentPathWireDelay(const Resizer& resizer, + const sta::PathExpanded& expanded, + const int path_index, + const SelectedArc& selected_arc) +{ + if (path_index + 1 >= expanded.size()) { + return 0.0f; + } + + const sta::Path* next_path = expanded.path(path_index + 1); + sta::Edge* wire_edge = next_path != nullptr + ? next_path->prevEdge(resizer.staState()) + : nullptr; + if (wire_edge == nullptr || !wire_edge->isWire()) { + return 0.0f; + } + + const int dcalc_ap + = selected_arc.scene->dcalcAnalysisPtIndex(selected_arc.min_max); + return sta::delayAsFloat(resizer.graph()->wireArcDelay( + wire_edge, selected_arc.outputRiseFall(), dcalc_ap)); +} + std::optional buildDelayStageState( const Resizer& resizer, sta::Instance* inst, @@ -1185,6 +1303,8 @@ std::optional buildDelayStageState( stage.load_cap = load_cap; stage.current_model_delay = current_model_delay; stage.current_delay = current_delay; + stage.current_wire_delay + = currentPathWireDelay(resizer, expanded, path_index, *selected_arc); stage.output_slew_merge_arcs = collectOutputSlewMergeArcs( resizer, *selected_arc, driver_pin, current_cell, load_cap); @@ -1431,6 +1551,11 @@ bool includeLatchFaninStages(const Resizer& resizer, return true; } +} // namespace + +// Shared with DelayEstimatorBufferTree.cc. +namespace estimator_detail { + DelayEstimate makeEstimate(const float current_delay, const float candidate_delay) { @@ -1442,6 +1567,10 @@ DelayEstimate makeEstimate(const float current_delay, : FailReason::kEstimateNonImproving}; } +} // namespace estimator_detail + +namespace { + // Record each input net of the target: the driver's delay per load and the // worst slack of each load pin. The selected-arc net skips the target pin, // which the window already scores with the exact fanin delta. diff --git a/src/rsz/src/DelayEstimator.hh b/src/rsz/src/DelayEstimator.hh index 88b3b168339..aedfcc8bf72 100644 --- a/src/rsz/src/DelayEstimator.hh +++ b/src/rsz/src/DelayEstimator.hh @@ -12,12 +12,15 @@ #include "OptimizerTypes.hh" #include "boost/functional/hash.hpp" +#include "sta/SearchClass.hh" namespace sta { +class dbSta; class Instance; class LibertyCell; class LibertyPort; class MinMax; +class MinMaxAll; class PathExpanded; class Pin; class Pvt; @@ -28,6 +31,7 @@ class TimingArc; namespace rsz { class Resizer; +struct RebufferTreePlan; struct Target; // Negative-region slack lost by the opposite output transition when its delay @@ -55,12 +59,11 @@ inline float sideLoadWeight( : off_path_weight; } -// Per-stage trace entry that the estimate functions (estimate(), -// estimateInsertion() and the buffer-tree estimates) produce when given a -// trace. Captures the inputs and outputs of the table-model lookup that was -// applied (with the candidate at the target stage) so diagnostic tools can -// present per-stage detail without re-implementing the estimator's stage -// walk. +// Per-stage trace entry that the estimate functions (estimate() and the +// buffer-tree estimates) produce when given a trace. Captures the inputs and +// outputs of the table-model lookup that was applied (with the candidate at the +// target stage) so diagnostic tools can present per-stage detail without +// re-implementing the estimator's stage walk. struct StageEvaluation { int path_index{-1}; @@ -71,11 +74,37 @@ struct StageEvaluation // buffer tree, which have no cell and carry their wire delay here. float stage_delay{0.0f}; float output_slew{0.0f}; + float wire_delay{0.0f}; // True when the target candidate lookup used relaxed (port-name+RF) // matching because no exact conditional/default arc was found. bool arc_match_relaxed{false}; }; +// === Buffer-tree side paths ================================================ + +// A side path of a buffer tree: the worst path through one load the tree +// moves, with its current timing. +struct BufferTreeSidePathContext +{ + const sta::Pin* load_pin{nullptr}; + float current_driver_to_load_delay{0.0f}; + float current_slack{0.0f}; + // TNS-loss weight of the side load. The side slack floor ignores it. + float weight{1.0f}; +}; + +// The estimate of one side path, for the accuracy report. +struct BufferTreeSidePathEvaluation +{ + const sta::Pin* load_pin{nullptr}; + // False when the side path has no estimate or falls below the side slack + // floor. + bool legal{false}; + float arrival_impr{0.0f}; + float slack_after{0.0f}; + std::vector trace; +}; + // === Delay estimator API ==================================================== // Reuses delay-estimator preparation results across targets in one policy run. @@ -233,6 +262,24 @@ class DelayEstimator static DelayEstimate estimate(const EstimatorContext& context, const sta::LibertyCell* candidate_cell, std::vector* trace = nullptr); + + // === Buffer-tree estimation (DelayEstimatorBufferTree.cc) ================= + // The target path through the tree, less the summed negative-region slack + // loss of the side paths. A side path pushed below the side_slack_floor + // guard makes the estimate illegal, as a fanin side load does. + static DelayEstimate estimateBufferTreeWithSidePaths( + const EstimatorContext& context, + const RebufferTreePlan& plan, + const std::vector& side_contexts, + std::vector* trace = nullptr, + std::vector* side_evaluations = nullptr); + // The setup path ends through any of through_pins, up to count per path + // group, sorted by slack. + static sta::PathEndSeq findPathEndsThrough( + sta::dbSta* sta, + const std::vector& through_pins, + const sta::MinMaxAll* min_max_all, + int count); }; } // namespace rsz diff --git a/src/rsz/src/DelayEstimatorBufferTree.cc b/src/rsz/src/DelayEstimatorBufferTree.cc new file mode 100644 index 00000000000..ab20dbc0be6 --- /dev/null +++ b/src/rsz/src/DelayEstimatorBufferTree.cc @@ -0,0 +1,609 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +// The buffer-tree part of DelayEstimator: the timing of a RebufferMt tree +// plan along the target path and its side paths. + +#include +#include +#include +#include +#include +#include + +#include "DelayEstimator.hh" +#include "DelayEstimatorInternal.hh" +#include "OptimizerTypes.hh" +#include "Rebuffer.hh" +#include "db_sta/dbSta.hh" +#include "rsz/Resizer.hh" +#include "sta/Delay.hh" +#include "sta/ExceptionPath.hh" +#include "sta/Graph.hh" +#include "sta/Liberty.hh" +#include "sta/LibertyClass.hh" +#include "sta/MinMax.hh" +#include "sta/Network.hh" +#include "sta/Path.hh" +#include "sta/PathEnd.hh" +#include "sta/PathExpanded.hh" +#include "sta/Scene.hh" +#include "sta/Sdc.hh" +#include "sta/SearchClass.hh" +#include "sta/Sta.hh" +#include "sta/TimingArc.hh" + +namespace rsz { + +using estimator_detail::estimateOutputSlew; +using estimator_detail::estimateReceiverInputSlew; +using estimator_detail::kElmoreSlewFactor; +using estimator_detail::kMinSlewRatioDenominator; +using estimator_detail::lookupArcDelayAndSlew; +using estimator_detail::lookupBufferDelayAndSlew; +using estimator_detail::makeEstimate; +using estimator_detail::wireElmoreDelay; + +namespace { + +// Calibration of the buffer-tree model. The factors were fitted against STA +// on the rebuffer accuracy report (report_delay_estimator_accuracy with +// -rebuffer_tree), which prints the estimate of every stage of the planned +// tree next to the delay STA measures after the tree is built. +// +// The effective load of a buffer stage as a fraction of its lumped downstream +// capacitance: on the target path, on a side path, and for a direct load. +constexpr float kBufferTreeTargetEffectiveLoadCapFactor = 0.65f; +constexpr float kBufferTreeSideEffectiveLoadCapFactor = 0.875f; +constexpr float kBufferTreeDirectLoadCapFactor = 0.60f; +// A final buffer whose downstream cap per input slew reaches the ratio takes +// its output slew at the lower effective-load fraction. +constexpr float kBufferTreeHighCapSlewRatio = 2.5e-3f; +constexpr float kBufferTreeHighCapSlewFactor = 0.60f; +// A stage that keeps part of its load keeps its slew at least to this power +// of the load ratio. +constexpr float kLoadRatioSlewFloorExponent = 0.85f; +// Elmore wire delays past the threshold shrink by the scale, and short wires +// add slew at a higher factor than the Elmore slew rule. +constexpr float kBufferTreeWireDelayScaleThreshold = 20.0e-12f; +constexpr float kBufferTreeWireDelayScale = 0.78f; +constexpr float kBufferTreeShortWireSlewFactor = 1.35f; + +float loadRatioSlewFloor(const DelayStageState& stage, const float load_cap) +{ + if (stage.load_cap <= 0.0f || stage.current_slew <= 0.0f + || load_cap <= 0.0f) { + return 0.0f; + } + + const float load_ratio = std::clamp(load_cap / stage.load_cap, 0.0f, 1.0f); + return stage.current_slew * std::pow(load_ratio, kLoadRatioSlewFloorExponent); +} + +float wireOutputSlewFromDelay(const float wire_delay, const float input_slew) +{ + const float slew_factor = wire_delay < kBufferTreeWireDelayScaleThreshold + ? kBufferTreeShortWireSlewFactor + : kElmoreSlewFactor; + const float wire_slew = slew_factor * std::max(wire_delay, 0.0f); + return std::hypot(std::max(input_slew, 0.0f), wire_slew); +} + +float bufferTreeEffectiveLoadCap(const float total_load_cap, const float factor) +{ + // Tree buffers drive distributed RC, not a lumped cap. The gate lookup uses + // a lighter effective cap, while reports and legality keep the full cap. + return std::max(total_load_cap * factor, 0.0f); +} + +float boundBufferTreeDownstreamStageDelay(const DelayStageState& stage, + const float input_slew, + const float stage_delay) +{ + const float current_input_slew + = std::max(stage.input_slew, kMinSlewRatioDenominator); + const float input_slew_ratio + = std::clamp(input_slew / current_input_slew, 0.0f, 1.0f); + // 0.45 + 0.35 * sqrt(ratio) tops out at 0.80 for a ratio of one. + const float min_delay_ratio + = std::max(0.5f, 0.45f + 0.35f * std::sqrt(input_slew_ratio)); + + return std::max(stage_delay, stage.current_delay * min_delay_ratio); +} + +float bufferTreeWireDelay(const WireRC& wire, const float load_cap) +{ + const float raw_delay = wireElmoreDelay(wire, load_cap); + if (raw_delay < kBufferTreeWireDelayScaleThreshold) { + return raw_delay; + } + + return raw_delay * kBufferTreeWireDelayScale; +} + +struct BranchTiming +{ + bool legal{false}; + float delay{0.0f}; + float load_slew{0.0f}; + FailReason reason{FailReason::kNone}; +}; + +// Dummy-root trie cache for replayed buffer-tree branch prefixes. The key +// has no effective-load-cap factor: a prefix the target path shares with a +// side path keeps the timing of whichever was evaluated first. +struct BranchTimingCacheNode +{ + bool valid{false}; + BufferTreePathStage stage; + BranchTiming prefix; + StageEvaluation trace_entry; + std::vector children; +}; + +bool sameWireRC(const WireRC& lhs, const WireRC& rhs) +{ + return lhs.length == rhs.length && lhs.res == rhs.res && lhs.cap == rhs.cap; +} + +bool sameBufferTreePathStage(const BufferTreePathStage& lhs, + const BufferTreePathStage& rhs) +{ + if (lhs.type != rhs.type || lhs.downstream_cap != rhs.downstream_cap + || lhs.drives_distributed_load != rhs.drives_distributed_load) { + return false; + } + + if (lhs.type == BufferTreeStageType::kBuffer) { + return lhs.buffer_cell == rhs.buffer_cell; + } + + return lhs.buffer_cell == rhs.buffer_cell + && sameWireRC(lhs.wire_rc, rhs.wire_rc); +} + +BranchTimingCacheNode* findBranchTimingCacheChild( + BranchTimingCacheNode& node, + const BufferTreePathStage& stage) +{ + for (BranchTimingCacheNode& child : node.children) { + if (child.valid && sameBufferTreePathStage(child.stage, stage)) { + return &child; + } + } + return nullptr; +} + +// The target driver into the tree input, taken from the cache root when it +// already holds it. False when the driver arc lookup fails. +bool branchDriverTiming(const DelayStageState& target_stage, + const RebufferTreePlan& plan, + std::vector* trace, + BranchTimingCacheNode& cache_root, + BranchTiming& branch) +{ + if (cache_root.valid) { + branch = cache_root.prefix; + if (trace != nullptr) { + trace->push_back(cache_root.trace_entry); + } + return true; + } + + // The driver delay is the plain table model at the tree input cap. The + // propagated slew comes from loadRatioSlewFloor(), not from the table. + float driver_delay = 0.0f; + float unused_slew = 0.0f; + if (!lookupArcDelayAndSlew(target_stage.arc, + target_stage.input_slew, + plan.tree_input_cap, + target_stage.arc.currentCell(), + driver_delay, + unused_slew)) { + return false; + } + + const float propagated_slew + = loadRatioSlewFloor(target_stage, plan.tree_input_cap); + const StageEvaluation driver_trace{.path_index = target_stage.path_index, + .cell = target_stage.arc.currentCell(), + .input_slew = target_stage.input_slew, + .load_cap = plan.tree_input_cap, + .stage_delay = driver_delay, + .output_slew = propagated_slew, + .wire_delay = 0.0f}; + branch = {.legal = true, + .delay = driver_delay, + .load_slew = propagated_slew, + .reason = FailReason::kEstimateLegal}; + + cache_root.valid = true; + cache_root.prefix = branch; + cache_root.trace_entry = driver_trace; + + if (trace != nullptr) { + trace->push_back(driver_trace); + } + + return true; +} + +// The timing of tree stage stage_index after branch: wires and vias add +// their Elmore delay, buffers add a Liberty lookup on the effective load. +// False when the buffer arc lookup fails. +bool treeStageTiming(const DelayStageState& target_stage, + const BufferTreeLoadPath& load_path, + const size_t stage_index, + const float path_effective_load_cap_factor, + const BranchTiming& branch, + StageEvaluation& stage_trace, + BranchTiming& next_branch) +{ + const BufferTreePathStage& tree_stage = load_path.stages[stage_index]; + switch (tree_stage.type) { + case BufferTreeStageType::kWire: + case BufferTreeStageType::kVia: { + const float input_slew = branch.load_slew; + const float wire_delay + = bufferTreeWireDelay(tree_stage.wire_rc, tree_stage.downstream_cap); + const float output_slew = wireOutputSlewFromDelay(wire_delay, input_slew); + stage_trace = {.path_index = target_stage.path_index, + .cell = nullptr, + .input_slew = input_slew, + .load_cap = tree_stage.downstream_cap, + .stage_delay = wire_delay, + .output_slew = output_slew, + .wire_delay = wire_delay}; + next_branch = {.legal = true, + .delay = branch.delay + wire_delay, + .load_slew = output_slew, + .reason = FailReason::kEstimateLegal}; + break; + } + case BufferTreeStageType::kBuffer: { + const float input_slew = branch.load_slew; + + // Distributed and direct load trees use separate effective-cap models, + // because STA's distributed RC is not a lumped-cap Liberty lookup. + const float gate_load_cap = bufferTreeEffectiveLoadCap( + tree_stage.downstream_cap, + tree_stage.drives_distributed_load ? path_effective_load_cap_factor + : kBufferTreeDirectLoadCapFactor); + + float buffer_delay = 0.0f; + float buffer_output_slew = 0.0f; + if (!lookupBufferDelayAndSlew(target_stage, + tree_stage.buffer_cell, + input_slew, + gate_load_cap, + buffer_delay, + buffer_output_slew)) { + return false; + } + + const bool high_cap_final_buffer + = tree_stage.drives_distributed_load + && !load_path.hasBufferAfter(stage_index) && input_slew > 0.0f + && tree_stage.downstream_cap / input_slew + >= kBufferTreeHighCapSlewRatio; + if (high_cap_final_buffer) { + float unused_buffer_delay = 0.0f; + float high_cap_output_slew = 0.0f; + const float slew_load_cap = bufferTreeEffectiveLoadCap( + tree_stage.downstream_cap, kBufferTreeHighCapSlewFactor); + if (lookupBufferDelayAndSlew(target_stage, + tree_stage.buffer_cell, + input_slew, + slew_load_cap, + unused_buffer_delay, + high_cap_output_slew)) { + buffer_output_slew = high_cap_output_slew; + } + } + + stage_trace = {.path_index = target_stage.path_index, + .cell = tree_stage.buffer_cell, + .input_slew = input_slew, + .load_cap = tree_stage.downstream_cap, + .stage_delay = buffer_delay, + .output_slew = buffer_output_slew, + .wire_delay = 0.0f}; + next_branch = {.legal = true, + .delay = branch.delay + buffer_delay, + .load_slew = buffer_output_slew, + .reason = FailReason::kEstimateLegal}; + break; + } + } + + return true; +} + +BranchTiming estimateBranchTiming(const DelayStageState& target_stage, + const RebufferTreePlan& plan, + const BufferTreeLoadPath& load_path, + const float path_effective_load_cap_factor, + std::vector* trace, + BranchTimingCacheNode& cache_root) +{ + BranchTiming branch; + if (!branchDriverTiming(target_stage, plan, trace, cache_root, branch)) { + return {.legal = false, .reason = FailReason::kMissingCurrentTimingArc}; + } + + BranchTimingCacheNode* parent = &cache_root; + for (size_t stage_index = 0; stage_index < load_path.stages.size(); + ++stage_index) { + const BufferTreePathStage& tree_stage = load_path.stages[stage_index]; + BranchTimingCacheNode* cached_child + = findBranchTimingCacheChild(*parent, tree_stage); + if (cached_child != nullptr) { + branch = cached_child->prefix; + parent = cached_child; + if (trace != nullptr) { + trace->push_back(cached_child->trace_entry); + } + continue; + } + + StageEvaluation stage_trace; + BranchTiming next_branch; + if (!treeStageTiming(target_stage, + load_path, + stage_index, + path_effective_load_cap_factor, + branch, + stage_trace, + next_branch)) { + return {.legal = false, .reason = FailReason::kMissingBufferTimingArc}; + } + + parent->children.push_back({.valid = true, + .stage = tree_stage, + .prefix = next_branch, + .trace_entry = stage_trace, + .children = {}}); + parent = &parent->children.back(); + + branch = next_branch; + if (trace != nullptr) { + trace->push_back(stage_trace); + } + } + + return branch; +} + +DelayEstimate estimateBufferTreeWindow(const EstimatorContext& context, + const RebufferTreePlan& plan, + std::vector* trace, + BranchTimingCacheNode& branch_cache) +{ + // RebufferMt plans always have buffers and a target load path. + if (!std::isfinite(plan.tree_input_cap)) { + return {.legal = false, .reason = FailReason::kInvalidBufferInsertion}; + } + + const BufferTreeLoadPath& target_load_path = *plan.targetPath(); + const std::vector& stages = context.path_stages; + const int target_index = context.target_stage_index; + const DelayStageState& target_stage = context.target(); + const float current_total_delay + = context.current_total_delay + target_stage.current_wire_delay; + + float candidate_total_delay = 0.0f; + + // Pre-target stages replay unchanged. + for (int stage_index = 0; stage_index < target_index; ++stage_index) { + const DelayStageState& stage = stages[stage_index]; + candidate_total_delay += stage.current_delay; + if (trace != nullptr) { + trace->push_back({.path_index = stage.path_index, + .cell = stage.arc.currentCell(), + .input_slew = stage.input_slew, + .load_cap = stage.load_cap, + .stage_delay = stage.current_delay, + .output_slew = stage.current_slew, + .wire_delay = stage.current_wire_delay}); + } + } + + // Target driver + buffer-tree branch (shared with side-paths via cache). + const BranchTiming branch + = estimateBranchTiming(target_stage, + plan, + target_load_path, + kBufferTreeTargetEffectiveLoadCapFactor, + trace, + branch_cache); + if (!branch.legal) { + return {.legal = false, .reason = branch.reason}; + } + + candidate_total_delay += branch.delay; + float propagated_slew = branch.load_slew; + + for (int stage_index = target_index + 1; + stage_index < static_cast(stages.size()); + ++stage_index) { + const DelayStageState& stage = stages[stage_index]; + const float input_slew + = stage_index == target_index + 1 + ? propagated_slew + : estimateReceiverInputSlew( + stages[stage_index - 1], stage, propagated_slew); + + float model_stage_delay = 0.0f; + float stage_slew = 0.0f; + if (!lookupArcDelayAndSlew(stage.arc, + input_slew, + stage.load_cap, + stage.arc.currentCell(), + model_stage_delay, + stage_slew)) { + return {.legal = false, .reason = FailReason::kMissingCurrentTimingArc}; + } + + const float stage_delay = boundBufferTreeDownstreamStageDelay( + stage, + input_slew, + stage.current_delay + (model_stage_delay - stage.current_model_delay)); + candidate_total_delay += stage_delay; + propagated_slew = estimateOutputSlew( + stage, stage.arc.currentCell(), input_slew, stage.load_cap, stage_slew); + + if (trace != nullptr) { + trace->push_back({.path_index = stage.path_index, + .cell = stage.arc.currentCell(), + .input_slew = input_slew, + .load_cap = stage.load_cap, + .stage_delay = stage_delay, + .output_slew = propagated_slew, + .wire_delay = stage.current_wire_delay}); + } + } + + return makeEstimate(current_total_delay, candidate_total_delay); +} + +DelayEstimate estimateSidePathWindow(const EstimatorContext& target_context, + const RebufferTreePlan& plan, + const BufferTreeSidePathContext& side, + std::vector* trace, + BranchTimingCacheNode& branch_cache) +{ + // Side contexts come from the plan's own load paths. + const BufferTreeLoadPath& load_path = *plan.loadPath(side.load_pin); + const DelayStageState& target_stage = target_context.target(); + const BranchTiming branch + = estimateBranchTiming(target_stage, + plan, + load_path, + kBufferTreeSideEffectiveLoadCapFactor, + trace, + branch_cache); + if (!branch.legal) { + return {.legal = false, .reason = branch.reason}; + } + + const float current_total_delay + = target_stage.current_delay + side.current_driver_to_load_delay; + return makeEstimate(current_total_delay, branch.delay); +} + +} // namespace + +sta::PathEndSeq DelayEstimator::findPathEndsThrough( + sta::dbSta* sta, + const std::vector& through_pins, + const sta::MinMaxAll* min_max_all, + const int count) +{ + // The search owns the exception objects. + sta::PinSet* pins = new sta::PinSet(sta->network()); + for (sta::Pin* pin : through_pins) { + pins->insert(pin); + } + + sta::ExceptionThru* through = sta->makeExceptionThru( + pins, nullptr, nullptr, sta::RiseFallBoth::riseFall(), sta->cmdSdc()); + + sta::ExceptionThruSeq* thrus = new sta::ExceptionThruSeq; + thrus->push_back(through); + sta::StringSeq group_names; + + return sta->findPathEnds(nullptr, + thrus, + nullptr, + false, + sta->scenes(), + min_max_all, + count, + count, + false, + false, + -sta::INF, + sta::INF, + true, + group_names, + true, + false, + false, + false, + false, + false); +} + +DelayEstimate DelayEstimator::estimateBufferTreeWithSidePaths( + const EstimatorContext& context, + const RebufferTreePlan& plan, + const std::vector& side_contexts, + std::vector* trace, + std::vector* side_evaluations) +{ + if (side_evaluations != nullptr) { + side_evaluations->clear(); + side_evaluations->reserve(side_contexts.size()); + } + + if (trace != nullptr) { + trace->clear(); + } + + BranchTimingCacheNode branch_cache; + DelayEstimate estimate + = estimateBufferTreeWindow(context, plan, trace, branch_cache); + const bool target_estimate_available + = estimate.reason == FailReason::kEstimateLegal + || estimate.reason == FailReason::kEstimateNonImproving; + if (!target_estimate_available) { + return estimate; + } + + // Side loads pay for their buffered detour below zero slack. Only a drop + // below the side slack floor rejects the plan. + for (const BufferTreeSidePathContext& side_context : side_contexts) { + std::vector side_trace; + const DelayEstimate side_estimate = estimateSidePathWindow( + context, + plan, + side_context, + side_evaluations != nullptr ? &side_trace : nullptr, + branch_cache); + const bool side_estimate_available + = side_estimate.reason == FailReason::kEstimateLegal + || side_estimate.reason == FailReason::kEstimateNonImproving; + const float side_impr = side_estimate_available + ? side_estimate.arrival_impr + : -std::numeric_limits::infinity(); + const float slack_after = side_context.current_slack + side_impr; + estimate.side_paths_evaluated++; + + bool side_legal = side_estimate_available; + if (!side_estimate_available) { + estimate.legal = false; + estimate.reason = side_estimate.reason; + } else { + estimate.side_loss += side_context.weight + * (std::min(side_context.current_slack, 0.0f) + - std::min(slack_after, 0.0f)); + if (slack_after < context.sideSlackFloor(estimate.arrival_impr)) { + side_legal = false; + estimate.legal = false; + estimate.reason = FailReason::kSideSlackFloorViolation; + } + } + + if (side_evaluations != nullptr) { + side_evaluations->push_back({.load_pin = side_context.load_pin, + .legal = side_legal, + .arrival_impr = side_impr, + .slack_after = slack_after, + .trace = side_trace}); + } + } + + return estimate; +} + +} // namespace rsz diff --git a/src/rsz/src/DelayEstimatorInternal.hh b/src/rsz/src/DelayEstimatorInternal.hh new file mode 100644 index 00000000000..1291f006345 --- /dev/null +++ b/src/rsz/src/DelayEstimatorInternal.hh @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#pragma once + +#include "OptimizerTypes.hh" + +namespace sta { +class LibertyCell; +} // namespace sta + +// Helpers DelayEstimator.cc shares with the buffer-tree estimator in +// DelayEstimatorBufferTree.cc. Not an API for other files. +namespace rsz::estimator_detail { + +inline constexpr float kMinSlewRatioDenominator = 1.0e-15f; +inline constexpr float kElmoreSlewFactor = 1.39f; + +// Delay and output slew of `cell` on the selected arc. +bool lookupArcDelayAndSlew(const SelectedArc& arc, + float input_slew, + float load_cap, + const sta::LibertyCell* cell, + float& delay, + float& output_slew, + ArcMatchMode match_mode = ArcMatchMode::kExact, + bool* match_relaxed_out = nullptr); +// Delay and output slew of the inserted buffer on the target transition. +bool lookupBufferDelayAndSlew(const DelayStageState& target_stage, + const sta::LibertyCell* buffer_cell, + float input_slew, + float load_cap, + float& delay, + float& output_slew); +// Calibrated output slew of `stage` with `cell` driving load_cap. +float estimateOutputSlew(const DelayStageState& stage, + const sta::LibertyCell* cell, + float path_input_slew, + float load_cap, + float selected_output_slew, + ArcMatchMode merge_arc_mode = ArcMatchMode::kExact, + bool* slew_relaxed_out = nullptr); +// Receiver input slew for a new driver output slew, keeping the current +// wire degradation ratio. +float estimateReceiverInputSlew(const DelayStageState& driver_stage, + const DelayStageState& receiver_stage, + float candidate_driver_output_slew); +float wireElmoreDelay(const WireRC& wire, float load_cap); +DelayEstimate makeEstimate(float current_delay, float candidate_delay); + +} // namespace rsz::estimator_detail diff --git a/src/rsz/src/DelayEstimatorReporter.hh b/src/rsz/src/DelayEstimatorReporter.hh index fec59663db4..4bdac5c8a61 100644 --- a/src/rsz/src/DelayEstimatorReporter.hh +++ b/src/rsz/src/DelayEstimatorReporter.hh @@ -39,13 +39,8 @@ class Resizer; // === Diagnostic estimator-accuracy reporter ================================= // -// Compares the candidate-cell delay improvement predicted by DelayEstimator -// (legacy or path-window MT estimator) against ground-truth STA results that -// are obtained by actually swapping the cell inside an ECO journal and then -// undoing the swap. -// -// Used by the Tcl command `report_delay_estimator_accuracy`. -// All compute paths are main-thread-only; no MT contract here. +// Compares predicted ECO timing gain with STA measured inside a reverted ECO. +// The report_delay_estimator_accuracy command runs this on the main thread. class DelayEstimatorReporter { public: @@ -68,6 +63,7 @@ class DelayEstimatorReporter explicit DelayEstimatorReporter(Resizer& resizer); + // Compare a temporary cell swap's predicted and STA-measured path gain. void reportAccuracyForSizing(sta::Instance* inst, sta::LibertyCell* replacement, const std::string& estimator, @@ -85,6 +81,7 @@ class DelayEstimatorReporter static Role roleFor(int stage_index, int target_index); // === Diagnostic data structures ========================================== + // Worst matching target path through the requested instance. struct TargetMatch { Target target; @@ -92,6 +89,7 @@ class DelayEstimatorReporter sta::Slack slack{0.0}; }; + // Electrical inputs and delay of one reported timing stage. struct StageProfileRow { int path_index{-1}; @@ -108,6 +106,7 @@ class DelayEstimatorReporter bool arc_match_relaxed{false}; }; + // Model-predicted stage rows and legality for one candidate. struct EstimatorProfile { std::vector current_stages; @@ -124,6 +123,7 @@ class DelayEstimatorReporter bool candidate_stages_incomplete{false}; }; + // STA-measured path stages and arrival change around a temporary ECO. struct GoldenProfile { std::vector before_stages; @@ -134,6 +134,7 @@ class DelayEstimatorReporter float arrival_impr{0.0f}; }; + // Stable pin identities and prepared timing for one path stage. struct FixedStage { DelayStageState state; @@ -144,6 +145,7 @@ class DelayEstimatorReporter }; // === Run-orchestration phases ============================================ + // Apply a temporary cell swap and measure the same path after timing update. void measureGoldenAfterSwap(const TargetMatch& before_match, const std::vector& fixed_stages, GoldenProfile& golden_profile) const; @@ -156,18 +158,23 @@ class DelayEstimatorReporter const GoldenProfile& golden_profile) const; // === Target selection ==================================================== + // Find the worst path-driver target through the requested instance. std::optional findWorstTargetForInstance(sta::Instance* inst, int delay_levels) const; std::vector outputPins(sta::Instance* inst) const; // === Profile construction ================================================ + // Build the original serial estimator's stage prediction. EstimatorProfile buildLegacyProfile(const Target& target, sta::LibertyCell* replacement) const; + // Build the path-window estimator's stage prediction. EstimatorProfile buildDelayEstimatorProfile(const Target& target, sta::LibertyCell* replacement, int delay_levels) const; + // Preserve path stage identities before the ECO invalidates path handles. std::vector captureFixedStages(const Target& target, int delay_levels) const; + // Measure STA delay through the fixed stages in the current design. std::vector buildGoldenStageRows( const std::vector& fixed_stages) const; StageProfileRow buildGoldenStageRow(const FixedStage& fixed_stage) const; @@ -178,6 +185,7 @@ class DelayEstimatorReporter int target_index) const; // === Path / context utilities ============================================ + // Prepare the same delay window used by MT candidate scoring. std::optional contextForTarget( const Target& target, int delay_levels, diff --git a/src/rsz/src/MoveCommitter.cc b/src/rsz/src/MoveCommitter.cc index 73dcb8cbc66..277c0369a23 100644 --- a/src/rsz/src/MoveCommitter.cc +++ b/src/rsz/src/MoveCommitter.cc @@ -614,7 +614,6 @@ void MoveCommitter::rejectPendingMoves() const int move_count = pendingMoveCount(); logPendingMoves("UNDO", move_count); resizer_.addRejectedLegacyMoveCount(move_count); - std::ranges::fill(pending_by_type_, 0); ++netlist_edits_; for (std::unordered_multiset& pending_instances : diff --git a/src/rsz/src/OptimizationPolicyConfig.hh b/src/rsz/src/OptimizationPolicyConfig.hh index 448634d349f..d17ed25fc02 100644 --- a/src/rsz/src/OptimizationPolicyConfig.hh +++ b/src/rsz/src/OptimizationPolicyConfig.hh @@ -37,12 +37,28 @@ struct OptimizationPolicyConfig // Drop replacements that do not drive at least as strong on rise and fall. bool dual_rf_drive_guard{true}; - // Weight of the SizeUpMt score; VtSwapMt keeps 1. + // Weight of the SizeUpMt score in the shared pool; VtSwapMt keeps 1. float size_up_mt_score_scale{0.75f}; // Largest drive-resistance ratio (current / replacement) of a size-up; // zero means no bound. float size_up_mt_max_drive_ratio{0.0f}; + + // --- RebufferMt ----------------------------------------------------------- + + // Weight of the RebufferMt score in the shared pool. + float rebuffer_mt_score_scale{0.10f}; + + // Minimum and maximum raw score of a buffer tree, in seconds; a maximum of + // zero or less turns the upper gate off. + float rebuffer_mt_min_score{5.0e-12f}; + float rebuffer_mt_max_score{45.0e-12f}; + + // Buffer trees generated per target. + int rebuffer_mt_max_candidates{3}; + + // Let one-buffer trees compete in the score pool. + bool allow_rebuffer_mt_single_buffer{false}; }; } // namespace rsz diff --git a/src/rsz/src/OptimizerTypes.cc b/src/rsz/src/OptimizerTypes.cc index 4ba8f179cb9..51cca0af163 100644 --- a/src/rsz/src/OptimizerTypes.cc +++ b/src/rsz/src/OptimizerTypes.cc @@ -57,6 +57,12 @@ const char* failReasonName(const FailReason reason) return "missing_candidate_port"; case FailReason::kMissingCandidateTimingArc: return "missing_candidate_timing_arc"; + + // Buffer insertion timing context. + case FailReason::kInvalidBufferInsertion: + return "invalid_buffer_insertion"; + case FailReason::kMissingBufferTimingArc: + return "missing_buffer_timing_arc"; case FailReason::kSideSlackFloorViolation: return "side_slack_floor_violation"; case FailReason::kDualRfNonImproving: diff --git a/src/rsz/src/OptimizerTypes.hh b/src/rsz/src/OptimizerTypes.hh index f56b6acbfab..59d1c482d3a 100644 --- a/src/rsz/src/OptimizerTypes.hh +++ b/src/rsz/src/OptimizerTypes.hh @@ -75,6 +75,10 @@ enum class FailReason : uint8_t kMissingCandidatePort, kMissingCandidateTimingArc, + // Buffer insertion timing context. + kInvalidBufferInsertion, + kMissingBufferTimingArc, + // A modeled side path pushed below the prepared global worst slack. kSideSlackFloorViolation, @@ -126,6 +130,9 @@ using PrepareCacheMask = uint32_t; inline constexpr PrepareCacheMask kNoPrepareCache = 0; inline constexpr PrepareCacheMask kEstimatorContextCache = 1u << 0; +// The flip-flop input-side checks a cell swap of the target may disturb +// (SequentialTimingEstimator); needs kEstimatorContextCache. +inline constexpr PrepareCacheMask kSequentialEstimatorCache = 1u << 8; // Generation inputs: one Mt generator's live STA reads, prepared on the main // thread so its generate() can run on a worker. Without them, the cell-swap @@ -194,9 +201,12 @@ struct SlewBiasSample float sta_slew{0.0f}; }; +// Three sampled corrections from Liberty output slew to current STA slew. struct SlewBiasModel { + // False when no stable correction could be sampled. bool valid{false}; + // Arc that controls the merged output slew at the sampled operating point. const sta::TimingArc* table_worst_arc{nullptr}; float input_slew{0.0f}; std::array samples; @@ -223,6 +233,7 @@ struct DelayStageState float load_cap{0.0f}; float current_model_delay{0.0f}; // Table-model cell delay baseline float current_delay{0.0f}; // STA graph cell delay baseline + float current_wire_delay{0.0f}; // Selected path wire delay after driver float current_model_slew{0.0f}; // Table-model merged slew baseline float current_slew{0.0f}; // STA graph merged slew baseline std::vector output_slew_merge_arcs; @@ -308,6 +319,14 @@ struct DelayEstimate FailReason reason{FailReason::kNone}; }; +// Lumped resistance and capacitance of one estimated wire segment. +struct WireRC +{ + float length{0.0f}; + float res{0.0f}; + float cap{0.0f}; +}; + // === Optimization target model ============================================= // One optimization target selected by a policy. A Target is the handle a @@ -335,6 +354,7 @@ using TargetViewMask = uint8_t; inline constexpr TargetViewMask kPathDriverView = 1u << 0; inline constexpr TargetViewMask kInstanceView = 1u << 1; +// Repairable instance or path driver with timing context prepared for moves. struct Target { // === Target identity ====================================================== @@ -363,6 +383,7 @@ struct Target // === Prepared data for MT generation/estimation ========================== std::optional estimator_context; + // Prepared flip-flop input-side checks for candidate Q-path swaps. std::shared_ptr sequential_timing_estimator; // === Generation inputs (kGenerationInputCaches) =========================== diff --git a/src/rsz/src/Rebuffer.cc b/src/rsz/src/Rebuffer.cc index 9a4046b1bf9..9d609e778c5 100644 --- a/src/rsz/src/Rebuffer.cc +++ b/src/rsz/src/Rebuffer.cc @@ -4,6 +4,7 @@ #include "Rebuffer.hh" #include +#include #include #include #include @@ -14,11 +15,13 @@ #include #include #include +#include #include #include #include "BufferedNet.hh" #include "RepairSetupProfiler.hh" +#include "boost/functional/hash.hpp" #include "db_sta/dbNetwork.hh" #include "db_sta/dbSta.hh" #include "est/EstimateParasitics.h" @@ -37,6 +40,7 @@ #include "sta/Mode.hh" #include "sta/NetworkClass.hh" #include "sta/Path.hh" +#include "sta/PathExpanded.hh" #include "sta/PortDirection.hh" #include "sta/Search.hh" #include "sta/StaState.hh" @@ -58,10 +62,38 @@ using BnetSeq = BufferedNetSeq; using BnetPtr = BufferedNetPtr; using BnetMetrics = BufferedNet::Metrics; +namespace { + +// The slack a tree may give back for area in RebufferMt planning: 1 ps while +// the best tree still violates, 20 ps once it has margin (slackLossFloor()). +constexpr float kNegativeSlackLossBudget = 1.0e-12f; +constexpr float kPositiveSlackLossBudget = 20.0e-12f; + +// Direct load or next-buffer stages are lumped loads; wires and branches are +// distributed loads for buffer Liberty lookup. +bool drivesDistributedLoadBeforeNextBuffer(const BufferedNetPtr& tree) +{ + switch (tree->type()) { + case BnetType::load: + case BnetType::buffer: + return false; + case BnetType::wire: + case BnetType::via: + case BnetType::junction: + return true; + default: + return true; + } +} + +} // namespace + Rebuffer::Rebuffer(Resizer* resizer) : resizer_(resizer) { } +Rebuffer::~Rebuffer() = default; + void Rebuffer::annotateLoadSlacks(BnetPtr& tree, sta::Vertex* root_vertex) { for (auto rf_index : sta::RiseFall::rangeIndex()) { @@ -87,12 +119,7 @@ void Rebuffer::annotateLoadSlacks(BnetPtr& tree, sta::Vertex* root_vertex) while (req_path && arrival_path->vertex(sta_) != root_vertex) { arrival_path = arrival_path->prevPath(); if (!arrival_path) { - logger_->warn( - RSZ, - 2006, - "failed to trace timing path for load {} when buffering {}", - network_->name(load_pin), - network_->name(pin_)); + warnPathTraceFailed(load_pin); break; } } @@ -223,14 +250,14 @@ std::tuple Rebuffer::drvrPinTiming( driver_path->minMax(sta_)); auto dcalc_result - = arc_delay_calc_->gateDelay(nullptr, - driver_arc, - slew, - bnet->cap() + drvr_port_->capacitance(), - nullptr, - load_pin_index_map, - driver_path->scene(sta_), - driver_path->minMax(sta_)); + = delayCalc()->gateDelay(nullptr, + driver_arc, + slew, + bnet->cap() + drvr_port_->capacitance(), + nullptr, + load_pin_index_map, + driver_path->scene(sta_), + driver_path->minMax(sta_)); rf_delay = dcalc_result.gateDelay(); sta::Arrival prev_arrival = driver_path->isClock(sta_) @@ -534,7 +561,7 @@ BnetPtr Rebuffer::attemptTopologyRewrite(const BnetPtr& node, // Find a buffer size with smallest input pin capacitance such that // crit2 stays being the critical branch; if none can be found // abort rewriting - for (BufferSize size : buffer_sizes_) { + for (const BufferSize& size : buffer_sizes_) { sta::LibertyPort *in, *out; size.cell->bufferPorts(in, out); @@ -582,9 +609,11 @@ static std::optional findWireLayer(BnetPtr node) return {}; } -// Find initial timing-optimized buffering choice over the provided tree -BnetPtr Rebuffer::bufferForTiming(const BnetPtr& tree, - bool allow_topology_rewrite) +// Find ranked timing-optimized buffering choices over the provided tree. +std::vector Rebuffer::bufferForTimingTopK( + const BnetPtr& tree, + bool allow_topology_rewrite, + int max_plans) { sta::LibertyPort* strong_driver; { @@ -823,18 +852,169 @@ BnetPtr Rebuffer::bufferForTiming(const BnetPtr& tree, tree); if (top_opts.empty()) { - logger_->warn( - RSZ, - 2009, - "Skipping buffering because no valid buffering solution satisfying the " - "design rules can be found for net connected to pin {}.", - network_->name(pin_)); - return nullptr; + warnNoDesignRuleSolution(); + return {}; } - FixedDelay best_slack = -FixedDelay::INF; - BufferedNetPtr best_option = nullptr; - int best_index = 0; + return selectTimingOptions(top_opts, max_plans); +} + +void Rebuffer::warnTopPortDriver(const sta::Pin* drvr_pin) +{ + logger_->warn(RSZ, + 2020, + "rebuffering does not support top port as the driver pin: {}", + network_->name(drvr_pin)); +} + +void Rebuffer::warnBufferedNetFailed(const sta::Pin* drvr_pin) +{ + logger_->warn(RSZ, + 75, + "makeBufferedNet failed for driver {}", + network_->pathName(drvr_pin)); +} + +void Rebuffer::errorAreaRecoveryFailed() +{ + logger_->error(RSZ, 2022, "failed area recovery"); +} + +void Rebuffer::errorAreaRecoveryUnreproducible() +{ + logger_->error( + RSZ, + 501, + "buffering pin {} failed: area recovery cannot reproduce solution", + network_->name(pin_)); +} + +void Rebuffer::warnPathTraceFailed(const sta::Pin* load_pin) +{ + logger_->warn(RSZ, + 2006, + "failed to trace timing path for load {} when buffering {}", + network_->name(load_pin), + network_->name(pin_)); +} + +void Rebuffer::warnNoDesignRuleSolution() +{ + logger_->warn(RSZ, + 2009, + "Skipping buffering because no valid buffering solution " + "satisfying the design rules can be found for net " + "connected to pin {}.", + network_->name(pin_)); +} + +void Rebuffer::warnNoViableBuffering(const sta::Pin* drvr_pin, const int rounds) +{ + logger_->warn(RSZ, + 2021, + "cannot find a viable buffering solution on pin {} after {} " + "rounds of buffering (no solution meets design rules)", + network_->name(drvr_pin), + rounds); +} + +bool Rebuffer::timingOrder(const RankedOption& lhs, const RankedOption& rhs) +{ + if (lhs.unconstrained != rhs.unconstrained) { + return lhs.unconstrained; + } + if (lhs.slack.toSeconds() != rhs.slack.toSeconds()) { + return lhs.slack > rhs.slack; + } + if (lhs.option->cap() != rhs.option->cap()) { + return lhs.option->cap() < rhs.option->cap(); + } + if (lhs.option->area() != rhs.option->area()) { + return lhs.option->area() < rhs.option->area(); + } + return lhs.option->bufferCount() < rhs.option->bufferCount(); +} + +bool Rebuffer::areaOrder(const RankedOption& lhs, const RankedOption& rhs) +{ + if (lhs.meets_target != rhs.meets_target) { + return lhs.meets_target; + } + if (lhs.preserves_best_slack != rhs.preserves_best_slack) { + return lhs.preserves_best_slack; + } + if (!lhs.preserves_best_slack + && lhs.slack.toSeconds() != rhs.slack.toSeconds()) { + return lhs.slack > rhs.slack; + } + if (lhs.meets_target && lhs.preserves_best_slack + && lhs.option->area() != rhs.option->area()) { + return lhs.option->area() < rhs.option->area(); + } + if (lhs.option->cap() != rhs.option->cap()) { + return lhs.option->cap() < rhs.option->cap(); + } + if (lhs.option->bufferCount() != rhs.option->bufferCount()) { + return lhs.option->bufferCount() < rhs.option->bufferCount(); + } + if (lhs.slack.toSeconds() != rhs.slack.toSeconds()) { + return lhs.slack > rhs.slack; + } + return lhs.index < rhs.index; +} + +FixedDelay Rebuffer::slackLossFloor(const FixedDelay best_slack) const +{ + const float slack_loss_budget = best_slack.toSeconds() < 0.0f + ? kNegativeSlackLossBudget + : kPositiveSlackLossBudget; + return best_slack - FixedDelay(slack_loss_budget, resizer_); +} + +std::vector Rebuffer::selectTimingOptions( + const BnetSeq& top_opts, + const int max_plans) +{ + if (max_plans == 1) { + FixedDelay best_slack = -FixedDelay::INF; + BufferedNetPtr best_option = nullptr; + int best_index = 0; + int i = 1; + debugPrint(logger_, RSZ, "rebuffer", 2, "timing-optimized options"); + for (const BufferedNetPtr& p : top_opts) { + // Find slack for drvr_pin into option. + std::optional slack = evaluateOption(p, i); + + if (!slack) { + // ignore this option as it doesn't pass ERC + continue; + } + + if (best_option == nullptr + || p->slackTransition() == nullptr /* buffer tree unconstrained */ + || slack.value() > best_slack) { + best_slack = slack.value(); + best_option = p; + best_index = i; + } + i++; + } + + if (best_option) { + debugPrint(logger_, RSZ, "rebuffer", 2, "best option {}", best_index); + return {RankedOption{ + .option = best_option, + .slack = best_slack, + .unconstrained = best_option->slackTransition() == nullptr, + .index = best_index}}; + } + + debugPrint(logger_, RSZ, "rebuffer", 2, "no available option"); + return {}; + } + + std::vector ranked_options; + ranked_options.reserve(top_opts.size()); int i = 1; debugPrint(logger_, RSZ, "rebuffer", 2, "timing-optimized options"); for (const BufferedNetPtr& p : top_opts) { @@ -846,22 +1026,44 @@ BnetPtr Rebuffer::bufferForTiming(const BnetPtr& tree, continue; } - if (best_option == nullptr - || p->slackTransition() == nullptr /* buffer tree unconstrained */ - || slack.value() > best_slack) { - best_slack = slack.value(); - best_option = p; - best_index = i; - } + ranked_options.push_back({.option = p, + .slack = slack.value(), + .unconstrained = p->slackTransition() == nullptr, + .index = i}); i++; } - if (best_option) { - debugPrint(logger_, RSZ, "rebuffer", 2, "best option {}", best_index); + + std::ranges::sort(ranked_options, timingOrder); + + if (!ranked_options.empty()) { + debugPrint(logger_, + RSZ, + "rebuffer", + 2, + "best option {}", + ranked_options.front().index); } else { debugPrint(logger_, RSZ, "rebuffer", 2, "no available option"); } - return best_option; + const int selected_count + = std::min(max_plans, static_cast(ranked_options.size())); + ranked_options.erase(ranked_options.begin() + selected_count, + ranked_options.end()); + return ranked_options; +} + +// Find initial timing-optimized buffering choice over the provided tree. +BnetPtr Rebuffer::bufferForTiming(const BnetPtr& tree, + bool allow_topology_rewrite) +{ + const std::vector top_options + = bufferForTimingTopK(tree, allow_topology_rewrite, 1); + if (top_options.empty()) { + return nullptr; + } + + return top_options.front().option; } static void pruneCapVsAreaOptions(sta::StaState* sta, BufferedNetSeq& options) @@ -917,10 +1119,8 @@ void Rebuffer::insertAssuredOption(BnetSeq& opts, opts.insert(it, std::move(assured_opt)); } -// Recover area on a rebuffering choice without regressing timing -BufferedNetPtr Rebuffer::recoverArea(const BufferedNetPtr& root, - FixedDelay slack_target, - float alpha) +FixedDelay Rebuffer::annotateArrivalDelays(const BufferedNetPtr& root, + FixedDelay slack_target) { sta::Delay slack_correction; std::tie(std::ignore, slack_correction, std::ignore) = drvrPinTiming(root); @@ -931,7 +1131,6 @@ BufferedNetPtr Rebuffer::recoverArea(const BufferedNetPtr& root, slack_correction = 0; slack_target = -FixedDelay::INF; } - // spread down arrival delay from the root of the tree to each // point on the tree visitTree( @@ -956,7 +1155,15 @@ BufferedNetPtr Rebuffer::recoverArea(const BufferedNetPtr& root, root, -FixedDelay(slack_correction, resizer_)); - BnetSeq top_opts = visitTree( + return slack_target; +} + +BnetSeq Rebuffer::areaRecoveryOptions(const BufferedNetPtr& root, + const FixedDelay slack_target, + const float alpha, + const bool strict_assured) +{ + return visitTree( [&](auto& recurse, int level, const BnetPtr& node, int upstream_wl) -> BufferedNetSeq { switch (node->type()) { @@ -977,12 +1184,17 @@ BufferedNetPtr Rebuffer::recoverArea(const BufferedNetPtr& root, FixedDelay threshold = FixedDelay::lerp( node->slack(), slack_target + node->arrivalDelay(), alpha); - insertBufferOptions(opts, - level, - /*next_segment_wl=*/upstream_wl, - /*area_oriented=*/true, - threshold, - /*exemplar=*/node.get()); + const bool valid_options + = insertBufferOptions(opts, + level, + /*next_segment_wl=*/upstream_wl, + /*area_oriented=*/true, + threshold, + /*exemplar=*/node.get(), + strict_assured); + if (!valid_options) { + return {}; + } return opts; } case BnetType::junction: { @@ -1038,6 +1250,12 @@ BufferedNetPtr Rebuffer::recoverArea(const BufferedNetPtr& root, } } + if (!assured_found && assured_fallback == nullptr) { + if (!strict_assured) { + return {}; + } + errorAreaRecoveryUnreproducible(); + } if (!assured_found) { insertAssuredOption(opts, assured_fallback, level); } @@ -1060,6 +1278,103 @@ BufferedNetPtr Rebuffer::recoverArea(const BufferedNetPtr& root, }, root, 0); +} + +// Recover area on rebuffering choices without regressing timing. +std::vector Rebuffer::recoverAreaTopK( + const BufferedNetPtr& root, + FixedDelay slack_target, + float alpha, + int max_plans, + bool strict_assured) +{ + slack_target = annotateArrivalDelays(root, slack_target); + const BnetSeq top_opts + = areaRecoveryOptions(root, slack_target, alpha, strict_assured); + + std::vector ranked_options; + ranked_options.reserve(top_opts.size()); + FixedDelay best_slack = -FixedDelay::INF; + BufferedNetPtr best_slack_option = nullptr; + int best_slack_index = 0; + int i = 1; + debugPrint(logger_, RSZ, "rebuffer", 2, "area-optimized options"); + for (const BufferedNetPtr& p : top_opts) { + // Find slack for drvr_pin into option. + std::optional slack = evaluateOption(p, i); + + if (!slack) { + // ignore this option as it doesn't pass ERC + continue; + } + + if (best_slack_option == nullptr + || p->slackTransition() == nullptr /* buffer tree unconstrained */ + || slack.value() > best_slack) { + best_slack = slack.value(); + best_slack_option = p; + best_slack_index = i; + } + + const bool unconstrained = p->slackTransition() == nullptr; + ranked_options.push_back( + {.option = p, + .slack = slack.value(), + .unconstrained = unconstrained, + .meets_target = slack.value() >= slack_target || unconstrained, + .preserves_best_slack = false, + .index = i}); + i++; + } + + const FixedDelay slack_floor = slackLossFloor(best_slack); + for (RankedOption& option : ranked_options) { + option.preserves_best_slack + = option.slack >= slack_floor || option.unconstrained; + } + + std::ranges::sort(ranked_options, areaOrder); + + if (!ranked_options.empty() && ranked_options.front().meets_target) { + debugPrint(logger_, + RSZ, + "rebuffer", + 2, + "best option {} (area optimized)", + ranked_options.front().index); + const int selected_count + = std::min(max_plans, static_cast(ranked_options.size())); + ranked_options.erase(ranked_options.begin() + selected_count, + ranked_options.end()); + return ranked_options; + } + + if (best_slack_option) { + debugPrint(logger_, + RSZ, + "rebuffer", + 2, + "best option {} (closest to meeting timing target)", + best_slack_index); + return {RankedOption{ + .option = best_slack_option, + .slack = best_slack, + .unconstrained = best_slack_option->slackTransition() == nullptr, + .index = best_slack_index}}; + } + + debugPrint(logger_, RSZ, "rebuffer", 2, "no available option"); + return {}; +} + +// Recover area on a rebuffering choice without regressing timing. +BufferedNetPtr Rebuffer::recoverArea(const BufferedNetPtr& root, + FixedDelay slack_target, + float alpha) +{ + slack_target = annotateArrivalDelays(root, slack_target); + const BnetSeq top_opts + = areaRecoveryOptions(root, slack_target, alpha, /*strict_assured=*/true); FixedDelay best_slack = -FixedDelay::INF; float best_area = std::numeric_limits::max(); @@ -1193,13 +1508,12 @@ FixedDelay Rebuffer::bufferDelay(sta::LibertyCell* cell, FixedDelay delay = FixedDelay::ZERO; if (rf) { + sta::LibertyPort *input, *output; + cell->bufferPorts(input, output); for (auto rf1 : rf->range()) { const sta::Scene* scene = arrival_paths_[rf1->index()]->scene(sta_); - sta::LibertyPort *input, *output; - cell->bufferPorts(input, output); - sta::ArcDelay gate_delays[sta::RiseFall::index_count]; - sta::Slew slews[sta::RiseFall::index_count]; - resizer_->gateDelays(output, load_cap, scene, max_, gate_delays, slews); + const std::array& gate_delays + = gateDelaysForLoad(output, scene, load_cap); delay = std::max( delay, FixedDelay(gate_delays[rf1->index()], resizer_)); } @@ -1208,6 +1522,57 @@ FixedDelay Rebuffer::bufferDelay(sta::LibertyCell* cell, return delay; } +size_t Rebuffer::BufferDelayKeyHash::operator()(const BufferDelayKey& key) const +{ + size_t seed = 0; + boost::hash_combine(seed, key.port); + boost::hash_combine(seed, key.scene); + boost::hash_combine(seed, key.load_bits); + boost::hash_combine(seed, key.rise_slew_bits); + boost::hash_combine(seed, key.fall_slew_bits); + return seed; +} + +const std::array& +Rebuffer::gateDelaysForLoad(sta::LibertyPort* output, + const sta::Scene* scene, + const float load_cap) +{ + auto float_bits + = [](const float value) { return std::bit_cast(value); }; + sta::Slew slews[sta::RiseFall::index_count]; + + // Annotated input slews make gateDelays() depend on the current instance. + if (!resizer_->input_slew_map_.empty()) { + resizer_->gateDelays(output, + load_cap, + scene, + max_, + delayCalc(), + uncached_gate_delays_.data(), + slews); + return uncached_gate_delays_; + } + + const BufferDelayKey key{ + .port = output, + .scene = scene, + .load_bits = float_bits(load_cap), + .rise_slew_bits + = float_bits(resizer_->tgt_slews_[sta::RiseFall::riseIndex()]), + .fall_slew_bits + = float_bits(resizer_->tgt_slews_[sta::RiseFall::fallIndex()])}; + BufferDelayCache::iterator cached = buffer_delay_cache_.find(key); + if (cached == buffer_delay_cache_.end()) { + std::array gate_delays; + resizer_->gateDelays( + output, load_cap, scene, max_, delayCalc(), gate_delays.data(), slews); + cached = buffer_delay_cache_.emplace(key, gate_delays).first; + } + + return cached->second; +} + BnetPtr Rebuffer::addWire(const BnetPtr& p, odb::Point wire_end, int wire_layer, @@ -1252,16 +1617,17 @@ BnetPtr Rebuffer::addWire(const BnetPtr& p, return z; } -void Rebuffer::insertBufferOptions( +bool Rebuffer::insertBufferOptions( BufferedNetSeq& opts, int level, int next_segment_wl, bool area_oriented /*=false*/, FixedDelay slack_threshold /*=0; used for area mode only*/, - BufferedNet* exemplar /*=nullptr; used for area mode only*/) + BufferedNet* exemplar /*=nullptr; used for area mode only*/, + bool strict_assured /*=true*/) { if (opts.empty()) { - return; + return true; } BufferSize& strong_driver = buffer_sizes_.back(); @@ -1315,7 +1681,7 @@ void Rebuffer::insertBufferOptions( } }; - for (BufferSize buffer_size : buffer_sizes_) { + for (const BufferSize& buffer_size : buffer_sizes_) { sta::LibertyCell* buffer_cell = buffer_size.cell; sta::LibertyPort *in, *out; buffer_cell->bufferPorts(in, out); @@ -1434,16 +1800,16 @@ void Rebuffer::insertBufferOptions( } if (!assured_satisfied) { - logger_->error( - RSZ, - 501, - "buffering pin {} failed: area recovery cannot reproduce solution", - network_->name(pin_), - delayAsString(slack_threshold.toSeconds(), 3, this)); + if (!strict_assured) { + opts.clear(); + return false; + } + errorAreaRecoveryUnreproducible(); } } new_opts.swap(opts); + return true; } static float bufferCin(const sta::LibertyCell* cell) @@ -1479,14 +1845,15 @@ void Rebuffer::init() .long_wire_asymptotics = {}}); } - std::ranges::sort(buffer_sizes_, [=](BufferSize a, BufferSize b) { - const float cin_a = bufferCin(a.cell); - const float cin_b = bufferCin(b.cell); - if (cin_a != cin_b) { - return cin_a < cin_b; - } - return a.cell->id() < b.cell->id(); - }); + std::ranges::sort(buffer_sizes_, + [=](const BufferSize& a, const BufferSize& b) { + const float cin_a = bufferCin(a.cell); + const float cin_b = bufferCin(b.cell); + if (cin_a != cin_b) { + return cin_a < cin_b; + } + return a.cell->id() < b.cell->id(); + }); buffer_sizes_index_.clear(); for (auto& size : buffer_sizes_) { @@ -1500,6 +1867,13 @@ void Rebuffer::initOnCorner(sta::Scene* corner) characterizeBuffers(); } +void Rebuffer::initForPlanning(sta::Scene* corner, const bool worker) +{ + init(); + owned_delay_calc_.reset(worker ? arc_delay_calc_->copy() : nullptr); + initOnCorner(corner); +} + float Rebuffer::findBufferLoadLimitImpliedByDriverSlew(sta::LibertyCell* cell) { sta::LibertyPort *inp, *outp; @@ -1519,14 +1893,14 @@ float Rebuffer::findBufferLoadLimitImpliedByDriverSlew(sta::LibertyCell* cell) for (sta::TimingArc* arc : arc_set->arcs()) { sta::LoadPinIndexMap load_pin_index_map(network_); sta::ArcDcalcResult dcalc_result - = arc_delay_calc_->gateDelay(nullptr, - arc, - in_slew, - load_cap, - nullptr, - load_pin_index_map, - corner_, - max_); + = delayCalc()->gateDelay(nullptr, + arc, + in_slew, + load_cap, + nullptr, + load_pin_index_map, + corner_, + max_); const sta::Slew& drvr_slew = dcalc_result.drvrSlew(); slew = std::max(slew, drvr_slew); } @@ -1572,7 +1946,8 @@ sta::Delay Rebuffer::characterizationDelay(BufferSize& size, float load_cap) sta::ArcDelay gate_delays[sta::RiseFall::index_count]; sta::Slew slews[sta::RiseFall::index_count]; size.cell->bufferPorts(input, output); - resizer_->gateDelays(output, load_cap, corner_, max_, gate_delays, slews); + resizer_->gateDelays( + output, load_cap, corner_, max_, delayCalc(), gate_delays, slews); return std::max(gate_delays[sta::RiseFall::rise()->index()], gate_delays[sta::RiseFall::fall()->index()]); } @@ -1874,6 +2249,238 @@ std::vector Rebuffer::collectImportedTreeBufferInstances( return insts; } +BufferedNetPtr Rebuffer::cloneBufferedNetTree(const BufferedNetPtr& tree) +{ + if (tree == nullptr) { + return nullptr; + } + + BufferedNetPtr cloned; + switch (tree->type()) { + case BnetType::load: + cloned = make_shared( + BnetType::load, tree->location(), tree->loadPin(), corner_, resizer_); + break; + case BnetType::wire: + cloned = make_shared(BnetType::wire, + tree->location(), + tree->layer(), + cloneBufferedNetTree(tree->ref()), + corner_, + resizer_, + estimate_parasitics_); + break; + case BnetType::via: + cloned = make_shared(BnetType::via, + tree->location(), + tree->layer(), + tree->refLayer(), + cloneBufferedNetTree(tree->ref()), + corner_, + resizer_); + break; + case BnetType::junction: + cloned = make_shared(BnetType::junction, + tree->location(), + cloneBufferedNetTree(tree->ref()), + cloneBufferedNetTree(tree->ref2()), + resizer_); + break; + case BnetType::buffer: + cloned = make_shared(BnetType::buffer, + tree->location(), + tree->bufferCell(), + cloneBufferedNetTree(tree->ref()), + corner_, + resizer_, + estimate_parasitics_); + break; + default: + logger_->critical(RSZ, 1009, "unhandled BufferedNet type"); + } + + cloned->setSlackTransition(tree->slackTransition()); + cloned->setSlack(tree->slack()); + cloned->setDelay(tree->delay()); + cloned->setArrivalDelay(tree->arrivalDelay()); + + return cloned; +} + +void Rebuffer::buildLoadPaths(const BufferedNetPtr& tree, + std::vector& prefix, + std::vector& load_paths) +{ + switch (tree->type()) { + case BnetType::load: + load_paths.push_back({.load_pin = tree->loadPin(), .stages = prefix}); + return; + case BnetType::wire: { + const BufferedNetPtr child = tree->ref(); + double layer_res = 0.0; + double layer_cap = 0.0; + tree->wireRC( + corner_, resizer_, estimate_parasitics_, layer_res, layer_cap); + const float wire_length + = static_cast(resizer_->dbuToMeters(tree->length())); + WireRC wire_rc{.length = wire_length, + .res = static_cast(wire_length * layer_res), + .cap = static_cast(wire_length * layer_cap)}; + prefix.push_back({.type = BufferTreeStageType::kWire, + .wire_rc = wire_rc, + .downstream_cap = child->cap()}); + buildLoadPaths(child, prefix, load_paths); + prefix.pop_back(); + return; + } + case BnetType::via: { + const BufferedNetPtr child = tree->ref(); + const float via_res = static_cast( + tree->viaResistance(corner_, resizer_, estimate_parasitics_)); + prefix.push_back( + {.type = BufferTreeStageType::kVia, + .wire_rc = {.length = 0.0f, .res = via_res, .cap = 0.0f}, + .downstream_cap = child->cap()}); + buildLoadPaths(child, prefix, load_paths); + prefix.pop_back(); + return; + } + case BnetType::buffer: { + const BufferedNetPtr child = tree->ref(); + sta::LibertyPort* input = nullptr; + sta::LibertyPort* output = nullptr; + tree->bufferCell()->bufferPorts(input, output); + const float output_cap = output != nullptr ? output->capacitance() : 0.0f; + const bool drives_distributed_load + = drivesDistributedLoadBeforeNextBuffer(child); + prefix.push_back({.type = BufferTreeStageType::kBuffer, + .buffer_cell = tree->bufferCell(), + .wire_rc = {}, + .downstream_cap = child->cap() + output_cap, + .drives_distributed_load = drives_distributed_load}); + buildLoadPaths(child, prefix, load_paths); + prefix.pop_back(); + return; + } + case BnetType::junction: + buildLoadPaths(tree->ref(), prefix, load_paths); + buildLoadPaths(tree->ref2(), prefix, load_paths); + return; + default: + logger_->critical(RSZ, 1011, "unhandled BufferedNet type"); + } +} + +Rebuffer::TreeSignature Rebuffer::treeSignature(const BufferedNetPtr& tree) +{ + TreeSignature signature; + appendTreeSignature(tree, signature); + return signature; +} + +void Rebuffer::appendTreeSignature(const BufferedNetPtr& tree, + TreeSignature& signature) +{ + auto pointer_bits = [](const void* pointer) { + return static_cast(reinterpret_cast(pointer)); + }; + auto int_bits = [](const int value) { + return static_cast(static_cast(value)); + }; + if (tree == nullptr) { + signature.push_back(0); + return; + } + + // Each node type fixes its child count, so a type-tagged pre-order walk is + // unambiguous without brackets. + signature.push_back(static_cast(tree->type()) + 1); + switch (tree->type()) { + case BnetType::load: + signature.push_back(pointer_bits(tree->loadPin())); + return; + case BnetType::wire: + signature.push_back(int_bits(tree->location().x())); + signature.push_back(int_bits(tree->location().y())); + signature.push_back(int_bits(tree->layer())); + appendTreeSignature(tree->ref(), signature); + return; + case BnetType::via: + signature.push_back(int_bits(tree->location().x())); + signature.push_back(int_bits(tree->location().y())); + signature.push_back(int_bits(tree->layer())); + signature.push_back(int_bits(tree->refLayer())); + appendTreeSignature(tree->ref(), signature); + return; + case BnetType::buffer: + signature.push_back(pointer_bits(tree->bufferCell())); + signature.push_back(int_bits(tree->location().x())); + signature.push_back(int_bits(tree->location().y())); + appendTreeSignature(tree->ref(), signature); + return; + case BnetType::junction: + signature.push_back(int_bits(tree->location().x())); + signature.push_back(int_bits(tree->location().y())); + appendTreeSignature(tree->ref(), signature); + appendTreeSignature(tree->ref2(), signature); + return; + default: + logger_->critical(RSZ, 1012, "unhandled BufferedNet type"); + } +} + +bool BufferTreeLoadPath::hasBufferAfter(const size_t index) const +{ + for (size_t later_index = index + 1; later_index < stages.size(); + ++later_index) { + if (stages[later_index].type == BufferTreeStageType::kBuffer) { + return true; + } + } + return false; +} + +const BufferTreeLoadPath* RebufferTreePlan::loadPath( + const sta::Pin* load_pin) const +{ + for (const BufferTreeLoadPath& load_path : load_paths) { + if (load_path.load_pin == load_pin) { + return &load_path; + } + } + return nullptr; +} + +const BufferTreeLoadPath* RebufferTreePlan::targetPath() const +{ + return loadPath(target_load_pin); +} + +RebufferTreePlan Rebuffer::makePlanFromTree(const BufferedNetPtr& tree, + const PlanRequest& request) +{ + BufferedNetPtr plan_tree = cloneBufferedNetTree(tree); + + // The load paths list every load of the tree, so the target path exists + // exactly when the target load is one of them. + std::vector load_paths; + std::vector load_path_prefix; + buildLoadPaths(plan_tree, load_path_prefix, load_paths); + + const FixedDelay final_slack = slackAtDriverPin(plan_tree); + return RebufferTreePlan{ + .tree = plan_tree, + .flat_net = request.flat_net, + .driver_pin = const_cast(request.drvr_pin), + .parent = request.parent, + .target_load_pin = request.target_load_pin, + .load_paths = std::move(load_paths), + .tree_input_cap = plan_tree->cap(), + .driver_slack_gain = (final_slack - request.original_slack).toSeconds(), + .buffer_count = plan_tree->bufferCount(), + .load_count = plan_tree->loadCount()}; +} + int Rebuffer::exportBufferTree(const BufferedNetPtr& choice, sta::Net* net, // Original Driver Net (flat) int level, @@ -2088,6 +2695,8 @@ void Rebuffer::setPin(sta::Pin* drvr_pin) pin_ = drvr_pin; drvr_port_ = network_->libertyPort(drvr_pin); drvr_load_high_water_mark_ = 0; + // The gate-delay memo serves the planning of one driver. + buffer_delay_cache_.clear(); { float fanout, max_fanout, fanout_slack; @@ -2404,118 +3013,410 @@ bool Rebuffer::hasTopLevelOutputPort(sta::Net* net) return false; } -int Rebuffer::rebufferPin(const sta::Pin* drvr_pin) +// Keep the anchor tree in a beam: it replaces its structural twin among the +// selected options, else the last slot or a free one. Signatures are unique +// after uniqueTreeOptions(), so the twin is a single option. +void Rebuffer::keepAnchorTree(const std::vector& ranked_options, + const RankedOption& anchor, + const TreeSignature& anchor_signature, + const int plan_limit, + std::vector& selected_options) { - const ProfileScope profile("buffer_tree_generation"); - RepairSetupProfiler::event("buffer_tree_plan_requests"); - if (network_->isTopLevelPort(drvr_pin)) { - logger_->warn(RSZ, - 2020, - "rebuffering does not support top port as the driver pin: {}", - network_->name(drvr_pin)); - return 0; + const BnetPtr* anchor_twin = nullptr; + for (const RankedOption& ranked_option : ranked_options) { + if (ranked_option.signature == anchor_signature) { + anchor_twin = &ranked_option.option; + break; + } } - sta::Net* const net = network_->net(drvr_pin); - odb::dbNet* const db_net = db_network_->flatNet(drvr_pin); + if (anchor_twin == nullptr) { + return; + } - drvr_port_ = network_->libertyPort(drvr_pin); - if (net && drvr_port_ && - // Verilog connects by net name, so there is no way to distinguish the - // net from the port. - !hasTopLevelOutputPort(net)) { - setPin(const_cast(drvr_pin)); - BufferedNetPtr bnet = resizer_->makeBufferedNet(drvr_pin, corner_); + for (RankedOption& selected_option : selected_options) { + if (selected_option.option == *anchor_twin) { + selected_option = anchor; + return; + } + } - if (!bnet) { - logger_->warn(RSZ, - 75, - "makeBufferedNet failed for driver {}", - network_->pathName(drvr_pin)); - return 0; + if (static_cast(selected_options.size()) < plan_limit) { + selected_options.push_back(anchor); + } else { + selected_options.back() = anchor; + } +} + +std::vector Rebuffer::uniqueTreeOptions( + std::vector options) +{ + std::vector unique_options; + // Deduplicate by the structural encoding of each tree. + std::unordered_set> signatures; + for (RankedOption& option : options) { + option.signature = treeSignature(option.option); + if (signatures.insert(option.signature).second) { + unique_options.push_back(std::move(option)); } + } - const bool debug = (drvr_pin == resizer_->debug_pin_); - if (debug) { - logger_->setDebugLevel(RSZ, "rebuffer", 4); + return unique_options; +} + +std::vector Rebuffer::limitTimingOptions( + std::vector options, + const RankedOption& anchor, + const int plan_limit) +{ + // The options keep the slack their selection evaluated. + std::vector ranked_options + = uniqueTreeOptions(std::move(options)); + std::ranges::sort(ranked_options, timingOrder); + + const int selected_count + = std::min(plan_limit, static_cast(ranked_options.size())); + std::vector selected_options( + ranked_options.begin(), ranked_options.begin() + selected_count); + + if (anchor.option != nullptr) { + keepAnchorTree(ranked_options, + anchor, + treeSignature(anchor.option), + plan_limit, + selected_options); + } + + return selected_options; +} + +std::vector Rebuffer::limitAreaOptions( + std::vector options, + const FixedDelay slack_target, + const RankedOption& anchor, + const int plan_limit) +{ + // The options keep the slack their selection evaluated; the target and the + // order position are this round's. + std::vector ranked_options + = uniqueTreeOptions(std::move(options)); + FixedDelay best_slack = -FixedDelay::INF; + int index = 0; + for (RankedOption& option : ranked_options) { + best_slack = std::max(best_slack, option.slack); + option.meets_target = option.slack >= slack_target || option.unconstrained; + option.index = index++; + } + + const FixedDelay slack_floor = slackLossFloor(best_slack); + for (RankedOption& option : ranked_options) { + option.preserves_best_slack + = option.slack >= slack_floor || option.unconstrained; + } + + std::ranges::sort(ranked_options, areaOrder); + + const int selected_count + = std::min(plan_limit, static_cast(ranked_options.size())); + std::vector selected_options( + ranked_options.begin(), ranked_options.begin() + selected_count); + + if (anchor.option != nullptr) { + keepAnchorTree(ranked_options, + anchor, + treeSignature(anchor.option), + plan_limit, + selected_options); + } + + return selected_options; +} + +FixedDelay Rebuffer::areaRecoveryTarget(const BnetPtr& tree) +{ + sta::Delay drvr_gate_delay; + std::tie(drvr_gate_delay, std::ignore, std::ignore) = drvrPinTiming(tree); + sta::Delay relaxation = (std::max(float(drvr_gate_delay), 0.0f) + + criticalPathDelay(logger_, tree).toSeconds()) + * relaxation_factor_; + return slackAtDriverPin(tree) - FixedDelay(relaxation, resizer_); +} + +bool Rebuffer::runTimingBeam(const BnetPtr& bnet, + const sta::Pin* drvr_pin, + const bool allow_topology_rewrite, + const int plan_limit, + std::vector& beam, + RankedOption& anchor) +{ + beam = {RankedOption{.option = bnet}}; + anchor = {.option = bnet}; + for (int i = 0; i < 3; i++) { + std::vector next_options; + RankedOption next_anchor; + for (const RankedOption& beam_option : beam) { + std::vector timing_options = bufferForTimingTopK( + beam_option.option, allow_topology_rewrite, plan_limit); + if (anchor.option != nullptr && beam_option.option == anchor.option + && !timing_options.empty()) { + next_anchor = timing_options.front(); + } + + next_options.insert( + next_options.end(), timing_options.begin(), timing_options.end()); } - debugPrint(logger_, - RSZ, - "rebuffer", - 2, - "driver {}", - sdc_network_->pathName(drvr_pin)); - sta::Vertex* drvr = graph_->pinDrvrVertex(drvr_pin); + beam = limitTimingOptions(std::move(next_options), next_anchor, plan_limit); + anchor = next_anchor; + if (beam.empty()) { + warnNoViableBuffering(drvr_pin, i + 1); + return false; + } + } - { - const ProfileScope sta_profile("incremental_sta"); - sta_->findRequireds(); + return true; +} + +void Rebuffer::runAreaRecoveryBeam(const int plan_limit, + std::vector& beam, + RankedOption& anchor) +{ + const FixedDelay target = areaRecoveryTarget(beam.front().option); + FixedDelay anchor_target = target; + if (anchor.option != nullptr) { + anchor_target = areaRecoveryTarget(anchor.option); + } + + for (int i = 0; i < 5 && !beam.empty(); i++) { + std::vector next_options; + if (plan_limit > 1) { + next_options = beam; } - annotateLoadSlacks(bnet, drvr); - - const bool allow_topology_rewrite - = (estimate_parasitics_->getParasiticsSrc() - == est::ParasiticsSrc::kPlacement); - - for (int i = 0; i < 3; i++) { - bnet = bufferForTiming(bnet, allow_topology_rewrite); - if (!bnet) { - logger_->warn( - RSZ, - 2021, - "cannot find a viable buffering solution on pin {} after {} " - "rounds of buffering (no solution meets design rules)", - network_->name(drvr_pin), - i + 1); - break; + + RankedOption next_anchor; + for (const RankedOption& beam_option : beam) { + if (anchor.option != nullptr && beam_option.option == anchor.option) { + std::vector anchor_area_options = recoverAreaTopK( + beam_option.option, anchor_target, ((float) (1 + i)) / 5, 1, true); + if (!anchor_area_options.empty()) { + next_anchor = anchor_area_options.front(); + } + + next_options.insert(next_options.end(), + anchor_area_options.begin(), + anchor_area_options.end()); } + + std::vector area_options + = recoverAreaTopK(beam_option.option, + target, + ((float) (1 + i)) / 5, + plan_limit, + /*strict_assured=*/false); + next_options.insert( + next_options.end(), area_options.begin(), area_options.end()); } - if (!bnet) { - return 0; + beam = limitAreaOptions( + std::move(next_options), target, next_anchor, plan_limit); + anchor = next_anchor; + } +} + +std::vector Rebuffer::plansFromBeam( + const std::vector& beam, + const RankedOption& anchor, + const PlanRequest& request, + const int plan_limit) +{ + std::vector plans; + plans.reserve(std::min(plan_limit, static_cast(beam.size()))); + for (const RankedOption& beam_option : beam) { + RebufferTreePlan plan = makePlanFromTree(beam_option.option, request); + plan.anchor_tree + = anchor.option != nullptr && beam_option.option == anchor.option; + if (plan.targetPath() != nullptr) { + plans.push_back(std::move(plan)); } + } - sta::Delay drvr_gate_delay; - std::tie(drvr_gate_delay, std::ignore, std::ignore) = drvrPinTiming(bnet); - sta::Delay relaxation = (std::max(float(drvr_gate_delay), 0.0f) - + criticalPathDelay(logger_, bnet).toSeconds()) - * relaxation_factor_; + return plans; +} + +BufferedNetPtr Rebuffer::makeBufferedNet(const sta::Pin* drvr_pin) +{ + if (network_->isTopLevelPort(drvr_pin)) { + warnTopPortDriver(drvr_pin); + return nullptr; + } - FixedDelay target - = slackAtDriverPin(bnet) - FixedDelay(relaxation, resizer_); + sta::Net* const net = network_->net(drvr_pin); - for (int i = 0; i < 5 && bnet; i++) { - bnet = recoverArea(bnet, target, ((float) (1 + i)) / 5); - } + // Verilog connects by net name, so there is no way to distinguish the net + // from the port. + if (net == nullptr || network_->libertyPort(drvr_pin) == nullptr + || hasTopLevelOutputPort(net)) { + return nullptr; + } + + BufferedNetPtr bnet = resizer_->makeBufferedNet(drvr_pin, corner_); + if (!bnet) { + warnBufferedNetFailed(drvr_pin); + } + + return bnet; +} + +std::vector Rebuffer::makeRebufferTreePlans( + const sta::Pin* drvr_pin, + BufferedNetPtr bnet, + const sta::Pin* target_load_pin, + int max_plans) +{ + const ProfileScope profile("buffer_tree_generation"); + RepairSetupProfiler::event("buffer_tree_plan_requests"); + if (bnet == nullptr) { + return {}; + } + + const int plan_limit = std::max(max_plans, 1); + odb::dbNet* const db_net = db_network_->flatNet(drvr_pin); + setPin(const_cast(drvr_pin)); + + // set_debug_pin raises the level for main-thread planning only: a worker + // planner must not write the shared logger's level while others read it. + const bool debug = (drvr_pin == resizer_->debug_pin_); + if (debug && owned_delay_calc_ == nullptr) { + logger_->setDebugLevel(RSZ, "rebuffer", 4); + } + + debugPrint(logger_, + RSZ, + "rebuffer", + 2, + "driver {}", + sdc_network_->pathName(drvr_pin)); + + sta::Vertex* drvr = graph_->pinDrvrVertex(drvr_pin); + + // annotateLoadSlacks() reads the required times the caller keeps current. + annotateLoadSlacks(bnet, drvr); + const PlanRequest request{.drvr_pin = drvr_pin, + .target_load_pin = target_load_pin, + .flat_net = db_network_->dbToSta(db_net), + .parent = db_network_->getOwningInstanceParent( + const_cast(drvr_pin)), + .original_slack = slackAtDriverPin(bnet)}; + + const bool allow_topology_rewrite = (estimate_parasitics_->getParasiticsSrc() + == est::ParasiticsSrc::kPlacement); + + // The anchor tree rides along in a reserved beam slot. The beam keeps the + // slack each option was evaluated at. + std::vector beam; + RankedOption anchor; + if (!runTimingBeam( + bnet, drvr_pin, allow_topology_rewrite, plan_limit, beam, anchor)) { + return {}; + } + + runAreaRecoveryBeam(plan_limit, beam, anchor); + if (beam.empty()) { + errorAreaRecoveryFailed(); + return {}; + } + return plansFromBeam(beam, anchor, request, plan_limit); +} + +int Rebuffer::applyRebufferTreePlan(const RebufferTreePlan& plan, + const char* instance_base_name) +{ + const ProfileScope profile("apply_buffer_tree"); + RepairSetupProfiler::event("buffer_tree_apply_requests"); + if (plan.flat_net == nullptr) { + return 0; + } + + return exportBufferTree( + plan.tree, plan.flat_net, 1, plan.parent, instance_base_name); +} + +int Rebuffer::rebufferPin(const sta::Pin* drvr_pin) +{ + const ProfileScope profile("buffer_tree_generation"); + RepairSetupProfiler::event("buffer_tree_plan_requests"); + BufferedNetPtr bnet = makeBufferedNet(drvr_pin); + if (bnet == nullptr) { + return 0; + } + + odb::dbNet* const db_net = db_network_->flatNet(drvr_pin); + setPin(const_cast(drvr_pin)); + + const bool debug = (drvr_pin == resizer_->debug_pin_); + if (debug) { + logger_->setDebugLevel(RSZ, "rebuffer", 4); + } + debugPrint(logger_, + RSZ, + "rebuffer", + 2, + "driver {}", + sdc_network_->pathName(drvr_pin)); + + sta::Vertex* drvr = graph_->pinDrvrVertex(drvr_pin); + + { + const ProfileScope sta_profile("incremental_sta"); + sta_->findRequireds(); + } + annotateLoadSlacks(bnet, drvr); + + const bool allow_topology_rewrite = (estimate_parasitics_->getParasiticsSrc() + == est::ParasiticsSrc::kPlacement); + + for (int i = 0; i < 3; i++) { + bnet = bufferForTiming(bnet, allow_topology_rewrite); if (!bnet) { - logger_->error(RSZ, 2022, "failed area recovery"); - return 0; + warnNoViableBuffering(drvr_pin, i + 1); + break; } + } - sta::Instance* parent - = db_network_->getOwningInstanceParent(const_cast(drvr_pin)); - odb::dbITerm* drvr_op_iterm = nullptr; - odb::dbBTerm* drvr_op_bterm = nullptr; - odb::dbModITerm* drvr_op_moditerm = nullptr; - db_network_->staToDb( - drvr_pin, drvr_op_iterm, drvr_op_bterm, drvr_op_moditerm); + if (!bnet) { + return 0; + } - int inserted_count; - { - const ProfileScope apply_profile("apply_buffer_tree"); - RepairSetupProfiler::event("buffer_tree_apply_requests"); - inserted_count = exportBufferTree( - bnet, db_network_->dbToSta(db_net), 1, parent, "rebuffer"); - } + const FixedDelay target = areaRecoveryTarget(bnet); + for (int i = 0; i < 5 && bnet; i++) { + bnet = recoverArea(bnet, target, ((float) (1 + i)) / 5); + } - debugPrint(logger_, RSZ, "rebuffer", 2, "-------------------------------"); + if (!bnet) { + errorAreaRecoveryFailed(); + return 0; + } + + sta::Instance* parent + = db_network_->getOwningInstanceParent(const_cast(drvr_pin)); + odb::dbITerm* drvr_op_iterm = nullptr; + odb::dbBTerm* drvr_op_bterm = nullptr; + odb::dbModITerm* drvr_op_moditerm = nullptr; + db_network_->staToDb( + drvr_pin, drvr_op_iterm, drvr_op_bterm, drvr_op_moditerm); - return inserted_count; + int inserted_count; + { + const ProfileScope apply_profile("apply_buffer_tree"); + RepairSetupProfiler::event("buffer_tree_apply_requests"); + inserted_count = exportBufferTree( + bnet, db_network_->dbToSta(db_net), 1, parent, "rebuffer"); } - return 0; + debugPrint(logger_, RSZ, "rebuffer", 2, "-------------------------------"); + + return inserted_count; } void Rebuffer::rebufferNet(const sta::Pin* drvr_pin) diff --git a/src/rsz/src/Rebuffer.hh b/src/rsz/src/Rebuffer.hh index e00072b8ef5..5ce5502abed 100644 --- a/src/rsz/src/Rebuffer.hh +++ b/src/rsz/src/Rebuffer.hh @@ -3,15 +3,19 @@ #pragma once +#include +#include #include #include #include #include +#include #include #include #include #include "BufferedNet.hh" +#include "OptimizerTypes.hh" #include "db_sta/dbNetwork.hh" #include "db_sta/dbSta.hh" #include "odb/geom.h" @@ -39,12 +43,88 @@ enum class BufferedNetType; using BufferedNetPtr = std::shared_ptr; using BufferedNetSeq = std::vector; +enum class BufferTreeStageType +{ + kWire, + kVia, + kBuffer +}; + +// One wire, via, or buffer encountered on a planned root-to-load path. +struct BufferTreePathStage +{ + BufferTreeStageType type{BufferTreeStageType::kWire}; + const sta::LibertyCell* buffer_cell{nullptr}; + WireRC wire_rc; + // Capacitance seen below this stage in the proposed tree. + float downstream_cap{0.0f}; + // True when the stage drives more than one load branch. + bool drives_distributed_load{false}; +}; + +// Ordered stages from the planned tree root to one load pin. +struct BufferTreeLoadPath +{ + const sta::Pin* load_pin{nullptr}; + std::vector stages; + + // True when a buffer stage follows the stage at index. + bool hasBufferAfter(size_t index) const; +}; + +// Buffer-tree proposal with the paths and loading needed for MT scoring. +struct RebufferTreePlan +{ + // Planner-owned tree kept alive until the candidate is applied. + BufferedNetPtr tree; + sta::Net* flat_net{nullptr}; + sta::Pin* driver_pin{nullptr}; + sta::Instance* parent{nullptr}; + const sta::Pin* target_load_pin{nullptr}; + // Root-to-load paths of every load; the target load's path carries the + // primary timing estimate, the others the side-load timing. + std::vector load_paths; + float tree_input_cap{0.0f}; + // Driver pin slack of the tree minus that of the current net. + float driver_slack_gain{0.0f}; + // The tree the serial Rebuffer would build; it keeps a beam slot. + bool anchor_tree{false}; + int buffer_count{0}; + int load_count{0}; + + // The load path of load_pin, or nullptr when the tree does not reach it. + const BufferTreeLoadPath* loadPath(const sta::Pin* load_pin) const; + // The load path of the target load, or nullptr when the tree does not reach + // it. + const BufferTreeLoadPath* targetPath() const; +}; + +// Plans and applies timing-driven buffer trees for a driver net. class Rebuffer : public sta::dbStaState { public: Rebuffer(Resizer* resizer); + ~Rebuffer() override; void fullyRebuffer(sta::Pin* user_pin = nullptr); void rebufferNet(const sta::Pin* drvr_pin); + // Prepare makeRebufferTreePlans() on `corner`. A worker planner gets its own + // delay calculator, because OpenSTA's keeps per-call scratch state. + void initForPlanning(sta::Scene* corner, bool worker = false); + + // The driver's net as a BufferedNet on the planner's corner, or nullptr + // with a warning when the driver cannot be buffered. Main thread only: the + // global-route reader behind it is not thread-safe. + BufferedNetPtr makeBufferedNet(const sta::Pin* drvr_pin); + // Tree plans over a net from makeBufferedNet(). The caller keeps the + // required times current; the planner reads them without an STA refresh. + std::vector makeRebufferTreePlans( + const sta::Pin* drvr_pin, + BufferedNetPtr net, + const sta::Pin* target_load_pin, + int max_plans); + // Materialize a scored tree in OpenDB and return its inserted-buffer count. + int applyRebufferTreePlan(const RebufferTreePlan& plan, + const char* instance_base_name); protected: void init(); @@ -86,13 +166,163 @@ class Rebuffer : public sta::dbStaState BufferedNetPtr importBufferTree(const sta::Pin* drvr_pin, const sta::Scene* corner); + BufferedNetPtr cloneBufferedNetTree(const BufferedNetPtr& tree); + // Appends the stages from the root to every load of tree. + void buildLoadPaths(const BufferedNetPtr& tree, + std::vector& prefix, + std::vector& load_paths); + // Structural identity of a buffer tree: pre-order, type-tagged encoding + // where pointers identify pins and cells within one planning call. + using TreeSignature = std::vector; + + // Encode the tree structure and referenced cells/pins for deduplication. + TreeSignature treeSignature(const BufferedNetPtr& tree); + // Append one subtree to that structural encoding. + void appendTreeSignature(const BufferedNetPtr& tree, + TreeSignature& signature); + // Gate delays of one buffer output port driving a lumped load, memoized on + // every gateDelays() input (port, scene, load, target slews). The memo is + // bypassed while annotated input slews are active and cleared by setPin() + // for each driver. + const std::array& + gateDelaysForLoad(sta::LibertyPort* output, + const sta::Scene* scene, + float load_cap); + + // === Messages ============================================================= + // The messages the serial and the RebufferMt planning share, each with one + // ID. + void warnTopPortDriver(const sta::Pin* drvr_pin); + void warnBufferedNetFailed(const sta::Pin* drvr_pin); + void warnPathTraceFailed(const sta::Pin* load_pin); + void warnNoDesignRuleSolution(); + void warnNoViableBuffering(const sta::Pin* drvr_pin, int rounds); + void errorAreaRecoveryFailed(); + void errorAreaRecoveryUnreproducible(); + + // === Option ranking ======================================================= + // A beam option with the measures the rankings compare. + struct RankedOption + { + BufferedNetPtr option; + // The driver slack evaluateOption() found when the option was selected; + // the beam keeps it instead of evaluating the option again. + FixedDelay slack{}; + // No slack transition: the tree is unconstrained and ranks first. + bool unconstrained{false}; + // Area recovery: the option meets the slack target, and keeps the best + // slack within slackLossFloor(). + bool meets_target{false}; + bool preserves_best_slack{false}; + TreeSignature signature{}; + // Position in the generated options: the tie-break, so fully tied trees + // keep their generation order instead of their heap order. + int index{0}; + }; + // Timing order: unconstrained, slack, cap, area, buffer count. + static bool timingOrder(const RankedOption& lhs, const RankedOption& rhs); + // Area-recovery order: meets the target, preserves the best slack, slack + // among the others, area among the rest, cap, buffer count, slack, index. + static bool areaOrder(const RankedOption& lhs, const RankedOption& rhs); + // The slack an option may fall to below best_slack and still preserve it: + // 1 ps under a negative best slack, 20 ps under a positive one. + FixedDelay slackLossFloor(FixedDelay best_slack) const; + // The timing options ranked by driver slack, each with the slack it was + // evaluated at: the single best when max_plans is 1, as the serial Rebuffer + // picks, else the max_plans best. + std::vector selectTimingOptions(const BufferedNetSeq& top_opts, + int max_plans); + std::vector bufferForTimingTopK(const BufferedNetPtr& tree, + bool allow_topology_rewrite, + int max_plans); + // The max_plans best area-recovered options of root that meet + // slack_target, else the one with the best slack. + std::vector recoverAreaTopK(const BufferedNetPtr& root, + FixedDelay slack_target, + float alpha, + int max_plans, + bool strict_assured = true); + // Keep the anchor in a beam of selected options: it replaces its + // structural twin, else the last slot or a free one. + static void keepAnchorTree(const std::vector& ranked_options, + const RankedOption& anchor, + const TreeSignature& anchor_signature, + int plan_limit, + std::vector& selected_options); + + // === RebufferMt tree planning (makeRebufferTreePlans) ===================== + // What one call plans for; the beam helpers read it when they turn a tree + // into a RebufferTreePlan. + struct PlanRequest + { + const sta::Pin* drvr_pin{nullptr}; + const sta::Pin* target_load_pin{nullptr}; + sta::Net* flat_net{nullptr}; + sta::Instance* parent{nullptr}; + FixedDelay original_slack{}; + }; - void insertBufferOptions(BufferedNetSeq& opts, + // Convert a buffered-net tree to an estimate-ready plan. + RebufferTreePlan makePlanFromTree(const BufferedNetPtr& tree, + const PlanRequest& request); + // Drop trees that repeat an earlier tree's structure and record the + // signature of the others. + std::vector uniqueTreeOptions( + std::vector options); + // The plan_limit best trees by driver slack; the anchor keeps a slot. + std::vector limitTimingOptions( + std::vector options, + const RankedOption& anchor, + int plan_limit); + // The plan_limit best area-recovered trees that meet slack_target; the + // anchor keeps a slot. + std::vector limitAreaOptions(std::vector options, + FixedDelay slack_target, + const RankedOption& anchor, + int plan_limit); + // The slack a tree may give back to area recovery. + FixedDelay areaRecoveryTarget(const BufferedNetPtr& tree); + // Three rounds of timing-driven buffering over the beam. Returns false + // when a round finds no viable tree. anchor follows the top-ranked option + // of the previous anchor's expansion. + bool runTimingBeam(const BufferedNetPtr& bnet, + const sta::Pin* drvr_pin, + bool allow_topology_rewrite, + int plan_limit, + std::vector& beam, + RankedOption& anchor); + // Five rounds of area recovery over the beam, the anchor tree recovered + // against its own target. + void runAreaRecoveryBeam(int plan_limit, + std::vector& beam, + RankedOption& anchor); + // Convert the surviving beam options to scored tree proposals. + std::vector plansFromBeam( + const std::vector& beam, + const RankedOption& anchor, + const PlanRequest& request, + int plan_limit); + // Annotate every node of root with its arrival delay from the driver and + // return the slack target area recovery works against: -INF when the tree + // is unconstrained. + FixedDelay annotateArrivalDelays(const BufferedNetPtr& root, + FixedDelay slack_target); + // Every tree that drops or downsizes buffers of root while each node keeps + // the slack alpha sets between its current slack and slack_target. With + // strict_assured, a node without such an option is an error. Otherwise the + // tree yields no option. + BufferedNetSeq areaRecoveryOptions(const BufferedNetPtr& root, + FixedDelay slack_target, + float alpha, + bool strict_assured); + + bool insertBufferOptions(BufferedNetSeq& opts, int level, int next_segment_wl = 0, bool area_oriented = false, FixedDelay slack_threshold = FixedDelay::ZERO, - BufferedNet* exemplar = nullptr); + BufferedNet* exemplar = nullptr, + bool strict_assured = true); void insertAssuredOption(BufferedNetSeq& opts, BufferedNetPtr assured_opt, @@ -159,6 +389,31 @@ class Rebuffer : public sta::dbStaState std::vector buffer_sizes_; std::map buffer_sizes_index_; + // Electrical inputs that uniquely identify a cached buffer delay lookup. + struct BufferDelayKey + { + const sta::LibertyPort* port{nullptr}; + const sta::Scene* scene{nullptr}; + uint32_t load_bits{0}; + uint32_t rise_slew_bits{0}; + uint32_t fall_slew_bits{0}; + bool operator==(const BufferDelayKey& other) const = default; + }; + + // Hashes the complete buffer-delay lookup key. + struct BufferDelayKeyHash + { + size_t operator()(const BufferDelayKey& key) const; + }; + + // Reuses gate delays at identical scene, load, and input-slew conditions. + using BufferDelayCache = std::unordered_map< + BufferDelayKey, + std::array, + BufferDelayKeyHash>; + BufferDelayCache buffer_delay_cache_; + std::array uncached_gate_delays_; + sta::Pin* pin_ = nullptr; float fanout_limit_ = 0.0f; float drvr_pin_max_slew_ = 0.0f; @@ -167,6 +422,15 @@ class Rebuffer : public sta::dbStaState sta::LibertyPort* drvr_port_ = nullptr; sta::Path* arrival_paths_[sta::RiseFall::index_count] = {nullptr}; + // A worker planner's private delay calculator (see initForPlanning); every + // gate delay this planner computes goes through delayCalc(). + std::unique_ptr owned_delay_calc_; + sta::ArcDelayCalc* delayCalc() const + { + return owned_delay_calc_ != nullptr ? owned_delay_calc_.get() + : arc_delay_calc_; + } + int resizer_max_wire_length_ = 0; double initial_design_area_ = 0.0; int print_interval_ = 0; diff --git a/src/rsz/src/RepairSetupProfiler.hh b/src/rsz/src/RepairSetupProfiler.hh index bd650103e99..41d2404a520 100644 --- a/src/rsz/src/RepairSetupProfiler.hh +++ b/src/rsz/src/RepairSetupProfiler.hh @@ -32,13 +32,17 @@ class RepairSetupProfiler : public odb::dbBlockCallBackObj // One logical record can update a counter, emit a trace row, or both. struct EventOptions { + // Counter increment; nullopt makes the event trace-only. std::optional count{1}; + // Counter key when it differs from the trace event name. std::string_view metric{}; + // Optional move-type dimension of the counter. std::string_view move{}; std::string_view object{}; std::string_view before{}; std::string_view after{}; std::string_view detail{}; + // Emit a detailed row only in profile mode 2 or higher. bool trace{false}; }; @@ -137,6 +141,7 @@ class RepairSetupProfiler : public odb::dbBlockCallBackObj { std::string name; Clock::time_point start; + // Nested-stage time excluded from this stage's self time. double children{0.0}; }; diff --git a/src/rsz/src/RepairTargetCollector.cc b/src/rsz/src/RepairTargetCollector.cc index 4ab780d758c..28f6d79b93d 100644 --- a/src/rsz/src/RepairTargetCollector.cc +++ b/src/rsz/src/RepairTargetCollector.cc @@ -863,6 +863,13 @@ void RepairTargetCollector::collectViolatingEndpoints() violating_endpoints_.clear(); crit_path_load_pins_.clear(); + // RebufferMt plans read required times without a refresh, so make them + // all current here. This is a no-op when timing is up to date. + { + const ProfileScope sta_profile("incremental_sta"); + sta_->findRequireds(); + } + if (restrictedToPathGroup()) { // One query for the whole group, already carrying each endpoint's slack // in it. Testing endpoints one at a time would repeat the same search diff --git a/src/rsz/src/RepairTargetCollector.hh b/src/rsz/src/RepairTargetCollector.hh index 8dfd28e3c0d..dc26de1b262 100644 --- a/src/rsz/src/RepairTargetCollector.hh +++ b/src/rsz/src/RepairTargetCollector.hh @@ -61,7 +61,7 @@ struct pinData int level; }; -// Class to collect instances with violating output pins. +// Collects violating setup endpoints and path-driver repair targets. class RepairTargetCollector { public: diff --git a/src/rsz/src/SequentialTimingEstimator.cc b/src/rsz/src/SequentialTimingEstimator.cc index 4aa7a7e297a..d93b8285892 100644 --- a/src/rsz/src/SequentialTimingEstimator.cc +++ b/src/rsz/src/SequentialTimingEstimator.cc @@ -42,7 +42,10 @@ namespace rsz { namespace { +// A side check's slack loss counts fully at or below zero slack and fades out +// by this much slack. constexpr double kSideSlackWeightCutoff = 10.0e-12; +// Rounding allowance before a side check counts as a new violation. constexpr double kSideSlackViolationTolerance = 0.001e-12; struct PreparedInputTiming diff --git a/src/rsz/src/SequentialTimingEstimator.hh b/src/rsz/src/SequentialTimingEstimator.hh index e2a4e4c4b37..974a55cb9b5 100644 --- a/src/rsz/src/SequentialTimingEstimator.hh +++ b/src/rsz/src/SequentialTimingEstimator.hh @@ -20,14 +20,16 @@ struct Target; class SequentialTimingEstimator { public: + // Scoring verdict and input-side slack loss for one candidate swap. struct Result { + // False when input-side timing cannot be scored or is illegal. bool legal{false}; // Maximum criticality-weighted side slack loss, in seconds; reduces gain. float weighted_side_slack_loss{0.0f}; // Maximum unweighted side slack loss, in seconds; for diagnostics. float max_side_slack_loss{0.0f}; - // Static storage, so it outlives the estimator. + // Static reason label; it remains valid after the estimator is destroyed. std::string_view reason; }; @@ -44,9 +46,12 @@ class SequentialTimingEstimator Result estimate(const sta::LibertyCell* candidate_cell) const; private: + // Prepared STA and Liberty context shared by candidate evaluations. struct PreparedFlipFlopTiming; + // Retains the immutable context while worker threads score candidates. explicit SequentialTimingEstimator( std::shared_ptr prepared_timing); + // Timing and equivalent-cell models captured on the main thread. const std::shared_ptr prepared_timing_; }; diff --git a/src/rsz/src/move/MoveCandidate.hh b/src/rsz/src/move/MoveCandidate.hh index 98eccba316e..bb5c3b4f84b 100644 --- a/src/rsz/src/move/MoveCandidate.hh +++ b/src/rsz/src/move/MoveCandidate.hh @@ -51,7 +51,7 @@ class MoveCandidate // === Candidate identity =================================================== Resizer& resizer_; - const Target& target_; // From the active poilicy target vector + const Target& target_; // From the active policy target vector. }; } // namespace rsz diff --git a/src/rsz/src/move/MoveGenerator.cc b/src/rsz/src/move/MoveGenerator.cc index 1feedd3e212..32b63e6d988 100644 --- a/src/rsz/src/move/MoveGenerator.cc +++ b/src/rsz/src/move/MoveGenerator.cc @@ -51,13 +51,4 @@ bool MoveGenerator::weakerCellFirst(const sta::LibertyCell* lhs, > std::tie(rhs_drive_resistance, rhs_intrinsic); } -std::vector MoveGenerator::inputCapBudgets( - const Target& target) const -{ - if (target.input_cap_budgets.has_value()) { - return *target.input_cap_budgets; - } - return resizer_.inputCapBudgets(target.inst(resizer_)); -} - } // namespace rsz diff --git a/src/rsz/src/move/MoveGenerator.hh b/src/rsz/src/move/MoveGenerator.hh index 9facafa44d8..2ce2414081e 100644 --- a/src/rsz/src/move/MoveGenerator.hh +++ b/src/rsz/src/move/MoveGenerator.hh @@ -104,16 +104,20 @@ class MoveGenerator const sta::LibertyPort* findScenePort(const sta::LibertyCell* cell, const std::string& port_name, int lib_ap_index) const; + // Compare equivalent cells by drive strength in the requested library. bool weakerCellFirst(const sta::LibertyCell* lhs, const sta::LibertyCell* rhs, const std::string& drvr_port_name, int lib_ap_index) const; // === Shared fanin max-cap helper ========================================= - // The cell-swap generators' fanin max-cap headroom: the prepared - // kInputCapBudgetCache, else a fresh main-thread capture. - std::vector inputCapBudgets( - const Target& target) const; + // The cell-swap generators' fanin max-cap headroom, prepared as + // kInputCapBudgetCache before generate(). + const std::vector& inputCapBudgets( + const Target& target) const + { + return *target.input_cap_budgets; + } // === Shared generator dependencies ======================================= Resizer& resizer_; diff --git a/src/rsz/src/move/RebufferMtCandidate.cc b/src/rsz/src/move/RebufferMtCandidate.cc new file mode 100644 index 00000000000..11f8be947aa --- /dev/null +++ b/src/rsz/src/move/RebufferMtCandidate.cc @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#include "RebufferMtCandidate.hh" + +#include +#include +#include + +#include "BufferedNet.hh" +#include "DelayEstimator.hh" +#include "MoveCandidate.hh" +#include "OptimizerTypes.hh" +#include "Rebuffer.hh" +#include "rsz/Resizer.hh" +#include "sta/Network.hh" +#include "sta/NetworkClass.hh" +#include "utl/Logger.h" + +namespace rsz { + +using utl::RSZ; + +RebufferMtCandidate::RebufferMtCandidate( + Resizer& resizer, + const Target& target, + const EstimatorContext& estimator_context, + const RebufferTreePlan& plan, + const float score_scale, + const float min_score, + std::vector side_contexts) + : MoveCandidate(resizer, target), + estimator_context_(estimator_context), + plan_(plan), + side_contexts_(std::move(side_contexts)), + score_scale_(score_scale), + min_score_(min_score) +{ +} + +Estimate RebufferMtCandidate::estimate() +{ + // Score the same topology that apply() commits. Side loads moved behind + // new buffers pay their negative-region slack loss out of the target gain. + const DelayEstimate delay_est + = DelayEstimator::estimateBufferTreeWithSidePaths( + estimator_context_, plan_, side_contexts_, nullptr, nullptr); + raw_score_ = delay_est.arrival_impr - delay_est.side_loss; + Estimate estimate{.reason = failReasonName(delay_est.reason)}; + estimate.score = scaledScore(); + if (delay_est.side_paths_evaluated > 0) { + estimate.side_loss = delay_est.side_loss; + } + + // An estimator reject keeps its own gain only when it computed one. + if (!delay_est.legal) { + if (delay_est.reason == FailReason::kEstimateNonImproving + || delay_est.reason == FailReason::kSideSlackFloorViolation) { + estimate.raw_gain = delay_est.arrival_impr; + } + return estimate; + } + + // The side loss or the score floor can still reject a legal estimate; a + // positive own gain stays admissible in the WNS tail. + estimate.raw_gain = delay_est.arrival_impr; + if (raw_score_ <= 0.0f || raw_score_ < min_score_) { + estimate.reason + = raw_score_ <= 0.0f ? "side_loss_non_improving" : "min_score"; + estimate.tail_admissible = delay_est.arrival_impr > 0.0f; + return estimate; + } + + estimate.legal = true; + return estimate; +} + +MoveResult RebufferMtCandidate::apply() +{ + const int inserted_count + = resizer_.rebuffer().applyRebufferTreePlan(plan_, "rebuffer_mt"); + if (inserted_count <= 0) { + debugPrint(resizer_.logger(), + RSZ, + "rebuffer_mt", + 1, + "REJECT rebuffer_mt {}: exportBufferTree failed", + logName()); + + return rejectedMove(); + } + + sta::Instance* driver_inst + = resizer_.network()->instance(target().driver_pin); + + debugPrint(resizer_.logger(), + RSZ, + "rebuffer_mt", + 1, + "ACCEPT rebuffer_mt {}: inserted {} buffers, tree loads {}, " + "raw_score {:.3e}, scaled_score {:.3e}", + logName(), + inserted_count, + plan_.load_count, + raw_score_, + scaledScore()); + + return { + .accepted = true, + .type = MoveType::kBuffer, + .move_count = inserted_count, + .touched_instances = {driver_inst}, + }; +} + +std::string RebufferMtCandidate::logName() const +{ + return resizer_.network()->pathName(target().driver_pin); +} + +} // namespace rsz diff --git a/src/rsz/src/move/RebufferMtCandidate.hh b/src/rsz/src/move/RebufferMtCandidate.hh new file mode 100644 index 00000000000..c762ef9f91f --- /dev/null +++ b/src/rsz/src/move/RebufferMtCandidate.hh @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#pragma once + +#include +#include + +#include "DelayEstimator.hh" +#include "MoveCandidate.hh" +#include "OptimizerTypes.hh" +#include "Rebuffer.hh" +#include "odb/geom.h" + +namespace rsz { + +// Candidate that scores and applies one full Rebuffer-generated buffer tree. +class RebufferMtCandidate : public MoveCandidate +{ + public: + // === Construction ========================================================= + // side_contexts lists the plan's other loads with their current slack; when + // non-empty, estimate() charges their buffered-detour loss against the gain. + RebufferMtCandidate(Resizer& resizer, + const Target& target, + const EstimatorContext& estimator_context, + const RebufferTreePlan& plan, + float score_scale, + float min_score, + std::vector side_contexts + = {}); + + // === MoveCandidate API ==================================================== + Estimate estimate() override; + MoveResult apply() override; + MoveType type() const override { return MoveType::kBuffer; } + float scaledScore() const { return raw_score_ * score_scale_; } + + private: + std::string logName() const; + + // === Candidate state ====================================================== + const EstimatorContext& estimator_context_; + RebufferTreePlan plan_; + std::vector side_contexts_; + const float score_scale_; + const float min_score_; + float raw_score_{0.0f}; +}; + +} // namespace rsz diff --git a/src/rsz/src/move/RebufferMtGenerator.cc b/src/rsz/src/move/RebufferMtGenerator.cc new file mode 100644 index 00000000000..91b3b874809 --- /dev/null +++ b/src/rsz/src/move/RebufferMtGenerator.cc @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#include "RebufferMtGenerator.hh" + +#include +#include +#include +#include +#include + +#include "MoveCandidate.hh" +#include "MoveCommitter.hh" +#include "MoveGenerator.hh" +#include "OptimizerTypes.hh" +#include "Rebuffer.hh" +#include "RebufferMtCandidate.hh" +#include "rsz/Resizer.hh" +#include "sta/Network.hh" +#include "sta/NetworkClass.hh" +#include "sta/Path.hh" +#include "sta/PathExpanded.hh" +#include "sta/StaState.hh" + +namespace rsz { + +namespace { + +constexpr int kRebufferMaxFanout = 20; + +const sta::Pin* targetLoadPin(const Target& target, Resizer& resizer) +{ + if (target.endpoint_path == nullptr || target.path_index < 0) { + return nullptr; + } + + sta::PathExpanded expanded(target.endpoint_path, resizer.staState()); + const int next_path_index = target.path_index + 1; + if (next_path_index >= static_cast(expanded.size())) { + return nullptr; + } + + const sta::Path* next_path = expanded.path(next_path_index); + if (next_path == nullptr) { + return nullptr; + } + + return next_path->pin(resizer.staState()); +} + +} // namespace + +RebufferMtGenerator::RebufferMtGenerator(const GeneratorContext& context) + : MoveGenerator(context) +{ +} + +bool RebufferMtGenerator::isApplicable(const Target& target) const +{ + return MoveGenerator::isApplicable(target) && target.fanout > 1 + && target.fanout < kRebufferMaxFanout + && resizer_.okToBufferNet(target.driver_pin) + && target.isPrepared(kEstimatorContextCache); +} + +std::vector> RebufferMtGenerator::generate( + const Target& target) +{ + // The main thread reads the net itself and charges no side loads. + Rebuffer& rebuffer_planner = resizer_.rebuffer(); + return generateWith(target, + rebuffer_planner, + rebuffer_planner.makeBufferedNet(target.driver_pin)); +} + +std::vector> RebufferMtGenerator::generateWith( + const Target& target, + Rebuffer& rebuffer_planner, + BufferedNetPtr net) const +{ + std::vector> candidates; + + if (committer_.hasMoves(MoveType::kBuffer, target.inst(resizer_))) { + return candidates; + } + + const sta::Pin* load_pin = targetLoadPin(target, resizer_); + if (load_pin == nullptr) { + return candidates; + } + + const int max_tree_candidates + = std::max(policy_config_.rebuffer_mt_max_candidates, 1); + const std::vector plans + = rebuffer_planner.makeRebufferTreePlans( + target.driver_pin, std::move(net), load_pin, max_tree_candidates); + addTreePlanCandidates(target, plans, candidates); + return candidates; +} + +void RebufferMtGenerator::addTreePlanCandidates( + const Target& target, + const std::vector& plans, + std::vector>& candidates) const +{ + for (const RebufferTreePlan& plan : plans) { + if (plan.buffer_count == 0) { + continue; + } + + // One-buffer trees compete in the pool only when the policy asks for + // them. + const bool single_buffer_plan = plan.buffer_count == 1; + if (single_buffer_plan && !plan.anchor_tree + && !policy_config_.allow_rebuffer_mt_single_buffer) { + continue; + } + + const float tree_score = std::fabs(plan.driver_slack_gain); + if (!plan.anchor_tree && policy_config_.rebuffer_mt_max_score > 0.0f + && tree_score > policy_config_.rebuffer_mt_max_score) { + continue; + } + + candidates.push_back(std::make_unique( + resizer_, + target, + target.estimator_context.value(), + plan, + policy_config_.rebuffer_mt_score_scale, + policy_config_.rebuffer_mt_min_score)); + } +} + +} // namespace rsz diff --git a/src/rsz/src/move/RebufferMtGenerator.hh b/src/rsz/src/move/RebufferMtGenerator.hh new file mode 100644 index 00000000000..af0ae5b7b91 --- /dev/null +++ b/src/rsz/src/move/RebufferMtGenerator.hh @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#pragma once + +#include +#include + +#include "DelayEstimator.hh" +#include "MoveCandidate.hh" +#include "MoveGenerator.hh" +#include "OptimizerTypes.hh" + +namespace rsz { + +class Rebuffer; +class Resizer; + +// MT form of Rebuffer. It plans several buffer trees for one driver, and a +// winning candidate commits exactly the tree it scored. +class RebufferMtGenerator : public MoveGenerator +{ + public: + // === Construction ========================================================= + explicit RebufferMtGenerator(const GeneratorContext& context); + + // === MoveGenerator API ==================================================== + MoveType type() const override { return MoveType::kBuffer; } + const char* name() const override { return "RebufferMtMove"; } + PrepareCacheMask prepareRequirements() const override + { + return kEstimatorContextCache; + } + bool isApplicable(const Target& target) const override; + // Plans with the Resizer's planner on the main thread. + std::vector> generate( + const Target& target) override; + + private: + // The candidates of one target: tree plans of `net` from `rebuffer_planner`. + std::vector> generateWith( + const Target& target, + Rebuffer& rebuffer_planner, + BufferedNetPtr net) const; + // One candidate per tree plan that passes the plan gates. + void addTreePlanCandidates( + const Target& target, + const std::vector& plans, + std::vector>& candidates) const; +}; + +} // namespace rsz diff --git a/src/rsz/src/move/SizeUpGenerator.cc b/src/rsz/src/move/SizeUpGenerator.cc index 5c2e7c2105e..eab7e198397 100644 --- a/src/rsz/src/move/SizeUpGenerator.cc +++ b/src/rsz/src/move/SizeUpGenerator.cc @@ -11,7 +11,6 @@ #include "MoveCandidate.hh" #include "MoveGenerator.hh" #include "OptimizerTypes.hh" -#include "RepairSetupProfiler.hh" #include "SizeUpCandidate.hh" #include "db_sta/dbSta.hh" #include "rsz/Resizer.hh" @@ -144,7 +143,6 @@ sta::LibertyCell* SizeUpGenerator::upsizeCell(sta::LibertyPort* in_port, const sta::Scene* scene, const sta::MinMax* min_max) const { - const ProfileScope profile("candidate_generation"); if (scene == nullptr || min_max == nullptr || in_port == nullptr || drvr_port == nullptr) { return nullptr; @@ -153,8 +151,6 @@ sta::LibertyCell* SizeUpGenerator::upsizeCell(sta::LibertyPort* in_port, const int lib_ap = scene->libertyIndex(min_max); sta::LibertyCell* cell = drvr_port->libertyCell(); sta::LibertyCellSeq swappable_cells = resizer_.getSwappableCells(cell); - RepairSetupProfiler::event( - "legacy_lib_options", swappable_cells.size(), moveName(type())); if (swappable_cells.empty()) { return nullptr; } @@ -171,8 +167,6 @@ sta::LibertyCell* SizeUpGenerator::upsizeCell(sta::LibertyPort* in_port, return weakerCellFirst(cell1, cell2, drvr_port_name, lib_ap); }); - // Separate delay evaluation from candidate enumeration and ordering. - const ProfileScope estimation("candidate_estimation"); const sta::LibertyPort* scene_drvr_port = static_cast(drvr_port)->scenePort(lib_ap); const sta::LibertyPort* scene_input_port @@ -200,12 +194,10 @@ sta::LibertyCell* SizeUpGenerator::upsizeCell(sta::LibertyPort* in_port, continue; } const float swappable_drive_r = swappable_drvr->driveResistance(); - RepairSetupProfiler::event("legacy_lib_estimated", 1, moveName(type())); const float swappable_delay = resizer_.gateDelay(swappable_drvr, load_cap, scene, min_max) + prev_drive * swappable_input->capacitance(); if (swappable_drive_r <= drive_r && swappable_delay < delay) { - RepairSetupProfiler::event("legacy_lib_selected", 1, moveName(type())); return swappable; } } diff --git a/src/rsz/src/move/SizeUpMtGenerator.cc b/src/rsz/src/move/SizeUpMtGenerator.cc index 5f3959d7091..f029b70460e 100644 --- a/src/rsz/src/move/SizeUpMtGenerator.cc +++ b/src/rsz/src/move/SizeUpMtGenerator.cc @@ -55,7 +55,7 @@ std::vector> SizeUpMtGenerator::generate( const std::vector& replacements = options.cells; // Fanin max-cap headroom is instance-level, captured once per target. - const std::vector budgets = inputCapBudgets(target); + const std::vector& budgets = inputCapBudgets(target); candidates.reserve(replacements.size()); for (sta::LibertyCell* replacement : replacements) { if (!resizer_.replacementFitsInputCapBudgets(budgets, replacement)) { diff --git a/src/rsz/src/move/SizeUpMtGenerator.hh b/src/rsz/src/move/SizeUpMtGenerator.hh index b3cbce6e8c6..7f6bc7ea10a 100644 --- a/src/rsz/src/move/SizeUpMtGenerator.hh +++ b/src/rsz/src/move/SizeUpMtGenerator.hh @@ -27,9 +27,9 @@ namespace rsz { // MT-safe generator that enumerates same-VT cells that do not weaken output // drive for one target using a prepared EstimatorContext. // -// prepareRequirements() asks for kEstimatorContextCache and -// kInputCapBudgetCache, so generate() runs on worker threads without live -// STA. Produces multiple SizeUpMtCandidate instances so +// prepareRequirements() asks for the estimator context, the flip-flop +// input-side checks and the input-cap budgets, so generate() runs on worker +// threads without live STA. Produces multiple SizeUpMtCandidate instances so // the policy can evaluate them in parallel and pick the best. class SizeUpMtGenerator : public MoveGenerator { @@ -41,7 +41,8 @@ class SizeUpMtGenerator : public MoveGenerator MoveType type() const override { return MoveType::kSizeUp; } PrepareCacheMask prepareRequirements() const override { - return kEstimatorContextCache | kInputCapBudgetCache; + return kEstimatorContextCache | kSequentialEstimatorCache + | kInputCapBudgetCache; } bool isApplicable(const Target& target) const override; std::vector> generate( diff --git a/src/rsz/src/move/VtSwapMtGenerator.cc b/src/rsz/src/move/VtSwapMtGenerator.cc index a8cdaddf3f0..6015b997a94 100644 --- a/src/rsz/src/move/VtSwapMtGenerator.cc +++ b/src/rsz/src/move/VtSwapMtGenerator.cc @@ -39,7 +39,7 @@ std::vector> VtSwapMtGenerator::generate( = selectCandidateCells(current_cell); // Fanin max-cap headroom is instance-level, captured once per target. - const std::vector budgets = inputCapBudgets(target); + const std::vector& budgets = inputCapBudgets(target); candidates.reserve(candidate_cells.size()); for (sta::LibertyCell* candidate_cell : candidate_cells) { if (policy_config_.dual_rf_drive_guard) { diff --git a/src/rsz/src/move/VtSwapMtGenerator.hh b/src/rsz/src/move/VtSwapMtGenerator.hh index c9787ac8658..80b417f28b6 100644 --- a/src/rsz/src/move/VtSwapMtGenerator.hh +++ b/src/rsz/src/move/VtSwapMtGenerator.hh @@ -20,9 +20,9 @@ namespace rsz { // MT-safe generator that enumerates VT-equivalent cells and produces // multiple VtSwapMtCandidate instances for parallel evaluation. // -// prepareRequirements() asks for kEstimatorContextCache and -// kInputCapBudgetCache, so generate() runs on worker threads without live -// STA. +// prepareRequirements() asks for the estimator context, the flip-flop +// input-side checks and the input-cap budgets, so generate() runs on worker +// threads without live STA. class VtSwapMtGenerator : public MoveGenerator { public: @@ -33,7 +33,8 @@ class VtSwapMtGenerator : public MoveGenerator MoveType type() const override { return MoveType::kVtSwap; } PrepareCacheMask prepareRequirements() const override { - return kEstimatorContextCache | kInputCapBudgetCache; + return kEstimatorContextCache | kSequentialEstimatorCache + | kInputCapBudgetCache; } bool isApplicable(const Target& target) const override; std::vector> generate( diff --git a/src/rsz/src/policy/OptimizationPolicy.cc b/src/rsz/src/policy/OptimizationPolicy.cc index 105da6a050b..47d0337f331 100644 --- a/src/rsz/src/policy/OptimizationPolicy.cc +++ b/src/rsz/src/policy/OptimizationPolicy.cc @@ -94,7 +94,7 @@ bool OptimizationPolicy::start() estimate_parasitics_ = resizer_.estimateParasitics(); max_ = resizer_.maxAnalysisMode(); resetRun(); - estimator_cache_ = DelayEstimatorCache{}; + estimator_cache_.clear(); // Start every phase with the progress table's area cache invalid. The // cache is keyed on MoveCommitter::netlistEdits(), which only counts // edits made through the committer; GlobalSizingPolicy replaces cells @@ -344,6 +344,9 @@ void OptimizationPolicy::prepareTarget(Target& target, if ((mask & kEstimatorContextCache) != 0) { prepareEstimatorContext(target); } + if ((mask & kSequentialEstimatorCache) != 0) { + prepareSequentialEstimator(target); + } // Generation inputs: each Mt generator's live STA reads, so that its // generate() runs on a worker thread. @@ -390,20 +393,28 @@ void OptimizationPolicy::prepareEstimatorContext(Target& target) const = policy_config_.fanin_side_loss_weight; } - if (target.estimator_context.has_value()) { - target.sequential_timing_estimator - = SequentialTimingEstimator::prepare(resizer_, target); - if (target.sequential_timing_estimator != nullptr) { - RepairSetupProfiler::event("sequential_targets_prepared"); - } - } - if (!target.estimator_context.has_value() && RepairSetupProfiler::enabled()) { RepairSetupProfiler::event(std::string("prepare_reject_") + failReasonName(fail_reason)); } } +void OptimizationPolicy::prepareSequentialEstimator(Target& target) const +{ + // Only SizeUpMt and VtSwapMt read it, so a run without them skips the + // flip-flop snapshots. + if (target.sequential_timing_estimator != nullptr + || !target.estimator_context.has_value()) { + return; + } + + target.sequential_timing_estimator + = SequentialTimingEstimator::prepare(resizer_, target); + if (target.sequential_timing_estimator != nullptr) { + RepairSetupProfiler::event("sequential_targets_prepared"); + } +} + void OptimizationPolicy::prepareInputCapBudgets(Target& target) const { if (!target.input_cap_budgets.has_value()) { diff --git a/src/rsz/src/policy/OptimizationPolicy.hh b/src/rsz/src/policy/OptimizationPolicy.hh index 539b23ead37..0ad83c26aaa 100644 --- a/src/rsz/src/policy/OptimizationPolicy.hh +++ b/src/rsz/src/policy/OptimizationPolicy.hh @@ -99,7 +99,6 @@ class OptimizationPolicy // before MT workers call generator code that reads those services. virtual void prewarmTargets(const std::vector& targets) const; virtual bool generatorEnabled(MoveType type) const; - // Main-thread prepare stage: capture per-target STA state before MT workers // generate or estimate candidates. virtual void prepareTargets(std::vector& targets) const; @@ -110,6 +109,8 @@ class OptimizationPolicy // Build the immutable arc-delay snapshot consumed by MT-safe candidates. void prepareEstimatorContext(Target& target) const; + // Prepare the flip-flop input-side checks of a cell swap of the target. + void prepareSequentialEstimator(Target& target) const; // Capture the target instance's fanin max-cap headroom. void prepareInputCapBudgets(Target& target) const; diff --git a/src/rsz/test/cpp/TestCellSwapMt.cc b/src/rsz/test/cpp/TestCellSwapMt.cc index dfe8749cd1c..055365872e2 100644 --- a/src/rsz/test/cpp/TestCellSwapMt.cc +++ b/src/rsz/test/cpp/TestCellSwapMt.cc @@ -85,7 +85,9 @@ TEST_F(TestResizerMt, SizeUpMtGeneratorCachesLibraryOptionsPerDriverPort) generator.buildSizeUpOptions(arc.outputPort(), arc.scene, arc.min_max) .cells); - // Generation consumes the cached options without changing the candidates. + // Generation consumes the cached options without changing the candidates; + // generate() reads the prepared fanin headroom. + target.input_cap_budgets = resizer_.inputCapBudgets(target.inst(resizer_)); const CandidateVector candidates = generator.generate(target); EXPECT_EQ(candidates.size(), first.cells.size()); EXPECT_EQ(generator.size_up_options_.size(), 1u); diff --git a/src/rsz/test/cpp/TestResizerMtFixture.hh b/src/rsz/test/cpp/TestResizerMtFixture.hh index d60e34cf4e3..0ee7df9bbf1 100644 --- a/src/rsz/test/cpp/TestResizerMtFixture.hh +++ b/src/rsz/test/cpp/TestResizerMtFixture.hh @@ -90,6 +90,7 @@ namespace rsz { +// Shared Nangate45 fixture for MT estimator, policy, and profiler tests. class TestResizerMt : public tst::IntegratedFixture { protected: @@ -219,6 +220,7 @@ class TestResizerMt : public tst::IntegratedFixture } }; +// Extends the MT fixture with a sequential cell and connected timing paths. class TestResizerMtSequential : public TestResizerMt { protected: @@ -372,6 +374,7 @@ inline void expectSameDelayContext(const EstimatorContext& lhs, EXPECT_FLOAT_EQ(left.load_cap, right.load_cap); EXPECT_FLOAT_EQ(left.current_model_delay, right.current_model_delay); EXPECT_FLOAT_EQ(left.current_delay, right.current_delay); + EXPECT_FLOAT_EQ(left.current_wire_delay, right.current_wire_delay); EXPECT_FLOAT_EQ(left.current_model_slew, right.current_model_slew); EXPECT_FLOAT_EQ(left.current_slew, right.current_slew); ASSERT_EQ(left.output_slew_merge_arcs.size(), @@ -387,6 +390,7 @@ inline void expectSameDelayContext(const EstimatorContext& lhs, } } +// Deterministic buffer move used to verify committer accounting in tests. class StubBufferMoveCandidate : public MoveCandidate { public: @@ -415,6 +419,7 @@ class StubBufferMoveCandidate : public MoveCandidate const bool accepted_; }; +// Owns a temporary profiler output directory for one test scope. class ProfileTestDirectory { public: From 32b4e4d06d7226b1914ba6736a563b1616ddf1a5 Mon Sep 17 00:00:00 2001 From: Jaehyun Kim Date: Fri, 16 Oct 2026 23:51:59 -0700 Subject: [PATCH 3/3] rsz: Add the experimental PARALLEL_EVAL setup repair policy What: PARALLEL_EVAL, an experimental setup repair policy that commits a batch of VtSwapMt, SizeUpMt and RebufferMt moves per timing update. Why: one timing update per batch instead of per move cuts the repair time. How: worker threads scan endpoints and generate and score candidates from prepared data; BatchSelector, BottleneckAnalysis and ParallelEvalState pick, rank and stop; each batch is kept, salvaged or rolled back by its WNS and TNS (the group's under -path_group). OptimizationPolicy::finish() ends each phase, and the Optimizer prints the final report after the last phase. LEGACY results do not change. Signed-off-by: Jaehyun Kim --- src/est/include/est/EstimateParasitics.h | 1 + src/rsz/BUILD | 11 + src/rsz/README.md | 46 +- src/rsz/doc/architecture.md | 6 +- src/rsz/include/rsz/Resizer.hh | 15 + src/rsz/src/CMakeLists.txt | 6 + src/rsz/src/MoveCommitter.cc | 84 +- src/rsz/src/MoveCommitter.hh | 13 +- src/rsz/src/MoveTracker.cc | 58 +- src/rsz/src/MoveTracker.hh | 5 + src/rsz/src/OptimizationPolicyConfig.cc | 53 + src/rsz/src/OptimizationPolicyConfig.hh | 190 +- src/rsz/src/Optimizer.cc | 133 +- src/rsz/src/Optimizer.hh | 15 +- src/rsz/src/OptimizerTypes.cc | 14 +- src/rsz/src/OptimizerTypes.hh | 54 +- src/rsz/src/Rebuffer.cc | 33 +- src/rsz/src/Rebuffer.hh | 22 +- src/rsz/src/RepairSetupContext.hh | 8 +- src/rsz/src/RepairSetupProfiler.cc | 78 +- src/rsz/src/RepairSetupProfiler.hh | 52 +- src/rsz/src/RepairTargetCollector.cc | 112 +- src/rsz/src/RepairTargetCollector.hh | 42 +- src/rsz/src/Resizer-py.i | 5 +- src/rsz/src/Resizer.cc | 25 + src/rsz/src/Resizer.i | 2 + src/rsz/src/Resizer.tcl | 26 +- src/rsz/src/move/MoveCandidate.cc | 60 + src/rsz/src/move/MoveCandidate.hh | 23 + src/rsz/src/move/MoveGenerator.hh | 16 + src/rsz/src/move/RebufferMtCandidate.cc | 43 + src/rsz/src/move/RebufferMtCandidate.hh | 2 + src/rsz/src/move/RebufferMtGenerator.cc | 163 +- src/rsz/src/move/RebufferMtGenerator.hh | 49 +- src/rsz/src/move/RebufferPlannerPool.cc | 70 + src/rsz/src/move/RebufferPlannerPool.hh | 70 + src/rsz/src/move/SizeUpMtCandidate.cc | 5 + src/rsz/src/move/SizeUpMtCandidate.hh | 5 + src/rsz/src/move/VtSwapMtCandidate.cc | 5 + src/rsz/src/move/VtSwapMtCandidate.hh | 5 + src/rsz/src/move/VtSwapMtGenerator.cc | 5 +- src/rsz/src/policy/BatchSelector.cc | 259 +++ src/rsz/src/policy/BatchSelector.hh | 115 ++ src/rsz/src/policy/BottleneckAnalysis.cc | 263 +++ src/rsz/src/policy/BottleneckAnalysis.hh | 89 + src/rsz/src/policy/OptimizationPolicy.cc | 164 +- src/rsz/src/policy/OptimizationPolicy.hh | 32 +- src/rsz/src/policy/ParallelEvalState.cc | 341 ++++ src/rsz/src/policy/ParallelEvalState.hh | 236 +++ src/rsz/src/policy/ProgressReporter.cc | 9 +- src/rsz/src/policy/ProgressReporter.hh | 7 +- src/rsz/src/policy/SetupCritVtSwapPolicy.cc | 2 +- src/rsz/src/policy/SetupDirectionalPolicy.cc | 2 +- src/rsz/src/policy/SetupDirectionalPolicy.hh | 2 - src/rsz/src/policy/SetupLastGaspPolicy.cc | 2 +- src/rsz/src/policy/SetupLegacyBase.cc | 11 +- src/rsz/src/policy/SetupLegacyPolicy.cc | 2 +- src/rsz/src/policy/SetupParallelEvalPolicy.cc | 1681 +++++++++++++++++ src/rsz/src/policy/SetupParallelEvalPolicy.hh | 313 +++ src/rsz/src/policy/SetupTnsPolicy.cc | 3 +- src/rsz/src/policy/SetupWnsPolicy.cc | 2 +- src/rsz/test/BUILD | 11 + src/rsz/test/CMakeLists.txt | 8 + src/rsz/test/cpp/CMakeLists.txt | 2 + src/rsz/test/cpp/TestDelayEstimatorMt.cc | 2 +- src/rsz/test/cpp/TestParallelEvalPolicy.cc | 995 ++++++++++ src/rsz/test/cpp/TestRepairSetupProfiler.cc | 3 +- src/rsz/test/cpp/TestRepairTargetCollector.cc | 98 + src/rsz/test/cpp/TestResizerMtFixture.hh | 12 + .../test/repair_setup_dont_touch_sizeup.ok | 11 + .../test/repair_setup_dont_touch_sizeup.tcl | 15 +- src/rsz/test/repair_setup_invalid_phase.ok | 4 +- src/rsz/test/repair_setup_parallel_eval.ok | 15 + src/rsz/test/repair_setup_parallel_eval.tcl | 13 + ...ir_setup_parallel_eval_default_sequence.ok | 46 + ...r_setup_parallel_eval_default_sequence.tcl | 31 + .../test/repair_setup_parallel_eval_effort.ok | 22 + .../repair_setup_parallel_eval_effort.tcl | 33 + .../repair_setup_parallel_eval_max_util.ok | 21 + .../repair_setup_parallel_eval_max_util.tcl | 24 + .../repair_setup_parallel_eval_path_group.ok | 38 + .../repair_setup_parallel_eval_path_group.tcl | 32 + .../repair_setup_parallel_eval_threads.ok | 15 + .../repair_setup_parallel_eval_threads.tcl | 5 + .../repair_setup_parallel_eval_vt_swap.ok | 20 + .../repair_setup_parallel_eval_vt_swap.tcl | 34 + ...air_setup_parallel_eval_vt_swap_corners.ok | 20 + ...ir_setup_parallel_eval_vt_swap_corners.tcl | 35 + src/rsz/test/repair_timing_fixed.ok | 2 +- src/rsz/test/repair_timing_fixed.tcl | 10 + src/rsz/test/rsz_aux.py | 4 + 91 files changed, 6418 insertions(+), 328 deletions(-) create mode 100644 src/rsz/src/OptimizationPolicyConfig.cc create mode 100644 src/rsz/src/move/RebufferPlannerPool.cc create mode 100644 src/rsz/src/move/RebufferPlannerPool.hh create mode 100644 src/rsz/src/policy/BatchSelector.cc create mode 100644 src/rsz/src/policy/BatchSelector.hh create mode 100644 src/rsz/src/policy/BottleneckAnalysis.cc create mode 100644 src/rsz/src/policy/BottleneckAnalysis.hh create mode 100644 src/rsz/src/policy/ParallelEvalState.cc create mode 100644 src/rsz/src/policy/ParallelEvalState.hh create mode 100644 src/rsz/src/policy/SetupParallelEvalPolicy.cc create mode 100644 src/rsz/src/policy/SetupParallelEvalPolicy.hh create mode 100644 src/rsz/test/cpp/TestParallelEvalPolicy.cc create mode 100644 src/rsz/test/cpp/TestRepairTargetCollector.cc create mode 100644 src/rsz/test/repair_setup_parallel_eval.ok create mode 100644 src/rsz/test/repair_setup_parallel_eval.tcl create mode 100644 src/rsz/test/repair_setup_parallel_eval_default_sequence.ok create mode 100644 src/rsz/test/repair_setup_parallel_eval_default_sequence.tcl create mode 100644 src/rsz/test/repair_setup_parallel_eval_effort.ok create mode 100644 src/rsz/test/repair_setup_parallel_eval_effort.tcl create mode 100644 src/rsz/test/repair_setup_parallel_eval_max_util.ok create mode 100644 src/rsz/test/repair_setup_parallel_eval_max_util.tcl create mode 100644 src/rsz/test/repair_setup_parallel_eval_path_group.ok create mode 100644 src/rsz/test/repair_setup_parallel_eval_path_group.tcl create mode 100644 src/rsz/test/repair_setup_parallel_eval_threads.ok create mode 100644 src/rsz/test/repair_setup_parallel_eval_threads.tcl create mode 100644 src/rsz/test/repair_setup_parallel_eval_vt_swap.ok create mode 100644 src/rsz/test/repair_setup_parallel_eval_vt_swap.tcl create mode 100644 src/rsz/test/repair_setup_parallel_eval_vt_swap_corners.ok create mode 100644 src/rsz/test/repair_setup_parallel_eval_vt_swap_corners.tcl diff --git a/src/est/include/est/EstimateParasitics.h b/src/est/include/est/EstimateParasitics.h index eb0279e6ae7..742fac04ab4 100644 --- a/src/est/include/est/EstimateParasitics.h +++ b/src/est/include/est/EstimateParasitics.h @@ -169,6 +169,7 @@ class EstimateParasitics : public sta::dbStaState, public ParasiticsService void parasiticsInvalid(const odb::dbNet* net); void eraseParasitics(const sta::Net* net); bool parasiticsValid() const; + size_t invalidNetCount() const { return parasitics_invalid_.size(); } ParasiticsSrc getParasiticsSrc() { return parasitics_src_; } void setParasiticsSrc(ParasiticsSrc src); diff --git a/src/rsz/BUILD b/src/rsz/BUILD index 372d176527d..3d67e8ca20d 100644 --- a/src/rsz/BUILD +++ b/src/rsz/BUILD @@ -28,6 +28,7 @@ cc_library( "src/MoveCommitter.cc", "src/MoveTracker.cc", "src/OdbCallBack.cc", + "src/OptimizationPolicyConfig.cc", "src/OptimizationPolicyConfig.hh", "src/Optimizer.cc", "src/Optimizer.hh", @@ -64,6 +65,8 @@ cc_library( "src/move/RebufferMtCandidate.cc", "src/move/RebufferMtCandidate.hh", "src/move/RebufferMtGenerator.cc", + "src/move/RebufferPlannerPool.cc", + "src/move/RebufferPlannerPool.hh", "src/move/RerouteCandidate.cc", "src/move/RerouteCandidate.hh", "src/move/RerouteGenerator.cc", @@ -101,10 +104,13 @@ cc_library( "src/move/VtSwapMtCandidate.cc", "src/move/VtSwapMtGenerator.cc", "src/move/VtSwapMtGenerator.hh", + "src/policy/BatchSelector.cc", + "src/policy/BottleneckAnalysis.cc", "src/policy/GlobalSizingPolicy.cc", "src/policy/GlobalSizingPolicy.hh", "src/policy/OptimizationPolicy.cc", "src/policy/OptimizationPolicy.hh", + "src/policy/ParallelEvalState.cc", "src/policy/ProgressReporter.cc", "src/policy/ProgressReporter.hh", "src/policy/SetupCritVtSwapPolicy.cc", @@ -115,6 +121,7 @@ cc_library( "src/policy/SetupLastGaspPolicy.hh", "src/policy/SetupLegacyBase.cc", "src/policy/SetupLegacyPolicy.cc", + "src/policy/SetupParallelEvalPolicy.cc", "src/policy/SetupReroutePolicy.cc", "src/policy/SetupReroutePolicy.hh", "src/policy/SetupTnsPolicy.cc", @@ -145,8 +152,12 @@ cc_library( "src/move/SizeUpMtCandidate.hh", "src/move/SizeUpMtGenerator.hh", "src/move/VtSwapMtCandidate.hh", + "src/policy/BatchSelector.hh", + "src/policy/BottleneckAnalysis.hh", + "src/policy/ParallelEvalState.hh", "src/policy/SetupLegacyBase.hh", "src/policy/SetupLegacyPolicy.hh", + "src/policy/SetupParallelEvalPolicy.hh", ], includes = [ "include", diff --git a/src/rsz/README.md b/src/rsz/README.md index 3b3a4f3267f..7bb27764b4a 100644 --- a/src/rsz/README.md +++ b/src/rsz/README.md @@ -238,6 +238,9 @@ repair_timing [-skip_vt_swap] [-skip_crit_vt_swap] [-skip_crpr] + [-effort low|medium|high] + [-profile_mode mode] + [-profile_dir dir] [-repair_tns tns_end_percent] [-max_passes passes] [-max_iterations iterations] @@ -259,7 +262,7 @@ repair_timing | `-hold_margin` | Add additional hold slack margin. | | `-allow_setup_violations` | While repairing hold violations, buffers are not inserted that will cause setup violations unless `-allow_setup_violations` is specified. | | `-sequence` | Specify a particular order of setup timing optimization moves. The default is "unbuffer vt_swap sizeup swap buffer clone split". Obeys skip flags also. | -| `-phases` | Specify a particular order of setup timing optimization phases. The default is "LEGACY LAST_GASP CRIT_VT_SWAP". | +| `-phases` | Specify a particular order of setup timing optimization phases. The default is "LEGACY LAST_GASP CRIT_VT_SWAP". The experimental `PARALLEL_EVAL` policy is described in [PARALLEL_EVAL Policy](#parallel_eval-policy). | | `-path_group` | Restrict setup and hold repair to one path group: `reg2reg`, `in2reg`, `reg2out`, `in2out` or `gated_clock`. Any other name warns and repairs every path group, which is the default. A supported group that the SDC has no `group_path` for is created, so timing reports group paths the same way `repair_timing` optimizes them. | | `-skip_pin_swap` | Flag to skip pin swap. The default is to perform pin swap transform during setup fixing. | | `-skip_gate_cloning` | Flag to skip gate cloning. The default is to perform gate cloning transform during setup fixing. | @@ -271,8 +274,9 @@ repair_timing | `-skip_vt_swap` | Flag to skip threshold voltage (VT) swap optimizations. The default is to perform VT swap optimization to improve timing QoR. | | `-skip_crit_vt_swap` | Flag to skip critical threshold voltage (VT) swap optimizations at the end of optimization. The default is to perform critical VT swap optimization to improve timing QoR beyond repairing just the worst path per each violating endpoint. | | `-skip_crpr` | Disable Clock Reconvergence Pessimism Removal (CRPR) in clock paths during repair to reduce runtime at the expense of increased area and power. Timing reported during repair excludes CRPR. | +| `-effort` | Optimization effort of the `PARALLEL_EVAL` policy: `low`, `medium` or `high` (default). `low` stops when the batches stall and skips the WNS tail on the worst endpoints. `medium` declares the batch stall after half as many batches, asks twice the progress to reset it, and gives the WNS tail a quarter of the maximum tail endpoints and a quarter of the work budget of `high`. `high` runs the full tail. Other policies ignore the option, and `repair_timing` warns when `-phases` does not list `PARALLEL_EVAL`. | | `-repair_tns` | Percentage of violating endpoints to repair (0-100). When `tns_end_percent` is zero, only the worst endpoint is repaired. When `tns_end_percent` is 100 (default), all violating endpoints are repaired. | -| `-profile_mode` | Setup repair profiler level: `0` (default) writes nothing, `1` records the WNS/TNS trajectory, `2` adds the pass events. Each `repair_timing -setup` call writes one `run_N` directory under `-profile_dir`. | +| `-profile_mode` | Setup repair profiler level: `0` (default) writes nothing, `1` records the WNS/TNS trajectory, `2` adds the pass and batch events. Each `repair_timing -setup` call writes one `run_N` directory under `-profile_dir`. | | `-profile_dir` | Directory of the profiler output; the default is `repair_setup_profile` under the current directory. | | `-max_repairs_per_pass` | Maximum repairs per pass, default is 1. On the worst paths, the maximum number of repairs is attempted. It gradually decreases until the final violations which only get 1 repair per pass. | | `-max_utilization` | Defines the percentage of core area used. | @@ -309,6 +313,44 @@ Use`-recover_power` to specify the percent of paths with positive slack which will be considered for gate resizing to save power. It is recommended that this option be used with global routing based parasitics. +#### PARALLEL_EVAL Policy + +`-phases PARALLEL_EVAL` repairs setup timing in batches: worker threads +estimate the moves of many targets at once, the non-conflicting moves with the +best estimates are committed as one batch, and one timing update judges the +batch against its checkpoint. Batches stop when they stall, and a WNS tail +then works on the worst endpoints one batch at a time. The policy differs from +`LEGACY` in these ways: + +- Threads: `set_thread_count` gives the policy its workers (the thread count + minus one). The result does not depend on the thread count. Endpoint scans + of 4096 endpoints or more also run on the workers. Buffer tree planning on + the workers counts its warnings instead of logging them, and the phase ends + with one warning that lists the counts; a single-thread run logs each one. +- Area: a batch that leaves the design above `-max_utilization` is undone, + and the first such batch prints a warning. The design area counts the + standard cells, not macros or fillers. Without `-max_utilization` there is + no area limit, as in `LEGACY`. +- Iterations: `-max_iterations` counts batches, the WNS tail included. +- Retries: an instance in three undone batches is not tried again in the + run, the WNS tail included. +- Progress: without `-verbose` the progress table prints the first and the + final row only; `-verbose` prints a row per batch. +- Effort: `-effort` tunes the stall and WNS tail limits. +- Moves: `-sequence` lists the moves of the batches; the default is + `vt_swap sizeup buffer`, and the `-skip_*` options apply. Buffering plans + up to three buffer trees per driver with fewer than 20 loads. +- Endpoints: `-repair_tns` limits each batch to that share of the violating + endpoints, as `LEGACY` limits the endpoints it repairs. +- Path groups: under `-path_group` a batch is judged by the group's WNS and + TNS, as in `LEGACY`. The paths beside a move are still kept above the + design's worst slack, whatever group they belong to. +- No effect: `-max_passes` and `-max_repairs_per_pass` drive the + per-endpoint policies only; `repair_timing` warns when they are given and + `-phases` lists only `PARALLEL_EVAL`. +- Profiling: `-profile_mode` records the batch decisions under + `-profile_dir`. + #### Instance Name Prefixes `repair_timing` uses the following prefixes for the buffer and gate instances that it inserts: diff --git a/src/rsz/doc/architecture.md b/src/rsz/doc/architecture.md index 9ab3e9dd33c..d4e1e1d5345 100644 --- a/src/rsz/doc/architecture.md +++ b/src/rsz/doc/architecture.md @@ -343,7 +343,6 @@ owned by `Resizer` / `dbNetwork`, not by individual `Target` objects. |---|---|---| | Swappable-cell cache | SizeUp and SizeUpMt candidate selection | Avoid repeated `getSwappableCells()` and VT metadata work. | | VT-equivalent-cell cache | VtSwap and VtSwapMt candidate selection | Avoid repeated `getVTEquivCells()` and VT metadata work. | -| Driver-pin cache | MT max-cap checks | Force `dbNetwork::drivers()` lazy cache population on the caller thread before worker-side checks. | The prewarm stage is a pragmatic guard for experimental MT policies. Some APIs look read-only from the caller but can populate internal caches on first use. @@ -544,6 +543,11 @@ Current generator/candidate families: - Currently, only VtSwap and SizeUp moves have simple multi-threading versions, which should be enhanced further. +A generator that keeps state per worker thread sets it up in `startPhase()`, +which learns the worker count, and releases it in `finishPhase()`. +`PARALLEL_EVAL` calls both on the main thread from its `start()` and +`finish()`; the default keeps no state. + ## Threading Model `utl::ThreadPool` was added as a common utility, not an rsz-local class. diff --git a/src/rsz/include/rsz/Resizer.hh b/src/rsz/include/rsz/Resizer.hh index 0bd52770f5d..3d633467b1c 100644 --- a/src/rsz/include/rsz/Resizer.hh +++ b/src/rsz/include/rsz/Resizer.hh @@ -116,6 +116,16 @@ enum class MoveType : uint8_t kCount }; +// How long repair_timing -setup keeps working once WNS and TNS converge. +// Only PARALLEL_EVAL reads it: low skips the WNS tail, medium stalls the +// batches and the tail sooner, and high runs the full tail. +enum class RepairEffort : uint8_t +{ + kLow, + kMedium, + kHigh +}; + // Voltage Threshold (VT) category identifier struct VTCategory { @@ -372,6 +382,7 @@ class Resizer : public sta::dbStaState, public sta::dbNetworkObserver bool skip_last_gasp, bool skip_vt_swap, bool skip_crit_vt_swap, + RepairEffort effort = RepairEffort::kHigh, int profile_mode = 0, const char* profile_dir = ""); // For testing. @@ -565,6 +576,8 @@ class Resizer : public sta::dbStaState, public sta::dbNetworkObserver void setDebugGraphics(std::shared_ptr graphics); static std::vector parseMoveSequence(const std::string& sequence); + // "low" or "medium"; anything else is the default, high. + static RepairEffort repairEffortFromString(const std::string& effort); void fullyRebuffer(sta::Pin* pin); bool hasFanout(sta::Vertex* drvr); @@ -986,6 +999,8 @@ class Resizer : public sta::dbStaState, public sta::dbNetworkObserver void journalRestore(); void journalMakeBuffer(sta::Instance* buffer); void initForJournalRestore(); + // An ECO undo skips the incremental area updates of the undone edits. + void recountDesignAreaAfterUndo(); void updateParasiticsAndTiming(); void invalidateParasitics(sta::Net* net); void addAcceptedLegacyMoveCount(int count); diff --git a/src/rsz/src/CMakeLists.txt b/src/rsz/src/CMakeLists.txt index c14dc86d252..10071957287 100644 --- a/src/rsz/src/CMakeLists.txt +++ b/src/rsz/src/CMakeLists.txt @@ -27,6 +27,7 @@ add_library(rsz_lib policy/ProgressReporter.cc Optimizer.cc OptimizerTypes.cc + OptimizationPolicyConfig.cc DelayEstimator.cc DelayEstimatorBufferTree.cc DelayEstimatorReporter.cc @@ -38,6 +39,10 @@ add_library(rsz_lib policy/SetupLegacyPolicy.cc policy/SetupTnsPolicy.cc policy/SetupWnsPolicy.cc + policy/BottleneckAnalysis.cc + policy/BatchSelector.cc + policy/ParallelEvalState.cc + policy/SetupParallelEvalPolicy.cc policy/SetupReroutePolicy.cc policy/GlobalSizingPolicy.cc Resizer.cc @@ -55,6 +60,7 @@ add_library(rsz_lib move/MoveGenerator.cc move/RebufferMtCandidate.cc move/RebufferMtGenerator.cc + move/RebufferPlannerPool.cc move/RerouteCandidate.cc move/RerouteGenerator.cc move/SizeDownFanoutCandidate.cc diff --git a/src/rsz/src/MoveCommitter.cc b/src/rsz/src/MoveCommitter.cc index 277c0369a23..5041b8a3951 100644 --- a/src/rsz/src/MoveCommitter.cc +++ b/src/rsz/src/MoveCommitter.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -55,7 +56,9 @@ MoveStateType moveTrackerState(const TrackedMoveStateType state) } // namespace -MoveCommitter::MoveCommitter(Resizer& resizer) : resizer_(resizer) +MoveCommitter::MoveCommitter(Resizer& resizer) + : resizer_(resizer), + move_tracker_(std::make_unique(resizer, false)) { } @@ -74,16 +77,12 @@ void MoveCommitter::init() const bool report_enabled = resizer_.logger()->debugCheck(RSZ, "move_tracker", 1); - if (report_enabled) { - move_tracker_ = std::make_unique(resizer_, report_enabled); - } else { - move_tracker_.reset(); - } + move_tracker_ = std::make_unique(resizer_, report_enabled); } bool MoveCommitter::moveTrackerEnabled(const int level) const { - return move_tracker_ != nullptr && move_tracker_->reportEnabled() + return move_tracker_->reportEnabled() && resizer_.logger()->debugCheck(RSZ, "move_tracker", level); } @@ -267,7 +266,7 @@ void MoveCommitter::printTrackerFinalReports( int MoveCommitter::getVisitCount(const sta::Pin* pin) const { - return move_tracker_ != nullptr ? move_tracker_->getVisitCount(pin) : 0; + return move_tracker_->getVisitCount(pin); } void MoveCommitter::printMoveSummary(const std::string& title) @@ -305,27 +304,6 @@ void MoveCommitter::printMissedOpportunitiesReport(const std::string& title) } } -void MoveCommitter::captureOriginalEndpointSlack() -{ - if (moveTrackerEnabled()) { - move_tracker_->captureOriginalEndpointSlack(); - } -} - -void MoveCommitter::capturePrePhaseSlack() -{ - if (moveTrackerEnabled()) { - move_tracker_->capturePrePhaseSlack(); - } -} - -void MoveCommitter::captureInitialSlackDistribution() -{ - if (moveTrackerEnabled()) { - move_tracker_->captureInitialSlackDistribution(); - } -} - void MoveCommitter::printSlackDistribution(const std::string& title) { if (moveTrackerEnabled()) { @@ -481,18 +459,16 @@ void MoveCommitter::beginJournal() pending_move_results_.emplace(); // Mirror the journal level in the tracker so a nested restore rejects only // its own moves. - if (move_tracker_) { - move_tracker_->beginJournal(); - } + move_tracker_->beginJournal(); } -void MoveCommitter::commitJournal() +void MoveCommitter::commitJournal(const bool update_timing) { if (!canManageJournal() || pending_move_results_.empty()) { return; } // Refresh timing only once after a batch of ECO edits becomes permanent - if (ecoHasPendingChanges()) { + if (update_timing && ecoHasPendingChanges()) { resizer_.updateParasiticsAndTiming(); } // Close the reversible batch. odb::commitEco appends the inner journal to its @@ -500,9 +476,7 @@ void MoveCommitter::commitJournal() commitEcoJournal(); // Mirror the journal commit in the tracker: an outermost commit finalizes // the tracked moves, a nested commit merges them into the parent level. - if (move_tracker_) { - move_tracker_->commitJournal(); - } + move_tracker_->commitJournal(); std::vector top = std::move(pending_move_results_.top()); pending_move_results_.pop(); @@ -543,9 +517,7 @@ void MoveCommitter::restoreJournal() restoreEcoJournal(); // Mirror the journal restore in the tracker so its moves are rejected, // whether this is an outermost or a nested restore. - if (move_tracker_) { - move_tracker_->restoreJournal(); - } + move_tracker_->restoreJournal(); // Unrecord just this level's accepted results; the parent's accumulated // pending state (if any) must survive. @@ -625,6 +597,38 @@ void MoveCommitter::rejectPendingMoves() logCommittedTotals(); } +bool MoveCommitter::rejectPendingCellMove(const MoveResult& result, + const sta::Pin* driver_pin) +{ + if (!result.accepted || result.move_count != 1 + || result.touched_instances.size() != 1 + || (result.type != MoveType::kSizeUp && result.type != MoveType::kVtSwap) + || pending_move_results_.empty()) { + return false; + } + + // Match the latest application, leaving earlier moves on this instance + // intact. + std::vector& pending = pending_move_results_.top(); + const std::vector::reverse_iterator move = std::find_if( + pending.rbegin(), pending.rend(), [&](const MoveResult& item) { + return item.accepted && item.type == result.type && item.move_count == 1 + && item.touched_instances == result.touched_instances; + }); + if (move == pending.rend()) { + return false; + } + + // The accepted inverse remains in OpenDB, but the original result must not + // be accepted or rejected again when the parent journal is closed. + unrecordAcceptedResult(*move); + resizer_.addRejectedLegacyMoveCount(1); + move_tracker_->rejectPendingMove(driver_pin, moveName(result.type)); + + pending.erase(std::next(move).base()); + return true; +} + int MoveCommitter::pendingMoves(const MoveType type) const { return pending_by_type_[typeIndex(type)]; diff --git a/src/rsz/src/MoveCommitter.hh b/src/rsz/src/MoveCommitter.hh index 0ddb6ca1e35..52fe0e31668 100644 --- a/src/rsz/src/MoveCommitter.hh +++ b/src/rsz/src/MoveCommitter.hh @@ -106,16 +106,15 @@ class MoveCommitter void printSlackDistribution(const std::string& title); void printTopBinEndpoints(const std::string& title, int max_endpoints = 20); void printCriticalEndpointPathHistogram(const std::string& title); - void captureInitialSlackDistribution(); - void captureOriginalEndpointSlack(); - void capturePrePhaseSlack(); + // The move tracker; while its report is off it ignores the capture calls. + MoveTracker& tracker() { return *move_tracker_; } int getVisitCount(const sta::Pin* pin) const; void clearTrackedMoves(); // === ECO journal lifecycle =============================================== // ECO journal lifecycle for setup-repair endpoint loops. void beginJournal(); - void commitJournal(); + void commitJournal(bool update_timing = true); void restoreJournal(); // === Move accounting ====================================================== @@ -123,6 +122,12 @@ class MoveCommitter // applied. void acceptPendingMoves(); void rejectPendingMoves(); + // Remove one native cell move from the current pending level after the + // caller swapped the cell back inside the open journal. Does not edit the + // database or settle the profiler; returns false if the result is + // unsupported or not pending. + bool rejectPendingCellMove(const MoveResult& result, + const sta::Pin* driver_pin); int pendingMoves(MoveType type) const; int committedMoves(MoveType type) const; diff --git a/src/rsz/src/MoveTracker.cc b/src/rsz/src/MoveTracker.cc index 3e8d3d05c85..0acf2f586a4 100644 --- a/src/rsz/src/MoveTracker.cc +++ b/src/rsz/src/MoveTracker.cc @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -268,10 +269,12 @@ void MoveTracker::trackMove(const sta::Pin* pin, // journal level, so a nested restore can reject exactly that level. With // no open journal it goes to the flat bucket commitMoves()/rejectMoves() // drain. + MoveStateData move(pin, move_type, state); + move.endpoint_pin = current_endpoint_; if (pending_move_levels_.empty()) { - pending_moves_.emplace_back(pin, move_type, state); + pending_moves_.push_back(std::move(move)); } else { - pending_move_levels_.top().emplace_back(pin, move_type, state); + pending_move_levels_.top().push_back(std::move(move)); } } @@ -282,16 +285,17 @@ void MoveTracker::finalizeMoves(const std::vector& level, moves_.emplace_back(move.pin, move_count_++, move.move_type, final_state); // Store in move event list for detailed reports pin_move_events_[move.pin].emplace_back(move.move_type, final_state); - } - // Track per-endpoint statistics - if (current_endpoint_ && !level.empty()) { - auto& counts = endpoint_move_counts_[current_endpoint_]; - std::get<0>(counts) += level.size(); // attempts - if (final_state == MoveStateType::ATTEMPT_COMMIT) { - std::get<2>(counts) += level.size(); // commits - } else { - std::get<1>(counts) += level.size(); // rejects + // Track per-endpoint statistics on the endpoint each move was recorded + // for; a batch journal level holds moves of many endpoints. + if (move.endpoint_pin != nullptr) { + auto& counts = endpoint_move_counts_[move.endpoint_pin]; + ++std::get<0>(counts); // attempts + if (final_state == MoveStateType::ATTEMPT_COMMIT) { + ++std::get<2>(counts); // commits + } else { + ++std::get<1>(counts); // rejects + } } } } @@ -344,6 +348,29 @@ void MoveTracker::restoreJournal() finalizeMoves(level, MoveStateType::ATTEMPT_REJECT); } +bool MoveTracker::rejectPendingMove(const sta::Pin* pin, + const std::string& move_type) +{ + if (pending_move_levels_.empty()) { + return false; + } + + std::vector& pending = pending_move_levels_.top(); + const std::vector::reverse_iterator move = std::find_if( + pending.rbegin(), pending.rend(), [&](const MoveStateData& item) { + return item.pin == pin && item.move_type == move_type; + }); + if (move == pending.rend()) { + return false; + } + + // finalizeMoves() charges the move to the endpoint it was recorded for. + finalizeMoves({*move}, MoveStateType::ATTEMPT_REJECT); + pending.erase(std::next(move).base()); + + return true; +} + int MoveTracker::getVisitCount(const sta::Pin* pin) const { auto it = visit_count_.find(pin); @@ -1482,6 +1509,9 @@ void MoveTracker::printMissedOpportunitiesReport(const std::string& title) void MoveTracker::captureOriginalEndpointSlack() { + if (!report_enabled_) { + return; + } // Capture the original slack for all violating endpoints // This should be called once at the very beginning before any phases sta::VertexSet& endpoints = sta_->search()->endpoints(); @@ -1507,6 +1537,9 @@ void MoveTracker::captureOriginalEndpointSlack() void MoveTracker::capturePrePhaseSlack() { + if (!report_enabled_) { + return; + } // Capture slack at the start of a phase for all tracked endpoints // This becomes the PreSlk column for (auto& [endpoint_pin, slack_tuple] : endpoint_slack_) { @@ -1521,6 +1554,9 @@ void MoveTracker::capturePrePhaseSlack() void MoveTracker::captureInitialSlackDistribution() { + if (!report_enabled_) { + return; + } // Clear any previous data initial_pin_slack_.clear(); initial_endpoint_slack_.clear(); diff --git a/src/rsz/src/MoveTracker.hh b/src/rsz/src/MoveTracker.hh index e2a395e690e..3f5c86043a9 100644 --- a/src/rsz/src/MoveTracker.hh +++ b/src/rsz/src/MoveTracker.hh @@ -100,6 +100,9 @@ struct MoveStateData int order; std::string move_type; MoveStateType state; + // The endpoint the move was recorded for; finalizeMoves() charges its counts + // to it. + const sta::Pin* endpoint_pin{nullptr}; MoveStateData(const sta::Pin* p, int c, @@ -169,6 +172,8 @@ class MoveTracker : public odb::dbBlockCallBackObj void beginJournal(); void commitJournal(); void restoreJournal(); + // Reject only the latest matching entry in the current pending journal. + bool rejectPendingMove(const sta::Pin* pin, const std::string& move_type); // === Phase and final reports ============================================= // Print statistics summary for current pass and cumulative totals diff --git a/src/rsz/src/OptimizationPolicyConfig.cc b/src/rsz/src/OptimizationPolicyConfig.cc new file mode 100644 index 00000000000..bea7076b74d --- /dev/null +++ b/src/rsz/src/OptimizationPolicyConfig.cc @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#include "OptimizationPolicyConfig.hh" + +namespace rsz { + +OptimizationPolicyConfig OptimizationPolicyConfig::forParallelEval( + const RepairEffort effort) +{ + OptimizationPolicyConfig config; + + // A batch commits on estimates alone, so the estimates also charge the side + // effects: fanin loads, buffer-tree side loads and the opposite transition. + config.fanin_side_loss = true; + config.rebuffer_mt_side_loads = true; + config.dual_rf_eval = true; + + // The estimate is less accurate for big size-up jumps, so the jump is + // bounded. The rise/fall guard is off; it leaves buffer chains with no + // candidate. + config.size_up_mt_max_drive_ratio = 2.5f; + config.dual_rf_drive_guard = false; + + // RebufferMt is the only buffering move here: one-buffer trees join the + // pool and the upper score gate is off. + config.allow_rebuffer_mt_single_buffer = true; + config.rebuffer_mt_max_score = 0.0f; + config.rebuffer_mt_score_scale = 0.40f; + + ParallelEvalPolicyConfig& parallel_eval = config.parallel_eval; + // -effort trades runtime for the last gains after the batches converge. + switch (effort) { + case RepairEffort::kLow: + // No WNS tail: the batch-stage stall ends the run. + parallel_eval.wns_tail_endpoints = 0; + break; + case RepairEffort::kMedium: + // Both stall rules fire sooner and the tail widens less. + parallel_eval.stall_max_batches = 12; + parallel_eval.progress_pct = 1.0f; + parallel_eval.wns_tail_max_endpoints = 16; + parallel_eval.wns_tail_work_pct = 100.0f; + parallel_eval.wns_tail_min_work = 15.0f; + break; + case RepairEffort::kHigh: + break; + } + + return config; +} + +} // namespace rsz diff --git a/src/rsz/src/OptimizationPolicyConfig.hh b/src/rsz/src/OptimizationPolicyConfig.hh index d17ed25fc02..fd3ab2b7e0d 100644 --- a/src/rsz/src/OptimizationPolicyConfig.hh +++ b/src/rsz/src/OptimizationPolicyConfig.hh @@ -3,14 +3,180 @@ #pragma once +#include "rsz/Resizer.hh" + namespace rsz { -// Every tunable of the setup repair policies and their moves. The default -// member initializer is the shipped value. +// The tunables of the setup repair policies and their moves; estimator and +// planner calibration constants stay in their files. The default member +// initializer is the shipped value; forParallelEval() sets the PARALLEL_EVAL +// values. Times are seconds and "pct" fields are percent. + +// Controls how stalled batches favor targets in shared violating cones. +// The rank is score * (1 + alpha * w^gamma), where w is relative cone need. +struct BatchPriorityConfig +{ + // Strength of the cone-need bonus. + float alpha{2.0f}; + // Emphasis on the most shared bottlenecks. + float gamma{2.0f}; + // The weight applies only after a saved batch left WNS unchanged, and for at + // most this many such batches in a row. Undone batches do not count. + int weight_max_wns_stall_batches{8}; + // Maximum endpoints weighed, worst slack first; each costs one bit per + // cone vertex. + int bottleneck_max_endpoints{1024}; + // Maximum distinct cone sources weighed, with the same per-vertex cost. + int bottleneck_max_sources{1024}; + // Batches one analysis serves; the start of the WNS tail refreshes it. + int bottleneck_refresh_batches{4}; +}; + +// Sets the score floor and worst-endpoint reserve of batch admission. +struct BatchSelectorConfig +{ + // The score floor applies once the batch holds this many moves. + int score_floor_start{16}; + // A candidate joins only when its score is at least this fraction of the + // best score. + float min_score_ratio{0.0f}; + // Percent of the batch kept for the worst-path targets, which the score + // order alone would not reach. + int wns_reserve_pct{0}; + // Endpoints at or above this slack (the -setup_slack_margin of the run) + // take no reserved slot. + float slack_margin{0.0f}; +}; + +// Controls batch evaluation, checkpoints, and stopping in PARALLEL_EVAL. +struct ParallelEvalPolicyConfig +{ + // --- Batch size --------------------------------------------------------- + + // Smallest batch cap. + int min_batch_cap{64}; + + // The cap is the failing endpoint count, clamped to the two caps. + int max_batch_cap{512}; + + // Batch cap of the one-time retry of a run whose batches kept no move. + int retry_batch_cap{8}; + + // --- Evaluation funnel ---------------------------------------------------- + + // Endpoints evaluated per chunk, worst slack first: eval_endpoint_factor + // times the batch cap, at least eval_min_endpoints. The next chunk runs + // only while the batch is not full (see evaluateTargets()). + int eval_endpoint_factor{2}; + // Minimum endpoint count evaluated in each chunk. + int eval_min_endpoints{256}; + + // Targets per batch that may plan buffer trees, worst slack first. + int rebuffer_max_targets{128}; + + // --- Batch selection ------------------------------------------------------ + + // Ranking across targets in stalled batches. Legality and each target's + // winner always use the local score. + BatchPriorityConfig priority; + + // Batch admission. The small-batch retry drops the floor and the reserve. + BatchSelectorConfig selector{.min_score_ratio = 0.05f, .wns_reserve_pct = 10}; + + // --- Checkpoint, salvage and undo ----------------------------------------- + + // Consecutive batches that may miss the checkpoint before the run stops. + int max_checkpoint_misses{8}; + // A batch that improves TNS is also saved while its WNS stays within this + // percent of the best checkpoint WNS. + float checkpoint_wns_tolerance_pct{1.0f}; + // Batches that skip the instances of an undone batch. + int miss_tabu_batches{8}; + + // Undone batches an instance may be in before it is blocked for the run. + int max_undos_per_instance{3}; + + // --- Stall rules ---------------------------------------------------------- + + // The batches stop after stall_max_batches batches without progress, or + // once those batches spent stall_batches times the current cap in moves. + // Progress is a saved checkpoint that improves WNS by progress_wns_pct or + // TNS by progress_pct percent of its initial value. + int stall_batches{6}; + // Absolute cap on consecutive stalled batches. + int stall_max_batches{24}; + // Minimum TNS improvement relative to the initial magnitude. + float progress_pct{0.5f}; + // Minimum WNS improvement relative to the initial magnitude. + float progress_wns_pct{0.1f}; + + // The batches also stop when the last stall_window saved batches, after + // spending stall_window_min_work, moved neither WNS nor the failing + // endpoint count by stall_rel_pct percent and TNS did not progress. + int stall_window{6}; + // Relative WNS/FEP change needed to reset the window stall check. + float stall_rel_pct{1.0f}; + // Minimum reference work before the window stall check applies. + float stall_window_min_work{30.0f}; + + // --- WNS tail ------------------------------------------------------------- + + // After the batches stall, the run works on the worst wns_tail_endpoints + // endpoints with small batches. Zero turns the tail off. + int wns_tail_endpoints{8}; + // Tail batch cap. A tail batch starts at one move, doubles after each + // saved batch and drops back to one after a miss. + int wns_tail_batch{8}; + // A tail stage ends after wns_tail_stall batches without a better WNS; the + // tail ends after wns_tail_max_batches batches. + int wns_tail_stall{6}; + int wns_tail_max_batches{300}; + // A stalled tail grows its endpoints and batch cap by wns_tail_grow, up to + // wns_tail_max_endpoints endpoints. + int wns_tail_grow{2}; + int wns_tail_max_endpoints{64}; + // A tail batch may cost at most this multiple of its WNS gain in TNS. + float wns_tail_tns_ratio{10.0f}; + // A tail batch is WNS progress only when it raises the best WNS by this + // percent of it. + float wns_tail_min_gain_pct{0.5f}; + // Tail work budget: this percent of the batch stage's work, at least + // wns_tail_min_work. + float wns_tail_work_pct{400.0f}; + // Absolute floor on the tail work budget, in reference seconds. + float wns_tail_min_work{60.0f}; + // Missed tail batches that may stay in the open journal. A later save + // keeps them all; otherwise they all roll back. + int wns_tail_speculate{6}; + + // --- Work meter ----------------------------------------------------------- + + // The stall window and the tail budget count work, not wall-clock time, + // so a run makes the same decisions on every machine. Work is in + // reference seconds: + // work_sta_per_vertex * graph_vertices * timing_updates + // + work_per_pending_net * nets_with_invalid_parasitics + // + work_per_estimate * candidate_estimates + // + work_per_plan * buffer_tree_plan_requests + // + work_per_endpoint * endpoints_evaluated + // A timing update is each parasitics-and-timing update the policy asks + // for, and each batch undo or checkpoint restore. The pending nets are + // read before the update or the rollback, so an undo or restore after the + // batch STA adds none; a salvage that re-commits a batch rolls back without + // a count. + float work_sta_per_vertex{5.0e-7f}; + float work_per_pending_net{1.93e-3f}; + float work_per_estimate{1.35e-5f}; + float work_per_plan{1.14e-3f}; + float work_per_endpoint{5.65e-4f}; +}; -// Knobs shared by the policies and their moves. +// Knobs of the Mt moves and the policies that run them. forParallelEval() +// returns the PARALLEL_EVAL values on top of the defaults. struct OptimizationPolicyConfig { + static OptimizationPolicyConfig forParallelEval(RepairEffort effort); + // --- Delay estimator ------------------------------------------------------ // Stages scored before and after the target stage; zero scores only the @@ -21,7 +187,7 @@ struct OptimizationPolicyConfig bool sta_slew_bias{true}; // Charge the slower fanin driver of a larger input pin to the other loads - // of the fanin net. + // of the fanin net. PARALLEL_EVAL turns it on. bool fanin_side_loss{false}; float fanin_side_loss_weight{3.0f}; // Weight of fanin loads off every violating worst path. @@ -30,18 +196,20 @@ struct OptimizationPolicyConfig // candidate's own gain. float side_floor_margin{1.0f}; // Also score the opposite output transition of a resized cell. + // PARALLEL_EVAL turns it on. bool dual_rf_eval{false}; // --- VtSwapMt and SizeUpMt ------------------------------------------------ // Drop replacements that do not drive at least as strong on rise and fall. + // PARALLEL_EVAL turns the guard off. bool dual_rf_drive_guard{true}; // Weight of the SizeUpMt score in the shared pool; VtSwapMt keeps 1. float size_up_mt_score_scale{0.75f}; // Largest drive-resistance ratio (current / replacement) of a size-up; - // zero means no bound. + // zero means no bound. PARALLEL_EVAL sets one. float size_up_mt_max_drive_ratio{0.0f}; // --- RebufferMt ----------------------------------------------------------- @@ -50,15 +218,23 @@ struct OptimizationPolicyConfig float rebuffer_mt_score_scale{0.10f}; // Minimum and maximum raw score of a buffer tree, in seconds; a maximum of - // zero or less turns the upper gate off. + // zero or less turns the upper gate off, as PARALLEL_EVAL does. float rebuffer_mt_min_score{5.0e-12f}; float rebuffer_mt_max_score{45.0e-12f}; // Buffer trees generated per target. int rebuffer_mt_max_candidates{3}; - // Let one-buffer trees compete in the score pool. + // Let one-buffer trees compete in the score pool. PARALLEL_EVAL turns it on. bool allow_rebuffer_mt_single_buffer{false}; + + // Charge the buffer-tree side loads: the loads a new buffer moves behind it + // lose slack. PARALLEL_EVAL turns it on. + bool rebuffer_mt_side_loads{false}; + + // --- PARALLEL_EVAL -------------------------------------------------------- + + ParallelEvalPolicyConfig parallel_eval; }; } // namespace rsz diff --git a/src/rsz/src/Optimizer.cc b/src/rsz/src/Optimizer.cc index d74a609fab5..8e481603b40 100644 --- a/src/rsz/src/Optimizer.cc +++ b/src/rsz/src/Optimizer.cc @@ -7,24 +7,27 @@ #include #include #include -#include #include #include "GlobalSizingPolicy.hh" #include "OptimizationPolicy.hh" #include "OptimizerTypes.hh" +#include "ProgressReporter.hh" #include "RepairSetupContext.hh" #include "RepairSetupProfiler.hh" +#include "RepairTargetCollector.hh" #include "SetupCritVtSwapPolicy.hh" #include "SetupDirectionalPolicy.hh" #include "SetupLastGaspPolicy.hh" #include "SetupLegacyBase.hh" #include "SetupLegacyPolicy.hh" +#include "SetupParallelEvalPolicy.hh" #include "SetupReroutePolicy.hh" #include "SetupTnsPolicy.hh" #include "SetupWnsPolicy.hh" #include "est/EstimateParasitics.h" #include "rsz/Resizer.hh" +#include "sta/Fuzzy.hh" #include "sta/StringUtil.hh" #include "utl/Logger.h" #include "utl/scope.h" @@ -127,16 +130,24 @@ std::unique_ptr Optimizer::makePolicyForPhase( return std::make_unique( resizer_, committer_, setup_context, config_); } + + if (phase_name == "PARALLEL_EVAL") { + return std::make_unique( + resizer_, committer_, setup_context, config_); + } if (phase_name == "GLOBAL_SIZING") { return std::make_unique( resizer_, committer_, setup_context, config_); } + // Only the phases of the README are listed; the experimental PARALLEL_EVAL + // has a README section. resizer_.logger()->error( utl::RSZ, 217, "Unknown phase name '{}'. Valid phase names are: LEGACY, WNS, " "WNS_PATH, WNS_CONE, TNS, ENDPOINT_FANIN, STARTPOINT_FANOUT, " - "LAST_GASP, CRIT_VT_SWAP, REROUTE, GLOBAL_SIZING", + "LAST_GASP, CRIT_VT_SWAP, REROUTE, GLOBAL_SIZING, " + "PARALLEL_EVAL (experimental)", phase_name); return nullptr; } @@ -180,14 +191,17 @@ bool Optimizer::run() 223, "No phase names specified. Valid phase names are: LEGACY, WNS, " "WNS_PATH, WNS_CONE, TNS, ENDPOINT_FANIN, STARTPOINT_FANOUT, " - "LAST_GASP, CRIT_VT_SWAP, REROUTE, GLOBAL_SIZING"); + "LAST_GASP, CRIT_VT_SWAP, REROUTE, GLOBAL_SIZING, " + "PARALLEL_EVAL (experimental)"); } const int phase_count = phase_names.size(); // Phase loop - Run multiple policies sequentially. - std::unique_ptr last_policy; RepairSetupContext setup_context(resizer_); setup_context.phase_pipeline_active = true; + // The last policy picks the layout of the closing progress row when no + // header was printed. + bool show_startpoint_metrics = false; for (int i = 0; i < phase_count; ++i) { const std::string& phase_name = phase_names[i]; profile.phase(phase_name, i + 1); @@ -205,17 +219,124 @@ bool Optimizer::run() while (!policy->converged()) { policy->iterate(); } - last_policy = std::move(policy); + policy->finish(); + show_startpoint_metrics = policy->showStartpointMetrics(); } // Final report const ProfileScope report_scope("final_reporting"); - const bool result = last_policy->finalizeAndReport(); + const bool result = reportRunResult(setup_context, show_startpoint_metrics); profile.complete(result); return result; } +bool Optimizer::reportRunResult(RepairSetupContext& setup_context, + const bool show_startpoint_metrics) +{ + // The closing row and the RSZ-62 verdict share one final WNS. + RepairTargetCollector final_targets(&resizer_); + final_targets.init(config_.setup_slack_margin, + setup_context.progress_header_show_startpoint_metrics); + setup_context.final_wns = final_targets.getWns(); + const ProgressReporter progress(resizer_, committer_, setup_context); + progress.printFinal(final_targets, show_startpoint_metrics); + committer_.printTrackerFinalReports( + setup_context.target_collector.getViolatingPins()); + return reportRepairSummary(setup_context); +} + +bool Optimizer::reportRepairSummary( + const RepairSetupContext& setup_context) const +{ + const ProfileScope profile("reporting"); + utl::Logger* logger = resizer_.logger(); + bool repaired = false; + + const int buffer_moves = committer_.summaryCommittedMoves(MoveType::kBuffer); + const int size_up_moves = committer_.summaryCommittedMoves(MoveType::kSizeUp); + const int size_down_fanout_moves + = committer_.summaryCommittedMoves(MoveType::kSizeDownFanout); + const int swap_pins_moves + = committer_.summaryCommittedMoves(MoveType::kSwapPins); + const int clone_moves = committer_.summaryCommittedMoves(MoveType::kClone); + const int split_load_moves + = committer_.summaryCommittedMoves(MoveType::kSplitLoad); + const int unbuffer_moves + = committer_.summaryCommittedMoves(MoveType::kUnbuffer); + const int vt_swap_moves = committer_.summaryCommittedMoves(MoveType::kVtSwap); + const int size_up_match_moves + = committer_.summaryCommittedMoves(MoveType::kSizeUpMatch); + const int reroute_moves + = committer_.summaryCommittedMoves(MoveType::kReroute); + const int buffer_to_inverters_moves + = committer_.summaryCommittedMoves(MoveType::kBufferToInverters); + + if (unbuffer_moves > 0) { + repaired = true; + logger->info(utl::RSZ, 59, "Removed {} buffers.", unbuffer_moves); + } + if (buffer_moves > 0 || split_load_moves > 0) { + repaired = true; + if (split_load_moves == 0) { + logger->info(utl::RSZ, 40, "Inserted {} buffers.", buffer_moves); + } else { + logger->info(utl::RSZ, + 45, + "Inserted {} buffers, {} to split loads.", + buffer_moves + split_load_moves, + split_load_moves); + } + } + if (buffer_to_inverters_moves > 0) { + repaired = true; + logger->info(utl::RSZ, + 109, + "Replaced {} buffers with inverters.", + buffer_to_inverters_moves); + } + logger->metric("design__instance__count__setup_buffer", + buffer_moves + split_load_moves); + if (size_up_moves + size_down_fanout_moves + size_up_match_moves + + vt_swap_moves + > 0) { + repaired = true; + logger->info(utl::RSZ, + 51, + "Resized {} instances: {} up, {} up match, {} down, {} VT", + size_up_moves + size_up_match_moves + size_down_fanout_moves + + vt_swap_moves, + size_up_moves, + size_up_match_moves, + size_down_fanout_moves, + vt_swap_moves); + } + if (swap_pins_moves > 0) { + repaired = true; + logger->info( + utl::RSZ, 43, "Swapped pins on {} instances.", swap_pins_moves); + } + if (clone_moves > 0) { + repaired = true; + logger->info(utl::RSZ, 49, "Cloned {} instances.", clone_moves); + } + if (reroute_moves > 0) { + repaired = true; + logger->info( + utl::RSZ, 53, "Rerouted {} nets resistance-aware.", reroute_moves); + } + + if (sta::fuzzyLess(setup_context.final_wns, config_.setup_slack_margin)) { + repaired = true; + logger->warn(utl::RSZ, 62, "Unable to repair all setup violations."); + } + if (resizer_.overMaxArea()) { + logger->error(utl::RSZ, 25, "max utilization reached."); + } + + return repaired; +} + // Single-endpoint setup repair (test/debug) - bypasses the phase pipeline. bool Optimizer::repairSetup(const sta::Pin* const end_pin) { diff --git a/src/rsz/src/Optimizer.hh b/src/rsz/src/Optimizer.hh index 2ad834b95f6..9a727b61a91 100644 --- a/src/rsz/src/Optimizer.hh +++ b/src/rsz/src/Optimizer.hh @@ -25,10 +25,11 @@ struct RepairSetupContext; // setup-repair context, then dispatches each token through // makePolicyForPhase. Every policy sees the same target collector and // tracker-report context. +// 3. reportRunResult() prints the final report after the last phase. // // The Optimizer owns `committer_` for the full run; the shared setup context -// and each phase/top-level OptimizationPolicy live for the duration of the -// dispatch loop only. +// lives for one run() call and each OptimizationPolicy for its own phase +// only. class Optimizer { public: @@ -53,6 +54,16 @@ class Optimizer std::string_view phase_name, RepairSetupContext& setup_context); + // === Final report ========================================================= + // The report of the run, once after the last phase: the closing progress + // row, the tracker reports and the move summary. show_startpoint_metrics + // picks the row layout when no progress header was printed. + bool reportRunResult(RepairSetupContext& setup_context, + bool show_startpoint_metrics); + // The committed moves and the RSZ-62 verdict. Returns true when the run + // committed a move or left a setup violation. + bool reportRepairSummary(const RepairSetupContext& setup_context) const; + // === Run state ============================================================ OptimizerRunConfig config_; Resizer& resizer_; diff --git a/src/rsz/src/OptimizerTypes.cc b/src/rsz/src/OptimizerTypes.cc index 51cca0af163..0f6b266d835 100644 --- a/src/rsz/src/OptimizerTypes.cc +++ b/src/rsz/src/OptimizerTypes.cc @@ -285,6 +285,9 @@ Target makePathDriverTarget(const sta::Path* endpoint_path, Target target; target.views = kPathDriverView; target.endpoint_path = endpoint_path; + target.endpoint_pin = endpoint_path != nullptr + ? endpoint_path->pin(resizer.staState()) + : nullptr; target.driver_path = expanded.path(path_index); target.scene = endpoint_path != nullptr ? endpoint_path->scene(resizer.staState()) @@ -308,14 +311,19 @@ bool Target::isPrepared(const PrepareCacheMask mask) const if ((mask & ~kEstimatorContextCache) != 0) { return false; } - if ((mask & kEstimatorContextCache) == 0) { - return true; + + if ((mask & kEstimatorContextCache) != 0 && !estimator_context.has_value()) { + return false; } - return estimator_context.has_value(); + + return true; } const sta::Pin* Target::endpointPin(const Resizer& resizer) const { + if (endpoint_pin != nullptr) { + return endpoint_pin; + } return endpoint_path != nullptr ? endpoint_path->pin(resizer.staState()) : nullptr; } diff --git a/src/rsz/src/OptimizerTypes.hh b/src/rsz/src/OptimizerTypes.hh index 59d1c482d3a..40d8adec89f 100644 --- a/src/rsz/src/OptimizerTypes.hh +++ b/src/rsz/src/OptimizerTypes.hh @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "OptimizationPolicyConfig.hh" @@ -26,6 +27,7 @@ class Instance; class LibertyCell; class LibertyPort; class MinMax; +class Net; class Path; class PathExpanded; class Pin; @@ -39,9 +41,11 @@ class Vertex; } // namespace sta namespace rsz { +class BufferedNet; class MoveCandidate; class Resizer; class SequentialTimingEstimator; +using BufferedNetPtr = std::shared_ptr; } // namespace rsz namespace rsz { @@ -136,9 +140,11 @@ inline constexpr PrepareCacheMask kSequentialEstimatorCache = 1u << 8; // Generation inputs: one Mt generator's live STA reads, prepared on the main // thread so its generate() can run on a worker. Without them, the cell-swap -// generators read live STA. +// generators and RebufferMt read live STA. inline constexpr PrepareCacheMask kInputCapBudgetCache = 1u << 2; -inline constexpr PrepareCacheMask kGenerationInputCaches = kInputCapBudgetCache; +inline constexpr PrepareCacheMask kRebufferInputCache = 1u << 3; +inline constexpr PrepareCacheMask kGenerationInputCaches + = kInputCapBudgetCache | kRebufferInputCache; // === Prepared timing data =================================================== @@ -351,6 +357,18 @@ struct WireRC // can also expose the instance reached through driver_pin. using TargetViewMask = uint8_t; +// Current slack and driver-to-load wire delay of one load on a RebufferMt +// target net, with the weight the side-load model gives it. +struct RebufferLoadTiming +{ + float slack{0.0f}; + float wire_delay{0.0f}; + float weight{1.0f}; +}; + +using RebufferLoadTimingMap + = std::unordered_map; + inline constexpr TargetViewMask kPathDriverView = 1u << 0; inline constexpr TargetViewMask kInstanceView = 1u << 1; @@ -365,6 +383,9 @@ struct Target // - kInstanceView: target instance output pin sta::Pin* driver_pin{nullptr}; + // The current path-segment endpoint survives STA refresh; Path* cannot. + const sta::Pin* endpoint_pin{nullptr}; + // Endpoint-side timing path handle. Expanding this path exposes the full // startpoint-to-endpoint chain. const sta::Path* endpoint_path{nullptr}; @@ -390,6 +411,10 @@ struct Target // Fanin max-cap headroom of the target instance (the cell-swap // generators); the STA capacitance check behind it is main-thread only. std::optional> input_cap_budgets; + // RebufferMt: the driver net as a buffered-net tree and, with the + // side-load model, the current timing of its loads (empty without it). + BufferedNetPtr rebuffer_net; + RebufferLoadTimingMap rebuffer_load_timing; // === Field validation ==================================================== bool canBePathDriver() const; @@ -414,6 +439,27 @@ struct Target const sta::Path* prevDriverPath(const Resizer& resizer) const; }; +// What a candidate's ECO writes and what its timing reads, from prepared data. +// The batch selector rejects write conflicts and candidates whose timing +// depends on a batch member. +struct MoveImpact +{ + // False when an estimator could not enumerate all reads and writes. + bool complete{false}; + // Database objects mutated by applying the candidate. + std::vector write_instances; + std::vector write_nets; + // Connectivity changed by a structural move. + std::vector reconnect_pins; + std::vector reconnect_instances; + // Objects whose timing the candidate can alter after application. + std::vector timing_effect_instances; + std::vector timing_effect_nets; + // Objects whose timing the candidate estimate read before application. + std::vector timing_dependency_instances; + std::vector timing_dependency_nets; +}; + Target makePathDriverTarget(const sta::Path* endpoint_path, const sta::PathExpanded& expanded, int path_index, @@ -515,6 +561,8 @@ struct TargetEvaluation std::vector estimates; MoveCandidate* best_candidate{nullptr}; Estimate best_estimate; + // Worker-local profiling time, merged only after all estimates complete. + double worker_elapsed{0.0}; bool hasBestCandidate() const { return best_candidate != nullptr; } MoveCandidate& bestCandidate() const { return *best_candidate; } @@ -548,6 +596,8 @@ struct OptimizerRunConfig bool skip_last_gasp{false}; bool skip_vt_swap{false}; bool skip_crit_vt_swap{false}; + // Stop-rule preset of PARALLEL_EVAL; the other policies ignore it. + RepairEffort effort{RepairEffort::kHigh}; // Setup repair profiler: 0 off, 1 timing samples, 2 adds the batch and // pass events. Files go to profile_dir, or "repair_setup_profile" when // empty, with one run_N per call. diff --git a/src/rsz/src/Rebuffer.cc b/src/rsz/src/Rebuffer.cc index 9d609e778c5..176f9bec927 100644 --- a/src/rsz/src/Rebuffer.cc +++ b/src/rsz/src/Rebuffer.cc @@ -891,6 +891,9 @@ void Rebuffer::errorAreaRecoveryUnreproducible() void Rebuffer::warnPathTraceFailed(const sta::Pin* load_pin) { + if (countWorkerWarning(2006)) { + return; + } logger_->warn(RSZ, 2006, "failed to trace timing path for load {} when buffering {}", @@ -900,6 +903,9 @@ void Rebuffer::warnPathTraceFailed(const sta::Pin* load_pin) void Rebuffer::warnNoDesignRuleSolution() { + if (countWorkerWarning(2009)) { + return; + } logger_->warn(RSZ, 2009, "Skipping buffering because no valid buffering solution " @@ -910,6 +916,9 @@ void Rebuffer::warnNoDesignRuleSolution() void Rebuffer::warnNoViableBuffering(const sta::Pin* drvr_pin, const int rounds) { + if (countWorkerWarning(2021)) { + return; + } logger_->warn(RSZ, 2021, "cannot find a viable buffering solution on pin {} after {} " @@ -918,6 +927,16 @@ void Rebuffer::warnNoViableBuffering(const sta::Pin* drvr_pin, const int rounds) rounds); } +bool Rebuffer::countWorkerWarning(const int id) +{ + // Only a worker planner owns a delay calculator (see initForPlanning()). + if (owned_delay_calc_ == nullptr) { + return false; + } + ++worker_warning_counts_[id]; + return true; +} + bool Rebuffer::timingOrder(const RankedOption& lhs, const RankedOption& rhs) { if (lhs.unconstrained != rhs.unconstrained) { @@ -1819,14 +1838,16 @@ static float bufferCin(const sta::LibertyCell* cell) return a->capacitance(); } -void Rebuffer::init() +void Rebuffer::init(const bool find_max_wire_length) { logger_ = resizer_->logger_; dbStaState::init(resizer_->sta_); db_network_ = resizer_->db_network_; estimate_parasitics_ = resizer_->estimate_parasitics_; - resizer_max_wire_length_ - = resizer_->metersToDbu(resizer_->findMaxWireLength()); + if (find_max_wire_length) { + resizer_max_wire_length_ + = resizer_->metersToDbu(resizer_->findMaxWireLength()); + } sta_->checkCapacitancesPreamble(sta_->scenes()); sta_->checkSlewsPreamble(); sta_->checkFanoutPreamble(); @@ -1869,7 +1890,11 @@ void Rebuffer::initOnCorner(sta::Scene* corner) void Rebuffer::initForPlanning(sta::Scene* corner, const bool worker) { - init(); + // A worker planner skips the max wire length search, which takes nearly all + // of a planner's set-up and whose result no planner reads. The Resizer's + // planner, which RebufferMtGenerator::startPhase() initializes first, still + // runs it, so the buffers are found and the wire RC is checked once. + init(/*find_max_wire_length=*/!worker); owned_delay_calc_.reset(worker ? arc_delay_calc_->copy() : nullptr); initOnCorner(corner); } diff --git a/src/rsz/src/Rebuffer.hh b/src/rsz/src/Rebuffer.hh index 5ce5502abed..14afbecce1d 100644 --- a/src/rsz/src/Rebuffer.hh +++ b/src/rsz/src/Rebuffer.hh @@ -87,7 +87,10 @@ struct RebufferTreePlan float tree_input_cap{0.0f}; // Driver pin slack of the tree minus that of the current net. float driver_slack_gain{0.0f}; - // The tree the serial Rebuffer would build; it keeps a beam slot. + // The anchor tree: the lineage of top-ranked options through the beam + // rounds, kept in a reserved slot and exempt from the score gates. Its + // selector is the top-K one, so it is not the tree the serial Rebuffer would + // build. bool anchor_tree{false}; int buffer_count{0}; int load_count{0}; @@ -111,6 +114,12 @@ class Rebuffer : public sta::dbStaState // delay calculator, because OpenSTA's keeps per-call scratch state. void initForPlanning(sta::Scene* corner, bool worker = false); + // The warnings a worker planner counted instead of logging them, by + // message ID (see countWorkerWarning()). + const std::map& workerWarningCounts() const + { + return worker_warning_counts_; + } // The driver's net as a BufferedNet on the planner's corner, or nullptr // with a warning when the driver cannot be buffered. Main thread only: the // global-route reader behind it is not thread-safe. @@ -127,7 +136,9 @@ class Rebuffer : public sta::dbStaState const char* instance_base_name); protected: - void init(); + // find_max_wire_length is false for a worker planner (see + // initForPlanning()). + void init(bool find_max_wire_length = true); void initOnCorner(sta::Scene* corner); void annotateLoadSlacks(BufferedNetPtr& tree, sta::Vertex* root_vertex); @@ -191,7 +202,8 @@ class Rebuffer : public sta::dbStaState // === Messages ============================================================= // The messages the serial and the RebufferMt planning share, each with one - // ID. + // ID. The warnings of tree planning may come from a worker planner, which + // counts them instead. void warnTopPortDriver(const sta::Pin* drvr_pin); void warnBufferedNetFailed(const sta::Pin* drvr_pin); void warnPathTraceFailed(const sta::Pin* load_pin); @@ -199,6 +211,10 @@ class Rebuffer : public sta::dbStaState void warnNoViableBuffering(const sta::Pin* drvr_pin, int rounds); void errorAreaRecoveryFailed(); void errorAreaRecoveryUnreproducible(); + // True when a worker planner counted the warning `id` instead of logging + // it, which would make the log depend on the thread schedule. + bool countWorkerWarning(int id); + std::map worker_warning_counts_; // === Option ranking ======================================================= // A beam option with the measures the rankings compare. diff --git a/src/rsz/src/RepairSetupContext.hh b/src/rsz/src/RepairSetupContext.hh index 471538cc7a3..7918e6d0d61 100644 --- a/src/rsz/src/RepairSetupContext.hh +++ b/src/rsz/src/RepairSetupContext.hh @@ -39,10 +39,10 @@ struct RepairSetupContext // every phase start, since a phase can edit outside the committer. double progress_design_area{0.0}; int progress_area_at_edit{-1}; - // WNS of what the run repaired, captured by finalizeAndReport before the - // end-of-run reports read it, so the progress table and the RSZ-62 verdict - // cannot disagree. Under `repair_timing -path_group` this is the group's - // WNS, which costs a path query per violating endpoint to compute. + // WNS of what the run repaired, captured by Optimizer::reportRunResult() + // before the end-of-run reports read it, so the progress table and the + // RSZ-62 verdict cannot disagree. Under `repair_timing -path_group` this is + // the group's WNS, which takes a path query per violating endpoint. sta::Slack final_wns{0.0}; // Legacy-derived setup phases share one preamble per repair_setup run. diff --git a/src/rsz/src/RepairSetupProfiler.cc b/src/rsz/src/RepairSetupProfiler.cc index 755e9e21497..90f91c01344 100644 --- a/src/rsz/src/RepairSetupProfiler.cc +++ b/src/rsz/src/RepairSetupProfiler.cc @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -47,8 +48,8 @@ std::string number(const double value) } // The phases the profiler covers completely; others are marked partial. -constexpr std::array kCoveredPhases - = {"LEGACY", "LAST_GASP", "CRIT_VT_SWAP"}; +constexpr std::array kCoveredPhases + = {"LEGACY", "LAST_GASP", "CRIT_VT_SWAP", "PARALLEL_EVAL"}; } // namespace @@ -208,6 +209,9 @@ void RepairSetupProfiler::event(const std::string_view name, if (enabled()) { current_->add({"count", std::string(metric), std::string(options.move)}, static_cast(*options.count)); + } else if (deferred_counts_ != nullptr) { + deferred_counts_->push_back( + {std::string(metric), std::string(options.move), *options.count}); } } @@ -238,6 +242,13 @@ void RepairSetupProfiler::event(const std::string_view name, event(name, {.count = std::nullopt, .detail = detail, .trace = true}); } +void RepairSetupProfiler::replay(const DeferredCounts& counts) +{ + for (const DeferredCount& deferred : counts) { + event(deferred.name, deferred.value, deferred.move); + } +} + void RepairSetupProfiler::enter(const char* stage) { stages_.push_back({stage, Clock::now(), 0.0}); @@ -258,6 +269,16 @@ void RepairSetupProfiler::leave() event(stage.name + "_calls"); } +void RepairSetupProfiler::workerTime(const std::string_view task, + const double seconds) +{ + if (enabled()) { + event(std::string(task) + "_tasks"); + current_->add({"worker_time", "candidate_" + std::string(task), ""}, + seconds); + } +} + void RepairSetupProfiler::accountRootStageInterval() { if (stages_.size() != 1) { @@ -391,6 +412,10 @@ void RepairSetupProfiler::emit(const std::string& name, void RepairSetupProfiler::stop(const std::string_view reason) { + if (!enabled()) { + return; + } + const std::string metric = "stop_" + std::string(reason); event("stop", {.metric = metric, .detail = reason, .trace = true}); } @@ -553,6 +578,55 @@ void RepairSetupProfiler::endApply(MoveCandidate& candidate, profiler.active_type_.clear(); } +void RepairSetupProfiler::beginRevert(MoveCandidate& candidate) +{ + if (!enabled()) { + return; + } + + RepairSetupProfiler& profiler = *current_; + assert(profiler.active_move_ == 0); + const Candidate& info = profiler.candidates_.at(&candidate); + assert(info.move_id != 0); + + // The inverse swap is recorded under the original move's identity. + profiler.active_move_ = info.move_id; + profiler.active_candidate_ = info.id; + profiler.active_type_ = moveName(candidate.type()); +} + +void RepairSetupProfiler::endRevert(const char* detail) +{ + if (!enabled()) { + return; + } + + RepairSetupProfiler& profiler = *current_; + assert(profiler.active_move_ != 0 && !profiler.journals_.empty()); + std::vector>& moves + = profiler.journals_.back().moves; + const std::vector>::iterator move + = std::find_if(moves.begin(), + moves.end(), + [&](const std::pair& item) { + return item.first == profiler.active_move_; + }); + // Remove the original identity before its parent journal can settle it. + if (move != moves.end()) { + moves.erase(move); + } + + event("move_reverted", + {.metric = "reverted", + .move = profiler.active_type_, + .detail = detail, + .trace = true}); + + profiler.active_move_ = 0; + profiler.active_candidate_ = 0; + profiler.active_type_.clear(); +} + void RepairSetupProfiler::edit(const char* name, const std::string& object, const std::string& before, diff --git a/src/rsz/src/RepairSetupProfiler.hh b/src/rsz/src/RepairSetupProfiler.hh index 41d2404a520..e379a275ef4 100644 --- a/src/rsz/src/RepairSetupProfiler.hh +++ b/src/rsz/src/RepairSetupProfiler.hh @@ -29,6 +29,18 @@ class RepairSetupProfiler : public odb::dbBlockCallBackObj public: // === Types =============================================================== + // A counted event made on a worker thread, where no profiler is current. A + // worker binds a sink with deferCounts() (see DeferredCountScope) and the + // main thread replays the sink after the join. + struct DeferredCount + { + std::string name; + std::string move; + uint64_t value{0}; + }; + + using DeferredCounts = std::vector; + // One logical record can update a counter, emit a trace row, or both. struct EventOptions { @@ -79,15 +91,15 @@ class RepairSetupProfiler : public odb::dbBlockCallBackObj std::string_view move = ""); // Record a trace-only event with a detail string. static void event(std::string_view name, std::string_view detail); - // Assign an identity and count a generated move candidate. + // Bind a deferred-count sink to the current worker thread. + static void deferCounts(DeferredCounts* sink) { deferred_counts_ = sink; } + // Merge a worker's deferred counts on the main thread. + static void replay(const DeferredCounts& counts); static void moveGenerated(MoveCandidate& candidate); - // Record a candidate estimate and its rejection reason. static void moveEstimated(MoveCandidate& candidate, const Estimate& estimate); - // Record selection using the candidate's original target. static void moveSelected(MoveCandidate& candidate); - - // === Timing, events and journals ======================================== - + // Worker-local time of one "estimation" or "generation" task. + static void workerTime(std::string_view task, double seconds); // Record WNS and TNS at a named observation point. static void qor(const char* point, double wns, double tns); // A stop reason: the stop_ counter and the "stop" event. @@ -101,6 +113,16 @@ class RepairSetupProfiler : public odb::dbBlockCallBackObj // Record the apply result and the post-move analysis snapshot. static void endApply(MoveCandidate& candidate, const MoveResult& result); + // An in-place salvage swaps an applied candidate back inside the batch + // journal. The edits between begin and end keep the move's identity, and + // end rejects the move the way an undone journal would. + static void beginRevert(MoveCandidate& candidate); + // Settle a selectively reverted move under its original identity. + static void endRevert(const char* detail); + + // === Phase and iteration lifecycle ====================================== + + // Flush the previous phase and begin a new one. void phase(const std::string& name, int index); // Write one run metadata field. void metadata(const std::string& key, const std::string& value); @@ -191,6 +213,10 @@ class RepairSetupProfiler : public odb::dbBlockCallBackObj inline static thread_local RepairSetupProfiler* current_{nullptr}; inline static thread_local uint64_t journal_depth_{0}; + inline static thread_local DeferredCounts* deferred_counts_{nullptr}; + + // === Run and output state ================================================ + Resizer& resizer_; Mode mode_{Mode::kOff}; Clock::time_point start_; @@ -242,6 +268,20 @@ class ProfileScope RepairSetupProfiler* profiler_; }; +// Binds the deferred-count sink of the current worker thread for a scope. +class DeferredCountScope +{ + public: + explicit DeferredCountScope(RepairSetupProfiler::DeferredCounts* sink) + { + RepairSetupProfiler::deferCounts(sink); + } + ~DeferredCountScope() { RepairSetupProfiler::deferCounts(nullptr); } + DeferredCountScope(const DeferredCountScope&) = delete; + DeferredCountScope& operator=(const DeferredCountScope&) = delete; +}; + +// Marks an iteration boundary and flushes its counters when the scope ends. class ProfileIteration { public: diff --git a/src/rsz/src/RepairTargetCollector.cc b/src/rsz/src/RepairTargetCollector.cc index 28f6d79b93d..09669f9d1a1 100644 --- a/src/rsz/src/RepairTargetCollector.cc +++ b/src/rsz/src/RepairTargetCollector.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,7 @@ #include "sta/TimingArc.hh" #include "sta/Transition.hh" #include "utl/Logger.h" +#include "utl/ThreadPool.h" namespace rsz { @@ -64,6 +66,9 @@ namespace { using PinSlack = std::pair; using VertexSlack = std::pair; +// Endpoint scan blocks per worker; more blocks smooth uneven path counts. +constexpr size_t kScanBlocksPerWorker = 8; + bool slackLess(const PinSlack& lhs, const PinSlack& rhs) { return lhs.second < rhs.second; @@ -833,6 +838,51 @@ void RepairTargetCollector::sortByHeuristic(float load_delay_threshold) } } +RepairTargetCollector::RepairTargetCollector(Resizer* resizer) + : resizer_(resizer) +{ + logger_ = resizer_->logger_; + sta_ = resizer_->sta_; + network_ = resizer_->network_; + max_ = resizer_->max_; + dcalc_ap_ = -1; + lib_ap_ = -1; + max_passes_per_endpoint_ = 1000; // Default value + cached_cone_threshold_ = 0.0; + needs_threshold_recompute_ = true; // Force compute on first call +} + +RepairTargetCollector::~RepairTargetCollector() = default; + +void RepairTargetCollector::setScanThreadPool(utl::ThreadPool* pool, + const size_t min_endpoints) +{ + scan_thread_pool_ + = pool != nullptr && pool->threadCount() > 0 ? pool : nullptr; + min_parallel_scan_endpoints_ = min_endpoints; +} + +sta::Slack RepairTargetCollector::endpointPathSlack(sta::Vertex* endpoint) const +{ + // Same reduction as Sta::slack(vertex, max_) over every scene, but without + // the per-call required-time search entry, so workers only read the graph. + sta::Slack slack = sta::MinMax::min()->initValue(); + sta::VertexPathIterator path_iter(endpoint, sta_); + while (path_iter.hasNext()) { + const sta::Path* path = path_iter.next(); + if (path->minMax(sta_) != max_) { + continue; + } + + const sta::Slack path_slack = path->slack(sta_); + if (sta::delayLess(path_slack, slack, sta_)) { + slack = path_slack; + } + } + + return slack; +} + void RepairTargetCollector::refreshGroupSlacks() { group_slacks_.clear(); @@ -863,13 +913,6 @@ void RepairTargetCollector::collectViolatingEndpoints() violating_endpoints_.clear(); crit_path_load_pins_.clear(); - // RebufferMt plans read required times without a refresh, so make them - // all current here. This is a no-op when timing is up to date. - { - const ProfileScope sta_profile("incremental_sta"); - sta_->findRequireds(); - } - if (restrictedToPathGroup()) { // One query for the whole group, already carrying each endpoint's slack // in it. Testing endpoints one at a time would repeat the same search @@ -877,10 +920,47 @@ void RepairTargetCollector::collectViolatingEndpoints() refreshGroupSlacks(); violating_endpoints_.assign(group_slacks_.begin(), group_slacks_.end()); } else { - for (sta::Vertex* endpoint : sta_->endpoints()) { - const sta::Slack slack = sta_->slack(endpoint, max_); - if (sta::fuzzyLess(slack, slack_margin_)) { - violating_endpoints_.emplace_back(endpoint->pin(), slack); + // Required times must be current before workers read path slacks. This + // is a no-op when timing is up to date. + { + const ProfileScope sta_profile("incremental_sta"); + sta_->findRequireds(); + } + + const sta::VertexSet& endpoint_set = sta_->endpoints(); + std::vector endpoints(endpoint_set.begin(), + endpoint_set.end()); + std::vector slacks(endpoints.size(), + sta::MinMax::min()->initValue()); + + utl::ThreadPool* pool = endpoints.size() < min_parallel_scan_endpoints_ + ? nullptr + : scan_thread_pool_; + if (pool == nullptr) { + for (size_t index = 0; index < endpoints.size(); ++index) { + slacks[index] = endpointPathSlack(endpoints[index]); + } + } else { + // Contiguous blocks keep task count small while balancing path counts. + const size_t block_count = std::min( + endpoints.size(), pool->threadCount() * kScanBlocksPerWorker); + const size_t block_size + = (endpoints.size() + block_count - 1) / block_count; + std::vector blocks(block_count); + std::iota(blocks.begin(), blocks.end(), size_t{0}); + pool->parallelFor(blocks, [&](const size_t& block) { + const size_t begin = block * block_size; + const size_t end = std::min(begin + block_size, endpoints.size()); + for (size_t index = begin; index < end; ++index) { + slacks[index] = endpointPathSlack(endpoints[index]); + } + }); + } + + for (size_t index = 0; index < endpoints.size(); ++index) { + if (sta::fuzzyLess(slacks[index], slack_margin_)) { + violating_endpoints_.emplace_back(endpoints[index]->pin(), + slacks[index]); } } } @@ -1169,7 +1249,10 @@ std::vector RepairTargetCollector::collectPathDriverTargets( } std::vector RepairTargetCollector::collectCritPathDriverPinTargets( - const std::function& is_valid_target) const + const std::function& is_valid_target, + const size_t begin_endpoint, + const size_t end_endpoint, + std::unordered_set& visited_pins) const { const ProfileScope profile("target_collection"); std::vector violated_endpoints; @@ -1187,8 +1270,9 @@ std::vector RepairTargetCollector::collectCritPathDriverPinTargets( std::ranges::stable_sort(violated_endpoints, pathEndpointLess); std::vector targets; - std::unordered_set visited_pins; - for (const VertexSlack& endpoint_data : violated_endpoints) { + const size_t end = std::min(end_endpoint, violated_endpoints.size()); + for (size_t index = begin_endpoint; index < end; ++index) { + const VertexSlack& endpoint_data = violated_endpoints[index]; sta::Vertex* endpoint = endpoint_data.first; const sta::Slack endpoint_slack = endpoint_data.second; sta::Path* path = findWorstSlackPath(endpoint); diff --git a/src/rsz/src/RepairTargetCollector.hh b/src/rsz/src/RepairTargetCollector.hh index dc26de1b262..32bffa811e8 100644 --- a/src/rsz/src/RepairTargetCollector.hh +++ b/src/rsz/src/RepairTargetCollector.hh @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,10 @@ namespace sta { class PathExpanded; } +namespace utl { +class ThreadPool; +} + namespace rsz { using std::pair; @@ -67,20 +72,18 @@ class RepairTargetCollector public: // === Construction and setup ============================================== // Constructor - RepairTargetCollector(Resizer* resizer) : resizer_(resizer) - { - logger_ = resizer_->logger_; - sta_ = resizer_->sta_; - network_ = resizer_->network_; - max_ = resizer_->max_; - dcalc_ap_ = -1; - lib_ap_ = -1; - max_passes_per_endpoint_ = 1000; // Default value - cached_cone_threshold_ = 0.0; - needs_threshold_recompute_ = true; // Force compute on first call - } + explicit RepairTargetCollector(Resizer* resizer); + ~RepairTargetCollector(); void init(float slack_margin, bool collect_startpoints = false); + // Endpoint scans of at least `min_endpoints` endpoints run on `pool`, which + // the caller keeps alive while set; null, or a pool without worker threads, + // keeps every scan on the caller thread. + void setScanThreadPool(utl::ThreadPool* pool, + size_t min_endpoints = kMinParallelScanEndpoints); + // Endpoint count below which a scan stays on the caller thread, so small + // designs avoid the thread hand-off. + static constexpr size_t kMinParallelScanEndpoints = 4096; void printHistogram(int numBins = 20) const; void printViolators(int numPrint) const; @@ -212,8 +215,14 @@ class RepairTargetCollector std::vector collectPathDriverTargets(sta::Path* path, sta::Slack path_slack) const; + // The path-driver targets of the violating endpoints [begin, end) in + // worst-slack-first order. visited_pins dedups drivers across successive + // chunks of one scan; the caller owns it. std::vector collectCritPathDriverPinTargets( - const std::function& is_valid_target) const; + const std::function& is_valid_target, + size_t begin_endpoint, + size_t end_endpoint, + std::unordered_set& visited_pins) const; // Startpoint iteration methods void setToStartpoint(int index); @@ -368,11 +377,18 @@ class RepairTargetCollector pins_with_slack, sta::Slack threshold); + // Read-only setup slack over the paths stored at one endpoint vertex. Safe + // to call from worker threads once required times are current. + sta::Slack endpointPathSlack(sta::Vertex* endpoint) const; + // === Shared services ====================================================== Resizer* resizer_; utl::Logger* logger_; sta::Sta* sta_; sta::Graph* graph_; + // Workers for large read-only endpoint scans, owned by the policy. + utl::ThreadPool* scan_thread_pool_{nullptr}; + size_t min_parallel_scan_endpoints_{kMinParallelScanEndpoints}; // Startpoint set for this collector; see walkStartpoints(). std::vector startpoints_; diff --git a/src/rsz/src/Resizer-py.i b/src/rsz/src/Resizer-py.i index f0409365b1d..5df9e657c19 100644 --- a/src/rsz/src/Resizer-py.i +++ b/src/rsz/src/Resizer-py.i @@ -37,7 +37,7 @@ using namespace rsz; const std::vector&, const char*, const char*, bool, bool, bool, bool, bool, bool, bool, bool, bool, - int, const char*); + rsz::RepairEffort, int, const char*); %ignore rsz::Resizer::computeNewDelaysSlews; %ignore rsz::Resizer::estimateSlewsAfterBufferRemoval; @@ -94,6 +94,7 @@ using namespace rsz; bool skip_last_gasp, bool skip_vt_swap, bool skip_crit_vt_swap, + const char* effort = "high", int profile_mode = 0, const char* profile_dir = "") { @@ -107,6 +108,8 @@ using namespace rsz; skip_pin_swap, skip_gate_cloning, skip_size_down_fanout, skip_buffering, skip_buffer_removal, skip_buffer_to_inverters, skip_last_gasp, skip_vt_swap, skip_crit_vt_swap, + rsz::Resizer::repairEffortFromString( + effort ? effort : "high"), profile_mode, profile_dir ? profile_dir : ""); } } diff --git a/src/rsz/src/Resizer.cc b/src/rsz/src/Resizer.cc index e4cb3346e5f..a0b0ff3692d 100644 --- a/src/rsz/src/Resizer.cc +++ b/src/rsz/src/Resizer.cc @@ -5455,6 +5455,7 @@ bool Resizer::repairSetup(double setup_margin, bool skip_last_gasp, bool skip_vt_swap, bool skip_crit_vt_swap, + RepairEffort effort, int profile_mode, const char* profile_dir) { @@ -5479,6 +5480,7 @@ bool Resizer::repairSetup(double setup_margin, config.skip_last_gasp = skip_last_gasp; config.skip_vt_swap = skip_vt_swap; config.skip_crit_vt_swap = skip_crit_vt_swap; + config.effort = effort; config.profile_mode = profile_mode; config.profile_dir = profile_dir != nullptr ? profile_dir : ""; @@ -5663,6 +5665,16 @@ void Resizer::initForJournalRestore() init(); } +void Resizer::recountDesignAreaAfterUndo() +{ + // init() counts the area before the undo, so overMaxArea() would still see + // the undone edits. Only overMaxArea() reads the tracked area, so the + // walk runs only under a -max_utilization limit. + if (max_area_) { + initDesignArea(); + } +} + void Resizer::updateParasiticsAndTiming() { if (estimate_parasitics_->isIncrementalParasiticsEnabled()) { @@ -6717,6 +6729,19 @@ std::vector Resizer::parseMoveSequence(const std::string& sequence) return result; } +RepairEffort Resizer::repairEffortFromString(const std::string& effort) +{ + if (effort == "low") { + return RepairEffort::kLow; + } + + if (effort == "medium") { + return RepairEffort::kMedium; + } + + return RepairEffort::kHigh; +} + bool Resizer::okToBufferNet(const sta::Pin* driver_pin) const { if (isTristateDriver(driver_pin)) { diff --git a/src/rsz/src/Resizer.i b/src/rsz/src/Resizer.i index 221460c799e..08822380c6f 100644 --- a/src/rsz/src/Resizer.i +++ b/src/rsz/src/Resizer.i @@ -376,6 +376,7 @@ repair_setup(double setup_margin, bool skip_last_gasp, bool skip_vt_swap, bool skip_crit_vt_swap, + const char* effort, int profile_mode, const char* profile_dir) { @@ -389,6 +390,7 @@ repair_setup(double setup_margin, skip_size_down_fanout, skip_buffering, skip_buffer_removal, skip_buffer_to_inverters, skip_last_gasp, skip_vt_swap, skip_crit_vt_swap, + Resizer::repairEffortFromString(effort), profile_mode, profile_dir); } diff --git a/src/rsz/src/Resizer.tcl b/src/rsz/src/Resizer.tcl index 6e4c3c70545..c5a2d319211 100644 --- a/src/rsz/src/Resizer.tcl +++ b/src/rsz/src/Resizer.tcl @@ -255,6 +255,7 @@ sta::define_cmd_args "repair_timing" {[-setup] [-hold]\ [-skip_vt_swap]\ [-skip_crit_vt_swap]\ [-skip_crpr]\ + [-effort low|medium|high]\ [-profile_mode mode]\ [-profile_dir dir]\ [-repair_tns tns_end_percent]\ @@ -273,7 +274,7 @@ proc repair_timing { args } { sta::parse_key_args "repair_timing" args \ keys {-setup_margin -hold_margin -slack_margin \ -libraries -max_utilization -max_buffer_percent -sequence \ - -phases -policy -policies -path_group -profile_mode -profile_dir \ + -phases -policy -policies -path_group -effort -profile_mode -profile_dir \ -recover_power -repair_tns -max_passes -max_iterations -max_repairs_per_pass} \ flags {-setup -hold -allow_setup_violations -skip_pin_swap -skip_gate_cloning \ -skip_size_down -skip_buffering -skip_buffer_removal -skip_buffer_to_inverters \ @@ -341,6 +342,16 @@ proc repair_timing { args } { set skip_vt_swap [info exists flags(-skip_vt_swap)] set skip_crit_vt_swap [info exists flags(-skip_crit_vt_swap)] set skip_crpr [info exists flags(-skip_crpr)] + set effort "high" + if { [info exists keys(-effort)] } { + set effort $keys(-effort) + if { [lsearch -exact {low medium high} $effort] == -1 } { + utl::error RSZ 3242 "-effort must be low, medium or high." + } + if { [lsearch -exact [string toupper $phases] "PARALLEL_EVAL"] == -1 } { + utl::warn RSZ 3244 "-effort has no effect unless -phases lists PARALLEL_EVAL." + } + } # Setup repair profiler level and output directory; 0 writes nothing. set profile_mode 0 if { [info exists keys(-profile_mode)] } { @@ -352,6 +363,9 @@ proc repair_timing { args } { set profile_dir "" if { [info exists keys(-profile_dir)] } { set profile_dir $keys(-profile_dir) + if { $profile_mode == 0 } { + utl::warn RSZ 3245 "-profile_dir has no effect without -profile_mode." + } } rsz::set_max_utilization [rsz::parse_max_util keys] set max_buffer_percent 20 @@ -399,6 +413,14 @@ proc repair_timing { args } { if { [info exists keys(-max_repairs_per_pass)] } { set max_repairs_per_pass $keys(-max_repairs_per_pass) } + # The pass options drive the per-endpoint policies only. + if { [info exists keys(-max_passes)] || [info exists keys(-max_repairs_per_pass)] } { + set pass_phases [lsearch -all -inline -not -exact [string toupper $phases] "PARALLEL_EVAL"] + if { $phases ne "" && [llength $pass_phases] == 0 } { + utl::warn RSZ 3246 \ + "-max_passes and -max_repairs_per_pass have no effect on the PARALLEL_EVAL policy." + } + } sta::check_argc_eq0 "repair_timing" $args est::check_parasitics @@ -425,7 +447,7 @@ proc repair_timing { args } { $sequence $phases $path_group \ $skip_pin_swap $skip_gate_cloning $skip_size_down_fanout $skip_buffering \ $skip_buffer_removal $skip_buffer_to_inverters $skip_last_gasp $skip_vt_swap \ - $skip_crit_vt_swap $profile_mode $profile_dir] + $skip_crit_vt_swap $effort $profile_mode $profile_dir] } if { $hold } { if { $skip_crpr } { diff --git a/src/rsz/src/move/MoveCandidate.cc b/src/rsz/src/move/MoveCandidate.cc index 96cf876e97c..f73e8eb9939 100644 --- a/src/rsz/src/move/MoveCandidate.cc +++ b/src/rsz/src/move/MoveCandidate.cc @@ -3,9 +3,13 @@ #include "MoveCandidate.hh" +#include + #include "OptimizerTypes.hh" #include "SequentialTimingEstimator.hh" #include "rsz/Resizer.hh" +#include "sta/Network.hh" +#include "sta/NetworkClass.hh" namespace rsz { @@ -23,6 +27,62 @@ MoveResult MoveCandidate::rejectedMove() const }; } +std::vector MoveCandidate::faninSidePins( + const EstimatorContext& estimator_context) +{ + std::vector pins; + for (const FaninSideNet& side_net : estimator_context.fanin_side_nets) { + for (const SideLoad& load : side_net.loads) { + if (load.pin != nullptr) { + pins.push_back(load.pin); + } + } + } + + return pins; +} + +MoveImpact MoveCandidate::impact() const +{ + MoveImpact impact; + if (!target_.estimator_context.has_value()) { + return impact; + } + + sta::Network* network = resizer_.network(); + for (const DelayStageState& stage : target_.estimator_context->path_stages) { + sta::Pin* driver_pin = stage.driver_pin; + if (driver_pin == nullptr) { + continue; + } + + appendUnique(impact.timing_dependency_instances, + network->instance(driver_pin)); + appendUnique(impact.timing_dependency_nets, network->net(driver_pin)); + } + + return impact; +} + +MoveImpact MoveCandidate::cellSwapImpact(sta::Instance* inst) const +{ + MoveImpact impact = MoveCandidate::impact(); + impact.complete = true; + appendUnique(impact.write_instances, inst); + appendUnique(impact.timing_effect_instances, inst); + + // A new cell changes its input capacitances and output timing, so every + // net on its pins is in the effect cone. + sta::Network* network = resizer_.network(); + std::unique_ptr pin_iter( + network->pinIterator(inst)); + while (pin_iter->hasNext()) { + appendUnique(impact.timing_effect_nets, network->net(pin_iter->next())); + } + + return impact; +} + void MoveCandidate::chargeCellSwapSideCosts(const DelayEstimate& delay_est, const sta::LibertyCell* replacement, const float score_scale, diff --git a/src/rsz/src/move/MoveCandidate.hh b/src/rsz/src/move/MoveCandidate.hh index bb5c3b4f84b..1311542722f 100644 --- a/src/rsz/src/move/MoveCandidate.hh +++ b/src/rsz/src/move/MoveCandidate.hh @@ -3,11 +3,24 @@ #pragma once +#include +#include +#include + #include "OptimizerTypes.hh" #include "rsz/Resizer.hh" namespace rsz { +// Appends a non-null value that the list does not hold yet. +template +void appendUnique(std::vector& values, const std::type_identity_t value) +{ + if (value != nullptr && std::ranges::find(values, value) == values.end()) { + values.push_back(value); + } +} + // === Move candidate interface ============================================== // Base class for one concrete ECO proposal (swap this cell to LVT, insert a @@ -35,10 +48,20 @@ class MoveCandidate virtual Estimate estimate() { return {.legal = true, .score = 1.0f}; } virtual MoveResult apply() = 0; virtual MoveType type() const = 0; + // What applying the move writes and what its estimate read. The base + // impact holds the target path's stages the estimate read. + virtual MoveImpact impact() const; + // Side pins whose slack the estimate charged; PARALLEL_EVAL checks them + // after the batch STA to find moves whose side effects were underestimated. + virtual std::vector guardedSidePins() const { return {}; } + static std::vector faninSidePins( + const EstimatorContext& estimator_context); const Target& target() const { return target_; } protected: MoveCandidate(Resizer& resizer, const Target& target); + // The base impact plus a new cell on inst: inst and every net on its pins. + MoveImpact cellSwapImpact(sta::Instance* inst) const; MoveResult rejectedMove() const; // The side costs of swapping the target cell for `replacement`, charged // against the own-path gain of delay_est: the flip-flop input-side checks, diff --git a/src/rsz/src/move/MoveGenerator.hh b/src/rsz/src/move/MoveGenerator.hh index 2ce2414081e..23c438d0854 100644 --- a/src/rsz/src/move/MoveGenerator.hh +++ b/src/rsz/src/move/MoveGenerator.hh @@ -3,8 +3,10 @@ #pragma once +#include #include #include +#include #include #include "MoveCandidate.hh" @@ -40,6 +42,9 @@ struct GeneratorContext MoveCommitter& committer; // For move conflict check in pending moves. const OptimizerRunConfig& run_config; const OptimizationPolicyConfig& policy_config; + // Load pins on the worst paths of violating endpoints, or null. Side-load + // models weight only these pins fully. + const std::unordered_set* critical_load_pins{nullptr}; }; // === Move generator interface ============================================== @@ -58,6 +63,9 @@ struct GeneratorContext // 3. generate() -- expand one Target into zero or more concrete // MoveCandidate objects. Candidates are owned by unique_ptr and // returned up to the policy which decides which to estimate/commit. +// 5. startPhase()/finishPhase() -- set up and release the generator's +// state for one phase, such as a planner per worker thread. The default +// keeps none. // // Thread-safety: generate()/isApplicable() run on worker threads for MT // policies. Implementations should read prepared Target fields and avoid STA @@ -93,6 +101,14 @@ class MoveGenerator virtual std::vector> generate( const Target& target) = 0; + // === Phase lifecycle ===================================================== + // Main thread, from the policy's start(): until finishPhase(), up to + // `worker_count` worker threads call generate() at once; with zero, the + // main thread calls it. + virtual void startPhase(size_t worker_count) {} + // Main thread, from the policy's finish(), after the last generate(). + virtual void finishPhase() {} + protected: // === Target-view requirements ============================================ diff --git a/src/rsz/src/move/RebufferMtCandidate.cc b/src/rsz/src/move/RebufferMtCandidate.cc index 11f8be947aa..a84e32cfde8 100644 --- a/src/rsz/src/move/RebufferMtCandidate.cc +++ b/src/rsz/src/move/RebufferMtCandidate.cc @@ -21,6 +21,19 @@ namespace rsz { using utl::RSZ; +namespace { + +void appendReconnectedLoad(MoveImpact& impact, + sta::Network* network, + const sta::Pin* load_pin) +{ + appendUnique(impact.reconnect_pins, load_pin); + appendUnique(impact.reconnect_instances, network->instance(load_pin)); + appendUnique(impact.timing_effect_instances, network->instance(load_pin)); +} + +} // namespace + RebufferMtCandidate::RebufferMtCandidate( Resizer& resizer, const Target& target, @@ -75,6 +88,36 @@ Estimate RebufferMtCandidate::estimate() return estimate; } +std::vector RebufferMtCandidate::guardedSidePins() const +{ + std::vector pins; + pins.reserve(side_contexts_.size()); + for (const BufferTreeSidePathContext& side : side_contexts_) { + pins.push_back(side.load_pin); + } + + return pins; +} + +MoveImpact RebufferMtCandidate::impact() const +{ + MoveImpact impact = MoveCandidate::impact(); + impact.complete = true; + + sta::Network* network = resizer_.network(); + sta::Net* net = plan_.flat_net; + appendUnique(impact.write_nets, net); + appendUnique(impact.timing_effect_nets, net); + sta::Instance* driver_inst = network->instance(target().driver_pin); + appendUnique(impact.write_instances, driver_inst); + appendUnique(impact.timing_effect_instances, driver_inst); + + for (const BufferTreeLoadPath& load_path : plan_.load_paths) { + appendReconnectedLoad(impact, network, load_path.load_pin); + } + return impact; +} + MoveResult RebufferMtCandidate::apply() { const int inserted_count diff --git a/src/rsz/src/move/RebufferMtCandidate.hh b/src/rsz/src/move/RebufferMtCandidate.hh index c762ef9f91f..b53fc7523e2 100644 --- a/src/rsz/src/move/RebufferMtCandidate.hh +++ b/src/rsz/src/move/RebufferMtCandidate.hh @@ -34,6 +34,8 @@ class RebufferMtCandidate : public MoveCandidate Estimate estimate() override; MoveResult apply() override; MoveType type() const override { return MoveType::kBuffer; } + MoveImpact impact() const override; + std::vector guardedSidePins() const override; float scaledScore() const { return raw_score_ * score_scale_; } private: diff --git a/src/rsz/src/move/RebufferMtGenerator.cc b/src/rsz/src/move/RebufferMtGenerator.cc index 91b3b874809..bfa3b3ca372 100644 --- a/src/rsz/src/move/RebufferMtGenerator.cc +++ b/src/rsz/src/move/RebufferMtGenerator.cc @@ -5,7 +5,11 @@ #include #include +#include +#include #include +#include +#include #include #include @@ -15,12 +19,18 @@ #include "OptimizerTypes.hh" #include "Rebuffer.hh" #include "RebufferMtCandidate.hh" +#include "RebufferPlannerPool.hh" +#include "RepairSetupProfiler.hh" #include "rsz/Resizer.hh" +#include "sta/Delay.hh" +#include "sta/Graph.hh" #include "sta/Network.hh" #include "sta/NetworkClass.hh" #include "sta/Path.hh" #include "sta/PathExpanded.hh" +#include "sta/Scene.hh" #include "sta/StaState.hh" +#include "utl/Logger.h" namespace rsz { @@ -51,10 +61,87 @@ const sta::Pin* targetLoadPin(const Target& target, Resizer& resizer) } // namespace RebufferMtGenerator::RebufferMtGenerator(const GeneratorContext& context) - : MoveGenerator(context) + : MoveGenerator(context), planners_(context.resizer) { } +RebufferLoadTimingMap RebufferMtGenerator::collectLoadTiming( + Resizer& resizer, + const Target& target, + const std::unordered_set* critical_load_pins, + const float off_path_weight) +{ + const ProfileScope profile("prepare_rebuffer_side_loads"); + RebufferLoadTimingMap load_timing; + const SelectedArc& arc = target.estimator_context->target().arc; + sta::dbSta* sta = resizer.sta(); + sta::Graph* graph = sta->graph(); + sta::Vertex* driver_vertex = graph->pinDrvrVertex(target.driver_pin); + if (driver_vertex == nullptr) { + return load_timing; + } + + const int dcalc_ap = arc.scene->dcalcAnalysisPtIndex(arc.min_max); + + // Wire edges from the driver reach every load on the net. + sta::VertexOutEdgeIterator edge_iter(driver_vertex, graph); + while (edge_iter.hasNext()) { + sta::Edge* edge = edge_iter.next(); + if (!edge->isWire()) { + continue; + } + + sta::Vertex* load_vertex = edge->to(graph); + const sta::Slack slack = sta->slack(load_vertex, arc.min_max); + if (sta::delayInf(slack, sta)) { + continue; + } + + // Loads off every violating worst path only lose pin slack, not TNS. + load_timing[load_vertex->pin()] + = {.slack = sta::delayAsFloat(slack), + .wire_delay = sta::delayAsFloat( + graph->wireArcDelay(edge, arc.outputRiseFall(), dcalc_ap)), + .weight = sideLoadWeight( + load_vertex->pin(), critical_load_pins, off_path_weight)}; + } + + RepairSetupProfiler::event("rebuffer_side_loads_collected", + load_timing.size()); + return load_timing; +} + +std::vector RebufferMtGenerator::makeSideContexts( + const RebufferTreePlan& plan, + const RebufferLoadTimingMap& load_timing) +{ + std::vector contexts; + if (load_timing.empty()) { + return contexts; + } + + for (const BufferTreeLoadPath& load_path : plan.load_paths) { + if (load_path.load_pin == nullptr + || load_path.load_pin == plan.target_load_pin) { + continue; + } + + const RebufferLoadTimingMap::const_iterator timing + = load_timing.find(load_path.load_pin); + if (timing == load_timing.end()) { + continue; + } + + contexts.push_back( + {.load_pin = load_path.load_pin, + .current_driver_to_load_delay = timing->second.wire_delay, + .current_slack = timing->second.slack, + .weight = timing->second.weight}); + } + + return contexts; +} + bool RebufferMtGenerator::isApplicable(const Target& target) const { return MoveGenerator::isApplicable(target) && target.fanout > 1 @@ -63,20 +150,78 @@ bool RebufferMtGenerator::isApplicable(const Target& target) const && target.isPrepared(kEstimatorContextCache); } +void RebufferMtGenerator::prepareInputs(const GeneratorContext& context, + Target& target) +{ + target.rebuffer_net + = context.resizer.rebuffer().makeBufferedNet(target.driver_pin); + // Without the side-load model the loads behind a new buffer cost nothing. + target.rebuffer_load_timing.clear(); + if (context.policy_config.rebuffer_mt_side_loads) { + target.rebuffer_load_timing + = collectLoadTiming(context.resizer, + target, + context.critical_load_pins, + context.policy_config.fanin_side_offpath_weight); + } +} + std::vector> RebufferMtGenerator::generate( const Target& target) { - // The main thread reads the net itself and charges no side loads. - Rebuffer& rebuffer_planner = resizer_.rebuffer(); + // The net and load timing prepareInputs() staged on the main thread, planned + // with a worker planner leased for this call. Without worker planners the + // lease hands out the Resizer's planner. + const RebufferPlannerPool::Lease lease(planners_); return generateWith(target, - rebuffer_planner, - rebuffer_planner.makeBufferedNet(target.driver_pin)); + lease.planner(), + target.rebuffer_net, + target.rebuffer_load_timing); +} + +void RebufferMtGenerator::startPhase(const size_t worker_count) +{ + // prepareInputs() stages nets with the Resizer's planner, which also plans + // when the phase has no worker threads. Each worker thread plans with a + // planner of its own. + const ProfileScope profile("rebuffer_planner_init"); + sta::Scene* scene = resizer_.sta()->cmdScene(); + resizer_.rebuffer().initForPlanning(scene); + planners_.reset(scene, worker_count); +} + +void RebufferMtGenerator::finishPhase() +{ + // The worker planners counted their warnings, so the log does not depend + // on the thread schedule. One line reports the counts by message ID. + const std::map warning_counts = planners_.workerWarningCounts(); + if (!warning_counts.empty()) { + int total = 0; + std::string counts; + for (const std::pair& id_count : warning_counts) { + counts += fmt::format("{}RSZ-{:04d} x{}", + counts.empty() ? "" : ", ", + id_count.first, + id_count.second); + total += id_count.second; + } + resizer_.logger()->warn(utl::RSZ, + 2034, + "{} RebufferMt planning warnings on worker threads " + "were not logged: {}. Run with set_thread_count 1 " + "to log them.", + total, + counts); + } + + planners_.reset(resizer_.sta()->cmdScene(), 0); } std::vector> RebufferMtGenerator::generateWith( const Target& target, Rebuffer& rebuffer_planner, - BufferedNetPtr net) const + BufferedNetPtr net, + const RebufferLoadTimingMap& load_timing) const { std::vector> candidates; @@ -94,13 +239,14 @@ std::vector> RebufferMtGenerator::generateWith( const std::vector plans = rebuffer_planner.makeRebufferTreePlans( target.driver_pin, std::move(net), load_pin, max_tree_candidates); - addTreePlanCandidates(target, plans, candidates); + addTreePlanCandidates(target, plans, load_timing, candidates); return candidates; } void RebufferMtGenerator::addTreePlanCandidates( const Target& target, const std::vector& plans, + const RebufferLoadTimingMap& load_timing, std::vector>& candidates) const { for (const RebufferTreePlan& plan : plans) { @@ -128,7 +274,8 @@ void RebufferMtGenerator::addTreePlanCandidates( target.estimator_context.value(), plan, policy_config_.rebuffer_mt_score_scale, - policy_config_.rebuffer_mt_min_score)); + policy_config_.rebuffer_mt_min_score, + makeSideContexts(plan, load_timing))); } } diff --git a/src/rsz/src/move/RebufferMtGenerator.hh b/src/rsz/src/move/RebufferMtGenerator.hh index af0ae5b7b91..f03c1d545ac 100644 --- a/src/rsz/src/move/RebufferMtGenerator.hh +++ b/src/rsz/src/move/RebufferMtGenerator.hh @@ -3,21 +3,29 @@ #pragma once +#include #include +#include #include #include "DelayEstimator.hh" #include "MoveCandidate.hh" #include "MoveGenerator.hh" #include "OptimizerTypes.hh" +#include "RebufferPlannerPool.hh" + +namespace sta { +class Pin; +} // namespace sta namespace rsz { class Rebuffer; class Resizer; -// MT form of Rebuffer. It plans several buffer trees for one driver, and a -// winning candidate commits exactly the tree it scored. +// MT form of Rebuffer. The main thread stages the driver net and its load +// timing. A worker plans buffer trees with its own planner, and a winning +// candidate commits exactly the tree it scored. class RebufferMtGenerator : public MoveGenerator { public: @@ -29,24 +37,53 @@ class RebufferMtGenerator : public MoveGenerator const char* name() const override { return "RebufferMtMove"; } PrepareCacheMask prepareRequirements() const override { - return kEstimatorContextCache; + return kEstimatorContextCache | kRebufferInputCache; } bool isApplicable(const Target& target) const override; - // Plans with the Resizer's planner on the main thread. + // Plans the prepared net with a worker planner from the pool, or with the + // Resizer's planner when the phase has no worker threads. std::vector> generate( const Target& target) override; + // kRebufferInputCache: the driver net as a buffered-net tree and, with + // rebuffer_mt_side_loads, the current timing of its loads. + static void prepareInputs(const GeneratorContext& context, Target& target); + + // Readies the Resizer's planner for prepareInputs() and makes one planner + // per worker thread. + void startPhase(size_t worker_count) override; + // Logs one line for the warnings the worker planners counted, then releases + // them. + void finishPhase() override; + private: - // The candidates of one target: tree plans of `net` from `rebuffer_planner`. + // The candidates of one target: the tree plans of `net` from + // `rebuffer_planner`. std::vector> generateWith( const Target& target, Rebuffer& rebuffer_planner, - BufferedNetPtr net) const; + BufferedNetPtr net, + const RebufferLoadTimingMap& load_timing) const; + // Main-thread STA reads for every load of the target driver net. + static RebufferLoadTimingMap collectLoadTiming( + Resizer& resizer, + const Target& target, + const std::unordered_set* critical_load_pins, + float off_path_weight); // One candidate per tree plan that passes the plan gates. void addTreePlanCandidates( const Target& target, const std::vector& plans, + const RebufferLoadTimingMap& load_timing, std::vector>& candidates) const; + // Side contexts without a downstream window for the plan's other loads. The + // main tree window scores the target load path. + static std::vector makeSideContexts( + const RebufferTreePlan& plan, + const RebufferLoadTimingMap& load_timing); + + // Empty outside a phase with worker threads. + RebufferPlannerPool planners_; }; } // namespace rsz diff --git a/src/rsz/src/move/RebufferPlannerPool.cc b/src/rsz/src/move/RebufferPlannerPool.cc new file mode 100644 index 00000000000..4a08a854742 --- /dev/null +++ b/src/rsz/src/move/RebufferPlannerPool.cc @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#include "RebufferPlannerPool.hh" + +#include +#include +#include +#include +#include + +#include "Rebuffer.hh" +#include "rsz/Resizer.hh" + +namespace rsz { + +RebufferPlannerPool::RebufferPlannerPool(Resizer& resizer) : resizer_(resizer) +{ +} + +RebufferPlannerPool::~RebufferPlannerPool() = default; + +void RebufferPlannerPool::reset(sta::Scene* scene, const size_t count) +{ + planners_.clear(); + idle_.clear(); + for (size_t index = 0; index < count; ++index) { + std::unique_ptr planner = std::make_unique(&resizer_); + planner->initForPlanning(scene, /* worker */ true); + idle_.push_back(planner.get()); + planners_.push_back(std::move(planner)); + } +} + +std::map RebufferPlannerPool::workerWarningCounts() const +{ + std::map counts; + for (const std::unique_ptr& planner : planners_) { + for (const std::pair& id_count : + planner->workerWarningCounts()) { + counts[id_count.first] += id_count.second; + } + } + return counts; +} + +Rebuffer& RebufferPlannerPool::acquire() +{ + // Inline tasks keep the Resizer's planner. + if (planners_.empty()) { + return resizer_.rebuffer(); + } + + const std::lock_guard lock(mutex_); + Rebuffer* planner = idle_.back(); + idle_.pop_back(); + return *planner; +} + +void RebufferPlannerPool::release(Rebuffer& planner) +{ + if (planners_.empty()) { + return; + } + + const std::lock_guard lock(mutex_); + idle_.push_back(&planner); +} + +} // namespace rsz diff --git a/src/rsz/src/move/RebufferPlannerPool.hh b/src/rsz/src/move/RebufferPlannerPool.hh new file mode 100644 index 00000000000..8e01e1f366c --- /dev/null +++ b/src/rsz/src/move/RebufferPlannerPool.hh @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#pragma once + +#include +#include +#include +#include +#include + +namespace sta { +class Scene; +} // namespace sta + +namespace rsz { + +class Rebuffer; +class Resizer; + +// One Rebuffer planner per worker thread, because planning keeps per-call +// scratch state. An empty pool hands out the Resizer's planner to tasks +// that run inline. +class RebufferPlannerPool +{ + public: + // A planner borrowed for one task. The lease returns it also when planning + // throws, because the other tasks of the batch still run. + class Lease + { + public: + explicit Lease(RebufferPlannerPool& pool) + : pool_(pool), planner_(pool.acquire()) + { + } + ~Lease() { pool_.release(planner_); } + Lease(const Lease&) = delete; + Lease& operator=(const Lease&) = delete; + + Rebuffer& planner() const { return planner_; } + + private: + RebufferPlannerPool& pool_; + Rebuffer& planner_; + }; + + explicit RebufferPlannerPool(Resizer& resizer); + ~RebufferPlannerPool(); + + // Make `count` worker planners on `scene`; zero leaves the pool empty. + void reset(sta::Scene* scene, size_t count); + // The warnings the worker planners counted instead of logging, by message + // ID. + std::map workerWarningCounts() const; + + private: + // Borrow a worker planner, or the shared planner for an inline task. + Rebuffer& acquire(); + // Return a planner after its task has finished. + void release(Rebuffer& planner); + + Resizer& resizer_; + // Planners with independent delay-calculator scratch state. + std::vector> planners_; + // Available planners protected by mutex_. + std::vector idle_; + std::mutex mutex_; +}; + +} // namespace rsz diff --git a/src/rsz/src/move/SizeUpMtCandidate.cc b/src/rsz/src/move/SizeUpMtCandidate.cc index c3d777b82b6..31d5583964d 100644 --- a/src/rsz/src/move/SizeUpMtCandidate.cc +++ b/src/rsz/src/move/SizeUpMtCandidate.cc @@ -116,6 +116,11 @@ MoveResult SizeUpMtCandidate::apply() }; } +MoveImpact SizeUpMtCandidate::impact() const +{ + return cellSwapImpact(inst_); +} + std::string SizeUpMtCandidate::logName() const { return resizer_.network()->pathName(target().driver_pin); diff --git a/src/rsz/src/move/SizeUpMtCandidate.hh b/src/rsz/src/move/SizeUpMtCandidate.hh index 4371e6031b5..a4a355799f3 100644 --- a/src/rsz/src/move/SizeUpMtCandidate.hh +++ b/src/rsz/src/move/SizeUpMtCandidate.hh @@ -42,6 +42,11 @@ class SizeUpMtCandidate : public MoveCandidate Estimate estimate() override; MoveResult apply() override; MoveType type() const override { return MoveType::kSizeUp; } + MoveImpact impact() const override; + std::vector guardedSidePins() const override + { + return faninSidePins(target().estimator_context.value()); + } private: std::string logName() const; diff --git a/src/rsz/src/move/VtSwapMtCandidate.cc b/src/rsz/src/move/VtSwapMtCandidate.cc index 6e1a4ff750c..e208233ed62 100644 --- a/src/rsz/src/move/VtSwapMtCandidate.cc +++ b/src/rsz/src/move/VtSwapMtCandidate.cc @@ -100,4 +100,9 @@ MoveResult VtSwapMtCandidate::apply() }; } +MoveImpact VtSwapMtCandidate::impact() const +{ + return cellSwapImpact(inst_); +} + } // namespace rsz diff --git a/src/rsz/src/move/VtSwapMtCandidate.hh b/src/rsz/src/move/VtSwapMtCandidate.hh index 06b1ba67d5c..9ce55946663 100644 --- a/src/rsz/src/move/VtSwapMtCandidate.hh +++ b/src/rsz/src/move/VtSwapMtCandidate.hh @@ -38,6 +38,11 @@ class VtSwapMtCandidate : public MoveCandidate // === MoveCandidate API ==================================================== Estimate estimate() override; MoveResult apply() override; + MoveImpact impact() const override; + std::vector guardedSidePins() const override + { + return faninSidePins(target().estimator_context.value()); + } MoveType type() const override { return MoveType::kVtSwap; } private: diff --git a/src/rsz/src/move/VtSwapMtGenerator.cc b/src/rsz/src/move/VtSwapMtGenerator.cc index 6015b997a94..d0544d67839 100644 --- a/src/rsz/src/move/VtSwapMtGenerator.cc +++ b/src/rsz/src/move/VtSwapMtGenerator.cc @@ -34,7 +34,10 @@ std::vector> VtSwapMtGenerator::generate( const EstimatorContext& estimator_context = target.estimator_context.value(); const SelectedArc& target_arc = estimator_context.target().arc; - sta::LibertyCell* current_cell = estimator_context.target().arc.currentCell(); + // The arc holds the cell of its scene. The Resizer caches, warmed before + // the workers start, key on the link cell. + sta::LibertyCell* current_cell + = resizer_.network()->libertyCell(target.inst(resizer_)); const std::vector candidate_cells = selectCandidateCells(current_cell); diff --git a/src/rsz/src/policy/BatchSelector.cc b/src/rsz/src/policy/BatchSelector.cc new file mode 100644 index 00000000000..1c254205f67 --- /dev/null +++ b/src/rsz/src/policy/BatchSelector.cc @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#include "BatchSelector.hh" + +#include +#include +#include +#include +#include +#include + +namespace rsz { + +namespace { + +// True when the two lists share an element. +template +bool intersects(const std::vector& lhs, const std::vector& rhs) +{ + return std::ranges::any_of(lhs, [&rhs](const T& value) { + return std::ranges::find(rhs, value) != rhs.end(); + }); +} + +} // namespace + +const char* batchConflictReasonName(const BatchConflictReason reason) +{ + switch (reason) { + case BatchConflictReason::kIncompleteImpact: + return "incomplete_impact"; + case BatchConflictReason::kWriteInstance: + return "write_instance"; + case BatchConflictReason::kWriteNet: + return "write_net"; + case BatchConflictReason::kReconnectPin: + return "reconnect_pin"; + case BatchConflictReason::kWriteReconnectInstance: + return "write_reconnect_instance"; + case BatchConflictReason::kForwardDependency: + return "forward_dependency"; + case BatchConflictReason::kCount: + break; + } + + return "unknown"; +} + +BatchSelector::BatchSelector(BatchSelectorConfig config) + : config_(std::move(config)) +{ +} + +BatchSelection BatchSelector::select( + const std::vector& candidates, + const int batch_cap, + const bool reserve_worst) const +{ + std::vector ranked; + ranked.reserve(candidates.size()); + for (const BatchCandidate& candidate : candidates) { + ranked.push_back(&candidate); + } + + std::ranges::stable_sort( + ranked, [](const BatchCandidate* lhs, const BatchCandidate* rhs) { + return lhs->priority_score > rhs->priority_score; + }); + + Admission admission; + admission.impacts.reserve(ranked.size()); + fillWorstReserve( + ranked, worstReserve(ranked.size(), batch_cap, reserve_worst), admission); + fillByScore(ranked, batch_cap, admission); + if (admission.selection.stats.wns_reserved > 0) { + orderByScore(candidates, admission.selection); + } + + return admission.selection; +} + +bool BatchSelector::admit(const BatchCandidate& candidate, + Admission& admission) const +{ + BatchSelection& selection = admission.selection; + ++selection.stats.considered; + if (!candidate.impact.complete) { + ++selection.stats.deferred[static_cast( + BatchConflictReason::kIncompleteImpact)]; + return false; + } + + std::optional conflict; + for (const MoveImpact* selected_impact : admission.impacts) { + const BatchConflictReason reason + = hardConflict(*selected_impact, candidate.impact); + if (reason != BatchConflictReason::kCount) { + conflict = reason; + break; + } + } + + if (!conflict.has_value()) { + for (const MoveImpact* selected_impact : admission.impacts) { + if (dependsOn(candidate.impact, *selected_impact)) { + conflict = BatchConflictReason::kForwardDependency; + break; + } + } + } + + if (conflict.has_value()) { + ++selection.stats.deferred[static_cast(*conflict)]; + return false; + } + + selection.evaluation_indices.push_back(candidate.evaluation_index); + admission.impacts.push_back(&candidate.impact); + admission.taken.insert(&candidate); + + return true; +} + +int BatchSelector::worstReserve(const size_t candidate_count, + const int batch_cap, + const bool reserve_worst) const +{ + // The score order favors TNS: with many winners per batch, the small-gain + // moves on the worst path never get in and WNS stalls. The reserve matters + // only when the batch is oversubscribed. + const bool oversubscribed = static_cast(candidate_count) > batch_cap; + if (!reserve_worst || config_.wns_reserve_pct <= 0 || !oversubscribed) { + return 0; + } + + return std::clamp(batch_cap * config_.wns_reserve_pct / 100, 1, batch_cap); +} + +void BatchSelector::fillWorstReserve( + const std::vector& ranked, + const int reserve, + Admission& admission) const +{ + if (reserve <= 0) { + return; + } + + std::vector critical = ranked; + std::ranges::stable_sort( + critical, [](const BatchCandidate* lhs, const BatchCandidate* rhs) { + return lhs->endpoint_slack < rhs->endpoint_slack; + }); + BatchSelectionStats& stats = admission.selection.stats; + for (const BatchCandidate* candidate : critical) { + if (stats.wns_reserved >= reserve + || candidate->endpoint_slack >= config_.slack_margin) { + break; + } + if (admit(*candidate, admission)) { + ++stats.wns_reserved; + } + } +} + +void BatchSelector::fillByScore( + const std::vector& ranked, + const int batch_cap, + Admission& admission) const +{ + float best_local_score = 0.0f; + bool local_score_ordered = true; + for (size_t index = 0; index < ranked.size(); ++index) { + best_local_score = std::max(best_local_score, ranked[index]->local_score); + if (index > 0 + && ranked[index]->local_score > ranked[index - 1]->local_score) { + local_score_ordered = false; + } + } + + BatchSelection& selection = admission.selection; + for (size_t ranked_index = 0; ranked_index < ranked.size(); ++ranked_index) { + if (static_cast(selection.evaluation_indices.size()) >= batch_cap) { + selection.stats.batch_limited + = static_cast(ranked.size() - ranked_index); + break; + } + + if (admission.taken.contains(ranked[ranked_index])) { + continue; + } + + if (config_.min_score_ratio > 0.0f && best_local_score > 0.0f + && static_cast(selection.evaluation_indices.size()) + >= config_.score_floor_start + && ranked[ranked_index]->local_score + < best_local_score * config_.min_score_ratio) { + if (local_score_ordered) { + selection.stats.score_limited + = static_cast(ranked.size() - ranked_index); + break; + } + + ++selection.stats.score_limited; + continue; + } + + admit(*ranked[ranked_index], admission); + } +} + +void BatchSelector::orderByScore(const std::vector& candidates, + BatchSelection& selection) +{ + std::unordered_map score_of; + score_of.reserve(candidates.size()); + for (const BatchCandidate& candidate : candidates) { + score_of[candidate.evaluation_index] = candidate.priority_score; + } + + std::ranges::stable_sort(selection.evaluation_indices, + [&score_of](const size_t lhs, const size_t rhs) { + return score_of[lhs] > score_of[rhs]; + }); +} + +BatchConflictReason BatchSelector::hardConflict(const MoveImpact& selected, + const MoveImpact& candidate) +{ + if (intersects(selected.write_instances, candidate.write_instances)) { + return BatchConflictReason::kWriteInstance; + } + + if (intersects(selected.write_nets, candidate.write_nets)) { + return BatchConflictReason::kWriteNet; + } + + if (intersects(selected.reconnect_pins, candidate.reconnect_pins)) { + return BatchConflictReason::kReconnectPin; + } + + if (intersects(selected.write_instances, candidate.reconnect_instances) + || intersects(candidate.write_instances, selected.reconnect_instances)) { + return BatchConflictReason::kWriteReconnectInstance; + } + + return BatchConflictReason::kCount; +} + +bool BatchSelector::dependsOn(const MoveImpact& candidate, + const MoveImpact& selected) +{ + return intersects(selected.timing_effect_instances, + candidate.timing_dependency_instances) + || intersects(selected.timing_effect_nets, + candidate.timing_dependency_nets); +} + +} // namespace rsz diff --git a/src/rsz/src/policy/BatchSelector.hh b/src/rsz/src/policy/BatchSelector.hh new file mode 100644 index 00000000000..1d8ec794779 --- /dev/null +++ b/src/rsz/src/policy/BatchSelector.hh @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#pragma once + +#include +#include +#include +#include + +#include "OptimizationPolicyConfig.hh" +#include "OptimizerTypes.hh" + +namespace rsz { + +enum class BatchConflictReason +{ + kIncompleteImpact, + kWriteInstance, + kWriteNet, + kReconnectPin, + kWriteReconnectInstance, + kForwardDependency, + kCount +}; + +const char* batchConflictReasonName(BatchConflictReason reason); + +// One scored move and the conflicts it would introduce into a batch. +struct BatchCandidate +{ + // Index of the owning TargetEvaluation in the batch pool. + size_t evaluation_index{0}; + // Candidate score before cross-target priority weighting. + float local_score{0.0f}; + // Score used to order candidates across targets. + float priority_score{0.0f}; + // Slack of the endpoint this target sits on; the reserve ranks on it. + float endpoint_slack{0.0f}; + MoveImpact impact; +}; + +// Counts batch admission decisions and conflict deferrals. +struct BatchSelectionStats +{ + int considered{0}; + int batch_limited{0}; + int score_limited{0}; + int wns_reserved{0}; + std::array(BatchConflictReason::kCount)> deferred{}; +}; + +// Holds selected evaluation indices and their admission statistics. +struct BatchSelection +{ + std::vector evaluation_indices; + BatchSelectionStats stats; +}; + +// Selects a conflict-free batch in score order. A candidate waits when it +// changes what a batch member changes, or when its timing depends on a batch +// member that its estimate did not see. +class BatchSelector +{ + public: + explicit BatchSelector(BatchSelectorConfig config); + + // Selects at most batch_cap candidates. reserve_worst turns on the + // worst-endpoint reserve; the policy asks for it once WNS stops improving. + BatchSelection select(const std::vector& candidates, + int batch_cap, + bool reserve_worst) const; + + private: + // The batch under construction: the selection, the impacts of its members + // for the conflict checks and the candidates already admitted. + struct Admission + { + BatchSelection selection; + std::vector impacts; + std::unordered_set taken; + }; + + // Takes one candidate when it does not conflict with the batch. + bool admit(const BatchCandidate& candidate, Admission& admission) const; + // Slots reserved for the worst endpoints; zero when the reserve is off or + // every candidate fits anyway. + int worstReserve(size_t candidate_count, + int batch_cap, + bool reserve_worst) const; + // Fills the reserve with the candidates of the most negative endpoints. + void fillWorstReserve(const std::vector& ranked, + int reserve, + Admission& admission) const; + // Takes the other candidates in score order, up to the cap and the score + // floor. + void fillByScore(const std::vector& ranked, + int batch_cap, + Admission& admission) const; + // Orders the selection by priority score, so the reserve changes which moves + // are in the batch, not the order they are applied in. + static void orderByScore(const std::vector& candidates, + BatchSelection& selection); + // Returns the first write or reconnect conflict between two moves. + static BatchConflictReason hardConflict(const MoveImpact& selected, + const MoveImpact& candidate); + // Tests whether the candidate's estimate read timing changed by a member. + static bool dependsOn(const MoveImpact& candidate, + const MoveImpact& selected); + + // Score floor and worst-endpoint reserve used for each selection. + BatchSelectorConfig config_; +}; + +} // namespace rsz diff --git a/src/rsz/src/policy/BottleneckAnalysis.cc b/src/rsz/src/policy/BottleneckAnalysis.cc new file mode 100644 index 00000000000..9d2f7feee91 --- /dev/null +++ b/src/rsz/src/policy/BottleneckAnalysis.cc @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#include "BottleneckAnalysis.hh" + +#include +#include +#include +#include +#include +#include +#include + +#include "db_sta/dbSta.hh" +#include "sta/Graph.hh" +#include "sta/MinMax.hh" +#include "sta/Network.hh" +#include "sta/TimingRole.hh" + +namespace rsz { + +namespace { + +// The fanin edges that carry data arrivals: no timing checks, no clock-to-Q +// or latch D-to-Q boundaries, no disabled loops. +template +void forEachDataFanin(const sta::Graph* graph, sta::Vertex* vertex, Fn&& fn) +{ + sta::VertexInEdgeIterator edge_iter(vertex, graph); + while (edge_iter.hasNext()) { + sta::Edge* edge = edge_iter.next(); + const sta::TimingRole* role = edge->role(); + if (role->isTimingCheck() || role == sta::TimingRole::regClkToQ() + || role->isLatchDtoQ() || edge->isDisabledLoop()) { + continue; + } + fn(edge->from(graph)); + } +} + +constexpr size_t kBitsPerWord = 64; + +} // namespace + +BottleneckAnalysis::BottleneckAnalysis(sta::dbSta* sta, + const sta::MinMax* min_max) + : sta_(sta), min_max_(min_max) +{ +} + +void BottleneckAnalysis::analyze( + const std::span violating_endpoints, + const float slack_margin, + const int max_endpoints, + const int max_sources) +{ + for (const Node& node : nodes_) { + node_of_[node.id] = -1; + } + nodes_.clear(); + order_.clear(); + max_cone_need_ = 0.0f; + + const std::vector endpoint_nodes = collectCone( + violating_endpoints.first(std::min(violating_endpoints.size(), + static_cast(max_endpoints))), + slack_margin); + if (nodes_.empty()) { + return; + } + + sumFanoutNeeds(endpoint_nodes); + sumFaninNeeds(max_sources); + for (const Node& node : nodes_) { + max_cone_need_ = std::max(max_cone_need_, coneNeed(node)); + } +} + +float BottleneckAnalysis::weightedScore( + const float local_score, + sta::Vertex* vertex, + const BatchPriorityConfig& priority) const +{ + const int32_t node_index = nodeOf(vertex); + const float weight = node_index >= 0 && max_cone_need_ > 0.0f + ? coneNeed(nodes_[node_index]) / max_cone_need_ + : 0.0f; + // The factor also multiplies a negative score, so a cone-central target with + // a loss ranks lower than the same loss elsewhere. + return local_score + * (1.0f + priority.alpha * std::pow(weight, priority.gamma)); +} + +size_t BottleneckAnalysis::admit(sta::Vertex* vertex, const float need) +{ + const size_t node_index = nodes_.size(); + const sta::VertexId id = sta_->graph()->id(vertex); + // Vertex ids start at 1 and a deleted vertex leaves its id unused, so an + // id can reach or pass the vertex count. + if (id >= node_of_.size()) { + node_of_.resize(id + 1, -1); + } + nodes_.push_back({.vertex = vertex, .id = id, .need = need}); + node_of_[id] = static_cast(node_index); + return node_index; +} + +int32_t BottleneckAnalysis::nodeOf(const sta::Vertex* vertex) const +{ + if (vertex == nullptr) { + return -1; + } + const sta::VertexId id = sta_->graph()->id(vertex); + return id < node_of_.size() ? node_of_[id] : -1; +} + +std::vector BottleneckAnalysis::collectCone( + const std::span endpoints, + const float slack_margin) +{ + const sta::Graph* graph = sta_->graph(); + std::vector endpoint_nodes; + std::vector queue; + for (const EndpointSlack& endpoint : endpoints) { + sta::Vertex* vertex = graph->pinLoadVertex(endpoint.first); + const float need = slack_margin - sta::delayAsFloat(endpoint.second); + if (vertex == nullptr || need <= 0.0f || nodeOf(vertex) >= 0) { + continue; + } + endpoint_nodes.push_back(admit(vertex, need)); + queue.push_back(endpoint_nodes.back()); + } + + // Backward search through the vertices that still violate: every vertex + // on a violating path has negative slack itself. + while (!queue.empty()) { + const size_t node_index = queue.back(); + queue.pop_back(); + forEachDataFanin(graph, nodes_[node_index].vertex, [&](sta::Vertex* fanin) { + if (nodeOf(fanin) >= 0) { + nodes_[node_index].has_cone_fanin = true; + return; + } + const float need + = slack_margin - sta::delayAsFloat(sta_->slack(fanin, min_max_)); + if (need <= 0.0f) { + return; + } + queue.push_back(admit(fanin, need)); + nodes_[node_index].has_cone_fanin = true; + }); + } + + // The sums run in level order: a fanin always has the lower level. + order_.resize(nodes_.size()); + std::iota(order_.begin(), order_.end(), size_t{0}); + std::ranges::stable_sort(order_, [this](const size_t lhs, const size_t rhs) { + return nodes_[lhs].vertex->level() < nodes_[rhs].vertex->level(); + }); + return endpoint_nodes; +} + +void BottleneckAnalysis::sumFanoutNeeds( + const std::vector& endpoint_nodes) +{ + const sta::Graph* graph = sta_->graph(); + const size_t words + = (endpoint_nodes.size() + kBitsPerWord - 1) / kBitsPerWord; + std::vector reach(nodes_.size() * words, 0); + for (size_t bit = 0; bit < endpoint_nodes.size(); ++bit) { + reach[endpoint_nodes[bit] * words + bit / kBitsPerWord] + |= uint64_t{1} << (bit % kBitsPerWord); + } + + // Descending level: a vertex's set is complete once every fanout of it + // has been visited, and is then handed to its fanins. + for (size_t position = order_.size(); position-- > 0;) { + const size_t node_index = order_[position]; + forEachDataFanin(graph, nodes_[node_index].vertex, [&](sta::Vertex* fanin) { + const int32_t fanin_index = nodeOf(fanin); + if (fanin_index < 0) { + return; + } + const size_t fanin_base = static_cast(fanin_index) * words; + const size_t base = node_index * words; + for (size_t word = 0; word < words; ++word) { + reach[fanin_base + word] |= reach[base + word]; + } + }); + } + + for (size_t node_index = 0; node_index < nodes_.size(); ++node_index) { + Node& node = nodes_[node_index]; + for (size_t word = 0; word < words; ++word) { + uint64_t bits = reach[node_index * words + word]; + while (bits != 0) { + const size_t bit = word * kBitsPerWord + std::countr_zero(bits); + node.fanout_need += nodes_[endpoint_nodes[bit]].need; + bits &= bits - 1; + } + } + } +} + +void BottleneckAnalysis::sumFaninNeeds(const int max_sources) +{ + // The cone sources, worst first. + std::vector sources; + for (size_t node_index = 0; node_index < nodes_.size(); ++node_index) { + if (!nodes_[node_index].has_cone_fanin) { + sources.push_back(node_index); + } + } + std::ranges::stable_sort(sources, [this](const size_t lhs, const size_t rhs) { + return nodes_[lhs].need > nodes_[rhs].need; + }); + sources.resize(std::min(sources.size(), static_cast(max_sources))); + + const sta::Graph* graph = sta_->graph(); + const size_t words = (sources.size() + kBitsPerWord - 1) / kBitsPerWord; + std::vector reach(nodes_.size() * words, 0); + for (size_t bit = 0; bit < sources.size(); ++bit) { + reach[sources[bit] * words + bit / kBitsPerWord] |= uint64_t{1} + << (bit % kBitsPerWord); + } + + // Ascending level: the fanins are complete before the vertex collects + // them. + for (const size_t node_index : order_) { + forEachDataFanin(graph, nodes_[node_index].vertex, [&](sta::Vertex* fanin) { + const int32_t fanin_index = nodeOf(fanin); + if (fanin_index < 0) { + return; + } + const size_t fanin_base = static_cast(fanin_index) * words; + const size_t base = node_index * words; + for (size_t word = 0; word < words; ++word) { + reach[base + word] |= reach[fanin_base + word]; + } + }); + } + + for (size_t node_index = 0; node_index < nodes_.size(); ++node_index) { + Node& node = nodes_[node_index]; + for (size_t word = 0; word < words; ++word) { + uint64_t bits = reach[node_index * words + word]; + while (bits != 0) { + const size_t bit = word * kBitsPerWord + std::countr_zero(bits); + node.fanin_need += nodes_[sources[bit]].need; + bits &= bits - 1; + } + } + } +} + +float BottleneckAnalysis::coneNeed(const Node& node) const +{ + // The geometric mean keeps the weight in seconds and balances the two + // sides, so neither the sources nor the endpoints dominate. + return std::sqrt(node.fanin_need * node.fanout_need); +} + +} // namespace rsz diff --git a/src/rsz/src/policy/BottleneckAnalysis.hh b/src/rsz/src/policy/BottleneckAnalysis.hh new file mode 100644 index 00000000000..da3ac2ec364 --- /dev/null +++ b/src/rsz/src/policy/BottleneckAnalysis.hh @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#pragma once + +#include +#include +#include +#include +#include + +#include "OptimizationPolicyConfig.hh" +#include "sta/Delay.hh" +#include "sta/GraphClass.hh" + +namespace sta { +class dbSta; +class MinMax; +class Pin; +class Vertex; +} // namespace sta + +namespace rsz { + +// Ranks shared violating-cone bottlenecks during stalled batches. +// analyze() walks the fanin of the worst violating endpoints through +// negative-slack vertices, stopping at registers and latches. For each +// vertex, fanout_need sums the need (margin - slack) of the distinct +// endpoints behind it, and fanin_need that of the distinct cone sources in +// front of it, so vertices near the sources are not favored. Each endpoint +// or source costs one bit per cone vertex, so both counts are capped. +class BottleneckAnalysis +{ + public: + using EndpointSlack = std::pair; + + BottleneckAnalysis(sta::dbSta* sta, const sta::MinMax* min_max); + + // Rebuild the weights from the current timing. + void analyze(std::span violating_endpoints, + float slack_margin, + int max_endpoints, + int max_sources); + // The rank of a target driven from vertex: local_score * (1 + alpha * + // w^gamma), where w is the vertex's cone need relative to the largest one + // (0 outside the cone). + float weightedScore(float local_score, + sta::Vertex* vertex, + const BatchPriorityConfig& priority) const; + size_t coneSize() const { return nodes_.size(); } + + private: + // Need accumulated at one graph vertex from its fanin and fanout cones. + struct Node + { + sta::Vertex* vertex{nullptr}; + sta::VertexId id{0}; + float need{0.0f}; + bool has_cone_fanin{false}; + float fanin_need{0.0f}; + float fanout_need{0.0f}; + }; + + // Inserts or updates a vertex in the analyzed cone. + size_t admit(sta::Vertex* vertex, float need); + // The node of a live vertex, or -1 outside the cone. + int32_t nodeOf(const sta::Vertex* vertex) const; + // The cone behind the endpoints; returns the node of each endpoint. + std::vector collectCone(std::span endpoints, + float slack_margin); + // Accumulates distinct downstream endpoint need at every cone vertex. + void sumFanoutNeeds(const std::vector& endpoint_nodes); + // Accumulates distinct upstream source need to normalize source proximity. + void sumFaninNeeds(int max_sources); + // Returns the balanced fanin/fanout need used for ranking. + float coneNeed(const Node& node) const; + + sta::dbSta* sta_; + const sta::MinMax* min_max_; + std::vector nodes_; + // nodes_ indices in ascending level order. + std::vector order_; + // Node index per graph vertex id, -1 outside the cone. Only the entries of + // nodes_ are set, and they are cleared before the next analysis. + std::vector node_of_; + float max_cone_need_{0.0f}; +}; + +} // namespace rsz diff --git a/src/rsz/src/policy/OptimizationPolicy.cc b/src/rsz/src/policy/OptimizationPolicy.cc index 47d0337f331..fea290736b0 100644 --- a/src/rsz/src/policy/OptimizationPolicy.cc +++ b/src/rsz/src/policy/OptimizationPolicy.cc @@ -16,6 +16,8 @@ #include "DelayEstimator.hh" #include "MoveCommitter.hh" #include "OptimizerTypes.hh" +#include "Rebuffer.hh" +#include "RebufferMtGenerator.hh" #include "RepairSetupContext.hh" #include "RepairSetupProfiler.hh" #include "RepairTargetCollector.hh" @@ -33,7 +35,6 @@ #include "odb/db.h" #include "rsz/Resizer.hh" #include "sta/Delay.hh" -#include "sta/Fuzzy.hh" #include "sta/Liberty.hh" #include "sta/LibertyClass.hh" #include "sta/Network.hh" @@ -73,11 +74,13 @@ char phaseMarkerForIndex(int phase_index) OptimizationPolicy::OptimizationPolicy(Resizer& resizer, MoveCommitter& committer, RepairSetupContext& setup_context, - const OptimizerRunConfig& config) + const OptimizerRunConfig& config, + OptimizationPolicyConfig policy_config) : resizer_(resizer), committer_(committer), setup_context_(setup_context), config_(config), + policy_config_(std::move(policy_config)), target_collector_(&setup_context_.target_collector), progress_(resizer, committer, setup_context) { @@ -112,117 +115,14 @@ bool OptimizationPolicy::start() return true; } -bool OptimizationPolicy::finalizeAndReport() -{ - RepairTargetCollector final_targets(&resizer_); - final_targets.init(config_.setup_slack_margin, - setup_context_.progress_header_show_startpoint_metrics); - setup_context_.final_wns = final_targets.getWns(); - progress_.printFinal(final_targets, showStartpointMetrics()); - committer_.printTrackerFinalReports(finalReportPins()); - return reportRepairSummary(); -} - -const std::vector& OptimizationPolicy::finalReportPins() const -{ - return target_collector_->getViolatingPins(); -} - -bool OptimizationPolicy::reportRepairSummary() const -{ - const ProfileScope profile("reporting"); - bool repaired = false; - - const int buffer_moves = committer_.summaryCommittedMoves(MoveType::kBuffer); - const int size_up_moves = committer_.summaryCommittedMoves(MoveType::kSizeUp); - const int size_down_fanout_moves - = committer_.summaryCommittedMoves(MoveType::kSizeDownFanout); - const int swap_pins_moves - = committer_.summaryCommittedMoves(MoveType::kSwapPins); - const int clone_moves = committer_.summaryCommittedMoves(MoveType::kClone); - const int split_load_moves - = committer_.summaryCommittedMoves(MoveType::kSplitLoad); - const int unbuffer_moves - = committer_.summaryCommittedMoves(MoveType::kUnbuffer); - const int vt_swap_moves = committer_.summaryCommittedMoves(MoveType::kVtSwap); - const int size_up_match_moves - = committer_.summaryCommittedMoves(MoveType::kSizeUpMatch); - const int reroute_moves - = committer_.summaryCommittedMoves(MoveType::kReroute); - const int buffer_to_inverters_moves - = committer_.summaryCommittedMoves(MoveType::kBufferToInverters); - - if (unbuffer_moves > 0) { - repaired = true; - logger_->info(utl::RSZ, 59, "Removed {} buffers.", unbuffer_moves); - } - if (buffer_moves > 0 || split_load_moves > 0) { - repaired = true; - if (split_load_moves == 0) { - logger_->info(utl::RSZ, 40, "Inserted {} buffers.", buffer_moves); - } else { - logger_->info(utl::RSZ, - 45, - "Inserted {} buffers, {} to split loads.", - buffer_moves + split_load_moves, - split_load_moves); - } - } - if (buffer_to_inverters_moves > 0) { - repaired = true; - logger_->info(utl::RSZ, - 109, - "Replaced {} buffers with inverters.", - buffer_to_inverters_moves); - } - logger_->metric("design__instance__count__setup_buffer", - buffer_moves + split_load_moves); - if (size_up_moves + size_down_fanout_moves + size_up_match_moves - + vt_swap_moves - > 0) { - repaired = true; - logger_->info(utl::RSZ, - 51, - "Resized {} instances: {} up, {} up match, {} down, {} VT", - size_up_moves + size_up_match_moves + size_down_fanout_moves - + vt_swap_moves, - size_up_moves, - size_up_match_moves, - size_down_fanout_moves, - vt_swap_moves); - } - if (swap_pins_moves > 0) { - repaired = true; - logger_->info( - utl::RSZ, 43, "Swapped pins on {} instances.", swap_pins_moves); - } - if (clone_moves > 0) { - repaired = true; - logger_->info(utl::RSZ, 49, "Cloned {} instances.", clone_moves); - } - if (reroute_moves > 0) { - repaired = true; - logger_->info( - utl::RSZ, 53, "Rerouted {} nets resistance-aware.", reroute_moves); - } - - if (sta::fuzzyLess(setup_context_.final_wns, config_.setup_slack_margin)) { - repaired = true; - logger_->warn(utl::RSZ, 62, "Unable to repair all setup violations."); - } - if (resizer_.overMaxArea()) { - logger_->error(utl::RSZ, 25, "max utilization reached."); - } - - return repaired; -} - GeneratorContext OptimizationPolicy::makeGeneratorContext() const { - return GeneratorContext{.resizer = resizer_, - .committer = committer_, - .run_config = config_, - .policy_config = policy_config_}; + return GeneratorContext{ + .resizer = resizer_, + .committer = committer_, + .run_config = config_, + .policy_config = policy_config_, + .critical_load_pins = &target_collector_->critPathLoadPins()}; } void OptimizationPolicy::buildMoveGenerators( @@ -328,9 +228,6 @@ void OptimizationPolicy::prewarmTargets( = findGenerator(MoveType::kVtSwap) != nullptr; prewarmTargetLibertyCaches( targets, prewarm_swappable_cells, prewarm_vt_equiv_cells); - if (prewarm_swappable_cells || prewarm_vt_equiv_cells) { - prewarmTargetDriverCaches(targets); - } } bool OptimizationPolicy::generatorEnabled(const MoveType) const @@ -353,9 +250,13 @@ void OptimizationPolicy::prepareTarget(Target& target, if ((mask & kGenerationInputCaches) == 0) { return; } + const GeneratorContext context = makeGeneratorContext(); if ((mask & kInputCapBudgetCache) != 0) { prepareInputCapBudgets(target); } + if ((mask & kRebufferInputCache) != 0) { + RebufferMtGenerator::prepareInputs(context, target); + } } void OptimizationPolicy::prepareEstimatorContext(Target& target) const @@ -382,7 +283,8 @@ void OptimizationPolicy::prepareEstimatorContext(Target& target) const policy_config_.dual_rf_eval); } - if ((policy_config_.fanin_side_loss || policy_config_.dual_rf_eval) + if ((policy_config_.fanin_side_loss || policy_config_.dual_rf_eval + || policy_config_.rebuffer_mt_side_loads) && target.estimator_context.has_value()) { // Side paths may not be pushed below the current global worst slack. target.estimator_context->side_slack_floor @@ -474,44 +376,12 @@ void OptimizationPolicy::prewarmTargetLibertyCaches( } } -bool OptimizationPolicy::hasSetupViolations(const OptimizerRunConfig& config, - const sta::MinMax* max) const -{ - const ProfileScope profile("qor_collection"); - return sta::fuzzyLess(resizer_.sta()->worstSlack(max), - config.setup_slack_margin); -} - sta::Slack OptimizationPolicy::totalNegativeSlack() const { const ProfileScope profile("qor_collection"); return target_collector_->totalNegativeSlack(); } -void OptimizationPolicy::prewarmTargetDriverCaches( - const std::vector& targets) const -{ - std::unordered_set pins; - for (const Target& target : targets) { - sta::Instance* inst = target.inst(resizer_); - if (inst == nullptr) { - continue; - } - - std::unique_ptr pin_iter( - network_->pinIterator(inst)); - while (pin_iter->hasNext()) { - pins.insert(pin_iter->next()); - } - } - - // Warm up dbNetwork's net-driver cache on the caller thread so MT workers do - // not populate net_drvr_pin_map_ while checking replacement legality. - for (const sta::Pin* pin : pins) { - static_cast(network_->drivers(pin)); - } -} - void OptimizationPolicy::prewarmStaForPrepareStage() const { sta::dbSta* sta = resizer_.sta(); diff --git a/src/rsz/src/policy/OptimizationPolicy.hh b/src/rsz/src/policy/OptimizationPolicy.hh index 0ad83c26aaa..43b0261c27e 100644 --- a/src/rsz/src/policy/OptimizationPolicy.hh +++ b/src/rsz/src/policy/OptimizationPolicy.hh @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include "DelayEstimator.hh" @@ -55,10 +56,11 @@ char phaseMarkerForIndex(int phase_index); // main thread (legacy path) // // Optimizer (sequencer) creates one or more OptimizationPolicy instances per -// run and drives each through start() -> iterate()* -> converged()/result() in -// that order. start() returns false when the policy determines that the -// optimizer run should stop before iterate(). Policies share run-level setup -// state through RepairSetupContext. +// run and drives each through start() -> iterate() until converged() -> +// finish() in that order. start() returns false when the policy determines +// that the optimizer run should stop before iterate(). Policies share +// run-level setup state through RepairSetupContext, and the Optimizer prints +// the final report of the run after the last phase. // // Shared helpers below (makeGeneratorContext, buildMoveGenerators, // accumulatePrepareRequirements, findGenerator, prepareTargets, @@ -70,19 +72,26 @@ class OptimizationPolicy using GeneratorVector = std::vector>; // === Policy interface ===================================================== + // policy_config carries the policy's tunables; the default is the serial + // policies' values (OptimizationPolicyConfig.hh). OptimizationPolicy(Resizer& resizer, MoveCommitter& committer, RepairSetupContext& setup_context, - const OptimizerRunConfig& config); + const OptimizerRunConfig& config, + OptimizationPolicyConfig policy_config = {}); virtual ~OptimizationPolicy(); virtual const char* name() const = 0; virtual bool start(); virtual void iterate() = 0; + // Called once after the phase converges, before the next phase starts: + // undoes the phase-local setup of start(). + virtual void finish() {} bool converged() const { return converged_; } bool result() const { return result_; } - bool finalizeAndReport(); + // Only startpoint-oriented policies opt in to StTNS progress reporting. + virtual bool showStartpointMetrics() const { return false; } protected: // === Generator setup ====================================================== @@ -119,20 +128,11 @@ class OptimizationPolicy bool prewarm_swappable_cells, bool prewarm_vt_equiv_cells) const; - // Prewarm dbNetwork driver caches touched by replacement legality checks. - void prewarmTargetDriverCaches(const std::vector& targets) const; - // === Run lifecycle helpers =============================================== - bool hasSetupViolations(const OptimizerRunConfig& config, - const sta::MinMax* max) const; // TNS of what the run repairs: the path group's under -path_group. sta::Slack totalNegativeSlack() const; void prewarmStaForPrepareStage() const; std::unique_ptr makeWorkerThreadPool() const; - // Only startpoint-oriented policies opt in to StTNS progress reporting. - virtual bool showStartpointMetrics() const { return false; } - virtual const std::vector& finalReportPins() const; - bool reportRepairSummary() const; // Reset run state at the beginning of start(); call once per run. void resetRun() { @@ -161,7 +161,7 @@ class OptimizationPolicy sta::Graph* graph_{nullptr}; est::EstimateParasitics* estimate_parasitics_{nullptr}; const sta::MinMax* max_{nullptr}; - OptimizationPolicyConfig policy_config_{}; + OptimizationPolicyConfig policy_config_; RepairTargetCollector* target_collector_; // The progress table shared by every policy. ProgressReporter progress_; diff --git a/src/rsz/src/policy/ParallelEvalState.cc b/src/rsz/src/policy/ParallelEvalState.cc new file mode 100644 index 00000000000..83b78e8d2a7 --- /dev/null +++ b/src/rsz/src/policy/ParallelEvalState.cc @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#include "ParallelEvalState.hh" + +#include +#include +#include +#include +#include + +#include "OptimizationPolicyConfig.hh" +#include "RepairSetupProfiler.hh" +#include "sta/Delay.hh" +#include "sta/NetworkClass.hh" + +namespace rsz { + +// === WorkMeter ============================================================== + +void WorkMeter::reset(const size_t graph_vertices) +{ + *this = WorkMeter(); + vertices_ = static_cast(graph_vertices); +} + +void WorkMeter::addTimingUpdate(const size_t pending_nets) +{ + ++timing_updates_; + pending_nets_ += pending_nets; + RepairSetupProfiler::event("work_timing_updates"); + RepairSetupProfiler::event("work_pending_nets", pending_nets); +} + +void WorkMeter::addEstimates(const size_t estimates) +{ + estimates_ += estimates; + RepairSetupProfiler::event("work_estimates", estimates); +} + +void WorkMeter::addPlanRequest() +{ + ++plan_requests_; + RepairSetupProfiler::event("work_plan_requests"); +} + +double WorkMeter::seconds(const ParallelEvalPolicyConfig& config) const +{ + return config.work_sta_per_vertex * vertices_ + * static_cast(timing_updates_) + + config.work_per_pending_net * static_cast(pending_nets_) + + config.work_per_estimate * static_cast(estimates_) + + config.work_per_plan * static_cast(plan_requests_) + + config.work_per_endpoint * static_cast(endpoints_); +} + +// === StallMonitor =========================================================== + +void StallMonitor::reset(const sta::Slack initial_wns, + const sta::Slack initial_tns) +{ + initial_wns_ = initial_wns; + initial_tns_ = initial_tns; + stalled_batches_ = 0; + stalled_moves_ = 0; + last_batch_moves_ = 0; + wns_stall_batches_ = 0; + window_.clear(); + window_stalled_ = false; +} + +void StallMonitor::onMissed() +{ + ++stalled_batches_; + stalled_moves_ += last_batch_moves_; + last_batch_moves_ = 0; +} + +void StallMonitor::onSaved(const sta::Slack prev_tns, + const sta::Slack best_wns, + const sta::Slack wns, + const sta::Slack tns, + const double work) +{ + // Progress must be a visible part of the initial WNS or TNS; otherwise tiny + // gains keep paying full timing updates. + const float tns_step = std::abs(sta::delayAsFloat(initial_tns_)) + * config_.progress_pct / 100.0f; + const float wns_step = std::abs(sta::delayAsFloat(initial_wns_)) + * config_.progress_wns_pct / 100.0f; + const bool wns_progressed + = sta::delayAsFloat(wns) - sta::delayAsFloat(best_wns) > wns_step; + const bool tns_progressed + = sta::delayAsFloat(tns) - sta::delayAsFloat(prev_tns) > tns_step; + const bool progressed = wns_progressed || tns_progressed; + updateWindow(wns, tns_progressed, work); + + // Once the batches stop moving WNS, the score order alone will not reach + // the worst path again, so the next batches reserve slots for it. + wns_stall_batches_ = wns_progressed ? 0 : wns_stall_batches_ + 1; + // A salvaged batch without progress counts twice, at its miss and here, + // so a run of misses ends the batches sooner. + stalled_batches_ = progressed ? 0 : stalled_batches_ + 1; + stalled_moves_ = progressed ? 0 : stalled_moves_ + last_batch_moves_; + last_batch_moves_ = 0; + RepairSetupProfiler::event("stall_moves_after_save", stalled_moves_); +} + +bool StallMonitor::weightsPriority() const +{ + return wns_stall_batches_ > 0 + && wns_stall_batches_ <= config_.priority.weight_max_wns_stall_batches; +} + +StallMonitor::Stall StallMonitor::stall() const +{ + if (stalled_moves_ >= moveBudget()) { + return Stall::kMoves; + } + if (stalled_batches_ >= config_.stall_max_batches) { + return Stall::kBatches; + } + return Stall::kNone; +} + +int StallMonitor::moveBudget() const +{ + return config_.stall_batches * std::max(config_.min_batch_cap, last_cap_); +} + +void StallMonitor::updateWindow(const sta::Slack wns, + const bool tns_progressed, + const double work) +{ + const int window = config_.stall_window; + window_.push_back({.wns = sta::delayAsFloat(wns), .fep = fep_, .work = work}); + if (static_cast(window_.size()) <= window) { + return; + } + + window_.pop_front(); + if (tns_progressed) { + return; + } + + // Cheap batches are never cut; the rule is for runs whose timing updates + // cost real work. + if (window_.back().work - window_.front().work + < config_.stall_window_min_work) { + return; + } + + // The average gain per saved batch over the window, relative to the current + // value. The window holds `window` checkpoints, so window - 1 intervals. + const SavedCheckpoint& first = window_.front(); + const SavedCheckpoint& last = window_.back(); + const float rel = config_.stall_rel_pct / 100.0f; + const float intervals = static_cast(window_.size() - 1); + const float wns_gain = (last.wns - first.wns) / intervals; + const float fep_gain + = static_cast(first.fep) - static_cast(last.fep); + const bool wns_moving = wns_gain >= rel * std::abs(last.wns); + const bool fep_moving + = fep_gain / intervals >= rel * static_cast(last.fep); + if (!wns_moving && !fep_moving) { + window_stalled_ = true; + RepairSetupProfiler::event("stall_window_triggered"); + } +} + +// === WnsTail ================================================================ + +void WnsTail::reset() +{ + active_ = false; + started_ = false; + batches_ = 0; + batches_without_gain_ = 0; + endpoints_ = 0; + max_batch_ = 0; + batch_cap_ = 0; + work_start_ = 0.0; + budget_work_ = 0.0; + speculative_batches_ = 0; + speculative_instances_.clear(); + greedy_stalled_ = false; +} + +bool WnsTail::start(const double work) +{ + if (config_.wns_tail_endpoints <= 0 || started_) { + return false; + } + + reset(); + active_ = true; + started_ = true; + endpoints_ = config_.wns_tail_endpoints; + max_batch_ = config_.wns_tail_batch; + batch_cap_ = 1; + work_start_ = work; + + // The tail budget is a percentage of the batch stage's work, with a floor. + budget_work_ = std::max(work_start_ * config_.wns_tail_work_pct / 100.0, + static_cast(config_.wns_tail_min_work)); + return true; +} + +void WnsTail::afterBatch(const sta::Slack best_wns_before, + const sta::Slack best_wns_after, + const bool batch_saved) +{ + ++batches_; + + // Only a visible WNS gain counts in the tail, not TNS-only saves or tiny + // gains. + const float tail_step = std::abs(sta::delayAsFloat(best_wns_before)) + * config_.wns_tail_min_gain_pct / 100.0f; + const bool tail_progressed + = sta::delayAsFloat(best_wns_after) - sta::delayAsFloat(best_wns_before) + > tail_step; + + // A run of speculative batches is judged when it ends. + if (speculative_batches_ == 0) { + batches_without_gain_ = tail_progressed ? 0 : batches_without_gain_ + 1; + } + + // A saved batch earns a larger next batch; a miss drops back to single + // moves. + batch_cap_ = batch_saved ? std::min(max_batch_, batch_cap_ * 2) : 1; +} + +WnsTail::Step WnsTail::nextStep(const int committed_moves, + const double work, + const char*& reason) const +{ + // The first rule that fires is the reason. + const char* const kNoMoves = "wns_tail_no_committed_moves"; + const char* const kStalled = "wns_tail_stalled"; + const char* const kMaxBatches = "wns_tail_max_batches"; + const char* const kWorkBudget = "wns_tail_work_budget"; + reason = nullptr; + if (committed_moves == 0) { + reason = kNoMoves; + } else if (batches_without_gain_ >= config_.wns_tail_stall) { + reason = kStalled; + } else if (batches_ >= config_.wns_tail_max_batches) { + reason = kMaxBatches; + } else if (work - work_start_ > budget_work_) { + reason = kWorkBudget; + } + + if (reason == nullptr) { + return Step::kContinue; + } + + // The work budget (when it is the reason) and the batch limit end the tail; + // the stage rules below may only widen it. + if (reason == kWorkBudget || batches_ >= config_.wns_tail_max_batches) { + return Step::kStop; + } + + // The greedy stage is stuck at this width: retry the same endpoints with + // speculation before widening, as the serial loop does with non-improving + // passes. + if (!greedy_stalled_ && reason == kStalled) { + return Step::kSpeculate; + } + + // Nothing more on these endpoints; widen the tail before giving up. + if (endpoints_ < config_.wns_tail_max_endpoints) { + return Step::kWiden; + } + + return Step::kStop; +} + +void WnsTail::widen() +{ + // The caller's journal restore drops an open speculative run. + speculative_batches_ = 0; + speculative_instances_.clear(); + endpoints_ = std::min(config_.wns_tail_max_endpoints, + endpoints_ * config_.wns_tail_grow); + max_batch_ *= config_.wns_tail_grow; + batches_without_gain_ = 0; + greedy_stalled_ = true; +} + +bool WnsTail::maySpeculate() const +{ + // Speculation starts only once a greedy tail stage has stalled. + return speculative_batches_ < config_.wns_tail_speculate && greedy_stalled_; +} + +void WnsTail::keepSpeculative(const std::vector& instances) +{ + ++speculative_batches_; + speculative_instances_.insert( + speculative_instances_.end(), instances.begin(), instances.end()); +} + +std::vector WnsTail::endSpeculation() +{ + speculative_batches_ = 0; + return std::exchange(speculative_instances_, {}); +} + +// === MissTabu =============================================================== + +void MissTabu::reset() +{ + until_.clear(); + undo_counts_.clear(); + excluded_.clear(); +} + +void MissTabu::addTabu(sta::Instance* inst, const int now) +{ + until_[inst] = now + config_.miss_tabu_batches; +} + +bool MissTabu::recordUndo(sta::Instance* inst, const int now) +{ + addTabu(inst, now); + if (++undo_counts_[inst] >= config_.max_undos_per_instance) { + excluded_.insert(inst); + return true; + } + return false; +} + +void MissTabu::expire(const int now) +{ + std::erase_if(until_, + [now](const std::pair& entry) { + return entry.second <= now; + }); +} + +} // namespace rsz diff --git a/src/rsz/src/policy/ParallelEvalState.hh b/src/rsz/src/policy/ParallelEvalState.hh new file mode 100644 index 00000000000..455ab210887 --- /dev/null +++ b/src/rsz/src/policy/ParallelEvalState.hh @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "OptimizationPolicyConfig.hh" +#include "sta/Delay.hh" +#include "sta/NetworkClass.hh" + +namespace rsz { + +// Run state of SetupParallelEvalPolicy. These classes hold counters and +// decisions only; the policy owns the journal, the STA and the generators. + +// The work the run spent, in reference seconds (see "Work meter" in +// ParallelEvalPolicyConfig). The stall rules read it instead of the wall +// clock, so every machine makes the same decisions. +class WorkMeter +{ + public: + // Start reference-work accounting for this timing graph. + void reset(size_t graph_vertices); + // One parasitics-and-timing update or journal restore. + void addTimingUpdate(size_t pending_nets); + // Charge candidate evaluations to the deterministic work budget. + void addEstimates(size_t estimates); + // Charge one buffer-tree planning request to the work budget. + void addPlanRequest(); + void addEndpoints(size_t endpoints) { endpoints_ += endpoints; } + // Convert accumulated counts to reference seconds using policy weights. + double seconds(const ParallelEvalPolicyConfig& config) const; + + private: + double vertices_{0.0}; + uint64_t timing_updates_{0}; + uint64_t pending_nets_{0}; + uint64_t estimates_{0}; + uint64_t plan_requests_{0}; + uint64_t endpoints_{0}; +}; + +// Stall detection of the batch stage. A saved checkpoint is progress when +// it moves WNS or TNS by a visible part of its initial value. +class StallMonitor +{ + public: + enum class Stall + { + kNone, + kMoves, + kBatches + }; + + explicit StallMonitor(const ParallelEvalPolicyConfig& config) + : config_(config) + { + } + + // Set the initial QoR against which progress thresholds are measured. + void reset(sta::Slack initial_wns, sta::Slack initial_tns); + // A new batch: its cap and the failing endpoint count it started with. + void beginBatch(int batch_cap, size_t fep) + { + last_cap_ = batch_cap; + fep_ = fep; + } + void endBatch(int committed_moves) { last_batch_moves_ = committed_moves; } + // A missed batch spent its moves without progress. A later salvage of the + // same batch resets the budget only if it progresses. + void onMissed(); + // Update progress and the sliding stall window after a saved checkpoint. + void onSaved(sta::Slack prev_tns, + sta::Slack best_wns, + sta::Slack wns, + sta::Slack tns, + double work); + + // The last saved batch did not move WNS past the best checkpoint. + bool wnsStalled() const { return wns_stall_batches_ > 0; } + // The cone weight ranks targets only after a saved batch left WNS + // unchanged, and for a limited number of such batches. + bool weightsPriority() const; + bool windowStalled() const { return window_stalled_; } + // The stall budget: too many moves or batches without progress. + Stall stall() const; + // Moves the batches may spend without progress: stall_batches full batches + // at the current cap, so one grown batch cannot use it all. + int moveBudget() const; + + private: + // QoR and work at one saved checkpoint of the stall window. + struct SavedCheckpoint + { + float wns{0.0f}; + size_t fep{0}; + double work{0.0}; + }; + + // Test whether recent saved checkpoints made enough QoR progress. + void updateWindow(sta::Slack wns, bool tns_progressed, double work); + + const ParallelEvalPolicyConfig& config_; + sta::Slack initial_wns_{0.0f}; + sta::Slack initial_tns_{0.0f}; + // Batches without progress and the moves they spent. + int stalled_batches_{0}; + int stalled_moves_{0}; + int last_batch_moves_{0}; + int last_cap_{0}; + // Saved batches in a row that did not move WNS. + int wns_stall_batches_{0}; + // Failing endpoints at the last collection and the last saved checkpoints. + size_t fep_{0}; + std::deque window_; + bool window_stalled_{false}; +}; + +// The WNS tail: after the batches stall, the run works on the worst endpoints +// with small batches. A stalled stage first retries with speculation, then +// widens; a work budget bounds the whole tail. +class WnsTail +{ + public: + enum class Step + { + kContinue, + kSpeculate, + kWiden, + kStop + }; + + explicit WnsTail(const ParallelEvalPolicyConfig& config) : config_(config) {} + + // Clear all tail and speculative state for a new policy run. + void reset(); + // Starts the tail once; false when it is off or already ran. + bool start(double work); + void stop() { active_ = false; } + bool active() const { return active_; } + int endpoints() const { return endpoints_; } + int batchCap() const { return batch_cap_; } + int batches() const { return batches_; } + double workStart() const { return work_start_; } + double workBudget() const { return budget_work_; } + + // Bookkeeping after a batch: the stall counter and the next batch cap. + void afterBatch(sta::Slack best_wns_before, + sta::Slack best_wns_after, + bool batch_saved); + // The tail's decision after a batch; `reason` names the rule that fired. + Step nextStep(int committed_moves, double work, const char*& reason) const; + // Retry the stalled endpoints with speculation. + void beginSpeculativeStage() + { + batches_without_gain_ = 0; + greedy_stalled_ = true; + } + // Grow the endpoint set and the batch cap. + // Increase the endpoint and batch-cap limits after a stalled stage. + void widen(); + + // Missed batches kept in the open journal since the last checkpoint. + bool maySpeculate() const; + void keepSpeculative(const std::vector& instances); + bool speculating() const { return speculative_batches_ > 0; } + int speculativeBatches() const { return speculative_batches_; } + // Ends the speculative run and returns the instances it committed. + std::vector endSpeculation(); + + private: + const ParallelEvalPolicyConfig& config_; + bool active_{false}; + bool started_{false}; + // Batches so far and batches without a better WNS. + int batches_{0}; + int batches_without_gain_{0}; + // Endpoints and batch caps of the stage; the cap doubles after a saved + // batch and drops back to one move after a miss. + int endpoints_{0}; + int max_batch_{0}; + int batch_cap_{0}; + // Work when the tail started and the work it may spend. + double work_start_{0.0}; + double budget_work_{0.0}; + // Missed tail batches retained until a later checkpoint or rollback. + int speculative_batches_{0}; + std::vector speculative_instances_; + bool greedy_stalled_{false}; +}; + +// Instances the batches skip. An undone batch puts its instances on the +// tabu for a few batches; an instance in too many undone batches is excluded +// for the rest of the run. +class MissTabu +{ + public: + explicit MissTabu(const ParallelEvalPolicyConfig& config) : config_(config) {} + + // Clear the tabu and the exclusions for a new policy run. + void reset(); + // Skip `inst` for miss_tabu_batches batches after batch `now`. + void addTabu(sta::Instance* inst, int now); + // addTabu() plus the undo count. True once `inst` was in + // max_undos_per_instance undone batches and is excluded for good. + bool recordUndo(sta::Instance* inst, int now); + // End the tabu of the instances whose tabu ends at batch `now`. + void expire(int now); + // Drop the tabu; the exclusions stay. + void clearTemporary() { until_.clear(); } + bool isExcluded(sta::Instance* inst) const + { + return !excluded_.empty() && excluded_.contains(inst); + } + bool isTabu(sta::Instance* inst) const + { + return !until_.empty() && until_.contains(inst); + } + + private: + const ParallelEvalPolicyConfig& config_; + // Batch index until which each instance is temporarily skipped. + std::unordered_map until_; + // Number of missed batches containing each instance. + std::unordered_map undo_counts_; + // Instances excluded for the remainder of the policy run. + std::unordered_set excluded_; +}; + +} // namespace rsz diff --git a/src/rsz/src/policy/ProgressReporter.cc b/src/rsz/src/policy/ProgressReporter.cc index 739aa464140..9074c78ceff 100644 --- a/src/rsz/src/policy/ProgressReporter.cc +++ b/src/rsz/src/policy/ProgressReporter.cc @@ -152,7 +152,7 @@ void ProgressReporter::printFinal(RepairTargetCollector& collector, resizer_.logger()->report(kRule); } -double ProgressReporter::areaGrowthPercent() const +double ProgressReporter::designArea() const { // computeDesignArea() walks every instance, and most rows follow passes that // changed nothing. The committer's edit count only grows, so a new count @@ -163,13 +163,18 @@ double ProgressReporter::areaGrowthPercent() const context_.progress_design_area = resizer_.computeDesignArea(); context_.progress_area_at_edit = edits; } + return context_.progress_design_area; +} +double ProgressReporter::areaGrowthPercent() const +{ + const double design_area = designArea(); const double initial_area = context_.initial_design_area; if (std::abs(initial_area) <= 0.0) { return std::numeric_limits::infinity(); } - return (context_.progress_design_area - initial_area) / initial_area * 100.0; + return (design_area - initial_area) / initial_area * 100.0; } } // namespace rsz diff --git a/src/rsz/src/policy/ProgressReporter.hh b/src/rsz/src/policy/ProgressReporter.hh index 449d0789890..22b46d37a8c 100644 --- a/src/rsz/src/policy/ProgressReporter.hh +++ b/src/rsz/src/policy/ProgressReporter.hh @@ -37,10 +37,13 @@ class ProgressReporter // header yet, show_startpoint_metrics picks the layout. void printFinal(RepairTargetCollector& collector, bool show_startpoint_metrics) const; + // The design area at the committer's current edit count: computeDesignArea() + // walks every instance, so the area is walked once per edit and shared with + // the utilization checks of the policies. + double designArea() const; private: - // Recomputed only when the committer's edit count changed, because - // computeDesignArea() walks every instance. + // The area growth since the run started, from designArea(). double areaGrowthPercent() const; Resizer& resizer_; diff --git a/src/rsz/src/policy/SetupCritVtSwapPolicy.cc b/src/rsz/src/policy/SetupCritVtSwapPolicy.cc index f9584c194df..d53165fdb89 100644 --- a/src/rsz/src/policy/SetupCritVtSwapPolicy.cc +++ b/src/rsz/src/policy/SetupCritVtSwapPolicy.cc @@ -66,7 +66,7 @@ void SetupCritVtSwapPolicy::iterate() } const ProfileIteration iteration("crit_instance_sweep"); - committer_.capturePrePhaseSlack(); + committer_.tracker().capturePrePhaseSlack(); if (swapVTCritCells(num_viols)) { resizer_.updateParasiticsAndTiming(); } diff --git a/src/rsz/src/policy/SetupDirectionalPolicy.cc b/src/rsz/src/policy/SetupDirectionalPolicy.cc index 40b5186c74e..781470a5d2a 100644 --- a/src/rsz/src/policy/SetupDirectionalPolicy.cc +++ b/src/rsz/src/policy/SetupDirectionalPolicy.cc @@ -84,7 +84,7 @@ void SetupDirectionalPolicy::repairSetupDirectional( fmt::format("{}{} Phase Time: {{}}", phase_name, phase_marker)); const char* point_type = use_startpoints ? "startpoint" : "endpoint"; const char* point_type_cap = use_startpoints ? "Startpoint" : "Endpoint"; - committer_.capturePrePhaseSlack(); + committer_.tracker().capturePrePhaseSlack(); const float margin_percentages[] = {0.20f, 0.50f, 1.00f}; const sta::Slack diminishing_returns_threshold(1e-11f); diff --git a/src/rsz/src/policy/SetupDirectionalPolicy.hh b/src/rsz/src/policy/SetupDirectionalPolicy.hh index ca3e2308f14..ee158510631 100644 --- a/src/rsz/src/policy/SetupDirectionalPolicy.hh +++ b/src/rsz/src/policy/SetupDirectionalPolicy.hh @@ -25,8 +25,6 @@ class SetupDirectionalPolicy : public SetupLegacyBase const char* name() const override { return "SetupDirectionalPolicy"; } void iterate() override; - - protected: bool showStartpointMetrics() const override { return use_startpoints_; } private: diff --git a/src/rsz/src/policy/SetupLastGaspPolicy.cc b/src/rsz/src/policy/SetupLastGaspPolicy.cc index 02a52f8f870..900935cc779 100644 --- a/src/rsz/src/policy/SetupLastGaspPolicy.cc +++ b/src/rsz/src/policy/SetupLastGaspPolicy.cc @@ -40,7 +40,7 @@ void SetupLastGaspPolicy::iterate() return; } - committer_.capturePrePhaseSlack(); + committer_.tracker().capturePrePhaseSlack(); int& num_viols = setup_context_.violation_count; const char phase_marker = phaseMarkerForIndex(setup_context_.phase_index); { diff --git a/src/rsz/src/policy/SetupLegacyBase.cc b/src/rsz/src/policy/SetupLegacyBase.cc index 4ddd0d9412b..a8b9431796a 100644 --- a/src/rsz/src/policy/SetupLegacyBase.cc +++ b/src/rsz/src/policy/SetupLegacyBase.cc @@ -384,8 +384,8 @@ bool SetupLegacyBase::prepareForPhasePipeline() } if (committer_.moveTrackerEnabled()) { - committer_.captureInitialSlackDistribution(); - committer_.captureOriginalEndpointSlack(); + committer_.tracker().captureInitialSlackDistribution(); + committer_.tracker().captureOriginalEndpointSlack(); } if (!config_.phases.empty()) { reportCustomPhaseSetup(); @@ -675,7 +675,8 @@ bool SetupLegacyBase::tryCandidateSequence( const ProfileScope profile("candidate_generation"); candidates = generator.generate(target); if (candidates.empty()) { - RepairSetupProfiler::event("skip_no_candidates", 1, generator.name()); + RepairSetupProfiler::event( + "skip_no_candidates", 1, moveName(generator.type())); } // Generation can return candidates that the serial pass never estimates. @@ -758,11 +759,11 @@ bool SetupLegacyBase::tryRepairTarget( MoveGenerator& generator = *generator_ptr; const MoveType type = generator.type(); if (rejected_types != nullptr && rejected_types->contains(type)) { - RepairSetupProfiler::event("skip_rejected_move", 1, generator.name()); + RepairSetupProfiler::event("skip_rejected_move", 1, moveName(type)); continue; } if (!generator.isApplicable(live_target)) { - RepairSetupProfiler::event("skip_not_applicable", 1, generator.name()); + RepairSetupProfiler::event("skip_not_applicable", 1, moveName(type)); continue; } diff --git a/src/rsz/src/policy/SetupLegacyPolicy.cc b/src/rsz/src/policy/SetupLegacyPolicy.cc index 7f03f0c42bb..abe3dceca3d 100644 --- a/src/rsz/src/policy/SetupLegacyPolicy.cc +++ b/src/rsz/src/policy/SetupLegacyPolicy.cc @@ -36,7 +36,7 @@ static constexpr int kDelayDigits = 3; void SetupLegacyPolicy::iterate() { - committer_.capturePrePhaseSlack(); + committer_.tracker().capturePrePhaseSlack(); buildMainMoveSequence(/*log_sequence=*/false); MainRepairState main_state; diff --git a/src/rsz/src/policy/SetupParallelEvalPolicy.cc b/src/rsz/src/policy/SetupParallelEvalPolicy.cc new file mode 100644 index 00000000000..d727308dca0 --- /dev/null +++ b/src/rsz/src/policy/SetupParallelEvalPolicy.cc @@ -0,0 +1,1681 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#include "SetupParallelEvalPolicy.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "BatchSelector.hh" +#include "MoveCandidate.hh" +#include "MoveCommitter.hh" +#include "MoveGenerator.hh" +#include "OptimizerTypes.hh" +#include "PathGroupFilter.hh" +#include "RebufferMtCandidate.hh" +#include "RebufferMtGenerator.hh" +#include "RepairSetupProfiler.hh" +#include "RepairTargetCollector.hh" +#include "SizeUpMtCandidate.hh" +#include "SizeUpMtGenerator.hh" +#include "VtSwapMtCandidate.hh" +#include "VtSwapMtGenerator.hh" +#include "db_sta/dbNetwork.hh" +#include "odb/db.h" +#include "rsz/Resizer.hh" +#include "sta/Fuzzy.hh" +#include "sta/Graph.hh" +#include "sta/GraphClass.hh" +#include "sta/Network.hh" +#include "utl/Logger.h" +#include "utl/ThreadPool.h" + +namespace rsz { + +SetupParallelEvalPolicy::SetupParallelEvalPolicy( + Resizer& resizer, + MoveCommitter& committer, + RepairSetupContext& setup_context, + const OptimizerRunConfig& config) + : OptimizationPolicy( + resizer, + committer, + setup_context, + config, + OptimizationPolicyConfig::forParallelEval(config.effort)), + stall_(policy_config_.parallel_eval), + tail_(policy_config_.parallel_eval), + tabu_(policy_config_.parallel_eval) +{ + is_experimental = true; +} + +namespace { + +// The wall time of one worker task for the profiler; a null sink measures +// nothing. +class WorkerTimer +{ + public: + explicit WorkerTimer(double* elapsed) : elapsed_(elapsed) + { + if (elapsed_ != nullptr) { + start_ = std::chrono::steady_clock::now(); + } + } + ~WorkerTimer() + { + if (elapsed_ != nullptr) { + *elapsed_ = std::chrono::duration(std::chrono::steady_clock::now() + - start_) + .count(); + } + } + + private: + double* elapsed_; + std::chrono::steady_clock::time_point start_; +}; + +// The PARALLEL_EVAL form of each move type. +std::unique_ptr makeParallelEvalGenerator( + const MoveType type, + const GeneratorContext& context) +{ + switch (type) { + case MoveType::kSizeUp: + return std::make_unique(context); + case MoveType::kBuffer: + return std::make_unique(context); + case MoveType::kVtSwap: + return std::make_unique(context); + case MoveType::kSwapPins: + case MoveType::kSplitLoad: + case MoveType::kSizeDownFanout: + case MoveType::kUnbuffer: + case MoveType::kClone: + case MoveType::kSizeUpMatch: + case MoveType::kReroute: + case MoveType::kBufferToInverters: + case MoveType::kCount: + break; + } + + return nullptr; +} + +} // namespace + +SetupParallelEvalPolicy::~SetupParallelEvalPolicy() = default; + +bool SetupParallelEvalPolicy::start() +{ + OptimizationPolicy::start(); + target_collector_->setRecordCritPathLoadPins(policy_config_.fanin_side_loss); + + selectMoveSequence(); + buildMoveGenerators(move_sequence_, makeGeneratorContext()); + bottleneck_ = std::make_unique(sta_, max_); + // The reserve follows the margin the targets are collected with. The + // selector copies its config, so the margin goes in first. + policy_config_.parallel_eval.selector.slack_margin + = config_.setup_slack_margin; + batch_selector_ + = std::make_unique(policy_config_.parallel_eval.selector); + resetRunState(); + + // The worker threads also run the collector's endpoint scans. + thread_pool_ = makeWorkerThreadPool(); + target_collector_->setScanThreadPool(thread_pool_.get()); + + // Collect the endpoints before the first QoR sample, so the sample follows + // the same required-time update as in LEGACY. + target_collector_->init(config_.setup_slack_margin); + + // -repair_tns below 100% limits every batch to as many worst endpoints as + // LEGACY would repair now, and at least one. + max_end_repairs_ = std::numeric_limits::max(); + if (config_.repair_tns_end_percent < 1.0) { + max_end_repairs_ = static_cast( + std::max(static_cast(target_collector_->getMaxEndpointCount() + * config_.repair_tns_end_percent), + 1)); + } + + saveInitialCheckpoint(); + committer_.tracker().captureInitialSlackDistribution(); + committer_.tracker().captureOriginalEndpointSlack(); + // The generators set up their state for the phase's worker threads. + for (const std::unique_ptr& generator : move_generators_) { + generator->startPhase(thread_pool_->threadCount()); + } + progress_.printHeader(showStartpointMetrics()); + + return true; +} + +void SetupParallelEvalPolicy::selectMoveSequence() +{ + // -sequence is the move set; without it the default runs. The candidates + // of a target compete on score, and the sequence order only breaks exact + // ties; SizeUpMt scores carry the 0.75 size_up_mt_score_scale. The default + // goes through the same filter, so -skip_vt_swap and a single-VT library + // still remove VtSwapMt. + std::vector requested_sequence = config_.sequence; + if (requested_sequence.empty()) { + requested_sequence + = {MoveType::kVtSwap, MoveType::kSizeUp, MoveType::kBuffer}; + } + + move_sequence_.clear(); + for (const MoveType type : requested_sequence) { + if (!moveEnabled(type)) { + continue; + } + + // BufferToInverters has no gain estimate to rank it. Clone, SizeUpMatch + // and Reroute have no Mt form; SwapPins, SplitLoad, Unbuffer and + // SizeDownFanout have none yet. + if (type == MoveType::kBufferToInverters || type == MoveType::kClone + || type == MoveType::kSizeUpMatch || type == MoveType::kReroute + || type == MoveType::kSwapPins || type == MoveType::kSplitLoad + || type == MoveType::kUnbuffer || type == MoveType::kSizeDownFanout) { + logger_->warn(utl::RSZ, + 2028, + "PARALLEL_EVAL has no form of move '{}'; it is skipped.", + moveName(type)); + continue; + } + + move_sequence_.push_back(type); + } +} + +void SetupParallelEvalPolicy::resetRunState() +{ + committed_moves_ = 0; + batch_index_ = 0; + tentative_moves_ = 0; + checkpoint_misses_ = 0; + small_batch_retry_started_ = false; + max_utilization_warned_ = false; + batch_journal_open_ = false; + batch_instances_.clear(); + batch_candidates_.clear(); + rebuffer_targets_in_batch_ = 0; + tail_.reset(); + tabu_.reset(); +} + +void SetupParallelEvalPolicy::saveInitialCheckpoint() +{ + const BatchQor qor = measureQor(); + checkpoint_wns_ = qor.wns; + checkpoint_tns_ = qor.tns; + best_checkpoint_wns_ = checkpoint_wns_; + stall_.reset(checkpoint_wns_, checkpoint_tns_); + work_.reset(graph_->vertexCount()); + if (RepairSetupProfiler::enabled()) { + RepairSetupProfiler::current()->metadata( + "graph_vertices", std::to_string(graph_->vertexCount())); + } + + RepairSetupProfiler::qor( + "checkpoint_initial", checkpoint_wns_, checkpoint_tns_); +} + +void SetupParallelEvalPolicy::buildMoveGenerators( + const std::vector& move_types, + const GeneratorContext& context) +{ + move_generators_.clear(); + move_generators_.reserve(move_types.size()); + for (const MoveType type : move_types) { + std::unique_ptr generator + = makeParallelEvalGenerator(type, context); + if (generator != nullptr) { + move_generators_.push_back(std::move(generator)); + } + } +} + +void SetupParallelEvalPolicy::iterate() +{ + if (converged_) { + return; + } + + const ProfileIteration iteration("batch_iter"); + tabu_.expire(batch_index_); + if (finishIfNoSetupViolations() || finishAtMaxArea()) { + return; + } + + // Collect, weigh, evaluate: the pool comes back with its batch selected, + // because the chunk loop needs the selection to know when to stop. + const BatchScope scope = collectTargetEndpoints(); + analyzeBottlenecks(scope); + // Design wide even under -path_group: the side pins of a missed batch may + // lie on any group's paths. + const sta::Slack pre_batch_wns = sta_->worstSlack(max_); + BatchPool pool = evaluateTargets(scope); + if (pool.targets.empty()) { + finishWithoutTargets(); + return; + } + + // Commit, judge, stop. + openBatchJournal(); + int batch_moves = commitBatch(pool.evaluations, pool.selection); + if (batch_moves > 0) { + updateTiming(); + } + ++batch_index_; + + const sta::Slack best_wns_before = best_checkpoint_wns_; + const bool batch_saved = judgeBatch(pool, pre_batch_wns, batch_moves); + if (tail_.active()) { + tail_.afterBatch(best_wns_before, best_checkpoint_wns_, batch_saved); + } + + applyStopRules(batch_moves); + printTrackerBatchSummary(); +} + +bool SetupParallelEvalPolicy::judgeBatch(BatchPool& pool, + const sta::Slack pre_batch_wns, + int& batch_moves) +{ + const BatchQor after = measureQor(); + RepairSetupProfiler::qor("batch_after", after.wns, after.tns); + stall_.endBatch(batch_moves); + const bool utilization_legal = withinMaxUtilization(); + if (utilization_legal && acceptsBatch(after.wns, after.tns)) { + if (!sta::fuzzyLess(checkpoint_wns_, after.wns)) { + RepairSetupProfiler::event("checkpoints_saved_wns_tolerance"); + } + + saveBatchCheckpoint(after.wns, after.tns); + return true; + } + + noteMissedBatch(utilization_legal); + if (salvageMissedBatch( + pool.evaluations, pool.selection, pre_batch_wns, batch_moves)) { + return true; + } + + settleMissedBatch(batch_moves); + return false; +} + +bool SetupParallelEvalPolicy::finishIfNoSetupViolations() +{ + // The verdict's slack decides. Under -path_group a clean group ends the run + // here; the empty target list would roll back the batch that cleaned it. + const BatchQor qor = measureQor(); + if (sta::fuzzyLess(qor.wns, config_.setup_slack_margin)) { + return false; + } + + if (batch_journal_open_) { + saveBatchCheckpoint(qor.wns, qor.tns); + } + + RepairSetupProfiler::stop("no_setup_violations"); + markRunComplete(true); + return true; +} + +bool SetupParallelEvalPolicy::finishAtMaxArea() +{ + // Like LEGACY, nothing runs at the -max_utilization area. The verdict keeps + // batches below it, so only a design that starts there stops here. + if (!resizer_.overMaxArea()) { + return false; + } + + RepairSetupProfiler::stop("max_area"); + markRunComplete(false); + return true; +} + +void SetupParallelEvalPolicy::finishWithoutTargets() +{ + RepairSetupProfiler::stop("no_targets"); + restoreBatchCheckpoint(); + markRunComplete(false); +} + +void SetupParallelEvalPolicy::analyzeBottlenecks(const BatchScope& scope) +{ + // The weights apply only while the saved batches stall WNS; after a pause + // the next analysis starts fresh. + if (!stall_.weightsPriority()) { + bottleneck_age_ = std::numeric_limits::max(); + return; + } + + // One analysis serves bottleneck_refresh_batches batches; the tail's narrower + // endpoint set gets a fresh one. + const BatchPriorityConfig& priority = policy_config_.parallel_eval.priority; + const bool tail_changed = tail_.active() != bottleneck_in_tail_; + if (bottleneck_age_ < std::numeric_limits::max()) { + ++bottleneck_age_; + } + if (!tail_changed && bottleneck_age_ < priority.bottleneck_refresh_batches) { + return; + } + bottleneck_age_ = 0; + bottleneck_in_tail_ = tail_.active(); + + const ProfileScope profile("bottleneck_analysis"); + const size_t endpoint_limit + = std::min(static_cast(priority.bottleneck_max_endpoints), + scope.target_endpoints); + bottleneck_->analyze(target_collector_->getViolatingEndpoints(), + static_cast(config_.setup_slack_margin), + static_cast(endpoint_limit), + priority.bottleneck_max_sources); + RepairSetupProfiler::event("bottleneck_analyses"); + RepairSetupProfiler::event("bottleneck_cone_vertices", + bottleneck_->coneSize()); +} + +SetupParallelEvalPolicy::BatchScope +SetupParallelEvalPolicy::collectTargetEndpoints() +{ + rebuffer_targets_in_batch_ = 0; + + { + const ProfileScope profile("init_per_iter"); + committer_.tracker().capturePrePhaseSlack(); + if (batch_index_ > 0) { + prewarmStaForPrepareStage(); + } + } + + target_collector_->init(config_.setup_slack_margin); + + // The row shows the state this collection saw: after batch_index_ + // batches. As in LEGACY, -verbose prints every row; without it only the + // first row and the final one appear. + if (config_.verbose || batch_index_ == 0) { + progress_.printRow( + fmt::format("{}{}", + batch_index_, + phaseMarkerForIndex(setup_context_.phase_index)), + *target_collector_, + nullptr, + showStartpointMetrics(), + /*worst_by_startpoint=*/false); + } + const size_t fep_count = target_collector_->getViolatingEndpoints().size(); + + // -repair_tns and the WNS tail visit only the worst endpoints, so the batch + // and the evaluation chunks are sized on that subset. + size_t target_endpoints = std::min(fep_count, max_end_repairs_); + if (tail_.active()) { + target_endpoints + = std::min(target_endpoints, static_cast(tail_.endpoints())); + } + + const int batch_cap = effectiveBatchCap(target_endpoints); + stall_.beginBatch(batch_cap, fep_count); + RepairSetupProfiler::event("batch_fep_at_collect", fep_count); + RepairSetupProfiler::event("batch_effective_cap", batch_cap); + + return {.target_endpoints = target_endpoints, .batch_cap = batch_cap}; +} + +SetupParallelEvalPolicy::BatchPool SetupParallelEvalPolicy::evaluateTargets( + const BatchScope& scope) +{ + // Why chunks: a batch commits at most batch_cap moves per timing update, + // but evaluation cost grows with every endpoint. So the endpoints are + // evaluated worst slack first, one chunk at a time, and the next chunk runs + // only while the batch is not full. The batch is built from the most + // critical endpoints first. + const size_t chunk_endpoints = evaluationEndpointLimit( + scope.batch_cap, + policy_config_.parallel_eval.eval_endpoint_factor, + policy_config_.parallel_eval.eval_min_endpoints); + + BatchPool pool; + std::unordered_set visited_pins; + size_t next_endpoint = 0; + while (next_endpoint < scope.target_endpoints) { + const size_t chunk_end + = std::min(scope.target_endpoints, next_endpoint + chunk_endpoints); + std::vector chunk + = collectTargets(next_endpoint, chunk_end, visited_pins); + next_endpoint = chunk_end; + if (!chunk.empty()) { + // Prepare reads live STA on the main thread; generation and estimation + // then run on the worker pool. + prewarmTargets(chunk); + prepareTargets(chunk); + + const std::vector targets = pool.adopt(std::move(chunk)); + std::vector evaluations = generateCandidates(targets); + // One worker owns each TargetEvaluation, so the pool is never nested. + estimateTargets(evaluations); + + pool.append(std::move(evaluations)); + } + + // The selection so far decides whether another chunk is needed; the last + // selection is the batch iterate() commits. A batch cut short by the score + // floor still takes another chunk: later endpoints can bring candidates + // above the floor. + selectBatch(pool, scope.batch_cap); + if (next_endpoint >= scope.target_endpoints + || static_cast(pool.selection.evaluation_indices.size()) + >= scope.batch_cap) { + break; + } + + RepairSetupProfiler::event("batch_eval_chunks_extended"); + } + + work_.addEndpoints(next_endpoint); + RepairSetupProfiler::event("batch_endpoints_evaluated", next_endpoint); + if (!pool.targets.empty()) { + recordSelection(pool); + } + + return pool; +} + +std::vector SetupParallelEvalPolicy::BatchPool::adopt( + std::vector chunk) +{ + std::vector adopted; + adopted.reserve(chunk.size()); + for (Target& target : chunk) { + targets.push_back(std::move(target)); + adopted.push_back(&targets.back()); + } + + return adopted; +} + +void SetupParallelEvalPolicy::BatchPool::append( + std::vector chunk_evaluations) +{ + std::ranges::move(chunk_evaluations, std::back_inserter(evaluations)); +} + +void SetupParallelEvalPolicy::selectBatch(BatchPool& pool, const int batch_cap) +{ + const ProfileScope profile("batch_selection"); + pool.candidates = makeBatchCandidates(pool.evaluations); + pool.selection = batch_selector_->select( + pool.candidates, batch_cap, stall_.wnsStalled()); +} + +void SetupParallelEvalPolicy::recordSelection(const BatchPool& pool) const +{ + if (!RepairSetupProfiler::enabled()) { + return; + } + + for (const size_t index : pool.selection.evaluation_indices) { + RepairSetupProfiler::moveSelected(pool.evaluations[index].bestCandidate()); + } + + const BatchSelectionStats& stats = pool.selection.stats; + RepairSetupProfiler::event("batch_considered", stats.considered); + RepairSetupProfiler::event("batch_limited", stats.batch_limited); + RepairSetupProfiler::event("score_limited", stats.score_limited); + for (size_t index = 0; index < stats.deferred.size(); ++index) { + const BatchConflictReason reason = static_cast(index); + RepairSetupProfiler::event( + std::string("conflict_") + batchConflictReasonName(reason), + stats.deferred[index]); + } +} + +SetupParallelEvalPolicy::BatchQor SetupParallelEvalPolicy::measureQor() const +{ + const ProfileScope profile("qor_collection"); + sta::Slack wns = 0.0; + sta::Vertex* worst_vertex = nullptr; + target_collector_->worstSlack(wns, worst_vertex); + return {.wns = wns, .tns = target_collector_->totalNegativeSlack()}; +} + +bool SetupParallelEvalPolicy::withinMaxUtilization() const +{ + // Only the -max_utilization area limit of repair_timing applies, as in + // LEGACY; without it any area is legal. The area is walked once per + // netlist edit. + if (resizer_.maxArea() <= 0.0) { + return true; + } + return !sta::fuzzyGreaterEqual(progress_.designArea(), resizer_.maxArea()); +} + +void SetupParallelEvalPolicy::noteMissedBatch(const bool utilization_legal) +{ + // The batch spent its moves without progress, so they count against the + // stall budget. A later salvage resets the budget only if it progresses. + ++checkpoint_misses_; + stall_.onMissed(); + RepairSetupProfiler::event("checkpoint_misses"); + if (!utilization_legal) { + RepairSetupProfiler::event("checkpoint_reject_utilization"); + // Without a warning the user sees no sign of the limit: the undone + // batches leave the design below it. + if (!max_utilization_warned_) { + max_utilization_warned_ = true; + const double core_area = resizer_.coreArea(); + logger_->warn(utl::RSZ, + 2033, + "PARALLEL_EVAL reached the -max_utilization limit of " + "{:.2f}%: batch {} would grow the design area to {:.1f} " + "um^2, over the {:.1f} um^2 limit. Batches over the " + "limit are undone.", + 100.0 * resizer_.maxArea() / core_area, + batch_index_, + progress_.designArea() * 1e12, + resizer_.maxArea() * 1e12); + } + } +} + +void SetupParallelEvalPolicy::settleMissedBatch(const int batch_moves) +{ + // A batch over the utilization limit is never built on. + if (tail_.active() && batch_moves > 0 && withinMaxUtilization() + && tail_.maySpeculate()) { + // Keep the missed batch and let the next batches build on it, like the + // serial loop's non-improving passes. A later save keeps the whole run; a + // spent budget rolls it all back. + tail_.keepSpeculative(batch_instances_); + RepairSetupProfiler::event( + "wns_tail_speculate", + {.metric = "wns_tail_speculative_batches", + .detail = std::to_string(tail_.speculativeBatches()), + .trace = true}); + } else { + undoMissedBatch(); + } +} + +void SetupParallelEvalPolicy::undoMissedBatch() +{ + if (tail_.speculating()) { + // Every batch of the failed run goes on the tabu list, or the first one + // would be picked again at once. Only the last batch counts toward the + // undo limit. + RepairSetupProfiler::event("wns_tail_speculation_failed"); + for (sta::Instance* inst : tail_.endSpeculation()) { + tabu_.addTabu(inst, batch_index_); + } + } + + undoBatch(); +} + +std::vector SetupParallelEvalPolicy::collectTargets( + const size_t begin_endpoint, + const size_t end_endpoint, + std::unordered_set& visited_pins) const +{ + std::vector targets + = target_collector_->collectCritPathDriverPinTargets( + [this](sta::Pin* pin) { + return resizer_.isEditableLogicStdCell(network_->instance(pin)); + }, + begin_endpoint, + end_endpoint, + visited_pins); + for (Target& target : targets) { + target.fanout = targetFanout(target); + } + + RepairSetupProfiler::event("targets_collected", targets.size()); + return targets; +} + +size_t SetupParallelEvalPolicy::evaluationEndpointLimit( + const int batch_cap, + const int eval_endpoint_factor, + const int min_endpoints) +{ + // The floor keeps small batches and small designs fully evaluated. + return std::max(static_cast(batch_cap) + * static_cast(eval_endpoint_factor), + static_cast(min_endpoints)); +} + +int SetupParallelEvalPolicy::targetFanout(const Target& target) const +{ + sta::Vertex* vertex = target.vertex(resizer_); + int fanout = 0; + sta::VertexOutEdgeIterator edge_iter(vertex, graph_); + while (edge_iter.hasNext()) { + if (edge_iter.next()->isWire()) { + ++fanout; + } + } + + return fanout; +} + +std::vector SetupParallelEvalPolicy::generateCandidates( + const std::vector& targets) +{ + const ProfileScope profile("candidate_generation"); + // The main thread applies the skip rules and prepares the generation + // inputs of each target. + std::vector tasks; + tasks.reserve(targets.size()); + for (Target* target : targets) { + tasks.push_back(planGeneration(*target)); + } + + // Every generator runs on the pool; a task without generators stays out. + std::vector worker_tasks; + for (GenerationTask& task : tasks) { + if (!task.generators.empty()) { + worker_tasks.push_back(&task); + } + } + + const bool profiling = RepairSetupProfiler::enabled(); + thread_pool_->parallelFor( + worker_tasks, [profiling](GenerationTask* const& task) { + const WorkerTimer timer(profiling ? &task->worker_elapsed : nullptr); + // Workers have no profiler; collectGenerated() replays their counts. + const DeferredCountScope counts(profiling ? &task->counts : nullptr); + for (size_t index = 0; index < task->generators.size(); ++index) { + task->generated[index] + = task->generators[index]->generate(*task->target); + } + }); + + // The main thread folds the tasks back in target order. + std::vector evaluations; + evaluations.reserve(tasks.size()); + for (GenerationTask& task : tasks) { + evaluations.push_back(collectGenerated(task)); + } + + return evaluations; +} + +SetupParallelEvalPolicy::GenerationTask SetupParallelEvalPolicy::planGeneration( + Target& target) +{ + GenerationTask task{.target = &target}; + sta::Instance* inst = target.inst(resizer_); + if (tabu_.isExcluded(inst)) { + RepairSetupProfiler::event("skip_undo_blocked"); + return task; + } + + if (tabu_.isTabu(inst)) { + RepairSetupProfiler::event("skip_miss_tabu"); + return task; + } + + for (const std::unique_ptr& generator : move_generators_) { + if (!generatorAllowed(*generator)) { + continue; + } + + if (!generator->isApplicable(target)) { + RepairSetupProfiler::event( + "skip_not_applicable", 1, moveName(generator->type())); + continue; + } + + if (generator->type() == MoveType::kBuffer) { + ++rebuffer_targets_in_batch_; + work_.addPlanRequest(); + } + + // Prepare the generator's live STA reads here, so its generate() can run + // on a worker. + prepareTarget(target, + generator->prepareRequirements() & kGenerationInputCaches); + task.generators.push_back(generator.get()); + } + + task.generated.resize(task.generators.size()); + return task; +} + +bool SetupParallelEvalPolicy::generatorAllowed( + const MoveGenerator& generator) const +{ + switch (generator.type()) { + case MoveType::kBuffer: { + // Targets come worst slack first, so the budget keeps the trees that + // matter. + const int max_targets = policy_config_.parallel_eval.rebuffer_max_targets; + if (rebuffer_targets_in_batch_ >= max_targets) { + RepairSetupProfiler::event("skip_rebuffer_target_budget"); + return false; + } + return true; + } + default: + return true; + } +} + +TargetEvaluation SetupParallelEvalPolicy::collectGenerated(GenerationTask& task) +{ + const bool profiling = RepairSetupProfiler::enabled(); + TargetEvaluation evaluation; + for (size_t index = 0; index < task.generators.size(); ++index) { + CandidateVector& generated = task.generated[index]; + if (generated.empty()) { + RepairSetupProfiler::event( + "skip_no_candidates", 1, moveName(task.generators[index]->type())); + } + + if (profiling) { + for (const std::unique_ptr& candidate : generated) { + RepairSetupProfiler::moveGenerated(*candidate); + } + } + + evaluation.candidates.reserve(evaluation.candidates.size() + + generated.size()); + std::ranges::move(generated, std::back_inserter(evaluation.candidates)); + } + + if (profiling && !task.generators.empty()) { + RepairSetupProfiler::replay(task.counts); + RepairSetupProfiler::workerTime("generation", task.worker_elapsed); + } + + return evaluation; +} + +void SetupParallelEvalPolicy::estimateTargets( + std::vector& evaluations) +{ + const ProfileScope profile("candidate_estimation"); + const bool profiling = RepairSetupProfiler::enabled(); + // The WNS tail keeps estimator-rejected candidates with a positive own + // gain; the batch STA judges them. + const bool permissive = tail_.active(); + + std::vector tasks; + tasks.reserve(evaluations.size()); + for (TargetEvaluation& evaluation : evaluations) { + tasks.push_back(&evaluation); + } + + thread_pool_->parallelFor( + tasks, [profiling, permissive](TargetEvaluation* const& evaluation) { + const WorkerTimer timer(profiling ? &evaluation->worker_elapsed + : nullptr); + estimateTarget(*evaluation, permissive); + }); + + // Fold the candidate counters into the profiler on the main thread, after + // the workers finish. + const ProfileScope reporting("reporting"); + for (TargetEvaluation& evaluation : evaluations) { + work_.addEstimates(evaluation.candidates.size()); + if (profiling) { + RepairSetupProfiler::workerTime("estimation", evaluation.worker_elapsed); + for (size_t index = 0; index < evaluation.candidates.size(); ++index) { + RepairSetupProfiler::moveEstimated(*evaluation.candidates[index], + evaluation.estimates[index]); + } + + if (evaluation.hasBestCandidate()) { + RepairSetupProfiler::event( + "winners", 1, moveName(evaluation.bestCandidate().type())); + } else { + RepairSetupProfiler::event("skip_no_legal_candidate"); + } + } + } +} + +void SetupParallelEvalPolicy::estimateTarget(TargetEvaluation& evaluation, + const bool permissive) +{ + evaluation.estimates.reserve(evaluation.candidates.size()); + for (std::unique_ptr& candidate : evaluation.candidates) { + Estimate estimate = candidate->estimate(); + if (permissive && !estimate.legal && estimate.tail_admissible) { + // Trust the own-path gain and leave the side cost to the batch STA. The + // adjusted score keeps clean estimates ahead: it is zero or less, or + // below the RebufferMt score floor, and ranks these by the smallest + // modelled loss. A side floor violation stays rejected, except + // SizeUpMt's opposite-transition reject: its score is the positive + // own-path gain, so it can rank ahead of clean estimates, and only the + // batch STA judges the opposite transition. + estimate.legal = true; + estimate.reason = "wns_tail_permissive"; + } + + evaluation.estimates.push_back(estimate); + if (!estimate.legal) { + continue; + } + + // The first candidate with the highest score wins. + if (!evaluation.hasBestCandidate() + || estimate.score > evaluation.best_estimate.score) { + evaluation.best_candidate = candidate.get(); + evaluation.best_estimate = estimate; + } + } +} + +std::vector SetupParallelEvalPolicy::makeBatchCandidates( + const std::vector& evaluations) const +{ + // A stalled batch ranks the targets by their share of the violating cone. + const bool weighted = stall_.weightsPriority(); + + std::vector candidates; + candidates.reserve(evaluations.size()); + for (size_t index = 0; index < evaluations.size(); ++index) { + const TargetEvaluation& evaluation = evaluations[index]; + if (!evaluation.hasBestCandidate()) { + continue; + } + + const MoveCandidate& best = evaluation.bestCandidate(); + const float local_score = evaluation.best_estimate.score; + candidates.push_back( + {.evaluation_index = index, + .local_score = local_score, + .priority_score = weighted ? bottleneck_->weightedScore( + local_score, + best.target().vertex(resizer_), + policy_config_.parallel_eval.priority) + : local_score, + .endpoint_slack = sta::delayAsFloat(best.target().slack), + .impact = best.impact()}); + } + + return candidates; +} + +int SetupParallelEvalPolicy::commitBatch( + std::vector& evaluations, + const BatchSelection& selection, + const std::unordered_set* skip) +{ + int batch_moves = 0; + batch_instances_.clear(); + batch_candidates_.clear(); + batch_swaps_.clear(); + for (const size_t evaluation_index : selection.evaluation_indices) { + TargetEvaluation& evaluation = evaluations[evaluation_index]; + MoveCandidate& candidate = evaluation.bestCandidate(); + if (skip != nullptr && skip->contains(&candidate)) { + continue; + } + + const Target& target = candidate.target(); + sta::Instance* inst = target.inst(resizer_); + + std::optional swap = recordSwap(candidate, inst); + const MoveResult result = committer_.commit(candidate); + if (!result.accepted) { + continue; + } + + batch_instances_.push_back(inst); + batch_candidates_.push_back(&candidate); + if (swap.has_value() && result.move_count == 1 + && result.touched_instances.size() == 1 + && result.touched_instances.front() == inst) { + swap->result = result; + batch_swaps_.push_back(std::move(*swap)); + } + + committer_.trackMoveAttempt( + candidate, target.driver_pin, target.endpointPin(resizer_)); + ++batch_moves; + ++committed_moves_; + ++tentative_moves_; + } + + return batch_moves; +} + +std::optional +SetupParallelEvalPolicy::recordSwap(MoveCandidate& candidate, + sta::Instance* inst) const +{ + // Remember the cell a size or VT swap replaces, so a salvage can revert it + // alone. replaceCell finds masters by name, so a cell whose name maps to + // another master than the instance has is not recorded. The target + // collection already checked that the instance has a Liberty cell and a + // master. + if (candidate.type() != MoveType::kSizeUp + && candidate.type() != MoveType::kVtSwap) { + return std::nullopt; + } + + const sta::LibertyCell* cell = network_->libertyCell(inst); + odb::dbInst* physical = resizer_.dbNetwork()->staToDb(inst); + if (resizer_.block()->getDb()->findMaster(cell->name().c_str()) + != physical->getMaster()) { + return std::nullopt; + } + + SwapRecord swap{.candidate = &candidate, .inst = inst, .before_cell = cell}; + physical->getLocation(swap.x, swap.y); + return swap; +} + +bool SetupParallelEvalPolicy::batchImprovesCheckpoint( + const sta::Slack checkpoint_wns, + const sta::Slack checkpoint_tns, + const sta::Slack current_wns, + const sta::Slack current_tns, + const sta::Slack best_wns, + const sta::Slack wns_tolerance) +{ + if (sta::fuzzyLess(checkpoint_wns, current_wns)) { + return true; + } + + // TNS progress counts while WNS stays in the tolerated band below the best + // checkpoint, so tiny WNS jitter cannot discard whole batches. + const bool wns_within_band + = sta::fuzzyEqual(checkpoint_wns, current_wns) + || sta::fuzzyGreaterEqual(current_wns, best_wns - wns_tolerance); + return wns_within_band && sta::fuzzyLess(checkpoint_tns, current_tns); +} + +bool SetupParallelEvalPolicy::acceptsBatch(const sta::Slack wns_after, + const sta::Slack tns_after) const +{ + if (!batchImprovesCheckpoint(checkpoint_wns_, + checkpoint_tns_, + wns_after, + tns_after, + best_checkpoint_wns_, + checkpointWnsTolerance())) { + return false; + } + + // The tail also takes estimator-rejected moves, so a WNS gain may cost + // only a bounded multiple of itself in TNS. + if (tail_.active()) { + const float wns_gain = std::max( + 0.0f, + sta::delayAsFloat(wns_after) - sta::delayAsFloat(checkpoint_wns_)); + if (sta::fuzzyLess( + tns_after, + checkpoint_tns_ + - wns_gain * policy_config_.parallel_eval.wns_tail_tns_ratio)) { + RepairSetupProfiler::event("wns_tail_reject_tns"); + return false; + } + } + + return true; +} + +sta::Slack SetupParallelEvalPolicy::checkpointWnsTolerance() const +{ + // The tail works on the worst path, so it allows no WNS drift. + if (tail_.active()) { + return 0.0f; + } + return std::abs(sta::delayAsFloat(best_checkpoint_wns_)) + * policy_config_.parallel_eval.checkpoint_wns_tolerance_pct / 100.0f; +} + +int SetupParallelEvalPolicy::adaptiveBatchSize(const size_t fep_count, + const int min_cap, + const int max_cap) +{ + // Each batch pays a full timing update, so the configured cap is a floor; + // the failing endpoint count raises it up to the bound. + return static_cast( + std::min(std::max(fep_count, static_cast(min_cap)), + static_cast(max_cap))); +} + +void SetupParallelEvalPolicy::startSmallBatchRetry() +{ + RepairSetupProfiler::event( + "retry", {.metric = "retries", .detail = "small_batch", .trace = true}); + small_batch_retry_started_ = true; + checkpoint_misses_ = 0; + + // The missed batches spent the stall budget at the full cap, and the small + // budget of the retry would end it after one batch. The retry also tries + // again the targets that the undone batches left on the tabu list. + stall_.reset(checkpoint_wns_, checkpoint_tns_); + tabu_.clearTemporary(); + + // The retry keeps the conflict rules but drops the score floor and the + // worst-path reserve. + BatchSelectorConfig selector = policy_config_.parallel_eval.selector; + selector.min_score_ratio = 0.0f; + selector.wns_reserve_pct = 0; + batch_selector_ = std::make_unique(std::move(selector)); +} + +int SetupParallelEvalPolicy::effectiveBatchCap(const size_t fep_count) const +{ + if (tail_.active()) { + return tail_.batchCap(); + } + + // The low-progress retry keeps its small fixed batches. + if (small_batch_retry_started_) { + return policy_config_.parallel_eval.retry_batch_cap; + } + + return adaptiveBatchSize(fep_count, + policy_config_.parallel_eval.min_batch_cap, + policy_config_.parallel_eval.max_batch_cap); +} + +void SetupParallelEvalPolicy::noteTimingUpdate() +{ + work_.addTimingUpdate(resizer_.estimateParasitics()->invalidNetCount()); +} + +void SetupParallelEvalPolicy::updateTiming() +{ + noteTimingUpdate(); + resizer_.updateParasiticsAndTiming(); +} + +void SetupParallelEvalPolicy::openBatchJournal() +{ + if (!batch_journal_open_) { + committer_.beginJournal(); + batch_journal_open_ = true; + } +} + +void SetupParallelEvalPolicy::saveBatchCheckpoint(const sta::Slack wns, + const sta::Slack tns) +{ + RepairSetupProfiler::event("checkpoints_saved"); + RepairSetupProfiler::qor("checkpoint_saved", wns, tns); + if (tail_.speculating()) { + RepairSetupProfiler::event("wns_tail_speculation_paid"); + RepairSetupProfiler::event("wns_tail_speculation_paid_batches", + tail_.speculativeBatches()); + tail_.endSpeculation(); + } + + if (batch_journal_open_) { + // Timing is already current from the batch-level update. + committer_.commitJournal(false); + batch_journal_open_ = false; + } + + stall_.onSaved(checkpoint_tns_, + best_checkpoint_wns_, + wns, + tns, + work_.seconds(policy_config_.parallel_eval)); + checkpoint_wns_ = wns; + checkpoint_tns_ = tns; + best_checkpoint_wns_ = std::max(best_checkpoint_wns_, wns); + tentative_moves_ = 0; + checkpoint_misses_ = 0; +} + +std::vector SetupParallelEvalPolicy::findBatchCulprits( + const sta::Slack pre_batch_wns, + std::vector& spared) const +{ + const ProfileScope profile("qor_collection"); + std::vector culprits; + std::vector victims; + sta::Graph* graph = sta_->graph(); + + // A target carries the slack its endpoint was collected with, the group's + // under -path_group, so the endpoint is read the same way. One group query + // covers the batch; a query per endpoint would test every path against the + // group_path exception again. + const PathGroupFilter path_group(&resizer_); + std::unordered_map group_slacks; + if (path_group.enabled()) { + sta::Slack slack_max = -sta::INF; + for (const MoveCandidate* candidate : batch_candidates_) { + slack_max = std::max(slack_max, candidate->target().slack); + } + const PathGroupFilter::GroupSlacks slacks + = path_group.groupEndpointSlacks(max_, slack_max); + group_slacks.insert(slacks.begin(), slacks.end()); + } + const auto endpoint_slack = [&](const sta::Pin* pin, sta::Vertex* vertex) { + if (!path_group.enabled()) { + return sta_->slack(vertex, max_); + } + // A missing endpoint is above every target slack, so it got no worse. + return group_slacks.contains(pin) ? group_slacks.at(pin) + : sta::Slack(sta::INF); + }; + + for (MoveCandidate* candidate : batch_candidates_) { + bool culprit = false; + bool victim = false; + + // The move's own endpoint must not end up worse than before the batch. + const sta::Pin* endpoint = candidate->target().endpointPin(resizer_); + sta::Vertex* endpoint_vertex + = endpoint != nullptr ? graph->pinLoadVertex(endpoint) : nullptr; + if (endpoint_vertex != nullptr + && sta::fuzzyLess(endpoint_slack(endpoint, endpoint_vertex), + candidate->target().slack)) { + culprit = true; + + // Many moves share an endpoint, so one bad estimate marks every move on + // that path. A swap whose own stages did not slow down lost its slack to + // another move, so it stays in the batch. + const std::optional self_delta = selfStageDelayDelta(*candidate); + if (self_delta.has_value() && *self_delta <= 0.0f) { + victim = true; + culprit = false; + } + } + + // A guarded side pin below the pre-batch WNS created a new worst path. + if (!culprit) { + culprit = std::ranges::any_of( + candidate->guardedSidePins(), [&](const sta::Pin* pin) { + sta::Vertex* vertex = graph->pinLoadVertex(pin); + return vertex != nullptr + && sta::fuzzyLess(sta_->slack(vertex, max_), pre_batch_wns); + }); + } + + // A move is one or the other: a spared swap that also sank a side pin is + // a culprit, so the two lists never revert the same move twice. + if (culprit) { + culprits.push_back(candidate); + } else if (victim) { + victims.push_back(candidate); + } + } + + if (!victims.empty()) { + RepairSetupProfiler::event("batch_victims_spared", victims.size()); + if (culprits.empty()) { + // No move slowed its own stages, so the endpoint rule is all there is. + RepairSetupProfiler::event("batch_culprits_endpoint_fallback"); + return victims; + } + + spared = std::move(victims); + } + + return culprits; +} + +std::optional SetupParallelEvalPolicy::selfStageDelayDelta( + const MoveCandidate& candidate) const +{ + const MoveType type = candidate.type(); + if (type != MoveType::kSizeUp && type != MoveType::kVtSwap) { + return std::nullopt; + } + + const std::optional& state + = candidate.target().estimator_context; + if (!state.has_value() || state->path_stages.empty()) { + return std::nullopt; + } + + // The swap changes its own arc and, through the input capacitance, the + // fanin arc that drives it; the prepared window holds both baselines. + float delta = 0.0f; + float baseline = 0.0f; + const int first = std::max(state->target_stage_index - 1, 0); + for (int index = first; index <= state->target_stage_index; ++index) { + const DelayStageState& stage = state->path_stages[index]; + const std::optional delay = graphStageDelay(stage); + if (!delay.has_value()) { + return std::nullopt; + } + + delta += *delay - stage.current_delay; + baseline += stage.current_delay; + } + + // Slew changes elsewhere in the batch move untouched arcs by a hair; only + // a visible fraction of the stage delay counts as a slowdown. + return delta - 0.01f * std::abs(baseline); +} + +std::optional SetupParallelEvalPolicy::graphStageDelay( + const DelayStageState& stage) const +{ + const sta::TimingArc* reference = stage.arc.ref_arc; + if (stage.driver_pin == nullptr || reference == nullptr) { + return std::nullopt; + } + + sta::Vertex* driver = graph_->pinDrvrVertex(stage.driver_pin); + if (driver == nullptr) { + return std::nullopt; + } + + const int dcalc_ap = stage.arc.scene->dcalcAnalysisPtIndex(stage.arc.min_max); + sta::VertexInEdgeIterator edge_iter(driver, graph_); + while (edge_iter.hasNext()) { + sta::Edge* edge = edge_iter.next(); + if (edge->isWire()) { + continue; + } + + // The swapped cell carries a new arc set; match it by ports and edges. + const sta::TimingArc* arc + = findMatchingTimingArc(reference, edge->timingArcSet()); + if (arc != nullptr) { + return sta::delayAsFloat(graph_->arcDelay(edge, arc, dcalc_ap)); + } + } + + return std::nullopt; +} + +bool SetupParallelEvalPolicy::salvageMissedBatch( + std::vector& evaluations, + const BatchSelection& selection, + const sta::Slack pre_batch_wns, + int& batch_moves) +{ + if (!batch_journal_open_ || batch_candidates_.empty()) { + return false; + } + + const size_t batch_size = batch_candidates_.size(); + std::vector victims; + const std::vector culprits + = findBatchCulprits(pre_batch_wns, victims); + if (culprits.empty() || culprits.size() >= batch_size) { + return false; + } + + RepairSetupProfiler::event("batch_salvage_attempts"); + RepairSetupProfiler::event("batch_culprits", culprits.size()); + + std::unordered_set skip; + const auto tabu_moves = [&](const std::vector& moves) { + for (MoveCandidate* move : moves) { + skip.insert(move); + tabu_.addTabu(move->target().inst(resizer_), batch_index_); + } + }; + tabu_moves(culprits); + + // Revert only the culprits and keep the other moves. Cell-swap culprits go + // back to their old cells in place; any other culprit rolls the batch back + // and re-commits the rest. Spared moves that still miss get a second pass. + bool in_place = true; + std::vector blocked = culprits; + for (int pass = 0; pass < 2; ++pass) { + if (in_place && revertInPlace(blocked, batch_moves)) { + RepairSetupProfiler::event("batch_salvage_in_place"); + } else { + // The roll-back would also drop the speculative batches kept in the + // same journal; the caller keeps or rolls back all of them together. + if (tail_.speculating()) { + RepairSetupProfiler::event("batch_salvage_skipped_speculating"); + return false; + } + + // Once the batch is rolled back, later passes re-commit as well. This + // restore is not counted as a timing update; the update below is. + in_place = false; + rollBackJournal(); + openBatchJournal(); + batch_moves = commitBatch(evaluations, selection, &skip); + if (batch_moves == 0) { + return false; + } + } + + updateTiming(); + const BatchQor after = measureQor(); + RepairSetupProfiler::qor("batch_salvaged_after", after.wns, after.tns); + if (withinMaxUtilization() && acceptsBatch(after.wns, after.tns)) { + RepairSetupProfiler::event("batches_salvaged"); + saveBatchCheckpoint(after.wns, after.tns); + return true; + } + + if (victims.empty() || skip.size() + victims.size() >= batch_size) { + break; + } + + RepairSetupProfiler::event("batch_salvage_second_pass"); + tabu_moves(victims); + blocked = std::move(victims); + victims.clear(); + } + + RepairSetupProfiler::event("batch_salvage_failed"); + return false; +} + +bool SetupParallelEvalPolicy::revertInPlace( + const std::vector& moves, + int& batch_moves) +{ + std::vector swaps; + swaps.reserve(moves.size()); + for (const MoveCandidate* move : moves) { + // Only a recorded cell swap can be reverted in place. + const std::vector::iterator swap + = std::ranges::find(batch_swaps_, move, &SwapRecord::candidate); + if (swap == batch_swaps_.end()) { + return false; + } + + // A move reverted by an earlier pass has nothing left to undo. + if (!swap->reverted) { + swaps.push_back(&*swap); + } + } + + const ProfileScope profile("apply"); + for (SwapRecord* swap : swaps) { + RepairSetupProfiler::beginRevert(*swap->candidate); + + // The inverse swap stays in the open batch journal, so a later restore of + // the whole batch still lands on the pre-batch state. + if (!resizer_.replaceCell(swap->inst, swap->before_cell)) { + throw std::logic_error( + "PARALLEL_EVAL salvage could not revert a cell swap"); + } + + resizer_.dbNetwork()->staToDb(swap->inst)->setLocation(swap->x, swap->y); + + // Drop the move's pending accounting so the journal commit does not + // accept it and a later restore does not reject it twice. + if (!committer_.rejectPendingCellMove( + swap->result, swap->candidate->target().driver_pin)) { + throw std::logic_error( + "PARALLEL_EVAL salvage lost its pending cell move"); + } + + RepairSetupProfiler::endRevert("salvage"); + swap->reverted = true; + } + + const int reverted = static_cast(swaps.size()); + batch_moves -= reverted; + committed_moves_ -= reverted; + tentative_moves_ -= reverted; + RepairSetupProfiler::event("batch_salvage_in_place_reverts", reverted); + + return true; +} + +void SetupParallelEvalPolicy::undoBatch() +{ + if (!batch_journal_open_) { + return; + } + + RepairSetupProfiler::event("batches_undone"); + RepairSetupProfiler::event("moves_undone", tentative_moves_); + noteTimingUpdate(); + rollBackJournal(); + + // The same targets would win again from this state, so skip them for a + // few batches. + for (sta::Instance* inst : batch_instances_) { + if (tabu_.recordUndo(inst, batch_index_)) { + RepairSetupProfiler::event("undo_blocked_instances"); + } + } + + batch_instances_.clear(); +} + +void SetupParallelEvalPolicy::restoreBatchCheckpoint() +{ + if (!batch_journal_open_) { + return; + } + + RepairSetupProfiler::event("checkpoints_restored"); + noteTimingUpdate(); + rollBackJournal(); +} + +void SetupParallelEvalPolicy::rollBackJournal() +{ + committer_.restoreJournal(); + // The restore leaves the undone edits in the tracked area, which later + // phases and the final report read through overMaxArea(). + resizer_.recountDesignAreaAfterUndo(); + batch_journal_open_ = false; + committed_moves_ -= tentative_moves_; + tentative_moves_ = 0; +} + +void SetupParallelEvalPolicy::applyStopRules(const int batch_moves) +{ + if (finishIfNoSetupViolations()) { + return; + } + + // -max_iterations ends the run: it starts no WNS tail and stops a running + // one. + if (config_.max_iterations > 0 && batch_index_ >= config_.max_iterations) { + RepairSetupProfiler::stop("max_iterations"); + restoreBatchCheckpoint(); + if (tail_.active()) { + RepairSetupProfiler::event("wns_tail_batches", tail_.batches()); + tail_.stop(); + } + markRunComplete(false); + return; + } + + if (tail_.active()) { + applyTailStopRules(batch_moves); + return; + } + + // The journal is closed here: the verdict saved or undid the batch. + if (batch_moves == 0) { + startTailOrFinish("no_committed_moves"); + return; + } + + if (stall_.windowStalled()) { + startTailOrFinish("stalled_window"); + return; + } + + const StallMonitor::Stall stall = stall_.stall(); + if (stall != StallMonitor::Stall::kNone) { + startTailOrFinish(stall == StallMonitor::Stall::kMoves ? "stalled_moves" + : "stalled_batches"); + return; + } + + if (checkpoint_misses_ + >= policy_config_.parallel_eval.max_checkpoint_misses) { + startTailOrFinish("max_checkpoint_misses"); + } +} + +void SetupParallelEvalPolicy::startTailOrFinish(const char* reason) +{ + // Whichever rule ends the batches, a run that has kept no move retries once + // with small batches first. Full batches spend the stall budget before + // they reach the miss limit, so the miss rule alone would never retry. + if (!small_batch_retry_started_ && committed_moves_ == 0) { + startSmallBatchRetry(); + return; + } + + if (startWnsTail(reason)) { + return; + } + + RepairSetupProfiler::stop(reason); + markRunComplete(false); +} + +bool SetupParallelEvalPolicy::startWnsTail(const char* reason) +{ + if (!tail_.start(work_.seconds(policy_config_.parallel_eval))) { + return false; + } + + if (RepairSetupProfiler::enabled()) { + RepairSetupProfiler::current()->metadata( + "wns_tail_work_start", fmt::format("{:.3f}", tail_.workStart())); + RepairSetupProfiler::current()->metadata( + "wns_tail_budget_work", fmt::format("{:.3f}", tail_.workBudget())); + } + + checkpoint_misses_ = 0; + + // The stalled main loop left the worst path's instances on the tabu list; + // the tail exists to work on exactly those. + tabu_.clearTemporary(); + + RepairSetupProfiler::event( + "wns_tail_begin", + {.metric = "wns_tail_starts", .detail = reason, .trace = true}); + RepairSetupProfiler::qor("wns_tail_begin", checkpoint_wns_, checkpoint_tns_); + debugPrint(logger_, + utl::RSZ, + "repair_setup", + 1, + "PARALLEL_EVAL WNS tail after {} at batch {} (WNS {:.3e})", + reason, + batch_index_, + sta::delayAsFloat(checkpoint_wns_)); + + return true; +} + +bool SetupParallelEvalPolicy::moveEnabled(const MoveType type) const +{ + switch (type) { + case MoveType::kVtSwap: + return !config_.skip_vt_swap && resizer_.vtCategoryCount() > 1; + case MoveType::kSizeUp: + case MoveType::kSizeUpMatch: + case MoveType::kReroute: + return true; + case MoveType::kBuffer: + return !config_.skip_buffering; + case MoveType::kSwapPins: + return !config_.skip_pin_swap; + // The topology moves follow the same skip options as the serial + // sequence. + case MoveType::kClone: + return !config_.skip_gate_cloning; + case MoveType::kSplitLoad: + return !config_.skip_buffering; + case MoveType::kUnbuffer: + return !config_.skip_buffer_removal; + case MoveType::kSizeDownFanout: + return !config_.skip_size_down_fanout; + case MoveType::kBufferToInverters: + return !config_.skip_buffer_to_inverters; + // parseMoveSequence() never yields kCount. + case MoveType::kCount: + break; + } + + return false; +} + +void SetupParallelEvalPolicy::applyTailStopRules(const int batch_moves) +{ + const char* reason = nullptr; + switch (tail_.nextStep( + batch_moves, work_.seconds(policy_config_.parallel_eval), reason)) { + case WnsTail::Step::kContinue: + return; + case WnsTail::Step::kSpeculate: + // The stalled greedy stage undid its last batch, so the journal is + // closed. + tail_.beginSpeculativeStage(); + tabu_.clearTemporary(); + RepairSetupProfiler::event("wns_tail_speculative_stage", + {.metric = "wns_tail_speculative_stages", + .detail = std::to_string(tail_.endpoints()), + .trace = true}); + return; + case WnsTail::Step::kWiden: + // The restore also drops an open speculative run. + restoreBatchCheckpoint(); + tail_.widen(); + tabu_.clearTemporary(); + RepairSetupProfiler::event( + "wns_tail_grow", + {.detail = std::to_string(tail_.endpoints()), .trace = true}); + return; + case WnsTail::Step::kStop: + break; + } + + RepairSetupProfiler::event("wns_tail_batches", tail_.batches()); + RepairSetupProfiler::stop(reason); + restoreBatchCheckpoint(); + tail_.stop(); + markRunComplete(false); +} + +void SetupParallelEvalPolicy::printTrackerBatchSummary() const +{ + if (!batch_journal_open_) { + committer_.printTrackerPhaseSummary("PARALLEL_EVAL Batch Summary", + "PARALLEL_EVAL Batch Endpoint Profiler", + true); + } +} + +void SetupParallelEvalPolicy::finish() +{ + // Only this policy reads the crit-path load pins of every endpoint. + target_collector_->setRecordCritPathLoadPins(false); + // The worker threads and the generators' state for them end with the phase. + target_collector_->setScanThreadPool(nullptr); + for (const std::unique_ptr& generator : move_generators_) { + generator->finishPhase(); + } + thread_pool_.reset(); + + if (RepairSetupProfiler::enabled()) { + // The work the run spent, for checking the meter against the wall clock. + RepairSetupProfiler::current()->metadata( + "work_seconds", + fmt::format("{:.3f}", work_.seconds(policy_config_.parallel_eval))); + } +} + +} // namespace rsz diff --git a/src/rsz/src/policy/SetupParallelEvalPolicy.hh b/src/rsz/src/policy/SetupParallelEvalPolicy.hh new file mode 100644 index 00000000000..4bb3d5a2d03 --- /dev/null +++ b/src/rsz/src/policy/SetupParallelEvalPolicy.hh @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "BatchSelector.hh" +#include "BottleneckAnalysis.hh" +#include "OptimizationPolicy.hh" +#include "OptimizerTypes.hh" +#include "ParallelEvalState.hh" +#include "RepairSetupContext.hh" +#include "RepairSetupProfiler.hh" +#include "rsz/Resizer.hh" + +namespace rsz { + +class MoveCommitter; +class MoveGenerator; + +// Batch setup repair. The main thread prepares each target's live STA reads; +// generation and scoring run in parallel. One conflict-free batch is then +// committed before a single parasitics and timing update. +class SetupParallelEvalPolicy : public OptimizationPolicy +{ + public: + SetupParallelEvalPolicy(Resizer& resizer, + MoveCommitter& committer, + RepairSetupContext& setup_context, + const OptimizerRunConfig& config); + ~SetupParallelEvalPolicy() override; + + const char* name() const override { return "SetupParallelEvalPolicy"; } + bool start() override; + // One batch: collect endpoints, evaluate them in chunks, select, commit, + // update timing once and judge the checkpoint. + void iterate() override; + void finish() override; + + private: + // === Batch types ========================================================== + // The violating endpoints this batch draws from and its move cap. + struct BatchScope + { + size_t target_endpoints{0}; + int batch_cap{0}; + }; + + // The candidate pool of one batch. Candidates keep references to their + // Target, so the targets live in a deque that never moves them. + struct BatchPool + { + std::deque targets; + std::vector evaluations; + std::vector candidates; + BatchSelection selection; + + // Takes a chunk; the returned pointers stay valid for the whole batch. + std::vector adopt(std::vector chunk); + void append(std::vector chunk_evaluations); + }; + + // WNS and TNS after a timing update, of the -path_group group when one is + // selected. + struct BatchQor + { + sta::Slack wns{0.0f}; + sta::Slack tns{0.0f}; + }; + + // One target's generation: its generators in -sequence order and what each + // produced. Workers write only the task's own slots and profiler counts; + // the main thread folds the task afterwards. + struct GenerationTask + { + Target* target{nullptr}; + std::vector generators{}; + std::vector generated{}; + RepairSetupProfiler::DeferredCounts counts{}; + double worker_elapsed{0.0}; + }; + + // A cell swap of the current batch with the cell and location it replaced, + // so a salvage can revert single moves inside the open journal. + struct SwapRecord + { + MoveCandidate* candidate{nullptr}; + sta::Instance* inst{nullptr}; + const sta::LibertyCell* before_cell{nullptr}; + int x{0}; + int y{0}; + MoveResult result{}; + bool reverted{false}; + }; + + // === Run setup ============================================================ + // start() steps: the move set from -sequence, the run counters and the first + // checkpoint. + void selectMoveSequence(); + void resetRunState(); + // Capture initial QoR and reset checkpoint, stall, and work references. + void saveInitialCheckpoint(); + void buildMoveGenerators(const std::vector& move_types, + const GeneratorContext& context) override; + // A move type runs only when its -skip_* option and the library allow it. + bool moveEnabled(MoveType type) const; + + // === Batch scope ========================================================== + // Collects the violating endpoints, prints the progress row and sets the + // batch scope. + BatchScope collectTargetEndpoints(); + // Clamp the current failing-endpoint count to the active batch limits. + int effectiveBatchCap(size_t fep_count) const; + // The failing endpoint count clamped to [min_cap, max_cap]. + static int adaptiveBatchSize(size_t fep_count, int min_cap, int max_cap); + // Restart a run whose batches kept no move with a smaller batch cap once. + void startSmallBatchRetry(); + // The cone weights while the saved batches stall WNS; one analysis serves + // several batches, and a pause starts the next one fresh. + void analyzeBottlenecks(const BatchScope& scope); + + // === Evaluation =========================================================== + // Evaluates the scope in chunks, worst slack first, until the batch is full. + // The returned pool carries the selected batch. + BatchPool evaluateTargets(const BatchScope& scope); + // Endpoints per chunk: a multiple of the batch cap, leaving room for + // conflicts and targets without candidates. + static size_t evaluationEndpointLimit(int batch_cap, + int eval_endpoint_factor, + int min_endpoints); + // The path-driver targets of the violating endpoints [begin, end); + // visited_pins dedups drivers across chunks. + std::vector collectTargets( + size_t begin_endpoint, + size_t end_endpoint, + std::unordered_set& visited_pins) const; + // Resolve the target driver's fanout for generation limits. + int targetFanout(const Target& target) const; + // Candidates in target order, each target's in -sequence order: + // planGeneration() on the main thread, generate() on the pool, then + // collectGenerated(). + std::vector generateCandidates( + const std::vector& targets); + // One target's skip rules, rebuffer budget, work count and generation + // inputs. + GenerationTask planGeneration(Target& target); + // Whether `generator` may run on a target of this batch. + bool generatorAllowed(const MoveGenerator& generator) const; + // The task's candidates in generator order; also replays the profiler + // counts and the generation time the worker recorded for the task. + TargetEvaluation collectGenerated(GenerationTask& task); + // Score each target's generated candidates on the worker pool. + void estimateTargets(std::vector& evaluations); + // permissive (WNS tail) keeps estimator-rejected candidates with a positive + // own gain; the batch STA judges them. + static void estimateTarget(TargetEvaluation& evaluation, bool permissive); + + // === Selection ============================================================ + // Candidates know their Target, so the pool alone identifies each entry. + std::vector makeBatchCandidates( + const std::vector& evaluations) const; + // The conflict-free batch over the whole pool. + void selectBatch(BatchPool& pool, int batch_cap); + // The profiler records of the batch evaluateTargets() selected. + void recordSelection(const BatchPool& pool) const; + + // === Commit and verdict =================================================== + // Start the journal that owns all moves of the next batch. + void openBatchJournal(); + // Capture the original cell and placement for selective salvage. + std::optional recordSwap(MoveCandidate& candidate, + sta::Instance* inst) const; + // skip lists candidates left out of a re-commit after a salvage. + int commitBatch(std::vector& evaluations, + const BatchSelection& selection, + const std::unordered_set* skip + = nullptr); + // A parasitics-and-timing update or journal restore the policy requests. + void noteTimingUpdate(); + // The batch's parasitics-and-timing update, charged to the work meter. + void updateTiming(); + // Read WNS and TNS after the batch timing update. Under -path_group they + // are the group's, as LEGACY judges: another group's worse paths at the + // same endpoints would hide every gain in the group. + BatchQor measureQor() const; + // The verdict: saves a batch that beats the checkpoint, else salvages it + // without its culprits, keeps it on speculation in the WNS tail, or undoes + // it. True when the batch or its salvage was saved. + bool judgeBatch(BatchPool& pool, sta::Slack pre_batch_wns, int& batch_moves); + bool withinMaxUtilization() const; + // WNS better, or TNS better while WNS stays within wns_tolerance of the best + // checkpoint WNS (a tie when the tolerance is 0). + static bool batchImprovesCheckpoint(sta::Slack checkpoint_wns, + sta::Slack checkpoint_tns, + sta::Slack current_wns, + sta::Slack current_tns, + sta::Slack best_wns, + sta::Slack wns_tolerance); + sta::Slack checkpointWnsTolerance() const; + // batchImprovesCheckpoint() plus the tail's limit on the TNS a WNS gain may + // cost. + bool acceptsBatch(sta::Slack wns_after, sta::Slack tns_after) const; + // Keep the batch and advance the saved WNS/TNS reference. + void saveBatchCheckpoint(sta::Slack wns, sta::Slack tns); + // Charge a missed batch to the checkpoint-miss and stall counters. + void noteMissedBatch(bool utilization_legal); + // Keeps a missed batch on speculation in the WNS tail, else undoes it. + void settleMissedBatch(int batch_moves); + // Undoes the current batch; a failed speculative run first puts all its + // instances on the tabu. + void undoMissedBatch(); + // Rolls back the current batch and puts its instances on the tabu. + void undoBatch(); + // Restore the last saved design before another batch is attempted. + void restoreBatchCheckpoint(); + // Restores the batch journal and takes back its moves. The callers count the + // timing update. + void rollBackJournal(); + + // === Salvage ============================================================== + // Reverts only the culprits of a missed batch: cell swaps in place, else a + // rollback and re-commit. True when the result is saved as a checkpoint. + bool salvageMissedBatch(std::vector& evaluations, + const BatchSelection& selection, + sta::Slack pre_batch_wns, + int& batch_moves); + // Moves whose guarded side pins fell below the pre-batch design WNS, or + // whose own endpoint got worse than the slack its target was collected with + // (the group's under -path_group). spared gets the endpoint-worse cell + // swaps whose own stages did not slow down. + std::vector findBatchCulprits( + sta::Slack pre_batch_wns, + std::vector& spared) const; + // The change of a cell swap's own and fanin stage delays since prepare; + // nullopt for other moves or when a stage arc is gone. + std::optional selfStageDelayDelta( + const MoveCandidate& candidate) const; + std::optional graphStageDelay(const DelayStageState& stage) const; + // Swaps the listed moves back to their old cells. False, with nothing + // touched, when a move is not a recorded cell swap. + bool revertInPlace(const std::vector& moves, + int& batch_moves); + + // === Stop rules =========================================================== + // Ends the run when no setup violation is left in what the verdict judges. + bool finishIfNoSetupViolations(); + // Ends the run at the -max_utilization area, where nothing may run. + bool finishAtMaxArea(); + // Ends the run when the evaluated endpoints yield no target. + void finishWithoutTargets(); + // Apply the batch-stage stall, miss, and work-budget stop rules. + void applyStopRules(int batch_moves); + // Hands a stalled run to the small-batch retry when no move was kept, else + // to the WNS tail, or ends it with `reason`. + void startTailOrFinish(const char* reason); + void printTrackerBatchSummary() const; + + // === WNS tail ============================================================= + // Starts the tail if it is on and has not run. False when the run should + // stop instead. + bool startWnsTail(const char* reason); + void applyTailStopRules(int batch_moves); + + // === State ================================================================ + std::unique_ptr batch_selector_; + // The cone weights of stalled batches; refreshed every few batches and when + // the WNS tail starts. + std::unique_ptr bottleneck_; + int bottleneck_age_{std::numeric_limits::max()}; + bool bottleneck_in_tail_{false}; + + // Applied moves, including the current tentative batch until rollback. + int committed_moves_{0}; + int batch_index_{0}; + // Moves committed since the last saved checkpoint. + int tentative_moves_{0}; + // Consecutive batches not saved as checkpoints. + int checkpoint_misses_{0}; + bool small_batch_retry_started_{false}; + // The -max_utilization warning prints once per run. + bool max_utilization_warned_{false}; + bool batch_journal_open_{false}; + sta::Slack checkpoint_wns_{0.0f}; + sta::Slack checkpoint_tns_{0.0f}; + // Best WNS among saved checkpoints; bounds the tolerated WNS drift. + sta::Slack best_checkpoint_wns_{0.0f}; + // Endpoints a batch may target under -repair_tns. + size_t max_end_repairs_{std::numeric_limits::max()}; + + // Moves of the batch under evaluation. + std::vector batch_instances_; + std::vector batch_candidates_; + // Cell swaps eligible for in-place salvage of the current batch. + std::vector batch_swaps_; + + WorkMeter work_; + StallMonitor stall_; + WnsTail tail_; + MissTabu tabu_; + + // RebufferMt targets planned in the current batch, against + // rebuffer_max_targets. + int rebuffer_targets_in_batch_{0}; +}; + +} // namespace rsz diff --git a/src/rsz/src/policy/SetupTnsPolicy.cc b/src/rsz/src/policy/SetupTnsPolicy.cc index 14e0732c977..53442d580ce 100644 --- a/src/rsz/src/policy/SetupTnsPolicy.cc +++ b/src/rsz/src/policy/SetupTnsPolicy.cc @@ -72,7 +72,8 @@ void SetupTnsPolicy::repairSetupTns(const float setup_slack_margin, "repair_setup", 10, fmt::format("TNS{} Phase Time: {{}}", phase_marker)); - committer_.capturePrePhaseSlack(); + + committer_.tracker().capturePrePhaseSlack(); setup_context_.overall_no_progress_count = 0; target_collector_->resetEndpointPasses(); debugPrint(logger_, diff --git a/src/rsz/src/policy/SetupWnsPolicy.cc b/src/rsz/src/policy/SetupWnsPolicy.cc index a4773bbf176..066c40147be 100644 --- a/src/rsz/src/policy/SetupWnsPolicy.cc +++ b/src/rsz/src/policy/SetupWnsPolicy.cc @@ -82,7 +82,7 @@ void SetupWnsPolicy::repairSetupWns(const float setup_slack_margin, 10, fmt::format("WNS{} Phase Time: {{}}", phase_marker)); constexpr int max_no_progress = 4; - committer_.capturePrePhaseSlack(); + committer_.tracker().capturePrePhaseSlack(); setup_context_.overall_no_progress_count = 0; rejected_pin_moves_current_endpoint_.clear(); target_collector_->resetEndpointPasses(); diff --git a/src/rsz/test/BUILD b/src/rsz/test/BUILD index 20a0dbd0092..472b8d3611c 100644 --- a/src/rsz/test/BUILD +++ b/src/rsz/test/BUILD @@ -279,6 +279,14 @@ TESTS = [ "split_load_hier", "repair_setup_vt_swap", "repair_setup_vtswap_legacy_asap7", + "repair_setup_parallel_eval", + "repair_setup_parallel_eval_vt_swap", + "repair_setup_parallel_eval_vt_swap_corners", + "repair_setup_parallel_eval_default_sequence", + "repair_setup_parallel_eval_effort", + "repair_setup_parallel_eval_threads", + "repair_setup_parallel_eval_max_util", + "repair_setup_parallel_eval_path_group", "repair_setup_vt_swap2", "global_sizing", "global_sizing_threads", @@ -351,6 +359,7 @@ extra_deps = { "repair_setup4_sequence": ["repair_setup4.tcl"], "repair_setup6_multi": ["repair_setup6.tcl"], "repair_setup7_multi": ["repair_setup7.tcl"], + "repair_setup_parallel_eval_threads": ["repair_setup_parallel_eval.tcl"], "repair_setup_phase_endpoint_fanin": ["repair_setup1.tcl"], "repair_setup_phase_startpoint_fanout": ["repair_setup1.tcl"], "repair_setup_phase_tns": ["repair_setup1.tcl"], @@ -610,7 +619,9 @@ cc_test( srcs = [ "cpp/TestCellSwapMt.cc", "cpp/TestDelayEstimatorMt.cc", + "cpp/TestParallelEvalPolicy.cc", "cpp/TestRepairSetupProfiler.cc", + "cpp/TestRepairTargetCollector.cc", "cpp/TestResizerMt.cc", "cpp/TestResizerMtFixture.hh", ], diff --git a/src/rsz/test/CMakeLists.txt b/src/rsz/test/CMakeLists.txt index 8a2c4017c59..676542b3f2b 100644 --- a/src/rsz/test/CMakeLists.txt +++ b/src/rsz/test/CMakeLists.txt @@ -266,6 +266,14 @@ or_integration_tests( report_buffers_no_lef repair_setup_vt_swap repair_setup_vtswap_legacy_asap7 + repair_setup_parallel_eval + repair_setup_parallel_eval_vt_swap + repair_setup_parallel_eval_vt_swap_corners + repair_setup_parallel_eval_default_sequence + repair_setup_parallel_eval_effort + repair_setup_parallel_eval_threads + repair_setup_parallel_eval_max_util + repair_setup_parallel_eval_path_group repair_setup_vt_swap2 repair_fanout7_skip_pin_swap global_sizing diff --git a/src/rsz/test/cpp/CMakeLists.txt b/src/rsz/test/cpp/CMakeLists.txt index e360667a2d5..fdde598c064 100644 --- a/src/rsz/test/cpp/CMakeLists.txt +++ b/src/rsz/test/cpp/CMakeLists.txt @@ -177,7 +177,9 @@ add_dependencies(build_and_test TestNestedJournal add_executable(TestResizerMt TestCellSwapMt.cc TestDelayEstimatorMt.cc + TestParallelEvalPolicy.cc TestRepairSetupProfiler.cc + TestRepairTargetCollector.cc TestResizerMt.cc ) target_link_libraries(TestResizerMt diff --git a/src/rsz/test/cpp/TestDelayEstimatorMt.cc b/src/rsz/test/cpp/TestDelayEstimatorMt.cc index 7a8b7103abc..51816c17be2 100644 --- a/src/rsz/test/cpp/TestDelayEstimatorMt.cc +++ b/src/rsz/test/cpp/TestDelayEstimatorMt.cc @@ -705,7 +705,7 @@ TEST_F(TestResizerMtSequential, EXPECT_GT(sta_->slack(data_vertex, sta::MinMax::max()), 0.0f); } -TEST(TestResizerMtThreadPool, OppositeRfSideLossChargesNegativeRegionOnly) +TEST(TestDelayEstimatorRules, OppositeRfSideLossChargesNegativeRegionOnly) { // Violating opposite transition: every picosecond of growth is lost. EXPECT_FLOAT_EQ(oppositeRfSideLoss(-10e-12f, 3e-12f), 3e-12f); diff --git a/src/rsz/test/cpp/TestParallelEvalPolicy.cc b/src/rsz/test/cpp/TestParallelEvalPolicy.cc new file mode 100644 index 00000000000..106744337b0 --- /dev/null +++ b/src/rsz/test/cpp/TestParallelEvalPolicy.cc @@ -0,0 +1,995 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +// PARALLEL_EVAL policy: presets, batch control, stop rules, selection +// and cone weights. + +#include "TestResizerMtFixture.hh" + +namespace rsz { + +TEST_F(TestResizerMt, BufferCatalogPreservesEstimatesAcrossThreadCounts) +{ + ep_.setHWireSignalRC(nullptr, sta_->cmdScene(), 1.0e3, 1.0e-10); + ep_.setVWireSignalRC(nullptr, sta_->cmdScene(), 1.0e3, 1.0e-10); + resizer_.runRepairSetupPreamble(); + std::vector> reference; + for (const int threads : {1, 4}) { + sta_->setThreadCount(threads); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + config.setup_slack_margin = 1.0; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + ASSERT_TRUE(policy.start()); + std::vector targets{makeTarget("out0", "target", "Z"), + makeTarget("path_out", "path_pre0", "Z"), + makeTarget("path_out", "path_target", "ZN")}; + policy.prewarmTargets(targets); + policy.prepareTargets(targets); + + std::vector scored; + for (Target& target : targets) { + scored.push_back(&target); + } + + std::vector evaluations + = policy.generateCandidates(scored); + policy.estimateTargets(evaluations); + for (size_t index = 0; index < evaluations.size(); ++index) { + const std::vector& estimates = evaluations[index].estimates; + ASSERT_FALSE(estimates.empty()); + if (threads == 1) { + reference.push_back(estimates); + continue; + } + + ASSERT_EQ(estimates.size(), reference[index].size()); + for (size_t move = 0; move < estimates.size(); ++move) { + EXPECT_EQ(estimates[move].legal, reference[index][move].legal); + EXPECT_EQ(estimates[move].reason, reference[index][move].reason); + EXPECT_EQ(estimates[move].raw_gain, reference[index][move].raw_gain); + EXPECT_EQ(estimates[move].score, reference[index][move].score); + } + } + } +} + +TEST_F(TestResizerMt, SetupParallelEvalPolicyCountsWorkerPlannerWarnings) +{ + // Placed loads give RebufferMt a Steiner tree to plan on. + int x = 1000; + for (odb::dbInst* inst : block_->getInsts()) { + inst->setLocation(x, 1000); + inst->setPlacementStatus(odb::dbPlacementStatus::PLACED); + x += 20000; + } + ep_.setHWireSignalRC(nullptr, sta_->cmdScene(), 1.0e3, 1.0e-10); + ep_.setVWireSignalRC(nullptr, sta_->cmdScene(), 1.0e3, 1.0e-10); + ep_.estimateWireParasitics(); + resizer_.runRepairSetupPreamble(); + // No buffer tree meets a 1 fs design slew limit, so the tree planning of + // RebufferMt fails with RSZ-2009 and RSZ-2021. + sta::Network* network = sta_->network(); + sta_->setSlewLimit(network->cell(network->topInstance()), + sta::MinMax::max(), + 1e-15, + sta_->cmdMode()->sdc()); + + for (const int threads : {1, 4}) { + sta_->setThreadCount(threads); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + config.sequence = {MoveType::kBuffer}; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + ASSERT_TRUE(policy.start()); + std::vector targets{makeTarget("out0", "target", "Z")}; + targets.front().fanout = policy.targetFanout(targets.front()); + policy.prewarmTargets(targets); + policy.prepareTargets(targets); + + logger_.redirectStringBegin(); + policy.generateCandidates({&targets.front()}); + const std::string generation_log = logger_.redirectStringEnd(); + logger_.redirectStringBegin(); + policy.finish(); + const std::string finish_log = logger_.redirectStringEnd(); + + if (threads == 1) { + // The main thread plans and logs each warning. + EXPECT_NE(generation_log.find("[WARNING RSZ-2009]"), std::string::npos); + EXPECT_NE(generation_log.find("[WARNING RSZ-2021]"), std::string::npos); + EXPECT_EQ(finish_log, ""); + } else { + // A worker planner counts its warnings, and the phase end logs one line. + EXPECT_EQ(generation_log, ""); + EXPECT_EQ(finish_log, + "[WARNING RSZ-2034] 2 RebufferMt planning warnings on worker " + "threads were not logged: RSZ-2009 x1, RSZ-2021 x1. Run with " + "set_thread_count 1 to log them.\n"); + } + } +} + +TEST_F(TestResizerMt, SetupParallelEvalPolicyDefaultSequenceIsFiltered) +{ + // The default set contains RebufferMt, whose planning needs wire RC. + ep_.setHWireSignalRC(nullptr, sta_->cmdScene(), 1.0e3, 1.0e-10); + ep_.setVWireSignalRC(nullptr, sta_->cmdScene(), 1.0e3, 1.0e-10); + resizer_.runRepairSetupPreamble(); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + + // One VT category drops the default VtSwapMt, as it drops a requested one. + // repair_setup_parallel_eval_default_sequence covers the multi-VT case. + ASSERT_EQ(resizer_.vtCategoryCount(), 1); + + OptimizerRunConfig config; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + ASSERT_TRUE(policy.start()); + const std::vector without_vt_swap{MoveType::kSizeUp, + MoveType::kBuffer}; + EXPECT_EQ(policy.move_sequence_, without_vt_swap); + + OptimizerRunConfig requested; + requested.sequence = {MoveType::kVtSwap, MoveType::kSizeUp}; + SetupParallelEvalPolicy explicit_policy( + resizer_, committer, setup_context, requested); + ASSERT_TRUE(explicit_policy.start()); + const std::vector size_up_only{MoveType::kSizeUp}; + EXPECT_EQ(explicit_policy.move_sequence_, size_up_only); + + // -skip_buffering removes the default RebufferMt the same way. + OptimizerRunConfig skipped; + skipped.skip_buffering = true; + SetupParallelEvalPolicy filtered(resizer_, committer, setup_context, skipped); + ASSERT_TRUE(filtered.start()); + EXPECT_EQ(filtered.move_sequence_, size_up_only); +} + +TEST_F(TestResizerMt, SetupParallelEvalPolicyRepairTnsLimitsTargetEndpoints) +{ + // Every output port violates. + resizer_.runRepairSetupPreamble(); + sta::Sdc* sdc = sta_->cmdMode()->sdc(); + for (const char* port : {"out0", "out1", "out2", "path_out"}) { + sta_->setOutputDelay(db_network_->findPin(port), + sta::RiseFallBoth::riseFall(), + sdc->findClock("clk"), + sta::RiseFall::rise(), + nullptr, + false, + false, + sta::MinMaxAll::all(), + false, + 1e-9, + sdc); + } + + resizer_.updateParasiticsAndTiming(); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + config.skip_buffering = true; + + // The default -repair_tns of 100% targets every violating endpoint. + SetupParallelEvalPolicy all_policy( + resizer_, committer, setup_context, config); + ASSERT_TRUE(all_policy.start()); + const size_t fep_count = all_policy.collectTargetEndpoints().target_endpoints; + ASSERT_GE(fep_count, 4u); + + // A smaller share targets the worst endpoints only, as many as LEGACY. + config.repair_tns_end_percent = 0.5; + SetupParallelEvalPolicy half_policy( + resizer_, committer, setup_context, config); + ASSERT_TRUE(half_policy.start()); + EXPECT_EQ(half_policy.collectTargetEndpoints().target_endpoints, + fep_count / 2); + + // -repair_tns 0 still repairs the worst endpoint. + config.repair_tns_end_percent = 0.0; + SetupParallelEvalPolicy worst_policy( + resizer_, committer, setup_context, config); + ASSERT_TRUE(worst_policy.start()); + EXPECT_EQ(worst_policy.collectTargetEndpoints().target_endpoints, 1u); +} + +TEST_F(TestResizerMt, SetupParallelEvalPolicyHonorsMaxUtilization) +{ + // -max_utilization is a share of the core area, which the fixture lacks. + const odb::Rect core(0, 0, 200000, 200000); + block_->setDieArea(core); + block_->setCoreArea(core); + prepareUniqueViolatingEndpoint(); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + config.skip_buffering = true; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + + // Below -max_utilization, every batch is legal. + EXPECT_TRUE(policy.withinMaxUtilization()); + + // A -max_utilization the design already exceeds fails every batch, and the + // run stops before it commits any move. + resizer_.setMaxUtilization(1.0e-6); + ASSERT_TRUE(resizer_.overMaxArea()); + EXPECT_FALSE(policy.withinMaxUtilization()); + ASSERT_TRUE(policy.start()); + policy.iterate(); + EXPECT_TRUE(policy.converged()); + EXPECT_EQ(policy.committed_moves_, 0); +} + +TEST_F(TestResizerMt, SetupParallelEvalPolicyHasNoUtilizationCap) +{ + // A core far smaller than the design puts utilization way above 100%. + const odb::Rect core(0, 0, 1000, 1000); + block_->setDieArea(core); + block_->setCoreArea(core); + prepareUniqueViolatingEndpoint(); + ASSERT_GT(resizer_.utilization(), 1.0); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + config.skip_buffering = true; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + + // Without -max_utilization, no utilization rejects a batch. + EXPECT_TRUE(policy.withinMaxUtilization()); +} + +TEST_F(TestResizerMt, JournalRestoreRecountsDesignArea) +{ + // -max_utilization is a share of the core area, which the fixture lacks. + const odb::Rect core(0, 0, 200000, 200000); + block_->setDieArea(core); + block_->setCoreArea(core); + prepareUniqueViolatingEndpoint(); + sta::Instance* target = db_network_->findInstance("target"); + sta::LibertyCell* original = db_network_->libertyCell(target); + sta::LibertyCell* larger = db_network_->findLibertyCell("BUF_X16"); + + // Put the -max_utilization limit just above the current area. + const double area = resizer_.designArea(); + resizer_.setMaxUtilization(area * 1.001 / resizer_.coreArea()); + ASSERT_FALSE(resizer_.overMaxArea()); + + // Undoing a batch that crossed the limit puts the design back below it, so + // later phases and the final report do not see the undone upsize. + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + config.skip_buffering = true; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + ASSERT_TRUE(policy.start()); + policy.openBatchJournal(); + ASSERT_TRUE(resizer_.replaceCell(target, larger)); + ASSERT_TRUE(resizer_.overMaxArea()); + policy.undoBatch(); + ASSERT_EQ(db_network_->libertyCell(target), original); + EXPECT_FALSE(resizer_.overMaxArea()); +} + +TEST_F(TestResizerMt, SetupParallelEvalPolicyEffortPresets) +{ + // The stop knobs a PARALLEL_EVAL run starts with for one effort. + struct StopKnobs + { + int stall_max_batches; + float progress_pct; + int endpoints; + int max_endpoints; + float work_pct; + float min_work; + }; + + const std::function knobs + = [](const RepairEffort effort) { + const ParallelEvalPolicyConfig tuned + = OptimizationPolicyConfig::forParallelEval(effort).parallel_eval; + return StopKnobs{tuned.stall_max_batches, + tuned.progress_pct, + tuned.wns_tail_endpoints, + tuned.wns_tail_max_endpoints, + tuned.wns_tail_work_pct, + tuned.wns_tail_min_work}; + }; + + // The preset values are tuning, so only their shape is checked: high is + // the default and the reference, and the others only stop sooner. + EXPECT_EQ(OptimizerRunConfig{}.effort, RepairEffort::kHigh); + const StopKnobs high = knobs(RepairEffort::kHigh); + EXPECT_GT(high.endpoints, 0); + + // medium keeps the tail, loosens nothing, and tightens at least one rule + // on the batch side and one on the tail side. + const StopKnobs medium = knobs(RepairEffort::kMedium); + EXPECT_EQ(medium.endpoints, high.endpoints); + EXPECT_LE(medium.stall_max_batches, high.stall_max_batches); + EXPECT_GE(medium.progress_pct, high.progress_pct); + EXPECT_LE(medium.max_endpoints, high.max_endpoints); + EXPECT_LE(medium.work_pct, high.work_pct); + EXPECT_LE(medium.min_work, high.min_work); + EXPECT_TRUE(medium.stall_max_batches < high.stall_max_batches + || medium.progress_pct > high.progress_pct); + EXPECT_TRUE(medium.max_endpoints < high.max_endpoints + || medium.work_pct < high.work_pct + || medium.min_work < high.min_work); + + // low never starts the tail and leaves the batch stall as high has it. + const StopKnobs low = knobs(RepairEffort::kLow); + EXPECT_EQ(low.endpoints, 0); + EXPECT_EQ(low.stall_max_batches, high.stall_max_batches); + EXPECT_FLOAT_EQ(low.progress_pct, high.progress_pct); + + EXPECT_EQ(Resizer::repairEffortFromString("low"), RepairEffort::kLow); + EXPECT_EQ(Resizer::repairEffortFromString("medium"), RepairEffort::kMedium); + EXPECT_EQ(Resizer::repairEffortFromString("high"), RepairEffort::kHigh); +} + +TEST_F(TestResizerMtSequential, + SequentialPolicyOffParityAndCandidateIntegration) +{ + prepareFlipFlop("DFF_X1"); + ASSERT_FALSE(HasFatalFailure()); + setDataSlack(5e-12, sta::MinMax::max()); + setDataSlack(5e-12, sta::MinMax::min()); + ASSERT_FALSE(HasFatalFailure()); + sta::LibertyCell* current = db_network_->findLibertyCell("DFF_X1"); + sta::LibertyCell* larger = db_network_->findLibertyCell("DFF_X2"); + sta::LibertyCell* low_vt = db_network_->findLibertyCell("DFF_X1_L"); + ASSERT_NE(larger, nullptr); + ASSERT_NE(low_vt, nullptr); + copyFlipFlopChecks(current, low_vt); + ASSERT_FALSE(HasFatalFailure()); + + // Isolate a small D-load loss while retaining the candidate's faster Q model. + for (const sta::RiseFall* rf : sta::RiseFall::range()) { + for (const sta::MinMax* min_max : sta::MinMax::range()) { + low_vt->findLibertyPort("D")->setCapacitance( + rf, + min_max, + current->findLibertyPort("D")->capacitance(rf, min_max) * 1.05f); + } + } + + for (const bool enabled : {true, false}) { + SCOPED_TRACE(enabled ? "sequential" : "combinational"); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + config.setup_slack_margin = 1.0; + config.sequence = {MoveType::kSizeUp}; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + ASSERT_TRUE(policy.start()); + policy.policy_config_.delay_estimation_levels = 0; + Target target = makeTarget("out0", "dq_seq", "Q"); + policy.prepareTarget(target, + kEstimatorContextCache | kSequentialEstimatorCache); + ASSERT_TRUE(target.estimator_context.has_value()); + ASSERT_NE(target.sequential_timing_estimator, nullptr); + const std::shared_ptr prepared + = target.sequential_timing_estimator; + policy.prepareTarget(target, + kEstimatorContextCache | kSequentialEstimatorCache); + EXPECT_EQ(target.sequential_timing_estimator, prepared); + // A target without the sequential estimator scores the plain window. + if (!enabled) { + target.sequential_timing_estimator = nullptr; + } + const std::optional baseline + = DelayEstimator::buildContext(resizer_, target, 0); + ASSERT_TRUE(baseline.has_value()); + expectSameDelayContext(*target.estimator_context, *baseline); + + for (const bool size_up : {true, false}) { + SCOPED_TRACE(size_up ? "size_up" : "vt_swap"); + sta::LibertyCell* replacement = size_up ? larger : low_vt; + const float scale = size_up ? 0.5f : 1.0f; + const DelayEstimate raw + = DelayEstimator::estimate(*target.estimator_context, replacement); + ASSERT_TRUE(raw.legal) << failReasonName(raw.reason); + SizeUpMtCandidate size( + resizer_, target, target.inst(resizer_), replacement, scale); + VtSwapMtCandidate vt( + resizer_, target, target.inst(resizer_), replacement); + const Estimate estimate = size_up ? size.estimate() : vt.estimate(); + ASSERT_TRUE(estimate.legal) << estimate.reason; + ASSERT_TRUE(estimate.raw_gain.has_value()); + EXPECT_FLOAT_EQ(*estimate.raw_gain, raw.arrival_impr); + if (!enabled) { + EXPECT_FALSE(estimate.sequential_penalty.has_value()); + EXPECT_FALSE(estimate.sequential_side_loss.has_value()); + EXPECT_FLOAT_EQ(estimate.score, raw.arrival_impr * scale); + continue; + } + + const SequentialTimingEstimator::Result side + = prepared->estimate(replacement); + ASSERT_TRUE(side.legal) << side.reason; + ASSERT_GT(side.weighted_side_slack_loss, 0.0f); + ASSERT_TRUE(estimate.sequential_penalty.has_value()); + ASSERT_TRUE(estimate.sequential_side_loss.has_value()); + EXPECT_FLOAT_EQ(*estimate.sequential_penalty, + side.weighted_side_slack_loss); + EXPECT_FLOAT_EQ(*estimate.sequential_side_loss, side.max_side_slack_loss); + const float adjusted_gain + = raw.arrival_impr - side.weighted_side_slack_loss; + EXPECT_FLOAT_EQ(estimate.score, adjusted_gain * scale); + } + } + + EXPECT_EQ(block_->findInst("dq_seq")->getMaster(), db_->findMaster("DFF_X1")); +} + +TEST_F(TestResizerMt, SetupParallelEvalPolicyAdaptiveBatchControlsAndRetry) +{ + resizer_.runRepairSetupPreamble(); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + policy.policy_config_.parallel_eval.min_batch_cap = 256; + policy.policy_config_.parallel_eval.retry_batch_cap = 8; + policy.startSmallBatchRetry(); + std::vector candidates(12); + for (size_t index = 0; index < candidates.size(); ++index) { + candidates[index].evaluation_index = index; + candidates[index].local_score = index == 0 ? 100.0f : 1.0f; + candidates[index].priority_score = candidates[index].local_score; + candidates[index].impact.complete = true; + } + + EXPECT_TRUE(policy.small_batch_retry_started_); + + // The retry ignores adaptive growth and keeps its small fixed cap. + EXPECT_EQ(policy.effectiveBatchCap(100000), 8); + EXPECT_EQ(policy.batch_selector_ + ->select(candidates, policy.effectiveBatchCap(100000), false) + .evaluation_indices.size(), + 8); + policy.small_batch_retry_started_ = false; + EXPECT_EQ(policy.effectiveBatchCap(100000), + policy.policy_config_.parallel_eval.max_batch_cap); +} + +TEST_F(TestResizerMt, SetupParallelEvalPolicyStallBudgetCountsMoves) +{ + resizer_.runRepairSetupPreamble(); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + ParallelEvalPolicyConfig& parallel_eval = policy.policy_config_.parallel_eval; + parallel_eval.min_batch_cap = 64; + parallel_eval.stall_batches = 6; + + // Budget: stall iterations worth of full batches. + EXPECT_EQ(policy.stall_.moveBudget(), 384); + + // A grown batch scales the budget so one missed batch cannot spend it. + policy.stall_.last_cap_ = 512; + EXPECT_EQ(policy.stall_.moveBudget(), 3072); + + // A small retry batch keeps the configured size as the floor. + policy.stall_.last_cap_ = 8; + EXPECT_EQ(policy.stall_.moveBudget(), 384); +} + +TEST_F(TestResizerMt, SetupParallelEvalPolicyRetriesWhenNoMoveIsKept) +{ + prepareUniqueViolatingEndpoint(); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + config.skip_buffering = true; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + sta::Instance* target = db_network_->findInstance("target"); + + // Missed batches of 512 moves spend the stall budget of three batches + // before the miss limit of five; the retry runs batches of four moves. + ParallelEvalPolicyConfig& parallel_eval = policy.policy_config_.parallel_eval; + parallel_eval.min_batch_cap = 64; + parallel_eval.stall_batches = 3; + parallel_eval.stall_max_batches = 24; + parallel_eval.max_checkpoint_misses = 5; + parallel_eval.retry_batch_cap = 4; + + // Each batch commits `cap` moves and misses the checkpoint. + const auto miss_batches = [&](const int count, const int cap) { + for (int batch = 0; batch < count; ++batch) { + policy.stall_.beginBatch(cap, 1000); + policy.stall_.endBatch(cap); + policy.noteMissedBatch(true); + policy.applyStopRules(cap); + } + }; + + // A run that kept a move hands the batches to the WNS tail at the third + // miss, which spends the stall budget. + ASSERT_TRUE(policy.start()); + policy.committed_moves_ = 1; + miss_batches(2, 512); + EXPECT_FALSE(policy.tail_.active()); + miss_batches(1, 512); + EXPECT_FALSE(policy.small_batch_retry_started_); + EXPECT_TRUE(policy.tail_.active()); + + // A run that kept no move retries with small batches instead, with a fresh + // stall budget and the tabu targets back. + ASSERT_TRUE(policy.start()); + policy.tabu_.addTabu(target, policy.batch_index_); + miss_batches(3, 512); + ASSERT_TRUE(policy.small_batch_retry_started_); + EXPECT_FALSE(policy.tail_.active()); + EXPECT_FALSE(policy.converged()); + EXPECT_EQ(policy.effectiveBatchCap(100000), 4); + EXPECT_EQ(policy.stall_.stall(), StallMonitor::Stall::kNone); + EXPECT_FALSE(policy.tabu_.isTabu(target)); + + // The retry runs once: its fifth miss hands the run to the tail. + miss_batches(4, 4); + EXPECT_FALSE(policy.tail_.active()); + miss_batches(1, 4); + EXPECT_TRUE(policy.tail_.active()); +} + +TEST(TestParallelEvalRules, ParallelEvalCheckpointPrioritizesWns) +{ + EXPECT_TRUE(SetupParallelEvalPolicy::batchImprovesCheckpoint( + -10.0f, -100.0f, -9.0f, -110.0f, -10.0f, 0.0f)); + EXPECT_FALSE(SetupParallelEvalPolicy::batchImprovesCheckpoint( + -10.0f, -100.0f, -11.0f, -90.0f, -10.0f, 0.0f)); + EXPECT_TRUE(SetupParallelEvalPolicy::batchImprovesCheckpoint( + -10.0f, -100.0f, -10.0f, -90.0f, -10.0f, 0.0f)); + EXPECT_FALSE(SetupParallelEvalPolicy::batchImprovesCheckpoint( + -10.0f, -100.0f, -10.0f, -110.0f, -10.0f, 0.0f)); + + // Within the tolerance band below the best WNS, TNS progress is enough. + EXPECT_TRUE(SetupParallelEvalPolicy::batchImprovesCheckpoint( + -10.0f, -100.0f, -10.05f, -90.0f, -10.0f, 0.1f)); + EXPECT_FALSE(SetupParallelEvalPolicy::batchImprovesCheckpoint( + -10.0f, -100.0f, -10.05f, -100.0f, -10.0f, 0.1f)); + EXPECT_FALSE(SetupParallelEvalPolicy::batchImprovesCheckpoint( + -10.0f, -100.0f, -10.2f, -90.0f, -10.0f, 0.1f)); + + // The band is anchored on the best checkpoint, not the last one. + EXPECT_FALSE(SetupParallelEvalPolicy::batchImprovesCheckpoint( + -10.1f, -100.0f, -10.15f, -90.0f, -10.0f, 0.1f)); +} + +TEST_F(TestResizerMt, BottleneckAnalysisWeighsTheViolatingCone) +{ + resizer_.runRepairSetupPreamble(); + // A margin above every slack makes every endpoint violating. + const float margin = 1.0f; + std::vector endpoints; + for (sta::Vertex* endpoint : sta_->endpoints()) { + endpoints.emplace_back(endpoint->pin(), + sta_->slack(endpoint, sta::MinMax::max())); + } + std::ranges::sort(endpoints, + [](const BottleneckAnalysis::EndpointSlack& lhs, + const BottleneckAnalysis::EndpointSlack& rhs) { + return lhs.second < rhs.second; + }); + ASSERT_FALSE(endpoints.empty()); + + BottleneckAnalysis analysis(sta_.get(), sta::MinMax::max()); + analysis.analyze(endpoints, margin, 1024, 1024); + EXPECT_GT(analysis.coneSize(), endpoints.size()); + + // The driver on out0's worst path sits in the cone, so its rank grows by + // 1 + alpha w^gamma with a normalized weight w in (0, 1]. + const BatchPriorityConfig priority{.alpha = 2.0f, .gamma = 2.0f}; + const Target target = makeTarget("out0", "target", "Z"); + const float driver + = analysis.weightedScore(1.0f, target.vertex(resizer_), priority); + EXPECT_GT(driver, 1.0f); + EXPECT_LE(driver, 3.0f); + + // A vertex outside the cone keeps its local score. + EXPECT_FLOAT_EQ(analysis.weightedScore(5.0f, nullptr, priority), 5.0f); +} + +// Vertex ids start at 1, so the newest vertex has an id equal to the vertex +// count. A buffer inserted in the violating cone gets its cone weight. +TEST_F(TestResizerMt, BottleneckAnalysisWeighsTheNewestVertex) +{ + resizer_.runRepairSetupPreamble(); + sta::Instance* buffer = resizer_.insertBufferAfterDriver( + db_network_->findNet("n1"), db_network_->findLibertyCell("BUF_X1")); + ASSERT_NE(buffer, nullptr); + resizer_.updateParasiticsAndTiming(); + + sta::Graph* graph = sta_->graph(); + sta::Vertex* newest = nullptr; + sta::VertexIterator vertex_iter(graph); + while (vertex_iter.hasNext()) { + sta::Vertex* vertex = vertex_iter.next(); + if (newest == nullptr || graph->id(vertex) > graph->id(newest)) { + newest = vertex; + } + } + ASSERT_NE(newest, nullptr); + ASSERT_EQ(db_network_->instance(newest->pin()), buffer); + ASSERT_GE(graph->id(newest), graph->vertexCount()); + + // A margin above every slack makes every endpoint violating. + std::vector endpoints; + for (sta::Vertex* endpoint : sta_->endpoints()) { + endpoints.emplace_back(endpoint->pin(), + sta_->slack(endpoint, sta::MinMax::max())); + } + BottleneckAnalysis analysis(sta_.get(), sta::MinMax::max()); + analysis.analyze(endpoints, 1.0f, 1024, 1024); + const BatchPriorityConfig priority{.alpha = 2.0f, .gamma = 2.0f}; + EXPECT_GT(analysis.weightedScore(1.0f, newest, priority), 1.0f); +} + +// -max_iterations ends the run in the WNS tail as well. +TEST_F(TestResizerMt, ParallelEvalMaxIterationsEndsTheTail) +{ + prepareUniqueViolatingEndpoint(); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + config.max_iterations = 2; + config.sequence = {MoveType::kSizeUp}; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + policy.start(); + ASSERT_FALSE(policy.converged()); + ASSERT_TRUE(policy.tail_.start(0.0)); + + policy.batch_index_ = config.max_iterations; + policy.applyStopRules(1); + EXPECT_FALSE(policy.tail_.active()); + EXPECT_TRUE(policy.converged()); +} + +TEST(TestParallelEvalRules, ParallelEvalWnsTailStopPrecedence) +{ + const ParallelEvalPolicyConfig config; + WnsTail tail(config); + ASSERT_TRUE(tail.start(100.0)); + const char* reason = nullptr; + EXPECT_EQ(tail.nextStep(1, 100.0, reason), WnsTail::Step::kContinue); + EXPECT_EQ(reason, nullptr); + + // The work budget alone ends the tail. + EXPECT_EQ(tail.nextStep(1, 1000.0, reason), WnsTail::Step::kStop); + EXPECT_STREQ(reason, "wns_tail_work_budget"); + + // A stall masks the spent budget and first retries with speculation. + tail.batches_without_gain_ = config.wns_tail_stall; + EXPECT_EQ(tail.nextStep(1, 1000.0, reason), WnsTail::Step::kSpeculate); + EXPECT_STREQ(reason, "wns_tail_stalled"); + + // No committed move masks the stall, so the stage widens instead. + EXPECT_EQ(tail.nextStep(0, 1000.0, reason), WnsTail::Step::kWiden); + EXPECT_STREQ(reason, "wns_tail_no_committed_moves"); + + // After the speculative stage, a stall widens. + tail.greedy_stalled_ = true; + EXPECT_EQ(tail.nextStep(1, 100.0, reason), WnsTail::Step::kWiden); + + // The batch limit ends the tail whatever rule fired first. + tail.batches_ = config.wns_tail_max_batches; + EXPECT_EQ(tail.nextStep(1, 100.0, reason), WnsTail::Step::kStop); + EXPECT_STREQ(reason, "wns_tail_stalled"); + + // At the widest stage, a stall ends the tail. + tail.batches_ = 0; + tail.endpoints_ = config.wns_tail_max_endpoints; + EXPECT_EQ(tail.nextStep(1, 100.0, reason), WnsTail::Step::kStop); +} + +TEST(TestParallelEvalRules, ParallelEvalAdaptiveBatchSize) +{ + // The configured cap is the floor, the failing endpoint count grows the + // batch and the adaptive maximum bounds it. + const size_t max_fep = std::numeric_limits::max(); + EXPECT_EQ(SetupParallelEvalPolicy::adaptiveBatchSize(0, 64, 512), 64); + EXPECT_EQ(SetupParallelEvalPolicy::adaptiveBatchSize(64, 64, 512), 64); + EXPECT_EQ(SetupParallelEvalPolicy::adaptiveBatchSize(65, 64, 512), 65); + EXPECT_EQ(SetupParallelEvalPolicy::adaptiveBatchSize(457, 64, 512), 457); + EXPECT_EQ(SetupParallelEvalPolicy::adaptiveBatchSize(1826, 64, 512), 512); + EXPECT_EQ(SetupParallelEvalPolicy::adaptiveBatchSize(max_fep, 64, 512), 512); +} + +TEST(TestBatchSelector, BatchSelectorDefersForwardDependency) +{ + sta::Instance* inst1 = reinterpret_cast(0x201); + sta::Instance* inst2 = reinterpret_cast(0x202); + sta::Instance* inst3 = reinterpret_cast(0x203); + sta::Net* net1 = reinterpret_cast(0x301); + sta::Net* net2 = reinterpret_cast(0x302); + sta::Net* net3 = reinterpret_cast(0x303); + + // impact1 reads net1, which impact0 writes, so it waits. impact2 reads + // net3, which only the later impact3 writes, so both of them stay. + MoveImpact impact0; + impact0.complete = true; + impact0.write_instances = {inst1}; + impact0.timing_effect_nets = {net1}; + MoveImpact impact1; + impact1.complete = true; + impact1.write_instances = {inst2}; + impact1.timing_dependency_nets = {net1}; + MoveImpact impact2; + impact2.complete = true; + impact2.write_instances = {inst2}; + impact2.timing_effect_nets = {net2}; + impact2.timing_dependency_nets = {net3}; + MoveImpact impact3; + impact3.complete = true; + impact3.write_instances = {inst3}; + impact3.timing_effect_nets = {net3}; + + std::vector candidates{{.evaluation_index = 0, + .local_score = 10.0f, + .priority_score = 10.0f, + .impact = impact0}, + {.evaluation_index = 1, + .local_score = 9.0f, + .priority_score = 9.0f, + .impact = impact1}, + {.evaluation_index = 2, + .local_score = 8.0f, + .priority_score = 8.0f, + .impact = impact2}, + {.evaluation_index = 3, + .local_score = 7.0f, + .priority_score = 7.0f, + .impact = impact3}}; + + const BatchSelector selector({}); + const BatchSelection selection = selector.select(candidates, 64, false); + EXPECT_EQ(selection.evaluation_indices, (std::vector{0, 2, 3})); + EXPECT_EQ(selection.stats.deferred[static_cast( + BatchConflictReason::kForwardDependency)], + 1); +} + +TEST(TestBatchSelector, BatchSelectorAlwaysRejectsHardConflicts) +{ + sta::Net* net = reinterpret_cast(0x401); + MoveImpact impact; + impact.complete = true; + impact.write_nets = {net}; + std::vector candidates{{.evaluation_index = 0, + .local_score = 2.0f, + .priority_score = 2.0f, + .impact = impact}, + {.evaluation_index = 1, + .local_score = 1.0f, + .priority_score = 1.0f, + .impact = impact}}; + + BatchSelector selector({}); + const BatchSelection selection = selector.select(candidates, 64, false); + + EXPECT_EQ(selection.evaluation_indices, (std::vector{0})); + EXPECT_EQ(selection.stats + .deferred[static_cast(BatchConflictReason::kWriteNet)], + 1); + const BatchSelection limited = selector.select(candidates, 2, false); + EXPECT_EQ(limited.evaluation_indices, selection.evaluation_indices); + EXPECT_EQ(limited.stats.deferred, selection.stats.deferred); +} + +TEST(TestBatchSelector, BatchSelectorRejectsSameDriverInstance) +{ + sta::Instance* driver = reinterpret_cast(0x451); + sta::Net* net1 = reinterpret_cast(0x452); + sta::Net* net2 = reinterpret_cast(0x453); + MoveImpact impact1; + impact1.complete = true; + impact1.write_instances = {driver}; + impact1.write_nets = {net1}; + MoveImpact impact2; + impact2.complete = true; + impact2.write_instances = {driver}; + impact2.write_nets = {net2}; + std::vector candidates{{.evaluation_index = 0, + .local_score = 2.0f, + .priority_score = 2.0f, + .impact = impact1}, + {.evaluation_index = 1, + .local_score = 1.0f, + .priority_score = 1.0f, + .impact = impact2}}; + + BatchSelector selector({}); + const BatchSelection selection = selector.select(candidates, 64, false); + + EXPECT_EQ(selection.evaluation_indices, (std::vector{0})); + EXPECT_EQ( + selection.stats + .deferred[static_cast(BatchConflictReason::kWriteInstance)], + 1); +} + +TEST(TestBatchSelector, BatchSelectorSupportsBatchCap) +{ + MoveImpact impact; + impact.complete = true; + std::vector candidates{{.evaluation_index = 0, + .local_score = 3.0f, + .priority_score = 3.0f, + .impact = impact}, + {.evaluation_index = 1, + .local_score = 2.0f, + .priority_score = 2.0f, + .impact = impact}, + {.evaluation_index = 2, + .local_score = 1.0f, + .priority_score = 1.0f, + .impact = impact}}; + + BatchSelector selector({}); + const BatchSelection selection = selector.select(candidates, 2, false); + + EXPECT_EQ(selection.evaluation_indices, (std::vector{0, 1})); + EXPECT_EQ(selection.stats.batch_limited, 1); + EXPECT_EQ(selector.select(candidates, 1, false).evaluation_indices.size(), + 1u); + EXPECT_EQ(selector.select(candidates, 1, false).stats.batch_limited, 2); + EXPECT_EQ(selector.select(candidates, 3, false).evaluation_indices, + (std::vector{0, 1, 2})); +} + +TEST(TestBatchSelector, BatchSelectorReservesSlotsForTheWorstPath) +{ + // Twenty candidates: the score order is the reverse of the criticality + // order, so a cap of four never reaches the worst endpoints on its own. + std::vector candidates; + candidates.reserve(20); + for (size_t index = 0; index < 20; ++index) { + MoveImpact impact; + impact.complete = true; + candidates.push_back({.evaluation_index = index, + .local_score = 100.0f - static_cast(index), + .priority_score = 100.0f - static_cast(index), + .endpoint_slack = -1e-9f * (1.0f + index), + .impact = impact}); + } + + BatchSelector selector({.wns_reserve_pct = 25}); + + // Off unless the policy asks for it. + const BatchSelection plain = selector.select(candidates, 4, false); + EXPECT_EQ(plain.evaluation_indices, (std::vector{0, 1, 2, 3})); + EXPECT_EQ(plain.stats.wns_reserved, 0); + + // One slot of four goes to the worst endpoint, and the batch still commits + // in score order. + const BatchSelection reserved = selector.select(candidates, 4, true); + EXPECT_EQ(reserved.stats.wns_reserved, 1); + EXPECT_EQ(reserved.evaluation_indices, (std::vector{0, 1, 2, 19})); + + // A batch that holds every candidate has nothing to reserve against. + const BatchSelection roomy = selector.select(candidates, 20, true); + EXPECT_EQ(roomy.stats.wns_reserved, 0); + EXPECT_EQ(roomy.evaluation_indices.size(), 20u); + + // Endpoints that already meet their constraint are never reserved. + for (BatchCandidate& candidate : candidates) { + candidate.endpoint_slack = 1e-12f; + } + + EXPECT_EQ(selector.select(candidates, 4, true).stats.wns_reserved, 0); +} + +TEST_F(TestResizerMt, SetupParallelEvalPolicyReservesInsideTheSetupMargin) +{ + // start() hands -setup_margin to the batch selector, so a stalled WNS + // reserves a slot for an endpoint that meets timing by less than the + // margin. + ep_.setHWireSignalRC(nullptr, sta_->cmdScene(), 1.0e3, 1.0e-10); + ep_.setVWireSignalRC(nullptr, sta_->cmdScene(), 1.0e3, 1.0e-10); + resizer_.runRepairSetupPreamble(); + MoveCommitter committer(resizer_); + RepairSetupContext setup_context(resizer_); + OptimizerRunConfig config; + config.setup_slack_margin = 20e-12; + SetupParallelEvalPolicy policy(resizer_, committer, setup_context, config); + ASSERT_TRUE(policy.start()); + + // Twenty candidates whose score order is the reverse of their criticality; + // every endpoint meets timing, but by less than the margin. + std::vector candidates; + candidates.reserve(20); + for (size_t index = 0; index < 20; ++index) { + MoveImpact impact; + impact.complete = true; + candidates.push_back( + {.evaluation_index = index, + .local_score = 100.0f - static_cast(index), + .priority_score = 100.0f - static_cast(index), + .endpoint_slack = 10e-12f - 0.1e-12f * static_cast(index), + .impact = impact}); + } + + // selectBatch() turns the reserve on when WNS stalls: the worst endpoint + // takes one of four slots. + const BatchSelection selection + = policy.batch_selector_->select(candidates, 4, true); + EXPECT_EQ(selection.stats.wns_reserved, 1); + EXPECT_EQ(selection.evaluation_indices, (std::vector{0, 1, 2, 19})); +} + +TEST(TestBatchSelector, BatchSelectorStopsAtScoreTail) +{ + std::vector candidates; + candidates.reserve(20); + for (size_t index = 0; index < 20; ++index) { + MoveImpact impact; + impact.complete = true; + const float score = index < 16 ? 100.0f - static_cast(index) : 10.0f; + candidates.push_back({.evaluation_index = index, + .local_score = score, + .priority_score = score, + .impact = impact}); + } + + BatchSelector selector({.score_floor_start = 16, .min_score_ratio = 0.25f}); + const BatchSelection selection = selector.select(candidates, 64, false); + + EXPECT_EQ(selection.evaluation_indices.size(), 16); + EXPECT_EQ(selection.stats.score_limited, 4); + for (size_t index = 8; index < candidates.size(); ++index) { + candidates[index].local_score = 10.0f; + candidates[index].priority_score = 10.0f; + } + + const BatchSelection limited = selector.select(candidates, 12, false); + EXPECT_EQ(limited.evaluation_indices.size(), 12u); + EXPECT_EQ(limited.stats.score_limited, 0); + EXPECT_EQ(limited.stats.batch_limited, 8); + EXPECT_EQ(selector.select(candidates, 64, false).evaluation_indices.size(), + 16u); +} + +TEST(TestBatchSelector, BatchSelectorRanksByPriorityAndKeepsLocalScoreTail) +{ + MoveImpact impact; + impact.complete = true; + const std::vector candidates{{.evaluation_index = 0, + .local_score = 100.0f, + .priority_score = 100.0f, + .impact = impact}, + {.evaluation_index = 1, + .local_score = 90.0f, + .priority_score = 200.0f, + .impact = impact}, + {.evaluation_index = 2, + .local_score = 10.0f, + .priority_score = 190.0f, + .impact = impact}, + {.evaluation_index = 3, + .local_score = 80.0f, + .priority_score = 180.0f, + .impact = impact}}; + + const BatchSelector selector( + {.score_floor_start = 1, .min_score_ratio = 0.25f}); + const BatchSelection selection = selector.select(candidates, 4, false); + + EXPECT_EQ(selection.evaluation_indices, (std::vector{1, 3, 0})); + EXPECT_EQ(selection.stats.score_limited, 1); +} + +TEST_F(TestResizerMt, SetupParallelEvalPolicyEvaluationEndpointLimit) +{ + EXPECT_EQ(SetupParallelEvalPolicy::evaluationEndpointLimit(64, 4, 256), 256u); + EXPECT_EQ(SetupParallelEvalPolicy::evaluationEndpointLimit(64, 2, 256), 256u); + EXPECT_EQ(SetupParallelEvalPolicy::evaluationEndpointLimit(8, 4, 0), 32u); + EXPECT_EQ(SetupParallelEvalPolicy::evaluationEndpointLimit(512, 2, 256), + 1024u); +} + +} // namespace rsz diff --git a/src/rsz/test/cpp/TestRepairSetupProfiler.cc b/src/rsz/test/cpp/TestRepairSetupProfiler.cc index 03234f4f62a..b854c01028f 100644 --- a/src/rsz/test/cpp/TestRepairSetupProfiler.cc +++ b/src/rsz/test/cpp/TestRepairSetupProfiler.cc @@ -21,6 +21,7 @@ TEST_F(TestResizerMt, RepairSetupProfilerOffProducesNoOutput) const ProfileIteration iteration("test_pass"); RepairSetupProfiler::event("off_marker"); RepairSetupProfiler::event("off_event"); + RepairSetupProfiler::workerTime("estimation", 1.0); RepairSetupProfiler::qor("off_qor", -1.0, -2.0); profiler.phase("LEGACY", 1); profiler.complete(true); @@ -153,7 +154,7 @@ TEST_F(TestResizerMt, RepairSetupProfilerNestedCommitThenOuterUndo) committer.beginJournal(); EXPECT_TRUE(commit(inner).accepted); EXPECT_FALSE(commit(rejected).accepted); - committer.commitJournal(); + committer.commitJournal(false); EXPECT_EQ(committer.pendingMoves(MoveType::kBuffer), 5); EXPECT_EQ(committer.committedMoves(MoveType::kBuffer), 0); committer.restoreJournal(); diff --git a/src/rsz/test/cpp/TestRepairTargetCollector.cc b/src/rsz/test/cpp/TestRepairTargetCollector.cc new file mode 100644 index 00000000000..e2de5c3e492 --- /dev/null +++ b/src/rsz/test/cpp/TestRepairTargetCollector.cc @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026-2026, The OpenROAD Authors + +// RepairTargetCollector endpoint scans and chunked target collection. + +#include "TestResizerMtFixture.hh" + +namespace rsz { + +TEST_F(TestResizerMt, CollectorEndpointScanMatchesStaSlack) +{ + resizer_.runRepairSetupPreamble(); + RepairTargetCollector collector(&resizer_); + + // A one-second margin makes every constrained endpoint a violator. + collector.init(1.0f); + const sta::MinMax* max = resizer_.maxAnalysisMode(); + size_t violating = 0; + for (sta::Vertex* endpoint : sta_->endpoints()) { + const sta::Slack expected = sta_->slack(endpoint, max); + EXPECT_FLOAT_EQ(collector.endpointPathSlack(endpoint), expected); + if (sta::fuzzyLess(expected, 1.0f)) { + ++violating; + } + } + + const std::vector>& violators + = collector.getViolatingEndpoints(); + EXPECT_GT(violators.size(), 0u); + EXPECT_EQ(violators.size(), violating); + for (size_t index = 1; index < violators.size(); ++index) { + EXPECT_LE(violators[index - 1].second, violators[index].second); + } +} + +TEST_F(TestResizerMt, CollectorParallelScanMatchesSerialScan) +{ + resizer_.runRepairSetupPreamble(); + RepairTargetCollector serial(&resizer_); + serial.init(1.0f); + ASSERT_EQ(serial.scan_thread_pool_, nullptr); + + // Force the worker path on the small test design. + utl::ThreadPool scan_pool(3); + RepairTargetCollector parallel(&resizer_); + parallel.setScanThreadPool(&scan_pool, 0); + parallel.init(1.0f); + + ASSERT_NE(parallel.scan_thread_pool_, nullptr); + ASSERT_EQ(parallel.getViolatingEndpoints().size(), + serial.getViolatingEndpoints().size()); + for (size_t index = 0; index < serial.getViolatingEndpoints().size(); + ++index) { + EXPECT_EQ(parallel.getViolatingEndpoints()[index].first, + serial.getViolatingEndpoints()[index].first); + EXPECT_FLOAT_EQ(parallel.getViolatingEndpoints()[index].second, + serial.getViolatingEndpoints()[index].second); + } +} + +TEST_F(TestResizerMt, CollectorChunkedCritPathTargetsMatchFullCollection) +{ + resizer_.runRepairSetupPreamble(); + RepairTargetCollector collector(&resizer_); + collector.init(1.0f); + const std::function valid = [this](sta::Pin* pin) { + return resizer_.isEditableLogicStdCell(resizer_.network()->instance(pin)); + }; + const size_t endpoint_count = collector.getViolatingEndpoints().size(); + std::unordered_set full_visited; + const std::vector full = collector.collectCritPathDriverPinTargets( + valid, 0, endpoint_count, full_visited); + ASSERT_FALSE(full.empty()); + + // Single-endpoint chunks with one shared visited set reproduce the full + // collection, including the cross-endpoint driver dedup. + std::vector chunked; + std::unordered_set visited; + for (size_t begin = 0; begin < endpoint_count; ++begin) { + for (Target& target : collector.collectCritPathDriverPinTargets( + valid, begin, begin + 1, visited)) { + chunked.push_back(std::move(target)); + } + } + + ASSERT_EQ(chunked.size(), full.size()); + for (size_t index = 0; index < full.size(); ++index) { + EXPECT_EQ(chunked[index].driver_pin, full[index].driver_pin); + EXPECT_FLOAT_EQ(chunked[index].slack, full[index].slack); + } + + EXPECT_TRUE(collector + .collectCritPathDriverPinTargets( + valid, endpoint_count, endpoint_count + 1, visited) + .empty()); +} + +} // namespace rsz diff --git a/src/rsz/test/cpp/TestResizerMtFixture.hh b/src/rsz/test/cpp/TestResizerMtFixture.hh index 0ee7df9bbf1..87809b4e2f3 100644 --- a/src/rsz/test/cpp/TestResizerMtFixture.hh +++ b/src/rsz/test/cpp/TestResizerMtFixture.hh @@ -38,6 +38,7 @@ #undef protected #include "DelayEstimator.hh" +#include "Rebuffer.hh" #include "RepairSetupProfiler.hh" #include "SequentialTimingEstimator.hh" #include "rsz/Resizer.hh" @@ -48,6 +49,9 @@ #include "RepairSetupContext.hh" #undef protected #undef private +#include "BatchSelector.hh" +#include "BottleneckAnalysis.hh" +#include "BufferGenerator.hh" #include "MoveCandidate.hh" #include "MoveCommitter.hh" #include "OptimizerTypes.hh" @@ -86,6 +90,14 @@ #include "tst/IntegratedFixture.h" #undef protected #include "MoveGenerator.hh" +#include "RebufferMtGenerator.hh" + +#define private public +#define protected public +#include "SetupParallelEvalPolicy.hh" +#undef protected +#undef private + #include "utl/ThreadPool.h" namespace rsz { diff --git a/src/rsz/test/repair_setup_dont_touch_sizeup.ok b/src/rsz/test/repair_setup_dont_touch_sizeup.ok index 2082539f867..c34d0f5f684 100644 --- a/src/rsz/test/repair_setup_dont_touch_sizeup.ok +++ b/src/rsz/test/repair_setup_dont_touch_sizeup.ok @@ -20,5 +20,16 @@ Repair policy: LEGACY ------------------------------------------------------------------------------------------------------------------------------ [WARNING RSZ-0062] Unable to repair all setup violations. All dont-touch instances preserved after LEGACY. +Manual ECO: restored r1 to DFF_X1. +Repair policy: PARALLEL_EVAL +[WARNING RSZ-2024] Experimental repair setup policy 'SetupParallelEvalPolicy' selected. Do not use this for production. + Iter | Removed | Resized | Inserted | Cloned | Pin | Area | WNS | TNS | Viol | Worst + | Buffers | Gates | Buffers | Gates | Swaps | | | | Endpts | Endpoint +------------------------------------------------------------------------------------------------------------------------------ + 0* | 0 | 0 | 0 | 0 | 0 | +0.0% | -0.230 | -0.2 | 1 | r2/D + final | 0 | 0 | 0 | 0 | 0 | +0.0% | -0.230 | -0.2 | 1 | r2/D +------------------------------------------------------------------------------------------------------------------------------ +[WARNING RSZ-0062] Unable to repair all setup violations. +All dont-touch instances preserved after PARALLEL_EVAL. worst slack max -0.23 tns max -0.230 diff --git a/src/rsz/test/repair_setup_dont_touch_sizeup.tcl b/src/rsz/test/repair_setup_dont_touch_sizeup.tcl index 1ae409eb189..6de875c13be 100644 --- a/src/rsz/test/repair_setup_dont_touch_sizeup.tcl +++ b/src/rsz/test/repair_setup_dont_touch_sizeup.tcl @@ -1,4 +1,4 @@ -# Check LEGACY setup size-up honors dont_touch instances. +# Check LEGACY and PARALLEL_EVAL setup size-up honor dont_touch instances. source "helpers.tcl" @@ -55,5 +55,18 @@ puts "Dont-touch instances: [dict size $before_refs]" run_sizeup_repair "LEGACY" $before_refs +# Restore the target cell through a manual ECO before testing PARALLEL_EVAL. +set target_inst r1 +set target_ref [dict get $before_refs $target_inst] +set target_lib_cell "NangateOpenCellLibrary/$target_ref" +unset_dont_touch $target_inst +if { [replace_cell $target_inst $target_lib_cell] == 0 } { + error "failed to restore $target_inst to $target_lib_cell" +} +set_dont_touch $target_inst +puts "Manual ECO: restored $target_inst to $target_ref." + +run_sizeup_repair "PARALLEL_EVAL" $before_refs + report_worst_slack -max report_tns -digits 3 diff --git a/src/rsz/test/repair_setup_invalid_phase.ok b/src/rsz/test/repair_setup_invalid_phase.ok index 301427a488f..e4a23482246 100644 --- a/src/rsz/test/repair_setup_invalid_phase.ok +++ b/src/rsz/test/repair_setup_invalid_phase.ok @@ -4,7 +4,7 @@ [INFO ODB-0131] Created 3 components and 15 component-terminals. [INFO ODB-0132] Created 2 special nets and 0 connections. [INFO ODB-0133] Created 7 nets and 9 connections. -[ERROR RSZ-0223] No phase names specified. Valid phase names are: LEGACY, WNS, WNS_PATH, WNS_CONE, TNS, ENDPOINT_FANIN, STARTPOINT_FANOUT, LAST_GASP, CRIT_VT_SWAP, REROUTE, GLOBAL_SIZING +[ERROR RSZ-0223] No phase names specified. Valid phase names are: LEGACY, WNS, WNS_PATH, WNS_CONE, TNS, ENDPOINT_FANIN, STARTPOINT_FANOUT, LAST_GASP, CRIT_VT_SWAP, REROUTE, GLOBAL_SIZING, PARALLEL_EVAL (experimental) Caught expected empty phase error: RSZ-0223 [INFO RSZ-0100] Repair move sequence: UnbufferMove SizeUpMove SwapPinsMove BufferMove CloneMove SplitLoadMove [INFO RSZ-0094] Found 2 endpoints with setup violations. @@ -16,5 +16,5 @@ Caught expected empty phase error: RSZ-0223 0* | 0 | 0 | 0 | 0 | 0 | +0.0% | -0.278 | -0.5 | 2 | y2 10* | 0 | 3 | 0 | 0 | 1 | +53.8% | -0.195 | -0.3 | 2 | y2 14* | 0 | 4 | 0 | 0 | 0 | +84.6% | -0.157 | -0.3 | 2 | y2 -[ERROR RSZ-0217] Unknown phase name 'BAD_PHASE'. Valid phase names are: LEGACY, WNS, WNS_PATH, WNS_CONE, TNS, ENDPOINT_FANIN, STARTPOINT_FANOUT, LAST_GASP, CRIT_VT_SWAP, REROUTE, GLOBAL_SIZING +[ERROR RSZ-0217] Unknown phase name 'BAD_PHASE'. Valid phase names are: LEGACY, WNS, WNS_PATH, WNS_CONE, TNS, ENDPOINT_FANIN, STARTPOINT_FANOUT, LAST_GASP, CRIT_VT_SWAP, REROUTE, GLOBAL_SIZING, PARALLEL_EVAL (experimental) Caught expected invalid phase error: RSZ-0217 diff --git a/src/rsz/test/repair_setup_parallel_eval.ok b/src/rsz/test/repair_setup_parallel_eval.ok new file mode 100644 index 00000000000..a89e2dd4bf5 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval.ok @@ -0,0 +1,15 @@ +[INFO ODB-0227] LEF file: Nangate45/Nangate45.lef, created 22 layers, 27 vias, 135 library cells +[INFO ODB-0128] Design: aes_cipher_top +[INFO ODB-0130] Created 167 pins. +[INFO ODB-0131] Created 281 components and 1577 component-terminals. +[INFO ODB-0133] Created 400 nets and 850 connections. +[WARNING RSZ-2024] Experimental repair setup policy 'SetupParallelEvalPolicy' selected. Do not use this for production. + Iter | Removed | Resized | Inserted | Cloned | Pin | Area | WNS | TNS | Viol | Worst + | Buffers | Gates | Buffers | Gates | Swaps | | | | Endpts | Endpoint +------------------------------------------------------------------------------------------------------------------------------ + 0* | 0 | 0 | 0 | 0 | 0 | +0.0% | -0.253 | -2.5 | 28 | inst13/D + 1* | 0 | 4 | 0 | 0 | 0 | +0.3% | -0.221 | -1.7 | 22 | inst13/D + final | 0 | 8 | 0 | 0 | 0 | +0.6% | -0.207 | -1.3 | 15 | inst13/D +------------------------------------------------------------------------------------------------------------------------------ +[INFO RSZ-0051] Resized 8 instances: 8 up, 0 up match, 0 down, 0 VT +[WARNING RSZ-0062] Unable to repair all setup violations. diff --git a/src/rsz/test/repair_setup_parallel_eval.tcl b/src/rsz/test/repair_setup_parallel_eval.tcl new file mode 100644 index 00000000000..2544db78c10 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval.tcl @@ -0,0 +1,13 @@ +# Exercise PARALLEL_EVAL target-parallel scoring and conflict-filtered batch commit. +source "helpers.tcl" + +read_liberty Nangate45/Nangate45_typ.lib +read_lef Nangate45/Nangate45.lef +read_def repair_setup_path11.def +read_sdc repair_setup_path11.sdc + +source Nangate45/Nangate45.rc +set_wire_rc -layer metal3 +estimate_parasitics -placement + +repair_timing -setup -phases "PARALLEL_EVAL" -max_iterations 2 -verbose diff --git a/src/rsz/test/repair_setup_parallel_eval_default_sequence.ok b/src/rsz/test/repair_setup_parallel_eval_default_sequence.ok new file mode 100644 index 00000000000..80d9b342d89 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_default_sequence.ok @@ -0,0 +1,46 @@ +[INFO ODB-0227] LEF file: asap7/asap7_tech_1x_201209.lef, created 30 layers, 9 vias +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_R_1x_220121a.lef, created 212 library cells +[INFO ODB-0394] Duplicate site asap7sc7p5t in asap7sc7p5t_28_L_1x_220121a already seen in asap7sc7p5t_28_R_1x_220121a +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_L_1x_220121a.lef, created 212 library cells +[INFO ODB-0394] Duplicate site asap7sc7p5t in asap7sc7p5t_28_SL_1x_220121a already seen in asap7sc7p5t_28_R_1x_220121a +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_SL_1x_220121a.lef, created 212 library cells +[INFO ODB-0128] Design: gcd +[INFO ODB-0130] Created 54 pins. +[INFO ODB-0131] Created 470 components and 2159 component-terminals. +[INFO ODB-0133] Created 416 nets and 1216 connections. +[WARNING RSZ-2024] Experimental repair setup policy 'SetupParallelEvalPolicy' selected. Do not use this for production. + Iter | Removed | Resized | Inserted | Cloned | Pin | Area | WNS | TNS | Viol | Worst + | Buffers | Gates | Buffers | Gates | Swaps | | | | Endpts | Endpoint +------------------------------------------------------------------------------------------------------------------------------ + 0* | 0 | 0 | 0 | 0 | 0 | +0.0% | -66.849 | -1781.2 | 46 | resp_msg[13] + 1* | 0 | 27 | 0 | 0 | 0 | +0.0% | -59.413 | -1430.1 | 43 | resp_msg[13] + 2* | 0 | 49 | 0 | 0 | 0 | +0.0% | -43.282 | -778.5 | 42 | resp_msg[15] + 3* | 0 | 65 | 0 | 0 | 0 | +0.0% | -28.082 | -349.5 | 35 | resp_msg[13] + 4* | 0 | 79 | 2 | 0 | 0 | +0.6% | -26.709 | -191.9 | 27 | resp_msg[13] + 5* | 0 | 88 | 8 | 0 | 0 | +2.2% | -24.757 | -120.8 | 21 | resp_msg[13] + 6* | 0 | 94 | 13 | 0 | 0 | +3.4% | -21.112 | -58.0 | 11 | resp_msg[13] + 7* | 0 | 102 | 13 | 0 | 0 | +3.4% | -19.006 | -36.9 | 5 | resp_msg[13] + 8* | 0 | 107 | 18 | 0 | 0 | +4.4% | -17.867 | -34.7 | 7 | resp_msg[13] + 9* | 0 | 115 | 18 | 0 | 0 | +4.7% | -15.861 | -24.3 | 4 | resp_msg[13] + 10* | 0 | 118 | 19 | 0 | 0 | +4.9% | -15.184 | -19.8 | 4 | resp_msg[13] + 11* | 0 | 120 | 20 | 0 | 0 | +5.1% | -14.311 | -16.9 | 3 | resp_msg[13] + 12* | 0 | 124 | 20 | 0 | 0 | +5.2% | -12.610 | -13.3 | 2 | resp_msg[13] + 13* | 0 | 125 | 21 | 0 | 0 | +5.4% | -11.746 | -11.7 | 1 | resp_msg[13] + 14* | 0 | 125 | 22 | 0 | 0 | +5.6% | -11.609 | -11.6 | 1 | resp_msg[13] + 15* | 0 | 126 | 22 | 0 | 0 | +5.6% | -11.263 | -11.3 | 1 | resp_msg[13] + 16* | 0 | 126 | 23 | 0 | 0 | +5.8% | -11.025 | -11.0 | 1 | resp_msg[13] + 17* | 0 | 126 | 23 | 0 | 0 | +5.8% | -11.025 | -11.0 | 1 | resp_msg[13] + 18* | 0 | 126 | 23 | 0 | 0 | +5.8% | -11.025 | -11.0 | 1 | resp_msg[13] + 19* | 0 | 126 | 24 | 0 | 0 | +6.0% | -8.667 | -13.7 | 2 | resp_msg[13] + 20* | 0 | 126 | 24 | 0 | 0 | +6.0% | -8.667 | -13.7 | 2 | resp_msg[13] + 21* | 0 | 126 | 24 | 0 | 0 | +6.0% | -8.667 | -13.7 | 2 | resp_msg[13] + 22* | 0 | 127 | 24 | 0 | 0 | +6.0% | -8.712 | -12.5 | 2 | resp_msg[13] + 23* | 0 | 126 | 24 | 0 | 0 | +6.0% | -8.667 | -13.7 | 2 | resp_msg[13] + 24* | 0 | 127 | 24 | 0 | 0 | +6.0% | -8.712 | -12.5 | 2 | resp_msg[13] + 25* | 0 | 126 | 24 | 0 | 0 | +6.0% | -8.667 | -13.7 | 2 | resp_msg[13] + 26* | 0 | 127 | 24 | 0 | 0 | +6.0% | -8.712 | -12.5 | 2 | resp_msg[13] + final | 0 | 126 | 24 | 0 | 0 | +6.0% | -8.667 | -13.7 | 2 | resp_msg[13] +------------------------------------------------------------------------------------------------------------------------------ +[INFO RSZ-0040] Inserted 24 buffers. +[INFO RSZ-0051] Resized 126 instances: 23 up, 0 up match, 0 down, 103 VT +[WARNING RSZ-0062] Unable to repair all setup violations. diff --git a/src/rsz/test/repair_setup_parallel_eval_default_sequence.tcl b/src/rsz/test/repair_setup_parallel_eval_default_sequence.tcl new file mode 100644 index 00000000000..3ea8e624459 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_default_sequence.tcl @@ -0,0 +1,31 @@ +# Exercise the PARALLEL_EVAL default move set: an omitted -sequence must still run +# VtSwapMt on a multi-VT library. +source "helpers.tcl" +source asap7/asap7.vars + +read_liberty asap7/asap7sc7p5t_AO_RVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_INVBUF_RVT_FF_nldm_220122.lib.gz +read_liberty asap7/asap7sc7p5t_OA_RVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SIMPLE_RVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SEQ_RVT_FF_nldm_220123.lib +read_liberty asap7/asap7sc7p5t_AO_LVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_INVBUF_LVT_FF_nldm_220122.lib.gz +read_liberty asap7/asap7sc7p5t_OA_LVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SIMPLE_LVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SEQ_LVT_FF_nldm_220123.lib +read_liberty asap7/asap7sc7p5t_AO_SLVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_INVBUF_SLVT_FF_nldm_220122.lib.gz +read_liberty asap7/asap7sc7p5t_OA_SLVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SIMPLE_SLVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SEQ_SLVT_FF_nldm_220123.lib + +read_lef asap7/asap7_tech_1x_201209.lef +read_lef asap7/asap7sc7p5t_28_R_1x_220121a.lef +read_lef asap7/asap7sc7p5t_28_L_1x_220121a.lef +read_lef asap7/asap7sc7p5t_28_SL_1x_220121a.lef +read_def gcd_asap7_placed.def +read_sdc gcd.sdc +source asap7/setRC.tcl +estimate_parasitics -placement + +repair_timing -setup -phases PARALLEL_EVAL -verbose diff --git a/src/rsz/test/repair_setup_parallel_eval_effort.ok b/src/rsz/test/repair_setup_parallel_eval_effort.ok new file mode 100644 index 00000000000..dfbdf40c483 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_effort.ok @@ -0,0 +1,22 @@ +[INFO ODB-0227] LEF file: asap7/asap7_tech_1x_201209.lef, created 30 layers, 9 vias +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_R_1x_220121a.lef, created 212 library cells +[INFO ODB-0394] Duplicate site asap7sc7p5t in asap7sc7p5t_28_L_1x_220121a already seen in asap7sc7p5t_28_R_1x_220121a +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_L_1x_220121a.lef, created 212 library cells +[INFO ODB-0394] Duplicate site asap7sc7p5t in asap7sc7p5t_28_SL_1x_220121a already seen in asap7sc7p5t_28_R_1x_220121a +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_SL_1x_220121a.lef, created 212 library cells +[INFO ODB-0128] Design: gcd +[INFO ODB-0130] Created 54 pins. +[INFO ODB-0131] Created 470 components and 2159 component-terminals. +[INFO ODB-0133] Created 416 nets and 1216 connections. +[WARNING RSZ-2024] Experimental repair setup policy 'SetupParallelEvalPolicy' selected. Do not use this for production. + Iter | Removed | Resized | Inserted | Cloned | Pin | Area | WNS | TNS | Viol | Worst + | Buffers | Gates | Buffers | Gates | Swaps | | | | Endpts | Endpoint +------------------------------------------------------------------------------------------------------------------------------ + 0* | 0 | 0 | 0 | 0 | 0 | +0.0% | -66.849 | -1781.2 | 46 | resp_msg[13] + final | 0 | 126 | 23 | 0 | 0 | +5.8% | -11.025 | -11.0 | 1 | resp_msg[13] +------------------------------------------------------------------------------------------------------------------------------ +[INFO RSZ-0040] Inserted 23 buffers. +[INFO RSZ-0051] Resized 126 instances: 23 up, 0 up match, 0 down, 103 VT +[WARNING RSZ-0062] Unable to repair all setup violations. +worst slack max -11.025 +tns max -11.025 diff --git a/src/rsz/test/repair_setup_parallel_eval_effort.tcl b/src/rsz/test/repair_setup_parallel_eval_effort.tcl new file mode 100644 index 00000000000..a1bed80b473 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_effort.tcl @@ -0,0 +1,33 @@ +# -effort low stops PARALLEL_EVAL at the batch-stage stall without the WNS tail. +source "helpers.tcl" +source asap7/asap7.vars + +read_liberty asap7/asap7sc7p5t_AO_RVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_INVBUF_RVT_FF_nldm_220122.lib.gz +read_liberty asap7/asap7sc7p5t_OA_RVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SIMPLE_RVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SEQ_RVT_FF_nldm_220123.lib +read_liberty asap7/asap7sc7p5t_AO_LVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_INVBUF_LVT_FF_nldm_220122.lib.gz +read_liberty asap7/asap7sc7p5t_OA_LVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SIMPLE_LVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SEQ_LVT_FF_nldm_220123.lib +read_liberty asap7/asap7sc7p5t_AO_SLVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_INVBUF_SLVT_FF_nldm_220122.lib.gz +read_liberty asap7/asap7sc7p5t_OA_SLVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SIMPLE_SLVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SEQ_SLVT_FF_nldm_220123.lib + +read_lef asap7/asap7_tech_1x_201209.lef +read_lef asap7/asap7sc7p5t_28_R_1x_220121a.lef +read_lef asap7/asap7sc7p5t_28_L_1x_220121a.lef +read_lef asap7/asap7sc7p5t_28_SL_1x_220121a.lef +read_def gcd_asap7_placed.def +read_sdc gcd.sdc +source asap7/setRC.tcl +estimate_parasitics -placement + +repair_timing -setup -phases PARALLEL_EVAL -effort low + +report_worst_slack -max -digits 3 +report_tns -digits 3 diff --git a/src/rsz/test/repair_setup_parallel_eval_max_util.ok b/src/rsz/test/repair_setup_parallel_eval_max_util.ok new file mode 100644 index 00000000000..f4e0aa69f85 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_max_util.ok @@ -0,0 +1,21 @@ +[INFO ODB-0227] LEF file: Nangate45/Nangate45.lef, created 22 layers, 27 vias, 135 library cells +[INFO ODB-0128] Design: aes_cipher_top +[INFO ODB-0130] Created 167 pins. +[INFO ODB-0131] Created 281 components and 1577 component-terminals. +[INFO ODB-0133] Created 400 nets and 850 connections. +max_utilization 1.4289% +Design area 894 um^2 1% utilization. +[WARNING RSZ-2024] Experimental repair setup policy 'SetupParallelEvalPolicy' selected. Do not use this for production. + Iter | Removed | Resized | Inserted | Cloned | Pin | Area | WNS | TNS | Viol | Worst + | Buffers | Gates | Buffers | Gates | Swaps | | | | Endpts | Endpoint +------------------------------------------------------------------------------------------------------------------------------ + 0* | 0 | 0 | 0 | 0 | 0 | +0.0% | -0.253 | -2.5 | 28 | inst13/D +[WARNING RSZ-2033] PARALLEL_EVAL reached the -max_utilization limit of 1.43%: batch 1 would grow the design area to 896.2 um^2, over the 894.7 um^2 limit. Batches over the limit are undone. + 1* | 0 | 0 | 0 | 0 | 0 | +0.0% | -0.253 | -2.5 | 28 | inst13/D + 2* | 0 | 0 | 0 | 0 | 0 | +0.0% | -0.253 | -2.5 | 28 | inst13/D + final | 0 | 1 | 0 | 0 | 0 | +0.1% | -0.253 | -2.4 | 28 | inst13/D +------------------------------------------------------------------------------------------------------------------------------ +[INFO RSZ-0051] Resized 1 instances: 1 up, 0 up match, 0 down, 0 VT +[WARNING RSZ-0062] Unable to repair all setup violations. +Design area 894 um^2 1% utilization. +utilization 1.4283% diff --git a/src/rsz/test/repair_setup_parallel_eval_max_util.tcl b/src/rsz/test/repair_setup_parallel_eval_max_util.tcl new file mode 100644 index 00000000000..a8c4f41e829 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_max_util.tcl @@ -0,0 +1,24 @@ +# PARALLEL_EVAL undoes batches that cross -max_utilization and warns once. +source "helpers.tcl" + +read_liberty Nangate45/Nangate45_typ.lib +read_lef Nangate45/Nangate45.lef +read_def repair_setup_path11.def +read_sdc repair_setup_path11.sdc + +source Nangate45/Nangate45.rc +set_wire_rc -layer metal3 +estimate_parasitics -placement + +# Every move adds area here, so a limit just above the current utilization +# makes each batch cross it. +set max_util [expr { 100.0 * [rsz::utilization] * 1.001 }] +puts [format "max_utilization %.4f%%" $max_util] +report_design_area + +repair_timing -setup -phases "PARALLEL_EVAL" -max_iterations 3 -verbose \ + -max_utilization $max_util + +# The undone batches leave the design below the limit. +report_design_area +puts [format "utilization %.4f%%" [expr { 100.0 * [rsz::utilization] }]] diff --git a/src/rsz/test/repair_setup_parallel_eval_path_group.ok b/src/rsz/test/repair_setup_parallel_eval_path_group.ok new file mode 100644 index 00000000000..bff685a29d1 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_path_group.ok @@ -0,0 +1,38 @@ +[INFO ODB-0227] LEF file: Nangate45/Nangate45.lef, created 22 layers, 27 vias, 135 library cells +[INFO ODB-0128] Design: gcd +[INFO ODB-0130] Created 54 pins. +[INFO ODB-0131] Created 571 components and 2554 component-terminals. +[INFO ODB-0132] Created 5 special nets and 1142 connections. +[INFO ODB-0133] Created 528 nets and 1412 connections. +[INFO RSZ-0228] Created path group 'reg2reg'. +[INFO RSZ-0228] Created path group 'in2reg'. +-- reg2reg before +Startpoint Endpoint Slack +---------------------------------------------------------------------------------- +_893_/Q (DFF_X1) _869_/D (DFF_X1) -0.1118 + +-- in2reg before +Startpoint Endpoint Slack +---------------------------------------------------------------------------------- +req_msg[28] (input) _877_/D (DFF_X1) -0.1611 + +[WARNING RSZ-2024] Experimental repair setup policy 'SetupParallelEvalPolicy' selected. Do not use this for production. + Iter | Removed | Resized | Inserted | Cloned | Pin | Area | WNS | TNS | Viol | Worst + | Buffers | Gates | Buffers | Gates | Swaps | | | | Endpts | Endpoint +------------------------------------------------------------------------------------------------------------------------------ + 0* | 0 | 0 | 0 | 0 | 0 | +0.0% | -0.112 | -2.5 | 32 | _869_/D + final | 0 | 116 | 22 | 0 | 0 | +14.0% | -0.040 | -0.5 | 28 | _880_/D +------------------------------------------------------------------------------------------------------------------------------ +[INFO RSZ-0040] Inserted 22 buffers. +[INFO RSZ-0051] Resized 116 instances: 116 up, 0 up match, 0 down, 0 VT +[WARNING RSZ-0062] Unable to repair all setup violations. +-- reg2reg after +Startpoint Endpoint Slack +---------------------------------------------------------------------------------- +_884_/Q (DFF_X1) _880_/D (DFF_X1) -0.0405 + +-- in2reg after +Startpoint Endpoint Slack +---------------------------------------------------------------------------------- +req_msg[12] (input) _893_/D (DFF_X1) -0.1593 + diff --git a/src/rsz/test/repair_setup_parallel_eval_path_group.tcl b/src/rsz/test/repair_setup_parallel_eval_path_group.tcl new file mode 100644 index 00000000000..71cfdee98b0 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_path_group.tcl @@ -0,0 +1,32 @@ +# PARALLEL_EVAL under -path_group judges its batches by the group's own slack, +# as LEGACY does in repair_timing_path_group4. Late req_msg data makes in2reg +# the worst path at every violating reg2reg endpoint, so the design WNS and +# TNS barely see the reg2reg repair; a verdict on them kept batches that made +# reg2reg worse. +source "helpers.tcl" +read_liberty Nangate45/Nangate45_typ.lib +read_lef Nangate45/Nangate45.lef +read_def gcd_nangate45_placed.def + +create_clock -name core_clock -period 0.55 [get_ports clk] +set_input_delay -clock core_clock 0.05 [all_inputs -no_clocks] +set_input_delay -clock core_clock 0.50 [get_ports req_msg*] +set_output_delay -clock core_clock 0.1 [all_outputs] + +source Nangate45/Nangate45.rc +set_wire_rc -layer metal3 +estimate_parasitics -placement + +rsz::resolve_path_group reg2reg +rsz::resolve_path_group in2reg + +foreach when {before after} { + if { $when == "after" } { + repair_timing -setup -path_group reg2reg -phases PARALLEL_EVAL + } + foreach path_group {reg2reg in2reg} { + puts "-- $path_group $when" + report_checks -path_group $path_group -path_delay max -digits 4 \ + -fields {} -group_path_count 1 -format summary + } +} diff --git a/src/rsz/test/repair_setup_parallel_eval_threads.ok b/src/rsz/test/repair_setup_parallel_eval_threads.ok new file mode 100644 index 00000000000..a89e2dd4bf5 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_threads.ok @@ -0,0 +1,15 @@ +[INFO ODB-0227] LEF file: Nangate45/Nangate45.lef, created 22 layers, 27 vias, 135 library cells +[INFO ODB-0128] Design: aes_cipher_top +[INFO ODB-0130] Created 167 pins. +[INFO ODB-0131] Created 281 components and 1577 component-terminals. +[INFO ODB-0133] Created 400 nets and 850 connections. +[WARNING RSZ-2024] Experimental repair setup policy 'SetupParallelEvalPolicy' selected. Do not use this for production. + Iter | Removed | Resized | Inserted | Cloned | Pin | Area | WNS | TNS | Viol | Worst + | Buffers | Gates | Buffers | Gates | Swaps | | | | Endpts | Endpoint +------------------------------------------------------------------------------------------------------------------------------ + 0* | 0 | 0 | 0 | 0 | 0 | +0.0% | -0.253 | -2.5 | 28 | inst13/D + 1* | 0 | 4 | 0 | 0 | 0 | +0.3% | -0.221 | -1.7 | 22 | inst13/D + final | 0 | 8 | 0 | 0 | 0 | +0.6% | -0.207 | -1.3 | 15 | inst13/D +------------------------------------------------------------------------------------------------------------------------------ +[INFO RSZ-0051] Resized 8 instances: 8 up, 0 up match, 0 down, 0 VT +[WARNING RSZ-0062] Unable to repair all setup violations. diff --git a/src/rsz/test/repair_setup_parallel_eval_threads.tcl b/src/rsz/test/repair_setup_parallel_eval_threads.tcl new file mode 100644 index 00000000000..df9d92de828 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_threads.tcl @@ -0,0 +1,5 @@ +# PARALLEL_EVAL with worker threads: the workers fold back in target order, so +# the log is the one of the single-thread run (repair_setup_parallel_eval.ok). +source "helpers.tcl" +set_thread_count 4 +source "repair_setup_parallel_eval.tcl" diff --git a/src/rsz/test/repair_setup_parallel_eval_vt_swap.ok b/src/rsz/test/repair_setup_parallel_eval_vt_swap.ok new file mode 100644 index 00000000000..9b40a45267e --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_vt_swap.ok @@ -0,0 +1,20 @@ +[INFO ODB-0227] LEF file: asap7/asap7_tech_1x_201209.lef, created 30 layers, 9 vias +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_R_1x_220121a.lef, created 212 library cells +[INFO ODB-0394] Duplicate site asap7sc7p5t in asap7sc7p5t_28_L_1x_220121a already seen in asap7sc7p5t_28_R_1x_220121a +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_L_1x_220121a.lef, created 212 library cells +[INFO ODB-0394] Duplicate site asap7sc7p5t in asap7sc7p5t_28_SL_1x_220121a already seen in asap7sc7p5t_28_R_1x_220121a +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_SL_1x_220121a.lef, created 212 library cells +[INFO ODB-0128] Design: gcd +[INFO ODB-0130] Created 54 pins. +[INFO ODB-0131] Created 470 components and 2159 component-terminals. +[INFO ODB-0133] Created 416 nets and 1216 connections. +[WARNING RSZ-2024] Experimental repair setup policy 'SetupParallelEvalPolicy' selected. Do not use this for production. + Iter | Removed | Resized | Inserted | Cloned | Pin | Area | WNS | TNS | Viol | Worst + | Buffers | Gates | Buffers | Gates | Swaps | | | | Endpts | Endpoint +------------------------------------------------------------------------------------------------------------------------------ + 0* | 0 | 0 | 0 | 0 | 0 | +0.0% | -66.849 | -1781.2 | 46 | resp_msg[13] + 1* | 0 | 27 | 0 | 0 | 0 | +0.0% | -59.413 | -1430.1 | 43 | resp_msg[13] + final | 0 | 49 | 0 | 0 | 0 | +0.0% | -43.282 | -778.5 | 42 | resp_msg[15] +------------------------------------------------------------------------------------------------------------------------------ +[INFO RSZ-0051] Resized 49 instances: 0 up, 0 up match, 0 down, 49 VT +[WARNING RSZ-0062] Unable to repair all setup violations. diff --git a/src/rsz/test/repair_setup_parallel_eval_vt_swap.tcl b/src/rsz/test/repair_setup_parallel_eval_vt_swap.tcl new file mode 100644 index 00000000000..ec21c489bc6 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_vt_swap.tcl @@ -0,0 +1,34 @@ +# Exercise PARALLEL_EVAL VtSwapMt selection through the repair_timing -sequence option. +source "helpers.tcl" +source asap7/asap7.vars + +read_liberty asap7/asap7sc7p5t_AO_RVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_INVBUF_RVT_FF_nldm_220122.lib.gz +read_liberty asap7/asap7sc7p5t_OA_RVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SIMPLE_RVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SEQ_RVT_FF_nldm_220123.lib +read_liberty asap7/asap7sc7p5t_AO_LVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_INVBUF_LVT_FF_nldm_220122.lib.gz +read_liberty asap7/asap7sc7p5t_OA_LVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SIMPLE_LVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SEQ_LVT_FF_nldm_220123.lib +read_liberty asap7/asap7sc7p5t_AO_SLVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_INVBUF_SLVT_FF_nldm_220122.lib.gz +read_liberty asap7/asap7sc7p5t_OA_SLVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SIMPLE_SLVT_FF_nldm_211120.lib.gz +read_liberty asap7/asap7sc7p5t_SEQ_SLVT_FF_nldm_220123.lib + +read_lef asap7/asap7_tech_1x_201209.lef +read_lef asap7/asap7sc7p5t_28_R_1x_220121a.lef +read_lef asap7/asap7sc7p5t_28_L_1x_220121a.lef +read_lef asap7/asap7sc7p5t_28_SL_1x_220121a.lef +read_def gcd_asap7_placed.def +read_sdc gcd.sdc +source asap7/setRC.tcl +estimate_parasitics -placement + +repair_timing -setup \ + -phases PARALLEL_EVAL \ + -sequence vt_swap \ + -max_iterations 2 \ + -verbose diff --git a/src/rsz/test/repair_setup_parallel_eval_vt_swap_corners.ok b/src/rsz/test/repair_setup_parallel_eval_vt_swap_corners.ok new file mode 100644 index 00000000000..05a2046d708 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_vt_swap_corners.ok @@ -0,0 +1,20 @@ +[INFO ODB-0227] LEF file: asap7/asap7_tech_1x_201209.lef, created 30 layers, 9 vias +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_R_1x_220121a.lef, created 212 library cells +[INFO ODB-0394] Duplicate site asap7sc7p5t in asap7sc7p5t_28_L_1x_220121a already seen in asap7sc7p5t_28_R_1x_220121a +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_L_1x_220121a.lef, created 212 library cells +[INFO ODB-0394] Duplicate site asap7sc7p5t in asap7sc7p5t_28_SL_1x_220121a already seen in asap7sc7p5t_28_R_1x_220121a +[INFO ODB-0227] LEF file: asap7/asap7sc7p5t_28_SL_1x_220121a.lef, created 212 library cells +[INFO ODB-0128] Design: gcd +[INFO ODB-0130] Created 54 pins. +[INFO ODB-0131] Created 470 components and 2159 component-terminals. +[INFO ODB-0133] Created 416 nets and 1216 connections. +[WARNING RSZ-2024] Experimental repair setup policy 'SetupParallelEvalPolicy' selected. Do not use this for production. + Iter | Removed | Resized | Inserted | Cloned | Pin | Area | WNS | TNS | Viol | Worst + | Buffers | Gates | Buffers | Gates | Swaps | | | | Endpts | Endpoint +------------------------------------------------------------------------------------------------------------------------------ + 0* | 0 | 0 | 0 | 0 | 0 | +0.0% | -88.835 | -2700.2 | 47 | resp_msg[13] + 1* | 0 | 29 | 0 | 0 | 0 | +0.0% | -78.128 | -2296.8 | 45 | resp_msg[13] + final | 0 | 49 | 0 | 0 | 0 | +0.0% | -61.275 | -1995.2 | 43 | resp_msg[14] +------------------------------------------------------------------------------------------------------------------------------ +[INFO RSZ-0051] Resized 49 instances: 0 up, 0 up match, 0 down, 49 VT +[WARNING RSZ-0062] Unable to repair all setup violations. diff --git a/src/rsz/test/repair_setup_parallel_eval_vt_swap_corners.tcl b/src/rsz/test/repair_setup_parallel_eval_vt_swap_corners.tcl new file mode 100644 index 00000000000..f9d13dec5a3 --- /dev/null +++ b/src/rsz/test/repair_setup_parallel_eval_vt_swap_corners.tcl @@ -0,0 +1,35 @@ +# PARALLEL_EVAL VtSwapMt with a liberty per corner. The worst paths are in +# the slow corner, whose timing arcs hold the cells of its own libraries. +source "helpers.tcl" +# Both corners read the same libraries. +suppress_message STA 1140 +suppress_message STA 1212 + +define_corners fast slow +foreach corner {fast slow} { + foreach vt {RVT LVT SLVT} { + read_liberty -corner $corner asap7/asap7sc7p5t_AO_${vt}_FF_nldm_211120.lib.gz + read_liberty -corner $corner \ + asap7/asap7sc7p5t_INVBUF_${vt}_FF_nldm_220122.lib.gz + read_liberty -corner $corner asap7/asap7sc7p5t_OA_${vt}_FF_nldm_211120.lib.gz + read_liberty -corner $corner \ + asap7/asap7sc7p5t_SIMPLE_${vt}_FF_nldm_211120.lib.gz + read_liberty -corner $corner asap7/asap7sc7p5t_SEQ_${vt}_FF_nldm_220123.lib + } +} + +read_lef asap7/asap7_tech_1x_201209.lef +read_lef asap7/asap7sc7p5t_28_R_1x_220121a.lef +read_lef asap7/asap7sc7p5t_28_L_1x_220121a.lef +read_lef asap7/asap7sc7p5t_28_SL_1x_220121a.lef +read_def gcd_asap7_placed.def +read_sdc gcd.sdc +source asap7/setRC.tcl +set_wire_rc -corner slow -signal -resistance 6.46302E-02 -capacitance 3.46646E-01 +estimate_parasitics -placement + +repair_timing -setup \ + -phases PARALLEL_EVAL \ + -sequence vt_swap \ + -max_iterations 2 \ + -verbose diff --git a/src/rsz/test/repair_timing_fixed.ok b/src/rsz/test/repair_timing_fixed.ok index 32b76f98747..abbd4a8ad07 100644 --- a/src/rsz/test/repair_timing_fixed.ok +++ b/src/rsz/test/repair_timing_fixed.ok @@ -8,5 +8,5 @@ [INFO ODB-0130] Created 54 pins. [INFO ODB-0131] Created 470 components and 2159 component-terminals. [INFO ODB-0133] Created 416 nets and 1216 connections. -Summary 18 / 18 (100% pass) +Summary 23 / 23 (100% pass) pass diff --git a/src/rsz/test/repair_timing_fixed.tcl b/src/rsz/test/repair_timing_fixed.tcl index 392b6431889..6e74778f7f1 100644 --- a/src/rsz/test/repair_timing_fixed.tcl +++ b/src/rsz/test/repair_timing_fixed.tcl @@ -110,6 +110,16 @@ check "repair_timing still sizes PLACED instances" \ check "repair_timing still swaps pins of FIRM instances" \ { expr { [changed $snap pins FIRM] > 0 } } 1 +# PARALLEL_EVAL keeps the footprint of a FIRM instance the same way. +set snap [snapshot] +run {repair_timing -setup -phases PARALLEL_EVAL -max_iterations 20 \ + -sequence vt_swap,sizeup} +check "PARALLEL_EVAL sizes no FIRM instance" { changed $snap width FIRM } 0 +check "PARALLEL_EVAL moves no FIRM instance" { changed $snap location FIRM } 0 +check "PARALLEL_EVAL keeps every FIRM instance FIRM" { changed $snap status FIRM } 0 +check "PARALLEL_EVAL still sizes PLACED instances" \ + { expr { [changed $snap width PLACED] > 0 } } 1 + create_clock -name core_clock -period 400 [get_ports clk] set snap [snapshot] run {repair_timing -recover_power 100} diff --git a/src/rsz/test/rsz_aux.py b/src/rsz/test/rsz_aux.py index dc6d5d5e8e5..5a6bcf77bfd 100644 --- a/src/rsz/test/rsz_aux.py +++ b/src/rsz/test/rsz_aux.py @@ -95,6 +95,7 @@ def repair_timing( skip_last_gasp=False, skip_vt_swap=False, skip_crit_vt_swap=False, + effort="high", repair_tns=100, max_passes=10000, max_iterations=-1, @@ -120,6 +121,8 @@ def repair_timing( _error(utl.RSZ, 1030, "repair_tns must be between 0 and 100.") if not (0 <= max_buffer_percent <= 100): _error(utl.RSZ, 1031, "max_buffer_percent must be between 0 and 100.") + if effort not in ("low", "medium", "high"): + _error(utl.RSZ, 1034, "effort must be low, medium or high.") if recover_power is not None: if not (0 <= recover_power <= 100): @@ -153,6 +156,7 @@ def repair_timing( skip_last_gasp, skip_vt_swap, skip_crit_vt_swap, + effort, ) if hold: