Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions docs/source/commands/trtllm-serve/render-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ On a CPU-only host, `trtllm-render` loads only the tokenizer, configuration and
trtllm-render --model <checkpoint> --port 8100
```

In a container with no GPU driver, `trtllm-render` loads the CUDA toolkit's `libcuda.so` stub from `$CUDA_HOME` or
`/usr/local/cuda*` so that `tensorrt_llm` can be imported. `python -m tensorrt_llm.serve.render` does not do this.

Give it the same model, tokenizer, chat template and parser options as the workers it fronts. Both processes publish
a rendering fingerprint on `GET /server_info`. Equal `render_fingerprint.digest` values mean the ids are valid for
that worker, so compare them once when a worker is added.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,12 +716,12 @@ def get_build_state_options():
'trtllm-bench=tensorrt_llm.commands.bench:main',
'trtllm-serve=tensorrt_llm.commands.serve:main',
'trtllm-eval=tensorrt_llm.commands.eval:main',
'trtllm-render=tensorrt_llm.serve.render.__main__:main'
],
},
scripts=[
'tensorrt_llm/llmapi/trtllm-llmapi-launch',
'tensorrt_llm/llmapi/_llmapi_process_guard.py',
'tensorrt_llm/serve/render/trtllm-render',
],
extras_require={
"devel": devel_deps + grpc_smg_deps,
Expand Down
76 changes: 76 additions & 0 deletions tensorrt_llm/serve/render/trtllm-render
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""``trtllm-render`` entry point.

``import tensorrt_llm`` links ``libcuda.so.1``, which a container with no GPU does not have. When it cannot
be loaded, this loads the CUDA toolkit stub in its place so the renderer starts on a CPU-only host. It is a
script outside the package so that importing ``tensorrt_llm`` anywhere else is unchanged.
"""

from __future__ import annotations

import ctypes
import glob
import os
import re
import sys
import warnings


def _version(path: str) -> list:
return [int(part) for part in re.findall(r"\d+", path)]


def _stub_candidates() -> list:
"""Toolkit stubs, most preferred first: ``$CUDA_HOME``, ``/usr/local/cuda``, then the newest versioned."""
roots = [os.environ["CUDA_HOME"]] if os.environ.get("CUDA_HOME") else []
roots.append("/usr/local/cuda")
roots += sorted(glob.glob("/usr/local/cuda-*"), key=_version, reverse=True)
candidates, seen = [], set()
for root in roots:
for pattern in ("lib64/stubs/libcuda.so", "targets/*/lib/stubs/libcuda.so"):
for stub in sorted(glob.glob(os.path.join(root, pattern))):
if os.path.realpath(stub) not in seen:
seen.add(os.path.realpath(stub))
candidates.append(stub)
return candidates


def _load_cuda_stub_if_needed() -> str | None:
"""Load a toolkit stub when ``libcuda.so.1`` is missing; return its path, or None if not needed."""
try:
ctypes.CDLL("libcuda.so.1")
return None
except OSError:
pass
failures = []
for stub in _stub_candidates():
try:
ctypes.CDLL(stub, mode=ctypes.RTLD_GLOBAL)
except OSError as error:
failures.append(f"{stub}: {error}")
continue
warnings.filterwarnings("ignore", message="CUDA initialization:", category=UserWarning)
return stub
sys.exit(
"trtllm-render: libcuda.so.1 not found and no usable CUDA toolkit stub under $CUDA_HOME or "
"/usr/local/cuda*; install the CUDA toolkit stubs or expose a GPU driver"
+ "".join(f"\n tried {failure}" for failure in failures)
)


def main() -> None:
stub = _load_cuda_stub_if_needed()
from tensorrt_llm.logger import logger
from tensorrt_llm.serve.render.__main__ import main as render_main
Comment thread
hchings marked this conversation as resolved.
Resolved

if stub:
logger.info(
f"trtllm-render: no GPU driver found; running CPU-only with the CUDA stub {stub}"
)
render_main()


if __name__ == "__main__":
main()
80 changes: 80 additions & 0 deletions tests/unittest/llmapi/apps/test_render_standalone.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,22 @@

from __future__ import annotations

import ctypes
import fnmatch
import glob
import importlib.machinery
import importlib.util
import json
import os
import shutil
import socket
import subprocess
import sys
import textwrap
import time
import urllib.error
import urllib.request
import warnings

import pytest

Expand Down Expand Up @@ -176,6 +183,79 @@ def test_a_bad_model_exits_non_zero_with_a_message(self, tmp_path) -> None:
class TestCpuOnlyStartup:
"""The renderer builds with no GPU, no weights and no server or executor object."""

def test_the_launcher_loads_a_cuda_stub_only_when_the_driver_is_missing(
self, monkeypatch
) -> None:
path = shutil.which("trtllm-render")
assert path is not None, "trtllm-render is not installed"
# The installed launcher starts the renderer end to end.
result = subprocess.run(
[sys.executable, path, "--help"], capture_output=True, text=True, timeout=600
)
assert result.returncode == 0, result.stderr[-2000:]
assert "--model" in result.stdout

loader = importlib.machinery.SourceFileLoader("trtllm_render_launcher", path)
launcher = importlib.util.module_from_spec(
importlib.util.spec_from_loader(loader.name, loader)
)
loader.exec_module(launcher)

stub = "{}/lib64/stubs/libcuda.so"
installed = ["/usr/local/cuda-12.4", "/usr/local/cuda-13.0"] + [
stub.format(root)
for root in (
"/usr/local/cuda",
"/usr/local/cuda-12.4",
"/usr/local/cuda-13.0",
"/opt/cuda",
)
]
driver_present = True
unloadable = set()
loaded = []

def cdll(name, mode=0):
loaded.append((name, mode))
if (name == "libcuda.so.1" and not driver_present) or name in unloadable:
raise OSError(name)

def fake_glob(pattern):
depth = len(pattern.split("/"))
return [
p for p in installed if len(p.split("/")) == depth and fnmatch.fnmatch(p, pattern)
]

def load():
loaded.clear()
return launcher._load_cuda_stub_if_needed()

monkeypatch.setattr(ctypes, "CDLL", cdll)
monkeypatch.setattr(glob, "glob", fake_glob)
monkeypatch.setattr(os.path, "realpath", lambda p: p)
monkeypatch.delenv("CUDA_HOME", raising=False)

with warnings.catch_warnings():
assert load() is None
assert loaded == [("libcuda.so.1", 0)]

driver_present = False
assert load() == stub.format("/usr/local/cuda")
assert loaded[-1] == (stub.format("/usr/local/cuda"), ctypes.RTLD_GLOBAL)

monkeypatch.setenv("CUDA_HOME", "/opt/cuda")
load()
assert loaded[-1][0] == stub.format("/opt/cuda")

monkeypatch.delenv("CUDA_HOME")
unloadable.add(stub.format("/usr/local/cuda"))
load()
assert loaded[-1][0] == stub.format("/usr/local/cuda-13.0")

unloadable.update(p for p in installed if p.endswith("libcuda.so"))
with pytest.raises(SystemExit, match=r"tried /usr/local/cuda/lib64/stubs/libcuda.so"):
load()

def test_building_the_app_initializes_no_cuda_and_imports_no_server(self, checkpoint) -> None:
code = textwrap.dedent(
"""
Expand Down
1 change: 1 addition & 0 deletions tests/unittest/scripts/test_serve_schema_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def test_wheel_contains_serving_schemas(tmp_path: Path, missing_schema: bool) ->
"tensorrt_llm/version.py",
"tensorrt_llm/llmapi/trtllm-llmapi-launch",
"tensorrt_llm/llmapi/_llmapi_process_guard.py",
"tensorrt_llm/serve/render/trtllm-render",
]
files += [path.name for path in _REPO_ROOT.glob("requirements*.txt")]
for name in files:
Expand Down
Loading