Prhub

#47991 [Model] Add RobertaForTokenClassification / XLMRobertaForTokenClassification

原始 PR 作者 krishy91 合并时间 2026-07-15 22:30 文件变更 5 提交数 4 评论 3 代码增减 +118 / -1

执行摘要

新增 RoBERTa/XLM-RoBERTa 的 Token 分类支持

当前 vLLM 的 /pooling 路由支持 BertForTokenClassification 和 ModernBertForTokenClassification,但 RoBERTa-family checkpoints(如 roberta-large-ner-english、xlm-roberta-base-ner-hrl)没有对应的 token-classification 入口,导致无法通过 vLLM 的 pooling runner 正确服务于这些模型。此 PR 填补该空白。

值得查阅以了解如何添加新的 pooling 模型。设计决策包括:通过装饰器指定 attention 类型和池化类型,复用 BertModel 并替换 embedding_class,利用 AutoWeightsLoader 自动映射权重。这展示了一种低代码复用的模型注册模式。

讨论亮点

该 PR 来自 fork,Claude 自动 review 未开启。审核者 noooop 直接批准,无额外讨论。

实现拆解

  1. 在 vllm/model_executor/models/roberta.py 中新增 RobertaForTokenClassification 类:使用 @attn_type('encoder_only')@default_pooling_type(tok_pooling_type='ALL') 装饰器标识为 encoder-only 模型和 ALL token 池化。构造函数初始化一个 BertModel(指定 embedding_class=RobertaEmbedding)和线性分类器,池化层使用 pooler_for_token_classify。load_weights 通过 AutoWeightsLoader 自动加载。forward 方法调用 roberta 子模块后调用分类器。

  2. 在 vllm/model_executor/models/registry.py 注册模型:在 _TOKEN_CLASSIFICATION_MODELS 字典中添加 RobertaForTokenClassificationXLMRobertaForTokenClassification,均指向 roberta 模块的 RobertaForTokenClassification 类。

  3. 测试配套:在 tests/models/language/pooling/test_token_classification.py 中新增 test_xlm_roberta_models 测试函数,使用 Davlan/xlm-roberta-base-ner-hrl 模型,比较 vLLM 输出与 HF Transformers 输出的 softmax logits,容忍度 3.2e-2 atol / 1e-3 rtol。同时更新 tests/models/registry.py,注册两个测试模型。

  4. 文档更新:在 docs/models/pooling_models/token_classify.md 的支持模型表中添加两行。

文件 模块 状态 重要度
vllm/model_executor/models/roberta.py 模型层 modified 8.12
vllm/model_executor/models/registry.py 模型注册 modified 4.94
tests/models/language/pooling/test_token_classification.py 测试 modified 5.73
tests/models/registry.py 测试 modified 3.92
docs/models/pooling_models/token_classify.md 文档 modified 1.53

关键符号

RobertaForTokenClassification.__init__ RobertaForTokenClassification.forward RobertaForTokenClassification.load_weights RobertaForTokenClassification.embed_input_ids test_xlm_roberta_models

关键源码片段

vllm/model_executor/models/roberta.py core-logic

核心实现文件,新增 RobertaForTokenClassification 类及其 forward/load_weights/embed_input_ids 方法,通过装饰器和复用 BertModel 实现 token 分类逻辑。

# vllm/model_executor/models/roberta.py# 新增的 RobertaForTokenClassification 类
@attn_type("encoder_only")
@default_pooling_type(tok_pooling_type="ALL")
class RobertaForTokenClassification(nn.Module):
    """基于 RoBERTa 的 Token 分类模型,直接复用 BertForTokenClassification 的结构,
    替换为 RobertaEmbedding 和 roberta 权重前缀。该别名也用于 XLM-RoBERTa。"""
​
    is_pooling_model = True
​
    def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
        super().__init__()
        config = vllm_config.model_config.hf_config
        self.head_dtype = vllm_config.model_config.head_dtype
        self.num_labels = config.num_labels
        # 使用 BertModel 并指定 embedding_class=RobertaEmbedding
        self.roberta = BertModel(
            vllm_config=vllm_config,
            prefix=maybe_prefix(prefix, "roberta"),
            embedding_class=RobertaEmbedding,
        )
        # 线性分类器,输出 num_labels 个 logits
        self.classifier = nn.Linear(
            config.hidden_size, config.num_labels, dtype=self.head_dtype
        )
​
        pooler_config = vllm_config.model_config.pooler_config
        assert pooler_config is not None
        # 使用 token 分类池化器(ALL pooling)
        self.pooler = pooler_for_token_classify(pooler_config)
​
    def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
        # 委托给 roberta 子模型
        return self.roberta.embed_input_ids(input_ids)
​
    def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
        # 使用 AutoWeightsLoader 自动处理权重映射
        loader = AutoWeightsLoader(self)
        return loader.load_weights(weights)
​
    def forward(
        self,
        input_ids: torch.Tensor | None,
        positions: torch.Tensor,
        intermediate_tensors: IntermediateTensors | None = None,
        inputs_embeds: torch.Tensor | None = None,
        token_type_ids: torch.Tensor | None = None,
    ) -> torch.Tensor:
        # 处理 token_type_ids 编码(与 Bert 相同)
        if token_type_ids is not None:
            assert self.roberta.config.vocab_size < (1 << TOKEN_TYPE_SHIFT)
            assert input_ids is not None
            _encode_token_type_ids(input_ids, token_type_ids)
​
        # 通过 RoBERTa backbone 获取 hidden states
        hidden_states = self.roberta(
            input_ids=input_ids,
            positions=positions,
            inputs_embeds=inputs_embeds,
            intermediate_tensors=intermediate_tensors,
        )
        # 转换到 head_dtype 后通过分类器
        hidden_states = hidden_states.to(self.head_dtype)
        return self.classifier(hidden_states)
tests/models/language/pooling/test_token_classification.py test-coverage

端到端测试验证新模型在 vLLM 与 HF Transformers 上输出一致,是质量保证的重要环节。

# tests/models/language/pooling/test_token_classification.py@pytest.mark.parametrize("model", ["Davlan/xlm-roberta-base-ner-hrl"])
@pytest.mark.parametrize("dtype", ["float"])
@torch.inference_mode
def test_xlm_roberta_models(hf_runner, vllm_runner, example_prompts, model, dtype):
    # 使用 vLLM runner 进行 token 分类,与 HF Transformers 对比
    with vllm_runner(model, max_model_len=None, dtype=dtype) as vllm_model:
        vllm_outputs = vllm_model.token_classify(example_prompts)
​
    # ROCm 上使用 eager attention 避免 flash attention 精度差异
    hf_model_kwargs = {}
    if current_platform.is_rocm():
        hf_model_kwargs["attn_implementation"] = "eager"
​
    with hf_runner(model, dtype=dtype, auto_cls=AutoModelForTokenClassification,
                   model_kwargs=hf_model_kwargs) as hf_model:
        tokenizer = hf_model.tokenizer
        hf_outputs = []
        for prompt in example_prompts:
            inputs = tokenizer([prompt], return_tensors="pt")
            inputs = hf_model.wrap_device(inputs)
            output = hf_model.model(**inputs)
            hf_outputs.append(softmax(output.logits[0]))
​
    # 比较每个 token 的 softmax logits 差异,误差容忍度与现有模型测试一致
    for hf_output, vllm_output in zip(hf_outputs, vllm_outputs):
        hf_output = hf_output.detach().clone().cpu().float()
        vllm_output = vllm_output.detach().clone().cpu().float()
        torch.testing.assert_close(hf_output, vllm_output, atol=3.2e-2, rtol=1e-3)

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

风险极低。新类完全模仿已存在的 BertForTokenClassification 结构,仅替换 embedding 层和权重前缀。RoBERTa 权重前缀(roberta.)与 vLLM 现有 RobertaForSequenceClassification 的处理方式一致,已在生产中使用。测试覆盖了代表性的 XLM-RoBERTa 模型。潜在风险包括:未来 HuggingFace 可能修改 RoBERTa 配置,但 vLLM 会持续适配。建议监控 RoBERTa 系列模型的 regression。

影响有限。仅扩展 pooling runner 的能力,不影响系统性能。用户现在可以使用 token-classify 模式加载 RoBERTa 和 XLM-RoBERTa 模型。团队在后续模型扩展时可以借鉴此模式。文档已更新,用户可发现新支持。

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论