Skip to content

C++20 placer: 0.0000 overlap, 0.2654 wirelength, 36.10s on tests 1-10 - #64

Open
lgoyal6 wants to merge 2 commits into
partcleda:mainfrom
lgoyal6:cpp-placer
Open

C++20 placer: 0.0000 overlap, 0.2654 wirelength, 36.10s on tests 1-10#64
lgoyal6 wants to merge 2 commits into
partcleda:mainfrom
lgoyal6:cpp-placer

Conversation

@lgoyal6

@lgoyal6lgoyal6 commented Aug 15, 2026

Copy link
Copy Markdown

This does not beat the top row. #1 is 0.2611 and I reached 0.2654, so this is
3rd on the wirelength column, at 36.10s against #1's 50.51s. Posting it anyway
because the approach is different from the entries above it and the writeup
includes the parts where it loses.

What it is

A mixed-size placer in C++20, one translation unit, called from placement.py
through a ctypes shim. No dependencies beyond a C++20 compiler; the library
builds itself on first import. Pipeline is the classic flow: analytic global
placement (Adam on the smoothed objective plus an auto-scaled overlap-area
penalty, uniform-grid neighbour search), then macro placement, then row
legalization, then detailed placement, wrapped in a multi-start over die aspect
ratio and macro arrangement.

The part I think is worth your time

Zero overlap here is structural, not tuned. Every standard cell in the generator
has height exactly 1.0, so they tile a unit-pitch row grid perfectly. Rows cannot
overlap in y, and intervals within a row are kept disjoint by construction, so
the placement cannot produce an overlap regardless of hyperparameters. Before
returning, positions are rounded to float32 (the dtype the harness stores them
in) and re-checked with a sweep-line version of calculate_cells_with_overlaps's
exact predicate; if anything overlaps, the shim raises rather than returning a
bad placement.

Two things I found reading the scoring code, neither of which I patched

  1. The scored objective is Chebyshev, not Manhattan. wirelength_attraction_loss
    costs each edge as alphalog(exp(|dx|/alpha) + exp(|dy|/alpha)) with
    alpha=0.1. The docstring calls it a smooth approximation of Manhattan
    distance, but alpha
    logsumexp(dx/alpha, dy/alpha) approximates max, within
    alpha*ln2 = 0.069 of max(|dx|,|dy|) everywhere. This solver optimises the
    L-inf function you actually score.

  2. Pin offsets are corner-relative while the overlap check is centre-relative.
    Pins land at cell_pos + offset with the offset in [0,w] x [0,h], but the
    overlap check treats cell_pos as the centre, so a cell's pin cloud is its body
    translated by (+w/2, +h/2), and the shift scales with the cell. That is
    exploitable: a small cell can park so its pin cloud lands inside a large
    neighbour's.

I deliberately left both alone. Changing either moves the metric and makes every
existing leaderboard row incomparable. Flagging rather than fixing.

How much of this metric is reachable

Edges are generated between pins, so when both endpoints sit on the same cell the
term is identical under every legal placement. Measured, that floor is 0.0330
averaged over tests 1-10, and it is very uneven: 26.2% of test 1's edges are
intra-cell, giving it a floor of 0.0902, versus 0.0004 for test 10. Against the
reachable part of the score, #1 is at 0.2281 and I am at 0.2326.

Where it loses

Runtime is a dial, not a result: the solver is anytime, so the honest form is the
curve. 0.2849 at 6.96s, 0.2699 at 12.53s, 0.2654 at 36.10s, 0.2653 at 48.16s. It
saturates, so the 0.0045 gap to #1 is algorithmic and not something more time
fixes. My weakest cases are the small macro-dominated ones, tests 1 to 4, at
0.285 to 0.334; my strongest is test 10 at 0.1665. Peak RSS for the whole run is
225 MB, most of it the harness importing torch.

I also implemented overlap_repulsion_loss properly and left the original Adam
loop reachable with PARTCL_SOLVER=torch. With the shipped hyperparameters it
scores 0.4742 overlap and 0.4801 wirelength, so a correct loss function on its
own does not get to zero overlap. The legalizer is what does.

Not attempted: extra credit tests 11 and 12. Test 12 has 100,010 cells and
calculate_cells_with_overlaps is a Python double loop over all pairs, so scoring
it is on the order of 5e9 Python iterations no matter how fast the placer is. A
grid or sweep-line version of that check would make the extra credit cases
practical to score; happy to send one if it is useful.

Reproduce

python run_first10.py

test.py as shipped runs all 12 cases and averages over 12, while the README asks
for the first 10, so run_first10.py reuses test.py's own run_placement_test and
TEST_CASES verbatim and only changes which slice is averaged. Full numbers,
including every axis where this loses, are in cpp_placer/README.md.

This adds cpp_placer, a placement solver written as a single C++20 translation
unit and driven from placement.py through a ctypes shim. It carries no
dependencies beyond a C++20 compiler, and build.sh compiles the shared library
on first import, so the existing Python entry points keep working exactly as
they did and nobody has to run a separate build step before scoring.
The pipeline itself is the conventional one rather than anything exotic.
Analytic global placement runs Adam on a smoothed wirelength objective with an
auto-scaled overlap-area penalty and a uniform grid for neighbour search, and
that is followed by macro placement, row legalization and detailed placement.
The whole sequence is wrapped in a multi-start over die aspect ratio and macro
arrangement, because the objective has enough local minima that a single start
leaves a meaningful amount on the table.
Two observations about the scoring are worth recording for whoever works on
this next, since neither is documented and both change how a solver should be
tuned. The objective the scorer actually computes is Chebyshev distance rather
than the Manhattan distance described in the docstring, so anyone optimising
against the documented metric is improving a different function from the one
being measured. Separately, intra-cell edges contribute a fixed 0.0330 to
every score regardless of where anything is placed, which means the range
genuinely available to a solver is a good deal narrower than the raw
leaderboard numbers make it look.
Measured on tests 1 through 10 using the repository's own scorer from a clean
checkout: 0.0000 overlap, 0.2654 wirelength, 36.10 seconds. That does not beat
the current top row of 0.2611. It places third on the wirelength column while
running faster than both entries above it. run_first10.py reproduces the run
end to end.
This records the result from the previous commit in the README table: 0.0000
overlap, 0.2654 wirelength and 36.10 seconds on tests 1 through 10, all
measured with the repository's own scorer from a clean checkout rather than
from a working tree with local modifications.
The row lands third on the wirelength column. I am submitting it rather than
holding it back until it wins, because the approach differs from the entries
above it, the accompanying write-up is explicit about where it loses as well
as where it does well, and the two scoring observations recorded in the
previous commit are useful to the project independently of where this entry
finishes in the ranking.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@lgoyal6