# Copyright 2022 The Plenoptix Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Abstracts for the Pipeline class. """ from __future__ import annotations import typing from abc import abstractmethod from dataclasses import dataclass, field from time import time from typing import Any, Dict, List, Optional, Type, Union, cast import torch import torch.distributed as dist from rich.progress import ( BarColumn, MofNCompleteColumn, Progress, TextColumn, TimeElapsedColumn, ) from torch import nn from torch.nn import Parameter from torch.nn.parallel import DistributedDataParallel as DDP # from nerfactory.configs.base import InstantiateConfig from nerfactory.configs import base as cfg from nerfactory.datamanagers.base import ( DataManager, VanillaDataManager, VanillaDataManagerConfig, ) from nerfactory.models.base import Model, ModelConfig from nerfactory.utils import profiler from nerfactory.utils.callbacks import TrainingCallback, TrainingCallbackAttributes from nerfactory.utils.misc import get_masked_dict def module_wrapper(ddp_or_model: Union[DDP, Model]) -> Model: """ If DDP, then return the .module. Otherwise, return the model. """ if isinstance(ddp_or_model, DDP): return cast(Model, ddp_or_model.module) return ddp_or_model class Pipeline(nn.Module): """The intent of this class is to provide a higher level interface for the Model that will be easy to use for our Trainer class. This class will contain high level functions for the model like getting the loss dictionaries and visualization code. It should have ways to get the next iterations training loss, evaluation loss, and generate whole images for visualization. Each model class should be 1:1 with a pipeline that can act as a standardized interface and hide differences in how each model takes in and outputs data. This class's function is to hide the data manager and model classes from the trainer, worrying about: 1) Fetching data with the data manager 2) Feeding the model the data and fetching the loss Hopefully this provides a higher level interface for the trainer to use, and simplifying the model classes, which each may have different forward() methods and so on. Args: config: configuration to instantiate pipeline device: location to place model and data test_mode: if True, loads test datset. if False, loads train/eval datasets world_size: total number of machines available local_rank: rank of current machine Attributes: self.datamanager: The data manager that will be used self.model: The model that will be used """ # pylint: disable=abstract-method datamanger: DataManager model: Model @property def device(self): """Returns the device that the model is on.""" return module_wrapper(self.model).device @profiler.time_function def get_train_loss_dict(self, step: int): """This function gets your training loss dict. This will be responsible for getting the next batch of data from the DataManager and interfacing with the Model class, feeding the data to the model's forward function. Args: step: current iteration step to update sampler if using DDP (distributed) """ if self.world_size > 1 and step: assert self.datamanager.train_sampler is not None self.datamanager.train_sampler.set_epoch(step) ray_bundle, batch = self.datamanager.next_train(step) model_outputs = self.model(ray_bundle, batch) metrics_dict = self.model.get_metrics_dict(model_outputs, batch) loss_dict = self.model.get_loss_dict(model_outputs, batch, metrics_dict) return model_outputs, loss_dict, metrics_dict @profiler.time_function def get_eval_loss_dict(self, step: int): """This function gets your evaluation loss dict. It needs to get the data from the DataManager and feed it to the model's forward function Args: step: current iteration step """ self.eval() if self.world_size > 1: assert self.datamanager.eval_sampler is not None self.datamanager.eval_sampler.set_epoch(step) ray_bundle, batch = self.datamanager.next_eval(step) model_outputs = self.model(ray_bundle, batch) loss_dict = self.model.get_loss_dict(model_outputs, batch) metrics_dict = self.model.get_metrics_dict(model_outputs, batch) self.train() return model_outputs, loss_dict, metrics_dict @abstractmethod @profiler.time_function def get_eval_image_metrics_and_images(self, step: int): """This function gets your evaluation loss dict. It needs to get the data from the DataManager and feed it to the model's forward function Args: step: current iteration step """ @abstractmethod @profiler.time_function def get_average_eval_image_metrics(self, step: Optional[int] = None): """Iterate over all the images in the eval dataset and get the average.""" def load_pipeline(self, loaded_state: Dict[str, Any]) -> None: """Load the checkpoint from the given path Args: loaded_state: pre-trained model state dict """ def get_training_callbacks( self, training_callback_attributes: TrainingCallbackAttributes ) -> List[TrainingCallback]: """Returns the training callbacks from both the Dataloader and the Model.""" def get_param_groups(self) -> Dict[str, List[Parameter]]: """Get the param groups for the pipeline. Returns: A list of dictionaries containing the pipeline's param groups. """ @dataclass class VanillaPipelineConfig(cfg.InstantiateConfig): """Configuration for pipeline instantiation""" _target: Type = field(default_factory=lambda: VanillaPipeline) """target class to instantiate""" datamanager: VanillaDataManagerConfig = VanillaDataManagerConfig() """specifies the datamanager config""" model: ModelConfig = ModelConfig() """specifies the model config""" class VanillaPipeline(Pipeline): """The pipeline class for the vanilla nerf setup of multiple cameras for one or a few scenes. config: configuration to instantiate pipeline device: location to place model and data test_mode: if True, loads test datset. if False, loads train/eval datasets world_size: total number of machines available local_rank: rank of current machine Attributes: self.datamanager: The data manager that will be used self.model: The model that will be used """ def __init__( self, config: VanillaPipelineConfig, device: str, test_mode: bool = False, world_size: int = 1, local_rank: int = 0, ): super().__init__() self.config = config self.datamanager: VanillaDataManager = config.datamanager.setup( device=device, test_mode=test_mode, world_size=world_size, local_rank=local_rank ) self.datamanager.to(device) # TODO(ethan): get rid of scene_bounds from the model assert self.datamanager.train_input_dataset is not None, "Missing input dataset" self.model = config.model.setup( scene_bounds=self.datamanager.train_input_dataset.dataset_inputs.scene_bounds, num_train_data=len(self.datamanager.train_input_dataset), ) self.model.to(device) self.world_size = world_size if world_size > 1: self.model = typing.cast( Model, typing.cast(Model, DDP(self.model, device_ids=[local_rank], find_unused_parameters=True)) ) dist.barrier(device_ids=[local_rank]) @property def device(self): """Returns the device that the model is on.""" return module_wrapper(self.model).device @profiler.time_function def get_train_loss_dict(self, step: int): """This function gets your training loss dict. This will be responsible for getting the next batch of data from the DataManager and interfacing with the Model class, feeding the data to the model's forward function. Args: step: current iteration step to update sampler if using DDP (distributed) """ ray_bundle, batch = self.datamanager.next_train(step) model_outputs = self.model(ray_bundle) if "valid_mask" in model_outputs: valid_mask = model_outputs["valid_mask"] batch = get_masked_dict(batch, valid_mask) metrics_dict = self.model.get_metrics_dict(model_outputs, batch) loss_dict = self.model.get_loss_dict(model_outputs, batch, metrics_dict) return model_outputs, loss_dict, metrics_dict def forward(self): """Blank forward method This is an nn.Module, and so requires a forward() method normally, although in our case we do not need a forward() method""" raise NotImplementedError @profiler.time_function def get_eval_loss_dict(self, step: int): """This function gets your evaluation loss dict. It needs to get the data from the DataManager and feed it to the model's forward function Args: step: current iteration step """ self.eval() ray_bundle, batch = self.datamanager.next_eval(step) model_outputs = self.model(ray_bundle) if "valid_mask" in model_outputs: valid_mask = model_outputs["valid_mask"] batch = get_masked_dict(batch, valid_mask) loss_dict = self.model.get_loss_dict(model_outputs, batch) metrics_dict = self.model.get_metrics_dict(model_outputs, batch) self.train() return model_outputs, loss_dict, metrics_dict @profiler.time_function def get_eval_image_metrics_and_images(self, step: int): """This function gets your evaluation loss dict. It needs to get the data from the DataManager and feed it to the model's forward function Args: step: current iteration step """ self.eval() image_idx, camera_ray_bundle, batch = self.datamanager.next_eval_image(step) outputs = module_wrapper(self.model).get_outputs_for_camera_ray_bundle(camera_ray_bundle) metrics_dict, images_dict = module_wrapper(self.model).get_image_metrics_and_images(outputs, batch) assert "image_idx" not in metrics_dict metrics_dict["image_idx"] = image_idx assert "num_rays" not in metrics_dict metrics_dict["num_rays"] = len(camera_ray_bundle) self.train() return metrics_dict, images_dict @profiler.time_function def get_average_eval_image_metrics(self, step: Optional[int] = None): """Iterate over all the images in the eval dataset and get the average. Returns: metrics_dict: dictionary of metrics """ self.eval() metrics_dict_list = [] num_images = len(self.datamanager.fixed_indices_eval_dataloader) with Progress( TextColumn("[progress.description]{task.description}"), BarColumn(), TimeElapsedColumn(), MofNCompleteColumn(), transient=True, ) as progress: task = progress.add_task("[green]Evaluating all eval images...", total=num_images) for camera_ray_bundle, batch in self.datamanager.fixed_indices_eval_dataloader: # time this the following line inner_start = time() height, width = camera_ray_bundle.shape num_rays = height * width outputs = module_wrapper(self.model).get_outputs_for_camera_ray_bundle(camera_ray_bundle) metrics_dict, _ = module_wrapper(self.model).get_image_metrics_and_images(outputs, batch) assert "num_rays_per_sec" not in metrics_dict metrics_dict["num_rays_per_sec"] = num_rays / (time() - inner_start) fps_str = f"fps_at_{height}x{width}" assert fps_str not in metrics_dict metrics_dict[fps_str] = metrics_dict["num_rays_per_sec"] / (height * width) metrics_dict_list.append(metrics_dict) progress.advance(task) # average the metrics list metrics_dict = {} for key in metrics_dict_list[0].keys(): metrics_dict[key] = float( torch.mean(torch.tensor([metrics_dict[key] for metrics_dict in metrics_dict_list])) ) self.train() return metrics_dict def load_pipeline(self, loaded_state: Dict[str, Any]) -> None: """Load the checkpoint from the given path Args: loaded_state: pre-trained model state dict """ state = {key.replace("module.", ""): value for key, value in loaded_state.items()} self.load_state_dict(state) # type: ignore def get_training_callbacks( self, training_callback_attributes: TrainingCallbackAttributes ) -> List[TrainingCallback]: """Returns the training callbacks from both the Dataloader and the Model.""" datamanager_callbacks = self.datamanager.get_training_callbacks(training_callback_attributes) model_callbacks = module_wrapper(self.model).get_training_callbacks(training_callback_attributes) callbacks = datamanager_callbacks + model_callbacks return callbacks def get_param_groups(self) -> Dict[str, List[Parameter]]: """Get the param groups for the pipeline. Returns: A list of dictionaries containing the pipeline's param groups. """ datamanager_params = self.datamanager.get_param_groups() model_params = module_wrapper(self.model).get_param_groups() # TODO(ethan): assert that key names don't overlap return {**datamanager_params, **model_params}