kvcache-ai / kvcache-ai/Mooncake

[RFC]: Traffic class hint API for TransferEngine — bridging upper-layer semantics (KV / EP / CTRL) to lower-layer SL/TC

Open
#2,568 6 comments 2 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.