kvcache-ai / kvcache-ai/Mooncake
[RFC]: Traffic class hint API for TransferEngine — bridging upper-layer semantics (KV / EP / CTRL) to lower-layer SL/TC
- Dominant language
- C++
- Stars
- 6.6k
- Forks
- 1.2k
- Avg merge
- 3d 5h
- Merged PRs (30d)
- 312
Description
## Summary
Extend the `SelectionContext` introduced in #2079 with a `traffic_class` field, 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*:
| Project | Knob | Layer |
|---|---|---|
| NCCL v2.30.7 | `NCCL_GIN_IB_TC` (separate TC for GIN control vs. data) | env var |
| Mooncake #1187 | `MC_IB_TC` (global RDMA traffic class) | env var |
| Mooncake #2525 | `MC_IB_SL` (global RDMA service level) | env var |
| Mooncake #2526 | SL/TC applied to the notification QP | code |
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:
> "This `submitTransfer` carries a KV write on the TTFT critical path; that one is an EP dispatch background flow; the third is a heartbeat."
The link layer is therefore forced into one of three suboptimal modes:
1. Static one-size-fits-all (today): all flows share the same SL/TC, contention is unmanaged.
2. Per-process pinning via env vars: requires running multiple `TransferEngine` instances to differentiate flows, wasting resources.
3. Caller-provided raw SL/TC: leaks link-layer details into vLLM / SGLang / DeepEP, violating layering.
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:
```cpp
enum class TrafficClass : uint8_t {
DEFAULT = 0, // unspecified — preserve current behavior
KV_PUT = 1, // P→D KV write, latency-critical
KV_GET = 2, // D pull from prefix cache
KV_LOOKUP_META = 3, // small control RPC for KV metadata
EP_DISPATCH = 4, // MoE expert-parallel forward all-to-all
EP_COMBINE = 5, // MoE expert-parallel backward all-to-all
CTRL = 6, // heartbeat, handshake, restart detection
USER_DEFINED_BASE = 128 // reserved for downstream extension
};
struct SelectionContext {
SegmentType segment_type;
bool same_machine;
MemoryType local_memory_type;
MemoryType remote_memory_type;
const std::vector* buffer_transports;
size_t transfer_size;
int priority_level;
std::optional policy_name;
// NEW: caller's semantic intent for this transfer
TrafficClass traffic_class = TrafficClass::DEFAULT;
};
```
`SelectionPolicy` gains an optional `traffic_class_filter` (mirroring how `priority` is already filtered today), and `TransportSelector` consults 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_MAP` env var, mirroring `NCCL_GIN_IB_TC`):
| TrafficClass | Default SL | Default TC | Default QP pool |
|------------------|------------|------------|-------------------------|
| `DEFAULT` | 0 | 0 | data QP (current) |
| `KV_PUT` | TBD | TBD | data QP |
| `KV_GET` | TBD | TBD | data QP |
| `KV_LOOKUP_META` | TBD | TBD | data QP |
| `EP_DISPATCH` | TBD | TBD | data QP |
| `EP_COMBINE` | TBD | TBD | data QP |
| `CTRL` | TBD | TBD | data QP |
All 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 `SelectionPolicy` JSON like any other rule, so operators override per fabric. The only hard semantic is `DEFAULT = 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 = 128` reserves the upper half of `uint8_t` for downstream projects (e.g., training frameworks adding `GRAD_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 vLLM `MooncakeStoreConnector`, SGLang `disaggregation/mooncake/conn.py`, and DeepEP V2 elastic buffer (#2503) opt in by passing the hint at `submitTransfer`. Transport may infer a default when caller leaves it `DEFAULT` (e.g., notification-QP path defaults to `CTRL`).
**Q3. How is `TrafficClass` mapped to `(SL, TC, QP_pool)`?**
Built-in default table (above) + per-deployment override via JSON `SelectionPolicy` (already supported by #2079) + env var `MC_TRAFFIC_CLASS_SL_MAP` for quick experiments (mirroring `NCCL_GIN_IB_TC` ergonomics). 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: `TrafficClass` becomes the decision input. For example, a deployment *could* configure `CTRL` to 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. `TrafficClass` is 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:
- Extend `SelectionContext` + `SelectionPolicy` with `traffic_class`.
- Default mapping table + env-var override.
- Notification-QP routing via `CTRL` / `KV_LOOKUP_META`.
- Per-class telemetry counters.
- Backward compatibility: `DEFAULT` preserves current behavior bit-for-bit.
Out of scope (explicit, to keep the RFC tractable):
- vLLM / SGLang / DeepEP integration (separate follow-up PRs once this lands).
- Cross-process traffic-class quota (would extend the shared-memory plan from #2048).
- NIXL pass-through (companion RFC in `ai-dynamo/nixl` once at least two backends adopt this shape).
- Notification-QP SL/TC configuration — handled by #2526; this RFC only provides the classification signal, not the QP-level plumbing.
## Open questions
1. Should `USER_DEFINED_BASE` be `128` or higher, leaving more room for future first-class values?
2. Should the default mapping table live in `Config` or be hard-coded with env-var override only?
3. Should `KV_LOOKUP_META` default to notification QP, or stay on data QP for symmetry with `KV_PUT`?
4. Telemetry naming — `mooncake_te_traffic_class_*` vs. a more concise `mc_te_tc_*`?
## References
- Mooncake #1187 — `MC_IB_TC` env var
- Mooncake #2048 — TENT QoS (priority + slot rotation)
- Mooncake #2079 — `TransportSelector` / `SelectionContext` (this RFC extends)
- Mooncake #2489 — CC plugin (orthogonal to this — CC = rate, this = classification)
- Mooncake #2525 — `MC_IB_SL` env var
- Mooncake #2526 — Notification-QP SL/TC plumbing
- sgl-project/sglang#28631 — orthogonal priority RFC (this RFC's companion at the SGLang layer)
- NCCL v2.30.7 release notes — `NCCL_GIN_IB_TC` precedent
## Acknowledgements
Cc: @alogfans @stmatengss @staryxchen @lvshufan — most active on the QoS / TENT / RDMA-config line (#2048, #2079, #2489, #2293).
Contributor guide
Research direction
Start with mooncake-transfer-engine/tent/include/tent/runtime/transport_selector.h and the SelectionPolicy and TransportSelector behavior introduced by #2079, especially existing priority filtering. Review #2525 and #2526 for the related SL/TC and notification-QP constraints. Done means the open mapping, configuration, routing, telemetry, and compatibility questions have an agreed scope before implementation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- backend-api-design, distributed-systems, networking, observability
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100