diff --git a/src/dataloaders/build_dataloader.py b/src/dataloaders/build_dataloader.py index af229d3..2449b7c 100644 --- a/src/dataloaders/build_dataloader.py +++ b/src/dataloaders/build_dataloader.py @@ -26,6 +26,7 @@ def build_dataloader( return torch_data_loader def build_torch_dataloader(self, torch_dataset): + self.dataset = torch_dataset sampler = self.get_torch_dataloader_sampler(torch_dataset) if "train" in self.args.mode: torch_data_loader = DataLoader( @@ -64,6 +65,8 @@ def collate_fn(self, batch): batch = [item for item in batch if item is not None] if len(batch) == 0: return None + max_len = max(item["elm_input_ids"].shape[0] for item in batch) + batch = [self.dataset.pad_to_batch(item, max_len) for item in batch] self._assert_same_structure_and_shapes(batch) return torch.utils.data.dataloader.default_collate(batch) diff --git a/src/dataloaders/data_representation/base.py b/src/dataloaders/data_representation/base.py index da1b19d..65b2709 100644 --- a/src/dataloaders/data_representation/base.py +++ b/src/dataloaders/data_representation/base.py @@ -158,6 +158,19 @@ def pad_input(self, tokens: list) -> list: padding_len = self.args.llm_input_len - len(tokens) return [self.llm_tokenizer.pad_token_id] * padding_len + tokens # left side padding + def pad_to_batch(self, item: dict, target_len: int) -> dict: + pad_len = target_len - item["elm_input_ids"].shape[0] + if pad_len == 0: + return item + pad_values = {"elm_input_ids": self.llm_tokenizer.pad_token_id, "elm_labels": -100, "elm_attention_mask": 0} + for key, value in pad_values.items(): + if key in item: + item[key] = torch.nn.functional.pad(item[key], (pad_len, 0), value=value) # left side padding + if "signal_id_indices" in item: + idx = item["signal_id_indices"] + item["signal_id_indices"] = torch.where(idx >= 0, idx + pad_len, idx) # left-pad shifts every real position by pad_len + return item + def make_prompt( self, text: str, diff --git a/src/dataloaders/data_representation/rgb.py b/src/dataloaders/data_representation/rgb.py index 621cbbf..1b31002 100644 --- a/src/dataloaders/data_representation/rgb.py +++ b/src/dataloaders/data_representation/rgb.py @@ -72,8 +72,8 @@ def prepare_training_set( signal_id_indices = self.find_signal_token_indices(truncated_padded_input) attention_mask = self.create_attention_mask(truncated_padded_input) labels = self.create_labels(truncated_padded_input) - assert len(truncated_padded_input) == len(attention_mask) == len(labels) == self.args.llm_input_len, ( - f"Length mismatch: {len(truncated_padded_input)} != {len(attention_mask)} != {len(labels)} != {self.args.llm_input_len}" + assert len(truncated_padded_input) == len(attention_mask) == len(labels), ( + f"Length mismatch: {len(truncated_padded_input)} != {len(attention_mask)} != {len(labels)}" ) elm = { "elm_input_ids": torch.tensor(truncated_padded_input, dtype=torch.int64), @@ -113,12 +113,6 @@ def augment_image(self, image: np.array): def trunc_pad_input(self, prompt: str): prompt_tokens = self.llm_tokenizer.encode(prompt, add_special_tokens=False) - if "train" in self.args.mode: - prompt_len = len(prompt_tokens) - if prompt_len == self.args.llm_input_len: - return prompt_tokens - elif prompt_len < self.args.llm_input_len: - return self.pad_input(prompt_tokens) + if "train" in self.args.mode and len(prompt_tokens) > self.args.llm_input_len: return self.truncate_input_preserving_signal_tokens(prompt_tokens) - else: - return prompt_tokens \ No newline at end of file + return prompt_tokens \ No newline at end of file diff --git a/src/dataloaders/data_representation/signal.py b/src/dataloaders/data_representation/signal.py index 6730544..7633ca2 100644 --- a/src/dataloaders/data_representation/signal.py +++ b/src/dataloaders/data_representation/signal.py @@ -47,8 +47,8 @@ def prepare_training_set( labels = self.create_labels(truncated_padded_input) # print("signal_id_indices", len(signal_id_indices), "\n") assert len(signal_id_indices) == self.args.num_encoder_tokens - assert len(truncated_padded_input) == len(attention_mask) == len(labels) == self.args.llm_input_len, ( - f"Length mismatch: {len(truncated_padded_input)} != {len(attention_mask)} != {len(labels)} != {self.args.llm_input_len}" + assert len(truncated_padded_input) == len(attention_mask) == len(labels), ( + f"Length mismatch: {len(truncated_padded_input)} != {len(attention_mask)} != {len(labels)}" ) elm = { "elm_input_ids": torch.tensor(truncated_padded_input, dtype=torch.int64), @@ -76,16 +76,9 @@ def prepare_eval_inference_set( def trunc_pad_input(self, prompt: str): prompt_tokens = self.llm_tokenizer.encode(prompt, add_special_tokens=False) - if "train" in self.args.mode: - prompt_len = len(prompt_tokens) - # print("prompt len", prompt_len, "\n") - if prompt_len == self.args.llm_input_len: - return prompt_tokens - elif prompt_len < self.args.llm_input_len: - return self.pad_input(prompt_tokens) + if "train" in self.args.mode and len(prompt_tokens) > self.args.llm_input_len: return self.truncate_input_preserving_signal_tokens(prompt_tokens) - else: - return prompt_tokens + return prompt_tokens def transform_ecg_signal(self, ecg_signal): if self.args.elm == "base_elf": diff --git a/src/dataloaders/data_representation/stacked_signal.py b/src/dataloaders/data_representation/stacked_signal.py index 9f078c3..c9ccfbb 100644 --- a/src/dataloaders/data_representation/stacked_signal.py +++ b/src/dataloaders/data_representation/stacked_signal.py @@ -55,8 +55,8 @@ def prepare_training_set( signal_id_indices = self.find_signal_token_indices(truncated_padded_input) attention_mask = self.create_attention_mask(truncated_padded_input) labels = self.create_labels(truncated_padded_input) - assert len(truncated_padded_input) == len(attention_mask) == len(labels) == self.args.llm_input_len, ( - f"Length mismatch: {len(truncated_padded_input)} != {len(attention_mask)} != {len(labels)} != {self.args.llm_input_len}" + assert len(truncated_padded_input) == len(attention_mask) == len(labels), ( + f"Length mismatch: {len(truncated_padded_input)} != {len(attention_mask)} != {len(labels)}" ) elm = { "elm_input_ids": torch.tensor(truncated_padded_input, dtype=torch.int64), @@ -90,12 +90,6 @@ def signal_to_stacked_signal(self, signal): def trunc_pad_input(self, prompt: str): prompt_tokens = self.llm_tokenizer.encode(prompt, add_special_tokens=False) - if "train" in self.args.mode: - prompt_len = len(prompt_tokens) - if prompt_len == self.args.llm_input_len: - return prompt_tokens - elif prompt_len < self.args.llm_input_len: - return self.pad_input(prompt_tokens) + if "train" in self.args.mode and len(prompt_tokens) > self.args.llm_input_len: return self.truncate_input_preserving_signal_tokens(prompt_tokens) - else: - return prompt_tokens + return prompt_tokens diff --git a/src/dataloaders/data_representation/symbolic.py b/src/dataloaders/data_representation/symbolic.py index 1e9b0cc..4c88f2e 100644 --- a/src/dataloaders/data_representation/symbolic.py +++ b/src/dataloaders/data_representation/symbolic.py @@ -55,8 +55,8 @@ def prepare_training_set( self.check_labels(labels) self.check_attention_mask(truncated_padded_input, attention_mask) - assert len(truncated_padded_input) == len(attention_mask) == len(labels) == self.args.llm_input_len, ( - f"Length mismatch: {len(truncated_padded_input)} != {len(attention_mask)} != {len(labels)} != {self.args.llm_input_len}" + assert len(truncated_padded_input) == len(attention_mask) == len(labels), ( + f"Length mismatch: {len(truncated_padded_input)} != {len(attention_mask)} != {len(labels)}" ) # print("truncated_padded_ecg_tokens", truncated_padded_ecg_tokens) # print("signal_id_indices", signal_id_indices) @@ -86,12 +86,8 @@ def trunc_pad_input(self, ecg_tokens: np.ndarray, prompt: str): min_ecg_token_len = int(self.args.min_ecg_tokens_len) before_len, after_len, ecg_token_len = len(before), len(after), len(ecg_tokens) - if before_len + after_len + ecg_token_len == self.args.llm_input_len: - # return before + ecg_tokens + after, self.convert_ecg_tokens(ecg_tokens) + if before_len + after_len + ecg_token_len <= self.args.llm_input_len: return before + ecg_tokens + after - elif before_len + after_len + ecg_token_len < self.args.llm_input_len: - # return self.pad_input(before + ecg_tokens + after), self.convert_ecg_tokens(ecg_tokens) - return self.pad_input(before + ecg_tokens + after) if before_len + min_ecg_token_len > self.args.llm_input_len: raise ValueError("before + min_ecg exceeds llm_input_len; lower min_ecg_tokens_len.") diff --git a/src/main_trainer.py b/src/main_trainer.py index 8b7636c..d3b1c85 100644 --- a/src/main_trainer.py +++ b/src/main_trainer.py @@ -13,7 +13,7 @@ from utils.checkpoint_manager import CheckpointManager from utils.seed_manager import set_seed -from utils.gpu_manager import is_main, init_dist, cleanup, GPUSetup, broadcast_value +from utils.gpu_manager import is_main, init_dist, cleanup, GPUSetup, broadcast_value, assert_max_batch_fits from utils.dir_file_manager import setup_experiment_folders from utils.wandb_manager import setup_wandb, cleanup_wandb @@ -67,6 +67,8 @@ def main(): if args.resume_ckpt and checkpoint_manager: start_epoch = checkpoint_manager.resume_checkpoint(args.resume_ckpt, elm, optimizer) runner = run_rl_train if getattr(args, "train_phase", "sft") == "rl" else run_train + if not args.dev and runner is run_train: + assert_max_batch_fits(elm, next(iter(dataloader)), args, optimizer) for epoch in range(start_epoch, args.epochs): train_result = runner(elm, optimizer, dataloader, epoch, args, checkpoint_manager) should_stop = False diff --git a/src/optimizers/optimizer_setup.py b/src/optimizers/optimizer_setup.py index 47188d9..c67dd7d 100644 --- a/src/optimizers/optimizer_setup.py +++ b/src/optimizers/optimizer_setup.py @@ -143,6 +143,15 @@ def _build_muon_optimizer(self, model): return MuonAdamW(muon_opt, adamw_opt, adamw_lr_ratio) + def estimated_state_bytes(self) -> int: + """Resident optimizer-state bytes once allocated: Adam/AdamW keep 2 tensors + (exp_avg, exp_avg_sq) per param; Muon keeps 1 momentum buffer per param.""" + def nbytes(param_groups, per_param): + return per_param * sum(p.numel() * p.element_size() for g in param_groups for p in g["params"]) + if self._is_muon: + return nbytes(self.optimizer.muon.param_groups, 1) + nbytes(self.optimizer.adamw.param_groups, 2) + return nbytes(self.optimizer.param_groups, 2) + def _log_config(self): if is_main(): if self._is_muon: diff --git a/src/utils/gpu_manager.py b/src/utils/gpu_manager.py index 61ab8aa..0b08f7f 100644 --- a/src/utils/gpu_manager.py +++ b/src/utils/gpu_manager.py @@ -70,6 +70,46 @@ def train_dev_break(enabled: bool, batch: dict, loss_value: float) -> bool: return broadcast_value(should_break, src=0) +def assert_max_batch_fits(model, batch: dict, args, optimizer=None, margin: float = 0.10) -> None: + if batch is None or not torch.cuda.is_available(): + return + F, ids = torch.nn.functional, batch["elm_input_ids"] + pad = args.llm_input_len - ids.shape[1] + if pad <= 0: + return # first batch already at the cap; training itself already stresses it + probe = dict(batch) + probe["elm_input_ids"] = F.pad(ids, (pad, 0), value=int(ids.reshape(-1)[0])) + probe["elm_attention_mask"] = F.pad(batch["elm_attention_mask"], (pad, 0), value=0) + if "elm_labels" in batch: + probe["elm_labels"] = F.pad(batch["elm_labels"], (pad, 0), value=-100) + if "signal_id_indices" in batch: + probe["signal_id_indices"] = torch.where(batch["signal_id_indices"] >= 0, batch["signal_id_indices"] + pad, batch["signal_id_indices"]) + device = next(model.parameters()).device + probe = {k: batch_to_device(v, device) for k, v in probe.items()} + torch.cuda.reset_peak_memory_stats(device) + try: + model(**probe).loss.backward() + except torch.cuda.OutOfMemoryError as e: + raise RuntimeError(f"[fit-check] worst-case batch (batch_size={ids.shape[0]} x llm_input_len=" + f"{args.llm_input_len}) does not fit; lower --batch_size / --llm_input_len or " + f"add --gradient_checkpointing. ({e})") from e + finally: + model.zero_grad(set_to_none=True) + torch.cuda.empty_cache() + # backward peak (params + grads + activations) + optimizer state resident during real steps + opt_bytes = optimizer.estimated_state_bytes() if optimizer is not None else 0 + peak = torch.cuda.max_memory_allocated(device) + opt_bytes + free, _ = torch.cuda.mem_get_info(device) + budget = free + torch.cuda.memory_allocated(device) # free accounts for other processes; + our resident params + gb = 1024 ** 3 + if peak * (1 + margin) > budget: + raise RuntimeError(f"[fit-check] worst-case peak ~{peak / gb:.1f} GB (incl. optimizer state) leaves " + f"<{margin:.0%} headroom of {budget / gb:.1f} GB available; lower --batch_size / " + f"--llm_input_len or add --gradient_checkpointing.") + if is_main(): + print(f"[fit-check] OK: worst-case ~{peak / gb:.1f} GB / {budget / gb:.1f} GB available ({margin:.0%} margin kept).") + + class GPUSetup: def __init__(self, args: argparse.Namespace): self.args = args @@ -82,7 +122,7 @@ def setup_gpu(self, model: torch.nn.Module, find_unused_parameters) -> torch.nn. if is_main(): print(f"find_unused_parameters: {find_unused_parameters}") if self.args.torch_compile: - model = torch.compile(model) + model = torch.compile(model, dynamic=True) return model def get_device(self) -> torch.device: