Flaky: test_mip_incumbent_stream hangs in PROCESSING after the preceding cancel test kills the gRPC worker
@ramakrishnap-nv is already working on this.
Since Aug 13, 2026.
- Dominant language
- Cuda
- Stars
- 1k
- Forks
- 233
- Avg merge
- 4d 4h
- Merged PRs (30d)
- 95
Description
Summary
TestGrpcClient::test_mip_incumbent_stream flakes in PR CI: the submitted MIP job never leaves PROCESSING, the client's 120 s poll expires, and the test fails with a misleading assertion. It passed on re-run of the same commit, so the failure is not related to the PR's changes.
Observed on run 31686594110, attempt 1 (conda-python-tests / 13.3.0, 3.14, arm64, ubuntu26.04, l4), PR #1700:
terminal = _poll_until_complete(client, job_id, _MIP_NAMES)
> assert terminal == JobStatus.COMPLETED
E assert <JobStatus.PROCESSING: 1> == <JobStatus.COMPLETED: 2>
tests/linear_programming/test_grpc_client.py:276
Everything else in the job passed (1 failed, 126 passed, 9 skipped). Attempt 2 of the same run was fully green.
What the failure actually means
The job reached PROCESSING, which in check_job_status() (cpp/src/grpc/server/grpc_job_management.cpp:191) only happens once a worker has CAS-claimed the queue slot. So a worker took the job and then produced no result for 120 s — even though the test sets time_limit = 30. The solver time limit only bounds solve_mip(); it does not bound anything before the solve starts (worker CUDA/RMM init, problem transfer over the worker pipe), and nothing on the server side ever times a claimed job out. A wedged or very slow worker leaves the job in PROCESSING indefinitely.
Two properties of the test environment make that window much more likely on this runner:
- The immediately preceding test SIGKILLs the worker.
test_cancel_job(line 221) cancels a mid-solveswath1.mpsjob;cancel_job()sendsSIGKILLto the worker PID (grpc_job_management.cpp:283). The worker-monitor thread then forks a replacement (grpc_server_threads.cpp:12-80), which must redocudaSetDevice+ allocate a fresh RMM pool (grpc_worker.cpp:43-83) while the killed process's GPU memory is still being reclaimed by the driver.test_mip_incumbent_streamsubmits into exactly that window. - Four xdist workers share one GPU. PR CI runs
pytest -n 4 --dist loadgroup(ci/run_cuopt_pytests.sh:41), so three other GPU test processes are competing with the respawning worker for the L4.
Also worth noting: if init_worker_rmm_pool() throws (rmm::bad_alloc under contention), nothing catches it — init_worker_cuda_environment() and worker_process() have no try/catch, so the worker aborts and the monitor respawns it in a 100 ms loop with no backoff.
None of this is diagnosable from CI today, because the fixture starts the server with stdout=DEVNULL, stderr=DEVNULL (python/cuopt/cuopt/tests/fixtures/grpc_server_fixtures.py, spawn_server). We have no server or worker log for the failing attempt.
Finally, PR CI does not rerun: --reruns 2 --reruns-delay 5 is applied only when RAPIDS_BUILD_TYPE=nightly (ci/run_cuopt_pytests.sh:36-41). So one flake blocks the PR and the classifier reports it as "1 genuine test failure".
Proposed fixes
Observability (do first — otherwise the next occurrence is equally opaque)
-
spawn_server()should tee server stdout/stderr to a file under the test's tmp dir instead ofDEVNULL, and thegrpc_serverfixture should dump the tail of that file when a test in the class fails. - Make
_poll_until_completeraise an explicit timeout error (elapsed time + last observed status + incumbent count) rather than returning a non-terminal status that surfaces asPROCESSING != COMPLETED.
Test-level flake removal
- Don't let a worker-killing test run immediately before a solve test on the same server: either move
test_cancel_jobto the end ofTestGrpcClient, or give it its own server instance (its owngrpc_port_offset). - After a cancel, wait for worker readiness before the next submit — needs a server-side health signal (below), or, as a stopgap, a fixture-level wait that submits a trivial probe job and waits for it to complete.
- Reduce poll pressure in
_poll_until_complete: it issuesstatus+resultevery 50 ms (~40 RPC/s), all takingtracker_mutex, which the result-retrieval thread also needs to mark the jobCOMPLETED. 200-250 ms is plenty. - Consider
--reruns 2for PR CI too, at least for thegrpc_serverxdist group.
Server-side (the real fix)
- Add a job watchdog: if a claimed job exceeds its requested
time_limitplus a margin, transition it toFAILEDwith a clear message instead of leaving itPROCESSINGforever. Today a wedged worker is invisible to the client. - Expose worker readiness (
active_workersinSharedMemoryControlis already tracked) via the health/status RPC so clients can distinguish "queued behind a restart" from "running". - Wrap
init_worker_cuda_environment()/init_worker_rmm_pool()in a try/catch that logs and exits cleanly, and add backoff to the monitor's respawn path so a persistently failing worker doesn't fork-storm.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.