Files
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

108 lines
3.2 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Base model utilities for omlx custom model implementations.
This module provides common utilities for implementing custom models
that are not yet supported by mlx-embeddings.
"""
from dataclasses import dataclass
from typing import Optional
import mlx.core as mx
@dataclass
class BaseModelArgs:
"""Base class for model configuration arguments."""
pass
@dataclass
class BaseModelOutput:
"""Base output class for model forward pass."""
last_hidden_state: mx.array
"""Hidden states from the last layer."""
text_embeds: Optional[mx.array] = None
"""Normalized text embeddings."""
pooler_output: Optional[mx.array] = None
"""Pooled output (e.g., CLS token or mean pooling)."""
hidden_states: Optional[tuple] = None
"""All hidden states if output_hidden_states=True."""
def mean_pooling(hidden_states: mx.array, attention_mask: mx.array) -> mx.array:
"""
Perform mean pooling over sequence with attention mask.
Args:
hidden_states: Shape (batch_size, seq_len, hidden_size)
attention_mask: Shape (batch_size, seq_len)
Returns:
Pooled output of shape (batch_size, hidden_size)
"""
# Expand mask to match hidden states shape
mask_expanded = attention_mask[:, :, None].astype(hidden_states.dtype)
# Sum embeddings weighted by mask
sum_embeddings = mx.sum(hidden_states * mask_expanded, axis=1)
# Sum mask values (clip to avoid division by zero)
sum_mask = mx.clip(mx.sum(mask_expanded, axis=1), a_min=1e-9, a_max=None)
return sum_embeddings / sum_mask
def last_token_pool(
hidden_states: mx.array, attention_mask: Optional[mx.array] = None
) -> mx.array:
"""
Pool the last *non-pad* token of each sequence (mask-aware).
Decoder embedding models (Qwen2/Qwen3, gte-Qwen2, jina-code) represent a
sequence by its final token's hidden state. The pool must be
mask-aware because these tokenizers may pad on either side: a hardcoded
``[:, -1]`` is only correct under left padding and silently corrupts vectors
under right padding.
Args:
hidden_states: Shape (batch_size, seq_len, hidden_size)
attention_mask: Shape (batch_size, seq_len). If None, uses the last
position.
Returns:
Pooled output of shape (batch_size, hidden_size)
"""
if attention_mask is None:
return hidden_states[:, -1]
# Left padding: every sequence ends on a real token, so the last position
# is always valid and we can index it directly.
left_padding = attention_mask[:, -1].sum() == attention_mask.shape[0]
if left_padding:
return hidden_states[:, -1]
# Right (or mixed) padding: the last valid token is at sum(mask) - 1.
sequence_lengths = attention_mask.sum(axis=1) - 1
batch_size = hidden_states.shape[0]
return hidden_states[mx.arange(batch_size), sequence_lengths]
def normalize_embeddings(embeddings: mx.array) -> mx.array:
"""
L2 normalize embeddings.
Args:
embeddings: Shape (..., hidden_size)
Returns:
Normalized embeddings with same shape
"""
return embeddings / mx.linalg.norm(embeddings, axis=-1, keepdims=True)