ForkUnion

repository·main·Indexed 18 days ago

https://github.com/ashvardanian/forkunion

A low-latency, NUMA-aware fork-join thread-pool library designed for high-performance parallel loops. Version 3.0.2 provides zero heap allocations, syscalls, CAS, or false-sharing on the hot path. It leverages hardware address monitors (e.g., x86 UMONITOR/UMWAIT, Arm WFET) for efficient worker parking and supports C++, Rust, C, and Zig. Optimized for massive core counts, it offers static and dynamic scheduling, NUMA-aware allocators, and a hardware topology model for compute and memory domains.

Tokens
6.1K
Snippets
15
Records
24
Agent score
13%

What's inside forkunion

  1. What is ForkUnion?

    main

    ForkUnion is a NUMA-aware fork-join thread-pool designed for high-performance parallel loops (similar to #pragma omp parallel for). Unlike traditional task-queue runtimes, it is optimized for low-latency dispatch in high-core-count environments.

    Key Characteristics:

    • Performance: Designed to minimize the 'fork-join tax'. It is significantly faster than OpenMP, Rayon, and Taskflow for parallel-for dispatches.
    • Zero Overhead on Hot Path: Makes zero heap allocations, zero system calls, and zero CAS (Compare-And-Swap) operations during execution. It avoids cache-line false-sharing.
    • Hardware-Level Parking: Uses hardware address monitors (e.g., x86 UMONITOR/UMWAIT, Arm WFET, RISC-V Zawrs) to allow idle workers to light-sleep on specific cache-lines, waking instantly upon a write without hot spinning or kernel futexes.
    • Scalability: Maintains flat dispatch latency even into hundreds of cores by using private fetch_add cursors to avoid interconnect thrashing on multi-socket systems.
    • Portability: Runs on Linux, FreeBSD, Windows, macOS, Android, and iOS. Supports asymmetric compute and memory topologies.
  2. Understand ForkUnion's Hardware Topology Model

    main

    ForkUnion models hardware using two independent axes to handle non-uniformity:

    1. Compute Domain: A bindable group of cores sharing a Quality-of-Service (QoS) class and locality. This is what a pool spawns onto.
    2. Memory Domain: A bank of memory with its own capacity and access cost (e.g., HBM, DDR, CXL). This is what the allocator targets.

    Key Concepts:

    • Levels: Dense ordinal groupings of domains with similar performance.
      • Compute levels grow with performance (higher is faster).
      • Memory levels grow with distance (lower is faster/closer).
    • Topology vs. Fabric:
      • Topology holds what the platform declares (domains, cores, QoS, volumes).
      • Fabric holds what ForkUnion observes (per-edge latencies, bandwidths, and distances).
    • Magnitudes:
      • compute_capacity_in: Kernel's rating of a core (normalized to 1024 for the fastest core; 0 on x86/Windows/Apple).
      • compute_cache_bytes_in: Deepest private cache size.
      • memory_distance: Relative cost of reaching memory (10 for local, higher for remote).
      • memory_bandwidth / memory_latency: Observed saturated read bandwidth and dependent-load latency.
  3. Core Thread Pool Operations and APIs

    main

    ForkUnion is designed for high-performance data parallelism and tight parallel loops, avoiding nested parallelism, exception handling, or future/promises.

    Core Operations

    • try_spawn: Initializes worker threads.
    • for_threads: Launches a blocking callback on all threads.

    Index-Addressable Task APIs

    • for_n: Distributes individual, evenly-sized tasks (static scheduling).
    • for_n_dynamic: Distributes individual, unevenly-sized tasks (dynamic scheduling/work stealing).
    • for_slices: Distributes slices of evenly-sized tasks.

    Flow Control and Tuning

    • sleep(microseconds): For longer naps.
    • terminate: Kills threads before the destructor is called.
    • unsafe_for_threads: Broadcasts a callback without blocking; returns a generation token.
    • unsafe_join: Blocks until a broadcasted generation is complete.
    • is_complete: Polls a generation token for completion without blocking.
  4. Comparison of ForkUnion with OpenMP, Rayon, and others

    main

    ForkUnion distinguishes itself from other libraries through its approach to memory and syscalls. Key differentiators include:

    • Zero Hot-path Heap Allocations: Unlike Rayon (per spawned task) or Taskflow (per task node), ForkUnion performs no allocations during the hot path.
    • Zero Syscalls to Dispatch: ForkUnion avoids the kernel/syscall overhead (like futex) used by OpenMP, Rayon, and Taskflow during dispatch.
    • Dynamic Scheduling: Uses fetch_add cursors rather than the Chase-Lev CAS deque used by Rayon, Taskflow, and oneTBB, or the CAS retry loops used by OpenMP.
    • NUMA Support: Provides NUMA pinning and local allocators, which are missing in Rayon and Taskflow.
    • Hardware Timed-wait: Supports hardware-level timed-wait, which is not available in Rayon, Taskflow, or oneTBB.
  5. Understand ForkUnion's core synchronization primitives

    main

    ForkUnion's thread-pool relies on a minimal set of atomic variables to manage lifecycle and task distribution. Understanding these helps in reasoning about the library's safety and performance.

    VariableUser PerspectiveInternal Usage
    stopStop the entire thread-poolTells workers when to exit the loop
    fork_generation"Forks" called since initTells workers to wake up on new forks
    threads_to_syncThreads not joined this forkTells main thread when workers finish
    claim.nextOne worker's stealable cursorNext task in that worker's slice

    Key Safety Guarantees:

    • Thread Count: The number of threads can only be changed by calling terminate and then try_spawn. These operations are mutually exclusive with running tasks, ensured by the stop variable.
    • Job Submission: New tasks are submitted from a single thread that updates the number of parts for each new fork. Workers remain asleep on old fork_generation values until the increment occurs, ensuring they only access new job pointers when safe.
  6. How caller-exclusive vs caller-inclusive pools work

    main

    ForkUnion uses two pool modes that change how work is dispatched and when it completes:

    • caller_exclusive_k pools: The calling thread is separate from the pool. Work starts immediately upon the construction of the RAII guard (in Rust/Zig) or after try_spawn (in C++). You can overlap your own work, poll is_complete, and join.
    • caller_inclusive_k pools: The calling thread is part of the pool and owes one slice of work. This work only runs inside unsafe_join or during destruction. Consequently, completion cannot be reached by polling alone; you must join or allow the object to be destroyed.
  7. When to use ForkUnion vs. other concurrency libraries

    main

    ForkUnion is optimized for high-performance, low-latency data-parallelism.

    Use ForkUnion when:

    • You are implementing data-parallel loops (e.g., parallel for).
    • You have bulk-synchronous phases.
    • You are performing NUMA-sharded scans.
    • You require nanosecond-scale dispatch without heap allocations or kernel syscalls on the hot path.

    Use alternatives (like Taskflow, Tokio, or oneTBB) when:

    • You need task graphs (DAGs).
    • You require async I/O, futures, or promises.
    • You need work that outlives its scope.
    • You require nested parallelism (ForkUnion deliberately bans nested parallelism).
  8. Configure NUMA and Huge Pages for ForkUnion

    main

    ForkUnion uses kernel interfaces (/sys/devices/system/node, mbind syscall, and mmap(MAP_HUGETLB)) for NUMA awareness and Huge Page placement. It does not require libnuma or libhugetlbfs to be linked.

    Note: Huge pages must be reserved at the system level before they can be used by the application. This is a runtime configuration, not a build-time dependency.

    # Check current hugepage availability
    cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
    
    # Reserve 1024 huge pages on node0
    echo 1024 | sudo tee /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
  9. Use Rayon-style parallel iterators in Rust

    main

    ForkUnion provides a parallel iterator API in Rust via the prelude. Unlike Rayon, these iterators allow explicit control over the thread pool and scheduling strategy.

    Static Scheduling (Default): Most efficient for statically shaped workloads. Dynamic Work-Stealing: Use .with_schedule(&mut pool, DynamicScheduler) for workloads with unpredictable task durations.

    Common Operations:

    • map, filter, zip: Standard iterator adaptors.
    • sum, count: Parallel reductions.
    • reduce: Custom parallel reductions.
    • find_first, find_last, find_any: Short-circuiting searches.
    • try_for_each, try_fold_with_scratch: Fallible parallel operations that stop on the first error.
    use forkunion as fu;
    use forkunion::prelude::*;
    
    let topology = fu::Topology::new().expect("Failed to detect hardware topology");
    let mut pool = fu::spawn(&topology, 4);
    let mut data: Vec<usize> = (0..1000).collect();
    
    // Static scheduling
    (&data[..])
        .into_par_iter()
        .with_pool(&mut pool)
        .for_each(|value| {
            println!("Value: {}", value);
        });
    
    // Dynamic work-stealing
    (&mut data[..])
        .into_par_iter()
        .with_schedule(&mut pool, DynamicScheduler)
        .for_each(|value| {
            *value *= 2;
        });
  10. Build and benchmark ForkUnion for Zig

    main

    Use zig build to manage testing and builds.

    • Use -Dplace-memory-on-domain=true to require NUMA-aware allocations (Linux only).
    • Use -Dportable=true to use only the STL thread pool.

    Benchmarks for nbody and propagation can be run from the scripts directory using specific environment variables to select the backend.

    # Testing and building
    zig build test --summary all
    zig build -Dplace-memory-on-domain=true
    zig build -Dportable=true
    
    # Running benchmarks
    cd scripts
    zig build -Doptimize=ReleaseFast
    NBODY_COUNT=512 NBODY_BACKEND=forkunion_static_shared ./zig-out/bin/forkunion_nbody
    PROPAGATION_BACKEND=forkunion_static_shared ./zig-out/bin/forkunion_propagation
  11. Verify Rust `no_std` compatibility and run tests

    main

    To ensure ForkUnion works in no_std environments, build the library with --no-default-features. You can also test the memory placement feature using the place-memory-on-domain feature.

    To find the Minimum Supported Rust Version (MSRV), use cargo-msrv.

    # Verify no_std compatibility
    cargo build --lib --no-default-features --release
    cargo build --lib --no-default-features --features place-memory-on-domain --release
    
    # Run tests
    cargo test --lib --release
    cargo test --doc --release
    cargo test --lib --features place-memory-on-domain --release
    
    # Find MSRV
    cargo +stable install cargo-msrv
    cargo msrv find --ignore-lockfile