mirror of
https://github.com/nerfstudio-project/nerfstudio.git
synced 2026-11-10 18:17:31 +08:00
Making TrainerConfig an InstantiateConfig so it can be swapped in and out with different methods. Also made the TrainerConfig a subclass of ExperimentConfig (which was changed from a printableconfig to an instantiateconfig), since we were already instantiating the Trainer class with the ExperimentConfig Removed the "config" field from the TrainerCallbackAttributes struct to eliminate circular dependencies when types are checked. Minor scripts and docs changes to accommodate the update to the config struct, meaning the flags that were associated with the TrainerConfig were moved up to the top level of the config.
257 lines
8.6 KiB
Python
Executable File
257 lines
8.6 KiB
Python
Executable File
#!/usr/bin/env python
|
|
"""Train a radiance field with nerfstudio.
|
|
For real captures, we recommend using the [bright_yellow]nerfacto[/bright_yellow] model.
|
|
|
|
Nerfstudio allows for customizing your training and eval configs from the CLI in a powerful way, but there are some
|
|
things to understand.
|
|
|
|
The most demonstrative and helpful example of the CLI structure is the difference in output between the following
|
|
commands:
|
|
|
|
ns-train -h
|
|
ns-train nerfacto -h nerfstudio-data
|
|
ns-train nerfacto nerfstudio-data -h
|
|
|
|
In each of these examples, the -h applies to the previous subcommand (ns-train, nerfacto, and nerfstudio-data).
|
|
|
|
In the first example, we get the help menu for the ns-train script.
|
|
In the second example, we get the help menu for the nerfacto model.
|
|
In the third example, we get the help menu for the nerfstudio-data dataparser.
|
|
|
|
With our scripts, your arguments will apply to the preceding subcommand in your command, and thus where you put your
|
|
arguments matters! Any optional arguments you discover from running
|
|
|
|
ns-train nerfacto -h nerfstudio-data
|
|
|
|
need to come directly after the nerfacto subcommand, since these optional arguments only belong to the nerfacto
|
|
subcommand:
|
|
|
|
ns-train nerfacto {nerfacto optional args} nerfstudio-data
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
import socket
|
|
import traceback
|
|
from datetime import timedelta
|
|
from typing import Any, Callable, Optional
|
|
|
|
import numpy as np
|
|
import torch
|
|
import torch.distributed as dist
|
|
import torch.multiprocessing as mp
|
|
import tyro
|
|
import yaml
|
|
from rich.console import Console
|
|
|
|
from nerfstudio.configs.config_utils import convert_markup_to_ansi
|
|
from nerfstudio.configs.method_configs import AnnotatedBaseConfigUnion
|
|
from nerfstudio.engine.trainer import TrainerConfig
|
|
from nerfstudio.utils import comms, profiler
|
|
|
|
CONSOLE = Console(width=120)
|
|
DEFAULT_TIMEOUT = timedelta(minutes=30)
|
|
|
|
# speedup for when input size to model doesn't change (much)
|
|
torch.backends.cudnn.benchmark = True # type: ignore
|
|
|
|
|
|
def _find_free_port() -> str:
|
|
"""Finds a free port."""
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.bind(("", 0))
|
|
port = sock.getsockname()[1]
|
|
sock.close()
|
|
return port
|
|
|
|
|
|
def _set_random_seed(seed) -> None:
|
|
"""Set randomness seed in torch and numpy"""
|
|
random.seed(seed)
|
|
np.random.seed(seed)
|
|
torch.manual_seed(seed)
|
|
|
|
|
|
def train_loop(local_rank: int, world_size: int, config: TrainerConfig, global_rank: int = 0):
|
|
"""Main training function that sets up and runs the trainer per process
|
|
|
|
Args:
|
|
local_rank: current rank of process
|
|
world_size: total number of gpus available
|
|
config: config file specifying training regimen
|
|
"""
|
|
_set_random_seed(config.machine.seed + global_rank)
|
|
trainer = config.setup(local_rank=local_rank, world_size=world_size)
|
|
trainer.setup()
|
|
trainer.train()
|
|
|
|
|
|
def _distributed_worker(
|
|
local_rank: int,
|
|
main_func: Callable,
|
|
world_size: int,
|
|
num_gpus_per_machine: int,
|
|
machine_rank: int,
|
|
dist_url: str,
|
|
config: TrainerConfig,
|
|
timeout: timedelta = DEFAULT_TIMEOUT,
|
|
) -> Any:
|
|
"""Spawned distributed worker that handles the initialization of process group and handles the
|
|
training process on multiple processes.
|
|
|
|
Args:
|
|
local_rank: Current rank of process.
|
|
main_func: Function that will be called by the distributed workers.
|
|
world_size: Total number of gpus available.
|
|
num_gpus_per_machine: Number of GPUs per machine.
|
|
machine_rank: Rank of this machine.
|
|
dist_url: URL to connect to for distributed jobs, including protocol
|
|
E.g., "tcp://127.0.0.1:8686".
|
|
It can be set to "auto" to automatically select a free port on localhost.
|
|
config: TrainerConfig specifying training regimen.
|
|
timeout: Timeout of the distributed workers.
|
|
|
|
Raises:
|
|
e: Exception in initializing the process group
|
|
|
|
Returns:
|
|
Any: TODO: determine the return type
|
|
"""
|
|
assert torch.cuda.is_available(), "cuda is not available. Please check your installation."
|
|
global_rank = machine_rank * num_gpus_per_machine + local_rank
|
|
|
|
dist.init_process_group(
|
|
backend="nccl",
|
|
init_method=dist_url,
|
|
world_size=world_size,
|
|
rank=global_rank,
|
|
timeout=timeout,
|
|
)
|
|
assert comms.LOCAL_PROCESS_GROUP is None
|
|
num_machines = world_size // num_gpus_per_machine
|
|
for i in range(num_machines):
|
|
ranks_on_i = list(range(i * num_gpus_per_machine, (i + 1) * num_gpus_per_machine))
|
|
pg = dist.new_group(ranks_on_i)
|
|
if i == machine_rank:
|
|
comms.LOCAL_PROCESS_GROUP = pg
|
|
|
|
assert num_gpus_per_machine <= torch.cuda.device_count()
|
|
output = main_func(local_rank, world_size, config, global_rank)
|
|
comms.synchronize()
|
|
dist.destroy_process_group()
|
|
return output
|
|
|
|
|
|
def launch(
|
|
main_func: Callable,
|
|
num_gpus_per_machine: int,
|
|
num_machines: int = 1,
|
|
machine_rank: int = 0,
|
|
dist_url: str = "auto",
|
|
config: Optional[TrainerConfig] = None,
|
|
timeout: timedelta = DEFAULT_TIMEOUT,
|
|
) -> None:
|
|
"""Function that spawns muliple processes to call on main_func
|
|
|
|
Args:
|
|
main_func (Callable): function that will be called by the distributed workers
|
|
num_gpus_per_machine (int): number of GPUs per machine
|
|
num_machines (int, optional): total number of machines
|
|
machine_rank (int, optional): rank of this machine.
|
|
dist_url (str, optional): url to connect to for distributed jobs.
|
|
config (TrainerConfig, optional): config file specifying training regimen.
|
|
timeout (timedelta, optional): timeout of the distributed workers.
|
|
"""
|
|
assert config is not None
|
|
world_size = num_machines * num_gpus_per_machine
|
|
if world_size <= 1:
|
|
# world_size=0 uses one CPU in one process.
|
|
# world_size=1 uses one GPU in one process.
|
|
try:
|
|
main_func(local_rank=0, world_size=world_size, config=config)
|
|
except KeyboardInterrupt:
|
|
# print the stack trace
|
|
CONSOLE.print(traceback.format_exc())
|
|
finally:
|
|
profiler.flush_profiler(config.logging)
|
|
elif world_size > 1:
|
|
# Using multiple gpus with multiple processes.
|
|
if dist_url == "auto":
|
|
assert num_machines == 1, "dist_url=auto is not supported for multi-machine jobs."
|
|
port = _find_free_port()
|
|
dist_url = f"tcp://127.0.0.1:{port}"
|
|
if num_machines > 1 and dist_url.startswith("file://"):
|
|
CONSOLE.log("file:// is not a reliable init_method in multi-machine jobs. Prefer tcp://")
|
|
|
|
process_context = mp.spawn(
|
|
_distributed_worker,
|
|
nprocs=num_gpus_per_machine,
|
|
join=False,
|
|
args=(
|
|
main_func,
|
|
world_size,
|
|
num_gpus_per_machine,
|
|
machine_rank,
|
|
dist_url,
|
|
config,
|
|
timeout,
|
|
),
|
|
)
|
|
# process_context won't be None because join=False, so it's okay to assert this
|
|
# for Pylance reasons
|
|
assert process_context is not None
|
|
try:
|
|
process_context.join()
|
|
except KeyboardInterrupt:
|
|
for i, process in enumerate(process_context.processes):
|
|
if process.is_alive():
|
|
CONSOLE.log(f"Terminating process {i}...")
|
|
process.terminate()
|
|
process.join()
|
|
CONSOLE.log(f"Process {i} finished.")
|
|
finally:
|
|
profiler.flush_profiler(config.logging)
|
|
|
|
|
|
def main(config: TrainerConfig) -> None:
|
|
"""Main function."""
|
|
|
|
config.set_timestamp()
|
|
if config.data:
|
|
CONSOLE.log("Using --data alias for --data.pipeline.datamanager.dataparser.data")
|
|
config.pipeline.datamanager.dataparser.data = config.data
|
|
|
|
if config.load_config:
|
|
CONSOLE.log(f"Loading pre-set config from: {config.load_config}")
|
|
config = yaml.load(config.load_config.read_text(), Loader=yaml.Loader)
|
|
|
|
# print and save config
|
|
config.print_to_terminal()
|
|
config.save_config()
|
|
|
|
launch(
|
|
main_func=train_loop,
|
|
num_gpus_per_machine=config.machine.num_gpus,
|
|
num_machines=config.machine.num_machines,
|
|
machine_rank=config.machine.machine_rank,
|
|
dist_url=config.machine.dist_url,
|
|
config=config,
|
|
)
|
|
|
|
|
|
def entrypoint():
|
|
"""Entrypoint for use with pyproject scripts."""
|
|
# Choose a base configuration and override values.
|
|
tyro.extras.set_accent_color("bright_yellow")
|
|
main(
|
|
tyro.cli(
|
|
AnnotatedBaseConfigUnion,
|
|
description=convert_markup_to_ansi(__doc__),
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
entrypoint()
|