项目文件夹

文件
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

330 行
12 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Native Qwen2 embedding adapter for omlx.
Serves Qwen2-decoder embedding models (``architectures=["Qwen2ForCausalLM"]``,
``model_type="qwen2"``) such as ``jinaai/jina-code-embeddings-1.5b`` (causal)
and ``Alibaba-NLP/gte-Qwen2-1.5B-instruct`` (bidirectional), without depending
on mlx-embeddings, which has no ``qwen2`` module and raises
``ValueError("Model type qwen2 not supported.")`` for these checkpoints.
These models pool the *last* (mask-aware) token of the final hidden state and
L2-normalize, matching their SentenceTransformers configs
(``pooling_mode_lasttoken: true`` + a Normalize module). Mean pooling would
silently corrupt every vector, so last-token pooling is load-bearing here; the
``_extract_embeddings_array`` consumer does not normalize, so the returned
``text_embeds`` are already L2-normalized.
Deltas from the Qwen3 embedder: Qwen2 has no QK-norm, applies attention bias on
the q/k/v projections (``o_proj`` unbiased), and derives
``head_dim = hidden_size // num_attention_heads`` (no explicit config field).
"""
from dataclasses import dataclass, field
from typing import List, Optional
import mlx.core as mx
import mlx.nn as nn
from .base_model import (
BaseModelArgs,
BaseModelOutput,
last_token_pool,
normalize_embeddings,
)
@dataclass
class ModelArgs(BaseModelArgs):
"""Qwen2 embedding model configuration."""
model_type: str = "qwen2"
hidden_size: int = 1536
num_hidden_layers: int = 28
intermediate_size: int = 8960
num_attention_heads: int = 12
num_key_value_heads: Optional[int] = None
head_dim: Optional[int] = None
max_position_embeddings: int = 32768
vocab_size: int = 151936
rms_norm_eps: float = 1e-6
rope_theta: float = 1000000.0
# Attention direction. A plain Qwen2 decoder-embedder (jina-code) is
# causal. Some Qwen2 embedders run the decoder *bidirectionally* and signal
# it with ``is_causal: false`` in config (Alibaba's gte-Qwen2 family, which
# ships a custom bidirectional modeling_qwen.py). Default causal; the config
# flips it off when needed.
is_causal: bool = True
tie_word_embeddings: bool = False
bos_token_id: Optional[int] = None
eos_token_id: Optional[int] = None
pad_token_id: Optional[int] = None
architectures: List[str] = field(
default_factory=lambda: ["Qwen2ForCausalLM"]
)
def __post_init__(self):
"""Derive grouped-query and head dims that Qwen2 leaves implicit."""
if self.num_key_value_heads is None:
self.num_key_value_heads = self.num_attention_heads
if self.head_dim is None:
if self.hidden_size % self.num_attention_heads != 0:
raise ValueError(
f"hidden_size ({self.hidden_size}) must be divisible by "
f"num_attention_heads ({self.num_attention_heads})"
)
self.head_dim = self.hidden_size // self.num_attention_heads
class Qwen2MLP(nn.Module):
"""SwiGLU MLP: SiLU(gate_proj(x)) * up_proj(x) -> down_proj."""
def __init__(self, config: ModelArgs):
super().__init__()
self.gate_proj = nn.Linear(
config.hidden_size, config.intermediate_size, bias=False
)
self.up_proj = nn.Linear(
config.hidden_size, config.intermediate_size, bias=False
)
self.down_proj = nn.Linear(
config.intermediate_size, config.hidden_size, bias=False
)
def __call__(self, x: mx.array) -> mx.array:
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
class Qwen2Attention(nn.Module):
"""
Grouped-query attention for Qwen2.
Unlike Qwen3 there is no query/key RMSNorm, and the q/k/v projections carry
a bias (``o_proj`` does not). Grouped-query head expansion is handled by
``mx.fast.scaled_dot_product_attention``.
"""
def __init__(self, config: ModelArgs):
super().__init__()
self.num_heads = config.num_attention_heads
self.head_dim = config.head_dim
self.num_key_value_heads = config.num_key_value_heads
self.scale = self.head_dim**-0.5
self.q_proj = nn.Linear(
config.hidden_size, self.num_heads * self.head_dim, bias=True
)
self.k_proj = nn.Linear(
config.hidden_size, self.num_key_value_heads * self.head_dim, bias=True
)
self.v_proj = nn.Linear(
config.hidden_size, self.num_key_value_heads * self.head_dim, bias=True
)
self.o_proj = nn.Linear(
self.num_heads * self.head_dim, config.hidden_size, bias=False
)
self.rotary_emb = nn.RoPE(
self.head_dim,
traditional=False,
base=config.rope_theta,
)
def __call__(
self,
hidden_states: mx.array,
attention_mask: Optional[mx.array] = None,
) -> mx.array:
bsz, q_len, _ = hidden_states.shape
queries = self.q_proj(hidden_states)
keys = self.k_proj(hidden_states)
values = self.v_proj(hidden_states)
queries = queries.reshape(
bsz, q_len, self.num_heads, self.head_dim
).transpose(0, 2, 1, 3)
keys = keys.reshape(
bsz, q_len, self.num_key_value_heads, self.head_dim
).transpose(0, 2, 1, 3)
values = values.reshape(
bsz, q_len, self.num_key_value_heads, self.head_dim
).transpose(0, 2, 1, 3)
queries = self.rotary_emb(queries)
keys = self.rotary_emb(keys)
attn_output = mx.fast.scaled_dot_product_attention(
queries, keys, values, scale=self.scale, mask=attention_mask
)
attn_output = attn_output.transpose(0, 2, 1, 3).reshape(
bsz, q_len, self.num_heads * self.head_dim
)
return self.o_proj(attn_output)
class Qwen2DecoderLayer(nn.Module):
"""Pre-norm transformer decoder layer (RMSNorm, residual, SwiGLU)."""
def __init__(self, config: ModelArgs):
super().__init__()
self.self_attn = Qwen2Attention(config)
self.mlp = Qwen2MLP(config)
self.input_layernorm = nn.RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
self.post_attention_layernorm = nn.RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
def __call__(
self,
hidden_states: mx.array,
attention_mask: Optional[mx.array] = None,
) -> mx.array:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
hidden_states = self.self_attn(hidden_states, attention_mask=attention_mask)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = residual + hidden_states
return hidden_states
class Qwen2Model(nn.Module):
"""Qwen2 transformer decoder stack."""
def __init__(self, config: ModelArgs):
super().__init__()
self.config = config
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
self.layers = [
Qwen2DecoderLayer(config) for _ in range(config.num_hidden_layers)
]
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def _build_attention_mask(
self,
attention_mask: Optional[mx.array],
seq_length: int,
dtype: mx.Dtype,
) -> mx.array:
"""
Additive (batch, 1, seq, seq) mask combining key padding with the
causal triangle (skipped for bidirectional embedders).
Built as a single boolean keep-mask (query i may attend key j iff key j
is a real token, and — when causal — ``j <= i``) mapped to one finite
``finfo.min`` fill. Using the dtype minimum rather than ``-inf`` keeps
fully-masked rows (the leading pad positions under *left* padding)
NaN-free: a uniform softmax over equal fills yields finite garbage at
pad positions that we never pool, instead of ``NaN`` that ``0 * NaN``
would propagate into real positions at the next layer. A single fill
also avoids the additive ``2 * finfo.min`` overflow back to ``-inf``.
"""
if self.config.is_causal:
keep = mx.tril(mx.ones((seq_length, seq_length), dtype=mx.bool_))
keep = keep[None, None] # (1, 1, seq, seq)
else:
keep = mx.ones((1, 1, seq_length, seq_length), dtype=mx.bool_)
if attention_mask is not None:
key_keep = (attention_mask != 0)[:, None, None, :] # (batch, 1, 1, seq)
keep = keep & key_keep
return mx.where(keep, 0.0, mx.finfo(dtype).min).astype(dtype)
def __call__(
self,
input_ids: mx.array,
attention_mask: Optional[mx.array] = None,
) -> mx.array:
_, seq_length = input_ids.shape
hidden_states = self.embed_tokens(input_ids)
if attention_mask is not None and attention_mask.ndim != 2:
# Already an additive (batch, 1, seq, seq) mask; use as-is.
mask = attention_mask
else:
mask = self._build_attention_mask(
attention_mask, seq_length, hidden_states.dtype
)
for layer in self.layers:
hidden_states = layer(hidden_states, attention_mask=mask)
return self.norm(hidden_states)
class Model(nn.Module):
"""Qwen2 decoder wrapped for embedding generation (last-token + L2)."""
def __init__(self, config: ModelArgs):
super().__init__()
self.config = config
self.model_type = config.model_type
self.model = Qwen2Model(config)
def __call__(
self,
input_ids: mx.array,
attention_mask: Optional[mx.array] = None,
) -> BaseModelOutput:
if input_ids.ndim != 2:
raise ValueError(f"input_ids must be 2D, got shape {input_ids.shape}")
batch_size, seq_len = input_ids.shape
if attention_mask is None:
attention_mask = mx.ones((batch_size, seq_len), dtype=mx.int32)
elif attention_mask.shape != (batch_size, seq_len):
raise ValueError(
f"attention_mask shape {attention_mask.shape} doesn't match "
f"input_ids shape {input_ids.shape}"
)
last_hidden_state = self.model(input_ids, attention_mask=attention_mask)
# Mask-aware last-token pool, then L2-normalize. The embedding consumer
# (_extract_embeddings_array) does not normalize, so text_embeds must be
# unit-norm already.
pooled_output = last_token_pool(last_hidden_state, attention_mask)
text_embeds = normalize_embeddings(pooled_output)
return BaseModelOutput(
text_embeds=text_embeds, last_hidden_state=last_hidden_state
)
def sanitize(self, weights: dict) -> dict:
"""
Map a HuggingFace Qwen2ForCausalLM checkpoint onto this module tree.
Drops the unused LM head (and any precomputed rotary inverse-frequency
buffers) and normalizes the transformer prefix to ``model.``.
"""
sanitized_weights = {}
for key, value in weights.items():
if "lm_head.weight" in key:
continue
if "rotary_emb.inv_freq" in key:
continue
if key.startswith("transformer."):
new_key = key.replace("transformer.", "model.", 1)
elif not key.startswith("model.") and "." in key:
new_key = f"model.{key}"
else:
new_key = key
sanitized_weights[new_key] = value
return sanitized_weights