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
    )