Skip to content

CPU pool: sleep between requests instead of spinning (fixes #4) - #9

Merged
Niko1221 merged 1 commit into
Niko1221:mainfrom
Mirtraxxx:idle-pool-sleep
Sep 26, 2026
Merged

Niko1221 merged 1 commit into
Niko1221:mainfrom
Mirtraxxx:idle-pool-sleep

Conversation

@Mirtraxxx

Copy link
Copy Markdown
Contributor

Fixes #4.

What it does

A parked expert-pool worker waited for work with an endless _mm_pause spin on epoch_, so every worker's core stayed
at 100% while the engine sat idle between requests. On a Ryzen 7 5700X that's 7 cores, which the issue reports as
"CPU 50% even when doing nothing".

Now a parked worker spins exactly as before for up to 20 ms (ExpertPool::kSpinBeforeSleep), then blocks on a
condition variable:

  • Every publish (run, run_phase, the destructor) goes through a new publish(). It bumps epoch_ and notifies
    only if a worker is actually asleep, so the token path pays one uncontended load and never touches the mutex.
  • Lost wakeups are ruled out by seq_cst on both sides. The worker does sleepers_++ and then reads epoch_ under the
    mutex. The host does epoch_++ and then reads sleepers_. At least one of them sees the other's write. On x86 the
    fetch_add is a locked xadd either way.
  • The clock is read once every 1024 pauses, so the spin itself is unchanged.
  • Inside a request the gaps between batches are microseconds, so workers don't sleep mid-token. During a long GPU-only
    stretch of a prefill they can; waking costs microseconds against chunks of hundreds of milliseconds.

Measured

RTX 3090, Ryzen 7 5700X (7 pool workers + host), 64 GB DDR4, Windows 11, Swift 1.5 IQ2_XS, --serve,
--expert-cache auto --spec 4 --adapt-swaps 0. Old and new engine, same flags, each run twice. CPU is process CPU
time over wall time, in cores.

old new
Idle before any request (10 s) 6.98 / 6.41 cores 0.00 / 0.00
Idle after requests (10 s) 6.95 / 6.92 cores 0.00 / 0.01
Decode, 2.3K-token code prompt 61.8 / 62.9 tok/s 64.1 / 62.8 tok/s
Decode, short math prompt (384 tokens) 52.5 / 52.0 tok/s 50.3 / 54.3 tok/s
Decode, short writing prompt 47.1 / 48.5 tok/s 50.6 / 48.0 tok/s
Prefill, 2,298 tokens 9.15 / 9.49 s 8.65 / 8.59 s

Decode speed is unchanged within run-to-run noise. The 2.3K-token prefill came out ~7% faster in both runs, likely
because the workers sleep through the GPU-only stretches and stop competing with the host for cores.

Greedy output: the code prompt is identical across all four runs. The other two diverge late in some runs, but the
old engine also diverges from itself between runs (math at token 53), so that's the existing run-to-run variation,
not this change. The pool's arithmetic isn't touched.

Only tested on Windows / one machine. The Linux path uses the same std::condition_variable.

🤖 Generated with Claude Code

A parked expert-pool worker spun on `epoch_` with `_mm_pause` forever, so every
worker's core stayed at 100% while the engine waited for a request: 7 cores
(44% of a 16-thread Ryzen 7 5700X) with nothing to do.

A parked worker now spins as before for up to 20 ms, then blocks on a
condition variable. Every publish goes through `publish()`, which bumps the
epoch and notifies only when a worker is actually asleep (one uncontended
load otherwise). Inside a request the gaps between batches are microseconds,
so the token path never sleeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lvckydrip

Copy link
Copy Markdown

Really like this PR! I actually tried implementing the same approach locally with std::atomic::wait() for the idle pool workers. It made a noticeable difference to CPU usage while the pool was idle.

Great to see this being addressed upstream!

@Niko1221

Copy link
Copy Markdown
Owner

Thanks on noticing. I will handle it soon.

@XeonG

XeonG commented Sep 26, 2026

Copy link
Copy Markdown

awesome

@Niko1221
Niko1221 merged commit 0cf01f5 into Niko1221:main Sep 26, 2026
@Niko1221

Copy link
Copy Markdown
Owner

Merged! Tested it on my box: idle CPU went from 5 cores to basically 0, and decode speed didn't move at all. The wakeup handshake is clean. Out in v0.1.3.

MistVVK referenced this pull request in MistVVK/XeStrata Oct 3, 2026
FlareP1 pushed a commit to FlareP1/Strata that referenced this pull request Oct 13, 2026
Niko1221 added a commit that referenced this pull request Oct 23, 2026
…he app's plain issues (TUI stress #9, C05, #3478)

plan_import now returns missing_shards (the file names of the split set that are not in the folder). problem_words and plain_issues say "<name>-00002-of-00002.gguf is missing" instead of "some parts of the model are missing"; more than two are counted. The three call sites (models import, strata launch --model-path, the Model screen's own-files field) pass the list.
nmrugg added a commit to nmrugg/Strata that referenced this pull request Oct 24, 2026
…he chat it belongs to and paint() resolves its row each frame, instead of holding one element for the whole turn (todo Niko1221#9)
RandoTeam referenced this pull request in RandoTeam/4gbVramStrata Nov 1, 2026
[Setup] a 4 GB card: warn, don't ask, when the model starts
qwertie added a commit to qwertie/Strataq that referenced this pull request Nov 4, 2026
…one hop away

On separate cards, strata-ds41 kept a moved layer's dense weights on the
old card and could not use the other card's expert copies, unlike
strata-q38. When the live cut moves, ds4_model.cu now reloads the moved
layers' dense weights on the card that runs them, and paged_model.cpp
runs a layer's expert on the other card's copy (one hop) with
--remote-experts; auto calibrates against the CPU. The shared paged
engine rolls a failed cut move back.

Two stages on one GPU give the same 96-token answers on one stage, a
fixed split and a live move from layer 10 to 20. A stress move
20 -> 11 -> 20 with a quarter-size pool differs from token 71 and
loses GPU experts; the cause is open (DETAILS.md). Hops and real cards
are untested here.

### Also

tools/nvidia_session: measure hops off/auto/on, per-layer dense-weight
moves and hop counts on two real cards, with memory guards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cpu 50% even when not procesesing anything, doing nothing...

4 participants