mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #2568] [RFC]: Traffic class hint API for TransferEngine — bridging upper-layer semantics (KV / EP / CTRL) to lower-layer SL/TC #674
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#674
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 @catyans on GitHub (Jun 23, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/2568
Summary
Extend the
SelectionContextintroduced in #2079 with atraffic_classfield, allowing callers to express application semantics (KV-cache transfer vs. EP all-to-all vs. control-plane RPC) so the engine can map them to the link-layer resources (SL / TC / QP pool) already exposed by #1187, #2525, and #2526.This RFC does not introduce a new abstraction — it adds one orthogonal dimension to an existing struct.
Motivation
Three independent pieces of work over the last six weeks have converged on "different traffic types should use different NIC resources", but the convergence is happening only at the link layer:
NCCL_GIN_IB_TC(separate TC for GIN control vs. data)MC_IB_TC(global RDMA traffic class)MC_IB_SL(global RDMA service level)All four expose link-layer knobs with global scope — one value per process, applied to every transfer regardless of its purpose. There is currently no way for a caller to say:
The link layer is therefore forced into one of three suboptimal modes:
TransferEngineinstances to differentiate flows, wasting resources.The orthogonal SGLang RFC sgl-project/sglang#28631 plumbs priority (
CRITICAL/BACKGROUND/IDLE) through the Python data path. As that RFC explicitly notes, priority decides when a transfer is sent (queue order); it does not decide which NIC channel it uses. The traffic-class dimension proposed here covers the latter and is independently meaningful.Proposed change
Extend
SelectionContext(mooncake-transfer-engine/tent/include/tent/runtime/transport_selector.h, introduced in #2079) with one optional field:SelectionPolicygains an optionaltraffic_class_filter(mirroring howpriorityis already filtered today), andTransportSelectorconsults it during rule matching. No new top-level concepts — the dispatch path, JSON config schema, and rule-evaluation logic all stay as #2079 left them.A default mapping table (overridable via
MC_TRAFFIC_CLASS_SL_MAPenv var, mirroringNCCL_GIN_IB_TC):DEFAULTKV_PUTKV_GETKV_LOOKUP_METAEP_DISPATCHEP_COMBINECTRLAll SL/TC values are deployment-specific (depend on switch VL-to-SL mapping and PFC configuration) and intentionally left as TBD here. The table goes through
SelectionPolicyJSON like any other rule, so operators override per fabric. The only hard semantic isDEFAULT = 0/0(preserves current behavior).Five questions the RFC must answer
Q1. Enum boundary — first-class vs. user-defined?
First-class: the seven values above (covering current Mooncake / vLLM / SGLang / DeepEP traffic). Extension:
USER_DEFINED_BASE = 128reserves the upper half ofuint8_tfor downstream projects (e.g., training frameworks addingGRAD_ALLREDUCE) without forcing them through this RFC.Q2. Who injects the hint?
Both — caller-explicit and transport-inferred default. Existing call sites continue to work (default =
DEFAULT= current behavior). New call sites in vLLMMooncakeStoreConnector, SGLangdisaggregation/mooncake/conn.py, and DeepEP V2 elastic buffer (#2503) opt in by passing the hint atsubmitTransfer. Transport may infer a default when caller leaves itDEFAULT(e.g., notification-QP path defaults toCTRL).Q3. How is
TrafficClassmapped to(SL, TC, QP_pool)?Built-in default table (above) + per-deployment override via JSON
SelectionPolicy(already supported by #2079) + env varMC_TRAFFIC_CLASS_SL_MAPfor quick experiments (mirroringNCCL_GIN_IB_TCergonomics). No new mechanism.Q4. Relationship with #2525 / #2526 dual-QP?
#2526 makes the notification QP's SL/TC configurable (previously hard-coded to 0). Once #2526 lands, a natural follow-up question is: who decides which transfers go through the notification QP vs. the data QP? Today that decision is implicit (only handshake/notify messages use the notification QP). This RFC makes it explicit:
TrafficClassbecomes the decision input. For example, a deployment could configureCTRLto route through the notification QP — but the default mapping keeps all traffic on the data QP to preserve current behavior. The RFC does not mandate notification-QP routing; it only provides the signal that enables it as a policy choice.Q5. Telemetry?
Per-NIC × per-class counters:
mooncake_te_traffic_class_in_flight_bytes{nic, class},_completed_count,_error_count. Reuses existing Mooncake metrics naming convention; existing dashboards consume the new dimension by adding one label.Anticipated concerns
"Why not let callers pass SL/TC directly?"
SL/TC are link-layer resources whose allocation is a fabric / operator concern. Applications should express intent (KV vs. EP vs. CTRL); the transport layer + operator config decide the implementation. This is the same split as DiffServ DSCP at the IP layer (apps tag class, routers map to queues) and
NCCL_GIN_IB_TC(NCCL maps GIN to its own TC; apps don't configure TC bits directly)."Why not reuse
priority_level?"priority_level(in #2079 / #2048) is a scheduling-layer concept — it determines dequeue order within a single queue.TrafficClassis a link-layer concept — it determines which queue (SL/VL → NIC TX queue → DSCP → switch egress) the transfer takes. The two are orthogonal (a(priority=HIGH, class=KV_PUT)and a(priority=LOW, class=KV_PUT)should share the same SL/TC but dequeue in different order). This is the same orthogonality articulated in @catyans/@alogfans's discussion on #2489 (CC vs. QoS).Scope (and explicit non-goals)
In scope:
SelectionContext+SelectionPolicywithtraffic_class.CTRL/KV_LOOKUP_META.DEFAULTpreserves current behavior bit-for-bit.Out of scope (explicit, to keep the RFC tractable):
ai-dynamo/nixlonce at least two backends adopt this shape).Open questions
USER_DEFINED_BASEbe128or higher, leaving more room for future first-class values?Configor be hard-coded with env-var override only?KV_LOOKUP_METAdefault to notification QP, or stay on data QP for symmetry withKV_PUT?mooncake_te_traffic_class_*vs. a more concisemc_te_tc_*?References
MC_IB_TCenv varTransportSelector/SelectionContext(this RFC extends)MC_IB_SLenv varNCCL_GIN_IB_TCprecedentAcknowledgements
Cc: @alogfans @stmatengss @staryxchen @lvshufan — most active on the QoS / TENT / RDMA-config line (#2048, #2079, #2489, #2293).
@github-actions[bot] commented on GitHub (Jun 23, 2026):
Thanks for opening this issue, @catyans!
18214026A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide
@alogfans commented on GitHub (Jun 24, 2026):
An important issue is that, if we expect to configure different traffic_classes for different tasks within the same transfer engine, we need to allocate multiple QPs, each assign different parameters during their handshakes. The control path may be changed significantly.
@staryxchen commented on GitHub (Jun 24, 2026):
Thanks for your proposal! But I have some concerns about extending
SelectionContextwith aTrafficClassenum.1. Layering inversion. Every existing field in
SelectionContextis an engine-observable fact (segment type, memory type, size, priority).KV_PUT/EP_DISPATCH/CTRLare application semantics — adding them asksTransferEngineto own a vocabulary of upper-layer scenarios and their mapping to link-layer resources. The DSCP analogy in the RFC actually argues against this: DSCP is opaque bits, not an enum routers interpret.2. Closed enum is a release bottleneck. Every new traffic shape (training gradient sync, parameter prefetch, …) comes back through this RFC. The
USER_DEFINED_BASE = 128escape hatch implicitly admits this — if extension is expected, the closed-enum shape was wrong.3. @alogfans's point is the load-bearing one. SL/TC are baked into QP attributes at handshake; real per-class differentiation needs per-class QP pools. A
traffic_classfield is just a label — it doesn't move that complexity. With every class defaulting to "data QP", the field lands as a no-op.Alternative — use what's already there.
SelectionContextalready haspolicy_name(transport_selector.h:83), andmatchesPolicyalready short-circuits on an exact match (transport_selector.cpp:251). That's the correct hook: the caller passes an opaque channel name, the engine never interprets it.SelectionContext: no change. Callers setpolicy_name = "kv-critical"/"ctrl".SelectionPolicy: extend with link-layer attributes:Zero churn in
SelectionContext, no business vocabulary in the engine, consistent with #2079, andqp_poolbecomes an explicit field instead of an enum-implied assumption. Telemetry labels bypolicy_name.@catyans commented on GitHub (Jun 25, 2026):
Thank you @staryxchen — this is a much better shape than what I proposed, and all three points are correct. Let me address each and adjust the RFC accordingly.
On (1) layering inversion — you're right, and the DSCP analogy does cut against me.
KV_PUT/EP_DISPATCH/CTRLare application vocabulary; the engine shouldn't own them. An opaque identifier the engine never interprets is the correct hook.On (2) closed enum — agreed.
USER_DEFINED_BASE = 128was a tell that the closed-enum shape was wrong. A config-driven approach where a new traffic shape is just a new JSON entry (no code change, no RFC round-trip) is strictly better.On (3) — the load-bearing point — this is the one that matters most, and I think it's right: SL/TC are baked into QP attributes at handshake, so a label alone is a no-op. Real per-class differentiation needs per-class QP pools, and that's the actual complexity.
qp_poolas an explicit field surfaces it instead of hiding it behind an enum-implied "data QP" default.Revised direction — adopting your proposal. I'll drop the
TrafficClassenum and theSelectionContextchange entirely, and instead:SelectionContext: no change. Callers setpolicy_name(e.g."kv-critical"/"ctrl"), whichmatchesPolicyalready short-circuits on (transport_selector.cpp:251).SelectionPolicy: extend with link-layer attributes —service_level,traffic_class,qp_pool— parsed from the same JSON as the existing fields (name/segment_type/devices/transports, transport_selector.h:90), and carried out viaSelectionResult.I'd suggest splitting this into two independent units so the no-op concern is addressed head-on rather than deferred:
Policy-level SL/TC (small, mergeable now). Add
service_level/traffic_classtoSelectionPolicy+SelectionResult, and apply the resolved SL/TC at QP setup. This makes the link-layer knobs from #2525/#2526 per-policy configurable instead of process-global — useful on its own, but I'll be explicit that without per-class QP pools, policies sharing a QP still collapse to one SL.Per-class QP pools (the real work). A follow-up RFC for pool creation/lifecycle/routing keyed by
qp_pool, so different policies actually land on QPs with different SL/TC. This is where the genuine differentiation lives, and I'd rather design it explicitly than imply it.Does this two-step split work for you? If so I'll rewrite the RFC around
policy_name+SelectionPolicyand open the step-1 PR first. Thanks again for redirecting this — the opaque-name + explicit-qp_pool framing is clearly the right one.@staryxchen commented on GitHub (Jun 25, 2026):
@catyans The two-step split is good for me. Only one reminder: When naming fields in the JSON schema, make sure to reserve space for Step 2 (either the “qp_pool” placeholder field or a versioned schema); otherwise, you’ll have to modify the schema again when Step 2 rolls around to ensure compatibility with older configurations.
@catyans commented on GitHub (Jun 26, 2026):
Thanks @staryxchen — good call on schema forward-compat. I'll reserve
qp_poolin theSelectionPolicyschema from step 1 (parsed and stored, defaulting to the current "data QP" behavior when unset), so step 2 only adds pool creation/routing semantics without a schema change or breaking existing configs. Will open the step-1 PR (service_level/traffic_class/qp_poolonSelectionPolicy+SelectionResult, applied at QP setup) along these lines.@github-actions[bot] commented on GitHub (Sep 25, 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 2, 2026):
Closing due to 3 months of inactivity. If this is still relevant, please comment and we can reopen.