Kahn's BFS iinitial implementation- still uses batch, but better than level. version 1
Signed-off-by: dsengupta0628 <dsengupta@precisioninno.com>
This commit is contained in:
parent
43177bba8f
commit
0f939c2a9d
|
|
@ -0,0 +1,405 @@
|
|||
# Kahn's Algorithm BFS for OpenSTA: Functional Specification
|
||||
|
||||
## 1. Motivation
|
||||
|
||||
OpenSTA's `BfsIterator::visitParallel()` processes the timing graph level by level, inserting a thread barrier (`dispatch_queue_->finishTasks()`) between every level. When a level contains few vertices, most threads sit idle at the barrier while waiting for the rest of the level to complete. This is the dominant source of parallel inefficiency in timing analysis for real designs with uneven level populations.
|
||||
|
||||
Kahn's algorithm removes the per-level barrier by processing each vertex as soon as all its predecessors are complete, allowing vertices at different levels to execute concurrently.
|
||||
|
||||
## 2. Functional Specification
|
||||
|
||||
### 2.1 Toggle and Predicate Setup
|
||||
|
||||
Kahn's is controlled by two settings on each `BfsIterator`:
|
||||
|
||||
```cpp
|
||||
// 1. Provide the edge filter Kahn's uses for discovery.
|
||||
// This is separate from search_pred_ used by the original BFS.
|
||||
iterator->setKahnPred(search_adj);
|
||||
|
||||
// 2. Enable Kahn's for visitParallel.
|
||||
iterator->setUseKahns(true);
|
||||
```
|
||||
|
||||
Both are required. If `kahn_pred_` is null when `use_kahns_` is true, the iterator falls back to the original level-based BFS silently.
|
||||
|
||||
Default for both is off/null (original behavior). The toggle affects only `visitParallel()`; the sequential `visit()`, `hasNext()`/`next()`, and `enqueue()` APIs are unchanged.
|
||||
|
||||
### 2.2 Why Kahn's Needs Its Own Predicate
|
||||
|
||||
In the original BFS, edge filtering happens **inside the visitor** at call time:
|
||||
|
||||
```
|
||||
visitor->visit(vertex)
|
||||
└─ enqueueAdjacentVertices(vertex, adj_pred_) ← visitor provides the filter
|
||||
```
|
||||
|
||||
The BFS iterator itself never decides which edges to follow -- the visitor does, one vertex at a time.
|
||||
|
||||
Kahn's algorithm cannot work this way. It must discover the **entire active subgraph upfront** (before any visitor runs) to compute in-degrees. This discovery needs an edge filter to know which edges to follow. The iterator's own `search_pred_` is often null (the arrival iterator is constructed with `nullptr` because the visitor was always expected to provide the filter).
|
||||
|
||||
`kahn_pred_` solves this by giving Kahn's its own dedicated filter, set once during construction, without changing how `search_pred_` or the visitor works.
|
||||
|
||||
In practice, `Search.cc` wires it up at construction:
|
||||
|
||||
```cpp
|
||||
// Search constructor:
|
||||
arrival_iter_->setKahnPred(search_adj_);
|
||||
required_iter_->setKahnPred(search_adj_);
|
||||
```
|
||||
|
||||
### 2.3 Behavioral Contract
|
||||
|
||||
When enabled, `visitParallel(to_level, visitor)` must:
|
||||
|
||||
1. Visit exactly the same set of vertices as the original level-based BFS.
|
||||
2. Visit each vertex exactly once.
|
||||
3. Visit every vertex only after all its predecessors (in the BFS-direction DAG) have been visited and their results are visible.
|
||||
4. Call `visitor->visit(vertex)` in a thread-safe manner (one thread per vertex, thread-local visitor copies).
|
||||
5. Call `visitor->levelFinished()` once after all vertices are processed.
|
||||
6. Leave the BFS queue in a consistent state (processed levels cleared, remaining levels tracked).
|
||||
7. Respect the `to_level` bound -- vertices beyond it remain queued for future calls.
|
||||
|
||||
### 2.4 Scope
|
||||
|
||||
The implementation is integrated into the existing `BfsIterator` class hierarchy:
|
||||
|
||||
- `BfsFwdIterator` -- forward arrival propagation (out-edges)
|
||||
- `BfsBkwdIterator` -- backward required-time propagation (in-edges)
|
||||
|
||||
Both directions are supported through the polymorphic `kahnForEachSuccessor()` virtual method.
|
||||
|
||||
## 3. Why the Graph Is a DAG
|
||||
|
||||
Kahn's algorithm requires a directed acyclic graph. Within a single `visitParallel()` call, the active graph is guaranteed acyclic because:
|
||||
|
||||
| Cycle Source | How It's Broken | Where |
|
||||
|---|---|---|
|
||||
| Flip-flop feedback (Q -> ... -> D) | D inputs are timing endpoints; clk-to-Q starts new propagation. `SearchAdj` skips `latchDtoQ` and timing-check edges. | `Search.cc:127-131`, `Search.cc:178-186` |
|
||||
| Latch D-to-Q | Explicitly excluded by `SearchThru::searchThru()` and `SearchAdj::searchThru()`. Convergence handled by multi-pass outer loop in `Search::findAllArrivals()`. | `Search.cc:130`, `Search.cc:1004-1012` |
|
||||
| Combinational loops | Levelizer DFS detects back edges and marks them `isDisabledLoop()`. All BFS predicates skip disabled-loop edges. | `Levelize.cc:232-330`, `Levelize.cc:428-446` |
|
||||
|
||||
## 4. Algorithm
|
||||
|
||||
### 4.1 Overview
|
||||
|
||||
```
|
||||
visitParallel(to_level, visitor):
|
||||
if thread_count == 1 → sequential visit() (unchanged)
|
||||
if !use_kahns_ || !kahn_pred_ → original level-based parallel BFS (unchanged)
|
||||
else → Kahn's three-phase algorithm:
|
||||
|
||||
Phase 1+2: Discovery + In-Degree Counting (single-threaded)
|
||||
Phase 3: Batch-Dispatch Parallel Traversal (multi-threaded)
|
||||
```
|
||||
|
||||
### 4.2 Phase 1+2: Discovery + In-Degree Counting
|
||||
|
||||
Single-threaded BFS from seed vertices (those already in the level queue). For each vertex discovered:
|
||||
|
||||
1. Assign in-degree = 0 for seeds, in-degree = count of active predecessors for discovered successors.
|
||||
2. Record vertex in the active set.
|
||||
3. Set `bfsInQueue` flag to prevent `enqueue()` from re-adding during Phase 3.
|
||||
|
||||
Data structures:
|
||||
- `in_degree_init`: flat `std::vector<int>` indexed by `graph_->id(vertex)`. Value -1 = not active, >= 0 = in-degree count. Grows dynamically if vertex IDs exceed initial capacity (see Section 6.1).
|
||||
- `active_vertices`: list of all discovered vertices for iteration.
|
||||
- Both are persistent across calls via `KahnState` to avoid re-allocation (see Section 5.4).
|
||||
|
||||
The discovery uses `kahn_pred_` -- the same `SearchAdj` filter used by the arrival and required paths -- ensuring identical edge filtering to the original BFS.
|
||||
|
||||
### 4.3 Phase 3: Batch-Dispatch Parallel Traversal
|
||||
|
||||
```
|
||||
ready_batch = {vertices with in_degree == 0}
|
||||
|
||||
while ready_batch is not empty:
|
||||
next_ready = {}
|
||||
|
||||
if batch is small (< thread_count):
|
||||
process single-threaded
|
||||
else:
|
||||
for each vertex in ready_batch:
|
||||
dispatch_queue_->dispatch(lambda(tid):
|
||||
visitor_copy[tid]->visit(vertex)
|
||||
for each successor of vertex:
|
||||
atomic decrement in_degree[successor]
|
||||
if in_degree reached 0:
|
||||
lock; next_ready.push_back(successor)
|
||||
)
|
||||
dispatch_queue_->finishTasks()
|
||||
|
||||
ready_batch.swap(next_ready)
|
||||
```
|
||||
|
||||
Key properties:
|
||||
- One task dispatched per vertex -- `DispatchQueue` handles load balancing across its thread pool.
|
||||
- `finishTasks()` uses condition_variable internally (no spin-wait).
|
||||
- Successor in-degree decrements use `std::memory_order_acq_rel` to ensure predecessor writes are visible.
|
||||
- Newly-ready vertices are collected into `next_ready` under a mutex, then swapped into the next batch.
|
||||
|
||||
### 4.4 Cleanup
|
||||
|
||||
After traversal:
|
||||
- Processed levels are cleared from the `LevelQueue`.
|
||||
- `first_level_` / `last_level_` are recalculated via `resetLevelBounds()` to track any remaining queued vertices (e.g., those beyond `to_level`).
|
||||
- Active vertex IDs are saved in `KahnState::prev_ids` for efficient reset on the next call.
|
||||
|
||||
## 5. Implementation Details
|
||||
|
||||
### 5.1 Files Modified
|
||||
|
||||
| File | Changes |
|
||||
|---|---|
|
||||
| `include/sta/Bfs.hh` | Added `<functional>`, `<memory>`. Added `kahnForEachSuccessor` pure virtual, `resetLevelBounds`, `KahnState` forward decl + `unique_ptr` member, `use_kahns_` flag, `kahn_pred_` pointer, and public accessors. |
|
||||
| `search/Bfs.cc` | Added `<atomic>`. Defined `KahnState` struct. Rewrote `visitParallel` with three branches (sequential / level-based / Kahn's). Added `kahnForEachSuccessor` overrides for Fwd (out-edges) and Bkwd (in-edges). Added `resetLevelBounds`. |
|
||||
| `search/Search.cc` | Added two lines in `Search` constructor to wire up `kahn_pred_` on both arrival and required iterators: `arrival_iter_->setKahnPred(search_adj_)` and `required_iter_->setKahnPred(search_adj_)`. |
|
||||
|
||||
### 5.2 Class Hierarchy
|
||||
|
||||
```
|
||||
BfsIterator (base)
|
||||
├─ kahnForEachSuccessor() = 0 [pure virtual]
|
||||
├─ resetLevelBounds()
|
||||
├─ KahnState (persistent arrays, pimpl)
|
||||
├─ use_kahns_ flag
|
||||
├─ kahn_pred_ (SearchPred* for Kahn's discovery, separate from search_pred_)
|
||||
│
|
||||
├─ BfsFwdIterator
|
||||
│ └─ kahnForEachSuccessor: iterates out-edges with
|
||||
│ searchFrom/searchThru/searchTo via kahn_pred_
|
||||
│
|
||||
└─ BfsBkwdIterator
|
||||
└─ kahnForEachSuccessor: iterates in-edges with
|
||||
searchTo/searchThru/searchFrom via kahn_pred_
|
||||
```
|
||||
|
||||
### 5.3 KahnState (Persistent Storage)
|
||||
|
||||
```cpp
|
||||
struct BfsIterator::KahnState {
|
||||
std::vector<int> in_degree_init; // flat array, indexed by VertexId
|
||||
std::unique_ptr<std::atomic<int>[]> in_degree; // atomic copy for parallel phase
|
||||
size_t in_degree_size = 0;
|
||||
std::vector<VertexId> prev_ids; // IDs to reset on next call
|
||||
};
|
||||
```
|
||||
|
||||
- Allocated lazily on first Kahn's call.
|
||||
- `in_degree_init` grows dynamically (never shrinks). Only touched entries are reset between calls via `prev_ids`.
|
||||
- `in_degree` (atomic array) is reallocated only when the max vertex ID grows.
|
||||
|
||||
### 5.4 Memory Ordering
|
||||
|
||||
| Operation | Ordering | Rationale |
|
||||
|---|---|---|
|
||||
| `in_degree_init` writes (discovery) | Non-atomic | Single-threaded phase; `dispatch()` provides happens-before. |
|
||||
| `in_degree[].store()` (setup) | `relaxed` | Single-threaded; `dispatch()` provides happens-before. |
|
||||
| `in_degree[].fetch_sub()` (worker) | `acq_rel` | Last predecessor's decrement synchronizes all prior arrival writes to the successor's reader thread. |
|
||||
| `total_visited.fetch_add()` | `relaxed` | Counter read only after `finishTasks()` barrier. |
|
||||
|
||||
### 5.5 Interaction with enqueue() During visit()
|
||||
|
||||
`ArrivalVisitor::visit()` calls `enqueueAdjacentVertices()` which calls `enqueue()`. During Kahn's:
|
||||
- Active vertices have `bfsInQueue` set during discovery -> `enqueue()` is a no-op (flag already set).
|
||||
- Vertices beyond `to_level` are not in the active set -> `enqueue()` adds them to the `LevelQueue` normally for future passes.
|
||||
|
||||
### 5.6 kahnForEachSuccessor vs enqueueAdjacentVertices
|
||||
|
||||
These two methods have nearly identical edge-iteration logic (same predicates, same edge direction). They are intentionally kept as separate methods because:
|
||||
- `enqueueAdjacentVertices` is called millions of times in the non-Kahn's path. Wrapping it in `std::function` would add overhead to all BFS operations.
|
||||
- `kahnForEachSuccessor` accepts a `std::function<void(Vertex*)>` callback, used in both discovery and the worker. The `std::function` overhead is negligible relative to per-vertex computation.
|
||||
|
||||
## 6. Pitfalls and Bugs Found
|
||||
|
||||
### 6.1 ObjectTable Block-Based Vertex IDs
|
||||
|
||||
**Problem**: `graph_->vertexCount()` returns the **live** object count, but `graph_->id(vertex)` returns `(block_index << 7) + slot_index`. After vertex deletions, live count drops but blocks persist. A vertex in block 2 can have ID 260 even when only 79 vertices are alive.
|
||||
|
||||
**How we found it**: The `rmp.gcd_restructure.tcl` OpenROAD test crashed with a segfault. The rmp module deletes cells during restructuring, creating gaps between live vertex count and max vertex ID. Our in-degree array was sized to `vertexCount() + 1` and accessed out of bounds.
|
||||
|
||||
**Solution**: `in_degree_init` grows dynamically during discovery (`resize(vid + 128, -1)` when any ID exceeds current capacity). Worker lambdas include a bounds check (`sid < in_deg_size`). The atomic array is sized to `max_id + 1` after discovery completes.
|
||||
|
||||
**Note**: The other developer's implementation has the same latent bug (`graph_->vertexCount() + 1` sizing) but it hasn't manifested because their code only runs on the delay-calc path, which doesn't encounter the deletion pattern that rmp triggers.
|
||||
|
||||
### 6.2 Null Search Predicate on Arrival Iterator
|
||||
|
||||
**Problem**: The arrival BFS iterator is constructed with `search_pred_ = nullptr`:
|
||||
|
||||
```cpp
|
||||
arrival_iter_(new BfsFwdIterator(BfsIndex::arrival, nullptr, this))
|
||||
```
|
||||
|
||||
This is intentional in the original BFS -- the visitor provides its own predicate (`adj_pred_`) at call time via `enqueueAdjacentVertices(vertex, adj_pred_)`. The null `search_pred_` is never dereferenced.
|
||||
|
||||
Kahn's discovery phase needs a predicate upfront (before any visitor runs) to know which edges to follow. Using `search_pred_` directly caused a null pointer dereference.
|
||||
|
||||
**How we found it**: The `rmp.gcd_restructure.tcl` test crashed in `kahnForEachSuccessor` with `pred->searchFrom(vertex)` dereferencing null. Stack trace showed the call came from arrival propagation via `Search::findArrivals1`.
|
||||
|
||||
**Solution**: Introduced `kahn_pred_` -- a separate predicate pointer dedicated to Kahn's discovery and successor decrement. Set via `setKahnPred()` and wired up in the `Search` constructor:
|
||||
|
||||
```cpp
|
||||
arrival_iter_->setKahnPred(search_adj_);
|
||||
required_iter_->setKahnPred(search_adj_);
|
||||
```
|
||||
|
||||
If `kahn_pred_` is null when Kahn's is enabled, `visitParallel` falls back to the original level-based BFS. This ensures no crash even if a caller enables Kahn's without setting the predicate.
|
||||
|
||||
### 6.3 Memory Visibility Across Threads
|
||||
|
||||
**Problem**: When predecessor P finishes computing arrivals and successor S starts reading them, S must see P's writes.
|
||||
|
||||
**Original BFS**: `finishTasks()` between levels provides a full memory fence.
|
||||
|
||||
**Kahn's**: The `fetch_sub(1, memory_order_acq_rel)` on the in-degree counter creates the happens-before chain. When the last predecessor's decrement triggers S's readiness, all prior writes by all predecessors are visible to S's processing thread. The batch-dispatch model adds a `finishTasks()` barrier between batches as an additional fence.
|
||||
|
||||
### 6.4 "Arrivals Unchanged" Optimization
|
||||
|
||||
**Original BFS**: If `ArrivalVisitor::visit()` finds arrivals haven't changed, it skips `enqueueAdjacentVertices` -- fanout is not re-evaluated.
|
||||
|
||||
**Kahn's**: The discovery phase conservatively discovers ALL reachable vertices. Fanout in-degrees are decremented unconditionally after visit, regardless of whether arrivals changed. This means some vertices may be visited unnecessarily. They will find no change and produce no further effect.
|
||||
|
||||
**Impact**: Slightly more work for incremental updates where only a small subset changes. Correct for all cases.
|
||||
|
||||
### 6.5 Latch Multi-Pass Convergence
|
||||
|
||||
Latch D-to-Q edges are excluded from the BFS by search predicates. Latch convergence is handled by the outer multi-pass loop in `Search::findAllArrivals()` / `Search::findFilteredArrivals()`, which re-seeds latch Q outputs between `visitParallel` calls. Kahn's operates within a single `visitParallel` call and is orthogonal to this mechanism.
|
||||
|
||||
### 6.6 levelFinished() Callback
|
||||
|
||||
`VertexVisitor::levelFinished()` is a virtual hook called at level boundaries. No override exists in the codebase (base implementation is empty). With Kahn's, it is called once after all vertices are processed. If a future subclass relies on per-level callbacks, it would need adaptation.
|
||||
|
||||
## 7. Comparison with Other Developer's Approach
|
||||
|
||||
The other implementation (`BfsFwdInDegreeIterator`) in the alternate repository takes a different design approach. Key differences:
|
||||
|
||||
### 7.1 Architecture
|
||||
|
||||
| Aspect | Other Developer | Our Approach |
|
||||
|---|---|---|
|
||||
| Class design | New standalone `BfsFwdInDegreeIterator : StaState` | Integrated into existing `BfsIterator` with toggle |
|
||||
| Scope | Forward-only, delay calc (`GraphDelayCalc`) only | Forward + backward, any `visitParallel` caller |
|
||||
| Integration | Requires caller to use new class and call `computeInDegrees()` explicitly | Drop-in: `setKahnPred(pred)` + `setUseKahns(true)` on existing iterator |
|
||||
|
||||
### 7.2 Discovery
|
||||
|
||||
| Aspect | Other Developer | Our Approach |
|
||||
|---|---|---|
|
||||
| Strategy | Iterate ALL vertices + ALL edges (full graph) | BFS from seeds (active subgraph only) |
|
||||
| Cost | O(V_total + E_total) every call | O(V_active + E_active) per call |
|
||||
| Incremental variant | Yes (`computeInDegrees(invalid_delays)` with reachability pass) | Natural (seed-based discovery covers only dirty subgraph) |
|
||||
| Loop breaking | `to_vertex->level() >= vertex->level()` (ad-hoc) | `SearchPred::searchThru()` (matches Levelizer exactly) |
|
||||
|
||||
### 7.3 Parallelism
|
||||
|
||||
| Aspect | Other Developer | Our Approach |
|
||||
|---|---|---|
|
||||
| Dispatch model | Batch: dispatch all ready -> `finishTasks()` -> next batch | Same (adopted from their approach -- see Section 7.6) |
|
||||
| Ready queue | `std::vector<Vertex*>` with `swap()` | Same |
|
||||
| Newly-ready collection | `ready_lock_` mutex on `ready_` vector | `next_ready_lock` mutex on `next_ready` vector |
|
||||
|
||||
### 7.4 Thread Safety
|
||||
|
||||
| Aspect | Other Developer | Our Approach |
|
||||
|---|---|---|
|
||||
| Active-set check | `vertex->visited()` (non-atomic `bool`) -- **data race risk** | `in_degree_init[id] >= 0` (read-only during parallel phase) -- safe |
|
||||
| Edge dedup | `std::set<Edge*> processed_edges_` with **per-edge mutex lock** -- serialization bottleneck | Not needed (in-degrees computed correctly upfront) |
|
||||
| Vertex marking | `vertex->setVisited(true)` from worker threads | `vertex->setBfsInQueue()` (atomic field) |
|
||||
|
||||
### 7.5 Array Sizing
|
||||
|
||||
| Aspect | Other Developer | Our Approach |
|
||||
|---|---|---|
|
||||
| Size | `graph_->vertexCount() + 1` (fixed) | Dynamic growth during discovery + bounds checks |
|
||||
| Risk | **Same ObjectTable ID bug** -- IDs can exceed vertexCount after deletions. Latent bug that hasn't manifested because their code path (delay calc) doesn't trigger the deletion pattern. | Fixed (see Section 6.1) |
|
||||
|
||||
### 7.6 What We Adopted from Their Approach
|
||||
|
||||
Our initial implementation used a custom `KahnReadyQueue` with spin-wait workers (`std::this_thread::yield()`). Comparing with their approach revealed that dispatching one task per vertex into `DispatchQueue` and using `finishTasks()` as a batch barrier is significantly more efficient:
|
||||
|
||||
- `DispatchQueue` uses `condition_variable` for blocking -- no wasted CPU on spin-wait.
|
||||
- Natural load balancing -- the thread pool picks up work items automatically.
|
||||
- Simpler code -- no custom queue class needed.
|
||||
|
||||
This change cut our test suite overhead from 87s to 28s (vs 27s for original BFS).
|
||||
|
||||
### 7.7 Performance Comparison
|
||||
|
||||
| Approach | STA Regression (6109 tests) |
|
||||
|---|---|
|
||||
| Original level-based BFS | 27-30s |
|
||||
| Our Kahn's v1 (hash map + spin-wait) | 87s |
|
||||
| Our Kahn's v2 (dense array + spin-wait) | 42s |
|
||||
| Our Kahn's v3 (dense array + batch dispatch) | 28s |
|
||||
| Other developer (delay-calc only, separate class) | Reports ~45% speedup on large designs |
|
||||
|
||||
## 8. Test Plan and Results
|
||||
|
||||
### 8.1 OpenSTA Standalone Regression
|
||||
|
||||
```bash
|
||||
cd tools/OpenROAD/src/sta/build
|
||||
cmake .. && make -j$(nproc)
|
||||
ctest -j$(nproc)
|
||||
```
|
||||
|
||||
**Pass criteria**: All tests pass with `use_kahns_ = true`. Results must be bit-identical to `use_kahns_ = false`.
|
||||
|
||||
**Result**: **PASS** -- 6109/6109 tests pass with both settings.
|
||||
|
||||
### 8.2 OpenROAD Full Regression
|
||||
|
||||
```bash
|
||||
cd tools/OpenROAD/build
|
||||
cmake --build . -j$(nproc)
|
||||
ctest -j$(nproc)
|
||||
```
|
||||
|
||||
**Pass criteria**: All OpenROAD tests pass, including flows that modify the netlist between timing updates.
|
||||
|
||||
**Key test cases exercised**:
|
||||
- `rmp.gcd_restructure.tcl` -- restructure deletes cells, causing vertex ID gaps. This test originally crashed (Section 6.1, 6.2) and drove two bug fixes (dynamic array sizing and `kahn_pred_` separation).
|
||||
- `rsz.*` -- resizer modifies netlist incrementally between timing updates.
|
||||
- `cts.*` -- clock tree synthesis adds buffers and triggers re-timing.
|
||||
|
||||
**Result**: **PASS** -- all OpenROAD regressions pass after the two fixes.
|
||||
|
||||
### 8.3 Thread Count Sweep
|
||||
|
||||
```tcl
|
||||
set_thread_count 1 ;# Falls back to sequential visit()
|
||||
set_thread_count 2
|
||||
set_thread_count 4
|
||||
set_thread_count 8
|
||||
```
|
||||
|
||||
**Pass criteria**: Identical timing reports across all thread counts.
|
||||
|
||||
### 8.4 Toggle Consistency
|
||||
|
||||
```tcl
|
||||
# Run 1: use_kahns_ = false
|
||||
report_checks -digits 6 > results_original.rpt
|
||||
# Run 2: use_kahns_ = true
|
||||
report_checks -digits 6 > results_kahns.rpt
|
||||
# diff results_original.rpt results_kahns.rpt
|
||||
```
|
||||
|
||||
**Pass criteria**: Reports are identical.
|
||||
|
||||
### 8.5 Performance Expectations
|
||||
|
||||
| Scenario | Expectation |
|
||||
|---|---|
|
||||
| Small designs (< 10K vertices) | Kahn's within 10% of original (discovery overhead amortized) |
|
||||
| Large designs (> 100K vertices) with uneven levels | Kahn's faster due to barrier elimination |
|
||||
| Incremental updates (small active set) | Kahn's overhead proportional to active set, not total graph |
|
||||
| High thread counts (8-16 threads) | Kahn's scales better (no idle threads at level barriers) |
|
||||
|
||||
### 8.6 Stress Tests
|
||||
|
||||
- Design with a single long chain (worst case -- no parallelism, discovery overhead for zero benefit).
|
||||
- Design with many parallel chains (best case -- maximum parallel utilization).
|
||||
- Design with latches (multi-pass convergence must work correctly).
|
||||
- Rapid incremental updates (persistent KahnState reuse exercised).
|
||||
- Netlist modification flows: rmp, rsz, cts (exercises ObjectTable ID gaps and graph rebuilds).
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
KAHN'S ALGORITHM BFS FOR OPENSTA
|
||||
Functional Specification
|
||||
April 2026
|
||||
|
||||
|
||||
1. MOTIVATION
|
||||
|
||||
OpenSTA's parallel BFS traversal (visitParallel) processes vertices one level at a time. All threads must finish the current level before any thread can start the next. If a level has only a handful of vertices, most threads sit idle waiting for them to finish. In real designs, level sizes vary widely -- some levels have thousands of vertices and some have very few -- making this wait-at-every-level approach a significant bottleneck for multi-threaded timing analysis.
|
||||
|
||||
Kahn's algorithm is a classical method for topological traversal of a directed acyclic graph. It tracks how many unprocessed predecessors each vertex has (its "in-degree"). A vertex becomes ready as soon as its in-degree reaches zero -- meaning all the vertices it depends on have been processed. This is a natural fit for timing analysis: a vertex's arrival time depends only on its fanin, so it can be computed the moment all fanin arrivals are known, without waiting for unrelated vertices at the same level to finish.
|
||||
|
||||
|
||||
2. PROPOSED SOLUTION
|
||||
|
||||
Replace the per-level barrier model with Kahn's topological traversal. Instead of waiting for all vertices at level L to finish before starting level L+1, a vertex becomes eligible for processing as soon as every one of its predecessors has been processed. This allows vertices at different levels to execute concurrently, keeping threads busy.
|
||||
|
||||
The implementation is integrated into the existing BfsIterator class hierarchy as a runtime toggle, supporting both forward (arrival) and backward (required-time) propagation. The original level-based BFS remains the default and is always available as a fallback.
|
||||
|
||||
|
||||
3. ALGORITHM
|
||||
|
||||
The timing graph is already a DAG within each visitParallel() call: flip-flop feedback is broken at D inputs, latch D-to-Q edges are excluded by search predicates, and combinational loops are broken by the Levelizer's disabled-loop edges. This satisfies Kahn's requirement for an acyclic graph.
|
||||
|
||||
When Kahn's is enabled, visitParallel() proceeds in two stages:
|
||||
|
||||
Stage 1: Discovery and In-Degree Counting (single-threaded)
|
||||
|
||||
Starting from the seed vertices already in the BFS queue, a forward BFS discovers all reachable vertices following the same edge-filtering rules used by the original traversal. As each new vertex is discovered, its in-degree (number of active predecessors) is recorded in a flat array indexed by graph vertex ID. Seed vertices have in-degree zero.
|
||||
|
||||
Stage 2: Batch-Dispatch Parallel Traversal (multi-threaded)
|
||||
|
||||
All zero-in-degree vertices form the initial ready batch. The algorithm loops:
|
||||
|
||||
1. Dispatch one task per ready vertex into the existing DispatchQueue thread pool.
|
||||
2. Each task visits the vertex (computing arrivals or required times), then atomically decrements the in-degree of each successor. Any successor whose in-degree reaches zero is collected into the next batch.
|
||||
3. finishTasks() waits for all tasks in the current batch to complete.
|
||||
4. Swap in the next batch and repeat until no vertices remain.
|
||||
|
||||
Small batches (fewer vertices than threads) are processed single-threaded to avoid dispatch overhead. The DispatchQueue uses condition_variable internally, so there is no spin-wait or wasted CPU.
|
||||
|
||||
|
||||
4. IMPLEMENTATION DETAILS
|
||||
|
||||
Files modified:
|
||||
|
||||
include/sta/Bfs.hh -- Added kahnForEachSuccessor pure virtual method (forward follows out-edges, backward follows in-edges), persistent KahnState storage, use_kahns_ toggle, kahn_pred_ pointer for the discovery edge filter, and resetLevelBounds helper.
|
||||
|
||||
search/Bfs.cc -- Defined KahnState struct holding persistent in-degree arrays (reused across calls to avoid re-allocation). Added a third branch to visitParallel: single-threaded / original-parallel / Kahn's-parallel. Implemented kahnForEachSuccessor for both BfsFwdIterator and BfsBkwdIterator.
|
||||
|
||||
search/Search.cc -- Two lines in the Search constructor wire the Kahn's edge filter (SearchAdj) onto the arrival and required iterators.
|
||||
|
||||
Enabling Kahn's requires two calls on a BfsIterator:
|
||||
|
||||
iterator->setKahnPred(predicate); // edge filter for discovery
|
||||
iterator->setUseKahns(true); // enable Kahn's
|
||||
|
||||
The edge filter is separate from the iterator's existing search_pred_ because the original BFS never uses search_pred_ directly for arrivals -- the visitor provides its own filter at call time. Kahn's discovery runs before any visitor, so it needs the filter upfront. If the filter is null, visitParallel falls back to the original BFS.
|
||||
|
||||
Persistent state (KahnState) stores the in-degree arrays across calls. On the first call it allocates; on subsequent calls it resets only the entries touched previously, avoiding full re-initialization.
|
||||
|
||||
|
||||
5. INCREMENTAL TIMING UPDATES
|
||||
|
||||
OpenSTA supports incremental timing: when a cell is resized or an edge delay changes, only the affected vertices need to be re-evaluated instead of recomputing the whole graph. This is driven by Search.cc, which tracks dirty vertices in an "invalid arrivals" set and enqueues them as seeds before the next findArrivals call. Our implementation hooks into this existing mechanism without modification.
|
||||
|
||||
When Kahn's runs, the seed vertices in the BFS queue are exactly the dirty ones supplied by the incremental framework. The discovery stage walks forward from those seeds and finds the downstream subgraph that could be affected. Only that subgraph -- not the whole graph -- gets in-degrees computed and gets visited in Stage 2. For small updates (a few changed cells in a large design), the active set is a small fraction of the total graph, and the work is proportional to it.
|
||||
|
||||
There is one behavioral difference from the original BFS worth noting. The original stops propagating through a vertex whose arrivals did not change after re-evaluation; it skips the enqueue of its fanout. Our Kahn's implementation discovers the full reachable subgraph upfront and decrements in-degrees unconditionally, so every reachable vertex is visited.
|
||||
|
||||
The reason is fundamental to Kahn's algorithm: every active predecessor must decrement its successor's in-degree exactly once, otherwise the counter never reaches zero and the vertex stalls forever. If we skipped a decrement because "arrivals didn't change," a downstream vertex with multiple predecessors could be left waiting on a decrement that will never come -- even if its other predecessors did change and genuinely need to propagate.
|
||||
|
||||
The practical cost is that vertices whose arrivals did not change are still visited, but the visitor detects no change and no downstream updates happen. This is correct but slightly more eager than the original. It has not caused test failures or measurable overhead in any regression so far.
|
||||
|
||||
|
||||
6. COMPARISON WITH ALTERNATE IMPLEMENTATION
|
||||
|
||||
An alternate implementation (BfsFwdInDegreeIterator) in a separate repository takes a standalone-class approach used only for delay calculation.
|
||||
|
||||
Architecture: The alternate creates a separate class. Ours integrates into the existing BfsIterator with a toggle, supporting both forward and backward BFS across all callers.
|
||||
|
||||
Discovery cost: The alternate scans every vertex and edge in the entire graph to compute in-degrees -- O(V_total + E_total) where V_total is all vertices in the graph and E_total is all edges. Even if only a small portion needs re-timing, the full graph is walked. Ours starts from the dirty seed vertices and only walks the subgraph reachable from them -- O(V_active + E_active) where V_active and E_active are only the vertices and edges that actually need processing. For loop breaking, the alternate uses a raw level comparison (to_level >= from_level) to decide which edges to skip. Ours uses the same SearchAdj filter that the Levelizer and the rest of the BFS already use, so the set of skipped edges (disabled loops, latch D-to-Q, timing checks) is guaranteed to be consistent.
|
||||
|
||||
Thread safety: The alternate uses a non-atomic visited flag from worker threads (data race risk) and maintains a per-edge mutex-locked set for deduplication (serialization bottleneck). Ours uses a read-only array for active-set checks and computes in-degrees upfront so edge tracking is unnecessary.
|
||||
|
||||
What we adopted: The alternate's batch-dispatch model (one task per vertex into DispatchQueue with finishTasks barriers) proved far more efficient than our initial spin-wait worker design. Adopting it cut test overhead from 87s to 28s.
|
||||
|
||||
|
||||
7. FINDINGS FROM REGRESSIONS
|
||||
|
||||
Finding 1: Vertex IDs can exceed vertexCount() after deletions
|
||||
|
||||
The graph's ObjectTable stores vertices in blocks of 128. graph->id(vertex) returns (block_index * 128 + slot), which can be much larger than graph->vertexCount() (the live count) after cells are deleted. Sizing the in-degree array to vertexCount()+1 caused an out-of-bounds segfault during the rmp.gcd_restructure flow, which deletes cells during restructuring.
|
||||
|
||||
Resolution: The in-degree array now grows dynamically during discovery when any vertex ID exceeds current capacity. Worker threads include bounds checks. The alternate implementation has the same latent issue but has not encountered it because its code path does not trigger the deletion pattern.
|
||||
|
||||
Finding 2: The arrival iterator has a null search predicate
|
||||
|
||||
The arrival BFS iterator is constructed with search_pred = nullptr because the original BFS never uses it -- the visitor always provides the filter. Kahn's discovery used search_pred directly, causing a null-pointer crash during arrival propagation in the rmp flow.
|
||||
|
||||
Resolution: Introduced kahn_pred, a dedicated predicate for Kahn's discovery, wired to SearchAdj in the Search constructor. This keeps the original BFS path completely unchanged.
|
||||
|
||||
Both findings were caught by rmp.gcd_restructure.tcl and resolved without changing the original BFS behavior.
|
||||
|
||||
|
||||
8. PERFORMANCE
|
||||
|
||||
On the OpenSTA regression suite (6109 tests), Kahn's BFS runs at parity with the original level-based BFS (28s vs 27-30s). On small test designs the discovery stage overhead is negligible. On large designs with uneven level populations, barrier elimination should produce net speedups, particularly at high thread counts where the original BFS leaves threads idle.
|
||||
|
||||
|
||||
9. TEST RESULTS
|
||||
|
||||
OpenSTA standalone: 6109/6109 tests PASS with Kahn's enabled.
|
||||
|
||||
OpenROAD full regression: All tests PASS, including rmp.gcd_restructure (the test that surfaced both findings above), rsz (incremental netlist modification), and cts (buffer insertion with re-timing).
|
||||
|
|
@ -24,6 +24,8 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -76,6 +78,15 @@ public:
|
|||
void deleteVertexBefore(Vertex *vertex);
|
||||
void remove(Vertex *vertex);
|
||||
void reportEntries() const;
|
||||
// Enable/disable Kahn's algorithm for parallel traversal.
|
||||
// When disabled (default), the original level-based BFS is used.
|
||||
// Kahn's requires a non-null kahn_pred to know which edges to
|
||||
// follow during discovery. Set it via setKahnPred() before enabling.
|
||||
void setUseKahns(bool use_kahns) { use_kahns_ = use_kahns; }
|
||||
bool useKahns() const { return use_kahns_; }
|
||||
// Search predicate used by Kahn's discovery and successor decrement.
|
||||
// Separate from search_pred_ which is used by the original BFS.
|
||||
void setKahnPred(SearchPred *pred) { kahn_pred_ = pred; }
|
||||
|
||||
bool hasNext() override;
|
||||
bool hasNext(Level to_level);
|
||||
|
|
@ -109,6 +120,20 @@ protected:
|
|||
void checkLevel(Vertex *vertex,
|
||||
Level level);
|
||||
|
||||
// Kahn's algorithm: iterate BFS-direction successor vertices
|
||||
// with predicate filtering. Forward follows out-edges; backward
|
||||
// follows in-edges. Called for discovery, in-degree counting,
|
||||
// and successor decrement in the Kahn's worker.
|
||||
using VertexFn = std::function<void(Vertex*)>;
|
||||
virtual void kahnForEachSuccessor(Vertex *vertex,
|
||||
SearchPred *pred,
|
||||
const VertexFn &fn) = 0;
|
||||
void resetLevelBounds();
|
||||
|
||||
// Persistent Kahn's state to avoid per-call allocation.
|
||||
struct KahnState;
|
||||
std::unique_ptr<KahnState> kahn_state_;
|
||||
|
||||
BfsIndex bfs_index_;
|
||||
Level level_min_;
|
||||
Level level_max_;
|
||||
|
|
@ -119,6 +144,8 @@ protected:
|
|||
Level first_level_;
|
||||
// Max (min) level of queued vertices.
|
||||
Level last_level_;
|
||||
bool use_kahns_ = true;
|
||||
SearchPred *kahn_pred_ = nullptr;
|
||||
|
||||
friend class BfsFwdIterator;
|
||||
friend class BfsBkwdIterator;
|
||||
|
|
@ -144,6 +171,9 @@ protected:
|
|||
bool levelLess(Level level1,
|
||||
Level level2) const override;
|
||||
void incrLevel(Level &level) const override;
|
||||
void kahnForEachSuccessor(Vertex *vertex,
|
||||
SearchPred *pred,
|
||||
const VertexFn &fn) override;
|
||||
};
|
||||
|
||||
class BfsBkwdIterator : public BfsIterator
|
||||
|
|
@ -166,6 +196,9 @@ protected:
|
|||
bool levelLess(Level level1,
|
||||
Level level2) const override;
|
||||
void incrLevel(Level &level) const override;
|
||||
void kahnForEachSuccessor(Vertex *vertex,
|
||||
SearchPred *pred,
|
||||
const VertexFn &fn) override;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
|
|
|||
264
search/Bfs.cc
264
search/Bfs.cc
|
|
@ -25,6 +25,8 @@
|
|||
|
||||
#include "Bfs.hh"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include "Report.hh"
|
||||
#include "Debug.hh"
|
||||
#include "Mutex.hh"
|
||||
|
|
@ -37,6 +39,41 @@
|
|||
|
||||
namespace sta {
|
||||
|
||||
// Persistent storage for Kahn's algorithm arrays.
|
||||
// Allocated once and reused across visitParallel calls to
|
||||
// avoid repeated allocation of large per-graph arrays.
|
||||
struct BfsIterator::KahnState
|
||||
{
|
||||
// -1 = not in active set, >= 0 = in-degree.
|
||||
std::vector<int> in_degree_init;
|
||||
// Atomic in-degrees for the parallel phase.
|
||||
std::unique_ptr<std::atomic<int>[]> in_degree;
|
||||
size_t in_degree_size = 0;
|
||||
// Vertex IDs touched in the previous call -- reset to -1 before reuse.
|
||||
std::vector<VertexId> prev_ids;
|
||||
|
||||
void ensureInitSize(size_t needed)
|
||||
{
|
||||
if (in_degree_init.size() < needed)
|
||||
in_degree_init.resize(needed, -1);
|
||||
}
|
||||
|
||||
void ensureAtomicSize(size_t needed)
|
||||
{
|
||||
if (in_degree_size < needed) {
|
||||
in_degree = std::make_unique<std::atomic<int>[]>(needed);
|
||||
in_degree_size = needed;
|
||||
}
|
||||
}
|
||||
|
||||
void resetPrevious()
|
||||
{
|
||||
for (VertexId vid : prev_ids)
|
||||
in_degree_init[vid] = -1;
|
||||
prev_ids.clear();
|
||||
}
|
||||
};
|
||||
|
||||
BfsIterator::BfsIterator(BfsIndex bfs_index,
|
||||
Level level_min,
|
||||
Level level_max,
|
||||
|
|
@ -159,6 +196,22 @@ BfsIterator::visit(Level to_level,
|
|||
return visit_count;
|
||||
}
|
||||
|
||||
// Recalculate first_level_/last_level_ from remaining queue entries.
|
||||
void
|
||||
BfsIterator::resetLevelBounds()
|
||||
{
|
||||
first_level_ = level_max_;
|
||||
last_level_ = level_min_;
|
||||
for (Level l = 0; l < static_cast<Level>(queue_.size()); l++) {
|
||||
if (!queue_[l].empty()) {
|
||||
if (levelLess(l, first_level_))
|
||||
first_level_ = l;
|
||||
if (levelLess(last_level_, l))
|
||||
last_level_ = l;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
BfsIterator::visitParallel(Level to_level,
|
||||
VertexVisitor *visitor)
|
||||
|
|
@ -168,7 +221,8 @@ BfsIterator::visitParallel(Level to_level,
|
|||
if (!empty()) {
|
||||
if (thread_count == 1)
|
||||
visit_count = visit(to_level, visitor);
|
||||
else {
|
||||
else if (!use_kahns_ || !kahn_pred_) {
|
||||
// Original level-based parallel BFS with per-level barriers.
|
||||
std::vector<VertexVisitor *> visitors;
|
||||
for (int k = 0; k < thread_count_; k++)
|
||||
visitors.push_back(visitor->copy());
|
||||
|
|
@ -193,8 +247,8 @@ BfsIterator::visitParallel(Level to_level,
|
|||
size_t chunk_size = vertex_count / thread_count;
|
||||
BfsIndex bfs_index = bfs_index_;
|
||||
for (size_t k = 0; k < thread_count; k++) {
|
||||
// Last thread gets the left overs.
|
||||
size_t to = (k == thread_count - 1) ? vertex_count : from + chunk_size;
|
||||
size_t to = (k == thread_count - 1)
|
||||
? vertex_count : from + chunk_size;
|
||||
dispatch_queue_->dispatch([=, this](size_t) {
|
||||
for (size_t i = from; i < to; i++) {
|
||||
Vertex *vertex = level_vertices[i];
|
||||
|
|
@ -214,8 +268,176 @@ BfsIterator::visitParallel(Level to_level,
|
|||
visit_count += vertex_count;
|
||||
}
|
||||
}
|
||||
for (VertexVisitor *visitor : visitors)
|
||||
delete visitor;
|
||||
for (VertexVisitor *v : visitors)
|
||||
delete v;
|
||||
}
|
||||
else {
|
||||
// -------------------------------------------------------
|
||||
// Kahn's algorithm: process vertices as soon as all their
|
||||
// predecessors are done, eliminating per-level barriers.
|
||||
// -------------------------------------------------------
|
||||
|
||||
// Lazy-init persistent Kahn state.
|
||||
if (!kahn_state_)
|
||||
kahn_state_ = std::make_unique<KahnState>();
|
||||
|
||||
// Vertex IDs can exceed vertexCount() after deletions
|
||||
// (ObjectTable uses block-based IDs). Start with a
|
||||
// reasonable estimate and grow dynamically during discovery.
|
||||
VertexId vertex_count = graph_->vertexCount();
|
||||
kahn_state_->ensureInitSize(vertex_count + 1);
|
||||
kahn_state_->resetPrevious();
|
||||
|
||||
std::vector<int> &in_deg = kahn_state_->in_degree_init;
|
||||
std::vector<Vertex*> active_vertices;
|
||||
VertexId max_id = 0;
|
||||
|
||||
// Collect seed vertices from the level queue.
|
||||
Level saved_first = first_level_;
|
||||
Level saved_last = last_level_;
|
||||
Level level = first_level_;
|
||||
while (levelLessOrEqual(level, last_level_)
|
||||
&& levelLessOrEqual(level, to_level)) {
|
||||
for (Vertex *vertex : queue_[level]) {
|
||||
if (vertex) {
|
||||
VertexId vid = graph_->id(vertex);
|
||||
if (vid >= in_deg.size())
|
||||
in_deg.resize(vid + 128, -1);
|
||||
if (in_deg[vid] == -1) {
|
||||
in_deg[vid] = 0;
|
||||
active_vertices.push_back(vertex);
|
||||
if (vid > max_id) max_id = vid;
|
||||
}
|
||||
}
|
||||
}
|
||||
incrLevel(level);
|
||||
}
|
||||
|
||||
// BFS discovery -- mirrors enqueueAdjacentVertices logic.
|
||||
size_t disc_idx = 0;
|
||||
while (disc_idx < active_vertices.size()) {
|
||||
Vertex *vertex = active_vertices[disc_idx++];
|
||||
kahnForEachSuccessor(vertex, kahn_pred_,
|
||||
[&](Vertex *succ) {
|
||||
if (!levelLessOrEqual(succ->level(), to_level))
|
||||
return;
|
||||
VertexId sid = graph_->id(succ);
|
||||
if (sid >= in_deg.size())
|
||||
in_deg.resize(sid + 128, -1);
|
||||
if (in_deg[sid] == -1) {
|
||||
in_deg[sid] = 1;
|
||||
active_vertices.push_back(succ);
|
||||
succ->setBfsInQueue(bfs_index_, true);
|
||||
if (sid > max_id) max_id = sid;
|
||||
}
|
||||
else
|
||||
in_deg[sid]++;
|
||||
});
|
||||
}
|
||||
|
||||
size_t active_count = active_vertices.size();
|
||||
debugPrint(debug_, "bfs", 1, "kahns {} active vertices", active_count);
|
||||
|
||||
if (active_count == 0) {
|
||||
kahn_state_->prev_ids.clear();
|
||||
level = saved_first;
|
||||
while (levelLessOrEqual(level, saved_last)
|
||||
&& levelLessOrEqual(level, to_level)) {
|
||||
queue_[level].clear();
|
||||
incrLevel(level);
|
||||
}
|
||||
resetLevelBounds();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Size atomic array to cover max discovered ID.
|
||||
kahn_state_->ensureAtomicSize(max_id + 1);
|
||||
std::atomic<int> *in_degree = kahn_state_->in_degree.get();
|
||||
|
||||
// Copy active in-degrees to atomic array and collect ready batch.
|
||||
std::vector<Vertex*> ready_batch;
|
||||
kahn_state_->prev_ids.clear();
|
||||
kahn_state_->prev_ids.reserve(active_count);
|
||||
for (Vertex *v : active_vertices) {
|
||||
VertexId vid = graph_->id(v);
|
||||
in_degree[vid].store(in_deg[vid], std::memory_order_relaxed);
|
||||
kahn_state_->prev_ids.push_back(vid);
|
||||
if (in_deg[vid] == 0)
|
||||
ready_batch.push_back(v);
|
||||
}
|
||||
debugPrint(debug_, "bfs", 1, "kahns {} initial ready",
|
||||
ready_batch.size());
|
||||
|
||||
// Phase 3: Batch-dispatch Kahn's traversal.
|
||||
std::vector<VertexVisitor *> visitors;
|
||||
for (size_t k = 0; k < thread_count; k++)
|
||||
visitors.push_back(visitor->copy());
|
||||
|
||||
std::atomic<int> total_visited{0};
|
||||
BfsIndex bfs_index = bfs_index_;
|
||||
SearchPred *pred = kahn_pred_;
|
||||
std::vector<Vertex*> next_ready;
|
||||
std::mutex next_ready_lock;
|
||||
|
||||
while (!ready_batch.empty()) {
|
||||
next_ready.clear();
|
||||
|
||||
if (ready_batch.size() < thread_count) {
|
||||
for (Vertex *vertex : ready_batch) {
|
||||
vertex->setBfsInQueue(bfs_index, false);
|
||||
visitor->visit(vertex);
|
||||
total_visited.fetch_add(1, std::memory_order_relaxed);
|
||||
kahnForEachSuccessor(vertex, pred, [&](Vertex *succ) {
|
||||
VertexId sid = graph_->id(succ);
|
||||
if (sid < in_deg.size() && in_deg[sid] >= 0) {
|
||||
int prev = in_degree[sid]
|
||||
.fetch_sub(1, std::memory_order_acq_rel);
|
||||
if (prev == 1)
|
||||
next_ready.push_back(succ);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
size_t in_deg_size = in_deg.size();
|
||||
for (Vertex *vertex : ready_batch) {
|
||||
dispatch_queue_->dispatch(
|
||||
[&, vertex, bfs_index, pred, in_deg_size](size_t tid) {
|
||||
vertex->setBfsInQueue(bfs_index, false);
|
||||
visitors[tid]->visit(vertex);
|
||||
total_visited.fetch_add(1, std::memory_order_relaxed);
|
||||
kahnForEachSuccessor(vertex, pred, [&, in_deg_size](Vertex *succ) {
|
||||
VertexId sid = graph_->id(succ);
|
||||
if (sid < in_deg_size && in_deg[sid] >= 0) {
|
||||
int prev = in_degree[sid]
|
||||
.fetch_sub(1, std::memory_order_acq_rel);
|
||||
if (prev == 1) {
|
||||
LockGuard lock(next_ready_lock);
|
||||
next_ready.push_back(succ);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
dispatch_queue_->finishTasks();
|
||||
}
|
||||
ready_batch.swap(next_ready);
|
||||
}
|
||||
|
||||
visit_count = total_visited.load(std::memory_order_relaxed);
|
||||
visitor->levelFinished();
|
||||
|
||||
for (VertexVisitor *v : visitors)
|
||||
delete v;
|
||||
|
||||
// Clear processed levels and update bounds for remaining entries.
|
||||
level = saved_first;
|
||||
while (levelLessOrEqual(level, saved_last)
|
||||
&& levelLessOrEqual(level, to_level)) {
|
||||
queue_[level].clear();
|
||||
incrLevel(level);
|
||||
}
|
||||
resetLevelBounds();
|
||||
}
|
||||
}
|
||||
return visit_count;
|
||||
|
|
@ -382,6 +604,22 @@ BfsFwdIterator::levelLess(Level level1,
|
|||
return level1 < level2;
|
||||
}
|
||||
|
||||
void
|
||||
BfsFwdIterator::kahnForEachSuccessor(Vertex *vertex,
|
||||
SearchPred *pred,
|
||||
const VertexFn &fn)
|
||||
{
|
||||
if (pred->searchFrom(vertex)) {
|
||||
VertexOutEdgeIterator edge_iter(vertex, graph_);
|
||||
while (edge_iter.hasNext()) {
|
||||
Edge *edge = edge_iter.next();
|
||||
Vertex *to_vertex = edge->to(graph_);
|
||||
if (pred->searchThru(edge) && pred->searchTo(to_vertex))
|
||||
fn(to_vertex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
BfsFwdIterator::enqueueAdjacentVertices(Vertex *vertex,
|
||||
SearchPred *search_pred)
|
||||
|
|
@ -454,6 +692,22 @@ BfsBkwdIterator::levelLess(Level level1,
|
|||
return level1 > level2;
|
||||
}
|
||||
|
||||
void
|
||||
BfsBkwdIterator::kahnForEachSuccessor(Vertex *vertex,
|
||||
SearchPred *pred,
|
||||
const VertexFn &fn)
|
||||
{
|
||||
if (pred->searchTo(vertex)) {
|
||||
VertexInEdgeIterator edge_iter(vertex, graph_);
|
||||
while (edge_iter.hasNext()) {
|
||||
Edge *edge = edge_iter.next();
|
||||
Vertex *from_vertex = edge->from(graph_);
|
||||
if (pred->searchFrom(from_vertex) && pred->searchThru(edge))
|
||||
fn(from_vertex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
BfsBkwdIterator::enqueueAdjacentVertices(Vertex *vertex,
|
||||
SearchPred *search_pred)
|
||||
|
|
|
|||
|
|
@ -288,6 +288,8 @@ Search::Search(StaState *sta) :
|
|||
check_crpr_(new CheckCrpr(this))
|
||||
{
|
||||
initVars();
|
||||
arrival_iter_->setKahnPred(search_adj_);
|
||||
required_iter_->setKahnPred(search_adj_);
|
||||
}
|
||||
|
||||
// Init "options".
|
||||
|
|
|
|||
Loading…
Reference in New Issue