跳转至

SigLIP2

1 引言

SigLIP2 是 Google 推出的一系列视觉-语言嵌入模型。每个检查点提供独立的文本和图像编码器,用于对比嵌入(而非文本生成)。vLLM 通过 llm.embed()(离线)或 /v1/embeddings(在线)将 SigLIP2 作为池化模型运行。

支持的使用场景包括图像-文本相似度、零样本 ImageNet 分类以及多模态检索。文本和图像必须在单独的请求中嵌入;请勿在一次调用中同时传入两者。

本指南介绍如何在 Atlas 300I DUO 上使用 vLLM Ascend 部署和评估 SigLIP2。

本文档基于 2026-09-03 的 vLLM-Ascend main 分支 编写并验证。当前模型(siglip2-base-patch16-224)在该分支上完全支持文本和图像嵌入。作为池化模型,SigLIP2 用于离线 llm.embed() 和在线 /v1/embeddings 服务;PD 分离和 MTP 等功能不适用。请使用该日期的 main 分支 快照或包含 SigLIP2 支持的后续官方版本。

2 支持的特性

请参阅 支持的模型 获取该模型的支持特性矩阵。

3 前提条件

3.1 模型权重

权重版本 硬件要求 下载链接
siglip2-base-patch16-224 (FP16) 1 个 Atlas 300I DUO 节点 ModelScope | HuggingFace

路径说明: 请将模型权重下载到您选择的目录并记录该路径。例如:/root/.cache/modelscope/hub/models/google/siglip2-base-patch16-224。在后续命令中,请将 <YOUR_MODEL_PATH> 替换为您在此记录的路径(本地目录或 Hugging Face / ModelScope 模型 ID,例如 google/siglip2-base-patch16-224)。

3.2 ImageNet 标签(可选,用于精度评估)

对于 ImageNet 验证集零样本 Top-1 评估,请准备:

4 安装

硬件支持: 本教程中的 SigLIP2 仅在 Atlas 300I DUO 上得到支持和验证。请使用相应的 Docker 镜像和以下安装步骤。

4.1 Docker 镜像安装

您可以使用官方一体化 Docker 镜像。有关可用的镜像标签和已发布版本,请参阅 使用 Docker。

  • 步骤 1:下载最新的 Docker 镜像
docker pull quay.io/ascend/vllm-ascend:v0.23.0-310p
  • 步骤 2:启动 Docker 容器
# Set the vLLM Ascend image name.
export IMAGE=quay.io/ascend/vllm-ascend:v0.23.0-310p
export NAME=siglip2-dev

# Start the container with the variables defined above.
# Atlas 300I DUO uses a single NPU device (/dev/davinci0).
docker run --rm \
--name $NAME \
--net=host \
--shm-size=1g \
--privileged=true \
--device /dev/davinci0 \
--device /dev/davinci_manager \
--device /dev/devmm_svm \
--device /dev/hisi_hdc \
-v /usr/local/dcmi:/usr/local/dcmi \
-v /usr/local/Ascend/driver/tools/hccn_tool:/usr/local/Ascend/driver/tools/hccn_tool \
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
-v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
-v /etc/ascend_install.info:/etc/ascend_install.info \
-v /root/.cache:/root/.cache \
-it $IMAGE bash
  • 步骤 3:安装验证

启动容器后,运行以下命令验证安装:

docker ps | grep $NAME

预期结果:容器以 Up 状态列出。您还可以在容器内验证 vllm-ascend 版本:

pip show vllm-ascend

预期结果:显示版本信息,与拉取的镜像版本一致。

4.2 源码安装

如果您不想使用上述 Docker 镜像,也可以从源码构建所有内容:

  • 从源码安装 vllm-ascend,请参阅 安装。

如果您想部署多节点环境,需要在每个节点上配置环境。

5 在线服务部署

5.1 单节点在线部署

单节点部署在单个 Atlas 300I DUO 节点上运行文本和图像嵌入,适用于开发、测试和在线 /v1/embeddings 服务。

启动命令:

#!/bin/sh
# Replace <YOUR_MODEL_PATH> with the path recorded in Section 3.1.
export MODEL_PATH=<YOUR_MODEL_PATH>

vllm serve $MODEL_PATH \
    --served-model-name $MODEL_PATH \
    --runner pooling \
    --chat-template template_basic.jinja \
    --limit-mm-per-prompt '{"image": 1}' \
    --compilation-config '{"cudagraph_capture_sizes": [64,32]}' \
    --additional-config '{"ascend_compilation_config": {"enable_npugraph_ex": false}}' \
    --dtype float16 \
    --port 8000 \
    --max-model-len 64

必需参数说明:

  • --compilation-config 对于 Atlas 300I DUO,由于硬件流数量有限,cudagraph_capture_sizes 的大小受到限制。

关键参数说明:

  • SigLIP2 在线服务不支持张量并行(TP)。请勿设置 --tensor-parallel-size;请按照上述方式在单个 Atlas 300I DUO NPU 上部署。
  • --served-model-name 必须与 /v1/embeddings 请求中的 "model" 字段匹配;请使用与 MODEL_PATH 相同的值。
  • 必须设置 --runner pooling。SigLIP2 是嵌入模型,而非生成式 LLM。
  • --max-model-len 64 与 SigLIP2 文本分词(padding=max_length,max_length=64)匹配。
  • 通过 /v1/embeddings 的 messages 发送图像时,需要设置 --chat-template template_basic.jinja。
  • --limit-mm-per-prompt '{"image": 1}' 允许每个请求包含一张图像。
  • 对于仅图像的 HTTP 嵌入,请在 messages 中使用空文本提示,或在离线模式下使用 prompt="" 并配合 multi_modal_data。

常见问题提示:如果遇到问题,请参阅 公共 FAQ 进行故障排除。

5.2 多节点 PD 分离部署

SigLIP2 是池化嵌入模型,不支持多节点 PD(Prefill-Decode)分离部署。请改用 §5.1 单节点在线部署。

6 功能验证

服务器启动后,您可以通过以下命令进行验证。

6.1 文本嵌入

export MODEL_PATH=<YOUR_MODEL_PATH>  # match --served-model-name from Section 5.1

curl -X POST http://127.0.0.1:8000/v1/embeddings \
  -H "Content-Type: application/json" \
  -d "{
    \"model\": \"${MODEL_PATH}\",
    \"input\": [\"This is a photo of a dog.\"]
  }"

对于零样本分类提示,请使用模板 "This is a photo of {}."。SigLIP2 在训练时对文本使用了 padding=max_length 和 max_length=64;vLLM 在使用离线 tokenization_kwargs 时会应用此设置。

6.2 图像嵌入

将图像编码为 base64,并通过 messages 发送:

export MODEL_PATH=<YOUR_MODEL_PATH>  # match --served-model-name from Section 5.1
IMG_B64=$(base64 -w 0 /path/to/image.jpg)

curl -X POST http://127.0.0.1:8000/v1/embeddings \
  -H "Content-Type: application/json" \
  -d "{
    \"model\": \"${MODEL_PATH}\",
    \"encoding_format\": \"float\",
    \"messages\": [{
      \"role\": \"user\",
      \"content\": [{
        \"type\": \"image_url\",
        \"image_url\": {\"url\": \"data:image/jpeg;base64,${IMG_B64}\"}
      }]
    }]
  }"

预期结果:

服务返回 HTTP 200 OK,JSON 响应中包含每个请求的 embedding 字段。

更多使用示例,请参考 vLLM pooling embed 示例。

6.3 离线嵌入

from vllm import LLM

MODEL_PATH = "<YOUR_MODEL_PATH>"  # Replace with the path recorded in Section 3.1

llm = LLM(
    model=MODEL_PATH,
    runner="pooling",
    limit_mm_per_prompt={"image": 1},
    max_model_len=64,
)

# Text
text_out = llm.embed(
    ["This is a photo of a dog."],
    tokenization_kwargs={"padding": "max_length", "max_length": 64},
)
print(len(text_out[0].outputs.embedding))

# Image (empty prompt; field name must be multi_modal_data)
from PIL import Image

img = Image.open("/path/to/image.jpg").convert("RGB")
img_out = llm.embed(
    {"prompt": "", "multi_modal_data": {"image": img}},
)
print(len(img_out[0].outputs.embedding))

7 精度评估

ImageNet 验证集零样本 Top-1 是 SigLIP2 常用的精度基准。

7.1 数据集与标签

  1. 下载 ImageNet ILSVRC 2012 验证集图像(需要登录)。
  2. 下载 val_label.txt(示例)。每行格式:ILSVRC2012_val_00000001.JPEG 65(PyTorch 类别 ID 0–999)。
  3. 下载 imagenet1000_clsidx_to_labels.txt 以获取 1000 个类别的文本模板。

7.2 离线评估

分别对 1000 个类别文本和验证集图像进行嵌入,然后计算余弦相似度(L2 归一化后的点积)。示例流程:

import ast
import numpy as np
from PIL import Image
from vllm import LLM

TEXT_TEMPLATE = "This is a photo of {}."
TOKEN_KWARGS = {"padding": "max_length", "max_length": 64}

def load_classnames(path):
    with open(path, encoding="utf-8") as f:
        d = ast.literal_eval(f.read())
    return [d[i] for i in range(1000)]

def load_val_label(path):
    gt = {}
    with open(path, encoding="utf-8") as f:
        for line in f:
            parts = line.strip().split()
            if len(parts) >= 2:
                gt[parts[0].split(".")[0]] = int(parts[1])
    return gt

MODEL_PATH = "<YOUR_MODEL_PATH>"  # Replace with the path recorded in Section 3.1

llm = LLM(
    model=MODEL_PATH,
    runner="pooling",
    limit_mm_per_prompt={"image": 1},
    max_model_len=64,
)

classnames = load_classnames("imagenet1000_clsidx_to_labels.txt")
prompts = [TEXT_TEMPLATE.format(c) for c in classnames]
text_feats = np.asarray(
    [o.outputs.embedding for o in llm.embed(prompts, tokenization_kwargs=TOKEN_KWARGS)],
    dtype=np.float32,
)
text_feats /= np.linalg.norm(text_feats, axis=1, keepdims=True)

gt = load_val_label("val_label.txt")
correct = 0
total = 0
for stem, label in gt.items():
    path = f"ImageNet/val/{stem}.JPEG"  # or .jpeg
    img = Image.open(path).convert("RGB")
    feat = np.asarray(
        llm.embed({"prompt": "", "multi_modal_data": {"image": img}})[0]
        .outputs.embedding,
        dtype=np.float32,
    )
    feat /= np.linalg.norm(feat)
    if np.argmax(feat @ text_feats.T) == label:
        correct += 1
    total += 1

print(f"Top-1 accuracy: {100.0 * correct / total:.2f}%")

ImageNet 验证集上的参考 Top-1(近似值):

Model Top-1
siglip2-base-patch16-224 ~69%

8 性能评估

使用以下脚本通过 HTTP 对 /v1/embeddings 进行基准测试。

8.1 HTTP 服务基准测试

按照 §5 在线服务部署 启动服务器,然后运行:

"""Benchmark SigLIP2 /v1/embeddings serving over HTTP."""

import base64
import io
import json
import statistics
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed

import numpy as np
from PIL import Image

BASE_URL = "http://127.0.0.1:8000"
MODEL = "<YOUR_MODEL_PATH>"  # match --served-model-name from Section 5.1
NUM_REQUESTS = 200
CONCURRENCY = 8
WARMUP = 10
MODE = "both"  # "text", "image", or "both"
TEXT = "This is a photo of a dog."
IMAGE_SIZE = (224, 224)
TIMEOUT_S = 120.0


def post_json(url: str, payload: dict) -> tuple[bool, float, str]:
    body = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=body,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    t0 = time.perf_counter()
    try:
        with urllib.request.urlopen(req, timeout=TIMEOUT_S) as resp:
            resp.read()
            ok = resp.status == 200
    except urllib.error.HTTPError as e:
        detail = e.read().decode("utf-8", errors="replace")[:200]
        return False, time.perf_counter() - t0, f"HTTP {e.code}: {detail}"
    except Exception as e:
        return False, time.perf_counter() - t0, str(e)
    return ok, time.perf_counter() - t0, ""


def random_jpeg_b64(width: int, height: int, seed: int) -> str:
    arr = np.random.default_rng(seed).integers(
        0, 256, size=(height, width, 3), dtype=np.uint8
    )
    buf = io.BytesIO()
    Image.fromarray(arr, mode="RGB").save(buf, format="JPEG")
    return base64.b64encode(buf.getvalue()).decode("ascii")


def build_payload(mode: str, seed: int) -> dict:
    if mode == "text":
        return {
            "model": MODEL,
            "input": [TEXT],
            "encoding_format": "float",
        }
    w, h = IMAGE_SIZE
    b64 = random_jpeg_b64(w, h, seed)
    return {
        "model": MODEL,
        "encoding_format": "float",
        "messages": [
            {
                "role": "user",
                "content": [
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{b64}",
                        },
                    }
                ],
            }
        ],
    }


def percentile(values: list[float], percent: float) -> float:
    sorted_values = sorted(values)
    rank = (len(sorted_values) - 1) * (percent / 100.0)
    lower = int(rank)
    upper = min(lower + 1, len(sorted_values) - 1)
    if lower == upper:
        return sorted_values[lower]
    return sorted_values[lower] + (sorted_values[upper] - sorted_values[lower]) * (
        rank - lower
    )


def run_benchmark(mode: str) -> None:
    url = f"{BASE_URL.rstrip('/')}/v1/embeddings"
    total = NUM_REQUESTS + WARMUP
    payloads = [build_payload(mode, i) for i in range(total)]

    def send_one(index: int) -> tuple[bool, float]:
        ok, latency_s, _ = post_json(url, payloads[index])
        return ok, latency_s

    if WARMUP:
        with ThreadPoolExecutor(max_workers=min(CONCURRENCY, WARMUP)) as pool:
            list(pool.map(send_one, range(WARMUP)))

    t0 = time.perf_counter()
    with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
        futures = [pool.submit(send_one, WARMUP + i) for i in range(NUM_REQUESTS)]
        results = [fut.result() for fut in as_completed(futures)]
    wall_s = time.perf_counter() - t0

    ok_lat_ms = [lat * 1000 for ok, lat in results if ok]
    failed = sum(1 for ok, _ in results if not ok)
    print(f"\n=== {mode} ===")
    print(f"  successful: {len(ok_lat_ms)}/{NUM_REQUESTS}")
    print(f"  failed:     {failed}")
    print(f"  duration:   {wall_s:.2f}s")
    print(f"  throughput: {len(ok_lat_ms) / wall_s:.2f} req/s")
    if ok_lat_ms:
        print(f"  mean E2EL:  {statistics.mean(ok_lat_ms):.2f} ms")
        print(f"  median E2EL:{statistics.median(ok_lat_ms):.2f} ms")
        print(f"  p99 E2EL:   {percentile(ok_lat_ms, 99):.2f} ms")


if __name__ == "__main__":
    if MODE in ("text", "both"):
        run_benchmark("text")
    if MODE in ("image", "both"):
        run_benchmark("image")

8.2 指标

该脚本会报告:

  • 请求吞吐量(req/s)
  • 平均 / 中位数 / p99 E2EL(端到端延迟,单位毫秒)

调整脚本顶部的 NUM_REQUESTS、CONCURRENCY 和 WARMUP。将 MODE 设置为 "text"、"image" 或 "both"。

大约几分钟后,即可获得性能评估结果。

9 性能调优

注意:以下配置在特定测试环境中经过验证,仅供参考。最佳配置取决于文本与图像工作负载、请求并发度、批处理大小和图像分辨率等因素。建议根据实际情况参考第 9.2 节进行调优。

9.1 推荐配置

以下配置已在 Atlas 300I DUO 上验证,并按使用场景分类。从 §5.1 单节点在线部署 命令开始,然后调整下方的 serve 参数。

Scenario Workload Deployment NPUs Max Num Seqs Max Num Batched Tokens Max Model Len Client Concurrency (ref.)
Text high throughput Text /v1/embeddings Single node 1 (300I DUO) 32 512 64 16–32
Image high throughput Image /v1/embeddings (224×224) Single node 1 (300I DUO) 16 256 64 8–16
Low latency Text or image Single node 1 (300I DUO) 8 128 64 4–8

注意:--max-num-seqs 和 --max-num-batched-tokens 在 vllm serve 启动时设置。§8.1 HTTP 服务基准测试 中的客户端并发度控制并行发送的 HTTP 请求数量;建议将其保持在接近 --max-num-seqs 的值以获得稳定的批处理效果。SigLIP2 不支持 TP 或 PD 分离。

文本高吞吐量场景的示例 serve 参数:

vllm serve $MODEL_PATH \
    ... \
    --max-num-seqs 32 \
    --max-num-batched-tokens 512 \
    --max-model-len 64

9.2 调优指南

9.2.1 模型特定优化

默认启用的优化

以下优化已在推荐的 §5.1 配置中启用:

Optimization Technique Technical Principle Performance Benefit
ACL graph capture 使用 --compilation-config '{"cudagraph_capture_sizes": [64,32]}' 在 Atlas 300I DUO 上捕获固定的小批量形状 降低短文本和图像嵌入路径的每请求调度开销
Pooling runner 使用 --runner pooling 进行仅嵌入的前向传播 SigLIP2 所必需;避免生成式解码路径
FP16 inference 在 Atlas 300I DUO 上使用 --dtype float16 匹配 300I DUO 支持的精度和模型权重
需要显式启用的优化
Optimization Technique Applicable Scenarios Enablement Method Technical Principle Precautions
Server batch tuning 在线文本/图像服务 在 serve 启动时设置 --max-num-seqs 和 --max-num-batched-tokens(参见第 9.1 节) 控制服务器上批处理的嵌入请求数量 如果发生 OOM,请减小这两个参数;图像嵌入通常需要比文本更小的批次
Client concurrency tuning HTTP 基准测试或生产客户端 增加并行的 /v1/embeddings 请求数(参见第 8.1 节) 提高发送到服务器批处理器的负载 当客户端并发度超过 --max-num-seqs 后,吞吐量提升趋于平缓;请关注 p99 延迟

9.2.2 通用调优参考

通用调优方法请参考 公共性能调优文档。

详细功能描述请参考 功能矩阵。

10 常见问题

有关常见的环境、安装和通用参数问题,请参考 公共常见问题;本章仅涵盖模型特定问题。

问:Top-1 准确率接近 0%,但嵌入看起来是有效的。

答:请检查真实标签是否使用 PyTorch 索引(val_label.txt),而不是使用包含 yrevar 类别名称的 devkit ILSVRC2012_validation_ground_truth.txt。

问:我可以在一个请求中同时嵌入文本和图像吗?

答:不可以。SigLIP2 每个请求只接受纯文本或纯图像输入。