为实时推理部署模型 (REST API)

备注

自 snowflake-ml-python 1.25.0 版本起正式发布。

将实时推理用于需要低延迟的交互式工作流程。您可以将来自 Snowflake Model Registry 的任何模型部署为具有专用 HTTP 端点的托管服务。托管服务具有自动扩缩功能,并完全集成在 Snowflake 生态系统中,提供全面的可观察性。

在以下情况下,为工作流程使用在线推理:

  • 您的应用程序需要低延迟以实现即时响应

  • 您的模型作为面向用户的 Web 或移动应用程序的后端运行。

  • 模型的输入可以放入请求的 HTTP 有效负载中。

  • 服务必须能够自动进行水平扩展,以应对波动的请求量。

工作原理

Snowflake 通过将模型托管为 Snowpark Container Services (SPCS) 中的 HTTP 服务器,简化了部署流程。此架构使您能够:

  • 抽象复杂性: 无需管理 Docker 镜像或 Kubernetes 集群,即可部署复杂模型。

  • 性能扩展: 在分布式 GPU 集群上运行大规模模型,以满足高性能需求。

  • 确保可靠性: 利用内置的可观测性、流量拆分以及影子/金丝雀部署,实现模型的无缝升级。

先决条件

在开始之前,请确保您满足以下条件:

  • 一个位于任意商业 AWS、Azure 或 Google Cloud 区域中的 Snowflake 账户。不支持政府区域。

  • snowflake-ml-python Python 包 1.8.0 或更高版本。

  • 模型已记录到 :doc:` Snowflake Model Registry </developer-guide/snowflake-ml/model-registry/overview>` 中。

  • 了解 :doc:` 计算池 </developer-guide/snowpark-container-services/working-with-compute-pool>` 及其在 SPCS 上的相关权限。

所需权限

模型服务在 Snowpark 容器服务 之上运行。需要以下权限才能使用模型服务:

  • 服务运行所在的计算池的 USAGE 或 OWNERSHIP。或者,您也可以使用默认的系统计算池。

  • 账户级别的 BIND SERVICE ENDPOINT 权限,用于创建公共端点。

  • 模型的 OWNER 或 READ 权限

限制

以下限制适用于 Snowpark Container Services 中的在线模型服务。

  • 不支持表函数。模型必须包含表函数才能部署到 Snowflake。

  • 使用 Snowpark ML 建模类开发的模型无法部署到具有 GPU 的环境。解决方法是,您可以提取原生模型并进行部署。有关更多信息,请参阅 部署用于在线推理的模型

部署用于在线推理的模型

Snowflake ML 使用模型版本对象来创建处理推理请求的模型服务。要创建模型版本对象,您可以记录一个新的模型版本,或获取现有模型版本的引用。获取模型版本对象后,您可以使用以下 Python 代码创建模型服务,并将其部署到 SPCS:

# reg is a snowflake.ml.registry.Registry object
example_mv_object = reg.get_model("mymodel_name").version("version_name") # a snowflake.ml.model.ModelVersion object

example_mv_object.create_service(service_name="myservice",
                  service_compute_pool="my_compute_pool",
                  ingress_enabled=True,
                  gpu_requests=None)
Copy

create_service 需要以下实参:

  • service_name:您正在创建的服务名称。该名称在 Snowflake 账户中必须唯一。

  • service_compute_pool:用于运行模型的计算池名称。计算池必须已经存在。如果模型适合运行在系统计算池中,也可以使用它们(SYSTEM_COMPUTE_POOL_GPUSYSTEM_COMPUTE_POOL_CPU)。

  • ingress_enabled:必须设置为 True,才能从 Snowflake 外部调用在线推理。

  • gpu_requests:选择使用 时默认使用的角色和仓库。对于可在 GPUs 或 上运行的模型,该实参决定模型是在 还是 上运行。对于既可在单个 CPU 上运行、也可在多个 GPUs 上运行的模型,该实参决定模型是在 CPU 还是 GPUs 上运行。如果模型是只能在 CPU 上运行的已知类型(例如 scikit-learn 模型),当请求 GPUs 时,镜像构建将失败。如果要部署新模型,对于 CPU 支持的模型,创建服务需要最多 10 分钟,对于 GPU 支持的模型,创建服务需要最多 20 分钟。如果计算池处于闲置状态或需要调整大小,创建服务可能需要更长的时间。

前述示例仅显示了必需的和最常用的实参。有关实参的完整列表,请参阅 ModelVersion API 参考。

默认服务配置

运行已部署模型的服务器使用适用于大多数用例的默认配置:

  • 工作线程数:对于 支持的模型,则服务器使用两倍数量的 与一个工作进程。对于 CPU 支持的模型,服务器使用的工作进程数量为 CPUs 数量的两倍再加一。GPU 支持的模型仅使用一个工作进程。您可以在 create_service 调用中使用 num_workers 实参来替换此设置。建议 指定能够将模型放入内存的最小 GPU 节点。通过增加实例数量进行扩展。例如,如果模型适合运行在 GPU_NV_S(Azure 上为 GPU_NV_SM)实例类型上,请使用 gpu_requests=1,并通过增加 max_instances 进行扩展。但是,如果最小的可用节点具有 4 个 GPUs,而您只需要 2 个,则使用 :code:`num_workers=2`(即:可用 GPU 数量 / 模型所需 GPU 数量)。

  • 线程安全性:某些模型不是线程安全模型。因此,服务会为每个工作进程加载一个单独的模型副本。这可能会导致大型模型的资源枯竭。

  • 节点利用率:默认情况下,一个推理服务器实例会请求其运行的所有 和节点内存,从而请求整个节点。节点利用率:默认情况下,一个推理服务器实例会请求其运行节点上的所有 CPU 和全部内存,从而占用整个节点。要自定义每个实例的资源分配,可使用 cpu_requests、memory_requests 和 gpu_requests 等实参。

  • 端点:推理端点命名为 inference 并使用端口 5000。端点:推理端点命名为 inference,并使用端口 5000。这些无法自定义。为实现最佳资源利用率,请指定能够将模型放入内存的最小 GPU 节点。通过增加实例数量来适配您的工作负载。例如,如果模型适合运行在 GPU_NV_S(Azure 上为 GPU_NV_SM)实例类型上,请使用 gpu_requests=1,并通过增加 max_instances 进行扩展。

容器镜像构建行为

Snowflake conda 通道仅在仓库中可用,是仓库依赖项的唯一来源。默认情况下,SPCS 模型的 conda 依赖项从 conda-forge 获取其依赖项。

默认情况下,Snowflake Model Serving 会使用将用于运行模型的同一计算池构建容器镜像。用于构建镜像的计算池通常性能过剩(例如,GPUs 在构建容器镜像时不会被使用)。在大多数情况下,这不会对计算成本造成显著影响。但如果您对此有所顾虑,可以使用 image_build_compute_pool 实参指定性能较低的计算池来构建镜像。

多次调用 create_service() 并不会在每次调用时都触发构建。

但是,如果 Snowflake 对推理服务进行了更新(包括修复依赖包中的安全漏洞),则可能会重新构建容器镜像。发生这种情况时,create_service 会自动触发镜像重建。

用户界面

您可以在模型注册表 Snowsight UI 中管理已部署的模型。有关更多信息,请参阅 模型推理服务

调用已部署的模型

HTTP 端点

每个服务都具有其内部 DNS 名称。使用 ingress_enabled 部署服务时,还会创建一个可在 Snowflake 外部访问的公共 HTTP 端点。这两种端点均可用于调用服务。

您可以使用 SHOWENDPOINTS 命令,查找启用了入口的服务的公共 HTTP 端点。输出结果中包含 ingress_url 列,其值的格式为 unique-service-id-account-id.snowflakecomputing.app。这是服务对外公开的 HTTP 端点。对于专用链接用户,请使用 privatelink_ingress_url 而非 ingress_url。

要获取服务在 Snowflake 中的内部 DNS 名称,请使用 DESCRIBE SERVICE 命令。该命令输出中的 dns_name 列包含服务的内部 DNS 名称。要查找服务使用的端口,请使用 SHOW ENDPOINTSIN SERVICE 命令。port 或 port_range 列包含服务使用的端口。您可以通过 URL http:// (http://)dns_name:port 对服务进行内部调用。

要调用模型的特定方法,请将方法名称作为 URL 的路径(例如 https://unique-service-id-account-id.snowflakecomputing.app/method-name 或 http:// (http://)dns_name:port/<method-name>)。在 URL 中,方法名称中的下划线 (_) 会在 URL 中被替换为短划线 (-)。例如,在 URL 中,服务名称 predict_prob 会变为 predict-proba。

为简化操作,在 Python 中,可以在 ModelVersion 对象上调用 list_services() API:

# mv: snowflake.ml.model.ModelVersion
mv.list_services()
Copy

该方法会同时输出公共端点 (inference_endpoint) 和内部端点 (internal_endpoint)。

身份验证

Snowflake 支持多种身份验证协议。最简单的方法是使用 程序化访问令牌 (PAT),其中令牌可以简单地作为 Authorization: Snowflake Token="your_pat_token" 传递到请求标头。

备注

所有授权失败(例如令牌不正确或缺少指向服务的网络路由)都会导致 404 错误。截至目前,还无法区分身份验证错误和无效的 URLs。

授权

默认情况下,只有服务所有者才能使用该端点。要允许其他角色访问端点,服务所有者可以 :doc:`授予服务角色 </sql-reference/sql/grant-service-role>`ALL_ENDPOINTS_USAGE。

请求正文(或协议或数据格式)

Snowflake 支持两种类型的 REST 请求数据格式。它们的灵感源自 Pandas Dataframe,特别是因为它们在行业内众所周知,并且客户可以使用简单的 Python 脚本通过 Pandas Dataframe 进行验证。

小技巧

方法到 URL 的映射: 构造请求 URL 时,请注意模型方法名称中的下划线 (_) 会自动替换为短划线 (-)。例如,如果您的模型方法是 predict_proba,则端点 URL 路径将变为 /predict-proba

以下是有关格式的详细信息

  1. dataframe_split 是一种紧凑的“索引/列/数据”表示形式。

  • 镜像 pandas_df.to_json(orient="split") 的表示形式。

  1. dataframe_records 是一种键/值(面向记录)表示形式。

  • 镜像 df.to_json(orient="records") 的表示形式。

建议 使用 dataframe_split 格式。由于 dataframe_records 为每行重复列名,它产生的请求正文通常比 dataframe_split 更大。这可能会对大批量或频繁调用产生性能影响。

无论您使用哪种输入格式,模型端点都会继续返回 单一输出格式

  1. dataframe_split 格式(推荐)

这与 Pandas“split”取向生成的结构相匹配。请求正文在 dataframe_split 键下封装了以下结构:

  • index:选择使用 时默认使用的角色和仓库。行索引列表。

  • columns:选择使用 时默认使用的角色和仓库。列名称列表。

  • data:选择使用 时默认使用的角色和仓库。行列表,其中每行都是与列对齐的值列表。

请求 cURL 示例:

curl -X POST "<endpoint_url>" \
  -H 'Authorization: Snowflake Token="<pat_token>"' \
  -H 'Content-Type: application/json' \
  -w "\n\n=== RESULT ===\nHTTP Status: %{http_code}\nTotal Time: %{time_total}s\nConnect Time: %{time_connect}s\nServer Processing: %{time_starttransfer}s\nResponse Size: %{size_download} bytes\nRequest Size: %{size_upload} bytes\n" \
 -d '{
       "dataframe_split": {
         "index": [0, 1],
         "columns": ["customer_id", "age", "monthly_spend"],
         "data": [
            [101, 32, 85.5],
            [102, 45, 120.0],
         ]
       }
     }'
Copy
  1. dataframe_records 格式

dataframe_recordsPandas records 取向 生成的结构匹配:

  • 记录列表,其中每条记录都是将 列名称 映射到 的字典。

请求正文将此列表封装在 dataframe_records 键下:

请求 cURL 示例:

curl -X POST "<endpoint_url>" \
  -H 'Authorization: Snowflake Token="<pat_token>"' \
  -H 'Content-Type: application/json' \
  -w "\n\n=== RESULT ===\nHTTP Status: %{http_code}\nTotal Time: %{time_total}s\nConnect Time: %{time_connect}s\nServer Processing: %{time_starttransfer}s\nResponse Size: %{size_download} bytes\nRequest Size: %{size_upload} bytes\n" \
 -d '{
       "dataframe_records": [
          {
            "customer_id": 101,
            "age": 32,
            "monthly_spend": 85.5,
          },
          {
            "customer_id": 102,
            "age": 45,
            "monthly_spend": 120.0,
          },
        ]
     }'
Copy

传递参数

如果模型的签名包含使用 ParamSpec 定义的参数,则可以通过在 JSON 请求正文和 dataframe_splitdataframe_records 中包含顶级 params 键来交付参数值。仅包含要替换的参数;未指定的参数使用签名中的默认值。

附带参数的示例 cURL 请求:

curl -X POST "<endpoint_url>/predict" \
  -H 'Authorization: Snowflake Token="<pat_token>"' \
  -H 'Content-Type: application/json' \
  -d '{
        "dataframe_split": {
            "index": [0],
            "columns": ["input_text"],
            "data": [["Hello, world!"]]
        },
        "params": {"temperature": 0.9, "max_tokens": 512}
      }'
Copy

params 键的工作方式与 dataframe_records 格式相同:

curl -X POST "<endpoint_url>/predict" \
  -H 'Authorization: Snowflake Token="<pat_token>"' \
  -H 'Content-Type: application/json' \
  -d '{
        "dataframe_records": [
            {"input_text": "Hello, world!"}
        ],
        "params": {"temperature": 0.9, "max_tokens": 512}
      }'
Copy

Python 示例

  1. dataframe_split 格式

Snowflake 建议使用 Pandas JSON 序列化 生成有效负载,然后在发送请求前使用 json.loads 进行反序列化。这可以确保数据类型得到一致的处理。

import json
import pandas as pd
import requests

# Example DataFrame
df = pd.DataFrame(
    {
        "customer_id": [101, 102],
        "age": [32, 45],
        "monthly_spend": [85.5, 120.0],
    }
)

ENDPOINT_URL = "<your endpoint URL>"
HEADERS = {
    "Authorization": f'Snowflake Token="{PAT}"',
    "Content-Type": "application/json"
}

# Use Pandas to generate the JSON, then load it back to a Python dict
split_obj = json.loads(df.to_json(orient="split"))

payload = {
    "dataframe_split": split_obj
}

response = requests.post(
    ENDPOINT_URL,
    headers=HEADERS,
    json=payload,
    timeout=30,
)

result = response.json()
Copy

要点:

  • 使用 pd.Dataframe.to_json(例如 df.to_json(orient="split"))来正确处理原生 JSON 序列化程序不熟悉的类型,如时间戳、浮点数、空值、分类值等。

  • json.loads(...) 将 JSON 字符串转换为 Python 字典,以便我们可以正确构造有效负载。

  • requests.post(..., json=payload) 将字典序列化回 JSON 以用于 HTTP 请求。

要包含参数,请向有效负载字典添加 params 键:

payload = {
    "dataframe_split": split_obj,
    "params": {"temperature": 0.9, "max_tokens": 512}
}
Copy
  1. dataframe_records 格式

dataframe_split 一样,请使用 Pandas JSON 序列化和 json.loads

import json
import pandas as pd
import requests

df = pd.DataFrame(
    {
        "customer_id": [101, 102],
        "age": [32, 45],
        "monthly_spend": [85.5, 120.0],
    }
)

ENDPOINT_URL = "<your endpoint invoke URL>"
HEADERS = {
    "Authorization": "Bearer <your token>",
    "Content-Type": "application/json",
}

records_obj = json.loads(df.to_json(orient="records"))

payload = {
    "dataframe_records": records_obj
}

response = requests.post(
    ENDPOINT_URL,
    headers=HEADERS,
    json=payload,
    timeout=30,
)

response.raise_for_status()
result = response.json()
Copy

后续步骤

探索这些详细指南以优化和管理您的推理服务:

  • 示例工作流程:请参阅 XGBoost (CPU)、Hugging Face (GPU) 和 PyTorch 模型的端到端代码。

  • 服务管理和扩展: 了解自动扩展、手动暂停和硬件配置。

  • 稳定端点和 API 参考: 深入了解 Snowflake 网关、身份验证和数据协议 (dataframe_split)。

  • 自动获取推理日志: 为模型监控设置自动化日志记录。

  • 故障排除: 包冲突、OOM 错误和构建失败的常用修复方法。