FuXi / scripts /train_long.py
OneScience's picture
Upload folder using huggingface_hub
2862bae verified
Raw
History Blame Contribute Delete
12.9 kB
import sys
from pathlib import Path
# 获取项目根目录(train.py上级的上级)
root_path = Path(__file__).parent.parent
sys.path.append(str(root_path))
import torch
import os
import numpy as np
import torch.distributed as dist
import logging
import time
from tqdm import tqdm
from torch.nn.parallel import DistributedDataParallel
from model.fuxi import Fuxi
from scripts.data_loader import ERA5Datapipe
from onescience.utils.YParams import YParams
from onescience.metrics.climate.loss import LatitudeWeightedLoss
from onescience.memory.checkpoint import replace_function
from apex import optimizers
def main():
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger()
## Model config init
config_file_path = os.path.join(current_path, "conf/config.yaml")
cfg = YParams(config_file_path, "model")
## Distributed config init
cfg.world_size = 1
if "WORLD_SIZE" in os.environ:
cfg.world_size = int(os.environ["WORLD_SIZE"])
world_rank = 0
local_rank = 0
if cfg.world_size > 1:
dist.init_process_group(backend="nccl", init_method="env://")
local_rank = int(os.environ["LOCAL_RANK"])
world_rank = dist.get_rank()
if not os.path.exists(f"{cfg.checkpoint_dir}/model_medium_bak.pth"):
if world_rank == 0:
print(f'❌❌The Fuxi medium model must be trained before this model.')
exit()
## DataLoader init
cfg_data = YParams(config_file_path, "datapipe")
datapipe = ERA5Datapipe(
dataset_dir=cfg_data.dataset.data_dir,
used_variables=cfg_data.dataset.channels,
used_years=cfg_data.dataset.train_time,
pattern='long',
distributed=dist.is_initialized(),
output_steps=2,
input_steps=2,
batch_size=cfg_data.dataloader.batch_size,
num_workers=cfg_data.dataloader.num_workers
)
train_dataloader, train_sampler = datapipe.get_dataloader("train")
datapipe = ERA5Datapipe(
dataset_dir=cfg_data.dataset.data_dir,
used_variables=cfg_data.dataset.channels,
used_years=cfg_data.dataset.val_time,
pattern='long',
distributed=dist.is_initialized(),
output_steps=2,
input_steps=2,
batch_size=cfg_data.dataloader.batch_size,
num_workers=cfg_data.dataloader.num_workers
)
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
## Model init
model = Fuxi(img_size=cfg_data.dataset.img_size,
patch_size=cfg.patch_size,
in_chans=len(cfg_data.dataset.channels),
out_chans=len(cfg_data.dataset.channels),
embed_dim=cfg.embed_dim,
num_groups=cfg.num_groups,
num_heads=cfg.num_heads,
window_size=cfg.window_size
).to(local_rank)
optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.train_lr)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode="min")
loss_obj = LatitudeWeightedLoss(loss_type="l1", normalize=True).to(local_rank)
## Train process init
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
train_loss_file = f"{cfg.checkpoint_dir}/tr_long_loss.npy"
valid_loss_file = f"{cfg.checkpoint_dir}/va_long_loss.npy"
best_valid_loss = 1.0e6
best_loss_epoch = 0
train_losses = np.empty((0,), dtype=np.float32)
valid_losses = np.empty((0,), dtype=np.float32)
current_epoch = 0
## Get model params count
if cfg.world_size == 1:
total_params = sum(p.numel() for p in model.parameters())
print("\n\n")
print("-" * 50)
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
print("-" * 50, "\n")
## Load model weight if there exist well-trained model
if not os.path.exists(f"{cfg.checkpoint_dir}/model_medium_bak.pth"):
print('⚠️ ⚠️ Please train to get medium model first...')
exit()
if os.path.exists(f"{cfg.checkpoint_dir}/model_long_bak.pth"):
if world_rank == 0:
print("\n\n")
print("-" * 50)
print(f"✅ There has a long-pattern model weight, load and continue training...")
print(f'If you want to finetune a new model, ensure there is no model_short_bak.pth file in {cfg.checkpoint_dir}')
print("-" * 50, "\n")
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_long_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
model.load_state_dict(ckpt["model_state_dict"])
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
best_valid_loss = ckpt["best_valid_loss"]
best_loss_epoch = ckpt["best_loss_epoch"]
current_epoch = ckpt["current_epoch"]
train_losses = np.load(f"{cfg.checkpoint_dir}/tr_long_loss.npy")
valid_losses = np.load(f"{cfg.checkpoint_dir}/va_long_loss.npy")
else:
if world_rank == 0:
print("\n\n")
print("-" * 50)
print(f"✅ Load medium model and continue to finetune...")
print("-" * 50, "\n")
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_medium_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
model.load_state_dict(ckpt["model_state_dict"])
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
## Distributed model
if cfg.world_size > 1:
model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank, find_unused_parameters=True)
world_rank == 0 and logger.info(f"start training ...")
for epoch in range(current_epoch, cfg.finetune_step):
if epoch > cfg.step_change_freq:
num_rollout_steps = epoch // cfg.step_change_freq + 2
if num_rollout_steps > 12: # Paper: 2~12 curriculum training schedule, then skip to 20.
num_rollout_steps = cfg.long_num_steps - cfg.medium_num_steps
if epoch % cfg.step_change_freq == 0 and world_rank == 0:
logger.info(f"⚠️ ⚠️ Switching to {num_rollout_steps}-step rollout!")
datapipe = ERA5Datapipe(
dataset_dir=cfg_data.dataset.data_dir,
used_variables=cfg_data.dataset.channels,
used_years=cfg_data.dataset.train_time,
pattern='long',
distributed=dist.is_initialized(),
output_steps=num_rollout_steps,
input_steps=2,
batch_size=cfg_data.dataloader.batch_size,
num_workers=cfg_data.dataloader.num_workers
)
train_dataloader, train_sampler = datapipe.get_dataloader("train")
datapipe = ERA5Datapipe(
dataset_dir=cfg_data.dataset.data_dir,
used_variables=cfg_data.dataset.channels,
used_years=cfg_data.dataset.val_time,
pattern='long',
distributed=dist.is_initialized(),
output_steps=num_rollout_steps,
input_steps=2,
batch_size=cfg_data.dataloader.batch_size,
num_workers=cfg_data.dataloader.num_workers
)
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
if dist.is_initialized():
train_sampler.set_epoch(epoch)
val_sampler.set_epoch(epoch)
model.train()
train_loss = 0
start_time = time.time()
for j, data in enumerate(train_dataloader):
invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
outvar = data[1].to(local_rank, dtype=torch.float32)
for t in range(outvar.shape[1]):
if t < outvar.shape[1] - 1:
with torch.no_grad():
outvar_pred = model(invar)
# B, 70, 2, 721, 1440
invar[:, :, 0] = invar[:, :, -1]
invar[:, :, -1] = outvar_pred.detach()
else:
with replace_function(model, ["cube_embedding", "u_transformer"], cfg.world_size > 1):
outvar_pred = model(invar)
loss = loss_obj(outvar_pred, outvar[:, t])
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
if world_rank == 0:
logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
f'loss:{train_loss / (j+1): .04f}')
train_loss /= len(train_dataloader)
model.eval()
valid_loss = 0
with torch.no_grad():
start_time = time.time()
for j, data in enumerate(val_dataloader):
invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
outvar = data[1].to(local_rank, dtype=torch.float32)
for t in range(outvar.shape[1]):
outvar_pred = model(invar)
# B, 70, 2, 721, 1440
invar[:, :, 0] = invar[:, :, -1]
invar[:, :, -1] = outvar_pred.detach()
loss = loss_obj(outvar_pred, outvar[:, -1])
if cfg.world_size > 1:
loss_tensor = loss.detach().to(local_rank)
dist.all_reduce(loss_tensor)
loss = loss_tensor.item() / cfg.world_size
valid_loss += loss
else:
valid_loss += loss.item()
if world_rank == 0:
logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
f'loss:{valid_loss / (j+1): .04f}')
valid_loss /= len(val_dataloader)
is_save_ckp = False
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
best_loss_epoch = epoch
world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir, epoch)
is_save_ckp = True
scheduler.step(valid_loss)
if world_rank == 0:
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
f"Train Loss: {train_loss:.4f}, "
f"Valid Loss: {valid_loss:.4f}, "
f"Best loss at Epoch: {best_loss_epoch + 1}"
+ (", saving checkpoint" if is_save_ckp else "")
)
train_losses = np.append(train_losses, train_loss)
valid_losses = np.append(valid_losses, valid_loss)
np.save(train_loss_file, train_losses)
np.save(valid_loss_file, valid_losses)
if epoch - best_loss_epoch > cfg.patience:
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
exit()
def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path, epoch):
model_to_save = model.module if hasattr(model, "module") else model
state = {"model_state_dict": model_to_save.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"scheduler_state_dict": scheduler.state_dict(),
"best_valid_loss": best_valid_loss,
"best_loss_epoch": best_loss_epoch,
"current_epoch": epoch
}
torch.save(state, f"{model_path}/model_long.pth")
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
os.system(f"mv {model_path}/model_long.pth {model_path}/model_long_bak.pth")
if __name__ == "__main__":
current_path = os.getcwd()
sys.path.append(current_path)
main()