| # Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # adapted from https://github.com/Kipok/NeMo-Skills/blob/v0.1/nemo_skills/inference/server/serve_trt.py | |
| import json | |
| import logging | |
| import sys | |
| import json | |
| from pathlib import Path | |
| from argparse import ArgumentParser | |
| import numpy as np | |
| import torch | |
| import tensorrt_llm | |
| from flask import Flask, jsonify, request | |
| from flask_restful import Api, Resource | |
| from tensorrt_llm.runtime import ModelRunnerCpp | |
| from mpi4py import MPI | |
| from transformers import AutoTokenizer | |
| class TritonServerGenerate(Resource): | |
| def __init__(self, model): | |
| self.model = model | |
| self.comm = MPI.COMM_WORLD | |
| def generate( | |
| self, | |
| prompts, | |
| max_new_tokens, | |
| temperature, | |
| top_k, | |
| top_p, | |
| repetition_penalty, | |
| random_seed, | |
| stop_words_list, | |
| max_attention_window_size=None | |
| ): | |
| output = self.model.forward( | |
| prompts, | |
| max_output_token=max_new_tokens, | |
| top_k=top_k, | |
| top_p=top_p, | |
| temperature=temperature, | |
| repetition_penalty=repetition_penalty, | |
| random_seed=random_seed, | |
| stop_words_list=stop_words_list, | |
| max_attention_window_size=max_attention_window_size, | |
| ) | |
| return output | |
| def put(self): | |
| logging.info("request IP: " + str(request.remote_addr)) | |
| logging.info(json.dumps(request.get_json())) | |
| input_request = request.get_json() | |
| tokens_to_generate = input_request.get("tokens_to_generate", 64) | |
| temperature = input_request.get("temperature", 1.0) | |
| top_k = input_request.get("top_k", 0) | |
| top_p = input_request.get("top_p", 1.0) | |
| repetition_penalty = input_request.get("repetition_penalty", 1.2) | |
| stop_words_list = input_request.get("stop_words_list") | |
| max_attention_window_size = input_request.get("max_attention_window_size") | |
| random_seed = input_request.get("random_seed", 0) | |
| prompts = input_request["prompts"] | |
| data = dict( | |
| prompts=prompts, | |
| max_new_tokens=tokens_to_generate, | |
| temperature=temperature, | |
| top_k=top_k, | |
| top_p=top_p, | |
| repetition_penalty=repetition_penalty, | |
| random_seed=random_seed, | |
| stop_words_list=stop_words_list, | |
| max_attention_window_size=max_attention_window_size, | |
| ) | |
| self.comm.Barrier() | |
| data = self.comm.bcast(data, root=0) | |
| out = self.generate(**data) | |
| return jsonify(out) | |
| def parse_input(input_texts: str, tokenizer): | |
| batch_input_ids = [ | |
| tokenizer.encode( | |
| input_text, | |
| add_special_tokens=False, # TODO: does this need to be true? | |
| ) | |
| for input_text in input_texts | |
| ] | |
| batch_input_ids = [torch.tensor(x, dtype=torch.int32, device="cuda") for x in batch_input_ids] | |
| input_lengths = [x.size(0) for x in batch_input_ids] | |
| return batch_input_ids, input_lengths | |
| def get_output(output_ids, input_lengths, max_output_len, tokenizer, eos_token): | |
| num_beams = output_ids.size(1) | |
| assert num_beams == 1 | |
| output_texts = [] | |
| for idx, input_len in enumerate(input_lengths): | |
| output_begin = input_len | |
| output_end = input_len + max_output_len | |
| outputs = output_ids[idx][0][output_begin:output_end] | |
| eos_ids = (outputs == eos_token).nonzero(as_tuple=True)[-1] | |
| if len(eos_ids) > 0: | |
| outputs = outputs[: eos_ids[0]] | |
| outputs = outputs.tolist() | |
| output_texts.append(tokenizer.decode(outputs)) | |
| return output_texts | |
| def prepare_stop_words(stop_words_list, tokenizer): | |
| # adapted from https://github.com/NVIDIA/TensorRT-LLM/blob/b310ec675145c9ee7668592549f733df4abf1e94/tensorrt_llm/runtime/generation.py#L46 | |
| flat_ids = [] | |
| offsets = [] | |
| for batch_stop_words in stop_words_list: | |
| item_flat_ids = [] | |
| item_offsets = [] | |
| for word in batch_stop_words: | |
| # there is a known issue in TensorRT-LLM that word ids are not unique and might change depending on | |
| # where in the text it appears. In our case we mainly need to stop on ids as they appear in the middle | |
| # of the text. The following is a workaround to get such ids that works for both <TOKEN> kind of stop | |
| # words as well as newlines that we commonly use. But note that it's not a universal fix, so this might | |
| # require refactoring if different stop words are used in the future. | |
| # Eventually, this needs to be fixed inside TensorRT-LLM itself. | |
| ids = tokenizer.encode('magic' + word) | |
| ids = ids[2:] # skipping "magic" | |
| if len(ids) == 0: | |
| continue | |
| item_flat_ids += ids | |
| item_offsets.append(len(ids)) | |
| flat_ids.append(np.array(item_flat_ids)) | |
| offsets.append(np.cumsum(np.array(item_offsets))) | |
| pad_to = max(1, max(len(ids) for ids in flat_ids)) | |
| for i, (ids, offs) in enumerate(zip(flat_ids, offsets)): | |
| flat_ids[i] = np.pad(ids, (0, pad_to - len(ids)), constant_values=0) | |
| offsets[i] = np.pad(offs, (0, pad_to - len(offs)), constant_values=-1) | |
| stop_words = np.array([flat_ids, offsets], dtype="int32").transpose((1, 0, 2)) | |
| return torch.Tensor(stop_words).to(torch.int32).to("cuda").contiguous() | |
| def load_tokenizer(tokenizer_dir: str): | |
| tokenizer = AutoTokenizer.from_pretrained( | |
| tokenizer_dir, | |
| legacy=False, | |
| trust_remote_code=True, | |
| ) | |
| if tokenizer.pad_token_id is None: | |
| tokenizer.pad_token_id = tokenizer.eos_token_id | |
| pad_id = tokenizer.pad_token_id | |
| end_id = tokenizer.eos_token_id | |
| return tokenizer, pad_id, end_id | |
| class TensorRTLLM: | |
| def __init__(self, model_path: str): | |
| self.tokenizer, self.pad_id, self.end_id = load_tokenizer(tokenizer_dir=model_path) | |
| self.runner = ModelRunnerCpp.from_dir(engine_dir=model_path, rank=tensorrt_llm.mpi_rank()) | |
| def forward( | |
| self, | |
| input_texts, | |
| max_output_token, | |
| top_k, | |
| top_p, | |
| temperature, | |
| repetition_penalty, | |
| random_seed, | |
| stop_words_list, | |
| max_attention_window_size, | |
| ): | |
| batch_input_ids, input_lengths = parse_input(input_texts, self.tokenizer) | |
| stop_words_list = [stop_words_list for _ in range(len(input_texts))] | |
| stop_words_list = prepare_stop_words(stop_words_list, self.tokenizer) | |
| # TODO: return dictionary with a proper error reporting | |
| try: | |
| output_ids = self.runner.generate( | |
| batch_input_ids, | |
| max_new_tokens=max_output_token, | |
| end_id=self.end_id, | |
| pad_id=self.pad_id, | |
| temperature=temperature, | |
| top_k=top_k, | |
| top_p=top_p, | |
| repetition_penalty=repetition_penalty, | |
| random_seed=random_seed, | |
| stop_words_list=stop_words_list, | |
| max_attention_window_size=max_attention_window_size, | |
| return_dict=False, | |
| ) | |
| torch.cuda.synchronize() | |
| output = get_output(output_ids, input_lengths, max_output_token, self.tokenizer, self.end_id) | |
| except RuntimeError as e: | |
| logging.error("RuntimeError: %s", e) | |
| output = [f"RuntimeError: {e}"] * len(input_texts) | |
| return output | |
| class WrapperServer: | |
| def __init__(self, model_path: str): | |
| self.comm = MPI.COMM_WORLD | |
| self.rank = self.comm.Get_rank() | |
| self.model = TensorRTLLM(model_path=model_path) | |
| if self.rank == 0: | |
| self.app = Flask(__file__, static_url_path="") | |
| api = Api(self.app) | |
| api.add_resource(TritonServerGenerate, "/generate", resource_class_args=[self.model]) | |
| def run(self, url, port=5000): | |
| if self.rank == 0: | |
| self.app.run(url, threaded=True, port=port, debug=False) | |
| else: | |
| self.worker_loop() | |
| def worker_loop(self): | |
| triton = TritonServerGenerate(self.model) | |
| while True: | |
| self.comm.Barrier() | |
| data = None | |
| data = self.comm.bcast(data, root=0) | |
| triton.generate(**data) | |
| if __name__ == "__main__": | |
| # TODO: can we reuse normal logger here? | |
| logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) | |
| parser = ArgumentParser() | |
| parser.add_argument("--model_path", required=True) | |
| parser.add_argument("--host", type=str, default="0.0.0.0") | |
| parser.add_argument("--port", type=int, default=5000) | |
| args = parser.parse_args() | |
| server = WrapperServer(model_path=args.model_path) | |
| server.run(args.host, args.port) | |
Xet Storage Details
- Size:
- 9.35 kB
- Xet hash:
- 9cdd3fe16955c9b23f610720197702b7cf4d8defa869d55457ba7d74f7aabb0c
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.