""" Utility functions for adding and managing IMG tokens for VLM-guided diffusion. """ import torch import torch.nn as nn from typing import List from llava.constants import IMG_TOKENS def add_img_tokens_to_tokenizer(tokenizer, img_tokens: List[str] = None, special_tokens: bool = True): """ Add IMG tokens to the tokenizer (safe for resume - won't double-add). Args: tokenizer: HuggingFace tokenizer img_tokens: List of IMG tokens (default: IMG_TOKENS from constants) special_tokens: If True, tokens are added as special tokens (auto-stripped with skip_special_tokens=True) If False, tokens are normal vocabulary (always kept in decode) Returns: num_added: Number of tokens added (0 if already present) token_ids: List of token IDs for the IMG tokens Note: - When decoding for debugging/metrics, use skip_special_tokens=False to see IMG tokens - MGIE uses special_tokens=True (default behavior) - Safe for resume: checks existing vocab to avoid double-adding """ if img_tokens is None: img_tokens = IMG_TOKENS # Check for existing tokens (CRITICAL for resume!) existing_vocab = set(tokenizer.get_vocab().keys()) tokens_to_add = [t for t in img_tokens if t not in existing_vocab] if not tokens_to_add: # All tokens already exist (likely resuming from checkpoint) print(f"IMG tokens already in tokenizer (resuming from checkpoint)") token_ids = tokenizer.convert_tokens_to_ids(img_tokens) print(f" Tokens: {img_tokens}") print(f" Token IDs: {token_ids}") print(f" Vocab size: {len(tokenizer)}") return 0, token_ids # Add only new tokens num_added = tokenizer.add_tokens(tokens_to_add, special_tokens=special_tokens) # Get all token IDs (including previously existing ones) token_ids = tokenizer.convert_tokens_to_ids(img_tokens) print(f"Added {num_added} IMG tokens to tokenizer") print(f" New tokens added: {tokens_to_add}") print(f" All IMG token IDs: {token_ids}") print(f" New vocab size: {len(tokenizer)}") print(f" Added as: {'special tokens' if special_tokens else 'normal tokens'}") if special_tokens: print(f" ⚠️ Remember: Use skip_special_tokens=False when decoding for debugging") return num_added, token_ids def save_tokenizer_with_img_tokens(tokenizer, output_dir: str): """ Save tokenizer with IMG tokens for later use. This is critical for: - Resuming training from checkpoint - Evaluation/inference - Preventing silent vocab mismatch errors Args: tokenizer: Tokenizer with IMG tokens added output_dir: Directory to save tokenizer """ import os os.makedirs(output_dir, exist_ok=True) tokenizer.save_pretrained(output_dir) print(f"✓ Saved tokenizer with {len(tokenizer)} tokens to: {output_dir}") def resize_token_embeddings_and_init(model, tokenizer, init_method: str = "mean", init_std: float = 0.02): """ Resize model token embeddings and initialize new IMG token embeddings. Args: model: LLaVA model tokenizer: Tokenizer (after adding IMG tokens) init_method: "mean" (MGIE-style, average of existing) or "normal" (N(0, std)) init_std: Standard deviation for normal initialization (only used if init_method="normal") Returns: new_token_ids: List of new token IDs """ old_vocab_size = model.config.vocab_size new_vocab_size = len(tokenizer) num_new_tokens = new_vocab_size - old_vocab_size if num_new_tokens == 0: print("No new tokens to add") return [] print(f"\nResizing token embeddings:") print(f" Old vocab size: {old_vocab_size}") print(f" New vocab size: {new_vocab_size}") print(f" Adding {num_new_tokens} new tokens") print(f" Initialization method: {init_method}") # Remember original dtype before resize original_dtype = model.get_model().embed_tokens.weight.dtype # Resize embeddings model.resize_token_embeddings(new_vocab_size) # CRITICAL: resize_token_embeddings may reset entire model to float32 # This is especially important when model is in float16/bfloat16 if model.get_model().embed_tokens.weight.dtype != original_dtype: print(f" ! Warning: resize changed dtype from {original_dtype} to {model.get_model().embed_tokens.weight.dtype}") print(f" ! Restoring entire model to {original_dtype}...") # Convert entire model back to original dtype if original_dtype == torch.float16: model = model.half() elif original_dtype == torch.bfloat16: model = model.bfloat16() elif original_dtype == torch.float32: model = model.float() print(f" ✓ Model restored to {original_dtype}") # ALSO convert vision tower if it exists (resize doesn't touch it, but we need consistency) vision_tower = model.get_vision_tower() if vision_tower is not None: vision_dtype = next(vision_tower.parameters()).dtype if vision_dtype != original_dtype: print(f" ! Vision tower dtype mismatch: {vision_dtype} vs expected {original_dtype}") print(f" ! Converting vision tower to {original_dtype}...") if original_dtype == torch.float16: vision_tower = vision_tower.half() elif original_dtype == torch.bfloat16: vision_tower = vision_tower.bfloat16() elif original_dtype == torch.float32: vision_tower = vision_tower.float() print(f" ✓ Vision tower converted to {original_dtype}") # Sanity check: ensure config is updated (HF usually does this automatically) assert model.config.vocab_size == new_vocab_size, \ f"Config vocab size mismatch! model.config.vocab_size={model.config.vocab_size}, expected={new_vocab_size}" # Get new token IDs (the last num_new_tokens) new_token_ids = list(range(old_vocab_size, new_vocab_size)) # Initialize new token embeddings with torch.no_grad(): embed_tokens = model.get_model().embed_tokens lm_head = model.lm_head if init_method == "mean": # MGIE-style: mean of existing embeddings (smoother start) # Compute mean - stays on same device and dtype as source embed_mean = embed_tokens.weight.data[:old_vocab_size].mean(dim=0, keepdim=True) lm_mean = lm_head.weight.data[:old_vocab_size].mean(dim=0, keepdim=True) # Direct assignment (mean is already on correct device from the slice) # Important: Don't use .to(device=...) as model may be distributed across GPUs for idx in new_token_ids: embed_tokens.weight.data[idx] = embed_mean.squeeze(0) lm_head.weight.data[idx] = lm_mean.squeeze(0) print(f" ✓ Initialized embeddings as mean of existing tokens (dtype: {embed_tokens.weight.dtype})") else: # Normal initialization - create on same device/dtype as existing weights # Use the embedding's own device and dtype new_embed = torch.randn( len(new_token_ids), embed_tokens.weight.shape[1], dtype=embed_tokens.weight.dtype, device=embed_tokens.weight.device ) * init_std new_lm = torch.randn( len(new_token_ids), lm_head.weight.shape[1], dtype=lm_head.weight.dtype, device=lm_head.weight.device ) * init_std embed_tokens.weight.data[new_token_ids] = new_embed lm_head.weight.data[new_token_ids] = new_lm print(f" ✓ Initialized embeddings with N(0, {init_std}) (dtype: {embed_tokens.weight.dtype})") # Final verification: check dtype consistency across model print(f"\n Verifying dtype consistency across all modules...") dtype_consistent, _ = verify_model_dtype_consistency(model, expected_dtype=original_dtype) if not dtype_consistent: print(f" ✗ WARNING: Model has dtype inconsistencies after resize!") print(f" ✗ This may cause 'mat1 and mat2 must have same dtype' errors!") print(f" ✗ Consider calling model.half() or model.float() to fix.") else: print(f" ✓ All modules consistent at {original_dtype}") return new_token_ids def setup_llava_trainable_params_mgie_style(model, train_vision_tower: bool = False, train_mm_projector: bool = False): """ Setup trainable parameters MGIE-style: train full embed_tokens and lm_head. This is simpler and safer than gradient masking for controlled training. Args: model: LLaVA model train_vision_tower: Whether to train vision tower (default: False) train_mm_projector: Whether to train mm_projector (default: False) Returns: trainable_param_names: List of trainable parameter names """ print(f"\nSetting up MGIE-style trainable parameters:") # Freeze everything first for param in model.parameters(): param.requires_grad = False trainable_params = [] # Train embed_tokens (full matrix) embed_tokens = model.get_model().embed_tokens embed_tokens.weight.requires_grad = True trainable_params.append("model.embed_tokens.weight") print(f" ✓ embed_tokens: trainable (full matrix, {embed_tokens.weight.shape})") # Train lm_head (full matrix) lm_head = model.lm_head lm_head.weight.requires_grad = True trainable_params.append("lm_head.weight") print(f" ✓ lm_head: trainable (full matrix, {lm_head.weight.shape})") # Optional: train vision tower if train_vision_tower: vision_tower = model.get_vision_tower() if vision_tower is not None: for param in vision_tower.parameters(): param.requires_grad = True trainable_params.append("vision_tower.*") print(f" ✓ vision_tower: trainable") # Optional: train mm_projector if train_mm_projector: mm_projector = model.get_model().mm_projector if mm_projector is not None: for param in mm_projector.parameters(): param.requires_grad = True trainable_params.append("mm_projector.*") print(f" ✓ mm_projector: trainable") # Count trainable parameters total_params = sum(p.numel() for p in model.parameters()) trainable_count = sum(p.numel() for p in model.parameters() if p.requires_grad) print(f"\n Total parameters: {total_params:,}") print(f" Trainable parameters: {trainable_count:,} ({100*trainable_count/total_params:.2f}%)") return trainable_params def setup_img_token_gradient_masks(model, tokenizer, img_tokens: List[str] = None): """ [ADVANCED/OPTIONAL] Setup gradient masks to train only IMG token embeddings and LM head rows. NOTE: For most use cases, use `setup_llava_trainable_params_mgie_style()` instead. This function is for advanced users who want fine-grained control. This registers hooks that zero out gradients for all non-IMG token rows, ensuring only the IMG token embeddings and their LM head projections are trained. MGIE-style (simpler, recommended): Train full matrices with low LR Gradient masking (this function): Train only specific rows with normal LR Args: model: LLaVA model tokenizer: Tokenizer (after adding IMG tokens) img_tokens: List of IMG tokens (default: IMG_TOKENS from constants) Returns: img_token_ids: List of IMG token IDs """ if img_tokens is None: img_tokens = IMG_TOKENS # Get IMG token IDs img_token_ids = tokenizer.convert_tokens_to_ids(img_tokens) print(f"\nSetting up gradient masks for IMG tokens:") print(f" IMG token IDs: {img_token_ids}") # Get embedding and lm_head embed_tokens = model.get_model().embed_tokens lm_head = model.lm_head # Create masks on the correct device and dtype vocab_size = lm_head.weight.size(0) # LM head mask: (vocab_size, 1) - broadcasts over hidden_dim # Match dtype and device of the weights lm_mask = torch.zeros(vocab_size, 1, device=lm_head.weight.device, dtype=lm_head.weight.dtype) lm_mask[img_token_ids] = 1.0 # Embedding mask: (vocab_size, 1) - broadcasts over embed_dim emb_mask = torch.zeros(embed_tokens.num_embeddings, 1, device=embed_tokens.weight.device, dtype=embed_tokens.weight.dtype) emb_mask[img_token_ids] = 1.0 # Register hooks def lm_head_grad_mask_hook(grad): """Zero out gradients for non-IMG token rows in LM head""" # Ensure mask matches gradient dtype mask = lm_mask.to(dtype=grad.dtype) if lm_mask.dtype != grad.dtype else lm_mask return grad * mask def embed_grad_mask_hook(grad): """Zero out gradients for non-IMG token rows in embeddings""" # Ensure mask matches gradient dtype mask = emb_mask.to(dtype=grad.dtype) if emb_mask.dtype != grad.dtype else emb_mask return grad * mask lm_head.weight.register_hook(lm_head_grad_mask_hook) embed_tokens.weight.register_hook(embed_grad_mask_hook) print(f" ✓ Registered gradient mask hooks") print(f" ✓ Only {len(img_token_ids)} token rows will be trained") return img_token_ids def verify_model_dtype_consistency(model, expected_dtype=None): """ Verify that all model parameters have consistent dtype. This is critical after resize_token_embeddings which may reset parts to float32. Args: model: LLaVA model expected_dtype: Expected dtype (if None, use embed_tokens dtype) Returns: all_consistent: Boolean dtype_report: Dict with dtype info per module """ if expected_dtype is None: expected_dtype = model.get_model().embed_tokens.weight.dtype dtype_report = {} all_consistent = True # Check key modules modules_to_check = { 'embed_tokens': model.get_model().embed_tokens, 'lm_head': model.lm_head, } # Add optional modules if hasattr(model.get_model(), 'mm_projector'): modules_to_check['mm_projector'] = model.get_model().mm_projector vision_tower = model.get_vision_tower() if vision_tower is not None: modules_to_check['vision_tower'] = vision_tower for name, module in modules_to_check.items(): param_dtypes = [p.dtype for p in module.parameters()] if param_dtypes: unique_dtypes = set(param_dtypes) dtype_report[name] = unique_dtypes if len(unique_dtypes) > 1: print(f" ✗ {name}: MIXED dtypes {unique_dtypes}") all_consistent = False elif expected_dtype not in unique_dtypes: actual = list(unique_dtypes)[0] print(f" ✗ {name}: dtype {actual}, expected {expected_dtype}") all_consistent = False else: print(f" ✓ {name}: {list(unique_dtypes)[0]}") return all_consistent, dtype_report def verify_img_token_setup(model, tokenizer, img_tokens: List[str] = None): """ Comprehensive sanity check for IMG token setup before training. This checks: - Tokenizer vocab matches model config - IMG tokens exist in vocabulary - IMG tokens are properly initialized - Model is ready for training Args: model: LLaVA model tokenizer: Tokenizer img_tokens: List of IMG tokens (default: IMG_TOKENS from constants) Returns: success: Boolean indicating if all checks passed """ if img_tokens is None: img_tokens = IMG_TOKENS print("\n" + "="*60) print("IMG TOKEN SETUP VERIFICATION") print("="*60) all_good = True # 1. Check tokenizer/model vocab size consistency print(f"\n1. Vocab size consistency:") tokenizer_size = len(tokenizer) model_config_size = model.config.vocab_size embed_size = model.get_model().embed_tokens.weight.shape[0] lm_head_size = model.lm_head.weight.shape[0] print(f" Tokenizer vocab size: {tokenizer_size}") print(f" Model config vocab size: {model_config_size}") print(f" Embedding matrix size: {embed_size}") print(f" LM head matrix size: {lm_head_size}") if tokenizer_size != model_config_size: print(f" ✗ Tokenizer/config mismatch!") all_good = False elif embed_size != tokenizer_size: print(f" ✗ Embedding size mismatch!") all_good = False elif lm_head_size != tokenizer_size: print(f" ✗ LM head size mismatch!") all_good = False else: print(f" ✓ All sizes match ({tokenizer_size})") # 2. Check IMG tokens exist print(f"\n2. IMG tokens in vocabulary:") token_ids = tokenizer.convert_tokens_to_ids(img_tokens) unk_id = tokenizer.unk_token_id missing_tokens = [] for token, token_id in zip(img_tokens, token_ids): if token_id == unk_id: print(f" ✗ {token}: MISSING (maps to UNK)") missing_tokens.append(token) all_good = False else: print(f" ✓ {token}: ID={token_id}") if missing_tokens: print(f" ✗ Missing tokens: {missing_tokens}") else: print(f" ✓ All {len(img_tokens)} IMG tokens present") # 3. Check initialization print(f"\n3. Embedding initialization:") with torch.no_grad(): embed_weights = model.get_model().embed_tokens.weight[token_ids] lm_weights = model.lm_head.weight[token_ids] embed_norms = embed_weights.norm(dim=1) lm_norms = lm_weights.norm(dim=1) print(f" Embedding norms: mean={embed_norms.mean():.4f}, min={embed_norms.min():.4f}, max={embed_norms.max():.4f}") print(f" LM head norms: mean={lm_norms.mean():.4f}, min={lm_norms.min():.4f}, max={lm_norms.max():.4f}") if embed_norms.mean() < 0.001 or lm_norms.mean() < 0.001: print(f" ⚠ Warning: Embeddings may not be initialized (norms very small)") all_good = False else: print(f" ✓ Embeddings appear initialized") # 4. Check dtype consistency print(f"\n4. Dtype consistency:") dtype_consistent, dtype_report = verify_model_dtype_consistency(model) if not dtype_consistent: print(f" ✗ Model has dtype inconsistencies!") all_good = False else: print(f" ✓ All modules have consistent dtype") # 5. Final sanity checks print(f"\n5. Final checks:") # Check that model config is consistent if model.config.vocab_size == tokenizer_size: print(f" ✓ Config vocab_size consistent with tokenizer") else: print(f" ✗ Config vocab_size inconsistent!") all_good = False # Remind about critical training setup print(f"\n6. Training setup reminders:") print(f" ⚠️ Save tokenizer: tokenizer.save_pretrained(output_dir)") print(f" ⚠️ Decode with: skip_special_tokens=False (for debugging)") print(f" ⚠️ Ensure IMG tokens in labels != IGNORE_INDEX") print(f" ⚠️ Use weight_decay=0.0 for embed_tokens and lm_head") print(f" ⚠️ Use low LR (5e-6 to 1e-5) for language matrices") print("\n" + "="*60) if all_good: print("✅ ALL CHECKS PASSED - Ready for training!") else: print("❌ SOME CHECKS FAILED - Fix issues before training!") print("="*60 + "\n") return all_good def verify_img_tokens(model, tokenizer, img_tokens: List[str] = None): """ Verify that IMG tokens exist and are properly set up. Args: model: LLaVA model tokenizer: Tokenizer img_tokens: List of IMG tokens (default: IMG_TOKENS from constants) Returns: success: Boolean indicating if all checks passed """ if img_tokens is None: img_tokens = IMG_TOKENS print("\n" + "="*60) print("VERIFYING IMG TOKENS") print("="*60) all_good = True # Check tokenizer print(f"\n1. Checking tokenizer:") token_ids = tokenizer.convert_tokens_to_ids(img_tokens) unk_id = tokenizer.unk_token_id for token, token_id in zip(img_tokens, token_ids): if token_id == unk_id: print(f" ✗ {token}: MISSING (maps to UNK)") all_good = False else: print(f" ✓ {token}: ID={token_id}") # Check model vocab size print(f"\n2. Checking model:") print(f" Tokenizer vocab size: {len(tokenizer)}") print(f" Model vocab size: {model.config.vocab_size}") print(f" Embedding weight shape: {model.get_model().embed_tokens.weight.shape}") print(f" LM head weight shape: {model.lm_head.weight.shape}") if model.config.vocab_size != len(tokenizer): print(f" ✗ Model vocab size mismatch!") all_good = False else: print(f" ✓ Vocab sizes match") # Check embedding norms (should be non-zero if initialized) print(f"\n3. Checking embedding initialization:") with torch.no_grad(): embed_weights = model.get_model().embed_tokens.weight[token_ids] lm_weights = model.lm_head.weight[token_ids] embed_norms = embed_weights.norm(dim=1) lm_norms = lm_weights.norm(dim=1) print(f" Embedding norms: mean={embed_norms.mean():.4f}, min={embed_norms.min():.4f}, max={embed_norms.max():.4f}") print(f" LM head norms: mean={lm_norms.mean():.4f}, min={lm_norms.min():.4f}, max={lm_norms.max():.4f}") if embed_norms.mean() < 0.001 or lm_norms.mean() < 0.001: print(f" ⚠ Warning: Embeddings may not be initialized (norms very small)") else: print(f" ✓ Embeddings appear initialized") print("\n" + "="*60) if all_good: print("✓ ALL CHECKS PASSED") else: print("✗ SOME CHECKS FAILED") print("="*60 + "\n") return all_good def get_img_token_ids(tokenizer, img_tokens: List[str] = None) -> List[int]: """ Get IMG token IDs from tokenizer. Args: tokenizer: HuggingFace tokenizer img_tokens: List of IMG tokens (default: IMG_TOKENS from constants) Returns: token_ids: List of IMG token IDs """ if img_tokens is None: img_tokens = IMG_TOKENS return tokenizer.convert_tokens_to_ids(img_tokens) def extract_img_token_positions(labels: torch.Tensor, img_token_ids: List[int]) -> torch.Tensor: """ Extract positions of IMG tokens in the labels tensor. Args: labels: (B, L) tensor of token IDs img_token_ids: List of IMG token IDs to search for Returns: positions: (B, K) tensor of positions for each IMG token -1 indicates the token was not found """ batch_size = labels.size(0) num_img_tokens = len(img_token_ids) # Initialize with -1 (not found) positions = torch.full((batch_size, num_img_tokens), -1, dtype=torch.long, device=labels.device) # For each batch for b in range(batch_size): # For each IMG token for k, token_id in enumerate(img_token_ids): # Find positions where this token appears matches = (labels[b] == token_id).nonzero(as_tuple=True)[0] if len(matches) > 0: # Take the first occurrence positions[b, k] = matches[0] return positions def create_img_token_mask(positions: torch.Tensor) -> torch.Tensor: """ Create a binary mask from IMG token positions. Args: positions: (B, K) tensor from extract_img_token_positions (-1 = not found) Returns: mask: (B, K) binary tensor (1=valid token, 0=missing) """ # positions >= 0 means token was found mask = (positions >= 0).to(dtype=torch.float32) return mask def gather_img_hidden_states(hidden_states: torch.Tensor, positions: torch.Tensor, fill_value: float = 0.0) -> torch.Tensor: """ Gather hidden states at IMG token positions. Args: hidden_states: (B, L, D) tensor of hidden states positions: (B, K) tensor of positions (from extract_img_token_positions) fill_value: Value to use for missing tokens (default: 0.0) Returns: img_hiddens: (B, K, D) tensor of IMG token hidden states """ batch_size, seq_len, hidden_dim = hidden_states.shape num_img_tokens = positions.size(1) # Initialize output with fill_value img_hiddens = torch.full( (batch_size, num_img_tokens, hidden_dim), fill_value, dtype=hidden_states.dtype, device=hidden_states.device ) # Gather hidden states at valid positions for b in range(batch_size): for k in range(num_img_tokens): pos = positions[b, k].item() if pos >= 0 and pos < seq_len: img_hiddens[b, k] = hidden_states[b, pos] return img_hiddens def extract_img_token_hiddens(hidden_states: torch.Tensor, labels: torch.Tensor, img_token_ids: List[int], fill_value: float = 0.0, return_mask: bool = False): """ Combined function to extract IMG token hidden states from labels and hidden states. Args: hidden_states: (B, L, D) tensor from model output (last layer) labels: (B, L) tensor of token IDs img_token_ids: List of IMG token IDs fill_value: Value to use for missing tokens (default: 0.0) return_mask: If True, also return a validity mask (default: False) Returns: img_hiddens: (B, K, D) tensor of IMG token hidden states mask: (B, K) binary mask (1=valid, 0=missing) - only if return_mask=True """ positions = extract_img_token_positions(labels, img_token_ids) img_hiddens = gather_img_hidden_states(hidden_states, positions, fill_value) if return_mask: mask = create_img_token_mask(positions) return img_hiddens, mask return img_hiddens