Skip to content

Commit 5d4c31b

Browse files
authored
feat: Support multi-LoRA for TensorRT-LLM backend in OpenAI-compatibl…(#8569) (#8575)
1 parent 069046b commit 5d4c31b

10 files changed

Lines changed: 263 additions & 63 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ cprofile
1616
# Test exclusions
1717
qa/L0_openai/openai
1818
tensorrtllm_models
19+
tensorrtllm_mistral_models/
1920
custom_tokenizer

python/openai/README.md

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,8 @@ pytest -v tests/
241241
### LoRA Adapters
242242

243243
If the command line argument `--lora-separator=<separator_string>` is provided
244-
when starting the OpenAI Frontend, a vLLM LoRA adaptor listed on the
245-
`multi_lora.json` may be selected by appending the LoRA name to the model name,
244+
when starting the OpenAI Frontend, a LoRA adaptor listed in `multi_lora.json`
245+
may be selected by appending the LoRA name to the model name,
246246
separated by the LoRA separator, on the inference request in
247247
`<model_name><separator_string><lora_name>` format.
248248

@@ -297,9 +297,56 @@ the same `<model_name><separator_string><lora_name>` format for each LoRA
297297
adapter listed on the `multi_lora.json`. Note: The LoRA name inclusion is
298298
limited to locally stored models, inference requests are not limited though.
299299

300+
#### vLLM
300301
See the
301302
[vLLM documentation](https://github.com/triton-inference-server/vllm_backend/blob/main/docs/llama_multi_lora_tutorial.md)
302-
on how to serve a model with LoRA adapters.
303+
on how to serve a vLLM model with LoRA adapters.
304+
305+
#### TensorRT-LLM
306+
Similarly, see [TensorRT-LLM document](https://github.com/triton-inference-server/tensorrtllm_backend/blob/main/docs/lora.md)
307+
on how to prepare LoRA-enabled TensorRT-LLM engines and generate LoRA tensors.
308+
The path of LoRA adapter in `multi_lora.json` is the directory of
309+
`model.lora_config.npy` and `model.lora_weights.npy` tensors.
310+
311+
<details>
312+
<summary>For example</summary>
313+
314+
model repository
315+
```
316+
inflight_batcher_llm
317+
├── postprocessing
318+
| ├── 1
319+
| | └── model.py
320+
| └── config.pbtxt
321+
├── preprocessing
322+
| ├── 1
323+
| | └── model.py
324+
| └── config.pbtxt
325+
├── tensorrt_llm
326+
| ├── 1
327+
| | └── model.py
328+
| └── config.pbtxt
329+
└── tensorrt_llm_bls
330+
├── 1
331+
| ├── Japanese-Alpaca-LoRA-7b-v0-weights
332+
| | ├── model.lora_config.npy
333+
| | └── model.lora_weights.npy
334+
| ├── luotuo-lora-7b-0.1-weights
335+
| | ├── model.lora_config.npy
336+
| | └── model.lora_weights.npy
337+
| ├── model.py
338+
| └── multi_lora.json
339+
└── config.pbtxt
340+
```
341+
342+
multi_lora.json
343+
```
344+
{
345+
"doll": "inflight_batcher_llm/tensorrt_llm_bls/1/luotuo-lora-7b-0.1-weights",
346+
"sheep": "inflight_batcher_llm/tensorrt_llm_bls/1/Japanese-Alpaca-LoRA-7b-v0-weights"
347+
}
348+
```
349+
</details>
303350

304351
### Embedding Models
305352
Currently, OpenAI-Compatible Frontend supports loading embedding models and embeddings endpoints via vLLM backend. Check [vLLM supported models](https://docs.vllm.ai/en/latest/models/supported_models.html#embedding) for all supported embedding models from vLLM.

python/openai/openai_frontend/engine/triton_engine.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from engine.utils.tool_call_parsers import ToolCallParser, ToolParserManager
5454
from engine.utils.triton import (
5555
RequestKind,
56+
TritonLoraConfig,
5657
_create_trtllm_embedding_request,
5758
_create_trtllm_generate_request,
5859
_create_vllm_embedding_request,
@@ -61,7 +62,7 @@
6162
_get_openai_completion_format_logprobs_from_vllm_response,
6263
_get_output,
6364
_get_usage_from_response,
64-
_get_vllm_lora_names,
65+
_parse_lora_configs,
6566
_StreamingUsageAccumulator,
6667
_validate_triton_responses_non_streaming,
6768
)
@@ -107,7 +108,7 @@ class TritonModelMetadata:
107108
# Tokenizers used for chat templates
108109
tokenizer: Optional[Any]
109110
# LoRA names supported by the backend
110-
lora_names: Optional[List[str]]
111+
lora_configs: Optional[List[TritonLoraConfig]]
111112
# Name of the input tensor enabling "echo" parameter in /v1/completions endpoint
112113
echo_tensor_name: Optional[str]
113114
# Time that model was loaded by Triton
@@ -160,11 +161,11 @@ def models(self) -> List[Model]:
160161
if (
161162
self.lora_separator is not None
162163
and len(self.lora_separator) > 0
163-
and metadata.lora_names is not None
164+
and metadata.lora_configs is not None
164165
):
165-
for lora_name in metadata.lora_names:
166+
for lora_config in metadata.lora_configs:
166167
model_names.append(
167-
f"{metadata.name}{self.lora_separator}{lora_name}"
168+
f"{metadata.name}{self.lora_separator}{lora_config.name}"
168169
)
169170

170171
for model_name in model_names:
@@ -210,7 +211,7 @@ async def chat(
210211
metadata.model,
211212
prompt,
212213
request,
213-
lora_name,
214+
self._get_lora_config(model_name, lora_name),
214215
metadata.echo_tensor_name,
215216
self.default_max_tokens,
216217
)
@@ -348,7 +349,7 @@ async def completion(
348349
metadata.model,
349350
request.prompt,
350351
request,
351-
lora_name,
352+
self._get_lora_config(model_name, lora_name),
352353
metadata.echo_tensor_name,
353354
self.default_max_tokens,
354355
)
@@ -505,11 +506,12 @@ def _get_model_metadata(self) -> Dict[str, TritonModelMetadata]:
505506
backend = "ensemble"
506507
print(f"Found model: {name=}, {backend=}")
507508

508-
lora_names = None
509-
if self.backend == "vllm" or backend == "vllm":
510-
lora_names = _get_vllm_lora_names(
511-
self.server.options.model_repository, name, model.version
512-
)
509+
lora_configs = _parse_lora_configs(
510+
self.server.options.model_repository,
511+
name,
512+
model.version,
513+
backend if self.backend is None else self.backend,
514+
)
513515

514516
echo_tensor_name = None
515517
for input in model.config()["input"]:
@@ -525,7 +527,7 @@ def _get_model_metadata(self) -> Dict[str, TritonModelMetadata]:
525527
backend=backend,
526528
model=model,
527529
tokenizer=self.tokenizer,
528-
lora_names=lora_names,
530+
lora_configs=lora_configs,
529531
echo_tensor_name=echo_tensor_name,
530532
create_time=self.create_time,
531533
inference_request_converter=self._determine_request_converter(
@@ -807,9 +809,10 @@ def _validate_chat_request(
807809
)
808810

809811
if (
810-
metadata.lora_names is not None
812+
metadata.lora_configs is not None
811813
and lora_name is not None
812-
and lora_name not in metadata.lora_names
814+
and lora_name
815+
not in [lora_config.name for lora_config in metadata.lora_configs]
813816
):
814817
raise ClientError(f"Unknown LoRA: {lora_name}; for model: {request.model}")
815818

@@ -970,9 +973,10 @@ def _validate_completion_request(
970973
)
971974

972975
if (
973-
metadata.lora_names is not None
976+
metadata.lora_configs is not None
974977
and lora_name is not None
975-
and lora_name not in metadata.lora_names
978+
and lora_name
979+
not in [lora_config.name for lora_config in metadata.lora_configs]
976980
):
977981
raise ClientError(f"Unknown LoRA: {lora_name}; for model: {request.model}")
978982

@@ -1081,3 +1085,14 @@ def _get_named_function_name(
10811085
tool_choice_required_function_name = None
10821086

10831087
return tool_choice_function_name or tool_choice_required_function_name
1088+
1089+
def _get_lora_config(
1090+
self, model_name: str, lora_name: Optional[str]
1091+
) -> TritonLoraConfig:
1092+
model_metadata = self.model_metadata.get(model_name)
1093+
if lora_name is None or model_metadata.lora_configs is None:
1094+
return None
1095+
for lora_config in model_metadata.lora_configs:
1096+
if lora_config.name == lora_name:
1097+
return lora_config
1098+
raise ClientError(f"Unknown LoRA: {lora_name}; for model: {model_name}")

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

Lines changed: 88 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import os
2929
import re
3030
import sys
31+
import traceback
3132
from dataclasses import asdict, dataclass, field
3233
from enum import Enum
3334
from pathlib import Path
@@ -56,11 +57,21 @@ class RequestKind(Enum):
5657
EMBEDDING = 2
5758

5859

60+
@dataclass
61+
class TritonLoraConfig:
62+
name: str
63+
64+
# Unique fields for TensorRT-LLM backend
65+
task_id: Optional[int] = None
66+
path: Optional[str] = None
67+
is_registered: Optional[bool] = False
68+
69+
5970
def _create_vllm_generate_request(
6071
model,
6172
prompt,
6273
request: CreateChatCompletionRequest | CreateCompletionRequest,
63-
lora_name: str | None,
74+
lora_config: TritonLoraConfig | None,
6475
echo_tensor_name: str | None,
6576
default_max_tokens: int,
6677
):
@@ -135,8 +146,8 @@ def _create_vllm_generate_request(
135146
request_logprobs = True
136147
inputs["return_logprobs"] = np.bool_([request_logprobs])
137148

138-
if lora_name is not None:
139-
sampling_parameters["lora_name"] = lora_name
149+
if lora_config is not None:
150+
sampling_parameters["lora_name"] = lora_config.name
140151

141152
guided_json = _get_guided_json_from_tool(request)
142153
if guided_json is not None:
@@ -167,15 +178,10 @@ def _create_trtllm_generate_request(
167178
model,
168179
prompt,
169180
request: CreateChatCompletionRequest | CreateCompletionRequest,
170-
lora_name: str | None,
181+
lora_config: TritonLoraConfig | None,
171182
echo_tensor_name: str | None,
172183
default_max_tokens: int,
173184
):
174-
if lora_name is not None:
175-
raise ClientError(
176-
"LoRA selection is currently not supported for TRT-LLM backend"
177-
)
178-
179185
inputs = {}
180186
inputs["text_input"] = [[prompt]]
181187
inputs["stream"] = np.bool_([[request.stream]])
@@ -221,6 +227,21 @@ def _create_trtllm_generate_request(
221227
inputs["guided_decoding_guide_type"] = [["json_schema"]]
222228
inputs["guided_decoding_guide"] = [[guided_json]]
223229

230+
if lora_config is not None:
231+
# To perform inference with a specific LoRA for the first time `lora_task_id` `lora_weights` and `lora_config` must all be given.
232+
# The LoRA will be cached, so that subsequent requests for the same task only require `lora_task_id`.
233+
inputs["lora_task_id"] = np.uint64([[lora_config.task_id]])
234+
if not lora_config.is_registered:
235+
lora_weights_data = np.load(
236+
os.path.join(lora_config.path, "model.lora_weights.npy")
237+
)
238+
lora_config_data = np.load(
239+
os.path.join(lora_config.path, "model.lora_config.npy")
240+
)
241+
inputs["lora_weights"] = lora_weights_data
242+
inputs["lora_config"] = lora_config_data
243+
lora_config.is_registered = True
244+
224245
inputs["return_num_input_tokens"] = np.bool_([[True]])
225246
inputs["return_num_output_tokens"] = np.bool_([[True]])
226247
return model.create_request(inputs=inputs)
@@ -594,9 +615,9 @@ def _get_guided_json_from_tool(
594615
return None
595616

596617

597-
def _get_vllm_lora_names(
598-
model_repository: str | list[str], model_name: str, model_version: int
599-
) -> None | List[str]:
618+
def _parse_lora_configs(
619+
model_repository: str | list[str], model_name: str, model_version: int, backend: str
620+
) -> None | List[tuple[str, str]]:
600621
if (
601622
len(model_name) == 0
602623
or model_name.isspace()
@@ -606,7 +627,9 @@ def _get_vllm_lora_names(
606627
raise ValueError(
607628
f"Invalid model name: '{model_name}'. Model names must be valid file-system-path segment names."
608629
)
609-
lora_names = []
630+
631+
lora_configs = []
632+
lora_task_id = 1
610633
repo_paths = model_repository
611634
if isinstance(repo_paths, str):
612635
repo_paths = [repo_paths]
@@ -618,6 +641,7 @@ def _get_vllm_lora_names(
618641
raise ValueError(
619642
f"Invalid model name: '{model_name}'. Model names must be valid file-system-path segment names."
620643
)
644+
621645
model_path = os.path.normpath(model_path)
622646
if not os.path.isdir(model_path):
623647
# Cloud path?
@@ -632,26 +656,60 @@ def _get_vllm_lora_names(
632656
# Model directory is malformed?
633657
return None
634658
version_path = os.path.join(model_path, str(model_version))
635-
is_lora_enabled = False
636-
model_file_path = os.path.join(version_path, "model.json")
637-
try:
638-
with open(model_file_path, "r") as f:
639-
config = json.load(f)
640-
if "enable_lora" in config:
641-
# The value could be a string or a bool.
642-
is_lora_enabled = str(config["enable_lora"]).lower() == "true"
643-
except Exception:
644-
# Model directory or model.json is malformed?
645-
return None
646-
if is_lora_enabled != True:
647-
continue
648659
lora_config_path = os.path.join(version_path, "multi_lora.json")
660+
661+
if backend == "vllm":
662+
is_lora_enabled = False
663+
model_file_path = os.path.join(version_path, "model.json")
664+
try:
665+
with open(model_file_path, "r") as f:
666+
config = json.load(f)
667+
if "enable_lora" in config:
668+
# The value could be a string or a bool.
669+
is_lora_enabled = str(config["enable_lora"]).lower() == "true"
670+
except Exception:
671+
# Model directory or model.json is malformed?
672+
return None
673+
if is_lora_enabled != True:
674+
continue
675+
else:
676+
# TRT-LLM backend does not use model.json
677+
if not os.path.exists(lora_config_path):
678+
continue
679+
649680
try:
650681
with open(lora_config_path, "r") as f:
651682
lora_config = json.load(f)
652-
for lora_name in lora_config.keys():
653-
lora_names.append(lora_name)
654-
except Exception:
683+
for lora_name, lora_path in lora_config.items():
684+
print(f"backend: {backend}")
685+
if backend == "vllm":
686+
lora_configs.append(TritonLoraConfig(name=lora_name))
687+
else:
688+
lora_weights_path = os.path.join(
689+
lora_path, "model.lora_weights.npy"
690+
)
691+
lora_config_path = os.path.join(
692+
lora_path, "model.lora_config.npy"
693+
)
694+
if not os.path.exists(lora_weights_path):
695+
raise ServerError(
696+
f"LoRA weights file not found for '{lora_name}' at path: {lora_weights_path}"
697+
)
698+
if not os.path.exists(lora_config_path):
699+
raise ServerError(
700+
f"LoRA config file not found for '{lora_name}' at path: {lora_config_path}"
701+
)
702+
703+
lora_configs.append(
704+
TritonLoraConfig(
705+
name=lora_name, path=lora_path, task_id=lora_task_id
706+
)
707+
)
708+
lora_task_id += 1
709+
except ServerError as e:
710+
raise e
711+
except Exception as e:
655712
# LoRA is enabled but its list is not provided or malformed?
713+
print(traceback.format_exc())
656714
return None
657-
return lora_names
715+
return lora_configs

0 commit comments

Comments
 (0)