Skip to content

[BUG] Solve() aborts the process (OMP kmp_alloc assertion) when each solve runs on a freshly created thread #1768

Description

@mal84emma

Note

The investigation and this report were prepared by an AI agent (Claude) on behalf of the reporter.

Describe the bug

When each Solve() call is made from a newly created Python thread, cuOpt aborts the process after a small number of sequential solves:

OMP: Error #13: Assertion failure at kmp_alloc.cpp(2725).

(exit 134), or a bare SIGSEGV with no message at all (exit 139). There is no Python exception and no traceback — the interpreter dies. Running the identical solves on the main thread, or on a single long-lived worker thread, is completely clean.

This looks like per-thread state in the bundled LLVM OpenMP runtime (libomp-8fe85495.so, a NEEDED of libcuopt.so, resolving to libcuopt_cu12.libs/libomp-8fe85495.so) that is not released when the thread that entered the solver exits, so each new solver-entering thread leaks a thread record until the runtime's allocator invariant breaks.

Both MILP and LP are affected. MILP merely reaches the threshold fastest — consistently on the 2nd solve here, versus the 5th–13th for LP. The failing signature is identical in both cases.

This is a hard, uncatchable process abort, and it is triggered by an entirely ordinary pattern: thread-per-request services, and worker-thread wrappers used to keep the calling thread interruptible.

Steps/Code to reproduce bug

Self-contained; only cuopt and numpy are needed. A small knapsack (20 variables, 3 <= rows) that is trivially feasible (x = 0) and bounded, solved N times sequentially, with a --mode switch controlling which thread each solve runs on.

#!/usr/bin/env python3
"""Minimal reproduction: cuOpt Solve() aborts the process when each solve runs
on a freshly created Python thread."""

import argparse
import queue
import sys
import threading

import numpy as np

from cuopt.linear_programming import DataModel, Solve, SolverSettings
from cuopt.linear_programming.solver.solver_parameters import CUOPT_TIME_LIMIT


def build_model(integer=True, n=20, seed=0):
    """Small, definitely feasible and bounded knapsack.

    maximize sum_i v_i x_i  s.t.  sum_i w_ij x_i <= cap_j  (j = 0..2),
    0 <= x_i <= 1, x_i integer (or continuous with --lp).
    """
    rng = np.random.default_rng(seed)
    m = 3
    w = rng.integers(1, 10, size=(m, n)).astype(np.float64)
    v = rng.integers(1, 20, size=n).astype(np.float64)
    cap = 0.5 * w.sum(axis=1)

    model = DataModel()
    offsets = np.arange(m + 1, dtype=np.int32) * n
    indices = np.tile(np.arange(n, dtype=np.int32), m)
    model.set_csr_constraint_matrix(w.reshape(-1), indices, offsets)
    model.set_row_types(np.array(["L"] * m, dtype=object))
    model.set_constraint_bounds(cap)
    model.set_objective_coefficients(v)
    model.set_maximize(True)
    model.set_variable_lower_bounds(np.zeros(n))
    model.set_variable_upper_bounds(np.ones(n))
    model.set_variable_types(np.array(["I" if integer else "C"] * n, dtype=object))
    return model


def solve_once(i, integer, out):
    model = build_model(integer=integer, seed=i)
    settings = SolverSettings()
    settings.set_parameter(CUOPT_TIME_LIMIT, 5.0)
    solution = Solve(model, settings)
    out.append((i, str(solution.get_termination_reason()),
                float(solution.get_primal_objective())))


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--mode", required=True,
                    choices=["main", "fresh-thread", "reused-thread"])
    ap.add_argument("--n-solves", type=int, default=10)
    ap.add_argument("--lp", action="store_true",
                    help="solve a pure LP (continuous variables) instead of a MILP")
    args = ap.parse_args()

    integer = not args.lp
    kind = "MILP" if integer else "LP"
    print(f"mode={args.mode} kind={kind} n_solves={args.n_solves}", flush=True)

    def run(i):
        out = []
        if args.mode == "main":
            solve_once(i, integer, out)
        elif args.mode == "fresh-thread":
            # A brand new OS thread for every single solve.
            t = threading.Thread(target=solve_once, args=(i, integer, out))
            t.start()
            t.join()
        return out

    if args.mode == "reused-thread":
        # One long-lived worker thread handles every solve.
        jobs, results = queue.Queue(), []

        def worker():
            while True:
                i = jobs.get()
                if i is None:
                    return
                solve_once(i, integer, results)
                print(f"  solve {i + 1}/{args.n_solves} -> {results[-1][1]}", flush=True)

        t = threading.Thread(target=worker)
        t.start()
        for i in range(args.n_solves):
            jobs.put(i)
        jobs.put(None)
        t.join()
        done = len(results)
    else:
        done = 0
        for i in range(args.n_solves):
            out = run(i)
            if not out:
                print(f"  solve {i + 1}/{args.n_solves} -> NO RESULT", flush=True)
                sys.exit(1)
            done += 1
            print(f"  solve {i + 1}/{args.n_solves} -> {out[-1][1]}", flush=True)

    print(f"ALL {done} SOLVES COMPLETED", flush=True)


if __name__ == "__main__":
    main()

Run each in its own process, so the abort is observable rather than killing a parent:

python repro.py --mode main          --n-solves 10        # MILP, main thread          -> clean
python repro.py --mode reused-thread --n-solves 10        # MILP, one worker thread    -> clean
python repro.py --mode fresh-thread  --n-solves 10        # MILP, thread per solve     -> ABORTS
python repro.py --mode fresh-thread  --n-solves 30 --lp   # LP,   thread per solve     -> ABORTS

Observed output in fresh-thread mode — the process dies mid-solve with no Python-level error:

Running cuOpt presolve
Running probing cache with 7 tasks
Assertion failure at kmp_alloc.cpp(2725): ((kmp_mem_descr_t *)((char *)next - sizeof(kmp_mem_descr_t))) ->size_allocated + 1 == ((kmp_mem_descr_t *)((char *)tail - sizeof(kmp_mem_descr_t))) ->size_allocated.
OMP: Error #13: Assertion failure at kmp_alloc.cpp(2725).
OMP: Hint Please submit a bug report with this message, compile and run commands used, and machine configuration info including native compiler and operating system versions. Faster response will be obtained by including all program sources. For information on submitting this issue, please see https://github.com/llvm/llvm-project/issues/.

Key evidence — where the solve runs is what decides it

Every row is one process; "solves completed" counts solves that finished before the process died.

Problem Solve thread Runs Result
MILP main thread 1 10/10 clean, exit 0
MILP one reused worker thread 1 10/10 clean, exit 0
MILP fresh thread per solve 6 6/6 aborted, always after exactly 1 completed solve (dies during solve 2) — exit 134 (5x), exit 139 (1x)
LP main thread 1 30/30 clean, exit 0
LP one reused worker thread 1 30/30 clean, exit 0
LP fresh thread per solve 4 4/4 aborted after 5, 8, 10 and 13 completed solves — exit 134 (2x), exit 139 (2x)

Two notes on the LP row, since it is easy to misread this as MIP-specific:

  • An LP fresh-thread run limited to 10 solves finished cleanly 10/10. Capped at small N, the LP path looks unaffected; at N=30 it fails every time. The LP path has a higher threshold, not immunity.
  • The LP runs take a genuine LP path — the log reports 20 variables (0 integers), PSLP presolve and concurrent dual simplex / PDLP / barrier, with no branch-and-bound anywhere.

Expected behavior

Every mode should print ALL <n> SOLVES COMPLETED and exit 0. Repeatedly entering the solver from short-lived threads should not accumulate unbounded per-thread OpenMP state, and certainly should not abort the process.

Environment details (please complete the following information):

  • Environment location: Cloud (Microsoft Azure VM), bare-metal Python virtualenv — no container
  • Method of cuOpt install: pip, from public PyPI (cuopt-cu12==26.8.0)
cuOpt cuopt-cu12 / libcuopt-cu12 26.8.0 (cuopt.__version__ = 26.08.00, git hash 400863c)
Latest on PyPI 26.8.0 — the newest published release is the one tested
OpenMP runtime bundled libomp-8fe85495.so (LLVM libomp) in libcuopt_cu12.libs, a NEEDED of libcuopt.so
GPU / driver Tesla T4, driver 535.274.02
CUDA cuOpt banner reports CUDA 12.9; nvidia-cuda-runtime-cu12 12.9.79
CPU AMD EPYC 7V12, 8 physical / 8 logical threads
OS / kernel Ubuntu 22.04.5 LTS, 6.8.0-1044-azure
Python 3.11.15

Additional context

Workaround: never let a thread that has entered Solve() exit and be replaced by a new one. Either solve on the main thread, or route every solve through a single long-lived worker thread. Both were clean across every run above, including LP at 30 solves.

Found while integrating cuOpt as a linopy backend, where solves were dispatched on a worker thread per call to keep the calling thread interruptible.

Related but, as far as I can tell, distinct existing issues:

One observation offered only as a possible lead, not a diagnosis: PR #1099 migrated most of the MIP solver from std::thread to OpenMP tasking, and notes that "only a single parallel region [is] created at the beginning of the solver, so it can be shared across the MIP solver." A parallel region opened once per solve entry, on whichever thread calls in, would be consistent with the MILP path exhausting the per-thread bookkeeping several times faster than the LP path does — but I have not traced the allocation to confirm that.

Full crash log — MILP, fresh-thread mode (tail)
New solution from early primal heuristics (CPUFJ). Objective +1.190000e+02. Time 0.01
New solution from early primal heuristics (CPUFJ). Objective +1.260000e+02. Time 0.01
New solution from early primal heuristics (CPUFJ). Objective +1.280000e+02. Time 0.01

Running Papilo presolve (git hash 32b3a87d)
Presolve status: did not result in any changes
Presolve removed: 0 constraints, 0 variables, 0 nonzeros
Presolved problem: 3 constraints, 20 variables (20 integer), 60 nonzeros
Papilo presolve time: 0.02
Objective offset -0.000000 scaling_factor -1.000000
Model fingerprint: 0x388ac148

Running cuOpt presolve
Running probing cache with 7 tasks
Assertion failure at kmp_alloc.cpp(2725): ((kmp_mem_descr_t *)((char *)next - sizeof(kmp_mem_descr_t))) ->size_allocated + 1 == ((kmp_mem_descr_t *)((char *)tail - sizeof(kmp_mem_descr_t))) ->size_allocated.
OMP: Error #13: Assertion failure at kmp_alloc.cpp(2725).
OMP: Hint Please submit a bug report with this message, compile and run commands used, and machine configuration info including native compiler and operating system versions. Faster response will be obtained by including all program sources. For information on submitting this issue, please see https://github.com/llvm/llvm-project/issues/.
Full crash log — LP, fresh-thread mode (tail), showing the LP-only solve path
Solving a problem with 3 constraints, 20 variables (0 integers), and 60 nonzeros
...
Using PSLP presolver
PSLP Presolved problem: 3 constraints, 20 variables, 60 non-zeros
PSLP presolve time: 0.00s
Objective offset -0.000000 scaling_factor -1.000000
Running concurrent (showing only PDLP log)

Dual simplex finished in 0.00 seconds
Barrier finished in 0.01 seconds
Assertion failure at kmp_alloc.cpp(2725): ((kmp_mem_descr_t *)((char *)next - sizeof(kmp_mem_descr_t))) ->size_allocated + 1 == ((kmp_mem_descr_t *)((char *)tail - sizeof(kmp_mem_descr_t))) ->size_allocated.
OMP: Error #13: Assertion failure at kmp_alloc.cpp(2725).
First-solve banner (version / device / build identification)
Setting parameter time_limit to 5.000000e+00
cuOpt version: 26.8.0, git hash: 400863c1, host arch: x86_64, device archs: 70-real,75-real,80-real,86-real,90a-real,100f-real,120a-real,120
CPU: AMD EPYC 7V12 64-Core Processor, threads (physical/logical): 8/8, RAM (available/total): 49.73 / 54.92 GiB
CUDA 12.9, device: Tesla T4 (ID 0), VRAM: 15.57 GiB

Metadata

Metadata

Assignees

No one assigned

    Labels

    awaiting responseThis expects a response from maintainer or contributor depending on who requested in last comment.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions