hybrid decode halves (23 → 11 tok/s) whenever a foreign thread lands on a pinned spinning CPU-MoE pool CPU; please detect/warn, consider SCHED_FIFO or a yielding spin
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 42/100
- issue の種類
- バグ
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- linux, python
- 領域
- backend, performance
調査の方向性
Start with the hybrid CPU-MoE pool and its pinned worker threads; the issue does not name source files or tests, so first locate their implementation and the existing --moe-collect-stats path (#231). Check whether pool CPUs can be monitored for competing runnable tasks and whether barrier wait times are already recorded. Done means the issue’s detection and warning, barrier statistics, robustness, or documentation work has a defined scope and can be verified against the reported slowdown.
索引モデルが issue の本文から書いたものです。
説明
Summary
With --moe-strategy hybrid on DeepSeek-V4.1-Flash, decode on one server process alternated between ~23 tok/s and ~11 tok/s for a day and a half. Same request, same flags, no log line in either mode. We traced it to any runnable foreign thread sharing a logical CPU with one of the pinned, spinning CPU-MoE pool threads: every layer's barrier waits for that one worker, so a small intruder costs a lot, and once a thread is parked there the kernel rarely moves it, so it looks like a sticky "mode". Fencing every other process off the pool's CPUs with systemd CPUAffinity removed it (0 slow requests since, under the loads that used to trigger it).
Filing because (a) nothing in the server surfaces this, so it cost us ~20 hours to find, and (b) a pool that spins on pinned cores could defend itself.
Environment
- FreeToken nightly
0.1.3+g8f49e0af9 - DeepSeek-V4.1-Flash-1017,
ds_fp4(act_block=32), 40 MoE layers, top-6 - 2× EPYC 7742 (2 NUMA nodes × 64 cores, SMT on, 256 logical CPUs), 1 TB DDR4-2666, RTX 3090 24 GB on node 1 (PCIe Gen4 x16), driver 595.x, Linux 7.0.0
numactl --physcpubind=64-127,192-255 --membind=1 ft serve --moe-strategy hybrid --disable-moe-prefill-overlap --kv-reserve-tokens 65536 --max-prefill-length 2048 --memory-ratio 0.85- Startup:
CPU MoE executor ready: threads=63 (pinned to cores 64..126) … spin_wait=True handshake=kernel,--moe-hybrid-max-fetch auto: 18.1%, CUDA graphs [1,2,4]
Symptom
Single stream, 150-token completions, temperature 0, thinking off:
| fast | slow | |
|---|---|---|
engine gen throughput |
22–24 tok/s | 10.6–13 tok/s |
| GPU util / power | ~60 %, at the 230 W cap | ~30 %, 185 W |
| per token | 44 ms | 88 ms (≈ +1.1 ms per MoE layer) |
| prefill / TTFT | unchanged | unchanged |
| main thread, workers, coordinator | 100 % busy | 100 % busy |
Fresh starts came up in either mode (6 of ~12 slow one afternoon). A sched_setaffinity on the main thread flipped fast→slow 4 of 6 times. Nsight (fast mode) shows the GPU idling a median 0.43 ms before each layer's combine kernel, i.e. waiting on the pool; we never got a slow-mode capture because the trigger stopped working once the box went quiet after midnight — which was the clue.
Cause (direct test on a live server, no restart)
A pinned busy thread placed by hand:
| intruder | decode |
|---|---|
| none | 23.9 tok/s |
| 20 %-duty thread pinned to a pool CPU (70) | 19.2–19.7; back to 23.6 the moment it is killed |
| same thread on that core's idle SMT sibling (198) | 23.0 (no effect) |
| 100 %-duty thread on pool CPU 70 | ~0.4 tok/s (150 tokens took 344 s) |
| 20 %-duty on the coordinator CPU (127) | 19.5 |
| six unpinned ~30 %-duty threads | 22.6–23.2 (kernel kept them off the pool) |
Every slow period in our logs coincided with other services on the box (a cron-like scheduler, an API server at ~80 % CPU with a thread observed on CPU 65, a remote-desktop daemon at 100 % on a node-1 CPU). The kernel normally places foreign threads on idle logical CPUs, so unpinned load is harmless most of the time; once one is parked on a pool CPU nothing pulls it off a busy core while the pool spins, so the slow state persists until some rebalance — which is why affinity calls re-rolled it and why idle periods sometimes recovered it.
Things that did NOT matter: FREETOKEN_CPU_MOE_SPIN=0 (slower in both modes, still bimodal), FREETOKEN_CPU_MOE_FLAG_SYNC=0 (always the slow speed), NUMA layout (--interleave=all, 127 threads per #18: still flips), CPU clocks, temperature, DRAM bandwidth, PCIe link state, page faults, GPU power cap.
Fix that worked (host side)
systemd CPUAffinity=0-63,128-191 on every other service (user and system units, including sshd so login shells inherit it), leaving 64-127 to the pool. Since then: 0 slow requests in ~40 probes across the loads that previously triggered it.
Asks
- Detect and warn. The pool knows its pinned CPUs; a periodic check of
/proc/*/task/*/stat(or/proc/schedstatrun-queue length per CPU) for another runnable task on a pool CPU, logged as a WARN with the pid/comm, would have pointed straight at this. Even a one-time startup check would help. - Per-layer barrier timing under
--moe-collect-stats(#231): median/p99 wait at thedonebarrier per worker, so a straggler core is visible from the stats rather than from Nsight. - Consider making the pool robust to it: e.g.
SCHED_FIFO/SCHED_RRfor pool threads when permitted (CAP_SYS_NICE, or documentulimit -r), or a spin withsched_yield/short backoff so an intruder on a pool CPU gets its 20 % without stalling the barrier; or a documented--moe-cpu-threadssizing note that leaving a few cores free does not help if other processes are not fenced. - Docs: a line in the hybrid-mode guide saying "fence everything else off the pool CPUs (systemd CPUAffinity / cpuset); an unrelated process landing on one of them halves decode" would have saved us a day.
Happy to run anything; the probe scripts (pinned intruder, per-thread sched deltas, Nsight gap analysis) are small and reusable.
- 主要言語
- Python
- スター
- 14.2k
- フォーク
- 1.4k
- 平均マージ
- 21時間 39分
- マージ済み PR(30日)
- 100
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
FlashML-org/FreeToken のほかの issue
-
Desktop feature
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
FlashML-org/FreeToken#800 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
FlashML-org/FreeToken#767 ·
メンテナーはふだん 1 日以内に返信
-
test_gemv_path_matches_the_transcription fails on sm_120 (bf16 rounding tie exceeds the tolerance)対応中かも @void-mckenzie が 16 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 77/100
FlashML-org/FreeToken#649 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
FlashML-org/FreeToken#648 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
FlashML-org/FreeToken#646 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
FlashML-org/FreeToken の issue をすべて見る
似ている issue
-
Support .aac audio uploads対応中かも @julien731 が今日担当しました。 オープンfeature
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
nimblehq/audio-transcriber#153 ·
-
help wanted
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
antonyrag/ragleap-core#1062 ·
-
needs-triage
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
gke-labs/kube-agents#4321 ·
メンテナーはふだん 1 日以内に返信
-
enhancement help wanted
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
Git-Hospital/githealthcheck#61 ·
メンテナーはふだん 1 日以内に返信
-
changelog investigate
難易度 2/5 1〜3時間 初心者へのやさしさ 67/100
ramnes/notion-sdk-py#433 ·