Skip to content

P/D handoff eagerly allocates KV state on the decode worker and can abort on insufficient NPU memory #47

Description

@Veilwalker

Summary

In P/D disaggregation mode, in the non-prefix-caching handoff path, when a request finishes prefill, it is transferred to a decode scheduler through:

Router.transfer_prefill_request() -> Scheduler.add_decode()

The decode side then immediately allocates the request's prefilled KV state on the selected decode worker's NPU memory. If the selected decode worker does not have enough free NPU
memory, MemoryModel.allocate(..., Device.NPU) raises a RuntimeError, causing the simulator to abort.

This seems to happen before the handoff path verifies whether the selected decode worker can actually admit the request.

Observed behavior

The relevant path appears to be:

  • serving/__main__.py

    • finished prefill requests are passed to router.transfer_prefill_request(finished_reqs)
  • serving/core/router.py

    • transfer_prefill_request() selects a decode scheduler and calls sched.add_decode(req)
  • serving/core/scheduler.py

    • add_decode() appends the request to the decode queue
    • then allocates the request's full KV on the decode worker
  • serving/core/memory_model.py

    • allocate(..., Device.NPU) raises if the NPU memory is insufficient

In the non-prefix-caching path, the code appears to be:

def add_decode(self, req):
    req.instance_id = self.instance_id
    self.request.append(req)
    ...
    kv_size = self.memory.get_total_kv(req)
    self.memory.allocate(kv_size, Device.NPU)

MemoryModel.allocate() then raises when the requested size exceeds available NPU memory:

if self.npu_used + size > self.npu_mem:
    raise RuntimeError(...)

As a result, the current behavior is:

  1. a request finishes prefill
  2. a decode worker is selected
  3. the full prefilled KV is allocated immediately on that decode worker
  4. if the selected decode worker does not have enough free NPU memory, the simulator aborts

Reproduction conditions

This behavior should be reproducible under a P/D configuration where:

  • the prefill completion rate exceeds the decode side's ability to admit or drain requests
  • decode workers are configured with relatively limited available NPU memory
  • the workload includes sufficiently long prompts, resulting in large prefilled KV states during handoff

Under these conditions, once a prefilled request is transferred to a decode worker without enough free NPU memory, the simulator aborts in MemoryModel.allocate().

Question

I would like to clarify whether this behavior is expected in the current simulator design.

Specifically, should decode-side memory exhaustion during P/D handoff be treated as a fatal simulation error, or should it be considered a missing admission-control / backpressure mechanism in the P/D handoff path?

I am asking because decode-side memory pressure seems like a natural condition in memory-constrained P/D disaggregation experiments. If the simulator terminates immediately in this case, it becomes difficult to study handoff queuing, decode-side congestion, or memory-aware decode scheduling.

If this is a known limitation rather than intended behavior, would future support for a recoverable P/D handoff path be within the project scope, such as decode-side admission checks, retrying another decode worker, or delaying the handoff until decode memory becomes available?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions