Skip to content

test_cpu_threadpool 'oversubscribed dispatch' ratio guard is unstable: it fired at 101.567 on the 4-core CI runner with a 0.48us denominator #631

Description

@localai-bot

What

build-test-cpu went red on an unrelated PR with one failure out of 404:

303/404 Test #303: test_cpu_threadpool ...***Failed    0.36 sec
tests/vt/test_cpu_threadpool.cpp:536: MESSAGE: empty-op dispatch: 2 threads 0.48 us, 5 threads 48.752 us, ratio 101.567
tests/vt/test_cpu_threadpool.cpp:539: ERROR: CHECK( ratio < 100.0 ) is NOT correct!
  values: CHECK( 101.567 <  100 )
[doctest] test cases:     9 |     8 passed | 1 failed | 1 skipped
[doctest] assertions: 19602 | 19601 passed | 1 failed |

Evidence: https://github.com/mudler/vllm.cpp/actions/runs/31698569035/job/94441971697

Why the guard is fragile

TEST CASE: oversubscribed dispatch does not cost a scheduler timeslice
(tests/vt/test_cpu_threadpool.cpp:499, added by 2028c8c74 for #391) asserts a
ratio of two wall-clock medians, over_us / fits_us, against a fixed 100.

The numerator and denominator are both machine-shape dependent, and the
denominator is the problem: it is the dispatch cost at a thread count that
fits the box. On a 2-core runner that was 0.48 us, small enough that
ordinary scheduler noise in the numerator moves the ratio by tens.

The two observations, same commit, same code:

Box fits over ratio
2-core CI runner 2 threads, 0.48 us 5 threads, 48.752 us 101.567 (RED)
20-core dev box 10 threads, 7.213 us 21 threads, 19.467 us 2.699 (GREEN, 37x margin)

A guard that reads 2.7 on one machine and 101.6 on another is not measuring what
it names. It fires at 1.6% over the line on the small box while a genuine
regression on the large box would need a 37x blowup to trip it. That is the
shape this repo has recorded before: a tolerance that bounds nothing at one end
and everything at the other.

Not a flake to be re-run away

It is intermittent -- build-test-cpu is green on the scheduled main lane,
including the newest baseline 7572b0f4e2fb -- but the cause is structural, not
random: it depends on the runner's core count and on how small fits_us lands.
Re-running moves the dice, it does not fix the guard.

Suggested direction (needs its own spec, per AGENTS.md)

The intent is "the waiter yields its core instead of spinning through a
scheduler timeslice". That is a claim about an ABSOLUTE cost -- a timeslice is
milliseconds -- not about a ratio. Bounding over_us against a timeslice-scaled
absolute, or skipping when fits_us is below a floor where the ratio stops
being meaningful, would both keep the finding and lose the machine dependence.

Explicitly not proposed: raising 100 to some larger number. That is widening
a scope to turn a red gate green, which the protocol forbids and which would
make the large-box end even more permissive than it already is.

Found while landing #606, whose diff is argument parsing only and cannot reach
the threadpool. Filed rather than fixed in flow: this changes a gate's semantics,
so it needs a spec and red-before evidence of its own.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions