[Model] Add Clef-Flash support (#61308)

Signed-off-by: Tianyu Guo <guoty@inferact.ai>
Signed-off-by: Isotr0py <Isotr0py@outlook.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Isotr0py <Isotr0py@outlook.com>
This commit is contained in:
Tianyu Guo
2026-10-13 05:46:37 +00:00
committed by GitHub
co-authored by Codex Isotr0py
parent 0d2689e0df
commit 380638afbb
11 changed files with 580 additions and 361 deletions
@@ -130,6 +130,30 @@ the request lists them, a `noul` labels its options `yes` and `no`, and a
| options per `choice` | 26 |
| levels per `score` | 10 |
## Clef-Flash
[Clef-Flash](https://huggingface.co/Cloudflare/clef-flash) uses a Qwen3.5-9B
backbone and a joint schema head to score all questions in one forward pass.
vLLM detects the head and serves the checkpoint as a pooling model.
```bash
vllm serve Cloudflare/clef-flash --max-model-len 16384
```
Send requests to `/v1/systemone` using `state` and `questions`, as in the
examples above. `choice`, `noul` and `score` questions are supported.
- Text and JSON states use the checkpoint's fixed prompt. JSON is compact
with sorted keys. `instructions` returns 400; `chat_template_kwargs`
has no effect.
- The state is truncated to fit 16,384 tokens, or `max_model_len` if smaller.
A schema that exceeds this bound returns 400.
- `images` and `videos` are placed before the state. Videos are sampled at
2 fps and processed by the Qwen3.5 vision processor. Audio inputs return
400, and video soundtracks are ignored.
- Each request performs one pooling read. `usage.input_tokens` counts the
prompt once, `output_tokens` is 0, and `label_mass` is 1.
## Clef-Omni
[Clef-Omni](https://huggingface.co/Cloudflare/clef-omni) is a decision model:
@@ -188,7 +212,7 @@ vllm serve LiquidAI/d1-3B
| `instructions` | optional context placed ahead of the questions |
| `chat_template_kwargs` | passed to the chat template, for example `{"enable_thinking": false}` |
| `cache_salt` | optional [prefix-cache salt](../../usage/security.md#cache-salting) applied to every question |
| `images`, `audio`, `videos` | optional media URLs or data URLs, for models that take them ([Clef-Omni](#clef-omni), [Liquid d1](#liquid-d1), images only) |
| `images`, `audio`, `videos` | optional media URLs or data URLs, for models that take them ([Clef-Flash](#clef-flash), images/video; [Clef-Omni](#clef-omni); [Liquid d1](#liquid-d1), images only) |
A question with any field other than `type`, `instructions` and `criteria` is
rejected with a 400.
+34
View File
@@ -70,6 +70,40 @@ def _load_groundtruth(filename: str) -> dict:
return json.load(f)
@pytest.mark.parametrize("has_joint_head", [False, True])
@pytest.mark.parametrize(
"architecture", ["Qwen3_5ForConditionalGeneration", "ColQwen3_5"]
)
def test_qwen35_joint_head_selects_clef_flash(tmp_path, has_joint_head, architecture):
"""Only a Qwen3.5 generation checkpoint with a joint head selects Clef."""
from transformers import Qwen3_5Config
head_config = {
"hidden_size": 4096,
"width": 1024,
"routing_layers": 2,
"layers": 4,
"heads": 16,
"feedforward": 4096,
}
if has_joint_head:
(tmp_path / "joint_head_config.json").write_text(json.dumps(head_config))
config = Qwen3_5Config(architectures=[architecture])
config.name_or_path = str(tmp_path)
model_config = SimpleNamespace(
hf_config=config,
hf_text_config=config.text_config,
_supports_multimodal_for_mm_prefix=lambda: True,
)
converted = ModelConfig.get_model_arch_config(model_config)
if has_joint_head and architecture == "Qwen3_5ForConditionalGeneration":
assert converted.architectures == ["ClefFlashForJointSchema"]
assert config.joint_head_config == head_config
else:
assert converted.architectures == [architecture]
assert not hasattr(config, "joint_head_config")
def _assert_model_arch_config(
model_config, expected: dict, check_head_size: bool = True
):
@@ -33,6 +33,7 @@ from vllm.entrypoints.generate.structured_decisions.serving import (
parse_questions,
)
from vllm.entrypoints.generate.structured_decisions.strategies import (
ClefFlashStrategy,
DiffusionGemmaCanvasStrategy,
JointSchemaStrategy,
LiquidStrategy,
@@ -94,6 +95,8 @@ def test_strategy_selection():
# Clef-Omni scores options with its head, so it reads no logprobs.
clef = model("ClefOmniForJointSchema", "raw_logits")
assert select_read_strategy(clef, ("token_classify",)) is JointSchemaStrategy
clef_flash = model("ClefFlashForJointSchema", "raw_logits")
assert select_read_strategy(clef_flash, ("token_classify",)) is ClefFlashStrategy
def test_route_is_registered_by_default():
@@ -366,8 +369,8 @@ JOINT_SCHEMA_REQUEST: dict[str, Any] = dict(
)
@pytest.fixture
def joint_schema(qwen, engine_and_renderer):
@pytest.fixture(params=[JointSchemaStrategy, ClefFlashStrategy])
def joint_schema(request, qwen, engine_and_renderer):
tokenizer, _ = qwen
engine, online_renderer = engine_and_renderer
# The probability the head gives each option, by option id.
@@ -382,9 +385,7 @@ def joint_schema(qwen, engine_and_renderer):
yield PoolingRequestOutput(request_id, PoolingOutput(data), ids, 0, True)
engine.encode.side_effect = encode
strategy = JointSchemaStrategy(
ReadContext(engine, online_renderer, None, "auto", {})
)
strategy = request.param(ReadContext(engine, online_renderer, None, "auto", {}))
return serving(engine, strategy), engine, tokenizer
@@ -468,7 +469,10 @@ async def test_media_needs_a_model_that_takes_it(serving_and_engine):
@pytest.mark.asyncio
async def test_joint_schema_reads_media_before_the_state(joint_schema, monkeypatch):
@pytest.mark.parametrize("num_images", [1, 2])
async def test_joint_schema_reads_media_before_the_state(
joint_schema, monkeypatch, num_images
):
serving, engine, tokenizer = joint_schema
strategy = serving.strategy
strategy.__dict__["_connector"] = MediaConnector()
@@ -491,23 +495,30 @@ async def test_joint_schema_reads_media_before_the_state(joint_schema, monkeypat
Image.new("RGB", (8, 8), "red").save(buffer, format="PNG")
image = "data:image/png;base64," + base64.b64encode(buffer.getvalue()).decode()
response = await serving.create_decision(
StructuredDecisionRequest(**JOINT_SCHEMA_REQUEST, images=[image])
StructuredDecisionRequest(**JOINT_SCHEMA_REQUEST, images=[image] * num_images)
)
assert isinstance(response, StructuredDecisionResponse)
placeholder = tokenizer.encode(
"<|vision_start|><|image_pad|><|vision_end|>\n", add_special_tokens=False
)
if isinstance(strategy, ClefFlashStrategy):
media_text = "<|vision_start|><|image_pad|><|vision_end|>" * num_images + "\n"
else:
media_text = "<|vision_start|><|image_pad|><|vision_end|>\n" * num_images
placeholder = tokenizer.encode(media_text, add_special_tokens=False)
assert rendered["prompt_token_ids"] == strategy.prefix + placeholder
assert rendered["multi_modal_data"]["image"][0].size == (8, 8)
assert rendered["mm_processor_kwargs"] == {
"images_kwargs": {"size": strategy.IMAGE_SIZE}
}
if isinstance(strategy, ClefFlashStrategy):
assert rendered["mm_processor_kwargs"] == {
"videos_kwargs": {"do_sample_frames": False}
}
else:
assert rendered["mm_processor_kwargs"] == {
"images_kwargs": {"size": strategy.IMAGE_SIZE}
}
prompt = engine.encode.call_args.args[0]
assert prompt["type"] == "multimodal"
ids = prompt["prompt_token_ids"]
expanded = len(strategy.prefix) + len(placeholder) + 2
assert ids[:expanded].count(image_pad) == 3
expanded = len(strategy.prefix) + len(placeholder) + 2 * num_images
assert ids[:expanded].count(image_pad) == 3 * num_images
state_and_questions = JOINT_SCHEMA_PROMPT.split("STATE:\n", 1)[1]
assert tokenizer.decode(ids[expanded:]) == state_and_questions
assert response.answers["team"]["choice"] == "billing"
@@ -515,7 +526,9 @@ async def test_joint_schema_reads_media_before_the_state(joint_schema, monkeypat
@pytest.mark.asyncio
async def test_joint_schema_hears_video_soundtracks(joint_schema, monkeypatch):
async def test_joint_schema_video_audio_follows_model_modalities(
joint_schema, monkeypatch
):
serving, engine, tokenizer = joint_schema
strategy = serving.strategy
video = MediaWithBytes(
@@ -532,6 +545,7 @@ async def test_joint_schema_hears_video_soundtracks(joint_schema, monkeypatch):
return video
async def fetch_audio_async(self, url):
assert not isinstance(strategy, ClefFlashStrategy)
if url == "https://example.org/silent.mp4":
raise ValueError("No audio stream found.")
return track
@@ -552,11 +566,17 @@ async def test_joint_schema_hears_video_soundtracks(joint_schema, monkeypatch):
)
)
assert isinstance(response, StructuredDecisionResponse)
assert rendered["multi_modal_data"] == {"video": [video], "audio": [track]}
assert rendered["mm_processor_kwargs"] == {
"images_kwargs": {"size": strategy.IMAGE_SIZE},
"use_audio_in_video": True,
}
if isinstance(strategy, ClefFlashStrategy):
assert rendered["multi_modal_data"] == {"video": [video]}
assert rendered["mm_processor_kwargs"] == {
"videos_kwargs": {"do_sample_frames": False}
}
else:
assert rendered["multi_modal_data"] == {"video": [video], "audio": [track]}
assert rendered["mm_processor_kwargs"] == {
"images_kwargs": {"size": strategy.IMAGE_SIZE},
"use_audio_in_video": True,
}
# A video without a decodable soundtrack is watched silently.
response = await serving.create_decision(
@@ -566,7 +586,8 @@ async def test_joint_schema_hears_video_soundtracks(joint_schema, monkeypatch):
)
assert isinstance(response, StructuredDecisionResponse)
assert rendered["multi_modal_data"] == {"video": [video]}
assert rendered["mm_processor_kwargs"]["use_audio_in_video"] is False
if not isinstance(strategy, ClefFlashStrategy):
assert rendered["mm_processor_kwargs"]["use_audio_in_video"] is False
# Audio clips cannot be read with videos that have soundtracks.
response = await serving.create_decision(
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""The Clef-Omni pooler against a port of the checkpoint's joint schema head,
"""The shared Clef pooler against a port of the checkpoint's joint schema head,
which loops over questions where the pooler batches them."""
import math
@@ -11,7 +11,10 @@ import pytest
import torch
import torch.nn.functional as F
from vllm.model_executor.models.clef_omni import JointSchemaHead, JointSchemaPooler
from vllm.model_executor.models.clef_joint_schema import (
JointSchemaHead,
JointSchemaPooler,
)
from vllm.pooling_params import PoolingParams
from vllm.v1.pool.metadata import PoolingMetadata, PoolingStates
+4
View File
@@ -802,6 +802,10 @@ _MULTIMODAL_EXAMPLE_MODELS = {
"Salesforce/blip2-opt-2.7b",
extras={"6b": "Salesforce/blip2-opt-6.7b"},
),
"ClefFlashForJointSchema": _HfExamplesInfo(
"Cloudflare/clef-flash",
max_model_len=4096,
),
"Cohere2VisionForConditionalGeneration": _HfExamplesInfo(
"CohereLabs/command-a-vision-07-2025",
extras={"command-a-plus": "CohereLabs/command-a-plus-05-2026-bf16"},
@@ -809,7 +809,63 @@ class JointSchemaStrategy(ReadStrategy):
return reads
class ClefFlashStrategy(JointSchemaStrategy):
"""Clef-Flash's joint schema prompt with Qwen3.5 image/video processing."""
modalities = frozenset({"image", "video"})
MAX_LENGTH = 16384
PLACEHOLDERS = {
"image": "<|vision_start|><|image_pad|><|vision_end|>",
"video": "<|vision_start|><|video_pad|><|vision_end|>",
}
async def _render_media(
self, media: Mapping[str, Sequence[str]], cache_salt: str | None
) -> MultiModalInput:
connector = self._connector
video_io = VideoMediaIO(ImageMediaIO(), num_frames=-1, fps=self.VIDEO_FPS)
try:
images, videos = await asyncio.gather(
asyncio.gather(
*map(connector.fetch_image_async, media.get("image", ()))
),
self._fetch(
media.get("video", ()), video_io, envs.VLLM_VIDEO_FETCH_TIMEOUT
),
)
except ValueError as e:
raise StructuredDecisionError(f"media: {e}") from e
text = (
self.PLACEHOLDERS["image"] * len(images)
+ self.PLACEHOLDERS["video"] * len(videos)
+ "\n"
)
mm_data: dict[str, Any] = {}
if images:
mm_data["image"] = images
if videos:
mm_data["video"] = videos
prompt = TokensPrompt(
prompt_token_ids=self.prefix + self._tokens(text),
multi_modal_data=mm_data,
mm_processor_kwargs={"videos_kwargs": {"do_sample_frames": False}},
)
if cache_salt is not None:
prompt["cache_salt"] = cache_salt
try:
engine_input = (
await self.context.online_renderer.renderer.process_for_engine_async(
prompt, time.time()
)
)
except ValueError as e:
raise StructuredDecisionError(f"media: {e}") from e
assert engine_input["type"] == "multimodal"
return engine_input
READ_STRATEGIES: dict[str, type[ReadStrategy]] = {
"ClefFlashForJointSchema": ClefFlashStrategy,
"ClefOmniForJointSchema": JointSchemaStrategy,
"Lfm2VlForConditionalGeneration": LiquidStrategy,
"Qwen3ForCausalLM": NextTokenStrategy,
+44
View File
@@ -0,0 +1,44 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Clef-Flash: the Qwen3.5 backbone with a joint schema decision head."""
import torch
from vllm.config import VllmConfig
from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
from vllm.model_executor.model_loader.default_loader import DefaultModelLoader
from vllm.multimodal import MULTIMODAL_REGISTRY
from .clef_joint_schema import JOINT_HEAD_WEIGHTS, JointSchemaHead, JointSchemaPooler
from .interfaces_base import default_pooling_type
from .qwen3_5 import Qwen3_5ForConditionalGeneration, Qwen3_5ProcessingInfo
from .qwen3_vl import Qwen3VLDummyInputsBuilder, Qwen3VLMultiModalProcessor
@default_pooling_type(tok_pooling_type="ALL")
@MULTIMODAL_REGISTRY.register_processor(
Qwen3VLMultiModalProcessor,
info=Qwen3_5ProcessingInfo,
dummy_inputs=Qwen3VLDummyInputsBuilder,
)
class ClefFlashForJointSchema(Qwen3_5ForConditionalGeneration):
is_pooling_model = True
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__(vllm_config=vllm_config, prefix=prefix)
model_config = vllm_config.model_config
self.pooler = JointSchemaPooler(
JointSchemaHead(**model_config.hf_config.joint_head_config),
self._embed_lexical,
)
self.secondary_weights = [
DefaultModelLoader.Source(
model_or_path=model_config.model,
revision=model_config.revision,
prefix="pooler.head.",
allow_patterns_overrides=[JOINT_HEAD_WEIGHTS],
)
]
def _embed_lexical(self, token_ids: torch.Tensor) -> torch.Tensor:
return VocabParallelEmbedding.forward(self.language_model.lm_head, token_ids)
@@ -0,0 +1,343 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Shared joint schema head for Clef decision models.
The head reads the final hidden states of the whole prompt, so it runs as a
``token_classify`` pooler.
Each request carries its schema in ``PoolingParams.extra_kwargs`` under
``"joint_schema"``:
- ``question_types``: 0 (noul), 1 (choice) or 2 (score) per question.
- ``question_spans``: the ``[start, end)`` of each question's instructions.
- ``option_spans``: the ``[start, end)`` of each option, question by question.
- ``option_counts``: the number of options of each question.
Spans count from the end of the prompt (``-len(prompt) <= start < end <= 0``),
so they hold after multimodal placeholders are expanded. The output has one
value per option, in ``option_spans`` order: its probability within its
question, or its logit with ``use_activation=False``.
"""
import math
from collections.abc import Callable, Set
from dataclasses import dataclass
from typing import Any
import torch
import torch.nn.functional as F
from torch import nn
from vllm.model_executor.layers.pooler import Pooler, PoolingParamsUpdate
from vllm.model_executor.layers.pooler.tokwise.methods import AllPool
from vllm.tasks import PoolingTask
from vllm.utils.torch_utils import async_tensor_h2d
from vllm.v1.outputs import PoolerOutput
from vllm.v1.pool.metadata import PoolingMetadata
JOINT_HEAD_WEIGHTS = "joint_head.safetensors"
NUM_QUESTION_TYPES = 3
MAX_LOGIT_SCALE = math.log(100.0)
def segment_mean(
values: torch.Tensor, segment_ids: torch.Tensor, lengths: torch.Tensor
) -> torch.Tensor:
sums = values.new_zeros(lengths.shape[0], values.shape[-1], dtype=torch.float32)
sums.index_add_(0, segment_ids, values.float())
return (sums / lengths[:, None]).to(values.dtype)
@dataclass
class SchemaIndices:
"""Where one prompt's questions and options are, on the model's device."""
positions: torch.Tensor
"""Every token of every span, questions first."""
span_ids: torch.Tensor
"""The span of each of ``positions``."""
span_lengths: torch.Tensor
"""``[Q + O]``."""
question_types: torch.Tensor
"""``[Q]``."""
option_question: torch.Tensor
"""The question of each option, ``[O]``."""
option_slots: torch.Tensor
"""Each option's flat index in a ``[Q, max_options]`` grid, ``[O]``."""
option_mask: torch.Tensor
"""``[Q, max_options]``, true at the slots of options."""
class EvidenceRoutingLayer(nn.Module):
def __init__(self, width: int, heads: int, feedforward: int) -> None:
super().__init__()
self.query_norm = nn.LayerNorm(width)
self.memory_norm = nn.LayerNorm(width)
self.attention = nn.MultiheadAttention(width, heads, batch_first=True)
self.feedforward_norm = nn.LayerNorm(width)
# The Identity takes the place of the checkpoint's dropout.
self.feedforward = nn.Sequential(
nn.Linear(width, feedforward),
nn.GELU(),
nn.Identity(),
nn.Linear(feedforward, width),
)
def forward(self, queries: torch.Tensor, memory: torch.Tensor) -> torch.Tensor:
# Separate key and value tensors, like the reference, keep attention's
# unpacked input projections.
routed, _ = self.attention(
self.query_norm(queries),
self.memory_norm(memory),
self.memory_norm(memory),
need_weights=False,
)
queries = queries + routed
return queries + self.feedforward(self.feedforward_norm(queries))
class JointSchemaHead(nn.Module):
def __init__(
self,
hidden_size: int,
width: int,
routing_layers: int,
layers: int,
heads: int,
feedforward: int,
) -> None:
super().__init__()
self.hidden_norm = nn.LayerNorm(hidden_size)
self.memory_projection = nn.Linear(hidden_size, width, bias=False)
self.question_projection = nn.Linear(hidden_size, width, bias=False)
self.option_question_projection = nn.Linear(hidden_size, width, bias=False)
self.global_projection = nn.Linear(hidden_size, width, bias=False)
self.option_context_projection = nn.Linear(hidden_size, width, bias=False)
self.option_lexical_projection = nn.Linear(hidden_size, width, bias=False)
self.type_embedding = nn.Embedding(NUM_QUESTION_TYPES, width)
self.evidence_layers = nn.ModuleList(
EvidenceRoutingLayer(width, heads, feedforward)
for _ in range(routing_layers)
)
self.option_summary_norm = nn.LayerNorm(width)
self.layers = nn.ModuleList(
nn.TransformerDecoderLayer(
d_model=width,
nhead=heads,
dim_feedforward=feedforward,
dropout=0.0,
activation="gelu",
batch_first=True,
norm_first=True,
)
for _ in range(layers)
)
self.field_norm = nn.LayerNorm(width)
self.option_norm = nn.LayerNorm(width)
self.residual_scorer = nn.Sequential(
nn.Linear(width * 4, width),
nn.GELU(),
nn.Identity(),
nn.Linear(width, 1),
)
self.prior_logit_scale = nn.Parameter(torch.zeros(()))
self.joint_logit_scale = nn.Parameter(torch.zeros(()))
self.residual_gate = nn.Parameter(torch.zeros(()))
def forward(
self,
hidden_states: torch.Tensor,
lexical: torch.Tensor,
schema: SchemaIndices,
) -> torch.Tensor:
"""The ``[O]`` option logits of one prompt, from its ``[L, hidden]``
final hidden states and each option's ``[O, hidden]`` mean output
embedding."""
num_questions = schema.question_types.shape[0]
option_question = schema.option_question
hidden_states = self.hidden_norm(hidden_states)
memory = self.memory_projection(hidden_states).unsqueeze(0)
global_vector = hidden_states[-1]
means = segment_mean(
hidden_states[schema.positions], schema.span_ids, schema.span_lengths
)
question_vectors, contexts = means[:num_questions], means[num_questions:]
options = (
self.option_context_projection(contexts)
+ self.option_lexical_projection(lexical)
+ self.option_question_projection(question_vectors)[option_question]
).unsqueeze(0)
for layer in self.evidence_layers:
options = layer(options, memory)
options = options[0]
# Each field summarizes its options, weighted by their match to it.
base_fields = self.question_projection(question_vectors)
width = options.shape[-1]
grid = options.new_zeros(schema.option_mask.numel(), width)
grid[schema.option_slots] = options
grid = grid.view(*schema.option_mask.shape, width)
routing = torch.einsum("qow,qw->qo", grid, base_fields) / math.sqrt(width)
routing = routing.masked_fill(~schema.option_mask, float("-inf"))
summaries = torch.einsum("qo,qow->qw", routing.softmax(dim=-1), grid)
fields = (
base_fields
+ self.option_summary_norm(summaries)
+ self.global_projection(global_vector).unsqueeze(0)
+ self.type_embedding(schema.question_types)
).unsqueeze(0)
for layer in self.layers:
fields = layer(fields, memory)
fields = self.field_norm(fields[0])[option_question]
anchors = F.normalize(question_vectors + global_vector, dim=-1)
prior_scale = self.prior_logit_scale.clamp(max=MAX_LOGIT_SCALE).exp()
prior = prior_scale * (
F.normalize(lexical, dim=-1) * anchors[option_question]
).sum(dim=-1)
options = self.option_norm(options)
cosine = F.cosine_similarity(fields, options, dim=-1)
features = torch.cat(
[fields, options, fields * options, torch.abs(fields - options)], dim=-1
)
residual = self.residual_scorer(features).squeeze(-1)
joint_scale = self.joint_logit_scale.clamp(max=MAX_LOGIT_SCALE).exp()
joint = joint_scale * cosine + residual
return prior + torch.sigmoid(self.residual_gate) * joint
def parse_joint_schema(
extra_kwargs: dict[str, Any] | None, prompt_len: int
) -> tuple[list[int], list[tuple[int, int]], list[int]] | None:
"""The question types, the absolute spans of the questions then of the
options, and the option counts. None if the schema is missing or invalid."""
schema = (extra_kwargs or {}).get("joint_schema")
if not isinstance(schema, dict):
return None
try:
types = [int(t) for t in schema["question_types"]]
counts = [int(c) for c in schema["option_counts"]]
question_spans = [
(int(start) + prompt_len, int(end) + prompt_len)
for start, end in schema["question_spans"]
]
option_spans = [
(int(start) + prompt_len, int(end) + prompt_len)
for start, end in schema["option_spans"]
]
except (KeyError, TypeError, ValueError):
return None
spans = question_spans + option_spans
if (
not types
or len(counts) != len(types)
or len(question_spans) != len(types)
or len(option_spans) != sum(counts)
or any(not 0 <= t < NUM_QUESTION_TYPES for t in types)
or any(c < 1 for c in counts)
or any(not 0 <= start < end <= prompt_len for start, end in spans)
):
return None
return types, spans, counts
class JointSchemaPooler(Pooler):
"""Runs the joint schema head on each finished prompt.
It never raises, because it runs in the engine core: a prompt without a
valid schema, such as a profiling dummy, gets an empty output.
"""
def __init__(
self,
head: JointSchemaHead,
embed_lexical: Callable[[torch.Tensor], torch.Tensor],
) -> None:
super().__init__()
self.pooling = AllPool()
self.head = head
self.embed_lexical = embed_lexical
def get_supported_tasks(self) -> Set[PoolingTask]:
return {"token_classify"}
def get_pooling_updates(self, task: PoolingTask) -> PoolingParamsUpdate:
return PoolingParamsUpdate(requires_token_ids=True)
def forward(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> PoolerOutput:
pooled = self.pooling(hidden_states, pooling_metadata)
prompt_token_ids = pooling_metadata.get_prompt_token_ids_cpu()
outputs: list[torch.Tensor | None] = []
for hidden, token_ids, params in zip(
pooled, prompt_token_ids, pooling_metadata.pooling_params
):
if hidden is None:
outputs.append(None)
continue
parsed = parse_joint_schema(params.extra_kwargs, hidden.shape[0])
if parsed is None:
outputs.append(hidden.new_empty(0, dtype=torch.float32))
continue
schema, lexical_ids, lexical_span_ids = self._indices(
*parsed, hidden.device
)
lexical = segment_mean(
self.embed_lexical(
async_tensor_h2d(token_ids[lexical_ids], hidden.device)
),
lexical_span_ids,
schema.span_lengths[len(parsed[0]) :],
)
logits = self.head(hidden, lexical, schema).float()
if params.use_activation is not False:
mask = schema.option_mask
grid = logits.new_full((mask.numel(),), float("-inf"))
grid[schema.option_slots] = logits
probs = grid.view(mask.shape).softmax(dim=-1)
logits = probs.view(-1)[schema.option_slots]
outputs.append(logits)
return outputs
@staticmethod
def _indices(
types: list[int],
spans: list[tuple[int, int]],
counts: list[int],
device: torch.device,
) -> tuple[SchemaIndices, torch.Tensor, torch.Tensor]:
"""The schema's indices, plus the option tokens' positions (on the CPU)
and their option spans, for the lexical embeddings."""
num_questions = len(types)
lengths = torch.tensor([end - start for start, end in spans])
span_ids = torch.repeat_interleave(torch.arange(len(spans)), lengths)
positions = torch.cat([torch.arange(start, end) for start, end in spans])
num_question_tokens = int(lengths[:num_questions].sum())
counts_cpu = torch.tensor(counts)
max_options = max(counts)
option_question = torch.repeat_interleave(
torch.arange(num_questions), counts_cpu
)
option_slots = torch.cat(
[torch.arange(count) + q * max_options for q, count in enumerate(counts)]
)
schema = SchemaIndices(
positions=async_tensor_h2d(positions, device),
span_ids=async_tensor_h2d(span_ids, device),
span_lengths=async_tensor_h2d(lengths, device),
question_types=async_tensor_h2d(types, device, dtype=torch.long),
option_question=async_tensor_h2d(option_question, device),
option_slots=async_tensor_h2d(option_slots, device),
option_mask=async_tensor_h2d(
torch.arange(max_options) < counts_cpu[:, None], device
),
)
lexical_span_ids = async_tensor_h2d(
span_ids[num_question_tokens:] - num_questions, device
)
return schema, positions[num_question_tokens:], lexical_span_ids
+3 -336
View File
@@ -1,47 +1,22 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Clef-Omni (https://huggingface.co/Cloudflare/clef-omni).
The Qwen3-Omni thinker with a joint schema head that scores every option of
every question in one forward pass. The head reads the final hidden states of
the whole prompt, so it runs as a ``token_classify`` pooler.
Each request carries its schema in ``PoolingParams.extra_kwargs`` under
``"joint_schema"``:
- ``question_types``: 0 (noul), 1 (choice) or 2 (score) per question.
- ``question_spans``: the ``[start, end)`` of each question's instructions.
- ``option_spans``: the ``[start, end)`` of each option, question by question.
- ``option_counts``: the number of options of each question.
Spans count from the end of the prompt (``-len(prompt) <= start < end <= 0``),
so they hold after multimodal placeholders are expanded. The output has one
value per option, in ``option_spans`` order: its probability within its
question, or its logit with ``use_activation=False``.
"""
"""The Qwen3-Omni thinker with a Clef joint schema head."""
import math
from collections.abc import Callable, Mapping, Set
from dataclasses import dataclass
from collections.abc import Mapping
from typing import Any
import numpy as np
import torch
import torch.nn.functional as F
from torch import nn
from transformers.feature_extraction_utils import BatchFeature
from vllm.config import VllmConfig
from vllm.model_executor.layers.pooler import Pooler, PoolingParamsUpdate
from vllm.model_executor.layers.pooler.tokwise.methods import AllPool
from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
from vllm.model_executor.model_loader.default_loader import DefaultModelLoader
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.tasks import PoolingTask
from vllm.utils.torch_utils import async_tensor_h2d
from vllm.v1.outputs import PoolerOutput
from vllm.v1.pool.metadata import PoolingMetadata
from .clef_joint_schema import JOINT_HEAD_WEIGHTS, JointSchemaHead, JointSchemaPooler
from .interfaces_base import default_pooling_type
from .qwen3_omni_moe_thinker import (
Qwen3OmniMoeThinkerDummyInputsBuilder,
@@ -50,314 +25,6 @@ from .qwen3_omni_moe_thinker import (
Qwen3OmniMoeThinkerProcessingInfo,
)
JOINT_HEAD_WEIGHTS = "joint_head.safetensors"
NUM_QUESTION_TYPES = 3
MAX_LOGIT_SCALE = math.log(100.0)
def segment_mean(
values: torch.Tensor, segment_ids: torch.Tensor, lengths: torch.Tensor
) -> torch.Tensor:
sums = values.new_zeros(lengths.shape[0], values.shape[-1], dtype=torch.float32)
sums.index_add_(0, segment_ids, values.float())
return (sums / lengths[:, None]).to(values.dtype)
@dataclass
class SchemaIndices:
"""Where one prompt's questions and options are, on the model's device."""
positions: torch.Tensor
"""Every token of every span, questions first."""
span_ids: torch.Tensor
"""The span of each of ``positions``."""
span_lengths: torch.Tensor
"""``[Q + O]``."""
question_types: torch.Tensor
"""``[Q]``."""
option_question: torch.Tensor
"""The question of each option, ``[O]``."""
option_slots: torch.Tensor
"""Each option's flat index in a ``[Q, max_options]`` grid, ``[O]``."""
option_mask: torch.Tensor
"""``[Q, max_options]``, true at the slots of options."""
class EvidenceRoutingLayer(nn.Module):
def __init__(self, width: int, heads: int, feedforward: int) -> None:
super().__init__()
self.query_norm = nn.LayerNorm(width)
self.memory_norm = nn.LayerNorm(width)
self.attention = nn.MultiheadAttention(width, heads, batch_first=True)
self.feedforward_norm = nn.LayerNorm(width)
# The Identity takes the place of the checkpoint's dropout.
self.feedforward = nn.Sequential(
nn.Linear(width, feedforward),
nn.GELU(),
nn.Identity(),
nn.Linear(feedforward, width),
)
def forward(self, queries: torch.Tensor, memory: torch.Tensor) -> torch.Tensor:
# Separate key and value tensors, like the reference, keep attention's
# unpacked input projections.
routed, _ = self.attention(
self.query_norm(queries),
self.memory_norm(memory),
self.memory_norm(memory),
need_weights=False,
)
queries = queries + routed
return queries + self.feedforward(self.feedforward_norm(queries))
class JointSchemaHead(nn.Module):
def __init__(
self,
hidden_size: int,
width: int,
routing_layers: int,
layers: int,
heads: int,
feedforward: int,
) -> None:
super().__init__()
self.hidden_norm = nn.LayerNorm(hidden_size)
self.memory_projection = nn.Linear(hidden_size, width, bias=False)
self.question_projection = nn.Linear(hidden_size, width, bias=False)
self.option_question_projection = nn.Linear(hidden_size, width, bias=False)
self.global_projection = nn.Linear(hidden_size, width, bias=False)
self.option_context_projection = nn.Linear(hidden_size, width, bias=False)
self.option_lexical_projection = nn.Linear(hidden_size, width, bias=False)
self.type_embedding = nn.Embedding(NUM_QUESTION_TYPES, width)
self.evidence_layers = nn.ModuleList(
EvidenceRoutingLayer(width, heads, feedforward)
for _ in range(routing_layers)
)
self.option_summary_norm = nn.LayerNorm(width)
self.layers = nn.ModuleList(
nn.TransformerDecoderLayer(
d_model=width,
nhead=heads,
dim_feedforward=feedforward,
dropout=0.0,
activation="gelu",
batch_first=True,
norm_first=True,
)
for _ in range(layers)
)
self.field_norm = nn.LayerNorm(width)
self.option_norm = nn.LayerNorm(width)
self.residual_scorer = nn.Sequential(
nn.Linear(width * 4, width),
nn.GELU(),
nn.Identity(),
nn.Linear(width, 1),
)
self.prior_logit_scale = nn.Parameter(torch.zeros(()))
self.joint_logit_scale = nn.Parameter(torch.zeros(()))
self.residual_gate = nn.Parameter(torch.zeros(()))
def forward(
self,
hidden_states: torch.Tensor,
lexical: torch.Tensor,
schema: SchemaIndices,
) -> torch.Tensor:
"""The ``[O]`` option logits of one prompt, from its ``[L, hidden]``
final hidden states and each option's ``[O, hidden]`` mean output
embedding."""
num_questions = schema.question_types.shape[0]
option_question = schema.option_question
hidden_states = self.hidden_norm(hidden_states)
memory = self.memory_projection(hidden_states).unsqueeze(0)
global_vector = hidden_states[-1]
means = segment_mean(
hidden_states[schema.positions], schema.span_ids, schema.span_lengths
)
question_vectors, contexts = means[:num_questions], means[num_questions:]
options = (
self.option_context_projection(contexts)
+ self.option_lexical_projection(lexical)
+ self.option_question_projection(question_vectors)[option_question]
).unsqueeze(0)
for layer in self.evidence_layers:
options = layer(options, memory)
options = options[0]
# Each field summarizes its options, weighted by their match to it.
base_fields = self.question_projection(question_vectors)
width = options.shape[-1]
grid = options.new_zeros(schema.option_mask.numel(), width)
grid[schema.option_slots] = options
grid = grid.view(*schema.option_mask.shape, width)
routing = torch.einsum("qow,qw->qo", grid, base_fields) / math.sqrt(width)
routing = routing.masked_fill(~schema.option_mask, float("-inf"))
summaries = torch.einsum("qo,qow->qw", routing.softmax(dim=-1), grid)
fields = (
base_fields
+ self.option_summary_norm(summaries)
+ self.global_projection(global_vector).unsqueeze(0)
+ self.type_embedding(schema.question_types)
).unsqueeze(0)
for layer in self.layers:
fields = layer(fields, memory)
fields = self.field_norm(fields[0])[option_question]
anchors = F.normalize(question_vectors + global_vector, dim=-1)
prior_scale = self.prior_logit_scale.clamp(max=MAX_LOGIT_SCALE).exp()
prior = prior_scale * (
F.normalize(lexical, dim=-1) * anchors[option_question]
).sum(dim=-1)
options = self.option_norm(options)
cosine = F.cosine_similarity(fields, options, dim=-1)
features = torch.cat(
[fields, options, fields * options, torch.abs(fields - options)], dim=-1
)
residual = self.residual_scorer(features).squeeze(-1)
joint_scale = self.joint_logit_scale.clamp(max=MAX_LOGIT_SCALE).exp()
joint = joint_scale * cosine + residual
return prior + torch.sigmoid(self.residual_gate) * joint
def parse_joint_schema(
extra_kwargs: dict[str, Any] | None, prompt_len: int
) -> tuple[list[int], list[tuple[int, int]], list[int]] | None:
"""The question types, the absolute spans of the questions then of the
options, and the option counts. None if the schema is missing or invalid."""
schema = (extra_kwargs or {}).get("joint_schema")
if not isinstance(schema, dict):
return None
try:
types = [int(t) for t in schema["question_types"]]
counts = [int(c) for c in schema["option_counts"]]
question_spans = [
(int(start) + prompt_len, int(end) + prompt_len)
for start, end in schema["question_spans"]
]
option_spans = [
(int(start) + prompt_len, int(end) + prompt_len)
for start, end in schema["option_spans"]
]
except (KeyError, TypeError, ValueError):
return None
spans = question_spans + option_spans
if (
not types
or len(counts) != len(types)
or len(question_spans) != len(types)
or len(option_spans) != sum(counts)
or any(not 0 <= t < NUM_QUESTION_TYPES for t in types)
or any(c < 1 for c in counts)
or any(not 0 <= start < end <= prompt_len for start, end in spans)
):
return None
return types, spans, counts
class JointSchemaPooler(Pooler):
"""Runs the joint schema head on each finished prompt.
It never raises, because it runs in the engine core: a prompt without a
valid schema, such as a profiling dummy, gets an empty output.
"""
def __init__(
self,
head: JointSchemaHead,
embed_lexical: Callable[[torch.Tensor], torch.Tensor],
) -> None:
super().__init__()
self.pooling = AllPool()
self.head = head
self.embed_lexical = embed_lexical
def get_supported_tasks(self) -> Set[PoolingTask]:
return {"token_classify"}
def get_pooling_updates(self, task: PoolingTask) -> PoolingParamsUpdate:
return PoolingParamsUpdate(requires_token_ids=True)
def forward(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> PoolerOutput:
pooled = self.pooling(hidden_states, pooling_metadata)
prompt_token_ids = pooling_metadata.get_prompt_token_ids_cpu()
outputs: list[torch.Tensor | None] = []
for hidden, token_ids, params in zip(
pooled, prompt_token_ids, pooling_metadata.pooling_params
):
if hidden is None:
outputs.append(None)
continue
parsed = parse_joint_schema(params.extra_kwargs, hidden.shape[0])
if parsed is None:
outputs.append(hidden.new_empty(0, dtype=torch.float32))
continue
schema, lexical_ids, lexical_span_ids = self._indices(
*parsed, hidden.device
)
lexical = segment_mean(
self.embed_lexical(
async_tensor_h2d(token_ids[lexical_ids], hidden.device)
),
lexical_span_ids,
schema.span_lengths[len(parsed[0]) :],
)
logits = self.head(hidden, lexical, schema).float()
if params.use_activation is not False:
mask = schema.option_mask
grid = logits.new_full((mask.numel(),), float("-inf"))
grid[schema.option_slots] = logits
probs = grid.view(mask.shape).softmax(dim=-1)
logits = probs.view(-1)[schema.option_slots]
outputs.append(logits)
return outputs
@staticmethod
def _indices(
types: list[int],
spans: list[tuple[int, int]],
counts: list[int],
device: torch.device,
) -> tuple[SchemaIndices, torch.Tensor, torch.Tensor]:
"""The schema's indices, plus the option tokens' positions (on the CPU)
and their option spans, for the lexical embeddings."""
num_questions = len(types)
lengths = torch.tensor([end - start for start, end in spans])
span_ids = torch.repeat_interleave(torch.arange(len(spans)), lengths)
positions = torch.cat([torch.arange(start, end) for start, end in spans])
num_question_tokens = int(lengths[:num_questions].sum())
counts_cpu = torch.tensor(counts)
max_options = max(counts)
option_question = torch.repeat_interleave(
torch.arange(num_questions), counts_cpu
)
option_slots = torch.cat(
[torch.arange(count) + q * max_options for q, count in enumerate(counts)]
)
schema = SchemaIndices(
positions=async_tensor_h2d(positions, device),
span_ids=async_tensor_h2d(span_ids, device),
span_lengths=async_tensor_h2d(lengths, device),
question_types=async_tensor_h2d(types, device, dtype=torch.long),
option_question=async_tensor_h2d(option_question, device),
option_slots=async_tensor_h2d(option_slots, device),
option_mask=async_tensor_h2d(
torch.arange(max_options) < counts_cpu[:, None], device
),
)
lexical_span_ids = async_tensor_h2d(
span_ids[num_question_tokens:] - num_questions, device
)
return schema, positions[num_question_tokens:], lexical_span_ids
MAX_FRAME_PIXELS = 256 * 32 * 32
+1
View File
@@ -287,6 +287,7 @@ _REWARD_MODELS = {
_TOKEN_CLASSIFICATION_MODELS = {
"BertForTokenClassification": ("bert", "BertForTokenClassification"),
"ClefFlashForJointSchema": ("clef_flash", "ClefFlashForJointSchema"),
"ClefOmniForJointSchema": ("clef_omni", "ClefOmniForJointSchema"),
"ModernBertForTokenClassification": (
"modernbert",
@@ -647,6 +647,27 @@ class MimoV2ModelArchConfigConvertor(ModelArchConfigConvertorBase):
_strip_mimo_v2_attention_chunk_size(hf_config, hf_text_config)
class Qwen3_5ModelArchConfigConvertor(ModelArchConfigConvertorBase):
def __init__(
self,
hf_config: PreTrainedConfig,
hf_text_config: PreTrainedConfig,
revision: str | None = None,
):
if getattr(hf_config, "architectures", None) == [
"Qwen3_5ForConditionalGeneration"
]:
model_path = hf_config.name_or_path
if model_path and file_or_path_exists(
model_path, "joint_head_config.json", revision
):
hf_config.architectures = ["ClefFlashForJointSchema"]
hf_config.joint_head_config = get_hf_file_to_dict(
"joint_head_config.json", model_path, revision
)
super().__init__(hf_config, hf_text_config, revision)
class Qwen3OmniMoeModelArchConfigConvertor(ModelArchConfigConvertorBase):
def __init__(
self,
@@ -850,6 +871,7 @@ MODEL_ARCH_CONFIG_CONVERTORS = {
"nemotron-nas": NemotronNasModelArchConfigConvertor,
"bailing_hybrid_v3_mtp": BailingHybridV3MTPModelArchConfigConvertor,
"pangu_ultra_moe_mtp": PanguUltraMoeMTPModelArchConfigConvertor,
"qwen3_5": Qwen3_5ModelArchConfigConvertor,
"qwen3_5_mtp": Qwen3_5MTPModelArchConfigConvertor,
"qwen3_omni_moe": Qwen3OmniMoeModelArchConfigConvertor,
"qwen3_next_mtp": Qwen3NextMTPModelArchConfigConvertor,