Spaces:
Paused
Paused
File size: 15,212 Bytes
aa87fd5 023e5c6 aa87fd5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 | # text_encoder.py
# Mamba Text Encoder wrapper cho CLIMP-PAR
# Dựa trên kiến trúc CLIMP (arXiv:2601.06891):
# - Mamba LLM pretrained làm text backbone (thay thế Transformer text encoder)
# - Last-token pooling: lấy hidden state ở vị trí token cuối (causal → full context)
# - Projection W_t chiếu sang shared embedding space
# - Hỗ trợ văn bản dài hơn 77 token (vượt giới hạn CLIP gốc)
import os
import sys
import torch
import torch.nn as nn
# Thêm đường dẫn mamba local vào sys.path
_MAMBA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'mamba')
if _MAMBA_DIR not in sys.path:
sys.path.insert(0, _MAMBA_DIR)
from mamba.mamba_ssm.models.mixer_seq_simple import MambaLMHeadModel
# ============================================================================
# Cấu hình Mamba Text Encoder
# ============================================================================
MAMBA_PRETRAINED_MODELS = {
# Mamba-1 variants (trên Hugging Face: state-spaces/mamba-*)
'mamba-130m': {
'hf_name': 'state-spaces/mamba-130m-hf',
'd_model': 768,
'n_layer': 24,
},
'mamba-370m': {
'hf_name': 'state-spaces/mamba-370m-hf',
'd_model': 1024,
'n_layer': 48,
},
'mamba-790m': {
'hf_name': 'state-spaces/mamba-790m-hf',
'd_model': 1536,
'n_layer': 48,
},
'mamba-1.4b': {
'hf_name': 'state-spaces/mamba-1.4b-hf',
'd_model': 2048,
'n_layer': 48,
},
# Mamba-2 variants
'mamba2-130m': {
'hf_name': 'state-spaces/mamba2-130m',
'd_model': 768,
'n_layer': 24,
},
'mamba2-370m': {
'hf_name': 'state-spaces/mamba2-370m',
'd_model': 1024,
'n_layer': 48,
},
'mamba2-780m': {
'hf_name': 'state-spaces/mamba2-780m',
'd_model': 1536,
'n_layer': 48,
},
'mamba2-1.3b': {
'hf_name': 'state-spaces/mamba2-1.3b',
'd_model': 2048,
'n_layer': 48,
},
}
class MambaTextEncoder(nn.Module):
"""
Text Encoder dựa trên Mamba LLM pretrained cho CLIMP-PAR.
Theo bài báo CLIMP (Section 3.2 - Text Encoder):
- Sử dụng pretrained Mamba LLM (Mamba-1 hoặc Mamba-2) làm text backbone
- Last-token pooling: trích hidden state tại vị trí token cuối cùng (non-padding)
→ Đây là vị trí duy nhất có đầy đủ ngữ cảnh do tính causal của Mamba
- Projection W_t chiếu representation sang shared embedding space
- Vượt giới hạn 77-token của CLIP nhờ cơ chế autoregressive
Công thức (Eq. từ CLIMP):
H = [h1, h2, ..., hL] = MambaBackbone(T)
text_repr = H[last_non_pad_idx]
text_embedding = W_t @ text_repr
Args:
model_name (str): Tên model Mamba pretrained. Mặc định: 'mamba-130m'
embed_dim (int): Chiều shared embedding space. Mặc định: 768
cache_dir (str, optional): Thư mục cache lưu model tải về.
freeze_backbone (bool): Đóng băng backbone khi huấn luyện. Mặc định: False
"""
def __init__(self, model_name='mamba-130m', embed_dim=768, cache_dir=None, freeze_backbone=False):
super().__init__()
self.model_name = model_name
self.embed_dim = embed_dim
# Lấy cấu hình
if model_name in MAMBA_PRETRAINED_MODELS:
config = MAMBA_PRETRAINED_MODELS[model_name]
self.d_model = config['d_model']
self.hf_name = config['hf_name']
else:
# Fallback: coi model_name là Hugging Face model name trực tiếp
self.hf_name = model_name
self.d_model = None # Sẽ xác định sau khi load model
# Cache directory mặc định
if cache_dir is None:
cache_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'checkpoints', 'mamba_text')
os.makedirs(cache_dir, exist_ok=True)
self.cache_dir = cache_dir
# 1. Load Mamba backbone + tokenizer từ Hugging Face
self.backbone, self.tokenizer = self._load_model_and_tokenizer()
# 2. Xác định d_model nếu chưa biết
if self.d_model is None:
self.d_model = self.backbone.config.d_model
# 3. Freeze backbone nếu yêu cầu
if freeze_backbone:
for param in self.backbone.parameters():
param.requires_grad = False
print(f"[MambaText] Backbone đã được đóng băng (freeze).")
# 4. Projection head: d_model → embed_dim
self.projection = nn.Linear(self.d_model, embed_dim)
# 5. Layer norm trước projection (stabilize training)
self.norm = nn.LayerNorm(self.d_model)
print(f"[MambaText] Khởi tạo thành công: {model_name}")
print(f"[MambaText] d_model={self.d_model}, embed_dim={embed_dim}")
def _load_model_and_tokenizer(self):
"""
Tải Mamba model và tokenizer.
Xử lý:
1. config.json từ HF chứa key thừa → lọc trước khi tạo MambaConfig
2. HF mới dùng safetensors thay vì pytorch_model.bin → hỗ trợ cả 2
"""
try:
from transformers import AutoTokenizer
from dataclasses import fields as dataclass_fields
from huggingface_hub import hf_hub_download
print(f"[MambaText] Đang tải model '{self.hf_name}'...")
# 1. Tải tokenizer — Mamba-130m dùng GPT-NeoX tokenizer
tokenizer = AutoTokenizer.from_pretrained(
"EleutherAI/gpt-neox-20b",
trust_remote_code=True
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# 2. Tải config.json
config_path = hf_hub_download(self.hf_name, "config.json")
import json
with open(config_path, 'r') as f:
config_data = json.load(f)
# 3. Lọc bỏ key không thuộc MambaConfig dataclass
from mamba_ssm.models.config_mamba import MambaConfig
valid_keys = {f.name for f in dataclass_fields(MambaConfig)}
filtered_config = {k: v for k, v in config_data.items() if k in valid_keys}
removed_keys = set(config_data.keys()) - valid_keys
if removed_keys:
print(f"[MambaText] Bỏ qua config keys: {removed_keys}")
# 4. Tạo model
config = MambaConfig(**filtered_config)
model = MambaLMHeadModel(config, device="cpu", dtype=torch.float32)
# 5. Tải weights — thử safetensors trước, fallback sang pytorch_model.bin
try:
weights_path = hf_hub_download(self.hf_name, "model.safetensors")
from safetensors.torch import load_file
state_dict = load_file(weights_path, device="cpu")
print(f"[MambaText] Tải weights từ safetensors thành công!")
except Exception:
try:
weights_path = hf_hub_download(self.hf_name, "pytorch_model.bin")
state_dict = torch.load(weights_path, map_location="cpu")
print(f"[MambaText] Tải weights từ pytorch_model.bin thành công!")
except Exception as e2:
raise RuntimeError(f"Không tìm thấy weights (safetensors hoặc bin): {e2}")
# Đổi tên key cho khớp với local MambaLMHeadModel
if "backbone.embeddings.weight" in state_dict:
state_dict["backbone.embedding.weight"] = state_dict.pop("backbone.embeddings.weight")
# Dùng strict=False vì lm_head.weight được tied với embedding, có thể thiếu trong state_dict
model.load_state_dict(state_dict, strict=False)
print(f"[MambaText] Tải model thành công!")
return model, tokenizer
except ImportError as e:
raise ImportError(
f"Cần cài đặt thư viện: {e}\n"
"Chạy: pip install transformers huggingface_hub safetensors"
)
except Exception as e:
raise RuntimeError(f"Không thể tải model Mamba '{self.hf_name}': {e}")
def _get_hidden_states(self, input_ids, attention_mask=None):
"""
Chạy backbone Mamba và trả về hidden states của layer cuối.
Args:
input_ids (Tensor): Token IDs, shape (B, L)
attention_mask (Tensor, optional): Mask, shape (B, L). 1=valid, 0=padding
Returns:
hidden_states (Tensor): shape (B, L, d_model)
"""
# Lấy hidden states từ local mamba_ssm
# MambaLMHeadModel trả về CausalLMOutput(logits=lm_logits), ta cần trích xuất hidden states.
# Hoặc dùng trực tiếp model.backbone
hidden_states = self.backbone.backbone(input_ids, inference_params=None)
return hidden_states
def _last_token_pooling(self, hidden_states, attention_mask):
"""
Last-token pooling theo CLIMP.
Theo bài báo: "We extract the hidden state at the last non-padding token
as the text representation"
Lý do: Mamba là mô hình causal (autoregressive), mỗi h_t chỉ chứa
thông tin từ t1 đến t_t. Do đó, token cuối cùng (non-padding) là vị trí
duy nhất có TOÀN BỘ ngữ cảnh của câu.
Args:
hidden_states (Tensor): shape (B, L, d_model)
attention_mask (Tensor): shape (B, L), 1=valid token, 0=padding
Returns:
pooled (Tensor): shape (B, d_model)
"""
if attention_mask is None:
# Không có mask → lấy token cuối cùng
return hidden_states[:, -1, :]
# Tìm vị trí token cuối cùng (non-padding) cho mỗi sample trong batch
# attention_mask.sum(1) - 1 = index của token cuối
sequence_lengths = attention_mask.sum(dim=1) - 1 # (B,)
sequence_lengths = sequence_lengths.long()
# Gather hidden state tại vị trí cuối
batch_size = hidden_states.shape[0]
pooled = hidden_states[torch.arange(batch_size, device=hidden_states.device), sequence_lengths]
return pooled
def tokenize(self, texts, max_length=None, device=None):
"""
Tokenize danh sách câu văn bản thành input_ids và attention_mask.
Args:
texts (list[str]): Danh sách câu prompt
max_length (int): Độ dài tối đa để padding/batching. Nếu None sẽ lấy theo câu dài nhất.
device: Device đích cho tensor output
Returns:
dict với 'input_ids' (B, L) và 'attention_mask' (B, L)
"""
tokens = self.tokenizer(
texts,
padding='max_length' if max_length else True,
truncation=True if max_length else False,
max_length=max_length,
return_tensors='pt'
)
if device is not None:
tokens = {k: v.to(device) for k, v in tokens.items()}
return tokens
def forward(self, input_ids=None, attention_mask=None, texts=None, max_length=None):
"""
Forward pass: Text → Text Embedding
Có thể truyền vào input_ids + attention_mask (đã tokenize sẵn)
hoặc truyền texts (list[str]) để tự động tokenize.
Args:
input_ids (Tensor, optional): Token IDs, shape (B, L)
attention_mask (Tensor, optional): Mask, shape (B, L)
texts (list[str], optional): Danh sách câu văn bản
max_length (int): Độ dài tối đa khi tự tokenize. (Không giới hạn nếu truyền None)
Returns:
text_features (Tensor): Text embedding, shape (B, embed_dim)
"""
# Tự động tokenize nếu truyền texts
if texts is not None:
device = next(self.parameters()).device
tokens = self.tokenize(texts, max_length=max_length, device=device)
input_ids = tokens['input_ids']
attention_mask = tokens['attention_mask']
assert input_ids is not None, "Phải truyền input_ids hoặc texts"
# 1. Lấy hidden states từ Mamba backbone
hidden_states = self._get_hidden_states(input_ids, attention_mask)
# 2. Last-token pooling → (B, d_model)
pooled = self._last_token_pooling(hidden_states, attention_mask)
# 3. Layer Norm
pooled = self.norm(pooled.float()) # Đảm bảo float32
# 4. Projection → shared embedding space
text_features = self.projection(pooled)
return text_features
def encode_prompts(self, prompt_list, max_length=None, batch_size=32):
"""
Mã hóa danh sách prompt thành embeddings (hỗ trợ batch lớn).
Hữu ích cho việc cache text features của tất cả thuộc tính.
Args:
prompt_list (list[str]): Danh sách tất cả prompts
max_length (int): Độ dài padding batching. None là tự động padding.
batch_size (int): Batch size khi encode
Returns:
all_features (Tensor): shape (num_prompts, embed_dim)
"""
all_features = []
for i in range(0, len(prompt_list), batch_size):
batch_texts = prompt_list[i:i + batch_size]
with torch.no_grad():
features = self.forward(texts=batch_texts, max_length=max_length)
all_features.append(features)
return torch.cat(all_features, dim=0)
def get_d_model(self):
"""Trả về chiều hidden state của backbone Mamba."""
return self.d_model
def get_embed_dim(self):
"""Trả về chiều embedding sau projection."""
return self.embed_dim
# ============================================================================
# Factory function
# ============================================================================
def create_text_encoder(model_name='mamba-130m', embed_dim=768, **kwargs):
"""
Factory function tạo Mamba Text Encoder.
Args:
model_name: Tên model ('mamba-130m', 'mamba-370m', 'mamba2-130m', ...)
embed_dim: Chiều embedding đầu ra
Returns:
MambaTextEncoder instance
"""
return MambaTextEncoder(
model_name=model_name,
embed_dim=embed_dim,
**kwargs
)
|