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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/cts/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,25 @@ package(
features = ["layering_check"],
)

cc_library(
name = "bottom_up_buffering",
srcs = [
"src/BottomUpBuffering.cpp",
"src/BufferingSearch.cpp",
],
hdrs = [
"src/BottomUpBuffering.h",
"src/BufferingSearch.h",
],
includes = ["src"],
)

cc_library(
name = "cts_impl",
srcs = [
"src/Clock.cpp",
"src/Clustering.cpp",
"src/CtsBuffering.cpp",
"src/CtsOptions.cpp",
"src/HTreeBuilder.cpp",
"src/LatencyBalancer.cpp",
Expand Down Expand Up @@ -44,6 +58,7 @@ cc_library(
],
visibility = ["//src/cts:__subpackages__"],
deps = [
":bottom_up_buffering",
"//src/dbSta",
"//src/dbSta:dbNetwork",
"//src/est",
Expand Down
52 changes: 52 additions & 0 deletions src/cts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,15 @@ clock_tree_synthesis
[-delay_buffer_derate derate_value]
[-library liberty_library_name]
[-repair_clock_nets]
[-bottom_up_buffering]
[-no_insertion_delay]
```

#### Options

| Switch Name | Description |
| ----- | ----- |
| `-bottom_up_buffering` | Use opt-in, bounded bottom-up buffering on the top-down H-tree. Compare wire-only and buffered alternatives using actual downstream load, rise/fall delay, skew, depth, and area, retaining the feasible legacy plan as a reference. Check the complete clock tree for max-cap and max-slew violations after latency balancing. Dummy-load policy is unchanged. Buffer masters must have equal height. |
| `-buf_list` | Tcl list of master cells (buffers) that will be considered when making the wire segments (e.g. `{BUFXX, BUFYY}`). |
| `-root_buffer` | The master cell of the buffer that serves as root for the clock tree. If this parameter is omitted, the first master cell from `-buf_list` is taken. |
| `-wire_unit` | Minimum unit distance between buffers for a specific wire. If this parameter is omitted, the code gets the value from ten times the height of `-root_buffer`. |
Expand All @@ -100,6 +102,56 @@ clock_tree_synthesis
| `-repair_clock_nets` | This option includes fixing long wires inside CTS prior to latency adjustment with delay buffers. This can lead to a more balanced clock tree. The default is not to perform clock net repair. |
| `-no_insertion_delay` | Ignore sink insertion delay in clock tree construction and balancing. |

#### Bottom-Up Buffering

`-bottom_up_buffering` is an experimental bounded search, not a globally optimal
skew, area, or placement solver. Normal top-down H-tree construction supplies a
legacy reference and legal insertion sites. Buffering is then selected separately
in postorder. Sites may be wire-only junctions: removing a buffer preserves all
downstream wire edges and capacitance, rather than treating that junction as a
zero-capacitance cell. A buffered child exposes its selected input capacitance,
not capacitance hidden behind the buffer. Bounded electrical expansion adds
repeater sites when needed for load, fanout, wire length, or slew.

At most 16 alternatives per site retain input load, rise/fall arrival ranges in
each scene, depth, buffer count, and area. Delay and skew rank ahead of depth,
count, and area; minimum depth alone does not determine a winner. The independently
evaluated feasible legacy plan is outside pruning and remains unchanged if chosen.
A replacement must not worsen that reference's modeled latest arrival or arrival
spread in any scene/transition. Explicit root, tree, and sink cell choices remain
fixed. Complete candidate evaluation includes the existing dummy-load selection
policy, unless `-dont_use_dummy_load` was requested.

The original-site search runs before greedy electrical expansion. A feasible
original-site solution survives expansion failure, and an independently verified
complete electrical seed survives frontier pruning. Wire-only states that already
exceed the wire-length limit are discarded before combining further branches.

A positive characterized maximum wire length bounds the search; zero means no
wire-length threshold, as in the H-tree builder. An explicit
`-distance_between_buffers` adds its simple-segment spacing constraint regardless
of that threshold. The default 100-micron heuristic is not a hard span limit
on an ordinary LUT-built H-tree. Reference rejection diagnostics identify the
site, constraint, load, fanout, and span; accepted-reference timing logs are
surrogate values, not STA measurements.

Timing uses existing Liberty/Resizer characterization with a conservative common
input-slew budget and placement star-wire RC, not extracted routing parasitics.
The comparison is a model safeguard, not a post-route QoR guarantee. Every loaded
scene must provide positive finite horizontal and
vertical resistance and capacitance for both clock and signal wires. The model
bounds all of these values, including signal RC used by register-terminal nets.
Infeasible loads, non-progressing legalization, a 64-level depth
limit, or a stage count exceeding 64 times the initial node count cause an error.
Pre-write planning errors undo gate cloning and macro/register splitting using
an OpenDB ECO journal, remove callback ownership, and clear transient run state
so that the command can be retried. Configuration changed during setup, including
wire units and inferred characterization/clustering values, is restored on errors.
Successful-run reset behavior is unchanged. The final audit starts only at actual tree
roots and requires coverage of every original sink and every generated subnet.
The final electrical audit runs after latency balancing; an audit failure leaves
the generated tree in the database for diagnosis. Routed timing must still be checked.

#### Instance Name Prefixes

`clock_tree_synthesis` uses the following prefixes for the instances that it inserts:
Expand Down
15 changes: 15 additions & 0 deletions src/cts/include/cts/TritonCTS.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ struct Tree;
} // namespace stt

namespace cts {
struct BufferingAudit;
struct BufferingWireRC;

class ClockInst;
class CtsOptions;
Expand Down Expand Up @@ -77,6 +79,9 @@ class TritonCTS
void setSinkBuffer(const char* buffers);

private:
friend class BottomUpClockTreeTest;
void resetRunState();
void runClockTreeSynthesis();
std::string selectRootBuffer(std::vector<std::string>& buffers);
std::string selectSinkBuffer(std::vector<std::string>& buffers);
std::string selectBestMaxCapBuffer(const std::vector<std::string>& buffers,
Expand All @@ -95,6 +100,13 @@ class TritonCTS
void checkCharacterization();
void findClockRoots();
void buildClockTrees();
void bufferClockTreeBottomUp(TreeBuilder* builder);
BufferingAudit checkClockTreeElectricals();
void checkClockPinElectricals(const sta::Pin* pin,
bool driver,
BufferingAudit& audit);
void finishClockTreeElectricals(BufferingAudit& audit);
BufferingWireRC validateBottomUpWireRC();
void writeDataToDb();

// NDR functions
Expand Down Expand Up @@ -203,6 +215,9 @@ class TritonCTS
std::unordered_set<odb::dbInst*>& dummies);
bool computeIdealOutputCaps(Clock& clockNet);
void findCandidateDummyCells(std::vector<sta::LibertyCell*>& dummyCandidates);
static sta::LibertyCell* findBestDummyCell(
const std::vector<sta::LibertyCell*>& dummyCandidates,
float deltaCap);
odb::dbInst* insertDummyCell(
Clock& clockNet,
ClockInst* inst,
Expand Down
220 changes: 220 additions & 0 deletions src/cts/src/BottomUpBuffering.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026, The OpenROAD Authors

#include "BottomUpBuffering.h"

#include <algorithm>
#include <cmath>
#include <functional>
#include <stdexcept>
#include <utility>
#include <vector>

namespace cts {
namespace {
double distance(BufferingPoint a, BufferingPoint b)
{
return std::abs(a.x - b.x) + std::abs(a.y - b.y);
}

BufferingPoint midpoint(BufferingPoint a, BufferingPoint b)
{
// Stay on a Manhattan path, rather than shortening a diagonal route.
const double half = distance(a, b) / 2;
const double dx = std::min(half, std::abs(a.x - b.x));
a.x += std::copysign(dx, b.x - a.x);
a.y += std::copysign(half - dx, b.y - a.y);
return a;
}
} // namespace

BufferingWireRC conservativeBufferingRC(std::span<const BufferingWireRC> rc)
{
if (rc.empty()) {
throw std::invalid_argument("No RC scenes available for bottom-up CTS");
}
BufferingWireRC bound;
for (const auto& value : rc) {
if (!std::isfinite(value.resistance) || value.resistance <= 0
|| !std::isfinite(value.capacitance) || value.capacitance <= 0) {
throw std::invalid_argument(
"Bottom-up CTS requires positive finite wire RC");
}
bound.resistance = std::max(bound.resistance, value.resistance);
bound.capacitance = std::max(bound.capacitance, value.capacitance);
}
return bound;
}

BottomUpBuffering::BottomUpBuffering(double cap_per_dbu,
double max_wire_length,
Selector selector,
Legalizer legalizer)
: cap_per_dbu_(cap_per_dbu),
max_wire_length_(max_wire_length),
selector_(std::move(selector)),
legalizer_(std::move(legalizer))
{
if (!std::isfinite(cap_per_dbu) || cap_per_dbu < 0
|| std::isnan(max_wire_length) || max_wire_length <= 0 || !selector_) {
throw std::invalid_argument("Invalid bottom-up buffering parameters");
}
}

BufferingLoad BottomUpBuffering::load(const std::vector<BufferingNode>& nodes,
size_t driver) const
{
BufferingLoad result;
const auto& node = nodes.at(driver);
result.fanout = node.children.size();
for (const size_t child : node.children) {
const auto& sink = nodes.at(child);
const double length = distance(node.point, sink.point);
result.pin_cap += sink.input_cap;
result.wire_cap += length * cap_per_dbu_;
result.max_wire_length = std::max(result.max_wire_length, length);
}
return result;
}

std::vector<BufferingNode> BottomUpBuffering::run(
std::vector<BufferingNode> nodes,
size_t root)
{
expansion_ = {};
if (nodes.empty() || root >= nodes.size()) {
throw std::invalid_argument("Bottom-up buffering requires a root");
}
std::vector<bool> visited(nodes.size(), false);
std::vector<size_t> pending{root};
while (!pending.empty()) {
const size_t index = pending.back();
pending.pop_back();
if (index >= nodes.size() || visited[index]) {
throw std::invalid_argument("Bottom-up buffering requires a tree");
}
visited[index] = true;
const auto& node = nodes[index];
if (!std::isfinite(node.point.x) || !std::isfinite(node.point.y)
|| !std::isfinite(node.input_cap) || node.input_cap < 0
|| (node.sink && !node.children.empty())
|| (!node.sink && node.children.empty())) {
throw std::invalid_argument("Invalid bottom-up buffering node");
}
for (const size_t child : node.children) {
pending.push_back(child);
}
}
if (std::ranges::find(visited, false) != visited.end()) {
throw std::invalid_argument("Unconnected bottom-up buffering node");
}
node_limit_ = nodes.size() * 64;
nodes_ = std::move(nodes);
buffer(root, 0);
return std::move(nodes_);
}

void BottomUpBuffering::buffer(size_t index, unsigned depth)
{
if (depth >= 64) {
throw std::runtime_error("Bottom-up buffering exceeded its depth limit");
}
if (nodes_[index].sink) {
return;
}
// Insertion can grow nodes_, so never keep a reference across recursion.
const auto children = nodes_[index].children;
for (const size_t child : children) {
buffer(child, depth + 1);
}
const auto driven_load = load(nodes_, index);
if (driven_load.max_wire_length <= max_wire_length_) {
if (auto choice = selector_(driven_load, nodes_[index].fixed_master)) {
if (choice->master.empty() || !std::isfinite(choice->input_cap)
|| choice->input_cap < 0) {
throw std::runtime_error("Invalid bottom-up buffering cell choice");
}
nodes_[index].master = choice->master;
nodes_[index].input_cap = choice->input_cap;
return;
}
}
const bool length = driven_load.max_wire_length > max_wire_length_;
if (expansion_.length_splits + expansion_.electrical_splits == 0) {
expansion_.first_node = index;
expansion_.first_length = length;
expansion_.first_load = driven_load;
}
if (length) {
++expansion_.length_splits;
} else {
++expansion_.electrical_splits;
}
split(index, depth);
}

size_t BottomUpBuffering::addStage(BufferingPoint point,
std::vector<size_t> children)
{
if (nodes_.size() >= node_limit_) {
throw std::runtime_error("Bottom-up buffering exceeded its stage limit");
}
if (legalizer_) {
point = legalizer_(point);
}
if (!std::isfinite(point.x) || !std::isfinite(point.y)) {
throw std::runtime_error("Invalid bottom-up buffer location");
}
BufferingNode node;
node.point = point;
node.children = std::move(children);
nodes_.push_back(std::move(node));
return nodes_.size() - 1;
}

void BottomUpBuffering::split(size_t index, unsigned depth)
{
const auto point = nodes_[index].point;
auto children = nodes_[index].children;
const auto longest
= std::ranges::max_element(children, {}, [&](size_t child) {
return distance(point, nodes_[child].point);
});
const size_t farthest = *longest;
const double length = distance(point, nodes_[farthest].point);
if (children.size() == 1 || length > max_wire_length_) {
// Physical placements use integer DBU; smaller spans cannot be split.
if (length < 1) {
throw std::runtime_error("No CTS buffer can drive a sink's capacitance");
}
Comment on lines +187 to +189

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The condition length < 1 uses a magic number 1 which may not be appropriate if the units are not in microns or if a very small distance is valid. If length is 0.5 dbu, this will throw an error, which seems incorrect. This should likely be length <= 0 or based on a defined minimum distance threshold.

const size_t stage
= addStage(midpoint(point, nodes_[farthest].point), {farthest});
*longest = stage;
nodes_[index].children = std::move(children);
} else {
// Split excessive fanout/load spatially. Keep the original H-tree
// branch point, adding only electrical stages below it.
std::ranges::sort(children, [&](size_t a, size_t b) {
const auto pa = nodes_[a].point;
const auto pb = nodes_[b].point;
return pa.x == pb.x ? pa.y < pb.y : pa.x < pb.x;
});
std::vector<size_t> stages;
const size_t middle = children.size() / 2;
for (const auto& [begin, end] :
{std::pair<size_t, size_t>{0, middle},
std::pair<size_t, size_t>{middle, children.size()}}) {
BufferingPoint center;
for (size_t i = begin; i < end; ++i) {
center.x += nodes_[children[i]].point.x / (end - begin);
center.y += nodes_[children[i]].point.y / (end - begin);
}
stages.push_back(
addStage(center, {children.begin() + begin, children.begin() + end}));
}
nodes_[index].children = std::move(stages);
}
buffer(index, depth + 1);
}

} // namespace cts
Loading
Loading