Skip to content

feat: bandwidth-aware load_aware P2P source selection#510

Open
yixinh-nv wants to merge 9 commits into
ai-dynamo:mainfrom
yixinh-nv:yixinh/load-aware-bandwidth-selection
Open

feat: bandwidth-aware load_aware P2P source selection#510
yixinh-nv wants to merge 9 commits into
ai-dynamo:mainfrom
yixinh-nv:yixinh/load-aware-bandwidth-selection

Conversation

@yixinh-nv

@yixinh-nv yixinh-nv commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a third P2P source-selection policy, load_aware, that biases weight-pull
source 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_hash selector interface
from #461: same ScoredSelector seam, one new scalar on the wire, no new RPCs, and
it collapses to rendezvous_hash whenever the load signal is absent — never worse
than 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 ListSources identically.

What changed

source_load: a source-published load scalar (proto + server passthrough)

  • New float source_load on SourceInstanceRef (=6) and UpdateStatusRequest (=5)
    in p2p.proto (Python + Go bindings regenerated).
  • The server plumbs it as an opaque passthrough: WorkerRecord.source_load, refreshed
    on update_status and surfaced on list_sources; memory / Redis (#[serde(default)])
    / Kubernetes-CRD (sourceLoad) backends all carry it. The server never derives it.

load_aware selector (client)

  • score = unit_hash(target, candidate) − w_load · source_load, source_load clamped
    to [0,1], w_load = MX_P2P_LOAD_WEIGHT (default 1.0, clamped ≥ 0).
  • Absent/zero load ⇒ every penalty is 0 ⇒ exact rendezvous_hash ordering (safe
    default; also covers old servers that predate the field).

Where the load number comes from (two providers, blended)

source_load is produced on the source by make_source_load_provider(device_id),
reporting max() of two independent signals so selection reacts to a NIC that is
physically hot and to an imminent serving spike a counter has not seen yet:

  1. RDMA NIC utilization (nic_metrics.py, runtime-agnostic, default). Reads the
    GPU-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 exact
    rail transfers pin to — resolved via the same ucx_utils.probe_nic_pin_for_device
    PCIe-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).
  2. Inference-runtime serving load (runtime_load.py, opt-in via
    MX_P2P_RUNTIME_METRICS_URL). Scrapes the co-located vLLM/SGLang Prometheus
    /metrics for a KV-cache-utilization gauge (vllm:kv_cache_usage_perc, or the
    legacy vllm:gpu_cache_usage_perc; sglang:token_usage) as a [0,1] serving-load
    proxy — 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_load scalar, and any provider error degrades to 0.0.

Observability

  • mx_p2p_source_load{scheme, source_worker_id} gauge recorded per candidate at
    selection (opt-in MX_METRICS_ENABLED=1), alongside the existing
    mx_p2p_source_selections_total / mx_p2p_transfer_seconds.

Compatibility

  • Additive proto field; old clients/servers ignore it and load_aware degrades to
    rendezvous_hash. Default policy remains random. No new RPCs, no publish-cadence
    change, no server-side state.

Validation

Unit tests

  • Python: load_aware collapse-to-rendezvous at zero load, steering away from the
    busiest source, w_load monotonicity, 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_load passthrough
    across 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, and
source_load = max(NIC util, vLLM KV-cache usage). Fleet driven by AIPerf (closed-loop
concurrency 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).

  • Mechanism, end to end. New replicas load weights via --load-format modelexpress
    and register as P2P sources publishing source_load; the NIC+KV blend, the selector,
    and the mx_p2p_source_load telemetry all work on real hardware.
  • The source_load signal is real and heterogeneous under load. On a memory-tight
    Qwen2.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_aware routed the new pullers to the idle sources, away from the 99.7%-loaded
    prefill sources
    (the intended steering).
  • No regression at typical model sizes. On Qwen2.5-14B, a scale-up under load
    held TTFT flat (p99 ≈ 380 ms baseline → ≈ 375 ms during scale-up) with throughput up
    ~5%; here the B200's KV headroom keeps source_load low, so load_aware rides its
    rendezvous_hash fallback — i.e. it matches the deterministic baseline exactly, the
    designed safety property.

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>
@copy-pr-bot

copy-pr-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

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>
@yixinh-nv yixinh-nv force-pushed the yixinh/load-aware-bandwidth-selection branch from a8faba2 to 2decfd4 Compare July 15, 2026 22:25
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>
@yixinh-nv

Copy link
Copy Markdown
Contributor Author

/ok to test 29a8d4e

@copy-pr-bot copy-pr-bot Bot temporarily deployed to automated-release July 15, 2026 22:30 Inactive
@copy-pr-bot copy-pr-bot Bot temporarily deployed to automated-release July 15, 2026 22:30 Inactive
Signed-off-by: Yixin Huang <yixinh@nvidia.com>
@yixinh-nv yixinh-nv marked this pull request as ready for review July 15, 2026 23:55
@yixinh-nv yixinh-nv enabled auto-merge (squash) July 15, 2026 23:56
@yixinh-nv yixinh-nv requested a review from zhengluo-nv July 16, 2026 00:01
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Load-aware P2P source selection

Layer / File(s) Summary
Protocol and server load propagation
modelexpress_common/proto/p2p.proto, modelexpress_server/src/p2p/*, modelexpress_client/python/modelexpress/p2p_pb2.py
Adds source_load to status, discovery, server records, and memory, Redis, and Kubernetes metadata paths.
Client load sampling and heartbeat publication
modelexpress_client/python/modelexpress/{nic_metrics.py,runtime_load.py}, modelexpress_client/python/modelexpress/metadata/*, modelexpress_client/python/modelexpress/client.py
Samples NIC and optional runtime metrics, combines them into source_load, and publishes the value during heartbeats with fallback behavior.
Load-aware selection and observability
modelexpress_client/python/modelexpress/source_selection.py, modelexpress_client/python/modelexpress/load_strategy/*, modelexpress_client/python/modelexpress/metrics.py, docs/*
Adds the load_aware policy, load-penalized rendezvous scoring, candidate load metrics, tests, and configuration documentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Poem

I’m a rabbit with metrics, hopping light,
Busy sources fade from sight.
NICs and runtimes whisper, “Load!”
Heartbeats carry the woodland code.
Hashes rank the paths just right—
Carrots saved for retries tonight.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds load-aware source_load telemetry, but #477 also required active_transfers metadata and server-side TTL-based load tracking, which are not present. Add the missing active_transfers/TTL load-tracking path and expose it through ListSources, or revise the issue scope to match the source_load design.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes stay focused on the load_aware P2P selection feature, related telemetry, docs, and tests, with no clear unrelated additions.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding load-aware P2P source selection.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
modelexpress_client/python/tests/test_publisher.py (1)

98-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the injected provider path.

These tests only assert the default 0.0 value. Add direct _tick() tests for a non-zero source_load_provider result and a raising provider falling back to 0.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 win

Assert nonzero source_load propagation. Current fixtures use 0.0 and 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 in ListSourcesResponse.
  • 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

📥 Commits

Reviewing files that changed from the base of the PR and between 656133f and a2b16a6.

⛔ Files ignored due to path filters (1)
  • modelexpress_client/go/gen/modelexpress/p2p/p2p.pb.go is excluded by !**/*.pb.go, !**/gen/**
📒 Files selected for processing (26)
  • docs/ARCHITECTURE.md
  • docs/DEPLOYMENT.md
  • modelexpress_client/python/modelexpress/client.py
  • modelexpress_client/python/modelexpress/envs.py
  • modelexpress_client/python/modelexpress/load_strategy/rdma_strategy.py
  • modelexpress_client/python/modelexpress/metadata/k8s_service_client.py
  • modelexpress_client/python/modelexpress/metadata/publish.py
  • modelexpress_client/python/modelexpress/metadata/publisher.py
  • modelexpress_client/python/modelexpress/metrics.py
  • modelexpress_client/python/modelexpress/nic_metrics.py
  • modelexpress_client/python/modelexpress/p2p_pb2.py
  • modelexpress_client/python/modelexpress/runtime_load.py
  • modelexpress_client/python/modelexpress/source_selection.py
  • modelexpress_client/python/tests/test_nic_metrics.py
  • modelexpress_client/python/tests/test_publisher.py
  • modelexpress_client/python/tests/test_runtime_load.py
  • modelexpress_client/python/tests/test_source_selection.py
  • modelexpress_common/proto/p2p.proto
  • modelexpress_server/src/p2p/backend.rs
  • modelexpress_server/src/p2p/backend/kubernetes.rs
  • modelexpress_server/src/p2p/backend/memory.rs
  • modelexpress_server/src/p2p/backend/redis.rs
  • modelexpress_server/src/p2p/k8s_types.rs
  • modelexpress_server/src/p2p/reaper.rs
  • modelexpress_server/src/p2p/service.rs
  • modelexpress_server/src/p2p/state.rs

Comment thread docs/ARCHITECTURE.md
Comment thread modelexpress_client/python/modelexpress/envs.py Outdated
Comment thread modelexpress_client/python/modelexpress/runtime_load.py
Comment thread modelexpress_client/python/tests/test_runtime_load.py
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant