` folder.
44 |
45 | To build the documentation for the stable version, checkout the branch with the
46 | stable version and run:
47 |
48 | ```
49 | make docs-stable
50 | ```
51 |
52 | This builds the documentaion in the `docs/build/stable` folder.
53 |
54 | Docs can be served locally with:
55 |
56 | ```
57 | make serve
58 | ```
59 |
60 | #### Writing Documentation
61 |
62 | The documentation source is in [docs/source](./docs/source). The documentation is
63 | written in Markdown (MyST flavor). For more information regarding formatting, see:
64 |
65 | - https://pradyunsg.me/furo/reference/
66 | - https://myst-parser.readthedocs.io/en/latest/syntax/typography.html
67 |
68 | ### Contributor License Agreement (CLA)
69 |
70 | To contribute to this repository, you must sign a Contributor License Agreement (CLA).
71 | This is a one-time process done through GitHub when you open your first pull request.
72 | You will be prompted automatically.
73 |
74 | By signing the CLA, you agree that your contributions may be used under the terms of the project license.
75 |
--------------------------------------------------------------------------------
/src/lightly_train/_transforms/random_order.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) Lightly AG and affiliates.
3 | # All rights reserved.
4 | #
5 | # This source code is licensed under the license found in the
6 | # LICENSE file in the root directory of this source tree.
7 | #
8 | from __future__ import annotations
9 |
10 | import numpy as np
11 | from albumentations import BaseCompose, BasicTransform, SomeOf
12 | from lightning_utilities.core.imports import RequirementCache
13 | from numpy.typing import NDArray
14 |
15 | ALBUMENTATIONS_GEQ_1_4_21 = RequirementCache("albumentations>=1.4.21")
16 |
17 | if not ALBUMENTATIONS_GEQ_1_4_21:
18 | from albumentations import random_utils
19 |
20 |
21 | class RandomOrder(SomeOf): # type: ignore[misc]
22 | def __init__(
23 | self,
24 | transforms: list[BasicTransform | BaseCompose],
25 | n: int | None = None,
26 | replace: bool = False,
27 | p: float = 1.0,
28 | ):
29 | """Apply a random number of transformations from a list in random order.
30 |
31 | Args:
32 | transforms: List of transformations to choose from.
33 | n: How many transformations to sample. If None, len(transforms) is used.
34 | replace: Whether to sample transformations with replacement.
35 | p: Probability of applying the entire pipeline, not each individual transform.
36 | """
37 | if n is None:
38 | n = len(transforms)
39 | super().__init__(transforms=transforms, n=n, replace=replace, p=p)
40 |
41 | def _get_idx(self) -> NDArray[np.int64]:
42 | if ALBUMENTATIONS_GEQ_1_4_21:
43 | return self.random_generator.choice( # type: ignore[no-any-return]
44 | len(self.transforms),
45 | size=self.n,
46 | replace=self.replace,
47 | )
48 | else:
49 | return random_utils.choice( # type: ignore[no-any-return]
50 | len(self.transforms),
51 | size=self.n,
52 | replace=self.replace,
53 | p=self.transforms_ps,
54 | )
55 |
--------------------------------------------------------------------------------
/docs/build.py:
--------------------------------------------------------------------------------
1 | # This script creates the build/index.html and build/versions.html files.
2 | # build/index.html redirects to build/stable/index.html.
3 | # build/versions.html lists all available documentation versions.
4 |
5 | import textwrap
6 | from argparse import ArgumentParser
7 | from pathlib import Path
8 |
9 |
10 | def build_index_html(build_dir: Path) -> None:
11 | """Creates the main index.html file that redirects to the stable version."""
12 | html = textwrap.dedent("""
13 |
14 |
15 |
16 |
17 | Redirecting...
18 |
19 |
20 | If you are not redirected, click here.
21 |
22 |
23 | """)
24 |
25 | with open(build_dir / "index.html", "w") as f:
26 | f.write(html)
27 |
28 |
29 | def build_versions_html(build_dir: Path) -> None:
30 | """Creates the versions.html file that lists all available versions."""
31 |
32 | header = textwrap.dedent("""
33 |
34 |
35 |
36 | LightlyTrain Documentation
37 |
38 |
39 | LightlyTrain Documentation
40 |
41 | """)
42 | footer = textwrap.dedent("""
43 |
44 |
45 |
46 | """)
47 |
48 | html = header
49 | versions = sorted(
50 | [path for path in build_dir.iterdir() if path.is_dir()], reverse=True
51 | )
52 | for version in versions:
53 | html += f' {version.name}\n'
54 | html += footer
55 |
56 | with open(build_dir / "versions.html", "w") as f:
57 | f.write(html)
58 |
59 |
60 | def main(build_dir: Path) -> None:
61 | build_index_html(build_dir=build_dir)
62 | build_versions_html(build_dir=build_dir)
63 |
64 |
65 | if __name__ == "__main__":
66 | parser = ArgumentParser()
67 | parser.add_argument("--build-dir", type=Path, required=True)
68 | args = parser.parse_args()
69 |
70 | main(build_dir=args.build_dir)
71 |
--------------------------------------------------------------------------------
/docs/source/pretrain_distill/models/timm.md:
--------------------------------------------------------------------------------
1 | (models-timm)=
2 |
3 | # TIMM
4 |
5 | This page describes how to use TIMM models with LightlyTrain.
6 |
7 | ```{important}
8 | [TIMM](https://github.com/huggingface/pytorch-image-models) must be installed with
9 | `pip install "lightly-train[timm]"`.
10 | ```
11 |
12 | ## Pretrain and Fine-tune a TIMM Model
13 |
14 | ### Pretrain
15 |
16 | Pretraining TIMM models with LightlyTrain is straightforward. Below we provide the
17 | minimum scripts for pretraining using `timm/resnet18` as an example:
18 |
19 | ````{tab} Python
20 | ```python
21 | import lightly_train
22 |
23 | if __name__ == "__main__":
24 | lightly_train.pretrain(
25 | out="out/my_experiment", # Output directory.
26 | data="my_data_dir", # Directory with images.
27 | model="timm/resnet18", # Pass the timm model.
28 | )
29 |
30 | ```
31 |
32 | Or alternatively, pass directly a TIMM model instance:
33 |
34 | ```python
35 | import timm
36 |
37 | import lightly_train
38 |
39 | if __name__ == "__main__":
40 | model = timm.create_model("resnet18") # Load the model.
41 | lightly_train.pretrain(
42 | out="out/my_experiment", # Output directory.
43 | data="my_data_dir", # Directory with images.
44 | model=model, # Pass the TIMM model.
45 | )
46 | ````
47 |
48 | ````{tab} Command Line
49 | ```bash
50 | lightly-train pretrain out="out/my_experiment" data="my_data_dir" model="timm/resnet18"
51 | ````
52 |
53 | ### Fine-tune
54 |
55 | After pretraining, you can load the exported model for fine-tuning with TIMM:
56 |
57 | ```python
58 | import timm
59 |
60 | model = timm.create_model(
61 | model_name="resnet18",
62 | checkpoint_path="out/my_experiment/exported_models/exported_last.pt",
63 | )
64 | ```
65 |
66 | ## Supported Models
67 |
68 | All timm models are supported, see [timm docs](https://github.com/huggingface/pytorch-image-models?tab=readme-ov-file#models) for a full list.
69 |
70 | Examples:
71 |
72 | - `timm/resnet50`
73 | - `timm/convnext_base`
74 | - `timm/vit_base_patch16_224`
75 |
--------------------------------------------------------------------------------
/src/lightly_train/_task_models/dinov2_eomt_semantic_segmentation/scheduler.py:
--------------------------------------------------------------------------------
1 | #
2 | # ---------------------------------------------------------------
3 | # © 2025 Mobile Perception Systems Lab at TU/e. All rights reserved.
4 | # Licensed under the MIT License.
5 | # ---------------------------------------------------------------
6 | #
7 |
8 | from __future__ import annotations
9 |
10 | from torch.optim.lr_scheduler import LRScheduler
11 | from torch.optim.optimizer import Optimizer
12 |
13 |
14 | class TwoStageWarmupPolySchedule(LRScheduler):
15 | def __init__(
16 | self,
17 | optimizer: Optimizer,
18 | num_backbone_params: int,
19 | warmup_steps: tuple[int, int],
20 | total_steps: int,
21 | poly_power: float,
22 | last_epoch: int = -1,
23 | ):
24 | self.num_backbone_params = num_backbone_params
25 | self.warmup_steps = warmup_steps
26 | self.total_steps = total_steps
27 | self.poly_power = poly_power
28 | super().__init__(optimizer, last_epoch)
29 |
30 | def get_lr(self) -> list[float]:
31 | step = self.last_epoch
32 | lrs = []
33 | non_vit_warmup, vit_warmup = self.warmup_steps
34 | for i, base_lr in enumerate(self.base_lrs):
35 | if i >= self.num_backbone_params:
36 | if non_vit_warmup > 0 and step < non_vit_warmup:
37 | lr = base_lr * (step / non_vit_warmup)
38 | else:
39 | adjusted = max(0, step - non_vit_warmup)
40 | max_steps = max(1, self.total_steps - non_vit_warmup)
41 | lr = base_lr * (1 - (adjusted / max_steps)) ** self.poly_power
42 | else:
43 | if step < non_vit_warmup:
44 | lr = 0
45 | elif step < non_vit_warmup + vit_warmup:
46 | lr = base_lr * ((step - non_vit_warmup) / vit_warmup)
47 | else:
48 | adjusted = max(0, step - non_vit_warmup - vit_warmup)
49 | max_steps = max(1, self.total_steps - non_vit_warmup - vit_warmup)
50 | lr = base_lr * (1 - (adjusted / max_steps)) ** self.poly_power
51 | lrs.append(lr)
52 | return lrs
53 |
--------------------------------------------------------------------------------
/src/lightly_train/_task_models/dinov3_eomt_instance_segmentation/scheduler.py:
--------------------------------------------------------------------------------
1 | #
2 | # ---------------------------------------------------------------
3 | # © 2025 Mobile Perception Systems Lab at TU/e. All rights reserved.
4 | # Licensed under the MIT License.
5 | # ---------------------------------------------------------------
6 | #
7 |
8 | from __future__ import annotations
9 |
10 | from torch.optim.lr_scheduler import LRScheduler
11 | from torch.optim.optimizer import Optimizer
12 |
13 |
14 | class TwoStageWarmupPolySchedule(LRScheduler):
15 | def __init__(
16 | self,
17 | optimizer: Optimizer,
18 | num_backbone_params: int,
19 | warmup_steps: tuple[int, int],
20 | total_steps: int,
21 | poly_power: float,
22 | last_epoch: int = -1,
23 | ):
24 | self.num_backbone_params = num_backbone_params
25 | self.warmup_steps = warmup_steps
26 | self.total_steps = total_steps
27 | self.poly_power = poly_power
28 | super().__init__(optimizer, last_epoch)
29 |
30 | def get_lr(self) -> list[float]:
31 | step = self.last_epoch
32 | lrs = []
33 | non_vit_warmup, vit_warmup = self.warmup_steps
34 | for i, base_lr in enumerate(self.base_lrs):
35 | if i >= self.num_backbone_params:
36 | if non_vit_warmup > 0 and step < non_vit_warmup:
37 | lr = base_lr * (step / non_vit_warmup)
38 | else:
39 | adjusted = max(0, step - non_vit_warmup)
40 | max_steps = max(1, self.total_steps - non_vit_warmup)
41 | lr = base_lr * (1 - (adjusted / max_steps)) ** self.poly_power
42 | else:
43 | if step < non_vit_warmup:
44 | lr = 0
45 | elif step < non_vit_warmup + vit_warmup:
46 | lr = base_lr * ((step - non_vit_warmup) / vit_warmup)
47 | else:
48 | adjusted = max(0, step - non_vit_warmup - vit_warmup)
49 | max_steps = max(1, self.total_steps - non_vit_warmup - vit_warmup)
50 | lr = base_lr * (1 - (adjusted / max_steps)) ** self.poly_power
51 | lrs.append(lr)
52 | return lrs
53 |
--------------------------------------------------------------------------------
/src/lightly_train/_task_models/dinov3_eomt_panoptic_segmentation/scheduler.py:
--------------------------------------------------------------------------------
1 | #
2 | # ---------------------------------------------------------------
3 | # © 2025 Mobile Perception Systems Lab at TU/e. All rights reserved.
4 | # Licensed under the MIT License.
5 | # ---------------------------------------------------------------
6 | #
7 |
8 | from __future__ import annotations
9 |
10 | from torch.optim.lr_scheduler import LRScheduler
11 | from torch.optim.optimizer import Optimizer
12 |
13 |
14 | class TwoStageWarmupPolySchedule(LRScheduler):
15 | def __init__(
16 | self,
17 | optimizer: Optimizer,
18 | num_backbone_params: int,
19 | warmup_steps: tuple[int, int],
20 | total_steps: int,
21 | poly_power: float,
22 | last_epoch: int = -1,
23 | ):
24 | self.num_backbone_params = num_backbone_params
25 | self.warmup_steps = warmup_steps
26 | self.total_steps = total_steps
27 | self.poly_power = poly_power
28 | super().__init__(optimizer, last_epoch)
29 |
30 | def get_lr(self) -> list[float]:
31 | step = self.last_epoch
32 | lrs = []
33 | non_vit_warmup, vit_warmup = self.warmup_steps
34 | for i, base_lr in enumerate(self.base_lrs):
35 | if i >= self.num_backbone_params:
36 | if non_vit_warmup > 0 and step < non_vit_warmup:
37 | lr = base_lr * (step / non_vit_warmup)
38 | else:
39 | adjusted = max(0, step - non_vit_warmup)
40 | max_steps = max(1, self.total_steps - non_vit_warmup)
41 | lr = base_lr * (1 - (adjusted / max_steps)) ** self.poly_power
42 | else:
43 | if step < non_vit_warmup:
44 | lr = 0
45 | elif step < non_vit_warmup + vit_warmup:
46 | lr = base_lr * ((step - non_vit_warmup) / vit_warmup)
47 | else:
48 | adjusted = max(0, step - non_vit_warmup - vit_warmup)
49 | max_steps = max(1, self.total_steps - non_vit_warmup - vit_warmup)
50 | lr = base_lr * (1 - (adjusted / max_steps)) ** self.poly_power
51 | lrs.append(lr)
52 | return lrs
53 |
--------------------------------------------------------------------------------
/src/lightly_train/_task_models/dinov3_eomt_semantic_segmentation/scheduler.py:
--------------------------------------------------------------------------------
1 | #
2 | # ---------------------------------------------------------------
3 | # © 2025 Mobile Perception Systems Lab at TU/e. All rights reserved.
4 | # Licensed under the MIT License.
5 | # ---------------------------------------------------------------
6 | #
7 |
8 | from __future__ import annotations
9 |
10 | from torch.optim.lr_scheduler import LRScheduler
11 | from torch.optim.optimizer import Optimizer
12 |
13 |
14 | class TwoStageWarmupPolySchedule(LRScheduler):
15 | def __init__(
16 | self,
17 | optimizer: Optimizer,
18 | num_backbone_params: int,
19 | warmup_steps: tuple[int, int],
20 | total_steps: int,
21 | poly_power: float,
22 | last_epoch: int = -1,
23 | ):
24 | self.num_backbone_params = num_backbone_params
25 | self.warmup_steps = warmup_steps
26 | self.total_steps = total_steps
27 | self.poly_power = poly_power
28 | super().__init__(optimizer, last_epoch)
29 |
30 | def get_lr(self) -> list[float]:
31 | step = self.last_epoch
32 | lrs = []
33 | non_vit_warmup, vit_warmup = self.warmup_steps
34 | for i, base_lr in enumerate(self.base_lrs):
35 | if i >= self.num_backbone_params:
36 | if non_vit_warmup > 0 and step < non_vit_warmup:
37 | lr = base_lr * (step / non_vit_warmup)
38 | else:
39 | adjusted = max(0, step - non_vit_warmup)
40 | max_steps = max(1, self.total_steps - non_vit_warmup)
41 | lr = base_lr * (1 - (adjusted / max_steps)) ** self.poly_power
42 | else:
43 | if step < non_vit_warmup:
44 | lr = 0
45 | elif step < non_vit_warmup + vit_warmup:
46 | lr = base_lr * ((step - non_vit_warmup) / vit_warmup)
47 | else:
48 | adjusted = max(0, step - non_vit_warmup - vit_warmup)
49 | max_steps = max(1, self.total_steps - non_vit_warmup - vit_warmup)
50 | lr = base_lr * (1 - (adjusted / max_steps)) ** self.poly_power
51 | lrs.append(lr)
52 | return lrs
53 |
--------------------------------------------------------------------------------
/src/lightly_train/_callbacks/export.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) Lightly AG and affiliates.
3 | # All rights reserved.
4 | #
5 | # This source code is licensed under the license found in the
6 | # LICENSE file in the root directory of this source tree.
7 | #
8 | from __future__ import annotations
9 |
10 | from pathlib import Path
11 |
12 | from pytorch_lightning import LightningModule, Trainer
13 | from pytorch_lightning.callbacks import Callback
14 | from pytorch_lightning.utilities import rank_zero_only
15 |
16 | from lightly_train._commands import common_helpers
17 | from lightly_train._commands.common_helpers import ModelFormat
18 | from lightly_train._configs.config import PydanticConfig
19 | from lightly_train._models import package_helpers
20 | from lightly_train._models.model_wrapper import ModelWrapper
21 |
22 |
23 | class ModelExportArgs(PydanticConfig):
24 | every_n_epochs: int = 1
25 |
26 |
27 | class ModelExport(Callback):
28 | def __init__(
29 | self,
30 | wrapped_model: ModelWrapper,
31 | out_dir: Path,
32 | every_n_epochs: int = 1,
33 | ):
34 | self._wrapped_model = wrapped_model
35 | self._out_dir = out_dir
36 | self._every_n_epochs = every_n_epochs
37 | self._package = package_helpers.get_package_from_model(
38 | self._wrapped_model, include_custom=True, fallback_custom=True
39 | )
40 |
41 | @rank_zero_only # type: ignore[misc]
42 | def _safe_export_model(self, export_path: Path) -> None:
43 | """Export the model to the specified path, deleting any existing file."""
44 | if export_path.exists():
45 | export_path.unlink(missing_ok=True)
46 |
47 | common_helpers.export_model(
48 | model=self._wrapped_model,
49 | out=export_path,
50 | format=ModelFormat.PACKAGE_DEFAULT,
51 | package=self._package,
52 | log_example=False,
53 | )
54 |
55 | def on_train_epoch_end(self, trainer: Trainer, pl_module: LightningModule) -> None:
56 | if pl_module.current_epoch % self._every_n_epochs == 0:
57 | # Delete the previous export if it exists
58 | export_path = self._out_dir / "exported_last.pt"
59 | self._safe_export_model(export_path)
60 |
--------------------------------------------------------------------------------
/docker/Dockerfile-amd64-cuda:
--------------------------------------------------------------------------------
1 | # Image is released at https://hub.docker.com/r/lightly/train
2 | # Image is CUDA-optimized for single/multi-GPU training and inference
3 |
4 | # Start FROM PyTorch image https://hub.docker.com/r/pytorch/pytorch
5 | # We use CUDA 11.8 because it is compatible with older CUDA Drivers (>=450.80.02 linux, >=452.39 windows).
6 | # See https://docs.nvidia.com/deploy/cuda-compatibility/#minor-version-compatibility
7 | # The CUDA Driver is the only component from the host system that has to be compatible with the docker image.
8 | # The PyTorch and cuDNN versions are independent of the host system.
9 | #
10 | # The PyTorch 2.5.1 image comes with Python 3.11.
11 | FROM pytorch/pytorch:2.5.1-cuda11.8-cudnn9-runtime AS runtime
12 |
13 | # Install packages into the system Python and skip creating a virtual environment.
14 | ENV UV_SYSTEM_PYTHON="true" \
15 | # Do not cache dependencies as they would also be saved in the docker image.
16 | UV_NO_CACHE="true"
17 |
18 | # Required for uv installation.
19 | RUN apt-get update && apt-get install -y make curl
20 |
21 | # Install Pillow-SIMD dependencies.
22 | RUN apt-get update && apt-get install -y python3.11-dev libjpeg8-dev libjpeg-turbo-progs libtiff5-dev libwebp-dev gcc
23 |
24 | # Create working directory
25 | WORKDIR /home/lightly_train
26 |
27 | # Set and create the directory to save pretrained torch models into
28 | ENV TORCH_HOME="/home/lightly_train/.cache/torch"
29 | RUN mkdir -p ${TORCH_HOME} && chmod -R a+w $TORCH_HOME
30 |
31 | # Set and create the directory to save pretrained models into
32 | ENV LIGHTLY_TRAIN_CACHE_DIR="/home/lightly_train/.cache/lightly-train"
33 | RUN mkdir -p ${LIGHTLY_TRAIN_CACHE_DIR} && chmod -R a+w $LIGHTLY_TRAIN_CACHE_DIR
34 | RUN WEIGHTS_PATH="${LIGHTLY_TRAIN_CACHE_DIR}/weights" && mkdir -p ${WEIGHTS_PATH} && chmod -R a+w $WEIGHTS_PATH
35 |
36 | # Install uv
37 | COPY Makefile /home/lightly_train
38 | RUN make install-uv
39 |
40 | # Add uv to PATH
41 | ENV PATH="/root/.local/bin:$PATH"
42 |
43 | # Install the package dependencies.
44 | COPY pyproject.toml /home/lightly_train
45 | RUN make install-docker-dependencies && make download-docker-models
46 |
47 | # Copy the package itself
48 | COPY src /home/lightly_train/src
49 |
50 | # Install the package.
51 | RUN make install-docker
52 |
--------------------------------------------------------------------------------
/src/lightly_train/_callbacks/mlflow_logging.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) Lightly AG and affiliates.
3 | # All rights reserved.
4 | #
5 | # This source code is licensed under the license found in the
6 | # LICENSE file in the root directory of this source tree.
7 | #
8 | try:
9 | from mlflow.system_metrics.system_metrics_monitor import SystemMetricsMonitor
10 | except ImportError:
11 | SystemMetricsMonitor = None # type: ignore[misc, assignment]
12 | from pytorch_lightning import LightningModule, Trainer
13 | from pytorch_lightning.callbacks import Callback
14 |
15 | from lightly_train._configs.config import PydanticConfig
16 | from lightly_train._loggers.mlflow import MLFlowLogger
17 |
18 |
19 | class MLFlowLoggingArgs(PydanticConfig):
20 | pass
21 |
22 |
23 | class MLFlowLogging(Callback):
24 | def on_fit_start(self, trainer: Trainer, pl_module: LightningModule) -> None:
25 | self.system_monitor = None
26 | for logger in trainer.loggers:
27 | if isinstance(logger, MLFlowLogger) and SystemMetricsMonitor is not None:
28 | self.system_monitor = SystemMetricsMonitor( # type: ignore[no-untyped-call]
29 | run_id=logger.run_id,
30 | )
31 | self.system_monitor.start() # type: ignore[no-untyped-call]
32 | with open(trainer.default_root_dir + "/train.log", "r") as _f:
33 | logger.experiment.log_text(
34 | run_id=logger.run_id,
35 | text=_f.read(),
36 | artifact_file="logs/train-start.log",
37 | )
38 | break
39 |
40 | def on_fit_end(self, trainer: Trainer, pl_module: LightningModule) -> None:
41 | if self.system_monitor is not None:
42 | self.system_monitor.finish() # type: ignore[no-untyped-call]
43 | for logger in trainer.loggers:
44 | if isinstance(logger, MLFlowLogger):
45 | with open(trainer.default_root_dir + "/train.log", "r") as _f:
46 | logger.experiment.log_text(
47 | run_id=logger.run_id,
48 | text=_f.read(),
49 | artifact_file="logs/train-end.log",
50 | )
51 | break
52 |
--------------------------------------------------------------------------------