feat: bandwidth-aware load_aware P2P source selection#510
Conversation
Adds a load_aware source-selection policy that biases the stateless rendezvous ordering away from sources whose RDMA NIC is busy, so pulling weights does not contend with a source's in-flight inference RDMA (e.g. a prefill node streaming KV cache): score = unit_hash(target, candidate) - w_load * nic_utilization The load signal is source-published (each source measures its own NIC and publishes nic_utilization in [0,1] on the candidate), not a coordinator-side counter -- so MX replicas stay stateless behind a load balancer and every replica ranks identically. When utilization is 0/unset the penalty vanishes and ordering collapses exactly to rendezvous_hash. This supersedes the counter-based approach in the closed ai-dynamo#477: the reviewer feedback was that the signal that matters is free NIC bandwidth (to protect serving latency), and that source-published telemetry is a cleaner fit for stateless replicas than an accumulated selection counter. This first commit is the consumer side + contract: - proto: SourceInstanceRef.nic_utilization = 6 (additive; not part of SourceIdentity, so mx_source_id is unaffected); regenerated p2p_pb2.py and p2p.pb.go. - client: LoadAwareSelector (source_selection.py), MX_P2P_LOAD_WEIGHT env (default 1.0), and unit tests (collapse-to-rendezvous, steer-away, determinism, weight monotonicity, missing-field, clamping). - server: ListSources passes the field through (0.0 placeholder until the source-side telemetry lands). Follow-ups (this is a draft): source-side NIC sampling + publishing via WorkerMetadata heartbeat, server passthrough of the measured value, Prometheus gauge, docs, and the inference-stability scale-up / rolling-update benchmark. Signed-off-by: Yixin Huang <yixinh@nvidia.com>
Completes the load_aware source signal end to end and makes it live, so a source that is busy serving inference over RDMA is steered away from as a weight-transfer source. Signal (stateless, source-self-reported): - Each source samples its own RDMA NIC utilization from InfiniBand port counters (/sys/class/infiniband/<dev>/ports/1/counters) over the heartbeat interval, and reports it as source_load in [0,1] on each UpdateStatus heartbeat. The server stores it and stamps it onto SourceInstanceRef; the client load_aware selector already consumes it. The server never computes or accumulates it, so replicas stay stateless behind a load balancer. Naming / extensibility: - Renamed the wire field nic_utilization -> source_load (SourceInstanceRef and UpdateStatusRequest), since the value is a normalized busyness that need not come from the NIC. make_source_load_provider() is the seam: it returns the RDMA-NIC provider today, and a runtime provider (vLLM/SGLang serving load, or a max-blend) can replace it with no server/proto/selector change. Robustness: - The sampler measures the GPU-affine NIC (the rail transfers actually use); if that can't be resolved it falls back to the busiest of all node NICs; with no RDMA NIC it reports 0.0 (collapse to rendezvous_hash). Works across IB/RoCE and adapts to link speed via the port rate file; unsupported fabrics (EFA/Slingshot) safely degrade to rendezvous. Plumbing: - proto: UpdateStatusRequest.source_load; SourceInstanceRef.nic_utilization -> source_load. Regenerated p2p_pb2.py and p2p.pb.go. - server: source_load threaded through WorkerRecord/SourceInstanceInfo and the update_status path in all three backends (memory/redis/kubernetes CRD), then onto SourceInstanceRef in list_sources (replaces the 0.0 placeholder). - client: NicUtilizationSampler + SourceLoadSampler + make_source_load_provider (nic_metrics.py); publisher samples on each heartbeat and sends source_load in update_status. Verified: cargo test p2p (91 passed), clippy --tests, cargo fmt --check; Python suite (543 passed) incl. new nic_metrics + selector tests. Follow-ups: Prometheus gauge, docs, and the inference-stability benchmark. Signed-off-by: Yixin Huang <yixinh@nvidia.com>
- metrics.py: mx_p2p_source_load{scheme, source_worker_id} gauge + setter;
rdma_strategy records each candidate's observed source_load at selection.
- docs: ARCHITECTURE.md documents the load_aware policy and the stateless
source_load signal (source-published NIC utilization, provider seam);
DEPLOYMENT.md adds MX_P2P_SOURCE_SELECTOR=load_aware and MX_P2P_LOAD_WEIGHT.
Signed-off-by: Yixin Huang <yixinh@nvidia.com>
Wires an inference-runtime provider into the source_load seam: when MX_P2P_RUNTIME_METRICS_URL is set, the source scrapes its co-located vLLM/SGLang /metrics for a serving-load proxy (vLLM gpu_cache_usage_perc, SGLang token_usage) and publishes source_load = max(NIC utilization, runtime load). The runtime signal is predictive -- a full KV cache / deep prefill queue foretells RDMA the NIC counter hasn't seen yet -- and covers fabrics sysfs can't read; unreachable/unset degrades to NIC-only. - runtime_load.py: RuntimeLoadProvider + _scrape_gauge; recognizes vLLM/SGLang metric names; best-effort (0.0 on any failure). tests/test_runtime_load.py. - nic_metrics.make_source_load_provider blends NIC + runtime via max(). - envs: MX_P2P_RUNTIME_METRICS_URL. ARCHITECTURE.md / DEPLOYMENT.md updated. Signed-off-by: Yixin Huang <yixinh@nvidia.com>
a8faba2 to
2decfd4
Compare
cargo fmt reflow of the map_or_else and update_status call sites touched while threading source_load; no behavior change. Signed-off-by: Yixin Huang <yixinh@nvidia.com>
|
/ok to test 29a8d4e |
Signed-off-by: Yixin Huang <yixinh@nvidia.com>
WalkthroughChangesLoad-aware P2P source selection
Estimated code review effort: 4 (Complex) | ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
modelexpress_client/python/tests/test_publisher.py (1)
98-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover the injected provider path.
These tests only assert the default
0.0value. Add direct_tick()tests for a non-zerosource_load_providerresult and a raising provider falling back to0.0.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@modelexpress_client/python/tests/test_publisher.py` around lines 98 - 119, Extend the PublisherThread _tick tests to cover source_load_provider: verify a non-zero provider result is passed as source_load in mx_client.update_status, and verify a provider that raises still publishes with source_load=0.0. Reuse the existing publisher setup and assert both outcomes through update_status.call_args_list.modelexpress_server/src/p2p/backend/redis.rs (1)
740-740: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAssert nonzero
source_loadpropagation. Current fixtures use0.0and wildcard predicates, so they validate field arity but not the new value flow.
modelexpress_server/src/p2p/backend/redis.rs#L740-L740: use a nonzero value and assert it survives JSON round-trip.modelexpress_server/src/p2p/service.rs#L637-L646: pass a nonzero request value and assert the backend receives it.modelexpress_server/src/p2p/service.rs#L688-L698: return distinct nonzero loads and assert they appear inListSourcesResponse.modelexpress_server/src/p2p/state.rs#L525-L544: replace the final wildcard matcher with the expected nonzero load.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@modelexpress_server/src/p2p/backend/redis.rs` at line 740, Update the source_load test fixtures to verify nonzero value propagation: in modelexpress_server/src/p2p/backend/redis.rs:740, use a nonzero source_load and assert it survives the JSON round-trip; in modelexpress_server/src/p2p/service.rs:637-646, send a nonzero request value and assert the backend receives it; in modelexpress_server/src/p2p/service.rs:688-698, return distinct nonzero loads and assert they appear in ListSourcesResponse; in modelexpress_server/src/p2p/state.rs:525-544, replace the final wildcard matcher with the expected nonzero load.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/ARCHITECTURE.md`:
- Around line 617-625: Update the stale architecture references around the
source-selection documentation: revise the old method signature at Line 559 to
include the current load-aware/source-load behavior, and change the Line 631
wording so load_aware is documented as shipped rather than deferred. Keep the
surrounding selector and topology-aware policy descriptions consistent with the
implemented three-policy registry.
In `@modelexpress_client/python/modelexpress/envs.py`:
- Around line 202-203: Update the MX_P2P_LOAD_WEIGHT configuration parser in
_env_float usage to reject or clamp negative values, ensuring LoadAwareSelector
receives a nonnegative w_load. Add a test covering negative MX_P2P_LOAD_WEIGHT
input and verify the invalid configuration follows the chosen behavior.
In `@modelexpress_client/python/modelexpress/runtime_load.py`:
- Around line 31-35: Add “vllm:kv_cache_usage_perc” first in _RATIO_METRICS,
retaining “vllm:gpu_cache_usage_perc” as the legacy fallback and preserving the
existing sglang metrics. Add or update a test covering runtime load selection
when the new vLLM metric is present.
In `@modelexpress_client/python/tests/test_runtime_load.py`:
- Around line 70-79: Update test_make_source_load_provider_blends_when_url_set
to avoid real socket I/O by patching the runtime metrics fetch/provider seam,
and the NIC sampler if necessary, to return deterministic failure values.
Preserve the test’s assertions that the blended provider does not crash and
returns a float within [0, 1].
---
Nitpick comments:
In `@modelexpress_client/python/tests/test_publisher.py`:
- Around line 98-119: Extend the PublisherThread _tick tests to cover
source_load_provider: verify a non-zero provider result is passed as source_load
in mx_client.update_status, and verify a provider that raises still publishes
with source_load=0.0. Reuse the existing publisher setup and assert both
outcomes through update_status.call_args_list.
In `@modelexpress_server/src/p2p/backend/redis.rs`:
- Line 740: Update the source_load test fixtures to verify nonzero value
propagation: in modelexpress_server/src/p2p/backend/redis.rs:740, use a nonzero
source_load and assert it survives the JSON round-trip; in
modelexpress_server/src/p2p/service.rs:637-646, send a nonzero request value and
assert the backend receives it; in
modelexpress_server/src/p2p/service.rs:688-698, return distinct nonzero loads
and assert they appear in ListSourcesResponse; in
modelexpress_server/src/p2p/state.rs:525-544, replace the final wildcard matcher
with the expected nonzero load.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: b0e3d3b3-d05d-448f-8ce7-cb03da69d17e
⛔ Files ignored due to path filters (1)
modelexpress_client/go/gen/modelexpress/p2p/p2p.pb.gois excluded by!**/*.pb.go,!**/gen/**
📒 Files selected for processing (26)
docs/ARCHITECTURE.mddocs/DEPLOYMENT.mdmodelexpress_client/python/modelexpress/client.pymodelexpress_client/python/modelexpress/envs.pymodelexpress_client/python/modelexpress/load_strategy/rdma_strategy.pymodelexpress_client/python/modelexpress/metadata/k8s_service_client.pymodelexpress_client/python/modelexpress/metadata/publish.pymodelexpress_client/python/modelexpress/metadata/publisher.pymodelexpress_client/python/modelexpress/metrics.pymodelexpress_client/python/modelexpress/nic_metrics.pymodelexpress_client/python/modelexpress/p2p_pb2.pymodelexpress_client/python/modelexpress/runtime_load.pymodelexpress_client/python/modelexpress/source_selection.pymodelexpress_client/python/tests/test_nic_metrics.pymodelexpress_client/python/tests/test_publisher.pymodelexpress_client/python/tests/test_runtime_load.pymodelexpress_client/python/tests/test_source_selection.pymodelexpress_common/proto/p2p.protomodelexpress_server/src/p2p/backend.rsmodelexpress_server/src/p2p/backend/kubernetes.rsmodelexpress_server/src/p2p/backend/memory.rsmodelexpress_server/src/p2p/backend/redis.rsmodelexpress_server/src/p2p/k8s_types.rsmodelexpress_server/src/p2p/reaper.rsmodelexpress_server/src/p2p/service.rsmodelexpress_server/src/p2p/state.rs
…rovider vLLM renamed the KV-cache utilization gauge from vllm:gpu_cache_usage_perc to vllm:kv_cache_usage_perc in v0.17. The runtime source_load provider only matched the old name, so the serving-load blend silently degraded to 0 on current vLLM. Match both names (new first). Signed-off-by: Yixin Huang <yixinh@nvidia.com>
- envs: clamp MX_P2P_LOAD_WEIGHT to >= 0 so a negative weight cannot invert LoadAwareSelector into preferring busy sources (+ test). - tests: patch the runtime fetch seam in the blend test so it does no real socket I/O (was hitting 127.0.0.1:1). - docs/ARCHITECTURE.md: update the stale update_status signature to include source_load, and drop the 'deferred' wording now that load_aware ships. Signed-off-by: Yixin Huang <yixinh@nvidia.com>
- test_publisher: cover the injected source_load_provider path -- a non-zero value is forwarded on update_status, and a raising provider falls back to 0.0. - Rust fixtures: use nonzero source_load and assert the value flows through the Redis JSON round-trip, the update_status backend call, ListSources responses, and the state-manager matcher (were 0.0 + wildcards, validating arity only). Signed-off-by: Yixin Huang <yixinh@nvidia.com>
Summary
Adds a third P2P source-selection policy,
load_aware, that biases weight-pullsource selection away from sources whose RDMA path is already busy, so scaling up
or rolling a live inference fleet does not steal bandwidth from sources that are
actively serving. It builds on the
random/rendezvous_hashselector interfacefrom #461: same
ScoredSelectorseam, one new scalar on the wire, no new RPCs, andit collapses to
rendezvous_hashwhenever the load signal is absent — never worsethan the deterministic baseline.
The MX servers stay stateless: the load signal is published by the source about
itself and passed through by the server — the server never counts or accumulates
anything — so replicas can sit behind a load balancer and scale arbitrarily, and any
two replicas rank an identical
ListSourcesidentically.What changed
source_load: a source-published load scalar (proto + server passthrough)float source_loadonSourceInstanceRef(=6) andUpdateStatusRequest(=5)in
p2p.proto(Python + Go bindings regenerated).WorkerRecord.source_load, refreshedon
update_statusand surfaced onlist_sources; memory / Redis (#[serde(default)])/ Kubernetes-CRD (
sourceLoad) backends all carry it. The server never derives it.load_awareselector (client)score = unit_hash(target, candidate) − w_load · source_load,source_loadclampedto
[0,1],w_load = MX_P2P_LOAD_WEIGHT(default 1.0, clamped≥ 0).rendezvous_hashordering (safedefault; also covers old servers that predate the field).
Where the load number comes from (two providers, blended)
source_loadis produced on the source bymake_source_load_provider(device_id),reporting
max()of two independent signals so selection reacts to a NIC that isphysically hot and to an imminent serving spike a counter has not seen yet:
nic_metrics.py, runtime-agnostic, default). Reads theGPU-affine RDMA NIC's InfiniBand port byte counters
(
/sys/class/infiniband/<dev>/ports/<p>/counters/{port_xmit_data,port_rcv_data},×4 for the 4-octet spec unit) and divides the delta since the last sample by the
elapsed time and the link capacity (parsed from
.../rate). The NIC is the exactrail transfers pin to — resolved via the same
ucx_utils.probe_nic_pin_for_devicePCIe-affinity assignment the NIXL transfer path uses — so it reflects the contention
a puller would actually hit. Degrades safely to
0.0(no device / unreadable sysfs/ unparseable rate), so IB/RoCE report a real number and other fabrics fall back to
rendezvous_hash. Sampled off the existing publisher heartbeat (no extra sleeps).runtime_load.py, opt-in viaMX_P2P_RUNTIME_METRICS_URL). Scrapes the co-located vLLM/SGLang Prometheus/metricsfor a KV-cache-utilization gauge (vllm:kv_cache_usage_perc, or thelegacy
vllm:gpu_cache_usage_perc;sglang:token_usage) as a[0,1]serving-loadproxy — predictive, since a full KV cache foretells RDMA the NIC counter has not
measured yet, and it works on fabrics whose sysfs counters cannot be read.
Neither provider touches the server, proto, or selector; the only wire contract is the
normalized
source_loadscalar, and any provider error degrades to0.0.Observability
mx_p2p_source_load{scheme, source_worker_id}gauge recorded per candidate atselection (opt-in
MX_METRICS_ENABLED=1), alongside the existingmx_p2p_source_selections_total/mx_p2p_transfer_seconds.Compatibility
load_awaredegrades torendezvous_hash. Default policy remainsrandom. No new RPCs, no publish-cadencechange, no server-side state.
Validation
Unit tests
load_awarecollapse-to-rendezvous at zero load, steering away from thebusiest source,
w_loadmonotonicity, negative-weight clamp,[0,1]clamping,missing-field tolerance; NIC sampler (injected reader/clock/link-rate); runtime
provider (vLLM new/legacy gauge names, SGLang, unreachable ⇒ 0); publisher forwards a
non-zero provider value and falls back to 0 on error. Rust:
source_loadpassthroughacross memory/Redis/k8s backends with non-zero round-trip assertions.
B200 E2E
Ran on B200, InfiniBand RDMA with a vLLM disaggregated
prefill/decode fleet, TP=1, kubernetes metadata backend,
MX_METRICS_ENABLED=1, andsource_load = max(NIC util, vLLM KV-cache usage). Fleet driven by AIPerf (closed-loopconcurrency 48, 512-in / 128-out, streaming chat) against the dynamo frontend, capturing
TTFT / request-latency / MX metrics at baseline, during a Case-1 scale-up
(decode 4→8 under load, new replicas pull weights over MX P2P), and during a Case-2
rolling update (decode replicas replaced one at a time under load).
--load-format modelexpressand register as P2P sources publishing
source_load; the NIC+KV blend, the selector,and the
mx_p2p_source_loadtelemetry all work on real hardware.source_loadsignal is real and heterogeneous under load. On a memory-tightQwen2.5-72B fleet the KV-cache signal saturated — prefill workers reported
99.7% KV usage vs. 0% on the idle decode workers — and during the scale-up
load_awarerouted the new pullers to the idle sources, away from the 99.7%-loadedprefill sources (the intended steering).
held TTFT flat (p99 ≈ 380 ms baseline → ≈ 375 ms during scale-up) with throughput up
~5%; here the B200's KV headroom keeps
source_loadlow, soload_awarerides itsrendezvous_hashfallback — i.e. it matches the deterministic baseline exactly, thedesigned safety property.