Prhub

#28958 Support nvidia/LocateAnything-3B

原始 PR 作者 Jyothirmaikottu 合并时间 2026-06-30 00:16 文件变更 11 提交数 7 评论 16 代码增减 +1116 / -0

执行摘要

支持 nvidia/LocateAnything-3B 视觉定位模型

本 PR 旨在响应 Feature Request #27674,为 SGLang 添加 nvidia/LocateAnything-3B 的原生支持。该模型是一个视觉接地/检测模型,覆盖目标检测、短语接地、场景文本检测、GUI 接地和指点等任务。PR body 指出模型架构复用 MoonViT、InternVL 投影器和 Qwen2 骨干,且无需 transformers fallback ,直接通过 SGLang 的 model_type 注册实现加载。

值得精读。本 PR 展示了在 SGLang 框架中集成新多模态模型的标准实践:利用已有组件(MoonViT、Qwen2)组装、注册 config/model_type、编写专属图像处理器,并适配多模态数据流水线。其 review 讨论涉及多图像拆分与 custom logit processor 集成的设计权衡,对后来者具有参考价值。特别是 LocateAnythingBoxGrammarLogitProcessor 的约束解码状态机实现,可作为自定义 logit 处理的参考范例。

讨论亮点
  1. 多图像拆分器对 image_grid_hws 的缺失:JustinTong 指出 get_new_expanded_mm_items 只查找 image_grid_thw,而 MoonViT 模型使用 image_grid_hws,导致多图像请求不会被拆分,影响 RadixAttention 缓存粒度。作者随后扩展了拆分器支持两种格式,并增加了回归测试。
  2. Box 语法处理器无法自动接入:JustinTong 发现 LocateAnythingBoxGrammarLogitProcessor 已定义并测试,但未被服务端自动关联,客户端需要手动通过 build_sampling_params 辅助函数配置 token ID 和服务端 flag。作者随后补全了该辅助函数和文档。
  3. build_sampling_params 示例导致 500 错误:JustinTong 指出 docstring 示例将 custom_logit_processor 错误地放入 sampling_params,而它应是 GenerateReqInput 的顶级字段,会导致 msgspec 报错。作者修正了 docstring 并澄清了 API 契约。
  4. 约束解码仅扫描 output_ids:作者确认 box 语法处理器只扫描 output_ids 而非 prompt+output,以避免引入 prompt 中未闭合 box 带来的复杂性,并在注释中明确这一设计决策。
  5. 缺少端到端 CI 测试:JustinTong 指出模型缺少自动化的端到端推理测试,仅有人工验证。作者添加了前向形状接线测试(stub vision tower),但仍未覆盖完整 GPU 推理路径。

实现拆解

  1. 配置与注册:在 python/sglang/srt/configs/locate_anything.py 中定义 LocateAnythingConfig,组合 MoonViTConfig(视觉)和 Qwen2Config(文本),并存储所有特殊 token ID(box_start/endcoord_start/endnone_token_id等)。通过 configs/__init__.pymodel_config.pyutils/hf_transformers/common.py 将其注册到全局架构映射中,使模型加载时能直接路由到 LocateAnythingForConditionalGeneration
  2. 核心模型实现:在 python/sglang/srt/models/locate_anything.py 中实现 LocateAnythingForConditionalGeneration,复用 MoonVitPretrainedModel(视觉 tower)和 Qwen2ForCausalLM(语言模型),并实现专属的 LocateAnythingMultiModalProjector(mlp1 投影器:先 patch merge 再 LayerNorm,与 Kimi-VL 的顺序不同)。get_image_feature 方法合并多图像特征并传入投影器;load_weights 执行权重前缀映射与 tied embedding 处理。额外提供可选的 LocateAnythingBoxGrammarLogitProcessor,在 <box>...</box> 块内约束解码为合法的 none / 2-d point / 4-d bbox 形式。
  3. 多模态图像处理:在 python/sglang/srt/multimodal/processors/locate_anything.py 中编写 LocateAnythingImageProcessor,继承基础多模态处理器,通过覆盖 process_mm_data_async 将 prompt 中的 <image-N> 占位符替换为图像特征嵌入 token,并复用 SGLang 标准的多模态数据加载流水线。
  4. 多图像拆分适配:在 python/sglang/srt/managers/mm_utils.pyget_new_expanded_mm_items 中添加对 image_grid_hws 的识别——MoonViT 模型使用的网格格式为 [h, w] 而非标准的 [t, h, w]。新增 _is_rank2_grid 辅助函数,保护退化(一维)网格的回退,确保多图像请求能正确拆分为每图像单独项,以维持 RadixAttention 缓存粒度和 chunked-prefill 快速路径。
  5. 测试与文档:新增 test/registered/unit/configs/test_locate_anything_config.py(配置单元测试)、test/registered/unit/models/test_locate_anything.py(投影仪和 box 语法处理器状态机测试,含前向形状接线测试)、test/registered/unit/managers/test_mm_utils_split.py(拆分器回归测试)。更新 docs_new/docs/supported-models/multimodal_language_models.mdx,为 LocateAnything-3B 增加一行支持记录,并注明需 --enable-custom-logit-processor 方可使用约束解码。
文件 模块 状态 重要度
python/sglang/srt/models/locate_anything.py 模型层 added 9.17
python/sglang/srt/multimodal/processors/locate_anything.py 图像处理 added 8.15
python/sglang/srt/configs/locate_anything.py 配置定义 added 7.96
python/sglang/srt/managers/mm_utils.py 多模态工具 modified 7.01
test/registered/unit/models/test_locate_anything.py 测试用例 added 7.97
test/registered/unit/managers/test_mm_utils_split.py 拆分测试 added 7.29

关键符号

LocateAnythingMultiModalProjector.__init__ LocateAnythingMultiModalProjector.forward LocateAnythingForConditionalGeneration.__init__ LocateAnythingForConditionalGeneration.get_image_feature LocateAnythingForConditionalGeneration.pad_input_ids LocateAnythingForConditionalGeneration.load_weights LocateAnythingBoxGrammarLogitProcessor.__init__ LocateAnythingBoxGrammarLogitProcessor.__call__ LocateAnythingConfig.__init__ LocateAnythingImageProcessor.__init__ LocateAnythingImageProcessor.process_mm_data_async get_new_expanded_mm_items _is_rank2_grid

关键源码片段

python/sglang/srt/multimodal/processors/locate_anything.py core-logic

多模态图像处理器,负责将图像数据转换为模型需要的多模态输入格式,包括标记替换和特征提取流程。

# SPDX-License-Identifier: Apache-2.0
import re
from typing import Dict, List, Unionfrom sglang.srt.managers.schedule_batch import MultimodalProcessorOutput
from sglang.srt.models.locate_anything import LocateAnythingForConditionalGeneration
from sglang.srt.multimodal.processors.base_processor import (
    BaseMultimodalProcessor as SGLangBaseProcessor,
)
from sglang.srt.multimodal.processors.base_processor import MultimodalSpecialTokensclass LocateAnythingImageProcessor(SGLangBaseProcessor):
    models = [LocateAnythingForConditionalGeneration]
    # The LocateAnything HF processor is remote-code and does not support tensor inputs.
    gpu_image_decode = False
​
    def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
        super().__init__(hf_config, server_args, _processor, *args, **kwargs)
        # The model's chat template emits numbered ``<image-N>`` placeholders.
        # The HF LocateAnythingProcessor expands each into
        # ``<img>`` + N×``<IMG_CONTEXT>`` + ``</img>`` and only the
        # ``<IMG_CONTEXT>`` (id 151665) run carries vision embeddings, so the
        # offset/embedding token id is ``image_token_index`` while the prompt-level
        # placeholder we split on is ``<image-N>``.
        self.mm_tokens = MultimodalSpecialTokens(
            image_token_id=hf_config.image_token_index,
            image_token_regex=re.compile(r"<image-\d+>"),
        ).build(_processor)
​
    async def process_mm_data_async(self, image_data, input_text, request_obj, *args, **kwargs):
        base_output = await self.load_mm_data(
            prompt=input_text,
            image_data=image_data,
            multimodal_tokens=self.mm_tokens,
        )
        mm_items, input_ids, _ = self.process_and_combine_mm_data(base_output, self.mm_tokens)
        return MultimodalProcessorOutput(
            input_ids=input_ids.tolist(),
            mm_items=mm_items,
            im_token_id=self.mm_tokens.image_token_id,
        )
python/sglang/srt/configs/locate_anything.py core-logic

模型配置类,定义了视觉、文本子配置和所有特殊标记 ID,是模型加载和初始化的入口。

# SPDX-License-Identifier: Apache-2.0
# Adapted from https://huggingface.co/nvidia/LocateAnything-3B/blob/main/configuration_locateanything.pyfrom typing import Optional, Union
from transformers.configuration_utils import PretrainedConfig
from transformers.models.qwen2 import Qwen2Config
from sglang.srt.configs.kimi_vl_moonvit import MoonViTConfigclass LocateAnythingConfig(PretrainedConfig):
    model_type = "locateanything"
​
    def __init__(
        self,
        vision_config: Optional[Union[dict, MoonViTConfig]] = None,
        text_config: Optional[Union[dict, Qwen2Config]] = None,
        image_token_index: int = 151665,
        box_start_token_id: int = 151668,
        box_end_token_id: int = 151669,
        ref_start_token_id: int = 151672,
        ref_end_token_id: int = 151673,
        coord_start_token_id: int = 151677,
        coord_end_token_id: int = 152677,
        none_token_id: int = 4064,
        mlp_connector_layers: int = 2,
        **kwargs,
    ):
        # Build sub-configs from dict or use defaults
        if vision_config is None:
            vision_config = MoonViTConfig()
        elif isinstance(vision_config, dict):
            vision_config = MoonViTConfig(**vision_config)
        self.vision_config = vision_config
​
        if text_config is None:
            text_config = Qwen2Config()
        elif isinstance(text_config, dict):
            text_config = Qwen2Config(**text_config)
        self.text_config = text_config
​
        # Store all special token IDs used by the optional box grammar processor
        self.image_token_index = image_token_index
        self.box_start_token_id = box_start_token_id
        self.box_end_token_id = box_end_token_id
        self.ref_start_token_id = ref_start_token_id
        self.ref_end_token_id = ref_end_token_id
        self.coord_start_token_id = coord_start_token_id
        self.coord_end_token_id = coord_end_token_id
        self.none_token_id = none_token_id
        self.mlp_connector_layers = mlp_connector_layers
​
        super().__init__(**kwargs)

评论区精华

多图像拆分器对 image_grid_hws 的支持 设计

JustinTong 指出 `get_new_expanded_mm_items` 只查找 `image_grid_thw`,而 MoonViT 模型使用 `image_grid_hws`,导致多图像请求不会拆分,影响 RadixAttention 缓存粒度。

结论:扩展拆分器同时支持 `image_grid_hws` 和 `image_grid_thw`,并添加回归测试(`test_mm_utils_split.py`)。 · 已解决

Box 语法处理器无法自动接入服务路径 设计

JustinTong 发现 `LocateAnythingBoxGrammarLogitProcessor` 已定义并测试,但未被服务端自动关联,客户端需要手动配置 token ID 和服务端 flag。

结论:添加 `build_sampling_params` 辅助函数,并在文档中注明需设置 `--enable-custom-logit-processor`。 · 已解决

build_sampling_params 示例导致 500 错误 正确性

JustinTong 指出 docstring 示例将 `custom_logit_processor` 错误地放入 `sampling_params`,而它应是 `GenerateReqInput` 的顶级字段,会导致 msgspec 报错并 500。

结论:修正 docstring,明确 `custom_params` 放入 `sampling_params`,`custom_logit_processor` 作为顶级字段传递。 · 已解决

约束解码只扫描 output_ids 的取舍 设计

作者说明 box 语法处理器只扫描 `output_ids` 而非 `prompt+output`,以避免 prompt 中 unclosed box 的额外复杂性。JustinTong 点明若 prompt 中有未闭合 box 则约束不会生效。

结论:保留 output_ids-only 扫描,在注释中明确这一设计决策和风险。 · 已解决

缺少拆分器回归测试 测试

JustinTong 指出拆分器修复没有回归测试,后续修改可能 silent regression。

结论:作者添加了 `test_mm_utils_split.py`,覆盖 HWS/THW/ 退化网格等场景。 · 已解决

风险与影响

  1. 多图像拆分正确性:对 image_grid_hws 的支持是新加入的逻辑,虽然已添加单元测试,但实际多图像请求在生产中可能遇到非预期的网格格式(如更高维张量),若不匹配会导致拆分失败或错误。修复中已包含一维网格退化保护,但未覆盖更高维情况。
  2. 约束解码的 Prompt 忽略LocateAnythingBoxGrammarLogitProcessor 只扫描 output_ids,若 prompt 中包含未闭合的 <box>(如 malformed few-shot),则约束不会生效,模型可能生成非法标记。作者已将此风险记录为已知限制。
  3. 缺少端到端 GPU 测试:仅有 CPU 单元测试,暂未将完整的 GPU 推理纳入 CI,若后续对 MoonViT 或 Qwen2 模型有修改可能造成回归而未被检测到。
  4. 投影器的 Reshape 使用LocateAnythingMultiModalProjector.forward 使用 reshape(而非 view)处理非连续张量,虽然测试覆盖了非连续场景,但若未来输入布局变化可能导致尺寸错误。测试已验证基本非连续场景。

用户侧:现在可以通过 python3 -m sglang.launch_server --model-path nvidia/LocateAnything-3B --trust-remote-code 加载该视觉接地模型,并通过设置 skip_special_tokens=False 获取结构化输出。高级用户可启用 --enable-custom-logit-processor 并使用 build_sampling_params 辅助函数来约束 box 内的解码格式。
系统侧:新增约 1.1k 行代码,主要集中在模型定义、配置和处理器上;对 mm_utils.py 的修改(约 36 行)采用了向后兼容的方式(key 不存在时 fallback),未影响现有行为。
维护团队:需要关注 MoonViT 和 Qwen2 模型的后续变更是否会破坏 LocateAnything 的复用部分;新增的拆分器测试可以捕获多图像拆分相关回归。

多图像拆分依赖新逻辑 自定义处理器需单独启用 缺少端到端 CI 覆盖 投影仪使用了 reshape 而非 view

关联 Issue

#27674 [Feature] Support for nvidia/LocateAnything-3B

完整报告

参与讨论