From fd9b90526958cf8550e511684f9c12aa837bcfce Mon Sep 17 00:00:00 2001 From: Philip Kiely - Baseten <98474633+philipkiely-baseten@users.noreply.github.com> Date: Tue, 15 Aug 2023 14:09:47 -0700 Subject: [PATCH] Philip/even more docs (#563) * tutorial refactor * CLI reference * usage * user guide * fix up 3 tutorials * 2 more tutorials * VLLM tutorial --- README.md | 2 +- docs/examples/models/overview.mdx | 16 +- docs/examples/performance/cached-weights.mdx | 220 ++++++++++++++++++- docs/examples/performance/vllm-server.mdx | 96 +++++++- docs/examples/private-model.mdx | 195 ++++++++++++++-- docs/examples/streaming.mdx | 27 +-- docs/examples/system-packages.mdx | 25 ++- docs/images/user-workflow.png | Bin 0 -> 98931 bytes docs/learn/intro.mdx | 8 +- docs/learn/model-serving/echo.mdx | 64 ------ docs/learn/model-serving/init.mdx | 53 +++-- docs/learn/model-serving/model-load.mdx | 52 +++-- docs/learn/model-serving/model-predict.mdx | 45 ++-- docs/learn/model-serving/predict.mdx | 43 ++-- docs/learn/model-serving/publish.mdx | 84 +++++++ docs/learn/model-serving/push.mdx | 54 +++-- docs/learn/model-serving/requirements.mdx | 61 ++--- docs/learn/model-serving/watch.mdx | 46 ++-- docs/mint.json | 71 ++---- docs/quickstart.mdx | 85 +++++-- docs/reference/cli.mdx | 49 ++++- docs/reference/cli/cleanup.mdx | 16 ++ docs/reference/cli/container.mdx | 68 ++++++ docs/reference/cli/image.mdx | 95 ++++++++ docs/reference/cli/init.mdx | 35 +++ docs/reference/cli/predict.mdx | 40 ++++ docs/reference/cli/push.mdx | 44 ++++ docs/reference/cli/watch.mdx | 39 ++++ docs/reference/config.mdx | 41 ---- docs/usage.mdx | 83 +++++++ docs/welcome.mdx | 8 +- 31 files changed, 1382 insertions(+), 383 deletions(-) create mode 100644 docs/images/user-workflow.png delete mode 100644 docs/learn/model-serving/echo.mdx create mode 100644 docs/learn/model-serving/publish.mdx create mode 100644 docs/reference/cli/cleanup.mdx create mode 100644 docs/reference/cli/container.mdx create mode 100644 docs/reference/cli/image.mdx create mode 100644 docs/reference/cli/init.mdx create mode 100644 docs/reference/cli/predict.mdx create mode 100644 docs/reference/cli/push.mdx create mode 100644 docs/reference/cli/watch.mdx diff --git a/README.md b/README.md index a557e1e2d..634f1c157 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## Why Truss? * **Write once, run anywhere:** Package and test model code, weights, and dependencies with a model server that behaves the same in development and production. -* **Fast developer loop:** Implement your model with fast feedback from a live reload server, and skip Docker and Kubernetes configuration with Truss' done-for-you model serving environment. +* **Fast developer loop:** Implement your model with fast feedback from a live reload server, and skip Docker and Kubernetes configuration with a batteries-included model serving environment. * **Support for all Python frameworks**: From `transformers` and `diffusors` to `PyTorch` and `Tensorflow` to `XGBoost` and `sklearn`, Truss supports models created with any framework, even entirely custom models. See Trusses for popular models including: diff --git a/docs/examples/models/overview.mdx b/docs/examples/models/overview.mdx index 2c38d8cc8..0e3cc5b3b 100644 --- a/docs/examples/models/overview.mdx +++ b/docs/examples/models/overview.mdx @@ -1,20 +1,20 @@ --- -title: Example models -description: "Description" +title: Example foundation models +description: "Step-by-step packaging instructions" --- - Lorem + A commercially-licensed LLM by Meta - Lorem + A text to image model by Stability AI - Lorem + An audio transcription model by OpenAI - - Lorem - + + See Trusses for dozens of models on GitHub. + diff --git a/docs/examples/performance/cached-weights.mdx b/docs/examples/performance/cached-weights.mdx index ddc4dfe2d..63cf9d9f0 100644 --- a/docs/examples/performance/cached-weights.mdx +++ b/docs/examples/performance/cached-weights.mdx @@ -1,4 +1,222 @@ --- title: Load cached model weights -description: "Description" +description: "Deploy a model with private Hugging Face weights" --- + +In this example, we will cover how you can use the `hf_cache` key in your Truss's `config.yml` to automatically bundle model weights from a private Hugging Face repo. + + +Bundling model weights can significantly reduce cold start times because your instance won't waste time downloading the model weights from Hugging Face's servers. + + +We use `Llama-2-7b`, a popular open-source large language model, as an example. In order to follow along with us, you need to request access to Llama 2. + +1. First, [sign up for a Hugging Face account](https://huggingface.co/join) if you don't already have one. +2. Request access to Llama 2 from [Meta's website](https://ai.meta.com/resources/models-and-libraries/llama-downloads/). +2. Next, request access to Llama 2 on [Hugging Face](https://huggingface.co/meta-llama/Llama-2-7b-chat-hf) by clicking the "Request access" button on the model page. + + +If you want to deploy on Baseten, you also need to create a Hugging Face API token and add it to your organizations's secrets. +1. [Create a Hugging Face API token](https://huggingface.co/settings/tokens) and copy it to your clipboard. +2. Add the token with the key `hf_access_token` to [your organization's secrets](https://app.baseten.co/settings/secrets) on Baseten. + + +### Step 0: Initialize Truss + +Get started by creating a new Truss: + +```sh +truss init llama-2-7b-chat +``` + +Select the `TrussServer` option then hit `y` to confirm Truss creation. Then navigate to the newly created directory: + +```sh +cd llama-2-7b-chat +``` + +### Step 1: Implement Llama 2 7B in Truss + +Next, we'll fill out the `model.py` file to implement Llama 2 7B in Truss. + + +In `model/model.py`, we write the class `Model` with three member functions: + +* `__init__`, which creates an instance of the object with a `_model` property +* `load`, which runs once when the model server is spun up and loads the `pipeline` model +* `predict`, which runs each time the model is invoked and handles the inference. It can use any JSON-serializable type as input and output. + +We will also create a helper function `format_prompt` outside of the `Model` class to appropriately format the incoming text according to the Llama 2 specification. + +[Read the quickstart guide](/quickstart) for more details on `Model` class implementation. + +```python model/model.py +from typing import Dict, List + +import torch +from transformers import LlamaForCausalLM, LlamaTokenizer + +DEFAULT_SYSTEM_PROMPT = "You are a helpful, respectful and honest assistant." + +B_INST, E_INST = "[INST]", "[/INST]" +B_SYS, E_SYS = "<>\n", "\n<>\n\n" + +class Model: + def __init__(self, **kwargs) -> None: + self._data_dir = kwargs["data_dir"] + self._config = kwargs["config"] + self._secrets = kwargs["secrets"] + self.model = None + self.tokenizer = None + + def load(self): + self.model = LlamaForCausalLM.from_pretrained( + "meta-llama/Llama-2-7b-chat-hf", + use_auth_token=self._secrets["hf_access_token"], + torch_dtype=torch.float16, + device_map="auto" + ) + self.tokenizer = LlamaTokenizer.from_pretrained( + "meta-llama/Llama-2-7b-chat-hf", + use_auth_token=self._secrets["hf_access_token"] + ) + + def predict(self, request: Dict) -> Dict[str, List]: + prompt = request.pop("prompt") + prompt = format_prompt(prompt) + + inputs = tokenizer(prompt, return_tensors="pt") + + outputs = model.generate(**inputs, do_sample=True, num_beams=1, max_new_tokens=100) + response = tokenizer.batch_decode(outputs, skip_special_tokens=True)[0] + + return {"response": response} + +def format_prompt(prompt: str, system_prompt: str = DEFAULT_SYSTEM_PROMPT) -> str: + return f"{B_INST} {B_SYS} {system_prompt} {E_SYS} {prompt} {E_INST}" +``` + +### Step 2: Set Python dependencies + +Now, we can turn our attention to configuring the model server in `config.yaml`. + +In addition to `transformers`, Llama 2 has three other dependencies. We list them below as follows: + +```yaml config.yaml +requirements: +- accelerate==0.21.0 +- safetensors==0.3.2 +- torch==2.0.1 +- transformers==4.30.2 +``` + + +Always pin exact versions for your Python dependencies. The ML/AI space moves fast, so you want to have an up-to-date version of each package while also being protected from breaking changes. + + +### Step 3: Configure Hugging Face caching + +Finally, we can configure Hugging Face caching in `config.yaml` by adding the `hf_cache` key. When building the image for your Llama 2 deployment, the Llama 2 model weights will be downloaded and cached for future use. + +```yaml config.yaml +hf_cache: +- repo_id: "meta-llama/Llama-2-7b-chat-hf" + ignore_patterns: + - "*.bin" +``` + +In this configuration: +- `meta-llama/Llama-2-7b-chat-hf` is the `repo_id`, pointing to the exact model to cache. +- We use a wild card to ignore all `.bin` files in the model directory by providing a pattern under `ignore_patterns`. This is because the model weights are stored in `.bin` and `safetensors` format, and we only want to cache the `safetensors` files. + + +### Step 4: Deploy the model + + +You'll need a [Baseten API key](https://app.baseten.co/settings/account/api_keys) for this step. Make sure you added your `HUGGING_FACE_HUB_TOKEN` to your organization's secrets. + + +We have successfully packaged Llama 2 as a Truss. Let's deploy! + +```sh +truss push --trusted +``` + +### Step 5: Invoke the model + +You can invoke the model with: + +```sh +truss predict -d '{"prompt": "What is a large language model?"}' +``` + + + +```yaml config.yaml +environment_variables: {} +external_package_dirs: [] +model_metadata: {} +model_name: null +python_version: py39 +requirements: +- accelerate==0.21.0 +- safetensors==0.3.2 +- torch==2.0.1 +- transformers==4.30.2 +hf_cache: +- repo_id: "NousResearch/Llama-2-7b-chat-hf" + ignore_patterns: + - "*.bin" +resources: + cpu: "4" + memory: 30Gi + use_gpu: True + accelerator: A10G +secrets: {} +``` + +```python model/model.py +from typing import Dict, List + +import torch +from transformers import LlamaForCausalLM, LlamaTokenizer + +DEFAULT_SYSTEM_PROMPT = "You are a helpful, respectful and honest assistant." + +B_INST, E_INST = "[INST]", "[/INST]" +B_SYS, E_SYS = "<>\n", "\n<>\n\n" + +class Model: + def __init__(self, **kwargs) -> None: + self._data_dir = kwargs["data_dir"] + self._config = kwargs["config"] + self._secrets = kwargs["secrets"] + self.model = None + self.tokenizer = None + + def load(self): + self.model = LlamaForCausalLM.from_pretrained( + "meta-llama/Llama-2-7b-chat-hf", + use_auth_token=self._secrets["hf_access_token"], + torch_dtype=torch.float16, + device_map="auto" + ) + self.tokenizer = LlamaTokenizer.from_pretrained( + "meta-llama/Llama-2-7b-chat-hf", + use_auth_token=self._secrets["hf_access_token"] + ) + + def predict(self, request: Dict) -> Dict[str, List]: + prompt = request.pop("prompt") + inputs = tokenizer(prompt, return_tensors="pt") + + outputs = model.generate(**inputs, do_sample=True, num_beams=1, max_new_tokens=100) + response = tokenizer.batch_decode(outputs, skip_special_tokens=True)[0] + + return {"response": response} + +def format_prompt(prompt: str, system_prompt: str = DEFAULT_SYSTEM_PROMPT) -> str: + return f"{B_INST} {B_SYS} {system_prompt} {E_SYS} {prompt} {E_INST}" +``` + + diff --git a/docs/examples/performance/vllm-server.mdx b/docs/examples/performance/vllm-server.mdx index 66e42a590..46d570940 100644 --- a/docs/examples/performance/vllm-server.mdx +++ b/docs/examples/performance/vllm-server.mdx @@ -1,4 +1,98 @@ --- title: Serve models with VLLM -description: "Description" +description: "Deploy a language model using vLLM" --- + +[vLLM](https://github.com/vllm-project/vllm) is a Python-based package that optimizes the Attention layer in Transformer models. By better allocating memory used during the attention computation, vLLM can reduce the memory footprint of a model and significantly improve inference speed. Truss supports vLLM out of the box, so you can deploy vLLM-optimized models with ease. We're going to walk through deploying a vLLM-optimized [OPT-125M model](https://huggingface.co/facebook/opt-125m). + + +You can see the config for the finished model on the right. Keep reading for step-by-step instructions on how to generate it. + + +This example will cover: + +1. Generating the base Truss +2. Setting sufficient model resources for inference +3. Deploying the model + +### Step 1: Generating the base Truss + +Get started by creating a new Truss: + +```sh +truss init opt125 +``` + +You're going to see a couple of prompts. Follow along with the instructions below: +1. Type `facebook/opt-125M` when prompted for `model`. +2. Press the `tab` key when prompted for `endpoint`. Select the `Completions` endpoint. +3. Give your model a name like `OPT-125M`. + + +The underlying server that we use is OpenAI compatible. If you plan on using the model as a chat model, then select `ChatCompletion`. OPT-125M is not a chat model so we selected `Completion`. + + +Finally, navigate to the directory: + +```sh +cd opt125 +``` + +### Step 2: Setting resources and other arguments + +You'll notice that there's a `config.yaml` in the new directory. This is where we'll set the resources and other arguments for the model. Open the file in your favorite editor. + +OPT-125M will need a GPU so let's set the correct resources. Update the `resources` key with the following: + +```yaml config.yaml +resources: + accelerator: T4 + cpu: "4" + memory: 16Gi + use_gpu: true +``` + +Also notice the `build` key which contains the `model_server` we're using as well as other arguments. These arguments are passed to the underlying vLLM server which you can find [here](https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/api_server.py). + +### Step 3: Deploy the model + + +You'll need a [Baseten API key](https://app.baseten.co/settings/account/api_keys) for this step. + + +Let's deploy our OPT-125M vLLM model. + +```sh +truss push +``` + +You can invoke the model with: + +```sh +truss predict -d '{"prompt": "What is a large language model?"}' +``` + + + +```yaml config.yaml +build: + arguments: + endpoint: Completions + model: facebook/opt-125M + model_server: VLLM +environment_variables: {} +external_package_dirs: [] +model_metadata: {} +model_name: OPT-125M +python_version: py39 +requirements: [] +resources: + accelerator: T4 + cpu: "4" + memory: 16Gi + use_gpu: true +secrets: {} +system_packages: [] +``` + + diff --git a/docs/examples/private-model.mdx b/docs/examples/private-model.mdx index 974fd34db..eac50fe0b 100644 --- a/docs/examples/private-model.mdx +++ b/docs/examples/private-model.mdx @@ -1,31 +1,190 @@ --- title: Private Hugging Face model -description: "Description" +description: "Load a model that requires authentication with Hugging Face" --- -PrivateLM on Baseten's HF account +## Summary + +To load a gated or private model from Hugging Face: + +1. Create an [access token](https://huggingface.co/settings/tokens) on your Hugging Face account. +2. Add the `hf_access_token` key to your `config.yaml` secrets and value to your [Baseten account](https://app.baseten.co/settings/secrets). +3. Add `use_auth_token` to the appropriate line in `model.py`. + +Example code: + + +```yaml config.yaml +secrets: + hf_access_token: null +``` + +```python model/model.py +self._model = pipeline( + "fill-mask", + model="baseten/docs-example-gated-model", + use_auth_token=self._secrets["hf_access_token"] +) +``` + + +## Step-by-step example + +[BERT base (uncased)](https://huggingface.co/bert-base-uncased) is a masked language model that can be used to infer missing words in a sentence. + +While the model is publicly available on Hugging Face, we copied it into a gated model to use in this tutorial. The process is the same for using a gated model as it is for a private model. + + +You can see the code for the finished private model Truss on the right. Keep reading for step-by-step instructions on how to build it. + + +This example will cover: + +1. Implementing a `transformers.pipeline` model in Truss +2. **Securely accessing secrets in your model server** +3. **Using a gated or private model with an access token** + + +### Step 0: Initialize Truss + +Get started by creating a new Truss: + +```sh +truss init private-bert +``` + +Give your model a name when prompted, like `Private Model Demo`. Then, navigate to the newly created directory: + +```sh +cd private-bert +``` + +### Step 1: Implement the `Model` class + +BERT base (uncased) is [a pipeline model](https://huggingface.co/docs/transformers/main_classes/pipelines), so it is straightforward to implement in Truss. + +In `model/model.py`, we write the class `Model` with three member functions: + +* `__init__`, which creates an instance of the object with a `_model` property +* `load`, which runs once when the model server is spun up and loads the `pipeline` model +* `predict`, which runs each time the model is invoked and handles the inference. It can use any JSON-serializable type as input and output. + +[Read the quickstart guide](/quickstart) for more details on `Model` class implementation. + +```python model/model.py +from transformers import pipeline + + +class Model: + def __init__(self, **kwargs) -> None: + self._secrets = kwargs["secrets"] + self._model = None + + def load(self): + self._model = pipeline( + "fill-mask", + model="baseten/docs-example-gated-model" + ) + + def predict(self, model_input): + return self._model(model_input) +``` + +### Step 2: Set Python dependencies + +Now, we can turn our attention to configuring the model server in `config.yaml`. + +BERT base (uncased) has two dependencies: + +```yaml config.yaml +requirements: +- torch==2.0.1 +- transformers==4.30.2 +``` + + +Always pin exact versions for your Python dependencies. The ML/AI space moves fast, so you want to have an up-to-date version of each package while also being protected from breaking changes. + + +### Step 3: Set required secret + +Now it's time to mix in access to the gated model: + +1. Go to the [model page on Hugging Face](https://huggingface.co/baseten/docs-example-gated-model) and accept the terms to access the model. +2. Create an [access token](https://huggingface.co/settings/tokens) on your Hugging Face account. +3. Add the `hf_access_token` key and value to your [Baseten workspace secret manager](https://app.baseten.co/settings/secrets). +4. In your `config.yaml`, add the key `hf_access_token`: + +```yaml config.yaml +secrets: + hf_access_token: null +``` + + +Never set the actual value of a secret in the `config.yaml` file. Only put secret values in secure places, like the Baseten workspace secret manager. + + +### Step 4: Use access token in load + +In `model/model.py`, you can give your model access to secrets in the init function: + +```python model/model.py +def __init__(self, **kwargs) -> None: + self._secrets = kwargs["secrets"] + self._model = None +``` + +Then, update the load function with `use_auth_token`: + +```python model/model.py +self._model = pipeline( + "fill-mask", + model="baseten/docs-example-gated-model", + use_auth_token=self._secrets["hf_access_token"] +) +``` + +This will allow the `pipeline` function to load the specified model from Hugging Face. + +### Step 5: Deploy the model + + +You'll need a [Baseten API key](https://app.baseten.co/settings/account/api_keys) for this step. + + +We have successfully packaged a gated model as a Truss. Let's deploy! + +Use `--trusted` with `truss push` to give the model server access to secrets stored on the remote host. + +```sh +truss push --trusted +``` + +Wait for the model to finish deployment before invoking. + +You can invoke the model with: + +```sh +truss predict -d '"It is a [MASK] world"' +``` ```yaml config.yaml environment_variables: {} -external_package_dirs: [] -model_metadata: {} -model_name: null +model_name: private-model python_version: py39 requirements: -- Pillow==10.0.0 -- pytesseract==0.3.10 - torch==2.0.1 - transformers==4.30.2 resources: - cpu: "4" - memory: 16Gi + cpu: "1" + memory: 2Gi use_gpu: false accelerator: null -secrets: {} -system_packages: -- tesseract-ocr +secrets: + hf_access_token: null +system_packages: [] ``` ```python model/model.py @@ -34,22 +193,18 @@ from transformers import pipeline class Model: def __init__(self, **kwargs) -> None: - self._data_dir = kwargs["data_dir"] - self._config = kwargs["config"] self._secrets = kwargs["secrets"] self._model = None def load(self): self._model = pipeline( - "document-question-answering", - model="impira/layoutlm-document-qa", + "fill-mask", + model="baseten/docs-example-gated-model", + use_auth_token=self._secrets["hf_access_token"] ) def predict(self, model_input): - return self._model( - model_input["url"], - model_input["prompt"] - ) + return self._model(model_input) ``` diff --git a/docs/examples/streaming.mdx b/docs/examples/streaming.mdx index 45db341c5..1767396b2 100644 --- a/docs/examples/streaming.mdx +++ b/docs/examples/streaming.mdx @@ -12,8 +12,7 @@ LLMs have two properties that make streaming output particularly useful: When you host your LLMs with Baseten, you can stream responses. Instead of having to wait for the entire output to be generated, you can immediately start returning results to users with a sub-one-second time-to-first-token. -In this example, we will show you how to deploy [Falcon 7B](https://huggingface.co/tiiuae/falcon-7b), an LLM, and stream -the output as it is generated. +In this example, we will show you how to deploy [Falcon 7B](https://huggingface.co/tiiuae/falcon-7b), an LLM, and stream the output as it is generated. @@ -29,7 +28,7 @@ Get started by creating a new Truss: truss init falcon-7b ``` -Select the default `TrussServer` option. Then, navigate to the newly created directory: +Give your model a name when prompted, like `falcon-streaming`. Then, navigate to the newly created directory: ```sh cd falcon-7b @@ -38,8 +37,7 @@ cd falcon-7b ### Step 1: Set up the `Model` class without streaming As mentioned before, Falcon 7B is an LLM. We will use the Huggingface Transformers library to -load and run the model. In this first step, we will generate output normally and return it without streaming -the output. +load and run the model. In this first step, we will generate output normally and return it without streaming the output. In `model/model.py`, we write the class `Model` with three member functions: @@ -114,20 +112,13 @@ class Model: ### Step 2: Add streaming support -Once we have a model that can produce the LLM outputs using the HuggingFace transformers library, -we can adapt it to support streaming. The key change that needs to happen here is in the `predict` -function. +Once we have a model that can produce the LLM outputs using the HuggingFace transformers library, we can adapt it to support streaming. The key change that needs to happen here is in the `predict` function. -While in the above example, the `predict` function returns a `Dict` containing the model output, -to stream results, we need to return a Python `Generator` from the `predict` function instead. -This will allow us to return partial results to the user as they are generated. +While in the above example, the `predict` function returns a `Dict` containing the model output, to stream results, we need to return a Python `Generator` from the `predict` function instead. This will allow us to return partial results to the user as they are generated. -To produce outputs incrementally for the LLM, we will pass a `TextIteratorStreamer` object to the -`generate` function. This object will return the model output as it is generated. We will then kick -off the generation on a separate thread. +To produce outputs incrementally for the LLM, we will pass a `TextIteratorStreamer` object to the `generate` function. This object will return the model output as it is generated. We will then kick off the generation on a separate thread. -What we return from the `predict` function is a generator that will yield the model output from -the streamer object as it is generated. +What we return from the `predict` function is a generator that will yield the model output from the streamer object as it is generated. ```python model/model.py import torch @@ -229,7 +220,7 @@ resources: You'll need a [Baseten API key](https://app.baseten.co/settings/account/api_keys) for this step. -We have successfully packaged LayoutLM Document QA as a Truss. Let's deploy! +We have successfully packaged Falcon as a Truss. Let's deploy! Run: ```sh truss push @@ -240,7 +231,7 @@ truss push You can invoke the model with: ```sh -truss predict +truss predict -d '{"prompt": "Tell me about falcons", "do_sample": true}' ``` diff --git a/docs/examples/system-packages.mdx b/docs/examples/system-packages.mdx index 769c60f80..625ac7718 100644 --- a/docs/examples/system-packages.mdx +++ b/docs/examples/system-packages.mdx @@ -3,7 +3,22 @@ title: Model with system packages description: "Deploy a model with both Python and system dependencies" --- -[LayoutLM Document QA](https://huggingface.co/impira/layoutlm-document-qa) is a multimodal model that answers questions about provided invoice documents. The model requires a system package `tesseract-ocr`, which we need to include in the model serving environment. +## Summary + +To add system packages to your model serving environment, open `config.yaml` and update the `system_packages` key with a list of apt-installable Debian packages. + +Example code: + +```yaml config.yaml +system_packages: +- tesseract-ocr +``` + +## Step-by-step example + +[LayoutLM Document QA](https://huggingface.co/impira/layoutlm-document-qa) is a multimodal model that answers questions about provided invoice documents. + +The model requires a system package, `tesseract-ocr`, which we need to include in the model serving environment. You can see the code for the finished LayoutLM Document QA Truss on the right. Keep reading for step-by-step instructions on how to build it. @@ -24,7 +39,7 @@ Get started by creating a new Truss: truss init layoutlm-document-qa ``` -Select the default `TrussServer` option. Then, navigate to the newly created directory: +Give your model a name when prompted, like `LayoutLM Document QA`. Then, navigate to the newly created directory: ```sh cd layoutlm-document-qa @@ -32,7 +47,7 @@ cd layoutlm-document-qa ### Step 1: Implement the `Model` class -LayoutLM Document QA is [a `transformers.pipeline` model](https://huggingface.co/docs/transformers/main_classes/pipelines) which are straightforward to implement in Truss. +LayoutLM Document QA is [a pipeline model](https://huggingface.co/docs/transformers/main_classes/pipelines), so it is straightforward to implement in Truss. In `model/model.py`, we write the class `Model` with three member functions: @@ -123,7 +138,7 @@ truss push You can invoke the model with: ```sh -truss predict +truss predict -d '{"url": "https://templates.invoicehome.com/invoice-template-us-neat-750px.png", "prompt": "What is the invoice number?"}' ``` @@ -132,7 +147,7 @@ truss predict environment_variables: {} external_package_dirs: [] model_metadata: {} -model_name: null +model_name: LayoutLM Document QA python_version: py39 requirements: - Pillow==10.0.0 diff --git a/docs/images/user-workflow.png b/docs/images/user-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..f9dbe2ebbface5c5b153ddf36872d5d1c54a9ccc GIT binary patch literal 98931 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

_J2~97R*`WwKF+J~1AaB}) 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 MKzD!qUW@Dh4~rgQXaE2J literal 0 HcmV?d00001 diff --git a/docs/learn/intro.mdx b/docs/learn/intro.mdx index a6c718e6b..cc1d5492f 100644 --- a/docs/learn/intro.mdx +++ b/docs/learn/intro.mdx @@ -3,7 +3,9 @@ title: Learn model deployment description: "Learn to package and deploy ML models" --- -This step-by-step tutorial will take you from the "Hello, World!" of model deployment to building your own drop-in replacement for ChatGPT backed by a state-of-the-art open-source large language model. +{/* This step-by-step tutorial will take you from the "Hello, World!" of model deployment to building your own drop-in replacement for ChatGPT backed by a state-of-the-art open-source large language model. */} + +This step-by-step tutorial will walk you through the essential features and workflows of deploying a model with Truss. - Expand on Truss 101 with a complex model on a GPU server. + (Coming soon) Expand on Truss 101 with a complex model on a GPU server. diff --git a/docs/learn/model-serving/echo.mdx b/docs/learn/model-serving/echo.mdx deleted file mode 100644 index f3366ffe7..000000000 --- a/docs/learn/model-serving/echo.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: "Step 1: Echo server" -description: "Implement a simple echo server." ---- - -You don't need an ML model to learn the core dev loop when working with Truss. Instead, we'll write a quick echo server, which simply echos your input back to you. - -This step introduces the `my-truss/model/model.py` file. This is one of two essential files in a Truss. In this file, you implement the `Model` class, which the model server runs. - -### Implement the echo server - -The `Model.predict()` function runs every time the model server endpoint is called. - - -Check the "Diff" tab to see exactly which lines of code change in each step. - - -Open `model/model.py` and update `predict()` to echo the `model_input`: - - - -```python model/model.py -def predict(self, model_input: Any) -> Any: - return model_input -``` - - -```diff model/model.py -def predict(self, model_input: Any) -> Any: -- model_output = {} -- # Invoke model on model_input and calculate predictions here. -- model_output["predictions"] = [] -- return model_output -+ return model_input -``` - - - -That's all the code for this step! Next, we'll deploy this echo server. - - - -```python model/model.py ● -from typing import Any - - -class Model: - def __init__(self, **kwargs) -> None: - self._data_dir = kwargs["data_dir"] - self._config = kwargs["config"] - self._secrets = kwargs["secrets"] - self._model = None - - def load(self): - # Load model here and assign to self._model. - pass - - def predict(self, model_input: Any) -> Any: - return model_input -``` - - - - diff --git a/docs/learn/model-serving/init.mdx b/docs/learn/model-serving/init.mdx index ce13cc592..80ab6ba8b 100644 --- a/docs/learn/model-serving/init.mdx +++ b/docs/learn/model-serving/init.mdx @@ -1,8 +1,15 @@ --- -title: "Step 0: Setup" -description: "Set up your development environment and create a Truss." +title: "Step 0: Create a Truss" +description: "Set up your development environment and create a Truss" --- +This tutorial will guide you step-by-step through: + +1. Creating a Truss +2. Connecting your local development environment to a model server +3. Deploying a [basic text classification model](https://huggingface.co/docs/transformers/main_classes/pipelines#transformers.pipeline) +4. Publishing your model to production + On the right side, you'll see what your Truss should look like after each step! @@ -20,39 +27,47 @@ pip install --upgrade truss Use the `truss init ` command to create a Truss. ```sh -truss init my-truss +truss init my-first-truss ``` -Select the default `TrussServer` option. Then, navigate to the newly created directory: +When prompted, give your Truss a name like `My First Truss` + +### Get ready to code! + +Navigate to the newly created directory: ```sh -cd my-truss +cd my-first-truss ``` +Open the directory in your favorite text editor, such as Visual Studio Code. + ```python model/model.py -from typing import Any - - class Model: - def __init__(self, **kwargs) -> None: - self._data_dir = kwargs["data_dir"] - self._config = kwargs["config"] - self._secrets = kwargs["secrets"] + def __init__(self, **kwargs): self._model = None def load(self): - # Load model here and assign to self._model. pass - def predict(self, model_input: Any) -> Any: - model_output = {} - # Invoke model on model_input and calculate predictions here. - model_output["predictions"] = [] - return model_output + def predict(self, model_input): + return model_input +``` + +```yaml config.yaml +environment_variables: {} +model_name: My First Truss +requirements: [] +resources: + accelerator: null + cpu: "1" + memory: 2Gi + use_gpu: false +secrets: {} +system_packages: [] ``` - diff --git a/docs/learn/model-serving/model-load.mdx b/docs/learn/model-serving/model-load.mdx index 9c8012a17..cb7affbac 100644 --- a/docs/learn/model-serving/model-load.mdx +++ b/docs/learn/model-serving/model-load.mdx @@ -1,29 +1,34 @@ --- title: "Step 5: Implement model load" -description: "Load an ML model into your model server" +description: "Load an ML model into your Truss" --- -In this step, we'll upgrade to an actual ML model: a text classification (sentiment analysis) model that will tell us if a snippet of text has a positive or negative tone. +The other essential file in a Truss is `model/model.py`. In this file, you write a `Model` class: an interface between the ML model that you're packaging and the model server that you're running it on. - -On the right side, you'll see what your Truss should look like after each step! - - -The `transformers` Python package is useful for working with all kinds of models, including the large language model (LLM) that we're going to work with in the Truss 201 module. It has a `pipeline` function that gives you access to a number of simple models with minimal config. +Open `model/model.py` in your text editor. ### Import transformers -Let's import `transformers.pipeline` at the top of `model/model.py`: +Import `transformers.pipeline` at the top of `model/model.py`: -```python + + +```python model/model.py from transformers import pipeline ``` + + +```diff model/model.py ++ from transformers import pipeline +``` + + ### Load the model -In Truss, the `Model.load()` function runs once when the model server is spun up. +The `Model.load()` function runs exactly once when the model server is spun up or patched and loads the model onto the model server. -We'll update `load()` to use the `text-classification` model from `transformers.pipeline`. +Update `load()` to bring in the `text-classification` model from `transformers.pipeline`: @@ -42,29 +47,38 @@ def load(self): -If you're paying attention to `truss watch`, you'll see that this step actually resulted in an error! That's ok. Errors are part of a developer loop. We'll fix it in the next step. +You should see this change patched onto the model server in your `truss watch` terminal tab. ```python model/model.py ● -from typing import Any from transformers import pipeline class Model: - def __init__(self, **kwargs) -> None: - self._data_dir = kwargs["data_dir"] - self._config = kwargs["config"] - self._secrets = kwargs["secrets"] + def __init__(self, **kwargs): self._model = None def load(self): self._model = pipeline("text-classification") - def predict(self, model_input: Any) -> Any: + def predict(self, model_input): return model_input ``` - +```yaml config.yaml +environment_variables: {} +model_name: My First Truss +requirements: + - torch==2.0.1 + - transformers==4.30.0 +resources: + accelerator: null + cpu: "1" + memory: 2Gi + use_gpu: false +secrets: {} +system_packages: [] +``` diff --git a/docs/learn/model-serving/model-predict.mdx b/docs/learn/model-serving/model-predict.mdx index f6fd59d4a..3e612eb13 100644 --- a/docs/learn/model-serving/model-predict.mdx +++ b/docs/learn/model-serving/model-predict.mdx @@ -1,42 +1,40 @@ --- -title: "Step 7: Implement model inference" -description: "Implement invocation for our new ML model" +title: "Step 6: Implement model inference" +description: "Add model inference and invoke the model server" --- -To finish this tutorial, we'll implement model inference and invoke our finished model. +To complete `model/model.py`, we'll implement model inference and invoke our finished model. ### Run model inference -In Truss, the `Model.predict()` function runs every time the model server endpoint is called. +The `Model.predict()` function runs every time the model server is called. -We'll call the model in `predict()` and return the results: +We'll use the text classification model in `predict()` and return the results: ```python model/model.py -def predict(self, model_input: Any) -> Any: +def predict(self, model_input): return self._model(model_input) ``` ```diff model/model.py -def predict(self, model_input: Any) -> Any: +def predict(self, model_input): - return model_input + return self._model(model_input) ``` -## Invoke your finished model +### Invoke your finished model -After `truss watch` shows that the server is updated, you're ready to go! If you haven't been using `truss watch`, you can update your model manually by running `truss push` again instead. - -Once the model server is ready, you can invoke your model the same was as before. +After `truss watch` shows that the server is updated, it's time to invoke your finished model using `truss predict` in your terminal: **Invocation** ```sh -truss predict -d '"I am happy!"' +truss predict -d '"Truss is awesome!"' ``` **Response** @@ -53,24 +51,33 @@ truss predict -d '"I am happy!"' ```python model/model.py ● -from typing import Any from transformers import pipeline class Model: - def __init__(self, **kwargs) -> None: - self._data_dir = kwargs["data_dir"] - self._config = kwargs["config"] - self._secrets = kwargs["secrets"] + def __init__(self, **kwargs): self._model = None def load(self): self._model = pipeline("text-classification") - def predict(self, model_input: Any) -> Any: + def predict(self, model_input): return self._model(model_input) ``` - +```yaml config.yaml +environment_variables: {} +model_name: My First Truss +requirements: + - torch==2.0.1 + - transformers==4.30.0 +resources: + accelerator: null + cpu: "1" + memory: 2Gi + use_gpu: false +secrets: {} +system_packages: [] +``` diff --git a/docs/learn/model-serving/predict.mdx b/docs/learn/model-serving/predict.mdx index b9f32155c..c54228dfa 100644 --- a/docs/learn/model-serving/predict.mdx +++ b/docs/learn/model-serving/predict.mdx @@ -1,53 +1,58 @@ --- -title: "Step 3: Invoke the model" -description: "Invoke our deployed \"model\" (aka echo server)." +title: "Step 2: Call the model server" +description: "Call predict to test the model server" --- -Once your model has finished deploying, you can call it from your terminal. +Once your model server has finished deploying, you can call it from your terminal. -### Run model invocation +### Call the model server -Model input must be JSON-serializable. For our echo server, we'll just send a basic string. +Model server input must be JSON-serializable: a string, number, list, or dictionary. Note the double quotes to make the data parameter a string. + +`truss predict` uses the same data formatting on the as cURL. **Invocation** ```sh -truss predict -d '"I am happy!"' +truss predict -d '"Truss is awesome!"' ``` **Response** ```json -"I am happy!" +"Truss is awesome!" ``` -Our model server is up and running. We can push to it and run model inference. In the next step, we'll set up the last piece of the developer loop on Truss. +Our model server is up and responding to requests. In the next step, we'll set up the last piece of the developer loop on Truss. ```python model/model.py -from typing import Any - - class Model: - def __init__(self, **kwargs) -> None: - self._data_dir = kwargs["data_dir"] - self._config = kwargs["config"] - self._secrets = kwargs["secrets"] + def __init__(self, **kwargs): self._model = None def load(self): - # Load model here and assign to self._model. pass - def predict(self, model_input: Any) -> Any: + def predict(self, model_input): return model_input ``` - - +```yaml config.yaml +environment_variables: {} +model_name: My First Truss +requirements: [] +resources: + accelerator: null + cpu: "1" + memory: 2Gi + use_gpu: false +secrets: {} +system_packages: [] +``` diff --git a/docs/learn/model-serving/publish.mdx b/docs/learn/model-serving/publish.mdx new file mode 100644 index 000000000..d5a9f76c8 --- /dev/null +++ b/docs/learn/model-serving/publish.mdx @@ -0,0 +1,84 @@ +--- +title: "Step 7: Publish your Truss" +description: "Push your Truss to production-ready infrastructure" +--- + +When you're happy with your Truss, it's time to publish it to production. This re-builds the model server on production-ready infrastructure. + +Before publishing your Truss, you can turn off `truss watch` as it only patches models under development, not published models. + +### Publish your Truss + +To publish your Truss, run: + +```sh +truss push --publish +``` + +Re-building your model server takes more time than patching it; it'll be a moment until the new server is ready to be called. + +### Call the published model + +Once the new model server is live, call it with `truss predict`: + +**Invocation** + +```sh +truss predict --published -d '"Truss is awesome!"' +``` + +**Response** + +```json +[ + { + "label": "POSITIVE", + "score": 0.999873161315918 + } +] +``` + +### Review your learning + +In this tutorial, you learned how to: + +1. Create a Truss +2. Connect your local development environment to a model server +3. Deploy a [basic text classification model](https://huggingface.co/docs/transformers/main_classes/pipelines#transformers.pipeline) +4. Publish your model to production + +For more step-by-step instructions, move on to the [Truss 201 tutorial](/learn/llms/init). Or, to find an example that matches your use case, see the [Truss examples docs](/examples). + + + +```python model/model.py +from transformers import pipeline + + +class Model: + def __init__(self, **kwargs): + self._model = None + + def load(self): + self._model = pipeline("text-classification") + + def predict(self, model_input): + return self._model(model_input) +``` + +```yaml config.yaml +environment_variables: {} +model_name: My First Truss +requirements: + - torch==2.0.1 + - transformers==4.30.0 +resources: + accelerator: null + cpu: "1" + memory: 2Gi + use_gpu: false +secrets: {} +system_packages: [] +``` + + diff --git a/docs/learn/model-serving/push.mdx b/docs/learn/model-serving/push.mdx index df8608621..195e1f3b3 100644 --- a/docs/learn/model-serving/push.mdx +++ b/docs/learn/model-serving/push.mdx @@ -1,53 +1,59 @@ --- -title: "Step 2: Deploy the model server" -description: "Deploy our simple echo server to production." +title: "Step 1: Spin up a model server" +description: "Configure a remote host and push the base Truss" --- -Truss is maintained by [Baseten](https://baseten.co), which provides infrastructure for running ML models in production. We'll use Baseten as the remote host for your model. +This is the first of three steps dedicated to learning the developer loop that Truss enables. We hope that once you try it, you'll agree with us that it's the most productive way to deploy ML models. -### Get an API key + +If this is your first time running `truss push`, you'll need to configure a remote host for your model server. -To set up the Baseten remote, you'll need a [Baseten API key](https://app.baseten.co/settings/account/api_keys). If you don't have a Baseten account, no worries, just [sign up for an account](https://app.baseten.co/signup/) and you'll be issued plenty of free credits to get you started. +Truss is maintained by [Baseten](https://baseten.co), which provides infrastructure for running ML models in production. We'll use Baseten as the remote host for your model server. -### Deploy your Truss +To set up the Baseten remote, you'll need a [Baseten API key](https://app.baseten.co/settings/account/api_keys). -Deploying a model with Truss uses the interactive `truss push` command. The first time you run the command, it will walk you through setting up and a remote host to run your model. +If you don't have a Baseten account, no worries, just [sign up for an account](https://app.baseten.co/signup/) and you'll be issued plenty of free credits to get you started. + -With your Baseten API key ready to go, you can deploy your model: +### Push your Truss + +To spin up a model server from your Truss, run: ```sh truss push ``` -You'll be prompted for a few setup values. To complete the deployment: - -1. Accept the default value for remote url (`https://app.baseten.co`) -2. Paste your API key when prompted -3. Give the model a name, like `my-first-model` +Paste your Baseten API key if prompted. -You can monitor your model deployment on [your model dashboard on Baseten](https://app.baseten.co/models/). + +Open up [your model dashboard on Baseten](https://app.baseten.co/models/) to monitor your deployment and view model server logs. + ```python model/model.py -from typing import Any - - class Model: - def __init__(self, **kwargs) -> None: - self._data_dir = kwargs["data_dir"] - self._config = kwargs["config"] - self._secrets = kwargs["secrets"] + def __init__(self, **kwargs): self._model = None def load(self): - # Load model here and assign to self._model. pass - def predict(self, model_input: Any) -> Any: + def predict(self, model_input): return model_input ``` - +```yaml config.yaml +environment_variables: {} +model_name: My First Truss +requirements: [] +resources: + accelerator: null + cpu: "1" + memory: 2Gi + use_gpu: false +secrets: {} +system_packages: [] +``` diff --git a/docs/learn/model-serving/requirements.mdx b/docs/learn/model-serving/requirements.mdx index e69cc6ece..d534d9009 100644 --- a/docs/learn/model-serving/requirements.mdx +++ b/docs/learn/model-serving/requirements.mdx @@ -1,19 +1,33 @@ --- -title: "Step 6: Set requirements" -description: "Add the model's Python requirements to the model server." +title: "Step 4: Set Python requirements" +description: "Add required Python packages to the model server." --- -This step introduces the other essential file in a Truss: `my-truss/config.yaml`, which controls the model server's configuration. For a complete list of the config options, see [the config reference](/reference/config). +For this tutorial, we want to package a [basic text classification model](https://huggingface.co/docs/transformers/main_classes/pipelines#transformers.pipeline) with this Truss to deploy it on our model server. The model is from the `transformers` package and thus requires two Python packages to run it: [Transformers](https://huggingface.co/docs/transformers/index) and [PyTorch](https://pytorch.org/). + +We'll use `config.yaml` to add the Python packages that the text classification model requires to the model server. + + +On the right side, you'll see what your Truss should look like after each step! + + +### Open `config.yaml` in a text editor + +One of the two essential files in a Truss is `config.yaml`, which configures the model serving environment. For a complete list of the config options, see [the config reference](/reference/config). -Note that the `model_name` parameter is already set to `my-first-model` or whatever name you picked the first time you ran `truss push`. Don't change this name as it must remain consistant with the remote host. +Note that the `model_name` parameter is already set to `My First Truss` or whatever name you picked when you ran `truss init`. Don't change this name as it must remain consistant with the remote host. ### Add Python requirements -All configuration for your model happens in `config.yaml`. +Python requirements are listed just like they appear in a `requirements.txt`. ML moves fast; always pin your requirement versions to make sure you're getting compatible packages. + +Update `config.yaml` with the required packages: -We'll set Python requirements. ML moves fast, always pin your requirements to make sure you're getting the right version. + +Check the "Diff" tab to see exactly which lines of code change. + @@ -33,49 +47,42 @@ requirements: - -You can set system requirements the same way with `system_packages`. - +### Check for a patch -On your `truss watch` tab, you should see that the model server is no longer showing an error. +After you save your changes to `config.yaml`, you should see two things happen: + +1. Your `truss watch` tab should show that a patch was applied to your model server. +2. The model server logs on your [Baseten account](https://app.baseten.co) should show log entries from the packages being installed in your model server. + +Now you're ready to add the text classification model. ```yaml config.yaml ● environment_variables: {} -external_package_dirs: [] -model_metadata: {} -model_name: my-first-truss -python_version: py39 +model_name: My First Truss requirements: - torch==2.0.1 - transformers==4.30.0 resources: accelerator: null - cpu: 500m - memory: 512Mi + cpu: "1" + memory: 2Gi use_gpu: false secrets: {} system_packages: [] ``` ```python model/model.py -from typing import Any -from transformers import pipeline - - class Model: - def __init__(self, **kwargs) -> None: - self._data_dir = kwargs["data_dir"] - self._config = kwargs["config"] - self._secrets = kwargs["secrets"] + def __init__(self, **kwargs): self._model = None def load(self): - self._model = pipeline("text-classification") + pass - def predict(self, model_input: Any) -> Any: - return self._model(model_input) + def predict(self, model_input): + return model_input ``` diff --git a/docs/learn/model-serving/watch.mdx b/docs/learn/model-serving/watch.mdx index 73f146c9f..7367d6891 100644 --- a/docs/learn/model-serving/watch.mdx +++ b/docs/learn/model-serving/watch.mdx @@ -1,13 +1,17 @@ --- -title: "Step 4: Watch for changes" -description: "Keep remote model server synced with local dev." +title: "Step 3: Enable live reload" +description: "Patch the model server with local changes" --- You don't have to run `truss push` every time you update your Truss during development. Instead, you can have a live reload workflow where local changes automatically are pushed to the production environment to give you a lightning-fast feedback loop. -### Set up Truss watch +### Watch for changes -In a new terminal tab, run: + +Run the `truss watch` command in a new terminal tab in the same working directory, as you'll need to leave it running while you work. + + +In a new terminal tab in the same `my-first-truss` working directory, run: ```sh truss watch @@ -15,31 +19,37 @@ truss watch Now, as you update your Truss, the changes will be patched onto the model server on the remote host. -Leave `truss watch` running in this new terminal tab for the rest of the tutorial. You can use `Control-C` to exit `truss watch` to stop automatically pushing changes. +### Use `truss watch` while working + +In the next three steps, we'll deploy a basic ML model to our model server. Check your `truss watch` tab and model logs on Baseten after each step to see the changes you make locally reflected live on your model server. -In the next three steps, we'll upgrade our echo server to instead run an ML model. Check the output of `truss watch` after every step to see your local changes reflected live on the production server! +Leave `truss watch` running in this new terminal tab for the rest of the tutorial. You can use `Control-C` to exit `truss watch` and stop automatically pushing changes. ```python model/model.py -from typing import Any -from transformers import pipeline - - class Model: - def __init__(self, **kwargs) -> None: - self._data_dir = kwargs["data_dir"] - self._config = kwargs["config"] - self._secrets = kwargs["secrets"] + def __init__(self, **kwargs): self._model = None def load(self): - self._model = pipeline("text-classification") + pass - def predict(self, model_input: Any) -> Any: - return self._model(model_input) + def predict(self, model_input): + return model_input ``` - +```yaml config.yaml +environment_variables: {} +model_name: My First Truss +requirements: [] +resources: + accelerator: null + cpu: "1" + memory: 2Gi + use_gpu: false +secrets: {} +system_packages: [] +``` diff --git a/docs/mint.json b/docs/mint.json index 60f8fe2d9..3d8878d0e 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -8,7 +8,7 @@ { "icon": "b", "name": "Baseten", - "url": "https://baseten.co" + "url": "https://www.baseten.co" }, { "icon": "newspaper", @@ -58,34 +58,8 @@ "examples/private-model", "examples/system-packages", "examples/streaming", - "examples/local-model", - "examples/bundled-package", - { - "group": "Performance optimization", - "pages": [ - "examples/performance/cached-weights", - "examples/performance/tgi-server", - "examples/performance/vllm-server" - ] - }, - { - "group": "Foundation models", - "pages": [ - "examples/models/overview", - "examples/models/wizardlm", - "examples/models/sdxl", - "examples/models/whisper" - ] - }, - { - "group": "Frameworks", - "pages": [ - "examples/frameworks/pytorch", - "examples/frameworks/sklearn", - "examples/frameworks/tensorflow", - "examples/frameworks/xgboost" - ] - } + "examples/performance/cached-weights", + "examples/performance/vllm-server" ] }, { @@ -105,34 +79,37 @@ "group": "Truss 101: Model serving", "pages": [ "learn/model-serving/init", - "learn/model-serving/echo", "learn/model-serving/push", "learn/model-serving/predict", "learn/model-serving/watch", - "learn/model-serving/model-load", "learn/model-serving/requirements", - "learn/model-serving/model-predict" + "learn/model-serving/model-load", + "learn/model-serving/model-predict", + "learn/model-serving/publish" ] }, { - "group": "Truss 201: Adding GPUs", + "group": "Truss reference", "pages": [ - "learn/llms/init", - "learn/llms/resources", - "learn/llms/model-load", - "learn/llms/private", - "learn/llms/cache", - "learn/llms/model-predict", - "learn/llms/streaming" + "reference/config" ] }, { - "group": "Reference", + "group": "CLI reference", "pages": [ - "reference/config", - "reference/model-class", - "reference/servers", - "reference/cli" + "reference/cli", + "reference/cli/init", + "reference/cli/push", + "reference/cli/watch", + "reference/cli/predict", + { + "group": "Advanced usage", + "pages": [ + "reference/cli/image", + "reference/cli/container", + "reference/cli/cleanup" + ] + } ] }, { @@ -146,9 +123,7 @@ { "group": "Deep dives", "pages": [ - "contribute/base-images", - "contribute/patching", - "contribute/remote-spec" + "contribute/base-images" ] } ], diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index c76d0f86b..5a3ad698e 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -3,11 +3,13 @@ title: 'Quickstart' description: 'Create, deploy, and invoke an ML model server in less than 5 minutes' --- -In this quickstart guide, we'll package and deploy a [text classification pipeline model](https://huggingface.co/docs/transformers/main_classes/pipelines) from the open-source `transformers` package. +In this quickstart guide, you will package and deploy a [text classification pipeline model](https://huggingface.co/docs/transformers/main_classes/pipelines). -If you want to go step-by-step through these concepts and more, check out the [learn model deployment tab](/learn/intro) for a detailed introduction to model deployment and Truss. + +If you want to go step-by-step through these concepts and more, check out the [learn model deployment tutorial](/learn/intro) for a detailed introduction to model deployment and Truss. + -## Install Truss +## Install the Truss package Install the latest version of Truss with: @@ -23,7 +25,9 @@ To get started, create a Truss with the following terminal command: truss init text-classification ``` -Select the default `TrussServer` option. Then, navigate to the newly created directory: +When prompted, give your Truss a name like `Text classification`. + +Then, navigate to the newly created directory: ```sh cd text-classification @@ -31,48 +35,79 @@ cd text-classification ## Implement the `Model` class -The `Model` class is the heart of a Truss. In it, you write similar code to what you'd write to invoke a model locally in a Python notebook or script. - - - - +One of the two essential files in a Truss is `model/model.py`. In this file, you write a `Model` class: an interface between the ML model that you're packaging and the model server that you're running it on. -The model serving code goes in `./text-classification/model/model.py` in your newly created Truss. There are two functions to implement: +There are two member functions that you must implement in the `Model` class: -* `load()` runs once when the model is spun up and is responsible for initializing `self._model` -* `predict()` runs each time the model is invoked and handles the inference. It can use any JSON-serializable type as input and output. +* `load()` loads the model onto the model server. It runs exactly once when the model server is spun up or patched. +* `predict()` handles model inference. It runs every time the model server is called. Here's the complete `model/model.py` for the text classification model: + + ```python model/model.py -from typing import List from transformers import pipeline class Model: - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs): self._model = None def load(self): self._model = pipeline("text-classification") - def predict(self, model_input: str) -> List: + def predict(self, model_input): return self._model(model_input) ``` + + +```diff model/model.py ++ from transformers import pipeline + + +class Model: + def __init__(self, **kwargs): + self._model = None + + def load(self): +- pass ++ self._model = pipeline("text-classification") + + def predict(self, model_input): +- return model_input ++ return self._model(model_input) +``` + + ## Add model dependencies -The pipeline model relies on Transformers and PyTorch. These dependencies must be specified in the Truss config. +The other essential file in a Truss is `config.yaml`, which configures the model serving environment. For a complete list of the config options, see [the config reference](/reference/config). -In `./text-classification/config.yaml`, find the line `requirements`. Replace the empty list with: +The pipeline model relies on [Transformers](https://huggingface.co/docs/transformers/index) and [PyTorch](https://pytorch.org/). These dependencies must be specified in the Truss config. +In `config.yaml`, find the line `requirements`. Replace the empty list with: + + + ```yaml config.yaml requirements: - torch==2.0.1 - transformers==4.30.0 ``` + + +```diff config.yaml +- requirements: [] ++ requirements: ++ - torch==2.0.1 ++ - transformers==4.30.0 +``` + + -No other configuration needs to be changed. +No other configuration is needed. ## Deploy the Truss @@ -84,15 +119,13 @@ To set up the Baseten remote, you'll need a [Baseten API key](https://app.basete ### Run `truss push` -Deploying a model with Truss uses the interactive `truss push` command. The first time you run the command, it will walk you through setting up and a remote host to run your model. - With your Baseten API key ready to paste when prompted, you can deploy your model: ```sh truss push ``` -You can monitor your model deployment on [your model dashboard on Baseten](https://app.baseten.co/models/). +You can monitor your model deployment from [your model dashboard on Baseten](https://app.baseten.co/models/). ## Invoke the model @@ -101,7 +134,7 @@ After the model has finished deploying, you can invoke it from the terminal. **Invocation** ```sh -truss predict -d '"I am happy!"' +truss predict -d '"Truss is awesome!"' ``` **Response** @@ -114,3 +147,11 @@ truss predict -d '"I am happy!"' } ] ``` + +## Learn more + +You've completed the quickstart by packaging, deploying, and invoking an ML model with Truss! Next up: + +* Discover a live reload model serving workflow with the [Truss user guide](/usage). +* Go step-by-step through essential concepts in the [Truss tutorial series](/learn/intro). +* Find a [Truss example](/examples) that matches your use case. diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index 832c79f47..56ff1aa02 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -1,4 +1,49 @@ --- -title: "CLI reference" -description: "Description" +title: "truss" +description: "The simplest way to serve models in production" --- + +``` Usage +truss [OPTIONS] COMMAND [ARGS]... +``` + +### Options + + +Show the version and exit. + + + +Show help message and exit. + + +### Main usage + + + +Create a new Truss. + + +Pushes a Truss to a TrussRemote. + + +Seamless remote development with Truss. + + +Invokes the packaged model. + + + +### Advanced usage + + + +Subcommands for `truss image`. + + +Subcommands for `truss container`. + + +Clean up Truss data. + + diff --git a/docs/reference/cli/cleanup.mdx b/docs/reference/cli/cleanup.mdx new file mode 100644 index 000000000..93e1ba7af --- /dev/null +++ b/docs/reference/cli/cleanup.mdx @@ -0,0 +1,16 @@ +--- +title: "truss cleanup" +description: "Clean up truss data." +--- + +``` +truss cleanup [OPTIONS] +``` + +Truss creates temporary directories for various operations such as for building Docker images. This command clears that data to free up disk space. + +### Options + + +Show help message and exit. + diff --git a/docs/reference/cli/container.mdx b/docs/reference/cli/container.mdx new file mode 100644 index 000000000..fdb166d79 --- /dev/null +++ b/docs/reference/cli/container.mdx @@ -0,0 +1,68 @@ +--- +title: "truss container" +description: "Subcommands for truss container." +--- + +``` +truss container [OPTIONS] COMMAND [ARGS]... +``` + +### Options + + +Show help message and exit. + + +## truss container kill + +Kills containers related to Truss. + +``` +truss container kill [OPTIONS] [TARGET_DIRECTORY] +``` + +### Options + + +Show help message and exit. + + +### Arguments + + +A Truss directory. If none, use current directory. + + +## truss container kill-all + +Kills all Truss containers that are not manually persisted. + +``` +truss container kill-all [OPTIONS] +``` + +### Options + + +Show help message and exit. + + +## truss container logs + +Get logs in a container is running for a Truss. + +``` +truss container logs [OPTIONS] [TARGET_DIRECTORY] +``` + +### Options + + +Show help message and exit. + + +### Arguments + + +A Truss directory. If none, use current directory. + diff --git a/docs/reference/cli/image.mdx b/docs/reference/cli/image.mdx new file mode 100644 index 000000000..0bc186dfc --- /dev/null +++ b/docs/reference/cli/image.mdx @@ -0,0 +1,95 @@ +--- +title: "truss image" +description: "Subcommands for truss image." +--- + +``` +truss image [OPTIONS] COMMAND [ARGS]... +``` + +### Options + + +Show help message and exit. + + +## truss image build + +Builds the docker image for a Truss. + +``` +truss image build [OPTIONS] [TARGET_DIRECTORY] [BUILD_DIR] +``` + +### Options + + +Docker image tag. + + +Show help message and exit. + + +### Arguments + + +A Truss directory. If none, use current directory. + + +Image context. If none, a temp directory is created. + + +## truss image build-context + +Create a docker build context for a Truss. + +``` +truss image build-context [OPTIONS] BUILD_DIR [TARGET_DIRECTORY] +``` + +### Options + + +Show help message and exit. + + +### Arguments + + +Folder where image context is built for Truss. + + +A Truss directory. If none, use current directory. + + +## truss image run + +Runs the docker image for a Truss. + +``` +truss image run [OPTIONS] [TARGET_DIRECTORY] [BUILD_DIR] +``` + +### Options + + +Docker build image tag. + + +Local port used to run image. + + +Flag for attaching the process. + + +Show help message and exit. + + +### Arguments + + +A Truss directory. If none, use current directory. + + +Image context. If none, a temp directory is created. + diff --git a/docs/reference/cli/init.mdx b/docs/reference/cli/init.mdx new file mode 100644 index 000000000..5e4da7c32 --- /dev/null +++ b/docs/reference/cli/init.mdx @@ -0,0 +1,35 @@ +--- +title: "truss init" +description: "Create a new Truss." +--- + +``` +truss init [OPTIONS] TARGET_DIRECTORY +``` + +### Options + + +Create a trainable truss. Deprecated. + + + +What type of server to create. Default: `TrussServer`. + + + +Show help message and exit. + + +### Arguments + + +A Truss is created in this directory + + + +### Example + +``` +truss init whisper-truss +``` diff --git a/docs/reference/cli/predict.mdx b/docs/reference/cli/predict.mdx new file mode 100644 index 000000000..63d802d4d --- /dev/null +++ b/docs/reference/cli/predict.mdx @@ -0,0 +1,40 @@ +--- +title: "truss predict" +description: "Invokes the packaged model." +--- + +``` +truss predict [OPTIONS] +``` + +### Options + + +A Truss directory. If none, use current directory. + + +Name of the remote in .trussrc to patch changes to. + + +String formatted as json that represents request. + + +Path to json file containing the request. + + +Invoked the published model version. + + +Show help message and exit. + + + +### Examples + +``` +truss predict -d '{"prompt": "What is the meaning of life?"}' +``` + +``` +truss predict --published -f my-prompt.json +``` diff --git a/docs/reference/cli/push.mdx b/docs/reference/cli/push.mdx new file mode 100644 index 000000000..561484326 --- /dev/null +++ b/docs/reference/cli/push.mdx @@ -0,0 +1,44 @@ +--- +title: "truss push" +description: "Pushes a truss to a TrussRemote." +--- + +``` Usage +truss push [OPTIONS] [TARGET_DIRECTORY] +``` + +### Options + + +Name of the remote in .trussrc to patch changes to + + +Publish truss as production deployment. + + +Give Truss access to secrets on remote host. + + +Show help message and exit. + + +### Arguments + + +A Truss directory. If none, use current directory. + + + +### Examples + +``` +truss push +``` + +``` +truss push --publish /path/to/my-truss +``` + +``` +truss push --remote baseten --publish --trusted +``` diff --git a/docs/reference/cli/watch.mdx b/docs/reference/cli/watch.mdx new file mode 100644 index 000000000..261141fca --- /dev/null +++ b/docs/reference/cli/watch.mdx @@ -0,0 +1,39 @@ +--- +title: "truss watch" +description: "Seamless remote development with truss." +--- + +``` +truss watch [OPTIONS] [TARGET_DIRECTORY] +``` + +### Options + + +Name of the remote in .trussrc to patch changes to + + + +Show help message and exit. + + +### Arguments + + +A Truss directory. If none, use current directory. + + + +### Examples + +``` +truss watch +``` + +``` +truss watch /path/to/my-truss +``` + +``` +truss watch --remote baseten +``` diff --git a/docs/reference/config.mdx b/docs/reference/config.mdx index 0af8891c4..f8921b6bb 100644 --- a/docs/reference/config.mdx +++ b/docs/reference/config.mdx @@ -6,7 +6,6 @@ description: "Set your model resources, dependencies, and more" Truss is configurable to its core. Every Truss must include a file config.yaml in its root directory, which is automatically generated when the Truss is created. However, configuration is optional. Every configurable value has a sensible default, and a completely empty config file is valid. - YAML syntax can be a bit non-obvious when dealing with empty lists and dictionaries. You may notice the following in the default Truss config file: ```yaml @@ -24,7 +23,6 @@ secrets: key1: default_value1 key2: default_value2 ``` - ## Config reference @@ -36,12 +34,6 @@ secrets: ```yaml WizardLM config description: An instruction-following LLM Using Evol-Instruct. environment_variables: {} -model_metadata: - example_model_input: {"prompt": "What is the difference between a wizard and a sorcerer?"} - avatar_url: https://cdn.baseten.co/production/static/explore/wizard.png - cover_image_url: https://cdn.baseten.co/production/static/explore/wizardlm.png - tags: - - text-generation model_name: WizardLM requirements: - accelerate==0.20.3 @@ -60,37 +52,4 @@ secrets: {} system_packages: [] ``` -```yaml SDXL config -description: Generate original images from text prompts. -environment_variables: {} -external_package_dirs: [] -model_metadata: - example_model_input: {"prompt": "A tree in a field under the night sky", "use_refiner": True} - pretty_name: Stable Diffusion XL - avatar_url: https://cdn.baseten.co/production/static/stability.png - cover_image_url: https://cdn.baseten.co/production/static/sd.png - tags: - - image-generation -model_name: Stable Diffusion XL -model_framework: custom -model_type: custom -python_version: py39 -requirements: -- transformers -- accelerate -- safetensors -- diffusers -- invisible-watermark>=0.2.0 -resources: - accelerator: A10G - cpu: 3500m - memory: 20Gi - use_gpu: true -secrets: {} -system_packages: -- ffmpeg -- libsm6 -- libxext6 -``` - diff --git a/docs/usage.mdx b/docs/usage.mdx index 53429535c..94ac1c9f5 100644 --- a/docs/usage.mdx +++ b/docs/usage.mdx @@ -2,3 +2,86 @@ title: User guide description: "Get up and running quickly with Truss' developer workflow" --- + +We built Truss because we were frustrated with the long feedback loops in ML model deployment. When you have to wait for your server to rebuild every time you make a change, iteration is painful. + +Meanwhile, web developers have enjoyed live reload workflows for years, where changes are patched onto a running server and available almost instantly. + +With Truss, you get the same live reload workflow for serving ML models. + + + + + +## Create a Truss + +```sh +truss init TARGET_DIRECTORY +``` + +A Truss is an abstraction for a model server. But more literally, it's a set of files. Running `truss init` creates those files in a target directory. + +To package a model with Truss, follow the [quickstart](/quickstart), a [step-by-step example](/examples), or an [example from GitHub](https://github.com/basetenlabs/truss-examples/). + +See the CLI reference for more information on [truss init](/reference/cli/init). + +## Spin up model server + +```sh +truss push +``` + +A model server takes requests, passes them through an ML model, and returns the model's output. When you're ready to start testing your Truss, use `truss push` to spin up a model server with your model and config. + +See the CLI reference for more information on [truss init](/reference/cli/push). + +## Test the model + +```sh +truss predict +``` + +Once your model server is live, you can invoke your model with `truss predict`. + +See the CLI reference for more information on [truss predict](/reference/cli/predict). + +## Watch for changes + + +Run the `truss watch` command in a new terminal tab in the same working directory, as you'll need to leave it running while you work. + + +```sh +truss watch +``` + +When you make a change with `truss watch` running, it will automatically attempt to patch that change onto the model server. Most changes to `model.py` and `config.yaml` can be patched. + + +The following changes should not be made in a live reload workflow: + +* Updates to `resources` in `config.yaml`, which must be set before the first `truss push` +* Changes to the `model_name` in `config.yaml`. Changing the model name requires a new `truss push` to create a new model server. + + +See the CLI reference for more information on [truss watch](/reference/cli/watch). + +## Publish your model + +Once you're happy with your model, stop `truss watch` and run: + +```sh +truss push --publish +``` + +This will re-build your model server on production infrastructure. + +## Use model in production + +To invoke the published model, run: + +```sh +truss predict --published +``` + +With [Baseten](https://baseten.co) as your [remote host](/remotes/baseten), your model is served behind [autoscaling infrastructure](https://docs.baseten.co/managing-models/resources) and is [available via an API endpoint](https://docs.baseten.co/building-with-models/invoke). diff --git a/docs/welcome.mdx b/docs/welcome.mdx index 25b61176e..8b538bad8 100644 --- a/docs/welcome.mdx +++ b/docs/welcome.mdx @@ -6,14 +6,14 @@ description: "The simplest way to serve AI/ML models in production" ## Why Truss? * **Write once, run anywhere:** Package and test model code, weights, and dependencies with a model server that behaves the same in development and production. -* **Fast developer loop:** Implement your model with fast feedback from a live reload server, and skip Docker and Kubernetes configuration with Truss' done-for-you model serving environment. +* **Fast developer loop:** Implement your model with fast feedback from a live reload server, and skip Docker and Kubernetes configuration with a batteries-included model serving environment. * **Support for all Python frameworks**: From `transformers` and `diffusors` to `PyTorch` and `Tensorflow` to `XGBoost` and `sklearn`, Truss supports models created with any framework, even entirely custom models. See Trusses for popular models including: -* 🦙 [Llama 2](/examples/models/llama-2) -* 🎨 [Stable Diffusion XL](/examples/models/sdxl) -* 🗣 [Whisper](/examples/models/whisper) +* 🦙 [Llama 2 7B](https://github.com/basetenlabs/truss-examples/tree/main/llama-2-7b-chat) ([13B](https://github.com/basetenlabs/truss-examples/tree/main/llama-2-13b-chat)) ([70B](https://github.com/basetenlabs/truss-examples/tree/main/llama-2-70b-chat)) +* 🎨 [Stable Diffusion XL](https://github.com/basetenlabs/truss-examples/tree/main/stable-diffusion-xl-1.0) +* 🗣 [Whisper](https://github.com/basetenlabs/truss-examples/tree/main/whisper-truss) and [dozens more examples on GitHub](https://github.com/basetenlabs/truss-examples/).