gpl: concurrent IO pin + cell placement in global placement - #11134
gpl: concurrent IO pin + cell placement in global placement#11134minjukim55 wants to merge 4 commits into
Conversation
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>
There was a problem hiding this comment.
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.
- 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 |
There was a problem hiding this comment.
Main fix - Make IO pin virtual cell
| continue; | ||
| } | ||
| // IO pins use wirelength gradients only. | ||
| densityGrads[i] = FloatPoint(0, 0); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
|
@codex review |
There was a problem hiding this comment.
💡 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".
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>
IO ports become variables of the same Nesterov solve as the cells, instead of fixed anchors placed beforehand.
-place_ioswrites the solved pin locations to the DB. It does not snap them to track slots —place_pinsstill owns assignment, layer choice and legalization. Same contract asdetailed_placementafterglobal_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"] endAandBpick the pin arrangement before the cells have converged.Cremoves 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.
gp -place_iosµ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 command —
global_placementfor the first two rows,place_pinsfor-place_ios. Per command:gp -place_iosgp -skip_io/gp -place_iosplace_pinsgp(cells against fixed pins)detailed_placementoptimize_mirroring† not a placement-quality number: the cells or the pins have not been placed yet at that point.
For
-place_iosthe solve lands at 17,621;place_pinscosts +5.1% snapping to legal track slots,detailed_placement+5.4%,optimize_mirroringgives back −1.3%.place_pinsnever re-solves — it quantizes what the solve already decided. In the ORFS default arm the same command lands at 23,740 and the secondglobal_placementhas to spend a full solve pulling it back to 20,895.Two smaller designs (54 and 390 pins) tie or lose slightly. Benefit tracks pin count and perimeter occupancy.
Flow note — give
place_pinsseveral layers per direction:place_pinslayersHow a pin becomes a variable
Two lines. The port's existing
GPinis re-linked to a newGCell.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"] endNo 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.
One representation, three cases:
exclude_io_pin_regionintervalset_io_pin_constraint -regionMirrored pairs: one degree of freedom
The follower is the exact reflection — no relaxation.
place_pinstreats it as a hard constraint and errors (PPL-82) if the mirrored position is not a free slot, soinitIoConstraints()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
-density, filler count and the overflow convergence point stay untouchedgetDistance())GPL-0176), CPU wirelength/HPWL backendsDeviceStateis built fromgCellStor_and does not modelioPinStor_.indexOfGCell()also gained a bounds check so an out-of-pool GCell resolves to-1instead of a garbage indexPorts 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 forplace_pins. So an unplaced port is a normal state during global placement, andGPL-326no longer fires for one under-place_ios.Scope: rejected at the Tcl layer
-timing_drivenGPL-0179place_pinslater moves-routability_drivenGPL-0181-incrementalGPL-0170-skip_ioGPL-0169-skip_nesterov_placeGPL-0182Next phase: timing-driven support
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,GPinre-linking after storage reallocation, handle validation. Large enough to stand alone.Also not in this PR
place_pins -displacement(min-L1 assignment)place_pins— preserving the solve's pin ordering is worse than letting the exact Hungarian re-choose itsolve()truncates toint, already negative at 1,546 pins)place_pinscan choose it. 9 formulations compared; IO wirelength lost per unit of cell wirelength gained never fell below 1.59Testing
-place_iospass/fail tests: basic, mirrored pins, region constraints, blockage-reduced slots.OMP_NUM_THREADS1 / 4 / 16. The mirror-pair gradient is snapshotted before the parallel loop for exactly this reason.check_placementclean; 0 duplicate slots, 0 off-edge pins, 0 mirror mismatches.Diff
16 files, +786 / −38, all under
src/gpl. Only 5 removed lines are innesterovBase.cpp: theisFiller()discriminator, a ctor initializer list, one core-box clamp that must not apply to perimeter pins, and the twogetDistance()call sites.