Skip to content

Commit 07a28c2

Browse files
authored
Merge branch 'main' into spolisetty/tri-669-psirt-triton-inference-server-vertex-ai-integration-access
2 parents 35eb942 + 36f4b17 commit 07a28c2

4 files changed

Lines changed: 196 additions & 25 deletions

File tree

python/openai/openai_frontend/engine/utils/triton.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,34 @@ def _get_guided_json_from_tool(
617617
return None
618618

619619

620+
def _validate_lora_path_trtllm(repo_path: str, lora_path: str, lora_name: str):
621+
if os.path.isabs(lora_path):
622+
raise ValueError(
623+
f"LoRA path '{lora_path}' for '{lora_name}' must be a relative path inside its model repository"
624+
)
625+
626+
# NOTE: Error messages should never contain the real/absolute paths.
627+
realpath_repo = os.path.realpath(repo_path)
628+
realpath_lora = os.path.realpath(os.path.join(realpath_repo, lora_path))
629+
# Always check if the LoRA path is inside the model repository before checking its existence.
630+
if os.path.commonpath([realpath_repo, realpath_lora]) != realpath_repo:
631+
raise ValueError(
632+
f"LoRA path '{lora_path}' for '{lora_name}' must be inside its model repository"
633+
)
634+
if not os.path.exists(realpath_lora):
635+
raise ServerError(
636+
f"LoRA directory '{lora_path}' not found for '{lora_name}' in its model repository"
637+
)
638+
639+
# Check if the files exist
640+
for lora_file in ["model.lora_weights.npy", "model.lora_config.npy"]:
641+
lora_file_path = os.path.join(realpath_lora, lora_file)
642+
if not os.path.exists(lora_file_path):
643+
raise ServerError(
644+
f"LoRA file '{lora_file}' not found for '{lora_name}' at path: {lora_file_path}"
645+
)
646+
647+
620648
def _parse_lora_configs(
621649
model_repository: str | list[str], model_name: str, model_version: int, backend: str
622650
) -> None | List[tuple[str, str]]:
@@ -683,25 +711,10 @@ def _parse_lora_configs(
683711
with open(lora_config_path, "r") as f:
684712
lora_config = json.load(f)
685713
for lora_name, lora_path in lora_config.items():
686-
print(f"backend: {backend}")
687714
if backend == "vllm":
688715
lora_configs.append(TritonLoraConfig(name=lora_name))
689716
else:
690-
lora_weights_path = os.path.join(
691-
lora_path, "model.lora_weights.npy"
692-
)
693-
lora_config_path = os.path.join(
694-
lora_path, "model.lora_config.npy"
695-
)
696-
if not os.path.exists(lora_weights_path):
697-
raise ServerError(
698-
f"LoRA weights file not found for '{lora_name}' at path: {lora_weights_path}"
699-
)
700-
if not os.path.exists(lora_config_path):
701-
raise ServerError(
702-
f"LoRA config file not found for '{lora_name}' at path: {lora_config_path}"
703-
)
704-
717+
_validate_lora_path_trtllm(repo_path, lora_path, lora_name)
705718
lora_configs.append(
706719
TritonLoraConfig(
707720
name=lora_name, path=lora_path, task_id=lora_task_id

python/openai/tests/test_lora.py

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1+
# Copyright 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
#
33
# Redistribution and use in source and binary forms, with or without
44
# modification, are permitted provided that the following conditions
@@ -35,10 +35,22 @@
3535
from openai_frontend.engine.utils.triton import (
3636
_parse_lora_configs as parse_lora_configs,
3737
)
38+
from openai_frontend.engine.utils.triton import (
39+
_validate_lora_path_trtllm as validate_lora_path_trtllm,
40+
)
3841

3942
from .utils import OpenAIServer
4043

4144

45+
def is_vllm_installed():
46+
try:
47+
import vllm as _
48+
49+
return True
50+
except ImportError:
51+
return False
52+
53+
4254
@pytest.mark.parametrize(
4355
"model_repository,model_name,expect_error",
4456
[
@@ -74,13 +86,62 @@ def test_parse_lora_configs(model_repository: str, model_name: str, expect_error
7486
)
7587

7688

77-
def is_vllm_installed():
89+
@pytest.mark.skipif(
90+
is_vllm_installed(),
91+
reason="VLLM backend does not validate LoRA paths",
92+
)
93+
@pytest.mark.parametrize(
94+
"lora_path,expect_error,error_message",
95+
[
96+
# Valid relative path inside repo (requires .npy files to exist at runtime).
97+
("tensorrt_llm_bls/1/luotuo-lora-7b-0.1-weights", False, None),
98+
("tensorrt_llm_bls/1/Japanese-Alpaca-LoRA-7b-v0-weights", False, None),
99+
# Absolute path not allowed.
100+
(
101+
os.path.join(
102+
os.path.abspath(os.curdir),
103+
"tests/tensorrtllm_models",
104+
"tensorrt_llm_bls/1/luotuo-lora-7b-0.1-weights",
105+
),
106+
True,
107+
f"must be a relative path inside its model repository",
108+
),
109+
("/etc/passwd", True, "must be a relative path inside its model repository"),
110+
# Path outside repo (traversal).
111+
("tensorrt_llm_bls/1//../1/luotuo-lora-7b-0.1-weights", False, None),
112+
("../outside/lora", True, "must be inside its model repository"),
113+
("subdir/../../etc/passwd", True, "must be inside its model repository"),
114+
# LoRA directory not found.
115+
("tensorrt_llm_bls/10", True, "LoRA directory 'tensorrt_llm_bls/10' not found"),
116+
(
117+
"tensorrt_llm_bls/1/non_exist",
118+
True,
119+
"LoRA directory 'tensorrt_llm_bls/1/non_exist' not found",
120+
),
121+
# LoRA file not found.
122+
("tensorrt_llm_bls/1", True, "LoRA file 'model.lora_weights.npy' not found"),
123+
],
124+
)
125+
def test_validate_lora_path_trtllm(
126+
lora_path: str,
127+
expect_error: bool,
128+
error_message: str,
129+
):
130+
lora_name = ""
131+
repo_path = "tests/tensorrtllm_models"
78132
try:
79-
import vllm as _
80-
81-
return True
82-
except ImportError:
83-
return False
133+
validate_lora_path_trtllm(repo_path, lora_path, lora_name)
134+
except Exception as e:
135+
if not expect_error:
136+
raise pytest.fail(
137+
f"repo_path='{repo_path}' raised exception unexpectedly: {e}"
138+
)
139+
assert error_message in str(e)
140+
else:
141+
if expect_error:
142+
raise pytest.fail(
143+
f"lora_path='{repo_path}' did not raise exception as expected."
144+
)
84145

85146

86147
class LoRATest(unittest.TestCase):

qa/L0_input_validation/input_validation_test.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
#
44
# Redistribution and use in source and binary forms, with or without
55
# modification, are permitted provided that the following conditions
@@ -288,5 +288,76 @@ def inference_helper(model_name, batch_size=1):
288288
inference_helper(model_name="plan_zero_1_float32_int32", batch_size=8)
289289

290290

291+
class ModelNameValidationTest(unittest.TestCase):
292+
INVALID_TRAVERSAL_NAMES = [
293+
"../etc",
294+
"a/../b",
295+
"../../etc/passwd",
296+
"../../../../etc",
297+
"model/..",
298+
"..",
299+
"/etc/passwd",
300+
"model/subdir",
301+
"model/",
302+
" ..",
303+
".. ",
304+
]
305+
306+
def test_model_name_invalid_load(self):
307+
client = tritongrpcclient.InferenceServerClient("localhost:8001")
308+
for model_name in self.INVALID_TRAVERSAL_NAMES:
309+
with self.assertRaises(InferenceServerException) as cm:
310+
client.load_model(model_name)
311+
self.assertIn(
312+
"model name must not contain path traversal characters",
313+
str(cm.exception),
314+
f"Expected traversal rejection for model name: {model_name!r}",
315+
)
316+
317+
def test_model_name_empty_load(self):
318+
client = tritongrpcclient.InferenceServerClient("localhost:8001")
319+
with self.assertRaises(InferenceServerException) as cm:
320+
client.load_model("")
321+
self.assertIn("model name must not be empty", str(cm.exception))
322+
323+
def test_model_name_whitespace_only_load(self):
324+
client = tritongrpcclient.InferenceServerClient("localhost:8001")
325+
whitespace_names = [" ", " ", "\t", "\n", "\r", "\f", "\v", " \t \n "]
326+
for model_name in whitespace_names:
327+
with self.assertRaises(InferenceServerException) as cm:
328+
client.load_model(model_name)
329+
self.assertIn(
330+
"model name must not contain only whitespace",
331+
str(cm.exception),
332+
f"Expected whitespace-only rejection for model name: {model_name!r}",
333+
)
334+
335+
def test_model_name_invalid_unload(self):
336+
# Unload should not trigger traversal check
337+
client = tritongrpcclient.InferenceServerClient("localhost:8001")
338+
for model_name in self.INVALID_TRAVERSAL_NAMES:
339+
try:
340+
client.unload_model(model_name)
341+
except InferenceServerException as e:
342+
self.assertNotIn(
343+
"model name must not contain path traversal characters",
344+
str(e),
345+
f"Unload should not trigger traversal rejection for model name: {model_name!r}",
346+
)
347+
348+
def test_model_name_valid(self):
349+
"""Verify that a syntactically valid model name is not rejected by
350+
the traversal check -- it should fail with a model not found error instead."""
351+
client = tritongrpcclient.InferenceServerClient("localhost:8001")
352+
with self.assertRaises(InferenceServerException) as cm:
353+
client.load_model("nonexistent_model")
354+
self.assertNotIn(
355+
"path traversal characters",
356+
str(cm.exception),
357+
"Valid model name should not trigger path traversal rejection",
358+
)
359+
self.assertIn("failed to poll from model repository", str(cm.exception))
360+
361+
291362
if __name__ == "__main__":
292363
unittest.main()

qa/L0_input_validation/test.sh

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/bin/bash
2-
# Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
#
44
# Redistribution and use in source and binary forms, with or without
55
# modification, are permitted provided that the following conditions
@@ -175,6 +175,32 @@ if [ $? -ne 0 ]; then
175175
fi
176176
set -e
177177

178+
# Model name validation test
179+
rm -rf test_models ; mkdir -p test_models
180+
SERVER_LOG="./model_name_validation_server.log"
181+
CLIENT_LOG="./model_name_validation_client.log"
182+
SERVER_ARGS="--model-repository=`pwd`/test_models --model-control-mode=explicit --log-verbose=1"
183+
run_server
184+
if [ "$SERVER_PID" == "0" ]; then
185+
echo -e "\n***\n*** Failed to start $SERVER\n***"
186+
cat $SERVER_LOG
187+
exit 1
188+
fi
189+
190+
set +e
191+
python3 -m pytest -s --junitxml="model_name_validation.report.xml" $TEST_PY::ModelNameValidationTest >> $CLIENT_LOG 2>&1
192+
193+
if [ $? -ne 0 ]; then
194+
cat $CLIENT_LOG
195+
cat $SERVER_LOG
196+
echo -e "\n***\n*** input_validation_test.py::ModelNameValidationTest FAILED. \n***"
197+
RET=1
198+
fi
199+
set -e
200+
201+
kill $SERVER_PID
202+
wait $SERVER_PID
203+
178204
if [ $RET -eq 0 ]; then
179205
echo -e "\n***\n*** Input Validation Test Passed\n***"
180206
else

0 commit comments

Comments
 (0)