2222import asyncio
2323import concurrent .futures
2424import logging
25+ import struct
2526import threading
2627import time
2728import traceback
3233from typing import Any , Awaitable , Callable , Dict , Optional , Union
3334
3435import msgspec
36+ import xxhash
3537import zmq
3638from prometheus_client import CollectorRegistry
3739
3840from 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
4148logging .basicConfig (level = logging .DEBUG )
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+
110177class ZmqKvEventPublisher :
111178 """
112179 Pure Python ZMQ PUBLISHER for TensorRT-LLM KV events.
@@ -431,8 +498,14 @@ def __init__(
431498 # A set to store the block hash of partial block (i.e. block containing less than kv_block_size tokens) hashes.
432499 # It is used to prevent sending remove event to kv router since partial blocks are not stored.
433500 self .partial_block_hashes : set [int ] = set ()
501+ # Maps oversized TRT engine block hashes to the synthetic router-visible
502+ # sequence hashes we publish for those blocks, so later remove events
503+ # evict the same hashes from the router indexer.
504+ self .synthetic_block_hashes : dict [int , list [int ]] = {}
434505 self .error_queue : Queue = Queue ()
435506 self ._stop_event = threading .Event ()
507+ self ._cleanup_lock = asyncio .Lock ()
508+ self ._cleanup_started = False
436509 # Track the last engine event_id to assert consecutive event IDs from the engine
437510 self ._last_engine_event_id : Optional [int ] = None
438511
@@ -815,26 +888,59 @@ def _handle_kv_event(self, event):
815888 block_mm_infos : list [dict | None ] = []
816889 kv_block_size = self .kv_block_size
817890 partial_block_hashes = self .partial_block_hashes
891+ synthetic_block_hashes = self .synthetic_block_hashes
892+ lora_name = data .get ("lora_name" )
893+ synthetic_parent_hash = parent_hash
818894 for block in data ["blocks" ]:
819895 block_tokens = block ["tokens" ]
820896 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
897+ raw_token_ids = [int (t ["token_id" ]) for t in block_tokens ]
826898 block_hash = _to_signed_i64 (block ["block_hash" ])
827899 if block_hash is None :
828900 logging .warning (
829901 f"Skipping block with None hash containing { token_num_in_block } tokens"
830902 )
831903 continue
904+
905+ if token_num_in_block > kv_block_size :
906+ (
907+ full_tokens ,
908+ synthetic_num_block_tokens ,
909+ synthetic_hashes ,
910+ stripped_special ,
911+ ) = _split_oversized_trt_block_for_router (
912+ raw_token_ids , kv_block_size , lora_name
913+ )
914+ if not synthetic_hashes :
915+ logging .debug (
916+ "Skipping oversized TRT KV block with no full router blocks: "
917+ f"token_count={ token_num_in_block } , kv_block_size={ kv_block_size } "
918+ )
919+ partial_block_hashes .add (block_hash )
920+ continue
921+
922+ signed_hashes = [_to_signed_i64 (h ) for h in synthetic_hashes ]
923+ token_ids .extend (full_tokens )
924+ num_block_tokens .extend (synthetic_num_block_tokens )
925+ block_hashes .extend (h for h in signed_hashes if h is not None )
926+ block_mm_infos .extend ([None ] * len (signed_hashes ))
927+ synthetic_block_hashes [block_hash ] = [
928+ h for h in signed_hashes if h is not None
929+ ]
930+ synthetic_parent_hash = None
931+ logging .debug (
932+ "Split oversized TRT KV block for router indexing: "
933+ f"engine_hash={ block_hash } , token_count={ token_num_in_block } , "
934+ f"published_blocks={ len (signed_hashes )} , stripped_special={ stripped_special } "
935+ )
936+ continue
937+
832938 if token_num_in_block < kv_block_size :
833939 partial_block_hashes .add (block_hash )
834940 break
835941 num_block_tokens .append (token_num_in_block )
836942 block_hashes .append (block_hash )
837- token_ids .extend (int ( t [ "token_id" ]) for t in block_tokens )
943+ token_ids .extend (raw_token_ids )
838944
839945 mm_keys = block .get ("mm_keys" )
840946 if mm_keys :
@@ -856,14 +962,18 @@ def _handle_kv_event(self, event):
856962 else :
857963 block_mm_infos .append (None )
858964
859- lora_name = data .get ("lora_name" )
965+ if not block_hashes :
966+ logging .debug (
967+ f"Skipping stored KV event { event_id } with no full router blocks"
968+ )
969+ return
860970
861971 # Get attention_dp_rank from event (TRT-LLM includes this in KVCacheEvent)
862972 # Default to 0 for backwards compatibility with older TRT-LLM versions
863973 attention_dp_rank = event .get ("attention_dp_rank" , 0 )
864974
865975 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 } "
976+ 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 } "
867977 )
868978 # Publish to ZMQ if consolidator is enabled, otherwise publish to NATS
869979 # Note: event_id is managed internally by the publisher (monotonic counter per dp_rank)
@@ -873,7 +983,7 @@ def _handle_kv_event(self, event):
873983 token_ids ,
874984 num_block_tokens ,
875985 block_hashes ,
876- parent_hash ,
986+ synthetic_parent_hash ,
877987 block_mm_infos ,
878988 attention_dp_rank ,
879989 lora_name ,
@@ -887,7 +997,7 @@ def _handle_kv_event(self, event):
887997 token_ids ,
888998 num_block_tokens ,
889999 block_hashes ,
890- parent_hash ,
1000+ synthetic_parent_hash ,
8911001 block_mm_infos ,
8921002 lora_name = lora_name ,
8931003 )
@@ -903,6 +1013,10 @@ def _handle_kv_event(self, event):
9031013 block_hash = _to_signed_i64 (block_hash )
9041014 if block_hash is None :
9051015 continue
1016+ synthetic_hashes = self .synthetic_block_hashes .pop (block_hash , None )
1017+ if synthetic_hashes :
1018+ removed_block_hashes .extend (synthetic_hashes )
1019+ continue
9061020 if block_hash in self .partial_block_hashes :
9071021 logging .debug (
9081022 f"Skipping removing block hash { block_hash } since it is a partial block"
@@ -962,38 +1076,47 @@ def check_error_queue(self) -> Optional[Exception]:
9621076
9631077 async def cleanup (self ) -> None :
9641078 """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 ()
1079+ async with self ._cleanup_lock :
1080+ if self ._cleanup_started :
1081+ return
1082+ self ._cleanup_started = True
1083+
1084+ self ._stop_event .set ()
1085+ # Add timeout to prevent hanging
1086+ cleanup_timeout = 5.0 # seconds
1087+
1088+ thread_warnings = (
1089+ (self .publish_stats_thread , "Stats thread did not stop within timeout" ),
1090+ (
1091+ self .publish_kv_cache_events_thread ,
1092+ "KV cache events thread did not stop within timeout" ,
1093+ ),
1094+ )
1095+ live_threads = [
1096+ (thread , warning )
1097+ for thread , warning in thread_warnings
1098+ if thread and thread .is_alive ()
1099+ ]
1100+ for thread , _warning in live_threads :
1101+ thread .stop ()
1102+ for thread , warning in live_threads :
1103+ thread .join (timeout = cleanup_timeout )
1104+ if thread .is_alive ():
1105+ logging .warning (warning )
1106+
1107+ # Shutdown ZMQ publisher if it exists
1108+ if self .zmq_kv_event_publisher :
1109+ self .zmq_kv_event_publisher .shutdown ()
9871110
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 } " )
1111+ # Shutdown FpmDirectPublisher (stops the per-rank serialization tasks
1112+ # and the event-plane publisher task on the Rust side). PyO3 surfaces
1113+ # shutdown failures as PyRuntimeError; narrower catch keeps real
1114+ # programming errors visible.
1115+ if self .fpm_publisher is not None :
1116+ try :
1117+ self .fpm_publisher .shutdown ()
1118+ except RuntimeError as e :
1119+ logging .warning (f"FpmDirectPublisher shutdown failed: { e } " )
9971120
9981121 def update_max_window_size (self , event : dict ) -> None :
9991122 if "window_size" in event :
0 commit comments