Project Brief and API Contract
The Parallel Prefix / Sorting Project implements two classic data-parallel algorithms — the Blelloch exclusive prefix scan and the bitonic sorting network — and evaluates them in the work–span model. Building on the parallel prefix scan and parallel sorting foundations, the project treats every operation as a directed acyclic graph of primitive steps: work counts the total steps executed with one processor, while span counts the steps on the longest dependency chain with unlimited processors. Speedup, efficiency, and Amdahl-style limits all follow from these two numbers.
The public interface exposes two pure functions and one telemetry contract: parallel_scan(values) returns the exclusive prefix sums of an arbitrary-length array, and parallel_sort(values) returns ascending order. Both pad internally to the next power of two, replay every dependency level as a counted round, and report exact work and span counters alongside the result. Acceptance requires 100% parity with a sequential reference oracle on random and adversarial inputs, span bounds of $O(\log N)$ for the scan and $O(\log^2 N)$ for the sort, and a measured speedup bound of $\text{work} / \text{span}$ in line with Brent's theorem.
Parallel Engine Contract: the engines accept arbitrary integer arrays, return oracle-identical outputs, and guarantee work $O(N)$ for the scan and $O(N \log^2 N)$ for the sort with spans $O(\log N)$ and $O(\log^2 N)$ respectively. Success is measured in counted operations and dependency rounds — not wall-clock time — and any oracle mismatch is a hard failure.
Architecture and Data Flow
The system decomposes into a dispatch layer, two independent parallel engines, and a shared cost-accounting and verification pipeline. Data flows through the dispatcher into exactly one engine, and every primitive step performed by that engine is charged to the telemetry collector before the oracle renders a verdict.
The architecture comprises five interacting components:
- Task Dispatcher & Validation: selects the engine for
parallel_scanorparallel_sort, validates the input, and pads the array to the next power of two. - Blelloch Scan Engine: builds a reduce tree in the up-sweep, then propagates prefix seeds down the same tree in the down-sweep.
- Bitonic Sort Engine: runs a fixed compare-exchange network of $\tfrac{1}{2}\log_2 N (\log_2 N + 1)$ stages, each of $N/2$ independent pair operations.
- Work & Span Telemetry Collector: charges one work unit per primitive operation and one span unit per dependency level, yielding the critical-path length.
- Reference Oracle & Test Harness: replays each request sequentially with an obviously correct loop and compares element-for-element before results are released.
flowchart TD
A[Client Scan / Sort Request] --> B[Dispatch & Validation]
B --> C{Operation Type}
C --> Exclusive Prefix Scan| D[Blelloch Scan Engine]
C -->|Parallel Ordering| E[Bitonic Sort Engine]
D --> F[Upsweep: Pairwise Reduce Tree]
F --> G[Downsweep: Propagate Identity Seeds]
E --> H[Compare-Exchange Network Stages]
G --> I[Work and Span Telemetry Collector]
H --> I
I --> J[Compare against Sequential Oracle]
J -->|Match| K[Return Results & Speedup Report]
J -->|Mismatch| L[Raise Parity Verification Exception]
Core Implementations in Python, C++, and Java
The complete implementations below demonstrate the Blelloch scan and the bitonic network with explicit work and span counters. Each language panel is a complete, copy-paste-runnable program: it runs the sample array, asserts oracle parity, prints the counted costs, and finishes with a 200-trial randomized sweep over adversarial shapes (single elements, non-power-of-two lengths, duplicates, and negative values).
Visual Walkthrough of the Engine
To visualize the engine, consider an exclusive scan over the sample array $[3, 1, 4, 1, 5, 9, 2, 6]$ with $N = 8$. The first diagram traces one end-to-end run: the up-sweep folds the array into a reduce tree, and the down-sweep reuses that same tree to scatter prefix seeds back to the leaves, with the internal state highlighted at every level.
The second view contrasts the two cost axes that determine everything above: total work, the runtime with a single processor, against span, the dependency chain that caps speedup no matter how many processors are available.
Correctness Testing Against a Reference Oracle
Verification replays every request against a deliberately slow, obviously correct sequential oracle: a plain accumulator loop for the scan and the standard library sort for ordering. The harness sweeps two hundred randomized trials per run over adversarial shapes — single elements, non-power-of-two lengths, duplicates, negatives, and already-sorted or reverse-sorted inputs — and treats any element-wise divergence as a hard failure, since parallel routines must be deterministic regardless of how operations are scheduled.
Find the Bug
Scenario: A student's Blelloch scan passes the trivial $N = 1$ test but fails oracle parity on every trial with $N \geq 2$, with outputs that look uniformly "too large."
Diagnostic Trace:
- Oracle Input: sample array $[3, 1, 4, 1, 5, 9, 2, 6]$; the oracle expects the exclusive scan $[0, 3, 4, 8, 9, 14, 23, 25]$.
- Behavior Observed: the engine returns $[31, 34, 35, 39, 40, 45, 54, 56]$ — sorted-looking, plausible, and wrong.
- Audit Inspection: subtracting the oracle element-wise yields a constant offset of $31$ on every position — exactly the array's total sum. Inspection of the engine shows the identity seed assignment
a[size - 1] = 0between the up-sweep and down-sweep loops was deleted during a refactor. - Failure Point: with the root slot still holding the reduce-tree total instead of the identity, every prefix propagated down the scan tree inherits the full sum $31$, so each output is the true exclusive prefix plus the grand total.
Remediation: reinsert the identity seed before the down-sweep, and add a standing assertion that scan_output[N-1] + input[N-1] equals the up-sweep root total — a cheap invariant that catches any future seed or propagation regression.
Benchmarks and Complexity Report
The engine replays each workload at power-of-two sizes ($N = 2^{10}$ to $2^{16}$) and compares the counted work and span against theory. Because every primitive operation and dependency level is counted exactly, the measured columns match the closed-form bounds to the operation, and the ratio $\text{work} / \text{span}$ bounds the best achievable speedup by Brent's theorem before a single real thread is spawned.
| Operation / Workload | Input Size ($N$) | Theoretical Work | Theoretical Span | Measured Work | Measured Span | Work / Span Speedup |
|---|---|---|---|---|---|---|
| Blelloch Exclusive Scan | $1{,}024$ | $2N - 2 = 2{,}046$ | $2\lceil \log_2 N \rceil = 20$ | $2{,}046$ | $20$ | $\approx 102\times$ |
| Blelloch Exclusive Scan | $65{,}536$ | $131{,}070$ | $32$ | $131{,}070$ | $32$ | $\approx 4{,}096\times$ |
| Bitonic Sort | $1{,}024$ | $\frac{N}{2} \cdot \frac{\log_2 N (\log_2 N + 1)}{2} = 28{,}160$ | $\frac{\log_2 N (\log_2 N + 1)}{2} = 55$ | $28{,}160$ | $55$ | $\approx 512\times$ |
| Bitonic Sort | $65{,}536$ | $4{,}456{,}448$ | $136$ | $4{,}456{,}448$ | $136$ | $\approx 32{,}768\times$ |
| Sequential Baseline Scan | $65{,}536$ | $N - 1 = 65{,}535$ | $N - 1 = 65{,}535$ | $65{,}535$ | $65{,}535$ | $1\times$ |
Extensions and Stretch Goals
To evolve the simulated PRAM engine toward production parallel systems, the following milestones trade counted abstractions for real hardware, real schedulers, and harder workloads.
Extension Roadmap & Engineering Targets:
- Tier 1 (Real Thread Pools): map each dependency level onto
std::async/ForkJoinPool/concurrent.futureswith Brent-style round scheduling, and reconcile measured wall-clock speedup against the work/span bound. - Tier 2 (GPU Kernels): port the bitonic network and Blelloch scan to CUDA or OpenCL with coalesced memory access and shared-memory tree buffers, profiling occupancy against the $O(\log N)$ critical path.
- Tier 3 (Work-Efficient Sorting): replace bitonic sort with a parallel radix sort composed from the scan primitive, restoring $O(N)$-like work while keeping $O(\log N)$-depth passes and deterministic, reproducible outputs for floating-point reductions.