Skip to content

Flaky: compute pool binds to a cpu list races on distinct > 1 #30

Description

@marcobambini

compute pool binds to a cpu list fails intermittently at tests/test_cpus.c:198,
CHECK(distinct > 1). Reported by @fab2s in
#27 (comment) while working on an
unrelated branch — neither commit there touches the pool or this test.

Observed rates, on a 24-thread Linux box:

  • 6 failures in 12 full suite runs, every one at distinct > 1
  • ./test_cpus bind standalone: 8/8 pass idle, 1/40 fail with 24 spinners running

Not reproduced here: macOS has no affinity readback, so check_bind returns 77 and the
suite reports SKIP. It needs a Linux or Windows box to see.

What the check asserts, and why it is not guaranteed

check_bind picks the first two CPUs in the process mask, so picked == 2 and
waste_pool_init(0, &want) gives nthreads == 2 — one worker plus the calling thread,
which is a worker too. Then:

waste_parallel_for(nthreads * 4, 1, note_range, NULL);

distinct > 1 says "more than the caller took a chunk", and the comment above it is
right about why it has to be there: without it the test proves only that the caller was
bound and says nothing about the pool. But nothing in waste_parallel_for guarantees
it. Chunks are handed out from g_pool.next_chunk under a mutex, and the caller loops
on that counter itself:

for (;;) {
    ...
    const int b = g_pool.next_chunk;
    if (b >= n) { ...; break; }
    g_pool.next_chunk = b + chunk;
    ...
    fn(b, e, arg);
}

If the caller comes back for the next chunk before the worker has woken from
pthread_cond_broadcast, it takes that one too. Drain them all and distinct == 1
which reads as "the pool did not participate" when it only did not need to.

Two details that change the fix

There are exactly nthreads chunks, not nthreads * 4. chunk is
ceil(n / nthreads), not min_chunk:

int chunk = (n + g_pool.nthreads - 1) / g_pool.nthreads;   /* (8 + 1) / 2 == 4 */
if (chunk < min_chunk) chunk = min_chunk;

so n = 8, min_chunk = 1, nthreads = 2 is 2 chunks of 4, one per thread. The
caller only has to beat the worker to the second one. This also means "tie the chunk
count to the thread count" — the fix floated in the PR thread — is already the case and
would not move anything.

The test already tries to prevent this, and the mitigation is the part that is
actually failing:

/* ...then burns enough time that the next chunk is taken
 * by a different thread rather than by this one coming back for it. */
volatile long sink = 0;
for (int i = b; i < e; i++)
    for (long k = 0; k < 2000000L; k++) sink += k;

That is a wall-clock assumption: the caller must still be spinning in range 1 when the
worker wakes. It holds on an idle machine and stops holding when the box is loaded —
which is exactly the shape of the numbers above, and why the failure is much more common
inside a full suite run (the suite is the load) than standalone.

Suggested fix

Make the participation a rendezvous instead of a race. Since waste_parallel_for emits
exactly nthreads chunks here, a barrier of nthreads participants inside note_range
is precisely the right assertion: every thread that is supposed to take a chunk must
arrive before any of them may leave, so the caller cannot drain the queue no matter how
the scheduler behaves.

It needs a timeout rather than a plain pthread_barrier_wait, so that a pool which
genuinely does not participate — the bug worth catching — fails the check instead of
hanging the suite. On timeout, report it as "only the caller took a chunk" and return 1.

The fixed spin can then go: it exists only to approximate the rendezvous, and it costs
8M iterations per range on every run.

Worth doing rather than muting — the check is the only thing in the suite that proves
--cpus / WASTE_CPUS reaches the pool's threads and not just the caller, and a flaky
check that people learn to re-run is one nobody reads when it means it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions