| |
| |
| |
| |
|
|
| from functools import partial |
| import logging |
| import numpy as np |
| import os |
| import random |
|
|
| import torch |
| import torch.distributed as dist |
|
|
| from dinov3.data import DatasetWithEnumeratedTargets, SamplerType, make_data_loader, make_dataset |
| import dinov3.distributed as distributed |
| from dinov3.eval.segmentation.eval import evaluate_segmentation_model |
| from dinov3.eval.segmentation.loss import MultiSegmentationLoss |
| from dinov3.eval.segmentation.metrics import SEGMENTATION_METRICS |
| from dinov3.eval.segmentation.models import build_segmentation_decoder |
| from dinov3.eval.segmentation.schedulers import build_scheduler |
| from dinov3.eval.segmentation.transforms import make_segmentation_eval_transforms, make_segmentation_train_transforms |
| from dinov3.logging import MetricLogger, SmoothedValue |
|
|
| logger = logging.getLogger("dinov3") |
|
|
|
|
| class InfiniteDataloader: |
| def __init__(self, dataloader: torch.utils.data.DataLoader): |
| self.dataloader = dataloader |
| self.data_iterator = iter(dataloader) |
| self.sampler = dataloader.sampler |
| if not hasattr(self.sampler, "epoch"): |
| self.sampler.epoch = 0 |
|
|
| def __iter__(self): |
| return self |
|
|
| def __len__(self) -> int: |
| return len(self.dataloader) |
|
|
| def __next__(self): |
| try: |
| data = next(self.data_iterator) |
| except StopIteration: |
| self.sampler.epoch += 1 |
| self.data_iterator = iter(self.dataloader) |
| data = next(self.data_iterator) |
| return data |
|
|
|
|
| def worker_init_fn(worker_id, num_workers, rank, seed): |
| """Worker init func for dataloader. |
| The seed of each worker equals to num_worker * rank + worker_id + user_seed |
| Args: |
| worker_id (int): Worker id. |
| num_workers (int): Number of workers. |
| rank (int): The rank of current process. |
| seed (int): The random seed to use. |
| """ |
| worker_seed = num_workers * rank + worker_id + seed |
| np.random.seed(worker_seed) |
| random.seed(worker_seed) |
| torch.manual_seed(worker_seed) |
|
|
|
|
| def validate( |
| segmentation_model: torch.nn.Module, |
| val_dataloader, |
| device, |
| autocast_dtype, |
| eval_res, |
| eval_stride, |
| decoder_head_type, |
| num_classes, |
| global_step, |
| metric_to_save, |
| current_best_metric_to_save_value, |
| ): |
| new_metric_values_dict = evaluate_segmentation_model( |
| segmentation_model, |
| val_dataloader, |
| device, |
| eval_res, |
| eval_stride, |
| decoder_head_type, |
| num_classes, |
| autocast_dtype, |
| ) |
| logger.info(f"Step {global_step}: {new_metric_values_dict}") |
| |
| |
| segmentation_model.module.segmentation_model[1].train() |
| is_better = False |
| if new_metric_values_dict[metric_to_save] > current_best_metric_to_save_value: |
| is_better = True |
| return is_better, new_metric_values_dict |
|
|
|
|
| def train_step( |
| segmentation_model: torch.nn.Module, |
| batch, |
| device, |
| scaler, |
| optimizer, |
| optimizer_gradient_clip, |
| scheduler, |
| criterion, |
| model_dtype, |
| global_step, |
| ): |
| |
| batch_img, (_, gt) = batch |
| batch_img = batch_img.to(device) |
| gt = gt.to(device) |
| optimizer.zero_grad(set_to_none=True) |
|
|
| |
| with torch.autocast("cuda", dtype=model_dtype, enabled=True if model_dtype is not None else False): |
| pred = segmentation_model(batch_img) |
| gt = torch.squeeze(gt).long() |
|
|
| |
| if gt.shape[-2:] != pred.shape[-2:]: |
| pred = torch.nn.functional.interpolate(input=pred, size=gt.shape[-2:], mode="bilinear", align_corners=False) |
| loss = criterion(pred, gt) |
|
|
| |
| if scaler is not None: |
| scaler.scale(loss).backward() |
| scaler.unscale_(optimizer) |
| torch.nn.utils.clip_grad_norm_(segmentation_model.module.parameters(), optimizer_gradient_clip) |
| scaler.step(optimizer) |
| scaler.update() |
| else: |
| loss.backward() |
| torch.nn.utils.clip_grad_norm_(segmentation_model.module.parameters(), optimizer_gradient_clip) |
| optimizer.step() |
|
|
| if global_step > 0: |
| scheduler.step() |
|
|
| return loss |
|
|
|
|
| def train_segmentation( |
| backbone, |
| config, |
| ): |
| assert config.decoder_head.type == "linear", "Only linear head is supported for training" |
| |
| logger.info("Initializing the segmentation model") |
| segmentation_model = build_segmentation_decoder( |
| backbone, |
| config.decoder_head.backbone_out_layers, |
| "linear", |
| num_classes=config.decoder_head.num_classes, |
| autocast_dtype=config.model_dtype.autocast_dtype, |
| ) |
| global_device = distributed.get_rank() |
| local_device = torch.cuda.current_device() |
| segmentation_model = torch.nn.parallel.DistributedDataParallel( |
| segmentation_model.to(local_device), device_ids=[local_device] |
| ) |
| model_parameters = filter(lambda p: p.requires_grad, segmentation_model.parameters()) |
| logger.info(f"Number of trainable parameters: {sum(p.numel() for p in model_parameters)}") |
|
|
| |
| train_transforms = make_segmentation_train_transforms( |
| img_size=config.transforms.train.img_size, |
| random_img_size_ratio_range=config.transforms.train.random_img_size_ratio_range, |
| crop_size=config.transforms.train.crop_size, |
| flip_prob=config.transforms.train.flip_prob, |
| reduce_zero_label=config.eval.reduce_zero_label, |
| ) |
| val_transforms = make_segmentation_eval_transforms( |
| img_size=config.transforms.eval.img_size, |
| inference_mode=config.eval.mode, |
| ) |
|
|
| train_dataset = DatasetWithEnumeratedTargets( |
| make_dataset( |
| dataset_str=f"{config.datasets.train}:root={config.datasets.root}", |
| transforms=train_transforms, |
| ) |
| ) |
| train_sampler_type = None |
| if distributed.is_enabled(): |
| train_sampler_type = SamplerType.DISTRIBUTED |
| init_fn = partial( |
| worker_init_fn, num_workers=config.num_workers, rank=global_device, seed=config.seed + global_device |
| ) |
| train_dataloader = InfiniteDataloader( |
| make_data_loader( |
| dataset=train_dataset, |
| batch_size=config.bs, |
| num_workers=config.num_workers, |
| sampler_type=train_sampler_type, |
| shuffle=True, |
| persistent_workers=False, |
| worker_init_fn=init_fn, |
| ) |
| ) |
|
|
| val_dataset = DatasetWithEnumeratedTargets( |
| make_dataset( |
| dataset_str=f"{config.datasets.val}:root={config.datasets.root}", |
| transforms=val_transforms, |
| ) |
| ) |
| val_sampler_type = None |
| if distributed.is_enabled(): |
| val_sampler_type = SamplerType.DISTRIBUTED |
| val_dataloader = make_data_loader( |
| dataset=val_dataset, |
| batch_size=1, |
| num_workers=config.num_workers, |
| sampler_type=val_sampler_type, |
| drop_last=False, |
| shuffle=False, |
| persistent_workers=True, |
| ) |
|
|
| |
| scaler = None |
| if config.model_dtype.autocast_dtype is not None: |
| scaler = torch.amp.GradScaler("cuda") |
|
|
| optimizer = torch.optim.AdamW( |
| [ |
| { |
| "params": filter(lambda p: p.requires_grad, segmentation_model.parameters()), |
| "lr": config.optimizer.lr, |
| "betas": (config.optimizer.beta1, config.optimizer.beta2), |
| "weight_decay": config.optimizer.weight_decay, |
| } |
| ] |
| ) |
| scheduler = build_scheduler( |
| config.scheduler.type, |
| optimizer=optimizer, |
| lr=config.optimizer.lr, |
| total_iter=config.scheduler.total_iter, |
| constructor_kwargs=config.scheduler.constructor_kwargs, |
| ) |
| criterion = MultiSegmentationLoss( |
| diceloss_weight=config.train.diceloss_weight, celoss_weight=config.train.celoss_weight |
| ) |
| total_iter = config.scheduler.total_iter |
| global_step = 0 |
| global_best_metric_values = {metric: 0.0 for metric in SEGMENTATION_METRICS} |
|
|
| |
| metric_logger = MetricLogger(delimiter=" ") |
| metric_logger.add_meter("loss", SmoothedValue(window_size=4, fmt="{value:.3f}")) |
| for batch in metric_logger.log_every( |
| train_dataloader, |
| 50, |
| header="Train: ", |
| start_iteration=global_step, |
| n_iterations=total_iter, |
| ): |
| if global_step >= total_iter: |
| break |
| loss = train_step( |
| segmentation_model, |
| batch, |
| local_device, |
| scaler, |
| optimizer, |
| config.optimizer.gradient_clip, |
| scheduler, |
| criterion, |
| config.model_dtype.autocast_dtype, |
| global_step, |
| ) |
| global_step += 1 |
| metric_logger.update(loss=loss) |
| if global_step % config.eval.eval_interval == 0: |
| dist.barrier() |
| is_better, best_metric_values_dict = validate( |
| segmentation_model, |
| val_dataloader, |
| local_device, |
| config.model_dtype.autocast_dtype, |
| config.eval.crop_size, |
| config.eval.stride, |
| config.decoder_head.type, |
| config.decoder_head.num_classes, |
| global_step, |
| config.metric_to_save, |
| global_best_metric_values[config.metric_to_save], |
| ) |
| if is_better: |
| logger.info(f"New best metrics at Step {global_step}: {best_metric_values_dict}") |
| global_best_metric_values = best_metric_values_dict |
|
|
| |
| if total_iter % config.eval.eval_interval: |
| is_better, best_metric_values_dict = validate( |
| segmentation_model, |
| val_dataloader, |
| local_device, |
| config.model_dtype.autocast_dtype, |
| config.eval.crop_size, |
| config.eval.stride, |
| config.decoder_head.type, |
| config.decoder_head.num_classes, |
| global_step, |
| config.metric_to_save, |
| global_best_metric_values[config.metric_to_save], |
| ) |
| if is_better: |
| logger.info(f"New best metrics at Step {global_step}: {best_metric_values_dict}") |
| global_best_metric_values = best_metric_values_dict |
| logger.info("Training is done!") |
| |
| |
| torch.save( |
| { |
| "model": {k: v for k, v in segmentation_model.module.state_dict().items() if "segmentation_model.1" in k}, |
| "optimizer": optimizer.state_dict(), |
| }, |
| os.path.join(config.output_dir, "model_final.pth"), |
| ) |
| logger.info(f"Final best metrics: {global_best_metric_values}") |
| return global_best_metric_values |
|
|