[cts] -useful_skew: add a test to document and enforce the intent at block boundaries

Open
#12,088 2 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@precisionmoon is already working on this.

Since Oct 31, 2026.

Assessment

This issue has not been assessed yet.

Description

cts documentation question rsz

I asked Claude to make a report on CTS useful skew on XiangShan and ask for the intended behavior to have a doc-as-test for pins.

Could a test be added that documents and enforces what clock_tree_synthesis -useful_skew (#11930) is meant to do for a register that also launches a path to an output port under set_max_delay? On a hardened block, every boundary register is such a register.

A test like that is gold for AI and humans alike: it states the intent clearly and formally, right next to the code, and CI keeps it up to date automatically. It goes well with the prose in the README.

I don't know if the repair is working as intended here or not, but the result for CTS today is that such a register gets no useful skew at all. On origin/master 18f08d7ead3eb2013ab840146a2b3e28d531ef32, save the script at the bottom as src/cts/test/useful_skew_port.tcl and run it there with your own build:

openroad -exit useful_skew_port.tcl

It is useful_skew.tcl's design unchanged, plus one output port po driven by r2_0 under set_max_delay 0.01, a budget no clock schedule can meet. Chain 1 is the same chain without the port:

[INFO RSZ-0603] compute_skew_budget: closure infeasible by skew alone; achievable WNS = -0.206.
[INFO CTS-0545] inserted 15 delay buffers for applying useful skew
r1->r2 slack, chain 0 (r2_0 drives po): -218 -> -190 ps
r1->r2 slack, chain 1 (no port):        -218 -> -30 ps

reproducer

Without the port (useful_skew.tcl as is), all eight chains reach about -35 ps. With the same port on r3_0 instead, a register the schedule does not need to delay, every chain still gets its skew.

As far as I can read the code, this follows from the formulation, which is why I'm asking about the intent rather than calling it a bug. Phase 1 maximises the worst slack epsilon over all edges, and phase 2 minimises the total shift while keeping every edge at epsilon or better:

https://github.com/The-OpenROAD-Project/OpenROAD/blob/18f08d7ead3eb2013ab840146a2b3e28d531ef32/src/rsz/src/SkewBudget.hh#L40-L46

A path to a port is an edge to the ground register, fixed at t = 0:

https://github.com/The-OpenROAD-Project/OpenROAD/blob/18f08d7ead3eb2013ab840146a2b3e28d531ef32/src/rsz/src/Resizer.cc#L5871-L5877

and CTS asks for a one-sided schedule, delays only:

https://github.com/The-OpenROAD-Project/OpenROAD/blob/18f08d7ead3eb2013ab840146a2b3e28d531ef32/src/cts/src/TritonCTS.cpp#L2858-L2863

So the r2_0 -> po edge gives epsilon <= slack_po - t(r2_0). It is the worst edge, so it sets epsilon, and phase 2 can then only keep t(r2_0) = 0. The clock-gating checks are frozen to don't-worsen for a related reason; port edges are not, and freezing them would not let r2_0 move either:

https://github.com/The-OpenROAD-Project/OpenROAD/blob/18f08d7ead3eb2013ab840146a2b3e28d531ef32/src/rsz/src/Resizer.cc#L6211

The two cases the test would settle: a violating boundary budget on a register that skew would delay (held at zero skew, or traded?), and the boundary constraint style to use for blocks built with -useful_skew.


Background on where this came from. XiangShan is moving along, so this is a snapshot from 2026-10-31; for the latest, see https://github.com/The-OpenROAD-Project/bazel-orfs/tree/main/test/coremark_joule/designs/asap7/xiangshan

We are taking XiangShan's tile, an open-source RISC-V core, through ORFS on asap7 with bazel-orfs: about 8.6 M standard cells in a parent and five hardened blocks. Turnaround time is QoR here. Setting up a design like this and iterating on its problems takes on the order of 100 builds, and one build is about 9 hours (blocks 3.8 h, then the parent 4.9 h), so in practice you watch one a day: 100 builds is 100 days. An AI loop needs turnaround in minutes to hours, and that is why the design is broken into macros; a flat repair_design on the core alone ran 6.5 hours and did not finish.

With macros, CTS is where it gets hard. Every block port carries a set_max_delay budget, and a one-sided schedule can only help a path that crosses a block boundary from its capture side. The block's schedule and the parent's are separate runs, so the budgets are the only way the two sides coordinate, and tuning them is a loop of rebuilds that takes a long time to converge at this turnaround.

crossing

Measured at global route, reg2reg period and the mass of endpoints over a 2,000 ps target, one run each so a few percent may be noise:

no useful skew -useful_skew in the parent
period 2,206 ps 2,315 ps
endpoints over 2,000 ps 1,377 1,266
excess over 2,000 ps 122,800 ps 119,600 ps
useful-skew delay buffers 0 60,567
RSZ-0603 achievable WNS -4,604 ps (worst reg2reg after global route: -1,842 ps)

The worst path got 109 ps worse, and it ends at a block's input pin. Most of what remains over target is in the parent's own paths:

excess

# What does clock_tree_synthesis -useful_skew do for a register that also
# launches a path to an output port under set_max_delay, as every boundary
# register of a hardened block does?
#
# useful_skew.tcl's design, unchanged: eight chains r1 -> (16 buffers) ->
# r2 -> r3, where delaying r2's clock helps the tight r1->r2 stage, and a
# macro so CTS builds the hierarchical tree the skew pass needs. Added: r2_0,
# and only r2_0, also drives output port po under set_max_delay 0.01, a
# budget no clock schedule can meet. Chain 1 is identical without the port.
#
# Save in src/cts/test and run there: openroad -exit useful_skew_port.tcl
source "helpers.tcl"
source Nangate45/Nangate45.vars

read_liberty $liberty_file
read_liberty array_tile_ins_delay.lib
read_lef Nangate45/Nangate45.lef
read_lef array_tile_ins_delay.lef

set num_chains 8
set chain_len 16
set clk_period 0.30
set row_pitch 6

set db [ord::get_db]
set tech [ord::get_db_tech]
set chip [odb::dbChip_create $db $tech]
set block [odb::dbBlock_create $chip "useful_skew_top"]
$block setDefUnits 2000
set dbu [$tech getDbUnitsPerMicron]

set dff [$db findMaster "DFF_X1"]
set buf [$db findMaster "BUF_X1"]

# Clock input port/net.
set clk [odb::dbNet_create $block "clk"]
$clk setSigType CLOCK
set clk_bterm [odb::dbBTerm_create $clk "clk"]
$clk_bterm setSigType CLOCK
$clk_bterm setIoType INPUT
set clk_bpin [odb::dbBPin_create $clk_bterm]
$clk_bpin setPlacementStatus PLACED
odb::dbBox_create $clk_bpin [$tech findLayer "metal5"] 0 0 2000 2000

proc place_inst { inst x_um y_um dbu } {
  $inst setLocation [expr int($x_um * $dbu)] [expr int($y_um * $dbu)]
  $inst setPlacementStatus PLACED
}

# Register pipelines with an asymmetric (tight r1->r2, loose r2->r3) slack.
for { set c 0 } { $c < $num_chains } { incr c } {
  set yy [expr 2 + $c * $row_pitch]
  set r1 [odb::dbInst_create $block $dff "r1_$c"]
  set r2 [odb::dbInst_create $block $dff "r2_$c"]
  set r3 [odb::dbInst_create $block $dff "r3_$c"]
  place_inst $r1 10 $yy $dbu
  place_inst $r2 185 $yy $dbu
  place_inst $r3 192 $yy $dbu
  foreach r [list $r1 $r2 $r3] {
    [$r findITerm "CK"] connect $clk
  }

  # Tight stage: r1.Q -> chain of buffers -> r2.D
  set prev [$r1 findITerm "Q"]
  for { set i 0 } { $i < $chain_len } { incr i } {
    set b [odb::dbInst_create $block $buf "u_${c}_$i"]
    place_inst $b [expr 20 + $i * 10] $yy $dbu
    set net [odb::dbNet_create $block "s1_${c}_$i"]
    $prev connect $net
    [$b findITerm "A"] connect $net
    set prev [$b findITerm "Z"]
  }
  set net [odb::dbNet_create $block "s1l_$c"]
  $prev connect $net
  [$r2 findITerm "D"] connect $net

  # Loose stage: r2.Q -> r3.D
  set net [odb::dbNet_create $block "s2_$c"]
  [$r2 findITerm "Q"] connect $net
  [$r3 findITerm "D"] connect $net
}

# Macro sink (insertion delay) so CTS builds a hierarchical clock tree.
set tile [$db findMaster "array_tile"]
set macro [odb::dbInst_create $block $tile "macro0"]
place_inst $macro 40 [expr 6 + $num_chains * $row_pitch] $dbu
[$macro findITerm "clk"] connect $clk

# The only change to useful_skew.tcl's design: r2_0 also drives port po.
set po_net [odb::dbNet_create $block "po"]
set po_bterm [odb::dbBTerm_create $po_net "po"]
$po_bterm setIoType OUTPUT
set po_bpin [odb::dbBPin_create $po_bterm]
$po_bpin setPlacementStatus PLACED
odb::dbBox_create $po_bpin [$tech findLayer "metal5"] 398000 0 400000 2000
[[$block findInst "r2_0"] findITerm "Q"] connect $po_net

ord::design_created

set die_h [expr 4 + $num_chains * $row_pitch + 80]
initialize_floorplan \
  -die_area [list 0 0 200 $die_h] \
  -core_area [list 0 0 200 $die_h] \
  -site $site
source $tracks_file

source Nangate45/Nangate45.rc
create_clock -name clk -period $clk_period clk
set_max_delay 0.01 -to [get_ports po]
set_wire_rc -signal -layer $wire_rc_layer
set_wire_rc -clock -layer $wire_rc_layer_clk
estimate_parasitics -placement

proc stage_slack { c } {
  # the tight r1_c -> r2_c stage
  return [get_property [find_timing_paths -path_delay max -to [get_pins r2_$c/D]] slack]
}
set before0 [stage_slack 0]
set before1 [stage_slack 1]

set_cts_config -root_buf CLKBUF_X3 \
  -buf_list CLKBUF_X3 \
  -sink_clustering_size 8 \
  -sink_clustering_max_diameter 60

clock_tree_synthesis -sink_clustering_enable -useful_skew

set_propagated_clock [all_clocks]
estimate_parasitics -placement
# picoseconds; the library's time unit is ns
puts [format "r1->r2 slack, chain 0 (r2_0 drives po): %.0f -> %.0f ps" [expr { $before0 * 1e3 }] [expr { [stage_slack 0] * 1e3 }]]
puts [format "r1->r2 slack, chain 1 (no port):        %.0f -> %.0f ps" [expr { $before1 * 1e3 }] [expr { [stage_slack 1] * 1e3 }]]
Dominant language
Verilog
Stars
3.2k
Forks
1.1k
Avg merge
2d 9h
Merged PRs (30d)
276

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from The-OpenROAD-Project/OpenROAD

All issues in The-OpenROAD-Project/OpenROAD

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.