Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
13e0e05
feat(openengine): support image inputs
krishung5 Oct 15, 2026
1748fb7
fix(openengine): harden multimodal media handling
krishung5 Oct 15, 2026
856630a
style(openengine): tighten media comments
krishung5 Oct 15, 2026
423d2b1
fix(openengine): harden decoded media accounting
krishung5 Oct 16, 2026
5d2e3b2
fix(media): prevent DNS rebinding during async fetches
krishung5 Oct 16, 2026
87f270c
feat(openengine): support image inputs
krishung5 Oct 15, 2026
d2cf114
fix(openengine): harden multimodal media handling
krishung5 Oct 15, 2026
1220a29
style(openengine): tighten media comments
krishung5 Oct 15, 2026
42d8a1c
fix(openengine): harden decoded media accounting
krishung5 Oct 16, 2026
14cb645
fix(media): prevent DNS rebinding during async fetches
krishung5 Oct 16, 2026
a3cff30
fix(openengine): address image review feedback
krishung5 Oct 19, 2026
9dc3ab1
Merge remote-tracking branch 'origin/main' into openengine-image-media
krishung5 Oct 19, 2026
dac2ab5
Merge remote-tracking branch 'fork/openengine-image-media' into opene…
krishung5 Oct 19, 2026
7af9e07
Merge remote-tracking branch 'origin/main' into openengine-image-media
krishung5 Oct 19, 2026
cfcc7b8
Merge remote-tracking branch 'origin/main' into openengine-image-media
krishung5 Oct 20, 2026
1e3f7f8
fix: preserve permanent media client errors
krishung5 Oct 20, 2026
220478b
fix(openengine): reserve request IDs before media loading
krishung5 Oct 20, 2026
db9a687
fix(inputs): scope shared media session to event loop
krishung5 Oct 20, 2026
36f4fb2
Merge remote-tracking branch 'origin/main' into openengine-image-media
krishung5 Oct 21, 2026
c0e8499
fix(openengine): validate and cancel multimodal request preparation
krishung5 Oct 21, 2026
c63d619
fix(shared-tensor): avoid global strategy races and reclaim exports
krishung5 Oct 22, 2026
67fb3b7
fix(openengine): own preprocessing exports and isolate media work
krishung5 Oct 22, 2026
bc89fc1
Merge origin/main into openengine-image-media
krishung5 Oct 22, 2026
53e6659
fix: resolve image OpenEngine L0 test failures
krishung5 Oct 23, 2026
750f17d
Merge origin/main into openengine-image-media
krishung5 Oct 26, 2026
af1d00b
fix: remove duplicate test request keyword after merge
krishung5 Oct 26, 2026
91090f5
Merge remote-tracking branch 'origin/main' into openengine-image-media
krishung5 Oct 26, 2026
53b9c63
Merge origin/main into openengine-image-media
krishung5 Oct 26, 2026
1604832
[None][feat] Publish image routing hints in OpenEngine KV events
krishung5 Oct 27, 2026
e637878
Merge image prerequisite into OpenEngine image routing
krishung5 Oct 27, 2026
3c872a4
[None][fix] Gate image routing and keep UUID metadata client-neutral
krishung5 Oct 27, 2026
6c15d02
Merge origin/main into openengine-image-routing
krishung5 Oct 27, 2026
1fffd60
[None][fix] Default multimodal processor media IO preferences
krishung5 Oct 27, 2026
e2b1d28
Merge origin/main after image support landed into image routing
krishung5 Oct 27, 2026
b882976
Merge remote-tracking branch 'origin/main' into openengine-image-routing
krishung5 Oct 28, 2026
d83bef8
[None][test] Fix KV-cache, ADP and mRoPE test fixtures
krishung5 Oct 28, 2026
756422c
[None][fix] Validate image routing UUID pairs before registration
krishung5 Oct 28, 2026
7495cfb
[None][fix] Preserve inference and metadata for unmapped image hints
krishung5 Oct 28, 2026
e6b1db5
Avoid duplicate image routing keys in KV events
krishung5 Oct 28, 2026
d550dcf
Merge remote-tracking branch 'origin/main' into openengine-image-routing
krishung5 Oct 28, 2026
bb4e1f3
Avoid repeated image hint registration during chunked prefill
krishung5 Oct 28, 2026
3b8f84d
Merge remote-tracking branch 'origin/main' into openengine-image-routing
krishung5 Oct 28, 2026
1daf67a
Narrow image routing scope and clarify event metadata docs
krishung5 Oct 29, 2026
3e3cc21
Merge remote-tracking branch 'origin/main' into openengine-image-routing
krishung5 Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cpp/tensorrt_llm/nanobind/batch_manager/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,16 @@ void initBindings(nb::module_& m)
}
return hashes;
})
.def_prop_ro("multimodal_uuids",
[](GenLlmReq& self)
{
std::optional<std::vector<std::optional<std::string>>> uuids = std::nullopt;
if (self.getMultimodalUuids())
{
uuids = *self.getMultimodalUuids().value();
}
return uuids;
})
.def_prop_ro("multimodal_positions",
[](GenLlmReq& self)
{
Expand Down
10 changes: 10 additions & 0 deletions docs/source/features/kvcache.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,16 @@ prompt = TextPrompt(

V2 derives `mm_keys` directly from the cached token sequence. Each entry identifies a continuous multimodal segment within that block: `hash` is the item's digest, and `start_offset` is the segment's first token offset within the item. An item spanning multiple blocks retains the same digest with increasing offsets. Text may separate segments of the same item. Items are processed in prompt order; one item cannot resume after another item has started. The item digest is distinct from `block_hash`, which also depends on the preceding token sequence.

Streaming V2 events can also include an optional `uuid` in each `mm_keys` entry.
It is a bounded routing hint; it does not replace the content-sensitive digest.
Missing, evicted, or conflicting hints omit `uuid`.
For multimodal blocks, the optional `BlockStored.trtllm_image_routing` extension
provides the routing format version and token-ID offset. The extension can accompany
video and audio keys as well as image keys; the OpenEngine capability flag advertises
only the currently validated image-routing support. See the
[OpenEngine image routing contract](source:tensorrt_llm/grpc/openengine/README.md#image-routing-metadata)
for capability requirements and consumer handling.


### Enable Offloading to Host Memory

Expand Down
10 changes: 10 additions & 0 deletions tensorrt_llm/_torch/pyexecutor/kv_cache/kv_cache_manager_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6126,6 +6126,16 @@ def _augment_tokens_for_block_reuse(
"reuse."
)

if chunk_start == 0 and isinstance(self.event_manager, StreamingKVCacheEventManager):
uuids = req.multimodal_uuids
if uuids is not None:
uuid_pairs = [
(_hash_to_digest(digest), uuid)
for digest, uuid in zip(req.multimodal_hashes, uuids, strict=True)
if uuid is not None
]
self.event_manager.remember_multimodal_uuids(uuid_pairs)

# Multimodal path: materialize a Python-int list (digest bytes get spliced in below),
# which flows through the per-element binding fallback. tokens may be a zero-copy numpy
# int32 view (get_tokens_view) — use tolist() so elements are Python ints, not np.int32.
Expand Down
67 changes: 56 additions & 11 deletions tensorrt_llm/_torch/pyexecutor/kv_cache_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
import time
import traceback
from abc import ABC, abstractmethod
from collections import deque
from collections import OrderedDict, deque
from itertools import count
from queue import Queue
from typing import Any, Iterator, Optional
from typing import Any, Iterable, Iterator, Optional

import msgspec
import zmq
Expand Down Expand Up @@ -80,6 +80,14 @@ class MultimodalKey(

hash: str
start_offset: int
uuid: str | None = None


class ImageRoutingMetadata(msgspec.Struct, frozen=True, gc=False):
"""Versioned digest-first image normalization metadata for KV routers."""

version: int
token_id_offset: int


class BlockStored(KVCacheWireEvent):
Expand All @@ -99,6 +107,7 @@ class BlockStored(KVCacheWireEvent):
locality: str | None = None
# Aligned one-for-one with block_hashes when multimodal decoding is enabled.
mm_keys: list[list[MultimodalKey]] | None = None
trtllm_image_routing: ImageRoutingMetadata | None = None


class BlockRemoved(KVCacheWireEvent):
Expand Down Expand Up @@ -551,6 +560,10 @@ def __init__(
raise ValueError("mm_token_id_offset must be non-negative")
self._block_size = block_size
self._include_mm_keys = mm_token_id_offset is not None
self._mm_token_id_offset = mm_token_id_offset
self._uuid_capacity = max_entries
self._uuids: OrderedDict[bytes, str | None] = OrderedDict()
self._uuid_lock = threading.Lock()
self._event_sink = kv_cache_manager_v2_runtime.StreamingEventSink(
max_entries=max_entries,
mm_token_id_offset=mm_token_id_offset,
Expand All @@ -560,13 +573,42 @@ def __init__(
def event_sink(self) -> kv_cache_manager_v2_runtime.StreamingEventSink:
return self._event_sink

def remember_multimodal_uuids(self, items: Iterable[tuple[bytes, str]]) -> None:
"""Retain bounded routing hints without changing native cache keys."""
for digest, uuid in items:
if len(uuid.encode("utf-8")) > 256:
continue
with self._uuid_lock:
if digest in self._uuids and self._uuids[digest] != uuid:
# Keep conflicting hints ambiguous until their bounded entry is evicted.
self._uuids[digest] = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Once a digest is set to None, this branch keeps it None forever while the digest is live: a later registration with the original UUID compares unequal to None, and move_to_end below refreshes recency on every call, so a frequently requested image never reaches the eviction that the README promises will clear the ambiguity ("until its entry is evicted"). test_streaming_uuid_hints_are_bounded_and_do_not_replace_content_hashes pins this as intended, so it is a documentation mismatch rather than a crash, but it means the routers lose hints for exactly the hot images where routing pays off, until a restart. Either let agreeing registrations re-set the value (for example keep a small per-digest candidate set, or skip move_to_end when the entry is already None so it ages out), or change the README sentence to say ambiguity persists as long as the digest keeps being registered.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Clarified in 1daf67a: registrations refresh LRU recency even for ambiguous entries, so frequently registered digests can remain ambiguous until publisher restart. Later agreeing registrations do not clear a conflict. This documents the existing conservative behavior.

else:
self._uuids[digest] = uuid
self._uuids.move_to_end(digest)
while len(self._uuids) > self._uuid_capacity:
self._uuids.popitem(last=False)

def _multimodal_key(self, key: tuple) -> MultimodalKey:
digest = bytes(key[0])
with self._uuid_lock:
uuid = self._uuids.get(digest)
return MultimodalKey(hash=digest.hex(), start_offset=key[1], uuid=uuid)

def set_target_life_cycle(self, life_cycle_id: int) -> None:
self._event_sink.set_target_life_cycle(life_cycle_id)

def drain_events(self) -> list[BlockStored | BlockRemoved | AllBlocksCleared]:
result: list[BlockStored | BlockRemoved | AllBlocksCleared] = []
for event in self._event_sink.drain_iteration_events():
if isinstance(event, kv_cache_manager_v2_runtime.StreamingBlockStoredData):
mm_keys = (
[
[self._multimodal_key(key) for key in block_keys]
for block_keys in event.mm_keys
]
if self._include_mm_keys
else None
)
result.append(
BlockStored(
block_hashes=list(event.block_hashes),
Expand All @@ -576,15 +618,14 @@ def drain_events(self) -> list[BlockStored | BlockRemoved | AllBlocksCleared]:
lora_id=event.lora_id,
medium="GPU",
lora_name=None,
mm_keys=(
[
[
MultimodalKey(hash=bytes(key[0]).hex(), start_offset=key[1])
for key in block_keys
]
for block_keys in event.mm_keys
]
if self._include_mm_keys
mm_keys=mm_keys,
trtllm_image_routing=(
ImageRoutingMetadata(
version=1, token_id_offset=self._mm_token_id_offset
)
if mm_keys is not None
and self._mm_token_id_offset is not None
and any(mm_keys)
Comment thread
brnguyen2 marked this conversation as resolved.
else None
),
)
Expand Down Expand Up @@ -646,6 +687,10 @@ def start(self) -> None:
"""Bind the publisher's sockets and start its background thread."""
self._publisher.start()

def remember_multimodal_uuids(self, items: Iterable[tuple[bytes, str]]) -> None:
"""Associate native MM digests with optional request identities."""
self._event_source.remember_multimodal_uuids(items)

def get_source_descriptor(self) -> dict[str, str | int] | None:
"""Describe a live ZMQ publisher for OpenEngine node discovery."""
publisher = self._publisher
Expand Down
35 changes: 34 additions & 1 deletion tensorrt_llm/grpc/openengine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ The `Inference.Generate` RPC loads the selected model through TensorRT-LLM's PyT

Clients must continuously consume the response stream. If response delivery remains stalled for 30 seconds, the server aborts the engine request and terminates the stream with a retryable overload error.

Features without a faithful TensorRT-LLM mapping return `UNIMPLEMENTED`: prefix-cache bypass, LoRA lifecycle selection, multimodal media, explicit-token or all-vocabulary log-probability selection, nonzero prompt-logprob offsets, per-request grammar-backend selection, and priority metadata. Attention-DP rank hints are validated and used for first-turn placement. With conversation affinity on, the first hint establishes the conversation's rank, and later turns retain that rank even if their hints differ; the request must carry a stable conversation ID. `ServerInfo.extra.trtllm_supports_dp_rank_targeting` advertises this support. The AutoDeploy backend is rejected at startup until it supports request cancellation. `Control` implements `GetServerInfo`, `GetModelInfo`, `GetLoad`, `GetKvEventSources`, `Health` and `Abort`; its LoRA lifecycle and `SubscribeKvEvents` RPCs return `UNIMPLEMENTED`.
Features without a faithful TensorRT-LLM mapping return `UNIMPLEMENTED`: prefix-cache bypass, LoRA lifecycle selection, explicit-token or all-vocabulary log-probability selection, nonzero prompt-logprob offsets, per-request grammar-backend selection, and priority metadata. Attention-DP rank hints are validated and used for first-turn placement. With conversation affinity on, the first hint establishes the conversation's rank, and later turns retain that rank even if their hints differ; the request must carry a stable conversation ID. `ServerInfo.extra.trtllm_supports_dp_rank_targeting` advertises this support. The AutoDeploy backend is rejected at startup until it supports request cancellation. `Control` implements `GetServerInfo`, `GetModelInfo`, `GetLoad`, `GetKvEventSources`, `Health` and `Abort`; its LoRA lifecycle and `SubscribeKvEvents` RPCs return `UNIMPLEMENTED`.

### Disaggregated serving

Expand Down Expand Up @@ -118,6 +118,39 @@ Enable load reporting independently with `--grpc --grpc-protocol openengine --op

For attention DP, each publisher binds `base_port + global_rank`. When event streaming is enabled, OpenEngine collects the live publisher descriptors at executor startup and advertises every rank, including remote nodes. Those hostnames and ports must be reachable from the subscriber. A missing publisher fails startup; an unavailable discovery snapshot fails the RPC rather than synthesizing socket addresses. PP and CP streaming remain unsupported. The extra placement collection is OpenEngine-only; native HTTP serving and OpenEngine with events disabled do not enable it.

### Image routing metadata

`GetModelInfo.extra.trtllm_image_routing_version = 1` advertises image routing
metadata for aggregated Qwen3-VL with KV cache manager V2 explicitly enabled,
block reuse enabled, and ZMQ event publishing. Image passthrough separately
requires `supports_multimodal` and `extra.trtllm_supported_modalities` to advertise
image support. Set `use_kv_cache_manager_v2=True` explicitly: `"auto"` is
excluded from capability discovery even if the engine resolves it to V2.
Video, audio, and disaggregated image routing are not advertised.

For image blocks, `BlockStored.trtllm_image_routing` contains
`version` and `token_id_offset` (the vocabulary size). The existing
`BlockStored.mm_keys` contains the keys, aligned with the stored blocks. Each key
retains its native content `hash` and `start_offset` and gains an optional `uuid`.
Keys are serialized only in `mm_keys`. The publisher emits this extension for
all multimodal keys, including video/audio keys; the name identifies the current
image-routing contract, not a modality filter. Only image routing has been
validated and advertised by OpenEngine. Native image tokens start with a digest
and continue with IDs above `token_id_offset`; a router can normalize those
tokens using identities it sent in `MediaItem.uuid`. Native block hashes, parent chains, and removals retain
their existing meaning. Cache hashing continues to include image content.

The publisher retains at most `max_entries` digest-to-UUID associations and skips
UUIDs longer than 256 UTF-8 bytes. Conflicting hints mark a digest ambiguous until
its entry is evicted or the publisher restarts, without failing inference.
Every registration refreshes the entry's LRU position, including ambiguous entries,
so a frequently registered digest can remain ambiguous for the publisher's lifetime.
An agreeing registration does not restore a hint after a conflict. Missing, evicted,
or ambiguous hints omit the UUID while retaining the image-routing metadata. Consumers
must ignore unmapped image stores while continuing to process removals. Routing
identities must refer to stable media content. This extension does not change the
OpenEngine schema.

### Node-local discovery

OpenEngine exposes optional, versioned `ServerInfo.extra.trtllm_node` metadata without changing the protocol schema. It describes a shared engine incarnation (`engine_id`), local hostname (`node_id`), whether this is the leader, node count, KV block size, local publishing DP ranks, and the global rank-to-node ownership map. The engine ID matches `GetServerInfo.instance_id` on the serving ingress and node metadata listeners. Consumers can use this information to partition telemetry collection without introducing routing-framework-specific server options.
Expand Down
16 changes: 16 additions & 0 deletions tensorrt_llm/grpc/openengine/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ def _loaded_model_type(llm: Any) -> str:
return resolve_top_level_model_type(llm.input_processor.config)


def supports_image_routing(llm: Any, modalities: Collection[str]) -> bool:
"""Whether the loaded model exposes the validated image KV-event contract."""
processor_config = getattr(getattr(llm, "input_processor", None), "config", None)
if "image" not in modalities or resolve_top_level_model_type(processor_config) != "qwen3_vl":
return False
cache = getattr(llm.args, "kv_cache_config", None)
events = getattr(cache, "kv_events_config", None)
return (
Comment thread
brnguyen2 marked this conversation as resolved.
getattr(llm.args, "cache_transceiver_config", None) is None
and getattr(cache, "use_kv_cache_manager_v2", False) is True
and getattr(cache, "enable_block_reuse", False) is True
and getattr(events, "enable_kv_cache_events", False) is True
and getattr(events, "publisher", None) == "zmq"
)


def resolve_media_capabilities(
llm: Any, implemented_modalities: Collection[str]
) -> MediaCapabilities:
Expand Down
4 changes: 3 additions & 1 deletion tensorrt_llm/grpc/openengine/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
openengine_pb2_grpc,
server_pb2,
)
from .capabilities import supported_guides
from .capabilities import supported_guides, supports_image_routing
from .coordinator import CoordinationError, FrontendClient, InvalidRequestIdError
from .errors import AbortFailedError
from .kv_events import data_parallel_size, events_config
Expand Down Expand Up @@ -296,6 +296,8 @@ async def GetModelInfo(
reasoning_parser=str(self._llm.args.reasoning_parser or ""),
)
info.extra.update({"trtllm_supported_modalities": sorted(media_modalities)})
if supports_image_routing(self._llm, media_modalities):
info.extra.update({"trtllm_image_routing_version": 1})

max_seq_len = _positive_int(self._llm.args.max_seq_len)
if max_seq_len is not None:
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_lists/test-db/l0_a10.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ l0_a10:
# false advertisement by construction. The image case uses Qwen3-VL-2B.
- unittest/grpc/openengine/test_capability_conformance.py
- unittest/grpc/openengine/test_multimodal_conformance.py
- unittest/grpc/openengine/test_image_routing_conformance.py
- unittest/disaggregated/test_coordinator_e2e.py
- unittest/disaggregated/test_coordinator_worker.py
- unittest/disaggregated/test_agent_multi_backends.py
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_lists/test-db/l0_cpu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ l0_cpu:
- unittest/_torch/test_shared_tensor_cpu.py
- unittest/grpc/openengine/test_frontend_lifecycle.py
- unittest/grpc/openengine/test_control.py
- unittest/grpc/openengine/test_image_routing.py
- unittest/grpc/openengine/test_multimodal.py
- unittest/grpc/openengine/test_node.py
- unittest/grpc/openengine/test_openengine.py
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def make_stub_manager(
# for a one-model draft, which reads D prompt tokens past the target's end.
mgr.reuse_match_backoff = reuse_match_backoff
mgr.vocab_size = 32000
mgr.event_manager = None
mgr.conversation_manager = None
mgr.kv_connector_manager = None
mgr.kv_cache_map = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
def _make_manager(vocab_size: int):
manager = KVCacheManagerV2.__new__(KVCacheManagerV2)
manager.vocab_size = vocab_size
manager.event_manager = None
return manager


Expand Down
Loading
Loading