timeagent / code /OpenTSLM /evaluation /memory /get_memory_use.py
roh8exe's picture
Upload folder using huggingface_hub
60b21d3 verified
Raw
History Blame Contribute Delete
14.6 kB
# SPDX-FileCopyrightText: 2025 Stanford University, ETH Zurich, and the project authors (see CONTRIBUTORS.md)
# SPDX-FileCopyrightText: 2025 This source file is part of the OpenTSLM open-source project.
#
# SPDX-License-Identifier: MIT
import argparse
import csv
import os
from datetime import datetime
from typing import Dict, List, Tuple
import torch
from tqdm.auto import tqdm
from torch.utils.data import DataLoader
import os as _os
import psutil
import pynvml # type: ignore
# Models
from opentslm.model.llm.OpenTSLMFlamingo import OpenTSLMFlamingo
from opentslm.model.llm.OpenTSLMSP import OpenTSLMSP
# Datasets
from opentslm.time_series_datasets.TSQADataset import TSQADataset
from opentslm.time_series_datasets.har_cot.HARCoTQADataset import HARCoTQADataset
from opentslm.time_series_datasets.sleep.SleepEDFCoTQADataset import SleepEDFCoTQADataset
from opentslm.time_series_datasets.ecg_qa.ECGQACoTQADataset import ECGQACoTQADataset
from opentslm.time_series_datasets.simulation.SimulationQADataset import SimulationQADataset
from opentslm.time_series_datasets.util import (
extend_time_series_to_match_patch_size_and_aggregate,
)
_NVML_AVAILABLE = True
def get_device(device_arg: str | None) -> str:
if device_arg:
return device_arg
return "cpu" # Default to CPU for easier testing
def measure_peak_cuda_bytes() -> int:
if not torch.cuda.is_available():
return -1
torch.cuda.synchronize()
return int(torch.cuda.max_memory_allocated())
def measure_peak_cuda_reserved_bytes() -> int:
if not torch.cuda.is_available():
return -1
torch.cuda.synchronize()
return int(torch.cuda.max_memory_reserved())
def measure_peak_cpu_bytes() -> int:
"""Measure peak CPU memory usage in bytes"""
try:
process = psutil.Process()
memory_info = process.memory_info()
return int(memory_info.rss) # Resident Set Size (physical memory)
except Exception:
return -1
def nvml_current_process_bytes() -> int:
if not _NVML_AVAILABLE or not torch.cuda.is_available():
return -1
try:
pynvml.nvmlInit()
pid = _os.getpid()
total_bytes = 0
found = False
device_count = pynvml.nvmlDeviceGetCount()
for device_index in range(device_count):
handle = pynvml.nvmlDeviceGetHandleByIndex(device_index)
# Try compute procs first
try:
procs = pynvml.nvmlDeviceGetComputeRunningProcesses_v3(handle)
except Exception:
procs = []
# Fallback to graphics procs
try:
procs_gfx = pynvml.nvmlDeviceGetGraphicsRunningProcesses_v3(handle)
except Exception:
procs_gfx = []
for p in list(procs) + list(procs_gfx):
if (
int(p.pid) == pid
and p.usedGpuMemory is not None
and p.usedGpuMemory >= 0
):
total_bytes += int(p.usedGpuMemory)
found = True
return total_bytes if found else -1
except Exception:
return -1
def get_first_batch(dataset, batch_size: int = 1) -> List[Dict[str, any]]:
# QADataset returns dict samples compatible with model.compute_loss
batch: List[Dict[str, any]] = []
for i in range(min(batch_size, len(dataset))):
batch.append(dataset[i])
# Ensure time series tensors are padded and converted
batch = extend_time_series_to_match_patch_size_and_aggregate(batch)
return batch
def build_optimizer(model, model_type: str, base_lr: float = 2e-4):
if model_type == "OpenTSLMSP":
enc_params = [
p for p in getattr(model, "encoder").parameters() if p.requires_grad
]
proj_params = [
p for p in getattr(model, "projector").parameters() if p.requires_grad
]
param_groups = []
if len(enc_params) > 0:
param_groups.append({"params": enc_params, "weight_decay": 0.1})
if len(proj_params) > 0:
param_groups.append({"params": proj_params, "weight_decay": 0.1})
return (
torch.optim.AdamW(param_groups, lr=base_lr)
if len(param_groups) > 0
else None
)
# Flamingo-like
named_params = list(model.named_parameters())
trainable = list(
filter(
lambda np: np[1].requires_grad
and not getattr(np[1], "exclude_from_optimizer", False),
named_params,
)
)
params_with_wd, params_without_wd = [], []
for name, p in trainable:
if "gated_cross_attn" in name:
params_with_wd.append(p)
else:
params_without_wd.append(p)
if len(params_with_wd) + len(params_without_wd) == 0:
return None
return torch.optim.AdamW(
[
{"params": params_with_wd, "weight_decay": 0.1},
{"params": params_without_wd, "weight_decay": 0.0},
],
lr=2e-4,
)
def train_for_steps(
model, model_type: str, dataset, steps: int
) -> Tuple[float, int, int, int]:
model.train()
optimizer = build_optimizer(model, model_type)
# Initialize memory tracking
if torch.cuda.is_available():
torch.cuda.reset_peak_memory_stats()
torch.cuda.synchronize()
# Track CPU memory baseline
cpu_memory_baseline = measure_peak_cpu_bytes()
last_loss = 0.0
# DataLoader with shuffle and collate that pads series
loader = DataLoader(
dataset,
batch_size=1,
shuffle=True,
collate_fn=lambda b: extend_time_series_to_match_patch_size_and_aggregate(b),
drop_last=False,
)
pbar = tqdm(total=steps, desc="Training", leave=False)
max_peak_bytes = -1
max_reserved_bytes = -1
max_nvml_bytes = -1
max_cpu_bytes = cpu_memory_baseline
step = 0
# Initialize postfix
pbar.set_postfix(
{
"alloc_gb": 0.0,
"res_gb": 0.0,
"nvml_gb": 0.0,
"cpu_gb": 0.0,
}
)
for batch in loader:
if optimizer:
optimizer.zero_grad(set_to_none=True)
loss = model.compute_loss(batch)
if optimizer and loss.requires_grad:
print(f"Backpropagating loss of {loss.item()} for step {step}")
loss.backward()
optimizer.step()
last_loss = float(loss.detach().item())
# Track peak memory across steps
if torch.cuda.is_available():
torch.cuda.synchronize()
current_peak = int(torch.cuda.max_memory_allocated())
current_reserved = int(torch.cuda.max_memory_reserved())
if current_peak > max_peak_bytes:
max_peak_bytes = current_peak
if current_reserved > max_reserved_bytes:
max_reserved_bytes = current_reserved
nvml_bytes = nvml_current_process_bytes()
if nvml_bytes > max_nvml_bytes:
max_nvml_bytes = nvml_bytes
# Track CPU memory
current_cpu_bytes = measure_peak_cpu_bytes()
if current_cpu_bytes > max_cpu_bytes:
max_cpu_bytes = current_cpu_bytes
# Update progress bar postfix in GB
def _to_gb(val: int) -> float:
return (
float(val) / (1024.0**3)
if isinstance(val, (int, float)) and val >= 0
else 0.0
)
pbar.set_postfix(
{
"alloc_gb": f"{_to_gb(max_peak_bytes):.2f}",
"res_gb": f"{_to_gb(max_reserved_bytes):.2f}",
"nvml_gb": f"{_to_gb(max_nvml_bytes):.2f}",
"cpu_gb": f"{_to_gb(max_cpu_bytes):.2f}",
}
)
step += 1
pbar.update(1)
if step >= steps:
break
pbar.close()
if torch.cuda.is_available():
peak_bytes = max_peak_bytes
peak_reserved_bytes = max_reserved_bytes
nvml_peak_bytes = max_nvml_bytes
else:
peak_bytes = max_cpu_bytes # Use CPU memory as fallback
peak_reserved_bytes = max_cpu_bytes
nvml_peak_bytes = max_cpu_bytes
return last_loss, peak_bytes, peak_reserved_bytes, nvml_peak_bytes
def ensure_csv(path: str, header: List[str]):
exists = os.path.exists(path)
if not exists:
with open(path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(header)
def append_row(path: str, row: List[any]):
with open(path, "a", newline="") as f:
writer = csv.writer(f)
writer.writerow(row)
def run_for_dataset(
model_name: str, model, dataset_name: str, dataset_obj
) -> Dict[str, any]:
result: Dict[str, any] = {
"model": model_name,
"dataset": dataset_name,
"loss": None,
"peak_cuda_bytes": None,
"status": "ok",
"error": "",
}
try:
# Train for half an epoch, capped at 10000 steps
steps = max(1, min(len(dataset_obj), 100))
loss, peak, peak_reserved, nvml_peak = train_for_steps(
model, model_name, dataset_obj, steps
)
result["loss"] = loss
result["peak_cuda_bytes"] = peak
result["peak_cuda_reserved_bytes"] = peak_reserved
result["nvml_peak_bytes"] = nvml_peak
except Exception as e:
result["status"] = "error"
result["error"] = str(e)
return result
def main():
parser = argparse.ArgumentParser(
description="Measure memory use for a single training iteration for a chosen model and dataset."
)
parser.add_argument(
"-llm_id", required=True, help="HuggingFace model id for the language model"
)
parser.add_argument(
"--model",
required=True,
choices=["OpenTSLMFlamingo", "OpenTSLMSP"],
help="Model to instantiate",
)
parser.add_argument(
"--dataset",
required=True,
choices=[
"TSQADataset",
"HARCoTQADataset",
"SleepEDFCoTQADataset",
"ECGQACoTQADataset",
"SimulationQADataset",
],
help="Dataset to use",
)
parser.add_argument(
"--device", default="cpu", help="Device to run on (e.g., cuda, cuda:0, cpu)"
)
parser.add_argument(
"--length",
type=int,
default=100,
help="Length of time series for SimulationQADataset (default: 100)",
)
parser.add_argument(
"--num_series",
type=int,
default=1,
help="Number of time series for SimulationQADataset (default: 1)",
)
parser.add_argument(
"--results_csv",
default=os.path.join(REPO_DIR, "memory_use.csv"),
help="Path to CSV file to append results",
)
args = parser.parse_args()
device = get_device(args.device)
# CSV header and file
header = [
"timestamp",
"llm_id",
"device",
"model",
"dataset",
"loss",
"peak_cuda_bytes",
"peak_cuda_gb",
"peak_cuda_reserved_bytes",
"peak_cuda_reserved_gb",
"nvml_peak_bytes",
"nvml_peak_gb",
"status",
"error",
]
ensure_csv(args.results_csv, header)
# Instantiate selected model
if args.model == "OpenTSLMFlamingo":
model = OpenTSLMFlamingo(
device=device,
llm_id=args.llm_id,
cross_attn_every_n_layers=1,
gradient_checkpointing=True,
)
eos = model.get_eos_token()
elif args.model == "OpenTSLMSP":
model = OpenTSLMSP(llm_id=args.llm_id, device=device)
eos = model.get_eos_token()
else:
raise ValueError(f"Unknown model: {args.model}")
# Make absolutely sure parameters are on the requested device
model.to(device)
# Instantiate selected dataset
if args.dataset == "TSQADataset":
dataset = TSQADataset(split="train", EOS_TOKEN=eos)
dataset_name = "TSQA"
elif args.dataset == "HARCoTQADataset":
dataset = HARCoTQADataset(split="train", EOS_TOKEN=eos)
dataset_name = "HAR-CoT"
elif args.dataset == "SleepEDFCoTQADataset":
dataset = SleepEDFCoTQADataset(split="train", EOS_TOKEN=eos)
dataset_name = "SleepEDF-CoT"
elif args.dataset == "ECGQACoTQADataset":
dataset = ECGQACoTQADataset(
split="train", EOS_TOKEN=eos, max_samples=1, preload_processed_data=False
)
dataset_name = "ECG-QA-CoT"
elif args.dataset == "SimulationQADataset":
dataset = SimulationQADataset(
split="train", EOS_TOKEN=eos, length=args.length, num_series=args.num_series
)
dataset_name = f"Simulation-L{args.length}-N{args.num_series}"
else:
raise ValueError(f"Unknown dataset: {args.dataset}")
# Run one iteration and append results
res = run_for_dataset(args.model, model, dataset_name, dataset)
peak_bytes = res["peak_cuda_bytes"]
peak_gb = (
(float(peak_bytes) / (1024.0**3))
if isinstance(peak_bytes, (int, float)) and peak_bytes >= 0
else -1
)
peak_reserved_bytes = res.get("peak_cuda_reserved_bytes", -1)
peak_reserved_gb = (
(float(peak_reserved_bytes) / (1024.0**3))
if isinstance(peak_reserved_bytes, (int, float)) and peak_reserved_bytes >= 0
else -1
)
nvml_peak_bytes = res.get("nvml_peak_bytes", -1)
nvml_peak_gb = (
(float(nvml_peak_bytes) / (1024.0**3))
if isinstance(nvml_peak_bytes, (int, float)) and nvml_peak_bytes >= 0
else -1
)
append_row(
args.results_csv,
[
datetime.utcnow().isoformat(),
args.llm_id,
device,
res["model"],
res["dataset"],
res["loss"],
res["peak_cuda_bytes"],
f"{peak_gb:.4f}"
if isinstance(peak_gb, float) and peak_gb >= 0
else peak_gb,
res.get("peak_cuda_reserved_bytes", -1),
f"{peak_reserved_gb:.4f}"
if isinstance(peak_reserved_gb, float) and peak_reserved_gb >= 0
else peak_reserved_gb,
res.get("nvml_peak_bytes", -1),
f"{nvml_peak_gb:.4f}"
if isinstance(nvml_peak_gb, float) and nvml_peak_gb >= 0
else nvml_peak_gb,
res["status"],
res["error"],
],
)
print(f"Done. Results appended to: {args.results_csv}")
if __name__ == "__main__":
main()