定义输入和输出类型

创建 BentoML 服务 (Service) 时,您需要指定服务 API 的输入和输出 (IO) 类型。这些类型是塑造服务 API 逻辑的核心,指导数据进出服务的流程。BentoML 支持 Python、Pydantic 中常用的大量数据类型,以及机器学习 (ML) 工作流程特有的类型。这确保了 BentoML 服务可以与不同的数据源和 ML 框架无缝集成。

本文档全面概述了 BentoML 中支持的服务 API 架构,并附有代码示例来说明其实现。

概览

BentoML 中支持的输入和输出类型包括:

  • 标准 Python 类型:基本类型如 strintfloatbooleanlistdict

  • Pydantic 字段类型:BentoML 将其类型支持扩展到 Pydantic 字段类型,为处理复杂数据架构提供了更结构化和经过验证的方法。

  • ML 特定类型:为了满足不同 ML 用例的需求,BentoML 支持 numpy.ndarraytorch.Tensortensorflow.Tensor 等类型来处理张量数据,pandas.DataFrame 用于处理表格数据,PIL.Image.Image 用于处理图像数据,以及 pathlib.Path 用于文件路径引用。

  • 根输入 (Root input):BentoML 支持根输入,它允许 API 接受单个仅位置参数,而无需在请求负载中指定键。

您可以使用 Python 的类型注解来定义每个 API 端点的预期输入和输出类型。这不仅有助于根据指定的架构验证数据,还增强了代码的清晰度和可读性。类型注解在生成 API、BentoML 客户端 和 UI 组件中起着重要作用,确保与服务交互的一致性和可预测性。

此外,您可以使用 pydantic.Field 来设置参数的附加信息,例如默认值和描述。这提高了 API 的可用性并提供了基本文档。详见以下示例。

定义 API 架构

本节提供了 BentoML 中支持的不同 API 架构的示例。每个示例都展示了如何为特定用例定义输入和输出类型。

标准 Python 类型

Python 的标准类型,如字符串、整数、浮点数、布尔值、列表和字典,通常用于简单的数据结构。您可以轻松地将这些类型集成到您的服务中。下面是一个示例:

from pydantic import Field
import bentoml

@bentoml.service
class LanguageModel:
    @bentoml.api
    def generate(
        self, prompt: str = Field(description="The prompt text"),
        temperature: float = Field(default=0.0, description="A sampling temperature between 0 and 2"),
        max_tokens: int = Field(default=1000, description="max tokens to use"),
    ) -> Generator[str, None, None]:
        # Implementation of the language generation model
        ...

此示例使用 Python 的类型注解(例如 strfloatint)来指定每个参数的预期数据类型。它返回一个生成器,可以流式传输响应。pydantic.Field 可用于设置参数的默认值和提供描述。

示例和可空输入

您可以定义接受带有示例或可空字段的输入的 API。

要设置示例值,您可以使用 pydantic.Field

from pydantic import Field
import bentoml

@bentoml.service
class IrisClassifier:
    @bentoml.api
    def classify(self, input: np.ndarray = Field(examples=[[0.1, 0.4, 0.2, 1.0]]) -> np.ndarray:
        ...

要处理可空输入,您可以使用 Optional

from pydantic import Field
from typing import Optional
import bentoml

@bentoml.service
class LanguageModel:
    @bentoml.api
    def generate(
        self, prompt: int = Field(description="The prompt text"),
        temperature: Optional[float] = Field(default=None, description="A sampling temperature between 0 and 2"),
        max_tokens: Optional[float] = Field(default=None, description="max tokens to use"),
    ) -> Generator[str, None, None]:
        ...

LanguageModel 类中,temperaturemax_tokens 字段被标记为 Optional,这意味着它们可以是 None。在 BentoML 中使用 Optional 类型时,必须提供默认值(此处为 default=None)。不支持通用联合类型。

Pydantic

Pydantic 模型支持更结构化且带验证的数据。当您的服务需要处理具有严格验证要求的复杂数据结构时,它们尤其有用。下面是一个示例:

from pydantic import BaseModel, Field
import bentoml

# Define a Pydantic model for structured data input
class AdsGenerationParams(BaseModel):
    prompt: str = Field(description="The prompt text")
    industry: str = Field(description="The industry the company belongs to")
    target_audience: str = Field(description="Target audience for the advertisement")
    temperature: float = Field(default=0.0, description="A sampling temperature between 0 and 2")

@bentoml.service
class AdsWriter:
    @bentoml.api
    def generate(self, params: AdsGenerationParams) -> str:
        # Implementation logic
        ...

在上面的代码片段中,AdsGenerationParams 类是一个 Pydantic 模型,定义了输入数据的结构和验证规则。类中的每个字段都带有类型注解,并且可以包含默认值和描述。Pydantic 会自动根据 AdsGenerationParams 架构验证接收到的数据。如果数据不符合架构,在方法执行之前会引发错误。

您还可以直接在顶层将 Pydantic 模型用于 BentoML 服务 API,而无需将负载包裹在一个键内。

from pydantic import BaseModel, Field
import typing as t
import bentoml

class AdsGenerationParams(BaseModel):
    prompt: str = Field(description="The prompt text")
    industry: str = Field(description="The industry the company belongs to")
    target_audience: str = Field(description="Target audience for the advertisement")
    temperature: float = Field(default=0.0, description="A sampling temperature between 0 and 2")

@bentoml.service
class AdsWriter:
    @bentoml.api(input_spec=AdsGenerationParams)
    def generate(self, **params: t.Any) -> str:

        # Access parameters from the request
        prompt = params['prompt']
        industry = params['industry']
        target_audience = params['target_audience']
        temperature = params['temperature']
        # Use the parameters in your Service logic
        # Implementation logic
        ...

在上面的代码片段中,来自传入请求的所有已验证和解析的字段作为存储在 params 字典中的关键字参数传递给 generate 方法。您可以直接通过 AdsGenerationParams 中定义的字段名称作为字典中的键来访问这些参数。

Pydantic 的 BaseModel 仅支持 Python 中的内置类型作为字段类型。您可以使用 bentoml.IODescriptor 代替 pydantic.BaseModel 来支持 numpy.ndarraypandas.DataFrametorch.Tensor 等类型。

import bentoml

class MyInputParams(bentoml.IODescriptor):
    data: np.ndarray[tuple[int], np.dtype[np.float16]]

文件

您可以使用 pathlib.Path 处理文件输入和输出。这对于处理音频、图像和文档等文件的服务非常有用。

这是一个简单的示例,接受一个 Path 对象作为输入,表示音频文件的路径。

from pathlib import Path
import bentoml

@bentoml.service
class WhisperX:
    @bentoml.api
    def to_text(self, audio: Path) -> str:
        # Implementation for converting audio files to text
        ...

要将文件类型限制为特定格式,例如音频文件,您可以使用带有 Annotated 类型的 ContentType 验证器。例如,您可以让 API 方法仅接受 MP3 音频文件:

from pathlib import Path
from bentoml.validators import ContentType
from typing import Annotated  # Python 3.9 or above
from typing_extensions import Annotated  # Older than 3.9
import bentoml

@bentoml.service
class WhisperX:
    @bentoml.api
    def to_text(self, audio: Annotated[Path, ContentType("audio/mp3")]) -> str:
        ...

要输出带有路径的文件,您可以使用 context.temp_dir 为每个请求提供一个唯一的临时目录来存储输出文件。例如:

from pathlib import Path
import bentoml

@bentoml.service
class Vits:
    @bentoml.api
    def to_speech(self, text: str, context: bentoml.Context) -> Path:
        # Example text-to-speech synthesis implementation
        audio_bytes = self.tts.synthesize(text)
        # Writing the audio bytes to a file in the temporary directory
        with open(Path(context.temp_dir) / "output.mp3", "wb") as f:
            f.write(audio_bytes)
        # Returning the path to the generated audio file directly
        return Path(context.temp_dir) / "output.mp3"

当方法返回指向生成文件的 Path 对象时,BentoML 会序列化此文件并将其包含在对客户端的响应中。

处理文件的更实际示例:

from pathlib import Path
from bentoml.validators import ContentType
from typing import Annotated  # Python 3.9 or above
from typing_extensions import Annotated  # Older than 3.9
import bentoml

@bentoml.service
class AppendStringToFile:

    @bentoml.api()
    def append_string_to_eof(
        self,
        context: bentoml.Context,
        txt_file: Annotated[Path, ContentType("text/plain")],
        input_string: str,
    ) -> Annotated[Path, ContentType("text/plain")]:
        with open(txt_file, "a") as file:
            file.write(input_string)
        return txt_file
from bentoml.validators import ContentType
from typing import Annotated  # Python 3.9 or above
from typing_extensions import Annotated  # Older than 3.9
from PIL import Image as im
import bentoml

@bentoml.service
class PDFtoImage:
    @bentoml.api
    def pdf_first_page_as_image(
        self,
        pdf: Annotated[Path, ContentType("application/pdf")],
    ) -> Image:
        from pdf2image import convert_from_path

        pages = convert_from_path(pdf)
        return pages[0].resize(pages[0].size, im.ANTIALIAS)
from pathlib import Path
from bentoml.validators import ContentType
from typing import Annotated  # Python 3.9 or above
from typing_extensions import Annotated  # Older than 3.9
import bentoml

@bentoml.service
class AudioSpeedUp:
    @bentoml.api
    def speed_up_audio(
        self,
        context: bentoml.Context,
        audio: Annotated[Path, ContentType("audio/mpeg")],
        velocity: float,
    ) -> Annotated[Path, ContentType("audio/mp3")]:

        import os
        from pydub import AudioSegment

        output_path = os.path.join(context.temp_dir, "output.mp3")
        sound = AudioSegment.from_file(audio)
        sound = sound.speedup(velocity)
        sound.export(output_path, format="mp3")
        return Path(output_path)

如果您不想将临时文件保存到磁盘,可以将数据作为 bytes 而不是 pathlib.Path 返回,并带有适当注解的 ContentType。这对于即时生成数据的服务非常有效。

张量

BentoML 支持各种张量类型,例如 numpy.ndarraytorch.Tensortensorflow.Tensor。此外,您可以使用 bentoml.validators,例如 bentoml.Shapebentoml.DType,来强制对张量输入进行特定的形状和数据类型限制。下面是一个示例:

import torch
from bentoml.validators import Shape, DType
from typing import Annotated  # Python 3.9 or above
from typing_extensions import Annotated  # Older than 3.9
from pydantic import Field
import bentoml

@bentoml.service
class IrisClassifier:
    @bentoml.api
    def classify(
        self,
        input: Annotated[torch.Tensor, Shape((1, 4)), DType("float32")]
        = Field(description="A 1x4 tensor with float32 dtype")
    ) -> np.ndarray:
        ...

在此示例中:

  • classify 方法期望 torch.Tensor 输入。

  • Annotated 类型与 ShapeDtype 验证器一起使用,以指定预期的张量形状应为 (1, 4),数据类型为 float32

  • pydantic.Field 为输入参数提供了附加描述,以提高 API 的可读性。

表格数据

Pandas DataFrame 常用于处理机器学习中的表格数据。BentoML 支持 Pandas DataFrame 输入,并允许您使用验证器对其进行注解,以确保数据符合预期的结构。

下面是一个示例:

from typing import Annotated  # Python 3.9 or above
from typing_extensions import Annotated  # Older than 3.9
import pandas as pd
from bentoml.validators import DataframeSchema
import bentoml

@bentoml.service
class IrisClassifier:
    @bentoml.api
    def classify(
        self,
        input: Annotated[pd.Dataframe, DataframeSchema(orient="records", columns=["petal_length", "petal_width"])
    ) -> int:
        # Classification logic using the input DataFrame
        ...

在此示例中:

  • IrisClassifier 服务的 classify 方法接受 Pandas DataFrame 作为输入。

  • Annotated 类型与 DataframeSchema 一起使用,以指定 DataFrame 的预期方向和列。

    • orient="records" 表示 DataFrame 预期采用记录导向格式。

    • columns=["petal_length", "petal_width"] 指定了 DataFrame 中预期的列。

DataframeSchema 验证器支持以下两种方向,它们决定了 API 接收数据时的结构方式:

  • records:每行表示为一个字典,其中键是列名。

  • columns:数据按列组织,字典中的每个键代表一列,对应的值是列值的列表。

图像

BentoML 服务可以通过 PIL.Image.Imagepathlib.Path 处理图像。

您可以通过 PIL.Image.Image 直接传递图像对象。

from PIL import Image as im
from PIL.Image import Image
import bentoml

@bentoml.service
class ImageResize:

    @bentoml.api
    def generate(self, image: Image, height: int = 64, width: int = 64) -> Image:
        size = height, width
        return image.resize(size, im.LANCZOS)

您可以使用带有 ContentType 验证器的 pathlib.Path 来处理图像文件:

from pathlib import Path
from typing import Annotated  # Python 3.9 or above
from typing_extensions import Annotated  # Older than 3.9
from bentoml.validators import ContentType
import bentoml

@bentoml.service
class MnistPredictor:
    @bentoml.api
    def infer(self, input: Annotated[Path, ContentType('image/jpeg')]) -> int:
        ...

根输入

根输入是一种特殊的输入类型,它不需要在 API 请求体中指定键。相反,输入数据本身直接在请求中传递。这对于直接处理图像、音频或原始文本等二进制数据特别有用。

要定义根输入,请在 Python 中使用仅位置参数,在函数签名中用 / 表示。

重要事项

  • 最多只能有一个仅位置参数(位于 / 之前的参数)。

  • 当您指定仅位置参数时,除了 bentoml.Context 之外,不允许使用其他参数。

示例实现:

from PIL import Image
import bentoml

@bentoml.service
class ImageProcessor:
    @bentoml.api
    def upload_image(self, image: Image.Image, /) -> int:
    # Process the image and return a result
      ...

在此示例中,upload_image 方法有一个仅位置参数(image),这意味着它必须在没有键的情况下传递。客户端必须将图像数据直接发送到 HTTP 请求体中,无需任何 JSON 包装。

使用 curl 进行 API 调用的示例:

curl -XPOST -sL https://:3000/upload_image --data-binary=@myimage.png

这是 HTTP 请求示例:

POST /upload_image HTTP/1.1
Content-Type: image/png

<image binary>

当使用 BentoML 客户端 调用带有根输入的 API 时,必须使用位置参数,而不指定参数名称:

client = bentoml.SyncClient("https://:3000")
image_path = Path("demo.png")

result = client.upload_image(image_path)  # CORRECT
result = client.upload_image(image=image_path)  # WRONG

复合类型

在高级用例中,仅处理单一数据类型往往不够。复杂场景可能需要处理不同数据类型的组合。

例如,您可以如下组合图像和 JSON 输入:

from pydantic import BaseModel, Field
from PIL import Image as PILImage
import bentoml

class ImageMetadata(BaseModel):
    description: str = Field(description="Description of the image")
    timestamp: str = Field(description="Timestamp of when the image was captured")

@bentoml.service
class ImageProcessingService:

    @bentoml.api
    def process_image(self, image: PILImage, metadata: ImageMetadata) -> dict:
        # Implementation for processing the image and metadata
        ...

在此示例中,PILImage 处理图像数据,而 Pydantic 模型 ImageMetadata 处理 JSON 输入。

BentoML 还支持复杂类型(例如图像和文件路径)的列表输入和输出。下面是定义一次处理图像和路径列表的 API 的示例:

from PIL import Image as PILImage
from pathlib import Path
from typing import List, Dict
import bentoml

@bentoml.service
class BatchImageService:
    @bentoml.api
    def enhance_images(self, images: List[PILImage]) -> PILImage:
        # Process images and return a single image
        ...

    @bentoml.api
    def process_files(self, files: List[Path]) -> List[Dict]:
        # Process files and return a list of dictionaries
        ...

请注意,目前 BentoML 不支持包含多个原始二进制数据或将原始二进制数据(如图像或文件)与普通字典数据直接组合的输出。

验证数据

对输入数据进行适当的验证对于 BentoML 服务至关重要,以确保正在处理的数据采用预期的格式并符合必要的质量标准。BentoML 提供了一个简单的验证机制,并默认支持 Pydantic 提供的所有验证功能。这允许对输入数据的结构、类型和约束进行全面检查。

下面是一个示例:

from typing import Annotated  # Python 3.9 or above
from typing_extensions import Annotated  # older than 3.9
from annotated_types import Ge, Lt, Gt, MultipleOf, MaxLen
import bentoml

@bentoml.service
class LLMPredictor:
    @bentoml.api
    def predict(
        self,
        prompt: Annotated[str, MaxLen(1000)],
        temperature: Annotated[float, Ge(0), Lt(2)],
        max_tokens: Annotated[int, Gt(0), MultipleOf(100)]
    ) -> int:
        ...

在此示例中,验证器确保 prompt 字符串不超过 1000 个字符,temperature 在 0 到 2 之间,并且 max_tokens 是 100 的正倍数。

常用 ML 类型的验证

BentoML 为常见的 ML 数据类型(如张量和数据帧)提供验证功能,以确保输入到模型的数据完整性。您可以在上面的章节中找到这些数据类型的验证示例。

下表包含 BentoML 支持的额外输入和输出类型,这些类型专为 ML 用例设计。每种类型允许使用的注解可用于进一步细化和验证数据。

类型名称

描述

允许的注解

numpy.ndarray

用于数值数据的多维数组,常用于 ML 任务。

bentoml.validators.Shape, bentoml.validators.DType

torch.Tensor

PyTorch 中用于表示张量数据的张量类型。

bentoml.validators.Shape, bentoml.validators.DType

tensorflow.Tensor

TensorFlow 中用于表示张量数据的张量类型。

bentoml.validators.Shape, bentoml.validators.DType

pandas.DataFrame

用于表格数据的数据结构,常用于数据分析。

bentoml.validators.DataframeSchema

PIL.Image.Image

PIL 库中的图像数据类型,用于图像处理。

bentoml.validators.ContentType

pathlib.Path

文件路径,用于文件输入和输出。

bentoml.validators.ContentType

BentoML 还支持所有 Pydantic 注解类型进行验证。有关更多信息,请参阅 Pydantic 文档

附录

本节提供了总结 BentoML 服务中支持的输入和输出类型的表格。

输入类型

类型

输入注解

HTTP 内容类型

示例输入 HTTP 请求体

JSON

predict(self, input1: str, input2: int)

application/json

curl -XPOST -d '{ "input1": "input_value", "input2": 2 }'

张量

  • predict(self, input1: torch.Tensor)

  • predict(self, input1: numpy.ndarray)

  • predict(self, input1: tensorflow.Tensor)

application/json

curl -XPOST -d '{ "input1": [[1, 1, 1, 1], [2, 2, 2, 2]] }'

表格数据

predict(self, input1: pandas.DataFrame)

application/json

curl -XPOST -d '{ "input1": [{"col1": 1, "col2": 2}, {"col1": 1, "col2": 2}] }'

图像

predict(self, input1: str, input2: PIL.Image.Image)

multipart/form-data

  • 路径: curl -XPOST -F input1="enter_your_prompt_here" -F input2="image=@/path/to/image.jpg"

  • URL: curl -XPOST -F input1="enter_your_prompt_here" -F input2="http://domain/path/to/image.jpg"

文件

predict(self, input1: str, input2: pathlib.Path)

multipart/form-data

  • 路径: curl -XPOST -F input1="enter_your_prompt_here" -F input2="image=@/path/to/image.jpg"

  • URL: curl -XPOST -F input1="enter_your_prompt_here" -F input2="http://domain/path/to/file.mp3"

输出类型

类型

输出注解

HTTP 内容类型

示例输出 HTTP 响应体

普通

  • -> str

  • -> bytes

text/plain

字符串

JSON

  • -> int

  • -> float

  • -> dict

  • -> list

application/json

  • 3

  • 1.1

  • {}

  • []

张量

  • -> torch.Tensor

  • -> numpy.ndarray

  • -> tensorflow.Tensor

application/json

[[1, 1, 1, 1], [2, 2, 2, 2]]

表格数据

-> pandas.DataFrame

application/json

[{ "col1": 1, "col2": 2 }, { "col1": 1, "col2": 2 }]

图像

-> PIL.Image.Image

image/<自动 MIME 类型>

二进制响应体

文件

-> pathlib.Path

<自动 MIME 类型>

二进制响应体

自定义文件

-> Annotated[pathlib.Path, ContentType("custom-type")]

custom-type

二进制响应体