Skip to content

Qwen3.8-Flash-Next: GDN prefill OOM on a long prompt kills the backend (prefill transient not budgeted) #3

Open

Description

@gdevenyi

Environment: RTX 5080 16 GB (sm_120), AMD Ryzen 9 9950X, 123 GB RAM, Ubuntu 24.04 (kernel 7.1.9-xanmod), driver 610.57.04, CUDA 13.3, torch 2.11.0+cu130, triton 3.6.0, FreeToken 0.1.2 editable. Checkpoint: nvidia/Qwen3.8-Flash-Next-NVFP4.

Commit: main e0886cc (before any merges), bf16 KV.

Command:

ft serve --model ~/models/Qwen3.8-Flash-Next-NVFP4 --max-running-requests 1 --cuda-graph-max-bs 1 --moe-cache-size 512 --disable-moe-prefill-overlap --memory-ratio 0.95 [--max-prefill-length 4096]

Startup log: Allocating 97984 tokens for KV cache, K + V = 2.31 GiB, Free GPU memory after capturing CUDA graphs: 0.67 GiB.

What happens: a single ~94.5K-token chat request crashes the scheduler in the first prefill chunk:

File ".../models/qwen4_exp/gdn.py", line 185, in forward
  result = gdn_prefill_chunk_fla(
File ".../models/qwen3_5_moe/gdn_kernels.py", line 33, in gdn_prefill_chunk_fla
torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 32.00 MiB. GPU 0 has a total capacity of 15.51 GiB of which 41.06 MiB is free.
Backend supervisor: backend worker freetoken-TP0-scheduler exited
Backend worker is gone and cannot be restarted; stopping the API server

With the default --max-prefill-length 8192 it fails as above. With 4096 it fails the same way ("Tried to allocate 48.00 MiB ... 63.06 MiB is free").

Measured prefill transient (peak minus idle, nvidia-smi sampled every 200 ms, same flags at --memory-ratio 0.85):

--max-prefill-length peak over idle
8192 +1428 MiB
2048 +392 MiB

Two problems:

  1. The cache budget (MoE + KV at memory_ratio) doesn't reserve the prefill activation transient implied by --max-prefill-length. (1 - memory_ratio) is the only headroom, so a config that boots and captures graphs can still OOM on its first long prompt.
  2. The OOM is fatal: one request takes down the backend permanently, and the API server exits instead of failing that request.

Workaround used: --max-prefill-length 2048 at bf16 (a 94.5K prompt then completes, peak +392 MiB), or nvfp4 KV (FlashML-org#408) with 4096 (peak +964 MiB with 1.09 GiB free).

Related

Activity

  1. gdevenyi commented on Sep 15, 2026

    @gdevenyi
    OwnerAuthor

    No PR for this yet: what a safe fix needs

    I looked at making a prefill OOM fail only the offending request (problem 2) on 5080deploy @ 2d56708 and stopped short of a PR. Catching torch.OutOfMemoryError around Scheduler._forward is the easy part. Releasing the request without corrupting later ones is not:

    1. MoE offload cache (silent corruption). OffloadMoeCache.ensure_experts (python/freetoken/moe/offload_cache.py) runs the device kernel that rewrites the slot map before copy_missing copies the expert weights in. An OOM between the two leaves slots claiming experts whose weights never arrived. Later requests would get wrong outputs with no error. Recovery must call moe_offload_cache.reset(), which cold-starts the cache and costs the next requests their hit rate.
    2. KV / GDN state must be discarded, not committed. _free_req_resources calls cache_manager.cache_req(req, finished=True).
      • For the hybrid cache (this model), _cache_req_hybrid donates the live GDN slot, and any pending ping-pong snapshot, into the radix tree (python/freetoken/scheduler/cache.py). After a mid-forward OOM the live state is advanced for the layers before the failure and not after, so the donated snapshot is garbage that a later prefix hit would COW-restore.
      • A discard path is needed: unlock the old handle, free [old_handle.cached_len, page_ceil(cached_len + extend)) and every GDN slot, insert nothing.
      • It needs a separate implementation for each cache type: radix, hybrid, SWA, DSV4 window tiers.
    3. Scheduler bookkeeping.
      • Remove the request from prefill_manager.pending_list: a chunked request is already re-queued via chunked_list when the batch is scheduled.
      • Release its encoder-cache claims and reply ErrorReplyMsg.
      • Overlap mode still has to drain the previous batch in the same iteration.
    4. TP > 1. Ranks can OOM asymmetrically. A lone rank that skips the forward desyncs the collectives, so this path would have to stay fatal under TP (the same call _execute_pending_rebuild makes for rebuild failures).

    That is a few hundred lines across the scheduler, all cache managers and the MoE cache, with a silent-corruption failure mode if any piece is missed. It needs a design agreed first, not a fix PR.

    Smaller options that would help problem 1 (a config boots, then dies on its first long prompt), not implemented:

    Upstream items for the same failure: FlashML-org#110, FlashML-org#401, FlashML-org#20.

    The current deployment avoids it with FP8 dense + nvfp4 KV at 8192-token chunks (#17): prefill peak 15,124 of 16,303 MiB.

  2. added a commit that references this issue on Sep 18, 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