vtx-embed-7M / vortex_embed_v4_5.py
Abhaykoul's picture
Fix dataclass import compatibility in client script
592dcd6 verified
Raw
History Blame Contribute Delete
10.8 kB
"""Vortex-Embed v4.5 — Standalone Single-File Native 4-Bit Embedding Engine.
Lightweight, 4.72 MB RAM native 4-bit embedding engine with Matryoshka support.
Repo: https://huggingface.co/VTXAI/Vortex-Embed-v4-5-sentence
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Sequence, Tuple, Union
import numpy as np
from safetensors.numpy import load_file, save_file
try:
from tokenizers import Tokenizer
except ImportError:
Tokenizer = None
class VortexEmbedConfig:
def __init__(
self,
vocab_size: int = 29528,
embedding_dim: int = 256,
block_size: int = 32,
num_blocks: int = 8,
model_type: str = "vortex-embed",
architectures: Optional[List[str]] = None,
quantization: str = "lf4",
bits: int = 4,
sif_a: float = 0.05,
sif_pc: float = 1.0,
pc_k: int = 1,
matryoshka_dim: Optional[int] = None,
**kwargs,
):
self.vocab_size = vocab_size
self.embedding_dim = embedding_dim
self.block_size = block_size
self.num_blocks = num_blocks
self.model_type = model_type
self.architectures = architectures or ["VortexEmbedV4_5"]
self.quantization = quantization
self.bits = bits
self.sif_a = sif_a
self.sif_pc = sif_pc
self.pc_k = pc_k
self.matryoshka_dim = matryoshka_dim
@classmethod
def from_dict(cls, d: dict) -> "VortexEmbedConfig":
return cls(**d)
def to_dict(self) -> dict:
return {
"vocab_size": self.vocab_size,
"embedding_dim": self.embedding_dim,
"block_size": self.block_size,
"num_blocks": self.num_blocks,
"model_type": self.model_type,
"architectures": self.architectures,
"quantization": self.quantization,
"bits": self.bits,
"sif_a": self.sif_a,
"sif_pc": self.sif_pc,
"pc_k": self.pc_k,
"matryoshka_dim": self.matryoshka_dim,
}
class VortexEmbedV4_5:
"""Vortex-Embed v4.5 — Native 4-Bit Sentence Embedding Model.
Features:
- 4.72 MB RAM Footprint (Zero FP32 matrix in RAM).
- On-the-fly dequantization per batch.
- Matryoshka Representation Learning (truncation to 256, 128, 64 dims).
"""
def __init__(
self,
packed: np.ndarray,
scales: np.ndarray,
zeros: np.ndarray,
tokenizer_data: Union[str, Path],
config: Union[dict, VortexEmbedConfig],
*,
matryoshka_dim: Optional[int] = None,
) -> None:
self.packed = np.asarray(packed, dtype=np.uint8)
self.scales = np.asarray(scales, dtype=np.float16)
self.zeros = np.asarray(zeros, dtype=np.float16)
self.tokenizer_data = str(tokenizer_data)
self.config = config if isinstance(config, VortexEmbedConfig) else VortexEmbedConfig.from_dict(config)
self.vocab_size = int(self.config.vocab_size)
self.dim = int(self.config.embedding_dim)
self.block_size = int(self.config.block_size)
self.num_blocks = int(self.config.num_blocks)
self.sif_a = float(self.config.sif_a)
self.sif_pc = float(self.config.sif_pc)
self.pc_k = int(self.config.pc_k)
self.matryoshka_dim = matryoshka_dim or self.config.matryoshka_dim
self._tokenizer: Optional[Tokenizer] = None
self._sif_weights: Optional[np.ndarray] = None
self._pc_directions: Optional[np.ndarray] = None
@property
def tokenizer(self) -> Tokenizer:
if self._tokenizer is None:
if Tokenizer is None:
raise RuntimeError("tokenizers required: pip install tokenizers")
self._tokenizer = Tokenizer.from_file(self.tokenizer_data)
return self._tokenizer
@property
def model_size_mb(self) -> float:
"""Returns actual in-RAM size of stored parameters (4.72 MB)."""
return (self.packed.nbytes + self.scales.nbytes + self.zeros.nbytes) / 1e6
@property
def on_disk_size_mb(self) -> float:
return (self.packed.nbytes + self.scales.nbytes + self.zeros.nbytes) / 1e6
@classmethod
def from_pretrained(
cls,
path_or_id: Union[str, Path],
matryoshka_dim: Optional[int] = None,
**overrides,
) -> "VortexEmbedV4_5":
path = Path(path_or_id)
if not path.is_dir():
from huggingface_hub import snapshot_download
path = Path(snapshot_download(str(path_or_id)))
tensors = load_file(str(path / "model.safetensors"))
config = json.loads((path / "config.json").read_text())
for k, v in overrides.items():
if k in VortexEmbedConfig.__dataclass_fields__:
config[k] = v
return cls(
packed=tensors["embedding_packed"],
scales=tensors["embedding_scales"],
zeros=tensors["embedding_zeros"],
tokenizer_data=str(path / "tokenizer.json"),
config=config,
matryoshka_dim=matryoshka_dim,
)
def save_pretrained(self, path: Union[str, Path]) -> None:
out = Path(path)
out.mkdir(parents=True, exist_ok=True)
save_file(
{
"embedding_packed": self.packed,
"embedding_scales": self.scales,
"embedding_zeros": self.zeros,
},
str(out / "model.safetensors"),
)
(out / "config.json").write_text(json.dumps(self.config.to_dict(), indent=2))
if not (out / "tokenizer.json").exists():
(out / "tokenizer.json").write_text(Path(self.tokenizer_data).read_text())
def _dequantize_ids_on_the_fly(self, token_ids: np.ndarray) -> np.ndarray:
if token_ids.size == 0:
return np.empty((0, self.dim), dtype=np.float32)
p = self.packed[token_ids]
s = self.scales[token_ids].astype(np.float32)[:, :, None]
z = self.zeros[token_ids].astype(np.float32)[:, :, None]
low = (p & 0x0F).astype(np.float32)
high = ((p >> 4) & 0x0F).astype(np.float32)
n = len(token_ids)
padded = p.shape[1] * 2
unpacked = np.empty((n, padded), dtype=np.float32)
unpacked[:, 0::2] = low
unpacked[:, 1::2] = high
blocked = unpacked.reshape(n, self.num_blocks, self.block_size)
out = (blocked * s + z).reshape(n, padded)
return out[:, : self.dim]
def fit_idf(self, corpus_token_lists: Sequence[Sequence[int]]) -> "VortexEmbedV4_5":
flat = (np.concatenate(corpus_token_lists) if corpus_token_lists else np.empty(0, dtype=np.int64))
total = max(int(flat.size), 1)
counts = np.bincount(flat, minlength=self.vocab_size).astype(np.float64)
p = counts / total
denom = self.sif_a + p
with np.errstate(divide="ignore", invalid="ignore"):
weights = np.where(p > 0, self.sif_a / denom, 1.0)
self._sif_weights = weights.astype(np.float32)
return self
def fit_pc(self, corpus_embeddings: np.ndarray, k: Optional[int] = None) -> "VortexEmbedV4_5":
if k is None:
k = self.pc_k
if corpus_embeddings.size == 0 or k <= 0:
return self
x = corpus_embeddings.astype(np.float32)
x = x - x.mean(axis=0, keepdims=True)
try:
_, _, vt = np.linalg.svd(x, full_matrices=False)
pcs = vt[:k].astype(np.float32)
pcs = pcs / (np.linalg.norm(pcs, axis=1, keepdims=True) + 1e-12)
self._pc_directions = pcs
except np.linalg.LinAlgError:
self._pc_directions = None
return self
def _apply_pc(self, x: np.ndarray) -> np.ndarray:
if self.sif_pc <= 0 or self._pc_directions is None:
return x
out = x
for pc in self._pc_directions:
proj = (out @ pc)[:, None] * pc[None, :]
out = out - self.sif_pc * proj
return out
def _tokenize_batch(self, texts: Sequence[str]) -> List[List[int]]:
encoded = self.tokenizer.encode_batch(list(texts))
return [[tid for tid in item.ids if 0 <= int(tid) < self.vocab_size] for item in encoded]
def encode_batch(
self,
texts: Sequence[str],
*,
normalize: bool = True,
truncate_dim: Optional[int] = None,
) -> np.ndarray:
if not texts:
return np.zeros((0, self.dim), dtype=np.float32)
token_lists = self._tokenize_batch(list(texts))
n = len(token_lists)
flat = (np.concatenate(token_lists) if token_lists else np.empty(0, dtype=np.int64))
if flat.size == 0:
return np.zeros((n, self.dim), dtype=np.float32)
unique_ids, inverse_indices = np.unique(flat, return_inverse=True)
unique_embs = self._dequantize_ids_on_the_fly(unique_ids)
token_embs = unique_embs[inverse_indices]
if self._sif_weights is not None:
w = self._sif_weights[flat].astype(np.float32)[:, None]
token_embs = token_embs * w
chunk_lens = np.array([len(ids) for ids in token_lists], dtype=np.int64)
chunk_ends = np.cumsum(chunk_lens)
boundaries = np.empty(n + 1, dtype=np.int64)
boundaries[0] = 0
boundaries[1:] = chunk_ends
sums = np.add.reduceat(token_embs, boundaries[:-1], axis=0)
if self._sif_weights is not None:
w_full = self._sif_weights[flat].astype(np.float32)
w_per_row = np.add.reduceat(w_full, boundaries[:-1])
w_per_row = np.maximum(w_per_row, 1e-12)
else:
w_per_row = np.maximum(chunk_lens.astype(np.float32), 1.0)
embeddings = sums / w_per_row[:, None]
embeddings = self._apply_pc(embeddings)
dim = truncate_dim if truncate_dim is not None else self.matryoshka_dim
if dim is not None and 0 < dim < self.dim:
embeddings = embeddings[:, :dim]
if normalize and embeddings.shape[0] > 0:
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
np.divide(embeddings, np.maximum(norms, 1e-12), out=embeddings)
return embeddings
def encode(
self,
texts: Union[str, Sequence[str]],
*,
normalize: bool = True,
truncate_dim: Optional[int] = None,
) -> np.ndarray:
if isinstance(texts, str):
return self.encode_batch([texts], normalize=normalize, truncate_dim=truncate_dim)[0]
return self.encode_batch(list(texts), normalize=normalize, truncate_dim=truncate_dim)