Module 99 min

Hierarchy: Ungroup, Flatten, and Boundary Optimization

When to dissolve module boundaries and when to keep them

RTL is written in a hierarchy of modules, but the synthesis tool does not have to respect that hierarchy in the gate-level netlist. Deciding which boundaries to keep and which to dissolve is one of the highest-leverage choices in a real synthesis flow, and it directly trades quality of results against runtime and reuse.

Why boundaries cost you timing

By default a module boundary is a wall: the tool will not optimize logic across it. A constant driven into a submodule cannot be propagated inside, a chain of inverters straddling the boundary cannot be merged, and the tool cannot resize a gate on one side to fix a path on the other. Those missed optimizations are exactly what boundary optimization and ungrouping recover.

The main controls

  • Boundary optimization (set_boundary_optimization): keeps the hierarchy but lets the tool push constants through ports, remove unconnected or unused ports, and invert logic across the boundary.
  • Ungroup (ungroup, or set_ungroup on an instance): removes one level of hierarchy so the parent and child logic optimize together, while higher levels stay intact.
  • Flatten (ungroup -all -flatten): dissolves all hierarchy into one sea of gates for maximum cross-boundary optimization.
  • Auto-ungroup: the compile can dissolve small hierarchies on its own based on size thresholds, so tiny glue modules do not block optimization.
ApproachQoRRuntime / memoryBest for
Hierarchical compileGood locallyLowest, scalableLarge SoCs, reuse, parallel work
Selective ungroupBetter on hot blocksModerateCritical paths crossing boundaries
Full flattenBest raw timing/areaHighest, can blow upSmall, timing-critical blocks

Choosing in practice: on a large SoC you compile hierarchically so the run fits in memory, finishes in reasonable time, and lets blocks be reused and worked on in parallel. You then ungroup only the specific small modules that sit on a critical path or that are too small to justify their own boundary. Full flatten is reserved for compact, timing-critical blocks where you can afford the cost.

Pro tip

the senior framing is that hierarchy is a budgeting and reuse tool, not a quality tool. Keep boundaries where you need a clean interface or reuse, and selectively ungroup the small modules that artificially split a critical path. Flattening everything is rarely the right answer at SoC scale because the run stops being tractable.

Watch out

ungrouping and flattening rename instances and destroy the hierarchical paths that your SDC, UPF, and analysis scripts may reference. Names like top/cpu/alu/u_add can disappear, breaking get_cells patterns, exceptions, and back-annotation. Decide your hierarchy strategy early and keep critical interface boundaries fixed so downstream constraints stay valid.