Skip to content

Commit 0decb04

Browse files
committed
feat(gms): integrate TensorRT-LLM GPU memory management
1 parent 8bc2ae7 commit 0decb04

18 files changed

Lines changed: 1800 additions & 111 deletions

File tree

components/src/dynamo/trtllm/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66
if "PYTHONHASHSEED" not in os.environ:
77
os.environ["PYTHONHASHSEED"] = "0"
88

9+
910
if __name__ == "__main__":
1011
from dynamo.common.snapshot.restore_context import maybe_run_restore_standby_mode
1112

1213
# In restore mode (DYN_SNAPSHOT_RESTORE_STANDBY=1), before importing TRT-LLM,
1314
# write selected restore-time env vars to snapshot-control/restore-context.json
1415
# and exec `sleep infinity` without initializing CUDA or backend/runtime state.
1516
maybe_run_restore_standby_mode()
17+
from dynamo.trtllm.startup import configure_gms_openmpi_defaults
18+
19+
configure_gms_openmpi_defaults()
1620

1721
from dynamo.trtllm.main import main
1822

components/src/dynamo/trtllm/main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,30 @@ async def worker(argv: list[str] | None = None):
134134
# The drain callback waits for in-flight requests to finish so that GPU
135135
# memory is not freed while transfers are active (issue #7319).
136136
engine_holder: list = []
137+
publisher_holder: list = []
137138
drain_callback = None
138139
if config.disaggregation_mode == DisaggregationMode.PREFILL:
139140
drain_callback = _make_drain_callback(engine_holder)
140141

142+
async def cleanup_engine() -> None:
143+
if publisher_holder:
144+
await publisher_holder[0].cleanup()
145+
logging.info("TRT-LLM publisher cleanup complete")
146+
147+
if not engine_holder:
148+
logging.info("TRT-LLM engine not initialized; skipping cleanup")
149+
return
150+
151+
await engine_holder[0].cleanup()
152+
logging.info("TRT-LLM engine cleanup complete")
153+
141154
install_signal_handlers(
142155
loop,
143156
runtime,
144157
shutdown_endpoints,
145158
shutdown_event,
146159
drain_callback=drain_callback,
160+
cleanup_callback=cleanup_engine,
147161
)
148162

149163
logging.info(f"Initializing the worker with config: {config}")
@@ -153,6 +167,7 @@ async def worker(argv: list[str] | None = None):
153167
shutdown_event,
154168
shutdown_endpoints,
155169
engine_holder=engine_holder,
170+
publisher_holder=publisher_holder,
156171
)
157172

158173

components/src/dynamo/trtllm/publisher.py

Lines changed: 164 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import asyncio
2323
import concurrent.futures
2424
import logging
25+
import struct
2526
import threading
2627
import time
2728
import traceback
@@ -32,11 +33,17 @@
3233
from typing import Any, Awaitable, Callable, Dict, Optional, Union
3334

3435
import msgspec
36+
import xxhash
3537
import zmq
3638
from prometheus_client import CollectorRegistry
3739

3840
from dynamo.common.utils.prometheus import LLMBackendMetrics
39-
from dynamo.llm import FpmDirectPublisher, KvEventPublisher, WorkerMetricsPublisher
41+
from dynamo.llm import (
42+
FpmDirectPublisher,
43+
KvEventPublisher,
44+
WorkerMetricsPublisher,
45+
compute_block_hash_for_seq,
46+
)
4047

4148
logger = logging.getLogger(__name__)
4249

@@ -107,6 +114,66 @@ def _to_signed_i64(value: int | None) -> int | None:
107114
return value
108115

109116

117+
_XXH3_SEED = 1337
118+
_UINT64_MASK = (1 << 64) - 1
119+
# TRT-LLM's PyExecutor/VANILLA path can emit request tokens with an
120+
# engine-added leading special token even when the router request token_ids did
121+
# not contain it. Strip only common BOS-style ids before computing router
122+
# hashes for oversized TRT event blocks.
123+
_TRT_LEADING_SPECIAL_TOKEN_IDS = {0, 1, 2, 128000, 128001, 128002}
124+
125+
126+
def _xxh3_u64(data: bytes, seed: int = _XXH3_SEED) -> int:
127+
return xxhash.xxh3_64_intdigest(data, seed=seed & _UINT64_MASK) & _UINT64_MASK
128+
129+
130+
def _compute_router_sequence_hashes(local_hashes: list[int]) -> list[int]:
131+
if not local_hashes:
132+
return []
133+
134+
sequence_hashes = [int(local_hashes[0]) & _UINT64_MASK]
135+
for local_hash in local_hashes[1:]:
136+
payload = struct.pack(
137+
"<QQ", sequence_hashes[-1], int(local_hash) & _UINT64_MASK
138+
)
139+
sequence_hashes.append(_xxh3_u64(payload))
140+
return sequence_hashes
141+
142+
143+
def _strip_trt_leading_special_token(
144+
token_ids: list[int], kv_block_size: int
145+
) -> tuple[list[int], bool]:
146+
if len(token_ids) <= kv_block_size or not token_ids:
147+
return token_ids, False
148+
if token_ids[0] in _TRT_LEADING_SPECIAL_TOKEN_IDS:
149+
stripped = token_ids[1:]
150+
if len(stripped) >= kv_block_size:
151+
return stripped, True
152+
return token_ids, False
153+
154+
155+
def _split_oversized_trt_block_for_router(
156+
token_ids: list[int], kv_block_size: int, lora_name: Optional[str] = None
157+
) -> tuple[list[int], list[int], list[int], bool]:
158+
if kv_block_size <= 0:
159+
return [], [], [], False
160+
161+
normalized_tokens, stripped_special = _strip_trt_leading_special_token(
162+
token_ids, kv_block_size
163+
)
164+
usable_len = len(normalized_tokens) - (len(normalized_tokens) % kv_block_size)
165+
if usable_len <= 0:
166+
return [], [], [], stripped_special
167+
168+
full_tokens = normalized_tokens[:usable_len]
169+
local_hashes = compute_block_hash_for_seq(
170+
[int(token) for token in full_tokens], kv_block_size, lora_name=lora_name
171+
)
172+
sequence_hashes = _compute_router_sequence_hashes(local_hashes)
173+
num_block_tokens = [kv_block_size] * len(sequence_hashes)
174+
return full_tokens, num_block_tokens, sequence_hashes, stripped_special
175+
176+
110177
class ZmqKvEventPublisher:
111178
"""
112179
Pure Python ZMQ PUBLISHER for TensorRT-LLM KV events.
@@ -426,11 +493,17 @@ def __init__(
426493
# A set to store the block hash of partial block (i.e. block containing less than kv_block_size tokens) hashes.
427494
# It is used to prevent sending remove event to kv router since partial blocks are not stored.
428495
self.partial_block_hashes: set[int] = set()
496+
# Maps oversized TRT engine block hashes to the synthetic router-visible
497+
# sequence hashes we publish for those blocks, so later remove events
498+
# evict the same hashes from the router indexer.
499+
self.synthetic_block_hashes: dict[int, list[int]] = {}
429500
self.error_queue: Queue = Queue()
430501
self._stop_event = threading.Event()
431502
# Track the last engine event_id per attention-DP rank. TRT-LLM emits
432503
# independent rank-local sequences before gathering them on rank 0.
433504
self._last_engine_event_id_by_rank: dict[int, int] = {}
505+
self._cleanup_lock = asyncio.Lock()
506+
self._cleanup_started = False
434507

435508
# Initialize ZMQ publisher if endpoint is provided (consolidator enabled)
436509
if zmq_endpoint:
@@ -826,26 +899,59 @@ def _handle_kv_event(self, event):
826899
block_mm_infos: list[dict | None] = []
827900
kv_block_size = self.kv_block_size
828901
partial_block_hashes = self.partial_block_hashes
902+
synthetic_block_hashes = self.synthetic_block_hashes
903+
lora_name = data.get("lora_name")
904+
synthetic_parent_hash = parent_hash
829905
for block in data["blocks"]:
830906
block_tokens = block["tokens"]
831907
token_num_in_block = len(block_tokens)
832-
if token_num_in_block > kv_block_size:
833-
logging.error(
834-
f"Block contains {token_num_in_block} tokens, which is greater than kv_block_size {kv_block_size}"
835-
)
836-
return
908+
raw_token_ids = [int(t["token_id"]) for t in block_tokens]
837909
block_hash = _to_signed_i64(block["block_hash"])
838910
if block_hash is None:
839911
logging.warning(
840912
f"Skipping block with None hash containing {token_num_in_block} tokens"
841913
)
842914
continue
915+
916+
if token_num_in_block > kv_block_size:
917+
(
918+
full_tokens,
919+
synthetic_num_block_tokens,
920+
synthetic_hashes,
921+
stripped_special,
922+
) = _split_oversized_trt_block_for_router(
923+
raw_token_ids, kv_block_size, lora_name
924+
)
925+
if not synthetic_hashes:
926+
logging.debug(
927+
"Skipping oversized TRT KV block with no full router blocks: "
928+
f"token_count={token_num_in_block}, kv_block_size={kv_block_size}"
929+
)
930+
partial_block_hashes.add(block_hash)
931+
continue
932+
933+
signed_hashes = [_to_signed_i64(h) for h in synthetic_hashes]
934+
token_ids.extend(full_tokens)
935+
num_block_tokens.extend(synthetic_num_block_tokens)
936+
block_hashes.extend(h for h in signed_hashes if h is not None)
937+
block_mm_infos.extend([None] * len(signed_hashes))
938+
synthetic_block_hashes[block_hash] = [
939+
h for h in signed_hashes if h is not None
940+
]
941+
synthetic_parent_hash = None
942+
logging.debug(
943+
"Split oversized TRT KV block for router indexing: "
944+
f"engine_hash={block_hash}, token_count={token_num_in_block}, "
945+
f"published_blocks={len(signed_hashes)}, stripped_special={stripped_special}"
946+
)
947+
continue
948+
843949
if token_num_in_block < kv_block_size:
844950
partial_block_hashes.add(block_hash)
845951
break
846952
num_block_tokens.append(token_num_in_block)
847953
block_hashes.append(block_hash)
848-
token_ids.extend(int(t["token_id"]) for t in block_tokens)
954+
token_ids.extend(raw_token_ids)
849955

850956
mm_keys = block.get("mm_keys")
851957
if mm_keys:
@@ -867,7 +973,11 @@ def _handle_kv_event(self, event):
867973
else:
868974
block_mm_infos.append(None)
869975

870-
lora_name = data.get("lora_name")
976+
if not block_hashes:
977+
logging.debug(
978+
f"Skipping stored KV event {event_id} with no full router blocks"
979+
)
980+
return
871981

872982
logger.debug(
873983
"Publishing stored KV event: engine_event_id=%s "
@@ -888,7 +998,7 @@ def _handle_kv_event(self, event):
888998
token_ids,
889999
num_block_tokens,
8901000
block_hashes,
891-
parent_hash,
1001+
synthetic_parent_hash,
8921002
block_mm_infos,
8931003
attention_dp_rank,
8941004
lora_name,
@@ -902,7 +1012,7 @@ def _handle_kv_event(self, event):
9021012
token_ids,
9031013
num_block_tokens,
9041014
block_hashes,
905-
parent_hash,
1015+
synthetic_parent_hash,
9061016
block_mm_infos,
9071017
lora_name=lora_name,
9081018
)
@@ -919,6 +1029,10 @@ def _handle_kv_event(self, event):
9191029
block_hash = _to_signed_i64(block_hash)
9201030
if block_hash is None:
9211031
continue
1032+
synthetic_hashes = self.synthetic_block_hashes.pop(block_hash, None)
1033+
if synthetic_hashes:
1034+
removed_block_hashes.extend(synthetic_hashes)
1035+
continue
9221036
if block_hash in self.partial_block_hashes:
9231037
self.partial_block_hashes.remove(block_hash)
9241038
skipped_partial_blocks += 1
@@ -981,38 +1095,47 @@ def check_error_queue(self) -> Optional[Exception]:
9811095

9821096
async def cleanup(self) -> None:
9831097
"""Cleanup threads and resources"""
984-
self._stop_event.set()
985-
# Add timeout to prevent hanging
986-
cleanup_timeout = 5.0 # seconds
987-
988-
if self.publish_stats_thread and self.publish_stats_thread.is_alive():
989-
self.publish_stats_thread.stop()
990-
self.publish_stats_thread.join(timeout=cleanup_timeout)
991-
if self.publish_stats_thread.is_alive():
992-
logging.warning("Stats thread did not stop within timeout")
993-
994-
if (
995-
self.publish_kv_cache_events_thread
996-
and self.publish_kv_cache_events_thread.is_alive()
997-
):
998-
self.publish_kv_cache_events_thread.stop()
999-
self.publish_kv_cache_events_thread.join(timeout=cleanup_timeout)
1000-
if self.publish_kv_cache_events_thread.is_alive():
1001-
logging.warning("KV cache events thread did not stop within timeout")
1002-
1003-
# Shutdown ZMQ publisher if it exists
1004-
if self.zmq_kv_event_publisher:
1005-
self.zmq_kv_event_publisher.shutdown()
1098+
async with self._cleanup_lock:
1099+
if self._cleanup_started:
1100+
return
1101+
self._cleanup_started = True
1102+
1103+
self._stop_event.set()
1104+
# Add timeout to prevent hanging
1105+
cleanup_timeout = 5.0 # seconds
1106+
1107+
thread_warnings = (
1108+
(self.publish_stats_thread, "Stats thread did not stop within timeout"),
1109+
(
1110+
self.publish_kv_cache_events_thread,
1111+
"KV cache events thread did not stop within timeout",
1112+
),
1113+
)
1114+
live_threads = [
1115+
(thread, warning)
1116+
for thread, warning in thread_warnings
1117+
if thread and thread.is_alive()
1118+
]
1119+
for thread, _warning in live_threads:
1120+
thread.stop()
1121+
for thread, warning in live_threads:
1122+
thread.join(timeout=cleanup_timeout)
1123+
if thread.is_alive():
1124+
logging.warning(warning)
1125+
1126+
# Shutdown ZMQ publisher if it exists
1127+
if self.zmq_kv_event_publisher:
1128+
self.zmq_kv_event_publisher.shutdown()
10061129

1007-
# Shutdown FpmDirectPublisher (stops the per-rank serialization tasks
1008-
# and the event-plane publisher task on the Rust side). PyO3 surfaces
1009-
# shutdown failures as PyRuntimeError; narrower catch keeps real
1010-
# programming errors visible.
1011-
if self.fpm_publisher is not None:
1012-
try:
1013-
self.fpm_publisher.shutdown()
1014-
except RuntimeError as e:
1015-
logging.warning(f"FpmDirectPublisher shutdown failed: {e}")
1130+
# Shutdown FpmDirectPublisher (stops the per-rank serialization tasks
1131+
# and the event-plane publisher task on the Rust side). PyO3 surfaces
1132+
# shutdown failures as PyRuntimeError; narrower catch keeps real
1133+
# programming errors visible.
1134+
if self.fpm_publisher is not None:
1135+
try:
1136+
self.fpm_publisher.shutdown()
1137+
except RuntimeError as e:
1138+
logging.warning(f"FpmDirectPublisher shutdown failed: {e}")
10161139

10171140
def update_max_window_size(self, event: dict) -> None:
10181141
if "window_size" in event:

0 commit comments

Comments
 (0)