File size: 8,520 Bytes
1f88cea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import datetime
import os
import warnings
from pathlib import Path
import time
from typing import Optional
import pytorch_lightning as pl
import torch
from omegaconf import OmegaConf, listconfig
from pytorch_lightning import LightningModule
from pytorch_lightning.callbacks.model_checkpoint import ModelCheckpoint
from pytorch_lightning.loggers import WandbLogger
from pytorch_lightning.strategies import DDPStrategy

from boltzgen.task.task import Task
from boltzgen.task.train.data import DataConfig, TrainingDataModule


class Training(Task):
    """Training configuration."""

    def __init__(
        self,
        data: DataConfig,
        model: LightningModule,
        output: str,
        name: str,
        trainer: Optional[dict] = None,
        resume: Optional[str] = None,
        pretrained: Optional[str] = None,
        wandb: Optional[dict] = None,
        disable_checkpoint: bool = False,
        slurm: bool = False,
        matmul_precision: Optional[str] = None,
        find_unused_parameters: Optional[bool] = False,
        ddp_timeout_seconds: Optional[int] = 6400,
        save_every_n_train_steps: Optional[int] = None,
        save_top_k: Optional[int] = 1,
        validation_only: bool = False,
        debug: bool = False,
        strict_loading: bool = True,
        metric_mode: str = "max",
    ) -> None:
        """Initialize training configuration.

        Parameters
        ----------
        data : DataConfig
            The data configuration.
        model : LightningModule
            The model.
        output : str
            The output directory.
        name : str
            The output subdirectory and wandb run name.
        trainer : Optional[dict], optional
            The trainer configuration, by default None.
        resume : Optional[str], optional
            The resume checkpoint, by default None
        pretrained : Optional[str], optional
            The pretrained model, by default None
        wandb : Optional[dict], optional
            The wandb configuration, by default None
        disable_checkpoint : bool, optional
            Disable checkpoint, by default False

        """
        if not isinstance(data, DataConfig):
            data = DataConfig(**data)

        self.data = data
        self.model = model
        self.output = output
        self.name = name
        self.trainer = trainer
        self.resume = resume
        self.pretrained = pretrained
        self.wandb = wandb
        self.disable_checkpoint = disable_checkpoint
        self.slurm = slurm
        self.matmul_precision = matmul_precision
        self.find_unused_parameters = find_unused_parameters
        self.ddp_timeout_seconds = ddp_timeout_seconds
        self.save_every_n_train_steps = save_every_n_train_steps
        self.save_top_k = save_top_k
        self.validation_only = validation_only
        self.debug = debug
        self.strict_loading = strict_loading
        self.metric_mode = metric_mode

    def run(self, config: OmegaConf) -> None:
        """Run training.

        Parameters
        ----------
        config : OmegaConf
            The configuration for the task, for bookkeeping.

        """
        # Disable some warnings
        warnings.filterwarnings(
            "ignore", ".*when logging on epoch level in distributed setting.*"
        )

        # Experiment with this during training (high or medium)
        if self.matmul_precision is not None:
            torch.set_float32_matmul_precision(self.matmul_precision)

        # Create trainer dict
        if self.trainer is None:
            self.trainer = {}

        # Flip some arguments in debug mode
        devices = self.trainer.get("devices", 1)

        if self.debug:
            self.slurm = False
            if isinstance(devices, int):
                devices = 1
            elif isinstance(devices, (list, listconfig.ListConfig)):
                devices = [devices[0]]
            self.trainer["devices"] = devices
            self.data.num_workers = 0
            if self.wandb:
                self.wandb = None

        # slurm
        if self.slurm:
            self.trainer["devices"] = int(
                os.environ.get("SLURM_NTASKS_PER_NODE", "auto")
            )
            self.trainer["num_nodes"] = int(os.environ.get("SLURM_NNODES", 1))

        # Create objects
        data_module = TrainingDataModule(self.data)
        model_module = self.model

        if self.pretrained and not self.resume:
            file_path = self.pretrained

            print(f"Loading model from {file_path}")
            model_module = type(model_module).load_from_checkpoint(
                file_path, map_location="cpu", strict=False, weights_only=False, **(model_module.hparams)
            )

        # Create checkpoint callback
        callbacks = self.trainer.get("callbacks", [])
        dirpath = f"{self.output}/{self.name}"
        os.makedirs(dirpath, exist_ok=True)

        if not self.disable_checkpoint:
            if self.slurm:
                jobid = os.environ.get("SLURM_JOB_ID", "")
                dirpath = Path(self.output) / jobid
                dirpath.mkdir(parents=True, exist_ok=True)
                print(
                    "Configuring ModelCheckpoint for SLURM to directory: ", str(dirpath)
                )
            mc = ModelCheckpoint(
                monitor="val/lddt",
                save_top_k=self.save_top_k,
                save_last=True,
                mode=self.metric_mode,
                every_n_epochs=1,
            )
            callbacks.append(mc)

        # Create wandb logger
        loggers = []
        if self.wandb:
            extra = {}
            if self.slurm:
                ckpt_path = (
                    "hpc"
                    if len([f for f in os.listdir(dirpath) if f.startswith("hpc")])
                    else None
                )
                extra = {"resume": "must"} if ckpt_path else {}

            wdb_logger = WandbLogger(
                name=self.name,
                group=self.wandb["group"],
                save_dir=dirpath,
                dir=f".tmp_wandb/{time.time()}",
                project=self.wandb["project"],
                entity=self.wandb["entity"],
                log_model=False,
                id=os.environ.get("SLURM_JOB_ID", None) if self.slurm else None,
                **extra,
            )
            loggers.append(wdb_logger)
            # Save the config to wandb
            # This fails when not on rank0, so just catching
            try:
                config_out = Path(wdb_logger.experiment.dir) / "run.yaml"
                with Path.open(config_out, "w") as f:
                    OmegaConf.save(config, f)
                wdb_logger.experiment.save(str(config_out))
            except Exception:  # noqa: BLE001, S110
                pass

        # Set up trainer
        strategy = "auto"
        if (isinstance(devices, int) and devices > 1) or (
            isinstance(devices, (list, listconfig.ListConfig)) and len(devices) > 1
        ):
            strategy = DDPStrategy(
                find_unused_parameters=self.find_unused_parameters,
                timeout=datetime.timedelta(seconds=self.ddp_timeout_seconds),
            )

        trainer = pl.Trainer(
            default_root_dir=str(dirpath),
            strategy=strategy,
            callbacks=callbacks,
            logger=loggers,
            enable_checkpointing=not self.disable_checkpoint,
            reload_dataloaders_every_n_epochs=1,
            **self.trainer,
        )

        # Run training
        if self.slurm:
            # If we are on SLURM, we need to check if we have a checkpoint for this job
            ckpt_path = (
                "hpc"
                if len([f for f in os.listdir(dirpath) if f.startswith("hpc")])
                else None
            )
            # If none check with we have a resume checkpoint passed in
            if ckpt_path is None:
                ckpt_path = self.resume
        else:
            ckpt_path = self.resume

        if not self.strict_loading:
            model_module.strict_loading = False

        if self.validation_only:
            trainer.validate(
                model_module,
                datamodule=data_module,
                ckpt_path=ckpt_path,
            )
        else:
            trainer.fit(
                model_module,
                datamodule=data_module,
                ckpt_path=ckpt_path,
            )