Skip to content

gpl: concurrent IO pin + cell placement in global placement - #11134

Open
minjukim55 wants to merge 4 commits into
The-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:secure-concurrent-io-e5
Open

gpl: concurrent IO pin + cell placement in global placement#11134
minjukim55 wants to merge 4 commits into
The-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:secure-concurrent-io-e5

Conversation

@minjukim55

Copy link
Copy Markdown
Contributor

IO ports become variables of the same Nesterov solve as the cells, instead of fixed anchors placed beforehand.

global_placement -place_ios
place_pins -hor_layers "M4 M6 M8" -ver_layers "M3 M5"

-place_ios writes the solved pin locations to the DB. It does not snap them to track slots — place_pins still owns assignment, layer choice and legalization. Same contract as detailed_placement after global_placement.

Without the flag: bit-identical to master.


Flows compared

flowchart LR
  subgraph C["gp -place_ios  (this PR)"]
    direction TB
    C1["global_placement -place_ios<br/>cells + pins, one solve"] --> C2["place_pins"]
  end
  subgraph B["ORFS default"]
    direction TB
    B1["global_placement -skip_io"] --> B2["place_pins"] --> B3["global_placement"]
  end
  subgraph A["pins fixed up front"]
    direction TB
    A1["place_pins + manual place_pin"] --> A2["global_placement"]
  end
Loading

A and B pick the pin arrangement before the cells have converged. C removes that ordering dependency, and runs global placement once instead of twice.


Results

Customer design · 1,546 IO pins · 7nm · same floorplan, constraints, options and binary in all three arms.

flow GP HPWL DP HPWL vs. default io_x int_x mean_dev
pins fixed up front 23,215 23,661 +10.3% 8,344 3,912 4.83
ORFS default 20,895 21,449 6,982 3,176 3.74
gp -place_ios 18,525 19,267 −10.2% 3,909 4,088 1.81

µm. io_x / int_x = horizontal HPWL of IO nets / cell-only nets. mean_dev = mean pin distance from its net's median.

GP HPWL is measured after each flow's last placement commandglobal_placement for the first two rows, place_pins for -place_ios. Per command:

after pins fixed up front ORFS default gp -place_ios
gp -skip_io / gp -place_ios 6,699 † 17,621
place_pins 67,335 † 23,740 18,525
gp (cells against fixed pins) 23,215 20,895
detailed_placement 24,209 21,875 19,517
optimize_mirroring 23,661 21,449 19,267

† not a placement-quality number: the cells or the pins have not been placed yet at that point.

For -place_ios the solve lands at 17,621; place_pins costs +5.1% snapping to legal track slots, detailed_placement +5.4%, optimize_mirroring gives back −1.3%. place_pins never re-solves — it quantizes what the solve already decided. In the ORFS default arm the same command lands at 23,740 and the second global_placement has to spend a full solve pulling it back to 20,895.

IO nets   6,982 ──────────────────────────────► 3,909    −44%   ✅
cells     3,176 ──────────────────────────────► 4,088    +29%   ❌
                                                          net  −10.2%

Two smaller designs (54 and 390 pins) tie or lose slightly. Benefit tracks pin count and perimeter occupancy.

Flow note — give place_pins several layers per direction:

place_pins layers legalization displacement per pin
3 hor + 2 ver 4,280,044 dbu 1.38 µm
1 hor + 1 ver 20,825,853 dbu 6.74 µm

How a pin becomes a variable

Two lines. The port's existing GPin is re-linked to a new GCell.

flowchart LR
  subgraph after["after"]
    P2["GPin(port)"] -- "gCell_" --> G2["GCell in ioPinStor_"]
    G2 --> V["WA gradient differentiates it<br/>= a variable"]
  end
  subgraph before["before"]
    P1["GPin(port)"] -- "gCell_ = nullptr" --> T["WA gradient sees a<br/>fixed terminal"]
  end
Loading

No IO-specific gradient code — the unchanged weighted-average implementation does the work.

A pin is one-dimensional

It only slides along the die perimeter. Legal locus = a set of segments (one die edge clipped to an along-edge interval). Every coordinate update projects onto the nearest point of the pin's own locus.

die perimeter, unrolled:

  bottom  ├────────────┤ ├──────────┤     ← exclude_io_pin_region carved out
  right   ├──────────────────────────┤
  top     ├──────┤ ├─────────────────┤
  left    ├──────────────────────────┤

  projection:  one axis pinned to the die edge, the other clamped into [lo, hi]

One representation, three cases:

case locus
unconstrained four edges minus every exclude_io_pin_region interval
set_io_pin_constraint -region that edge+interval, intersected with the above
mirrored pair master's locus ∩ mirror(follower's locus)

Mirrored pairs: one degree of freedom

flowchart TB
  M["master<br/>free variable"] -->|"exact reflection"| F["follower<br/>no DOF"]
  F -->|"wl gradient"| M
  M -->|"÷ summed preconditioner"| S["one shared axis"]
Loading

The follower is the exact reflection — no relaxation. place_pins treats it as a hard constraint and errors (PPL-82) if the mirrored position is not a free slot, so initIoConstraints() intersects the master's locus with the mirror of the follower's up front and errors if they cannot agree.


Three interactions that needed care

what why
Density IO pins get no 2-D density force and contribute no bin area they sit outside the core bin grid; -density, filler count and the overflow convergence point stay untouched
Step length excluded from the Barzilai-Borwein norm (count passed to getDistance()) their displacement is a projection jump, not a gradient step. Including them left overflow oscillating at 0.25–0.43 instead of reaching 0.1
GPU coords stay host-resident (GPL-0176), CPU wirelength/HPWL backends DeviceState is built from gCellStor_ and does not model ioPinStor_. indexOfGCell() also gained a bounds check so an out-of-pool GCell resolves to -1 instead of a garbage index

Ports the solve does not model

Power/ground, and anything not on a signal or clock net, own no GPin → their gradient is identically zero → left for place_pins. So an unplaced port is a normal state during global placement, and GPL-326 no longer fires for one under -place_ios.


Scope: rejected at the Tcl layer

flag id reason
-timing_driven GPL-0179 mid-solve resizer would repair IO nets against positions place_pins later moves
-routability_driven GPL-0181 router would build congestion from pins parked on their solve-time layer
-incremental GPL-0170 starts from locked cells
-skip_io GPL-0169 opposite request
-skip_nesterov_place GPL-0182 skips the solve that moves the pins

Next phase: timing-driven support

flowchart LR
  A["solve"] --> B["legalize + freeze pins"] --> C["final repair_design"] --> D["solve continues"]
Loading

Without B, the resizer repairs against positions that later move. Prototyped and measured: mid-loop buffer count 4,285 → 336 on an 11k-pin design.

Also brings back the bookkeeping that survives nb_gcells_ mutation from buffer insertion and filler cutting — index remapping, GPin re-linking after storage reallocation, handle validation. Large enough to stand alone.

Also not in this PR

item status
routability-driven support needs the same bookkeeping + a layer decision for the router
place_pins -displacement (min-L1 assignment) implemented, measured, lost to plain place_pins — preserving the solve's pin ordering is worse than letting the exact Hungarian re-choose it
two integer-width fixes in ppl's Hungarian path (solve() truncates to int, already negative at 1,546 pins) unrelated; going out separately
pin-to-pin spreading force removed after measurement: clustering is not a legality problem (assignment only needs slots ≥ pins), and any spreading force fixes the ordering before place_pins can choose it. 9 formulations compared; IO wirelength lost per unit of cell wirelength gained never fell below 1.59

Testing

  • Existing gpl / ppl regressions unchanged with the flag off.
  • 4 -place_ios pass/fail tests: basic, mirrored pins, region constraints, blockage-reduced slots.
  • Determinism: identical at OMP_NUM_THREADS 1 / 4 / 16. The mirror-pair gradient is snapshotted before the parallel loop for exactly this reason.
  • check_placement clean; 0 duplicate slots, 0 off-edge pins, 0 mirror mismatches.

Diff

16 files, +786 / −38, all under src/gpl. Only 5 removed lines are in nesterovBase.cpp: the isFiller() discriminator, a ctor initializer list, one core-box clamp that must not apply to perimeter pins, and the two getDistance() call sites.

Add `global_placement -place_ios`, which turns every movable IO port into a
variable of the same Nesterov solve as the cells instead of treating it as a
fixed anchor. The solved pin locations are written to the database; snapping
them onto routing-track slots stays place_pins' job, the same way
detailed_placement follows global_placement for cells.

An IO pin becomes a GCell in NesterovBase::ioPinStor_ and its existing GPin is
re-linked to it, so the unchanged weighted-average wirelength gradient starts
differentiating the pin - no IO-specific gradient code. A pin is confined to
the die perimeter by projecting it, every iteration, onto a set of perimeter
segments that models the unconstrained case, set_io_pin_constraint regions and
exclude_io_pin_region uniformly. Mirrored pairs carry one degree of freedom:
the follower is the exact reflection of its master, and initIoConstraints()
intersects the master's locus with the mirror of the follower's so that
reflection is always legal - ppl treats it as a hard constraint and errors
(PPL-82) otherwise.

Three interactions needed care:

  - IO pins carry no 2-D density force. They sit on the die perimeter, outside
    the core bin grid, and contribute no area to it, so -place_ios does not
    move the cells' overflow convergence point.
  - Their step-to-step displacement is a projection jump rather than a gradient
    step, and the Barzilai-Borwein step length is shared by every GCell, so
    they are excluded from that norm. Including them left overflow oscillating
    at 0.25-0.43 instead of reaching the 0.1 target.
  - The device-resident coordinate, wirelength and HPWL pipelines are built
    from NesterovBaseCommon::gCellStor_ and do not model ioPinStor_, so
    -place_ios keeps them host-resident and forces the CPU backends.

Ports the solve does not model - power/ground, and anything not on a signal or
clock net - own no GPin, so their gradient would be identically zero. They are
left for place_pins to place, which means an unplaced port is a normal state
during global placement and no longer an error (GPL-326).

-place_ios is mutually exclusive with -timing_driven, -routability_driven,
-incremental, -skip_io and -skip_nesterov_place.

Signed-off-by: Minju Kim <mkim@precisioninno.com>
NesterovBase::ioNbPos() called std::vector<GCellHandle>::size() inside the
class body, where GCellHandle is still only forward declared - its definition
follows NesterovBase in the same header. GCC accepted it, clang/libc++ did not:

  vector.h:385:48: error: arithmetic on a pointer to an incomplete type
  'gpl::GCellHandle'

which broke every Bazel target depending on //src/gpl:gpl.

Keep the declaration in the class and move the body below GCellHandle.

Signed-off-by: Minju Kim <mkim@precisioninno.com>
@minjukim55 minjukim55 self-assigned this Aug 13, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request implements concurrent IO pin placement (-place_ios) in the global placement tool (gpl), allowing movable IO pins to be co-optimized with cells. Because device pipelines do not model IO pin GCells, CPU backends are forced when this mode is enabled. The code review identified several important issues: potential undefined behavior in indexOfGCell due to relational pointer comparisons across different pools, a logic bug in initIoConstraints where non-overlapping blocked regions can create out-of-bounds segments, a limitation in rectToPerimSegment that ignores 2D rectangles with non-zero thickness, and a potential division-by-zero vulnerability in getDistance when n is zero.

Comment thread src/gpl/src/gpu/deviceState.cpp Outdated
Comment thread src/gpl/src/nesterovBase.cpp
Comment thread src/gpl/src/nesterovBase.cpp
Comment thread src/gpl/src/nesterovBase.cpp
- indexOfGCell() compared pointers from two different pools with < and >=,
  which is undefined behavior. std::less gives a total order over all
  pointers, so the range check stays valid when the GCell comes from
  ioPinStor_.
- getDistance() divided by n, which is nb_gcells_.size() - ioPinStor_.size()
  and can be zero when every movable object is an IO pin. The resulting NaN
  step length propagates into the whole solve.
- A blocked region that does not overlap the edge span produced a free
  segment reaching past the end of that span. Not reachable today, since
  odb clamps every region into the die area, but the function no longer
  depends on that.
- A constraint region that is not a die-edge interval is a top-layer region:
  the pin belongs on the top-layer grid inside the die, not on the perimeter,
  and ppl makes the same distinction in getConstraintsFromDB(). Such a pin
  was seeded as a movable IO GCell and warned that it would be placed on the
  free perimeter; the solve then dragged it to a die edge and placed the
  cells against a position place_pins discards. Leave those pins to
  place_pins, and reuse GPL-172 to report how many the perimeter model
  skipped.

Signed-off-by: Minju Kim <mkim@precisioninno.com>
std::unordered_map<odb::dbBTerm*, size_t> bterm_to_io_index;
bterm_to_io_index.reserve(ioPinStor_.size());

// Create virtual GCells

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Main fix - Make IO pin virtual cell

continue;
}
// IO pins use wirelength gradients only.
densityGrads[i] = FloatPoint(0, 0);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Density gradient is 0 for now

// A mirror pair has one DOF; add the follower contribution to master.
const DieEdge me = ioEdgeOnLocus(io_i, gCell->dCx(), gCell->dCy());
if (isHorizontalEdge(me)) {
sumGrads[i].x = sumGrads[i].x + fGrad.x;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The mirror follower has no independent degree of freedom since its position is fully determined by the master. Therefore, its wirelength gradient must be accumulated onto the master so that the master update reflects the objective of both pins in the mirror pair.

@minjukim55
minjukim55 marked this pull request as ready for review August 13, 2026 03:43
@minjukim55
minjukim55 requested a review from a team as a code owner August 13, 2026 03:43
@eder-matheus

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6181737974

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gpl/src/nesterovBase.cpp
Comment thread src/gpl/src/nesterovBase.cpp Outdated
Comment thread src/gpl/src/nesterovBase.cpp Outdated
A movable port whose set_io_pin_constraint region is an area rather than a
die-edge interval - `-region up:{llx lly urx ury}`, the define_pin_shape_pattern
grid used for flip-chip bumps - was skipped by initIoPinGCells() and left to
place_pins. Skipping it built no GCell, but its GPin stayed in the Nesterov net
with gCell_ == nullptr, and Pin::updateCoordi() leaves an unplaced BTerm at the
default (0, 0) under -place_ios. The weighted-average wirelength reads every
GPin of a net regardless of GCell, so the deferred port acted as a fixed anchor
at the die origin and pulled its cells into the corner, silently, with no
diagnostic beyond a warning that said place_pins would handle the pin.

Give those ports a GCell too. The solve already carried no IO-specific gradient
- the perimeter is enforced only by projecting the coordinate onto a legal locus
after each step - so a 2D region needs no new gradient path: a box is convex, so
projectIoPin() clamps to it and the projected-gradient step is unchanged.
Seeding is unchanged as well, since seedIoPinGCell() already projects the
connected cells' centroid onto whatever locus the pin has.

Three places did have to learn about the box:

  - The pin has no die edge, so it cannot mirror. Mirroring reflects across an
    edge and the master's chain-rule term picks its axis from that edge, so box
    pins are excluded when the mirror pairs are built. This also keeps
    ioEdgeOnLocus() - which dereferences nearestSegment() unguarded - off a
    locus with no segments.
  - The written BPin belongs on the pin shape pattern's layer with that grid's
    size, not on the dummy horizontal/vertical routing layer picked for
    perimeter pins; a bump written on metal1 in the middle of the core is wrong.
    -place_ios now errors (GPL-183) if such a region exists without a grid.
  - initIoConstraints() leaves them out of the perimeter segment arithmetic.

Two neighboring defects found in the same review:

  - A rectilinear die was accepted even though the locus is built from the four
    edges of its bounding box, so pins could be optimized onto spans outside the
    die. Reject it (GPL-173). The check runs in Replace, before PlacerBaseCommon
    is built, because -place_ios makes an unplaced port a normal state there.
  - exclude_io_pin_region covering the whole perimeter left io_free_segments_
    empty, and the fallback restored the full perimeter - writing pins into
    explicitly blocked regions. Error instead (GPL-180), unless every movable
    pin is box-constrained and the perimeter is unused.

Signed-off-by: Minju Kim <mkim@precisioninno.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants