Skip to content

Commit 25e9868

Browse files
committed
gms: integrate persistent KV with trtllm v2
Add TRT-LLM V2 GMS setup, VMM IPC KV integration, V2 lease/reclaim hooks, failover sleep/wake handling, startup defaults, and tests. Router placement and local rootfs MPI changes are kept for later branches.
1 parent c33626d commit 25e9868

13 files changed

Lines changed: 1266 additions & 94 deletions

File tree

components/src/dynamo/trtllm/__main__.py

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

9+
from dynamo.trtllm.startup import configure_gms_openmpi_defaults
10+
11+
configure_gms_openmpi_defaults()
12+
913
from dynamo.trtllm.main import main
1014

1115
if __name__ == "__main__":

components/src/dynamo/trtllm/main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,30 @@ async def worker():
102102
# The drain callback waits for in-flight requests to finish so that GPU
103103
# memory is not freed while transfers are active (issue #7319).
104104
engine_holder: list = []
105+
publisher_holder: list = []
105106
drain_callback = None
106107
if config.disaggregation_mode == DisaggregationMode.PREFILL:
107108
drain_callback = _make_drain_callback(engine_holder)
108109

110+
async def cleanup_engine() -> None:
111+
if publisher_holder:
112+
await publisher_holder[0].cleanup()
113+
logging.info("TRT-LLM publisher cleanup complete")
114+
115+
if not engine_holder:
116+
logging.info("TRT-LLM engine not initialized; skipping cleanup")
117+
return
118+
119+
await engine_holder[0].cleanup()
120+
logging.info("TRT-LLM engine cleanup complete")
121+
109122
install_signal_handlers(
110123
loop,
111124
runtime,
112125
shutdown_endpoints,
113126
shutdown_event,
114127
drain_callback=drain_callback,
128+
cleanup_callback=cleanup_engine,
115129
)
116130

117131
logging.info(f"Initializing the worker with config: {config}")
@@ -121,6 +135,7 @@ async def worker():
121135
shutdown_event,
122136
shutdown_endpoints,
123137
engine_holder=engine_holder,
138+
publisher_holder=publisher_holder,
124139
)
125140

126141

components/src/dynamo/trtllm/publisher.py

Lines changed: 166 additions & 42 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
@@ -31,12 +32,19 @@
3132
from queue import Queue
3233
from typing import Any, Awaitable, Callable, Dict, Optional, Union
3334

35+
import msgpack
3436
import msgspec
37+
import xxhash
3538
import zmq
3639
from prometheus_client import CollectorRegistry
3740

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

4149
logging.basicConfig(level=logging.DEBUG)
4250

@@ -107,6 +115,66 @@ def _to_signed_i64(value: int | None) -> int | None:
107115
return value
108116

109117

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

@@ -815,26 +889,59 @@ def _handle_kv_event(self, event):
815889
block_mm_infos: list[dict | None] = []
816890
kv_block_size = self.kv_block_size
817891
partial_block_hashes = self.partial_block_hashes
892+
synthetic_block_hashes = self.synthetic_block_hashes
893+
lora_name = data.get("lora_name")
894+
synthetic_parent_hash = parent_hash
818895
for block in data["blocks"]:
819896
block_tokens = block["tokens"]
820897
token_num_in_block = len(block_tokens)
821-
if token_num_in_block > kv_block_size:
822-
logging.error(
823-
f"Block contains {token_num_in_block} tokens, which is greater than kv_block_size {kv_block_size}"
824-
)
825-
return
898+
raw_token_ids = [int(t["token_id"]) for t in block_tokens]
826899
block_hash = _to_signed_i64(block["block_hash"])
827900
if block_hash is None:
828901
logging.warning(
829902
f"Skipping block with None hash containing {token_num_in_block} tokens"
830903
)
831904
continue
905+
906+
if token_num_in_block > kv_block_size:
907+
(
908+
full_tokens,
909+
synthetic_num_block_tokens,
910+
synthetic_hashes,
911+
stripped_special,
912+
) = _split_oversized_trt_block_for_router(
913+
raw_token_ids, kv_block_size, lora_name
914+
)
915+
if not synthetic_hashes:
916+
logging.debug(
917+
"Skipping oversized TRT KV block with no full router blocks: "
918+
f"token_count={token_num_in_block}, kv_block_size={kv_block_size}"
919+
)
920+
partial_block_hashes.add(block_hash)
921+
continue
922+
923+
signed_hashes = [_to_signed_i64(h) for h in synthetic_hashes]
924+
token_ids.extend(full_tokens)
925+
num_block_tokens.extend(synthetic_num_block_tokens)
926+
block_hashes.extend(h for h in signed_hashes if h is not None)
927+
block_mm_infos.extend([None] * len(signed_hashes))
928+
synthetic_block_hashes[block_hash] = [
929+
h for h in signed_hashes if h is not None
930+
]
931+
synthetic_parent_hash = None
932+
logging.debug(
933+
"Split oversized TRT KV block for router indexing: "
934+
f"engine_hash={block_hash}, token_count={token_num_in_block}, "
935+
f"published_blocks={len(signed_hashes)}, stripped_special={stripped_special}"
936+
)
937+
continue
938+
832939
if token_num_in_block < kv_block_size:
833940
partial_block_hashes.add(block_hash)
834941
break
835942
num_block_tokens.append(token_num_in_block)
836943
block_hashes.append(block_hash)
837-
token_ids.extend(int(t["token_id"]) for t in block_tokens)
944+
token_ids.extend(raw_token_ids)
838945

839946
mm_keys = block.get("mm_keys")
840947
if mm_keys:
@@ -856,14 +963,18 @@ def _handle_kv_event(self, event):
856963
else:
857964
block_mm_infos.append(None)
858965

859-
lora_name = data.get("lora_name")
966+
if not block_hashes:
967+
logging.debug(
968+
f"Skipping stored KV event {event_id} with no full router blocks"
969+
)
970+
return
860971

861972
# Get attention_dp_rank from event (TRT-LLM includes this in KVCacheEvent)
862973
# Default to 0 for backwards compatibility with older TRT-LLM versions
863974
attention_dp_rank = event.get("attention_dp_rank", 0)
864975

865976
logging.debug(
866-
f"publish stored event: engine_event_id: {event_id}, attention_dp_rank: {attention_dp_rank}, token_ids: {token_ids}, num_block_tokens: {num_block_tokens}, block_hashes: {block_hashes}, lora_name: {lora_name}, parent_hash: {parent_hash}"
977+
f"publish stored event: engine_event_id: {event_id}, attention_dp_rank: {attention_dp_rank}, token_ids: {token_ids}, num_block_tokens: {num_block_tokens}, block_hashes: {block_hashes}, lora_name: {lora_name}, parent_hash: {synthetic_parent_hash}"
867978
)
868979
# Publish to ZMQ if consolidator is enabled, otherwise publish to NATS
869980
# Note: event_id is managed internally by the publisher (monotonic counter per dp_rank)
@@ -873,7 +984,7 @@ def _handle_kv_event(self, event):
873984
token_ids,
874985
num_block_tokens,
875986
block_hashes,
876-
parent_hash,
987+
synthetic_parent_hash,
877988
block_mm_infos,
878989
attention_dp_rank,
879990
lora_name,
@@ -887,7 +998,7 @@ def _handle_kv_event(self, event):
887998
token_ids,
888999
num_block_tokens,
8891000
block_hashes,
890-
parent_hash,
1001+
synthetic_parent_hash,
8911002
block_mm_infos,
8921003
lora_name=lora_name,
8931004
)
@@ -903,6 +1014,10 @@ def _handle_kv_event(self, event):
9031014
block_hash = _to_signed_i64(block_hash)
9041015
if block_hash is None:
9051016
continue
1017+
synthetic_hashes = self.synthetic_block_hashes.pop(block_hash, None)
1018+
if synthetic_hashes:
1019+
removed_block_hashes.extend(synthetic_hashes)
1020+
continue
9061021
if block_hash in self.partial_block_hashes:
9071022
logging.debug(
9081023
f"Skipping removing block hash {block_hash} since it is a partial block"
@@ -962,38 +1077,47 @@ def check_error_queue(self) -> Optional[Exception]:
9621077

9631078
async def cleanup(self) -> None:
9641079
"""Cleanup threads and resources"""
965-
self._stop_event.set()
966-
# Add timeout to prevent hanging
967-
cleanup_timeout = 5.0 # seconds
968-
969-
if self.publish_stats_thread and self.publish_stats_thread.is_alive():
970-
self.publish_stats_thread.stop()
971-
self.publish_stats_thread.join(timeout=cleanup_timeout)
972-
if self.publish_stats_thread.is_alive():
973-
logging.warning("Stats thread did not stop within timeout")
974-
975-
if (
976-
self.publish_kv_cache_events_thread
977-
and self.publish_kv_cache_events_thread.is_alive()
978-
):
979-
self.publish_kv_cache_events_thread.stop()
980-
self.publish_kv_cache_events_thread.join(timeout=cleanup_timeout)
981-
if self.publish_kv_cache_events_thread.is_alive():
982-
logging.warning("KV cache events thread did not stop within timeout")
983-
984-
# Shutdown ZMQ publisher if it exists
985-
if self.zmq_kv_event_publisher:
986-
self.zmq_kv_event_publisher.shutdown()
1080+
async with self._cleanup_lock:
1081+
if self._cleanup_started:
1082+
return
1083+
self._cleanup_started = True
1084+
1085+
self._stop_event.set()
1086+
# Add timeout to prevent hanging
1087+
cleanup_timeout = 5.0 # seconds
1088+
1089+
thread_warnings = (
1090+
(self.publish_stats_thread, "Stats thread did not stop within timeout"),
1091+
(
1092+
self.publish_kv_cache_events_thread,
1093+
"KV cache events thread did not stop within timeout",
1094+
),
1095+
)
1096+
live_threads = [
1097+
(thread, warning)
1098+
for thread, warning in thread_warnings
1099+
if thread and thread.is_alive()
1100+
]
1101+
for thread, _warning in live_threads:
1102+
thread.stop()
1103+
for thread, warning in live_threads:
1104+
thread.join(timeout=cleanup_timeout)
1105+
if thread.is_alive():
1106+
logging.warning(warning)
1107+
1108+
# Shutdown ZMQ publisher if it exists
1109+
if self.zmq_kv_event_publisher:
1110+
self.zmq_kv_event_publisher.shutdown()
9871111

988-
# Shutdown FpmDirectPublisher (stops the per-rank serialization tasks
989-
# and the event-plane publisher task on the Rust side). PyO3 surfaces
990-
# shutdown failures as PyRuntimeError; narrower catch keeps real
991-
# programming errors visible.
992-
if self.fpm_publisher is not None:
993-
try:
994-
self.fpm_publisher.shutdown()
995-
except RuntimeError as e:
996-
logging.warning(f"FpmDirectPublisher shutdown failed: {e}")
1112+
# Shutdown FpmDirectPublisher (stops the per-rank serialization tasks
1113+
# and the event-plane publisher task on the Rust side). PyO3 surfaces
1114+
# shutdown failures as PyRuntimeError; narrower catch keeps real
1115+
# programming errors visible.
1116+
if self.fpm_publisher is not None:
1117+
try:
1118+
self.fpm_publisher.shutdown()
1119+
except RuntimeError as e:
1120+
logging.warning(f"FpmDirectPublisher shutdown failed: {e}")
9971121

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

0 commit comments

Comments
 (0)