mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #2132] [RFC]: TENT Local Transfer Admission Queue #587
Labels
No labels
ascend
auto-closed
bug
bug
CI/Build
Common
documentation
enhancement
feature request
good first issue
help wanted
high priority
MetaX
nightly-failure
pull-request
PyTorch Backend
question
RFC
Roadmap
stale
Store
Tests
Transfer Engine
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/Mooncake#587
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @zbchi on GitHub (May 18, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/2132
Originally assigned to: @alogfans on GitHub.
Changes proposed
Summary
This RFC proposes a local transfer admission queue inside
TransferEngineImpl. The queue orders logical transfers and applies backpressure before work is handed toTransport::submitTransferTasks()orProxyManager::submit().This covers the local-runtime part of the Transfer Queue roadmap item in #1883, before adding distributed pressure signals or receiver credits.
It does not replace transport or ProxyManager execution. RDMA slice scheduling, CQ/QP handling, staging state machines, retries, and KVCache placement remain owned by their existing layers.
Phase 1 keeps the normal submit path direct and adds a private queue model plus compatibility tests. Distributed pressure and receiver credits can be added later as inputs to local admission; this RFC does not propose a cluster-wide per-transfer executor.
Motivation
The current path is optimized for direct submission:
This is fast when the process is not overloaded. Under heavy load, background prefetch or migration traffic and foreground Store
gettraffic can enter transport queues through the same direct path. By the time foreground work is visible to the runtime, transport-side queues may already contain background work.Transport execution and ProxyManager staging remain owned by their existing layers. This RFC adds an earlier runtime admission point in
TransferEngineImpl::submitTransfer(), so accepted work can be capped and latency-sensitive requests can be ordered before handoff.Current Code Shape
The queue would be inserted into the current
submitTransfer()path:In the direct path, public task state is published before any queue admission point. The queued path must therefore:
UNSPEC,TaskInfo::xport_priority, or transportsub_task_id.Request::source.BatchIDis effectively aBatch*, andSegmentTracker::ref_countis not a transfer pin.ProxyManager::submit()storesTaskInfo*.Architecture
The queue sits between caller submit and transport/proxy execution. L2 signals are inputs to the queue, not executors:
In Phase 1, the normal submit path stays direct; the new code only exercises the queue model, side-table state, and owner mapping to ensure direct-path compatibility.
Runtime Queue Design
Data Model
A queue item represents one logical transfer owner. It is not an RDMA slice and does not copy payload data. The first implementation can keep this as a private model, but the model should already reflect production boundaries such as batch ownership, derived-task mapping, byte charge, priority class, route plan, queue state, and reserved internal work.
The logical owner is the single queued task for a merged request. Derived public task ids resolve to that owner and are not independently queued, dispatched, retried, or charged.
An implementation can start with a private shape like this:
The key point is to keep queue state in this private model instead of encoding it in
TaskInfofields.Admission
For the production queued path, queue admission for one
submitTransfer(batch_id, request_list)call is all-or-nothing. If every logical owner derived from the call can be admitted, it returnsStatus::OK. If admission would exceed hard limits, it returnsStatus::TooManyRequestsand leaves no partial submission behind. Transport execution can still fail later and report failure through status, matching the existing asynchronous contract.When the production queued path is enabled,
Status::OKmeans the work was published to the runtime queue, not necessarily that a transport has already accepted it. Applications may observePENDINGfor tasks that have not reachedTransport::submitTransferTasks()orProxyManager::submit()yet.This is the target production queued commit sequence:
This avoids rolling back partially visible task ids and prevents submit hooks from covering rejected work.
Admission also rejects a submit call that would exceed the caller-provided batch request capacity or a planned transport sub-batch capacity. That rule belongs to the queued path, not to the currently disabled direct-submit path. Request merging can reduce the number of transport owners, but it cannot allow more public task ids than the batch was allocated to hold.
Queued work remains public
PENDING. Internally, queued state is explicit: do not encode it asUNSPEC, a fakesub_task_id, or a missingSubBatch, because current status polling treatsUNSPECas retry/fallback.Transport resolution, notification hooks, queue entries, owner mapping, and future local-buffer pins use the same publish-all-or-publish-none rule. If no route is available, the queued path either rejects before publishing or enters bounded
RoutePending.State Model
The queue adds states before a task reaches transport ownership. For one logical owner, the intended transition shape is:
Public status mapping stays compatible: non-terminal queue states map to
PENDING; terminal states map to the existing terminal status values.Queue state lives in runtime queue metadata, not in
TaskInfo.status.TaskInfo.statusremains the public-facing task result seen by status polling and metrics.QueuedPENDINGRoutePendingPENDINGDispatchingPENDINGSubmittedPENDINGTransportorProxyManagerCompletedCOMPLETEDFailedFAILEDCanceledRoutePendingis only for bounded route refresh or policy retry, such as temporarily stale metadata. It is not a generic overflow state. Once retry budget or deadline is exhausted, the owner task becomes terminal failure.Canceledis internal shutdown or future cancellation state; this RFC does not add public cancellation semantics.Status lookup and notification hooks resolve derived task ids through their owner. Detailed hook firing and retirement semantics belong to the later lifecycle-correct dispatcher work.
Dispatch Boundary
The production queue will sit at the transport/proxy dispatch boundary:
The dispatcher groups ready items by transport to preserve batching, while honoring
batch->max_sizeand per-transport sub-batch capacity. In the first patch, these rules can be validated with a synchronous model or mock dispatcher.The risky cases are:
getTransferStatus()expects a realsub_task_idfor non-staging tasksPENDINGfrom queue metadatasub_task_idbeforeTransport::submitTransferTasks(), and the transport API returns onlyStatusSubBatchcapacityTooManyRequestswhenrequest_list.size() + task_list.size() > max_size; GDS has a larger backend charge modelThe first patch can model request merge, initial transport resolution, staging policy lookup, and transport charge as inputs. The normal
submitTransfer()path remains direct-submit. In a later production queued path, final transport selection can move into the dispatcher when it can use queue depth, transport pressure, and receiver credits.Scheduling
The scheduler policy can be weighted by priority class and fair by bytes. Weighted deficit round robin with aging is a practical default: foreground traffic gets lower queueing latency, large transfers pay by byte charge, and old low-priority work still makes progress.
Because the current public API has no priority field, the first model can still validate multi-class scheduling. When a real queued submit path is enabled, caller traffic can initially use the default class until a later API or internal policy assigns foreground, background, prefetch, or staging-progress classes.
The runtime does not split large requests in the first slices. RDMA already slices after dispatch. Runtime-level chunking can be added later if benchmarks show priority inversion from very large requests.
The scheduler model selects entries whose byte charge fits the current class deficit and configured dispatch window. Production dispatch will also apply aggregate transport pressure and receiver credits when those signals exist. Merged requests are charged once, at the owner task; derived tasks observe the owner result and do not receive independent dispatch quota.
The first dispatcher can be only a model: enqueue owners, pick entries that fit the current task and byte window, and mark them as dispatching.
Backpressure
Backpressure separates admission from dispatch. Admission limits decide whether a submit call can be accepted; dispatch limits decide whether accepted work may enter transport or ProxyManager ownership.
Admission can cap queued tasks, queued bytes, total outstanding work, and reserved internal credits. When a hard admission limit would be exceeded,
submitTransfer()fails fast withStatus::TooManyRequests. If a dispatch limit is saturated while queue capacity remains, the entry stays queued and publicPENDING; this is different from rejecting a new submit.Soft pressure comes from transports and affects dispatch rate, not admission correctness. No transport pressure API exists today, so the first reviewable PR does not depend on one. A later milestone can add aggregate signals such as saturated state, recommended dispatch bytes, backlog, retry deltas, and timeout deltas.
Runtime dispatch windows should use aggregate transport pressure only. Detailed RDMA state remains transport-owned and can later feed advisory pressure without being mutated by the queue.
The dispatcher also needs bounded dispatch windows even before rich transport pressure exists. If it drains accepted work into RDMA as fast as possible, priority only moves from the runtime queue into RDMA worker queues, and later latency-sensitive work is again ordered by transport-side queues.
Today,
TooManyRequestscovers several distinct conditions. The queued path therefore classifies it by source:SubBatchcapacityUntil transports expose typed pressure separately from capacity, a raw
Status::TooManyRequestsfromTransport::submitTransferTasks()is better treated as a capacity or implementation failure at the runtime boundary, not as proof that the entry can be safely requeued.Staging
Staging stays owned by
ProxyManager. For a staged request, the outer user-visible request should not be the unit being shaped. It is mostly the task whose status is reported back to the user asPENDING,COMPLETED, orFAILED.The actual traffic comes from the smaller transfers that
ProxyManagercreates while staging the data.Queueing and backpressure should apply to the internal
submitTransfer()work created byProxyManager, not the outer staged request passed toProxyManager::submit(). If a later queued submit path adds priority or deadline policy, these internal submits can inherit it from the outer staged request. Remote delegate work remains owned byProxyManagerand the remote side. The outer staged request becomes complete only after the required internal work completes, or fails if one of those required internal transfers fails.Today
ProxyManager::submit()pushes into an internal shardstd::queue<StagingTask>and returnsOK. Stage-buffer allocation, the local/cross/remote chunk pipeline, and the remote delegate path stay insideProxyManager.The queued path avoids introducing new long-lived raw
TaskInfo*ownership. Production staged work should be addressable by stable batch ownership plus task id, or the batch storage needs to be made stable before ProxyManager receives the task.Stage-buffer pressure appears after
ProxyManager::submit(), inside worker-side pinning.pinStageBuffer()can report no free chunk asStatus::TooManyRequests, while some staging cache failures are currently fatal. Production staged queue dispatch should turn these cases into bounded retry or explicit task failure, not process termination in a worker thread.Proxy-generated sub-transfers allocate internal batches and call back into
TransferEngineImpltoday:If these internal
submitTransfer()calls use the same queue as ordinary user submits, already-accepted staged work can deadlock:Those internal calls therefore need one of two policies:
Post-accept ProxyManager stage-buffer pressure still needs a ProxyManager-visible retry/fail result or internal staging pressure signal.
ProxyManager shutdown is also part of the design. Tasks already accepted into shard queues need to be drained to terminal status or explicitly failed before workers exit; accepted-but-unstarted staging work should not disappear silently during runtime teardown.
The outer staged task remains complete only when
ProxyManagerreports the staging status as terminal.API, Config, And Metrics
SubmitOptionsfor priority, deadline, or maximum queue waitallocateBatch(batch_size), task id ranges, notification hooks, polling, and StoreTransferFuturecleanup unchangedImplementation Plan
Phase 1: Local Queue Foundation
Phase 1 proves local queue semantics while the queued submit path remains disabled. It does not export public headers, start a
TransferEngineImplbackground dispatcher, submit to real transports, or require Store and ProxyManager to compile against queue-specific types.xport_priorityModel-boundary pseudocode:
The exact names can change. The important contract is all-or-nothing admission, window-bounded dispatch, and public task ids resolving through one logical owner.
Phase 1 validates admission, scheduling, route-pending behavior, owner mapping, transport charge modeling, and direct-path compatibility while queued submit remains disabled.
The side table stays separate from
TaskInfo: queued owners map to publicPENDING, and derived task ids resolve through the owner map without copying transport fields into anotherTaskInfo. Any direct-path derived status cleanup should be a separate behavior-preserving refactor.Phase 1 should stop before production async dispatch. Public API changes, transport pressure signals, ProxyManager ownership changes, and distributed credits can be discussed once the local model and compatibility tests are in place.
Phase 2: Lifecycle-Correct Local Dispatch
Add explicit batch references or engine-wide batch ownership.
freeBatch()becomes deferred cleanup. User polling and the completion tracker share the same transition helper.It also adds transport handoff safety for partial sub-batch mutation, local-buffer pin enforcement, hook retirement, stale-completion rejection, and shutdown/drain ordering. One path owns terminal transition, credit release, and derived-task updates.
Phase 3: Pressure And Credits
Add aggregate pressure signals from transports and use them to adjust dispatch windows. Then add host-local pressure sharing and receiver credits. RDMA should expose aggregate pressure only; route selection and CQ/QP details remain inside RDMA. Distributed state influences dispatch windows and admission decisions; it does not become a per-transfer execution service.
Rollback is config-only: disable the transfer queue and use the current direct submit path. Performance gates should compare queue wait latency and transport execution latency separately; RDMA throughput regressions should be debugged without changing RDMA slicing, spraying, or CQ polling.
Before submitting a new issue...
@alogfans commented on GitHub (May 19, 2026):
We appreciate your proposal and we will work together to support this feature, which is potentially useful for more dynamic workloads. Some questions:
@zbchi commented on GitHub (May 27, 2026):
Hi @alogfans . I revised the RFC direction based on the two points above.
For staging, I would not queue the outer request passed to
ProxyManager::submit().The queue should shape the internal transfers created by
ProxyManager.submitLocalStage()andsubmitCrossStage()allocate internal batches and call back intoTransferEngineImpl::submitTransfer(), so those internal submits are the units I would apply admission/backpressure to. Remote delegate work stays owned byProxyManagerand the remote side.For inter-transport retry, I would keep the same public task across attempts. Today
resubmitTransferTask()already retries on the sameTaskInfoby re-running transport selection and assigning a new transport/sub-task id. In the queued path, the failed attempt should release its queue/dispatch resources before the same task is dispatched again; an attempt id can prevent late results from an old attempt from overwriting the newer status.If this direction looks reasonable,would it be useful for me to start with a small PR for the local queue model, owner mapping, and compatibility tests first, before touching the real submit path?
@alogfans commented on GitHub (May 28, 2026):
I agree with your proposal and it's okay to build a initiate prototype based on current codebase. BTW, we expect the remote delegate work more seperate to the non-delegation logic.
@catyans commented on GitHub (Jun 18, 2026):
Following this thread closely since it's the foundation for deadline/SLO-aware scheduling. I filed RFC #2519 (deadline-aware admission & adaptive degradation) which is designed to build directly on top of this admission queue rather than alongside it:
QueueOwnermodel and the dispatch boundary introduced here — no new queue, no change to the WDRR/priority machinery.deadline_nson the request (0 = no deadline = exactly today's FIFO behavior), and an opt-in feasibility check insidepickForDispatch. When no deadline is supplied, nothing changes.Since
pickForDispatchis currently capacity-only FIFO, the deadline policy would be a natural extension once this lands and gets wired into the real submit path. I'm happy to fold the #2519 discussion into this thread if you'd prefer to keep the admission-policy design in one place — whatever's easiest for review. cc @alogfans @zbchi@zbchi commented on GitHub (Jun 18, 2026):
Hi @catyans thanks for the link, I agree it would be easier to fold the scheduler policy discussions into this thread.I’ll try to keep the admission/dispatch side generic enough for priority, deadline, and pressure signals without creating separate scheduling paths.
@catyans commented on GitHub (Jul 10, 2026):
Phase 3 receiver-credit follow-up is now available for review: https://github.com/kvcache-ai/Mooncake/issues/2849. It specifies receiver-owned multi-resource grants, epoch/restart safety, dispatch gating, rollout compatibility, and the cluster validation gaps that must be closed before implementation.
@github-actions[bot] commented on GitHub (Oct 9, 2026):
This issue has had no activity for 90 days and will be closed in 7 days if there is no further activity. Please comment or react if it should stay open.
@github-actions[bot] commented on GitHub (Oct 17, 2026):
Closing due to 3 months of inactivity. If this is still relevant, please comment and we can reopen.