stringzilla 5.0.5

Search, hash, sort, fingerprint, and fuzzy-match strings faster via SWAR, SIMD, and GPGPU
Documentation
/**
 *  @file c/stringzillas/runtime.cuh
 *  @brief Parallel runtime: version, capabilities, device scopes, unified allocator shim (CPU + CUDA backends).
 *  @author Ash Vardanian
 *  @date March 23, 2025
 */
#ifndef STRINGZILLAS_RUNTIME_CUH_
#define STRINGZILLAS_RUNTIME_CUH_
#include "stringzillas.cuh"

extern "C" {

#pragma region Metadata

SZ_API_RUNTIME int szs_version_major(void) { return STRINGZILLA_H_VERSION_MAJOR; }
SZ_API_RUNTIME int szs_version_minor(void) { return STRINGZILLA_H_VERSION_MINOR; }
SZ_API_RUNTIME int szs_version_patch(void) { return STRINGZILLA_H_VERSION_PATCH; }

SZ_API_RUNTIME sz_capability_t szs_capabilities_comptime(void) {
    return (sz_capability_t)(sz_capabilities_comptime_implementation_() | // Serial & the CPU SIMD tiers
                             sz_cap_parallel_k |                          // Fork Union is always linked in
                             (sz_cap_cuda_k * SZ_USE_CUDA) |              //
                             (sz_cap_kepler_k * SZ_USE_KEPLER) |          //
                             (sz_cap_hopper_k * SZ_USE_HOPPER));
}

SZ_API_RUNTIME sz_capability_t szs_capabilities_runtime(void) {
    sz_capability_t caps = sz_capabilities_runtime_implementation_();

    // The Fork Union pool is linked in, so every machine can run the parallel engines. How many cores to
    // give them is a sizing question the device scopes answer - `szs_device_scope_init_cpu_cores` already
    // folds a one-core request back onto the serial scope - so no topology is harvested here.
    caps = static_cast<sz_capability_t>(caps | sz_cap_parallel_k);

#if SZ_USE_CUDA
    // Every way a GPU can be unavailable - absent, driver-less, stubbed, version-mismatched - means the
    // same thing here, so the failure needs no taxonomy: it contributes no bits and leaves the CPU alone.
    sz::gpu_specs_t first_gpu_specs;
    if (static_cast<sz::status_t>(szs::gpu_specs_fetch(first_gpu_specs)) == sz::status_t::success_k) {
        caps = static_cast<sz_capability_t>(caps | sz_cap_cuda_k);
        if (first_gpu_specs.sm_code >= 30) caps = static_cast<sz_capability_t>(caps | sz_cap_kepler_k);
        if (first_gpu_specs.sm_code >= 90) caps = static_cast<sz_capability_t>(caps | sz_cap_hopper_k);
    }
#endif // SZ_USE_CUDA

    return caps;
}

SZ_API_RUNTIME sz_capability_t szs_capabilities(void) {
    // Both sides always carry `sz_cap_serial_k`, so a settled answer is never `sz_caps_none_k` and that
    // value doubles as the "not yet probed" sentinel without a second flag to keep in sync.
    static sz_capability_t static_caps = sz_caps_none_k;
    if (static_caps == sz_caps_none_k)
        static_caps = (sz_capability_t)(szs_capabilities_comptime() & szs_capabilities_runtime());
    return static_caps;
}

SZ_API_RUNTIME sz_status_t sz_memory_allocator_init_unified(sz_memory_allocator_t *alloc, char const **error_message) {
#if SZ_USE_CUDA
    alloc->allocate = &sz_memory_allocate_from_unified_;
    alloc->free = &sz_memory_free_from_unified_;
    alloc->handle = nullptr;
    return propagate_error(sz::status_t::success_k, error_message);
#else
    sz_unused_(alloc); // Suppress unused parameter warning when CUDA is not used
    return propagate_error(sz::status_t::missing_gpu_k, error_message);
#endif
}

#pragma endregion Metadata

#pragma region Device Scopes

SZ_API_RUNTIME sz_status_t szs_device_scope_init_default(szs_device_scope_t *scope_punned, char const **error_message) {
    sz_assert_(scope_punned != nullptr && "Scope must not be null");
    auto *scope = new device_scope_t {default_scope_t {}};
    if (!scope) return propagate_error(sz::status_t::bad_alloc_k, error_message, "Failed to allocate device scope");
    *scope_punned = reinterpret_cast<szs_device_scope_t>(scope);
    return propagate_error(sz::status_t::success_k, error_message);
}

SZ_API_RUNTIME sz_status_t szs_device_scope_init_cpu_cores(sz_size_t cpu_cores, szs_device_scope_t *scope_punned,
                                                           char const **error_message) {
    sz_assert_(scope_punned != nullptr && "Scope must not be null");

    // `cpu_cores == 0` sizes the pool from the ForkUnion topology: the ALLOWED cores, not the raw hardware count.
    if (cpu_cores == 0) cpu_cores = szs::forkunion_executor_t::allowed_cores_count();
    if (cpu_cores == 0)
        return propagate_error(sz::status_t::unknown_k, error_message, "Failed to probe the CPU topology");

    // If `cpu_cores` is 1, redirect to default scope
    if (cpu_cores == 1) return szs_device_scope_init_default(scope_punned, error_message);

    sz::cpu_specs_t specs;
    auto executor = std::make_unique<szs::forkunion_executor_t>();
    if (executor->try_spawn(cpu_cores) != sz::status_t::success_k)
        return propagate_error(sz::status_t::bad_alloc_k, error_message, "Failed to spawn thread pool");

    auto *scope = new (std::nothrow)
        device_scope_t(std::in_place_type_t<cpu_scope_t> {}, std::move(executor), std::move(specs));
    if (!scope) return propagate_error(sz::status_t::bad_alloc_k, error_message, "Failed to allocate CPU device scope");

    *scope_punned = reinterpret_cast<szs_device_scope_t>(scope);
    return propagate_error(sz::status_t::success_k, error_message);
}

SZ_API_RUNTIME sz_status_t szs_device_scope_init_gpu_device(sz_size_t gpu_device, szs_device_scope_t *scope_punned,
                                                            char const **error_message) {
    sz_assert_(scope_punned != nullptr && "Scope must not be null");

#if SZ_USE_CUDA
    sz::gpu_specs_t specs;
    auto specs_status = szs::gpu_specs_fetch(specs, static_cast<int>(gpu_device));
    if (specs_status.status != sz::status_t::success_k) { return propagate_error(specs_status, error_message); }
    szs::cuda_executor_t executor;
    auto executor_status = executor.try_scheduling(static_cast<int>(gpu_device));
    if (executor_status.status != sz::status_t::success_k) return propagate_error(executor_status, error_message);

    auto *scope = new (std::nothrow)
        device_scope_t {gpu_scope_t {.executor = std::move(executor), .specs = std::move(specs)}};
    if (!scope) return propagate_error(sz::status_t::bad_alloc_k, error_message, "Failed to allocate GPU device scope");
    *scope_punned = reinterpret_cast<szs_device_scope_t>(scope);
    return propagate_error(sz::status_t::success_k, error_message);
#else
    sz_unused_(gpu_device);
    sz_unused_(scope_punned);
    return propagate_error(sz::status_t::missing_gpu_k, error_message, "CUDA support not compiled in");
#endif
}

SZ_API_RUNTIME sz_status_t szs_device_scope_get_cpu_cores(szs_device_scope_t scope_punned, sz_size_t *cpu_cores,
                                                          char const **error_message) {
    if (scope_punned == nullptr || cpu_cores == nullptr)
        return propagate_error(sz::status_t::unknown_k, error_message, "Invalid null pointer argument");
    auto *scope = reinterpret_cast<device_scope_t *>(scope_punned);

    if (std::holds_alternative<cpu_scope_t>(scope->variants)) {
        auto &cpu_scope = std::get<cpu_scope_t>(scope->variants);
        if (cpu_scope.executor_ptr) {
            *cpu_cores = cpu_scope.executor_ptr->threads_count();
            return propagate_error(sz::status_t::success_k, error_message);
        }
    }
    // Default scope is single-threaded
    else if (std::holds_alternative<default_scope_t>(scope->variants)) {
        *cpu_cores = 1;
        return propagate_error(sz::status_t::success_k, error_message);
    }

    return propagate_error(sz::status_t::unknown_k, error_message, "Device scope is GPU-only");
}

SZ_API_RUNTIME sz_status_t szs_device_scope_get_gpu_device(szs_device_scope_t scope_punned, sz_size_t *gpu_device,
                                                           char const **error_message) {
    if (scope_punned == nullptr || gpu_device == nullptr)
        return propagate_error(sz::status_t::unknown_k, error_message, "Invalid null pointer argument");

#if SZ_USE_CUDA
    auto *scope = reinterpret_cast<device_scope_t *>(scope_punned);
    if (std::holds_alternative<gpu_scope_t>(scope->variants)) {
        auto &gpu_scope = std::get<gpu_scope_t>(scope->variants);
        *gpu_device = static_cast<sz_size_t>(gpu_scope.executor.device_id());
        return propagate_error(sz::status_t::success_k, error_message);
    }
#else
    sz_unused_(scope_punned);
    sz_unused_(gpu_device);
#endif

    return propagate_error(sz::status_t::unknown_k, error_message, "Device scope is CPU-only");
}

SZ_API_RUNTIME void szs_device_scope_free(szs_device_scope_t scope_punned) {
    if (scope_punned == nullptr) return;
    auto *scope = reinterpret_cast<device_scope_t *>(scope_punned);
    delete scope;
}

SZ_API_RUNTIME sz_status_t szs_device_scope_get_capabilities(szs_device_scope_t scope_punned,
                                                             sz_capability_t *capabilities,
                                                             char const **error_message) {

    if (scope_punned == nullptr || capabilities == nullptr)
        return propagate_error(sz::status_t::unknown_k, error_message, "Invalid null pointer argument");
    sz_capability_t system_caps = szs_capabilities();

#if SZ_USE_CUDA
    auto *scope = reinterpret_cast<device_scope_t *>(scope_punned);
    if (std::holds_alternative<gpu_scope_t>(scope->variants)) {
        // For GPU scope, intersect system capabilities with CUDA capabilities
        *capabilities = static_cast<sz_capability_t>(system_caps & sz_caps_cuda_k);
        return propagate_error(sz::status_t::success_k, error_message);
    }
#endif

    // For default and CPU scopes, intersect system capabilities with CPU capabilities
    *capabilities = static_cast<sz_capability_t>(system_caps & sz_caps_cpus_k);
    return propagate_error(sz::status_t::success_k, error_message);
}

#pragma endregion Device Scopes

#pragma region Unified Allocator

SZ_API_RUNTIME void *szs_unified_alloc(sz_size_t size_bytes) {
#if SZ_USE_CUDA
    return szs::unified_alloc_t {}.allocate(size_bytes);
#else
    return std::malloc(size_bytes);
#endif
}

SZ_API_RUNTIME void szs_unified_free(void *ptr, sz_size_t size_bytes) {
    if (!ptr) return;
#if SZ_USE_CUDA
    szs::unified_alloc_t {}.deallocate(static_cast<char *>(ptr), size_bytes);
#else
    sz_unused_(size_bytes);
    std::free(ptr);
#endif
}

#pragma endregion Unified Allocator
}

#endif // STRINGZILLAS_RUNTIME_CUH_