跳转至

SigLIP2

1 简介

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

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

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

本文档基于 vLLM-Ascend main 分支(截至 2026-09-03)编写并验证。该分支上当前模型(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 中使用空文本提示,或在离线场景中使用带有 multi_modal_data 的 prompt=""。

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

5.2 多节点 PD 分离部署

SigLIP2 是一个池化(pooling)嵌入模型,不支持多节点 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 进行训练;使用离线 tokenization_kwargs 时,vLLM 会应用这些设置。

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(近似值):

模型 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,单位:ms)

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

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

9 性能调优

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

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

场景 工作负载 部署方式 NPU 最大序列数 最大批处理 Token 数 最大模型长度 客户端并发(参考)
文本高吞吐 文本 /v1/embeddings 单节点 1(300I DUO) 32 512 64 16–32
图像高吞吐 图像 /v1/embeddings(224×224) 单节点 1(300I DUO) 16 256 64 8–16
低延迟 文本或图像 单节点 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 配置中已启用:

优化技术 技术原理 性能收益
ACL 图捕获 在 Atlas 300I DUO 上使用 --compilation-config '{"cudagraph_capture_sizes": [64,32]}' 捕获固定的小批量形状 降低短文本和图像嵌入路径的每请求调度开销
Pooling 运行器 使用 --runner pooling 进行仅嵌入的前向传递 SigLIP2 所必需;避免生成式解码路径
FP16 推理 在 Atlas 300I DUO 上使用 --dtype float16 与 300I DUO 支持的精度和模型权重匹配
需显式启用的优化
优化技术 适用场景 启用方法 技术原理 注意事项
服务端批处理调优 在线文本/图像服务 在 serve 启动时设置 --max-num-seqs 和 --max-num-batched-tokens(参见第 9.1 节) 控制服务器上批处理的嵌入请求数量 如果发生 OOM,请减小这两个参数;图像嵌入通常需要比文本更小的批量
客户端并发调优 HTTP 基准测试或生产环境客户端 增加并行的 /v1/embeddings 请求(参见第 8.1 节) 提高提供给服务器批处理器的负载 当客户端并发超过 --max-num-seqs 后,吞吐量提升趋于平稳;请关注 p99 延迟

9.2.2 通用调优参考

常规调优方法请参阅 公共性能调优文档。

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

10 常见问题

关于常见的环境、安装和一般参数问题,请参阅 公共 FAQ;本章仅介绍模型相关问题。

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

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

问:能否在同一个请求中嵌入文本和图像?

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

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 vl/vllm-ascend