| |
| |
| |
| |
|
|
| from types import SimpleNamespace |
| from opentslm.model.encoder.CNNTokenizer import CNNTokenizer |
| from opentslm.model.llm.TimeSeriesFlamingoWithTrainableEncoder import ( |
| TimeSeriesFlamingoWithTrainableEncoder, |
| ) |
| from open_flamingo.src.flamingo_lm import FlamingoLMMixin |
| from open_flamingo.src.utils import extend_instance |
| import torch |
| import torch._dynamo |
| from typing import List, Dict, Tuple |
| from transformers import AutoTokenizer, AutoModelForCausalLM |
|
|
| from opentslm.model_config import ENCODER_OUTPUT_DIM |
| from opentslm.model.llm.TimeSeriesLLM import TimeSeriesLLM |
| from opentslm.prompt.full_prompt import FullPrompt |
| from opentslm.time_series_datasets.util import ( |
| extend_time_series_to_match_patch_size_and_aggregate, |
| ) |
|
|
| |
| from open_flamingo.src.flamingo_lm import FlamingoLayer |
|
|
|
|
| def _attention_type_property(self): |
| """Proxy the attention_type attribute from the underlying decoder layer.""" |
| return getattr(self.decoder_layer, "attention_type", None) |
|
|
|
|
| |
| FlamingoLayer.attention_type = property(_attention_type_property) |
|
|
|
|
| class OpenTSLMFlamingo(TimeSeriesLLM): |
| def __init__( |
| self, |
| device: str, |
| llm_id: str = "meta-llama/Llama-3.2-1B", |
| cross_attn_every_n_layers: int = 1, |
| decoder_layers_attr_name: str = None, |
| freeze_lm_embeddings: bool = False, |
| **flamingo_kwargs, |
| ): |
| super().__init__(device) |
| print(f"Flamingo Using device: {self.device}") |
| time_series_encoder = CNNTokenizer().to(device) |
|
|
| text_tokenizer = AutoTokenizer.from_pretrained( |
| llm_id, |
| local_files_only=False, |
| trust_remote_code=True, |
| cache_dir=None, |
| ) |
|
|
| lang_encoder = AutoModelForCausalLM.from_pretrained( |
| llm_id, |
| local_files_only=False, |
| trust_remote_code=True, |
| cache_dir=None, |
| device_map={"": device}, |
| attn_implementation="eager", |
| ) |
|
|
| |
| text_tokenizer.add_special_tokens( |
| {"additional_special_tokens": ["<|endofchunk|>", "<image>"]} |
| ) |
| if text_tokenizer.pad_token is None: |
| text_tokenizer.add_special_tokens({"pad_token": "<PAD>"}) |
| text_tokenizer.pad_token = "<PAD>" |
|
|
| |
| extend_instance(lang_encoder, FlamingoLMMixin) |
|
|
| def _infer_decoder_layers_attr_name(model): |
| __KNOWN_DECODER_LAYERS_ATTR_NAMES = { |
| "opt": "model.decoder.layers", |
| "gptj": "transformer.h", |
| "gpt-j": "transformer.h", |
| "pythia": "gpt_neox.layers", |
| "llama": "model.layers", |
| "gptneoxforcausallm": "gpt_neox.layers", |
| "mpt": "transformer.blocks", |
| "mosaicgpt": "transformer.blocks", |
| "gemma": "model.layers", |
| "gemma2": "model.layers", |
| "gemma3": "model.layers", |
| "medgemma": "model.layers", |
| } |
|
|
| |
| model_class_name = model.__class__.__name__ |
| if "gemma3" in model_class_name.lower(): |
| if "ConditionalGeneration" in model_class_name: |
| |
| return "language_model.layers" |
| else: |
| |
| return "model.layers" |
|
|
| |
| for k in __KNOWN_DECODER_LAYERS_ATTR_NAMES: |
| if k.lower() in model.__class__.__name__.lower(): |
| return __KNOWN_DECODER_LAYERS_ATTR_NAMES[k] |
|
|
| raise ValueError( |
| f"We require the attribute name for the nn.ModuleList in the decoder storing the transformer block layers. Please supply this string manually." |
| ) |
|
|
| decoder_layers_attr_name = _infer_decoder_layers_attr_name(lang_encoder) |
| lang_encoder.set_decoder_layers_attr_name(decoder_layers_attr_name) |
| lang_encoder.resize_token_embeddings(len(text_tokenizer)) |
|
|
| |
| if hasattr(lang_encoder.config, "text_config") and hasattr( |
| lang_encoder.config.text_config, "hidden_size" |
| ): |
| if not hasattr(lang_encoder.config, "hidden_size"): |
| lang_encoder.config.hidden_size = ( |
| lang_encoder.config.text_config.hidden_size |
| ) |
|
|
| model = TimeSeriesFlamingoWithTrainableEncoder( |
| SimpleNamespace(visual=time_series_encoder), |
| lang_encoder, |
| text_tokenizer.encode("<|endofchunk|>")[-1], |
| text_tokenizer.encode("<image>")[-1], |
| vis_dim=ENCODER_OUTPUT_DIM, |
| cross_attn_every_n_layers=cross_attn_every_n_layers, |
| **flamingo_kwargs, |
| ) |
|
|
| |
| model.requires_grad_(False) |
| assert sum(p.numel() for p in model.parameters() if p.requires_grad) == 0 |
|
|
| |
| model.perceiver.requires_grad_(True) |
| model.lang_encoder.gated_cross_attn_layers.requires_grad_(True) |
| if not freeze_lm_embeddings: |
| model.lang_encoder.get_input_embeddings().requires_grad_(True) |
| |
|
|
| |
| model.vision_encoder.requires_grad_(True) |
|
|
| self.model = model |
| self.llm = model |
| self.text_tokenizer = text_tokenizer |
|
|
| def pad_and_apply_batch( |
| self, batch: List[Dict[str, any]], include_labels: bool |
| ) -> Tuple[torch.Tensor, torch.Tensor]: |
| def pad_time_series(batch, max_length=None): |
| """Pad time series to the same length (either max in batch or specified max)""" |
| time_series = [item["time_series"] for item in batch] |
|
|
| |
| if max_length is None: |
| max_length = max(ts.shape[1] for ts in time_series) |
|
|
| padded_series = [] |
| for ts in time_series: |
| current_length = ts.shape[1] |
| if current_length < max_length: |
| |
| |
| padding_shape = list(ts.shape) |
| padding_shape[1] = max_length - current_length |
| padding = torch.zeros( |
| padding_shape, device=ts.device, dtype=ts.dtype |
| ) |
| padded = torch.cat([ts, padding], dim=1) |
| else: |
| |
| padded = ts[:, :max_length] |
|
|
| padded_series.append(padded) |
|
|
| return torch.stack(padded_series) |
|
|
| cast_dtype = None |
| tokenizer = self.text_tokenizer |
| media_token_id = tokenizer("<image>", add_special_tokens=False)["input_ids"][-1] |
| endofchunk_token_id = tokenizer("<|endofchunk|>", add_special_tokens=False)[ |
| "input_ids" |
| ][-1] |
|
|
| |
| images = pad_time_series(batch).to( |
| self.device, dtype=cast_dtype, non_blocking=True |
| ) |
| images = images.unsqueeze(1) |
|
|
| |
| text_inputs = [] |
| |
| text_inputs = [] |
| prompt_lengths = [] |
|
|
| for item in batch: |
| |
| prompt_text = item["pre_prompt"] |
| for ts_text in item["time_series_text"]: |
| prompt_text += f" {tokenizer.decode([media_token_id])} {ts_text} {tokenizer.decode([endofchunk_token_id])}" |
| if item["post_prompt"]: |
| prompt_text += f" {item['post_prompt']}" |
|
|
| if include_labels: |
| text_inputs.append(prompt_text) |
| continue |
|
|
| |
| prompt_tokens = tokenizer(prompt_text, add_special_tokens=False).input_ids |
| prompt_lengths.append(len(prompt_tokens)) |
|
|
| |
| full_text = prompt_text + f" {item['answer']}" |
| text_inputs.append(full_text) |
|
|
| |
| tokenized = tokenizer(text_inputs, padding="longest", return_tensors="pt") |
| input_ids = tokenized.input_ids.to(self.device, non_blocking=True) |
| attention_mask = tokenized.attention_mask.to(self.device, non_blocking=True) |
|
|
| if include_labels: |
| return input_ids, images, attention_mask, None |
|
|
| |
| labels = torch.full_like(input_ids, -100) |
|
|
| |
| for i, prompt_length in enumerate(prompt_lengths): |
| non_padding_indices = torch.where(input_ids[i] != tokenizer.pad_token_id)[0] |
| answer_indices = non_padding_indices[non_padding_indices >= prompt_length] |
|
|
| if len(answer_indices) > 0: |
| labels[i, answer_indices] = input_ids[i, answer_indices] |
|
|
| return input_ids, images, attention_mask, labels |
|
|
| def generate( |
| self, batch: List[Dict[str, any]], max_new_tokens: int = 50, **generate_kwargs |
| ) -> List[str]: |
| |
| original_disable = torch._dynamo.config.disable |
| torch._dynamo.config.disable = True |
|
|
| try: |
| with torch.inference_mode(): |
| input_ids, images, attention_mask, _ = self.pad_and_apply_batch( |
| batch, include_labels=True |
| ) |
|
|
| gen_ids = self.llm.generate( |
| vision_x=images, |
| lang_x=input_ids, |
| attention_mask=attention_mask, |
| max_new_tokens=max_new_tokens, |
| eos_token_id=self.text_tokenizer.eos_token_id, |
| pad_token_id=self.text_tokenizer.pad_token_id, |
| **generate_kwargs, |
| ) |
|
|
| |
| answer_only_ids = gen_ids[:, input_ids.shape[1] :] |
|
|
| return self.text_tokenizer.batch_decode( |
| answer_only_ids, skip_special_tokens=True |
| ) |
| finally: |
| |
| torch._dynamo.config.disable = original_disable |
|
|
| def compute_loss(self, batch: List[Dict[str, any]]) -> torch.Tensor: |
| """ |
| batch: same format as generate() |
| answers: List[str] of length B |
| """ |
| input_ids, images, attention_mask, labels = self.pad_and_apply_batch( |
| batch, include_labels=False |
| ) |
|
|
| output = self.model( |
| vision_x=images, |
| lang_x=input_ids, |
| attention_mask=attention_mask, |
| labels=labels, |
| ) |
| return output[0] |
|
|
| def get_eos_token(self) -> str: |
| return self.text_tokenizer.eos_token |
|
|
| def store_to_file(self, path: str = "best_model.pt"): |
| |
| |
| |
| |
| state_dict = { |
| "llm": self.llm.state_dict(), |
| } |
| torch.save(state_dict, path) |
| print(f"Model saved to {path}") |
|
|
| def load_from_file(self, path: str = "best_model.pt"): |
| """ |
| Load model parameters with non-strict loading to handle Flamingo-specific layers. |
| """ |
| checkpoint = torch.load(path, map_location=self.device) |
|
|
| if "llm" in checkpoint: |
| model_state = checkpoint["llm"] |
| elif "model_state" in checkpoint: |
| model_state = checkpoint["model_state"] |
| else: |
| raise RuntimeError("No recognized model state key in checkpoint.") |
|
|
| |
| if hasattr(self, "module"): |
| model_state = {f"module.{k}": v for k, v in model_state.items()} |
|
|
| |
| if all(k.startswith("model.") for k in model_state.keys()): |
| model_state = { |
| k.replace("model.", "", 1): v for k, v in model_state.items() |
| } |
|
|
| |
| missing_keys, unexpected_keys = self.load_state_dict(model_state, strict=False) |
| if missing_keys: |
| print(f"⚠️ Warning: Missing keys when loading checkpoint:") |
| for key in missing_keys[:10]: |
| print(f" - {key}") |
| if len(missing_keys) > 10: |
| print(f" ... and {len(missing_keys) - 10} more keys") |
| if unexpected_keys: |
| print(f"⚠️ Warning: Unexpected keys when loading checkpoint:") |
| for key in unexpected_keys[:10]: |
| print(f" - {key}") |
| if len(unexpected_keys) > 10: |
| print(f" ... and {len(unexpected_keys) - 10} more keys") |
| self.to(self.device) |
|
|
| def eval_prompt( |
| self, prompt: FullPrompt, max_new_tokens: int = 1000, normalize: bool = False |
| ) -> str: |
| """ |
| Evaluate a prompt and return the generated text. |
| """ |
| |
| original_disable = torch._dynamo.config.disable |
| torch._dynamo.config.disable = True |
| try: |
| batch = [prompt.to_dict()] |
| self.eval() |
| batch = extend_time_series_to_match_patch_size_and_aggregate( |
| batch, normalize=normalize |
| ) |
| print("Generating") |
| output = self.generate(batch, max_new_tokens=max_new_tokens) |
| print(f"Generated output: {output[0]}") |
| return output[0] |
| finally: |
| |
| torch._dynamo.config.disable = original_disable |
|
|