Serving Your Own Model – Hamel’s Blog - Hamel Husain
Заметка Hamel Husain о том, как развернуть собственные модели в TorchServe на примере двух моделей с HuggingFace: distilgpt2 для генерации текста и stable-diffusion-2 для генерации изображений. Автор сначала проверяет инференс локально, затем пишет кастомные обработчики GptHandler и DiffusionHandler, наследуемые от BaseHandler, разбирая ключевые методы initialize, preprocess, inference и postprocess. Описана упаковка моделей через torch-model-archiver, настройка config.properties с увеличенным max_response_size для крупных ответов diffusion и сборка Docker-образа на базе pytorch/torchserve:latest-gpu. Контейнер запускается с пробросом портов 8080–8082 и томов с model_store и конфигом. В конце демонстрируется работа с REST API: список моделей через /models и предсказания через /predictions/gpt и /predictions/diffusion с помощью curl и requests. По мнению автора и его источника, помимо авто-батчинга и управления воркерами разница между TorchServe и FastAPI невелика.
Подготовка: проверяем инференс локально
Прежде чем загружать модели в Torch Serve, я скачаю две разные модели с HuggingFace и убежусь, что могу выполнить инференс в ноутбуке.
GPT-2
GPT-2 на фоне GPT-3 выглядит архаично
from transformers import pipeline pipe = pipeline(task="text-generation", model="distilgpt2")
preds = pipe(["How do you use Torch Serve for model inference?", "The quick brown fox jumps over the lazy"]) preds
Setting `pad_token_id` to `eos_token_id`:50256 for open-end generation. Setting `pad_token_id` to `eos_token_id`:50256 for open-end generation.
[[{'generated_text': 'How do you use Torch Serve for model inference? Or just use Http to help. Or as a framework, where the actual code runs as expected, like something like JRuby on top of the project? Or maybe you use some way to get'}], [{'generated_text': 'The quick brown fox jumps over the lazy wolf, then hops over the hoot and follows him.'}]]
Diffusion
Здесь определённо нужен GPU
from diffusers import DiffusionPipeline, DPMSolverMultistepScheduler import torch repo_id = "stabilityai/stable-diffusion-2" pipe = DiffusionPipeline.from_pretrained(repo_id, torch_dtype=torch.float16, revision="fp16") pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) pipe = pipe.to("cuda:0")
prompt = "A Butterly in space" image = pipe(prompt, num_inference_steps=25) img = image.images[0] img
PIL можно сконвертировать в структуры, сериализуемые в JSON, вот так:
import numpy as np img_as_list = np.array(img).tolist()
Определяем кастомный handler
Чтобы понять, что происходит, придётся прочитать исходный код BaseHandler. Полезной будет эта запись в блоге. А вот пример того, как делать diffusion в Torch Serve.
Важные методы в BaseHandler:
self.handle: показывает примерный жизненный цикл BaseHandler: initialize -> handle -> preprocess -> inference -> postprocess. initialize явно из handler не вызывается, значит, вызов происходит где-то ещё. Я не нашёл его нигде в кодовой базе, так что, возможно, он вызывается из Java (то, что управляет сервером). preprocess: по умолчанию конвертирует данные в Torch tensor. Нам это в наших примерах не нужно, потому что мы используем pipeline. self.initialize — это место, где вы загружаете и инстанцируете модель; имя свойства, в котором лежит модель, не важно, лишь бы вы использовали его одинаково во всех остальных методах. self.inference: вывод self.preprocess передаётся в self.inference, который возвращает предсказания. Где-то между этим и вызовом self.inference вызывается self.initialize. Похоже, важно установить свойство self.initialized в True внутри self.initialize (см. примеры). self.postprocess: вывод self.inference передаётся в self.postprocess. По умолчанию он вызывает метод .tolist() на том объекте, что вернул self.inference.
Есть также свойства context и manifest, в которых содержатся значения, передаваемые через конфиг или CLI-команды (например, директорию модели). Что именно лежит в этих переменных — неясно, единственный способ узнать — добавить логирование. Ещё один способ получить подсказки — посмотреть примеры.
Этим python-кодом управляет Java-код, поэтому, к сожалению, интерактивный отладчик не поможет понять, что происходит. Единственный способ что-то рассмотреть — через логирование.
Python-код в итоге упирается в тупик, где приходится читать Java-код, так что просто чтением кода всех ответов не получить (но читать BaseHandler всё равно полезно).
Я обсуждал context и manifest с доверенным источником, и он сказал, что люди часто используют переменные окружения вместо того, чтобы полагаться на context. Когда вы переопределяете BaseHandler и используете собственные переменные окружения, TorchServe делает не так уж и много, кроме авто-батчинга и управления воркерами. Мы сошлись на том, что, помимо этих двух фич, разницы между Torch Serve и FastAPI почти нет.
(В этом примере я так делать не буду)
GPT Handler
%%writefile gpt_handler.py #gpt_handler.py import logging import torch from transformers import pipeline from ts.torch_handler.base_handler import BaseHandler logger = logging.getLogger(__name__) logger.info("Starting GPT Handler") class GptHandler(BaseHandler): def __init__(self): self.initialized = False def initialize(self, ctx): self.manifest = ctx.manifest properties = ctx.system_properties self.device = torch.device( "cuda:" + str(properties.get("gpu_id")) if torch.cuda.is_available() and properties.get("gpu_id") is not None else "cpu" ) # you might normaly get the model from disk, but we don't have to in this case. self.pipe = pipeline(task="text-generation", model="distilgpt2") self.initialized = True def preprocess(self, data): text = data[0].get("data") if text is None: text = data[0].get("body") logging.info(f'Here is the text: {text}') sentences = text.decode('utf-8') return sentences def inference(self, data): return self.pipe(data) def postprocess(self, data): return data
Overwriting gpt_handler.py
Diffusion Handler
%%writefile diffusion_handler.py #diffusion_handler.py import logging import torch import numpy as np from diffusers import DiffusionPipeline, DPMSolverMultistepScheduler from ts.torch_handler.base_handler import BaseHandler logger = logging.getLogger(__name__) logger.info("Starting Diffusion Handler") class DiffusionHandler(BaseHandler): def __init__(self): self.initialized = False def initialize(self, ctx): self.manifest = ctx.manifest properties = ctx.system_properties self.device = torch.device( "cuda:" + str(properties.get("gpu_id")) if torch.cuda.is_available() and properties.get("gpu_id") is not None else "cpu" ) repo_id = "stabilityai/stable-diffusion-2" self.pipe = DiffusionPipeline.from_pretrained(repo_id, torch_dtype=torch.float16, revision="fp16") self.pipe.scheduler = DPMSolverMultistepScheduler.from_config(self.pipe.scheduler.config) self.pipe = self.pipe.to(self.device) self.initialized = True def preprocess(self, data): text = data[0].get("data") if text is None: text = data[0].get("body") prompt = text.decode('utf-8') return prompt def inference(self, data): image = self.pipe(data, num_inference_steps=25) img = image.images[0] return np.array(img) def postprocess(self, data): return [data.tolist()]
Overwriting diffusion_handler.py
Упаковываем артефакты
Создаём архив модели
!mkdir -p model_store ! torch-model-archiver \ --export-path model_store \ --model-name "gpt" --version 1.0 \ --handler "./gpt_handler.py" \ --force ! torch-model-archiver \ --export-path model_store \ --model-name "diffusion" --version 1.0 \ --handler "./diffusion_handler.py" \ --force
WARNING - Overwriting model_store/gpt.mar ... WARNING - Overwriting model_store/diffusion.mar ...
Настраиваем config.properties
Ответ от diffusion больше дефолтного допустимого размера, поэтому нужно передать конфиг. Пример лежит здесь. Не знаю, почему приходится явно задавать порты вот так — ведь это дефолтные значения (но без этого ничего не работает).
%%writefile config/config.properties inference_address=http://0.0.0.0:8080 management_address=http://0.0.0.0:8081 metrics_address=http://0.0.0.0:8082 load_models=all max_response_size=655350000
Overwriting config/config.properties
Создаём Dockerfile
Идеи можно почерпнуть из их Dockerfile
%%writefile Dockerfile.gpt FROM pytorch/torchserve:latest-gpu RUN python -m pip install transformers diffusers ENTRYPOINT ["/usr/local/bin/dockerd-entrypoint.sh"] CMD ["serve"]
Overwriting Dockerfile.gpt
Собираем Dockerfile
! docker build -f Dockerfile.gpt . -t pytorch/torchserve:gpu-hf;
Sending build context to Docker daemon 334.6MB Step 1/4 : FROM pytorch/torchserve:latest-gpu ---> 046086392ab2 Step 2/4 : RUN python -m pip install transformers diffusers ---> Using cache ---> 13135ca5603f Step 3/4 : ENTRYPOINT ["/usr/local/bin/dockerd-entrypoint.sh"] ---> Using cache ---> 6910f9182230 Step 4/4 : CMD ["serve"] ---> Using cache ---> bbed6fd312c2 Successfully built bbed6fd312c2 Successfully tagged pytorch/torchserve:gpu-hf
Запускаем torch serve
Аргументы нам не нужны, поскольку BaseHandler сам подтягивает модель из Hub и т.д.
docker run --rm -it --gpus '"device=0"' \ -p 8080:8080 \ -p 8081:8081 \ -p 8082:8082 \ -p 7070:7070 \ -p 7071:7071 \ -v /home/hamel/hamel/notes/serving/torchserve/model_store/:/home/model-server/model_store/ \ -v /home/hamel/hamel/notes/serving/torchserve/config/:/home/model-server/custom_config/ \ pytorch/torchserve:gpu-hf \ torchserve \ --model-store /home/model-server/model_store \ --models gpt=gpt.mar diffusion=diffusion.mar \ --ts-config /home/model-server/custom_config/config.properties
Используем REST API
!curl http://127.0.0.1:8081/models
{ "models": [ { "modelName": "diffusion", "modelUrl": "diffusion.mar" }, { "modelName": "gpt", "modelUrl": "gpt.mar" } ] }
Делаем предсказания
GPT
С curl
! echo "The quick brown fox jumps over the lazy" > gpt.txt ! cat gpt.txt
The quick brown fox jumps over the lazy
!curl http://127.0.0.1:8080/predictions/gpt -T gpt.txt
{ "generated_text": "The quick brown fox jumps over the lazy\nI do find some funny gifs to do.\nI used to have cats but I never find one\nI used to have dogs. But I couldn't really find a cute dog but now I enjoy" }
С requests
import requests resp = requests.post('http://127.0.0.1:8080/predictions/gpt', data={'data': "The quick brown fox jumps over the lazy"}) resp.text
'{\n "generated_text": "The quick brown fox jumps over the lazy blonde to win, the latter has to wait for the rest to come on, and she also needs a hug and a hug. The adorable feline can barely contain itself, but the kitten is quite responsive,"\n}'
Diffusion
import requests import json from PIL import Image resp = requests.post('http://127.0.0.1:8080/predictions/diffusion', data={'data': "A butterfly in space with glasses."})
dimg = np.array(json.loads(resp.text), dtype='uint8') Image.fromarray(dimg)