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
45 changes: 23 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,29 @@ We will review submissions on a rolling basis.
|------|-----------------|-------------|-----------------|-------------|----------------------|
| 1 | Brayden Rudisill | 0.0000 | 0.2611 | 50.51 | Timed on a mac air |
| 2 | manuhalapeth | 0.0000 | 0.2630 | 196.8 | |
| 3 | Neil Teje | 0.0000 | 0.2700 | 24.00s | |
| 4 | Leison Gao | 0.0000 | 0.2796 | 50.14s | |
| 5 | William Pan | 0.0000 | 0.2848 | 155.33s | |
| 6 | Ashmit Dutta | 0.0000 | 0.2870 | 995.58 | Spent my entire morning (12 am - 6 am) doing this :P |
| 7 | Pawan Paleja | 0.0000 | 0.3311 | 1.74s | Implemented hint for loss func, cosine annealing on learning rate with warmup, std annealing on lambda weight. Used optuna to tune hyperparam. Tested on gh codespaces 2-core. |
8 | Shashank Shriram | 0.0000 | 0.3312 | 11.32 | 🏎️💥 |
| 9 | Gabriel Del Monte | 0.0000 | 0.3427 | 606.07 | |
| 10 | Aleksey Valouev| 0.0000 | 0.3577 | 118.98 | |
| 11 | Mohul Shukla | 0.0000 | 0.5048 | 54.60s | |
| 12 | Ryan Hulke | 0.0000 | 0.5226 | 166.24 | |
| 13 | Neel Shah | 0.0000 | 0.5445 | 45.40 | Zero overlaps on all tests, adaptive schedule + early stop |
| 14 | Nawel Asgar | 0.0000 | 0.5675 | 81.49 | Adaptive penalty scaling with cubic gradients and design-size optimization
| 15 | Shiva Baghel | 0.0000 | 0.5885 | 491.00 | Stable zero-overlap with balanced optimization |
| 16 | Vansh Jain | 0.0000 | 0.9352 | 86.36 | |
| 17 | Akash Pai | 0.0006 | 0.4933 | 326.25s | |
| 18 | Zade Mahayni | 0.00665 | 0.5157 | 127.4 | Will try again tomorrow |
| 19 | Nithin Yanna | 0.0148 | 0.5034 | 247.30s | aggressive overlap penalty with quadratic scaling |
| 20 | Sean Ko | 0.0271 | .5138 | 31.83s | lr increase, decrease epoch, increase lambda overlap and decreased lambda wire_length + log penalty loss |
| 21 | Keya Gohil | 0.0155 | 0.4678 | 1513.07 | Still working |
| 22 | Prithvi Seran | 0.0499 | 0.4890 | 398.58 | |
| 23 | partcl example | 0.8 | 0.4 | 5 | example |
| 24 | Add Yours! | | | | |
| 3 | Laksh Goyal | 0.0000 | 0.2654 | 36.10 | C++20 placer (analytic global -> row legalization -> detailed), zero overlap by construction, not by tuning. Anytime: 0.2699 @ 12.5s, 0.2849 @ 7.0s. M3 Pro. https://github.com/lgoyal6/intern_challenge |
| 4 | Neil Teje | 0.0000 | 0.2700 | 24.00s | |
| 5 | Leison Gao | 0.0000 | 0.2796 | 50.14s | |
| 6 | William Pan | 0.0000 | 0.2848 | 155.33s | |
| 7 | Ashmit Dutta | 0.0000 | 0.2870 | 995.58 | Spent my entire morning (12 am - 6 am) doing this :P |
| 8 | Pawan Paleja | 0.0000 | 0.3311 | 1.74s | Implemented hint for loss func, cosine annealing on learning rate with warmup, std annealing on lambda weight. Used optuna to tune hyperparam. Tested on gh codespaces 2-core. |
9 | Shashank Shriram | 0.0000 | 0.3312 | 11.32 | 🏎️💥 |
| 10 | Gabriel Del Monte | 0.0000 | 0.3427 | 606.07 | |
| 11 | Aleksey Valouev| 0.0000 | 0.3577 | 118.98 | |
| 12 | Mohul Shukla | 0.0000 | 0.5048 | 54.60s | |
| 13 | Ryan Hulke | 0.0000 | 0.5226 | 166.24 | |
| 14 | Neel Shah | 0.0000 | 0.5445 | 45.40 | Zero overlaps on all tests, adaptive schedule + early stop |
| 15 | Nawel Asgar | 0.0000 | 0.5675 | 81.49 | Adaptive penalty scaling with cubic gradients and design-size optimization
| 16 | Shiva Baghel | 0.0000 | 0.5885 | 491.00 | Stable zero-overlap with balanced optimization |
| 17 | Vansh Jain | 0.0000 | 0.9352 | 86.36 | |
| 18 | Akash Pai | 0.0006 | 0.4933 | 326.25s | |
| 19 | Zade Mahayni | 0.00665 | 0.5157 | 127.4 | Will try again tomorrow |
| 20 | Nithin Yanna | 0.0148 | 0.5034 | 247.30s | aggressive overlap penalty with quadratic scaling |
| 21 | Sean Ko | 0.0271 | .5138 | 31.83s | lr increase, decrease epoch, increase lambda overlap and decreased lambda wire_length + log penalty loss |
| 22 | Keya Gohil | 0.0155 | 0.4678 | 1513.07 | Still working |
| 23 | Prithvi Seran | 0.0499 | 0.4890 | 398.58 | |
| 24 | partcl example | 0.8 | 0.4 | 5 | example |
| 25 | Add Yours! | | | | |

> **To add your results:**
> Insert a new row in the table above with your name, overlap, wirelength, and any notes. Ensure you sort by overlap.
Expand Down
2 changes: 2 additions & 0 deletions cpp_placer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build/
__pycache__/
79 changes: 79 additions & 0 deletions cpp_placer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# cpp_placer - a C++20 mixed-size placer for the par.tcl intern challenge

A standard-cell + macro placer written in C++20, called from `placement.py`
through a ~90 line ctypes shim. No dependencies beyond a C++20 compiler; the
library builds itself on first import.

## Run it

```bash
python run_first10.py # tests 1-10, the leaderboard numbers
python run_first10.py 11 12 # extra credit
PARTCL_BUDGET=1.0 python run_first10.py # seconds of search per design
PARTCL_SOLVER=torch python run_first10.py # the PyTorch reference path instead
```

`test.py` as shipped runs all 12 cases and averages over 12; the README asks for
the first 10. `run_first10.py` reuses `test.py`'s own `run_placement_test` and
`TEST_CASES` verbatim and only changes which slice is averaged.

## What the objective actually is

`wirelength_attraction_loss()` costs each edge as
`alpha * log(exp(|dx|/alpha) + exp(|dy|/alpha))` with `alpha = 0.1`. The
docstring calls this "a smooth approximation of Manhattan distance", but
`alpha*logsumexp(dx/alpha, dy/alpha)` approximates **max**, not the sum: it is
within `alpha*ln2 = 0.069` of `max(|dx|,|dy|)` everywhere. The scored objective
is Chebyshev (L-inf) wirelength. This solver optimises that function directly.

Second, pins sit at `cell_pos + pin_offset` with the offset drawn from
`[0,w] x [0,h]`, while the overlap check treats `cell_pos` as the cell *centre*.
A cell's pin cloud is therefore its body translated by `(+w/2, +h/2)`, and the
shift is bigger for bigger cells. The solver never assumes pins are centred, so
a small cell can park where its pin cloud lands inside a large neighbour's.

Neither of these is worth "fixing" in the harness: changing them would move the
metric and make the leaderboard incomparable. They are noted, not patched.

## Pipeline

1. **Global placement** - Adam on smoothed-L-inf wirelength plus a pairwise
overlap-area penalty. The penalty multiplier is auto-scaled each iteration
against the wirelength gradient norm, so one schedule works from 22 to
100,000 cells with no per-design tuning. Neighbour search is a uniform grid
for standard cells; macros are few and checked against everything. The step
size decays geometrically, which is what stops the layout from bouncing
instead of settling.
2. **Macro placement** - either legalised from the global placement, or
shelf-packed (first-fit decreasing) into one edge band of the die. Macros
have roughly 100x lower pin density per unit area than standard cells, so
giving up the middle is usually right.
3. **Row legalization** - every standard cell has height exactly 1.0, so they go
into a unit-pitch row grid bounded by the die, with macros as blocked
intervals. Legality here is structural rather than hoped for: rows cannot
overlap in y, and intervals within a row are kept disjoint by construction.
So zero overlap does not depend on the penalty weight converging.
4. **Detailed placement** - each cell is relocated to the exact 1-D optimum of
the real objective (the cost is convex and piecewise linear in x for fixed y,
so the optimum sits on a breakpoint), then snapped to the nearest free slot.
Plus equal-width swaps, which are legal by construction. Then the die is
dropped and the same passes run unbounded: there is no fixed outline in this
problem, the die is only a device to force a dense pack.
5. **Multi-start** - round 0 sweeps 13 die aspect ratios against 9 constructive
macro arrangements; later rounds hill-climb macro offsets at the winning
aspect. The die utilisation is found by bisection rather than guessed.

## Zero overlap is verified, not assumed

Before returning, positions are rounded to float32 (the dtype the harness stores
them in) and checked with a sweep-line implementation of
`calculate_cells_with_overlaps`'s exact predicate. If any cell overlaps, the
shim raises rather than returning a placement that would score a nonzero
overlap. The legality margin is 5e-3, about 80x the float32 resolution at these
coordinates, so rounding cannot open a gap.

## Files

- `src/partcl_place.cpp` - the whole solver, single translation unit.
- `build.sh` - `c++ -std=c++20 -O3 -shared -fPIC`. Nothing else.
- `__init__.py` - ctypes bridge, builds the library on first import.
88 changes: 88 additions & 0 deletions cpp_placer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""ctypes bridge to the C++20 placer.

The library is built on first use if it is missing, so `python test.py` works
from a clean checkout with no extra setup step.
"""

import ctypes
import os
import subprocess
import sys

import numpy as np
import torch

_DIR = os.path.dirname(os.path.abspath(__file__))
_LIB_NAME = "libpartcl_place.dylib" if sys.platform == "darwin" else "libpartcl_place.so"
_LIB_PATH = os.path.join(_DIR, "build", _LIB_NAME)

_lib = None


def _load():
global _lib
if _lib is not None:
return _lib
if not os.path.exists(_LIB_PATH):
subprocess.run(["bash", os.path.join(_DIR, "build.sh")], check=True)
lib = ctypes.CDLL(_LIB_PATH)
f64 = np.ctypeslib.ndpointer(dtype=np.float64, flags="C_CONTIGUOUS")
i32 = np.ctypeslib.ndpointer(dtype=np.int32, flags="C_CONTIGUOUS")
lib.partcl_place.restype = ctypes.c_int
lib.partcl_place.argtypes = [
ctypes.c_int, f64, f64, f64, f64, # n, w, h, x, y
ctypes.c_int, i32, f64, f64, # n_pins, pin_cell, pin_ox, pin_oy
ctypes.c_int, i32, i32, # n_edges, edge_a, edge_b
ctypes.c_double, ctypes.c_uint, ctypes.c_int, # budget_s, seed, verbose
]
_lib = lib
return lib


def place(cell_features, pin_features, edge_list, budget_s=2.0, seed=12345, verbose=False):
"""Run the C++ placer. Returns (x, y) numpy float64 arrays of cell centres.

Pin absolute position is `cell_pos + pin_offset`, exactly as
wirelength_attraction_loss() computes it.
"""
lib = _load()
n = int(cell_features.shape[0])

w = np.ascontiguousarray(cell_features[:, 4].detach().numpy(), dtype=np.float64)
h = np.ascontiguousarray(cell_features[:, 5].detach().numpy(), dtype=np.float64)
x = np.ascontiguousarray(cell_features[:, 2].detach().numpy(), dtype=np.float64)
y = np.ascontiguousarray(cell_features[:, 3].detach().numpy(), dtype=np.float64)

pin_cell = np.ascontiguousarray(pin_features[:, 0].detach().numpy(), dtype=np.int32)
pin_ox = np.ascontiguousarray(pin_features[:, 1].detach().numpy(), dtype=np.float64)
pin_oy = np.ascontiguousarray(pin_features[:, 2].detach().numpy(), dtype=np.float64)

e = edge_list.detach().numpy()
ea = np.ascontiguousarray(e[:, 0], dtype=np.int32)
eb = np.ascontiguousarray(e[:, 1], dtype=np.int32)

bad = lib.partcl_place(
n, w, h, x, y,
pin_cell.shape[0], pin_cell, pin_ox, pin_oy,
ea.shape[0], ea, eb,
ctypes.c_double(budget_s), ctypes.c_uint(seed), ctypes.c_int(1 if verbose else 0),
)
if bad != 0:
raise RuntimeError(f"solver returned a placement with {bad} overlapping cells")
return x, y


def to_features(cell_features, x, y):
out = cell_features.clone()
out[:, 2] = torch.from_numpy(x).to(out.dtype)
out[:, 3] = torch.from_numpy(y).to(out.dtype)
return out


# Build/load at import time, not on the first solve: `test.py` times
# train_placement(), and compiling the library inside that window would land a
# one-off ~1s build cost in the reported runtime of test 1.
try:
_load()
except Exception: # no compiler available; the PARTCL_SOLVER=torch path still works
pass
16 changes: 16 additions & 0 deletions cpp_placer/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Builds the C++20 placer into a shared library loaded by placement.py via ctypes.
# No dependencies beyond a C++20 compiler.
set -euo pipefail
cd "$(dirname "$0")"

CXX="${CXX:-c++}"
case "$(uname -s)" in
Darwin) EXT=dylib ;;
*) EXT=so ;;
esac

mkdir -p build
"$CXX" -std=c++20 -O3 -DNDEBUG -ffast-math -fno-finite-math-only \
-shared -fPIC src/partcl_place.cpp -o "build/libpartcl_place.$EXT"
echo "built build/libpartcl_place.$EXT"
Loading