Skip to content

Commit f0f2411

Browse files
committed
feat: add RDMA transport for ViT output
Build a reusable Tensor RDMA transport contract around an owner-side RdmaExport API and a reader-side RdmaRead API. Define stable descriptors, per-tensor manifests, lease-based slot ownership, batch reads, provider selection, and a no-op implementation so applications do not depend on a specific RDMA library. Decouple separated ViT communication from its data plane. Keep gRPC as the control plane for capability negotiation, requests, receipts, deadlines, and lease release, while routing output delivery through transport backends and receipt readers. The ViT and multimodal processor business paths now consume a single transport interface instead of branching on grpc-inline or RDMA. Adapt the complete ViT output to the common RDMA contract. Export embedding, position IDs, and extra inputs from registered GPU slots, return descriptors in the receipt, and reconstruct the original MultimodalOutput on the LLM side. Oversized outputs are split into ordered slots without changing split_size semantics. Keep the ViT exporter and LLM receipt reader in separate build targets. Expose the exporter through libmm_rdma_exporter.so so a ViT process does not load the LLM and Embedding engine bindings in libth_transformer.so. Validate descriptors and manifests, bound in-flight slot memory, release slots through the control plane, and reclaim abandoned leases with GC. RDMA setup, export, or READ failures use one bounded grpc-inline fallback. The open-source build provides the transport contract and fallback; the internal build supplies the Barex provider. MM_TRANSPORT_MODE=auto tries RDMA first, while MM_TRANSPORT_MODE=grpc forces inline delivery; invalid modes fail fast. Configure GPU-NIC affinity during service startup using the physical GPU mapping.
1 parent b7a5437 commit f0f2411

80 files changed

Lines changed: 4382 additions & 501 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

BUILD

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,19 @@ cc_binary(
184184
}),
185185
)
186186

187+
cc_binary(
188+
name = "mm_rdma_exporter",
189+
copts = copts(),
190+
linkopts = [
191+
"-Wl,-rpath='$$ORIGIN'",
192+
],
193+
linkshared = 1,
194+
visibility = ["//visibility:public"],
195+
deps = [
196+
"//rtp_llm/cpp/pybind:mm_rdma_exporter_pybind",
197+
],
198+
)
199+
187200
cc_binary(
188201
name = "th_transformer",
189202
srcs = [

arch_config/arch_select.bzl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ load("@pip_gpu_rocm_torch//:requirements.bzl", requirement_gpu_rocm="requirement
88
load("@rtp_llm//bazel:defs.bzl", "copy_so")
99

1010
def copy_all_so():
11+
copy_so("@rtp_llm//:mm_rdma_exporter")
1112
copy_so("@rtp_llm//:th_transformer")
1213
copy_so("@rtp_llm//:th_transformer_config")
1314
copy_so("@rtp_llm//:th_grammar_tokenizer_info")
@@ -55,6 +56,14 @@ def cache_store_deps():
5556
actual = "@rtp_llm//rtp_llm/cpp/disaggregate/cache_store:cache_store_base_impl"
5657
)
5758

59+
def rdma_transport_deps():
60+
# Open-source builds expose the same factory API but have no RDMA provider.
61+
native.alias(
62+
name = "rdma_transport_arch_select_impl",
63+
actual = "@rtp_llm//rtp_llm/cpp/rdma_transport:rdma_transport_no_impl",
64+
visibility = ["//visibility:public"],
65+
)
66+
5867
def transfer_rdma_deps():
5968
native.alias(
6069
name = "transfer_rdma_impl",

bazel/py_proto.bzl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def _generate_grpc_proto_impl(ctx):
1717
# use create_grpc_proto generate proto py files
1818
ctx.actions.run(
1919
outputs = [pb2_file, pb2_grpc_file],
20-
inputs = [proto_file],
20+
inputs = [proto_file] + ctx.files.proto_deps,
2121
executable = ctx.executable.create_grpc_proto,
2222
arguments = [proto_file.path, output_dir],
2323
tools = [ctx.executable.create_grpc_proto]
@@ -43,5 +43,8 @@ generate_grpc_proto = rule(
4343
cfg = "exec",
4444
mandatory = True,
4545
),
46+
"proto_deps": attr.label_list(
47+
allow_files = [".proto"],
48+
),
4649
},
47-
)
50+
)

rtp_llm/async_decoder_engine/rpc_engine.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,8 @@ def __init__(
4747
and self.model.vit_config.vit_separation
4848
== VitSeparation.VIT_SEPARATION_LOCAL
4949
and engine_config.parallelism_config.tp_rank == 0
50-
and (
51-
engine_config.pd_sep_config.role_type == RoleType.PREFILL
52-
or engine_config.pd_sep_config.role_type == RoleType.PDFUSION
53-
)
50+
and engine_config.pd_sep_config.role_type
51+
in (RoleType.PREFILL, RoleType.PDFUSION)
5452
):
5553
self.mm_process_engine = (
5654
MultimodalMixinFactory.create_multimodal_process_engine(

rtp_llm/config/py_config_modules.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,38 @@ def to_string(self):
263263
)
264264

265265

266+
# Keep these transport defaults aligned with cpp/config/ConfigModules.h::MMTransportConfig.
267+
MM_TRANSPORT_MODE_AUTO = "auto"
268+
MM_TRANSPORT_MODE_GRPC = "grpc"
269+
MM_TRANSPORT_MODES = (MM_TRANSPORT_MODE_AUTO, MM_TRANSPORT_MODE_GRPC)
270+
DEFAULT_MM_TIMEOUT_MS = 120000
271+
272+
273+
class MMRdmaConfig:
274+
def __init__(self):
275+
self.bind_ip: str = ""
276+
self.port: int = 0
277+
self.connect_timeout_ms: int = 250
278+
self.read_timeout_ms: int = 3000
279+
self.qp_count: int = 8
280+
self.slot_gc_timeout_ms: int = 60 * 1000
281+
self.max_slot_bytes: int = 1024 * 1024 * 1024
282+
283+
284+
class MMControlConfig:
285+
def __init__(self):
286+
self.release_timeout_ms: int = 1000
287+
288+
289+
class MMTransportConfig:
290+
def __init__(self):
291+
self.mode: str = MM_TRANSPORT_MODE_AUTO
292+
self.control = MMControlConfig()
293+
self.rdma = MMRdmaConfig()
294+
295+
266296
class VitConfig:
267-
DEFAULT_MM_TIMEOUT_MS: int = 120000
297+
DEFAULT_MM_TIMEOUT_MS: int = DEFAULT_MM_TIMEOUT_MS
268298
DEFAULT_MM_IMAGE_MAX_FILE_SIZE_KB: int = 100 * 1024
269299
DEFAULT_MM_VIDEO_MAX_FILE_SIZE_KB: int = 2 * 1024 * 1024
270300

@@ -296,6 +326,7 @@ def __init__(self):
296326
self.disable_access_log: bool = False
297327
self.use_local_preprocess: bool = False
298328
self.vit_proxy_load_balance_strategy: str = "round_robin"
329+
self.output_transport = MMTransportConfig()
299330
# Cross-request GPU batching is inferred from gpu_max_batch_size alone:
300331
# == 1 -> serial (one request per forward, no wait window); > 1 -> merge
301332
# compatible requests within gpu_batch_wait_ms. Default 1 keeps the old
@@ -344,6 +375,9 @@ def embedding_scheduler_args(self) -> Dict[str, int]:
344375
}
345376

346377
def to_string(self):
378+
transport = self.output_transport
379+
control = transport.control
380+
rdma = transport.rdma
347381
return (
348382
f"vit_separation: {self.vit_separation}\n"
349383
f"vit_trt: {self.vit_trt}\n"
@@ -368,6 +402,15 @@ def to_string(self):
368402
f"disable_access_log: {self.disable_access_log}\n"
369403
f"use_local_preprocess: {self.use_local_preprocess}\n"
370404
f"vit_proxy_load_balance_strategy: {self.vit_proxy_load_balance_strategy}\n"
405+
f"mm_transport_mode: {transport.mode}\n"
406+
f"mm_rdma_bind_ip: {rdma.bind_ip}\n"
407+
f"mm_rdma_port: {rdma.port}\n"
408+
f"mm_rdma_connect_timeout_ms: {rdma.connect_timeout_ms}\n"
409+
f"mm_rdma_read_timeout_ms: {rdma.read_timeout_ms}\n"
410+
f"mm_rdma_qp_count: {rdma.qp_count}\n"
411+
f"mm_rdma_release_timeout_ms: {control.release_timeout_ms}\n"
412+
f"mm_rdma_slot_gc_timeout_ms: {rdma.slot_gc_timeout_ms}\n"
413+
f"mm_rdma_max_slot_bytes: {rdma.max_slot_bytes}\n"
371414
f"gpu_batch_wait_ms: {self.gpu_batch_wait_ms}\n"
372415
f"gpu_max_batch_size: {self.gpu_max_batch_size}\n"
373416
f"gpu_max_batch_images: {self.gpu_max_batch_images}\n"

rtp_llm/config/server_config_setup.py

Lines changed: 140 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import os
44
import socket
5+
import subprocess
56
from typing import Optional
67

78
import torch
@@ -521,6 +522,144 @@ def fetch_model_files_to_local(py_env_configs: PyEnvConfigs):
521522
)
522523

523524

525+
def _physical_gpu_index(local_rank: int) -> Optional[str]:
526+
visible_devices = os.environ.get("CUDA_VISIBLE_DEVICES")
527+
if not visible_devices:
528+
return str(local_rank)
529+
530+
devices = [device.strip() for device in visible_devices.split(",")]
531+
if local_rank < 0 or local_rank >= len(devices) or not devices[local_rank]:
532+
logging.warning(
533+
"local rank %d is outside CUDA_VISIBLE_DEVICES=%s",
534+
local_rank,
535+
visible_devices,
536+
)
537+
return None
538+
539+
device = devices[local_rank]
540+
if device.isdigit():
541+
return device
542+
if not device.startswith("GPU-"):
543+
logging.warning("unsupported CUDA_VISIBLE_DEVICES entry: %s", device)
544+
return None
545+
546+
try:
547+
result = subprocess.run(
548+
[
549+
"nvidia-smi",
550+
"--query-gpu=index,uuid",
551+
"--format=csv,noheader,nounits",
552+
],
553+
check=True,
554+
stdout=subprocess.PIPE,
555+
stderr=subprocess.PIPE,
556+
text=True,
557+
timeout=10,
558+
)
559+
except Exception as e:
560+
logging.warning("failed to resolve CUDA device UUID %s: %s", device, e)
561+
return None
562+
563+
for line in result.stdout.splitlines():
564+
index, separator, uuid = line.partition(",")
565+
if separator and uuid.strip().startswith(device):
566+
return index.strip()
567+
logging.warning("CUDA device UUID %s was not reported by nvidia-smi", device)
568+
return None
569+
570+
571+
def load_gpu_nic_affinity() -> bool:
572+
"""Load the host GPU-NIC map before a service configures its CUDA device."""
573+
if os.environ.get("ACCL_NIC_GPU_AFFINITY") is not None:
574+
return True
575+
576+
run_affinity_path = "/usr/local/bin/run_affinity"
577+
if not os.path.exists(run_affinity_path):
578+
logging.info("get gpu nic affinity failed, %s not exist", run_affinity_path)
579+
return False
580+
581+
try:
582+
subprocess.run(
583+
[run_affinity_path],
584+
check=True,
585+
stdout=subprocess.PIPE,
586+
stderr=subprocess.PIPE,
587+
timeout=30,
588+
)
589+
with open("npu_nic_affinity.json") as affinity_file:
590+
content = affinity_file.read().strip()
591+
affinity = json.loads(content)
592+
if not isinstance(affinity, dict) or not affinity:
593+
raise ValueError("affinity content is not a non-empty object")
594+
if not all(
595+
isinstance(rank, str) and isinstance(nic, str) and nic
596+
for rank, nic in affinity.items()
597+
):
598+
raise ValueError("affinity keys and NIC values must be non-empty strings")
599+
except Exception as e:
600+
logging.info(
601+
"get gpu nic affinity failed, run %s failed, exception is %s",
602+
run_affinity_path,
603+
e,
604+
)
605+
return False
606+
607+
os.environ["ACCL_NIC_GPU_AFFINITY"] = content
608+
logging.info(
609+
"get gpu nic affinity success, set env ACCL_NIC_GPU_AFFINITY to %s",
610+
content,
611+
)
612+
return True
613+
614+
615+
def _configure_gpu_nic_affinity(local_rank: int) -> bool:
616+
configured_nics = os.environ.get("ACCL_USE_NICS")
617+
if configured_nics:
618+
logging.info("keep explicit ACCL_USE_NICS=%s", configured_nics)
619+
return True
620+
621+
content = os.environ.get("ACCL_NIC_GPU_AFFINITY")
622+
if not content:
623+
return False
624+
625+
try:
626+
affinity = json.loads(content)
627+
except (json.JSONDecodeError, TypeError) as e:
628+
logging.warning(
629+
"invalid ACCL_NIC_GPU_AFFINITY; ACCL_USE_NICS will not be derived: %s",
630+
e,
631+
)
632+
return False
633+
if not isinstance(affinity, dict):
634+
logging.warning(
635+
"invalid ACCL_NIC_GPU_AFFINITY; ACCL_USE_NICS will not be derived: expected object"
636+
)
637+
return False
638+
639+
physical_gpu = _physical_gpu_index(local_rank)
640+
affinity_nic = affinity.get(physical_gpu) if physical_gpu is not None else None
641+
# Keep compatibility with existing mappings keyed by process-local rank.
642+
if not affinity_nic:
643+
affinity_nic = affinity.get(str(local_rank))
644+
if not isinstance(affinity_nic, str) or not affinity_nic:
645+
logging.warning(
646+
"local rank %d (physical GPU %s) get affinity nic failed, content is %s",
647+
local_rank,
648+
physical_gpu,
649+
content,
650+
)
651+
return False
652+
653+
os.environ["ACCL_USE_NICS"] = affinity_nic
654+
logging.info(
655+
"local rank %d maps to physical GPU %s, set ACCL_USE_NICS to %s",
656+
local_rank,
657+
physical_gpu,
658+
affinity_nic,
659+
)
660+
return True
661+
662+
524663
def setup_cuda_device_and_accl_env(local_rank: int) -> None:
525664
"""Apply CUDA device and ACCL env side effects (same as ParallelInfo.from_params)."""
526665
if torch.cuda.is_available():
@@ -531,27 +670,7 @@ def setup_cuda_device_and_accl_env(local_rank: int) -> None:
531670
os.environ["ACCL_SELECT_PORT"] = select_port
532671
logging.info(f"local rank {local_rank} set accl select port to {select_port} ")
533672

534-
if (
535-
os.environ.get("ACCL_USE_NICS") is None
536-
and os.environ.get("ACCL_NIC_GPU_AFFINITY") is not None
537-
):
538-
content = os.environ.get("ACCL_NIC_GPU_AFFINITY")
539-
try:
540-
gpu_nic_affinity = json.loads(content)
541-
if str(local_rank) in gpu_nic_affinity:
542-
affinity_nic = gpu_nic_affinity[str(local_rank)]
543-
os.environ["ACCL_USE_NICS"] = affinity_nic
544-
logging.info(
545-
f"local rank {local_rank} use cuda device {local_rank} set ACCL_USE_NICS to {affinity_nic}"
546-
)
547-
else:
548-
logging.info(
549-
f"local rank {local_rank} use cuda device {local_rank} get affinity nic failed, content is {content}"
550-
)
551-
except json.JSONDecodeError:
552-
logging.info(
553-
f"try decode ACCL_NIC_GPU_AFFINITY failed, content is {content}"
554-
)
673+
_configure_gpu_nic_affinity(local_rank)
555674

556675

557676
def setup_and_configure_server(py_env_configs: PyEnvConfigs):

rtp_llm/cpp/config/BUILD

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ cc_library(
2525
"@havenask//aios/autil:env_util",
2626
":role_types",
2727
":eplb_config",
28+
":mm_transport_mode",
29+
"//rtp_llm/cpp/rdma_transport:rdma_config",
2830
"//rtp_llm/models_py/bindings/core:types",
2931
],
3032
copts = copts(),
@@ -38,6 +40,25 @@ cc_library(
3840
copts = copts(),
3941
)
4042

43+
cc_library(
44+
name = "config_extract",
45+
hdrs = ["ConfigExtract.h"],
46+
deps = [
47+
":config_modules",
48+
":mm_transport_mode",
49+
"//:rtp_compute_ops",
50+
],
51+
visibility = ["//visibility:public"],
52+
copts = copts(),
53+
)
54+
55+
cc_library(
56+
name = "mm_transport_mode",
57+
hdrs = ["MMTransportMode.h"],
58+
visibility = ["//visibility:public"],
59+
copts = copts(),
60+
)
61+
4162
cc_library(
4263
name = "special_tokens",
4364
hdrs = ["SpecialTokens.h"],

0 commit comments

Comments
 (0)