Skip to content

Commit 5f484d9

Browse files
committed
feat(gms): add TensorRT-LLM host-tier adapter
Signed-off-by: mkhadkevich <mkhadkevich@nvidia.com>
1 parent 2abe24e commit 5f484d9

8 files changed

Lines changed: 1735 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""TRT-LLM engine bridge for gms_kv_ring."""
5+
6+
from gms_kv_ring.engines.trtllm.install_kv_ring import install_for_trtllm
7+
8+
__all__ = ["install_for_trtllm"]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Install gms_kv_ring into a TRT-LLM worker.
5+
6+
Mirrors `install_for_vllm` and `install_for_sglang`. The TRT-LLM
7+
worker calls this once at `register_kv_caches` time with the
8+
per-(layer × kv_factor) descriptors derived from
9+
`KVCacheBlockPool::primaryPtr`."""
10+
11+
from __future__ import annotations
12+
13+
import logging
14+
from typing import Callable, Optional
15+
16+
from gms_kv_ring.engines.handle import GMSKvRing
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
def install_for_trtllm(
22+
*,
23+
engine_id: str,
24+
daemon_socket: str,
25+
layers: "list[dict]",
26+
on_evict: Optional[Callable[[GMSKvRing], None]] = None,
27+
on_hit: Optional[Callable[[GMSKvRing], None]] = None,
28+
evict_ring_capacity: int = 4096,
29+
restore_ring_capacity: int = 4096,
30+
num_counters: int = 512,
31+
) -> GMSKvRing:
32+
"""Build a `GMSKvRing` handle bound to this TRT-LLM worker's
33+
KV pool. Parameter semantics identical to the vLLM/SGLang
34+
install helpers."""
35+
handle = GMSKvRing(
36+
engine_id=engine_id,
37+
daemon_socket=daemon_socket,
38+
layers=layers,
39+
evict_ring_capacity=evict_ring_capacity,
40+
restore_ring_capacity=restore_ring_capacity,
41+
num_counters=num_counters,
42+
)
43+
if on_evict is not None:
44+
on_evict(handle)
45+
if on_hit is not None:
46+
on_hit(handle)
47+
logger.info("gms_kv_ring installed for TRT-LLM engine %r", engine_id)
48+
return handle
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Real-engine smoke for the TensorRT-LLM GMS connector.
5+
6+
Loads a small HF model through TensorRT-LLM's PyTorch backend with our
7+
GMSTrtllmKvCacheScheduler / Worker wired in, generates a few tokens,
8+
and asserts the connector lifecycle hooks fired (register_kv_caches +
9+
build_connector_meta + wait_for_save / start_load_kv).
10+
11+
Run from a Dynamo TensorRT-LLM runtime container or equivalent dev
12+
environment with TensorRT-LLM importable:
13+
14+
cd /path/to/dynamo
15+
export PYTHONPATH=/path/to/dynamo/lib:/path/to/dynamo/lib/gpu_memory_service
16+
export CUDA_VISIBLE_DEVICES=1 # if GPU 0 is busy
17+
export GMS_KVR_REAL_TRTLLM=1
18+
export GMS_KVR_REAL_TRTLLM_MODEL=TinyLlama/TinyLlama-1.1B-Chat-v1.0
19+
export GMS_KVR_REAL_TRTLLM_MEM_FRACTION=0.3
20+
python -m pytest lib/gms_kv_ring/tests/real_engine/test_trtllm_connector.py -v
21+
22+
Env knobs:
23+
GMS_KVR_REAL_TRTLLM=1 required gate (default skip)
24+
GMS_KVR_REAL_TRTLLM_MODEL HF id; default TinyLlama-1.1B
25+
GMS_KVR_REAL_TRTLLM_DAEMON daemon UDS path
26+
GMS_KVR_REAL_TRTLLM_MEM_FRACTION gpu memory budget (default 0.4)
27+
GMS_KVR_REAL_TRTLLM_PROMPT prompt; default short factual
28+
"""
29+
30+
from __future__ import annotations
31+
32+
import asyncio
33+
import os
34+
import tempfile
35+
import threading
36+
import time
37+
38+
import pytest
39+
40+
pytestmark = [
41+
pytest.mark.filterwarnings("ignore::DeprecationWarning"),
42+
pytest.mark.filterwarnings("ignore::UserWarning"),
43+
]
44+
45+
if os.environ.get("GMS_KVR_REAL_TRTLLM") != "1":
46+
pytest.skip(
47+
"Set GMS_KVR_REAL_TRTLLM=1 to run real-engine TRT-LLM tests "
48+
"(needs TensorRT-LLM + GPU + meaningful runtime).",
49+
allow_module_level=True,
50+
)
51+
52+
# pytest's importorskip turns DeprecationWarning-from-import into
53+
# ImportError under strict-warnings configs. Some of TRT-LLM's
54+
# transitive deps (torchao) emit DeprecationWarnings during import.
55+
# Try a plain import inside a warning-quieted block first.
56+
import warnings as _w # noqa: E402
57+
58+
with _w.catch_warnings():
59+
_w.filterwarnings("ignore", category=DeprecationWarning)
60+
_w.filterwarnings("ignore", category=UserWarning)
61+
try:
62+
import tensorrt_llm as trtllm # noqa: F401
63+
except ImportError as _exc:
64+
pytest.skip(
65+
f"tensorrt_llm not importable: {_exc}",
66+
allow_module_level=True,
67+
)
68+
69+
MODEL = os.environ.get(
70+
"GMS_KVR_REAL_TRTLLM_MODEL",
71+
"TinyLlama/TinyLlama-1.1B-Chat-v1.0",
72+
)
73+
DAEMON_SOCK = os.environ.get(
74+
"GMS_KVR_REAL_TRTLLM_DAEMON",
75+
"/tmp/gms-real-trtllm.sock",
76+
)
77+
MEM_FRACTION = float(
78+
os.environ.get(
79+
"GMS_KVR_REAL_TRTLLM_MEM_FRACTION",
80+
"0.4",
81+
)
82+
)
83+
PROMPT = os.environ.get(
84+
"GMS_KVR_REAL_TRTLLM_PROMPT",
85+
"The capital of France is",
86+
)
87+
88+
# Engine id pinned for cross-process consistency.
89+
os.environ.setdefault("GMS_TRTLLM_ENGINE_ID", "trtllm-real-test")
90+
os.environ.setdefault("GMS_TRTLLM_DAEMON_SOCKET", DAEMON_SOCK)
91+
92+
93+
# ---------------------------------------------------------------------
94+
# Daemon fixture
95+
# ---------------------------------------------------------------------
96+
97+
98+
@pytest.fixture(scope="module")
99+
def gms_daemon():
100+
"""Spin up a GMS daemon on the configured UDS for the whole module."""
101+
if os.path.exists(DAEMON_SOCK):
102+
os.unlink(DAEMON_SOCK)
103+
from gms_kv_ring.daemon.server import Daemon
104+
105+
daemon = Daemon(
106+
listen_socket=DAEMON_SOCK,
107+
storage_dir=tempfile.mkdtemp(prefix="gms-real-trtllm-"),
108+
supervise_backend=False,
109+
)
110+
lh = {}
111+
112+
def _run():
113+
loop = asyncio.new_event_loop()
114+
asyncio.set_event_loop(loop)
115+
lh["loop"] = loop
116+
try:
117+
loop.run_until_complete(daemon.serve())
118+
finally:
119+
loop.close()
120+
121+
t = threading.Thread(target=_run, daemon=True)
122+
t.start()
123+
# Wait for the daemon to bind.
124+
deadline = time.monotonic() + 10
125+
while time.monotonic() < deadline and not os.path.exists(DAEMON_SOCK):
126+
time.sleep(0.05)
127+
assert os.path.exists(DAEMON_SOCK), "daemon never bound socket"
128+
yield daemon
129+
try:
130+
if "loop" in lh:
131+
lh["loop"].call_soon_threadsafe(daemon.stop)
132+
t.join(timeout=5)
133+
except Exception: # noqa: BLE001
134+
pass
135+
136+
137+
# ---------------------------------------------------------------------
138+
# Engine fixture
139+
# ---------------------------------------------------------------------
140+
141+
142+
@pytest.fixture(scope="module")
143+
def llm(gms_daemon):
144+
"""Build a TRT-LLM PyTorch-backend LLM with our connector wired in.
145+
146+
The connector is plugged via TRT-LLM's standard ``KvCacheConnectorConfig``
147+
using explicit module + class names (no preset). Module path matches
148+
where ``GMSTrtllmKvCacheScheduler`` and ``GMSTrtllmKvCacheWorker`` are
149+
exported (see ``integrations/trtllm/gms_connector.py``)."""
150+
from tensorrt_llm import LLM
151+
from tensorrt_llm.llmapi.llm_args import KvCacheConfig, KvCacheConnectorConfig
152+
153+
connector_config = KvCacheConnectorConfig(
154+
connector_module=("gpu_memory_service.integrations.trtllm.gms_connector"),
155+
connector_scheduler_class="GMSTrtllmKvCacheScheduler",
156+
connector_worker_class="GMSTrtllmKvCacheWorker",
157+
)
158+
159+
# Attention backend override — on Blackwell (sm_100a) the default
160+
# TRTLLM FMHA kernels are JIT-compiled by NVRTC at startup and the
161+
# search path can lack cuda.h in some runtime containers. FLASHINFER
162+
# is a stable fallback. Override via env knob.
163+
attn_backend = os.environ.get(
164+
"GMS_KVR_REAL_TRTLLM_ATTN_BACKEND",
165+
"FLASHINFER",
166+
)
167+
# Disable chunked prefill — it routes through a different FMHA
168+
# kernel path that has the same NVRTC dep.
169+
obj = LLM(
170+
model=MODEL,
171+
backend="pytorch",
172+
kv_cache_config=KvCacheConfig(
173+
free_gpu_memory_fraction=MEM_FRACTION,
174+
enable_block_reuse=True,
175+
),
176+
kv_connector_config=connector_config,
177+
attn_backend=attn_backend,
178+
enable_chunked_prefill=False,
179+
# Single GPU + eager-only for stability under the smoke harness.
180+
tensor_parallel_size=1,
181+
max_seq_len=256,
182+
)
183+
yield obj
184+
try:
185+
obj.shutdown()
186+
except Exception: # noqa: BLE001
187+
pass
188+
189+
190+
# ---------------------------------------------------------------------
191+
# Tests
192+
# ---------------------------------------------------------------------
193+
194+
195+
def test_smoke_one_generation(llm):
196+
"""Generate a few tokens through the real engine with the GMS
197+
connector wired in. Asserts the model produces non-empty output —
198+
proves the full TRT-LLM ↔ KvCacheConnector ↔ GMS-daemon path
199+
initializes and serves at least one forward pass."""
200+
from tensorrt_llm import SamplingParams
201+
202+
out = llm.generate(
203+
[PROMPT],
204+
SamplingParams(max_tokens=8, temperature=0.0),
205+
)
206+
assert len(out) == 1
207+
text = out[0].outputs[0].text
208+
assert isinstance(text, str)
209+
assert len(text) > 0, "engine produced empty completion"
210+
print(f"\n[trtllm smoke] {PROMPT!r} ->{text!r}")
211+
212+
213+
def test_output_equality_on_cache_hit(llm):
214+
"""Second generation of the same prompt should produce the same
215+
tokens (greedy decoding) AND benefit from the connector's
216+
prefix-cache path. We don't enforce a cache-hit count here (TRT-LLM
217+
doesn't expose connector telemetry via the public API), but we
218+
verify byte-correctness: the connector mediating the KV cache
219+
didn't corrupt the prefix bytes between requests."""
220+
from tensorrt_llm import SamplingParams
221+
222+
sp = SamplingParams(max_tokens=12, temperature=0.0)
223+
o1 = llm.generate([PROMPT], sp)
224+
o2 = llm.generate([PROMPT], sp)
225+
t1 = o1[0].outputs[0].token_ids
226+
t2 = o2[0].outputs[0].token_ids
227+
assert list(t1) == list(t2), (
228+
f"Greedy generation diverged across requests; " f"t1={list(t1)} t2={list(t2)}"
229+
)

0 commit comments

Comments
 (0)