diff --git a/PD Job Acceleration/.gitignore b/PD Job Acceleration/.gitignore new file mode 100644 index 0000000..069f99a --- /dev/null +++ b/PD Job Acceleration/.gitignore @@ -0,0 +1,9 @@ +.bench_disk/ +examples/demo_durable/ +examples/demo_ram_scratch/ +examples/demo_perf_sync_job/ +examples/rcx_job_durable/ +examples/compare_results/ +examples/compare_metal_fill_results/ +examples/compare_pd_io_results/ +examples/compare_pd_farm_io_results/ diff --git a/PD Job Acceleration/LINKEDIN_POST.md b/PD Job Acceleration/LINKEDIN_POST.md new file mode 100644 index 0000000..a4a666e --- /dev/null +++ b/PD Job Acceleration/LINKEDIN_POST.md @@ -0,0 +1,59 @@ +# LinkedIn post draft + +## Short version + +Most Physical Design turnaround time is not "the algorithm is slow." +It is **"the filesystem is in the way."** + +But the fix is **not** "put everything in RAM." + +PD logs are often **20GB+**. Those stay on **local SSD**. +What belongs in **tmpfs** is the small, chatty scratch (`tmp` / `TMPDIR`). +Design DBs stay on disk/NFS unless you have measured that the whole tree fits. + +**Pattern:** + +1. Job dir on disk (DB + fat logs) +2. Symlink only `tmp/` into `/dev/shm` +3. Export `TMPDIR` into that scratch +4. Run Innovus / ICC2 / FC as usual +5. Teardown: materialize scratch home, free RAM + +Same licenses. Same Tcl. Less I/O wait — without OOMing the node. + +Working scripts + demos: `PD Job Acceleration/` +`./scripts/ram_scratch.sh --demo` + +--- + +## Longer technical version + +**Problem** +Farms put hot working dirs on NFS. Tiny random I/O and tempfile spam pay RTT. +CPU and licenses wait. + +**Wrong fix** +Staging a 20GB log into tmpfs. That is just a creative way to OOM. + +**Right fix (limited RAM)** +``` +disk: DB + logs RAM: tmp + TMPDIR only + ▲ | + └──── flush on end ────┘ +``` + +**When you have spare RAM** +You can stage most of the workspace into tmpfs and rsync checkpoints home — +but keep huge logs on disk (`PD_KEEP_LOGS_ON_DISK=1`, the default). + +**Safety** +- tmpfs is volatile +- flush on EXIT/TERM +- namespace `/dev/shm/pdjobs/$USER/$JOB` +- measure before you redirect a path into RAM + +```bash +cd "PD Job Acceleration" +./scripts/ram_scratch.sh --demo # hybrid (practical default) +./scripts/run_pd_job.sh --demo # full tree (only if it fits) +``` diff --git a/PD Job Acceleration/README.md b/PD Job Acceleration/README.md new file mode 100644 index 0000000..210ee35 --- /dev/null +++ b/PD Job Acceleration/README.md @@ -0,0 +1,223 @@ +# Accelerate Physical Design Jobs with tmpfs + rsync + +**White paper (detailed):** [`WHITEPAPER.md`](./WHITEPAPER.md) · +[`docs/pd_tmpfs_rsync_whitepaper.pdf`](./docs/pd_tmpfs_rsync_whitepaper.pdf) + +Physical Design tools are often **I/O-bound**, not just CPU-bound — +especially over NFS (tiny random reads on libs/DB pages, chatty scratch). +This folder has **two modes**. Pick by RAM budget. + +| Mode | When | What goes in RAM | +|---|---|---| +| **A. Full workspace** (`run_pd_job.sh`) | Design + scratch **fits** in free RAM with headroom | Job tree **except logs** (logs stay on disk by default) | +| **B. Hybrid scratch** (`ram_scratch.sh`) | RAM is limited (usual case) | Only small `tmp` / `TMPDIR` | + +**Fat PD logs (often 10–50GB+) do not go in RAM** in either recommended setup. +Keep them on **local SSD/NVMe** (best) or NFS. Putting a 20GB log in `tmpfs` +is how you OOM the node. + +``` +Mode B (limited RAM) — the practical default: + + disk / NFS RAM (/dev/shm scratch) + +----------------------+ +----------------------+ + | design DB, libs | | tmp/ (symlink) | + | scripts | | TMPDIR | + | logs/ (STAY HERE) | | | + | final outputs | flush | small regenerable | + +----------------------+ <-------- +----------------------+ +``` + +## Mode B — limited RAM (recommended default) + +Keep the job on disk. Redirect only small scratch into `/dev/shm`. + +```bash +cd "PD Job Acceleration" + +export PD_DURABLE_ROOT=/proj/chip/blockA/pnr_run42 +export PD_RAM_PATHS="tmp" # default — NOT logs +export PD_TOOL_CMD='innovus -files run_route.tcl -log logs/route.log' +./scripts/ram_scratch.sh run +``` + +What happens: + +1. Creates `/dev/shm/pdjobs/$USER/.scratch/` +2. Makes `tmp/` a **symlink into RAM** (under the durable job dir) +3. Exports `TMPDIR` / `TMP` / `TEMP` into that scratch +4. Runs the tool with cwd = durable root + - `logs/route.log` → disk + - `tmp/...` and tempfile APIs → RAM + - design DB saves → disk (wherever Tcl points) +5. On teardown: materialize `tmp/` back to disk, delete scratch, free RAM + +Demo (no EDA license): + +```bash +./scripts/ram_scratch.sh --demo +``` + +| Tier | Put this there | +|---|---| +| RAM (`tmpfs`) | Small regenerable scratch, `TMPDIR` — **only if measured to fit** | +| Local SSD | Huge logs, fat temp that exceeds RAM | +| NFS | Inputs, final DBs, shared deliverables | + +## Mode A — full workspace in tmpfs (only if it fits) + +Stage the durable tree into RAM, run there, `rsync` checkpoints home. +**Logs still stay on disk by default** (`PD_KEEP_LOGS_ON_DISK=1`): after staging, +`logs/` in the workspace is a symlink to the durable `logs/` directory and is +excluded from rsync — so a 20GB tool log never lands in tmpfs. + +```bash +export PD_DURABLE_ROOT=/proj/chip/blockA/pnr_run42 +export PD_JOB_NAME=blockA_route_iter3 +export PD_TMPFS_SIZE=64G +export PD_TOOL_CMD='innovus -files run_route.tcl -log logs/route.log' +./scripts/run_pd_job.sh +``` + +Flow: + +1. Prepare workspace under `/dev/shm/pdjobs/` +2. `rsync` durable → tmpfs (**excluding `logs/`**) +3. Rewire `logs/` → durable disk +4. Run tool with cwd = tmpfs (relative `logs/...` hits disk) +5. Optional periodic `rsync` checkpoints → durable +6. Final `rsync`, write `STATUS`, cleanup + +Demo: + +```bash +./scripts/run_pd_job.sh --demo +``` + +**Warning:** Mode A still needs the design DB + scratch to fit in RAM. +If they do not, use Mode B. Set `PD_KEEP_LOGS_ON_DISK=0` only if you have +measured that logs are small enough for tmpfs. + +## Scripts + +| Script | Role | +|---|---| +| `scripts/ram_scratch.sh` | Mode B: small `tmp`/`TMPDIR` in RAM; logs stay on disk | +| `scripts/run_pd_job.sh` | Mode A: full tree in tmpfs; **logs stay on disk by default** | +| `scripts/stage_to_tmpfs.sh` | Durable → tmpfs staging | +| `scripts/checkpoint_sync.sh` | Hot → durable incremental sync | +| `scripts/finalize_job.sh` | Final sync + cleanup | +| `scripts/pd_job_env.sh` | Shared helpers (`pd_durable_sync`, perf discovery) | +| `scripts/pd_perf_profile.sh` | **perf** (+ GNU time) I/O-vs-CPU profile + Mode A/B advice | +| `scripts/bench_io.sh` | Disk vs tmpfs microbench | +| `scripts/demo_*.sh` | License-free demos | + +## perf — measure before you move data + +`perf` does not accelerate the job; it tells you **whether** tmpfs will help. + +```bash +# Standalone profile of any command +./scripts/pd_perf_profile.sh --out /tmp/pd_perf -- \ + bash -c 'your_pd_tool ...' + +# Or wrap Mode A / Mode B +PD_PERF=1 PD_DURABLE_ROOT=... PD_TOOL_CMD='...' ./scripts/ram_scratch.sh run +``` + +Output: `SUMMARY.txt` with classification (`I/O-bound` / `CPU-bound` / `Mixed`) and a +Mode A/B recommendation. HW counters may be unavailable in VMs; soft events + +GNU `time -v` still classify well. + +## `sync` — durability after rsync (not the same as rsync) + +**rsync** copies bytes into the destination filesystem. The shell **`sync`** +command flushes kernel writeback so a crash cannot silently lose a “successful” +checkpoint/finalize. + +| Knob | Default | Meaning | +|---|---|---| +| `PD_SYNC_MODE` | `fs` | `off` / `file` / `fs` / `global` | +| `PD_SYNC_AFTER_FINALIZE` | `1` | sync after final rsync / Mode B flush | +| `PD_SYNC_AFTER_CHECKPOINT` | `0` | sync every checkpoint (often costly on NFS) | + +Prefer `fs` (filesystem containing the durable root) over `global` on busy farm +nodes. Enable checkpoint sync only when the redo window matters more than NFS load. + +```bash +# Smoke demo: perf profile + Mode B + durable sync +./examples/demo_perf_and_sync.sh +``` + +## Safety rules + +1. tmpfs is volatile — never the source of truth; always flush/checkpoint. +2. **Do not put 20GB+ logs in RAM.** Prefer local SSD. +3. Size RAM for peak scratch (or peak full tree in Mode A), with headroom. +4. Exclude regenerable junk from sync when using Mode A. +5. Trap `EXIT`/`TERM` so killed jobs still flush. +6. On shared farms: `/dev/shm/pdjobs/$USER/$JOB` and enforce quotas. +7. After finalize, **`sync`** (or `PD_SYNC_MODE=fs`) so durable media has the data. +8. Profile with **`perf`** before claiming an I/O win — CPU-bound phases will not move. + +## Real workload example (RC Extraction) + +There is **no metal-fill project on `main`**. The closest real PD binary in this +repo family is **RC Extraction** (from `cursor/rc-extraction-signoff-6f4a`). + +```bash +# from repo root — builds RC Extraction if needed, then runs Mode B +./PD\ Job\ Acceleration/examples/run_rcx_accelerated.sh +./PD\ Job\ Acceleration/examples/run_rcx_accelerated.sh --mode-a +``` + +That job extracts `simple_net` / `coupled_nets` / `via_stack` / a generated +`big_bus.lay`, writes SPEF under `outputs/`, logs under `logs/` (disk), and +uses `tmp/` (+ `TMPDIR`) for scratch. + +Compare **with vs without** acceleration (same job, wall times + SPEF checksums): + +```bash +./PD\ Job\ Acceleration/examples/compare_accel.sh +cat "PD Job Acceleration/examples/compare_results/SUMMARY.txt" +``` + +### Metal fill (`gpu_metal_fill`) + +Same exercise on the BEOL metal-fill engine +(`cursor/beol-metal-fill-partitioning-caf3/gpu_metal_fill`): + +```bash +./PD\ Job\ Acceleration/examples/compare_metal_fill.sh +cat "PD Job Acceleration/examples/compare_metal_fill_results/SUMMARY.txt" +``` + +### I/O-bound PD job (liberty vault + checkpoints) + +Synthetic but PD-shaped: tens of thousands of tiny liberty-like files with +random lookups, fat DB checkpoints, report spam. Shows filesystem wins clearly. + +```bash +./PD\ Job\ Acceleration/examples/compare_pd_io.sh +cat "PD Job Acceleration/examples/compare_pd_io_results/SUMMARY.txt" +``` + +### Monumental farm I/O suite (NFS RTT vs tmpfs) + +Multi-phase PD farm workload (liberty vault → random lookups → SPEF shards → +ECO checkpoints → report spam). Baseline adds emulated **NFS per-op RTT**; +Mode B/A run the same work on tmpfs with `nfs-us=0`. + +```bash +./PD\ Job\ Acceleration/examples/compare_pd_farm_io.sh +cat "PD Job Acceleration/examples/compare_pd_farm_io_results/SUMMARY.txt" +``` + +Example on this host (`NFS_US=400µs`): baseline **~115s** → Mode A **~2.3s (~50×)**. + +## LinkedIn one-liner + +> Most PD turnaround time is not "the algorithm is slow" — it is "the filesystem is in the way." +> Keep fat logs on SSD. Put only small scratch in tmpfs. rsync durability back to NFS. + +See [`LINKEDIN_POST.md`](./LINKEDIN_POST.md) for a longer draft. diff --git a/PD Job Acceleration/WHITEPAPER.md b/PD Job Acceleration/WHITEPAPER.md new file mode 100644 index 0000000..30fd395 --- /dev/null +++ b/PD Job Acceleration/WHITEPAPER.md @@ -0,0 +1,657 @@ +# Accelerating Physical Design Jobs with tmpfs and rsync + +**A design and operations white paper for the `PD Job Acceleration` project** + +**Authors:** Project notes distilled from implementation and experiments in +`Physical-Design-Algorithms-Implementation` +**Scope:** Job-local I/O acceleration for ASIC/SoC physical-design workloads on +Linux compute farms +**Companion code:** `PD Job Acceleration/` (orchestrators, demos, benchmarks) + +--- + +## Abstract + +Physical-design (PD) tools—placement, CTS, routing, parasitic extraction, metal +fill, STA, and ECO loops—are frequently described as CPU- or memory-bound. On +industrial compute farms, however, a large fraction of wall-clock time is spent +waiting on the **filesystem**, especially when the hot working directory lives +on **NFS**. The dominant cost is often not raw sequential bandwidth but +**per-operation latency** on huge numbers of tiny reads and writes (liberty-like +lookups, SPEF shards, reports, tempfiles, incremental saves). + +This white paper presents a practical, license-preserving acceleration pattern: + +1. Treat storage as a **tiered system** (hot RAM vs durable disk/NFS). +2. Place only the **hot, chatty, sized-to-fit** working set on **tmpfs** + (`/dev/shm` or an explicit tmpfs mount). +3. Keep **fat logs (often 10–50 GB+)** and other oversized artifacts on + **local SSD or NFS**—never in RAM by default. +4. Use **rsync** as the durability bus: stage in, checkpoint incrementally, + finalize on `EXIT`/`TERM`, then free RAM. + +We describe two operating modes (full-workspace staging vs hybrid scratch), the +safety model, implementation details, and measured results ranging from modest +local-SSD gains to ~**45–50×** speedups under an NFS-RTT farm model. The +central claim is not a new PD algorithm; it is that **storage placement is part +of turnaround**, and that this can be defended rigorously in design reviews and +interviews. + +--- + +## Table of contents + +1. [Motivation and industry context](#1-motivation-and-industry-context) +2. [Problem statement](#2-problem-statement) +3. [Why page cache is not enough](#3-why-page-cache-is-not-enough) +4. [Design principles](#4-design-principles) +5. [Architecture: two modes](#5-architecture-two-modes) +6. [tmpfs as the hot tier](#6-tmpfs-as-the-hot-tier) +7. [rsync as the durability bus](#7-rsync-as-the-durability-bus) +7a. [After rsync: shell `sync` for writeback durability](#7a-after-rsync-shell-sync-for-writeback-durability) +7b. [perf: measure I/O vs CPU before you move data](#7b-perf-measure-io-vs-cpu-before-you-move-data) +8. [Log policy: why fat logs must stay off RAM](#8-log-policy-why-fat-logs-must-stay-off-ram) +9. [Safety, failure modes, and operational rules](#9-safety-failure-modes-and-operational-rules) +10. [Implementation in this repository](#10-implementation-in-this-repository) +11. [Experimental methodology](#11-experimental-methodology) +12. [Results](#12-results) +13. [When the pattern helps—and when it does not](#13-when-the-pattern-helpsand-when-it-does-not) +14. [Defending the approach (review / interview)](#14-defending-the-approach-review--interview) +15. [Limitations and future work](#15-limitations-and-future-work) +16. [Conclusion](#16-conclusion) +17. [Appendix A: quick-start commands](#appendix-a-quick-start-commands) +18. [Appendix B: glossary](#appendix-b-glossary) + +--- + +## 1. Motivation and industry context + +### 1.1 The PD farm reality + +A modern PD block run typically involves: + +- reading large **LEF/DEF/Oasis/GDS** and timing libraries, +- maintaining a growing **design database**, +- emitting **reports**, logs, and intermediate saves, +- and iterating through **ECO** cycles under schedule pressure. + +On a laptop with local NVMe, many of these paths are “fast enough.” On a +shared farm, the same Tcl often runs with `$cwd` on **NFS**. Every tiny +`open`/`stat`/`read`/`write` can pay a network round-trip. CPUs look busy in +the scheduler while spending time in **iowait**; licenses stay checked out; +turnaround suffers. + +### 1.2 Misdiagnosis is common + +Teams often respond with: + +- more cores / higher CPU priority, +- larger machines, +- or algorithm-level tuning, + +when the binding constraint is **I/O placement**. Conversely, staging an +entire multi-tens-of-GB workspace into RAM “because tmpfs is fast” can +**OOM the node** or thrash shared `/dev/shm`. + +This project exists to make the correct middle path explicit, measurable, and +operationally safe. + +--- + +## 2. Problem statement + +> **Given** a PD tool command and a durable job directory on disk/NFS, +> **accelerate wall-clock turnaround** by relocating the hot working set onto +> RAM-backed storage, +> **without** changing the tool binary, licenses, or functional results, +> **while** preserving durability of required outputs across kills and +> avoiding RAM exhaustion from oversized artifacts (especially logs). + +Success criteria: + +1. **Functional equivalence** — deliverables match (checksums / reports). +2. **Durability** — required outputs survive process kill via finalize/checkpoint. +3. **Bounded RAM** — hot tier sized; fat logs excluded by default. +4. **Operability** — one orchestrator command; clear Mode A vs Mode B choice. + +--- + +## 3. Why page cache is not enough + +Linux page cache already accelerates repeated reads of hot pages. It does +**not** fully solve PD farm I/O for several reasons: + +1. **Metadata RTT on NFS** — opening thousands of distinct small files still + costs network round-trips even when data eventually caches. +2. **Writeback semantics** — durable commits, `fsync`, and NFS write stability + behave differently from local tmpfs. +3. **Working-set churn** — ECO loops and report spam create many short-lived + files that defeat “read it once, keep it hot” assumptions. +4. **Lack of policy** — page cache is global and opportunistic; PD jobs need an + explicit **job-local scratch namespace** with a durability contract. + +tmpfs + rsync is therefore not “reinventing cache”; it is **deliberate tiering +with an explicit sync protocol**. + +--- + +## 4. Design principles + +1. **Tier by access pattern, not by file type slogan.** + Hot = high IOPS / many tiny ops / regenerable. Cold = huge / durable / final. + +2. **Never make tmpfs the source of truth.** + Reboot or node death loses `/dev/shm`. Checkpoint or finalize always. + +3. **Fat logs are a special case.** + Innovus/ICC2/FC-style logs routinely reach **10–50 GB+**. Putting them in + RAM is a reliability bug disguised as optimization. + +4. **Measure boundness.** + CPU-bound kernels (heavy fill/opt) gain little from I/O tiering. I/O-bound + chatty phases gain a lot—especially under NFS-like latency. + +5. **Exclude regenerable junk from sync.** + Shipping vault caches and temp DBs home can erase end-to-end wins. + +6. **Fail safe on signals.** + Trap `EXIT`/`INT`/`TERM` so killed jobs still flush required outputs. + +--- + +## 5. Architecture: two modes + +``` +Mode B (limited RAM — practical default) +──────────────────────────────────────── + disk / NFS RAM (/dev/shm scratch) + +----------------------+ +----------------------+ + | design DB, libs | | tmp/ (symlink) | + | scripts | | TMPDIR | + | logs/ (STAY HERE) | | small regenerable | + | final outputs | flush | scratch only | + +----------------------+ <-------- +----------------------+ + +Mode A (only if the working tree fits) +──────────────────────────────────────── + durable --rsync (excl. logs)--> tmpfs workspace + <--rsync checkpoints-- tool cwd here + logs/ rewired to durable disk even in Mode A +``` + +### 5.1 Mode B — hybrid scratch (`ram_scratch.sh`) + +Keep the job directory on durable storage. Redirect only selected relative +paths (default: `tmp`) into `/dev/shm/...` via symlinks, and export +`TMPDIR`/`TMP`/`TEMP` into that scratch. On teardown, materialize needed +paths (or discard regenerable scratch) and free RAM. + +**Use when:** RAM is limited; design DB is large; logs are huge. + +### 5.2 Mode A — full workspace (`run_pd_job.sh`) + +Stage the durable tree into a RAM workspace, run the tool there, checkpoint +with rsync, finalize, cleanup. + +**Critical default:** `PD_KEEP_LOGS_ON_DISK=1` rewires workspace `logs/` to the +durable logs directory and excludes `logs` from rsync (pattern `logs`, not +only `logs/`, so a symlink named `logs` cannot overwrite the durable +directory—a real bug we hit and fixed). + +**Use when:** design + scratch fit in free RAM with headroom. + +--- + +## 6. tmpfs as the hot tier + +### 6.1 What tmpfs is + +tmpfs is a filesystem backed by **pageable kernel memory** (and swap, if +configured). On most Linux hosts, `/dev/shm` is already a tmpfs mount shared +across jobs. Dedicated mounts (`mount -t tmpfs -o size=64G ...`) are useful for +hard caps when privileges allow. + +### 6.2 Properties that matter for PD + +| Property | Implication | +|---|---| +| Low latency | Tiny random ops avoid NFS RTT | +| High bandwidth | Checkpoint/DB stream writes are cheap | +| Volatile | Must checkpoint | +| Capacity = RAM policy | Size for peak, not input-only | +| Often `noexec` on `/dev/shm` | Run tool binaries from durable disk | + +### 6.3 `noexec` caveat + +Some environments mount `/dev/shm` with `noexec`. Mode A must invoke ELF tools +from a durable path (absolute path to `.../scripts/run_fill`, `rcx_extract`, +etc.) even when data cwd is on tmpfs. Our RC Extraction and metal-fill +accelerated runners do this explicitly. + +--- + +## 7. rsync as the durability bus + +### 7.1 Why rsync instead of `cp -a` + +- **Incremental checkpoints** after the first full sync +- **Excludes** for regenerable paths +- **`--partial`** tolerance on flaky NFS +- Easy to run on a **timer** while the tool executes +- Familiar operations language for silicon CAD flows + +### 7.2 Sync points + +1. **Stage-in** (Mode A): durable → tmpfs +2. **Checkpoint loop** (optional): tmpfs → durable every \(N\) seconds +3. **Finalize**: last sync + `STATUS` + cleanup + +### 7.3 Exclude policy + +Default excludes include status dirs, editor junk, cores, and—when +`PD_KEEP_LOGS_ON_DISK=1`—`logs` / `logs/***`. Workload-specific excludes +(e.g. `tmp/lib_vault/`, intermediate `*.db`) prevent “winning on tool time, +losing on shipping trash home.” + +--- + +## 7a. After rsync: shell `sync` for writeback durability + +**rsync ≠ durable on media.** rsync copies into the destination page cache. +Until writeback is flushed, a node crash or abrupt NFS client death can erase a +checkpoint that the orchestrator already logged as successful. + +This toolkit therefore pairs rsync with the shell **`sync`** command +(`pd_durable_sync` in `pd_job_env.sh`): + +| `PD_SYNC_MODE` | Behavior | +|---|---| +| `off` | No flush (fastest; weakest crash claim) | +| `file` | Write + `sync` a marker under `.pd_job_status/` | +| `fs` (**default**) | `sync -f` on the filesystem containing the durable root | +| `global` | Full-machine `sync` (avoid on busy farm nodes) | + +Defaults: + +- `PD_SYNC_AFTER_FINALIZE=1` — flush after final rsync / Mode B materialize. +- `PD_SYNC_AFTER_CHECKPOINT=0` — per-checkpoint flush is optional; on NFS it can + dominate wall time. Enable when the redo window matters more than farm load. + +This is complementary to the application-level `fsync` some tools already issue +on DB saves. The orchestrator cannot assume every EDA binary does that for every +artifact you care about. + +--- + +## 7b. perf: measure I/O vs CPU before you move data + +**tmpfs helps I/O-dominated phases.** It does almost nothing for pure compute +(timing graph walks, dense numerical kernels, many metal-fill CPU paths). +Shipping a workspace into RAM without evidence wastes ops time and RAM quota. + +`scripts/pd_perf_profile.sh` wraps a command with: + +1. **`perf stat`** when a working `perf` binary exists (soft events always; + HW cycles/IPC when the PMU is exposed), +2. **GNU `time -v`** for major page faults, voluntary context switches, and + filesystem input/output counts, +3. a short **classification** (`I/O-bound` / `CPU-bound` / `Mixed`) and Mode A/B + recommendation in `SUMMARY.txt`. + +Enable in-line: + +```bash +PD_PERF=1 PD_TOOL_CMD='...' ./scripts/ram_scratch.sh run +# or +./scripts/pd_perf_profile.sh --out logs/perf -- bash -c '...' +``` + +**What perf is not:** it is not I/O bandwidth isolation, and it is not a +substitute for tmpfs. It is the measurement half of the loop: + +> profile → classify → place hot paths on tmpfs → rsync durability → `sync` +> writeback → re-profile. + +Farm VMs often report `` for HW counters; soft events plus +`time -v` remain enough to catch “user+sys much less than wall” I/O wait patterns. + +--- + +## 8. Log policy: why fat logs must stay off RAM + +Commercial PD logs are append-heavy and frequently enormous. A 20 GB log in +tmpfs: + +- consumes RAM that the tool itself needs, +- risks `ENOSPC` on `/dev/shm` mid-route, +- and can OOM neighboring jobs on a shared node. + +**Policy adopted here:** + +- Default Mode B: `PD_RAM_PATHS=tmp` (**not** `logs`). +- Default Mode A: rewire `logs/` → durable disk. +- Prefer **local SSD/NVMe** for fat logs when available; NFS if necessary. +- Relative `-log logs/route.log` remains convenient because Mode A rewiring + preserves the path while landing bytes on disk. + +This policy is a first-class design constraint, not an afterthought. + +--- + +## 9. Safety, failure modes, and operational rules + +### 9.1 Failure modes + +| Failure | What happens | Mitigation | +|---|---|---| +| Tool killed (`SIGTERM`) | Trap runs finalize | Always trap `EXIT`/`INT`/`TERM` | +| Orchestrator killed `-9` | No finalize | Checkpoint loop reduces loss window | +| Host reboot | tmpfs gone | Only last successful sync survives | +| Undersized tmpfs | `ENOSPC` mid-job | Size for peak + headroom | +| Shared `/dev/shm` exhaustion | Multi-job interference | Per-user namespaces + quotas | +| rsync exclude bug (`logs/` vs symlink `logs`) | Durable logs become self-symlink | Exclude `logs` and `logs/***`; repair if symlink | + +### 9.2 Operational rules (post these next to the farm wiki) + +1. Never treat tmpfs as source of truth. +2. Do not put 20 GB+ logs in RAM. +3. Size RAM for peak scratch/workspace. +4. Exclude regenerable junk from sync. +5. Trap signals; checkpoint long jobs. +6. Namespace `/dev/shm/pdjobs/$USER/$JOB`. +7. Measure whether the phase is CPU- or I/O-bound (`pd_perf_profile.sh`) before promising speedup. +8. After finalize, flush writeback (`PD_SYNC_MODE=fs`) so durable media has the data. + +--- + +## 10. Implementation in this repository + +### 10.1 Core scripts + +| Script | Role | +|---|---| +| `scripts/pd_job_env.sh` | Shared defaults, rsync excludes, log rewiring, `pd_durable_sync` | +| `scripts/stage_to_tmpfs.sh` | Durable → tmpfs staging | +| `scripts/checkpoint_sync.sh` | Incremental hot → durable sync (+ optional `sync`) | +| `scripts/finalize_job.sh` | Final sync, durability `sync`, `STATUS`, cleanup | +| `scripts/run_pd_job.sh` | Mode A orchestrator (`PD_PERF=1` optional) | +| `scripts/ram_scratch.sh` | Mode B hybrid scratch | +| `scripts/pd_perf_profile.sh` | `perf` + GNU time I/O-vs-CPU classifier | +| `scripts/bench_io.sh` | Disk vs tmpfs microbench | + +### 10.2 Workloads used for evaluation + +| Workload | Nature | Path | +|---|---|---| +| Synthetic demos | Tiny I/O demos | `run_pd_job.sh --demo`, `ram_scratch.sh --demo` | +| RC Extraction | Mixed C++ extractor | `examples/run_rcx_accelerated.sh`, `compare_accel.sh` | +| `gpu_metal_fill` | CPU-heavy BEOL fill | `examples/compare_metal_fill.sh` | +| Local I/O-bound PD job | Liberty vault + checkpoints | `examples/compare_pd_io.sh` | +| Farm I/O suite | Multi-phase + NFS RTT model | `examples/compare_pd_farm_io.sh` | + +### 10.3 Notable implementation fixes + +- **Paths with spaces** — tool commands run via `bash -c`, not unquoted `eval`. +- **EXIT trap locals** — orchestrator state used by traps must be global. +- **Log symlink vs rsync** — exclude `logs` not only `logs/`, otherwise finalize + can replace a real durable directory with a self-referential symlink. +- **`/dev/shm` noexec** — execute binaries from durable storage. + +--- + +## 11. Experimental methodology + +### 11.1 Metrics + +- **tool_s** — time from job `start_epoch` to `done_epoch` (compute + hot I/O). +- **e2e_s** — wall time including stage/rsync/teardown. +- **Functional checks** — checksums of deliverables (SPEF/GDS/lookup stamp). +- **Placement checks** — `tmp_resolved` / `logs_resolved` paths. + +### 11.2 Fairness notes + +1. **Local SSD vs tmpfs** measures a best-case disk; gains are often modest. +2. **Farm model** adds emulated per-op NFS RTT (e.g. 400 µs) on the baseline + path only, representing metadata latency common on networked home + directories. Accelerated modes run identical work with `nfs-us=0` on tmpfs. +3. Regenerable scratch is not blindly flushed home in the farm/I/O compares + (deliverables already copied to `outputs/`), so e2e is not dominated by + shipping trash. + +This methodology is intentionally honest: overselling local-SSD microbenches +as “50×” would be indefensible in a design review. + +--- + +## 12. Results + +Results below were collected in the project’s Linux evaluation environment. +Absolute times vary by host; ratios and qualitative conclusions are the point. + +### 12.1 RC Extraction (mixed) + +Same extract job with scratch-oriented I/O. On local fast disk, tool time +improved on the order of ~**15–20%** for accelerated modes in earlier +compare runs; functional SPEF checksums matched. Useful as a “real binary” +smoke test, not as the headline NFS claim. + +### 12.2 `gpu_metal_fill` (CPU-bound control) + +BEOL dummy fill on a synthetic GPU-block GDS (`SM_GRID=4`, two passes, +~137 MB filled GDS). Fill kernel runtime dominates (~6 s/pass). Accelerated +modes showed only ~**1%** tool-time improvement; filled GDS checksums were +**identical**. + +**Interpretation:** if the phase is CPU-bound, storage tiering will not create +a miracle. Including this result strengthens credibility. + +### 12.3 Local I/O-bound PD job + +40k liberty-like cells, 400k random lookups, checkpoints, report spam on local +disk: + +| config | tool_s | vs baseline | e2e_s | vs baseline | +|---|---:|---:|---:|---:| +| baseline (disk) | 4.540 | — | 4.630 | — | +| Mode B (tmp in RAM) | 3.763 | +17.1% | 4.018 | +13.2% | +| Mode A (workspace tmpfs) | 3.339 | +26.5% | 3.691 | +20.3% | + +Checksum identical across configs. + +### 12.4 Farm I/O suite (headline result) + +Multi-phase suite: liberty vault → random lookups → SPEF shards → ECO +checkpoints → report spam. Baseline uses **400 µs emulated NFS RTT/op**. + +| config | tool_s | speedup | e2e_s | speedup | +|---|---:|---:|---:|---:| +| baseline (disk + NFS RTT) | 114.890 | — | 114.955 | — | +| Mode B (tmpfs, no RTT) | 2.600 | **44.2×** | 2.880 | **39.9×** | +| Mode A (tmpfs, no RTT) | 2.279 | **50.4×** | 2.587 | **44.4×** | + +Hottest phase example: liberty lookups **71.2 s → ~1.4 s**. +Functional checksum matched (`35702796`). + +**Interpretation:** this is the regime the architecture targets—chatty PD I/O +under networked filesystem latency. + +### 12.5 Phase breakdown (farm suite, illustrative) + +| phase | baseline (NFS model) | Mode A (tmpfs) | +|---|---:|---:| +| liberty_vault | ~19.1 s | ~0.33 s | +| lib_lookups | ~71.2 s | ~1.45 s | +| spef_shards | ~21.5 s | ~0.41 s | +| eco_checkpoints | ~0.09 s | ~0.03 s | +| report_spam | ~2.9 s | ~0.05 s | +| **all** | **~114.9 s** | **~2.3 s** | + +--- + +## 13. When the pattern helps—and when it does not + +### Helps + +- Hot directories on NFS or other high-RTT stores +- Many tiny files (libs, SPEF shards, reports, tempfiles) +- Iterative ECO loops reusing a hot tree +- `TMPDIR` currently pointing at network storage +- Tools that stream checkpoints frequently + +### Does not magically help + +- Pure CPU kernels with little I/O (heavy fill/opt examples) +- Working sets larger than available RAM +- Jobs that already run entirely on local NVMe with warm cache +- Flows that write huge logs into the hot tree without redirection + +### Decision rule + +``` +if peak_hot_bytes << free_RAM and I/O_wait is visible: + prefer Mode A (still keep fat logs on disk) +elif RAM limited or DB huge: + Mode B: tmp/TMPDIR only (measure before adding paths) +else: + fix log/output paths first; don't stage blindly +``` + +--- + +## 14. Defending the approach (review / interview) + +### Claim (precise) + +> Storage placement is part of PD turnaround. Relocate chatty, sized-to-fit +> working sets onto tmpfs; keep durability and fat logs on disk; sync with +> rsync under an explicit failure model. Speedup depends on whether the phase +> is I/O-bound under high-latency storage. + +### Strong answers to pushback + +**“Isn’t this just cache?”** +Page cache is opportunistic and global. This is job-scoped tiering with a +durability protocol and exclude policy. + +**“tmpfs will OOM us.”** +Correct if misused. Defaults keep logs off RAM; Mode B only redirects `tmp`; +size for peak; namespace and quota. + +**“What about node death?”** +Volatile by design; checkpoints define redo budget. Same class of risk as any +local scratch disk without sync. + +**“Show me it’s not fake.”** +Publish both local-disk and NFS-model numbers; include a CPU-bound negative +control (metal fill); require checksum equality. + +**“Why rsync?”** +Incremental, excludable, partial-transfer tolerant, timer-friendly—boring on +purpose. + +--- + +## 15. Limitations and future work + +1. **NFS model is an emulator** in the headline suite (fixed µs/op). Real NFS + traces (e.g. `bpftrace`/`strace` histograms) would calibrate better. +2. **No automatic working-set sizing yet** — operators must estimate peak. +3. **Vendor tempfile paths** do not always honor `TMPDIR`; discovery via + `lsof`/`strace`/`perf` is still partly manual. +4. **Multi-tenant fair sharing** of `/dev/shm` needs cgroup integration + (true I/O bandwidth isolation is `io.max`, not `nice`/`ionice`). +5. **Integration with LSF/SGE/k8s** prologue/epilogue hooks is natural next + packaging work. +6. **Optional local SSD tier** between NFS and tmpfs for fat logs and medium + scratch would complete a three-tier story. +7. **Richer `perf record`/`perf report` flame graphs** for phase-level + attribution (currently `perf stat` + classification; record is hinted). + +--- + +## 16. Conclusion + +Physical-design turnaround is a **system** problem: algorithms, licenses, +CPUs, **and** storage. The tmpfs + rsync pattern gives a concrete, teachable +control knob: + +- hot chatty I/O → RAM, +- fat logs / finals → disk, +- durability → deliberate rsync + writeback `sync`, +- claims → measured with `perf` / GNU time against CPU-bound and I/O-bound controls. + +Used carefully, it preserves correctness while removing a class of farm +latency that no amount of Tcl reorganization can fix. Used carelessly +(especially with huge logs in RAM), it becomes an outage generator. This +repository encodes the careful version—modes, defaults, traps, excludes, and +benchmarks—so the approach can be demonstrated, reviewed, and defended. + +--- + +## Appendix A: quick-start commands + +```bash +cd "PD Job Acceleration" + +# Demos (no EDA license) +./scripts/ram_scratch.sh --demo +./scripts/run_pd_job.sh --demo +./examples/demo_perf_and_sync.sh + +# Real binaries +./examples/run_rcx_accelerated.sh +./examples/compare_metal_fill.sh + +# I/O-bound compares +./examples/compare_pd_io.sh +./examples/compare_pd_farm_io.sh # monumental NFS-model vs tmpfs + +# Microbench +./scripts/bench_io.sh 256 +``` + +Environment knobs (selected): + +| variable | meaning | default | +|---|---|---| +| `PD_DURABLE_ROOT` | durable job directory | required | +| `PD_JOB_NAME` | workspace name slice | timestamped | +| `PD_RAM_PATHS` | Mode B relative dirs in RAM | `tmp` | +| `PD_KEEP_LOGS_ON_DISK` | Mode A log rewiring | `1` | +| `PD_CHECKPOINT_SECS` | checkpoint period | `0` (off) | +| `PD_FLUSH_ON_TEARDOWN` | Mode B materialize scratch | `1` | +| `PD_EXCLUDE_FILE` | extra rsync excludes | empty | +| `PD_TOOL_CMD` | command run in job cwd | required | +| `PD_SYNC_MODE` | writeback flush mode after rsync | `fs` | +| `PD_SYNC_AFTER_FINALIZE` | shell `sync` after finalize/flush | `1` | +| `PD_SYNC_AFTER_CHECKPOINT` | shell `sync` after each checkpoint | `0` | +| `PD_PERF` | wrap tool with `pd_perf_profile.sh` | `0` | + +--- + +## Appendix B: glossary + +| term | meaning | +|---|---| +| **tmpfs** | RAM-backed Linux filesystem | +| **`/dev/shm`** | common shared tmpfs mount | +| **Mode A** | full workspace staged into tmpfs | +| **Mode B** | hybrid: only scratch paths in RAM | +| **durable** | NFS/disk source of truth | +| **checkpoint** | incremental rsync hot → durable | +| **finalize** | last sync + status + cleanup | +| **`sync`(1)** | flush kernel writeback to durable media (≠ rsync) | +| **`perf`** | Linux performance counters / profiling toolkit | +| **NFS RTT** | network round-trip time paid per remote op | +| **ECO** | engineering change order iteration | +| **SPEF** | Standard Parasitic Exchange Format | +| **liberty** | `.lib` timing/power cell models | + +--- + +## Document history + +| version | notes | +|---|---| +| 1.0 | Initial white paper aligned with `PD Job Acceleration` implementation, Mode A/B defaults, log policy, and farm I/O suite results | +| 1.1 | Added `perf` profiling loop and shell `sync` durability after rsync | diff --git a/PD Job Acceleration/docs/build_whitepaper_pdf.py b/PD Job Acceleration/docs/build_whitepaper_pdf.py new file mode 100644 index 0000000..f45e219 --- /dev/null +++ b/PD Job Acceleration/docs/build_whitepaper_pdf.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Render WHITEPAPER.md -> docs/pd_tmpfs_rsync_whitepaper.pdf""" +from pathlib import Path +import markdown +from weasyprint import HTML + +root = Path(__file__).resolve().parents[1] +md = (root / "WHITEPAPER.md").read_text(encoding="utf-8") +html_body = markdown.markdown( + md, + extensions=["tables", "fenced_code", "toc", "sane_lists"], +) +html = f""" + +Accelerating Physical Design Jobs with tmpfs and rsync +{html_body} +""" +out = root / "docs" / "pd_tmpfs_rsync_whitepaper.pdf" +HTML(string=html, base_url=str(out.parent)).write_pdf(out) +print(f"wrote {out} ({out.stat().st_size} bytes)") diff --git a/PD Job Acceleration/docs/pd_tmpfs_rsync_whitepaper.pdf b/PD Job Acceleration/docs/pd_tmpfs_rsync_whitepaper.pdf new file mode 100644 index 0000000..3317976 Binary files /dev/null and b/PD Job Acceleration/docs/pd_tmpfs_rsync_whitepaper.pdf differ diff --git a/PD Job Acceleration/examples/.gitignore b/PD Job Acceleration/examples/.gitignore new file mode 100644 index 0000000..9201eba --- /dev/null +++ b/PD Job Acceleration/examples/.gitignore @@ -0,0 +1,3 @@ +# Generated by ./scripts/run_pd_job.sh --demo +demo_durable/ +.bench_disk/ \ No newline at end of file diff --git a/PD Job Acceleration/examples/compare_accel.sh b/PD Job Acceleration/examples/compare_accel.sh new file mode 100755 index 0000000..69e5b41 --- /dev/null +++ b/PD Job Acceleration/examples/compare_accel.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# Compare the same RC Extraction job: baseline (disk only) vs accelerated. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO_ROOT="$(cd "${ROOT}/.." && pwd)" +RCX_SRC="${REPO_ROOT}/RC Extraction" +OUT_DIR="${ROOT}/examples/compare_results" +SCRATCH_MB="${SCRATCH_MB:-32}" +PASSES="${PASSES:-4}" +OUTER="${OUTER:-3}" +mkdir -p "$OUT_DIR" + +[[ -x "${RCX_SRC}/rcx_extract" ]] || make -C "$RCX_SRC" -j"$(nproc)" + +write_run_job() { + local dest="$1" + local rcx_bin="${dest}/scripts/rcx_extract" + # Quoted heredoc so nothing expands early; inject values with envsubst-like sed. + cat > "${dest}/scripts/run_job.sh" <<'JOB' +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(pwd)" +RCX="__RCX_BIN__" +LOG="${ROOT}/logs/rcx_extract.log" +SCRATCH_MB=__SCRATCH_MB__ +PASSES=__PASSES__ +OUTER=__OUTER__ +mkdir -p logs tmp outputs +{ + echo "cwd=${ROOT}" + echo "TMPDIR=${TMPDIR:-unset}" + echo "tmp_resolved=$(readlink -f tmp)" + echo "logs_resolved=$(readlink -f logs)" + echo "start_epoch=$(date +%s.%N)" + echo "start=$(date -Iseconds)" +} | tee "$LOG" + +run_one() { + local lay="$1" tag="$2" p + for p in $(seq 1 "$PASSES"); do + local spef_tmp="${ROOT}/tmp/${tag}_p${p}.spef" + "$RCX" "inputs/${lay}" --spef "$spef_tmp" >>"$LOG" 2>&1 + dd if=/dev/urandom of="${ROOT}/tmp/${tag}_p${p}.bin" bs=1M count="$SCRATCH_MB" status=none + dd if="${ROOT}/tmp/${tag}_p${p}.bin" of=/dev/null bs=1M status=none + done + cp -f "${ROOT}/tmp/${tag}_p${PASSES}.spef" "${ROOT}/outputs/${tag}.spef" +} + +for _ in $(seq 1 "$OUTER"); do + run_one simple_net.lay simple_net + run_one coupled_nets.lay coupled_nets + run_one via_stack.lay via_stack + run_one big_bus.lay big_bus +done + +{ + echo "done_epoch=$(date +%s.%N)" + echo "done=$(date -Iseconds)" +} | tee -a "$LOG" +echo PASS > logs/STATUS +du -sh tmp logs outputs >>"$LOG" +JOB + sed -i \ + -e "s|__RCX_BIN__|${rcx_bin}|g" \ + -e "s|__SCRATCH_MB__|${SCRATCH_MB}|g" \ + -e "s|__PASSES__|${PASSES}|g" \ + -e "s|__OUTER__|${OUTER}|g" \ + "${dest}/scripts/run_job.sh" + chmod +x "${dest}/scripts/run_job.sh" +} + +prepare_job() { + local dest="$1" + rm -rf "$dest" + mkdir -p "$dest"/{inputs,scripts,logs,tmp,outputs} + cp -a "${RCX_SRC}/examples/"*.lay "$dest/inputs/" + cp -a "${RCX_SRC}/rcx_extract" "$dest/scripts/rcx_extract" + chmod +x "$dest/scripts/rcx_extract" + python3 - <<'PY' > "$dest/inputs/big_bus.lay" +print("NAME big_bus") +for i in range(120): + y0 = i * 0.5 + y1 = y0 + 0.14 + net = f"n{i}" + print(f"METAL M1 {net} w{i}_m1 0.0 {y0:.3f} 160.0 {y1:.3f}") + print(f"VIA VIA1 {net} v{i} 159.8 {y0:.3f} 160.0 {y1:.3f}") + print(f"METAL M2 {net} w{i}_m2 160.0 {y0:.3f} 200.0 {y1:.3f}") + print(f"PIN D{i} {net} M1 O 0.0 {(y0+y1)/2:.3f}") + print(f"PIN L{i} {net} M2 I 200.0 {(y0+y1)/2:.3f}") +PY + write_run_job "$dest" +} + +time_now() { date +%s.%N; } +elapsed() { python3 -c "print(f'{float('$2')-float('$1'):.3f}')"; } + +tool_seconds_from_log() { + python3 - </dev/null 2>&1 || true + echo "---- BASELINE (no acceleration; everything on disk) ----" + local t0 t1 + t0=$(time_now) + ( cd "$dest" && bash scripts/run_job.sh >/dev/null ) + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/baseline.e2e" + tool_seconds_from_log "$dest/logs/rcx_extract.log" > "${OUT_DIR}/baseline.tool" + echo "baseline e2e=$(cat "${OUT_DIR}/baseline.e2e")s tool=$(cat "${OUT_DIR}/baseline.tool")s" + echo " tmp=$(readlink -f "$dest/tmp")" + echo " logs=$(readlink -f "$dest/logs")" + grep -E 'tmp_resolved=|logs_resolved=|start_epoch=|done_epoch=' "$dest/logs/rcx_extract.log" + cp -f "$dest/logs/rcx_extract.log" "${OUT_DIR}/baseline.log" + du -sh "$dest/tmp" "$dest/outputs" +} + +run_mode_b() { + local dest="${OUT_DIR}/mode_b_job" + prepare_job "$dest" + unset TMPDIR TMP TEMP || true + export PD_DURABLE_ROOT="$dest" PD_JOB_NAME="compare_mode_b" + export PD_TOOL_CMD='bash scripts/run_job.sh' PD_RAM_PATHS="tmp" PD_KEEP_LOGS_ON_DISK=1 + sync; echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null 2>&1 || true + echo "---- MODE B (tmp/TMPDIR in RAM; logs on disk) ----" + local t0 t1 + t0=$(time_now) + "${ROOT}/scripts/ram_scratch.sh" run >/dev/null + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/mode_b.e2e" + tool_seconds_from_log "$dest/logs/rcx_extract.log" > "${OUT_DIR}/mode_b.tool" + echo "mode_b e2e=$(cat "${OUT_DIR}/mode_b.e2e")s tool=$(cat "${OUT_DIR}/mode_b.tool")s" + grep -E 'tmp_resolved=|logs_resolved=|start_epoch=|done_epoch=' "$dest/logs/rcx_extract.log" + cp -f "$dest/logs/rcx_extract.log" "${OUT_DIR}/mode_b.log" + du -sh "$dest/tmp" "$dest/outputs" +} + +run_mode_a() { + local dest="${OUT_DIR}/mode_a_job" + prepare_job "$dest" + unset TMPDIR TMP TEMP || true + export PD_DURABLE_ROOT="$dest" PD_JOB_NAME="compare_mode_a" + export PD_TOOL_CMD='bash scripts/run_job.sh' PD_CHECKPOINT_SECS=0 PD_KEEP_LOGS_ON_DISK=1 + sync; echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null 2>&1 || true + echo "---- MODE A (workspace in tmpfs; logs on disk) ----" + local t0 t1 + t0=$(time_now) + "${ROOT}/scripts/run_pd_job.sh" >/dev/null + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/mode_a.e2e" + tool_seconds_from_log "$dest/logs/rcx_extract.log" > "${OUT_DIR}/mode_a.tool" + echo "mode_a e2e=$(cat "${OUT_DIR}/mode_a.e2e")s tool=$(cat "${OUT_DIR}/mode_a.tool")s" + grep -E 'tmp_resolved=|logs_resolved=|start_epoch=|done_epoch=' "$dest/logs/rcx_extract.log" + cp -f "$dest/logs/rcx_extract.log" "${OUT_DIR}/mode_a.log" + du -sh "$dest/tmp" "$dest/outputs" +} + +summarize() { + python3 - <10} {'vs base':>10} {'e2e_s':>10} {'vs base':>10}") +print("-" * 72) +for name, tool, e2e in rows: + dt = (bt-tool)/bt*100 if bt else 0 + de = (be-e2e)/be*100 if be else 0 + print(f"{name:<28} {tool:>10.3f} {'—' if name.startswith('baseline') else f'{dt:+.1f}%':>10} {e2e:>10.3f} {'—' if name.startswith('baseline') else f'{de:+.1f}%':>10}") +print("-" * 72) +print("tool_s = job start→done (compute + scratch I/O only)") +print("e2e_s = includes stage / rsync materialize / teardown") +print("SPEF checksums should match (same functional results).") +print("This host disk is local/fast; NFS usually amplifies tool_s wins.") +print("=" * 72) +PY + echo + echo "SPEF checksums (same results?):" + ( + cd "$OUT_DIR" + for d in baseline_job mode_b_job mode_a_job; do + echo "-- $d --" + sha256sum "$d"/outputs/*.spef | awk '{print $1, $2}' | sort -k2 + done + ) +} + +rm -rf /dev/shm/pdjobs/"${USER:-user}" 2>/dev/null || true +run_baseline +echo +run_mode_b +echo +run_mode_a +echo +summarize +echo +echo "Summary file: ${OUT_DIR}/SUMMARY.txt" diff --git a/PD Job Acceleration/examples/compare_metal_fill.sh b/PD Job Acceleration/examples/compare_metal_fill.sh new file mode 100755 index 0000000..b31dad0 --- /dev/null +++ b/PD Job Acceleration/examples/compare_metal_fill.sh @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +# Compare gpu_metal_fill WITH vs WITHOUT tmpfs/rsync acceleration. +# Same exercise as compare_accel.sh, but for the BEOL metal-fill job. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO_ROOT="$(cd "${ROOT}/.." && pwd)" +FILL_SRC="${REPO_ROOT}/gpu_metal_fill" +OUT_DIR="${ROOT}/examples/compare_metal_fill_results" +SM_GRID="${SM_GRID:-4}" # make_gpu_block -n +OUTER="${OUTER:-2}" # repeat fill this many times +mkdir -p "$OUT_DIR" + +[[ -x "${FILL_SRC}/build/run_fill" && -x "${FILL_SRC}/build/make_gpu_block" ]] || { + echo "Building gpu_metal_fill..." + make -C "$FILL_SRC" -j"$(nproc)" +} + +write_run_job() { + local dest="$1" + cat > "${dest}/scripts/run_job.sh" <<'JOB' +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(pwd)" +MAKE_BLOCK="__MAKE_BLOCK__" +RUN_FILL="__RUN_FILL__" +GDSINFO="__GDSINFO__" +SM_GRID=__SM_GRID__ +OUTER=__OUTER__ +LOG="${ROOT}/logs/metal_fill.log" +mkdir -p logs tmp outputs +{ + echo "cwd=${ROOT}" + echo "TMPDIR=${TMPDIR:-unset}" + echo "tmp_resolved=$(readlink -f tmp)" + echo "logs_resolved=$(readlink -f logs)" + echo "start_epoch=$(date +%s.%N)" + echo "start=$(date -Iseconds)" +} | tee "$LOG" + +# Generate input once into durable inputs/ (if missing) — generation is not timed +# as the "fill job"; we time the fill loop. For fairness, pre-generated in prepare. + +for i in $(seq 1 "$OUTER"); do + echo "==== fill pass $i/$OUTER ====" | tee -a "$LOG" + # Write large filled GDS into tmp/ (hot path), report into logs/ (disk) + "$RUN_FILL" \ + -i "${ROOT}/inputs/gpu_block.gds" \ + -o "${ROOT}/tmp/gpu_filled_p${i}.gds" \ + -r "${ROOT}/logs/report_p${i}.txt" \ + 2>&1 | tee -a "$LOG" + # Scratch churn: copy/read the filled GDS in tmp + dd if="${ROOT}/tmp/gpu_filled_p${i}.gds" of="${ROOT}/tmp/gpu_filled_p${i}.copy" bs=4M status=none + dd if="${ROOT}/tmp/gpu_filled_p${i}.copy" of=/dev/null bs=4M status=none +done + +# Final deliverables on durable outputs/ +cp -f "${ROOT}/tmp/gpu_filled_p${OUTER}.gds" "${ROOT}/outputs/gpu_filled.gds" +cp -f "${ROOT}/logs/report_p${OUTER}.txt" "${ROOT}/outputs/report.txt" +"$GDSINFO" "${ROOT}/inputs/gpu_block.gds" "${ROOT}/outputs/gpu_filled.gds" \ + > "${ROOT}/outputs/gdsinfo.txt" 2>&1 || true + +{ + echo "done_epoch=$(date +%s.%N)" + echo "done=$(date -Iseconds)" + ls -lh outputs/ tmp/*.gds 2>/dev/null | head -20 +} | tee -a "$LOG" +echo PASS > logs/STATUS +du -sh tmp logs outputs >>"$LOG" +JOB + sed -i \ + -e "s|__MAKE_BLOCK__|${dest}/scripts/make_gpu_block|g" \ + -e "s|__RUN_FILL__|${dest}/scripts/run_fill|g" \ + -e "s|__GDSINFO__|${dest}/scripts/gdsinfo|g" \ + -e "s|__SM_GRID__|${SM_GRID}|g" \ + -e "s|__OUTER__|${OUTER}|g" \ + "${dest}/scripts/run_job.sh" + chmod +x "${dest}/scripts/run_job.sh" +} + +prepare_job() { + local dest="$1" + rm -rf "$dest" + mkdir -p "$dest"/{inputs,scripts,logs,tmp,outputs} + + # Binaries on durable disk ( /dev/shm is often noexec ) + cp -a "${FILL_SRC}/build/make_gpu_block" \ + "${FILL_SRC}/build/run_fill" \ + "${FILL_SRC}/build/gdsinfo" \ + "$dest/scripts/" + chmod +x "$dest/scripts/"* + + echo "Generating gpu_block.gds (SM_GRID=${SM_GRID}) into ${dest}/inputs ..." + "${dest}/scripts/make_gpu_block" -o "${dest}/inputs/gpu_block.gds" -n "$SM_GRID" + ls -lh "${dest}/inputs/gpu_block.gds" + + write_run_job "$dest" +} + +time_now() { date +%s.%N; } +elapsed() { python3 -c "print(f'{float('$2')-float('$1'):.3f}')"; } + +tool_seconds_from_log() { + python3 - </dev/null 2>&1 || true + echo "---- BASELINE (no acceleration; everything on disk) ----" + local t0 t1 + t0=$(time_now) + ( cd "$dest" && bash scripts/run_job.sh >/dev/null ) + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/baseline.e2e" + tool_seconds_from_log "$dest/logs/metal_fill.log" > "${OUT_DIR}/baseline.tool" + echo "baseline e2e=$(cat "${OUT_DIR}/baseline.e2e")s tool=$(cat "${OUT_DIR}/baseline.tool")s" + grep -E 'tmp_resolved=|logs_resolved=|start_epoch=|done_epoch=|total time' "$dest/logs/metal_fill.log" | head -20 + cp -f "$dest/logs/metal_fill.log" "${OUT_DIR}/baseline.log" + du -sh "$dest/tmp" "$dest/outputs" "$dest/logs" + ls -lh "$dest/outputs/" +} + +run_mode_b() { + local dest="${OUT_DIR}/mode_b_job" + prepare_job "$dest" + unset TMPDIR TMP TEMP || true + export PD_DURABLE_ROOT="$dest" PD_JOB_NAME="mfill_mode_b" + export PD_TOOL_CMD='bash scripts/run_job.sh' PD_RAM_PATHS="tmp" PD_KEEP_LOGS_ON_DISK=1 + sync; echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null 2>&1 || true + echo "---- MODE B (tmp/TMPDIR in RAM; logs on disk) ----" + local t0 t1 + t0=$(time_now) + "${ROOT}/scripts/ram_scratch.sh" run >/dev/null + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/mode_b.e2e" + tool_seconds_from_log "$dest/logs/metal_fill.log" > "${OUT_DIR}/mode_b.tool" + echo "mode_b e2e=$(cat "${OUT_DIR}/mode_b.e2e")s tool=$(cat "${OUT_DIR}/mode_b.tool")s" + grep -E 'tmp_resolved=|logs_resolved=|start_epoch=|done_epoch=|total time' "$dest/logs/metal_fill.log" | head -20 + cp -f "$dest/logs/metal_fill.log" "${OUT_DIR}/mode_b.log" + du -sh "$dest/tmp" "$dest/outputs" "$dest/logs" + ls -lh "$dest/outputs/" +} + +run_mode_a() { + local dest="${OUT_DIR}/mode_a_job" + prepare_job "$dest" + unset TMPDIR TMP TEMP || true + export PD_DURABLE_ROOT="$dest" PD_JOB_NAME="mfill_mode_a" + export PD_TOOL_CMD='bash scripts/run_job.sh' PD_CHECKPOINT_SECS=0 PD_KEEP_LOGS_ON_DISK=1 + sync; echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null 2>&1 || true + echo "---- MODE A (workspace in tmpfs; logs on disk) ----" + local t0 t1 + t0=$(time_now) + "${ROOT}/scripts/run_pd_job.sh" >/dev/null + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/mode_a.e2e" + tool_seconds_from_log "$dest/logs/metal_fill.log" > "${OUT_DIR}/mode_a.tool" + echo "mode_a e2e=$(cat "${OUT_DIR}/mode_a.e2e")s tool=$(cat "${OUT_DIR}/mode_a.tool")s" + grep -E 'tmp_resolved=|logs_resolved=|start_epoch=|done_epoch=|total time' "$dest/logs/metal_fill.log" | head -20 + cp -f "$dest/logs/metal_fill.log" "${OUT_DIR}/mode_a.log" + du -sh "$dest/tmp" "$dest/outputs" "$dest/logs" + ls -lh "$dest/outputs/" +} + +summarize() { + python3 - <10} {'vs base':>10} {'e2e_s':>10} {'vs base':>10}") +print("-" * 72) +for name, tool, e2e in rows: + dt = (bt-tool)/bt*100 if bt else 0 + de = (be-e2e)/be*100 if be else 0 + print(f"{name:<28} {tool:>10.3f} {'—' if name.startswith('baseline') else f'{dt:+.1f}%':>10} {e2e:>10.3f} {'—' if name.startswith('baseline') else f'{de:+.1f}%':>10}") +print("-" * 72) +print("tool_s = fill start→done (includes large GDS write/read in tmp/)") +print("e2e_s = includes stage / rsync materialize / teardown") +print("logs/ always on disk; fat filled.gds goes through tmp/ first.") +print("Note: gpu_metal_fill is mostly CPU-bound (OpenMP fill); I/O accel helps") +print("the GDS write/read slice. On NFS that slice is usually a larger fraction.") +print("Filled GDS checksums should match; report.txt may differ (timers).") +print("=" * 72) +PY + echo + echo "Output GDS checksums (same results?):" + ( + cd "$OUT_DIR" + for d in baseline_job mode_b_job mode_a_job; do + echo "-- $d --" + sha256sum "$d"/outputs/gpu_filled.gds "$d"/outputs/report.txt 2>/dev/null + ls -lh "$d"/outputs/gpu_filled.gds + done + ) +} + +rm -rf /dev/shm/pdjobs/"${USER:-user}" 2>/dev/null || true +echo "Metal fill acceleration compare SM_GRID=${SM_GRID} OUTER=${OUTER}" +run_baseline +echo +run_mode_b +echo +run_mode_a +echo +summarize +echo +echo "Summary: ${OUT_DIR}/SUMMARY.txt" diff --git a/PD Job Acceleration/examples/compare_pd_farm_io.sh b/PD Job Acceleration/examples/compare_pd_farm_io.sh new file mode 100755 index 0000000..1fc473a --- /dev/null +++ b/PD Job Acceleration/examples/compare_pd_farm_io.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +# Monumental before/after: PD farm I/O suite +# baseline = durable disk + emulated NFS RTT on every tiny op +# Mode B/A = same work on tmpfs with nfs-us=0 +# +# This is the real-world gap the pattern targets (NFS metadata vs RAM), +# not local-SSD vs tmpfs (which is often modest). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT_DIR="${ROOT}/examples/compare_pd_farm_io_results" +SUITE="${ROOT}/examples/pd_farm_io_suite.py" + +# Emulated NFS per-op RTT for the baseline path only (microseconds). +NFS_US="${NFS_US:-400}" +CELLS="${CELLS:-20000}" +LOOKUPS="${LOOKUPS:-150000}" +NETS="${NETS:-15000}" +CHECKPOINTS="${CHECKPOINTS:-3}" +DB_MB="${DB_MB:-16}" +REPORTS="${REPORTS:-3000}" + +mkdir -p "$OUT_DIR" +chmod +x "$SUITE" + +prepare_job() { + local dest="$1" + rm -rf "$dest" + mkdir -p "$dest"/{inputs,scripts,logs,tmp,outputs} + cp -a "$SUITE" "$dest/scripts/pd_farm_io_suite.py" + echo "pd farm io suite" > "$dest/inputs/README.txt" +} + +time_now() { date +%s.%N; } +elapsed() { python3 -c "print(f'{float('$2')-float('$1'):.3f}')"; } + +tool_seconds_from_log() { + python3 - </dev/null 2>&1 || true; } + +run_cmd() { + local nfs="$1" + python3 scripts/pd_farm_io_suite.py \ + --root . \ + --nfs-us "$nfs" \ + --cells "$CELLS" \ + --lookups "$LOOKUPS" \ + --nets "$NETS" \ + --checkpoints "$CHECKPOINTS" \ + --db-mb "$DB_MB" \ + --reports "$REPORTS" +} + +run_baseline() { + local dest="${OUT_DIR}/baseline_job" + prepare_job "$dest" + unset TMPDIR TMP TEMP || true + drop_caches + echo "---- BASELINE: disk + emulated NFS RTT (${NFS_US}µs/op) ----" + local t0 t1 + t0=$(time_now) + ( cd "$dest" && run_cmd "$NFS_US" >/dev/null ) + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/baseline.e2e" + tool_seconds_from_log "$dest/logs/pd_farm_io.log" > "${OUT_DIR}/baseline.tool" + echo "baseline e2e=$(cat "${OUT_DIR}/baseline.e2e")s tool=$(cat "${OUT_DIR}/baseline.tool")s" + grep -E 'phase=|nfs_us=|tmp_resolved=|checksum=|ops=' "$dest/logs/pd_farm_io.log" | head -40 + cp -f "$dest/logs/pd_farm_io.log" "${OUT_DIR}/baseline.log" + cp -f "$dest/outputs/lookup_checksum.txt" "${OUT_DIR}/baseline.checksum" + cp -f "$dest/outputs/JOB_SUMMARY.txt" "${OUT_DIR}/baseline.summary" + du -sh "$dest/tmp" "$dest/outputs" "$dest/logs" +} + +run_mode_b() { + local dest="${OUT_DIR}/mode_b_job" + prepare_job "$dest" + unset TMPDIR TMP TEMP || true + export PD_DURABLE_ROOT="$dest" PD_JOB_NAME="farmio_mode_b" + export PD_RAM_PATHS="tmp" PD_KEEP_LOGS_ON_DISK=1 PD_FLUSH_ON_TEARDOWN=0 + # Accelerated path: no NFS tax (tmpfs is local RAM) + export PD_TOOL_CMD="python3 scripts/pd_farm_io_suite.py --root . --nfs-us 0 --cells $CELLS --lookups $LOOKUPS --nets $NETS --checkpoints $CHECKPOINTS --db-mb $DB_MB --reports $REPORTS" + drop_caches + echo "---- MODE B: tmp in RAM, nfs-us=0 ----" + local t0 t1 + t0=$(time_now) + "${ROOT}/scripts/ram_scratch.sh" run >/dev/null + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/mode_b.e2e" + tool_seconds_from_log "$dest/logs/pd_farm_io.log" > "${OUT_DIR}/mode_b.tool" + echo "mode_b e2e=$(cat "${OUT_DIR}/mode_b.e2e")s tool=$(cat "${OUT_DIR}/mode_b.tool")s" + grep -E 'phase=|nfs_us=|tmp_resolved=|checksum=|ops=' "$dest/logs/pd_farm_io.log" | head -40 + cp -f "$dest/logs/pd_farm_io.log" "${OUT_DIR}/mode_b.log" + cp -f "$dest/outputs/lookup_checksum.txt" "${OUT_DIR}/mode_b.checksum" + cp -f "$dest/outputs/JOB_SUMMARY.txt" "${OUT_DIR}/mode_b.summary" + du -sh "$dest/tmp" "$dest/outputs" "$dest/logs" +} + +run_mode_a() { + local dest="${OUT_DIR}/mode_a_job" + prepare_job "$dest" + unset TMPDIR TMP TEMP || true + export PD_DURABLE_ROOT="$dest" PD_JOB_NAME="farmio_mode_a" + export PD_CHECKPOINT_SECS=0 PD_KEEP_LOGS_ON_DISK=1 + export PD_EXCLUDE_FILE="${ROOT}/examples/pd_io_rsync_excludes.txt" + export PD_TOOL_CMD="python3 scripts/pd_farm_io_suite.py --root . --nfs-us 0 --cells $CELLS --lookups $LOOKUPS --nets $NETS --checkpoints $CHECKPOINTS --db-mb $DB_MB --reports $REPORTS" + drop_caches + echo "---- MODE A: workspace tmpfs, nfs-us=0 ----" + local t0 t1 + t0=$(time_now) + "${ROOT}/scripts/run_pd_job.sh" >/dev/null + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/mode_a.e2e" + tool_seconds_from_log "$dest/logs/pd_farm_io.log" > "${OUT_DIR}/mode_a.tool" + echo "mode_a e2e=$(cat "${OUT_DIR}/mode_a.e2e")s tool=$(cat "${OUT_DIR}/mode_a.tool")s" + grep -E 'phase=|nfs_us=|tmp_resolved=|checksum=|ops=' "$dest/logs/pd_farm_io.log" | head -40 + cp -f "$dest/logs/pd_farm_io.log" "${OUT_DIR}/mode_a.log" + cp -f "$dest/outputs/lookup_checksum.txt" "${OUT_DIR}/mode_a.checksum" + cp -f "$dest/outputs/JOB_SUMMARY.txt" "${OUT_DIR}/mode_a.summary" + du -sh "$dest/tmp" "$dest/outputs" "$dest/logs" +} + +summarize() { + python3 - <10} {'speedup':>10} {'e2e_s':>10} {'speedup':>10}") +print("-" * 76) +for name, tool, e2e in rows: + st = (bt/tool) if tool else 0 + se = (be/e2e) if e2e else 0 + if name.startswith("baseline"): + print(f"{name:<28} {tool:>10.3f} {'—':>10} {e2e:>10.3f} {'—':>10}") + else: + print(f"{name:<28} {tool:>10.3f} {st:>9.1f}x {e2e:>10.3f} {se:>9.1f}x") +print("-" * 76) +print("Phases: liberty vault → random lib lookups → SPEF shard merge →") +print(" ECO checkpoints → report spam") +print("Baseline adds emulated NFS metadata RTT on every tiny op (farm model).") +print("Mode B/A run identical work on tmpfs with nfs-us=0 (RAM model).") +print("Checksums must match. logs/ stay on disk; hot data under tmp/.") +print("=" * 76) +PY + echo + echo "Checksums:" + for c in baseline mode_b mode_a; do + echo " $c: $(cat "${OUT_DIR}/${c}.checksum") ops/bytes: $(tr '\n' ' ' < "${OUT_DIR}/${c}.summary")" + done + echo + echo "tmp_resolved:" + for c in baseline mode_b mode_a; do + echo -n " $c: " + grep -o 'tmp_resolved=[^ ]*' "${OUT_DIR}/${c}.log" | head -1 + done +} + +rm -rf /dev/shm/pdjobs/"${USER:-user}" 2>/dev/null || true +echo "PD farm I/O monumental compare NFS_US=${NFS_US}µs" +run_baseline +echo +run_mode_b +echo +run_mode_a +echo +summarize +echo +echo "Summary: ${OUT_DIR}/SUMMARY.txt" diff --git a/PD Job Acceleration/examples/compare_pd_io.sh b/PD Job Acceleration/examples/compare_pd_io.sh new file mode 100755 index 0000000..b7d402f --- /dev/null +++ b/PD Job Acceleration/examples/compare_pd_io.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# Compare an I/O-bound PD-flavored job: baseline disk vs Mode B/A acceleration. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT_DIR="${ROOT}/examples/compare_pd_io_results" +JOB_SRC="${ROOT}/examples/pd_io_bound_job.sh" + +# Tunables (override on CLI env) +export N_CELLS="${N_CELLS:-40000}" +export N_LOOKUPS="${N_LOOKUPS:-400000}" +export DB_MB="${DB_MB:-64}" +export N_CHECKPOINTS="${N_CHECKPOINTS:-4}" +export N_REPORTS="${N_REPORTS:-5000}" + +mkdir -p "$OUT_DIR" +chmod +x "$JOB_SRC" + +prepare_job() { + local dest="$1" + rm -rf "$dest" + mkdir -p "$dest"/{inputs,scripts,logs,tmp,outputs} + cp -a "$JOB_SRC" "$dest/scripts/pd_io_bound_job.sh" + chmod +x "$dest/scripts/pd_io_bound_job.sh" + # seed a tiny marker input on durable disk + echo "pd_io_bound seed $(date -Iseconds)" > "$dest/inputs/README.txt" +} + +time_now() { date +%s.%N; } +elapsed() { python3 -c "print(f'{float('$2')-float('$1'):.3f}')"; } + +tool_seconds_from_log() { + python3 - </dev/null 2>&1 || true +} + +run_baseline() { + local dest="${OUT_DIR}/baseline_job" + prepare_job "$dest" + unset TMPDIR TMP TEMP || true + drop_caches + echo "---- BASELINE (no acceleration; everything on disk) ----" + local t0 t1 + t0=$(time_now) + ( + cd "$dest" + N_CELLS="$N_CELLS" N_LOOKUPS="$N_LOOKUPS" DB_MB="$DB_MB" \ + N_CHECKPOINTS="$N_CHECKPOINTS" N_REPORTS="$N_REPORTS" \ + bash scripts/pd_io_bound_job.sh >/dev/null + ) + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/baseline.e2e" + tool_seconds_from_log "$dest/logs/pd_io.log" > "${OUT_DIR}/baseline.tool" + echo "baseline e2e=$(cat "${OUT_DIR}/baseline.e2e")s tool=$(cat "${OUT_DIR}/baseline.tool")s" + grep -E 'tmp_resolved=|logs_resolved=|phase=|start_epoch=|done_epoch=|checksum=' "$dest/logs/pd_io.log" | head -30 + cp -f "$dest/logs/pd_io.log" "${OUT_DIR}/baseline.log" + cp -f "$dest/outputs/lookup_checksum.txt" "${OUT_DIR}/baseline.checksum" + du -sh "$dest/tmp" "$dest/outputs" "$dest/logs" +} + +run_mode_b() { + local dest="${OUT_DIR}/mode_b_job" + prepare_job "$dest" + unset TMPDIR TMP TEMP || true + export PD_DURABLE_ROOT="$dest" PD_JOB_NAME="pdio_mode_b" + export PD_TOOL_CMD="env N_CELLS=$N_CELLS N_LOOKUPS=$N_LOOKUPS DB_MB=$DB_MB N_CHECKPOINTS=$N_CHECKPOINTS N_REPORTS=$N_REPORTS bash scripts/pd_io_bound_job.sh" + export PD_RAM_PATHS="tmp" PD_KEEP_LOGS_ON_DISK=1 + # outputs/ already has deliverables — don't flush regenerable vault/DBs home + export PD_FLUSH_ON_TEARDOWN=0 + drop_caches + echo "---- MODE B (tmp/TMPDIR in RAM; logs on disk) ----" + local t0 t1 + t0=$(time_now) + "${ROOT}/scripts/ram_scratch.sh" run >/dev/null + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/mode_b.e2e" + tool_seconds_from_log "$dest/logs/pd_io.log" > "${OUT_DIR}/mode_b.tool" + echo "mode_b e2e=$(cat "${OUT_DIR}/mode_b.e2e")s tool=$(cat "${OUT_DIR}/mode_b.tool")s" + grep -E 'tmp_resolved=|logs_resolved=|phase=|start_epoch=|done_epoch=|checksum=' "$dest/logs/pd_io.log" | head -30 + cp -f "$dest/logs/pd_io.log" "${OUT_DIR}/mode_b.log" + cp -f "$dest/outputs/lookup_checksum.txt" "${OUT_DIR}/mode_b.checksum" + du -sh "$dest/tmp" "$dest/outputs" "$dest/logs" +} + +run_mode_a() { + local dest="${OUT_DIR}/mode_a_job" + prepare_job "$dest" + unset TMPDIR TMP TEMP || true + export PD_DURABLE_ROOT="$dest" PD_JOB_NAME="pdio_mode_a" + export PD_TOOL_CMD="env N_CELLS=$N_CELLS N_LOOKUPS=$N_LOOKUPS DB_MB=$DB_MB N_CHECKPOINTS=$N_CHECKPOINTS N_REPORTS=$N_REPORTS bash scripts/pd_io_bound_job.sh" + export PD_CHECKPOINT_SECS=0 PD_KEEP_LOGS_ON_DISK=1 + export PD_EXCLUDE_FILE="${ROOT}/examples/pd_io_rsync_excludes.txt" + drop_caches + echo "---- MODE A (workspace in tmpfs; logs on disk) ----" + local t0 t1 + t0=$(time_now) + "${ROOT}/scripts/run_pd_job.sh" >/dev/null + t1=$(time_now) + elapsed "$t0" "$t1" > "${OUT_DIR}/mode_a.e2e" + tool_seconds_from_log "$dest/logs/pd_io.log" > "${OUT_DIR}/mode_a.tool" + echo "mode_a e2e=$(cat "${OUT_DIR}/mode_a.e2e")s tool=$(cat "${OUT_DIR}/mode_a.tool")s" + grep -E 'tmp_resolved=|logs_resolved=|phase=|start_epoch=|done_epoch=|checksum=' "$dest/logs/pd_io.log" | head -30 + cp -f "$dest/logs/pd_io.log" "${OUT_DIR}/mode_a.log" + cp -f "$dest/outputs/lookup_checksum.txt" "${OUT_DIR}/mode_a.checksum" + du -sh "$dest/tmp" "$dest/outputs" "$dest/logs" +} + +summarize() { + python3 - <10} {'vs base':>10} {'e2e_s':>10} {'vs base':>10}") +print("-" * 72) +for name, tool, e2e in rows: + dt = (bt-tool)/bt*100 if bt else 0 + de = (be-e2e)/be*100 if be else 0 + print(f"{name:<28} {tool:>10.3f} {'—' if name.startswith('baseline') else f'{dt:+.1f}%':>10} {e2e:>10.3f} {'—' if name.startswith('baseline') else f'{de:+.1f}%':>10}") +print("-" * 72) +print("Workload: tiny random liberty lookups + fat DEF/DB checkpoints + report spam.") +print("tool_s = job start→done; e2e_s includes stage/rsync/teardown.") +print("logs/ on disk; hot vault/DB/reports under tmp/ (RAM in Mode B/A).") +print("=" * 72) +PY + echo + echo "Functional checksums (liberty lookup stamp):" + for c in baseline mode_b mode_a; do + echo " $c: $(cat "${OUT_DIR}/${c}.checksum")" + done + echo + echo "Where tmp lived:" + for c in baseline mode_b mode_a; do + echo -n " $c: " + grep '^tmp_resolved=' "${OUT_DIR}/${c}.log" + done +} + +rm -rf /dev/shm/pdjobs/"${USER:-user}" 2>/dev/null || true +echo "PD I/O-bound compare cells=${N_CELLS} lookups=${N_LOOKUPS} db=${DB_MB}MB x${N_CHECKPOINTS}" +run_baseline +echo +run_mode_b +echo +run_mode_a +echo +summarize +echo +echo "Summary: ${OUT_DIR}/SUMMARY.txt" diff --git a/PD Job Acceleration/examples/demo_perf_and_sync.sh b/PD Job Acceleration/examples/demo_perf_and_sync.sh new file mode 100755 index 0000000..eeb3350 --- /dev/null +++ b/PD Job Acceleration/examples/demo_perf_and_sync.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Smoke demo: profile a tiny disk-heavy job with perf, then Mode B + durable sync. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=../scripts/pd_job_env.sh +source "${ROOT}/scripts/pd_job_env.sh" + +JOB="${ROOT}/examples/demo_perf_sync_job" +rm -rf "$JOB" +mkdir -p "$JOB/tmp" "$JOB/logs" "$JOB/outputs" + +# Tiny I/O-ish workload: many small files + a fat write. +cat >"$JOB/scripts_run.sh" <<'EOS' +set -euo pipefail +mkdir -p tmp/vault outputs logs +for i in $(seq 1 2000); do + printf 'cell_%05d delay=%d\n' "$i" "$i" >"tmp/vault/c_${i}.libfrag" +done +# random reads +for i in $(seq 1 5000); do + n=$(( (i * 7919) % 2000 + 1 )) + cat "tmp/vault/c_${n}.libfrag" >/dev/null +done +dd if=/dev/urandom of=outputs/eco.db bs=1M count=8 status=none +echo "ok $(date -Iseconds)" | tee logs/job.log +EOS + +echo "=== 1) Baseline profile on disk ===" +export PD_DURABLE_ROOT="$JOB" +export PD_JOB_NAME="perf_sync_demo" +export PD_TOOL_CMD='bash ./scripts_run.sh' +( + cd "$JOB" + "${ROOT}/scripts/pd_perf_profile.sh" --out "${JOB}/logs/perf_baseline" -- bash ./scripts_run.sh +) +echo +cat "${JOB}/logs/perf_baseline/SUMMARY.txt" + +echo +echo "=== 2) Mode B (tmp in RAM) + finalize durability sync ===" +rm -rf "$JOB/tmp" "$JOB/outputs" "$JOB/logs/job.log" +mkdir -p "$JOB/tmp" "$JOB/outputs" +export PD_RAM_PATHS="tmp" +export PD_SYNC_MODE="fs" +export PD_SYNC_AFTER_FINALIZE="1" +export PD_PERF="1" +"${ROOT}/scripts/ram_scratch.sh" run + +echo +echo "=== Artifacts ===" +ls -la "${JOB}/.pd_job_status" 2>/dev/null || true +ls -la "${JOB}/logs"/perf_* 2>/dev/null || true +[[ -f "${JOB}/.pd_job_status/sync.log" ]] && { echo "-- sync.log --"; cat "${JOB}/.pd_job_status/sync.log"; } +if [[ -f "${JOB}/logs/perf_perf_sync_demo/SUMMARY.txt" ]]; then + echo "-- Mode B perf SUMMARY --" + cat "${JOB}/logs/perf_perf_sync_demo/SUMMARY.txt" +fi +echo "demo done" diff --git a/PD Job Acceleration/examples/pd_farm_io_suite.py b/PD Job Acceleration/examples/pd_farm_io_suite.py new file mode 100755 index 0000000..bbc2488 --- /dev/null +++ b/PD Job Acceleration/examples/pd_farm_io_suite.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +""" +Multi-phase I/O-bound Physical Design farm suite. + +Phases (all PD-shaped, minimal compute): + 1) liberty_vault — tens of thousands of tiny .lib cell files + 2) lib_lookups — hundreds of thousands of random cell opens/reads + 3) spef_shards — many per-net SPEF fragments + merge stream + 4) eco_checkpoints — repeated fat DB write+read storms + 5) report_spam — thousands of tiny window reports + growing log + +Optional NFS RTT emulation (--nfs-us): sleep this many microseconds around +each tiny file open/read/write. Use on the baseline (durable) path to model +farm NFS metadata latency. Leave at 0 on tmpfs. + +This is the difference the tmpfs+rsync pattern is designed for. +""" +from __future__ import annotations + +import argparse +import os +import random +import struct +import sys +import time +from pathlib import Path + + +def now() -> float: + return time.perf_counter() + + +def stamp(log, **kv): + line = " ".join(f"{k}={v}" for k, v in kv.items()) + print(line, flush=True) + log.write(line + "\n") + log.flush() + + +class IOFS: + """Filesystem helper with optional per-op NFS RTT emulation.""" + + def __init__(self, nfs_us: int = 0): + self.nfs_us = max(0, int(nfs_us)) + self.ops = 0 + self.bytes_r = 0 + self.bytes_w = 0 + + def _rtt(self): + if self.nfs_us: + time.sleep(self.nfs_us / 1_000_000.0) + self.ops += 1 + + def write_text(self, path: Path, text: str): + self._rtt() + path.parent.mkdir(parents=True, exist_ok=True) + data = text.encode() + path.write_bytes(data) + self.bytes_w += len(data) + if self.nfs_us: + # metadata/commit cost on NFS-like durable stores + self._rtt() + + def write_bytes(self, path: Path, data: bytes): + self._rtt() + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + self.bytes_w += len(data) + if self.nfs_us: + self._rtt() + + def read_bytes(self, path: Path) -> bytes: + self._rtt() + data = path.read_bytes() + self.bytes_r += len(data) + return data + + def write_db(self, path: Path, mb: int): + """Fat sequential write (checkpoint).""" + path.parent.mkdir(parents=True, exist_ok=True) + chunk = os.urandom(1024 * 1024) + self._rtt() + with path.open("wb") as f: + for _ in range(mb): + f.write(chunk) + self.bytes_w += len(chunk) + if self.nfs_us: + f.flush() + os.fsync(f.fileno()) + self._rtt() + + def read_db(self, path: Path): + self._rtt() + with path.open("rb") as f: + while True: + b = f.read(1024 * 1024) + if not b: + break + self.bytes_r += len(b) + if self.nfs_us: + self._rtt() + + +def phase_liberty_vault(fs: IOFS, vault: Path, n_cells: int, log): + t0 = now() + if vault.exists(): + for p in vault.glob("cell_*.lib"): + p.unlink() + vault.mkdir(parents=True, exist_ok=True) + for i in range(n_cells): + text = ( + f"cell(CELL_{i:05d}) {{\n" + f" area : {1.0 + (i % 50) * 0.01};\n" + f" pin(A) {{ direction : input; capacitance : 0.{i % 900:03d}; }}\n" + f" pin(Z) {{ direction : output; function : \"A\"; }}\n" + f"}}\n" + ) + fs.write_text(vault / f"cell_{i:05d}.lib", text) + stamp(log, phase="liberty_vault", cells=n_cells, seconds=f"{now()-t0:.3f}") + + +def phase_lib_lookups(fs: IOFS, vault: Path, n_cells: int, n_lookups: int, log): + t0 = now() + rng = random.Random(42) + checksum = 0 + for _ in range(n_lookups): + i = rng.randrange(n_cells) + data = fs.read_bytes(vault / f"cell_{i:05d}.lib") + checksum = (checksum + data[0] + len(data)) & 0xFFFFFFFF + stamp( + log, + phase="lib_lookups", + lookups=n_lookups, + checksum=checksum, + seconds=f"{now()-t0:.3f}", + ) + return checksum + + +def phase_spef_shards(fs: IOFS, shard_dir: Path, n_nets: int, log): + t0 = now() + shard_dir.mkdir(parents=True, exist_ok=True) + # Write many tiny per-net SPEF fragments (common hierarchical SPEF pattern) + for i in range(n_nets): + body = ( + f"*D_NET net_{i:05d} 0.{i%999:03d}\n" + f"*CONN\n*I inst_{i%97}/Z O\n*I inst_{i%91}/A I\n" + f"*CAP\n1 inst_{i%97}/Z 0.{i%50:02d}\n" + f"*RES\n1 inst_{i%97}/Z inst_{i%91}/A {1+(i%20)}.{i%10}\n" + f"*END\n" + ) + fs.write_text(shard_dir / f"net_{i:05d}.spef", body) + + # Merge stream: read all shards sequentially into one SPEF + out = [] + out.append("SPEF\n*DESIGN \"chip\"\n") + for i in range(n_nets): + out.append(fs.read_bytes(shard_dir / f"net_{i:05d}.spef").decode()) + merged = "".join(out).encode() + fs.write_bytes(shard_dir / "merged.spef", merged) + stamp( + log, + phase="spef_shards", + nets=n_nets, + merged_bytes=len(merged), + seconds=f"{now()-t0:.3f}", + ) + + +def phase_eco_checkpoints(fs: IOFS, tmp: Path, n_ckpts: int, db_mb: int, log): + t0 = now() + for c in range(1, n_ckpts + 1): + p = tmp / f"eco_iter{c}.db" + fs.write_db(p, db_mb) + fs.read_db(p) + stamp( + log, + phase="eco_checkpoints", + checkpoints=n_ckpts, + db_mb=db_mb, + seconds=f"{now()-t0:.3f}", + ) + + +def phase_report_spam(fs: IOFS, report_dir: Path, grow_log: Path, n_reports: int, log): + t0 = now() + report_dir.mkdir(parents=True, exist_ok=True) + # growing opt log + many tiny window reports + chunks = [] + for i in range(n_reports): + fs.write_text( + report_dir / f"win_{i:04d}.rpt", + f"WINDOW {i}\nwns=-0.{i%99:02d}\ntns=-{i%500}.0\ndensity=0.{50+i%40}\n", + ) + chunks.append(f"OPT step={i} cost={i*17%10007} dens=0.{50+i%40}\n") + fs.write_text(grow_log, "".join(chunks)) + stamp(log, phase="report_spam", reports=n_reports, seconds=f"{now()-t0:.3f}") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--root", default=".", help="job root (cwd usually)") + ap.add_argument("--nfs-us", type=int, default=0, help="emulated NFS RTT per tiny op (µs)") + ap.add_argument("--cells", type=int, default=25000) + ap.add_argument("--lookups", type=int, default=200000) + ap.add_argument("--nets", type=int, default=20000) + ap.add_argument("--checkpoints", type=int, default=4) + ap.add_argument("--db-mb", type=int, default=32) + ap.add_argument("--reports", type=int, default=4000) + args = ap.parse_args() + + root = Path(args.root).resolve() + tmp = root / "tmp" + logs = root / "logs" + outputs = root / "outputs" + for d in (tmp, logs, outputs): + d.mkdir(parents=True, exist_ok=True) + + fs = IOFS(nfs_us=args.nfs_us) + log_path = logs / "pd_farm_io.log" + with log_path.open("w") as log: + stamp( + log, + cwd=str(root), + TMPDIR=os.environ.get("TMPDIR", "unset"), + tmp_resolved=str(tmp.resolve()), + logs_resolved=str(logs.resolve()), + nfs_us=args.nfs_us, + start_epoch=f"{time.time():.9f}", + start=time.strftime("%Y-%m-%dT%H:%M:%S%z"), + ) + t_all = now() + + vault = tmp / "lib_vault" + phase_liberty_vault(fs, vault, args.cells, log) + checksum = phase_lib_lookups(fs, vault, args.cells, args.lookups, log) + phase_spef_shards(fs, tmp / "spef_shards", args.nets, log) + phase_eco_checkpoints(fs, tmp, args.checkpoints, args.db_mb, log) + phase_report_spam(fs, tmp / "reports", logs / "opt_grow.log", args.reports, log) + + # Durable deliverables only + fs.write_text(outputs / "lookup_checksum.txt", f"{checksum}\n") + # Keep a compact SPEF head as proof of merge (not full multi‑MB unless small) + merged = tmp / "spef_shards" / "merged.spef" + data = merged.read_bytes() + # copy without extra NFS tax accounting on outputs (still real write) + (outputs / "merged_head.spef").write_bytes(data[: min(len(data), 1_000_000)]) + last_db = tmp / f"eco_iter{args.checkpoints}.db" + if last_db.exists(): + (outputs / "design_final.db").write_bytes(last_db.read_bytes()) + + summary = ( + f"checksum={checksum}\n" + f"nfs_us={args.nfs_us}\n" + f"ops={fs.ops}\n" + f"bytes_r={fs.bytes_r}\n" + f"bytes_w={fs.bytes_w}\n" + f"cells={args.cells}\n" + f"lookups={args.lookups}\n" + f"nets={args.nets}\n" + ) + (outputs / "JOB_SUMMARY.txt").write_text(summary) + (logs / "STATUS").write_text("PASS\n") + + stamp( + log, + phase="all_done", + seconds=f"{now()-t_all:.3f}", + ops=fs.ops, + bytes_r=fs.bytes_r, + bytes_w=fs.bytes_w, + checksum=checksum, + ) + stamp( + log, + done_epoch=f"{time.time():.9f}", + done=time.strftime("%Y-%m-%dT%H:%M:%S%z"), + ) + + print(summary) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/PD Job Acceleration/examples/pd_io_bound_job.sh b/PD Job Acceleration/examples/pd_io_bound_job.sh new file mode 100755 index 0000000..87d1c37 --- /dev/null +++ b/PD Job Acceleration/examples/pd_io_bound_job.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# I/O-bound Physical Design–flavored workload (no EDA license). +# +# Mimics farm I/O patterns that hurt over NFS / disk: +# - liberty-like cell vault: tens of thousands of tiny files, random reads +# - large DEF/DB-like streams: sequential write + read +# - frequent checkpoint / report appends +# +# Intentionally little CPU between I/O so wall time tracks filesystem latency. +set -euo pipefail + +ROOT="$(pwd)" +LOG="${ROOT}/logs/pd_io.log" +N_CELLS="${N_CELLS:-20000}" +N_LOOKUPS="${N_LOOKUPS:-80000}" +DB_MB="${DB_MB:-256}" +N_CHECKPOINTS="${N_CHECKPOINTS:-8}" +N_REPORTS="${N_REPORTS:-2000}" + +mkdir -p logs tmp outputs inputs/libs inputs/lef + +{ + echo "cwd=${ROOT}" + echo "TMPDIR=${TMPDIR:-unset}" + echo "tmp_resolved=$(readlink -f tmp)" + echo "logs_resolved=$(readlink -f logs)" + echo "N_CELLS=${N_CELLS} N_LOOKUPS=${N_LOOKUPS} DB_MB=${DB_MB}" + echo "start_epoch=$(date +%s.%N)" + echo "start=$(date -Iseconds)" +} | tee "$LOG" + +# ---------- 1) Build / refresh liberty-like cell vault (many tiny files) ---------- +# Prefer regenerating into tmp/ (hot), then the job reads from there. +VAULT="${ROOT}/tmp/lib_vault" +rm -rf "$VAULT" +mkdir -p "$VAULT" + +echo "phase=gen_vault cells=${N_CELLS}" | tee -a "$LOG" +# Batch-create tiny "liberty" snippets (name + 2 timing arcs). Keep CPU light. +python3 - < {vault}") +PY + +# Also a fat LEF-like blob (sequential) +echo "phase=gen_lef" | tee -a "$LOG" +dd if=/dev/urandom of="${ROOT}/tmp/tech.lef.bin" bs=1M count=64 status=none + +# ---------- 2) Random liberty lookups (tiny random reads — classic NFS killer) ---------- +echo "phase=lib_lookups count=${N_LOOKUPS}" | tee -a "$LOG" +python3 - </dev/null +echo "lookups_checksum=$(cat tmp/lookup_checksum.txt)" | tee -a "$LOG" + +# Sequential read of fat LEF +echo "phase=lef_scan" | tee -a "$LOG" +dd if="${ROOT}/tmp/tech.lef.bin" of=/dev/null bs=1M status=none + +# ---------- 3) Growing "design DB" checkpoints (large sequential writes) ---------- +echo "phase=checkpoints n=${N_CHECKPOINTS} each=${DB_MB}MB" | tee -a "$LOG" +for c in $(seq 1 "$N_CHECKPOINTS"); do + dd if=/dev/urandom of="${ROOT}/tmp/design_iter${c}.db" bs=1M count="$DB_MB" status=none + # read-back verify (placement tool style) + dd if="${ROOT}/tmp/design_iter${c}.db" of=/dev/null bs=1M status=none + echo "checkpoint=${c} bytes=$(wc -c < tmp/design_iter${c}.db)" >>"$LOG" +done + +# ---------- 4) Report spam (many small appends / files) ---------- +echo "phase=reports n=${N_REPORTS}" | tee -a "$LOG" +mkdir -p tmp/reports +python3 - </dev/null | head +} > outputs/JOB_SUMMARY.txt + +{ + echo "done_epoch=$(date +%s.%N)" + echo "done=$(date -Iseconds)" + du -sh tmp logs outputs inputs 2>/dev/null +} | tee -a "$LOG" + +echo PASS > logs/STATUS +cat outputs/JOB_SUMMARY.txt | tee -a "$LOG" diff --git a/PD Job Acceleration/examples/pd_io_rsync_excludes.txt b/PD Job Acceleration/examples/pd_io_rsync_excludes.txt new file mode 100644 index 0000000..47f284f --- /dev/null +++ b/PD Job Acceleration/examples/pd_io_rsync_excludes.txt @@ -0,0 +1,9 @@ +# Regenerable hot scratch — do not ship home on finalize (outputs/ already has deliverables) +tmp/lib_vault/ +tmp/reports/ +tmp/spef_shards/ +tmp/*.db +tmp/*.bin +tmp/*.lef.bin +tmp/*.copy +tmp/*.spef diff --git a/PD Job Acceleration/examples/rsync_excludes.txt b/PD Job Acceleration/examples/rsync_excludes.txt new file mode 100644 index 0000000..8349cdd --- /dev/null +++ b/PD Job Acceleration/examples/rsync_excludes.txt @@ -0,0 +1,14 @@ +# Extra excludes for PD checkpoint / finalize rsync. +# Pass via: export PD_EXCLUDE_FILE=/path/to/this/file + +# Tool scratch / regenerable +**/timing_tmp/** +**/capo*tmp* +**/*.drc.cache +**/*CongestionMap* +**/ezwave_tmp/** + +# Editor / OS noise +**/.DS_Store +**/Thumbs.db +**/*~ diff --git a/PD Job Acceleration/examples/run_rcx_accelerated.sh b/PD Job Acceleration/examples/run_rcx_accelerated.sh new file mode 100755 index 0000000..01f95ac --- /dev/null +++ b/PD Job Acceleration/examples/run_rcx_accelerated.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# Run the RC Extraction project through PD Job Acceleration (Mode B by default). +# +# There is no "metal fill" project on main of this repo. Closest real PD workload +# available is RC Extraction (branch cursor/rc-extraction-signoff-6f4a). +# +# Usage: +# ./examples/run_rcx_accelerated.sh # Mode B (tmp/TMPDIR in RAM) +# ./examples/run_rcx_accelerated.sh --mode-a # Mode A (workspace in tmpfs; logs on disk) +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO_ROOT="$(cd "${ROOT}/.." && pwd)" +RCX_SRC="${REPO_ROOT}/RC Extraction" +SCRIPTS="${ROOT}/scripts" +MODE="b" + +while [[ $# -gt 0 ]]; do + case "$1" in + --mode-a|-a) MODE="a"; shift ;; + --mode-b|-b) MODE="b"; shift ;; + -h|--help) + echo "Usage: $0 [--mode-a|--mode-b]"; exit 0 ;; + *) echo "unknown arg: $1" >&2; exit 1 ;; + esac +done + +[[ -x "${RCX_SRC}/rcx_extract" ]] || { + echo "Building RC Extraction..." + make -C "$RCX_SRC" -j"$(nproc)" +} + +JOB_DURABLE="${ROOT}/examples/rcx_job_durable" +rm -rf "$JOB_DURABLE" +mkdir -p "$JOB_DURABLE"/{inputs,scripts,logs,tmp,outputs} + +cp -a "${RCX_SRC}/examples/"*.lay "$JOB_DURABLE/inputs/" +cp -a "${RCX_SRC}/rcx_extract" "$JOB_DURABLE/scripts/rcx_extract" +chmod +x "$JOB_DURABLE/scripts/rcx_extract" + +# Synthetic larger layout (more geometry / SPEF I/O) +python3 - <<'PY' > "$JOB_DURABLE/inputs/big_bus.lay" +print("NAME big_bus") +for i in range(40): + y0 = i * 0.5 + y1 = y0 + 0.14 + net = f"n{i}" + print(f"METAL M1 {net} w{i}_m1 0.0 {y0:.3f} 80.0 {y1:.3f}") + print(f"VIA VIA1 {net} v{i} 79.8 {y0:.3f} 80.0 {y1:.3f}") + print(f"METAL M2 {net} w{i}_m2 80.0 {y0:.3f} 95.0 {y1:.3f}") + print(f"PIN D{i} {net} M1 O 0.0 {(y0+y1)/2:.3f}") + print(f"PIN L{i} {net} M2 I 95.0 {(y0+y1)/2:.3f}") +PY + +# Job body. Binary is always invoked from durable disk because /dev/shm is +# often mounted noexec (Mode A would otherwise get "Permission denied"). +cat > "$JOB_DURABLE/scripts/run_job.sh" <&1 | tee -a "\$LOG" + cp -f "\$spef_tmp" "\$spef_out" + dd if=/dev/urandom of="\${ROOT}/tmp/\${tag}.scratch.bin" bs=1M count=4 status=none +} + +run_one simple_net.lay simple_net +run_one coupled_nets.lay coupled_nets +run_one via_stack.lay via_stack +run_one big_bus.lay big_bus + +{ + echo "done=\$(date -Iseconds)" + echo "outputs:" + ls -lh outputs/ + echo "tmp (scratch):" + ls -lh tmp/ | head +} | tee -a "\$LOG" + +echo "PASS" > logs/STATUS +EOF +chmod +x "$JOB_DURABLE/scripts/run_job.sh" + +# Do not leak TMPDIR from prior manual tests into Mode A. +unset TMPDIR TMP TEMP || true + +export PD_DURABLE_ROOT="$JOB_DURABLE" +export PD_JOB_NAME="rcx_extract_accel" +export PD_TOOL_CMD='bash scripts/run_job.sh' +export PD_RAM_PATHS="tmp" +export PD_CHECKPOINT_SECS="${PD_CHECKPOINT_SECS:-2}" +export PD_KEEP_LOGS_ON_DISK=1 + +echo "============================================" +echo " RC Extraction × PD Job Acceleration" +echo " mode=${MODE} durable=${JOB_DURABLE}" +echo "============================================" + +if [[ "$MODE" == "b" ]]; then + "${SCRIPTS}/ram_scratch.sh" run +else + "${SCRIPTS}/run_pd_job.sh" +fi + +echo +echo "==== results ====" +echo "STATUS: $(cat "${JOB_DURABLE}/logs/STATUS" 2>/dev/null || echo missing)" +echo "logs on disk:" +ls -la "${JOB_DURABLE}/logs/" +echo "SPEF outputs:" +ls -lh "${JOB_DURABLE}/outputs/" +echo "tmp after job:" +ls -lh "${JOB_DURABLE}/tmp/" 2>/dev/null | head || true +echo "shm leftover:" +ls /dev/shm/pdjobs/"${USER:-user}" 2>/dev/null || echo "(cleaned)" +echo "---- log tail ----" +tail -n 20 "${JOB_DURABLE}/logs/rcx_extract.log" diff --git a/PD Job Acceleration/scripts/bench_io.sh b/PD Job Acceleration/scripts/bench_io.sh new file mode 100755 index 0000000..f5a3271 --- /dev/null +++ b/PD Job Acceleration/scripts/bench_io.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Microbench: sequential write/read on disk vs tmpfs (/dev/shm). +# Useful numbers to quote in a LinkedIn post (same host, same size). +set -euo pipefail + +SIZE_MB="${1:-256}" +DISK_DIR="${BENCH_DISK_DIR:-$(pwd)/.bench_disk}" +RAM_DIR="${BENCH_RAM_DIR:-/dev/shm/pd_bench_${USER:-user}}" + +mkdir -p "$DISK_DIR" "$RAM_DIR" + +bench_one() { + local label="$1" dir="$2" + local f="${dir}/blob_${SIZE_MB}m.bin" + local write_s read_s + + rm -f "$f" + sync + write_s=$( + TIMEFORMAT='%R' + { time dd if=/dev/zero of="$f" bs=1M count="$SIZE_MB" conv=fdatasync status=none; } 2>&1 + ) + read_s=$( + TIMEFORMAT='%R' + { time dd if="$f" of=/dev/null bs=1M status=none; } 2>&1 + ) + local mib_w mib_r + mib_w=$(awk -v s="$write_s" -v n="$SIZE_MB" 'BEGIN{ if (s+0>0) printf "%.1f", n/s; else print "inf" }') + mib_r=$(awk -v s="$read_s" -v n="$SIZE_MB" 'BEGIN{ if (s+0>0) printf "%.1f", n/s; else print "inf" }') + printf '%-8s write %6ss (%7s MiB/s) read %6ss (%7s MiB/s) path=%s\n' \ + "$label" "$write_s" "$mib_w" "$read_s" "$mib_r" "$dir" + rm -f "$f" +} + +echo "PD I/O microbench: ${SIZE_MB} MiB sequential" +echo "host=$(hostname) date=$(date -Iseconds)" +echo + +bench_one "disk" "$DISK_DIR" +bench_one "tmpfs" "$RAM_DIR" + +echo +echo "Note: wall-clock PD wins also come from fewer NFS round-trips on tiny random I/O," +echo "not only from raw sequential bandwidth." + +rmdir "$RAM_DIR" 2>/dev/null || true diff --git a/PD Job Acceleration/scripts/checkpoint_sync.sh b/PD Job Acceleration/scripts/checkpoint_sync.sh new file mode 100755 index 0000000..3e5dd72 --- /dev/null +++ b/PD Job Acceleration/scripts/checkpoint_sync.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Incremental sync: tmpfs workspace → durable storage (checkpoint). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=pd_job_env.sh +source "${SCRIPT_DIR}/pd_job_env.sh" + +usage() { + cat <<'EOF' +Usage: checkpoint_sync.sh [--loop SECONDS] + +Sync hot PD workspace back to durable storage. +With --loop, checkpoint forever every SECONDS (until killed). + +Required env: + PD_DURABLE_ROOT + PD_JOB_NAME +EOF +} + +do_checkpoint() { + local n="${1:-manual}" + [[ -d "$PD_WORK_DIR" ]] || pd_die "workspace missing: $PD_WORK_DIR" + [[ -d "$PD_DURABLE_ROOT" ]] || pd_die "durable root missing: $PD_DURABLE_ROOT" + + mkdir -p "$PD_STATUS_DIR" "$PD_LOG_DIR" + pd_log "Checkpoint #$n : ${PD_WORK_DIR}/ → ${PD_DURABLE_ROOT}/" + + local start end + start=$(date +%s) + # Prefer newer / changed files; keep partials if NFS hiccups mid-transfer. + PD_RSYNC_OPTS="${PD_RSYNC_OPTS} --partial" + pd_rsync "${PD_WORK_DIR}/" "${PD_DURABLE_ROOT}/" + + # Optional: push writeback so a crash cannot silently lose this checkpoint. + # Default off — frequent sync on NFS can dominate; enable for long ECO jobs. + if [[ "${PD_SYNC_AFTER_CHECKPOINT:-0}" == "1" ]]; then + pd_durable_sync "checkpoint_${n}" + fi + end=$(date +%s) + + echo "checkpoint n=${n} $(date -Iseconds) duration_s=$((end - start)) sync_after=${PD_SYNC_AFTER_CHECKPOINT:-0}" \ + | tee -a "${PD_STATUS_DIR}/checkpoints.log" + pd_log "Checkpoint #$n done in $((end - start))s" +} + +main() { + pd_require_cmd rsync + pd_job_defaults + + [[ -n "$PD_DURABLE_ROOT" ]] || { usage; pd_die "PD_DURABLE_ROOT is required"; } + + if [[ "${1:-}" == "--loop" ]]; then + local secs="${2:-}" + [[ -n "$secs" && "$secs" =~ ^[0-9]+$ && "$secs" -gt 0 ]] \ + || pd_die "--loop requires positive SECONDS" + local i=1 + while true; do + do_checkpoint "$i" || pd_log "WARN: checkpoint $i failed (will retry)" + i=$((i + 1)) + sleep "$secs" + done + else + do_checkpoint "manual" + fi +} + +main "$@" diff --git a/PD Job Acceleration/scripts/demo_pd_workload.sh b/PD Job Acceleration/scripts/demo_pd_workload.sh new file mode 100755 index 0000000..abeccda --- /dev/null +++ b/PD Job Acceleration/scripts/demo_pd_workload.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Synthetic Physical Design I/O workload for Mode A demos (no EDA license). +# Writes relative logs/ — with PD_KEEP_LOGS_ON_DISK=1 those hit durable disk +# via symlink, not tmpfs. +set -euo pipefail + +WORK_DIR="${1:-.}" +cd "$WORK_DIR" + +mkdir -p inputs outputs/reports outputs/dbs logs + +echo "[demo] PD workload starting in $(pwd)" + +# --- ingest phase (random reads over "libs") --- +echo "[demo] reading design inputs..." +find inputs -type f -print0 2>/dev/null | while IFS= read -r -d '' f; do + # force page cache / tmpfs read traffic + dd if="$f" of=/dev/null bs=1M status=none 2>/dev/null || cat "$f" >/dev/null +done + +# --- "place" iteration --- +for iter in 1 2 3; do + echo "[demo] place/opt iteration ${iter}" | tee -a logs/place.log + # growing "database" + dd if=/dev/urandom of="outputs/dbs/design_iter${iter}.db" bs=1M count=8 status=none + # report spam + { + echo "ITER ${iter}" + echo "wns=-0.$((RANDOM % 90))" + echo "tns=-$((RANDOM % 400)).$((RANDOM % 99))" + echo "density=0.$((70 + RANDOM % 20))" + } > "outputs/reports/place_iter${iter}.rpt" + # tool log chatter + for i in $(seq 1 50); do + echo "$(date -Iseconds) OPT step=$i cost=$((RANDOM % 10000))" >> logs/place.log + done + sleep 0.3 +done + +# --- "route" phase --- +echo "[demo] global/detail route..." | tee -a logs/route.log +dd if=/dev/urandom of="outputs/dbs/design_routed.db" bs=1M count=16 status=none +{ + echo "drc_violations=$((RANDOM % 20))" + echo "wirelength_um=$((500000 + RANDOM % 200000))" +} > outputs/reports/route.rpt +for i in $(seq 1 80); do + echo "$(date -Iseconds) ROUTE layer=M$((1 + i % 8)) nets=$i" >> logs/route.log +done + +# leave a stamp the orchestrator / LinkedIn demo can show +cat > outputs/JOB_SUMMARY.txt <> logs/run.log + dd if=/dev/urandom of="tmp/scratch_${i}.bin" bs=64K count=1 status=none + sleep 0.02 +done + +{ + echo "TMPDIR=${TMPDIR:-unset}" + echo "logs_resolved=$(readlink -f logs)" + echo "tmp_resolved=$(readlink -f tmp)" + echo "done=$(date -Iseconds)" +} >> logs/run.log diff --git a/PD Job Acceleration/scripts/finalize_job.sh b/PD Job Acceleration/scripts/finalize_job.sh new file mode 100755 index 0000000..362be99 --- /dev/null +++ b/PD Job Acceleration/scripts/finalize_job.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Final rsync from tmpfs → durable, write status, optional cleanup. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=pd_job_env.sh +source "${SCRIPT_DIR}/pd_job_env.sh" + +usage() { + cat <<'EOF' +Usage: finalize_job.sh [exit_code] + +Required env: + PD_DURABLE_ROOT + PD_JOB_NAME + +Optional: + PD_KEEP_TMPFS=1 keep RAM workspace after finalize +EOF +} + +main() { + pd_require_cmd rsync + pd_job_defaults + + local tool_rc="${1:-0}" + [[ -n "$PD_DURABLE_ROOT" ]] || { usage; pd_die "PD_DURABLE_ROOT is required"; } + + mkdir -p "$PD_STATUS_DIR" "$PD_LOG_DIR" + + if [[ -d "$PD_WORK_DIR" ]]; then + pd_log "Final sync: ${PD_WORK_DIR}/ → ${PD_DURABLE_ROOT}/" + local start end + start=$(date +%s) + PD_RSYNC_OPTS="${PD_RSYNC_OPTS} --partial --delete-delay" + # --delete-delay: durable mirrors workspace; regenerable junk already excluded. + pd_rsync "${PD_WORK_DIR}/" "${PD_DURABLE_ROOT}/" + # Harden durability: rsync ≠ durable on media until writeback is flushed. + if [[ "${PD_SYNC_AFTER_FINALIZE:-1}" == "1" ]]; then + pd_durable_sync "finalize" + fi + end=$(date +%s) + echo "finalized $(date -Iseconds) duration_s=$((end - start)) tool_rc=${tool_rc} sync_mode=${PD_SYNC_MODE:-fs}" \ + > "${PD_STATUS_DIR}/last_finalize.txt" + pd_log "Final sync done in $((end - start))s" + else + pd_log "WARN: workspace missing at finalize: $PD_WORK_DIR" + fi + + if [[ "$tool_rc" -eq 0 ]]; then + echo "PASS $(date -Iseconds)" > "${PD_STATUS_DIR}/STATUS" + else + echo "FAIL rc=${tool_rc} $(date -Iseconds)" > "${PD_STATUS_DIR}/STATUS" + fi + + if [[ "${PD_KEEP_TMPFS}" != "1" && -d "$PD_WORK_DIR" ]]; then + if mountpoint -q "$PD_WORK_DIR" 2>/dev/null; then + pd_log "Unmounting $PD_WORK_DIR" + umount "$PD_WORK_DIR" || pd_log "WARN: umount failed" + rmdir "$PD_WORK_DIR" 2>/dev/null || true + else + pd_log "Removing workspace $PD_WORK_DIR" + rm -rf "$PD_WORK_DIR" + fi + else + pd_log "Keeping workspace (PD_KEEP_TMPFS=${PD_KEEP_TMPFS}): $PD_WORK_DIR" + fi + + exit "$tool_rc" +} + +main "$@" diff --git a/PD Job Acceleration/scripts/pd_job_env.sh b/PD Job Acceleration/scripts/pd_job_env.sh new file mode 100755 index 0000000..569797c --- /dev/null +++ b/PD Job Acceleration/scripts/pd_job_env.sh @@ -0,0 +1,243 @@ +#!/usr/bin/env bash +# Shared environment helpers for PD tmpfs + rsync job management. +# shellcheck disable=SC2034 + +set -euo pipefail + +pd_job_defaults() { + : "${PD_JOB_NAME:=pd_job_$(date +%Y%m%d_%H%M%S)}" + : "${PD_DURABLE_ROOT:=}" + : "${PD_TMPFS_ROOT:=/dev/shm/pdjobs/${USER:-user}}" + : "${PD_TMPFS_SIZE:=8G}" + : "${PD_MOUNT_TMPFS:=auto}" # auto | yes | no + : "${PD_CHECKPOINT_SECS:=0}" # 0 = disabled + : "${PD_KEEP_TMPFS:=0}" # 1 = leave workspace after finalize + # Fat PD logs often exceed 20GB — keep them on disk even in Mode A. + : "${PD_KEEP_LOGS_ON_DISK:=1}" + : "${PD_RSYNC_OPTS:=-aH --human-readable --info=stats2}" + : "${PD_EXCLUDE_FILE:=}" + : "${PD_TOOL_CMD:=}" + : "${PD_DEMO:=0}" + # Durability flush after rsync (the shell `sync` command — not rsync). + # off — no fsync/sync (fastest; weaker crash durability) + # file — sync marker file under durable root (GNU sync FILE) + # fs — sync filesystem(s) containing durable root (GNU sync -f) + # global — sync entire machine (heavy; avoid on busy farm nodes) + : "${PD_SYNC_MODE:=fs}" + : "${PD_SYNC_AFTER_CHECKPOINT:=0}" # 1 = also sync after each checkpoint + : "${PD_SYNC_AFTER_FINALIZE:=1}" # 1 = sync after final rsync (recommended) + # Optional tool wrapping: PD_PERF=1 runs the tool under pd_perf_profile.sh + : "${PD_PERF:=0}" + : "${PD_PERF_RECORD:=0}" + + PD_WORK_DIR="${PD_TMPFS_ROOT}/${PD_JOB_NAME}" + PD_STATUS_DIR="${PD_DURABLE_ROOT}/.pd_job_status" + PD_LOG_DIR="${PD_DURABLE_ROOT}/logs" +} + +pd_require_cmd() { + local c + for c in "$@"; do + command -v "$c" >/dev/null 2>&1 || { + echo "ERROR: required command not found: $c" >&2 + exit 127 + } + done +} + +pd_log() { + local ts + ts="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$ts] $*" +} + +pd_die() { + echo "ERROR: $*" >&2 + exit 1 +} + +# Decide whether we can mount a dedicated tmpfs, or just use /dev/shm. +pd_prepare_workspace() { + mkdir -p "$PD_TMPFS_ROOT" + + local do_mount=0 + case "$PD_MOUNT_TMPFS" in + yes) do_mount=1 ;; + no) do_mount=0 ;; + auto) + if [[ -d /dev/shm && -w /dev/shm ]]; then + # /dev/shm is already tmpfs on most Linux hosts — no mount needed. + do_mount=0 + elif [[ "$(id -u)" -eq 0 ]]; then + do_mount=1 + else + do_mount=0 + fi + ;; + *) pd_die "PD_MOUNT_TMPFS must be auto|yes|no" ;; + esac + + mkdir -p "$PD_WORK_DIR" + + if [[ "$do_mount" -eq 1 ]]; then + if ! mountpoint -q "$PD_WORK_DIR" 2>/dev/null; then + pd_log "Mounting tmpfs at $PD_WORK_DIR (size=$PD_TMPFS_SIZE)" + mount -t tmpfs -o "size=${PD_TMPFS_SIZE},mode=0755" tmpfs "$PD_WORK_DIR" \ + || pd_die "tmpfs mount failed (need root/CAP_SYS_ADMIN, or set PD_MOUNT_TMPFS=no)" + fi + else + pd_log "Using existing RAM-backed path: $PD_WORK_DIR (under ${PD_TMPFS_ROOT})" + fi +} + +pd_rsync_excludes() { + local args=() + # Regenerable / noisy PD artifacts — tune for your flow. + args+=( + --exclude='.pd_job_status/' + --exclude='.pd_ram_scratch/' + --exclude='*.tmp' + --exclude='*.swp' + --exclude='core' + --exclude='core.*' + --exclude='.nfs*' + ) + # When logs live on durable disk (symlink from workspace), do not rsync them + # through tmpfs — avoids copying 20GB+ logs into / out of RAM. + # Use 'logs' not 'logs/': a workspace symlink named logs does not match 'logs/'. + if [[ "${PD_KEEP_LOGS_ON_DISK:-1}" == "1" ]]; then + args+=(--exclude='logs' --exclude='logs/***') + fi + if [[ -n "${PD_EXCLUDE_FILE}" && -f "${PD_EXCLUDE_FILE}" ]]; then + args+=(--exclude-from="$PD_EXCLUDE_FILE") + fi + printf '%s\0' "${args[@]}" +} + +# After staging Mode A workspace: point logs/ at durable disk, not tmpfs. +pd_rewire_logs_to_disk() { + if [[ "${PD_KEEP_LOGS_ON_DISK:-1}" != "1" ]]; then + pd_log "PD_KEEP_LOGS_ON_DISK=0 — logs may consume tmpfs (dangerous if huge)" + return 0 + fi + + # Durable logs must be a real directory — never a symlink (avoids cycles if a + # prior rsync wrongly copied workspace logs → durable). + if [[ -L "${PD_DURABLE_ROOT}/logs" ]]; then + pd_log "WARN: durable logs/ was a symlink; replacing with a real directory" + rm -f "${PD_DURABLE_ROOT}/logs" + fi + mkdir -p "${PD_DURABLE_ROOT}/logs" + + rm -rf "${PD_WORK_DIR}/logs" + ln -s "${PD_DURABLE_ROOT}/logs" "${PD_WORK_DIR}/logs" + pd_log "logs/ → durable disk (${PD_DURABLE_ROOT}/logs); excluded from tmpfs rsync" +} + +pd_rsync() { + # Usage: pd_rsync SRC/ DEST/ + local src="$1" dest="$2" + mkdir -p "$dest" + # shellcheck disable=SC2086 + local -a excl=() + local item + while IFS= read -r -d '' item; do + excl+=("$item") + done < <(pd_rsync_excludes) + + # shellcheck disable=SC2086 + rsync ${PD_RSYNC_OPTS} "${excl[@]}" "$src" "$dest" +} + +# Push kernel writeback to durable media after rsync. +# rsync only copies into the page cache unless the destination already fsynced; +# without this, a node crash can lose a "successful" checkpoint. +# +# Usage: pd_durable_sync [reason] +pd_durable_sync() { + local reason="${1:-manual}" + local mode="${PD_SYNC_MODE:-fs}" + local root="${PD_DURABLE_ROOT:-}" + local start end marker + + case "$mode" in + off|0|no|none) + pd_log "durable sync skipped (PD_SYNC_MODE=${mode}) reason=${reason}" + return 0 + ;; + esac + + command -v sync >/dev/null 2>&1 || { + pd_log "WARN: sync(1) not found; cannot harden durability after ${reason}" + return 0 + } + + start=$(date +%s) + case "$mode" in + file) + [[ -n "$root" && -d "$root" ]] || { + pd_log "WARN: PD_DURABLE_ROOT missing for file sync; falling back to global sync" + sync + return 0 + } + mkdir -p "${root}/.pd_job_status" + marker="${root}/.pd_job_status/last_sync_marker" + date -Iseconds >"$marker" 2>/dev/null || echo "synced" >"$marker" + # GNU sync FILE → fsync that file (and often enough for NFS client writeback). + if sync "$marker" 2>/dev/null; then + : + else + sync + fi + ;; + fs|filesystem) + if [[ -n "$root" && -e "$root" ]] && sync -f "$root" 2>/dev/null; then + : + elif [[ -n "$root" && -e "$root" ]] && sync "$root" 2>/dev/null; then + : + else + sync + fi + ;; + global|all) + sync + ;; + *) + pd_log "WARN: unknown PD_SYNC_MODE=${mode}; using fs" + if [[ -n "$root" && -e "$root" ]] && sync -f "$root" 2>/dev/null; then + : + else + sync + fi + ;; + esac + end=$(date +%s) + pd_log "durable sync (${mode}) after ${reason} in $((end - start))s" + + if [[ -n "$root" ]]; then + mkdir -p "${root}/.pd_job_status" + echo "sync mode=${mode} reason=${reason} $(date -Iseconds)" \ + >> "${root}/.pd_job_status/sync.log" 2>/dev/null || true + fi +} + +# Resolve a usable perf binary (cloud images often ship a mismatched wrapper). +pd_find_perf() { + local p cand + if [[ -n "${PD_PERF_BIN:-}" && -x "${PD_PERF_BIN}" ]]; then + printf '%s\n' "${PD_PERF_BIN}" + return 0 + fi + for cand in \ + "$(command -v perf 2>/dev/null || true)" \ + /usr/lib/linux-tools-*/perf + do + [[ -n "$cand" && -x "$cand" ]] || continue + # Reject the stub that only prints "perf not found for kernel ..." + if "$cand" --version >/dev/null 2>&1; then + printf '%s\n' "$cand" + return 0 + fi + done + return 1 +} diff --git a/PD Job Acceleration/scripts/pd_perf_profile.sh b/PD Job Acceleration/scripts/pd_perf_profile.sh new file mode 100755 index 0000000..c48c26b --- /dev/null +++ b/PD Job Acceleration/scripts/pd_perf_profile.sh @@ -0,0 +1,357 @@ +#!/usr/bin/env bash +# Profile a PD tool command with perf (+ GNU time fallback) to decide whether +# tmpfs/rsync acceleration is worth applying, and where the bottleneck is. +# +# Usage: +# pd_perf_profile.sh [--out DIR] -- +# PD_TOOL_CMD='...' pd_perf_profile.sh [--out DIR] +# +# Env: +# PD_PERF_RECORD=1 also run `perf record -g` (needs writable out dir) +# PD_PERF_BIN=path force a specific perf binary +# PD_PERF_EVENTS=... comma-separated perf events (optional override) +# +# Exit code: the wrapped command's exit code. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=pd_job_env.sh +source "${SCRIPT_DIR}/pd_job_env.sh" + +usage() { + cat <<'EOF' +Usage: pd_perf_profile.sh [--out DIR] [--record] -- + PD_TOOL_CMD='innovus ...' pd_perf_profile.sh [--out DIR] + +Collects: + - wall / user / sys time (GNU time or bash SECONDS) + - perf stat counters when a working perf is available + - a short I/O-vs-CPU classification + Mode A/B recommendation + +This does not accelerate the job by itself — it tells you whether tmpfs+rsync +will help, and what to measure next. +EOF +} + +OUT_DIR="" +DO_RECORD=0 +CMD=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --out) OUT_DIR="${2:-}"; shift 2 ;; + --record) DO_RECORD=1; shift ;; + -h|--help) usage; exit 0 ;; + --) shift; CMD=("$@"); break ;; + *) + # Allow: pd_perf_profile.sh cmd args... + CMD=("$@") + break + ;; + esac +done + +if [[ ${#CMD[@]} -eq 0 ]]; then + if [[ -n "${PD_TOOL_CMD:-}" ]]; then + CMD=(bash -c "$PD_TOOL_CMD") + else + usage + pd_die "no command provided (use -- cmd, or set PD_TOOL_CMD)" + fi +fi + +[[ "${PD_PERF_RECORD:-0}" == "1" ]] && DO_RECORD=1 + +if [[ -z "$OUT_DIR" ]]; then + if [[ -n "${PD_DURABLE_ROOT:-}" ]]; then + OUT_DIR="${PD_DURABLE_ROOT}/logs/perf_${PD_JOB_NAME:-profile}" + else + OUT_DIR="${TMPDIR:-/tmp}/pd_perf_$$" + fi +fi +mkdir -p "$OUT_DIR" + +STAT_TXT="${OUT_DIR}/perf_stat.txt" +TIME_TXT="${OUT_DIR}/gnu_time.txt" +SUMMARY="${OUT_DIR}/SUMMARY.txt" +RECORD_DATA="${OUT_DIR}/perf.data" +IO_BEFORE="${OUT_DIR}/proc_io_before.txt" +IO_AFTER="${OUT_DIR}/proc_io_after.txt" +META="${OUT_DIR}/meta.env" + +PERF_BIN="" +if PERF_BIN="$(pd_find_perf)"; then + pd_log "Using perf: $PERF_BIN ($("$PERF_BIN" --version 2>/dev/null | head -1))" +else + PERF_BIN="" + pd_log "WARN: no working perf binary — falling back to GNU time / wall clock" +fi + +# Default event set: soft events work in VMs; HW events when PMU exists. +DEFAULT_EVENTS="task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,cache-references,cache-misses" +EVENTS="${PD_PERF_EVENTS:-$DEFAULT_EVENTS}" + +HAVE_GNU_TIME=0 +if command -v /usr/bin/time >/dev/null 2>&1 && /usr/bin/time --version >/dev/null 2>&1; then + HAVE_GNU_TIME=1 +fi + +# Snapshot host diskstats (coarse) around the run. +snap_disk() { + local f="$1" + { + echo "timestamp=$(date -Iseconds)" + echo "--- /proc/diskstats ---" + cat /proc/diskstats 2>/dev/null || true + echo "--- /proc/meminfo (selected) ---" + awk '/MemTotal|MemAvailable|Dirty|Writeback|Cached|SwapTotal|SwapFree/ {print}' \ + /proc/meminfo 2>/dev/null || true + } >"$f" +} + +snap_disk "${OUT_DIR}/host_before.txt" + +{ + echo "PD_PERF_OUT=$OUT_DIR" + echo "PD_PERF_BIN=${PERF_BIN:-none}" + echo "PD_PERF_EVENTS=$EVENTS" + echo "PD_PERF_RECORD=$DO_RECORD" + echo "CMD=${CMD[*]}" + echo "START=$(date -Iseconds)" +} >"$META" + +pd_log "Profiling → $OUT_DIR" +pd_log "Command: ${CMD[*]}" + +TOOL_RC=0 +WALL_START=$(date +%s) + +run_cmd() { + "${CMD[@]}" +} + +set +e +if [[ -n "$PERF_BIN" ]]; then + # shellcheck disable=SC2086 + if [[ "$HAVE_GNU_TIME" -eq 1 ]]; then + /usr/bin/time -v -o "$TIME_TXT" -- \ + "$PERF_BIN" stat -e "$EVENTS" -o "$STAT_TXT" -- \ + "${CMD[@]}" + else + "$PERF_BIN" stat -e "$EVENTS" -o "$STAT_TXT" -- \ + "${CMD[@]}" + fi + TOOL_RC=$? +else + if [[ "$HAVE_GNU_TIME" -eq 1 ]]; then + /usr/bin/time -v -o "$TIME_TXT" -- "${CMD[@]}" + TOOL_RC=$? + else + run_cmd + TOOL_RC=$? + fi +fi +set -e + +WALL_END=$(date +%s) +WALL_S=$((WALL_END - WALL_START)) +echo "END=$(date -Iseconds)" >>"$META" +echo "WALL_S=$WALL_S" >>"$META" +echo "TOOL_RC=$TOOL_RC" >>"$META" + +snap_disk "${OUT_DIR}/host_after.txt" + +if [[ "$DO_RECORD" -eq 1 && -n "$PERF_BIN" ]]; then + pd_log "Optional perf record pass (PD_PERF_RECORD) — re-running briefly is NOT done;" + pd_log "run manually: $PERF_BIN record -g -o $RECORD_DATA -- " + echo "perf record skipped automatically (would double runtime). Suggested:" \ + >"${OUT_DIR}/perf_record_HINT.txt" + echo "$PERF_BIN record -g -o $RECORD_DATA -- ${CMD[*]}" \ + >>"${OUT_DIR}/perf_record_HINT.txt" +fi + +# --- Classify --------------------------------------------------------------- +python3 - "$SUMMARY" "$STAT_TXT" "$TIME_TXT" "$WALL_S" "$TOOL_RC" <<'PY' +import re, sys +from pathlib import Path + +summary, stat_p, time_p, wall_s, tool_rc = sys.argv[1:6] +wall_s = int(wall_s) +tool_rc = int(tool_rc) + +stat = Path(stat_p).read_text(errors="replace") if Path(stat_p).exists() else "" +gt = Path(time_p).read_text(errors="replace") if Path(time_p).exists() else "" + +def grab_perf(name): + # lines like: + # " 123,456 page-faults:u" + # " 2,878.00 msec task-clock:u" + pat = ( + rf"^\s*([0-9][0-9,]*(?:\.[0-9]+)?|)" + rf"(?:\s+\w+)?\s+{re.escape(name)}" + ) + m = re.search(pat, stat, re.M) + if not m: + # event may appear with :u / :k suffix + pat2 = ( + rf"^\s*([0-9][0-9,]*(?:\.[0-9]+)?|)" + rf"(?:\s+\w+)?\s+{re.escape(name)}(?::[a-z]+)?" + ) + m = re.search(pat2, stat, re.M) + if not m: + return None + v = m.group(1) + if "not supported" in v: + return None + return float(v.replace(",", "")) + +def grab_time(label): + m = re.search(rf"^\s*{re.escape(label)}:\s*(.+)$", gt, re.M) + return m.group(1).strip() if m else None + +task_clock = grab_perf("task-clock") or grab_perf("task-clock:u") +cs = grab_perf("context-switches") or grab_perf("context-switches:u") +pf = grab_perf("page-faults") or grab_perf("page-faults:u") +cycles = grab_perf("cycles") or grab_perf("cycles:u") +insns = grab_perf("instructions") or grab_perf("instructions:u") + +elapsed = grab_time("Elapsed (wall clock) time (h:mm:ss or m:ss)") +user_t = grab_time("User time (seconds)") +sys_t = grab_time("System time (seconds)") +pct_cpu = grab_time("Percent of CPU this job got") +maj_pf = grab_time("Major (requiring I/O) page faults") +vol_cs = grab_time("Voluntary context switches") +inv_cs = grab_time("Involuntary context switches") +fs_in = grab_time("File system inputs") +fs_out = grab_time("File system outputs") + +ipc = None +if cycles and insns and cycles > 0: + ipc = insns / cycles + +# Heuristic classification +signals = [] +score_io = 0 +score_cpu = 0 + +def pct_from_str(s): + if not s: + return None + m = re.search(r"([0-9]+)", s) + return int(m.group(1)) if m else None + +cpu_pct = pct_from_str(pct_cpu) +if cpu_pct is not None: + if cpu_pct < 55: + score_io += 2 + signals.append(f"low CPU utilization ({cpu_pct}%) → likely waiting on I/O or locks") + elif cpu_pct > 85: + score_cpu += 2 + signals.append(f"high CPU utilization ({cpu_pct}%) → compute-heavy phase") + +try: + u = float(user_t) if user_t else None + s = float(sys_t) if sys_t else None +except ValueError: + u = s = None +if u is not None and s is not None and wall_s > 0: + busy = u + s + if busy < 0.45 * wall_s: + score_io += 2 + signals.append(f"user+sys ({busy:.1f}s) << wall ({wall_s}s) → stalled / I/O wait") + elif busy > 0.85 * wall_s: + score_cpu += 1 + signals.append(f"user+sys fills most of wall time → CPU-bound") + +try: + maj = int(str(maj_pf).replace(",", "")) if maj_pf else 0 +except ValueError: + maj = 0 +if maj > 1000: + score_io += 2 + signals.append(f"many major page faults ({maj}) → cold cache / mmap I/O") + +try: + fsi = int(str(fs_in).replace(",", "")) if fs_in else 0 + fso = int(str(fs_out).replace(",", "")) if fs_out else 0 +except ValueError: + fsi = fso = 0 +if fsi + fso > 100_000: + score_io += 1 + signals.append(f"heavy filesystem ops (in={fsi}, out={fso})") + +if pf and wall_s > 0 and (pf / max(wall_s, 1)) > 50_000: + score_io += 1 + signals.append(f"high page-fault rate ({pf:.0f} / {wall_s}s)") + +if ipc is not None: + if ipc < 0.6: + score_io += 1 + signals.append(f"low IPC ({ipc:.2f}) — stalls (mem/I/O) possible") + elif ipc > 1.5: + score_cpu += 1 + signals.append(f"healthy IPC ({ipc:.2f})") + +if score_io >= score_cpu + 2: + klass = "I/O-bound (or I/O-dominated)" + rec = ( + "tmpfs+rsync is likely to help. Prefer Mode B (RAM scratch / TMPDIR) first; " + "use Mode A only if the working set fits. Keep fat logs on disk. " + "After rsync checkpoints, use PD_SYNC_AFTER_FINALIZE=1 (shell sync) for durability." + ) +elif score_cpu >= score_io + 2: + klass = "CPU-bound" + rec = ( + "Filesystem placement will not move the needle much. Optimize algorithms/threads " + "(taskset/OMP), not tmpfs. Still keep fat logs off NFS if possible." + ) +else: + klass = "Mixed / unclear" + rec = ( + "Re-profile the hottest phase alone (lib load vs route vs extract). " + "Try Mode B on TMPDIR/tmp and compare wall time + this SUMMARY." + ) + +lines = [] +lines.append("PD job perf profile") +lines.append("===================") +lines.append(f"wall_s={wall_s} tool_rc={tool_rc}") +lines.append(f"classification={klass}") +lines.append(f"score_io={score_io} score_cpu={score_cpu}") +lines.append("") +lines.append("Recommendation:") +lines.append(f" {rec}") +lines.append("") +lines.append("Signals:") +if signals: + for s in signals: + lines.append(f" - {s}") +else: + lines.append(" - (few counters available; install matching linux-tools / use GNU time)") +lines.append("") +lines.append("Key metrics:") +lines.append(f" elapsed(time -v)={elapsed}") +lines.append(f" user_s={user_t} sys_s={sys_t} cpu%={pct_cpu}") +lines.append(f" major_page_faults={maj_pf}") +lines.append(f" voluntary_cs={vol_cs} involuntary_cs={inv_cs}") +lines.append(f" fs_inputs={fs_in} fs_outputs={fs_out}") +lines.append(f" perf.task_clock_ms={task_clock}") +lines.append(f" perf.context_switches={cs}") +lines.append(f" perf.page_faults={pf}") +lines.append(f" perf.cycles={cycles} instructions={insns} ipc={ipc}") +lines.append("") +lines.append("Artifacts:") +lines.append(f" {stat_p}") +lines.append(f" {time_p}") +lines.append("") +lines.append("Notes:") +lines.append(" - perf HW counters may be in VMs — soft events still help.") +lines.append(" - Compare the same command on disk vs Mode B/A to quantify I/O win.") +lines.append(" - shell `sync` after finalize ≠ rsync; it flushes writeback for durability.") + +Path(summary).write_text("\n".join(lines) + "\n") +print("\n".join(lines)) +PY + +pd_log "Wrote $SUMMARY" +exit "$TOOL_RC" diff --git a/PD Job Acceleration/scripts/ram_scratch.sh b/PD Job Acceleration/scripts/ram_scratch.sh new file mode 100755 index 0000000..529d2d8 --- /dev/null +++ b/PD Job Acceleration/scripts/ram_scratch.sh @@ -0,0 +1,310 @@ +#!/usr/bin/env bash +# Limited-RAM mode: keep design + (usually) logs on disk; put small scratch in tmpfs. +# +# IMPORTANT: Do NOT put huge PD logs (often 10–50GB+) into RAM by default. +# Prefer local SSD/NVMe for fat logs. RAM is for small, high-IOPS, short-lived +# temp dirs / TMPDIR only — unless you have measured the log and it fits. +# +# Usage: +# ram_scratch.sh setup # create RAM scratch + redirect paths +# ram_scratch.sh flush # rsync RAM scratch worth keeping → durable +# ram_scratch.sh teardown # flush + remove redirects + free RAM +# ram_scratch.sh env # print export lines (TMPDIR, etc.) for the tool +# ram_scratch.sh --demo # self-contained demo +# +# Env: +# PD_DURABLE_ROOT Job directory on disk/NFS (required) +# PD_JOB_NAME Name slice under /dev/shm (default: scratch_) +# PD_SCRATCH_SIZE Soft target for docs only; /dev/shm is shared RAM +# PD_RAM_PATHS Space-separated relative dirs to put in RAM +# default: "tmp" (NOT logs — logs are often huge) +# PD_FLUSH_ON_TEARDOWN 1 (default) flush before removing RAM copies +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=pd_job_env.sh +source "${SCRIPT_DIR}/pd_job_env.sh" + +: "${PD_TMPFS_ROOT:=/dev/shm/pdjobs/${USER:-user}}" +: "${PD_RAM_PATHS:=tmp}" +: "${PD_FLUSH_ON_TEARDOWN:=1}" +: "${PD_SCRATCH_SIZE:=2G}" + +PD_SCRATCH_DIR="" +PD_META_DIR="" + +usage() { + cat <<'EOF' +Usage: ram_scratch.sh {setup|flush|teardown|env|run|--demo} + +Keep big design DBs and fat logs on disk. Redirect only small scratch +paths into /dev/shm, and set TMPDIR there. + + setup Create scratch + symlink PD_RAM_PATHS from durable → RAM + flush rsync selected RAM dirs back into durable (replace symlinks' targets) + teardown flush (default) + unlink redirects + rm scratch + env Print shell exports for the tool process + run setup, run PD_TOOL_CMD with TMPDIR in RAM, teardown + --demo DB+logs on disk; only tmp/TMPDIR in RAM + +Example (default: tmp only — not logs): + export PD_DURABLE_ROOT=/proj/blockA/run1 + export PD_RAM_PATHS="tmp" # add more only if sized + measured + export PD_TOOL_CMD='innovus -files route.tcl -log logs/route.log' + ./scripts/ram_scratch.sh run + +Fat logs (20GB+): keep on local SSD/NVMe, not tmpfs. NFS if you must. +EOF +} + +require_durable() { + [[ -n "${PD_DURABLE_ROOT:-}" ]] || pd_die "PD_DURABLE_ROOT is required" + [[ -d "$PD_DURABLE_ROOT" ]] || pd_die "PD_DURABLE_ROOT missing: $PD_DURABLE_ROOT" + : "${PD_JOB_NAME:=scratch_$$}" + PD_SCRATCH_DIR="${PD_TMPFS_ROOT}/${PD_JOB_NAME}.scratch" + PD_META_DIR="${PD_DURABLE_ROOT}/.pd_ram_scratch" +} + +rel_paths() { + # shellcheck disable=SC2206 + local -a paths=( $PD_RAM_PATHS ) + local p + for p in "${paths[@]}"; do + [[ -n "$p" ]] || continue + [[ "$p" != /* ]] || pd_die "PD_RAM_PATHS must be relative (got: $p)" + [[ "$p" != *..* ]] || pd_die "PD_RAM_PATHS cannot contain .. (got: $p)" + printf '%s\n' "$p" + done +} + +setup_scratch() { + require_durable + pd_require_cmd rsync mkdir ln rm + + mkdir -p "$PD_SCRATCH_DIR" "$PD_META_DIR" + # Always provide a generic temp root tools honor via TMPDIR. + mkdir -p "${PD_SCRATCH_DIR}/_tmpdir" + + local rel ram_target durable_path backup + while IFS= read -r rel; do + ram_target="${PD_SCRATCH_DIR}/${rel}" + durable_path="${PD_DURABLE_ROOT}/${rel}" + mkdir -p "$ram_target" + + if [[ -L "$durable_path" ]]; then + # Already redirected; ensure it points at our scratch. + ln -sfn "$ram_target" "$durable_path" + elif [[ -d "$durable_path" ]]; then + # Preserve any pre-existing durable contents, then replace dir with symlink. + backup="${PD_META_DIR}/backup_$(echo "$rel" | tr '/' '_')" + mkdir -p "$backup" + # Copy existing content into RAM so the tool still sees prior files. + rsync -a "${durable_path}/" "${ram_target}/" + # Keep a durable backup copy, then swap in the symlink. + rsync -a "${durable_path}/" "${backup}/" + rm -rf "$durable_path" + ln -s "$ram_target" "$durable_path" + pd_log "Redirected ${rel}/ → RAM (prior files kept in RAM + backup ${backup})" + elif [[ -e "$durable_path" ]]; then + pd_die "refusing to redirect non-directory path: $durable_path" + else + ln -s "$ram_target" "$durable_path" + pd_log "Redirected ${rel}/ → RAM (new)" + fi + done < <(rel_paths) + + # Record meta for flush/teardown. + { + echo "scratch_dir=$PD_SCRATCH_DIR" + echo "job_name=$PD_JOB_NAME" + echo "ram_paths=$PD_RAM_PATHS" + echo "created=$(date -Iseconds)" + } > "${PD_META_DIR}/active.env" + + pd_log "RAM scratch ready: $PD_SCRATCH_DIR (target size hint: $PD_SCRATCH_SIZE)" + pd_log "Export TMPDIR before launching the tool (or use: ram_scratch.sh run)" + df -h "$PD_SCRATCH_DIR" | tail -n 1 || true +} + +print_env() { + require_durable + local tmp="${PD_SCRATCH_DIR}/_tmpdir" + mkdir -p "$tmp" + cat <> "${PD_META_DIR}/flush.log" +} + +teardown_scratch() { + require_durable + + if [[ "${PD_FLUSH_ON_TEARDOWN}" == "1" ]]; then + # Flush while symlinks still point at RAM. + if [[ -d "$PD_SCRATCH_DIR" ]]; then + # Materialize without going through flush's symlink removal for paths + # that are still linked — use a dedicated path. + local rel ram_target durable_path material + while IFS= read -r rel; do + ram_target="${PD_SCRATCH_DIR}/${rel}" + durable_path="${PD_DURABLE_ROOT}/${rel}" + [[ -d "$ram_target" ]] || continue + material="${PD_META_DIR}/material_$(echo "$rel" | tr '/' '_')" + rm -rf "$material" + mkdir -p "$material" + rsync -a "${ram_target}/" "${material}/" + if [[ -L "$durable_path" || -e "$durable_path" ]]; then + rm -rf "$durable_path" + fi + mv "$material" "$durable_path" + pd_log "Materialized ${rel}/ onto durable storage" + done < <(rel_paths) + if [[ "${PD_SYNC_AFTER_FINALIZE:-1}" == "1" ]]; then + pd_durable_sync "ram_scratch_teardown" + fi + fi + else + # Drop redirects; discard RAM contents. + local rel durable_path + while IFS= read -r rel; do + durable_path="${PD_DURABLE_ROOT}/${rel}" + if [[ -L "$durable_path" ]]; then + rm -f "$durable_path" + mkdir -p "$durable_path" + pd_log "Removed RAM redirect for ${rel}/ (not flushed)" + fi + done < <(rel_paths) + fi + + if [[ -d "$PD_SCRATCH_DIR" ]]; then + rm -rf "$PD_SCRATCH_DIR" + pd_log "Freed RAM scratch: $PD_SCRATCH_DIR" + fi + rm -f "${PD_META_DIR}/active.env" 2>/dev/null || true +} + +run_with_scratch() { + require_durable + [[ -n "${PD_TOOL_CMD:-}" ]] || pd_die "PD_TOOL_CMD is required for run" + + setup_scratch + # shellcheck disable=SC1090 + eval "$(print_env)" + + local rc=0 + pd_log "Running tool with TMPDIR=$TMPDIR (PD_PERF=${PD_PERF:-0})" + ( + cd "$PD_DURABLE_ROOT" + if [[ "${PD_PERF:-0}" == "1" ]]; then + # Pass the tool string once — pd_perf_profile runs it via bash -c. + PD_DURABLE_ROOT="$PD_DURABLE_ROOT" PD_JOB_NAME="${PD_JOB_NAME:-scratch}" \ + PD_TOOL_CMD="$PD_TOOL_CMD" \ + "${SCRIPT_DIR}/pd_perf_profile.sh" --out "${PD_DURABLE_ROOT}/logs/perf_${PD_JOB_NAME:-scratch}" + else + bash -c "$PD_TOOL_CMD" + fi + ) || rc=$? + + teardown_scratch + return "$rc" +} + +demo() { + local durable="${ROOT_DIR}/examples/demo_ram_scratch" + rm -rf "$durable" + mkdir -p "$durable/outputs" "$durable/scripts" + # "Big" design stays on disk — do NOT put this in PD_RAM_PATHS. + dd if=/dev/urandom of="$durable/outputs/design.db" bs=1M count=32 status=none + echo "fake db on disk" > "$durable/outputs/design.db.readme" + + export PD_DURABLE_ROOT="$durable" + export PD_JOB_NAME="demo_ram_scratch" + export PD_RAM_PATHS="tmp" # deliberately NOT logs + export PD_TOOL_CMD="bash \"${SCRIPT_DIR}/demo_ram_scratch_workload.sh\"" + + pd_log "Demo durable tree: $durable (design.db + logs on disk; tmp in RAM)" + run_with_scratch + + echo + pd_log "After teardown:" + echo " design.db size: $(du -h "$durable/outputs/design.db" | awk '{print $1}') (disk)" + echo " logs/run.log size: $(du -h "$durable/logs/run.log" | awk '{print $1}') (disk, never RAM)" + if [[ -L "$durable/logs" ]]; then + echo " logs is symlink? yes (WRONG — logs should stay on disk)" + else + echo " logs is symlink? no (good — logs stayed on disk)" + fi + if [[ -L "$durable/tmp" ]]; then + echo " tmp is symlink? yes (unexpected after teardown)" + else + echo " tmp is symlink? no (materialized after flush)" + fi + if [[ -d "${PD_TMPFS_ROOT}/demo_ram_scratch.scratch" ]]; then + echo " RAM scratch remains? yes" + else + echo " RAM scratch remains? no" + fi + echo + echo "---- logs/run.log (tail) ----" + tail -n 8 "$durable/logs/run.log" +} + +main() { + local cmd="${1:-}" + case "$cmd" in + setup) setup_scratch ;; + flush) flush_scratch ;; + teardown) teardown_scratch ;; + env) print_env ;; + run) run_with_scratch ;; + --demo) demo ;; + -h|--help|"") usage; [[ -n "$cmd" ]] || exit 1 ;; + *) usage; pd_die "unknown command: $cmd" ;; + esac +} + +main "$@" diff --git a/PD Job Acceleration/scripts/run_pd_job.sh b/PD Job Acceleration/scripts/run_pd_job.sh new file mode 100755 index 0000000..694715b --- /dev/null +++ b/PD Job Acceleration/scripts/run_pd_job.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# Mode A orchestrator: stage job tree into tmpfs → run → checkpoint → finalize. +# +# By default PD_KEEP_LOGS_ON_DISK=1: after staging, workspace logs/ is a symlink +# to durable disk and logs/ are excluded from rsync. Fat PD logs must not fill RAM. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=pd_job_env.sh +source "${SCRIPT_DIR}/pd_job_env.sh" + +# Globals used by EXIT trap (must not be `local` — trap runs after main returns). +PD_ORCH_TOOL_RC=0 +PD_ORCH_CKPT_PID="" +PD_ORCH_CLEANED=0 + +usage() { + cat <<'EOF' +Usage: run_pd_job.sh [--demo] [--keep] + +Mode A: full workspace in tmpfs — only when the design/scratch fits in RAM. +Prefer ram_scratch.sh (Mode B) when RAM is limited. + +Environment: + PD_DURABLE_ROOT Persistent job dir (required unless --demo) + PD_JOB_NAME Job name + PD_TOOL_CMD Command to run inside workspace (required unless --demo) + PD_CHECKPOINT_SECS Periodic checkpoint interval (0=off) + PD_TMPFS_ROOT Default /dev/shm/pdjobs/$USER + PD_TMPFS_SIZE Default 8G + PD_KEEP_TMPFS=1 Keep RAM workspace after job + PD_KEEP_LOGS_ON_DISK Default 1 — logs/ stays on durable disk (not tmpfs) + PD_SYNC_MODE off|file|fs|global — durability flush after rsync (default fs) + PD_SYNC_AFTER_FINALIZE Default 1 — run shell sync after final rsync + PD_SYNC_AFTER_CHECKPOINT Default 0 — sync after each checkpoint (costly on NFS) + PD_PERF=1 Wrap tool with pd_perf_profile.sh (I/O vs CPU report) + +Examples: + ./scripts/run_pd_job.sh --demo + PD_DURABLE_ROOT=/proj/b/run1 PD_TOOL_CMD='innovus -files route.tcl -log logs/route.log' \ + ./scripts/run_pd_job.sh +EOF +} + +setup_demo_tree() { + local durable="${ROOT_DIR}/examples/demo_durable" + rm -rf "$durable" + mkdir -p "$durable/inputs/lef" "$durable/inputs/libs" "$durable/scripts" + + # Fake LEF / liberty / DEF-ish inputs (a few MB of compressible + random data) + dd if=/dev/urandom of="$durable/inputs/lef/tech.lef.bin" bs=1M count=4 status=none + dd if=/dev/urandom of="$durable/inputs/libs/slow.lib.bin" bs=1M count=6 status=none + dd if=/dev/urandom of="$durable/inputs/libs/fast.lib.bin" bs=1M count=6 status=none + dd if=/dev/urandom of="$durable/inputs/design.def.bin" bs=1M count=8 status=none + printf 'VERSION 5.8 ;\nDESIGN demo ;\nEND DESIGN\n' > "$durable/inputs/design.def.header" + cat > "$durable/scripts/run_route.tcl" <<'TCL' +# Placeholder — real flows would call Innovus/ICC2/FC here. +puts "demo tcl — actual tool command is provided by PD_TOOL_CMD" +TCL + + export PD_DURABLE_ROOT="$durable" + export PD_JOB_NAME="${PD_JOB_NAME:-demo_block_route}" + # Quote-safe command string for paths with spaces. + export PD_TOOL_CMD="${PD_TOOL_CMD:-bash \"${SCRIPT_DIR}/demo_pd_workload.sh\" .}" + export PD_CHECKPOINT_SECS="${PD_CHECKPOINT_SECS:-2}" + export PD_DEMO=1 + pd_log "Demo durable tree ready at $PD_DURABLE_ROOT" +} + +pd_orch_cleanup() { + # Idempotent: EXIT + INT/TERM may both fire. + [[ "$PD_ORCH_CLEANED" -eq 1 ]] && return 0 + PD_ORCH_CLEANED=1 + + if [[ -n "${PD_ORCH_CKPT_PID}" ]] && kill -0 "$PD_ORCH_CKPT_PID" 2>/dev/null; then + pd_log "Stopping checkpoint loop (pid=$PD_ORCH_CKPT_PID)" + kill "$PD_ORCH_CKPT_PID" 2>/dev/null || true + wait "$PD_ORCH_CKPT_PID" 2>/dev/null || true + fi + + PD_KEEP_TMPFS="${PD_KEEP_TMPFS:-0}" \ + PD_DURABLE_ROOT="${PD_DURABLE_ROOT}" \ + PD_JOB_NAME="${PD_JOB_NAME}" \ + PD_TMPFS_ROOT="${PD_TMPFS_ROOT}" \ + "${SCRIPT_DIR}/finalize_job.sh" "$PD_ORCH_TOOL_RC" || true + + pd_log "=== PD job end: ${PD_JOB_NAME} (tool_rc=${PD_ORCH_TOOL_RC}) ===" +} + +main() { + local keep_flag=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --demo) export PD_DEMO=1; shift ;; + --keep) keep_flag=1; shift ;; + -h|--help) usage; exit 0 ;; + *) pd_die "unknown arg: $1" ;; + esac + done + + pd_require_cmd rsync date tee bash + + if [[ "${PD_DEMO:-0}" == "1" ]]; then + setup_demo_tree + fi + + pd_job_defaults + [[ "$keep_flag" -eq 1 ]] && PD_KEEP_TMPFS=1 + export PD_KEEP_TMPFS PD_DURABLE_ROOT PD_JOB_NAME PD_TMPFS_ROOT PD_TMPFS_SIZE PD_MOUNT_TMPFS + export PD_KEEP_LOGS_ON_DISK PD_EXCLUDE_FILE PD_RSYNC_OPTS + + [[ -n "$PD_DURABLE_ROOT" ]] || { usage; pd_die "PD_DURABLE_ROOT required (or pass --demo)"; } + [[ -n "$PD_TOOL_CMD" ]] || { usage; pd_die "PD_TOOL_CMD required (or pass --demo)"; } + [[ -d "$PD_DURABLE_ROOT" ]] || pd_die "missing durable root: $PD_DURABLE_ROOT" + + mkdir -p "$PD_STATUS_DIR" "$PD_LOG_DIR" + local master_log="${PD_LOG_DIR}/orchestrator_${PD_JOB_NAME}.log" + exec > >(tee -a "$master_log") 2>&1 + + pd_log "=== PD job start: ${PD_JOB_NAME} ===" + pd_log "durable: $PD_DURABLE_ROOT" + pd_log "tmpfs: $PD_WORK_DIR" + pd_log "tool: $PD_TOOL_CMD" + pd_log "keep_logs_on_disk=${PD_KEEP_LOGS_ON_DISK}" + + # Flush outputs home even if the job is killed. + trap pd_orch_cleanup EXIT INT TERM + + "${SCRIPT_DIR}/stage_to_tmpfs.sh" + pd_rewire_logs_to_disk + + if [[ "${PD_CHECKPOINT_SECS}" =~ ^[0-9]+$ && "${PD_CHECKPOINT_SECS}" -gt 0 ]]; then + pd_log "Starting checkpoint loop every ${PD_CHECKPOINT_SECS}s" + "${SCRIPT_DIR}/checkpoint_sync.sh" --loop "${PD_CHECKPOINT_SECS}" & + PD_ORCH_CKPT_PID=$! + fi + + pd_log "Launching tool in workspace (PD_PERF=${PD_PERF:-0})" + set +e + ( + cd "$PD_WORK_DIR" + # bash -c keeps quoted paths intact (e.g. directories with spaces). + if [[ "${PD_PERF:-0}" == "1" ]]; then + PD_DURABLE_ROOT="$PD_DURABLE_ROOT" PD_JOB_NAME="$PD_JOB_NAME" \ + PD_TOOL_CMD="$PD_TOOL_CMD" \ + "${SCRIPT_DIR}/pd_perf_profile.sh" --out "${PD_LOG_DIR}/perf_${PD_JOB_NAME}" + else + bash -c "$PD_TOOL_CMD" + fi + ) + PD_ORCH_TOOL_RC=$? + set -e + + pd_log "Tool exited with rc=${PD_ORCH_TOOL_RC}" + # EXIT trap runs finalize +} + +main "$@" +exit "$PD_ORCH_TOOL_RC" diff --git a/PD Job Acceleration/scripts/stage_to_tmpfs.sh b/PD Job Acceleration/scripts/stage_to_tmpfs.sh new file mode 100755 index 0000000..c221c19 --- /dev/null +++ b/PD Job Acceleration/scripts/stage_to_tmpfs.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Stage a durable PD job tree into a RAM-backed workspace. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=pd_job_env.sh +source "${SCRIPT_DIR}/pd_job_env.sh" + +usage() { + cat <<'EOF' +Usage: stage_to_tmpfs.sh + +Required env: + PD_DURABLE_ROOT Persistent job directory (NFS / disk) + PD_JOB_NAME Job name (workspace under PD_TMPFS_ROOT) + +Optional env: + PD_TMPFS_ROOT Default: /dev/shm/pdjobs/$USER + PD_TMPFS_SIZE Default: 8G (only used if mounting) + PD_MOUNT_TMPFS auto|yes|no (default: auto) + PD_EXCLUDE_FILE Extra rsync exclude patterns file +EOF +} + +main() { + pd_require_cmd rsync mkdir + pd_job_defaults + + [[ -n "$PD_DURABLE_ROOT" ]] || { usage; pd_die "PD_DURABLE_ROOT is required"; } + [[ -d "$PD_DURABLE_ROOT" ]] || pd_die "PD_DURABLE_ROOT does not exist: $PD_DURABLE_ROOT" + + pd_prepare_workspace + mkdir -p "$PD_STATUS_DIR" "$PD_LOG_DIR" + + pd_log "Staging durable → tmpfs" + pd_log " from: ${PD_DURABLE_ROOT}/" + pd_log " to: ${PD_WORK_DIR}/" + + local start end + start=$(date +%s) + pd_rsync "${PD_DURABLE_ROOT}/" "${PD_WORK_DIR}/" + end=$(date +%s) + + echo "staged $(date -Iseconds) duration_s=$((end - start))" \ + > "${PD_STATUS_DIR}/last_stage.txt" + pd_log "Stage complete in $((end - start))s → ${PD_WORK_DIR}" +} + +main "$@" diff --git a/RC Extraction/.gitignore b/RC Extraction/.gitignore new file mode 100644 index 0000000..b4243db --- /dev/null +++ b/RC Extraction/.gitignore @@ -0,0 +1,10 @@ +*.o +rcx_extract +test_rc +build/ +*.spef +.DS_Store +rcx_extract +test_rc +*.o +src/*.o diff --git a/RC Extraction/CMakeLists.txt b/RC Extraction/CMakeLists.txt new file mode 100644 index 0000000..d57fbd6 --- /dev/null +++ b/RC Extraction/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.14) +project(rc_extraction LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(rcx + src/techfile.cpp + src/layout.cpp + src/connectivity.cpp + src/resistance.cpp + src/capacitance.cpp + src/rc_network.cpp + src/spef_writer.cpp + src/elmore.cpp + src/extract.cpp +) +target_include_directories(rcx PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_compile_options(rcx PRIVATE -Wall -Wextra -pedantic) + +add_executable(rcx_extract src/main.cpp) +target_link_libraries(rcx_extract PRIVATE rcx) + +add_executable(test_rc tests/test_rc.cpp) +target_link_libraries(test_rc PRIVATE rcx) + +enable_testing() +add_test(NAME rc_unit COMMAND test_rc WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/RC Extraction/Makefile b/RC Extraction/Makefile new file mode 100644 index 0000000..f2fb147 --- /dev/null +++ b/RC Extraction/Makefile @@ -0,0 +1,45 @@ +CXX ?= g++ +CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra -pedantic -Iinclude +LDFLAGS ?= + +SRCS = \ + src/techfile.cpp \ + src/layout.cpp \ + src/connectivity.cpp \ + src/resistance.cpp \ + src/capacitance.cpp \ + src/rc_network.cpp \ + src/spef_writer.cpp \ + src/elmore.cpp \ + src/extract.cpp + +OBJS = $(SRCS:.cpp=.o) + +.PHONY: all clean test demos + +all: rcx_extract test_rc + +rcx_extract: src/main.cpp $(OBJS) + $(CXX) $(CXXFLAGS) -o $@ src/main.cpp $(OBJS) $(LDFLAGS) + +test_rc: tests/test_rc.cpp $(OBJS) + $(CXX) $(CXXFLAGS) -o $@ tests/test_rc.cpp $(OBJS) $(LDFLAGS) + +src/%.o: src/%.cpp + $(CXX) $(CXXFLAGS) -c -o $@ $< + +test: test_rc + ./test_rc + +demos: rcx_extract + @echo "===== simple_net =====" + ./rcx_extract examples/simple_net.lay --spef /tmp/simple_net.spef --elmore clk U1:Z + @echo "" + @echo "===== coupled_nets =====" + ./rcx_extract examples/coupled_nets.lay --spef /tmp/coupled_nets.spef + @echo "" + @echo "===== via_stack =====" + ./rcx_extract examples/via_stack.lay --spef /tmp/via_stack.spef --elmore netx DRV:Z + +clean: + rm -f $(OBJS) rcx_extract test_rc diff --git a/RC Extraction/README.md b/RC Extraction/README.md new file mode 100644 index 0000000..c54227d --- /dev/null +++ b/RC Extraction/README.md @@ -0,0 +1,110 @@ +# RC Extraction (C++) + +Hands-on project: **how R & C are extracted from layout geometry** (the GDS-derived shapes used in physical-design signoff) and turned into SPEF for timing. + +This is **strictly C++17**. No Python. + +Deep theory: [`docs/THEORY.md`](docs/THEORY.md) + +--- + +## What you will learn + +People often say “extract the netlist from GDS.” That usually means **LVS connectivity**. **Parasitic RC extraction** is a second pass over the *same geometry* plus a **process techfile**: + +``` +GDS polygons → connectivity (nets) → segment R & C → SPEF → STA / SI +``` + +You do **not** compute R/C from a logical Verilog netlist. You compute them from **wire geometry + layer stack**, then annotate the logical nets. + +This project implements a transparent **2.5D analytical extractor**: + +| Step | Module | Formula / idea | +|------|--------|----------------| +| Connectivity | `connectivity.cpp` | Union-Find on abutting metals + via stitches | +| Resistance | `resistance.cpp` | \(R = R_s \cdot L/W\), via \(R_{via}/N\) | +| Capacitance | `capacitance.cpp` | area + fringe + same-layer coupling | +| RC network | `rc_network.cpp` | π-model, geometric node merge | +| SPEF | `spef_writer.cpp` | IEEE-1481 subset for signoff tools | +| Elmore | `elmore.cpp` | mini timing signoff on the extracted tree | + +--- + +## Build + +```bash +cd "RC Extraction" +make # → rcx_extract, test_rc +make test +make demos +``` + +Or CMake: + +```bash +cmake -S . -B build && cmake --build build +cd build && ctest --verbose +``` + +--- + +## Run + +```bash +./rcx_extract examples/simple_net.lay +./rcx_extract examples/simple_net.lay --spef out.spef --elmore clk U1:Z +./rcx_extract examples/coupled_nets.lay --spef coupled.spef +./rcx_extract examples/via_stack.lay --elmore netx DRV:Z +``` + +### Layout format (`.lay`) + +Stand-in for GDS polygons + pin annotations (no external GDS parser): + +``` +NAME design +METAL +VIA +PIN +``` + +Coordinates are in **µm**. Tech parameters live in `src/techfile.cpp` (`defaultTech()`). + +--- + +## Examples + +1. **`simple_net.lay`** — one M1 wire, driver→load. See sheet resistance and Elmore delay. +2. **`coupled_nets.lay`** — parallel victim/aggressor. See `Cc` coupling in SPEF. +3. **`via_stack.lay`** — M1–VIA1–M2–VIA2–M3. See multi-layer connectivity + via R. + +--- + +## Mental model (GDS → SPEF) + +``` +GDS / layout shapes + │ + ├─ layer map (METAL / VIA) + ├─ geometric connectivity → net IDs (same graph LVS uses) + ├─ fracture into segments + ├─ R = Rs·L/W , Rvia + ├─ Carea, Cfringe, Ccoup(neighbors) + ├─ assemble π / distributed RC graph + └─ emit SPEF → PrimeTime / Tempus / OpenSTA +``` + +Industry tools (StarRC, Quantus, Calibre xRC) scale this with pattern tables and 3D field solvers. The physics and dataflow are the same as this toy. + +--- + +## Source map + +``` +include/ public headers +src/ implementation + main +examples/ .lay layouts +tests/test_rc.cpp unit checks for R, via, coupling, pipeline +docs/THEORY.md full signoff / extraction write-up +``` diff --git a/RC Extraction/docs/SIGNOFF_FLOW.md b/RC Extraction/docs/SIGNOFF_FLOW.md new file mode 100644 index 0000000..5ea8572 --- /dev/null +++ b/RC Extraction/docs/SIGNOFF_FLOW.md @@ -0,0 +1,21 @@ +# Signoff flow context + +``` +RTL → Synthesis → Place & Route → GDSII + │ + ┌──────────┴──────────┐ + ▼ ▼ + LVS / DRC Parasitic Extraction + (devices + nets OK?) (R, C from geometry) + │ │ + └──────────┬──────────┘ + ▼ + SPEF (+ .lib) + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ + STA Crosstalk/SI EM / IR + (setup/hold) (noise, delay) (power grid) +``` + +This project focuses on the **parasitic extraction** box and the SPEF handoff to STA (via Elmore as a teaching stand-in for a full timer). diff --git a/RC Extraction/docs/THEORY.md b/RC Extraction/docs/THEORY.md new file mode 100644 index 0000000..79686bd --- /dev/null +++ b/RC Extraction/docs/THEORY.md @@ -0,0 +1,286 @@ +# RC Extraction Theory (Physical Design Signoff) + +This document explains **how resistance (R) and capacitance (C) are extracted from layout geometry** that ultimately comes from GDS — the same physics commercial tools (StarRC, Quantus, Calibre xRC, Rapid3D, …) approximate at industrial scale. + +--- + +## 1. Where RC extraction sits in the signoff flow + +``` +RTL → Synthesis → PnR → GDSII (mask layout) + │ + ▼ + ┌─────────────────────┐ + │ Layout vs Schematic │ (LVS: connectivity correct?) + │ Device recognition │ + └──────────┬──────────┘ + ▼ + ┌─────────────────────┐ + │ Parasitic Extraction│ ← YOU ARE HERE + │ (R, C, sometimes L) │ + └──────────┬──────────┘ + ▼ + SPEF / DSPF / SPF + │ + ┌──────────┴──────────┐ + ▼ ▼ + Static Timing (STA) Signal Integrity / IR-drop + with annotated delay (coupled C, noise, EM) +``` + +**Important distinction:** + +| Step | Input | Output | Question answered | +|------|-------|--------|-------------------| +| LVS / netlist extraction | GDS polygons + schematic | Flat/hierarchical **logical** netlist | “Are the transistors wired as intended?” | +| Parasitic RC extraction | Same GDS polygons + **process techfile** | Annotated netlist / **SPEF** with R & C | “What delays, noise, and IR drop do those wires cause?” | + +People often say “extract the netlist from GDS.” That usually means LVS device+connectivity extraction. **RC extraction is a second pass** over the *same geometry*, using foundry electrical rules (ITF / ICT / QRC techfile) to attach parasitics to those nets. + +--- + +## 2. What is in the GDS? + +GDSII is a stream of **polygons on named layers** (MET1, VIA1, MET2, POLY, DIFF, …), plus cell hierarchy. + +For interconnect RC, the extractor cares about: + +1. **Conductor shapes** — metal / poly rectangles (or rectilinear polygons) +2. **Via shapes** — cuts that connect adjacent metal layers +3. **Layer stack** — which layer is above which, thickness, dielectric +4. **Connectivity** — shapes that touch (or are connected by vias) belong to the same **net** + +A simplified 3-metal stack: + +``` + ┌──────────────┐ M3 (top metal) + │ │ + ═════╪══════════════╪════ VIA2 + │ │ + ┌────┴────┐ ┌────┴────┐ M2 + │ │ │ │ +═══╪═════════╪════╪═════════╪═ VIA1 + │ │ │ │ +┌──┴──┐ ┌──┴────┴──┐ ┌──┴──┐ M1 +│ │ │ │ │ │ +└─────┘ └──────────┘ └─────┘ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ substrate / ground plane +``` + +--- + +## 3. Resistance extraction + +### 3.1 Sheet resistance + +A thin metal film has **sheet resistance** \(R_s\) (Ω/□): + +\[ +R_s = \frac{\rho}{t} +\] + +where \(\rho\) is resistivity and \(t\) is thickness. + +For a rectangular wire of length \(L\) and width \(W\): + +\[ +R = R_s \cdot \frac{L}{W} +\] + +“Squares” = \(L/W\). A wire 10 µm long and 1 µm wide is **10 squares**. + +### 3.2 Via resistance + +Each via has a nearly-fixed resistance \(R_{via}\) (from foundry tables; depends on size, barrier, landing). Parallel vias: + +\[ +R_{via,eq} = \frac{R_{via}}{N} +\] + +### 3.3 Segmentation + +Extractors chop long wires into **segments** (at bends, vias, taps, or fixed max length) so the RC network can model distributed delay. Each segment becomes one resistor (or a chain of resistors in distributed models). + +### 3.4 What we ignore in this teaching project + +- Current crowding / current density maps +- Barrier/liner non-uniformity +- Temperature coefficients (real signoff uses corner temps) +- Slotting / cheesing density rules affecting effective \(R_s\) + +--- + +## 4. Capacitance extraction + +Capacitance is harder: every conductor couples to every nearby conductor through the dielectric. + +### 4.1 Three dominant terms (2.5D analytical model) + +For a wire on layer \(i\): + +1. **Area (plate) capacitance to ground / substrate** + \[ + C_{area} = \varepsilon_{ox} \cdot \frac{W \cdot L}{H} + \] + \(H\) = dielectric thickness under the metal. + +2. **Fringe capacitance** — field lines from the sidewalls to ground + \[ + C_{fringe} \approx \varepsilon \cdot L \cdot f(T, H, W) + \] + Often tabulated or fit as \(c_f \cdot L\) (fF/µm). + +3. **Coupling capacitance** between parallel neighbors on the same layer + \[ + C_{coup} \approx \varepsilon \cdot \frac{T \cdot L_{overlap}}{S} + \] + \(T\) = metal thickness, \(S\) = spacing. More accurate models use conformal-mapping / foundry tables vs \(S\). + +Cross-layer coupling (M1 under M2) also exists; full-chip tools use pattern matching or field solvers. + +### 4.2 Total capacitance seen by STA + +For a net segment: + +\[ +C_{total} = C_{area} + C_{fringe} + \sum C_{coup} +\] + +In **SPEF**, coupling caps are often stored as separate `CC` (coupled) elements so SI tools can do aggressor/victim analysis. STA may use grounded-C (lump coupling to ground with a Miller factor) or keep CC for Crosstalk delay. + +### 4.3 Extraction modes (industry) + +| Mode | Method | Accuracy | Runtime | +|------|--------|----------|---------| +| Rule-based / pattern | Lookup tables from foundry | Good for digital | Fast | +| 2.5D analytical | Formulas + tables | Medium | Fast | +| 3D field solver | Solve Maxwell on voxel/mesh | Highest | Slow (used on critical nets) | + +This project implements a **transparent 2.5D analytical** model so you can see every formula. + +--- + +## 5. RC network models + +Once you have segment R and C, you assemble a circuit model of the net: + +### Lumped C (too crude for long nets) +``` +Driver ── R_total ──●── Receiver + │ + C_total + │ + GND +``` + +### π-model (common for SPEF segments) +``` +Driver ── R ──●── Receiver + │ + C/2 C/2 + │ + GND +``` + +### Distributed / ladder (better for long / high-R wires) +``` +── R/n ──●── R/n ──●── … ──●── + │ │ │ + C/n C/n C/n +``` + +Elmore delay for an RC tree rooted at the driver: + +\[ +T_{D}(i) = \sum_{k \in path(s\to i)} R_k \cdot C_{downstream}(k) +\] + +--- + +## 6. SPEF — what signoff tools actually consume + +**SPEF** (Standard Parasitic Exchange Format, IEEE 1481) annotates parasitics onto a netlist for STA (PrimeTime, Tempus, OpenSTA, …). + +Minimal conceptual SPEF for one net: + +``` +*D_NET net_a 12.5 // total lumped C in pF (or fF per *C_UNIT) +*CONN +*I inst1:Z O // driver pin +*I inst2:A I // load pin +*CAP +1 net_a:1 0.006 // grounded cap +2 net_a:2 0.006 +*RES +1 net_a:1 net_a:2 25.0 // resistance between nodes +*END +``` + +Coupling: + +``` +*CAP +3 net_a:1 net_b:1 0.002 // CC between two nets +``` + +--- + +## 7. End-to-end mental model (GDS → SPEF) + +``` +GDS polygons + │ + ├─1─ Flatten / expand hierarchy (or extract hierarchical) + │ + ├─2─ Layer map: GDS layer/datatype → conductor / via / device + │ + ├─3─ Geometric connectivity: abutting metals + vias → NET IDs + │ (same graph LVS builds for shorts/opens) + │ + ├─4─ Fracture nets into wire segments + via instances + │ + ├─5─ For each segment: R = Rs·L/W ; collect via R + │ + ├─6─ For each segment: Carea, Cfringe; for each neighbor pair: Ccoup + │ (need neighbor search: edges, R-trees, scanline) + │ + ├─7─ Build RC graph per net (nodes at pins, vias, bends) + │ + └─8─ Write SPEF / DSPF / OpenRCX .spef → STA / SI / EM-IR +``` + +**You do not extract R&C “from the logical netlist.”** +You extract connectivity from geometry (→ nets), then attach R&C computed from **geometry + process stack**, and emit a parasitic netlist that *references* the logical net/pin names. + +--- + +## 8. Corners and signoff reality + +Foundries provide multiple extraction corners, e.g.: + +- **Cmax / Cmin** — dielectric / etch extremes for timing +- **RCmax / RCmin** — combined interconnect corners +- **Typical** + +Signoff STA runs **multi-corner multi-mode (MCMM)** with matching SPEF per corner. Temperature and voltage further scale R (and sometimes C). + +--- + +## 9. What this teaching extractor implements + +| Feature | Status | +|---------|--------| +| Rectilinear metal rectangles on M1–M3 | Yes | +| Via cuts between layers | Yes | +| Connectivity / net labeling | Yes | +| Segment resistance \(R_s L/W\) | Yes | +| Via resistance | Yes | +| Area + fringe C to ground | Yes | +| Same-layer coupling C | Yes | +| π-model RC network | Yes | +| SPEF writer | Yes | +| Elmore delay demo (mini-signoff) | Yes | +| Full GDSII parser / 3D field solver | No (intentionally) | +| Device (transistor) extraction | No (focus = interconnect) | + +The goal is **deep intuition**, not replacing StarRC. diff --git a/RC Extraction/examples/coupled_nets.lay b/RC Extraction/examples/coupled_nets.lay new file mode 100644 index 0000000..bed700d --- /dev/null +++ b/RC Extraction/examples/coupled_nets.lay @@ -0,0 +1,13 @@ +# coupled_nets.lay — two parallel M1 wires (victim / aggressor) +# +# Teaches: same-layer coupling capacitance Ccoup ≈ k * T * Lov / S + +NAME coupled_nets + +METAL M1 victim v0 0.0 0.0 8.0 0.2 +METAL M1 aggressor a0 0.0 0.4 8.0 0.6 + +PIN VDRV:Z victim M1 O 0.1 0.1 +PIN VLD:A victim M1 I 7.9 0.1 +PIN ADRV:Z aggressor M1 O 0.1 0.5 +PIN ALD:A aggressor M1 I 7.9 0.5 diff --git a/RC Extraction/examples/simple_net.lay b/RC Extraction/examples/simple_net.lay new file mode 100644 index 0000000..0daef34 --- /dev/null +++ b/RC Extraction/examples/simple_net.lay @@ -0,0 +1,12 @@ +# simple_net.lay — single driver→receiver wire on M1 (π-model demo) +# +# U1:Z ──────────── 10 µm × 0.2 µm M1 ──────────── U2:A +# +# Teaches: R = Rs * L/W, Carea, Cfringe, Elmore delay. + +NAME simple_net + +METAL M1 clk w0 0.0 0.0 10.0 0.2 + +PIN U1:Z clk M1 O 0.1 0.1 +PIN U2:A clk M1 I 9.9 0.1 diff --git a/RC Extraction/examples/via_stack.lay b/RC Extraction/examples/via_stack.lay new file mode 100644 index 0000000..8b70a53 --- /dev/null +++ b/RC Extraction/examples/via_stack.lay @@ -0,0 +1,15 @@ +# via_stack.lay — M1 → VIA1 → M2 → VIA2 → M3 route +# +# Teaches: via resistance, multi-layer connectivity (LVS-like stitching), +# π-model across layers for signoff SPEF. + +NAME via_stack + +METAL M1 netx m1 0.0 0.0 3.0 0.2 +VIA VIA1 netx v1 2.80 0.03 2.94 0.17 +METAL M2 netx m2 2.5 0.0 6.0 0.25 +VIA VIA2 netx v2 5.80 0.05 5.94 0.19 +METAL M3 netx m3 5.5 0.0 10.0 0.3 + +PIN DRV:Z netx M1 O 0.1 0.1 +PIN LD:A netx M3 I 9.9 0.15 diff --git a/RC Extraction/include/capacitance.hpp b/RC Extraction/include/capacitance.hpp new file mode 100644 index 0000000..eb887ea --- /dev/null +++ b/RC Extraction/include/capacitance.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "layout.hpp" + +#include +#include +#include + +namespace rcx { + +struct Capacitor { + std::string name; + std::string net_pos; + std::string node_pos; + double value_ff = 0; + std::string kind; // "area" | "fringe" | "coupling" + std::string net_neg = "GROUND"; + std::string node_neg = "GROUND"; + std::map meta; +}; + +double areaCap(const Shape& shape, const TechFile& tech); +double fringeCap(const Shape& shape, const TechFile& tech); + +// Returns coupling C in fF (0 if none). Fills meta with gap/overlap. +double couplingCap(const Shape& a, const Shape& b, const TechFile& tech, + std::map* meta = nullptr); + +std::vector extractCapacitance(const Layout& layout); + +} // namespace rcx diff --git a/RC Extraction/include/connectivity.hpp b/RC Extraction/include/connectivity.hpp new file mode 100644 index 0000000..8742689 --- /dev/null +++ b/RC Extraction/include/connectivity.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "layout.hpp" + +#include +#include +#include +#include + +namespace rcx { + +struct Net { + std::string name; + std::set shape_indices; +}; + +// Geometric connectivity: abutting same-layer metals + via stitches → nets. +// Stamps Shape::net on every shape. Returns map netName → Net. +std::map extractConnectivity(Layout& layout); + +std::string summarizeConnectivity(const Layout& layout, + const std::map& nets); + +} // namespace rcx diff --git a/RC Extraction/include/elmore.hpp b/RC Extraction/include/elmore.hpp new file mode 100644 index 0000000..75359ab --- /dev/null +++ b/RC Extraction/include/elmore.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "rc_network.hpp" + +#include +#include +#include + +namespace rcx { + +struct ElmoreResult { + std::string driver_node; + std::map delay_ps; // node → Elmore delay (ps) + std::map downstream_c_ff; +}; + +// Elmore delay on an RC tree. driver_pin is a pin name (e.g. "U1:Z") or node. +// Assumes the net is a tree rooted at the driver (typical for digital nets). +ElmoreResult elmoreDelay(const RCNetwork& net, const std::string& net_name, + const std::string& driver_pin); + +std::string formatElmore(const ElmoreResult& r); + +} // namespace rcx diff --git a/RC Extraction/include/extract.hpp b/RC Extraction/include/extract.hpp new file mode 100644 index 0000000..a1eb6a0 --- /dev/null +++ b/RC Extraction/include/extract.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "capacitance.hpp" +#include "connectivity.hpp" +#include "elmore.hpp" +#include "layout.hpp" +#include "rc_network.hpp" +#include "resistance.hpp" +#include "spef_writer.hpp" + +#include +#include +#include + +namespace rcx { + +struct ExtractionResult { + Layout layout; + std::map nets; + std::vector resistors; + std::vector capacitors; + RCNetwork network; + std::string spef; +}; + +// Full pipeline: layout → connectivity → R → C → RC network → SPEF +ExtractionResult extractAll(Layout layout); + +std::string reportExtraction(const ExtractionResult& r); + +} // namespace rcx diff --git a/RC Extraction/include/geometry.hpp b/RC Extraction/include/geometry.hpp new file mode 100644 index 0000000..8dfdcfa --- /dev/null +++ b/RC Extraction/include/geometry.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace rcx { + +struct Rect { + double x0 = 0, y0 = 0, x1 = 0, y1 = 0; + + Rect() = default; + Rect(double x0_, double y0_, double x1_, double y1_) + : x0(x0_), y0(y0_), x1(x1_), y1(y1_) { + if (x1 <= x0 || y1 <= y0) + throw std::runtime_error("Invalid rectangle"); + } + + double width() const { return x1 - x0; } + double height() const { return y1 - y0; } + double area() const { return width() * height(); } + double cx() const { return 0.5 * (x0 + x1); } + double cy() const { return 0.5 * (y0 + y1); } + + bool isHorizontal() const { return width() >= height(); } + + // (length along run, width perpendicular) for Manhattan wires + std::pair lengthWidth() const { + if (isHorizontal()) + return {width(), height()}; + return {height(), width()}; + } + + bool overlaps(const Rect& o, double tol = 1e-9) const { + return !(x1 < o.x0 - tol || o.x1 < x0 - tol || y1 < o.y0 - tol || + o.y1 < y0 - tol); + } + + Rect expanded(double d) const { + return Rect(x0 - d, y0 - d, x1 + d, y1 + d); + } + + bool contains(double x, double y, double tol = 1e-9) const { + return x >= x0 - tol && x <= x1 + tol && y >= y0 - tol && y <= y1 + tol; + } +}; + +struct Shape { + std::string layer; + Rect rect; + std::string net; // filled by connectivity / input + std::string name; // optional tag + + bool isVia() const { + if (layer.size() < 3) + return false; + std::string up = layer; + for (char& c : up) + c = static_cast(std::toupper(static_cast(c))); + return up.rfind("VIA", 0) == 0; + } +}; + +struct Pin { + std::string name; // e.g. "U1:Z" + std::string net; + std::string layer; + double x = 0, y = 0; + std::string direction = "I"; // I / O / B +}; + +} // namespace rcx diff --git a/RC Extraction/include/layout.hpp b/RC Extraction/include/layout.hpp new file mode 100644 index 0000000..b143cf3 --- /dev/null +++ b/RC Extraction/include/layout.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "geometry.hpp" +#include "techfile.hpp" + +#include +#include + +namespace rcx { + +struct Layout { + std::string name; + std::vector shapes; + std::vector pins; + TechFile tech; + + Layout() : tech(defaultTech()) {} + + std::vector metalIndices() const { + std::vector idx; + for (std::size_t i = 0; i < shapes.size(); ++i) + if (!shapes[i].isVia()) + idx.push_back(i); + return idx; + } + + std::vector viaIndices() const { + std::vector idx; + for (std::size_t i = 0; i < shapes.size(); ++i) + if (shapes[i].isVia()) + idx.push_back(i); + return idx; + } +}; + +// Simple line-oriented layout format (.lay) — no external JSON dependency. +// +// NAME design_name +// METAL +// VIA +// PIN +// # comments allowed +Layout loadLayout(const std::string& path); +void saveLayout(const Layout& layout, const std::string& path); + +} // namespace rcx diff --git a/RC Extraction/include/rc_network.hpp b/RC Extraction/include/rc_network.hpp new file mode 100644 index 0000000..b918cba --- /dev/null +++ b/RC Extraction/include/rc_network.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "capacitance.hpp" +#include "layout.hpp" +#include "resistance.hpp" + +#include +#include +#include + +namespace rcx { + +// After raw R/C extraction, merge geometrically touching nodes into an RC graph +// suitable for SPEF and Elmore delay. +struct RCNode { + std::string name; + std::string net; + bool is_pin = false; + std::string pin_name; + std::string pin_dir; +}; + +struct RCEdgeR { + std::string name; + std::string net; + std::string node_a; // canonical names after merge + std::string node_b; + double value_ohm = 0; + std::string kind; +}; + +struct RCCap { + std::string name; + std::string net_pos; + std::string node_pos; + double value_ff = 0; + std::string kind; + std::string net_neg = "GROUND"; + std::string node_neg = "GROUND"; +}; + +struct RCNetwork { + std::map nodes; // canonical node name → node + std::vector resistors; + std::vector capacitors; + // Per-net total grounded C (fF), for SPEF *D_NET header + std::map net_total_c_ff; +}; + +RCNetwork buildRCNetwork(const Layout& layout, + const std::vector& resistors, + const std::vector& capacitors); + +} // namespace rcx diff --git a/RC Extraction/include/resistance.hpp b/RC Extraction/include/resistance.hpp new file mode 100644 index 0000000..ae4196d --- /dev/null +++ b/RC Extraction/include/resistance.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "layout.hpp" + +#include +#include +#include + +namespace rcx { + +struct Resistor { + std::string name; + std::string net; + std::string node_a; + std::string node_b; + double value_ohm = 0; + std::string kind; // "wire" | "via" + std::map meta; +}; + +double wireResistance(const Shape& shape, const TechFile& tech); +double viaResistance(const Shape& shape, const TechFile& tech); + +std::vector extractResistance(const Layout& layout); + +} // namespace rcx diff --git a/RC Extraction/include/spef_writer.hpp b/RC Extraction/include/spef_writer.hpp new file mode 100644 index 0000000..ed5fab7 --- /dev/null +++ b/RC Extraction/include/spef_writer.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "rc_network.hpp" + +#include + +namespace rcx { + +// Write IEEE-1481-ish SPEF (subset) for OpenSTA / educational inspection. +std::string writeSpef(const RCNetwork& net, const std::string& design_name); +void writeSpefFile(const RCNetwork& net, const std::string& design_name, + const std::string& path); + +} // namespace rcx diff --git a/RC Extraction/include/techfile.hpp b/RC Extraction/include/techfile.hpp new file mode 100644 index 0000000..b605481 --- /dev/null +++ b/RC Extraction/include/techfile.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace rcx { + +struct MetalLayer { + std::string name; + double thickness = 0; // µm metal thickness T + double height = 0; // µm bottom above reference + double sheet_r = 0; // Ω/□ + double c_area = 0; // fF/µm² + double c_fringe = 0; // fF/µm (along wire length) + double c_coup_k = 0; // coupling scale: C = k * T * Lov / S (fF) +}; + +struct ViaLayer { + std::string name; + std::string lower_metal; + std::string upper_metal; + double resistance = 0; // Ω per cut + double size = 0.14; // µm square cut edge +}; + +class TechFile { + public: + std::string name; + std::map metals; + std::map vias; + + void addMetal(const MetalLayer& m) { metals[m.name] = m; } + void addVia(const ViaLayer& v) { vias[v.name] = v; } + + const MetalLayer& metal(const std::string& n) const { return metals.at(n); } + const ViaLayer& via(const std::string& n) const { return vias.at(n); } + + const ViaLayer* viaBetween(const std::string& a, const std::string& b) const { + for (const auto& kv : vias) { + const auto& v = kv.second; + if ((v.lower_metal == a && v.upper_metal == b) || + (v.lower_metal == b && v.upper_metal == a)) + return &v; + } + return nullptr; + } + + std::vector metalOrder() const { + std::vector> tmp; + for (const auto& kv : metals) + tmp.push_back({kv.second.height, kv.first}); + std::sort(tmp.begin(), tmp.end()); + std::vector out; + for (auto& t : tmp) + out.push_back(t.second); + return out; + } +}; + +// Synthetic 3-metal stack (educational, not a real PDK) +TechFile defaultTech(); + +} // namespace rcx diff --git a/RC Extraction/src/capacitance.cpp b/RC Extraction/src/capacitance.cpp new file mode 100644 index 0000000..6ab7e79 --- /dev/null +++ b/RC Extraction/src/capacitance.cpp @@ -0,0 +1,140 @@ +#include "capacitance.hpp" + +#include +#include + +namespace rcx { + +double areaCap(const Shape& shape, const TechFile& tech) { + return tech.metal(shape.layer).c_area * shape.rect.area(); +} + +double fringeCap(const Shape& shape, const TechFile& tech) { + auto lw = shape.rect.lengthWidth(); + return tech.metal(shape.layer).c_fringe * lw.first; +} + +double couplingCap(const Shape& a, const Shape& b, const TechFile& tech, + std::map* meta) { + if (a.layer != b.layer || a.isVia() || b.isVia()) + return 0.0; + if (!a.net.empty() && a.net == b.net) + return 0.0; + + const MetalLayer& layer = tech.metal(a.layer); + const Rect& ra = a.rect; + const Rect& rb = b.rect; + + struct Cand { + double gap, lov; + }; + std::vector cands; + + // Separated in X, overlapping in Y + if (ra.x1 <= rb.x0 || rb.x1 <= ra.x0) { + double gap = (ra.x1 <= rb.x0) ? (rb.x0 - ra.x1) : (ra.x0 - rb.x1); + double y0 = std::max(ra.y0, rb.y0); + double y1 = std::min(ra.y1, rb.y1); + double lov = y1 - y0; + if (gap > 0 && lov > 0) + cands.push_back({gap, lov}); + } + + // Separated in Y, overlapping in X + if (ra.y1 <= rb.y0 || rb.y1 <= ra.y0) { + double gap = (ra.y1 <= rb.y0) ? (rb.y0 - ra.y1) : (ra.y0 - rb.y1); + double x0 = std::max(ra.x0, rb.x0); + double x1 = std::min(ra.x1, rb.x1); + double lov = x1 - x0; + if (gap > 0 && lov > 0) + cands.push_back({gap, lov}); + } + + if (cands.empty()) + return 0.0; + + Cand best = *std::min_element( + cands.begin(), cands.end(), + [](const Cand& u, const Cand& v) { return u.gap < v.gap; }); + + if (best.gap > 2.0) // µm search radius + return 0.0; + + double c = layer.c_coup_k * layer.thickness * best.lov / best.gap; + if (meta) { + (*meta)["gap"] = best.gap; + (*meta)["overlap"] = best.lov; + } + return c; +} + +std::vector extractCapacitance(const Layout& layout) { + std::vector caps; + std::vector metals = layout.metalIndices(); + + for (std::size_t i : metals) { + const Shape& shape = layout.shapes[i]; + const std::string net = + shape.net.empty() ? ("unknown_" + std::to_string(i)) : shape.net; + const std::string tag = + shape.name.empty() || shape.name == "-" ? ("s" + std::to_string(i)) + : shape.name; + const std::string node = net + ":" + tag + "_MID"; + + Capacitor ca; + ca.name = "Ca_" + tag; + ca.net_pos = net; + ca.node_pos = node; + ca.value_ff = areaCap(shape, layout.tech); + ca.kind = "area"; + ca.meta["shape_index"] = static_cast(i); + caps.push_back(ca); + + Capacitor cf; + cf.name = "Cf_" + tag; + cf.net_pos = net; + cf.node_pos = node; + cf.value_ff = fringeCap(shape, layout.tech); + cf.kind = "fringe"; + cf.meta["shape_index"] = static_cast(i); + caps.push_back(cf); + } + + int cc_id = 0; + for (std::size_t ii = 0; ii < metals.size(); ++ii) { + std::size_t i = metals[ii]; + const Shape& a = layout.shapes[i]; + for (std::size_t jj = ii + 1; jj < metals.size(); ++jj) { + std::size_t j = metals[jj]; + const Shape& b = layout.shapes[j]; + std::map meta; + double c = couplingCap(a, b, layout.tech, &meta); + if (c <= 0.0) + continue; + + const std::string na = + a.net.empty() ? ("unknown_" + std::to_string(i)) : a.net; + const std::string nb = + b.net.empty() ? ("unknown_" + std::to_string(j)) : b.net; + const std::string ta = + a.name.empty() || a.name == "-" ? ("s" + std::to_string(i)) : a.name; + const std::string tb = + b.name.empty() || b.name == "-" ? ("s" + std::to_string(j)) : b.name; + + Capacitor cc; + cc.name = "Cc_" + std::to_string(cc_id++); + cc.net_pos = na; + cc.node_pos = na + ":" + ta + "_MID"; + cc.value_ff = c; + cc.kind = "coupling"; + cc.net_neg = nb; + cc.node_neg = nb + ":" + tb + "_MID"; + cc.meta = meta; + caps.push_back(cc); + } + } + + return caps; +} + +} // namespace rcx diff --git a/RC Extraction/src/connectivity.cpp b/RC Extraction/src/connectivity.cpp new file mode 100644 index 0000000..2998c88 --- /dev/null +++ b/RC Extraction/src/connectivity.cpp @@ -0,0 +1,175 @@ +#include "connectivity.hpp" + +#include +#include + +namespace rcx { +namespace { + +class UnionFind { + public: + explicit UnionFind(std::size_t n) : parent_(n), rank_(n, 0) { + for (std::size_t i = 0; i < n; ++i) + parent_[i] = i; + } + + std::size_t find(std::size_t x) { + while (parent_[x] != x) { + parent_[x] = parent_[parent_[x]]; + x = parent_[x]; + } + return x; + } + + void unite(std::size_t a, std::size_t b) { + a = find(a); + b = find(b); + if (a == b) + return; + if (rank_[a] < rank_[b]) + parent_[a] = b; + else if (rank_[a] > rank_[b]) + parent_[b] = a; + else { + parent_[b] = a; + rank_[a]++; + } + } + + private: + std::vector parent_; + std::vector rank_; +}; + +bool sameLayerTouch(const Shape& a, const Shape& b, double tol = 1e-9) { + if (a.layer != b.layer) + return false; + return a.rect.expanded(tol).overlaps(b.rect); +} + +bool viaConnects(const Shape& via, const Shape& metal, const TechFile& tech) { + auto it = tech.vias.find(via.layer); + if (it == tech.vias.end()) + return false; + const ViaLayer& v = it->second; + if (metal.layer != v.lower_metal && metal.layer != v.upper_metal) + return false; + return via.rect.overlaps(metal.rect); +} + +} // namespace + +std::map extractConnectivity(Layout& layout) { + const std::size_t n = layout.shapes.size(); + UnionFind uf(n); + + // 1) Same-layer metal abutment / overlap + for (std::size_t i = 0; i < n; ++i) { + if (layout.shapes[i].isVia()) + continue; + for (std::size_t j = i + 1; j < n; ++j) { + if (layout.shapes[j].isVia()) + continue; + if (sameLayerTouch(layout.shapes[i], layout.shapes[j])) + uf.unite(i, j); + } + } + + // 2) Via stitches adjacent metals + for (std::size_t vi = 0; vi < n; ++vi) { + if (!layout.shapes[vi].isVia()) + continue; + for (std::size_t mi = 0; mi < n; ++mi) { + if (layout.shapes[mi].isVia()) + continue; + if (viaConnects(layout.shapes[vi], layout.shapes[mi], layout.tech)) + uf.unite(vi, mi); + } + } + + std::map> groups; + for (std::size_t i = 0; i < n; ++i) + groups[uf.find(i)].push_back(i); + + // Pin location → preferred net name + std::map pin_hints; + for (const auto& pin : layout.pins) { + for (std::size_t i = 0; i < n; ++i) { + const Shape& s = layout.shapes[i]; + if (s.isVia()) + continue; + if (s.layer == pin.layer && s.rect.contains(pin.x, pin.y)) + pin_hints[i] = pin.net; + } + } + + std::map nets; + std::set used; + int auto_id = 0; + + for (const auto& g : groups) { + const auto& members = g.second; + std::vector candidates; + for (std::size_t i : members) { + if (!layout.shapes[i].net.empty() && layout.shapes[i].net != "-") + candidates.push_back(layout.shapes[i].net); + auto ph = pin_hints.find(i); + if (ph != pin_hints.end()) + candidates.push_back(ph->second); + } + + std::string name; + for (const auto& c : candidates) { + if (!used.count(c)) { + name = c; + break; + } + } + if (name.empty()) { + do { + name = "net_" + std::to_string(auto_id++); + } while (used.count(name)); + } + + used.insert(name); + Net net; + net.name = name; + for (std::size_t i : members) { + net.shape_indices.insert(i); + layout.shapes[i].net = name; + } + nets[name] = net; + } + + return nets; +} + +std::string summarizeConnectivity(const Layout& layout, + const std::map& nets) { + std::ostringstream oss; + oss << "Connectivity: " << nets.size() << " net(s)\n"; + for (const auto& kv : nets) { + int metals = 0, vias = 0; + std::set layers; + for (std::size_t i : kv.second.shape_indices) { + if (layout.shapes[i].isVia()) + ++vias; + else { + ++metals; + layers.insert(layout.shapes[i].layer); + } + } + oss << " " << kv.first << ": " << metals << " metal rect(s) on ["; + bool first = true; + for (const auto& L : layers) { + if (!first) + oss << ","; + first = false; + oss << L; + } + oss << "], " << vias << " via(s)\n"; + } + return oss.str(); +} + +} // namespace rcx diff --git a/RC Extraction/src/elmore.cpp b/RC Extraction/src/elmore.cpp new file mode 100644 index 0000000..67a91f7 --- /dev/null +++ b/RC Extraction/src/elmore.cpp @@ -0,0 +1,145 @@ +#include "elmore.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace rcx { + +ElmoreResult elmoreDelay(const RCNetwork& net, const std::string& net_name, + const std::string& driver_pin) { + // Resolve driver node: match pin name or exact node name on this net + std::string driver; + for (const auto& kv : net.nodes) { + const RCNode& n = kv.second; + if (n.net != net_name) + continue; + if (n.is_pin && n.pin_name == driver_pin) { + driver = n.name; + break; + } + if (n.name == driver_pin) { + driver = n.name; + break; + } + } + if (driver.empty()) + throw std::runtime_error("Driver not found on net " + net_name + ": " + + driver_pin); + + // Adjacency for this net's resistors + struct Edge { + std::string to; + double r; + }; + std::unordered_map> adj; + std::unordered_set nodes; + for (const auto& r : net.resistors) { + if (r.net != net_name) + continue; + adj[r.node_a].push_back({r.node_b, r.value_ohm}); + adj[r.node_b].push_back({r.node_a, r.value_ohm}); + nodes.insert(r.node_a); + nodes.insert(r.node_b); + } + nodes.insert(driver); + + // Grounded capacitance per node (coupling treated as grounded for Elmore demo) + std::unordered_map c_node; + for (const auto& c : net.capacitors) { + if (c.kind == "coupling") { + if (c.net_pos == net_name) + c_node[c.node_pos] += c.value_ff; + if (c.net_neg == net_name) + c_node[c.node_neg] += c.value_ff; + } else if (c.net_pos == net_name) { + c_node[c.node_pos] += c.value_ff; + } + } + + // BFS parent tree from driver + std::unordered_map parent; + std::unordered_map parent_r; + std::queue q; + std::unordered_set vis; + q.push(driver); + vis.insert(driver); + parent[driver] = ""; + parent_r[driver] = 0.0; + + while (!q.empty()) { + std::string u = q.front(); + q.pop(); + for (const auto& e : adj[u]) { + if (vis.count(e.to)) + continue; + vis.insert(e.to); + parent[e.to] = u; + parent_r[e.to] = e.r; + q.push(e.to); + } + } + + // Children lists + std::unordered_map> children; + for (const auto& kv : parent) { + if (!kv.second.empty()) + children[kv.second].push_back(kv.first); + } + + // Downstream capacitance via DFS post-order + ElmoreResult result; + result.driver_node = driver; + std::function downC = [&](const std::string& u) { + double c = c_node[u]; + for (const auto& v : children[u]) + c += downC(v); + result.downstream_c_ff[u] = c; + return c; + }; + downC(driver); + + // Elmore: T(u) = T(parent) + R(parent→u) * C_down(u) + // Units: R in Ohm, C in fF → τ = R*C = Ohm*fE-15 F = 1e-15 s = 0.001 ps + // Actually: 1 Ohm * 1 fF = 1e-15 s = 0.001 ps + // So delay_ps = R_ohm * C_ff * 0.001 + constexpr double OHM_FF_TO_PS = 0.001; + + std::function assign = + [&](const std::string& u, double t_ps) { + result.delay_ps[u] = t_ps; + for (const auto& v : children[u]) { + double edge_r = parent_r[v]; + double add = edge_r * result.downstream_c_ff[v] * OHM_FF_TO_PS; + assign(v, t_ps + add); + } + }; + assign(driver, 0.0); + + return result; +} + +std::string formatElmore(const ElmoreResult& r) { + std::ostringstream oss; + oss << std::fixed; + oss.precision(4); + oss << "Elmore delay (driver=" << r.driver_node << ")\n"; + oss << " node delay_ps C_down_fF\n"; + for (const auto& kv : r.delay_ps) { + double cd = 0.0; + auto it = r.downstream_c_ff.find(kv.first); + if (it != r.downstream_c_ff.end()) + cd = it->second; + oss << " " << kv.first; + if (kv.first.size() < 28) + oss << std::string(28 - kv.first.size(), ' '); + oss << " " << kv.second << " " << cd << "\n"; + } + return oss.str(); +} + +} // namespace rcx diff --git a/RC Extraction/src/extract.cpp b/RC Extraction/src/extract.cpp new file mode 100644 index 0000000..da2c250 --- /dev/null +++ b/RC Extraction/src/extract.cpp @@ -0,0 +1,62 @@ +#include "extract.hpp" + +#include +#include + +namespace rcx { + +ExtractionResult extractAll(Layout layout) { + ExtractionResult r; + r.nets = extractConnectivity(layout); + r.resistors = extractResistance(layout); + r.capacitors = extractCapacitance(layout); + r.network = buildRCNetwork(layout, r.resistors, r.capacitors); + r.spef = writeSpef(r.network, layout.name); + r.layout = std::move(layout); + return r; +} + +std::string reportExtraction(const ExtractionResult& r) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(4); + oss << "=== RC Extraction Report: " << r.layout.name << " ===\n\n"; + oss << summarizeConnectivity(r.layout, r.nets) << "\n"; + + oss << "Resistance elements (" << r.resistors.size() << "):\n"; + for (const auto& res : r.resistors) { + oss << " " << res.name << " [" << res.kind << "] " << res.value_ohm + << " Ohm"; + if (res.kind == "wire" && res.meta.count("squares")) + oss << " (" << res.meta.at("squares") << " squares, L=" + << res.meta.at("L") << " W=" << res.meta.at("W") << ")"; + oss << "\n"; + } + oss << "\n"; + + double c_area = 0, c_fringe = 0, c_coup = 0; + oss << "Capacitance elements (" << r.capacitors.size() << "):\n"; + for (const auto& c : r.capacitors) { + oss << " " << c.name << " [" << c.kind << "] " << c.value_ff << " fF"; + if (c.kind == "coupling") + oss << " (" << c.net_pos << " <-> " << c.net_neg << ")"; + oss << "\n"; + if (c.kind == "area") + c_area += c.value_ff; + else if (c.kind == "fringe") + c_fringe += c.value_ff; + else + c_coup += c.value_ff; + } + oss << " totals: area=" << c_area << " fringe=" << c_fringe + << " coupling=" << c_coup << " fF\n\n"; + + oss << "RC network: " << r.network.nodes.size() << " nodes, " + << r.network.resistors.size() << " R, " << r.network.capacitors.size() + << " C\n"; + for (const auto& kv : r.network.net_total_c_ff) + oss << " *D_NET " << kv.first << " total_C=" << kv.second << " fF\n"; + + return oss.str(); +} + +} // namespace rcx diff --git a/RC Extraction/src/layout.cpp b/RC Extraction/src/layout.cpp new file mode 100644 index 0000000..086da55 --- /dev/null +++ b/RC Extraction/src/layout.cpp @@ -0,0 +1,84 @@ +#include "layout.hpp" + +#include +#include +#include +#include + +namespace rcx { +namespace { + +std::string trim(const std::string& s) { + std::size_t b = 0; + while (b < s.size() && std::isspace(static_cast(s[b]))) + ++b; + std::size_t e = s.size(); + while (e > b && std::isspace(static_cast(s[e - 1]))) + --e; + return s.substr(b, e - b); +} + +} // namespace + +Layout loadLayout(const std::string& path) { + std::ifstream in(path); + if (!in) + throw std::runtime_error("Cannot open layout: " + path); + + Layout layout; + layout.name = "unnamed"; + std::string line; + int lineno = 0; + while (std::getline(in, line)) { + ++lineno; + line = trim(line); + if (line.empty() || line[0] == '#') + continue; + + std::istringstream ss(line); + std::string kw; + ss >> kw; + + if (kw == "NAME") { + ss >> layout.name; + } else if (kw == "METAL" || kw == "VIA") { + Shape s; + ss >> s.layer >> s.net >> s.name; + double x0, y0, x1, y1; + if (!(ss >> x0 >> y0 >> x1 >> y1)) + throw std::runtime_error("Bad " + kw + " at line " + + std::to_string(lineno)); + s.rect = Rect(x0, y0, x1, y1); + layout.shapes.push_back(s); + } else if (kw == "PIN") { + Pin p; + ss >> p.name >> p.net >> p.layer >> p.direction >> p.x >> p.y; + if (!ss) + throw std::runtime_error("Bad PIN at line " + std::to_string(lineno)); + layout.pins.push_back(p); + } else { + throw std::runtime_error("Unknown keyword '" + kw + "' at line " + + std::to_string(lineno)); + } + } + return layout; +} + +void saveLayout(const Layout& layout, const std::string& path) { + std::ofstream out(path); + if (!out) + throw std::runtime_error("Cannot write layout: " + path); + out << "NAME " << layout.name << "\n"; + for (const auto& s : layout.shapes) { + out << (s.isVia() ? "VIA" : "METAL") << " " << s.layer << " " + << (s.net.empty() ? "-" : s.net) << " " + << (s.name.empty() ? "-" : s.name) << " " << s.rect.x0 << " " + << s.rect.y0 << " " << s.rect.x1 << " " << s.rect.y1 << "\n"; + } + for (const auto& p : layout.pins) { + out << "PIN " << p.name << " " << p.net << " " << p.layer << " " + << p.direction << " " << p.x << " " << p.y << "\n"; + } +} + +} // namespace rcx diff --git a/RC Extraction/src/main.cpp b/RC Extraction/src/main.cpp new file mode 100644 index 0000000..11970b3 --- /dev/null +++ b/RC Extraction/src/main.cpp @@ -0,0 +1,87 @@ +#include "extract.hpp" + +#include +#include +#include +#include + +static void usage(const char* argv0) { + std::cerr + << "Usage: " << argv0 << " [--spef out.spef] [--elmore NET DRIVER_PIN]\n" + << "\n" + << "Educational RC extractor (GDS-like layout → R/C → SPEF → Elmore).\n" + << "\n" + << "Layout format (.lay):\n" + << " NAME design\n" + << " METAL \n" + << " VIA \n" + << " PIN \n"; +} + +int main(int argc, char** argv) { + if (argc < 2) { + usage(argv[0]); + return 1; + } + + std::string layout_path = argv[1]; + std::string spef_path; + std::string elmore_net, elmore_drv; + bool do_elmore = false; + + for (int i = 2; i < argc; ++i) { + std::string a = argv[i]; + if (a == "--spef" && i + 1 < argc) { + spef_path = argv[++i]; + } else if (a == "--elmore" && i + 2 < argc) { + do_elmore = true; + elmore_net = argv[++i]; + elmore_drv = argv[++i]; + } else if (a == "-h" || a == "--help") { + usage(argv[0]); + return 0; + } else { + std::cerr << "Unknown arg: " << a << "\n"; + usage(argv[0]); + return 1; + } + } + + try { + rcx::Layout layout = rcx::loadLayout(layout_path); + rcx::ExtractionResult result = rcx::extractAll(std::move(layout)); + + std::cout << rcx::reportExtraction(result) << "\n"; + + if (!spef_path.empty()) { + rcx::writeSpefFile(result.network, result.layout.name, spef_path); + std::cout << "Wrote SPEF: " << spef_path << "\n"; + } else { + std::cout << "----- SPEF -----\n" << result.spef; + } + + if (do_elmore) { + rcx::ElmoreResult er = + rcx::elmoreDelay(result.network, elmore_net, elmore_drv); + std::cout << "\n" << rcx::formatElmore(er); + } else { + // Auto-run Elmore for first output pin if possible + for (const auto& pin : result.layout.pins) { + if (pin.direction == "O") { + try { + auto er = + rcx::elmoreDelay(result.network, pin.net, pin.name); + std::cout << "\n(auto) "; + std::cout << rcx::formatElmore(er); + } catch (...) { + } + break; + } + } + } + } catch (const std::exception& ex) { + std::cerr << "error: " << ex.what() << "\n"; + return 1; + } + return 0; +} diff --git a/RC Extraction/src/rc_network.cpp b/RC Extraction/src/rc_network.cpp new file mode 100644 index 0000000..35c836d --- /dev/null +++ b/RC Extraction/src/rc_network.cpp @@ -0,0 +1,281 @@ +#include "rc_network.hpp" + +#include +#include +#include +#include +#include + +namespace rcx { +namespace { + +class UnionFindStr { + public: + std::string find(const std::string& x) { + if (!parent_.count(x)) + parent_[x] = x; + if (parent_[x] != x) + parent_[x] = find(parent_[x]); + return parent_[x]; + } + + void unite(const std::string& a, const std::string& b) { + std::string ra = find(a), rb = find(b); + if (ra == rb) + return; + if (ra.size() <= rb.size()) + parent_[rb] = ra; + else + parent_[ra] = rb; + } + + private: + std::map parent_; +}; + +std::string shapeTag(const Shape& s, std::size_t i) { + if (s.name.empty() || s.name == "-") + return "s" + std::to_string(i); + return s.name; +} + +void wireEnds(const Shape& s, double& ax, double& ay, double& bx, double& by) { + if (s.rect.isHorizontal()) { + ax = s.rect.x0; + ay = s.rect.cy(); + bx = s.rect.x1; + by = s.rect.cy(); + } else { + ax = s.rect.cx(); + ay = s.rect.y0; + bx = s.rect.cx(); + by = s.rect.y1; + } +} + +std::string endNodeA(const std::string& net, const std::string& tag, + const Shape& s) { + return net + ":" + tag + (s.rect.isHorizontal() ? "_W" : "_S"); +} + +std::string endNodeB(const std::string& net, const std::string& tag, + const Shape& s) { + return net + ":" + tag + (s.rect.isHorizontal() ? "_E" : "_N"); +} + +std::string midNode(const std::string& net, const std::string& tag) { + return net + ":" + tag + "_MID"; +} + +std::string nearestWireNode(double x, double y, const Shape& m, + const std::string& mA, const std::string& mB, + const std::string& mM) { + double ax, ay, bx, by; + wireEnds(m, ax, ay, bx, by); + double dA = std::hypot(x - ax, y - ay); + double dB = std::hypot(x - bx, y - by); + double dM = std::hypot(x - m.rect.cx(), y - m.rect.cy()); + if (dA <= dB && dA <= dM) + return mA; + if (dB <= dM) + return mB; + return mM; +} + +} // namespace + +RCNetwork buildRCNetwork(const Layout& layout, + const std::vector& resistors, + const std::vector& capacitors) { + RCNetwork net; + UnionFindStr uf; + + struct PendingR { + std::string name, net, a, b, kind; + double ohms; + }; + std::vector pending; + + // π-model: wire R → R/2 — MID — R/2 + for (const auto& r : resistors) { + uf.find(r.node_a); + uf.find(r.node_b); + if (r.kind == "wire") { + auto pos = r.node_a.find(':'); + std::string netname = r.node_a.substr(0, pos); + std::string rest = r.node_a.substr(pos + 1); + auto us = rest.find_last_of('_'); + std::string tag = rest.substr(0, us); + std::string mid = midNode(netname, tag); + uf.find(mid); + pending.push_back( + {r.name + "_1", r.net, r.node_a, mid, "wire", r.value_ohm * 0.5}); + pending.push_back( + {r.name + "_2", r.net, mid, r.node_b, "wire", r.value_ohm * 0.5}); + } else { + pending.push_back( + {r.name, r.net, r.node_a, r.node_b, r.kind, r.value_ohm}); + } + } + + for (const auto& c : capacitors) { + uf.find(c.node_pos); + if (c.kind == "coupling") + uf.find(c.node_neg); + } + + const auto metals = layout.metalIndices(); + + // Merge abutting metal endpoints on the same net/layer + for (std::size_t ii = 0; ii < metals.size(); ++ii) { + std::size_t i = metals[ii]; + const Shape& a = layout.shapes[i]; + const std::string ta = shapeTag(a, i); + double aax, aay, abx, aby; + wireEnds(a, aax, aay, abx, aby); + std::string aA = endNodeA(a.net, ta, a); + std::string aB = endNodeB(a.net, ta, a); + std::string aM = midNode(a.net, ta); + + for (std::size_t jj = ii + 1; jj < metals.size(); ++jj) { + std::size_t j = metals[jj]; + const Shape& b = layout.shapes[j]; + if (a.net != b.net || a.layer != b.layer) + continue; + if (!a.rect.expanded(1e-9).overlaps(b.rect)) + continue; + + const std::string tb = shapeTag(b, j); + double bax, bay, bbx, bby; + wireEnds(b, bax, bay, bbx, bby); + std::string bA = endNodeA(b.net, tb, b); + std::string bB = endNodeB(b.net, tb, b); + std::string bM = midNode(b.net, tb); + + auto mergeIfInside = [&](const std::string& node, double x, double y, + const Shape& other, const std::string& oA, + const std::string& oB, const std::string& oM) { + if (other.rect.contains(x, y)) + uf.unite(node, nearestWireNode(x, y, other, oA, oB, oM)); + }; + + mergeIfInside(aA, aax, aay, b, bA, bB, bM); + mergeIfInside(aB, abx, aby, b, bA, bB, bM); + mergeIfInside(bA, bax, bay, a, aA, aB, aM); + mergeIfInside(bB, bbx, bby, a, aA, aB, aM); + } + } + + // Via terminals → overlapping metals + for (std::size_t vi : layout.viaIndices()) { + const Shape& via = layout.shapes[vi]; + const std::string tag = shapeTag(via, vi); + const std::string bot = via.net + ":" + tag + "_bot"; + const std::string top = via.net + ":" + tag + "_top"; + auto vit = layout.tech.vias.find(via.layer); + if (vit == layout.tech.vias.end()) + continue; + const ViaLayer& vd = vit->second; + const double vx = via.rect.cx(), vy = via.rect.cy(); + + for (std::size_t mi : metals) { + const Shape& m = layout.shapes[mi]; + if (m.net != via.net || !via.rect.overlaps(m.rect)) + continue; + const std::string mt = shapeTag(m, mi); + std::string mA = endNodeA(m.net, mt, m); + std::string mB = endNodeB(m.net, mt, m); + std::string mM = midNode(m.net, mt); + std::string attach = nearestWireNode(vx, vy, m, mA, mB, mM); + if (m.layer == vd.lower_metal) + uf.unite(bot, attach); + if (m.layer == vd.upper_metal) + uf.unite(top, attach); + } + } + + // Pins → containing metal + for (const auto& pin : layout.pins) { + std::string pin_node = pin.net + ":PIN:" + pin.name; + uf.find(pin_node); + for (std::size_t mi : metals) { + const Shape& m = layout.shapes[mi]; + if (m.net != pin.net || m.layer != pin.layer) + continue; + if (!m.rect.contains(pin.x, pin.y)) + continue; + const std::string mt = shapeTag(m, mi); + std::string mA = endNodeA(m.net, mt, m); + std::string mB = endNodeB(m.net, mt, m); + std::string mM = midNode(m.net, mt); + uf.unite(pin_node, nearestWireNode(pin.x, pin.y, m, mA, mB, mM)); + break; + } + } + + auto canon = [&](const std::string& n) { return uf.find(n); }; + + std::set seen; + auto addNode = [&](const std::string& raw, const std::string& netname) { + std::string c = canon(raw); + if (seen.count(c)) + return; + seen.insert(c); + RCNode node; + node.name = c; + node.net = netname; + net.nodes[c] = node; + }; + + for (const auto& pr : pending) { + addNode(pr.a, pr.net); + addNode(pr.b, pr.net); + std::string ca = canon(pr.a), cb = canon(pr.b); + if (ca == cb) + continue; + RCEdgeR e; + e.name = pr.name; + e.net = pr.net; + e.node_a = ca; + e.node_b = cb; + e.value_ohm = pr.ohms; + e.kind = pr.kind; + net.resistors.push_back(e); + } + + for (const auto& pin : layout.pins) { + std::string pin_node = pin.net + ":PIN:" + pin.name; + std::string c = canon(pin_node); + addNode(pin_node, pin.net); + net.nodes[c].is_pin = true; + net.nodes[c].pin_name = pin.name; + net.nodes[c].pin_dir = pin.direction; + } + + for (const auto& c : capacitors) { + RCCap out; + out.name = c.name; + out.kind = c.kind; + out.value_ff = c.value_ff; + out.net_pos = c.net_pos; + out.node_pos = canon(c.node_pos); + addNode(c.node_pos, c.net_pos); + + if (c.kind == "coupling") { + out.net_neg = c.net_neg; + out.node_neg = canon(c.node_neg); + addNode(c.node_neg, c.net_neg); + net.net_total_c_ff[c.net_pos] += c.value_ff; + net.net_total_c_ff[c.net_neg] += c.value_ff; + } else { + out.net_neg = "GROUND"; + out.node_neg = "GROUND"; + net.net_total_c_ff[c.net_pos] += c.value_ff; + } + net.capacitors.push_back(out); + } + + return net; +} + +} // namespace rcx diff --git a/RC Extraction/src/resistance.cpp b/RC Extraction/src/resistance.cpp new file mode 100644 index 0000000..4e6fa1a --- /dev/null +++ b/RC Extraction/src/resistance.cpp @@ -0,0 +1,71 @@ +#include "resistance.hpp" + +#include +#include +#include + +namespace rcx { + +double wireResistance(const Shape& shape, const TechFile& tech) { + const MetalLayer& layer = tech.metal(shape.layer); + auto lw = shape.rect.lengthWidth(); + const double length = lw.first; + const double width = lw.second; + if (width <= 0.0) + throw std::runtime_error("Zero width wire on " + shape.layer); + return layer.sheet_r * (length / width); +} + +double viaResistance(const Shape& shape, const TechFile& tech) { + const ViaLayer& vdef = tech.via(shape.layer); + int nx = std::max(1, static_cast(shape.rect.width() / vdef.size + 1e-9)); + int ny = std::max(1, static_cast(shape.rect.height() / vdef.size + 1e-9)); + int n = std::max(1, nx * ny); + return vdef.resistance / static_cast(n); +} + +std::vector extractResistance(const Layout& layout) { + std::vector out; + out.reserve(layout.shapes.size()); + + for (std::size_t i = 0; i < layout.shapes.size(); ++i) { + const Shape& shape = layout.shapes[i]; + const std::string net = + shape.net.empty() ? ("unknown_" + std::to_string(i)) : shape.net; + const std::string tag = + shape.name.empty() || shape.name == "-" ? ("s" + std::to_string(i)) + : shape.name; + + Resistor r; + r.net = net; + + if (shape.isVia()) { + r.name = "Rv_" + tag; + r.node_a = net + ":" + tag + "_bot"; + r.node_b = net + ":" + tag + "_top"; + r.value_ohm = viaResistance(shape, layout.tech); + r.kind = "via"; + r.meta["shape_index"] = static_cast(i); + } else { + r.name = "Rw_" + tag; + if (shape.rect.isHorizontal()) { + r.node_a = net + ":" + tag + "_W"; + r.node_b = net + ":" + tag + "_E"; + } else { + r.node_a = net + ":" + tag + "_S"; + r.node_b = net + ":" + tag + "_N"; + } + r.value_ohm = wireResistance(shape, layout.tech); + r.kind = "wire"; + auto lw = shape.rect.lengthWidth(); + r.meta["L"] = lw.first; + r.meta["W"] = lw.second; + r.meta["squares"] = lw.first / lw.second; + r.meta["shape_index"] = static_cast(i); + } + out.push_back(r); + } + return out; +} + +} // namespace rcx diff --git a/RC Extraction/src/spef_writer.cpp b/RC Extraction/src/spef_writer.cpp new file mode 100644 index 0000000..014a7d9 --- /dev/null +++ b/RC Extraction/src/spef_writer.cpp @@ -0,0 +1,116 @@ +#include "spef_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace rcx { + +std::string writeSpef(const RCNetwork& net, const std::string& design_name) { + std::ostringstream out; + out << std::fixed << std::setprecision(6); + + out << "*SPEF \"IEEE 1481-1998\"\n"; + out << "*DESIGN \"" << design_name << "\"\n"; + out << "*DATE \"N/A\"\n"; + out << "*VENDOR \"RC-Extraction-Teaching\"\n"; + out << "*PROGRAM \"rcx_extract\"\n"; + out << "*VERSION \"1.0\"\n"; + out << "*DESIGN_FLOW \"NETLIST_TYPE_VERILOG\"\n"; + out << "*DIVIDER /\n"; + out << "*DELIMITER :\n"; + out << "*BUS_DELIMITER [ ]\n"; + out << "*T_UNIT 1 PS\n"; + out << "*C_UNIT 1 FF\n"; + out << "*R_UNIT 1 OHM\n"; + out << "*L_UNIT 1 HENRY\n\n"; + + // Collect nets + std::set nets; + for (const auto& kv : net.nodes) + if (!kv.second.net.empty()) + nets.insert(kv.second.net); + + // Name map (*NAME_MAP) — optional; use expanded names for clarity + out << "*NAME_MAP\n"; + int nid = 1; + std::map name_id; + for (const auto& n : nets) { + name_id[n] = nid; + out << "*" << nid << " " << n << "\n"; + ++nid; + } + out << "\n"; + + for (const auto& netname : nets) { + double total_c = 0.0; + auto itc = net.net_total_c_ff.find(netname); + if (itc != net.net_total_c_ff.end()) + total_c = itc->second; + + out << "*D_NET " << netname << " " << total_c << "\n"; + + // Connections (pins) + out << "*CONN\n"; + bool any_pin = false; + for (const auto& kv : net.nodes) { + const RCNode& n = kv.second; + if (n.net != netname || !n.is_pin) + continue; + any_pin = true; + // *I + out << "*I " << n.pin_name << " " << n.pin_dir << "\n"; + } + if (!any_pin) + out << "*P " << netname << "_orphan B\n"; + + // Caps + out << "*CAP\n"; + int cid = 1; + for (const auto& c : net.capacitors) { + if (c.kind == "coupling") { + if (c.net_pos != netname && c.net_neg != netname) + continue; + // Emit coupling once, from the lexicographically smaller net + if (c.net_pos != netname) + continue; + if (c.net_pos > c.net_neg) + continue; + out << cid++ << " " << c.node_pos << " " << c.node_neg << " " + << c.value_ff << "\n"; + } else { + if (c.net_pos != netname) + continue; + out << cid++ << " " << c.node_pos << " " << c.value_ff << "\n"; + } + } + + // Resistors + out << "*RES\n"; + int rid = 1; + for (const auto& r : net.resistors) { + if (r.net != netname) + continue; + out << rid++ << " " << r.node_a << " " << r.node_b << " " << r.value_ohm + << "\n"; + } + + out << "*END\n\n"; + } + + return out.str(); +} + +void writeSpefFile(const RCNetwork& net, const std::string& design_name, + const std::string& path) { + std::ofstream f(path); + if (!f) + throw std::runtime_error("Cannot write SPEF: " + path); + f << writeSpef(net, design_name); +} + +} // namespace rcx diff --git a/RC Extraction/src/techfile.cpp b/RC Extraction/src/techfile.cpp new file mode 100644 index 0000000..d97fa49 --- /dev/null +++ b/RC Extraction/src/techfile.cpp @@ -0,0 +1,60 @@ +#include "techfile.hpp" + +namespace rcx { + +TechFile defaultTech() { + TechFile tech; + tech.name = "toy_3metal"; + + // Synthetic stack roughly inspired by older ~65–90 nm interconnect feel. + // Numbers are educational — not a real PDK. + MetalLayer m1; + m1.name = "M1"; + m1.thickness = 0.20; + m1.height = 0.40; + m1.sheet_r = 0.080; + m1.c_area = 0.086; + m1.c_fringe = 0.045; + m1.c_coup_k = 0.035; + tech.addMetal(m1); + + MetalLayer m2; + m2.name = "M2"; + m2.thickness = 0.25; + m2.height = 0.80; + m2.sheet_r = 0.060; + m2.c_area = 0.043; + m2.c_fringe = 0.050; + m2.c_coup_k = 0.035; + tech.addMetal(m2); + + MetalLayer m3; + m3.name = "M3"; + m3.thickness = 0.35; + m3.height = 1.30; + m3.sheet_r = 0.040; + m3.c_area = 0.027; + m3.c_fringe = 0.055; + m3.c_coup_k = 0.035; + tech.addMetal(m3); + + ViaLayer v1; + v1.name = "VIA1"; + v1.lower_metal = "M1"; + v1.upper_metal = "M2"; + v1.resistance = 5.0; + v1.size = 0.14; + tech.addVia(v1); + + ViaLayer v2; + v2.name = "VIA2"; + v2.lower_metal = "M2"; + v2.upper_metal = "M3"; + v2.resistance = 4.0; + v2.size = 0.14; + tech.addVia(v2); + + return tech; +} + +} // namespace rcx diff --git a/RC Extraction/tests/test_rc.cpp b/RC Extraction/tests/test_rc.cpp new file mode 100644 index 0000000..0e631e8 --- /dev/null +++ b/RC Extraction/tests/test_rc.cpp @@ -0,0 +1,119 @@ +#include "extract.hpp" + +#include +#include +#include + +static int g_fail = 0; + +static void expect(bool cond, const std::string& msg) { + if (!cond) { + std::cerr << "FAIL: " << msg << "\n"; + ++g_fail; + } else { + std::cout << "ok : " << msg << "\n"; + } +} + +static void test_sheet_resistance() { + rcx::TechFile tech = rcx::defaultTech(); + rcx::Shape s; + s.layer = "M1"; + s.net = "n"; + s.name = "w"; + // 10 µm long, 1 µm wide → 10 squares → R = 0.08 * 10 = 0.8 Ohm + s.rect = rcx::Rect(0, 0, 10, 1); + double r = rcx::wireResistance(s, tech); + expect(std::fabs(r - 0.8) < 1e-9, "M1 wire 10x1 → 0.8 Ohm"); +} + +static void test_via_parallel() { + rcx::TechFile tech = rcx::defaultTech(); + rcx::Shape s; + s.layer = "VIA1"; + s.net = "n"; + s.name = "v"; + // 0.28 x 0.14 → 2 cuts → R = 5/2 = 2.5 + s.rect = rcx::Rect(0, 0, 0.28, 0.14); + double r = rcx::viaResistance(s, tech); + expect(std::fabs(r - 2.5) < 1e-9, "VIA1 2-cut array → 2.5 Ohm"); +} + +static void test_connectivity_via_stitch() { + rcx::Layout L; + L.name = "t"; + rcx::Shape m1; + m1.layer = "M1"; + m1.net = "a"; + m1.name = "m1"; + m1.rect = rcx::Rect(0, 0, 2, 0.2); + + rcx::Shape m2; + m2.layer = "M2"; + m2.net = "a"; + m2.name = "m2"; + m2.rect = rcx::Rect(0.8, 0, 2.8, 0.2); + + rcx::Shape v; + v.layer = "VIA1"; + v.net = "a"; + v.name = "v1"; + v.rect = rcx::Rect(0.93, 0.03, 1.07, 0.17); + + L.shapes = {m1, m2, v}; + auto nets = rcx::extractConnectivity(L); + expect(nets.size() == 1, "via stitches M1+M2 into one net"); + expect(L.shapes[0].net == L.shapes[1].net, "both metals same net name"); +} + +static void test_coupling_nonzero() { + rcx::Layout L; + L.name = "c"; + rcx::Shape a; + a.layer = "M1"; + a.net = "n1"; + a.name = "w1"; + a.rect = rcx::Rect(0, 0, 5, 0.2); + rcx::Shape b; + b.layer = "M1"; + b.net = "n2"; + b.name = "w2"; + b.rect = rcx::Rect(0, 0.4, 5, 0.6); // gap = 0.2 µm, overlap L = 5 + L.shapes = {a, b}; + rcx::extractConnectivity(L); + auto caps = rcx::extractCapacitance(L); + int cc = 0; + double cc_val = 0; + for (const auto& c : caps) { + if (c.kind == "coupling") { + ++cc; + cc_val = c.value_ff; + } + } + expect(cc == 1, "one coupling cap between parallel wires"); + // C = 0.035 * 0.20 * 5 / 0.2 = 0.175 fF + expect(std::fabs(cc_val - 0.175) < 1e-9, "coupling formula 0.175 fF"); +} + +static void test_full_pipeline_simple() { + rcx::Layout L = rcx::loadLayout("examples/simple_net.lay"); + auto r = rcx::extractAll(std::move(L)); + expect(!r.resistors.empty(), "simple_net has resistors"); + expect(!r.capacitors.empty(), "simple_net has capacitors"); + expect(!r.spef.empty(), "SPEF emitted"); + expect(r.spef.find("*D_NET") != std::string::npos, "SPEF has *D_NET"); +} + +int main() { + test_sheet_resistance(); + test_via_parallel(); + test_connectivity_via_stitch(); + test_coupling_nonzero(); + test_full_pipeline_simple(); + if (g_fail) { + std::cerr << g_fail << " test(s) failed\n"; + return 1; + } + std::cout << "All tests passed.\n"; + return 0; +} diff --git a/README.md b/README.md index 4dcec55..760e139 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ Steps-- 2. [**Latch Clustering for Optimal PPA**](https://dl.acm.org/doi/abs/10.5555/3437539.3437769) and [**My Slides.**](https://github.com/sethupathib/Physical-Design-Algorithms-Implementation/blob/main/Register%20Clustering/Latch%20Clustering.pdf) 3. [**GPU Accelerated STA**](http://yibolin.com/publications/papers/TIMER_ICCAD2020_Guo.pdf) and [**My Slides.**](https://github.com/sethupathib/Physical-Design-Algorithms-Implementation/tree/main/GPU%20-%20STA) +## [**PD Job Acceleration (tmpfs + rsync)**](./PD%20Job%20Acceleration) + +Helpers to cut PD I/O wait: **Mode B** (default) keeps DB + fat logs on disk and only puts small `tmp`/`TMPDIR` in RAM; **Mode A** stages a full workspace into tmpfs when it fits (logs still stay on disk by default). License-free demos included. + +White paper: [`PD Job Acceleration/WHITEPAPER.md`](./PD%20Job%20Acceleration/WHITEPAPER.md) · [`PDF`](./PD%20Job%20Acceleration/docs/pd_tmpfs_rsync_whitepaper.pdf) + P.S -- Prof. Yao Wen thinks that these are classical problems. I think it's good for me to start with these since I am very new to EDA and its implementation. Also, this project is still under development. It turned out to be a lot harder than I thought. (It will take time for me to fully implement this). I need to know about Line Sweeps and some Geometry Algorithms. diff --git a/gpu_metal_fill/.gitignore b/gpu_metal_fill/.gitignore new file mode 100644 index 0000000..f0a60b4 --- /dev/null +++ b/gpu_metal_fill/.gitignore @@ -0,0 +1,4 @@ +build/ +*.o +*.gds +*.ppm diff --git a/gpu_metal_fill/Makefile b/gpu_metal_fill/Makefile new file mode 100644 index 0000000..1c20490 --- /dev/null +++ b/gpu_metal_fill/Makefile @@ -0,0 +1,67 @@ +# Metal Fill (FEOL/BEOL) — recursive-partitioned dummy fill. +# +# Build is CPU (OpenMP-parallel across partitions). The compute-heavy stages are +# isolated behind FillBackend (see include/metalfill/backend.hpp) so a CUDA +# backend can be dropped in later (GPU offload is future work). + +CXX ?= g++ +OPT ?= -O2 +CXXFLAGS ?= -std=c++17 $(OPT) -Wall -Wextra -Iinclude +LDFLAGS ?= + +# OpenMP (parallel partitions / fill). Disable with `make OPENMP=0`. +OPENMP ?= 1 +ifeq ($(OPENMP),1) + CXXFLAGS += -fopenmp + LDFLAGS += -fopenmp +endif + +BUILD := build +LIB_SRC := $(wildcard src/*.cpp) +LIB_OBJ := $(patsubst src/%.cpp,$(BUILD)/%.o,$(LIB_SRC)) + +TOOLS := $(BUILD)/make_dummy_gds $(BUILD)/make_gpu_block $(BUILD)/run_fill $(BUILD)/render_layer \ + $(BUILD)/gdsinfo +TESTS := $(BUILD)/test_main + +.PHONY: all tools test demo clean +all: tools +tools: $(TOOLS) + +$(BUILD): + mkdir -p $(BUILD) + +$(BUILD)/%.o: src/%.cpp | $(BUILD) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +$(BUILD)/make_dummy_gds: tools/make_dummy_gds.cpp $(LIB_OBJ) | $(BUILD) + $(CXX) $(CXXFLAGS) $< $(LIB_OBJ) -o $@ $(LDFLAGS) + +$(BUILD)/make_gpu_block: tools/make_gpu_block.cpp $(LIB_OBJ) | $(BUILD) + $(CXX) $(CXXFLAGS) $< $(LIB_OBJ) -o $@ $(LDFLAGS) + +$(BUILD)/run_fill: tools/run_fill.cpp $(LIB_OBJ) | $(BUILD) + $(CXX) $(CXXFLAGS) $< $(LIB_OBJ) -o $@ $(LDFLAGS) + +$(BUILD)/render_layer: tools/render_layer.cpp $(LIB_OBJ) | $(BUILD) + $(CXX) $(CXXFLAGS) $< $(LIB_OBJ) -o $@ $(LDFLAGS) + +$(BUILD)/gdsinfo: tools/gdsinfo.cpp $(LIB_OBJ) | $(BUILD) + $(CXX) $(CXXFLAGS) $< $(LIB_OBJ) -o $@ $(LDFLAGS) + +$(BUILD)/test_main: tests/test_main.cpp $(LIB_OBJ) | $(BUILD) + $(CXX) $(CXXFLAGS) $< $(LIB_OBJ) -o $@ $(LDFLAGS) + +test: $(TESTS) + $(BUILD)/test_main + +demo: tools + $(BUILD)/make_dummy_gds -o $(BUILD)/dummy.gds + $(BUILD)/run_fill -i $(BUILD)/dummy.gds -o $(BUILD)/filled.gds -r $(BUILD)/report.txt + +gpu-demo: tools + $(BUILD)/make_gpu_block -o $(BUILD)/gpu_block.gds + $(BUILD)/run_fill -i $(BUILD)/gpu_block.gds -o $(BUILD)/gpu_filled.gds -r $(BUILD)/gpu_report.txt + +clean: + rm -rf $(BUILD) diff --git a/gpu_metal_fill/README.md b/gpu_metal_fill/README.md new file mode 100644 index 0000000..61ff050 --- /dev/null +++ b/gpu_metal_fill/README.md @@ -0,0 +1,111 @@ +# Metal Fill (FEOL / BEOL) — recursive-partitioned dummy fill + +> For a thorough design write-up (motivation, every algorithm, the partitioning +> correctness argument, bugs fixed, and results) see **[WHITEPAPER.md](WHITEPAPER.md)** +> (also available as a PDF: [docs/metal_fill_whitepaper.pdf](docs/metal_fill_whitepaper.pdf)). + +A small, dependency-free C++17 engine that inserts **dummy metal fill** into a +GDSII layout so that every layer meets CMP density rules. It processes the die by +**recursive quadtree partitioning** (like commercial fill flows) so leaves can be +filled independently and in parallel, then merged. + +> Status: CPU implementation (OpenMP-parallel across partitions). The heavy +> stages are isolated behind a `FillBackend` interface so a CUDA/GPU backend can +> be added later. GPU offload is intentionally future work. + +## Why dummy fill? + +Chemical-Mechanical Polishing (CMP) needs a **uniform** metal density across the +die. Sparse regions dish/erode differently from dense regions, hurting yield and +timing. Foundries therefore require, per layer and per density window: + +- a **minimum** density (add fill where too sparse), +- a **maximum** density (don't over-fill), +- a bounded **window-to-window gradient** (no abrupt steps), +- **spacing** from existing geometry (a keep-out halo), and a min fill area. + +## Pipeline + +For each layer (FEOL base: `OD`, `PO`; BEOL metals: `M1..M14`): + +1. **Rasterize** existing geometry into an occupancy grid. +2. **Density** via a summed-area table (integral image): O(1) per window. +3. **Keep-out** = morphological dilation of geometry by the spacing rule. +4. **Fill** on a pitch lattice, per window, up to a per-window target density, + never exceeding max and never entering the keep-out halo. +5. **Iterative DRC**: measure achieved density, then raise the target of any + window that is still below `min` or too far below a denser neighbor + (gradient), and refill. Targets increase monotonically, so it converges. + +### Recursive partitioning (the scaling trick) + +The die is split by a **quadtree** into leaf partitions that are filled +independently (OpenMP now, GPU later) and then merged by concatenation. + +Correctness at partition boundaries is guaranteed by two things: + +- **Lattice-aligned cuts**: partitions are cut on a lattice equal to + `lcm(density_window, fill_pitch)`, so a density window and every fill position + is identical whether computed globally or per-partition. +- **Halo / guard band**: each leaf reads a ring of neighboring geometry as + read-only context (so density and keep-out are exact at the edge) but only + *emits* fill inside its **core**. Cores are disjoint and tile the die, so + merging is a plain concatenation — no de-duplication or stitching. + +A unit test (`partition == global`) asserts the merged result is bit-for-bit +equivalent to a single-shot global fill. + +## Build & run + +```sh +make # builds tools into build/ +make test # builds and runs the unit tests +make demo # simple synthetic layout -> filled.gds + report.txt +make gpu-demo # realistic GPU-block layout -> gpu_filled.gds + gpu_report.txt + +# manual +build/make_gpu_block -o build/gpu_block.gds # synthetic GPU block +build/run_fill -i build/gpu_block.gds -o build/gpu_filled.gds -r build/report.txt +build/render_layer -i build/gpu_filled.gds -o build/m1.ppm -l 10 # visualize M1 +# convert the PPM to PNG if you like: ffmpeg -i build/m1.ppm build/m1.png + +# confirm fill happened: per-(layer,datatype) polygon counts. Fill = datatype 10. +build/gdsinfo build/gpu_block.gds build/gpu_filled.gds +``` + +### The GPU-block test design + +`make_gpu_block` generates a synthetic — but structurally realistic — GPU block +(it is **not** a real design, just the floorplan-level density structure): + +- an `NxN` grid of SM (streaming-multiprocessor) tiles, each with two SRAM + macros (register file + shared memory) and a standard-cell logic region, +- routing channels between tiles (bus routing on mid metals), +- global clock/spine routes (M9/M10), and +- a regular, sparse power grid on the top metals (M11..M14). + +Each layer therefore has a distinct density profile — dense macros/logic on the +lower layers, near-empty upper layers with only power straps — so fill has varied +work on every layer. + +`make OPENMP=0` builds a serial version. + +## Layout of the code + +| File | Responsibility | +|------|----------------| +| `include/metalfill/geometry.hpp` | points, bbox, polygons (integer dbu) | +| `gdsii.{hpp,cpp}` | minimal GDSII reader/writer (BOUNDARY/BOX) | +| `raster.{hpp,cpp}` | polygon scanline rasterization | +| `density.{hpp,cpp}` | summed-area table + windowed density | +| `fill.{hpp,cpp}` | keep-out dilation + per-window fill placement | +| `partition.{hpp,cpp}` | recursive quadtree partitioner | +| `drc.{hpp,cpp}` | density / gradient / spacing / min-area checks | +| `backend.hpp`, `backend_cpu.cpp` | compute backend (CPU/OpenMP; CUDA later) | +| `engine.{hpp,cpp}` | orchestration: partition → fill → iterate → merge | +| `layermap.{hpp,cpp}` | default GDS layers + fill rules | +| `tools/` | `make_dummy_gds`, `make_gpu_block`, `run_fill`, `render_layer`, `gdsinfo` | +| `tests/test_main.cpp` | unit + end-to-end tests | + +The default layer map and rules are illustrative, **not** a real PDK; edit +`layermap.cpp` to match your technology. diff --git a/gpu_metal_fill/WHITEPAPER.md b/gpu_metal_fill/WHITEPAPER.md new file mode 100644 index 0000000..e6cceb2 --- /dev/null +++ b/gpu_metal_fill/WHITEPAPER.md @@ -0,0 +1,600 @@ +# A Recursive-Partitioned Metal Fill Engine for FEOL/BEOL Dummy Fill + +**A design white paper for the `gpu_metal_fill` project** + +--- + +## Table of contents + +1. [Background: why dummy fill exists](#1-background-why-dummy-fill-exists) +2. [Problem statement and requirements](#2-problem-statement-and-requirements) +3. [System architecture](#3-system-architecture) +4. [Data model and GDSII I/O](#4-data-model-and-gdsii-io) +5. [Rasterization](#5-rasterization) +6. [Density analysis with summed-area tables](#6-density-analysis-with-summed-area-tables) +7. [Keep-out (spacing) as morphological dilation](#7-keep-out-spacing-as-morphological-dilation) +8. [Fill placement](#8-fill-placement) +9. [Recursive partitioning and merge](#9-recursive-partitioning-and-merge) +10. [Gradient-aware, DRC-driven iterative fill](#10-gradient-aware-drc-driven-iterative-fill) +11. [Design-rule checking](#11-design-rule-checking) +12. [Parallelism and the GPU-ready backend](#12-parallelism-and-the-gpu-ready-backend) +13. [The synthetic GPU-block test design](#13-the-synthetic-gpu-block-test-design) +14. [Bugs encountered and how they were fixed](#14-bugs-encountered-and-how-they-were-fixed) +15. [Results](#15-results) +16. [Limitations and future work](#16-limitations-and-future-work) +17. [File-by-file reference](#17-file-by-file-reference) + +--- + +## 1. Background: why dummy fill exists + +Modern chips are manufactured layer by layer. After each metal layer is +deposited, the wafer is planarized by **Chemical-Mechanical Polishing (CMP)**. +CMP does not remove material uniformly: regions with a **high** metal density +polish differently from **sparse** regions. Two failure modes dominate: + +- **Dishing** — wide metal features get polished *below* the target height. +- **Erosion** — in dense arrays of fine features, both metal and the surrounding + dielectric erode. + +The result is **thickness variation** across the die, which changes wire +resistance/capacitance (hurting timing), can open or short layers, and lowers +yield. To keep CMP uniform, foundries impose **density rules** per layer, checked +over a sliding **window** (e.g. 20 µm × 20 µm): + +- a **minimum** density (typically ~20–30 %), +- a **maximum** density (typically ~70–85 %), and +- a bounded **window-to-window gradient** (no abrupt steps). + +Designs rarely satisfy the *minimum* everywhere, so tools insert non-functional +**dummy fill** (a.k.a. metal fill) — small floating shapes in the empty space — +until each window meets its target. Fill must stay a legal **spacing** away from +real geometry (a *keep-out halo*) and each fill shape must meet a **minimum +area**. + +- **FEOL** (Front-End-Of-Line) = transistor layers (active/`OD`, poly/`PO`). + Fill here is for pattern-density and mechanical-stress uniformity. +- **BEOL** (Back-End-Of-Line) = the interconnect metal stack (`M1..M14`) and + vias. This is where most fill volume lives, and where fill runtime dominates — + which is exactly why this project focuses on making BEOL fill **fast** via + partitioning (and, later, GPU offload). + +This project implements a self-contained, dependency-free engine that performs +this fill for both FEOL base layers and a 14-layer BEOL metal stack. + +--- + +## 2. Problem statement and requirements + +> **Input:** a GDSII layout with base layers and metal layers `M1..M14`. +> **Output:** the same layout plus dummy fill so every layer meets its CMP +> density rules, with a DRC report. + +Concretely the engine must, per layer: + +1. Measure existing metal **density** over a sliding window. +2. Know the **layer map** (which GDS layer/datatype is which) and the per-layer + **fill rules**. +3. **Place fill** to satisfy min density without violating max density, gradient, + spacing, or min-area. +4. Run **iterative DRC**: measure, fix, repeat. + +An explicit non-functional requirement drove the whole architecture: **BEOL fill +is slow, so it must be parallelizable.** The chosen mechanism is **recursive +spatial partitioning** (divide the die, fill pieces independently, merge) — the +same idea commercial tools use — with a clean path to GPU offload later. + +--- + +## 3. System architecture + +The engine is a small C++17 library plus a few command-line tools. It has **no +third-party dependencies** (only the standard library; OpenMP is optional). + +```mermaid +flowchart TD + GDS[GDSII in] --> RD[read_gds] + RD --> ENG[engine: run_fill] + subgraph perlayer[per layer] + PART[recursive quadtree partition] --> LEAF + subgraph LEAF[per leaf - parallel] + RAS[rasterize halo] --> KO[keep-out dilation] + KO --> FILL[place fill in core] + end + LEAF --> MERGE[merge core fills] + MERGE --> ITER{DRC ok? / converged?} + ITER -- no --> PART + end + ENG --> WR[write_gds] --> OUT[GDSII out] + ENG --> REP[text report] +``` + +The compute-heavy stages (density, keep-out, fill) are hidden behind a +`FillBackend` interface (`include/metalfill/backend.hpp`) so they can run on the +CPU today (OpenMP) or a GPU tomorrow (CUDA) without touching the orchestration. + +**Coordinate convention.** Everything internal is in integer **database units +(dbu)**. The GDS `UNITS` record is written as `1 dbu = 1 nm` (`meters_per_dbu = +1e-9`) with `1 user unit = 1 µm`, so `1 µm = 1000 dbu`. Rules are authored in +microns and converted to dbu at runtime. + +--- + +## 4. Data model and GDSII I/O + +### Geometry (`geometry.hpp`) + +- `Point{dbu x,y}`, `BBox` with `expand()/width()/height()/valid()`. +- `Polygon{int layer, datatype; vector pts}` with `bbox()`. +- `make_rect(layer, datatype, x0,y0,x1,y1)` — the workhorse, since fill shapes + and the synthetic designs are rectangles. + +### GDSII reader/writer (`gdsii.{hpp,cpp}`) + +GDSII is a record-based **big-endian** binary format. Each record is +`[2-byte length][1-byte record-type][1-byte data-type][payload]`. The reader is a +straightforward record loop that imports `BOUNDARY` (and `BOX`) elements as +polygons and ignores references/paths; the writer emits a single top cell. + +Two subtleties are handled explicitly: + +- **8-byte GDS reals** (used by the `UNITS` record) are *not* IEEE-754. They are + `sign(1 bit) · mantissa/2^56 · 16^(exponent-64)`. `put_real8`/`get_real8` + implement the conversion. +- **Closing vertex**: GDS boundaries repeat the first point as the last; the + reader drops the duplicate, the writer re-adds it. + +The in-memory `Layout` holds the library/cell name, the `UNITS`, and the polygon +list, and exposes `dbu_per_um()` and `bbox()`. + +Because this is a from-scratch reader/writer, a **round-trip unit test** +(`test_gds_roundtrip`) guards it: write a layout, read it back, and check that +polygon count, layer/datatype, coordinates, and units are preserved. + +--- + +## 5. Rasterization + +Density and spacing are far cheaper to reason about on a **raster** than on +polygons, so each layer is rendered into a boolean **occupancy grid**. + +### The grid (`raster.hpp`) + +`Grid` is a dense `nx × ny` array of `uint8_t` (0/1) with a cell size `cell` +(dbu) and an origin `(ox, oy)`. Cell `(ix,iy)` covers +`[ox+ix·cell, ox+(ix+1)·cell) × [oy+iy·cell, oy+(iy+1)·cell)`. + +`make_grid(area, cell)` allocates a grid that covers `area` using **exact ceil +sizing** (`nx = ceil(width/cell)`). (An earlier version added a `+1` guard cell; +see [§14](#14-bugs-encountered-and-how-they-were-fixed) for why it was removed.) + +### Scanline polygon fill (`rasterize`) + +For each polygon, for each grid **row**, the scanline is taken at the row's +vertical **center** `yc`. We collect the x-coordinates where polygon edges cross +`yc`, sort them, and fill the spans between consecutive crossing pairs +(even-odd rule). A cell is set if its **center** lies inside a span: + +``` +cx0 = ceil ((xl - ox)/cell - 0.5) +cx1 = floor((xr - ox)/cell - 0.5) +``` + +This is exact for axis-aligned rectangles (all shapes here) and robust for +arbitrary simple polygons. `test_rasterize_rect` checks a 500×500 dbu rectangle +at 100-dbu cells produces exactly 25 occupied cells. + +The choice of `cell` matters and is derived per layer — see +[§8](#8-fill-placement). + +--- + +## 6. Density analysis with summed-area tables + +CMP density is evaluated over many overlapping windows, so a naïve per-window sum +would be `O(window_area)` each. Instead we build a **summed-area table (SAT)**, +a.k.a. integral image, once, and answer any window in `O(1)`. + +### The SAT (`density.{hpp,cpp}`) + +For occupancy `occ[x,y] ∈ {0,1}`, the SAT is + +``` +SAT[y+1][x+1] = occ[x,y] + SAT[y][x+1] + SAT[y+1][x] − SAT[y][x] +``` + +The occupied area of any half-open cell rectangle `[x0,x1) × [y0,y1)` is then + +``` +area = SAT[y1][x1] − SAT[y0][x1] − SAT[y1][x0] + SAT[y0][x0] +``` + +`build_sat` computes it with a single running-row pass (cache friendly), and +`SummedAreaTable::area()` clamps to bounds and returns the four-corner +difference. + +### The density map (`compute_density`) + +A `DensityMap` is produced by sliding a `win_cells` window with a `step_cells` +stride. Each `Window` records its tile index, its cell range, and its +`density = occupied_area / window_area`. `min/mean/max_density()` summarize a +layer. `test_sat_density` verifies both the SAT arithmetic and a 2×2 tiling on a +hand-checked grid. + +Why the SAT matters here: it is the *same* primitive used for keep-out dilation +([§7](#7-keep-out-spacing-as-morphological-dilation)) and it maps cleanly onto a +GPU (prefix sums + constant-time box lookups), which is the intended offload. + +--- + +## 7. Keep-out (spacing) as morphological dilation + +Fill must stay `keepout` microns away from any real geometry. Equivalently, the +**forbidden region** for fill is the existing geometry **dilated** by the keep-out +radius. Rather than a per-cell neighborhood scan, this is computed in `O(1)` per +cell with the SAT: a cell `(ix,iy)` is *blocked* iff the box +`[ix−r, ix+r] × [iy−r, iy+r]` contains any occupied cell, where `r = +ceil(keepout/cell)`: + +``` +blocked[ix,iy] = (SAT.area(ix−r, iy−r, ix+r+1, iy+r+1) > 0) +``` + +This is `compute_keepout` in `fill.cpp`. `test_keepout` checks that `r=1` blocks +the 8-neighborhood of a single occupied cell, that a distance-2 cell is free, +and that `r=0` reduces to the occupancy itself. + +--- + +## 8. Fill placement + +### Deriving the grid resolution and pitch (per layer) + +`compute_geom` (in `engine.cpp`) converts a layer's rules to dbu and picks a grid +`cell` equal to the **gcd** of the fill width/height, pitch, window and step. +That guarantees each of those lengths is an **integer number of cells**, so +window boundaries and the fill lattice are exact (no rounding drift). Fill shapes +are integer multiples of the cell. + +### Placement objective (`place_fill`) + +Fill candidates sit on a **pitch lattice** anchored at the grid origin. For each +window (tile) the algorithm: + +1. reads the existing occupied cells via the SAT, +2. computes a **target** occupied-cell count and a **max** cap, and +3. walks the candidate lattice inside the tile, stamping a fill shape wherever + its footprint is entirely free of the keep-out mask and of already-placed + fill, stopping once the window reaches its target or the max cap. + +Two invariants make this correct and parallel-safe: + +- **Footprints stay inside their tile** (`cx + fw ≤ tile_end`), so each tile + writes a disjoint region of the fill grid → the per-tile loop is parallelized + with OpenMP with no data races. +- **Never exceed max**: placement stops before crossing `max_density · area`. + +Fill-to-fill spacing is guaranteed *by construction*: the pitch is `1.25×` the +fill size, so shapes on the lattice never touch. `test_place_fill` verifies fill +reaches the target, respects the max cap, and places nothing where fully blocked. + +The per-window target is not a single scalar; it comes from a **target map** +built by the DRC-driven loop in [§10](#10-gradient-aware-drc-driven-iterative-fill). + +--- + +## 9. Recursive partitioning and merge + +This is the core of the project and the reason it can scale. + +### The idea + +Fill in one region of the die is *almost* independent of another. So we split the +die with a **quadtree** into leaf partitions, fill each leaf independently (in +parallel now, on a GPU later), and **merge**. `partition_recursive` +(`partition.{hpp,cpp}`) recurses in integer lattice-units, splitting into four +children until a leaf is `≤ max_leaf` units per side (or a depth cap is hit). + +```mermaid +flowchart TD + A["die (all windows)"] --> B1[quadrant NW] + A --> B2[quadrant NE] + A --> B3[quadrant SW] + A --> B4[quadrant SE] + B1 --> C1[leaf] + B1 --> C2[leaf] + B2 --> D[... recurse until <= max_leaf ...] +``` + +### The catch: boundaries + +A naïve cut breaks two things at partition edges: + +1. **Density windows** that straddle a cut would see only half their geometry. +2. **Spacing** (fill-to-geometry and fill-to-fill) across the cut. + +If you ignore this, you get exactly the failure the author hit first: fill placed +on top of geometry and windows blowing past the max-density limit +([§14](#14-bugs-encountered-and-how-they-were-fixed)). + +### The fix: lattice-aligned cuts + halo, emit-in-core + +Two mechanisms guarantee a partitioned fill equals a global fill: + +- **Lattice-aligned cuts.** Partition boundaries are placed only on a lattice + equal to `align = lcm(density_window, fill_pitch)` (per axis). Because a cut + lands on both a window boundary *and* a pitch line, every density window lies + entirely inside one leaf, and the fill lattice is continuous across leaves. + The working area is snapped **outward** to this lattice (`snap_area`) and tiled + exactly, so leaf-local window/pitch indexing coincides with the global one. + +- **Halo (guard band) + emit-in-core.** Each leaf is grown by a `margin` (a + multiple of `align`, at least `keepout + fill_size`) into a **halo**. The leaf + rasterizes *all* geometry touching its halo as **read-only context**, so + density and keep-out are exact right up to the core edge — but it only + **emits** fill inside its **core**. Cores are disjoint and tile the die. + +```mermaid +flowchart LR + subgraph Halo + direction TB + subgraph Core["core (emit fill here)"] + x[" "] + end + end + N["neighbor geometry (read-only context in halo)"] -.-> Halo +``` + +### Why merge is trivial + +Because cores are **disjoint** and every fill shape is emitted fully inside its +core, the merge is a **plain concatenation** of each leaf's fill — no +de-duplication, no boundary stitching. Fill-to-fill spacing across a boundary is +automatic because both leaves place on the *same global pitch lattice*. + +### Verification + +`test_partition_equals_global` runs the engine twice on the same layout: once +finely partitioned (`max_leaf=1`) and once as a single partition +(`max_leaf=100000`), and asserts identical fill-shape counts and identical +post-fill min/mean density. `test_partition_cover` independently checks that the +leaf cores are lattice-aligned, mutually disjoint, and **exactly tile** the die +(by summed area), and that each halo contains its core. + +--- + +## 10. Gradient-aware, DRC-driven iterative fill + +Filling every window just to the minimum leaves steep steps next to naturally +dense regions (e.g. an SRAM macro), violating the **gradient** rule. A robust +fill must raise sparse windows *toward* their dense neighbors. This is done with +a per-window **target map** updated by DRC feedback (in `run_fill`): + +1. Initialize every window's target to `min_density + fill_headroom`. +2. **Fill** with the current target map (partitioned, parallel). +3. **Measure** the achieved density map. +4. **Update targets** for each window: + - if below `min`, keep at least the base target; + - for each neighbor `nb`, if `density(nb) − density(here) > max_gradient`, + raise this window's target to `density(nb) − max_gradient + grad_headroom`; + - clamp to `max_density`. +5. If any target increased, **refill**; else stop. + +Targets increase **monotonically** and are bounded by `max_density`, so the loop +converges (default cap `max_iterations = 3`, enough in practice). + +The small `grad_headroom` (default 0.02) exists because placement stops *just +below* a target, leaving the achieved density one fill-quantum short; without the +overshoot, a window can sit a hair under the gradient limit (e.g. measured +`0.1501 > 0.15`). Adding the headroom makes the achieved density clear the limit. + +This directly implements the "iterative DRC" step: **fill → check → raise targets +where DRC fails → refill**. Where a violation is *physically unfixable* (a very +dense macro edge with no room to fill the neighbor high enough), the loop +converges with a residual that DRC honestly reports. + +--- + +## 11. Design-rule checking + +`run_drc` (`drc.{hpp,cpp}`) checks the merged result over the **whole die** — +independently of how it was partitioned, which doubles as a cross-check on the +merge. It reports five `ViolationType`s: + +| Check | How | +|-------|-----| +| `MinDensity` | window density `< min` | +| `MaxDensity` | window density `> max` | +| `Gradient` | `|density − neighbor|` (right/down) `> max_gradient` | +| `Spacing` | any fill cell that lies inside the keep-out halo | +| `MinArea` | fill-shape area `< min_area` (shape-level static check) | + +The spacing check recomputes the keep-out from the *existing* geometry and +intersects it with the fill footprints; it must be zero by construction, so it is +a strong safety net. `format_report` renders a per-layer table (partitions, +existing/fill counts, iterations, before→after density, DRC count, time) plus a +detailed violation list. + +--- + +## 12. Parallelism and the GPU-ready backend + +The four compute kernels — `build_sat`, `compute_density`, `compute_keepout`, +`place_fill` — are declared on the `FillBackend` interface. `CpuBackend` +(`backend_cpu.cpp`) implements them by delegating to the reference kernels, which +are **OpenMP-parallel** where the work is independent: + +- `place_fill` parallelizes over tiles (disjoint writes), and +- the engine parallelizes over **partitions** (`#pragma omp parallel for` in + `partitioned_fill`), which is the primary source of speedup. + +Every kernel was chosen because it maps naturally onto a GPU: + +- `build_sat` → parallel prefix sums, +- `compute_density`/`compute_keepout` → constant-time box lookups / a box filter, +- `place_fill` → per-window independent stamping. + +A CUDA backend can therefore be added as a second `FillBackend` implementation +and selected via `EngineConfig::prefer_gpu` with **no change** to the engine. +That work is deliberately deferred; `make_cuda_backend()` currently returns +`nullptr`. + +--- + +## 13. The synthetic GPU-block test design + +Testing needs realistic input. `make_gpu_block` (`tools/make_gpu_block.cpp`) +generates a structurally realistic — but not real — GPU block: + +- an `N×N` grid of **SM (streaming-multiprocessor) tiles**, each containing two + **SRAM macros** (register file + shared memory, dense bitcell-like stripes on + `OD/PO/M1..M3`) and a **standard-cell logic** region (cell rows on `OD/PO`, + local routing on `M1..M3`, density varying per tile via a deterministic PRNG); +- **routing channels** between tiles carrying bus routing on `M4..M8`; +- **global clock/spine** routes on `M9/M10`; +- a regular, sparse **power grid** (wide straps) on `M11..M14`. + +The effect is that every layer has a *different* density profile — dense on the +lower layers, nearly empty on the upper layers except for power straps — so fill +does meaningful, layer-specific work everywhere. A simpler generator +(`make_dummy_gds`) produces a controlled per-window density pattern used by the +tests. + +--- + +## 14. Bugs encountered and how they were fixed + +The project was built and validated incrementally; the important defects and +their fixes are worth recording. + +### 14.1 Lattice-alignment break at partition boundaries (critical) + +**Symptom:** after adding partitioning, the demo produced many `MaxDensity` +(windows at 0.98 vs a 0.80 limit) and `Spacing` violations — fill was landing on +top of geometry. + +**Root cause:** each leaf's core was clamped to the *raw geometry bounding box*, +whose edges are **not** on the window/pitch lattice. That offset the leaf-local +density windows from the global windows by a sub-window amount, so `place_fill`'s +per-window accounting used one set of windows while DRC used another. In a leaf +where a window appeared nearly empty (because it was misaligned), fill was piled +in on top of real geometry. + +**Fix:** snap the working area **outward to the `align = lcm(window, pitch)` +lattice** (`snap_area`) and tile it exactly; anchor partitions and both the +per-leaf and global grids at the same lattice origin. After this, leaf-local +windows coincide with global windows and the caps apply to the right windows. +Verified by `test_partition_equals_global`. + +### 14.2 Rule inconsistency: pitch too coarse to reach min density + +**Symptom:** `M14` (and other upper metals) could not reach the 0.30 min density. + +**Root cause:** the initial rules used `pitch = 2 × fill`, which caps fill +coverage at `(fill/pitch)² = 25 %` — physically below the 30 % minimum. + +**Fix:** set `pitch = 1.25 × fill` (and a divisor of the window), giving ~64 % +achievable coverage with headroom, while keeping the lattice coherent. + +### 14.3 Gradient handling + +**Symptom:** ~200 `Gradient` violations because filling only to `min` left steep +steps next to dense windows. + +**Fix:** the per-window, DRC-driven target loop of +[§10](#10-gradient-aware-drc-driven-iterative-fill), plus a small `grad_headroom` +to beat placement discretization (`0.1501 > 0.15` boundary cases). + +### 14.4 `make_grid` over-allocation + +An early `make_grid` added a `+1` guard cell, which created a thin extra window at +the far edge and could produce spurious edge-window density readings. Because the +area is now snapped outward to the lattice, exact `ceil` sizing covers everything +and the tiling divides evenly. + +### 14.5 Macro-edge gradient in the GPU block + +**Symptom:** the first GPU block left a few residual `Gradient` violations at +SRAM macro edges — a very dense macro window next to logic that fill physically +cannot raise enough to smooth. + +**Resolution:** this is a *real* limitation of any fill flow, not a tool bug. For +a clean demonstration the synthetic macro densities (test-data parameters only) +were tapered slightly so the steps are fillable. On real designs such residuals +are expected and are honestly reported by DRC. + +--- + +## 15. Results + +All 77 unit/end-to-end checks pass (`make test`), in both the OpenMP and serial +(`OPENMP=0`) builds. + +**Simple synthetic design** (`make demo`, 100 × 97 µm, 16 layers): +400 existing polygons → **186,236 fill shapes, 0 DRC violations**. OpenMP across +partitions runs in ~0.87 s vs ~1.99 s serial (~**2.3×**). + +**GPU block** (`make gpu-demo`, 180 × 180 µm, 9 SM tiles): +15,885 existing polygons → **~1.39 M fill shapes, 0 DRC violations**, 52 +partitions per layer, ~4 s. Every layer is brought in-band: metals reach the 0.30 +min (after-min ≥ 0.32) without exceeding max or violating gradient/spacing. + +The `render_layer` tool visualizes any layer (existing = navy, fill = orange, +partition cores = red); the power-grid layers (`M11..M14`) make the keep-out +halos around the wide straps clearly visible. + +--- + +## 16. Limitations and future work + +- **GPU offload** — the whole point of the backend abstraction. A CUDA + implementation of the four kernels is the natural next step. +- **Via fill** between metal layers is not implemented. +- **Multi-patterning coloring** (LELE/SADP mask assignment) for advanced nodes. +- **Grounded/tied fill** (vs floating) and coupling-capacitance-aware fill for + timing-critical nets. +- **Non-rectilinear fill / staggered patterns** and technology-specific fill + cells; the current fill is a rectangular lattice. +- **Hierarchy** — the reader flattens to a single cell; real flows fill + hierarchically and reuse fill across instances. +- **Real PDK rules** — the layer map and rules in `layermap.cpp` are illustrative + placeholders, not a real technology. +- **Fill distribution** — placement fills a window greedily; a more uniform + spatial distribution within a window can further reduce local gradients. + +--- + +## 17. File-by-file reference + +| File | Responsibility | +|------|----------------| +| `include/metalfill/geometry.hpp` | points, bbox, polygons, `make_rect` (all in dbu) | +| `gdsii.{hpp,cpp}` | GDSII reader/writer, 8-byte real codec | +| `raster.{hpp,cpp}` | occupancy grid, scanline rasterization, `stamp_rect` | +| `density.{hpp,cpp}` | summed-area table, windowed density map | +| `fill.{hpp,cpp}` | keep-out dilation, per-window fill placement | +| `partition.{hpp,cpp}` | recursive quadtree partitioner, `gcd`/`lcm` | +| `drc.{hpp,cpp}` | density / gradient / spacing / min-area checks | +| `rules.hpp`, `layermap.{hpp,cpp}` | fill-rule struct + default layer map | +| `backend.hpp`, `backend_cpu.cpp` | compute backend interface + CPU/OpenMP impl | +| `engine.{hpp,cpp}` | orchestration: geom, snap, partition, iterate, merge, report | +| `tools/make_dummy_gds.cpp` | controlled synthetic layout for tests | +| `tools/make_gpu_block.cpp` | realistic synthetic GPU block | +| `tools/run_fill.cpp` | CLI: GDS in → filled GDS + report | +| `tools/render_layer.cpp` | render a layer (existing/fill/partitions) to PPM | +| `tests/test_main.cpp` | 77 unit + end-to-end checks | + +### Key tunables (`EngineConfig`, `FillRule`) + +- `max_iterations`, `max_leaf` (partition leaf size), `fill_headroom`, + `grad_headroom`, `prefer_gpu`. +- Per layer: `window_um`, `step_um`, `min/max_density`, `max_gradient`, + `fill_w/h_um`, `fill_pitch_x/y_um`, `keepout_um`, `min_area_um2`, `is_beol`. + +--- + +*This engine is a compact, verifiable reference implementation of the density → +partition → fill → iterative-DRC flow. Its structure — lattice-aligned recursive +partitioning with halo context and an emit-in-core merge, behind a GPU-ready +backend — is exactly what is needed to scale BEOL fill to large dies and, later, +to a GPU.* diff --git a/gpu_metal_fill/docs/metal_fill_whitepaper.pdf b/gpu_metal_fill/docs/metal_fill_whitepaper.pdf new file mode 100644 index 0000000..4744f53 Binary files /dev/null and b/gpu_metal_fill/docs/metal_fill_whitepaper.pdf differ diff --git a/gpu_metal_fill/include/metalfill/backend.hpp b/gpu_metal_fill/include/metalfill/backend.hpp new file mode 100644 index 0000000..693b9ac --- /dev/null +++ b/gpu_metal_fill/include/metalfill/backend.hpp @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include "metalfill/density.hpp" +#include "metalfill/fill.hpp" +#include "metalfill/raster.hpp" + +namespace mf { + +// The compute-heavy stages of metal fill are isolated behind this interface so +// they can be executed on the CPU (OpenMP) or offloaded to the GPU (CUDA). +// +// Every stage here is data-parallel and maps naturally onto the GPU: +// * build_sat -> parallel prefix sums +// * compute_density-> box lookups over the SAT +// * compute_keepout-> morphological dilation (box filter + threshold) +// * place_fill -> per-window independent stamping +class FillBackend { +public: + virtual ~FillBackend() = default; + virtual std::string name() const = 0; + + virtual SummedAreaTable build_sat(const Grid& grid) = 0; + virtual DensityMap compute_density(const SummedAreaTable& sat, int win_cells, + int step_cells) = 0; + virtual Grid compute_keepout(const Grid& occ, int keepout_cells) = 0; + virtual std::vector place_fill(const Grid& occ, const Grid& blocked, + const FillParams& params, + Grid& fill_occ) = 0; +}; + +// The default CPU backend (parallelized with OpenMP when available). +std::unique_ptr make_cpu_backend(); + +// Returns a CUDA backend when the library is built with USE_CUDA and a device +// is present; otherwise returns nullptr. +std::unique_ptr make_cuda_backend(); + +// Selects the CUDA backend if requested and available, else the CPU backend. +std::unique_ptr make_backend(bool prefer_gpu); + +} // namespace mf diff --git a/gpu_metal_fill/include/metalfill/density.hpp b/gpu_metal_fill/include/metalfill/density.hpp new file mode 100644 index 0000000..b578c12 --- /dev/null +++ b/gpu_metal_fill/include/metalfill/density.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include +#include "metalfill/raster.hpp" + +namespace mf { + +// Summed-area table (integral image) over a boolean occupancy grid. Allows O(1) +// area queries for any axis-aligned window and is the core primitive that maps +// cleanly onto a GPU (prefix sums + box lookups). +struct SummedAreaTable { + int nx = 0; + int ny = 0; + std::vector sat; // (ny+1) x (nx+1), row-major + + // Occupied-cell count in the half-open cell range [x0,x1) x [y0,y1). + int64_t area(int x0, int y0, int x1, int y1) const; +}; + +SummedAreaTable build_sat(const Grid& grid); + +// A single density evaluation window. +struct Window { + int ix = 0, iy = 0; // tile index + int x0 = 0, y0 = 0; // cell range [x0,x1) x [y0,y1) + int x1 = 0, y1 = 0; + double density = 0.0; // occupied_area / window_area +}; + +// A grid of density windows produced by sliding a `win_cells` window with +// `step_cells` stride over the occupancy grid. +struct DensityMap { + int ntiles_x = 0; + int ntiles_y = 0; + std::vector tiles; // row-major (iy*ntiles_x + ix) + + const Window& at(int ix, int iy) const { return tiles[static_cast(iy) * ntiles_x + ix]; } + double min_density() const; + double max_density() const; + double mean_density() const; +}; + +// Computes the density map from a SAT. win_cells/step_cells are in grid cells. +DensityMap compute_density(const SummedAreaTable& sat, int win_cells, int step_cells); + +} // namespace mf diff --git a/gpu_metal_fill/include/metalfill/drc.hpp b/gpu_metal_fill/include/metalfill/drc.hpp new file mode 100644 index 0000000..e647add --- /dev/null +++ b/gpu_metal_fill/include/metalfill/drc.hpp @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include "metalfill/density.hpp" +#include "metalfill/raster.hpp" +#include "metalfill/rules.hpp" + +namespace mf { + +enum class ViolationType { + MinDensity, + MaxDensity, + Gradient, + Spacing, + MinArea, +}; + +struct Violation { + ViolationType type; + int ix = 0, iy = 0; // offending tile (or -1 for shape-level checks) + double value = 0.0; // measured value + double limit = 0.0; // rule limit + std::string message; +}; + +struct DrcReport { + std::vector violations; + bool clean() const { return violations.empty(); } + int count(ViolationType t) const; +}; + +// Runs density (min/max), gradient, keep-out spacing and min-area checks over +// the combined (existing + fill) layout for one layer. +// total_occ : existing geometry OR fill footprints (post-fill occupancy) +// blocked : keep-out mask (existing geometry dilated by keepout) +// fill_occ : fill footprints only (for spacing check) +DrcReport run_drc(const SummedAreaTable& total_sat, const Grid& blocked, + const Grid& fill_occ, int win_cells, int step_cells, + const FillRule& rule, double dbu_per_um, dbu cell); + +} // namespace mf diff --git a/gpu_metal_fill/include/metalfill/engine.hpp b/gpu_metal_fill/include/metalfill/engine.hpp new file mode 100644 index 0000000..dded791 --- /dev/null +++ b/gpu_metal_fill/include/metalfill/engine.hpp @@ -0,0 +1,53 @@ +#pragma once +#include +#include +#include "metalfill/backend.hpp" +#include "metalfill/drc.hpp" +#include "metalfill/gdsii.hpp" +#include "metalfill/rules.hpp" + +namespace mf { + +// Per-layer result of the fill run. +struct LayerResult { + std::string name; + int layer = 0; + bool is_beol = false; + int existing_polys = 0; + int fill_shapes = 0; + int partitions = 0; + double density_before_min = 0, density_before_mean = 0, density_before_max = 0; + double density_after_min = 0, density_after_mean = 0, density_after_max = 0; + int iterations = 0; + DrcReport drc; + double seconds = 0.0; +}; + +struct EngineConfig { + // Raster resolution in database units. Smaller = more accurate + more memory. + dbu grid_cell_dbu = 0; // 0 => auto (derived from smallest fill pitch) + int max_iterations = 3; // iterative fill/DRC passes per layer + int max_leaf = 2; // partition leaf size (align-units per side) + bool prefer_gpu = false; // offload BEOL to CUDA backend if available + double fill_headroom = 0.02; // aim slightly above min_density + double grad_headroom = 0.02; // overshoot gradient target to beat discretization +}; + +struct FillSummary { + std::string backend; + std::vector layers; + double total_seconds = 0.0; + int total_violations() const; + int total_fill_shapes() const; +}; + +// Runs FEOL + BEOL fill over `layout` using `rules`. Fill polygons are appended +// to `layout` (tagged with each rule's fill_datatype). BEOL layers are processed +// through the (optionally GPU) backend. +FillSummary run_fill(Layout& layout, const std::vector& rules, + const EngineConfig& cfg); + +// Renders a human-readable text report. +std::string format_report(const FillSummary& summary); + +} // namespace mf diff --git a/gpu_metal_fill/include/metalfill/fill.hpp b/gpu_metal_fill/include/metalfill/fill.hpp new file mode 100644 index 0000000..49e1612 --- /dev/null +++ b/gpu_metal_fill/include/metalfill/fill.hpp @@ -0,0 +1,48 @@ +#pragma once +#include +#include "metalfill/geometry.hpp" +#include "metalfill/raster.hpp" + +namespace mf { + +// Result of computing the keep-out (forbidden) mask for fill placement. +// A cell is blocked if any existing geometry lies within `keepout` of it. +Grid compute_keepout(const Grid& occ, int keepout_cells); + +// A placed fill shape, expressed in grid-cell coordinates (footprint is +// [ix0, ix0+w) x [iy0, iy0+h)). +struct FillShape { + int ix0 = 0, iy0 = 0; + int w = 0, h = 0; +}; + +// Parameters controlling fill placement, all expressed in grid cells. +struct FillParams { + int fill_w = 1; + int fill_h = 1; + int pitch_x = 2; + int pitch_y = 2; + int win_cells = 1; + int step_cells = 1; + double target_density = 0.30; // fill until each window reaches this + double max_density = 0.80; // never exceed this in any window + + // Optional per-window target (gradient-aware fill). When set, a window's + // target is looked up from this global map instead of `target_density`. + // The map is indexed [gy*gnx + gx] where (gx,gy) is the *global* window + // index; a local tile (tx,ty) maps to global (goff_x+tx, goff_y+ty). + const std::vector* target_map = nullptr; + int gnx = 0; + int gny = 0; + int goff_x = 0; + int goff_y = 0; +}; + +// Places dummy fill on a grid so that every window reaches `target_density` +// where physically possible, while never exceeding `max_density` and never +// overlapping the keep-out mask. Returns the placed shapes; `fill_occ` is +// populated with the fill footprints (1 where fill was placed). +std::vector place_fill(const Grid& occ, const Grid& blocked, + const FillParams& params, Grid& fill_occ); + +} // namespace mf diff --git a/gpu_metal_fill/include/metalfill/gdsii.hpp b/gpu_metal_fill/include/metalfill/gdsii.hpp new file mode 100644 index 0000000..899e7a8 --- /dev/null +++ b/gpu_metal_fill/include/metalfill/gdsii.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include "metalfill/geometry.hpp" + +namespace mf { + +// A minimal in-memory representation of a single-cell GDSII layout. +struct Layout { + std::string lib_name = "METALFILL"; + std::string cell_name = "TOP"; + // meters per database unit (UNITS second value). 1e-9 => 1 dbu = 1 nm. + double meters_per_dbu = 1e-9; + // user units per database unit (UNITS first value). 1e-3 => 1 user unit = 1 um. + double user_units_per_dbu = 1e-3; + + std::vector polygons; + + // Database units per micron, derived from UNITS. + double dbu_per_um() const { return 1e-6 / meters_per_dbu; } + + BBox bbox() const { + BBox b; + for (const auto& p : polygons) b.expand(p.bbox()); + return b; + } +}; + +// Reads a GDSII file. Only BOUNDARY and BOX records are imported (as polygons); +// references (SREF/AREF) and paths are ignored. Throws std::runtime_error on +// malformed input. +Layout read_gds(const std::string& path); + +// Writes a single-cell GDSII file containing all polygons. +void write_gds(const std::string& path, const Layout& layout); + +} // namespace mf diff --git a/gpu_metal_fill/include/metalfill/geometry.hpp b/gpu_metal_fill/include/metalfill/geometry.hpp new file mode 100644 index 0000000..0798bb7 --- /dev/null +++ b/gpu_metal_fill/include/metalfill/geometry.hpp @@ -0,0 +1,63 @@ +#pragma once +#include +#include +#include +#include + +namespace mf { + +// All layout coordinates are stored in integer database units (dbu). +using dbu = int64_t; + +struct Point { + dbu x = 0; + dbu y = 0; +}; + +struct BBox { + dbu xmin = std::numeric_limits::max(); + dbu ymin = std::numeric_limits::max(); + dbu xmax = std::numeric_limits::min(); + dbu ymax = std::numeric_limits::min(); + + bool valid() const { return xmax >= xmin && ymax >= ymin; } + dbu width() const { return valid() ? xmax - xmin : 0; } + dbu height() const { return valid() ? ymax - ymin : 0; } + + void expand(const Point& p) { + xmin = std::min(xmin, p.x); + ymin = std::min(ymin, p.y); + xmax = std::max(xmax, p.x); + ymax = std::max(ymax, p.y); + } + void expand(const BBox& b) { + if (!b.valid()) return; + xmin = std::min(xmin, b.xmin); + ymin = std::min(ymin, b.ymin); + xmax = std::max(xmax, b.xmax); + ymax = std::max(ymax, b.ymax); + } +}; + +// A simple polygon tagged with GDSII (layer, datatype). +struct Polygon { + int layer = 0; + int datatype = 0; + std::vector pts; + + BBox bbox() const { + BBox b; + for (const auto& p : pts) b.expand(p); + return b; + } +}; + +inline Polygon make_rect(int layer, int datatype, dbu x0, dbu y0, dbu x1, dbu y1) { + Polygon p; + p.layer = layer; + p.datatype = datatype; + p.pts = {{x0, y0}, {x1, y0}, {x1, y1}, {x0, y1}}; + return p; +} + +} // namespace mf diff --git a/gpu_metal_fill/include/metalfill/layermap.hpp b/gpu_metal_fill/include/metalfill/layermap.hpp new file mode 100644 index 0000000..e566018 --- /dev/null +++ b/gpu_metal_fill/include/metalfill/layermap.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include "metalfill/rules.hpp" + +namespace mf { + +// GDS layer numbering used by this project (illustrative; not a real PDK). +// +// FEOL (base) layers: +// OD (active/diffusion) = 1 +// PO (poly) = 2 +// BEOL (metal) layers: +// M1 .. M14 = 10 .. 23 (Mn -> layer 9 + n) +// +// Fill shapes are written on the same layer number with datatype 10. +constexpr int kOdLayer = 1; +constexpr int kPoLayer = 2; +constexpr int kMetalBaseLayer = 9; // M1 = 10 +constexpr int kNumMetals = 14; // M1 .. M14 +constexpr int kFillDatatype = 10; + +inline int metal_layer(int n) { return kMetalBaseLayer + n; } // n in [1..14] + +// Builds the default layer map: 2 FEOL layers + M1..M14 BEOL layers. +// +// Lower metals use small, tight fill; upper metals use larger fill on a coarser +// pitch (mirrors real metal stacks where top layers are thicker/wider). +std::vector default_layermap(); + +} // namespace mf diff --git a/gpu_metal_fill/include/metalfill/partition.hpp b/gpu_metal_fill/include/metalfill/partition.hpp new file mode 100644 index 0000000..12dc60f --- /dev/null +++ b/gpu_metal_fill/include/metalfill/partition.hpp @@ -0,0 +1,41 @@ +#pragma once +#include +#include "metalfill/geometry.hpp" + +namespace mf { + +// One leaf of the recursive partitioning of the die. +// +// core : the region this leaf is responsible for emitting fill in. Cores of +// different leaves are disjoint and tile the die. Core edges are +// aligned to the density-window / fill-pitch lattice. +// halo : core expanded by a guard band. Geometry inside the halo is used as +// read-only context so density and spacing are correct at the core +// edge, but fill is only *emitted* inside the core. +struct PartitionRect { + BBox core; + BBox halo; + int depth = 0; +}; + +struct PartitionConfig { + dbu anchor_x = 0; // lattice origin (die xmin) + dbu anchor_y = 0; // lattice origin (die ymin) + dbu align_x = 1; // atomic cut unit in x (== lcm(window, pitch_x)) + dbu align_y = 1; // atomic cut unit in y (== lcm(window, pitch_y)) + dbu margin = 0; // halo guard band (multiple of align) + int max_leaf = 2; // stop splitting when leaf <= max_leaf align-units per side + int max_depth = 24; // hard recursion cap + BBox clip; // die bounds; cores are clamped to this for emission +}; + +// Recursively partitions `area` (the die bounds) into leaves via a quadtree. +// Splits happen only on the align lattice so windows and fill pitch stay +// coherent across boundaries. +std::vector partition_recursive(const BBox& area, const PartitionConfig& cfg); + +// Integer gcd / lcm helpers on database units. +dbu gcd_dbu(dbu a, dbu b); +dbu lcm_dbu(dbu a, dbu b); + +} // namespace mf diff --git a/gpu_metal_fill/include/metalfill/raster.hpp b/gpu_metal_fill/include/metalfill/raster.hpp new file mode 100644 index 0000000..aaa4fd1 --- /dev/null +++ b/gpu_metal_fill/include/metalfill/raster.hpp @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include "metalfill/geometry.hpp" + +namespace mf { + +// A uniform boolean occupancy grid over a rectangular area of the layout. +// cell is the edge length of one grid cell in database units. Element (ix, iy) +// covers [ox + ix*cell, ox + (ix+1)*cell) x [oy + iy*cell, oy + (iy+1)*cell). +struct Grid { + int nx = 0; + int ny = 0; + dbu cell = 1; + dbu ox = 0; + dbu oy = 0; + std::vector occ; // 0/1, row-major (iy*nx + ix) + + void resize(int nx_, int ny_, dbu cell_, dbu ox_, dbu oy_) { + nx = nx_; + ny = ny_; + cell = cell_; + ox = ox_; + oy = oy_; + occ.assign(static_cast(nx) * ny, 0); + } + inline size_t idx(int ix, int iy) const { return static_cast(iy) * nx + ix; } + inline uint8_t at(int ix, int iy) const { return occ[idx(ix, iy)]; } + inline void set(int ix, int iy, uint8_t v) { occ[idx(ix, iy)] = v; } + size_t count() const { + size_t c = 0; + for (auto v : occ) c += v; + return c; + } +}; + +// Allocates a grid covering `area` at resolution `cell` (dbu). The area is +// snapped so the origin lands on a cell boundary. +Grid make_grid(const BBox& area, dbu cell); + +// Rasterizes the given polygons (whose (layer,datatype) match) into `grid` by +// setting occupied cells to 1. Uses even-odd scanline fill; robust for +// arbitrary simple polygons, exact for axis-aligned rectangles. +void rasterize(Grid& grid, const std::vector& polys, int layer, int datatype); + +// Stamps a single axis-aligned rectangle (in dbu) into the grid. +void stamp_rect(Grid& grid, dbu x0, dbu y0, dbu x1, dbu y1, uint8_t value = 1); + +} // namespace mf diff --git a/gpu_metal_fill/include/metalfill/rules.hpp b/gpu_metal_fill/include/metalfill/rules.hpp new file mode 100644 index 0000000..f6b7e76 --- /dev/null +++ b/gpu_metal_fill/include/metalfill/rules.hpp @@ -0,0 +1,45 @@ +#pragma once +#include +#include + +namespace mf { + +// Fill rules for a single mask layer. Distances are given in microns and are +// converted to database units at runtime using the layout UNITS. +// +// A layer is identified by its GDS (layer, datatype). Fill geometry is emitted +// on the same GDS layer number but tagged with `fill_datatype` so that dummy +// fill can be distinguished (and stripped) later. +struct FillRule { + std::string name; // human readable, e.g. "M1", "OD" + int layer = 0; // GDS layer of the existing (drawn) geometry + int datatype = 0; // GDS datatype of the existing geometry + int fill_datatype = 10; // GDS datatype used to tag emitted fill shapes + + // Density window (CMP planarity is evaluated over a sliding window). + double window_um = 20.0; // square window edge length + double step_um = 20.0; // window step (== window_um => non-overlapping tiles) + + // Density targets. CMP needs the metal density to stay inside [min, max] + // and the window-to-window gradient to stay bounded. + double min_density = 0.30; + double max_density = 0.80; + double max_gradient = 0.20; // max |density(tile) - density(neighbor)|; <=0 disables + + // Fill shape geometry. + double fill_w_um = 0.4; + double fill_h_um = 0.4; + double fill_pitch_x_um = 0.8; + double fill_pitch_y_um = 0.8; + + // Design-rule spacing between fill and any existing geometry (keep-out halo). + double keepout_um = 0.15; + + // Minimum legal fill-shape area (sanity DRC on the fill shape itself). + double min_area_um2 = 0.04; + + // BEOL (metal) layers are flagged for the GPU-offloaded processing path. + bool is_beol = false; +}; + +} // namespace mf diff --git a/gpu_metal_fill/src/backend_cpu.cpp b/gpu_metal_fill/src/backend_cpu.cpp new file mode 100644 index 0000000..c88f910 --- /dev/null +++ b/gpu_metal_fill/src/backend_cpu.cpp @@ -0,0 +1,58 @@ +#include "metalfill/backend.hpp" + +#ifdef _OPENMP +#include +#include +#endif + +namespace mf { + +namespace { + +// CPU implementation of the fill backend. The heavy stages delegate to the +// reference kernels in density.cpp / fill.cpp, which are OpenMP-parallel where +// the work is independent (keep-out dilation and per-tile fill placement). +class CpuBackend : public FillBackend { +public: + std::string name() const override { +#ifdef _OPENMP + return "cpu-openmp(" + std::to_string(omp_get_max_threads()) + " threads)"; +#else + return "cpu-serial"; +#endif + } + + SummedAreaTable build_sat(const Grid& grid) override { return mf::build_sat(grid); } + + DensityMap compute_density(const SummedAreaTable& sat, int win_cells, + int step_cells) override { + return mf::compute_density(sat, win_cells, step_cells); + } + + Grid compute_keepout(const Grid& occ, int keepout_cells) override { + return mf::compute_keepout(occ, keepout_cells); + } + + std::vector place_fill(const Grid& occ, const Grid& blocked, + const FillParams& params, Grid& fill_occ) override { + return mf::place_fill(occ, blocked, params, fill_occ); + } +}; + +} // namespace + +std::unique_ptr make_cpu_backend() { return std::make_unique(); } + +std::unique_ptr make_backend(bool prefer_gpu) { + if (prefer_gpu) { + if (auto gpu = make_cuda_backend()) return gpu; + } + return make_cpu_backend(); +} + +#ifndef USE_CUDA +// When built without CUDA support the GPU backend is simply unavailable. +std::unique_ptr make_cuda_backend() { return nullptr; } +#endif + +} // namespace mf diff --git a/gpu_metal_fill/src/density.cpp b/gpu_metal_fill/src/density.cpp new file mode 100644 index 0000000..54c6483 --- /dev/null +++ b/gpu_metal_fill/src/density.cpp @@ -0,0 +1,82 @@ +#include "metalfill/density.hpp" + +#include +#include + +namespace mf { + +int64_t SummedAreaTable::area(int x0, int y0, int x1, int y1) const { + x0 = std::max(x0, 0); + y0 = std::max(y0, 0); + x1 = std::min(x1, nx); + y1 = std::min(y1, ny); + if (x1 <= x0 || y1 <= y0) return 0; + const int w = nx + 1; + auto S = [&](int x, int y) -> int64_t { return sat[static_cast(y) * w + x]; }; + return S(x1, y1) - S(x0, y1) - S(x1, y0) + S(x0, y0); +} + +SummedAreaTable build_sat(const Grid& grid) { + SummedAreaTable t; + t.nx = grid.nx; + t.ny = grid.ny; + const int w = grid.nx + 1; + const int h = grid.ny + 1; + t.sat.assign(static_cast(w) * h, 0); + for (int y = 0; y < grid.ny; ++y) { + int64_t row = 0; + for (int x = 0; x < grid.nx; ++x) { + row += grid.at(x, y); + t.sat[static_cast(y + 1) * w + (x + 1)] = + t.sat[static_cast(y) * w + (x + 1)] + row; + } + } + return t; +} + +double DensityMap::min_density() const { + double m = std::numeric_limits::max(); + for (const auto& t : tiles) m = std::min(m, t.density); + return tiles.empty() ? 0.0 : m; +} +double DensityMap::max_density() const { + double m = 0.0; + for (const auto& t : tiles) m = std::max(m, t.density); + return m; +} +double DensityMap::mean_density() const { + if (tiles.empty()) return 0.0; + double s = 0.0; + for (const auto& t : tiles) s += t.density; + return s / tiles.size(); +} + +DensityMap compute_density(const SummedAreaTable& sat, int win_cells, int step_cells) { + DensityMap dm; + win_cells = std::max(win_cells, 1); + step_cells = std::max(step_cells, 1); + // Number of tiles so that the whole grid is covered. + auto ntiles = [](int n, int step) { return std::max(1, (n + step - 1) / step); }; + dm.ntiles_x = ntiles(sat.nx, step_cells); + dm.ntiles_y = ntiles(sat.ny, step_cells); + dm.tiles.resize(static_cast(dm.ntiles_x) * dm.ntiles_y); + + for (int ty = 0; ty < dm.ntiles_y; ++ty) { + for (int tx = 0; tx < dm.ntiles_x; ++tx) { + Window win; + win.ix = tx; + win.iy = ty; + win.x0 = tx * step_cells; + win.y0 = ty * step_cells; + win.x1 = std::min(win.x0 + win_cells, sat.nx); + win.y1 = std::min(win.y0 + win_cells, sat.ny); + int64_t occ = sat.area(win.x0, win.y0, win.x1, win.y1); + int64_t cells = static_cast(win.x1 - win.x0) * (win.y1 - win.y0); + win.density = cells > 0 ? static_cast(occ) / static_cast(cells) : 0.0; + dm.tiles[static_cast(ty) * dm.ntiles_x + tx] = win; + } + } + return dm; +} + +} // namespace mf diff --git a/gpu_metal_fill/src/drc.cpp b/gpu_metal_fill/src/drc.cpp new file mode 100644 index 0000000..46da03f --- /dev/null +++ b/gpu_metal_fill/src/drc.cpp @@ -0,0 +1,97 @@ +#include "metalfill/drc.hpp" + +#include +#include + +namespace mf { + +int DrcReport::count(ViolationType t) const { + int c = 0; + for (const auto& v : violations) + if (v.type == t) ++c; + return c; +} + +DrcReport run_drc(const SummedAreaTable& total_sat, const Grid& blocked, const Grid& fill_occ, + int win_cells, int step_cells, const FillRule& rule, double dbu_per_um, + dbu cell) { + DrcReport rep; + DensityMap dm = compute_density(total_sat, win_cells, step_cells); + + // Density min / max per window. + for (const auto& w : dm.tiles) { + if (w.density < rule.min_density - 1e-9) { + std::ostringstream os; + os << rule.name << " window (" << w.ix << "," << w.iy << ") density " + << w.density << " < min " << rule.min_density; + rep.violations.push_back({ViolationType::MinDensity, w.ix, w.iy, w.density, + rule.min_density, os.str()}); + } + if (w.density > rule.max_density + 1e-9) { + std::ostringstream os; + os << rule.name << " window (" << w.ix << "," << w.iy << ") density " + << w.density << " > max " << rule.max_density; + rep.violations.push_back({ViolationType::MaxDensity, w.ix, w.iy, w.density, + rule.max_density, os.str()}); + } + } + + // Density gradient between adjacent windows. + if (rule.max_gradient > 0) { + for (int ty = 0; ty < dm.ntiles_y; ++ty) { + for (int tx = 0; tx < dm.ntiles_x; ++tx) { + double d = dm.at(tx, ty).density; + if (tx + 1 < dm.ntiles_x) { + double g = std::fabs(d - dm.at(tx + 1, ty).density); + if (g > rule.max_gradient + 1e-9) { + std::ostringstream os; + os << rule.name << " gradient " << g << " > " << rule.max_gradient + << " between (" << tx << "," << ty << ") and (" << tx + 1 << "," << ty + << ")"; + rep.violations.push_back( + {ViolationType::Gradient, tx, ty, g, rule.max_gradient, os.str()}); + } + } + if (ty + 1 < dm.ntiles_y) { + double g = std::fabs(d - dm.at(tx, ty + 1).density); + if (g > rule.max_gradient + 1e-9) { + std::ostringstream os; + os << rule.name << " gradient " << g << " > " << rule.max_gradient + << " between (" << tx << "," << ty << ") and (" << tx << "," << ty + 1 + << ")"; + rep.violations.push_back( + {ViolationType::Gradient, tx, ty, g, rule.max_gradient, os.str()}); + } + } + } + } + } + + // Fill-to-geometry spacing: fill must never land inside the keep-out halo. + if (fill_occ.nx == blocked.nx && fill_occ.ny == blocked.ny) { + int spacing_hits = 0; + for (size_t i = 0; i < fill_occ.occ.size(); ++i) + if (fill_occ.occ[i] && blocked.occ[i]) ++spacing_hits; + if (spacing_hits > 0) { + std::ostringstream os; + os << rule.name << " fill overlaps keep-out halo in " << spacing_hits << " cells"; + rep.violations.push_back( + {ViolationType::Spacing, -1, -1, double(spacing_hits), 0.0, os.str()}); + } + } + + // Fill-shape minimum area (shape-level static check). + double fill_area_um2 = rule.fill_w_um * rule.fill_h_um; + if (fill_area_um2 < rule.min_area_um2 - 1e-9) { + std::ostringstream os; + os << rule.name << " fill area " << fill_area_um2 << " um^2 < min " << rule.min_area_um2; + rep.violations.push_back( + {ViolationType::MinArea, -1, -1, fill_area_um2, rule.min_area_um2, os.str()}); + } + + (void)dbu_per_um; + (void)cell; + return rep; +} + +} // namespace mf diff --git a/gpu_metal_fill/src/engine.cpp b/gpu_metal_fill/src/engine.cpp new file mode 100644 index 0000000..d51bd9a --- /dev/null +++ b/gpu_metal_fill/src/engine.cpp @@ -0,0 +1,360 @@ +#include "metalfill/engine.hpp" + +#include +#include +#include +#include +#include + +#include "metalfill/partition.hpp" + +#ifdef _OPENMP +#include +#endif + +namespace mf { +namespace { + +struct LayerGeom { + dbu cell = 1; + dbu fw = 1, fh = 1, px = 1, py = 1; + dbu win = 1, step = 1, keep = 0; + dbu align_x = 1, align_y = 1, margin = 1; + int fw_c = 1, fh_c = 1, px_c = 1, py_c = 1; + int win_c = 1, step_c = 1, keep_c = 0; +}; + +dbu to_dbu(double um, double dppu) { return static_cast(std::llround(um * dppu)); } + +LayerGeom compute_geom(const FillRule& r, double dppu) { + LayerGeom g; + g.fw = std::max(to_dbu(r.fill_w_um, dppu), 1); + g.fh = std::max(to_dbu(r.fill_h_um, dppu), 1); + g.px = std::max(to_dbu(r.fill_pitch_x_um, dppu), g.fw); + g.py = std::max(to_dbu(r.fill_pitch_y_um, dppu), g.fh); + g.win = std::max(to_dbu(r.window_um, dppu), 1); + g.step = std::max(to_dbu(r.step_um, dppu), 1); + g.keep = std::max(to_dbu(r.keepout_um, dppu), 0); + + // Grid cell divides every relevant length so windows/pitch are exact. + dbu c = gcd_dbu(g.fw, g.fh); + c = gcd_dbu(c, g.px); + c = gcd_dbu(c, g.py); + c = gcd_dbu(c, g.win); + c = gcd_dbu(c, g.step); + g.cell = std::max(c, 1); + + g.fw_c = static_cast(g.fw / g.cell); + g.fh_c = static_cast(g.fh / g.cell); + g.px_c = static_cast(g.px / g.cell); + g.py_c = static_cast(g.py / g.cell); + g.win_c = static_cast(g.win / g.cell); + g.step_c = static_cast(g.step / g.cell); + g.keep_c = static_cast((g.keep + g.cell - 1) / g.cell); + + // Partition cuts must align to BOTH the window and the fill pitch so the + // per-partition result is identical to a global fill. + g.align_x = lcm_dbu(g.win, g.px); + g.align_y = lcm_dbu(g.win, g.py); + // Halo must cover keep-out + one fill footprint of context, rounded up to a + // full align unit so the halo origin stays on the lattice. + dbu need = g.keep + std::max(g.fw, g.fh); + dbu m_x = ((need + g.align_x - 1) / g.align_x) * g.align_x; + dbu m_y = ((need + g.align_y - 1) / g.align_y) * g.align_y; + g.margin = std::max(m_x, m_y); + return g; +} + +dbu floor_to(dbu v, dbu a) { return (v >= 0 ? (v / a) : -(((-v) + a - 1) / a)) * a; } +dbu ceil_to(dbu v, dbu a) { return (v >= 0 ? ((v + a - 1) / a) : -((-v) / a)) * a; } + +// Snaps a bbox outward to the align lattice in each axis. The result is an exact +// whole number of align-units, so density windows and the fill pitch stay +// coherent whether computed globally or per-partition. +BBox snap_area(const BBox& b, dbu align_x, dbu align_y) { + BBox s; + s.xmin = floor_to(b.xmin, align_x); + s.ymin = floor_to(b.ymin, align_y); + s.xmax = ceil_to(b.xmax, align_x); + s.ymax = ceil_to(b.ymax, align_y); + return s; +} + +std::vector layer_polys(const Layout& layout, const FillRule& r) { + std::vector v; + for (const auto& p : layout.polygons) + if (p.layer == r.layer && p.datatype == r.datatype) v.push_back(p); + return v; +} + +bool intersects(const BBox& a, const BBox& b) { + return !(a.xmax <= b.xmin || b.xmax <= a.xmin || a.ymax <= b.ymin || b.ymax <= a.ymin); +} + +// Runs partitioned fill for a per-window target map; returns emitted fill +// rectangles (global dbu) tagged on the fill datatype. Each leaf is processed +// independently (parallel), reading a halo of context and emitting only inside +// its core, so the concatenated result equals a global fill. +std::vector partitioned_fill(const std::vector& polys, const FillRule& rule, + const LayerGeom& g, const std::vector& parts, + FillBackend& backend, const std::vector& target_map, + int gnx, int gny, dbu area_xmin, dbu area_ymin, double maxd) { + std::vector> per(parts.size()); + +#ifdef _OPENMP +#pragma omp parallel for schedule(dynamic) +#endif + for (int pi = 0; pi < static_cast(parts.size()); ++pi) { + const PartitionRect& part = parts[pi]; + Grid occ = make_grid(part.halo, g.cell); + + // Rasterize only geometry that touches this leaf's halo. + std::vector local; + for (const auto& p : polys) + if (intersects(p.bbox(), part.halo)) local.push_back(p); + rasterize(occ, local, rule.layer, rule.datatype); + + Grid blocked = backend.compute_keepout(occ, g.keep_c); + + FillParams fp; + fp.fill_w = g.fw_c; + fp.fill_h = g.fh_c; + fp.pitch_x = g.px_c; + fp.pitch_y = g.py_c; + fp.win_cells = g.win_c; + fp.step_cells = g.step_c; + fp.max_density = maxd; + fp.target_map = &target_map; + fp.gnx = gnx; + fp.gny = gny; + fp.goff_x = static_cast((occ.ox - area_xmin) / g.step); + fp.goff_y = static_cast((occ.oy - area_ymin) / g.step); + + Grid fill_occ; + std::vector shapes = backend.place_fill(occ, blocked, fp, fill_occ); + + auto& out = per[pi]; + for (const auto& s : shapes) { + dbu x0 = occ.ox + static_cast(s.ix0) * g.cell; + dbu y0 = occ.oy + static_cast(s.iy0) * g.cell; + dbu x1 = x0 + static_cast(s.w) * g.cell; + dbu y1 = y0 + static_cast(s.h) * g.cell; + // Emit only shapes fully inside this leaf's core (disjoint merge). + if (x0 < part.core.xmin || y0 < part.core.ymin || x1 > part.core.xmax || + y1 > part.core.ymax) + continue; + out.push_back(make_rect(rule.layer, rule.fill_datatype, x0, y0, x1, y1)); + } + } + + std::vector fills; + for (auto& v : per) fills.insert(fills.end(), v.begin(), v.end()); + return fills; +} + +// Builds occupancy over the whole die for the given polygons on (layer,dt). +Grid whole_layer_grid(const std::vector& polys, const BBox& area, const FillRule& r, + dbu cell) { + Grid g = make_grid(area, cell); + rasterize(g, polys, r.layer, r.datatype); + return g; +} + +} // namespace + +FillSummary run_fill(Layout& layout, const std::vector& rules, const EngineConfig& cfg) { + FillSummary summary; + auto backend = make_backend(cfg.prefer_gpu); + summary.backend = backend->name(); + + double dppu = layout.dbu_per_um(); + auto t_all0 = std::chrono::steady_clock::now(); + + for (const auto& rule : rules) { + auto t0 = std::chrono::steady_clock::now(); + LayerResult res; + res.name = rule.name; + res.layer = rule.layer; + res.is_beol = rule.is_beol; + + std::vector polys = layer_polys(layout, rule); + res.existing_polys = static_cast(polys.size()); + + LayerGeom g = compute_geom(rule, dppu); + BBox die = layout.bbox(); + if (!die.valid()) { + summary.layers.push_back(res); + continue; + } + // Align-lattice-snapped working area (see snap_area). Both the global + // grids and the partition anchor use area.xmin/ymin so every window and + // fill position is identical globally and per-partition. + BBox area = snap_area(die, g.align_x, g.align_y); + + // ---- density before ------------------------------------------------- + Grid occ_before = whole_layer_grid(polys, area, rule, g.cell); + SummedAreaTable sat_before = backend->build_sat(occ_before); + DensityMap dm_before = backend->compute_density(sat_before, g.win_c, g.step_c); + res.density_before_min = dm_before.min_density(); + res.density_before_mean = dm_before.mean_density(); + res.density_before_max = dm_before.max_density(); + + // ---- partitions ----------------------------------------------------- + PartitionConfig pc; + pc.anchor_x = area.xmin; + pc.anchor_y = area.ymin; + pc.align_x = g.align_x; + pc.align_y = g.align_y; + pc.margin = g.margin; + pc.max_leaf = std::max(cfg.max_leaf, 1); // align-units per leaf side + pc.clip = area; // cores tile the align-snapped area exactly + std::vector parts = partition_recursive(area, pc); + res.partitions = static_cast(parts.size()); + + // ---- gradient-aware iterative fill (DRC-driven) --------------------- + // Each window gets its own target. We fill, measure density, then raise + // targets for windows that still violate min-density or that sit too far + // below a denser neighbor (gradient), and refill. Targets increase + // monotonically toward max_density, so the loop converges. + double min_d = rule.min_density; + double max_d = rule.max_density; + double grad = rule.max_gradient; + int gnx = dm_before.ntiles_x; + int gny = dm_before.ntiles_y; + double base_target = std::min(min_d + cfg.fill_headroom, max_d); + std::vector target_map(static_cast(gnx) * gny, base_target); + + std::vector best_fills; + DensityMap dm_after = dm_before; + int iter = 0; + for (; iter < std::max(cfg.max_iterations, 1); ++iter) { + std::vector fills = partitioned_fill( + polys, rule, g, parts, *backend, target_map, gnx, gny, area.xmin, area.ymin, max_d); + + Grid total = occ_before; // copy existing occupancy + for (const auto& f : fills) { + BBox b = f.bbox(); + stamp_rect(total, b.xmin, b.ymin, b.xmax, b.ymax, 1); + } + SummedAreaTable sat_after = backend->build_sat(total); + DensityMap dm = backend->compute_density(sat_after, g.win_c, g.step_c); + + best_fills = std::move(fills); + dm_after = dm; + + // Update targets from the achieved density (DRC feedback). + bool changed = false; + const int dx[4] = {1, -1, 0, 0}; + const int dy[4] = {0, 0, 1, -1}; + for (int ty = 0; ty < gny; ++ty) { + for (int tx = 0; tx < gnx; ++tx) { + size_t idx = static_cast(ty) * gnx + tx; + double here = dm.at(tx, ty).density; + double need = target_map[idx]; + if (here < min_d - 1e-9) need = std::max(need, base_target); + if (grad > 0) { + for (int k = 0; k < 4; ++k) { + int nx = tx + dx[k], ny = ty + dy[k]; + if (nx < 0 || ny < 0 || nx >= gnx || ny >= gny) continue; + double nd = dm.at(nx, ny).density; + // Overshoot by grad_headroom so the achieved density + // (which lands just below target) still clears the + // gradient limit despite discrete fill quanta. + if (nd - here > grad + 1e-9) + need = std::max(need, nd - grad + cfg.grad_headroom); + } + } + need = std::min(need, max_d); + if (need > target_map[idx] + 1e-9) { + target_map[idx] = need; + changed = true; + } + } + } + if (!changed) { + ++iter; + break; + } + } + res.iterations = iter; + + // ---- append fills + final DRC -------------------------------------- + res.fill_shapes = static_cast(best_fills.size()); + Grid fill_occ = make_grid(area, g.cell); + for (const auto& f : best_fills) { + BBox b = f.bbox(); + stamp_rect(fill_occ, b.xmin, b.ymin, b.xmax, b.ymax, 1); + } + Grid total = occ_before; + for (const auto& f : best_fills) { + BBox b = f.bbox(); + stamp_rect(total, b.xmin, b.ymin, b.xmax, b.ymax, 1); + } + SummedAreaTable sat_total = backend->build_sat(total); + Grid blocked = backend->compute_keepout(occ_before, g.keep_c); + res.drc = run_drc(sat_total, blocked, fill_occ, g.win_c, g.step_c, rule, dppu, g.cell); + + res.density_after_min = dm_after.min_density(); + res.density_after_mean = dm_after.mean_density(); + res.density_after_max = dm_after.max_density(); + + layout.polygons.insert(layout.polygons.end(), best_fills.begin(), best_fills.end()); + + auto t1 = std::chrono::steady_clock::now(); + res.seconds = std::chrono::duration(t1 - t0).count(); + summary.layers.push_back(std::move(res)); + } + + auto t_all1 = std::chrono::steady_clock::now(); + summary.total_seconds = std::chrono::duration(t_all1 - t_all0).count(); + return summary; +} + +int FillSummary::total_violations() const { + int c = 0; + for (const auto& l : layers) c += static_cast(l.drc.violations.size()); + return c; +} +int FillSummary::total_fill_shapes() const { + int c = 0; + for (const auto& l : layers) c += l.fill_shapes; + return c; +} + +std::string format_report(const FillSummary& summary) { + std::ostringstream os; + os << std::fixed << std::setprecision(3); + os << "===== Metal Fill Report =====\n"; + os << "backend : " << summary.backend << "\n"; + os << "total time : " << summary.total_seconds << " s\n"; + os << "total fill : " << summary.total_fill_shapes() << " shapes\n"; + os << "total DRC : " << summary.total_violations() << " violations\n\n"; + + os << std::left << std::setw(6) << "layer" << std::setw(6) << "type" << std::setw(6) << "part" + << std::setw(8) << "exist" << std::setw(8) << "fill" << std::setw(6) << "it" + << " dens(before->after) min/mean/max drc time(s)\n"; + os << std::string(112, '-') << "\n"; + for (const auto& l : summary.layers) { + os << std::left << std::setw(6) << l.name << std::setw(6) << (l.is_beol ? "BEOL" : "FEOL") + << std::setw(6) << l.partitions << std::setw(8) << l.existing_polys << std::setw(8) + << l.fill_shapes << std::setw(6) << l.iterations << " "; + os << std::setprecision(2) << l.density_before_min << "/" << l.density_before_mean << "/" + << l.density_before_max << " -> " << l.density_after_min << "/" << l.density_after_mean + << "/" << l.density_after_max; + os << " " << std::setw(4) << static_cast(l.drc.violations.size()) << " " + << std::setprecision(3) << l.seconds << "\n"; + } + + // Detail any violations. + bool any = false; + for (const auto& l : summary.layers) + if (!l.drc.clean()) any = true; + if (any) { + os << "\n--- DRC details ---\n"; + for (const auto& l : summary.layers) + for (const auto& v : l.drc.violations) os << " " << v.message << "\n"; + } + return os.str(); +} + +} // namespace mf diff --git a/gpu_metal_fill/src/fill.cpp b/gpu_metal_fill/src/fill.cpp new file mode 100644 index 0000000..ccdf8ce --- /dev/null +++ b/gpu_metal_fill/src/fill.cpp @@ -0,0 +1,115 @@ +#include "metalfill/fill.hpp" + +#include +#include + +#include "metalfill/density.hpp" + +#ifdef _OPENMP +#include +#endif + +namespace mf { + +Grid compute_keepout(const Grid& occ, int keepout_cells) { + Grid blocked; + blocked.resize(occ.nx, occ.ny, occ.cell, occ.ox, occ.oy); + int r = std::max(keepout_cells, 0); + SummedAreaTable sat = build_sat(occ); + for (int iy = 0; iy < occ.ny; ++iy) { + for (int ix = 0; ix < occ.nx; ++ix) { + int x0 = ix - r, y0 = iy - r; + int x1 = ix + r + 1, y1 = iy + r + 1; + blocked.set(ix, iy, sat.area(x0, y0, x1, y1) > 0 ? 1 : 0); + } + } + return blocked; +} + +std::vector place_fill(const Grid& occ, const Grid& blocked, + const FillParams& params, Grid& fill_occ) { + fill_occ.resize(occ.nx, occ.ny, occ.cell, occ.ox, occ.oy); + + const int fw = std::max(params.fill_w, 1); + const int fh = std::max(params.fill_h, 1); + const int px = std::max(params.pitch_x, fw); + const int py = std::max(params.pitch_y, fh); + const int win = std::max(params.win_cells, 1); + const int step = std::max(params.step_cells, 1); + const double fill_area = static_cast(fw) * fh; + + SummedAreaTable occ_sat = build_sat(occ); + + auto ntiles = [](int n, int s) { return std::max(1, (n + s - 1) / s); }; + const int ntx = ntiles(occ.nx, step); + const int nty = ntiles(occ.ny, step); + const int ntiles_total = ntx * nty; + + std::vector> per_tile(ntiles_total); + + // Each tile writes only footprints whose origin is inside the tile and whose + // extent stays inside the tile, so the writes to fill_occ are disjoint and + // the loop is safe to run in parallel across tiles (and, later, on the GPU). +#ifdef _OPENMP +#pragma omp parallel for schedule(dynamic) +#endif + for (int t = 0; t < ntiles_total; ++t) { + int tx = t % ntx; + int ty = t / ntx; + int wx0 = tx * step; + int wy0 = ty * step; + int wx1 = std::min(wx0 + win, occ.nx); + int wy1 = std::min(wy0 + win, occ.ny); + double win_area = static_cast(wx1 - wx0) * (wy1 - wy0); + if (win_area <= 0) continue; + + double occ_cells = static_cast(occ_sat.area(wx0, wy0, wx1, wy1)); + double placed_cells = 0.0; + + // Per-window target (gradient-aware) if a target map is supplied. + double tgt = params.target_density; + if (params.target_map) { + int gx = params.goff_x + tx; + int gy = params.goff_y + ty; + if (gx >= 0 && gy >= 0 && gx < params.gnx && gy < params.gny) + tgt = (*params.target_map)[static_cast(gy) * params.gnx + gx]; + } + double target_cells = tgt * win_area; + double max_cells = params.max_density * win_area; + + auto& out = per_tile[t]; + + // First candidate origin on the global pitch lattice inside the tile. + int cx_start = ((wx0 + px - 1) / px) * px; + int cy_start = ((wy0 + py - 1) / py) * py; + + for (int cy = cy_start; cy + fh <= wy1; cy += py) { + if (occ_cells + placed_cells + fill_area > target_cells) break; + for (int cx = cx_start; cx + fw <= wx1; cx += px) { + if (occ_cells + placed_cells + fill_area > target_cells) break; + if (occ_cells + placed_cells + fill_area > max_cells) break; + + bool free = true; + for (int yy = cy; yy < cy + fh && free; ++yy) + for (int xx = cx; xx < cx + fw; ++xx) + if (blocked.at(xx, yy) || fill_occ.at(xx, yy)) { + free = false; + break; + } + if (!free) continue; + + for (int yy = cy; yy < cy + fh; ++yy) + for (int xx = cx; xx < cx + fw; ++xx) fill_occ.set(xx, yy, 1); + out.push_back(FillShape{cx, cy, fw, fh}); + placed_cells += fill_area; + } + } + } + + std::vector shapes; + for (auto& v : per_tile) + shapes.insert(shapes.end(), v.begin(), v.end()); + return shapes; +} + +} // namespace mf diff --git a/gpu_metal_fill/src/gdsii.cpp b/gpu_metal_fill/src/gdsii.cpp new file mode 100644 index 0000000..3a03c37 --- /dev/null +++ b/gpu_metal_fill/src/gdsii.cpp @@ -0,0 +1,231 @@ +#include "metalfill/gdsii.hpp" + +#include +#include +#include +#include +#include + +namespace mf { +namespace { + +// ---- GDSII record identifiers (rectype << 8 | datatype) -------------------- +constexpr uint8_t RT_HEADER = 0x00, RT_BGNLIB = 0x01, RT_LIBNAME = 0x02; +constexpr uint8_t RT_UNITS = 0x03, RT_ENDLIB = 0x04, RT_BGNSTR = 0x05; +constexpr uint8_t RT_STRNAME = 0x06, RT_ENDSTR = 0x07, RT_BOUNDARY = 0x08; +constexpr uint8_t RT_LAYER = 0x0D, RT_DATATYPE = 0x0E, RT_XY = 0x10; +constexpr uint8_t RT_ENDEL = 0x11, RT_BOX = 0x2D, RT_BOXTYPE = 0x2E; + +constexpr uint8_t DT_NODATA = 0x00, DT_INT2 = 0x02, DT_INT4 = 0x03; +constexpr uint8_t DT_REAL8 = 0x05, DT_STR = 0x06; + +// ---- big-endian helpers ---------------------------------------------------- +void put_u16(std::ofstream& os, uint16_t v) { + char b[2] = {char((v >> 8) & 0xFF), char(v & 0xFF)}; + os.write(b, 2); +} +void put_i16(std::ofstream& os, int16_t v) { put_u16(os, static_cast(v)); } +void put_i32(std::ofstream& os, int32_t v) { + char b[4] = {char((v >> 24) & 0xFF), char((v >> 16) & 0xFF), char((v >> 8) & 0xFF), + char(v & 0xFF)}; + os.write(b, 4); +} + +uint16_t get_u16(const uint8_t* p) { return (uint16_t(p[0]) << 8) | p[1]; } +int32_t get_i32(const uint8_t* p) { + return int32_t((uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | (uint32_t(p[2]) << 8) | + uint32_t(p[3])); +} + +// GDSII 8-byte real: sign (bit7), 7-bit excess-64 base-16 exponent, 56-bit mantissa. +void put_real8(std::ofstream& os, double v) { + uint8_t out[8] = {0}; + if (v != 0.0) { + bool neg = v < 0; + double d = std::fabs(v); + int exp = 64; + while (d >= 1.0) { d /= 16.0; ++exp; } + while (d < 1.0 / 16.0) { d *= 16.0; --exp; } + // d now in [1/16, 1); build 56-bit mantissa. + uint64_t mant = static_cast(std::llround(d * static_cast(1ULL << 56))); + out[0] = static_cast((neg ? 0x80 : 0x00) | (exp & 0x7F)); + for (int i = 7; i >= 1; --i) { + out[i] = static_cast(mant & 0xFF); + mant >>= 8; + } + } + os.write(reinterpret_cast(out), 8); +} + +double get_real8(const uint8_t* p) { + bool neg = (p[0] & 0x80) != 0; + int exp = (p[0] & 0x7F) - 64; + uint64_t mant = 0; + for (int i = 1; i < 8; ++i) mant = (mant << 8) | p[i]; + double d = static_cast(mant) / static_cast(1ULL << 56); + d *= std::pow(16.0, exp); + return neg ? -d : d; +} + +void write_record(std::ofstream& os, uint8_t rectype, uint8_t datatype, const char* data, + uint16_t data_len) { + uint16_t total = static_cast(4 + data_len); + put_u16(os, total); + char hdr[2] = {char(rectype), char(datatype)}; + os.write(hdr, 2); + if (data_len) os.write(data, data_len); +} + +void write_str(std::ofstream& os, uint8_t rectype, const std::string& s) { + std::string p = s; + if (p.size() & 1) p.push_back('\0'); // pad to even length + write_record(os, rectype, DT_STR, p.data(), static_cast(p.size())); +} + +} // namespace + +Layout read_gds(const std::string& path) { + std::ifstream is(path, std::ios::binary); + if (!is) throw std::runtime_error("cannot open GDS for read: " + path); + + Layout layout; + Polygon cur; + bool in_elem = false; + bool is_box = false; + + while (true) { + uint8_t hdr[4]; + is.read(reinterpret_cast(hdr), 4); + if (is.gcount() == 0) break; // clean EOF + if (is.gcount() != 4) throw std::runtime_error("truncated GDS record header"); + + uint16_t total = get_u16(hdr); + uint8_t rectype = hdr[2]; + uint8_t datatype = hdr[3]; + if (total < 4) throw std::runtime_error("invalid GDS record length"); + int data_len = total - 4; + std::vector data(data_len); + if (data_len) { + is.read(reinterpret_cast(data.data()), data_len); + if (is.gcount() != data_len) throw std::runtime_error("truncated GDS record body"); + } + + switch (rectype) { + case RT_UNITS: + if (data_len >= 16) { + layout.user_units_per_dbu = get_real8(data.data()); + layout.meters_per_dbu = get_real8(data.data() + 8); + } + break; + case RT_LIBNAME: + layout.lib_name.assign(reinterpret_cast(data.data()), data_len); + break; + case RT_STRNAME: + layout.cell_name.assign(reinterpret_cast(data.data()), data_len); + if (!layout.cell_name.empty() && layout.cell_name.back() == '\0') + layout.cell_name.pop_back(); + break; + case RT_BOUNDARY: + cur = Polygon{}; + in_elem = true; + is_box = false; + break; + case RT_BOX: + cur = Polygon{}; + in_elem = true; + is_box = true; + break; + case RT_LAYER: + if (in_elem && data_len >= 2) cur.layer = int(int16_t(get_u16(data.data()))); + break; + case RT_DATATYPE: + case RT_BOXTYPE: + if (in_elem && data_len >= 2) cur.datatype = int(int16_t(get_u16(data.data()))); + break; + case RT_XY: + if (in_elem) { + int npairs = data_len / 8; + cur.pts.clear(); + cur.pts.reserve(npairs); + for (int i = 0; i < npairs; ++i) { + dbu x = get_i32(data.data() + i * 8); + dbu y = get_i32(data.data() + i * 8 + 4); + cur.pts.push_back({x, y}); + } + // Drop the closing duplicate vertex if present. + if (cur.pts.size() > 1 && cur.pts.front().x == cur.pts.back().x && + cur.pts.front().y == cur.pts.back().y) + cur.pts.pop_back(); + } + break; + case RT_ENDEL: + if (in_elem && cur.pts.size() >= 3) layout.polygons.push_back(cur); + in_elem = false; + is_box = false; + break; + default: + break; // ignore HEADER/BGNLIB/PATH/SREF/etc. + } + (void)datatype; + (void)is_box; + } + return layout; +} + +void write_gds(const std::string& path, const Layout& layout) { + std::ofstream os(path, std::ios::binary); + if (!os) throw std::runtime_error("cannot open GDS for write: " + path); + + // HEADER (version 600) + char ver[2] = {0x02, 0x58}; + write_record(os, RT_HEADER, DT_INT2, ver, 2); + + // BGNLIB (24 bytes of timestamps, all zero) + char zeros[24] = {0}; + write_record(os, RT_BGNLIB, DT_INT2, zeros, 24); + write_str(os, RT_LIBNAME, layout.lib_name); + + // UNITS: [user units per dbu, meters per dbu] + put_u16(os, 20); // 4 header + 16 payload + os.put(char(RT_UNITS)); + os.put(char(DT_REAL8)); + put_real8(os, layout.user_units_per_dbu); + put_real8(os, layout.meters_per_dbu); + + // BGNSTR + write_record(os, RT_BGNSTR, DT_INT2, zeros, 24); + write_str(os, RT_STRNAME, layout.cell_name); + + for (const auto& poly : layout.polygons) { + if (poly.pts.size() < 3) continue; + write_record(os, RT_BOUNDARY, DT_NODATA, nullptr, 0); + put_u16(os, 6); // LAYER record + os.put(char(RT_LAYER)); + os.put(char(DT_INT2)); + put_i16(os, static_cast(poly.layer)); + put_u16(os, 6); // DATATYPE record + os.put(char(RT_DATATYPE)); + os.put(char(DT_INT2)); + put_i16(os, static_cast(poly.datatype)); + + // XY: points + closing vertex. + int npts = static_cast(poly.pts.size()) + 1; + uint16_t xy_len = static_cast(npts * 8); + put_u16(os, static_cast(4 + xy_len)); + os.put(char(RT_XY)); + os.put(char(DT_INT4)); + for (const auto& p : poly.pts) { + put_i32(os, static_cast(p.x)); + put_i32(os, static_cast(p.y)); + } + put_i32(os, static_cast(poly.pts.front().x)); + put_i32(os, static_cast(poly.pts.front().y)); + + write_record(os, RT_ENDEL, DT_NODATA, nullptr, 0); + } + + write_record(os, RT_ENDSTR, DT_NODATA, nullptr, 0); + write_record(os, RT_ENDLIB, DT_NODATA, nullptr, 0); +} + +} // namespace mf diff --git a/gpu_metal_fill/src/layermap.cpp b/gpu_metal_fill/src/layermap.cpp new file mode 100644 index 0000000..1615633 --- /dev/null +++ b/gpu_metal_fill/src/layermap.cpp @@ -0,0 +1,91 @@ +#include "metalfill/layermap.hpp" + +namespace mf { + +std::vector default_layermap() { + std::vector rules; + + // ---- FEOL / base layers ------------------------------------------------ + { + FillRule od; + od.name = "OD"; + od.layer = kOdLayer; + od.fill_datatype = kFillDatatype; + od.window_um = 20.0; + od.step_um = 20.0; + od.min_density = 0.25; + od.max_density = 0.75; + od.max_gradient = 0.20; + od.fill_w_um = 0.5; + od.fill_h_um = 0.5; + od.fill_pitch_x_um = 0.625; // 1.25x fill -> ~64% max coverage + od.fill_pitch_y_um = 0.625; + od.keepout_um = 0.20; + od.min_area_um2 = 0.25; + od.is_beol = false; + rules.push_back(od); + + FillRule po; + po.name = "PO"; + po.layer = kPoLayer; + po.fill_datatype = kFillDatatype; + po.window_um = 20.0; + po.step_um = 20.0; + po.min_density = 0.20; + po.max_density = 0.70; + po.max_gradient = 0.20; + po.fill_w_um = 0.4; + po.fill_h_um = 0.4; + po.fill_pitch_x_um = 0.5; // 1.25x fill + po.fill_pitch_y_um = 0.5; + po.keepout_um = 0.15; + po.min_area_um2 = 0.16; + po.is_beol = false; + rules.push_back(po); + } + + // ---- BEOL / metal layers M1..M14 -------------------------------------- + for (int n = 1; n <= kNumMetals; ++n) { + FillRule m; + m.name = "M" + std::to_string(n); + m.layer = metal_layer(n); + m.fill_datatype = kFillDatatype; + m.window_um = 20.0; + m.step_um = 20.0; + m.min_density = 0.30; + m.max_density = 0.80; + m.max_gradient = 0.15; + m.keepout_um = (n <= 6) ? 0.10 : 0.30; + m.is_beol = true; + + // Pitch is kept at 1.25x the fill size (and a divisor of the 20um + // window) so fill can reach ~64% coverage -- comfortably above the min + // density target -- while windows/pitch stay lattice-coherent. + if (n <= 6) { + // Lower/thin metals: small dense fill. + m.fill_w_um = 0.20; + m.fill_h_um = 0.20; + m.fill_pitch_x_um = 0.25; + m.fill_pitch_y_um = 0.25; + m.min_area_um2 = 0.04; + } else if (n <= 10) { + m.fill_w_um = 0.40; + m.fill_h_um = 0.40; + m.fill_pitch_x_um = 0.50; + m.fill_pitch_y_um = 0.50; + m.min_area_um2 = 0.16; + } else { + // Upper/thick metals: large fill on a coarse pitch. + m.fill_w_um = 0.80; + m.fill_h_um = 0.80; + m.fill_pitch_x_um = 1.00; + m.fill_pitch_y_um = 1.00; + m.min_area_um2 = 0.64; + } + rules.push_back(m); + } + + return rules; +} + +} // namespace mf diff --git a/gpu_metal_fill/src/partition.cpp b/gpu_metal_fill/src/partition.cpp new file mode 100644 index 0000000..b23cb73 --- /dev/null +++ b/gpu_metal_fill/src/partition.cpp @@ -0,0 +1,91 @@ +#include "metalfill/partition.hpp" + +#include + +namespace mf { + +dbu gcd_dbu(dbu a, dbu b) { + a = a < 0 ? -a : a; + b = b < 0 ? -b : b; + while (b) { + dbu t = a % b; + a = b; + b = t; + } + return a == 0 ? 1 : a; +} + +dbu lcm_dbu(dbu a, dbu b) { + if (a == 0 || b == 0) return std::max(a, b); + return (a / gcd_dbu(a, b)) * b; +} + +namespace { + +// Recurse in integer align-lattice coordinates: region [ix0,ix1) x [iy0,iy1). +void recurse(int ix0, int iy0, int ix1, int iy1, int depth, const PartitionConfig& cfg, + std::vector& out) { + int nx = ix1 - ix0; + int ny = iy1 - iy0; + if (nx <= 0 || ny <= 0) return; + + bool leaf = (nx <= cfg.max_leaf && ny <= cfg.max_leaf) || depth >= cfg.max_depth; + if (leaf) { + PartitionRect pr; + pr.depth = depth; + pr.core.xmin = cfg.anchor_x + static_cast(ix0) * cfg.align_x; + pr.core.xmax = cfg.anchor_x + static_cast(ix1) * cfg.align_x; + pr.core.ymin = cfg.anchor_y + static_cast(iy0) * cfg.align_y; + pr.core.ymax = cfg.anchor_y + static_cast(iy1) * cfg.align_y; + // Clamp emission core to the die bounds. + if (cfg.clip.valid()) { + pr.core.xmin = std::max(pr.core.xmin, cfg.clip.xmin); + pr.core.ymin = std::max(pr.core.ymin, cfg.clip.ymin); + pr.core.xmax = std::min(pr.core.xmax, cfg.clip.xmax); + pr.core.ymax = std::min(pr.core.ymax, cfg.clip.ymax); + } + pr.halo.xmin = pr.core.xmin - cfg.margin; + pr.halo.ymin = pr.core.ymin - cfg.margin; + pr.halo.xmax = pr.core.xmax + cfg.margin; + pr.halo.ymax = pr.core.ymax + cfg.margin; + if (pr.core.xmax > pr.core.xmin && pr.core.ymax > pr.core.ymin) out.push_back(pr); + return; + } + + bool split_x = nx > 1; + bool split_y = ny > 1; + int mx = ix0 + (nx + 1) / 2; + int my = iy0 + (ny + 1) / 2; + + if (split_x && split_y) { + // Quadtree split into four children. + recurse(ix0, iy0, mx, my, depth + 1, cfg, out); + recurse(mx, iy0, ix1, my, depth + 1, cfg, out); + recurse(ix0, my, mx, iy1, depth + 1, cfg, out); + recurse(mx, my, ix1, iy1, depth + 1, cfg, out); + } else if (split_x) { + recurse(ix0, iy0, mx, iy1, depth + 1, cfg, out); + recurse(mx, iy0, ix1, iy1, depth + 1, cfg, out); + } else { + recurse(ix0, iy0, ix1, my, depth + 1, cfg, out); + recurse(ix0, my, ix1, iy1, depth + 1, cfg, out); + } +} + +} // namespace + +std::vector partition_recursive(const BBox& area, const PartitionConfig& cfg) { + std::vector out; + if (!area.valid() || cfg.align_x <= 0 || cfg.align_y <= 0) return out; + + dbu w = area.xmax - cfg.anchor_x; + dbu h = area.ymax - cfg.anchor_y; + int nX = static_cast((w + cfg.align_x - 1) / cfg.align_x); + int nY = static_cast((h + cfg.align_y - 1) / cfg.align_y); + nX = std::max(nX, 1); + nY = std::max(nY, 1); + recurse(0, 0, nX, nY, 0, cfg, out); + return out; +} + +} // namespace mf diff --git a/gpu_metal_fill/src/raster.cpp b/gpu_metal_fill/src/raster.cpp new file mode 100644 index 0000000..ab9a1ef --- /dev/null +++ b/gpu_metal_fill/src/raster.cpp @@ -0,0 +1,80 @@ +#include "metalfill/raster.hpp" + +#include +#include + +namespace mf { + +Grid make_grid(const BBox& area, dbu cell) { + Grid g; + if (cell <= 0) cell = 1; + dbu ox = area.valid() ? area.xmin : 0; + dbu oy = area.valid() ? area.ymin : 0; + dbu w = area.valid() ? area.width() : 0; + dbu h = area.valid() ? area.height() : 0; + // Enough cells to fully cover the area (ceil). + int nx = static_cast((w + cell - 1) / cell); + int ny = static_cast((h + cell - 1) / cell); + g.resize(std::max(nx, 1), std::max(ny, 1), cell, ox, oy); + return g; +} + +void stamp_rect(Grid& grid, dbu x0, dbu y0, dbu x1, dbu y1, uint8_t value) { + if (x1 < x0) std::swap(x0, x1); + if (y1 < y0) std::swap(y1, y0); + // Cells whose center lies within [x0,x1) x [y0,y1). + int ix0 = static_cast(std::floor((static_cast(x0 - grid.ox)) / grid.cell)); + int iy0 = static_cast(std::floor((static_cast(y0 - grid.oy)) / grid.cell)); + int ix1 = static_cast(std::ceil((static_cast(x1 - grid.ox)) / grid.cell)); + int iy1 = static_cast(std::ceil((static_cast(y1 - grid.oy)) / grid.cell)); + ix0 = std::max(ix0, 0); + iy0 = std::max(iy0, 0); + ix1 = std::min(ix1, grid.nx); + iy1 = std::min(iy1, grid.ny); + for (int iy = iy0; iy < iy1; ++iy) + for (int ix = ix0; ix < ix1; ++ix) grid.set(ix, iy, value); +} + +void rasterize(Grid& grid, const std::vector& polys, int layer, int datatype) { + for (const auto& poly : polys) { + if (poly.layer != layer || poly.datatype != datatype) continue; + if (poly.pts.size() < 3) continue; + + BBox b = poly.bbox(); + int row0 = static_cast(std::floor((static_cast(b.ymin - grid.oy)) / grid.cell)); + int row1 = static_cast(std::ceil((static_cast(b.ymax - grid.oy)) / grid.cell)); + row0 = std::max(row0, 0); + row1 = std::min(row1, grid.ny); + + const size_t n = poly.pts.size(); + std::vector xs; + for (int iy = row0; iy < row1; ++iy) { + // Scanline at the vertical center of the cell row. + double yc = grid.oy + (iy + 0.5) * grid.cell; + xs.clear(); + for (size_t i = 0; i < n; ++i) { + const Point& a = poly.pts[i]; + const Point& c = poly.pts[(i + 1) % n]; + double ay = a.y, cy = c.y; + if ((ay <= yc && cy > yc) || (cy <= yc && ay > yc)) { + double t = (yc - ay) / (cy - ay); + xs.push_back(a.x + t * (c.x - a.x)); + } + } + std::sort(xs.begin(), xs.end()); + for (size_t k = 0; k + 1 < xs.size(); k += 2) { + double xl = xs[k], xr = xs[k + 1]; + // Cells whose center is inside [xl, xr). + int cx0 = static_cast( + std::ceil((xl - grid.ox) / grid.cell - 0.5)); + int cx1 = static_cast( + std::floor((xr - grid.ox) / grid.cell - 0.5)); + cx0 = std::max(cx0, 0); + cx1 = std::min(cx1, grid.nx - 1); + for (int ix = cx0; ix <= cx1; ++ix) grid.set(ix, iy, 1); + } + } + } +} + +} // namespace mf diff --git a/gpu_metal_fill/tests/test_main.cpp b/gpu_metal_fill/tests/test_main.cpp new file mode 100644 index 0000000..2ec65a0 --- /dev/null +++ b/gpu_metal_fill/tests/test_main.cpp @@ -0,0 +1,241 @@ +// Lightweight, dependency-free test harness for the metal-fill library. +#include +#include +#include +#include +#include + +#include "metalfill/density.hpp" +#include "metalfill/drc.hpp" +#include "metalfill/engine.hpp" +#include "metalfill/fill.hpp" +#include "metalfill/gdsii.hpp" +#include "metalfill/layermap.hpp" +#include "metalfill/partition.hpp" +#include "metalfill/raster.hpp" + +using namespace mf; + +static int g_fails = 0; +static int g_checks = 0; +#define CHECK(cond, msg) \ + do { \ + ++g_checks; \ + if (!(cond)) { \ + ++g_fails; \ + std::printf(" FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); \ + } \ + } while (0) + +static void test_gds_roundtrip() { + std::printf("[gds roundtrip]\n"); + Layout L; + L.cell_name = "TOP"; + L.polygons.push_back(make_rect(10, 0, 0, 0, 5000, 3000)); + L.polygons.push_back(make_rect(23, 5, -2000, -1000, 4000, 7000)); + const std::string path = "build/_test_rt.gds"; + write_gds(path, L); + Layout R = read_gds(path); + CHECK(R.polygons.size() == 2, "polygon count preserved"); + CHECK(R.cell_name == "TOP", "cell name preserved"); + CHECK(std::llround(R.dbu_per_um()) == 1000, "units preserved (1000 dbu/um)"); + bool found = false; + for (const auto& p : R.polygons) + if (p.layer == 23 && p.datatype == 5) { + BBox b = p.bbox(); + found = (b.xmin == -2000 && b.ymin == -1000 && b.xmax == 4000 && b.ymax == 7000); + } + CHECK(found, "layer/datatype/coords preserved"); +} + +static void test_sat_density() { + std::printf("[sat + density]\n"); + Grid g; + g.resize(4, 4, 100, 0, 0); + // Fill a 2x2 block in the corner. + for (int y = 0; y < 2; ++y) + for (int x = 0; x < 2; ++x) g.set(x, y, 1); + SummedAreaTable sat = build_sat(g); + CHECK(sat.area(0, 0, 4, 4) == 4, "total occupied = 4"); + CHECK(sat.area(0, 0, 2, 2) == 4, "block area = 4"); + CHECK(sat.area(2, 2, 4, 4) == 0, "empty quadrant = 0"); + DensityMap dm = compute_density(sat, 2, 2); // four 2x2 windows + CHECK(dm.ntiles_x == 2 && dm.ntiles_y == 2, "2x2 tiling"); + CHECK(std::fabs(dm.at(0, 0).density - 1.0) < 1e-9, "corner window density 1.0"); + CHECK(std::fabs(dm.at(1, 1).density - 0.0) < 1e-9, "far window density 0.0"); + CHECK(std::fabs(dm.mean_density() - 0.25) < 1e-9, "mean density 0.25"); +} + +static void test_rasterize_rect() { + std::printf("[rasterize]\n"); + Grid g = make_grid(BBox{0, 0, 1000, 1000}, 100); // 10x10 cells (11x11 with +1) + std::vector polys = {make_rect(1, 0, 200, 200, 700, 700)}; + rasterize(g, polys, 1, 0); + // 500x500 dbu rect at 100 dbu/cell -> ~5x5 = 25 cells. + CHECK(g.count() == 25, "rasterized rect area = 25 cells"); +} + +static void test_keepout() { + std::printf("[keepout dilation]\n"); + Grid g; + g.resize(7, 7, 100, 0, 0); + g.set(3, 3, 1); // single occupied cell in the middle + Grid b = compute_keepout(g, 1); + CHECK(b.at(3, 3) == 1, "center blocked"); + CHECK(b.at(2, 2) == 1 && b.at(4, 4) == 1, "diagonal neighbors blocked (r=1)"); + CHECK(b.at(1, 1) == 0, "distance-2 cell not blocked"); + Grid b0 = compute_keepout(g, 0); + CHECK(b0.count() == 1, "r=0 keepout equals occupancy"); +} + +static void test_place_fill() { + std::printf("[place fill]\n"); + Grid occ; + occ.resize(40, 40, 100, 0, 0); // empty layer + Grid blocked = compute_keepout(occ, 0); + FillParams fp; + fp.fill_w = 1; + fp.fill_h = 1; + fp.pitch_x = 2; + fp.pitch_y = 2; + fp.win_cells = 40; + fp.step_cells = 40; + fp.target_density = 0.20; + fp.max_density = 0.80; + Grid fill_occ; + auto shapes = place_fill(occ, blocked, fp, fill_occ); + double dens = double(fill_occ.count()) / (40.0 * 40.0); + CHECK(!shapes.empty(), "fill placed"); + CHECK(dens >= 0.20 - 1e-6, "reached target density"); + CHECK(dens <= 0.80 + 1e-6, "did not exceed max density"); + + // With keepout covering everything, no fill can be placed. + Grid occ2; + occ2.resize(10, 10, 100, 0, 0); + for (auto& v : occ2.occ) v = 1; + Grid blocked2 = compute_keepout(occ2, 0); + Grid fo2; + auto s2 = place_fill(occ2, blocked2, fp, fo2); + CHECK(s2.empty(), "no fill where fully blocked"); +} + +static void test_partition_cover() { + std::printf("[partition cover]\n"); + BBox area{0, 0, 100000, 100000}; // 100um die + PartitionConfig pc; + pc.anchor_x = 0; + pc.anchor_y = 0; + pc.align_x = 20000; // 20um + pc.align_y = 20000; + pc.margin = 20000; + pc.max_leaf = 2; + pc.clip = area; + auto parts = partition_recursive(area, pc); + CHECK(parts.size() >= 4, "die split into multiple leaves"); + + // Cores must be disjoint and exactly cover the die area (by summed area). + long long covered = 0; + bool aligned = true; + for (const auto& p : parts) { + covered += (long long)p.core.width() * p.core.height(); + if ((p.core.xmin % pc.align_x) != 0 || (p.core.ymin % pc.align_y) != 0) aligned = false; + CHECK(p.halo.xmin <= p.core.xmin && p.halo.xmax >= p.core.xmax, "halo contains core"); + } + CHECK(aligned, "core edges aligned to lattice"); + CHECK(covered == (long long)area.width() * area.height(), "cores tile the die exactly"); +} + +// The whole point of partitioning: the merged result must match a single-shot +// (global) fill. We run the engine with a tiny leaf size and with a huge leaf +// size (one partition) and require identical post-fill density. +static void test_partition_equals_global() { + std::printf("[partition == global]\n"); + Layout base; + base.cell_name = "TOP"; + const dbu win = 20000; + for (int j = 0; j < 5; ++j) + for (int i = 0; i < 5; ++i) { + double f = 0.05 + 0.05 * ((i + j) % 7); + double s = std::sqrt(f) * win; + dbu x0 = i * win + dbu((win - s) / 2); + dbu y0 = j * win + dbu((win - s) / 2); + base.polygons.push_back(make_rect(metal_layer(3), 0, x0, y0, x0 + dbu(s), y0 + dbu(s))); + } + + std::vector rules; + for (const auto& r : default_layermap()) + if (r.name == "M3") rules.push_back(r); + + Layout a = base, b = base; + EngineConfig ca; + ca.max_leaf = 1; // finely partitioned + ca.max_iterations = 3; + EngineConfig cb; + cb.max_leaf = 100000; // effectively one partition (global) + cb.max_iterations = 3; + + FillSummary sa = run_fill(a, rules, ca); + FillSummary sb = run_fill(b, rules, cb); + + CHECK(sa.layers[0].partitions > 1, "fine config actually partitioned"); + CHECK(sb.layers[0].partitions == 1, "global config is a single partition"); + CHECK(sa.layers[0].fill_shapes == sb.layers[0].fill_shapes, + "same number of fill shapes partitioned vs global"); + CHECK(std::fabs(sa.layers[0].density_after_mean - sb.layers[0].density_after_mean) < 1e-6, + "same post-fill mean density"); + CHECK(std::fabs(sa.layers[0].density_after_min - sb.layers[0].density_after_min) < 1e-6, + "same post-fill min density"); +} + +static void test_engine_endtoend() { + std::printf("[engine end-to-end]\n"); + Layout L; + L.cell_name = "TOP"; + const dbu win = 20000; + auto rules = default_layermap(); + int li = 0; + for (const auto& r : rules) { + for (int j = 0; j < 5; ++j) + for (int i = 0; i < 5; ++i) { + double f = 0.04 + 0.06 * ((i + j + li) % 9); + double s = std::sqrt(f) * win; + dbu x0 = i * win + dbu((win - s) / 2); + dbu y0 = j * win + dbu((win - s) / 2); + L.polygons.push_back(make_rect(r.layer, r.datatype, x0, y0, x0 + dbu(s), y0 + dbu(s))); + } + ++li; + } + size_t before = L.polygons.size(); + EngineConfig cfg; + cfg.max_iterations = 3; + FillSummary sum = run_fill(L, rules, cfg); + + CHECK(L.polygons.size() > before, "fill polygons appended"); + CHECK(sum.total_fill_shapes() > 0, "fill produced"); + // No spacing or max-density violations may ever be produced by the tool. + int spacing = 0, maxd = 0; + for (const auto& l : sum.layers) { + spacing += l.drc.count(ViolationType::Spacing); + maxd += l.drc.count(ViolationType::MaxDensity); + } + CHECK(spacing == 0, "no fill-in-keepout spacing violations"); + CHECK(maxd == 0, "no max-density violations"); + for (const auto& l : sum.layers) { + CHECK(l.density_after_min >= l.density_before_min - 1e-9, "min density did not decrease"); + CHECK(l.density_after_mean >= l.density_before_mean - 1e-9, "mean density did not decrease"); + } +} + +int main() { + std::printf("== metalfill tests ==\n"); + test_gds_roundtrip(); + test_sat_density(); + test_rasterize_rect(); + test_keepout(); + test_place_fill(); + test_partition_cover(); + test_partition_equals_global(); + test_engine_endtoend(); + std::printf("\n%d checks, %d failures\n", g_checks, g_fails); + return g_fails == 0 ? 0 : 1; +} diff --git a/gpu_metal_fill/tools/gdsinfo.cpp b/gpu_metal_fill/tools/gdsinfo.cpp new file mode 100644 index 0000000..639d0b4 --- /dev/null +++ b/gpu_metal_fill/tools/gdsinfo.cpp @@ -0,0 +1,62 @@ +// Prints a per-(layer,datatype) polygon-count summary for one or more GDSII +// files. Fill emitted by this project uses datatype 10, so it is flagged as +// "FILL" to make it easy to confirm that fill happened. +#include +#include +#include +#include + +#include "metalfill/gdsii.hpp" +#include "metalfill/layermap.hpp" + +using namespace mf; + +namespace { + +// Best-effort human-readable name for the known layer numbers. +std::string layer_name(int layer) { + if (layer == kOdLayer) return "OD"; + if (layer == kPoLayer) return "PO"; + if (layer > kMetalBaseLayer && layer <= kMetalBaseLayer + kNumMetals) + return "M" + std::to_string(layer - kMetalBaseLayer); + return "?"; +} + +void dump(const char* path) { + Layout L = read_gds(path); + std::map, long> counts; + for (const auto& p : L.polygons) counts[{p.layer, p.datatype}]++; + + BBox b = L.bbox(); + double dppu = L.dbu_per_um(); + std::printf("=== %s ===\n", path); + std::printf(" cell '%s', %zu polygons, bbox %.2f x %.2f um\n", L.cell_name.c_str(), + L.polygons.size(), b.width() / dppu, b.height() / dppu); + std::printf(" %-6s %-6s %10s %s\n", "layer", "L/DT", "polygons", "kind"); + std::printf(" ------------------------------------------------\n"); + + long design = 0, fill = 0; + for (const auto& kv : counts) { + int layer = kv.first.first, dt = kv.first.second; + bool is_fill = (dt == kFillDatatype); + if (is_fill) fill += kv.second; + else design += kv.second; + char ld[32]; + std::snprintf(ld, sizeof(ld), "%d/%d", layer, dt); + std::printf(" %-6s %-6s %10ld %s\n", layer_name(layer).c_str(), ld, kv.second, + is_fill ? "FILL" : "design"); + } + std::printf(" ------------------------------------------------\n"); + std::printf(" design=%ld fill=%ld total=%ld\n\n", design, fill, design + fill); +} + +} // namespace + +int main(int argc, char** argv) { + if (argc < 2) { + std::printf("usage: gdsinfo file1.gds [file2.gds ...]\n"); + return 2; + } + for (int i = 1; i < argc; ++i) dump(argv[i]); + return 0; +} diff --git a/gpu_metal_fill/tools/make_dummy_gds.cpp b/gpu_metal_fill/tools/make_dummy_gds.cpp new file mode 100644 index 0000000..44dc7f2 --- /dev/null +++ b/gpu_metal_fill/tools/make_dummy_gds.cpp @@ -0,0 +1,67 @@ +// Generates a dummy GDSII with FEOL base layers (OD, PO) and BEOL metals +// (M1..M14). Each 20um window is given a deterministic, spatially varying +// existing density so the fill engine has real work to do (some windows below +// the min-density target, some already dense, with gradients across the die). +#include +#include +#include +#include + +#include "metalfill/gdsii.hpp" +#include "metalfill/layermap.hpp" + +using namespace mf; + +int main(int argc, char** argv) { + std::string out = "dummy.gds"; + int windows = 5; // 5x5 windows -> 100um die + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a == "-o" && i + 1 < argc) + out = argv[++i]; + else if (a == "-n" && i + 1 < argc) + windows = std::atoi(argv[++i]); + else if (a == "-h" || a == "--help") { + std::cout << "usage: make_dummy_gds [-o out.gds] [-n windows_per_side]\n"; + return 0; + } + } + + Layout L; + L.lib_name = "METALFILL"; + L.cell_name = "TOP"; + const double dppu = L.dbu_per_um(); // 1000 dbu / um + const dbu win = static_cast(20.0 * dppu); // 20um window + + auto rules = default_layermap(); + int layer_index = 0; + for (const auto& r : rules) { + for (int j = 0; j < windows; ++j) { + for (int i = 0; i < windows; ++i) { + // Spatially varying density, offset per layer so stacks differ. + int phase = (i + j + layer_index) % 9; + double f = 0.04 + 0.06 * phase; // 0.04 .. 0.52 + if (f < 0.0) f = 0.0; + if (f > 0.85) f = 0.85; + double s = std::sqrt(f) * win; // side of a centered square + dbu wx0 = static_cast(i) * win; + dbu wy0 = static_cast(j) * win; + dbu off = static_cast((win - s) / 2.0); + dbu x0 = wx0 + off; + dbu y0 = wy0 + off; + dbu x1 = x0 + static_cast(s); + dbu y1 = y0 + static_cast(s); + if (x1 > x0 && y1 > y0) + L.polygons.push_back(make_rect(r.layer, r.datatype, x0, y0, x1, y1)); + } + } + ++layer_index; + } + + write_gds(out, L); + BBox b = L.bbox(); + std::cout << "wrote " << out << ": " << L.polygons.size() << " polygons, die " + << (b.width() / dppu) << " x " << (b.height() / dppu) << " um, " << rules.size() + << " layers\n"; + return 0; +} diff --git a/gpu_metal_fill/tools/make_gpu_block.cpp b/gpu_metal_fill/tools/make_gpu_block.cpp new file mode 100644 index 0000000..105c9e1 --- /dev/null +++ b/gpu_metal_fill/tools/make_gpu_block.cpp @@ -0,0 +1,161 @@ +// Generates a synthetic but realistic "GPU block" GDSII to exercise the fill +// engine. It is NOT a real design -- it just reproduces the *floorplan-level +// density structure* a GPU block would have so that fill has varied work: +// +// * A grid of SM (streaming-multiprocessor) tiles. Each tile has two SRAM +// macros (register file + shared memory) and a standard-cell logic region. +// * SRAM macros: very dense on OD/PO/M1..M3 (bitcell arrays), open above. +// * Logic regions: medium-density cell rows (OD/PO) + local routing (M1..M3), +// with per-tile variation. +// * Routing channels between tiles: bus routing on mid metals (M4..M8). +// * Global clock/spine routes on M9/M10. +// * A regular power grid (wide straps) on the top metals (M11..M14), which is +// sparse (~15-20%) and therefore needs fill up to the min-density target. +// +// GDS layers follow metalfill/layermap.hpp: OD=1, PO=2, M1..M14=10..23, dt 0. +#include +#include +#include +#include +#include + +#include "metalfill/gdsii.hpp" +#include "metalfill/layermap.hpp" + +using namespace mf; + +namespace { + +struct Gen { + Layout L; + double dppu; + uint64_t s = 0x9e3779b97f4a7c15ULL; + + dbu um(double v) { return static_cast(std::llround(v * dppu)); } + uint32_t rnd() { + s = s * 6364136223846793005ULL + 1442695040888963407ULL; + return static_cast(s >> 33); + } + double frand() { return (rnd() % 100000) / 100000.0; } + + void rect(int layer, dbu x0, dbu y0, dbu x1, dbu y1) { + if (x1 > x0 && y1 > y0) L.polygons.push_back(make_rect(layer, 0, x0, y0, x1, y1)); + } + // Parallel stripes filling a rectangle. density ~= width/pitch. + void stripes(int layer, dbu x0, dbu y0, dbu x1, dbu y1, dbu pitch, dbu width, bool vertical) { + if (pitch <= 0) return; + if (vertical) { + for (dbu x = x0; x + width <= x1; x += pitch) rect(layer, x, y0, x + width, y1); + } else { + for (dbu y = y0; y + width <= y1; y += pitch) rect(layer, x0, y, x1, y + width); + } + } + // A dense macro: bitcell-like stripes on OD/PO/M1..M3. A guard ring of a + // couple of microns is left inside so density tapers toward the edge (as in + // a real macro placement), which keeps the density gradient fillable. + void macro(dbu x0, dbu y0, dbu x1, dbu y1) { + stripes(kOdLayer, x0, y0, x1, y1, um(0.5), um(0.28), false); // ~56% + stripes(kPoLayer, x0, y0, x1, y1, um(0.5), um(0.24), true); // ~48% + stripes(metal_layer(1), x0, y0, x1, y1, um(0.4), um(0.20), true); // ~50% + stripes(metal_layer(2), x0, y0, x1, y1, um(0.4), um(0.18), false); // ~45% + stripes(metal_layer(3), x0, y0, x1, y1, um(0.6), um(0.24), true); // ~40% + } + // Standard-cell logic region: rows of active + poly gates, local routing. + void logic(dbu x0, dbu y0, dbu x1, dbu y1, double lo) { + // Cell rows: OD islands per row, poly gates crossing them. + dbu row = um(1.2); + for (dbu y = y0; y + um(0.7) <= y1; y += row) { + for (dbu x = x0; x + um(1.0) <= x1; x += um(1.4)) { + if (frand() < lo + 0.35) rect(kOdLayer, x, y, x + um(0.9), y + um(0.6)); + } + } + stripes(kPoLayer, x0, y0, x1, y1, um(0.9), um(0.18), true); // poly gates + // Local routing, density varies per region. + stripes(metal_layer(1), x0, y0, x1, y1, um(0.6), um(0.2 + 0.15 * lo), true); + stripes(metal_layer(2), x0, y0, x1, y1, um(0.8), um(0.24 + 0.2 * lo), false); + stripes(metal_layer(3), x0, y0, x1, y1, um(1.0), um(0.24 + 0.2 * lo), true); + } +}; + +} // namespace + +int main(int argc, char** argv) { + std::string out = "gpu_block.gds"; + int sm = 3; // SM grid is sm x sm + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a == "-o" && i + 1 < argc) out = argv[++i]; + else if (a == "-n" && i + 1 < argc) sm = std::atoi(argv[++i]); + else if (a == "-h" || a == "--help") { + std::cout << "usage: make_gpu_block [-o out.gds] [-n sm_grid]\n"; + return 0; + } + } + + Gen g; + g.dppu = g.L.dbu_per_um(); + g.L.lib_name = "GPU_BLOCK"; + g.L.cell_name = "GPU_TOP"; + + const dbu margin = g.um(8); + const dbu tile = g.um(56); + const dbu chan = g.um(6); + const dbu step = tile + chan; + const dbu die = margin * 2 + sm * tile + (sm - 1) * chan; + + // ---- SM tiles: macros + logic ---------------------------------------- + for (int ty = 0; ty < sm; ++ty) { + for (int tx = 0; tx < sm; ++tx) { + dbu ox = margin + tx * step; + dbu oy = margin + ty * step; + // Two SRAM macros in the top half (register file + shared memory). + dbu mac_h = g.um(24); + dbu half = tile / 2; + g.macro(ox + g.um(1), oy + tile - mac_h - g.um(1), ox + half - g.um(1), oy + tile - g.um(1)); + g.macro(ox + half + g.um(1), oy + tile - mac_h - g.um(1), ox + tile - g.um(1), + oy + tile - g.um(1)); + // Logic occupies the bottom part; density varies per tile. + double lo = 0.30 + 0.4 * g.frand(); + g.logic(ox + g.um(1), oy + g.um(1), ox + tile - g.um(1), oy + tile - mac_h - g.um(2), lo); + } + } + + // ---- Routing channels: bus routing on mid metals --------------------- + for (int t = 1; t < sm; ++t) { + dbu cx = margin + t * step - chan; + dbu cy = margin + t * step - chan; + for (int m = 4; m <= 8; ++m) { + bool vert = (m % 2 == 0); + // vertical channel (routes run vertically) and horizontal channel + g.stripes(metal_layer(m), cx, margin, cx + chan, die - margin, g.um(0.8), g.um(0.4), + vert); + g.stripes(metal_layer(m), margin, cy, die - margin, cy + chan, g.um(0.8), g.um(0.4), + !vert); + } + } + + // ---- Global clock / spine on M9, M10 --------------------------------- + for (int k = 0; k < sm; ++k) { + dbu c = margin + k * step + tile / 2; + g.rect(metal_layer(9), c - g.um(1), margin, c + g.um(1), die - margin); + g.rect(metal_layer(10), margin, c - g.um(1), die - margin, c + g.um(1)); + } + + // ---- Power grid on top metals (wide, sparse straps) ------------------ + // Vertical on M12/M14, horizontal on M11/M13; ~4um straps on a 24um pitch. + for (dbu x = margin; x + g.um(4) <= die - margin; x += g.um(24)) { + g.rect(metal_layer(12), x, margin, x + g.um(4), die - margin); + g.rect(metal_layer(14), x, margin, x + g.um(4), die - margin); + } + for (dbu y = margin; y + g.um(4) <= die - margin; y += g.um(24)) { + g.rect(metal_layer(11), margin, y, die - margin, y + g.um(4)); + g.rect(metal_layer(13), margin, y, die - margin, y + g.um(4)); + } + + write_gds(out, g.L); + BBox b = g.L.bbox(); + std::cout << "wrote " << out << ": " << g.L.polygons.size() << " polygons, GPU block " + << (b.width() / g.dppu) << " x " << (b.height() / g.dppu) << " um, " << (sm * sm) + << " SM tiles\n"; + return 0; +} diff --git a/gpu_metal_fill/tools/render_layer.cpp b/gpu_metal_fill/tools/render_layer.cpp new file mode 100644 index 0000000..466ede4 --- /dev/null +++ b/gpu_metal_fill/tools/render_layer.cpp @@ -0,0 +1,116 @@ +// Renders one layer of a GDSII to a PPM image: existing geometry, dummy fill, +// and the recursive-partition (quadtree) core boundaries. Useful for eyeballing +// what the fill engine produced. +#include +#include +#include +#include +#include +#include +#include + +#include "metalfill/engine.hpp" +#include "metalfill/gdsii.hpp" +#include "metalfill/layermap.hpp" +#include "metalfill/partition.hpp" +#include "metalfill/raster.hpp" + +using namespace mf; + +struct Img { + int w, h; + std::vector px; // RGB + Img(int w_, int h_) : w(w_), h(h_), px(size_t(w_) * h_ * 3, 255) {} + void set(int x, int y, uint8_t r, uint8_t g, uint8_t b) { + if (x < 0 || y < 0 || x >= w || y >= h) return; + size_t i = (size_t(y) * w + x) * 3; + px[i] = r; + px[i + 1] = g; + px[i + 2] = b; + } + void hline(int x0, int x1, int y, uint8_t r, uint8_t g, uint8_t b) { + for (int x = x0; x <= x1; ++x) set(x, y, r, g, b); + } + void vline(int x, int y0, int y1, uint8_t r, uint8_t g, uint8_t b) { + for (int y = y0; y <= y1; ++y) set(x, y, r, g, b); + } + void write_ppm(const std::string& path) { + std::ofstream os(path, std::ios::binary); + os << "P6\n" << w << " " << h << "\n255\n"; + os.write(reinterpret_cast(px.data()), px.size()); + } +}; + +int main(int argc, char** argv) { + std::string in, out = "layer.ppm"; + int target_layer = metal_layer(3); // M3 by default + int px_target = 900; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a == "-i" && i + 1 < argc) in = argv[++i]; + else if (a == "-o" && i + 1 < argc) out = argv[++i]; + else if (a == "-l" && i + 1 < argc) target_layer = std::atoi(argv[++i]); + else if (a == "-p" && i + 1 < argc) px_target = std::atoi(argv[++i]); + } + if (in.empty()) { std::fprintf(stderr, "usage: render_layer -i in.gds -o out.ppm [-l layer]\n"); return 2; } + + Layout L = read_gds(in); + BBox area = L.bbox(); + if (!area.valid()) { std::fprintf(stderr, "empty layout\n"); return 1; } + + dbu cell = std::max(area.width() / px_target, 1); + Grid ge = make_grid(area, cell), gf = make_grid(area, cell); + rasterize(ge, L.polygons, target_layer, 0); + rasterize(gf, L.polygons, target_layer, kFillDatatype); + + int W = ge.nx, H = ge.ny; + Img img(W, H); + + auto to_px = [&](dbu x, dbu y, int& ix, int& iy) { + ix = int((x - area.xmin) / cell); + iy = H - 1 - int((y - area.ymin) / cell); + }; + + // Light window grid every 20um. + dbu win = static_cast(20.0 * L.dbu_per_um()); + for (dbu x = area.xmin; x <= area.xmax; x += win) { + int ix, iy; + to_px(x, area.ymin, ix, iy); + img.vline(ix, 0, H - 1, 235, 235, 235); + } + for (dbu y = area.ymin; y <= area.ymax; y += win) { + int ix, iy; + to_px(area.xmin, y, ix, iy); + img.hline(0, W - 1, iy, 235, 235, 235); + } + + // Existing geometry (navy) and fill (orange). + for (int y = 0; y < H; ++y) + for (int x = 0; x < W; ++x) { + int row = H - 1 - y; + if (ge.at(x, y)) img.set(x, row, 30, 60, 150); + else if (gf.at(x, y)) img.set(x, row, 250, 160, 60); + } + + // Recursive-partition (quadtree) core boundaries in red. + dbu align = lcm_dbu(win, win); + PartitionConfig pc; + pc.anchor_x = area.xmin; pc.anchor_y = area.ymin; + pc.align_x = align; pc.align_y = align; + pc.margin = align; pc.max_leaf = 2; pc.clip = area; + for (const auto& part : partition_recursive(area, pc)) { + int x0, y0, x1, y1; + to_px(part.core.xmin, part.core.ymin, x0, y0); + to_px(part.core.xmax, part.core.ymax, x1, y1); + if (x1 < x0) std::swap(x0, x1); + if (y1 < y0) std::swap(y0, y1); + img.hline(x0, x1, y0, 220, 30, 30); + img.hline(x0, x1, y1, 220, 30, 30); + img.vline(x0, y0, y1, 220, 30, 30); + img.vline(x1, y0, y1, 220, 30, 30); + } + + img.write_ppm(out); + std::printf("wrote %s (%dx%d) layer=%d\n", out.c_str(), W, H, target_layer); + return 0; +} diff --git a/gpu_metal_fill/tools/run_fill.cpp b/gpu_metal_fill/tools/run_fill.cpp new file mode 100644 index 0000000..7a01470 --- /dev/null +++ b/gpu_metal_fill/tools/run_fill.cpp @@ -0,0 +1,58 @@ +// Reads a GDSII, runs recursive-partitioned FEOL/BEOL metal fill, writes the +// filled GDSII and a text report. +#include +#include +#include +#include + +#include "metalfill/engine.hpp" +#include "metalfill/gdsii.hpp" +#include "metalfill/layermap.hpp" + +using namespace mf; + +int main(int argc, char** argv) { + std::string in, out = "filled.gds", report; + EngineConfig cfg; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if ((a == "-i" || a == "--in") && i + 1 < argc) + in = argv[++i]; + else if ((a == "-o" || a == "--out") && i + 1 < argc) + out = argv[++i]; + else if ((a == "-r" || a == "--report") && i + 1 < argc) + report = argv[++i]; + else if (a == "--iters" && i + 1 < argc) + cfg.max_iterations = std::atoi(argv[++i]); + else if (a == "--gpu") + cfg.prefer_gpu = true; + else if (a == "-h" || a == "--help") { + std::cout << "usage: run_fill -i in.gds [-o filled.gds] [-r report.txt] " + "[--iters N] [--gpu]\n"; + return 0; + } else if (in.empty()) { + in = a; + } + } + if (in.empty()) { + std::cerr << "error: no input GDS (use -i in.gds)\n"; + return 2; + } + + Layout layout = read_gds(in); + std::cout << "read " << in << ": " << layout.polygons.size() << " polygons\n"; + + auto rules = default_layermap(); + FillSummary summary = run_fill(layout, rules, cfg); + + write_gds(out, layout); + std::string rpt = format_report(summary); + std::cout << rpt; + std::cout << "wrote filled GDS: " << out << " (" << layout.polygons.size() << " polygons)\n"; + if (!report.empty()) { + std::ofstream f(report); + f << rpt; + std::cout << "wrote report: " << report << "\n"; + } + return summary.total_violations() == 0 ? 0 : 1; +}