Created
March 3, 2026 19:39
-
-
Save yrashk/d3a164c5c08c608efee07dbd3e754ece to your computer and use it in GitHub Desktop.
Ultra ALPS GP evolution hang: unsigned integer underflow in selection (tournament_size=0)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Minimal reproduction: Ultra ALPS GP evolution hang | |
| // | |
| // Two distinct bugs exist when using evolution<> directly (not via src::search): | |
| // | |
| // Bug 1: Unsigned integer underflow in ALPS selection (tournament_size=0) | |
| // - parameters::evolution::tournament_size defaults to 0 (sentinel) | |
| // - parameters::init() sets it to 5, but src::problem constructor does NOT | |
| // - ALPS selection (evolution_selection.tcc:147) computes: | |
| // for (auto rounds(tournament_size - 1); rounds; --rounds) | |
| // With tournament_size=0, rounds wraps to SIZE_MAX (~18 quintillion) | |
| // - Each iteration evaluates an individual, so selection never returns | |
| // - Manifests as: evolution stuck at generation 0, all CPUs busy | |
| // | |
| // Bug 2: Lock contention in ALPS replacement (separate from Bug 1) | |
| // - alps::try_add_to_layer() holds exclusive lock during evaluation | |
| // - With tournament_size properly set, this causes thread starvation | |
| // - Severity depends on hardware, population size, and dataset size | |
| // | |
| // This test isolates Bug 1 by running WITHOUT init(). Bug 1 is platform- | |
| // independent: it hangs on any hardware because selection loops SIZE_MAX times. | |
| // | |
| // Test matrix: | |
| // Section A: alps_es WITHOUT init() -- reproduces Bug 1 (should all timeout) | |
| // Section B: alps_es WITH init() -- checks if Bug 2 causes hangs | |
| // Section C: std_es WITHOUT init() -- SEGFAULT (empty parents vector) | |
| #include "kernel/ultra.h" | |
| #include "kernel/gp/src/evaluator.h" | |
| #include "kernel/gp/src/problem.h" | |
| #include <csignal> | |
| #include <iostream> | |
| #include <sstream> | |
| #include <string> | |
| #include <sys/wait.h> | |
| #include <thread> | |
| #include <unistd.h> | |
| // --------------------------------------------------------------------------- | |
| // Helpers | |
| // --------------------------------------------------------------------------- | |
| static ultra::src::problem make_problem(int n_examples) | |
| { | |
| std::string csv = "y,x1,x2\n"; | |
| for (int i = 0; i < n_examples; ++i) | |
| { | |
| double x1 = static_cast<double>(i) / 20.0 - 5.0; | |
| double x2 = static_cast<double>(i % 20) / 10.0 - 1.0; | |
| double y = x1 * x1 + 2.0 * x2; | |
| csv += std::to_string(y) + "," + std::to_string(x1) | |
| + "," + std::to_string(x2) + "\n"; | |
| } | |
| std::istringstream stream(csv); | |
| ultra::src::problem prob(stream); | |
| prob.insert<ultra::real::abs>(); | |
| prob.insert<ultra::real::cos>(); | |
| prob.insert<ultra::real::sqrt>(); | |
| prob.insert<ultra::real::sigmoid>(); | |
| prob.insert<ultra::real::ife>(); | |
| prob.insert<ultra::real::max>(); | |
| return prob; | |
| } | |
| struct trial_params | |
| { | |
| int n_examples; | |
| int individuals; | |
| int subgroups; | |
| int generations; | |
| int code_length; | |
| bool call_init; // whether to call prob.params.init() | |
| bool use_alps; // false = std_es, true = alps_es | |
| }; | |
| static trial_params g_params; | |
| static void run_trial() | |
| { | |
| auto prob = make_problem(g_params.n_examples); | |
| if (g_params.call_init) | |
| prob.params.init(); | |
| prob.params.evolution.generations = g_params.generations; | |
| prob.params.population.individuals = g_params.individuals; | |
| prob.params.population.init_subgroups = g_params.subgroups; | |
| prob.params.slp.code_length = g_params.code_length; | |
| // Print tournament_size so we can confirm its value | |
| std::cerr << " [child] tournament_size=" | |
| << prob.params.evolution.tournament_size | |
| << " mate_zone=" << prob.params.evolution.mate_zone | |
| << " p_cross=" << prob.params.evolution.p_cross | |
| << " brood=" << prob.params.evolution.brood_recombination | |
| << "\n"; | |
| ultra::src::mae_evaluator<ultra::gp::individual> eva(prob.data); | |
| ultra::evolution<decltype(eva)> evo(prob, eva); | |
| if (g_params.use_alps) | |
| { | |
| auto result = evo.run<ultra::alps_es>(); | |
| std::cerr << " [child] alps_es best fitness = " << result.best().fit | |
| << "\n"; | |
| } | |
| else | |
| { | |
| auto result = evo.run<ultra::std_es>(); | |
| std::cerr << " [child] std_es best fitness = " << result.best().fit | |
| << "\n"; | |
| } | |
| } | |
| static int run_in_child(int timeout_sec) | |
| { | |
| pid_t pid = fork(); | |
| if (pid < 0) { perror("fork"); return -1; } | |
| if (pid == 0) | |
| { | |
| run_trial(); | |
| _exit(0); | |
| } | |
| for (int elapsed = 0; elapsed < timeout_sec; ++elapsed) | |
| { | |
| int status = 0; | |
| pid_t ret = waitpid(pid, &status, WNOHANG); | |
| if (ret > 0) | |
| { | |
| if (WIFSIGNALED(status)) return -WTERMSIG(status); | |
| if (WIFEXITED(status)) return WEXITSTATUS(status); | |
| return -1; | |
| } | |
| sleep(1); | |
| } | |
| kill(pid, SIGKILL); | |
| int status = 0; | |
| waitpid(pid, &status, 0); | |
| return -SIGALRM; // timeout sentinel | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Main | |
| // --------------------------------------------------------------------------- | |
| int main() | |
| { | |
| struct test_case | |
| { | |
| const char *label; | |
| trial_params params; | |
| int timeout_sec; | |
| int trials; | |
| }; | |
| // Section A: alps_es WITHOUT init() -- Bug 1 (unsigned underflow) | |
| // tournament_size=0, so ALPS selection loops SIZE_MAX times. | |
| // Expected: ALL timeout (any hardware). | |
| test_case section_a[] = { | |
| {"[A] alps NO-INIT ind=3 sub=4 gen=20", | |
| {200, 3, 4, 20, 16, /*init=*/false, /*alps=*/true}, 10, 1}, | |
| {"[A] alps NO-INIT ind=50 sub=4 gen=20", | |
| {200, 50, 4, 20, 16, false, true}, 10, 1}, | |
| {"[A] alps NO-INIT ind=200 sub=4 gen=20", | |
| {200, 200, 4, 20, 16, false, true}, 10, 1}, | |
| }; | |
| // Section B: alps_es WITH init() -- tests lock contention only | |
| // tournament_size=5, so no underflow. | |
| // Expected: pass (may timeout on constrained hardware due to lock contention). | |
| test_case section_b[] = { | |
| {"[B] alps INIT ind=3 sub=4 gen=20", | |
| {200, 3, 4, 20, 16, /*init=*/true, /*alps=*/true}, 30, 1}, | |
| {"[B] alps INIT ind=50 sub=4 gen=20", | |
| {200, 50, 4, 20, 16, true, true}, 30, 1}, | |
| {"[B] alps INIT ind=200 sub=4 gen=20", | |
| {200, 200, 4, 20, 16, true, true}, 30, 1}, | |
| }; | |
| // Section C: std_es WITHOUT init() -- SEGFAULT | |
| // tournament_size=0, empty parents vector, parents[0] crash. | |
| // Expected: crash with signal 11 (SIGSEGV) in Release builds. | |
| test_case section_c[] = { | |
| {"[C] std NO-INIT ind=100 sub=1 gen=5", | |
| {200, 100, 1, 5, 16, /*init=*/false, /*alps=*/false}, 10, 1}, | |
| }; | |
| std::cout << "=== Ultra GP Bug Reproduction ===\n"; | |
| std::cout << "Compiler: " | |
| #if defined(__clang__) | |
| << "Clang " << __clang_major__ << "." << __clang_minor__ | |
| #elif defined(__GNUC__) | |
| << "GCC " << __GNUC__ << "." << __GNUC_MINOR__ | |
| #else | |
| << "unknown" | |
| #endif | |
| << "\n"; | |
| std::cout << "Build: " | |
| #ifdef NDEBUG | |
| << "Release" | |
| #else | |
| << "Debug" | |
| #endif | |
| << "\n"; | |
| std::cout << "Platform: " | |
| #if defined(__APPLE__) | |
| << "macOS" | |
| #elif defined(__linux__) | |
| << "Linux" | |
| #else | |
| << "other" | |
| #endif | |
| << " " << sizeof(void *) * 8 << "-bit\n"; | |
| std::cout << "Hardware threads: " | |
| << std::thread::hardware_concurrency() << "\n\n"; | |
| auto run_section = [](const char *title, test_case *cases, int n, | |
| int &total_pass, int &total_timeout, int &total_crash) | |
| { | |
| std::cout << "======= " << title << " =======\n\n"; | |
| for (int i = 0; i < n; ++i) | |
| { | |
| const auto &tc = cases[i]; | |
| std::cout << "--- " << tc.label << " ---\n"; | |
| int pass = 0, timeout = 0, crash = 0; | |
| for (int t = 0; t < tc.trials; ++t) | |
| { | |
| g_params = tc.params; | |
| int rc = run_in_child(tc.timeout_sec); | |
| if (rc == -SIGALRM) | |
| { | |
| ++timeout; | |
| std::cout << " Trial " << (t + 1) << "/" << tc.trials | |
| << ": TIMEOUT (" << tc.timeout_sec << "s)\n"; | |
| } | |
| else if (rc < 0) | |
| { | |
| ++crash; | |
| std::cout << " Trial " << (t + 1) << "/" << tc.trials | |
| << ": CRASHED (signal " << (-rc) << ")\n"; | |
| } | |
| else if (rc == 0) | |
| { | |
| ++pass; | |
| std::cout << " Trial " << (t + 1) << "/" << tc.trials | |
| << ": completed\n"; | |
| } | |
| else | |
| { | |
| ++crash; | |
| std::cout << " Trial " << (t + 1) << "/" << tc.trials | |
| << ": exit code " << rc << "\n"; | |
| } | |
| } | |
| int failures = timeout + crash; | |
| if (failures > 0) | |
| { | |
| std::cout << " => FAILED ("; | |
| if (crash > 0) std::cout << crash << " crashed"; | |
| if (crash > 0 && timeout > 0) std::cout << ", "; | |
| if (timeout > 0) std::cout << timeout << " timed out"; | |
| std::cout << ")\n"; | |
| } | |
| else | |
| std::cout << " => PASSED\n"; | |
| std::cout << "\n"; | |
| total_pass += pass; | |
| total_timeout += timeout; | |
| total_crash += crash; | |
| } | |
| }; | |
| int total_pass = 0, total_timeout = 0, total_crash = 0; | |
| run_section("Section A: alps_es WITHOUT init() (Bug 1: unsigned underflow)", | |
| section_a, 3, total_pass, total_timeout, total_crash); | |
| run_section("Section B: alps_es WITH init() (lock contention test)", | |
| section_b, 3, total_pass, total_timeout, total_crash); | |
| run_section("Section C: std_es WITHOUT init() (Bug 2: SEGFAULT)", | |
| section_c, 1, total_pass, total_timeout, total_crash); | |
| std::cout << "=== Totals: " << total_pass << " passed, " | |
| << total_timeout << " timed out, " | |
| << total_crash << " crashed ===\n\n"; | |
| std::cout << "Expected results:\n"; | |
| std::cout << " Section A: ALL timeout (tournament_size=0 -> SIZE_MAX loop)\n"; | |
| std::cout << " Section B: ALL pass (tournament_size=5, no underflow)\n"; | |
| std::cout << " Section C: crash signal 11 (empty parents[0] access)\n"; | |
| return 0; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| cmake_minimum_required(VERSION 3.24) | |
| project(alps_repro LANGUAGES CXX) | |
| set(CMAKE_CXX_STANDARD 23) | |
| set(CMAKE_CXX_STANDARD_REQUIRED ON) | |
| include(FetchContent) | |
| # Only apply the <print> compatibility patch (needed for compilation). | |
| # Do NOT apply the ALPS lock fix -- we want to reproduce the bug. | |
| set(PRINT_PATCH ${CMAKE_CURRENT_SOURCE_DIR}/print-compat.patch) | |
| FetchContent_Declare( | |
| ultra | |
| GIT_REPOSITORY https://github.com/morinim/ultra.git | |
| GIT_TAG main | |
| SOURCE_SUBDIR src | |
| PATCH_COMMAND sh -c "git apply --reverse --check ${PRINT_PATCH} 2>/dev/null || git apply ${PRINT_PATCH}" | |
| ) | |
| FetchContent_MakeAvailable(ultra) | |
| add_executable(alps_repro alps_repro.cc) | |
| target_link_libraries(alps_repro PRIVATE ultra) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/src/utility/log.cc b/src/utility/log.cc | |
| index 6f7eebb..39a4e89 100644 | |
| --- a/src/utility/log.cc | |
| +++ b/src/utility/log.cc | |
| @@ -15,7 +15,11 @@ | |
| #include <algorithm> | |
| #include <chrono> | |
| #include <fstream> | |
| +#if __has_include(<print>) | |
| #include <print> | |
| +#else | |
| +#include <iostream> | |
| +#endif | |
| #include <string_view> | |
| auto std::formatter<ultra::log::level>::format(ultra::log::level l, | |
| @@ -86,14 +90,26 @@ log::~log() | |
| if (level_ >= reporting_level) // `stdout` is selective | |
| { | |
| // Clear the line using a width specifier and a carriage return. | |
| +#if __cpp_lib_print >= 202207L | |
| std::print("\r{:60}\r", ""); | |
| +#else | |
| + std::cout << "\r" << std::string(60, ' ') << "\r"; | |
| +#endif | |
| // Print the tag if necessary. | |
| if (level_ != lSTDOUT && level_ != lPAROUT) | |
| +#if __cpp_lib_print >= 202207L | |
| std::print("[{}] ", level_); | |
| +#else | |
| + std::cout << "[" << std::format("{}", level_) << "] "; | |
| +#endif | |
| // Flush for console. | |
| +#if __cpp_lib_print >= 202207L | |
| std::println("{}", message); | |
| +#else | |
| + std::cout << message << std::endl; | |
| +#endif | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment