newsmode
search
Меню
arrow_back Назад

Serving Your Own Model – Hamel’s Blog - Hamel Husain

auto_awesomeКраткое саммари

Заметка 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 невелика.

Prep: Test Inference locally

Подготовка: проверяем инференс локально

Before we try to load models into Torch Serve, I’m going to download two different HuggingFace models and make sure I can do inference in a notebook.

Прежде чем загружать модели в Torch Serve, я скачаю две разные модели с HuggingFace и убежусь, что могу выполнить инференс в ноутбуке.

GPT-2

GPT-2

GPT-2 looks archaic compared to GPT-3

GPT-2 на фоне GPT-3 выглядит архаично

from transformers import pipeline pipe = pipeline(task="text-generation", model="distilgpt2")

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

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.

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.'}]]

[[{'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

Diffusion

This definitely requires a GPU

Здесь определённо нужен 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")

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

prompt = "A Butterly in space" image = pipe(prompt, num_inference_steps=25) img = image.images[0] img

You can convert PIL to JSON serializable structures like this:

PIL можно сконвертировать в структуры, сериализуемые в JSON, вот так:

import numpy as np img_as_list = np.array(img).tolist()

import numpy as np img_as_list = np.array(img).tolist()

Define a custom handler

Определяем кастомный handler

You have to read the source code of BaseHandler to understand what is happening. This blog post is helpful. This is an example of doing diffusion in Torch Serve.

Чтобы понять, что происходит, придётся прочитать исходный код BaseHandler. Полезной будет эта запись в блоге. А вот пример того, как делать diffusion в Torch Serve.

Important methods in BaseHandler:

Важные методы в BaseHandler:

  • self.handle: this shows you the rough lifecycle of BaseHandler: initialize -> handle -> preprocess -> inference -> postprocess
    • initialize isn’t explicitly called in the handler, so it must be called elsewhere. I could not find this called anywhere in the code base, so perhaps its called in Java somehow (the thing that drives the server).
  • preprocess: by default this will convert data to a Torch tensor. For the examples we are working with we don’t want this, because we are using pipeline.
  • self.initialize is where you load and instantiate the model, the name of the property that has the model is not important as long as you use it consistently across the other methods.
  • self.inference: The output of self.preprocess is passed to self.inference, which then returns predictions. Somewhere along the line before self.inference is called, self.initialize is called. It seems important to set the self.initialized property to True in self.initialize (see examples)
  • self.postprocess: The output of self.inference is passed to self.postprocess. By default, this calls the .tolist() method on whatever object is returned from self.inference.
  • 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.

    There is also a context and manifest properties which contain values that you might supply through a config or as CLI commands (like the model’s directory). It’s not clear what is contained in these variables, and the only way is to use logging statements to find out. Another way to try to get clues is to look at the examples.

    Есть также свойства context и manifest, в которых содержатся значения, передаваемые через конфиг или CLI-команды (например, директорию модели). Что именно лежит в этих переменных — неясно, единственный способ узнать — добавить логирование. Ещё один способ получить подсказки — посмотреть примеры.

    There is Java code that drives this python code, so unfortunately an interactive debugger does not work to figure out what is happening. Your only way to introspect things is through logging statements.

    Этим python-кодом управляет Java-код, поэтому, к сожалению, интерактивный отладчик не поможет понять, что происходит. Единственный способ что-то рассмотреть — через логирование.

    The python code eventually leads to a dead end where you have to start reading Java code, so just looking at the code doesn’t provide all the answers (but reading BaseHandler is still helpful).

    Python-код в итоге упирается в тупик, где приходится читать Java-код, так что просто чтением кода всех ответов не получить (но читать BaseHandler всё равно полезно).

    I was talking to a trusted source about context and manifest and he says that people often use environment variables instead of relying on context. When you override BaseHandler and use your own environment variables, TorchServe isn’t doing so much aside from auto batching and worker management. We agreed that aside from these two features, there is little difference between using Torch Serve and FastAPI.

    Я обсуждал context и manifest с доверенным источником, и он сказал, что люди часто используют переменные окружения вместо того, чтобы полагаться на context. Когда вы переопределяете BaseHandler и используете собственные переменные окружения, TorchServe делает не так уж и много, кроме авто-батчинга и управления воркерами. Мы сошлись на том, что, помимо этих двух фич, разницы между Torch Serve и FastAPI почти нет.

    (I am not going to do this in this example)

    (В этом примере я так делать не буду)

    GPT Handler

    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

    %%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

    Overwriting gpt_handler.py

    Diffusion Handler

    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()]

    %%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

    Overwriting diffusion_handler.py

    Package The Artifacts

    Упаковываем артефакты

    Create the model archive

    Создаём архив модели

    !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

    !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 ...

    WARNING - Overwriting model_store/gpt.mar ... WARNING - Overwriting model_store/diffusion.mar ...

    Set config.properties

    Настраиваем config.properties

    The diffusion response is bigger than the allowable default so we must pass a config. An example is here. I don’t know why I have to set the different ports like this, since these are the defaults (If I do not set these, things do not work properly).

    Ответ от 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

    %%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

    Overwriting config/config.properties

    Create a Dockerfile

    Создаём Dockerfile

    We can get ideas from their 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"]

    %%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

    Overwriting Dockerfile.gpt

    Build the Dockerfile

    Собираем Dockerfile

    ! docker build -f Dockerfile.gpt . -t pytorch/torchserve:gpu-hf;

    ! 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

    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

    Start torch serve

    Запускаем torch serve

    We don’t need any arguments since the BaseHandler pulls the model from the Hub, etc.

    Аргументы нам не нужны, поскольку 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

    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

    Use the REST API

    Используем REST API

    !curl http://127.0.0.1:8081/models

    !curl http://127.0.0.1:8081/models

    { "models": [ { "modelName": "diffusion", "modelUrl": "diffusion.mar" }, { "modelName": "gpt", "modelUrl": "gpt.mar" } ] }

    { "models": [ { "modelName": "diffusion", "modelUrl": "diffusion.mar" }, { "modelName": "gpt", "modelUrl": "gpt.mar" } ] }

    Make Predictions

    Делаем предсказания

    GPT

    GPT

    With curl

    С curl

    ! echo "The quick brown fox jumps over the lazy" > gpt.txt ! cat gpt.txt

    ! echo "The quick brown fox jumps over the lazy" > gpt.txt ! cat gpt.txt

    The quick brown fox jumps over the lazy

    The quick brown fox jumps over the lazy

    !curl http://127.0.0.1:8080/predictions/gpt -T gpt.txt

    !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" }

    { "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" }

    With requests

    С 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

    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}'

    '{\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

    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."})

    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)

    dimg = np.array(json.loads(resp.text), dtype='uint8') Image.fromarray(dimg)