Skip to content

decode deadlock: PLE disk wait-sync sets its flag only after cuGraphLaunch returns, which can block (hybrid MoE graphs) #65

Open

Description

@gdevenyi

On 5080deploy @ 8975217 (RTX 5080, Ryzen 9 9950X), ft serve with the production flags plus --moe-strategy hybrid starts normally and logs --moe-hybrid-max-fetch auto: fetching 49.0% of each decode step's expert misses over PCIe ... the rest on the CPU. A 32K-token chat request then completes all prefill chunks (last one shows #running-req: 1) and never produces a decode step: no further log lines for 9+ minutes, GPU utilisation 0%, one CPU thread pinned at 100%.

Repro: production launch flags (see #17) + --moe-strategy hybrid; send any chat request.

Why it matters: decode here is bound by expert-cache misses (miss rate ~0.5, ~730 MB of PCIe copies per token at 1280 slots), and the saved ft bench bw profile shows CPU MoE + PCIe overlapped at ~66 GB/s vs ~53 GB/s PCIe alone -- hybrid is the one lever that could beat slot tuning.

Not investigated further yet (found during a serving-parameter sweep).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bu6LgoxLR4wETqb7RPR2vt

Activity

  1. gdevenyi commented on Sep 24, 2026

    @gdevenyi
    OwnerAuthor

    Narrowed down (2026-09-23). The hang is not hybrid mode itself: it needs hybrid + --moe-collect-stats + CUDA-graph decode.

    Config Result
    hybrid, no stats, graphs (bs 1,2) works: 2K/12K/32K contexts, 64-512 tokens, ~42.5 tok/s decode (offload at 1650 slots: ~40)
    hybrid + stats, graphs hangs on the first decode steps (even a 240-token reply, so not the 256-step stats emission)
    hybrid + stats, FREETOKEN_CPU_MOE_FLAG_SYNC=0 (host-func handshake) hangs too -> not the memop flag handshake
    hybrid + stats, --cuda-graph-max-bs 0 (eager) works
    hybrid + stats, graphs, record_decode_stats_hybrid no-op'd hangs
    hybrid + stats, graphs, collect_decode_freq forced off hangs
    hybrid + stats, graphs, both of the above off works
    offload + stats, graphs works

    State at the hang (gdb, native, scheduler process): main thread in cuGraphLaunch (driver sched_yield spin = the 100% core) launching step N+1 while step N's graph is stalled on the GPU (0% utilization); the CPU MoE coordinator is in its idle sleep_for (it never saw a doorbell); all 15 CPU MoE workers idle on their condition variable; the flag watchdog asleep and silent (no pending doorbell). So the captured decode graph stalls on the GPU before the CPU submit node is reached, and only when either stats hook adds ops/allocations into the captured hybrid decode.

    Likely area: interaction of extra captured allocations/ops with the hybrid path in layers/moe.py::_decode_hybrid (raw/cpu_ids/out temporaries, the D2H into pinned IO, ensure_experts_hybrid rewriting topk_ids in place) under graph capture; root-causing further needs nsys / cuda-gdb on the replay. Workaround: don't combine --moe-collect-stats with --moe-strategy hybrid.

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01Bu6LgoxLR4wETqb7RPR2vt

  2. changed the title [-]moe-strategy hybrid hangs at the first decode step (qwen4_exp, NVFP4 experts)[/-] [+]moe-strategy hybrid + --moe-collect-stats hangs the captured decode graph (qwen4_exp)[/+] on Sep 24, 2026
  3. gdevenyi commented on Sep 24, 2026

    @gdevenyi
    OwnerAuthor

    Root cause found (2026-09-23): a deadlock between the PLE disk backend's wait-sync protocol and cuGraphLaunch, not the CPU MoE path.

    Evidence (instrumented reproduction, hybrid + --moe-collect-stats):

    • A watcher thread dumping the CPU-MoE flag handshake during the hang shows all 96 slots at rest (ready=0 done=1 err=0) with served count 1 each -- only the eager warmup before capture. The captured decode graph never reached a single MoE doorbell, i.e. the GPU stalls before layer 0's MoE.
    • gdb: scheduler main thread spinning inside cuGraphLaunch (driver sched_yield), GPU at 0%.
    • DiskRowTable (models/qwen4_exp/ple_disk.py, wait-sync mode) records a stream-memop WAIT on _flag inside the decode graph; the host sets that flag in forward_host_ctx's exit path, i.e. only after the graph launch returns.
    • cuGraphLaunch is allowed to block when the launch doesn't fit the channel's command queue. Hybrid adds per-layer doorbell writes/waits and D2H/H2D copies (x48 layers); stats adds more ops. Once the graph is big enough, the launch blocks waiting for the GPU to drain, the GPU is parked on the PLE flag, and the flag is only set after the launch returns -> deadlock. Deterministic with stats, intermittent without (near the threshold; two overlapped steps in flight tip it).

    One-variable confirmation: FREETOKEN_PLE_SYNC=gate (fill the rows before the launch; the graph never waits on the host) with the otherwise-identical reproduction: 3/3 x 300-token requests complete (previously hung within seconds, every time).

    Fix direction: the fill-and-signal must not depend on the launch returning -- run the deferred fill on a dedicated worker thread (FIFO, so fills stay in step order) started before the launch. The same latent deadlock exists in offload mode; its graph is just smaller (never observed there).

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01Bu6LgoxLR4wETqb7RPR2vt

  4. changed the title [-]moe-strategy hybrid + --moe-collect-stats hangs the captured decode graph (qwen4_exp)[/-] [+]decode deadlock: PLE disk wait-sync sets its flag only after cuGraphLaunch returns, which can block (hybrid MoE graphs)[/+] on Sep 24, 2026
  5. gdevenyi commented on Sep 24, 2026

    @gdevenyi
    OwnerAuthor

    Directly observed (boundary logging in the scheduler, 2026-09-23 23:06): prefill (eager) completed -> decode step 1: PLE rows staged with the fill deferred -> cuGraphLaunch ENTERED and never returned (main thread still in torch.cuda.graphs.replay) -> the deferred PLE fill never started -> PLE _flag = 0 -> all 96 CPU-MoE doorbell slots at rest. The deadlock closes on the very first decode step. (cuda-gdb attached after the fact reports no CUDA contexts -- the process was not started with debugger support -- so device-side state was not inspected; the host-side boundary evidence is complete.)

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01Bu6LgoxLR4wETqb7RPR2vt

  6. gdevenyi commented on Sep 24, 2026

    @gdevenyi
    OwnerAuthor

    Fixed by #67 (6ebfa18; merged into 5080deploy as a898557). The PLE graph fill now runs on a dedicated filler thread and is submitted before cuGraphLaunch, so a blocking launch no longer deadlocks against the captured WAIT.

    Validation on the 5080: the deterministic repro (hybrid + --moe-collect-stats) that hung on the first request now completes 3/3. Hybrid without stats also passes quality (14/15), needle 32K/128K, and a 255K context. The regression test (tests/models/qwen4_exp/test_ple_disk_fill_order.py) fails on the old code and passes with the fix.

    Hybrid itself gives no decode gain over offload here (~38.2 vs ~38.8 tok/s in a back-to-back A/B), so production stays on offload.

  7. added a commit that references this issue on Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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