From fc45759a19708e074c44ee8e41bea2507959fd9c Mon Sep 17 00:00:00 2001 From: zoechbauer1 Date: Fri, 16 Feb 2024 16:51:15 +0100 Subject: [PATCH 1/8] fixed distributed trainer in cyclones use case --- use-cases/cyclones/cyclones_vgg.py | 176 ++++++++++++++--------------- use-cases/cyclones/trainer.py | 11 +- 2 files changed, 94 insertions(+), 93 deletions(-) diff --git a/use-cases/cyclones/cyclones_vgg.py b/use-cases/cyclones/cyclones_vgg.py index 79e4136d..a2272505 100644 --- a/use-cases/cyclones/cyclones_vgg.py +++ b/use-cases/cyclones/cyclones_vgg.py @@ -582,94 +582,94 @@ def VGG_V4(patch_size, label_no_cyclone, channels, activation, regularizer): """ -# def ModelV5(patch_size, channels, last_activation, kernel_size=3): -# # kernel initializer -# initializer = tf.random_normal_initializer(0.0, 0.02) - -# # input layer -# inputs = tf.keras.layers.Input(shape=(patch_size, patch_size, -# channels[0])) - -# conv_blocks = [ -# ConvBlock( -# filters=32, -# initializer=initializer, -# kernel_size=kernel_size, -# strides=2, -# apply_batchnorm=True, -# apply_dropout=False, -# apply_gaussian_noise=True, -# ), -# ConvBlock( -# filters=64, -# initializer=initializer, -# kernel_size=kernel_size, -# strides=2, -# apply_batchnorm=False, -# apply_dropout=False, -# apply_gaussian_noise=False, -# ), -# ConvBlock( -# filters=128, -# initializer=initializer, -# kernel_size=3, -# strides=2, -# apply_batchnorm=False, -# apply_dropout=True, -# apply_gaussian_noise=False, -# ), -# ConvBlock( -# filters=256, -# initializer=initializer, -# kernel_size=3, -# strides=2, -# apply_batchnorm=False, -# apply_dropout=False, -# apply_gaussian_noise=True, -# ), -# ConvBlock( -# filters=512, -# initializer=initializer, -# kernel_size=3, -# strides=2, -# apply_batchnorm=False, -# apply_dropout=False, -# apply_gaussian_noise=False, -# ), -# ConvBlock( -# filters=1024, -# initializer=initializer, -# kernel_size=3, -# strides=2, -# apply_batchnorm=True, -# apply_dropout=True, -# apply_gaussian_noise=False, -# ), -# ] -# x = inputs -# for block in conv_blocks: -# x = block(x) - -# x = tf.keras.layers.Flatten()(x) -# x = tf.keras.layers.Dense( -# units=1024, activation="relu", kernel_initializer=initializer -# )(x) -# x = tf.keras.layers.Dense( -# units=512, activation="relu", kernel_initializer=initializer -# )(x) -# x = tf.keras.layers.Dense( -# units=256, activation="relu", kernel_initializer=initializer -# )(x) -# x = tf.keras.layers.Dense( -# units=128, activation="relu", kernel_initializer=initializer -# )(x) - -# outputs = tf.keras.layers.Dense( -# channels[1], activation=last_activation, -# kernel_initializer=initializer -# )(x) - -# return tf.keras.Model(inputs=inputs, outputs=outputs, name="model_V5") +def ModelV5(patch_size, channels, last_activation, kernel_size=3): + # kernel initializer + initializer = tf.random_normal_initializer(0.0, 0.02) + + # input layer + inputs = tf.keras.layers.Input(shape=(patch_size, patch_size, +channels[0])) + + conv_blocks = [ + ConvBlock( + filters=32, + initializer=initializer, + kernel_size=kernel_size, + strides=2, + apply_batchnorm=True, + apply_dropout=False, + apply_gaussian_noise=True, + ), + ConvBlock( + filters=64, + initializer=initializer, + kernel_size=kernel_size, + strides=2, + apply_batchnorm=False, + apply_dropout=False, + apply_gaussian_noise=False, + ), + ConvBlock( + filters=128, + initializer=initializer, + kernel_size=3, + strides=2, + apply_batchnorm=False, + apply_dropout=True, + apply_gaussian_noise=False, + ), + ConvBlock( + filters=256, + initializer=initializer, + kernel_size=3, + strides=2, + apply_batchnorm=False, + apply_dropout=False, + apply_gaussian_noise=True, + ), + ConvBlock( + filters=512, + initializer=initializer, + kernel_size=3, + strides=2, + apply_batchnorm=False, + apply_dropout=False, + apply_gaussian_noise=False, + ), + ConvBlock( + filters=1024, + initializer=initializer, + kernel_size=3, + strides=2, + apply_batchnorm=True, + apply_dropout=True, + apply_gaussian_noise=False, + ), + ] + x = inputs + for block in conv_blocks: + x = block(x) + + x = tf.keras.layers.Flatten()(x) + x = tf.keras.layers.Dense( + units=1024, activation="relu", kernel_initializer=initializer + )(x) + x = tf.keras.layers.Dense( + units=512, activation="relu", kernel_initializer=initializer + )(x) + x = tf.keras.layers.Dense( + units=256, activation="relu", kernel_initializer=initializer + )(x) + x = tf.keras.layers.Dense( + units=128, activation="relu", kernel_initializer=initializer + )(x) + + outputs = tf.keras.layers.Dense( + channels[1], activation=last_activation, +kernel_initializer=initializer + )(x) + + return tf.keras.Model(inputs=inputs, outputs=outputs, name="model_V5") """ diff --git a/use-cases/cyclones/trainer.py b/use-cases/cyclones/trainer.py index 2fb3c1bc..1c47819b 100644 --- a/use-cases/cyclones/trainer.py +++ b/use-cases/cyclones/trainer.py @@ -44,10 +44,10 @@ def __init__( self.regularization_strength, self.regularizer = ( regularization_strength.value ) - self.loss_name, self.loss = loss.value - # Optimizers, Losses - self.optimizer = keras.optimizers.Adam(learning_rate=learning_rate) + # Loss name and learning rate + self.loss_name = loss.value + self.learning_rate = learning_rate # Parse global config self.setup_config(self.global_config) @@ -86,9 +86,10 @@ def execute(self, train_data, validation_data, channels) -> None: logging.debug( f"Model loaded from backup at {self.best_model_name}") + optimizer = keras.optimizers.Adam(learning_rate=self.learning_rate) metrics = [keras.metrics.MeanAbsoluteError(name="mae")] - model.compile(loss=self.loss, - optimizer=self.optimizer, metrics=metrics) + model.compile(loss=self.loss_name, + optimizer=optimizer, metrics=metrics) logging.debug("Model compiled") # print model summary to check if model's architecture is correct From 61e742d66a3a5f7af5047f637889a7a6cb8b72ee Mon Sep 17 00:00:00 2001 From: Matteo Bunino <48362942+matbun@users.noreply.github.com> Date: Thu, 21 Mar 2024 08:08:38 +0100 Subject: [PATCH 2/8] 3dgan integration (#118) * fixed distributed trainer in cyclones use case * commiting integration of 3dgan scripts * ADD: Download dataset * FIX: DDP distributed training with manual optimization * ADD: log with MLFlow * Sqaaas code (#88) * Create sqaaas.yml * Update sqaaas.yml * Update sqaaas.yml * Point to the current repo * Remove unnecessary checkout step * Rename step --------- Co-authored-by: orviz * Sqaaas code (#89) * Create sqaaas.yml * Update sqaaas.yml * Update sqaaas.yml * Point to the current repo * Remove unnecessary checkout step * Rename step * ADD: adaptive branch discovery for SQAaaS action * Update sqaaas.yml --------- Co-authored-by: orviz * ADD: draft predictor and saver * ADD: stub for inference pipeline * ADD: small docs * UPDATE: inference pipeline components * UPDATE: reorg * ADD: image generation for inference * update tag * ADD: threshold * ADD: draft inference * ADD: draft inference wf * ADD: working inference workflow * ADD: 3D scatter plots * ADD: Dockerfile + refactor * ADD: .dockerignore * Update .dockerignore * ADD: skip download option * ADD: cern pipeline.yaml * UPDATE: dataset loading function * UPDATE: dataset loading function * UPDATE conf * UPDATE refactor * UPDATE refactor * UPDATE training docs * Update readme * update README * FIX typo * Update README * Update mkdir * UPDATE data paths * UPDATE Dockerfile * UPDATE Dockerfiles * UPDATE for Singularity execution * FIX version mismatch * UPDATE Singularity docs * Named steps pipe (#100) * ADD: dict steps pipe * Relax dependency constraint * UPDATE Singularity exec command * UPDATE: Image version * UPDATE: load components from pipeline * ADD: docs * Simplify 3DGAN model config * ADD: mlflow autologging support for PL trainer * UPDATE container info * Refactor * UPDATE dependencies * FIX linter problem * Simplified workflow configuration (#108) * Add SQAaaS dynamic badge for dev branch (#104) * Add SQAaaS dynamic badge * Upgrade to sqaaas-assessment-action@v2 * Add draft example * UPDATE credits field * ADD docs * REFACTOR components and pipeline code * UPDATE docstring * UPDATE mnist torch uc * ADD config file parser draft * ADD itwinaiCLI and ConfigParser * ADD docs * ADD pipeline parser and serializer plus tests * UPDATE docs * ADD adapter component and tests (incl parser) * ADD splitter component, improve pipeline, tests * UPDATE test * REMOVE todos * ADD component tests * ADD serializer tests * FIX linter * ADD basic workflow tutorial * ADD basic intermediate tutorial * ADD advanced tutorial * UPDATE advanced tutorial * UPDATE use cases * UPDATE save parameters * FIX linter * FIX cyclones use case workflow --------- Co-authored-by: orviz * Simplified workflow configuration (#109) * Add SQAaaS dynamic badge for dev branch (#104) * Add SQAaaS dynamic badge * Upgrade to sqaaas-assessment-action@v2 * Add draft example * UPDATE credits field * ADD docs * REFACTOR components and pipeline code * UPDATE docstring * UPDATE mnist torch uc * ADD config file parser draft * ADD itwinaiCLI and ConfigParser * ADD docs * ADD pipeline parser and serializer plus tests * UPDATE docs * ADD adapter component and tests (incl parser) * ADD splitter component, improve pipeline, tests * UPDATE test * REMOVE todos * ADD component tests * ADD serializer tests * FIX linter * ADD basic workflow tutorial * ADD basic intermediate tutorial * ADD advanced tutorial * UPDATE advanced tutorial * UPDATE use cases * UPDATE save parameters * FIX linter * FIX cyclones use case workflow * ADD slurm jobscript * FIX merge error * FIX components template --------- Co-authored-by: orviz * ADD integration tests * FIX test * FIX 3dgan inference test * ADD GPU support and update tag * FIX linter * ADD override example * UPDATE 3DGAN inference * UPDATE inference execution tutorials * UPDATE README * UPDATE saver saving sparse tensors * ADD interlink pods * UPDATE pod name * UPDATE annotations * FIX README * CLEANUP * Merge * update * ADD tf cpu env * U[date Makefile * FIX 3DGAN tests * FIX data folder path --------- Co-authored-by: zoechbauer1 Co-authored-by: Kalliopi Tsolaki Co-authored-by: orviz --- .github/workflows/workflows-dt.yml | 2 +- Makefile | 11 +- env-files/tensorflow/tensorflow-2.13-cpu.yml | 14 ++ src/itwinai/cli.py | 10 ++ tests/use-cases/test_3dgan.py | 14 +- use-cases/3dgan/Dockerfile | 19 +++ use-cases/3dgan/Dockerfile.inference | 25 --- use-cases/3dgan/README.md | 149 +++++++++++++++--- use-cases/3dgan/dataloader.py | 1 - use-cases/3dgan/inference-pipeline.yaml | 18 ++- .../3dgan/interLink/3dgan-inference-cpu.yaml | 74 +++++++++ .../3dgan/interLink/3dgan-inference.yaml | 74 +++++++++ use-cases/3dgan/interLink/README.md | 55 +++++++ use-cases/3dgan/saver.py | 38 +++-- use-cases/3dgan/startscript | 4 +- use-cases/3dgan/train.py | 44 ------ use-cases/cyclones/dataloader.py | 6 +- use-cases/cyclones/pipeline.yaml | 2 +- 18 files changed, 441 insertions(+), 119 deletions(-) create mode 100644 env-files/tensorflow/tensorflow-2.13-cpu.yml create mode 100644 use-cases/3dgan/Dockerfile delete mode 100644 use-cases/3dgan/Dockerfile.inference create mode 100644 use-cases/3dgan/interLink/3dgan-inference-cpu.yaml create mode 100644 use-cases/3dgan/interLink/3dgan-inference.yaml create mode 100644 use-cases/3dgan/interLink/README.md delete mode 100644 use-cases/3dgan/train.py diff --git a/.github/workflows/workflows-dt.yml b/.github/workflows/workflows-dt.yml index 4e1e7835..53a72e43 100644 --- a/.github/workflows/workflows-dt.yml +++ b/.github/workflows/workflows-dt.yml @@ -23,7 +23,7 @@ jobs: - name: Make tensorflow env shell: bash -l {0} - run: make tf-2.13 + run: make tf-2.13-cpu - name: Install dev version shell: bash -l {0} diff --git a/Makefile b/Makefile index 9e225b19..377e6df4 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ # Install PyTorch env (GPU support) torch-gpu: env-files/torch/pytorch-env-gpu.yml micromamba env create -p ./.venv-pytorch --file env-files/torch/pytorch-env-gpu.yml -y - micromamba run -p ./.venv-pytorch python -m pip install -e . + micromamba run -p ./.venv-pytorch python -m pip install -e .[dev] # Install PyTorch env (CPU only) torch-cpu: env-files/torch/pytorch-env-cpu.yml micromamba env create -p ./.venv-pytorch --file env-files/torch/pytorch-env-cpu.yml -y - micromamba run -p ./.venv-pytorch python -m pip install -e . + micromamba run -p ./.venv-pytorch python -m pip install -e .[dev] # Install TensorFlow 2.13. Creates ./.venv-tf folder. @@ -53,3 +53,10 @@ tf-2.10: env-files/tensorflow/tensorflow-2.10.yml micromamba run -p ./.venv-tf pip install tensorflow==2.10 micromamba run -p ./.venv-tf pip install -e . +# Install TensorFlow 2.13 for CPU only systems. Creates ./.venv-tf folder. +tf-2.13-cpu: env-files/tensorflow/tensorflow-2.13-cpu.yml + echo "Installing TensorFlow 2.13 env" + micromamba env create -p ./.venv-tf --file env-files/tensorflow/tensorflow-2.13-cpu.yml -y + micromamba run -p ./.venv-tf pip install --upgrade pip + micromamba run -p ./.venv-tf pip install tensorflow==2.13.* + micromamba run -p ./.venv-tf pip install -e . diff --git a/env-files/tensorflow/tensorflow-2.13-cpu.yml b/env-files/tensorflow/tensorflow-2.13-cpu.yml new file mode 100644 index 00000000..2d4740a6 --- /dev/null +++ b/env-files/tensorflow/tensorflow-2.13-cpu.yml @@ -0,0 +1,14 @@ +# https://www.tensorflow.org/install/pip#step-by-step_instructions +# https://phoenixnap.com/kb/how-to-install-tensorflow-ubuntu +# https://skeptric.com/tensorflow-conda/ +name: tensorflow-env +channels: + - conda-forge +dependencies: + - python=3.9.12 + - pip + - pip: + - tensorflow-addons + - tensorflow-datasets + # - git+https://github.com/interTwin-eu/T6.5-AI-and-ML.git@pipelines_backend#egg=itwinai&subdirectory=ai + diff --git a/src/itwinai/cli.py b/src/itwinai/cli.py index 1bf2feb9..20977961 100644 --- a/src/itwinai/cli.py +++ b/src/itwinai/cli.py @@ -28,6 +28,9 @@ def exec_pipeline( help=("Key in the configuration file identifying " "the pipeline object to execute.") )] = "pipeline", + print_config: Annotated[bool, typer.Option( + help=("Print config to be executed after overrides.") + )] = False, overrides_list: Annotated[ Optional[List[str]], typer.Option( "--override", "-o", @@ -60,6 +63,13 @@ def exec_pipeline( in map(lambda x: (x.split('=')[0], x.split('=')[1]), overrides_list) } parser = ConfigParser(config=config, override_keys=overrides) + if print_config: + import json + print() + print("#="*15 + " Used configuration " + "#="*15) + print(json.dumps(parser.config, indent=2)) + print("#="*50) + print() pipeline = parser.parse_pipeline(pipeline_nested_key=pipe_key) pipeline.execute() diff --git a/tests/use-cases/test_3dgan.py b/tests/use-cases/test_3dgan.py index 10d6b46c..c57e21ff 100644 --- a/tests/use-cases/test_3dgan.py +++ b/tests/use-cases/test_3dgan.py @@ -36,8 +36,12 @@ def test_3dgan_train(install_requirements): install_requirements(CERN_PATH, pytest.TORCH_PREFIX) # cmd = (f"micromamba run -p {pytest.TORCH_PREFIX} python " # f"{CERN_PATH}/train.py -p {CERN_PATH}/pipeline.yaml") + trainer_params = "pipeline.init_args.steps.training_step.init_args" cmd = (f"micromamba run -p {pytest.TORCH_PREFIX} itwinai exec-pipeline " - f"--config {CERN_PATH}/pipeline.yaml") + f"--config {CERN_PATH}/pipeline.yaml " + f'-o {trainer_params}.config.trainer.accelerator=cpu ' + f'-o {trainer_params}.config.trainer.strategy=auto ' + ) subprocess.run(cmd.split(), check=True) @@ -52,16 +56,18 @@ def test_3dgan_inference(install_requirements, fake_model_checkpoint): # cmd = (f"micromamba run -p {pytest.TORCH_PREFIX} itwinai exec-pipeline " # f"--config {CERN_PATH}/inference-pipeline.yaml") - getter_params = "pipeline.init_args.steps.0.init_args" - trainer_params = "pipeline.init_args.steps.1.init_args" + getter_params = "pipeline.init_args.steps.dataloading_step.init_args" + trainer_params = "pipeline.init_args.steps.inference_step.init_args" logger_params = trainer_params + ".config.trainer.logger.init_args" data_params = trainer_params + ".config.data.init_args" - saver_params = "pipeline.init_args.steps.2.init_args" + saver_params = "pipeline.init_args.steps.saver_step.init_args" cmd = ( 'itwinai exec-pipeline ' '--config use-cases/3dgan/inference-pipeline.yaml ' f'-o {getter_params}.data_path=exp_data ' f'-o {trainer_params}.model.init_args.model_uri={CKPT_PATH} ' + f'-o {trainer_params}.config.trainer.accelerator=cpu ' + f'-o {trainer_params}.config.trainer.strategy=auto ' f'-o {logger_params}.save_dir=ml_logs/mlflow_logs ' f'-o {data_params}.datapath=exp_data/*/*.h5 ' f'-o {saver_params}.save_dir=3dgan-generated-data ' diff --git a/use-cases/3dgan/Dockerfile b/use-cases/3dgan/Dockerfile new file mode 100644 index 00000000..c10d8ec8 --- /dev/null +++ b/use-cases/3dgan/Dockerfile @@ -0,0 +1,19 @@ +# FROM python:3.9.12 +FROM nvcr.io/nvidia/pytorch:23.09-py3 + +WORKDIR /usr/src/app + +RUN pip install --upgrade pip +RUN pip install --no-cache-dir lightning + +# Add 3DGAN custom requirements +COPY use-cases/3dgan/requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Install itwinai and dependencies +COPY pyproject.toml ./ +COPY src ./ +RUN pip install --no-cache-dir . + +# Add 3DGAN use case files +COPY use-cases/3dgan/* ./ \ No newline at end of file diff --git a/use-cases/3dgan/Dockerfile.inference b/use-cases/3dgan/Dockerfile.inference deleted file mode 100644 index 515caff0..00000000 --- a/use-cases/3dgan/Dockerfile.inference +++ /dev/null @@ -1,25 +0,0 @@ -FROM python:3.9.12 - -WORKDIR /usr/src/app - -RUN pip install --upgrade pip - -# Install pytorch (cpuonly) -# Ref:https://pytorch.org/get-started/previous-versions/#linux-and-windows-5 -RUN pip install --no-cache-dir torch==1.13.1+cpu torchvision==0.14.1+cpu torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cpu -RUN pip install --no-cache-dir lightning - -# Add 3DGAN custom requirements -COPY use-cases/3dgan/requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt - -# Install itwinai and dependencies -COPY pyproject.toml ./ -COPY src ./ -RUN pip install --no-cache-dir . - -# Add 3DGAN use case files -COPY use-cases/3dgan/* ./ - -# Run inference -CMD [ "python", "train.py", "-p", "inference-pipeline.yaml"] \ No newline at end of file diff --git a/use-cases/3dgan/README.md b/use-cases/3dgan/README.md index 7d1d3d16..d0bf2c82 100644 --- a/use-cases/3dgan/README.md +++ b/use-cases/3dgan/README.md @@ -23,20 +23,20 @@ At CERN, use the dedicated configuration file: ```bash cd use-cases/3dgan -python train.py -p cern-pipeline.yaml +itwinai exec-pipeline --config cern-pipeline.yaml # Or better: -micromamba run -p ../../.venv-pytorch/ torchrun --nproc_per_node gpu train.py -p cern-pipeline.yaml +micromamba run -p ../../.venv-pytorch/ torchrun --nproc_per_node gpu itwinai exec-pipeline --config cern-pipeline.yaml ``` Anywhere else, use the general purpose training configuration: ```bash cd use-cases/3dgan -python train.py -p pipeline.yaml +itwinai exec-pipeline --config pipeline.yaml # Or better: -micromamba run -p ../../.venv-pytorch/ torchrun --nproc_per_node gpu train.py -p pipeline.yaml +micromamba run -p ../../.venv-pytorch/ torchrun --nproc_per_node gpu itwinai exec-pipeline --config pipeline.yaml ``` To visualize the logs with MLFLow run the following in the terminal: @@ -49,7 +49,7 @@ And select the "3DGAN" experiment. ## Inference -The following is preliminary and not 100% ML/scientifically sound. +Disclaimer: the following is preliminary and not 100% ML/scientifically sound. 1. As inference dataset we can reuse training/validation dataset, for instance the one downloaded from Google Drive folder: if the @@ -77,18 +77,16 @@ we can create a dummy version of it with: torch.save(my_gan, '3dgan-inference.pth') ``` -3. Run inference command. This will generate a "3dgan-generated" +3. Run inference command. This will generate a `3dgan-generated-data` folder containing generated particle traces in form of torch tensors (.pth files) and 3D scatter plots (.jpg images). ```bash - python train.py -p inference-pipeline.yaml + itwinai exec-pipeline --config inference-pipeline.yaml ``` -Note the same entry point as for training. - The inference execution will produce a folder called -"3dgan-generated-data" containing +`3dgan-generated-data` containing generated 3D particle trajectories (overwritten if already there). Each generated 3D image is stored both as a torch tensor (.pth) and 3D scatter plot (.jpg): @@ -102,21 +100,56 @@ torch tensor (.pth) and 3D scatter plot (.jpg): | ├── energy=1.664689540863037&angle=1.4906378984451294.jpg ``` +However, if `aggregate_predictions` in the `ParticleImagesSaver` step is set to `True`, +only one pickled file will be generated inside `3dgan-generated-data` folder. +Notice that multiple inference calls will create new files under `3dgan-generated-data` folder. + +With fields overriding: + +```bash +# Override variables +export CERN_DATA_ROOT="../.." # data root +export TMP_DATA_ROOT=$CERN_DATA_ROOT +export CERN_CODE_ROOT="." # where code and configuration are stored +export MAX_DATA_SAMPLES=20000 # max dataset size +export BATCH_SIZE=1024 # increase to fill up GPU memory +export NUM_WORKERS_DL=4 # num worker processes used by the dataloader to pre-fetch data +export AGGREGATE_PREDS="true" # write predictions in a single file +export ACCELERATOR="gpu" # choose "cpu" or "gpu" +export STRATEGY="auto" # distributed strategy +export DEVICES="0," # GPU devices list + + +itwinai exec-pipeline --print-config --config $CERN_CODE_ROOT/inference-pipeline.yaml \ +-o pipeline.init_args.steps.dataloading_step.init_args.data_path=$TMP_DATA_ROOT/exp_data \ +-o pipeline.init_args.steps.inference_step.init_args.config.trainer.logger.init_args.save_dir=$TMP_DATA_ROOT/ml_logs/mlflow_logs \ +-o pipeline.init_args.steps.inference_step.init_args.config.trainer.strategy=$STRATEGY \ +-o pipeline.init_args.steps.inference_step.init_args.config.trainer.devices=$DEVICES \ +-o pipeline.init_args.steps.inference_step.init_args.config.trainer.accelerator=$ACCELERATOR \ +-o pipeline.init_args.steps.inference_step.init_args.model.init_args.model_uri=$CERN_CODE_ROOT/3dgan-inference.pth \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.datapath=$TMP_DATA_ROOT/exp_data/*/*.h5 \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.max_samples=$MAX_DATA_SAMPLES \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.batch_size=$BATCH_SIZE \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.num_workers=$NUM_WORKERS_DL \ +-o pipeline.init_args.steps.saver_step.init_args.save_dir=$TMP_DATA_ROOT/3dgan-generated-data \ +-o pipeline.init_args.steps.saver_step.init_args.aggregate_predictions=$AGGREGATE_PREDS +``` + ### Docker image Build from project root with ```bash # Local -docker buildx build -t itwinai-mnist-torch-inference -f use-cases/3dgan/Dockerfile.inference . +docker buildx build -t itwinai:0.0.1-3dgan-0.1 -f use-cases/3dgan/Dockerfile . # Ghcr.io -docker buildx build -t ghcr.io/intertwin-eu/itwinai-3dgan-inference:0.0.3 -f use-cases/3dgan/Dockerfile.inference . -docker push ghcr.io/intertwin-eu/itwinai-3dgan-inference:0.0.3 +docker buildx build -t ghcr.io/intertwin-eu/itwinai:0.0.1-3dgan-0.1 -f use-cases/3dgan/Dockerfile . +docker push ghcr.io/intertwin-eu/itwinai:0.0.1-3dgan-0.1 ``` -From wherever a sample of MNIST jpg images is available -(folder called 'mnist-sample-data/'): +You can run inference from wherever a sample of H5 files is available +(folder called `exp_data/`'): ```text ├── $PWD @@ -129,10 +162,10 @@ From wherever a sample of MNIST jpg images is available ``` ```bash -docker run -it --rm --name running-inference -v "$PWD":/tmp/data ghcr.io/intertwin-eu/itwinai-3dgan-inference:0.0.3 +docker run -it --rm --name running-inference -v "$PWD":/tmp/data ghcr.io/intertwin-eu/itwinai:0.0.1-3dgan-0.1 ``` -This command will store the results in a folder called "3dgan-generated-data": +This command will store the results in a folder called `3dgan-generated-data`: ```text ├── $PWD @@ -144,12 +177,86 @@ This command will store the results in a folder called "3dgan-generated-data": | │ ├── energy=1.664689540863037&angle=1.4906378984451294.jpg ``` +To override fields in the configuration file at runtime, you can use the `-o` +flag. Example: `-o path.to.config.element=NEW_VALUE`. + +Please find a complete exampled below, showing how to override default configurations +by setting some env variables: + +```bash +# Override variables +export CERN_DATA_ROOT="/usr/data" +export CERN_CODE_ROOT="/usr/src/app" +export MAX_DATA_SAMPLES=10 # max dataset size +export BATCH_SIZE=64 # increase to fill up GPU memory +export NUM_WORKERS_DL=4 # num worker processes used by the dataloader to pre-fetch data +export AGGREGATE_PREDS="true" # write predictions in a single file +export ACCELERATOR="gpu" # choose "cpu" or "gpu" + +docker run -it --rm --name running-inference \ +-v "$PWD":/usr/data ghcr.io/intertwin-eu/itwinai:0.0.1-3dgan-0.1 \ +/bin/bash -c "itwinai exec-pipeline \ +--config inference-pipeline.yaml --print-config \ +-o pipeline.init_args.steps.dataloading_step.init_args.data_path=$CERN_DATA_ROOT/exp_data \ +-o pipeline.init_args.steps.inference_step.init_args.config.trainer.logger.init_args.save_dir=$CERN_DATA_ROOT/ml_logs/mlflow_logs \ +-o pipeline.init_args.steps.inference_step.init_args.config.trainer.accelerator=$ACCELERATOR \ +-o pipeline.init_args.steps.inference_step.init_args.model.init_args.model_uri=$CERN_CODE_ROOT/3dgan-inference.pth \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.datapath=$CERN_DATA_ROOT/exp_data/*/*.h5 \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.max_samples=$MAX_DATA_SAMPLES \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.batch_size=$BATCH_SIZE \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.num_workers=$NUM_WORKERS_DL \ +-o pipeline.init_args.steps.saver_step.init_args.save_dir=$CERN_DATA_ROOT/3dgan-generated-data \ +-o pipeline.init_args.steps.saver_step.init_args.aggregate_predictions=$AGGREGATE_PREDS " +``` + +#### How to fully exploit GPU resources + +Keeping the example above as reference, increase the value of `BATCH_SIZE` as much as possible +(just below "out of memory" errors). Also, make sure that `ACCELERATOR="gpu"`. Also, make sure +to use a dataset large enough by changing the value of `MAX_DATA_SAMPLES` to collect meaningful +performance data. Consider that each H5 file contains roughly 5k items, thus setting +`MAX_DATA_SAMPLES=10000` should be enough to use all items in each input H5 file. + +You can try: + +```bash +export MAX_DATA_SAMPLES=10000 # max dataset size +export BATCH_SIZE=1024 # increase to fill up GPU memory +export ACCELERATOR="gpu +``` + ### Singularity -Run overriding the working directory (`--pwd /usr/src/app`, restores Docker's WORKDIR) -and providing a writable filesystem (`-B "$PWD":/usr/data`): +Run Docker container with Singularity: + +```bash +singularity run --nv -B "$PWD":/usr/data docker://ghcr.io/intertwin-eu/itwinai:0.0.1-3dgan-0.1 /bin/bash -c \ +"cd /usr/src/app && itwinai exec-pipeline --config inference-pipeline.yaml" +``` + +Example with overrides (as above for Docker): ```bash -singularity exec -B "$PWD":/usr/data docker://ghcr.io/intertwin-eu/itwinai-3dgan-inference:0.0.3 / -bash -c "cd /usr/src/app && python train.py -p inference-pipeline.yaml" +# Override variables +export CERN_DATA_ROOT="/usr/data" +export CERN_CODE_ROOT="/usr/src/app" +export MAX_DATA_SAMPLES=10 # max dataset size +export BATCH_SIZE=64 # increase to fill up GPU memory +export NUM_WORKERS_DL=4 # num worker processes used by the dataloader to pre-fetch data +export AGGREGATE_PREDS="true" # write predictions in a single file +export ACCELERATOR="gpu" # choose "cpu" or "gpu" + +singularity run --nv -B "$PWD":/usr/data docker://ghcr.io/intertwin-eu/itwinai:0.0.1-3dgan-0.1 /bin/bash -c \ +"cd /usr/src/app && itwinai exec-pipeline \ +--config inference-pipeline.yaml --print-config \ +-o pipeline.init_args.steps.dataloading_step.init_args.data_path=$CERN_DATA_ROOT/exp_data \ +-o pipeline.init_args.steps.inference_step.init_args.config.trainer.logger.init_args.save_dir=$CERN_DATA_ROOT/ml_logs/mlflow_logs \ +-o pipeline.init_args.steps.inference_step.init_args.config.trainer.accelerator=$ACCELERATOR \ +-o pipeline.init_args.steps.inference_step.init_args.model.init_args.model_uri=$CERN_CODE_ROOT/3dgan-inference.pth \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.datapath=$CERN_DATA_ROOT/exp_data/*/*.h5 \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.max_samples=$MAX_DATA_SAMPLES \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.batch_size=$BATCH_SIZE \ +-o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.num_workers=$NUM_WORKERS_DL \ +-o pipeline.init_args.steps.saver_step.init_args.save_dir=$CERN_DATA_ROOT/3dgan-generated-data \ +-o pipeline.init_args.steps.saver_step.init_args.aggregate_predictions=$AGGREGATE_PREDS " ``` diff --git a/use-cases/3dgan/dataloader.py b/use-cases/3dgan/dataloader.py index f21e57d9..89234895 100644 --- a/use-cases/3dgan/dataloader.py +++ b/use-cases/3dgan/dataloader.py @@ -92,7 +92,6 @@ def fetch_data(self) -> None: and len(self.data[field]) >= self.max_samples): for field, vals_array in self.data.items(): self.data[field] = vals_array[:self.max_samples] - break def GetDataAngleParallel( diff --git a/use-cases/3dgan/inference-pipeline.yaml b/use-cases/3dgan/inference-pipeline.yaml index 59d8f54f..f5125576 100644 --- a/use-cases/3dgan/inference-pipeline.yaml +++ b/use-cases/3dgan/inference-pipeline.yaml @@ -2,12 +2,14 @@ pipeline: class_path: itwinai.pipeline.Pipeline init_args: steps: - - class_path: dataloader.Lightning3DGANDownloader + dataloading_step: + class_path: dataloader.Lightning3DGANDownloader init_args: data_path: /usr/data/exp_data/ data_url: https://drive.google.com/drive/folders/1uPpz0tquokepptIfJenTzGpiENfo2xRX - - class_path: trainer.Lightning3DGANPredictor + inference_step: + class_path: trainer.Lightning3DGANPredictor init_args: model: class_path: trainer.LightningModelLoader @@ -93,10 +95,12 @@ pipeline: class_path: dataloader.ParticlesDataModule init_args: datapath: /usr/data/exp_data/*/*.h5 - batch_size: 64 - num_workers: 2 - max_samples: 10 + batch_size: 64 #1024 + num_workers: 2 #4 + max_samples: 10 #null, 10000 - - class_path: saver.ParticleImagesSaver + saver_step: + class_path: saver.ParticleImagesSaver init_args: - save_dir: /usr/data/3dgan-generated-data \ No newline at end of file + save_dir: /usr/data/3dgan-generated-data + aggregate_predictions: false \ No newline at end of file diff --git a/use-cases/3dgan/interLink/3dgan-inference-cpu.yaml b/use-cases/3dgan/interLink/3dgan-inference-cpu.yaml new file mode 100644 index 00000000..2ba3c0a8 --- /dev/null +++ b/use-cases/3dgan/interLink/3dgan-inference-cpu.yaml @@ -0,0 +1,74 @@ +apiVersion: v1 +kind: Pod +metadata: + name: 3dgan-cpu + annotations: + slurm-job.vk.io/flags: "-p gpu --gres=gpu:1 --cpus-per-task=4 --mem=100G --ntasks-per-node=1 --nodes=1" + job.vk.io/singularity-mounts: "--bind /ceph/hpc/data/st2301-itwin-users/egarciagarcia:/exp_data" + #job.vk.io/pre-exec: "singularity pull /ceph/hpc/data/st2301-itwin-users/itwinaiv6_1.sif docker://ghcr.io/intertwin-eu/itwinai:0.0.1-3dgan-0.2" +spec: + automountServiceAccountToken: false + containers: + - args: + - -c + - "\" cd /usr/src/app && itwinai exec-pipeline --print-config --config \\$CERN_CODE_ROOT/inference-pipeline.yaml \ + -o pipeline.init_args.steps.dataloading_step.init_args.data_path=\\$CERN_DATA_ROOT \ + -o pipeline.init_args.steps.inference_step.init_args.config.trainer.logger.init_args.save_dir=\\$TMP_DATA_ROOT/ml_logs/mlflow_logs \ + -o pipeline.init_args.steps.inference_step.init_args.config.trainer.strategy=\\$STRATEGY \ + -o pipeline.init_args.steps.inference_step.init_args.config.trainer.devices=\\$DEVICES \ + -o pipeline.init_args.steps.inference_step.init_args.config.trainer.accelerator=\\$ACCELERATOR \ + -o pipeline.init_args.steps.inference_step.init_args.model.init_args.model_uri=\\$CERN_CODE_ROOT/3dgan-inference.pth \ + -o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.datapath=\\$CERN_DATA_ROOT/*.h5 \ + -o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.max_samples=\\$MAX_DATA_SAMPLES \ + -o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.batch_size=\\$BATCH_SIZE \ + -o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.num_workers=\\$NUM_WORKERS_DL \ + -o pipeline.init_args.steps.saver_step.init_args.save_dir=\\$TMP_DATA_ROOT/3dgan-generated-data \ + -o pipeline.init_args.steps.saver_step.init_args.aggregate_predictions=\\$AGGREGATE_PREDS \"" + command: + - /bin/sh + env: + - name: CERN_DATA_ROOT + value: "/exp_data" + - name: CERN_CODE_ROOT + value: "/usr/src/app" + - name: TMP_DATA_ROOT + value: "/exp_data" + - name: MAX_DATA_SAMPLES + value: "5000" + - name: BATCH_SIZE + value: "1024" + - name: NUM_WORKERS_DL + value: "4" + - name: AGGREGATE_PREDS + value: "true" + - name: ACCELERATOR + value: "cpu" + - name: STRATEGY + value: "auto" + - name: DEVICES + value: "auto" + image: /ceph/hpc/data/st2301-itwin-users/itwinaiv6_1.sif + imagePullPolicy: Always + name: oscar-container + resources: + limits: + cpu: "1" + memory: 1Gi + requests: + cpu: "1" + memory: 1Gi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + nodeSelector: + kubernetes.io/hostname: vega-new-vk + tolerations: + - key: virtual-node.interlink/no-schedule + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 \ No newline at end of file diff --git a/use-cases/3dgan/interLink/3dgan-inference.yaml b/use-cases/3dgan/interLink/3dgan-inference.yaml new file mode 100644 index 00000000..4a9b4575 --- /dev/null +++ b/use-cases/3dgan/interLink/3dgan-inference.yaml @@ -0,0 +1,74 @@ +apiVersion: v1 +kind: Pod +metadata: + name: 3dgan + annotations: + slurm-job.vk.io/flags: "-p gpu --gres=gpu:1 --cpus-per-task=4 --mem=100G --ntasks-per-node=1 --nodes=1" + job.vk.io/singularity-mounts: "--bind /ceph/hpc/data/st2301-itwin-users/egarciagarcia:/exp_data" + #job.vk.io/pre-exec: "singularity pull /ceph/hpc/data/st2301-itwin-users/itwinaiv6_1.sif docker://ghcr.io/intertwin-eu/itwinai:0.0.1-3dgan-0.2" +spec: + automountServiceAccountToken: false + containers: + - args: + - -c + - "\" cd /usr/src/app && itwinai exec-pipeline --print-config --config \\$CERN_CODE_ROOT/inference-pipeline.yaml \ + -o pipeline.init_args.steps.dataloading_step.init_args.data_path=\\$CERN_DATA_ROOT \ + -o pipeline.init_args.steps.inference_step.init_args.config.trainer.logger.init_args.save_dir=\\$TMP_DATA_ROOT/ml_logs/mlflow_logs \ + -o pipeline.init_args.steps.inference_step.init_args.config.trainer.strategy=\\$STRATEGY \ + -o pipeline.init_args.steps.inference_step.init_args.config.trainer.devices=\\$DEVICES \ + -o pipeline.init_args.steps.inference_step.init_args.config.trainer.accelerator=\\$ACCELERATOR \ + -o pipeline.init_args.steps.inference_step.init_args.model.init_args.model_uri=\\$CERN_CODE_ROOT/3dgan-inference.pth \ + -o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.datapath=\\$CERN_DATA_ROOT/*.h5 \ + -o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.max_samples=\\$MAX_DATA_SAMPLES \ + -o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.batch_size=\\$BATCH_SIZE \ + -o pipeline.init_args.steps.inference_step.init_args.config.data.init_args.num_workers=\\$NUM_WORKERS_DL \ + -o pipeline.init_args.steps.saver_step.init_args.save_dir=\\$TMP_DATA_ROOT/3dgan-generated-data \ + -o pipeline.init_args.steps.saver_step.init_args.aggregate_predictions=\\$AGGREGATE_PREDS \"" + command: + - /bin/sh + env: + - name: CERN_DATA_ROOT + value: "/exp_data" + - name: CERN_CODE_ROOT + value: "/usr/src/app" + - name: TMP_DATA_ROOT + value: "/exp_data" + - name: MAX_DATA_SAMPLES + value: "5000" + - name: BATCH_SIZE + value: "2501" + - name: NUM_WORKERS_DL + value: "4" + - name: AGGREGATE_PREDS + value: "true" + - name: ACCELERATOR + value: "gpu" + - name: STRATEGY + value: "auto" + - name: DEVICES + value: "auto" + image: /ceph/hpc/data/st2301-itwin-users/itwinaiv6_1.sif + imagePullPolicy: Always + name: oscar-container + resources: + limits: + cpu: "1" + memory: 1Gi + requests: + cpu: "1" + memory: 1Gi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + nodeSelector: + kubernetes.io/hostname: vega-new-vk + tolerations: + - key: virtual-node.interlink/no-schedule + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 \ No newline at end of file diff --git a/use-cases/3dgan/interLink/README.md b/use-cases/3dgan/interLink/README.md new file mode 100644 index 00000000..d4b6dcca --- /dev/null +++ b/use-cases/3dgan/interLink/README.md @@ -0,0 +1,55 @@ +# Offloading through interLink + +This folder contains kubernetes pod examples to be used alongside with +[interLink](https://github.com/interTwin-eu/interLink), +to offload computation to remote HPC providers. + +To use these pods, you will need to install `kubectl` and setup a `kubeconfig`. + +## Manage pod + +```bash +# A pod needs to be deleted before re-submitting another one with the same name +kubectl delete pod POD_NAME +# Alternatively +kubectl apply --overwrite --force -f test.yaml + +# Submit pod +kubectl apply -f my-pod.yaml + +# Get status +kubectl get nodes +kubectl get pods + +# Get pod STDOUT +kubectl logs --insecure-skip-tls-verify-backend POD_NAME +``` + +## Pod annotations + +Allocate resources through SLURM: + +```yaml +slurm-job.vk.io/flags: "-p gpu --gres=gpu:1 --cpus-per-task=4 --mem=100G --ntasks-per-node=1 --nodes=1" +``` + +On some HPC system it may be needed to download the docker +container before submitting the offloaded job. T0 do so, you can use the +following annotation: + +```yaml +job.vk.io/pre-exec: "singularity pull /ceph/hpc/data/st2301-itwin-users/itwinaiv6_1.sif docker://ghcr.io/intertwin-eu/itwinai:0.0.1-3dgan-0.2" +``` + +IMPORTANT: add this annotation only once, when the image is not there. + +## Node selector + +To select to which remote system to offload, change the value in the node selector: + +```yaml +nodeSelector: + kubernetes.io/hostname: vega-new-vk +``` + +Additional info in [interLink](https://github.com/interTwin-eu/interLink) docs. diff --git a/use-cases/3dgan/saver.py b/use-cases/3dgan/saver.py index fd9bd710..bacf9ab7 100644 --- a/use-cases/3dgan/saver.py +++ b/use-cases/3dgan/saver.py @@ -1,6 +1,8 @@ from typing import Dict import os import shutil +import pickle +import random import torch from torch import Tensor @@ -15,11 +17,13 @@ class ParticleImagesSaver(Saver): def __init__( self, - save_dir: str = '3dgan-generated' + save_dir: str = '3dgan-generated', + aggregate_predictions: bool = False ) -> None: self.save_parameters(**self.locals2params(locals())) super().__init__() self.save_dir = save_dir + self.aggregate_predictions = aggregate_predictions @monitor_exec def execute(self, generated_images: Dict[str, Tensor]) -> None: @@ -29,15 +33,31 @@ def execute(self, generated_images: Dict[str, Tensor]) -> None: generated_images (Dict[str, Tensor]): maps unique item ID to the generated image. """ - if os.path.exists(self.save_dir): - shutil.rmtree(self.save_dir) - os.makedirs(self.save_dir) + if self.aggregate_predictions: + os.makedirs(self.save_dir, exist_ok=True) + sparse_generated_images = dict() + for name, res in generated_images.items(): + sparse_generated_images[name] = res.to_sparse() + del generated_images + with open(self._random_file(), 'wb') as fp: + pickle.dump(sparse_generated_images, fp) + else: + if os.path.exists(self.save_dir): + shutil.rmtree(self.save_dir) + os.makedirs(self.save_dir) + # Save as torch tensor and jpg image + for img_id, img in generated_images.items(): + img_path = os.path.join(self.save_dir, img_id) + torch.save(img, img_path + '.pth') + self._save_image(img, img_id, img_path + '.jpg') - # Save as torch tensor and jpg image - for img_id, img in generated_images.items(): - img_path = os.path.join(self.save_dir, img_id) - torch.save(img, img_path + '.pth') - self._save_image(img, img_id, img_path + '.jpg') + def _random_file(self, extension: str = 'pkl') -> str: + fname = "%032x.%s" % (random.getrandbits(128), extension) + fpath = os.path.join(self.save_dir, fname) + while os.path.exists(fpath): + fname = "%032x.%s" % (random.getrandbits(128), extension) + fpath = os.path.join(self.save_dir, fname) + return fpath def _save_image( self, diff --git a/use-cases/3dgan/startscript b/use-cases/3dgan/startscript index 579ce3b3..4d3e2a74 100644 --- a/use-cases/3dgan/startscript +++ b/use-cases/3dgan/startscript @@ -29,6 +29,6 @@ ml Stages/2023 StdEnv/2023 NVHPC/23.1 OpenMPI/4.1.4 cuDNN/8.6.0.163-CUDA-11.7 Py source ~/.bashrc # ON LOGIN NODE download datasets: -# $ micromamba run -p ../../.venv-pytorch python train.py -p pipeline.yaml --download-only +# $ micromamba run -p ../../.venv-pytorch itwinai exec-pipeline --config pipeline.yaml --download-only -srun micromamba run -p ../../.venv-pytorch python train.py -p pipeline.yaml \ No newline at end of file +srun micromamba run -p ../../.venv-pytorch itwinai exec-pipeline --config pipeline.yaml \ No newline at end of file diff --git a/use-cases/3dgan/train.py b/use-cases/3dgan/train.py deleted file mode 100644 index d12ee05e..00000000 --- a/use-cases/3dgan/train.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Training pipeline. To run this script, use the following commands. - -On login node: - ->>> micromamba run -p ../../.venv-pytorch/ \ - python train.py -p pipeline.yaml -d - -On compute nodes: - ->>> micromamba run -p ../../.venv-pytorch/ \ - python train.py -p pipeline.yaml - -""" - -import argparse - -from itwinai.parser import ConfigParser - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "-p", "--pipeline", type=str, required=True, - help='Configuration file to the pipeline to execute.' - ) - parser.add_argument( - '-d', '--download-only', - action=argparse.BooleanOptionalAction, - default=False, - help=('Whether to download only the dataset and exit execution ' - '(suggested on login nodes of HPC systems)') - ) - args = parser.parse_args() - - # Create parser for the pipeline - pipe_parser = ConfigParser(config=args.pipeline) - pipeline = pipe_parser.parse_pipeline() - - if args.download_only: - print('Downloading datasets and exiting...') - pipeline = pipeline[:1] - - pipeline.execute() diff --git a/use-cases/cyclones/dataloader.py b/use-cases/cyclones/dataloader.py index ee19b805..7f224157 100644 --- a/use-cases/cyclones/dataloader.py +++ b/use-cases/cyclones/dataloader.py @@ -41,7 +41,8 @@ def __init__( experiment: Dict, global_config: Dict, shuffle_buffer: int = None, - data_path: str = "tmp_data" + data_path: str = "tmp_data", + local_dataset_path: str = "trainval" ): super().__init__() self.save_parameters(**self.locals2params(locals())) @@ -58,6 +59,7 @@ def __init__( self.global_config = global_config self.shuffle = shuffle self.data_path = data_path + self.local_dataset_path = local_dataset_path self.drv_vars, self.coo_vars = ( experiment["DRV_VARS_1"], experiment["COO_VARS_1"], @@ -184,7 +186,7 @@ def setup_config(self, config: Dict) -> None: # Scalar fields self.root_dir = root_dir self.dataset_dir = join(root_dir, self.data_path, - "tfrecords", "trainval/") + self.local_dataset_path) self.scaler_file = join(config["scaler_dir"], "minmax.tfrecord") # get records filenames diff --git a/use-cases/cyclones/pipeline.yaml b/use-cases/cyclones/pipeline.yaml index 97cfc083..32e8138c 100644 --- a/use-cases/cyclones/pipeline.yaml +++ b/use-cases/cyclones/pipeline.yaml @@ -5,7 +5,7 @@ pipeline: download-step: class_path: dataloader.TensorflowDataGetter init_args: - data_url: https://drive.google.com/drive/folders/15DEq33MmtRvIpe2bNCg44lnfvEiHcPaf + data_url: https://drive.google.com/drive/folders/1TnmujO4T-8_j4bCxqNe5HEw9njJIIBQD #https://drive.google.com/drive/folders/15DEq33MmtRvIpe2bNCg44lnfvEiHcPaf patch_type: NEAREST shuffle: False split_ratio: [0.75, 0.25] From 150bcef8786652f69b8a8b988540ff76dec10675 Mon Sep 17 00:00:00 2001 From: Pablo Orviz Date: Fri, 22 Mar 2024 14:15:18 +0100 Subject: [PATCH 3/8] Unit test 4 dev (#113) * Define a step for pytest execution * Fix: use v1 of step action * Print result of step composition * Rename step * Use step previous definition in the assessment * Rename input: workflow -> steps * Avoid caching by using 1.0.0 * Set container image * Bump to v1 * Bump to sqaaas-assessment-action@v2 * Remove 'id' property * Adapt inputs to v2 * Remove current branch * Disable test_cyclones_train_tf * ADD marker * ADD skip memory heavy * Disable for PRs --------- Co-authored-by: Matteo Bunino --- .github/workflows/sqaaas.yml | 21 ++++++++++++++++++--- pyproject.toml | 1 + tests/use-cases/test_cyclones.py | 1 + 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sqaaas.yml b/.github/workflows/sqaaas.yml index 3e2bb7b2..e1dd4258 100644 --- a/.github/workflows/sqaaas.yml +++ b/.github/workflows/sqaaas.yml @@ -7,13 +7,28 @@ name: SQAaaS on: push: branches: [main, dev] - pull_request: - branches: [main, dev] + # pull_request: + # branches: [main, dev] jobs: sqaaas_job: runs-on: ubuntu-latest name: Job that triggers SQAaaS platform steps: - - name: SQAaaS assessment step + - name: Step definition for validating the workflow + uses: eosc-synergy/sqaaas-step-action@v1 + with: + name: workflow_validation_step + tool: commands + commands: | + make torch-cpu + make tf-2.13 + micromamba run -p ./.venv-pytorch pip install .[dev] + micromamba run -p ./.venv-pytorch pytest -v ./tests/ -m "not slurm and not memory_heavy" + container: eoscsynergy/sqaaas-micromamba:1.5.3-1 + - name: Print out payload + run: cat workflow_validation_step.json + - name: SQAaaS assessment with unit testing (QC.Uni) step uses: eosc-synergy/sqaaas-assessment-action@v2 + with: + qc_uni_steps: workflow_validation_step diff --git a/pyproject.toml b/pyproject.toml index 5e93f3ec..dd6408c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,4 +75,5 @@ markers = [ "integration: integration tests (deselect with '-m \"not integration\"')", "slurm: needs SLURM and HPC resources (multiple GPUs/nodes). (deselect with '-m \"not slurm\"')", "functional: functional tests. (deselect with '-m \"not functional\"')", + "memory_heavy: high memory footprint. (deselect with '-m \"not heavy_memory\"')", ] diff --git a/tests/use-cases/test_cyclones.py b/tests/use-cases/test_cyclones.py index 6b11db1e..1a5ebb3f 100644 --- a/tests/use-cases/test_cyclones.py +++ b/tests/use-cases/test_cyclones.py @@ -17,6 +17,7 @@ def test_structure_cyclones(check_folder_structure): @pytest.mark.functional +@pytest.mark.memory_heavy def test_cyclones_train_tf(install_requirements): """ Test Cyclones tensorflow trainer by running it end-to-end. From e8c1ed6e43bbd957cee5aa8c68d27f9799319d68 Mon Sep 17 00:00:00 2001 From: Matteo Bunino <48362942+matbun@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:04:01 +0200 Subject: [PATCH 4/8] Distributed strategy launcher (#117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ADD: distrib launcher mockup * REFACTOR: cluster env, strategy and launcher * ADD: Torch Elastic Launcher * ADD: info on env vars * ADD: distributed tooling and examples * new folder * UPDATE: distributed strategy setup * generalized for DDP and DS * add config file * UPDATE: kwargs * Update general_trainer.py * Update general_startscript * Update general_trainer.py * UPDATE .gitignore * Update distrib strategy * UPDATE torch distributed strategy classes * Updated docstrings * Small fixes * UPDATE docstrings * ADD deepespeed config loader * ADD first deepspeed tutorial draft * UPDATE DDP Dp distrib strategy * UPDATE horovod strategy * UPDATE tutorial on torch distributed strategies * UPDATE torch strategies tutorial * Update createEnvJSC.sh * Update hvd_slurm.sh * Update README.md * UPDATE distributed tutorial * Delete tutorials/distributed-ml/torch-ddp-deepspeed-horovod/0 * Fixes to deepspeed startscript * Update distributed.py * Update trainer.py * UPDATE tutorial * ADD draft MNIST tutorial * UPDATE DDP tutorial for MNIST * FIX small details * Update distributed.py * Added TF tutorials * Fixes to tutorials * Add files via upload * Update Makefile * Update README.md * UPDATE tutorials * UPDATE documentation and improve explainability * UPDATE SLURM scripts * FIX local rank mismatch * fixed distributed trainer in cyclones use case * UPDATE launcher * UPDATE linter * UPDATE format * FIX linter * FIX linter * Update workflow * UPDATE workflow * update * Update workflow * UPDATE super linter to v6 * UPDATE super linter to v6.3.0 * UPDATE super linter to slim * Cleanup * Update tfmirrored_slurm.sh * Update tfmirrored_slurm.sh * REMOVE workflows legacy * DELETE cyclegan use case * UPDATE dist training tutorials torch * RENAME folders with torch * DRAFT torch imagenet tutorial * UPDATE configuration * UPDATE imagenet tutorial * DRAFT scaling test * ADD scaling analysis report * FIX deepspeed micro batchsize * UPDATE data path * UPDATE checkpoint to avoid race conditions * UPDATE scalability report * UPDATE dataset path * Update createEnvJSC.sh * Update createEnvJSC.sh * Update createEnvJSC.sh * Update createEnvJSC.sh * Update createEnvJSC.sh * Update createEnvJSCTF.sh * Update README.md * Update README.md * JUBE benchmarks * Update createEnvJSC.sh * Update createEnvJSCTF.sh * ADD logy scale option * Extract JUBE tutorial * CLEANUP baselines * Log epoch time in real-time * FIX deepspeed dataloader for potential performances improvement * UPDATE SC bash severity * FIX deepspeed and horovod trainers * FIX some code checks * Unify redundant SLURM job scripts and configuration files * CLEANUP unused configuration * Reorg configurations * Refactor configurations and add documentation * Update README * ADD report image * Improve plot resolution * UPDATE scaling test * UPDATE launcher scripts * FIX linter * REMOVE jube tutorial --------- Co-authored-by: Mario Rüttgers Co-authored-by: r-sarma <126173968+r-sarma@users.noreply.github.com> Co-authored-by: r-sarma Co-authored-by: zoechbauer1 --- .github/linters/.jscpd.json | 3 +- .github/workflows/lint.yml | 8 +- .gitignore | 32 +- .vscode/settings.json | 7 +- Makefile | 11 +- README.md | 11 +- env-files/tensorflow/createEnvJSCTF.sh | 108 ++ env-files/torch/createEnvJSC.sh | 191 ++++ env-files/torch/pytorch-env-gpu.yml | 1 + experimental/cluster.py | 97 ++ experimental/distrib_launcher.py | 117 +++ experimental/distributed_tools.py | 68 ++ experimental/example_0.py | 125 +++ experimental/example_1.py | 106 ++ experimental/example_2.py | 107 ++ experimental/example_3.py | 77 ++ experimental/launcher.py | 295 ++++++ experimental/launcher_factory.py | 144 +++ experimental/strategy.py | 150 +++ experimental/trainer/DS_config.json | 15 + experimental/trainer/general_startscript | 135 +++ experimental/trainer/general_trainer.py | 482 +++++++++ pyproject.toml | 26 +- src/itwinai/cli.py | 142 +++ src/itwinai/distributed.py | 5 + src/itwinai/loggers.py | 27 + src/itwinai/parser.py | 188 +--- src/itwinai/tensorflow/distributed.py | 36 + src/itwinai/tensorflow/trainer.py | 16 + src/itwinai/torch/distributed.py | 920 ++++++++++++++++++ src/itwinai/torch/engine.py | 276 ++++++ src/itwinai/torch/trainer.py | 6 + src/itwinai/torch/types.py | 2 + tests/components/test_components.py | 2 +- .../tf-tutorial-0-basics/README.md | 20 + .../tf-tutorial-0-basics/tfmirrored_slurm.sh | 62 ++ .../tf-tutorial-0-basics/train.py | 109 +++ .../torch-scaling-test/README.md | 113 +++ .../torch-scaling-test/config/base.yaml | 16 + .../torch-scaling-test/config/ddp.yaml | 1 + .../torch-scaling-test/config/deepspeed.yaml | 1 + .../torch-scaling-test/config/horovod.yaml | 3 + .../torch-scaling-test/ddp_trainer.py | 269 +++++ .../torch-scaling-test/deepspeed_trainer.py | 283 ++++++ .../torch-scaling-test/horovod_trainer.py | 318 ++++++ .../torch-scaling-test/img/report.png | Bin 0 -> 45730 bytes .../torch-scaling-test/itwinai_trainer.py | 339 +++++++ .../torch-scaling-test/runall.sh | 61 ++ .../torch-scaling-test/scaling-test.sh | 10 + .../torch-scaling-test/slurm.sh | 123 +++ .../torch-scaling-test/utils.py | 55 ++ .../torch-tutorial-0-basics/README.md | 45 + .../torch-tutorial-0-basics/ddp_slurm.sh | 66 ++ .../deepspeed_slurm.sh | 75 ++ .../torch-tutorial-0-basics/hvd_slurm.sh | 60 ++ .../torch-tutorial-0-basics/runall.sh | 6 + .../torch-tutorial-0-basics/train.py | 143 +++ .../torch-tutorial-1-mnist/README.md | 55 ++ .../torch-tutorial-1-mnist/config.yaml | 26 + .../torch-tutorial-1-mnist/ddp_slurm.sh | 66 ++ .../torch-tutorial-1-mnist/deepspeed_slurm.sh | 74 ++ .../torch-tutorial-1-mnist/hvd_slurm.sh | 60 ++ .../torch-tutorial-1-mnist/runall.sh | 6 + .../torch-tutorial-1-mnist/train.py | 546 +++++++++++ .../torch-tutorial-2-imagenet/README.md | 47 + .../torch-tutorial-2-imagenet/config.yaml | 25 + .../torch-tutorial-2-imagenet/ddp_slurm.sh | 66 ++ .../deepspeed_slurm.sh | 74 ++ .../torch-tutorial-2-imagenet/hvd_slurm.sh | 60 ++ .../torch-tutorial-2-imagenet/runall.sh | 6 + .../torch-tutorial-2-imagenet/scaling-test.sh | 11 + .../torch-tutorial-2-imagenet/train.py | 499 ++++++++++ use-cases/zebra2horse/cyclegan.py | 507 ---------- use-cases/zebra2horse/dataloader.py | 85 -- use-cases/zebra2horse/pipeline.yaml | 47 - use-cases/zebra2horse/pix2pix.py | 111 --- use-cases/zebra2horse/requriements.txt | 1 - use-cases/zebra2horse/startscript | 32 - use-cases/zebra2horse/train.py | 22 - use-cases/zebra2horse/trainer.py | 47 - workflows/README.md | 5 - workflows/cwl/README.md | 0 workflows/snakemake/README.md | 0 83 files changed, 7515 insertions(+), 1077 deletions(-) create mode 100644 env-files/tensorflow/createEnvJSCTF.sh create mode 100644 env-files/torch/createEnvJSC.sh create mode 100644 experimental/cluster.py create mode 100644 experimental/distrib_launcher.py create mode 100644 experimental/distributed_tools.py create mode 100644 experimental/example_0.py create mode 100644 experimental/example_1.py create mode 100644 experimental/example_2.py create mode 100644 experimental/example_3.py create mode 100644 experimental/launcher.py create mode 100644 experimental/launcher_factory.py create mode 100644 experimental/strategy.py create mode 100644 experimental/trainer/DS_config.json create mode 100755 experimental/trainer/general_startscript create mode 100755 experimental/trainer/general_trainer.py create mode 100644 src/itwinai/distributed.py create mode 100644 src/itwinai/tensorflow/distributed.py create mode 100644 src/itwinai/torch/distributed.py create mode 100644 src/itwinai/torch/engine.py create mode 100644 tutorials/distributed-ml/tf-tutorial-0-basics/README.md create mode 100644 tutorials/distributed-ml/tf-tutorial-0-basics/tfmirrored_slurm.sh create mode 100644 tutorials/distributed-ml/tf-tutorial-0-basics/train.py create mode 100644 tutorials/distributed-ml/torch-scaling-test/README.md create mode 100644 tutorials/distributed-ml/torch-scaling-test/config/base.yaml create mode 100644 tutorials/distributed-ml/torch-scaling-test/config/ddp.yaml create mode 100644 tutorials/distributed-ml/torch-scaling-test/config/deepspeed.yaml create mode 100644 tutorials/distributed-ml/torch-scaling-test/config/horovod.yaml create mode 100755 tutorials/distributed-ml/torch-scaling-test/ddp_trainer.py create mode 100644 tutorials/distributed-ml/torch-scaling-test/deepspeed_trainer.py create mode 100755 tutorials/distributed-ml/torch-scaling-test/horovod_trainer.py create mode 100644 tutorials/distributed-ml/torch-scaling-test/img/report.png create mode 100644 tutorials/distributed-ml/torch-scaling-test/itwinai_trainer.py create mode 100644 tutorials/distributed-ml/torch-scaling-test/runall.sh create mode 100644 tutorials/distributed-ml/torch-scaling-test/scaling-test.sh create mode 100644 tutorials/distributed-ml/torch-scaling-test/slurm.sh create mode 100644 tutorials/distributed-ml/torch-scaling-test/utils.py create mode 100644 tutorials/distributed-ml/torch-tutorial-0-basics/README.md create mode 100644 tutorials/distributed-ml/torch-tutorial-0-basics/ddp_slurm.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-0-basics/deepspeed_slurm.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-0-basics/hvd_slurm.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-0-basics/runall.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-0-basics/train.py create mode 100644 tutorials/distributed-ml/torch-tutorial-1-mnist/README.md create mode 100644 tutorials/distributed-ml/torch-tutorial-1-mnist/config.yaml create mode 100644 tutorials/distributed-ml/torch-tutorial-1-mnist/ddp_slurm.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-1-mnist/deepspeed_slurm.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-1-mnist/hvd_slurm.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-1-mnist/runall.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-1-mnist/train.py create mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/README.md create mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/config.yaml create mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/ddp_slurm.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/deepspeed_slurm.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/hvd_slurm.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/runall.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/scaling-test.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/train.py delete mode 100644 use-cases/zebra2horse/cyclegan.py delete mode 100644 use-cases/zebra2horse/dataloader.py delete mode 100644 use-cases/zebra2horse/pipeline.yaml delete mode 100644 use-cases/zebra2horse/pix2pix.py delete mode 100644 use-cases/zebra2horse/requriements.txt delete mode 100644 use-cases/zebra2horse/startscript delete mode 100644 use-cases/zebra2horse/train.py delete mode 100644 use-cases/zebra2horse/trainer.py delete mode 100644 workflows/README.md delete mode 100644 workflows/cwl/README.md delete mode 100644 workflows/snakemake/README.md diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json index 8a003c54..1a035770 100644 --- a/.github/linters/.jscpd.json +++ b/.github/linters/.jscpd.json @@ -1,6 +1,7 @@ { "threshold": 2.0, "ignore": [ - "**/itwinai/loggers.py" + "**/itwinai/loggers.py", + "**/itwinai/torch/engine.py" ] } \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ecadafb0..8eca0a3c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,7 +30,7 @@ jobs: # Runs the Super-Linter action - name: Run Super-Linter on new changes - uses: github/super-linter/slim@v5 + uses: super-linter/super-linter/slim@v6.3.0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -43,10 +43,14 @@ jobs: VALIDATE_PYTHON_PYLINT: false VALIDATE_HTML: false VALIDATE_GITLEAKS: false + VALIDATE_BASH_EXEC: false + VALIDATE_CHECKOV: false # activate to lint k8s pods + VALIDATE_SHELL_SHFMT: false # Only check new or edited files VALIDATE_ALL_CODEBASE: false # Fail on errors DISABLE_ERRORS: false # Skip linting of docs - FILTER_REGEX_EXCLUDE: .*docs/index.md|.*docs/docs/.*|.*ISSUE_TEMPLATE/.*|use-cases/.* + FILTER_REGEX_EXCLUDE: .*docs/index.md|.*docs/docs/.*|.*ISSUE_TEMPLATE/.*|use-cases/.*|experimental/.* + BASH_SEVERITY: error diff --git a/.gitignore b/.gitignore index 2f0ad142..dd495607 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,44 @@ +s*.png +*.pdf *_logs -exp_data/ +logs_* TODO /data nohup* -lightning_logs -mlruns tmp* .tmp* checkpoints/ mamba* -MNIST -mllogs -*.out -*.err -.logs/ pl-training.yml *-predictions/ *-data/ -*.pth *.tar.gz +*.pth +*.csv +*tar.gz +0 +*.tar + +# Use cases files +MNIST +3dgan-generated-data/ +mnist-sample-data/ +exp_data/ + # Custom envs .venv* +envAI_* # Logs logs/ +ml_logs/ +mllogs/ +*.out +*.err +.logs/ +lightning_logs/ +mlruns/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 38dc1230..6f581e8c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,4 @@ { - "python.linting.flake8Enabled": true, - "python.linting.enabled": true, "editor.formatOnSave": true, "editor.defaultFormatter": null, "cSpell.ignoreWords": [ @@ -16,6 +14,7 @@ "fromlist", "hyperparameters", "hyperparams", + "imagenet", "ipython", "itwinai", "Lockfiles", @@ -55,10 +54,6 @@ "[python]": { "editor.defaultFormatter": "ms-python.autopep8" }, - "python.formatting.provider": "none", - "[markdown]": { - "editor.formatOnSave": false - }, "python.testing.pytestArgs": [ "tests" ], diff --git a/Makefile b/Makefile index 377e6df4..52183fd2 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,16 @@ # Install PyTorch env (GPU support) torch-gpu: env-files/torch/pytorch-env-gpu.yml micromamba env create -p ./.venv-pytorch --file env-files/torch/pytorch-env-gpu.yml -y - micromamba run -p ./.venv-pytorch python -m pip install -e .[dev] + micromamba run -p ./.venv-pytorch python -m pip install -e .[distributed,dev] + +# Install PyTorch env (GPU support) on Juelich Super Computer (tested on HDFML system) +torch-gpu-jsc: env-files/torch/createEnvJSC.sh + sh env-files/torch/createEnvJSC.sh + +# Install Tensorflow env (GPU support) on Juelich Super Computer (tested on HDFML system) +tf-gpu-jsc: env-files/tensorflow/createEnvJSCTF.sh + sh env-files/tensorflow/createEnvJSCTF.sh + # Install PyTorch env (CPU only) torch-cpu: env-files/torch/pytorch-env-cpu.yml diff --git a/README.md b/README.md index b0f1d66d..dc9a60dc 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,9 @@ [![GitHub Super-Linter](https://github.com/interTwin-eu/T6.5-AI-and-ML/actions/workflows/check-links.yml/badge.svg)](https://github.com/marketplace/actions/markdown-link-check) [![SQAaaS source code](https://github.com/EOSC-synergy/itwinai.assess.sqaaas/raw/dev/.badge/status_shields.svg)](https://sqaaas.eosc-synergy.eu/#/full-assessment/report/https://raw.githubusercontent.com/eosc-synergy/itwinai.assess.sqaaas/dev/.report/assessment_output.json) -See the latest version of our [docs](https://intertwin-eu.github.io/T6.5-AI-and-ML/) +See the latest version of our [docs](https://intertwin-eu.github.io/itwinai/) for a quick overview of this platform for advanced AI/ML workflows in digital twin applications. -If you want to integrate a new use case, you can follow this -[step-by-step guide](https://intertwin-eu.github.io/T6.5-AI-and-ML/docs/How-to-use-this-software.html). - ## Installation Requirements: @@ -21,7 +18,7 @@ Requirements: To manage Conda environments we use micromamba, a light weight version of conda. It is suggested to refer to the -[Manual installation guide](https://mamba.readthedocs.io/en/latest/micromamba-installation.html#umamba-install). +[Manual installation guide](https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html#manual-installation). Consider that Micromamba can eat a lot of space when building environments because packages are cached on the local filesystem after being downloaded. To clear cache you can use `micromamba clean -a`. @@ -44,12 +41,12 @@ MAMBA_ROOT_PREFIX='my-mamba-root' echo 'PATH="$(dirname $MAMBA_EXE):$PATH"' >> ~/.bashrc ``` -**Reference**: [Micromamba installation guide](https://mamba.readthedocs.io/en/latest/installation.html#micromamba). +**Reference**: [Micromamba installation guide](https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html). ### Documentation folder Documentation for this repository is maintained under `./docs` location. -If you are using code from a previous release, you can build the docs webpage +If you are using code from a previous release, you can build the docs web page locally using [these instructions](docs/README#building-and-previewing-your-site-locally). ## Environment setup diff --git a/env-files/tensorflow/createEnvJSCTF.sh b/env-files/tensorflow/createEnvJSCTF.sh new file mode 100644 index 00000000..8838347c --- /dev/null +++ b/env-files/tensorflow/createEnvJSCTF.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# -*- coding: utf-8 -*- +# author: RS +# version: 220302a +# creates machine specific python env + +# set modules +ml --force purge + +# get sys info +cDir=$PWD +sysN="$(uname -n | cut -f2- -d.)" +echo "system:${sysN}" +echo + +cont1=false +if [ "$sysN" = 'deepv' ] ; then + ml use "$OTHERSTAGES" + ml Stages/2022 GCC OpenMPI cuDNN NCCL Python CMake + cont1=true +elif [ "$sysN" = 'juwels' ] ; then + ml Stages/2022 GCC ParaStationMPI Python CMake NCCL libaio cuDNN + cont1=true +elif [ "$sysN" = 'hdfml' ] ; then + #ml Stages/2022 GCC OpenMPI Python NCCL cuDNN libaio CMake + #ml Stages/2023 NVHPC/23.1 ParaStationMPI/5.8.0-1-mt NCCL/default-CUDA-11.7 cuDNN/8.6.0.163-CUDA-11.7 Python CMake + ml Stages/2024 GCC/12.3.0 OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py CMake cuDNN/8.9.5.29-CUDA-12 + cont1=true +else + echo + echo 'unknown system detected' + echo 'canceling' + echo +fi +echo "modules loaded" +echo + +# get python version +pver="$(python --version 2>&1 | awk '{print $2}' | cut -f1-2 -d.)" +echo "python version is ${pver}" +echo + +if [ "$cont1" = true ] ; then + if [ -d "${cDir}/envAItf_${sysN}" ];then + echo 'env already exist' + echo + + source envAItf_${sysN}/bin/activate + else + # create env + python3 -m venv envAItf_${sysN} + + # get headers for pip + if [ -f "${cDir}/envAItf_${sysN}/bin/pip3" ]; then + echo 'pip already exist' + else + cp "$(which pip3)" $cDir/envAItf_${sysN}/bin/ + ln -s $cDir/envAItf_${sysN}/bin/pip3 $cDir/envAItf_${sysN}/bin/pip${pver} + var="#!$cDir/envAItf_${sysN}/bin/python${pver}" + sed -i "1s|.*|$var|" $cDir/envAItf_${sysN}/bin/pip3 + fi + + # activate env + source envAItf_${sysN}/bin/activate + + echo "a new env is created in ${cDir}" + echo "activation is done via:" + echo "source ${cDir}/envAItf_${sysN}/bin/activate" + fi +fi + +# install TF +if [ -f "${cDir}/envAItf_${sysN}/bin/tensorboard" ]; then + echo 'TF already installed' + echo +else + export TMPDIR=${cDir} + + pip3 install --upgrade tensorflow[and-cuda] --no-cache-dir +fi + +# install horovod +if [ -f "${cDir}/envAItf_${sysN}/bin/horovodrun" ]; then + echo 'Horovod already installed' + echo +else + export HOROVOD_GPU=CUDA + export HOROVOD_GPU_OPERATIONS=NCCL + export HOROVOD_WITH_TENSORFLOW=1 + export TMPDIR=${cDir} + + pip3 install --no-cache-dir horovod --ignore-installed +fi + +# JUBE benchmarking environment +if [ -f "${cDir}/envAI_${sysN}/bin/jube" ]; then + echo 'JUBE already installed' +else + pip3 install --no-cache-dir http://apps.fz-juelich.de/jsc/jube/jube2/download.php?version=latest +fi + +# get rest of the libraries$ +if [ "$cont1" = true ] ; then + pip3 install -r reqs_TF.txt --ignore-installed +fi + + +# eof diff --git a/env-files/torch/createEnvJSC.sh b/env-files/torch/createEnvJSC.sh new file mode 100644 index 00000000..6b0fa226 --- /dev/null +++ b/env-files/torch/createEnvJSC.sh @@ -0,0 +1,191 @@ +#!/bin/bash +# -*- coding: utf-8 -*- +# author: EI, RS, Matteo Bunino + +# set dir +cDir=$PWD + +# environmental variables +mkdir -p tmp +export TMPDIR=${cDir}/tmp # set tmp dir env var + +# get sys info +sysN="$(uname -n | cut -f2- -d.)" +sysN="${sysN%%[0-9]*}" + +# load modules +ml Stages/2024 GCC OpenMPI CUDA/12 cuDNN MPI-settings/CUDA +ml Python CMake HDF5 PnetCDF libaio mpi4py +# echo "these modules are loaded:" +# ml + +# get python version +pver="$(python --version 2>&1 | awk '{print $2}' | cut -f1-2 -d.)" + +# use pyenv if exist +if [ -d "$HOME/.pyenv" ];then + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" +fi + +# create environment +if [ -d "${cDir}/envAI_${sysN}" ];then + echo 'env already exist' + + source envAI_${sysN}/bin/activate +else + python3 -m venv envAI_${sysN} + + # activate env + source envAI_${sysN}/bin/activate + + echo "envAI_${sysN} environment is created in ${cDir}" +fi + +# get wheel -- setuptools extension +pip3 install --no-cache-dir wheel + +# install Torch +if [ -f "${cDir}/envAI_${sysN}/bin/torchrun" ]; then + echo 'Torch already installed' +else + pip3 install --no-cache-dir \ + torch==2.1.0+cu121 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 +fi + +# HPO - RayTune +if [ -f "${cDir}/envAI_${sysN}/bin/ray" ]; then + echo 'Ray already installed' +else + pip3 install --no-cache-dir ray ray[tune] +fi + +# install deepspeed +if [ -f "${cDir}/envAI_${sysN}/bin/deepspeed" ]; then + echo 'DeepSpeed already installed' +else + export DS_BUILD_CCL_COMM=1 + export DS_BUILD_UTILS=1 + export DS_BUILD_AIO=1 + export DS_BUILD_FUSED_ADAM=1 + export DS_BUILD_FUSED_LAMB=1 + export DS_BUILD_TRANSFORMER=1 + export DS_BUILD_STOCHASTIC_TRANSFORMER=1 + export DS_BUILD_TRANSFORMER_INFERENCE=1 + + # this will pass + pip3 install --no-cache-dir DeepSpeed + + # fix .triton/autotune/Fp16Matmul_2d_kernel.pickle bug + line=$(cat -n envAI_${sysN}/lib/python${pver}/site-packages/deepspeed/ops/transformer/inference/triton/matmul_ext.py | grep os.rename | awk '{print $1}' | head -n 1) + sed -i "${line}s|^|#|" envAI_${sysN}/lib/python${pver}/site-packages/deepspeed/ops/transformer/inference/triton/matmul_ext.py +fi + +# # install heat +# if [ -d "${cDir}/envAI_${sysN}/lib/python${pver}/site-packages/heat" ]; then +# echo 'HeAT already installed' +# else +# # need to modify setup.py to accep torch>2.1 for heat +# git clone --recurse-submodules https://github.com/helmholtz-analytics/heat.git +# line=$(cat -n heat/setup.py | grep torch | awk '{print $1}' | head -n 1) +# var=' "torch>=2.1.0",' +# sed -i "${line}s|.*|$var|" heat/setup.py + +# # create tar! +# rm -rf heat.tar.gz +# tar czf heat.tar.gz heat + +# # install +# pip3 install --no-cache-dir 'heat.tar.gz[hdf5,netcdf]' +# fi + +# install horovod +if [ -f "${cDir}/envAI_${sysN}/bin/horovodrun" ]; then + echo 'Horovod already installed' +else + # compiler vars + export LDSHARED="$CC -shared" && + export CMAKE_CXX_STANDARD=17 + + # CPU vars + export HOROVOD_MPI_THREADS_DISABLE=1 + export HOROVOD_CPU_OPERATIONS=MPI + + # GPU vars + export HOROVOD_GPU_ALLREDUCE=NCCL + export HOROVOD_NCCL_LINK=SHARED + export HOROVOD_NCCL_HOME=$EBROOTNCCL + + # Host language vars + export HOROVOD_WITH_PYTORCH=1 + export HOROVOD_WITHOUT_TENSORFLOW=1 + export HOROVOD_WITHOUT_MXNET=1 + + # need to modify for torch 2.1.0 + git clone --recurse-submodules https://github.com/horovod/horovod.git + line=$(cat -n horovod/CMakeLists.txt | grep CMAKE_CXX_STANDARD | awk '{print $1}' | head -n 1) + var='set(CMAKE_CXX_STANDARD 17)' + sed -i "${line}s|.*|$var|" horovod/CMakeLists.txt + line=$(cat -n horovod/horovod/torch/CMakeLists.txt | grep CMAKE_CXX_STANDARD | awk '{print $1}' | head -n 1) + var=' set(CMAKE_CXX_STANDARD 17)' + sed -i "${line}s|.*|$var|" horovod/horovod/torch/CMakeLists.txt + + # create tar! + rm -rf horovod.tar.gz + tar czf horovod.tar.gz horovod + + # install + pip3 install --no-cache-dir horovod.tar.gz +fi + +# get required libraries in reqs.txt +if [ -f "${cDir}/envAI_${sysN}/lib/python${pver}/site-packages/torchnlp/_third_party/weighted_random_sampler.py" ]; then + echo 'required libs already exist' +else + pip3 install -r Scripts/reqs.txt --no-cache-dir + + # fix int bug: modify l.4 of /torchnlp/_third_party/weighted_random_sampler.py + var='int_classes = int' + sed -i "4s|.*|$var|" \ + ${cDir}/envAI_${sysN}/lib/python${pver}/site-packages/torchnlp/_third_party/weighted_random_sampler.py +fi + +# fix IB IP config - FZJ specific +if [ -f "${cDir}/envAI_${sysN}/bin/torchrun" ]; then + sed -i -e '5,100s/^/#/' ${cDir}/envAI_${sysN}/bin/torchrun + echo """ +import re +import sys +from torch.distributed.run import main +from torch.distributed.elastic.agent.server import api as sapi + +def new_get_fq_hostname(): + return _orig_get_fq_hostname().replace('.', 'i.', 1) + +if __name__ == '__main__': + _orig_get_fq_hostname = sapi._get_fq_hostname + sapi._get_fq_hostname = new_get_fq_hostname + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) +""" >> ${cDir}/envAI_${sysN}/bin/torchrun +fi + +# JUBE benchmarking environment +if [ -f "${cDir}/envAI_${sysN}/bin/jube" ]; then + echo 'JUBE already installed' +else + pip3 install --no-cache-dir http://apps.fz-juelich.de/jsc/jube/jube2/download.php?version=latest +fi + +# some tests +echo "unit tests:" +for item in 'torch' 'deepspeed' 'horovod';do + python3 -c "import $item; print('$item version:',$item.__version__)" +done + +# Install itwinai +pip install --upgrade pip +pip install -e . + +# cleanup +rm -rf horovod *.tar.gz diff --git a/env-files/torch/pytorch-env-gpu.yml b/env-files/torch/pytorch-env-gpu.yml index 1c82cc30..6352cd0b 100644 --- a/env-files/torch/pytorch-env-gpu.yml +++ b/env-files/torch/pytorch-env-gpu.yml @@ -16,3 +16,4 @@ dependencies: - cudatoolkit=10.1 - lightning=2.0.0 - torchmetrics + - cuda-compiler diff --git a/experimental/cluster.py b/experimental/cluster.py new file mode 100644 index 00000000..78ae8ead --- /dev/null +++ b/experimental/cluster.py @@ -0,0 +1,97 @@ +import abc +import os +import time + +from lightning.pytorch.plugins.environments import ( + ClusterEnvironment as LightningClusterEnvironment, + SLURMEnvironment as LightningSLURMEnvironment, + TorchElasticEnvironment as LightningTorchElasticEnvironment, + LightningEnvironment +) + + +class ClusterEnvironment(LightningClusterEnvironment): + @abc.abstractmethod + def num_nodes(self) -> int: + """Returns the number of nodes allocated for the current job.""" + + @abc.abstractmethod + def job_id(self) -> str: + """Returns the current job ID inferred from the cluster.""" + + +class SLURMEnvironment(LightningSLURMEnvironment): + def num_nodes(self) -> int: + """Returns the number of nodes allocated for the current job.""" + if os.environ.get('SLURM_JOB_NUM_NODES'): + return int(os.environ['SLURM_JOB_NUM_NODES']) + if os.environ.get('SLURM_NNODES'): + return int(os.environ['SLURM_NNODES']) + raise RuntimeError('Number of nodes not found in SLURM env variables') + + def job_id(self) -> str: + """Returns the current job ID inferred from the cluster.""" + return os.environ['SLURM_JOB_ID'] + + +class TorchElasticEnvironment(LightningTorchElasticEnvironment): + def num_nodes(self) -> int: + """Returns the number of nodes allocated for the current job.""" + gwsize = int(os.environ['WORLD_SIZE']) + lwsize = int(os.environ['LOCAL_WORLD_SIZE']) + return gwsize//lwsize + + def job_id(self) -> str: + """Returns the current job ID inferred from the cluster.""" + return os.environ['TORCHELASTIC_RUN_ID'] + + +class LocalEnvironment(LightningEnvironment): + + _job_id: str = None + + def world_size(self) -> int: + # if os.environ.get('WORLD_SIZE'): + # return int(os.environ.get('WORLD_SIZE')) + print( + "WARNING: world_size() method in 'LocalEnvironment' returns " + f"a fixed-value placeholder world_size={self._world_size}. " + "Use it carefully!" + ) + return self._world_size + + def global_rank(self) -> int: + # if os.environ.get('RANK'): + # return int(os.environ.get('RANK')) + print( + "WARNING: global_rank() method in 'LocalEnvironment' returns " + f"a fixed-value placeholder global_rank={self._global_rank}. " + "Use it carefully!" + ) + return self._global_rank + + def num_nodes(self) -> int: + """Returns the number of nodes allocated for the current job.""" + return 1 + + def job_id(self) -> str: + """Returns the current job ID inferred from the cluster.""" + if self._job_id is None: + self._job_id = str(time.time()) + return self._job_id + + +def detect_cluster() -> ClusterEnvironment: + """Defines a protocol to select the ClusterEnvironment + depending on availability and priority. + """ + + if SLURMEnvironment.detect(): + cluster = SLURMEnvironment() + elif TorchElasticEnvironment.detect(): + cluster = TorchElasticEnvironment() + elif LocalEnvironment.detect(): + cluster = LocalEnvironment() + else: + raise NotImplementedError("Unrecognized cluster env") + return cluster diff --git a/experimental/distrib_launcher.py b/experimental/distrib_launcher.py new file mode 100644 index 00000000..d8f4e881 --- /dev/null +++ b/experimental/distrib_launcher.py @@ -0,0 +1,117 @@ +import os + +import torch +from torch import nn +from torch.utils.data import DataLoader, Dataset + +from strategy import Strategy, DDPStrategy +from launcher import DummyTorchElasticLauncher, TorchElasticLauncher +from launcher_factory import ( + LauncherFactory, + SimpleLauncherFactory, + TorchElasticLauncherFactory +) +from distributed_tools import DistributedTooling + + +class UniformRndDataset(Dataset): + def __init__(self, x_size: int, y_size: int, len: int = 100): + super().__init__() + self.x_size = x_size + self.y_size = y_size + self.len = len + + def __len__(self): + return self.len + + def __getitem__(self, index): + return torch.rand(self.x_size), torch.rand(self.y_size) + + +def trainer_entrypoint_fn(a, strategy: Strategy): + """Dummy training function.""" + strategy.setup() + print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " + f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") + + # Local model + model = nn.Linear(3, 4) + optim = torch.optim.Adam(model.parameters(), lr=1e-3) + loss_fn = nn.MSELoss() + # Distributed model + model: nn.Module = strategy.distribute_model(model) + optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) + + # Data + train_set = UniformRndDataset(x_size=3, y_size=4) + train_loader = DataLoader(train_set, batch_size=10, num_workers=1) + # Distributed dataloader + train_loader: DataLoader = strategy.distribute_dataloader(train_loader) + + for epoch in range(2): + for (x, y) in train_loader: + # print(f"tensor to cuda:{strategy.device}") + x = x.to(strategy.device) + y = y.to(strategy.device) + + optim.zero_grad() + y_pred = model(x) + loss = loss_fn(y_pred, y) + loss.backward() + optim.step() + + if strategy.is_main_worker(): + print(f"Loss [epoch={epoch}]: {loss.item()}") + + strategy.teardown() + return 123 + + +LAUNCHER = 'torch-elastic-no' +STRATEGY = 'ddp' + +RUN_ID = "my_run_id" +MIN_NODES = 1 +MAX_NODES = 1 +NPROC_PRE_NODE = 4 +MAX_RESTARTS = 2 + +if __name__ == "__main__": + # # STRATEGY BUILDER + + # # Instantiate Launcher Factory + # # launcher = DummyTorchElasticLauncher( + # # n_workers_per_node=NPROC_PRE_NODE, + # # min_nodes=MIN_NODES, + # # max_nodes=MAX_NODES + # # ) + # # launcher = TorchElasticLauncher( + # # rdzv_id=RUN_ID, + # # nproc_per_node=NPROC_PRE_NODE, + # # nnodes=f"{MIN_NODES}:{MAX_NODES}", + # # max_restarts=MAX_RESTARTS + # # ) + # if LAUNCHER == 'torch-elastic': + # launcher_builder: LauncherFactory = TorchElasticLauncherFactory() + # else: + # launcher_builder: LauncherFactory = SimpleLauncherFactory() + + # # Instantiate launcher + # launcher = launcher_builder.createLauncher( + # n_workers_per_node=NPROC_PRE_NODE + # ) + + # # Instantiate Strategy + # if (STRATEGY == 'ddp' + # and torch.cuda.is_available() + # and torch.cuda.device_count() > 1): + # strategy = DDPStrategy(cluster=None, backend='nccl') + # else: + # raise NotImplementedError + + dist_tools = DistributedTooling(n_workers_per_node=NPROC_PRE_NODE) + launcher, strategy = dist_tools.getTools('ddp') + + # CLIENT CODE + # Launch training from launcher + launcher.run(func=trainer_entrypoint_fn, args=("foobar", strategy)) diff --git a/experimental/distributed_tools.py b/experimental/distributed_tools.py new file mode 100644 index 00000000..83bf241f --- /dev/null +++ b/experimental/distributed_tools.py @@ -0,0 +1,68 @@ +from typing import Tuple +import abc + +from launcher import Launcher +from strategy import Strategy, DDPStrategy +from launcher_factory import TorchElasticLauncherFactory + + +class Assembler(abc.ABC): + """Abstract Assembler class.""" + + +class DistributedTooling(Assembler): + """ + Assembles a set of objects used to enable distributed ML. + Suggests working presets of Launcher and Strategy, providing + an easy entry point for the end user. + """ + + def __init__(self, n_workers_per_node: int = 1) -> None: + super().__init__() + self.n_workers_per_node = n_workers_per_node + + def getTools(self, strategy: str) -> Tuple[Launcher, Strategy]: + if strategy == 'ddp': + return self.getTorchDDPTools() + if strategy == 'deepspeed': + return self.getDeepSpeedTools() + if strategy == 'horovod': + return self.getHorovodTools() + raise ValueError(f"Unrecognized strategy={strategy}") + + def getTorchDDPTools(self) -> Tuple[Launcher, Strategy]: + """ + Returns a suggested preset of Launcher + Strategy + for torch distributed data parallel. + """ + import torch + if not torch.cuda.is_available(): + raise RuntimeError( + "Torch DDP cannot be used. GPUs not available." + ) + if not torch.cuda.device_count() > 1: + raise RuntimeError( + "Torch DDP cannot be used. Only one GPU is available." + ) + launcher_builder = TorchElasticLauncherFactory() + elastic_launcher = launcher_builder.createLauncher( + n_workers_per_node=self.n_workers_per_node + ) + strategy = DDPStrategy(backend='nccl') + return elastic_launcher, strategy + + def getDeepSpeedTools(self) -> Tuple[Launcher, Strategy]: + """ + Returns a suggested preset of Launcher + Strategy + for DeepSpeed distributed ML. + """ + # TODO: complete + raise NotImplementedError + + def getHorovodTools(self) -> Tuple[Launcher, Strategy]: + """ + Returns a suggested preset of Launcher + Strategy + for Horovod distributed ML. + """ + # TODO: complete + raise NotImplementedError diff --git a/experimental/example_0.py b/experimental/example_0.py new file mode 100644 index 00000000..5a67cfd8 --- /dev/null +++ b/experimental/example_0.py @@ -0,0 +1,125 @@ +""" +Run this with torchrun +""" + +import os + +import torch +from torch import nn +from torch.utils.data import DataLoader, Dataset + +from strategy import Strategy, DDPStrategy, HorovodStrategy + + +class UniformRndDataset(Dataset): + def __init__(self, x_size: int, y_size: int, len: int = 100): + super().__init__() + self.x_size = x_size + self.y_size = y_size + self.len = len + + def __len__(self): + return self.len + + def __getitem__(self, index): + return torch.rand(self.x_size), torch.rand(self.y_size) + + +def trainer_entrypoint_fn(a, strategy: Strategy): + """Dummy training function.""" + strategy.setup() + print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " + f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") + + # Local model + model = nn.Linear(3, 4) + optim = torch.optim.Adam(model.parameters(), lr=1e-3) + loss_fn = nn.MSELoss() + # Distributed model + model: nn.Module = strategy.distribute_model(model) + optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) + + # Data + train_set = UniformRndDataset(x_size=3, y_size=4) + train_loader = DataLoader(train_set, batch_size=10, num_workers=1) + # Distributed dataloader + train_loader: DataLoader = strategy.distribute_dataloader(train_loader) + + for epoch in range(2): + for (x, y) in train_loader: + # print(f"tensor to cuda:{strategy.device}") + x = x.to(strategy.device) + y = y.to(strategy.device) + + optim.zero_grad() + y_pred = model(x) + loss = loss_fn(y_pred, y) + loss.backward() + optim.step() + + if strategy.is_main_worker(): + print(f"Loss [epoch={epoch}]: {loss.item()}") + + strategy.teardown() + return 123 + + +def trainer_entrypoint_fn_mario(a, strategy: Strategy): + """Dummy training function.""" + + print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " + f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") + + # Local model + model = nn.Linear(3, 4) + optim = torch.optim.Adam(model.parameters(), lr=1e-3) + loss_fn = nn.MSELoss() + # Data + train_set = UniformRndDataset(x_size=3, y_size=4) + train_loader = DataLoader(train_set, batch_size=10, num_workers=1) + + strategy.setup(model, train_set, optim) + # Distributed model + model: nn.Module = strategy.distribute_model(model) + optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) + # Distributed dataloader + train_loader: DataLoader = strategy.distribute_dataloader(train_loader) + + for epoch in range(2): + for (x, y) in train_loader: + # print(f"tensor to cuda:{strategy.device}") + x = x.to(strategy.device) + y = y.to(strategy.device) + + optim.zero_grad() + y_pred = model(x) + loss = loss_fn(y_pred, y) + loss.backward() + optim.step() + + if strategy.is_main_worker(): + print(f"Loss [epoch={epoch}]: {loss.item()}") + + strategy.teardown() + return 123 + + +STRATEGY = 'ddp' + + +if __name__ == "__main__": + + # Instantiate Strategy + if STRATEGY == 'ddp': + if (not torch.cuda.is_available() + or not torch.cuda.device_count() > 1): + raise RuntimeError('Resources unavailable') + + strategy = DDPStrategy(cluster=None, backend='nccl') + elif STRATEGY == 'horovod': + strategy = HorovodStrategy() + else: + raise NotImplementedError + + # Launch distributed training + trainer_entrypoint_fn("foobar", strategy) diff --git a/experimental/example_1.py b/experimental/example_1.py new file mode 100644 index 00000000..3cc2e452 --- /dev/null +++ b/experimental/example_1.py @@ -0,0 +1,106 @@ +""" +Introduction of launcher. Torchrun is not needed anymore. +""" +import os + +import torch +from torch import nn +from torch.utils.data import DataLoader, Dataset + +from strategy import Strategy, DDPStrategy, HorovodStrategy +from launcher import TorchElasticLauncher, SimpleLauncher + + +class UniformRndDataset(Dataset): + def __init__(self, x_size: int, y_size: int, len: int = 100): + super().__init__() + self.x_size = x_size + self.y_size = y_size + self.len = len + + def __len__(self): + return self.len + + def __getitem__(self, index): + return torch.rand(self.x_size), torch.rand(self.y_size) + + +def trainer_entrypoint_fn(a, strategy: Strategy): + """Dummy training function.""" + strategy.setup() + print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " + f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") + + # Local model + model = nn.Linear(3, 4) + optim = torch.optim.Adam(model.parameters(), lr=1e-3) + loss_fn = nn.MSELoss() + # Distributed model + model: nn.Module = strategy.distribute_model(model) + optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) + + # Data + train_set = UniformRndDataset(x_size=3, y_size=4) + train_loader = DataLoader(train_set, batch_size=10, num_workers=1) + # Distributed dataloader + train_loader: DataLoader = strategy.distribute_dataloader(train_loader) + + for epoch in range(2): + for (x, y) in train_loader: + # print(f"tensor to cuda:{strategy.device}") + x = x.to(strategy.device) + y = y.to(strategy.device) + + optim.zero_grad() + y_pred = model(x) + loss = loss_fn(y_pred, y) + loss.backward() + optim.step() + + if strategy.is_main_worker(): + print(f"Loss [epoch={epoch}]: {loss.item()}") + + strategy.teardown() + return 123 + + +LAUNCHER = 'torch-elastic' +STRATEGY = 'ddp' +RUN_ID = "my_run_id" +MIN_NODES = 1 +MAX_NODES = 1 +NPROC_PRE_NODE = 4 +MAX_RESTARTS = 2 + +if __name__ == "__main__": + + # Instantiate Launcher Factory + if LAUNCHER == 'torch-elastic': + launcher = TorchElasticLauncher( + rdzv_id=RUN_ID, + nproc_per_node=NPROC_PRE_NODE, + nnodes=f"{MIN_NODES}:{MAX_NODES}", + max_restarts=MAX_RESTARTS + ) + elif LAUNCHER == 'simple-launcher': + launcher = SimpleLauncher( + nproc_per_node=NPROC_PRE_NODE + ) + else: + raise NotImplementedError + + # Instantiate Strategy + if STRATEGY == 'ddp': + if (not torch.cuda.is_available() + or not torch.cuda.device_count() > 1): + raise RuntimeError('Resources unavailable') + + strategy = DDPStrategy(cluster=None, backend='nccl') + elif STRATEGY == 'horovod': + strategy = HorovodStrategy() + else: + raise NotImplementedError + + # CLIENT CODE + # Launch training from launcher + launcher.run(func=trainer_entrypoint_fn, args=("foobar", strategy)) diff --git a/experimental/example_2.py b/experimental/example_2.py new file mode 100644 index 00000000..14685753 --- /dev/null +++ b/experimental/example_2.py @@ -0,0 +1,107 @@ +""" +Unified interface for launchers. +Most of the complexity is hidden inside "factory" classes. +""" + +import os + +import torch +from torch import nn +from torch.utils.data import DataLoader, Dataset + +from strategy import Strategy, DDPStrategy, HorovodStrategy +from launcher_factory import ( + LauncherFactory, + SimpleLauncherFactory, + TorchElasticLauncherFactory +) + + +class UniformRndDataset(Dataset): + def __init__(self, x_size: int, y_size: int, len: int = 100): + super().__init__() + self.x_size = x_size + self.y_size = y_size + self.len = len + + def __len__(self): + return self.len + + def __getitem__(self, index): + return torch.rand(self.x_size), torch.rand(self.y_size) + + +def trainer_entrypoint_fn(a, strategy: Strategy): + """Dummy training function.""" + strategy.setup() + print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " + f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") + + # Local model + model = nn.Linear(3, 4) + optim = torch.optim.Adam(model.parameters(), lr=1e-3) + loss_fn = nn.MSELoss() + # Distributed model + model: nn.Module = strategy.distribute_model(model) + optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) + + # Data + train_set = UniformRndDataset(x_size=3, y_size=4) + train_loader = DataLoader(train_set, batch_size=10, num_workers=1) + # Distributed dataloader + train_loader: DataLoader = strategy.distribute_dataloader(train_loader) + + for epoch in range(2): + for (x, y) in train_loader: + # print(f"tensor to cuda:{strategy.device}") + x = x.to(strategy.device) + y = y.to(strategy.device) + + optim.zero_grad() + y_pred = model(x) + loss = loss_fn(y_pred, y) + loss.backward() + optim.step() + + if strategy.is_main_worker(): + print(f"Loss [epoch={epoch}]: {loss.item()}") + + strategy.teardown() + return 123 + + +LAUNCHER = 'torch-elastic' +STRATEGY = 'ddp' +NPROC_PRE_NODE = 4 + +if __name__ == "__main__": + # STRATEGY BUILDER + + # Instantiate Launcher Factory + if LAUNCHER == 'torch-elastic': + launcher_builder: LauncherFactory = TorchElasticLauncherFactory() + elif LAUNCHER == 'simple-launcher': + launcher_builder: LauncherFactory = SimpleLauncherFactory() + else: + raise NotImplementedError + + # Instantiate launcher + launcher = launcher_builder.createLauncher( + n_workers_per_node=NPROC_PRE_NODE + ) + + # Instantiate Strategy + if STRATEGY == 'ddp': + if (not torch.cuda.is_available() + or not torch.cuda.device_count() > 1): + raise RuntimeError('Resources unavailable') + + strategy = DDPStrategy(cluster=None, backend='nccl') + elif STRATEGY == 'horovod': + strategy = HorovodStrategy() + else: + raise NotImplementedError + + # CLIENT CODE + # Launch training from launcher + launcher.run(func=trainer_entrypoint_fn, args=("foobar", strategy)) diff --git a/experimental/example_3.py b/experimental/example_3.py new file mode 100644 index 00000000..d38dd78c --- /dev/null +++ b/experimental/example_3.py @@ -0,0 +1,77 @@ +""" +Hide the selection of launcher and strategy inside a class. +""" +import os + +import torch +from torch import nn +from torch.utils.data import DataLoader, Dataset + +from strategy import Strategy +from distributed_tools import DistributedTooling + + +class UniformRndDataset(Dataset): + def __init__(self, x_size: int, y_size: int, len: int = 100): + super().__init__() + self.x_size = x_size + self.y_size = y_size + self.len = len + + def __len__(self): + return self.len + + def __getitem__(self, index): + return torch.rand(self.x_size), torch.rand(self.y_size) + + +def trainer_entrypoint_fn(a, strategy: Strategy): + """Dummy training function.""" + strategy.setup() + print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " + f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") + + # Local model + model = nn.Linear(3, 4) + optim = torch.optim.Adam(model.parameters(), lr=1e-3) + loss_fn = nn.MSELoss() + # Distributed model + model: nn.Module = strategy.distribute_model(model) + optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) + + # Data + train_set = UniformRndDataset(x_size=3, y_size=4) + train_loader = DataLoader(train_set, batch_size=10, num_workers=1) + # Distributed dataloader + train_loader: DataLoader = strategy.distribute_dataloader(train_loader) + + for epoch in range(2): + for (x, y) in train_loader: + # print(f"tensor to cuda:{strategy.device}") + x = x.to(strategy.device) + y = y.to(strategy.device) + + optim.zero_grad() + y_pred = model(x) + loss = loss_fn(y_pred, y) + loss.backward() + optim.step() + + if strategy.is_main_worker(): + print(f"Loss [epoch={epoch}]: {loss.item()}") + + strategy.teardown() + return 123 + + +STRATEGY = 'ddp' +NPROC_PRE_NODE = 4 + + +if __name__ == "__main__": + dist_tools = DistributedTooling(n_workers_per_node=NPROC_PRE_NODE) + launcher, strategy = dist_tools.getTools('ddp') + + # CLIENT CODE + # Launch training from launcher + launcher.run(func=trainer_entrypoint_fn, args=("foobar", strategy)) diff --git a/experimental/launcher.py b/experimental/launcher.py new file mode 100644 index 00000000..d9733b8f --- /dev/null +++ b/experimental/launcher.py @@ -0,0 +1,295 @@ +import datetime +import os +import shutil +import abc +import time +import uuid +from typing import Callable, Tuple, Any, Union, List, Optional + +from torch.distributed.elastic.agent.server.local_elastic_agent import ( + LocalElasticAgent +) +from torch.distributed.elastic.agent.server import WorkerSpec +from torch.distributed.elastic.rendezvous.dynamic_rendezvous import ( + DynamicRendezvousHandler +) +from torch.distributed.elastic.rendezvous.c10d_rendezvous_backend import ( + C10dRendezvousBackend +) +from torch.distributed import TCPStore +from torch.distributed.elastic.multiprocessing import Std, start_processes + +from torch.distributed.launcher.api import LaunchConfig, elastic_launch +from torch.distributed.run import config_from_args + +from cluster import ClusterEnvironment, detect_cluster + + +class Launcher(abc.ABC): + cluster: ClusterEnvironment + + @abc.abstractmethod + def run(self, *args) -> Any: + """Launches the distributed execution.""" + + +class DummyTorchElasticLauncher(Launcher): + """Simplified Torch Elastic launcher.""" + + def __init__( + self, + cluster: Optional[ClusterEnvironment] = None, + n_workers_per_node: int = 1, + min_nodes: int = 1, + max_nodes: int = 1, + max_restarts: int = 1 + ) -> None: + super().__init__() + # detect_cluster() is preferred + self.cluster = cluster if cluster is not None else detect_cluster() + print(f"DummyTorchElasticLauncher with cluster '{self.cluster}'") + self.n_workers_per_node = n_workers_per_node + self.min_nodes = min_nodes + self.max_nodes = max_nodes + self.max_restarts = max_restarts + self.run_id = str(time.time()) + + if cluster.creates_processes_externally and n_workers_per_node > 1: + print("WARNING: the cluster may already spawn worker " + "processes for you... Consider setting " + "'n_workers_per_node=1'") + + g_world_size = cluster.num_nodes() * self.n_workers_per_node + + store = TCPStore( + host_name=cluster.main_address, + port=cluster.main_port, # could conflict! + world_size=g_world_size, + is_master=cluster.global_rank() == 0, + timeout=datetime.timedelta(seconds=3) + ) + backend = C10dRendezvousBackend(store, self.run_id) + self.rdzv_handler = DynamicRendezvousHandler.from_backend( + run_id=self.run_id, + store=store, + backend=backend, + min_nodes=self.min_nodes, + max_nodes=self.max_nodes + ) + + def run( + self, + func: Callable, + args: Tuple = (), + redirect: bool = False, + log_dir: str = 'launcher_logs', + tee_ranks: Union[str, int, List[int]] = None + ) -> List[Any]: + """Launches the distributed execution with Torch Elastic.""" + # Suppress all printing to console: + # redirects={0: Std.ALL} # do no print, but save to file. + # linked to Agent's log_dir + redirects = Std.ALL if redirect else Std.NONE + + # Fore back printing to console, while redirecting to file + # tee={0: Std.ALL} reactivates print to console + save to + # log file for RANK 0 + if tee_ranks == 'all': + tee = Std.ALL + elif tee_ranks is None: + tee = Std.NONE + elif isinstance(tee_ranks, int): + tee = {tee_ranks: Std.ALL} + elif isinstance(tee_ranks, list): + # tee_ranks is a list of int + tee = {rnk: Std.ALL for rnk in tee_ranks} + else: + raise ValueError(f"unrecognized 'tee_ranks={tee_ranks}'") + + spec = WorkerSpec( + role="worker", + local_world_size=self.n_workers_per_node, + entrypoint=func, + args=args, + rdzv_handler=self.rdzv_handler, + max_restarts=self.max_restarts, + # monitor_interval=monitor_interval, + redirects=redirects, + tee=tee + ) + + agent = LocalElasticAgent(spec, start_method="spawn", log_dir=log_dir) + # try: + run_result = agent.run() + if run_result.is_failed(): + print(f"worker 0 failed with: {run_result.failures[0]}") + result = None + else: + print(f"worker 0 return value is: {run_result.return_values[0]}") + result = run_result.return_values + # except Exception ex: + # # handle exception + return result + + +class TorchElasticLauncher(Launcher): + """ + Official Torch Elastic launcher. + Does NOT support passing values as environment variables. + + Adapted from: + https://github.com/pytorch/pytorch/blob/main/torch/distributed/run.py + """ + + def __init__( + self, + nnodes: str = '1:1', + nproc_per_node: str = '1', + rdzv_backend: str = 'static', + rdzv_endpoint: str = '', + rdzv_id: str = 'none', + rdzv_conf: str = '', + standalone: bool = False, + max_restarts: int = 0, + monitor_interval: float = 5, + start_method: str = 'spawn', + role: str = 'default', + module: bool = False, + no_python: bool = False, + run_path: bool = False, + log_dir: Optional[str] = None, + redirects: str = '0', + tee: str = '0', + node_rank: int = 0, + master_addr: str = "127.0.0.1", + master_port: int = 29500, + local_addr: Optional[str] = None + ) -> None: + super().__init__() + # emulate CLI args + # TODO: include logic for 'action=check_env' or 'action=env' + self.nnodes = nnodes + self.nproc_per_node = nproc_per_node + self.rdzv_backend = rdzv_backend + self.rdzv_endpoint = rdzv_endpoint + self.rdzv_id = rdzv_id + self.rdzv_conf = rdzv_conf + self.standalone = standalone + self.max_restarts = max_restarts + self.monitor_interval = monitor_interval + self.start_method = start_method + self.role = role + self.module = module + self.no_python = no_python + self.run_path = run_path + self.log_dir = log_dir + self.redirects = redirects + self.tee = tee + self.node_rank = node_rank + self.master_addr = master_addr + self.master_port = master_port + self.local_addr = local_addr + # Placeholders + self.training_script = "placeholder.py" + self.training_script_args = [] + + def config_from_args( + self + ) -> Tuple[LaunchConfig, Union[Callable, str], List[str]]: + return config_from_args(self) + + def run( + self, + func: Callable, + args: Tuple = () + ) -> Any: + if self.standalone: + self.rdzv_backend = "c10d" + self.rdzv_endpoint = "localhost:29400" + self.rdzv_id = str(uuid.uuid4()) + # log.info( + # f"\n**************************************\n" + # f"Rendezvous info:\n" + # f"--rdzv_backend={self.rdzv_backend} " + # f"--rdzv_endpoint={self.rdzv_endpoint} " + # f"--rdzv_id={self.rdzv_id}\n" + # f"**************************************\n" + # ) + + config, _, _ = self.config_from_args() + elastic_launch( + config=config, + entrypoint=func, + )(*args) + + +class SimpleLauncher(Launcher): + """Simple launcher based on multiprocessing. + Use ONLY for single node applications. + """ + + def __init__( + self, + nproc_per_node: int, + run_id: Optional[str] = None, + master_addr: str = "127.0.0.1", + master_port: int = 29500 + ) -> None: + super().__init__() + self.nproc_per_node = nproc_per_node + self.run_id = run_id if run_id is not None else f"RunID:{time.time()}" + self.master_addr = master_addr + self.master_port = master_port + self.log_dir = f'{self.__class__.__name__}_logs' + if os.path.exists(self.log_dir): + shutil.rmtree(self.log_dir) + os.makedirs(self.log_dir) + + def run( + self, + func: Callable, + args: Tuple = () + ) -> Any: + # Adapted from: + # https://pytorch.org/docs/stable/elastic/multiprocessing.html + w_args = {i: args for i in range(self.nproc_per_node)} + # Emulates the env variables set by torch Elastic + w_envs = { + i: dict( + RANK=str(i), + LOCAL_RANK=str(i), + GROUP_RANK=str(0), + ROLE_RANK=str(i), + WORLD_SIZE=str(self.nproc_per_node), + LOCAL_WORLD_SIZE=str(self.nproc_per_node), + ROLE_WORLD_SIZE=str(self.nproc_per_node), + TORCHELASTIC_RUN_ID=str(self.run_id), + MASTER_ADDR=str(self.master_addr), + MASTER_PORT=str(self.master_port) + ) + for i in range(self.nproc_per_node) + } + ctx = start_processes( + name=self.__class__.__name__, + entrypoint=func, + args=w_args, + envs=w_envs, + log_dir=self.log_dir + ) + ctx.wait() + return ctx.return_values + + +class DeepSpeedLauncher(Launcher): + """Official DeepSpeed launcher.""" + + def __init__(self) -> None: + super().__init__() + + def run( + self, + func: Callable, + args: Tuple = () + ) -> Any: + # TODO: complete + raise NotImplementedError diff --git a/experimental/launcher_factory.py b/experimental/launcher_factory.py new file mode 100644 index 00000000..fce12a0c --- /dev/null +++ b/experimental/launcher_factory.py @@ -0,0 +1,144 @@ +""" +Factories to instantiate Launcher classes. +They introduce a level of indirection to provide a unified interface +for all the launchers. The common interface is provided by the +`createLauncher` factory method. +""" + +from typing import Optional, Dict, Any +import abc + +from launcher import ( + Launcher, + TorchElasticLauncher, + SimpleLauncher, + DeepSpeedLauncher +) +from cluster import detect_cluster + + +class LauncherFactory(abc.ABC): + """ + Factory class to instantiate a Launcher classes. + It introduces a level of indirection to provide a unified interface + for all the launchers. The common interface is provided by the + `createLauncher` factory method. + """ + + def createLauncher( + self, + n_workers_per_node: int, + run_id: Optional[str] = None, + master_addr: Optional[str] = None, + master_port: Optional[int] = None, + **kwargs + ) -> Launcher: + """ + Simplifies the instantiation of a Launcher. + Advanced configuration is pre-computed in the body + of this method, leaving few parameters to the end user. + """ + + +class TorchElasticLauncherFactory(LauncherFactory): + """Factory class to instantiate a TorchElasticLauncher class.""" + + def createLauncher( + self, + n_workers_per_node: int, + run_id: Optional[str] = None, + master_addr: Optional[str] = None, + master_port: Optional[int] = None, + **kwargs + ) -> Launcher: + """ + Simplifies the instantiation of a TorchElasticLauncher. + Advanced configuration is pre-computed in the body + of this method, leaving few parameters to the end user. + """ + cluster = detect_cluster() + + kwargs['nproc_per_node'] = n_workers_per_node + # If given, propagate the args + if run_id: + kwargs['rdzv_id'] = run_id + if master_addr: + kwargs['master_addr'] = master_addr + if master_port: + kwargs['master_port'] = master_port + + # Compute and add TorchElastic specific args, if not + # provided as **kwargs + n_nodes = cluster.num_nodes() + safe_add(kwargs, 'nnodes', f"{n_nodes}:{n_nodes}") + safe_add(kwargs, 'rdzv_id', cluster.job_id()) + is_host_flag = '1' if cluster.node_rank() == 0 else '0' + safe_add(kwargs, 'rdzv_conf', f'is_host={is_host_flag}') + safe_add(kwargs, 'rdzv_backend', 'c10d') + safe_add( + kwargs, + 'rdzv_endpoint', + f'{cluster.main_address}:{cluster.main_port}' + ) + safe_add(kwargs, 'max_restarts', 3) + + return TorchElasticLauncher(**kwargs) + + +class SimpleLauncherFactory(LauncherFactory): + """Factory class to instantiate a SimpleLauncherFactory class.""" + + def createLauncher( + self, + n_workers_per_node: int, + run_id: Optional[str] = None, + master_addr: Optional[str] = None, + master_port: Optional[int] = None, + **kwargs + ) -> Launcher: + """ + Simplifies the instantiation of a SimpleLauncher. + Advanced configuration is pre-computed in the body + of this method, leaving few parameters to the end user. + """ + + kwargs['nproc_per_node'] = n_workers_per_node + # If given, propagate the args + if run_id: + kwargs['run_id'] = run_id + if master_addr: + kwargs['master_addr'] = master_addr + if master_port: + kwargs['master_port'] = master_port + + return SimpleLauncher(**kwargs) + + +class DeepSpeedLauncherFactory(LauncherFactory): + """Factory class to instantiate a DeepSpeedLauncher class.""" + + def createLauncher( + self, + n_workers_per_node: int, + run_id: Optional[str] = None, + master_addr: Optional[str] = None, + master_port: Optional[int] = None, + **kwargs + ) -> Launcher: + """ + Simplifies the instantiation of a DeepSpeedLauncher. + Advanced configuration is pre-computed in the body + of this method, leaving few parameters to the end user. + """ + # TODO: complete + raise NotImplementedError + return DeepSpeedLauncher(...) + + +def safe_add(map: Dict, key: str, value: Any) -> None: + """ + Add a key-value pair to a dict if the key + is not already present. + """ + if map.get(key) is None: + map[key] = value diff --git a/experimental/strategy.py b/experimental/strategy.py new file mode 100644 index 00000000..59dd7a4f --- /dev/null +++ b/experimental/strategy.py @@ -0,0 +1,150 @@ +import os +import abc +from typing import Any, Optional + +import torch +from torch import nn +from torch.nn.parallel import DistributedDataParallel +from torch import optim +from torch.utils.data import DataLoader, DistributedSampler +from torch.distributed import init_process_group + +# from lightning.pytorch.plugins.environments import ClusterEnvironment +from cluster import ClusterEnvironment, detect_cluster + + +class Strategy(abc.ABC): + cluster: ClusterEnvironment + + @property + @abc.abstractmethod + def device(self) -> int: + """Device used by this worker""" + + @abc.abstractmethod + def setup(self) -> None: + """Setup the strategy once in a distributed environment.""" + + @abc.abstractmethod + def teardown(self) -> None: + """Frees the distributed strategy resources.""" + + @abc.abstractmethod + def is_main_worker(self) -> bool: + """Returns True if called from the main process of the pool.""" + + @abc.abstractmethod + def _is_env_setup(self) -> bool: + """Checks whether the distributed environment is correctly setup.""" + + @abc.abstractmethod + def distribute_model(self, model: Any) -> Any: + """Distributes a neural network.""" + + @abc.abstractmethod + def distribute_optimizer(self, optimizer: Any) -> Any: + """Distributes an optimizer.""" + + @abc.abstractmethod + def distribute_dataloader(self, dataloader: Any) -> Any: + """Distributes a dataloader.""" + + +class DDPStrategy(Strategy): + def __init__( + self, + backend: str = 'nccl', + cluster: Optional[ClusterEnvironment] = None + ) -> None: + super().__init__() + self.cluster = cluster + self.backend = backend + + @property + def device(self) -> int: + """Returns the local rank. Assumes one worker per GPU.""" + return self.cluster.local_rank() + + def setup(self, **kwargs) -> None: + """Setup the strategy in a distributed context.""" + if not self._is_env_setup(): + raise RuntimeError( + "Distributed environment not setup correctly. Use a launcher.") + + # detect_cluster() is preferred + if self.cluster is None: + self.cluster = detect_cluster() + print(f"DDPStrategy executed on '{self.cluster}' cluster") + + # Initializes the default distributed process group + # and the distributed package + init_process_group(backend=self.backend) + + def teardown(self) -> None: + torch.distributed.barrier() + torch.distributed.destroy_process_group() + + def _is_env_setup(self) -> bool: + if (os.environ.get('RANK') is not None): + # and torch.distributed.is_available()): + return True + return False + + def is_main_worker(self) -> bool: + return self.cluster.global_rank() == 0 + + def distribute_model(self, model: nn.Module) -> nn.Module: + model = model.to(f"cuda:{self.device}") + return DistributedDataParallel( + model, + device_ids=[self.device], + output_device=self.device + ) + + def distribute_optimizer( + self, + optimizer: optim.Optimizer + ) -> optim.Optimizer: + return optimizer + + def distribute_dataloader( + self, + dataloader: DataLoader, + shuffle: bool = True + ) -> DataLoader: + """Makes a torch DataLoader distributed by substituting its sampler.""" + sampler = DistributedSampler( + dataloader.dataset, + num_replicas=self.cluster.world_size(), + rank=self.cluster.global_rank(), + shuffle=shuffle + ) + # Recreate dataloader, with updated sampler + return DataLoader( + dataloader.dataset, + batch_size=dataloader.batch_size, + sampler=sampler, + num_workers=dataloader.num_workers, + collate_fn=dataloader.collate_fn, + pin_memory=dataloader.pin_memory, + drop_last=dataloader.drop_last, + timeout=dataloader.timeout, + worker_init_fn=dataloader.worker_init_fn, + multiprocessing_context=dataloader.multiprocessing_context, + generator=dataloader.generator, + prefetch_factor=dataloader.prefetch_factor, + persistent_workers=dataloader.persistent_workers, + pin_memory_device=dataloader.pin_memory_device + ) + + +class LocalStrategy(Strategy): + ... + + +class HorovodStrategy(Strategy): + ... + + +class DeepSpeedStrategy(Strategy): + ... diff --git a/experimental/trainer/DS_config.json b/experimental/trainer/DS_config.json new file mode 100644 index 00000000..544cab17 --- /dev/null +++ b/experimental/trainer/DS_config.json @@ -0,0 +1,15 @@ +{ + "train_micro_batch_size_per_gpu": 32, + "gradient_accumulation_steps": 1, + "optimizer": { + "type": "Adam", + "params": { + "lr": 0.01 + } + }, + "fp16": { + "enabled": false + }, + "zero_optimization": false +} + diff --git a/experimental/trainer/general_startscript b/experimental/trainer/general_startscript new file mode 100755 index 00000000..455466b4 --- /dev/null +++ b/experimental/trainer/general_startscript @@ -0,0 +1,135 @@ +#!/bin/bash + +# general configuration of the job +#SBATCH --job-name=TorchTest +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job.out +#SBATCH --error=job.err +#SBATCH --time=00:15:00 + +# configure node and process count on the CM +#SBATCH --partition=batch +#SBATCH --nodes=4 +#SBATCH --ntasks-per-node=1 +#SBATCH --cpus-per-task=32 +#SBATCH --gpus-per-node=4 +#SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# parallelization strategy (DDP, HVD, DS) +strategy='DS' + +# parameters +debug=false # do debug +bs=32 # batch-size +epochs=1 # epochs +lr=0.01 # learning rate + +# AT +dataDir="/p/scratch/raise-ctp2/data_MNIST/" + +# set modules +ml --force purge + +ml Stages/2022 NVHPC/22.1 ParaStationMPI/5.5.0-1-mt NCCL/2.11.4-CUDA-11.5 cuDNN/8.3.1.22-CUDA-11.5 +ml Python/3.9.6 CMake HDF5 PnetCDF libaio/0.3.112 mpi-settings/CUDA + +# set env +source /p/project/intertwin/rakesh/T6.5-AI-and-ML/dist_trainer/envAI_hdfml/bin/activate + +# sleep a sec +sleep 1 + +# job info +echo "DEBUG: TIME: $(date)" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$debug" = true ] ; then + export NCCL_DEBUG=INFO +fi +echo + +# set comm +export CUDA_VISIBLE_DEVICES="0,1,2,3" +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_TASK" > 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK +fi + +COMMAND="general_trainer_v2.py" + +#launch +if [[ $strategy == *"HVD"* ]]; +then + EXEC="$COMMAND \ + --strategy $strategy \ + --batch-size $bs \ + --epochs $epochs \ + --lr $lr \ + --data-dir $dataDir" + + srun --cpu-bind=none python3 -u $EXEC + +elif [[ $strategy == *"DDP"* ]]; +then + EXEC="$COMMAND \ + --strategy $strategy \ + --batch-size $bs \ + --epochs $epochs \ + --lr $lr \ + --nworker $SLURM_CPUS_PER_TASK \ + --data-dir $dataDir" + + srun --cpu-bind=none bash -c "torchrun \ + --log_dir='logs' \ + --nnodes=$SLURM_NNODES \ + --nproc_per_node=$SLURM_GPUS_PER_NODE \ + --rdzv_id=$SLURM_JOB_ID \ + --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ + --rdzv_backend=c10d \ + --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ + $EXEC" + +else + EXEC="$COMMAND \ + --strategy $strategy \ + --batch-size $bs \ + --epochs $epochs \ + --lr $lr \ + --nworker $SLURM_CPUS_PER_TASK \ + --data-dir $dataDir" + + #### do not change this part + # create node-list + sysN=$(eval "scontrol show hostnames") + for i in $sysN; do + x+=\"$i\":[$CUDA_VISIBLE_DEVICES], + done + WID=`echo {${x::-1}} | base64 -w 0` + + # modify config file with parameters + sed -i "2s|.*| \"train_micro_batch_size_per_gpu\": ${bs},|" DS_config.json + sed -i "7s|.*| \"lr\": ${lr}|" DS_config.json + #### + + # launch + srun python -m deepspeed.launcher.launch \ + --node_rank $SLURM_PROCID \ + --master_addr ${SLURMD_NODENAME}i \ + --master_port 29500 \ + --world_info $WID \ + $EXEC --deepspeed_mpi --deepspeed_config DS_config.json + +fi diff --git a/experimental/trainer/general_trainer.py b/experimental/trainer/general_trainer.py new file mode 100755 index 00000000..33c21ced --- /dev/null +++ b/experimental/trainer/general_trainer.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# author: RS, adapted from https://gitlab.jsc.fz-juelich.de/CoE-RAISE/FZJ/ai4hpc +# version: 211029a + +# std libs +from typing import Any, Union +import argparse +import sys +import os +import time +import numpy as np +import random +import abc + +# ml libs +import deepspeed +import torch +import torch.distributed as dist +import torch.nn as nn +import torch.nn.functional as F +from torchvision import datasets, transforms + +from itwinai.torch.distributed import ( + DDPDistributedStrategy_old, + DSDistributedStrategy_old, + HVDDistributedStrategy_old +) + +# parsed settings + + +def pars_ini(): + global args + parser = argparse.ArgumentParser(description='PyTorch MNIST Example') + + # IO parsers + parser.add_argument('--data-dir', default='./', + help='location of the training dataset in the local filesystem') + parser.add_argument('--restart-int', type=int, default=10, + help='restart interval per epoch (default: 10)') + + # model parsers + parser.add_argument('--strategy', type=str, default='DDP', + help='strategy for parallelization (DDP, HVD, DS)') + parser.add_argument('--batch-size', type=int, default=64, + help='input batch size for training (default: 64)') + parser.add_argument('--epochs', type=int, default=10, + help='number of epochs to train (default: 10)') + parser.add_argument('--lr', type=float, default=0.01, + help='learning rate (default: 0.01)') + parser.add_argument('--concM', type=int, default=100, + help='conc MNIST to this factor (default: 100)') + parser.add_argument('--momentum', type=float, default=0.5, + help='momentum in SGD optimizer (default: 0.5)') + parser.add_argument('--shuff', action='store_true', default=False, + help='shuffle dataset (default: False)') + + # debug parsers + parser.add_argument('--testrun', action='store_true', default=False, + help='do a test run with seed (default: False)') + parser.add_argument('--nseed', type=int, default=0, + help='seed integer for reproducibility (default: 0)') + parser.add_argument('--log-int', type=int, default=10, + help='log interval per training') + + # parallel parsers + parser.add_argument('--backend', type=str, default='nccl', + help='backend for parrallelisation (default: nccl)') + parser.add_argument('--nworker', type=int, default=0, + help='number of workers in DataLoader (default: 0 - only main)') + parser.add_argument('--prefetch', type=int, default=2, + help='prefetch data in DataLoader (default: 2)') + parser.add_argument('--no-cuda', action='store_true', default=False, + help='disables GPGPUs') + parser.add_argument('--local_rank', type=int, default=-1, + help='local rank passed from distributed launcher') + + try: + parser = deepspeed.add_config_arguments(parser) + except: + pass + + args = parser.parse_args() + + +class Net(nn.Module): + def __init__(self): + super(Net, self).__init__() + self.conv1 = nn.Conv2d(1, 10, kernel_size=5) + self.conv2 = nn.Conv2d(10, 20, kernel_size=5) + self.conv2_drop = nn.Dropout2d() + self.fc1 = nn.Linear(320, 50) + self.fc2 = nn.Linear(50, 10) + + def forward(self, x): + x = F.relu(F.max_pool2d(self.conv1(x), 2)) + x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) + x = x.view(-1, 320) + x = F.relu(self.fc1(x)) + x = F.dropout(x, training=self.training) + x = self.fc2(x) + return F.log_softmax(x) + +# train loop + + +def train(model, device, train_loader, optimizer, epoch, grank, gwsize, args): + model.train() + t_list = [] + loss_acc = 0 + if grank == 0: + print("\n") + for batch_idx, (data, target) in enumerate(train_loader): + t = time.perf_counter() + data, target = data.to(device), target.to(device) + optimizer.zero_grad() + output = model(data) + loss = F.nll_loss(output, target) + loss.backward() + optimizer.step() + if batch_idx % args.log_int == 0 and grank == 0: + print( + f'Train epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)/gwsize} ' + f'({100.0 * batch_idx / len(train_loader):.0f}%)]\t\tLoss: {loss.item():.6f}') + t_list.append(time.perf_counter() - t) + loss_acc += loss.item() + if grank == 0: + print('TIMER: train time', sum(t_list) / len(t_list), 's') + return loss_acc + +# test loop + + +def test(model, device, test_loader, grank, gwsize, args): + model.eval() + test_loss = 0 + correct = 0 + with torch.no_grad(): + for data, target in test_loader: + data, target = data.to(device), target.to(device) + output = model(data) + # sum up batch loss + test_loss += F.nll_loss(output, target, reduction="sum").item() + # get the index of the max log-probability + pred = output.argmax(dim=1, keepdim=True) + correct += pred.eq(target.view_as(pred)).sum().item() + test_loss /= len(test_loader.dataset) + if grank == 0: + print( + f'Test set: average loss: {test_loss:.4f}\t' + f'accurate samples: {correct}/{len(test_loader.dataset)/gwsize}') + acc_test = 100.0 * correct * gwsize / len(test_loader.dataset) + return acc_test + + +# save state of the training +def save_state(epoch, distrib_model, loss_acc, optimizer, res_name, grank, gwsize, is_best, my_trainer): + rt = time.time() + # find if is_best happened in any worker + if torch.cuda.is_available(): + is_best_m = my_trainer.par_allgather_obj(is_best, gwsize) + + if torch.cuda.is_available(): + if any(is_best_m): + # find which rank is_best happened - select first rank if multiple + is_best_rank = np.where(np.array(is_best_m) == True)[0][0] + + # collect state + state = {'epoch': epoch + 1, + 'state_dict': distrib_model.state_dict(), + 'best_acc': loss_acc, + 'optimizer': optimizer.state_dict()} + + # write on worker with is_best + if grank == is_best_rank: + torch.save(state, './'+res_name) + print( + f'DEBUG: state in {grank} is saved on epoch:{epoch} in {time.time()-rt} s') + else: + # collect state + state = {'epoch': epoch + 1, + 'state_dict': distrib_model.state_dict(), + 'best_acc': loss_acc, + 'optimizer': optimizer.state_dict()} + + torch.save(state, './'+res_name) + print( + f'DEBUG: state in {grank} is saved on epoch:{epoch} in {time.time()-rt} s') + + +# deterministic dataloader +def seed_worker(worker_id): + worker_seed = torch.initial_seed() % 2**32 + np.random.seed(worker_seed) + random.seed(worker_seed) + + +# +# +# MAIN +# +# +def main(): + # get parse args + pars_ini() + + # check CUDA availibility + args.cuda = not args.no_cuda and torch.cuda.is_available() + + # Strategy for distributed training + if args.strategy == 'DDP': + my_trainer = DDPDistributedStrategy_old() + + elif args.strategy == 'DS': + my_trainer = DSDistributedStrategy_old() + + elif args.strategy == 'HVD': + my_trainer = HVDDistributedStrategy_old() + + # limit # of CPU threads to be used per worker + torch.set_num_threads(1) + + # get directory + program_dir = os.getcwd() + + # start the time.time for profiling + st = time.time() + + # initializes the distributed backend which will take care of sychronizing nodes/GPUs + my_trainer.init_backend(backend=args.backend) + + # deterministic testrun + if args.testrun: + torch.manual_seed(args.nseed) + g = torch.Generator() + g.manual_seed(args.nseed) + + # get job rank info - rank==0 master gpu + if torch.cuda.is_available(): + # local world size - per node + lwsize = my_trainer.local_world_size() if args.cuda else 0 + gwsize = my_trainer.global_world_size() # global world size - per run + grank = my_trainer.dist_grank() # global rank - assign per run + lrank = my_trainer.dist_lrank() # local rank - assign per node + else: + gwsize = 1 + grank = 0 + + # some debug + if grank == 0: + print('TIMER: initialise:', time.time()-st, 's') + print('DEBUG: local ranks:', lwsize, '/ global ranks:', gwsize) + print('DEBUG: sys.version:', sys.version, '\n') + + print('DEBUG: IO parsers:') + print('DEBUG: args.data_dir:', args.data_dir) + print('DEBUG: args.restart_int:', args.restart_int, '\n') + + print('DEBUG: model parsers:') + print('DEBUG: args.batch_size:', args.batch_size) + print('DEBUG: args.epochs:', args.epochs) + print('DEBUG: args.lr:', args.lr) + print('DEBUG: args.concM:', args.concM) + print('DEBUG: args.momentum:', args.momentum) + print('DEBUG: args.shuff:', args.shuff, '\n') + + print('DEBUG: debug parsers:') + print('DEBUG: args.testrun:', args.testrun) + print('DEBUG: args.nseed:', args.nseed) + print('DEBUG: args.log_int:', args.log_int, '\n') + + print('DEBUG: parallel parsers:') + print('DEBUG: args.backend:', args.backend) + print('DEBUG: args.nworker:', args.nworker) + print('DEBUG: args.prefetch:', args.prefetch) + print('DEBUG: args.cuda:', args.cuda, '\n') + + # encapsulate the model on the GPU assigned to the current process + device = torch.device( + 'cuda' if args.cuda and torch.cuda.is_available() else 'cpu', lrank) + if args.cuda: + torch.cuda.set_device(lrank) + # deterministic testrun + if args.testrun: + torch.cuda.manual_seed(args.nseed) + +# read data + data_dir = args.data_dir + mnist_scale = args.concM + largeData = [] + for i in range(mnist_scale): + largeData.append( + datasets.MNIST(data_dir, train=True, download=False, + transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)) + ])) + ) + + # concat data + train_dataset = torch.utils.data.ConcatDataset(largeData) + + mnist_scale = args.concM + largeData = [] + for i in range(mnist_scale): + largeData.append( + datasets.MNIST(data_dir, train=False, download=False, + transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)) + ])) + ) + + # concat data + test_dataset = torch.utils.data.ConcatDataset(largeData) + + # restricts data loading to a subset of the dataset exclusive to the current process + args.shuff = args.shuff and not args.testrun + if torch.cuda.is_available(): + train_sampler = torch.utils.data.distributed.DistributedSampler( + train_dataset, num_replicas=gwsize, rank=grank, shuffle=args.shuff) + test_sampler = torch.utils.data.distributed.DistributedSampler( + test_dataset, num_replicas=gwsize, rank=grank, shuffle=args.shuff) + +# distribute dataset to workers + # persistent workers is not possible for nworker=0 + pers_w = True if args.nworker > 1 else False + + # deterministic testrun - the same dataset each run + kwargs = {'worker_init_fn': seed_worker, + 'generator': g} if args.testrun else {} + + if torch.cuda.is_available(): + train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=args.batch_size, + sampler=train_sampler, num_workers=args.nworker, pin_memory=True, + persistent_workers=pers_w, prefetch_factor=args.prefetch, **kwargs) + test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=args.batch_size, + sampler=test_sampler, num_workers=args.nworker, pin_memory=True, + persistent_workers=pers_w, prefetch_factor=args.prefetch, **kwargs) + else: + train_loader = torch.utils.data.DataLoader( + train_dataset, batch_size=args.batch_size) + test_loader = torch.utils.data.DataLoader( + test_dataset, batch_size=args.batch_size) + + if grank == 0: + print('TIMER: read and concat data:', time.time()-st, 's') + + # create CNN model + model = Net().to(device) + + # distribute model to workers + distrib_model = my_trainer.distribute_model(model, device) + + # optimizer + optimizer = torch.optim.SGD( + distrib_model.parameters(), lr=args.lr, momentum=args.momentum) + + my_trainer.broadcast_params(distrib_model, optimizer) + + optimizer = my_trainer.distribute_optimizer(optimizer, distrib_model) + +# resume state + start_epoch = 1 + best_acc = np.Inf + res_name = 'checkpoint.pth.tar' + if os.path.isfile(res_name): + try: + if torch.cuda.is_available(): + dist.barrier() + # Map model to be loaded to specified single gpu. + loc = {'cuda:%d' % 0: 'cuda:%d' % lrank} if args.cuda else { + 'cpu:%d' % 0: 'cpu:%d' % lrank} + checkpoint = torch.load( + program_dir+'/'+res_name, map_location=loc) + else: + checkpoint = torch.load(program_dir+'/'+res_name) + start_epoch = checkpoint['epoch'] + best_acc = checkpoint['best_acc'] + distrib_model.load_state_dict(checkpoint['state_dict']) + optimizer.load_state_dict(checkpoint['optimizer']) + if torch.cuda.is_available(): + if grank == 0: + print(f'WARNING: restarting from {start_epoch} epoch') + else: + print(f'WARNING: restarting from {start_epoch} epoch') + except: + if torch.cuda.is_available(): + if grank == 0: + print(f'WARNING: restart file cannot be loaded, restarting!') + else: + print(f'WARNING: restart file cannot be loaded, restarting!') + + if start_epoch > args.epochs: + if torch.cuda.is_available(): + if grank == 0: + print(f'WARNING: given epochs are less than the one in the restart file!\n' + f'WARNING: SYS.EXIT is issued') + + my_trainer.clean_up() + sys.exit() + else: + print(f'WARNING: given epochs are less than the one in the restart file!\n' + f'WARNING: SYS.EXIT is issued') + sys.exit() + +# start trainin/testing loop + if grank == 0: + print('TIMER: broadcast:', time.time()-st, 's') + print(f'\nDEBUG: start training') + print(f'--------------------------------------------------------') + + et = time.time() + for epoch in range(start_epoch, args.epochs + 1): + lt = time.time() + # training + loss_acc = train(distrib_model, device, train_loader, + optimizer, epoch, grank, gwsize, args) + + # testing + acc_test = test(distrib_model, device, + test_loader, grank, gwsize, args) + + # save first epoch timer + if epoch == start_epoch: + first_ep_t = time.time()-lt + + # final epoch + if epoch + 1 == args.epochs: + train_loader.last_epoch = True + test_loader.last_epoch = True + + if grank == 0: + print('TIMER: epoch time:', time.time()-lt, 's') + print('DEBUG: accuracy:', acc_test, '%') + + # save state if found a better state + is_best = loss_acc < best_acc + if epoch % args.restart_int == 0: + save_state(epoch, distrib_model, loss_acc, optimizer, + res_name, grank, gwsize, is_best, my_trainer) + # reset best_acc + best_acc = min(loss_acc, best_acc) + +# finalise + # save final state + save_state(epoch, distrib_model, loss_acc, optimizer, + res_name, grank, gwsize, True, my_trainer) + # if torch.cuda.is_available(): + # dist.barrier() + + # some debug + if grank == 0: + print(f'\n--------------------------------------------------------') + print('DEBUG: training results:\n') + print('TIMER: first epoch time:', first_ep_t, ' s') + print('TIMER: last epoch time:', time.time()-lt, ' s') + print('TIMER: average epoch time:', (time.time()-et)/args.epochs, ' s') + print('TIMER: total epoch time:', time.time()-et, ' s') + if epoch > 1: + print('TIMER: total epoch-1 time:', + time.time()-et-first_ep_t, ' s') + print('TIMER: average epoch-1 time:', + (time.time()-et-first_ep_t)/(args.epochs-1), ' s') + print('DEBUG: last accuracy:', acc_test, '%') + print('DEBUG: memory req:', int(torch.cuda.memory_reserved(lrank)/1024/1024), 'MB') \ + if args.cuda else 'DEBUG: memory req: - MB' + print('DEBUG: memory summary:\n\n', + torch.cuda.memory_summary(0)) if args.cuda else '' + + if grank == 0: + print(f'TIMER: final time: {time.time()-st} s\n') + + my_trainer.clean_up() + + +if __name__ == "__main__": + main() + sys.exit() + +# eof diff --git a/pyproject.toml b/pyproject.toml index dd6408c6..b89f1a6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,19 +26,22 @@ maintainers = [ classifiers = ["Development Status :: Beta", "Programming Language :: Python"] dependencies = [ - "wandb>=0.15.11", - "mlflow>=2.7", - "jsonargparse[signatures]>=4.17.0", - "pyyaml>=6.0.1", - "omegaconf>=2.3.0", - "submitit>=1.4.6", - "typing-extensions==4.5.0", - "typing_extensions==4.5.0", + "wandb", + "mlflow", + "jsonargparse[signatures]", + "pyyaml", + "omegaconf", "rich>=13.5.3", "typer>=0.9.0", - "urllib3>=1.26.18", - "lightning>=2.0.0", - "torchmetrics>=1.2.0", + # "wandb>=0.15.11", + # "mlflow>=2.7", + # "jsonargparse[signatures]>=4.17.0", + # "pyyaml>=6.0.1", + # "omegaconf>=2.3.0", + # "submitit>=1.4.6", + # "typing-extensions==4.5.0", + # "typing_extensions==4.5.0", + # "urllib3>=2.0.5", ] # dynamic = ["version", "description"] @@ -47,6 +50,7 @@ dependencies = [ # TODO: add torch and tensorflow # torch = [] # tf = [] +distributed = ["deepspeed>=0.13.1", "horovod[tensorflow,keras,pytorch]>=0.28.1"] dev = [ "pytest>=7.4.2", "pytest-mock>=3.11.1", diff --git a/src/itwinai/cli.py b/src/itwinai/cli.py index 20977961..275d853a 100644 --- a/src/itwinai/cli.py +++ b/src/itwinai/cli.py @@ -19,6 +19,148 @@ app = typer.Typer() +@app.command() +def scalability_report( + pattern: Annotated[str, typer.Option( + help="Python pattern matching names of CSVs in sub-folders." + )], + plot_title: Annotated[Optional[str], typer.Option( + help=("Plot name.") + )] = None, + logy: Annotated[bool, typer.Option( + help=("Log scale on y axis.") + )] = False, + skip_id: Annotated[Optional[int], typer.Option( + help=("Skip epoch ID.") + )] = None, + archive: Annotated[Optional[str], typer.Option( + help=("Archive name to backup the data, without extension.") + )] = None, +): + """ + Generate scalability report merging all CSVs containing epoch time + records in sub-folders. + + Example: + >>> itwinai scalability-report --pattern="^epoch.+\\.csv$" --skip-id 0 \\ + >>> --plot-title "Some title" --logy --archive archive_name + """ + # TODO: add max depth and path different from CWD + import os + import re + import shutil + import pandas as pd + import matplotlib.pyplot as plt + # import numpy as np + + regex = re.compile(r'{}'.format(pattern)) + combined_df = pd.DataFrame() + csv_files = [] + for root, _, files in os.walk(os.getcwd()): + for file in files: + if regex.match(file): + fpath = os.path.join(root, file) + csv_files.append(fpath) + df = pd.read_csv(fpath) + if skip_id is not None: + df = df.drop(df[df.epoch_id == skip_id].index) + combined_df = pd.concat([combined_df, df]) + print("Merged CSV:") + print(combined_df) + + avg_times = ( + combined_df + .drop(columns='epoch_id') + .groupby(['name', 'nodes']) + .mean() + .reset_index() + ) + print("\nAvg over name and nodes:") + print(avg_times.rename(columns=dict(time='avg(time)'))) + + # fig, (sp_up_ax, eff_ax) = plt.subplots(1, 2, figsize=(12, 4)) + fig, sp_up_ax = plt.subplots(1, 1, figsize=(6, 4)) + if plot_title is not None: + fig.suptitle(plot_title) + + for name in set(avg_times.name.values): + df = avg_times[avg_times.name == name].drop(columns='name') + + # Debug + # compute_time = [3791., 1884., 1011., 598.] + # nodes = [1, 2, 4, 8] + # d = {'nodes': nodes, 'time': compute_time} + # df = pd.DataFrame(data=d) + + df["NGPUs"] = df["nodes"]*4 + # speedup + df["Speedup - ideal"] = df["nodes"].astype(float) + df["Speedup"] = df["time"].iloc[0] / df["time"] + df["Nworkers"] = 1 + + # efficiency + df["Threadscaled Sim. Time / s"] = df["time"] * \ + df["nodes"] * df["Nworkers"] + df["Efficiency"] = df["Threadscaled Sim. Time / s"].iloc[0] / \ + df["Threadscaled Sim. Time / s"] + + # Plot + # when lines are very close to each other + if logy: + sp_up_ax.semilogy( + df["NGPUs"].values, df["Speedup"].values, + marker='*', lw=1.0, label=name) + else: + sp_up_ax.plot( + df["NGPUs"].values, df["Speedup"].values, + marker='*', lw=1.0, label=name) + + if logy: + sp_up_ax.semilogy(df["NGPUs"].values, df["Speedup - ideal"].values, + ls='dashed', lw=1.0, c='k', label="ideal") + else: + sp_up_ax.plot(df["NGPUs"].values, df["Speedup - ideal"].values, + ls='dashed', lw=1.0, c='k', label="ideal") + sp_up_ax.legend(ncol=1) + + sp_up_ax.set_xticks(df["NGPUs"].values) + # sp_up_ax.set_yticks( + # np.arange(1, np.max(df["Speedup - ideal"].values) + 2, 1)) + + sp_up_ax.set_ylabel('Speedup') + sp_up_ax.set_xlabel('NGPUs (4 per node)') + sp_up_ax.grid() + plot_png = f"scaling_plot_{plot_title}.png" + plt.tight_layout() + plt.savefig(plot_png, bbox_inches='tight', format='png', dpi=300) + print("Saved scaling plot to: ", plot_png) + + if archive is not None: + if '/' in archive: + raise ValueError("Archive name must NOT contain a path. " + f"Received: '{archive}'") + if '.' in archive: + raise ValueError("Archive name must NOT contain an extension. " + f"Received: '{archive}'") + if os.path.isdir(archive): + raise ValueError(f"Folder '{archive}' already exists. " + "Change archive name.") + os.makedirs(archive) + for csvfile in csv_files: + shutil.copyfile(csvfile, os.path.join(archive, + os.path.basename(csvfile))) + shutil.copyfile(plot_png, os.path.join(archive, plot_png)) + avg_times.to_csv(os.path.join(archive, "avg_times.csv"), index=False) + archive_name = shutil.make_archive( + base_name=archive, # archive file name + format='gztar', + # root_dir='.', + base_dir=archive # folder path inside archive + ) + shutil.rmtree(archive) + print("Archived logs and plot at: ", archive_name) + + @app.command() def exec_pipeline( config: Annotated[Path, typer.Option( diff --git a/src/itwinai/distributed.py b/src/itwinai/distributed.py new file mode 100644 index 00000000..868f993a --- /dev/null +++ b/src/itwinai/distributed.py @@ -0,0 +1,5 @@ +import abc + + +class DistributedStrategy(abc.ABC): + """Abstract class to define the distributed backend methods.""" diff --git a/src/itwinai/loggers.py b/src/itwinai/loggers.py index d04becd7..d5ed0008 100644 --- a/src/itwinai/loggers.py +++ b/src/itwinai/loggers.py @@ -1,6 +1,7 @@ """Abstraction for loggers.""" import os +import csv from abc import ABCMeta, abstractmethod from contextlib import contextmanager from typing import Any, Dict, List, Optional, Union @@ -448,3 +449,29 @@ def log( batch_idx=batch_idx, **kwargs ) + + +class EpochTimeTracker: + def __init__(self, series_name: str, csv_file: str) -> None: + self.series_name = series_name + self._data = [] + self.csv_file = csv_file + with open(csv_file, 'w') as csvfile: + csvwriter = csv.writer(csvfile) + csvwriter.writerow(['name', 'nodes', 'epoch_id', 'time']) + + def add_epoch_time(self, epoch_idx, time): + n_nodes = os.environ.get('SLURM_NNODES', -1) + fields = (self.series_name, n_nodes, epoch_idx, time) + self._data.append(fields) + with open(self.csv_file, 'a') as csvfile: + csvwriter = csv.writer(csvfile) + csvwriter.writerow(fields) + + def save(self, csv_file: Optional[str] = None): + if not csv_file: + csv_file = self.csv_file + with open(csv_file, 'w') as csvfile: + csvwriter = csv.writer(csvfile) + csvwriter.writerow(['name', 'nodes', 'epoch_id', 'time']) + csvwriter.writerows(self._data) diff --git a/src/itwinai/parser.py b/src/itwinai/parser.py index 8e393652..24c521cd 100644 --- a/src/itwinai/parser.py +++ b/src/itwinai/parser.py @@ -5,190 +5,10 @@ import logging import os -from typing import Dict, Any, List, Type, Union, Optional +from typing import List, Type, Union, Optional from jsonargparse import ArgumentParser as JAPArgumentParser from jsonargparse import ActionConfigFile -import json from jsonargparse._formatters import DefaultHelpFormatter -from omegaconf import OmegaConf -from pathlib import Path - -from .components import BaseComponent -from .pipeline import Pipeline -from .utils import load_yaml - - -def add_replace_field( - config: Dict, - key_chain: str, - value: Any -) -> None: - """Replace or add (if not present) a field in a dictionary, following a - path of dot-separated keys. Adding is not supported for list items. - Inplace operation. - Args: - config (Dict): dictionary to be modified. - key_chain (str): path of nested (dot-separated) keys to specify the - location - of the new value (e.g., 'foo.bar.line' adds/overwrites the value - located at config['foo']['bar']['line']). - value (Any): the value to insert. - """ - sub_config = config - for idx, k in enumerate(key_chain.split('.')): - if idx >= len(key_chain.split('.')) - 1: - # Last key reached - break - - if isinstance(sub_config, (list, tuple)): - k = int(k) - next_elem = sub_config[k] - else: - next_elem = sub_config.get(k) - - if not isinstance(next_elem, (dict, list, tuple)): - sub_config[k] = dict() - - sub_config = sub_config[k] - if isinstance(sub_config, (list, tuple)): - k = int(k) - sub_config[k] = value - - -class ConfigParser: - """ - Parses a pipeline from a configuration file. - It also provides functionalities for dynamic override - of fields by means of nested key notation. - - Args: - config (Union[str, Dict]): path to YAML configuration file - or dict storing a configuration. - override_keys (Optional[Dict[str, Any]], optional): dict mapping - nested keys to the value to override. Defaults to None. - - Example: - - >>> # pipeline.yaml file - >>> pipeline: - >>> class_path: itwinai.pipeline.Pipeline - >>> init_args: - >>> steps: - >>> - class_path: dataloader.MNISTDataModuleTorch - >>> init_args: - >>> save_path: .tmp/ - >>> - >>> - class_path: itwinai.torch.trainer.TorchTrainerMG - >>> init_args: - >>> model: - >>> class_path: model.Net - >>> loss: - >>> class_path: torch.nn.NLLLoss - >>> init_args: - >>> reduction: mean - - >>> from itwinai.parser import ConfigParser - >>> - >>> parser = ConfigParser( - >>> config='pipeline.yaml', - >>> override_keys={ - >>> 'pipeline.init_args.steps.0.init_args.save_path': /save/path - >>> } - >>> ) - >>> pipeline = parser.parse_pipeline() - >>> print(pipeline) - >>> print(pipeline.steps) - >>> - >>> dataloader = parser.parse_step(0) - >>> print(dataloader) - >>> print(dataloader.save_path) - """ - - config: Dict - pipeline: Pipeline - - def __init__( - self, - config: Union[str, Dict], - override_keys: Optional[Dict[str, Any]] = None - ) -> None: - self.config = config - self.override_keys = override_keys - if isinstance(self.config, (str, Path)): - self.config = load_yaml(self.config) - self._dynamic_override_keys() - self._omegaconf_interpolate() - - def _dynamic_override_keys(self): - if self.override_keys is not None: - for key_chain, value in self.override_keys.items(): - add_replace_field(self.config, key_chain, value) - - def _omegaconf_interpolate(self) -> None: - """Performs variable interpolation with OmegaConf on internal - configuration file. - """ - conf = OmegaConf.create(self.config) - self.config = OmegaConf.to_container(conf, resolve=True) - - def parse_pipeline( - self, - pipeline_nested_key: str = "pipeline", - verbose: bool = False - ) -> Pipeline: - """Merges steps into pipeline and parses it. - - Args: - pipeline_nested_key (str, optional): nested key in the - configuration file identifying the pipeline object. - Defaults to "pipeline". - verbose (bool): if True, prints the assembled pipeline - to console formatted as JSON. - - Returns: - Pipeline: instantiated pipeline. - """ - pipe_parser = JAPArgumentParser() - pipe_parser.add_subclass_arguments(Pipeline, "pipeline") - - pipe_dict = self.config - for key in pipeline_nested_key.split('.'): - pipe_dict = pipe_dict[key] - # pipe_dict = self.config[pipeline_nested_key] - pipe_dict = {"pipeline": pipe_dict} - - if verbose: - print("Assembled pipeline:") - print(json.dumps(pipe_dict, indent=4)) - - # Parse pipeline dict once merged with steps - conf = pipe_parser.parse_object(pipe_dict) - pipe = pipe_parser.instantiate_classes(conf) - self.pipeline = pipe["pipeline"] - return self.pipeline - - def parse_step( - self, - step_idx: Union[str, int], - pipeline_nested_key: str = "pipeline", - verbose: bool = False - ) -> BaseComponent: - pipeline_dict = self.config - for key in pipeline_nested_key.split('.'): - pipeline_dict = pipeline_dict[key] - - step_dict_config = pipeline_dict['init_args']['steps'][step_idx] - - if verbose: - print(f"STEP '{step_idx}' CONFIG:") - print(json.dumps(step_dict_config, indent=4)) - - # Wrap config under "step" field and parse it - step_dict_config = {'step': step_dict_config} - step_parser = JAPArgumentParser() - step_parser.add_subclass_arguments(BaseComponent, "step") - parsed_namespace = step_parser.parse_object(step_dict_config) - return step_parser.instantiate_classes(parsed_namespace)["step"] class ArgumentParser(JAPArgumentParser): @@ -208,7 +28,11 @@ def __init__( default_meta: bool = True, **kwargs, ) -> None: - """Initializer for ArgumentParser instance. + """Initializer for ArgumentParser instance. It can parse arguments from + a series of configuration files. Example: + + >>> python main.py --config base-conf.yaml --config other-conf.yaml \\ + >>> --param OVERRIDE_VAL All the arguments from the initializer of `argparse.ArgumentParser `_ diff --git a/src/itwinai/tensorflow/distributed.py b/src/itwinai/tensorflow/distributed.py new file mode 100644 index 00000000..e6c5f28a --- /dev/null +++ b/src/itwinai/tensorflow/distributed.py @@ -0,0 +1,36 @@ +import tensorflow as tf +import os + + +def get_strategy(): + """Strategy for distributed TensorFlow training""" + cluster_resolver = tf.distribute.cluster_resolver.SlurmClusterResolver( + port_base=12345) + implementation = tf.distribute.experimental.CommunicationImplementation.NCCL + communication_options = tf.distribute.experimental.CommunicationOptions( + implementation=implementation) + + # declare distribution strategy + tf_dist_strategy = tf.distribute.MultiWorkerMirroredStrategy( + cluster_resolver=cluster_resolver, + communication_options=communication_options + ) + + # number of workers + n_workers = int(os.environ['SLURM_NTASKS']) + # list of devices per worker + devices = tf.config.experimental.list_physical_devices('GPU') + # number of devices per worker + n_gpus_per_worker = len(devices) + # total number of GPUs + n_gpus = n_workers * n_gpus_per_worker + + # get total number of detected GPUs + print("Number of detected devices: {}".format( + n_gpus)) + + # get total number of workers + print("Number of devices: {}".format( + tf_dist_strategy.num_replicas_in_sync)) + + return tf_dist_strategy, tf_dist_strategy.num_replicas_in_sync diff --git a/src/itwinai/tensorflow/trainer.py b/src/itwinai/tensorflow/trainer.py index f1a10214..d8c40012 100644 --- a/src/itwinai/tensorflow/trainer.py +++ b/src/itwinai/tensorflow/trainer.py @@ -5,6 +5,7 @@ import tensorflow as tf from ..components import Trainer, monitor_exec +from itwinai.tensorflow.distributed import get_strategy def import_class(name): @@ -31,6 +32,8 @@ class TensorflowTrainer(Trainer): def __init__( self, epochs, + train_dataset, + validation_dataset, batch_size, callbacks, model_dict: Dict, @@ -54,6 +57,18 @@ def __init__( # Create distributed TF vars if self.strategy: + tf_dist_strategy, n_devices = get_strategy() + # get total number of workers + print("Number of devices: {}".format(n_devices)) + # distribute datasets among MirroredStrategy's replicas + dist_train_dataset = ( + tf_dist_strategy.experimental_distribute_dataset( + train_dataset + )) + dist_validation_dataset = ( + tf_dist_strategy.experimental_distribute_dataset( + validation_dataset + )) with self.strategy.scope(): # TODO: move loss, optimizer and metrics instantiation under # here @@ -61,6 +76,7 @@ def __init__( # https://www.tensorflow.org/guide/distributed_training#use_tfdistributestrategy_with_keras_modelfit # self.model: tf.keras.Model = parser.instantiate_classes( # model_dict).model + # TODO: add dataloaders and model instances self.model: tf.keras.Model = instance_from_dict(model_dict) compile_conf = self.instantiate_compile_conf(compile_conf) self.model.compile(**compile_conf) diff --git a/src/itwinai/torch/distributed.py b/src/itwinai/torch/distributed.py new file mode 100644 index 00000000..34174346 --- /dev/null +++ b/src/itwinai/torch/distributed.py @@ -0,0 +1,920 @@ +import abc +from typing import Any, List, Optional, Tuple +from pathlib import Path +import json +import os + +import deepspeed +import torch +import torch.distributed as dist +import horovod.torch as hvd +import torch.nn as nn +import torch.optim as optim +from torch.optim.lr_scheduler import _LRScheduler as LRScheduler +from torch.optim.optimizer import Optimizer + +from ..distributed import DistributedStrategy + + +class TorchDistributedStrategy(DistributedStrategy): + """Abstract class to define the distributed backend methods for + PyTorch models. + """ + @abc.abstractmethod + def init(self) -> None: + """Initializes the chosen distributed backend""" + + # @abc.abstractmethod + # def distributed_engine( + # self, model: nn.Module, optimizer: Optimizer, + # lr_scheduler: Optional[LRScheduler] = None + # ) -> ModelEngine: + # """Build a distributed model engine.""" + + @abc.abstractmethod + def distributed( + self, model: nn.Module, optimizer: Optimizer, + lr_scheduler: Optional[LRScheduler] = None + ) -> Tuple[nn.Module, Optimizer, Optional[LRScheduler]]: + """Setup model, optimizer and scheduler for distributed.""" + + @abc.abstractmethod + def dist_gwsize(self) -> int: + """Returns the total number of processes (global world size). + + Returns: + int: global world size. + """ + + @abc.abstractmethod + def dist_lwsize(self) -> int: + """Returns the number of local workers available on a node + (local world size). + Usually it is equal to the number of available GPUs. + + Returns: + int: local world size. + """ + + @abc.abstractmethod + def dist_grank(self) -> int: + """Returns the global rank of the current process. + Rank ranges from 0 to world_size. + + Returns: + int: global rank. + """ + + @abc.abstractmethod + def dist_lrank(self) -> int: + """Returns the local rank of the current process. + + Returns: + int: local rank. + """ + + def is_main_worker(self) -> bool: + """Checks if local worker has global rank equal to zero. + + Returns: + bool: True if main worker. + """ + return self.dist_grank() == 0 + + def dist_device(self) -> str: + """Device used by local worker. + + Returns: + str: torch device in the form 'cuda:N'. + """ + return f"cuda:{self.dist_lrank()}" + + @abc.abstractmethod + def clean_up(self) -> None: + """Cleans up resources allocated by distributed strategy.""" + + @abc.abstractmethod + def par_allgather_obj(self, obj: Any) -> List[Any]: + """Gathers any object from the whole group in a list (to all workers). + + Args: + obj (Any): object to gather from all workers. + + Returns: + List[Any]: list of objects gathered from all workers. + """ + + +class DDPDistributedStrategy(TorchDistributedStrategy): + """PyTorch DDP distributed strategy class. + + Args: + backend (str): Name of the communication backend to employ. + """ + + backend: str + + def __init__(self, backend: str) -> None: + super().__init__() + self.backend = backend + + def init(self) -> None: + """Initializes the distributed process group and the distributed + package. + """ + if torch.cuda.is_available() and torch.cuda.device_count() > 1: + dist.init_process_group(backend=self.backend) + else: + print("WARNING: trying to run distributed on insufficient" + " resources. Skipping distributed process group setup.") + + # def distributed_engine( + # self, model: nn.Module, optimizer: Optimizer, + # lr_scheduler: Optional[LRScheduler] = None, + # mixed_precision: bool = False + # ) -> ModelEngine: + # """Build a distributed model engine.""" + # if torch.cuda.is_available(): + # # device = self.dist_lrank() + # model = model.to(self.dist_device()) + # dist_model = torch.nn.parallel.DistributedDataParallel( + # model, + # device_ids=[self.dist_device()], + # output_device=self.dist_device() + # ) + # else: + # dist_model = model + + # model_engine = DDPModelEngine( + # dist_model, optimizer, lr_scheduler, + # mixed_precision=mixed_precision + # ) + + # return model_engine + + def distributed( + self, model: nn.Module, optimizer: Optimizer, + lr_scheduler: Optional[LRScheduler] = None, + **kwargs + ) -> Tuple[nn.Module, Optimizer, Optional[LRScheduler]]: + """Setup model, optimizer and scheduler for distributed.""" + if torch.cuda.is_available(): + # device = self.dist_lrank() + model = model.to(self.dist_device()) + dist_model = torch.nn.parallel.DistributedDataParallel( + model, + device_ids=[self.dist_device()], + output_device=self.dist_device() + ) + else: + dist_model = model + + return dist_model, optimizer, lr_scheduler + + def dist_gwsize(self) -> int: + """Returns the total number of processes (global world size). + + Returns: + int: global world size. + """ + return dist.get_world_size() + + def dist_lwsize(self) -> int: + """Returns the local number of workers available per node, + which is usually the number of GPUs available. + + Returns: + int: local world size. + """ + return torch.cuda.device_count() + + def dist_grank(self) -> int: + """Returns the global rank of the current process, where + rank ranges from 0 to world_size. + + Returns: + int: global rank. + """ + return dist.get_rank() + + def dist_lrank(self) -> int: + """Returns the local rank of the current process. + + Returns: + int: local rank. + """ + return dist.get_rank() % torch.cuda.device_count() + + def clean_up(self) -> None: + """Destroys the current process group.""" + if torch.cuda.is_available(): + dist.barrier() + dist.destroy_process_group() + + def par_allgather_obj(self, obj: Any) -> List[Any]: + """Gathers any object from the whole group + in a list (to all workers). + + Args: + obj (Any): Object to gather from all workers. + + Returns: + List[Any]: List of gathered objects. + """ + res = [None] * self.dist_gwsize() + dist.all_gather_object(res, obj) + return res + + +class DSDistributedStrategy(TorchDistributedStrategy): + """DeepSpeed distributed strategy class. + + Args: + backend (str): Name of the communication backend to employ. + config (Union[dict, Path, str]): DeepSpeed config. Either a + dictionary or a path to a JSON file. + """ + + backend: str + + def __init__( + self, + backend: str + ) -> None: + super().__init__() + self.backend = backend + + def _load_config(self, ds_config) -> None: + if isinstance(ds_config, (str, Path)): + with open(ds_config) as fp: + self.config = json.load(fp) + elif isinstance(ds_config, dict): + self.config = ds_config + else: + raise ValueError("ds_config is neither a dictionary not a path.") + + def init(self) -> None: + """Initializes the distributed process group and the distributed + package. + """ + # https://github.com/Lightning-AI/pytorch-lightning/issues/13567 + ompi_lrank = os.environ.get('OMPI_COMM_WORLD_LOCAL_RANK') + os.environ['OMPI_COMM_WORLD_LOCAL_RANK'] = os.environ.get( + 'LOCAL_RANK', ompi_lrank) + + # https://deepspeed.readthedocs.io/en/latest/initialize.html#training-initialization + deepspeed.init_distributed(dist_backend=self.backend) + + def distributed( + self, model: nn.Module, optimizer: Optional[Optimizer] = None, + lr_scheduler: Optional[LRScheduler] = None, + model_parameters: Optional[Any] = None, + **init_kwargs + ) -> Tuple[nn.Module, Optimizer, Optional[LRScheduler]]: + """Setup model, optimizer and scheduler for distributed.""" + if init_kwargs.get("config"): + self._load_config(init_kwargs.get("config")) + # https://deepspeed.readthedocs.io/en/latest/initialize.html#training-initialization + # To prioritize optim in the config, you need to pass optim=None + distrib_model, optimizer, _, lr_scheduler = deepspeed.initialize( + model=model, + model_parameters=model_parameters, + optimizer=optimizer, + lr_scheduler=lr_scheduler, + dist_init_required=True, + **init_kwargs + ) + return distrib_model, optimizer, lr_scheduler + + def dist_gwsize(self) -> int: + """Returns the total number of processes (global world size). + + Returns: + int: global world size. + """ + return dist.get_world_size() + + def dist_lwsize(self) -> int: + """Returns the local number of workers available per node, + which is usually the number of GPUs available. + + Returns: + int: local world size. + """ + return torch.cuda.device_count() + + def dist_grank(self) -> int: + """Returns the global rank of the current process, where + rank ranges from 0 to world_size. + + Returns: + int: global rank. + """ + return dist.get_rank() + + def dist_lrank(self) -> int: + """Returns the local rank of the current process. + + Returns: + int: local rank. + """ + return dist.get_rank() % torch.cuda.device_count() + + def clean_up(self) -> None: + """Destroys the current process group.""" + deepspeed.sys.exit() + + def par_allgather_obj(self, obj: Any) -> list[Any]: + """Gathers any object from the whole group + in a list (to all workers). + + Args: + obj (Any): Object to gather from all workers. + + Returns: + List[Any]: List of gathered objects. + """ + res = [None] * self.dist_gwsize() + dist.all_gather_object(res, obj) + return res + + +class HVDDistributedStrategy(TorchDistributedStrategy): + """Horovod distributed strategy class.""" + + def init(self) -> None: + """Initializes the Horovod distributed backend.""" + hvd.init() + torch.cuda.set_device(hvd.local_rank()) + + def distributed( + self, model: nn.Module, optimizer: Optional[Optimizer] = None, + lr_scheduler: Optional[LRScheduler] = None, + **optim_kwargs + ) -> Tuple[nn.Module, Optimizer, Optional[LRScheduler]]: + """Setup model, optimizer and scheduler for distributed.""" + + model.to(self.dist_device()) + + # Scale learning rate + # https://github.com/horovod/horovod/issues/1653#issuecomment-574764452 + lr_scaler = 1 + if optim_kwargs.get('op') == hvd.Adasum: + lr_scaler = hvd.local_size() + elif optim_kwargs.get('op') == hvd.Average: + lr_scaler = hvd.size() + for g in optimizer.param_groups: + g['lr'] *= lr_scaler + + self._broadcast_params(model, optimizer) + + distOptimizer = hvd.DistributedOptimizer( + optimizer, + named_parameters=model.named_parameters(), + **optim_kwargs + ) + return model, distOptimizer, lr_scheduler + + def _broadcast_params( + self, model: nn.Module, optimizer: optim.Optimizer + ) -> None: + """Broadcasts variables from root rank to all other processes. + + Args: + model (nn.Module): ML model that is to be broadcasted + across processes. + optimizer (optim.Optimizer): Optimizer that is to be broadcasted + across processes. + """ + hvd.broadcast_parameters(model.state_dict(), root_rank=0) + hvd.broadcast_optimizer_state(optimizer, root_rank=-0) + + def dist_gwsize(self) -> int: + """Returns the total number of processes (global world size). + + Returns: + int: global world size. + """ + return hvd.size() + + def dist_lwsize(self) -> int: + """Returns the local number of workers available per node, + which is usually the number of GPUs available. + + Returns: + int: local world size. + """ + return hvd.local_size() + + def dist_grank(self) -> int: + """Returns the global rank of the current process, where + rank ranges from 0 to world_size. + + Returns: + int: global rank. + """ + return hvd.rank() + + def dist_lrank(self) -> int: + """Returns the local rank of the current process. + + Returns: + int: local rank. + """ + return hvd.local_rank() + + def clean_up(self) -> None: + """Shuts Horovod down.""" + hvd.shutdown() + + def par_allgather_obj(self, obj: Any) -> list[Any]: + """Gathers scalar objects across all workers to a + list with size(#worker), uses horovod communicator + + Args: + obj (Any): object in a worker. + + Returns: + list: gathered list with size(#worker). + """ + return hvd.allgather_object(obj) + + +# class TorchDistributedStrategy_old(DistributedStrategy): +# """Abstract class to define the distributed backend methods for +# PyTorch models. +# """ +# @abc.abstractmethod +# def init_backend(self) -> None: +# """Initializes the chosen distributed backend""" + +# @abc.abstractmethod +# def distribute_model(self, model: Any) -> Any: +# """Distributes a machine learning model. + +# Args: +# model (Any): a generic ML model to be distributed. + +# Returns: +# Any: distributed model instance. +# """ + +# @abc.abstractmethod +# def broadcast_params(self, model: Any, optimizer: Any) -> None: +# """Broadcasts variables from root rank to all other processes/ + +# Args: +# model (Any): distributed model. +# optimizer (Any): optimizer. +# """ + +# @abc.abstractmethod +# def distribute_optimizer(self, optimizer: Any, model: Any) -> Any: +# """Distribute optimizer. + +# Args: +# optimizer (Any): optimizer. +# model (Any): distributed model. + +# Returns: +# Any: distributed optimizer. +# """ + +# @abc.abstractmethod +# def dist_gwsize(self) -> int: +# """Returns the total number of processes (global world size). + +# Returns: +# int: global world size. +# """ + +# @abc.abstractmethod +# def dist_lwsize(self) -> int: +# """Returns the number of local workers available on a node +# (local world size). +# Usually it is equal to the number of available GPUs. + +# Returns: +# int: local world size. +# """ + +# @abc.abstractmethod +# def dist_grank(self) -> int: +# """Returns the global rank of the current process. +# Rank ranges from 0 to world_size. + +# Returns: +# int: global rank. +# """ + +# @abc.abstractmethod +# def dist_lrank(self) -> int: +# """Returns the local rank of the current process. + +# Returns: +# int: local rank. +# """ + +# def is_main_worker(self) -> bool: +# """Checks if local worker has global rank equal to zero. + +# Returns: +# bool: True if main worker. +# """ +# return self.dist_grank() == 0 + +# def dist_device(self) -> str: +# """Device used by local worker. + +# Returns: +# str: torch device in the form 'cuda:N'. +# """ +# return f"cuda:{self.dist_lrank()}" + +# @abc.abstractmethod +# def clean_up(self) -> None: +# """Cleans up resources allocated by distributed strategy.""" + +# @abc.abstractmethod +# def par_allgather_obj(self, obj: Any) -> List[Any]: +# """Gathers any object from the whole group in a list +# (to all workers). + +# Args: +# obj (Any): object to gather from all workers. + +# Returns: +# List[Any]: list of objects gathered from all workers. +# """ + + +# class DDPDistributedStrategy_old(TorchDistributedStrategy_old): +# """PyTorch DDP distributed strategy class. + +# Args: +# backend (str): Name of the communication backend to employ. +# """ + +# backend: str + +# def __init__(self, backend: str) -> None: +# super().__init__() +# self.backend = backend + +# def init_backend(self) -> None: +# """Initializes the distributed process group and the distributed +# package. +# """ +# if torch.cuda.is_available(): +# dist.init_process_group(backend=self.backend) + +# def distribute_model(self, model: nn.Module) -> nn.Module: +# """Achieves data parallelism by synchronizing the gradients +# across each model replica located in each available +# computing device. + +# Args: +# model (nn.Module): ML model to be distributed. + +# Returns: +# nn.Module: Distributed model replicas across all devices. +# that are to be synchronized. +# """ +# if torch.cuda.is_available(): +# # device = self.dist_lrank() +# model = model.to(self.dist_device()) +# dist_model = torch.nn.parallel.DistributedDataParallel( +# model, +# device_ids=[self.dist_device()], +# output_device=self.dist_device() +# ) +# else: +# dist_model = model + +# return dist_model + +# def broadcast_params( +# self, +# model: nn.Module, +# optimizer: optim.Optimizer +# ) -> None: +# """Do nothing. Only applicable for Horovod. + +# Args: +# model (nn.Module): ML model +# optimizer (optim.Optimizer): Optimizer +# """ +# pass + +# def distribute_optimizer( +# self, +# optimizer: optim.Optimizer, +# model: nn.Module = None +# ) -> optim.Optimizer: +# """Returns the optimizer from argument. + +# Args: +# optimizer (optim.Optimizer): optimizer. +# model (nn.Module): ML model. Unused here. + +# Returns: +# optim.Optimizer: Distributed optimizer. +# """ +# return optimizer + +# def dist_gwsize(self) -> int: +# """Returns the total number of processes (global world size). + +# Returns: +# int: global world size. +# """ +# return dist.get_world_size() + +# def dist_lwsize(self) -> int: +# """Returns the local number of workers available per node, +# which is usually the number of GPUs available. + +# Returns: +# int: local world size. +# """ +# return torch.cuda.device_count() + +# def dist_grank(self) -> int: +# """Returns the global rank of the current process, where +# rank ranges from 0 to world_size. + +# Returns: +# int: global rank. +# """ +# return dist.get_rank() + +# def dist_lrank(self) -> int: +# """Returns the local rank of the current process. + +# Returns: +# int: local rank. +# """ +# return dist.get_rank() % torch.cuda.device_count() + +# def clean_up(self) -> None: +# """Destroys the current process group.""" +# if torch.cuda.is_available(): +# dist.barrier() +# dist.destroy_process_group() + +# def par_allgather_obj(self, obj: Any) -> List[Any]: +# """Gathers any object from the whole group +# in a list (to all workers). + +# Args: +# obj (Any): Object to gather from all workers. + +# Returns: +# List[Any]: List of gathered objects. +# """ +# res = [None] * self.dist_gwsize() +# dist.all_gather_object(res, obj) +# return res + + +# class DSDistributedStrategy_old(TorchDistributedStrategy_old): +# """DeepSpeed distributed strategy class. + +# Args: +# backend (str): Name of the communication backend to employ. +# config (Union[dict, Path, str]): DeepSpeed config. Either a +# dictionary or a path to a JSON file. +# """ + +# config: Dict = None +# backend: str + +# def __init__( +# self, +# backend: str, +# config: Union[Dict, Path, str] +# ) -> None: +# super().__init__() +# self.backend = backend +# self._load_config(config) + +# def _load_config(self, ds_config): +# if isinstance(ds_config, (str, Path)): +# with open(ds_config) as fp: +# self.config = json.load(fp) +# elif isinstance(ds_config, dict): +# self.config = ds_config +# else: +# raise ValueError("ds_config is not a dictionary not a path.") + +# def init_backend(self) -> None: +# """Initializes the distributed process group and the distributed +# package. +# """ +# deepspeed.init_distributed(dist_backend=self.backend) + +# def distribute_model(self, model: nn.Module) -> nn.Module: +# """Achieves data parallelism by synchronizing the gradients +# across each model replica located in each available +# computing device. + +# Args: +# model (nn.Module): ML model to be distributed. + +# Returns: +# nn.Module: Distributed model replicas across all devices +# that are to be synchronized. +# """ +# distrib_model, __, __, __ = deepspeed.initialize( +# model=model, +# model_parameters=model.parameters(), +# dist_init_required=True, +# config=self.config +# ) +# return distrib_model + +# def broadcast_params( +# self, model: nn.Module, optimizer: optim.Optimizer +# ) -> None: +# """Only applicable for Horovod. Does nothing. + +# Args: +# model (nn.Module): ML model. +# optimizer (optim.Optimizer): optimizer. +# """ +# pass + +# def distribute_optimizer( +# self, +# optimizer: optim.Optimizer, +# model: nn.Module = None +# ) -> optim.Optimizer: +# """Returns the optimizer from argument. + +# Args: +# optimizer (optim.Optimizer): torch optimizer. +# model (nn.Module): torch neural network. + +# Returns: +# optim.Optimizer: distributed optimizer. +# """ +# return optimizer + +# def dist_gwsize(self) -> int: +# """Returns the total number of processes (global world size). + +# Returns: +# int: global world size. +# """ +# return dist.get_world_size() + +# def dist_lwsize(self) -> int: +# """Returns the local number of workers available per node, +# which is usually the number of GPUs available. + +# Returns: +# int: local world size. +# """ +# return torch.cuda.device_count() + +# def dist_grank(self) -> int: +# """Returns the global rank of the current process, where +# rank ranges from 0 to world_size. + +# Returns: +# int: global rank. +# """ +# return dist.get_rank() + +# def dist_lrank(self) -> int: +# """Returns the local rank of the current process. + +# Returns: +# int: local rank. +# """ +# return dist.get_rank() % torch.cuda.device_count() + +# def clean_up(self) -> None: +# """Destroys the current process group.""" +# deepspeed.sys.exit() + +# def par_allgather_obj(self, obj: Any) -> list[Any]: +# """Gathers any object from the whole group +# in a list (to all workers). + +# Args: +# obj (Any): Object to gather from all workers. + +# Returns: +# List[Any]: List of gathered objects. +# """ +# res = [None] * self.dist_gwsize() +# dist.all_gather_object(res, obj) +# return res + + +# class HVDDistributedStrategy_old(TorchDistributedStrategy_old): +# """Horovod distributed strategy class.""" + +# def init_backend(self) -> None: +# """Initializes the Horovod distributed backend.""" +# hvd.init() + +# def distribute_model(self, model: nn.Module) -> nn.Module: +# """Only applicable for DDP and DeepSpeed. +# For Horovod, returns the same model passed as argument. + +# Args: +# model (nn.Module): ML model to be distributed. + +# Returns: +# nn.Module: ML model passed in the argument. +# """ +# return model + +# def broadcast_params( +# self, model: nn.Module, optimizer: optim.Optimizer +# ) -> None: +# """Broadcasts variables from root rank to all other processes. + +# Args: +# model (nn.Module): ML model that is to be broadcasted +# across processes. +# optimizer (optim.Optimizer): Optimizer that is to be broadcasted +# across processes. +# """ +# hvd.broadcast_parameters(model.state_dict(), root_rank=0) +# hvd.broadcast_optimizer_state(optimizer, root_rank=-0) + +# def distribute_optimizer( +# self, +# optimizer: optim.Optimizer, +# model: nn.Module +# ) -> optim.Optimizer: +# """Constructs a DistributedOptimizer, for computing single-process +# gradient values and applying gradient updates after the gradients +# have been combined across all the Horovod ranks. + +# Args: +# optimizer (optim.Optimizer): Optimizer to be distributed. +# model (nn.Module): ML model to be trained. + +# Returns: +# optim.Optimizer: Distributed optimizer across all ranks. +# """ +# distOptimizer = hvd.DistributedOptimizer( +# optimizer, +# named_parameters=model.named_parameters(), +# op=hvd.Average +# ) +# return distOptimizer + +# def dist_gwsize(self) -> int: +# """Returns the total number of processes (global world size). + +# Returns: +# int: global world size. +# """ +# return hvd.size() + +# def dist_lwsize(self) -> int: +# """Returns the local number of workers available per node, +# which is usually the number of GPUs available. + +# Returns: +# int: local world size. +# """ +# return hvd.local_size() + +# def dist_grank(self) -> int: +# """Returns the global rank of the current process, where +# rank ranges from 0 to world_size. + +# Returns: +# int: global rank. +# """ +# return hvd.rank() + +# def dist_lrank(self) -> int: +# """Returns the local rank of the current process. + +# Returns: +# int: local rank. +# """ +# return hvd.local_rank() + +# def clean_up(self) -> None: +# """Shuts Horovod down.""" +# hvd.shutdown() + +# def par_allgather_obj(self, obj: Any) -> list[Any]: +# """Gathers scalar objects across all workers to a +# list with size(#worker), uses horovod communicator + +# Args: +# obj (Any): object in a worker. + +# Returns: +# list: gathered list with size(#worker). +# """ +# return hvd.allgather_object(obj) diff --git a/src/itwinai/torch/engine.py b/src/itwinai/torch/engine.py new file mode 100644 index 00000000..7084d6ec --- /dev/null +++ b/src/itwinai/torch/engine.py @@ -0,0 +1,276 @@ +""" +Model engine which wraps a torch NN. Still under development. May be removed... +""" + +import abc +from typing import Any, Union, Optional, Callable + +from pydantic import BaseModel + +import torch +import torch.nn as nn +import torch.optim as optim +from torch.optim.lr_scheduler import _LRScheduler as LRScheduler +from torch.cuda import amp +from torch import autocast + + +class OptimizerConfig: + def __init__(self, optim_class, **kwargs) -> None: + self.optim_class = optim_class + self.kwargs = kwargs + + def to_optim(self, parameters) -> optim.Optimizer: + return self.optim_class(parameters, **self.kwargs) + + +class LRSchedulerConfig: + def __init__(self, scheduler_class, **kwargs) -> None: + self.scheduler_class = scheduler_class + self.kwargs = kwargs + + def to_scheduler(self, optim) -> LRScheduler: + return self.scheduler_class(optim, **self.kwargs) + + +class ModelEngineConfig(BaseModel): + mixed_precision: bool = False + + +class ModelEngine(abc.ABC): + """Wrapper around ML model, which abstracts from distributed and + mixed-precision models. + """ + + model: nn.Module + _model_parameters: Any + optimizer: optim.Optimizer + lr_scheduler: LRScheduler + # config: ModelEngineConfig + mixed_precision: bool = False + grad_scaler: amp.GradScaler = None + + def __init__( + self, + model: nn.Module, + # model_parameters: Any, + optimizer: Union[optim.Optimizer, OptimizerConfig], + lr_scheduler: Optional[Union[LRScheduler, LRSchedulerConfig]] = None, + mixed_precision: bool = False + # config: Optional[ModelEngineConfig] = None + ) -> None: + super().__init__() + self.model = model + self.optimizer = optimizer + self.lr_scheduler = lr_scheduler + # self._model_parameters = model_parameters + # if isinstance(optimizer, OptimizerConfig): + # self.optimizer = optimizer.to_optim(model_parameters) + # else: + # self.optimizer = optimizer + + # if isinstance(lr_scheduler, LRSchedulerConfig): + # self.lr_scheduler = lr_scheduler.to_scheduler(self.optimizer) + # else: + # self.lr_scheduler = lr_scheduler + + # if not config: + # self.config = ModelEngineConfig() + self.mixed_precision = mixed_precision + if mixed_precision: + self.grad_scaler = amp.GradScaler() + + def __call__(self, *args: Any, **kwds: Any) -> Any: + """Performs the forward operation.""" + # Wrapper of self.forward() + return self.forward(*args, **kwds) + + def forward(self, *args: Any, **kwds: Any) -> Any: + """Performs the forward operation.""" + return self.model(*args, **kwds) + + def train(self, mode: bool = True) -> nn.Module: + """Set model in training mode.""" + self.model.train(mode=mode) + return self.model + + def eval(self) -> nn.Module: + """Set model in inference mode.""" + self.model.eval() + return self.model + + def to(self, device) -> nn.Module: + """Move model to specified device.""" + self.model.to(device) + return self.model + + @abc.abstractmethod + def zero_grad(): + """Set gradients to zero for the optimizer.""" + + @abc.abstractmethod + def backward(self, loss_fn: Callable, *loss_args) -> torch.Tensor: + """Perform backward pass and return the loss. + + Args: + loss_fn (Callable): computes the loss. + *loss_args: are the arguments to be passed to ``loss_fn``. + + Returns: + torch.Tensor: computed loss. + """ + + @abc.abstractmethod + def optimizer_step(self): + """Perform optimizer step.""" + + @abc.abstractmethod + def lr_scheduler_step(self): + """Perform lr scheduler step, if present.""" + # This should be incorporated in the optim step: + # https://deepspeed.readthedocs.io/en/latest/schedulers.html + # scheduler is updated automatically at each training step + + @abc.abstractmethod + def save_checkpoint(self): + """Save checkpoint to persistent storage.""" + + +class DDPModelEngine(ModelEngine): + """Model engine for torch DDP distributed strategy.""" + + def forward(self, *args: Any, **kwds: Any) -> Any: + """Performs the forward operation.""" + if self.mixed_precision: + # https://pytorch.org/docs/stable/notes/amp_examples.html + # Runs the forward pass with autocasting. + with autocast(device_type='cuda', dtype=torch.float16): + return self.model(*args, **kwds) + else: + return self.model(*args, **kwds) + + def zero_grad(self): + """Set gradients to zero for the optimizer.""" + self.optimizer.zero_grad() + + def backward(self, loss_fn: Callable, *loss_args) -> torch.Tensor: + """Perform backward pass and return the loss. + + Args: + loss_fn (Callable): computes the loss. + *loss_args: are the arguments to be passed to ``loss_fn``. + + Returns: + torch.Tensor: computed loss. + """ + if self.mixed_precision: + # https://pytorch.org/docs/stable/notes/amp_examples.html + # Runs the forward pass with autocasting. + with autocast(device_type='cuda', dtype=torch.float16): + loss = loss_fn(*loss_args) + + # Scales loss. Calls backward() on scaled loss to create scaled + # gradients. + # Backward passes under autocast are not recommended. + # Backward ops run in the same dtype autocast chose for + # corresponding forward ops. + loss = self.grad_scaler.scale(loss) + else: + loss = loss_fn(*loss_args) + loss.backward() + return loss + + def optimizer_step(self): + """Perform optimizer step.""" + if self.mixed_precision: + # https://pytorch.org/docs/stable/notes/amp_examples.html#typical-mixed-precision-training + # scaler.step() first unscales the gradients of the optimizer's + # assigned params. + # If these gradients do not contain infs or NaNs, optimizer.step() + # is then called, + # otherwise, optimizer.step() is skipped. + self.grad_scaler.step(self.optimizer) + + # Updates the scale for next iteration. + self.grad_scaler.update() + else: + self.optimizer.step() + + def lr_scheduler_step(self): + """Perform lr scheduler step, if present.""" + if self.lr_scheduler: + self.lr_scheduler.step() + + def save_checkpoint(self): + """Save checkpoint to persistent storage.""" + raise NotImplementedError + + +class DSModelEngine(ModelEngine): + """Model engine for DeeSpeed distributed strategy.""" + + def forward(self, *args: Any, **kwds: Any) -> Any: + """Performs the forward operation.""" + if self.mixed_precision: + # https://pytorch.org/docs/stable/notes/amp_examples.html + # Runs the forward pass with autocasting. + with autocast(device_type='cuda', dtype=torch.float16): + return self.model(*args, **kwds) + else: + return self.model(*args, **kwds) + + def zero_grad(self): + """Set gradients to zero for the optimizer.""" + self.optimizer.zero_grad() + + def backward(self, loss_fn: Callable, *loss_args) -> torch.Tensor: + """Perform backward pass and return the loss. + + Args: + loss_fn (Callable): computes the loss. + *loss_args: are the arguments to be passed to ``loss_fn``. + + Returns: + torch.Tensor: computed loss. + """ + if self.mixed_precision: + # https://pytorch.org/docs/stable/notes/amp_examples.html + # Runs the forward pass with autocasting. + with autocast(device_type='cuda', dtype=torch.float16): + loss = loss_fn(*loss_args) + + # Scales loss. Calls backward() on scaled loss to create scaled + # gradients. + # Backward passes under autocast are not recommended. + # Backward ops run in the same dtype autocast chose for + # corresponding forward ops. + loss = self.grad_scaler.scale(loss) + else: + loss = loss_fn(*loss_args) + loss.backward() + return loss + + def optimizer_step(self): + """Perform optimizer step.""" + if self.mixed_precision: + # https://pytorch.org/docs/stable/notes/amp_examples.html#typical-mixed-precision-training + # scaler.step() first unscales the gradients of the optimizer's + # assigned params. + # If these gradients do not contain infs or NaNs, optimizer.step() + # is then called, + # otherwise, optimizer.step() is skipped. + self.grad_scaler.step(self.optimizer) + + # Updates the scale for next iteration. + self.grad_scaler.update() + else: + self.optimizer.step() + + def lr_scheduler_step(self): + """Perform lr scheduler step, if present.""" + if self.lr_scheduler: + self.lr_scheduler.step() + + def save_checkpoint(self): + """Save checkpoint to persistent storage.""" + raise NotImplementedError diff --git a/src/itwinai/torch/trainer.py b/src/itwinai/torch/trainer.py index 31794c49..f0ad1c03 100644 --- a/src/itwinai/torch/trainer.py +++ b/src/itwinai/torch/trainer.py @@ -26,6 +26,12 @@ from ..loggers import LogMixin, Logger, ConsoleLogger from ..utils import dynamically_import_class from ..cluster import ClusterEnvironment +# from .distributed import ( +# TorchDistributedStrategy, +# DDPDistributedStrategy, +# DSDistributedStrategy, +# HVDDistributedStrategy +# ) def preproc_dataloader(dataloader: DataLoader, gwsize, grank): diff --git a/src/itwinai/torch/types.py b/src/itwinai/torch/types.py index 6f6e5c9f..614462ad 100644 --- a/src/itwinai/torch/types.py +++ b/src/itwinai/torch/types.py @@ -42,6 +42,8 @@ class TorchDistributedStrategy(BaseEnum): DEFAULT = None NONE = None DDP = 'ddp' + HVD = 'horovod' + DS = 'deepspeed' class TorchLoss(BaseEnum): diff --git a/tests/components/test_components.py b/tests/components/test_components.py index 364b4917..3ec55453 100644 --- a/tests/components/test_components.py +++ b/tests/components/test_components.py @@ -105,7 +105,7 @@ def test_adapter(): assert result == (0, 0, 0, 0) adapter = Adapter( - policy=[f"{prefix}{i%2}" for i in range(4)] + policy=[f"{prefix}{i % 2}" for i in range(4)] ) result = adapter.execute(0, 1, 2, 3) assert result == (0, 1, 0, 1) diff --git a/tutorials/distributed-ml/tf-tutorial-0-basics/README.md b/tutorials/distributed-ml/tf-tutorial-0-basics/README.md new file mode 100644 index 00000000..c2c49595 --- /dev/null +++ b/tutorials/distributed-ml/tf-tutorial-0-basics/README.md @@ -0,0 +1,20 @@ +# Tutorial: distributed strategies for Tensorflow + +In this tutorial we show how to use Tensorflow `MultiWorkerMirroredStrategy`. +Note that the environment is tested on the HDFML system at JSC. +For other systems, the module versions might need change accordingly. +Other strategies will be updated here. + +First, from the root of this repository, build the environment containing +Tensorflow. You can *try* with: + +```bash +# Creates a Python venv called envAItf_hdfml +make tf-gpu-jsc +``` + +If you want to distribute the code in `train.py`, run from terminal: + +```bash +sbatch tfmirrored_slurm.sh +``` diff --git a/tutorials/distributed-ml/tf-tutorial-0-basics/tfmirrored_slurm.sh b/tutorials/distributed-ml/tf-tutorial-0-basics/tfmirrored_slurm.sh new file mode 100644 index 00000000..e1c8d54b --- /dev/null +++ b/tutorials/distributed-ml/tf-tutorial-0-basics/tfmirrored_slurm.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# general configuration of the job +#SBATCH --job-name=TFTest +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job.out +#SBATCH --error=job.err +#SBATCH --time=00:15:00 + +# configure node and process count on the CM +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --cpus-per-task=32 +#SBATCH --gpus-per-node=4 +#SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +set -x +unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY + +# set modules +ml --force purge +ml Stages/2024 GCC/12.3.0 OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py CMake cuDNN/8.9.5.29-CUDA-12 + +# set env +source /p/project/intertwin/rakesh/T6.5-AI-and-ML/dist_trainer/TF_runs/testAI_hdfml/bin/activate + +# sleep a sec +sleep 1 + +# job info +echo "DEBUG: TIME: $(date)" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +echo "DEBUG: SLURM_NODELIST: $SLURM_NODELIST" +echo + +# set comm +export CUDA_VISIBLE_DEVICES="0,1,2,3" +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK +fi + +COMMAND="train.py" + +EXEC="$COMMAND " + +srun python -u $EXEC diff --git a/tutorials/distributed-ml/tf-tutorial-0-basics/train.py b/tutorials/distributed-ml/tf-tutorial-0-basics/train.py new file mode 100644 index 00000000..ee29bca5 --- /dev/null +++ b/tutorials/distributed-ml/tf-tutorial-0-basics/train.py @@ -0,0 +1,109 @@ +""" +Show how to use TensorFlow MultiWorkerMirroredStrategy on itwinai. + +with SLURM: +>>> sbatch tfmirrored_slurm.sh + +""" +from typing import Any +import argparse +import tensorflow as tf +from tensorflow import keras +import os +from itwinai.tensorflow.distributed import get_strategy + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "--strategy", "-s", type=str, + choices=['mirrored'], + default='mirrored' + ) + parser.add_argument( + "--batch_size", "-bs", type=int, + default=64 + ) + parser.add_argument( + "--shuffle_dataloader", + action=argparse.BooleanOptionalAction + ) + + args = parser.parse_args() + return args + + +def tf_rnd_dataset(): + """Dummy TF dataset.""" + (x_train, y_train), (x_test, y_test) = \ + tf.keras.datasets.mnist.load_data( + path=os.getcwd()+'/.keras/datasets/mnist.npz') + + train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train)) + train_dataset = train_dataset.batch(args.batch_size) + + test_dataset = tf.data.Dataset.from_tensor_slices((x_test, y_test)) + test_dataset = test_dataset.batch(args.batch_size) + + return train_dataset, test_dataset + + +def trainer_entrypoint_fn( + foo: Any, args: argparse.Namespace, strategy +) -> int: + """Dummy training function, similar to custom code developed + by some use case. + """ + # dataset to be trained + train_dataset, test_dataset = tf_rnd_dataset(args) + + # distribute datasets among mirrored replicas + dist_train = strategy.experimental_distribute_dataset( + train_dataset + ) + dist_test = strategy.experimental_distribute_dataset( + test_dataset + ) + + # define and compile model within strategy.scope() + with strategy.scope(): + # Local model + model = tf.keras.models.Sequential([ + tf.keras.layers.Flatten(input_shape=(28, 28)), + tf.keras.layers.Dense(128, activation='relu'), + tf.keras.layers.Dense(10) + ]) + + model.compile(loss=keras.losses.SparseCategoricalCrossentropy + (from_logits=True), + optimizer=keras.optimizers.RMSprop(), + metrics=['accuracy'] + ) + + model.fit(dist_train, + epochs=5, + steps_per_epoch=2000) + + test_scores = model.evaluate(dist_test, verbose=0, steps=500) + + print('Test loss:', test_scores[0]) + print('Test accuracy:', test_scores[1]) + + return 123 + + +if __name__ == "__main__": + + args = parse_args() + + # Instantiate Strategy + if args.strategy == 'mirrored': + if (len(tf.config.list_physical_devices('GPU')) == 0): + raise RuntimeError('Resources unavailable') + strategy, num_replicas = get_strategy() + else: + raise NotImplementedError( + f"Strategy {args.strategy} is not recognized/implemented.") + + # Launch distributed training + trainer_entrypoint_fn("foobar", args, strategy) diff --git a/tutorials/distributed-ml/torch-scaling-test/README.md b/tutorials/distributed-ml/torch-scaling-test/README.md new file mode 100644 index 00000000..74e316c0 --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/README.md @@ -0,0 +1,113 @@ +# Scaling tests for PyTorch of ResNet152 on Imagenet + +## Introduction + +This tutorial contains six training configurations: three baselines plus the itwinai +trainer, which allows to switch from DDP, Horovod, and DeepSpeed in a simplified way. + +The training scripts are: + +- `ddp_trainer.py`: baseline of distributed training with vanilla torch DDP +- `deepspeed_trainer.py`: baseline of distributed training with vanilla Microsoft DeepSpeed +- `horovod_trainer.py`: baseline of distributed training with vanilla Horovod +- `itwinai_trainer.py`: provides the same functionalities as all the above, +using the unified itwinai's distributed training interface. + +Configuration files are stored into `config/` folder. `base.yaml` provides the +configuration common to all training experiments, whereas `ddp.yaml`, `deepspeed.yaml`, +and `horovod.yaml` provide framework-specific configuration. +Thanks to `itwinai.parser.ArgumentParser`, the CLI arguments can be parsed from a list of +configuration files, while also allowing for online override. +Example: + +```bash +# Rather than requiring a LONG list of inline configuration params... +python ddp_trainer.py --data-dir some/dir --log-int 10 --verbose --nworker 4 ... + +# ...itwinai's ArgumentParser allows to load them from a set of configuration files +# with inline override, if needed +python ddp_trainer.py -c config/base.yaml -c config/ddp.yaml --log-int 42 +``` + +## Run a single training + +Training runs are meant to be submitted via SLURM, from a unified job script file: +`slurm.sh`. +You can select the distributed training algorithm and provide the command to execute +setting SLURM environment variables using the `--export` option: + +```bash +# Launch a distributed training setup with Torch DDP +DIST_MODE="ddp" +RUN_NAME="ddp-bl-imagenent" +TRAINING_CMD="ddp_trainer.py -c config/base.yaml -c config/ddp.yaml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD" \ + --job-name="$RUN_NAME" slurm.sh +``` + +## Run all training configurations + +To run all training configurations you can use the `runall.sh` script, which provides +further insight how different training configurations can be launched using the same +SLURM job script. + +```bash +bash runall.sh +``` + +And check the newly created jobs in the SLURM queue: + +```bash +squeue -u YOUR_USERNAME +``` + +Each execution will generate a `.csv` file recording the time that each training epoch +took to complete. Below you can learn more on how to analyze these files to produce report. + +## Launch scaling test + +Similarly to `runall.sh`, there is another script which is meant to launch a scalability +analysis experiment. This will launch all the training configuration for different number +of node allocations. By default it will run the same distributed trainings on 1, 2, 4, and +8 nodes. Each independent execution will generate a separate `.csv` file which can be +analyzed later to produce a scalability report. + +Launch the scaling test: + +```bash +bash scaling-test.sh +``` + +And check the newly created jobs in the SLURM queue: + +```bash +squeue -u YOUR_USERNAME +``` + +## Analyze results + +Once all jobs have completed, you can automatically generate scalability report +using itwinai's CLI: + +```bash +# First, activate you Python virtual environment + +# For more info run +itwinai scalability-report --help + +# Generate a scalability report +itwinai scalability-report --pattern="^epoch.+\.csv$" \ + --plot-title "ResNet152 on Imagenet" --archive imagenet_results +``` + +The last command prints to terminal the average epoch time per training +configuration and per number of nodes, and it generated scaling test +analysis plot, which is saved as `.png` file. This command will also +create a `.tar.gz` archive of all the analyzed `.csv` files and +the generated plots, allowing you to easily organize different experiments +and reducing the risk of overwriting the logs generated during the scaling +test. + +Example of scalability plot generated by `itwinai scalability-report`: + +![report](img/report.png) diff --git a/tutorials/distributed-ml/torch-scaling-test/config/base.yaml b/tutorials/distributed-ml/torch-scaling-test/config/base.yaml new file mode 100644 index 00000000..3cbadd07 --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/config/base.yaml @@ -0,0 +1,16 @@ +# Data and logging +data_dir: /p/scratch/intertwin/datasets/imagenet/ILSVRC2012/train/ # tmp_data/ +log_int: 10 +verbose: True +nworker: 4 # num workers dataloader +prefetch: 2 + +# Model +batch_size: 64 # micro batch size +epochs: 3 +lr: 0.001 +momentum: 0.5 +shuff: False + +# Reproducibility +rnd_seed: 10 diff --git a/tutorials/distributed-ml/torch-scaling-test/config/ddp.yaml b/tutorials/distributed-ml/torch-scaling-test/config/ddp.yaml new file mode 100644 index 00000000..e872ffc9 --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/config/ddp.yaml @@ -0,0 +1 @@ +backend: nccl \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-scaling-test/config/deepspeed.yaml b/tutorials/distributed-ml/torch-scaling-test/config/deepspeed.yaml new file mode 100644 index 00000000..e872ffc9 --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/config/deepspeed.yaml @@ -0,0 +1 @@ +backend: nccl \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-scaling-test/config/horovod.yaml b/tutorials/distributed-ml/torch-scaling-test/config/horovod.yaml new file mode 100644 index 00000000..fce89755 --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/config/horovod.yaml @@ -0,0 +1,3 @@ +fp16_allreduce: False +use_adasum: False +gradient_predivide_factor: 1.0 \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-scaling-test/ddp_trainer.py b/tutorials/distributed-ml/torch-scaling-test/ddp_trainer.py new file mode 100755 index 00000000..54f64fef --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/ddp_trainer.py @@ -0,0 +1,269 @@ +""" +Scaling test of torch Distributed Data Parallel on Imagenet using Resnet. +""" +from typing import Optional +import argparse +import sys +import os +from timeit import default_timer as timer +import time + +import torch +import torch.distributed as dist +import torch.nn as nn +import torch.nn.functional as F +from torch.utils.data import DataLoader +from torch.utils.data.distributed import DistributedSampler +import torchvision + +from itwinai.parser import ArgumentParser as ItAIArgumentParser +from itwinai.loggers import EpochTimeTracker + +from utils import seed_worker, imagenet_dataset, set_seed + + +def parse_params(): + parser = ItAIArgumentParser(description='PyTorch Imagenet scaling test') + + # Data and logging + parser.add_argument('--data-dir', default='./', + help=('location of the training dataset in the ' + 'local filesystem')) + parser.add_argument('--log-int', type=int, default=10, + help='log interval per training. Disabled if < 0.') + parser.add_argument('--verbose', + action=argparse.BooleanOptionalAction, + help='Print parsed arguments') + parser.add_argument('--nworker', type=int, default=0, + help=('number of workers in DataLoader ' + '(default: 0 - only main)')) + parser.add_argument('--prefetch', type=int, default=2, + help='prefetch data in DataLoader (default: 2)') + + # Model + parser.add_argument('--batch-size', type=int, default=64, + help='input batch size for training (default: 64)') + parser.add_argument('--epochs', type=int, default=10, + help='number of epochs to train (default: 10)') + parser.add_argument('--lr', type=float, default=0.01, + help='learning rate (default: 0.01)') + parser.add_argument('--momentum', type=float, default=0.5, + help='momentum in SGD optimizer (default: 0.5)') + parser.add_argument('--shuff', action='store_true', default=False, + help='shuffle dataset (default: False)') + + # Reproducibility + parser.add_argument('--rnd-seed', type=Optional[int], default=None, + help='seed integer for reproducibility (default: 0)') + + # Distributed ML + parser.add_argument('--backend', type=str, default='nccl', + help='backend for parrallelisation (default: nccl)') + parser.add_argument('--no-cuda', action='store_true', default=False, + help='disables GPGPUs') + + args = parser.parse_args() + + if args.verbose: + args_list = [f"{key}: {val}" for key, val in args.items()] + print("PARSED ARGS:\n", '\n'.join(args_list)) + return args + + +def train(model, device, train_loader, optimizer, epoch, grank, gwsize, args): + model.train() + t_list = [] + loss_acc = 0 + if grank == 0: + print("\n") + for batch_idx, (data, target) in enumerate(train_loader): + # if grank == 0: + # print(f"BS == DATA: {data.shape}, TARGET: {target.shape}") + t = timer() + data, target = data.to(device), target.to(device) + optimizer.zero_grad() + output = model(data) + loss = F.nll_loss(output, target) + loss.backward() + optimizer.step() + if grank == 0 and args.log_int > 0 and batch_idx % args.log_int == 0: + print( + f'Train epoch: {epoch} [{batch_idx * len(data)}/' + f'{len(train_loader.dataset) / gwsize} ' + f'({100.0 * batch_idx / len(train_loader):.0f}%)]\t\tLoss: ' + f'{loss.item():.6f}') + t_list.append(timer() - t) + loss_acc += loss.item() + if grank == 0: + print('TIMER: train time', sum(t_list) / len(t_list), 's') + return loss_acc + + +def main(): + # Parse CLI args + args = parse_params() + + # Check resources availability + use_cuda = not args.no_cuda and torch.cuda.is_available() + is_distributed = False + if use_cuda and torch.cuda.device_count() > 0: + is_distributed = True + + # Limit # of CPU threads to be used per worker + # torch.set_num_threads(1) + + # Start the timer for profiling + st = timer() + + if is_distributed: + # Initializes the distributed backend which will + # take care of synchronizing the workers (nodes/GPUs) + dist.init_process_group(backend=args.backend) + + # Set random seed for reproducibility + torch_prng = set_seed(args.rnd_seed, use_cuda) + + if is_distributed: + # get job rank info - rank==0 master gpu + lwsize = torch.cuda.device_count() # local world size - per run + gwsize = dist.get_world_size() # global world size - per run + grank = dist.get_rank() # global rank - assign per run + lrank = dist.get_rank() % lwsize # local rank - assign per node + else: + # Use a single worker (either on GPU or CPU) + lwsize = 1 + gwsize = 1 + grank = 0 + lrank = 0 + + if grank == 0: + print('TIMER: initialise:', timer()-st, 's') + print('DEBUG: local ranks:', lwsize, '/ global ranks:', gwsize) + print('DEBUG: sys.version:', sys.version) + print('DEBUG: args.data_dir:', args.data_dir) + print('DEBUG: args.log_int:', args.log_int) + print('DEBUG: args.nworker:', args.nworker) + print('DEBUG: args.prefetch:', args.prefetch) + print('DEBUG: args.batch_size:', args.batch_size) + print('DEBUG: args.epochs:', args.epochs) + print('DEBUG: args.lr:', args.lr) + print('DEBUG: args.momentum:', args.momentum) + print('DEBUG: args.shuff:', args.shuff) + print('DEBUG: args.rnd_seed:', args.rnd_seed) + print('DEBUG: args.backend:', args.backend) + print('DEBUG: args.no_cuda:', args.no_cuda, '\n') + + # Encapsulate the model on the GPU assigned to the current process + device = torch.device('cuda' if use_cuda else 'cpu', lrank) + if use_cuda: + torch.cuda.set_device(lrank) + + # Dataset + train_dataset = imagenet_dataset(args.data_dir) + + if is_distributed: + # Distributed sampler restricts data loading to a subset of the dataset + # exclusive to the current process. + # `mun_replicas` and `rank` are automatically retrieved from + # the current distributed group. + train_sampler = DistributedSampler( + train_dataset, # num_replicas=gwsize, rank=grank, + shuffle=(args.shuff and args.rnd_seed is None) + ) + + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size, + sampler=train_sampler, num_workers=args.nworker, pin_memory=True, + persistent_workers=(args.nworker > 1), + prefetch_factor=args.prefetch, generator=torch_prng, + worker_init_fn=seed_worker + ) + else: + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size, generator=torch_prng, + worker_init_fn=seed_worker + ) + + # Create CNN model + model = torchvision.models.resnet152().to(device) + + # Distribute model to workers + if is_distributed: + model = nn.parallel.DistributedDataParallel( + model, + device_ids=[device], + output_device=device) + + # Optimizer + optimizer = torch.optim.SGD( + model.parameters(), lr=args.lr, momentum=args.momentum) + + # Start training loop + if grank == 0: + print('TIMER: broadcast:', timer()-st, 's') + print('\nDEBUG: start training') + print('--------------------------------------------------------') + nnod = os.environ.get('SLURM_NNODES', 'unk') + epoch_time_tracker = EpochTimeTracker( + series_name="ddp-bl", + csv_file=f"epochtime_ddp-bl_{nnod}N.csv" + ) + + et = timer() + start_epoch = 1 + for epoch in range(start_epoch, args.epochs + 1): + lt = timer() + if is_distributed: + # Inform the sampler that a new epoch started: shuffle + # may be needed + train_sampler.set_epoch(epoch) + + # Training + train(model, device, train_loader, + optimizer, epoch, grank, gwsize, args) + # Save first epoch timer + if epoch == start_epoch: + first_ep_t = timer()-lt + + # Final epoch + if epoch + 1 == args.epochs: + train_loader.last_epoch = True + + if grank == 0: + print('TIMER: epoch time:', timer() - lt, 's') + epoch_time_tracker.add_epoch_time(epoch-1, timer() - lt) + + if is_distributed: + dist.barrier() + + if grank == 0: + print('\n--------------------------------------------------------') + print('DEBUG: training results:\n') + print('TIMER: first epoch time:', first_ep_t, ' s') + print('TIMER: last epoch time:', timer() - lt, ' s') + print('TIMER: average epoch time:', (timer() - et)/args.epochs, ' s') + print('TIMER: total epoch time:', timer() - et, ' s') + if epoch > 1: + print('TIMER: total epoch-1 time:', + timer() - et - first_ep_t, ' s') + print('TIMER: average epoch-1 time:', + (timer() - et - first_ep_t) / (args.epochs - 1), ' s') + if use_cuda: + print('DEBUG: memory req:', + int(torch.cuda.memory_reserved(lrank) / 1024 / 1024), 'MB') + print('DEBUG: memory summary:\n\n', + torch.cuda.memory_summary(0)) + print(f'TIMER: final time: {timer() - st} s\n') + + time.sleep(1) + print(f" - TRAINING FINISHED") + + # Clean-up + if is_distributed: + dist.barrier() + dist.destroy_process_group() + + +if __name__ == "__main__": + main() + sys.exit() diff --git a/tutorials/distributed-ml/torch-scaling-test/deepspeed_trainer.py b/tutorials/distributed-ml/torch-scaling-test/deepspeed_trainer.py new file mode 100644 index 00000000..691712e8 --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/deepspeed_trainer.py @@ -0,0 +1,283 @@ +""" +Scaling test of Microsoft Deepspeed on Imagenet using Resnet. +""" +from typing import Optional +import argparse +import sys +import os +from timeit import default_timer as timer +import time +import deepspeed + +import torch +import torch.distributed as dist +import torch.nn.functional as F +from torch.utils.data import DataLoader +from torch.utils.data.distributed import DistributedSampler +import torchvision + +from itwinai.parser import ArgumentParser as ItAIArgumentParser +from itwinai.loggers import EpochTimeTracker + +from utils import seed_worker, set_seed, imagenet_dataset + + +def parse_params(): + parser = ItAIArgumentParser(description='PyTorch Imagenet scaling test') + + # Data and logging + parser.add_argument('--data-dir', default='./', + help=('location of the training dataset in the ' + 'local filesystem')) + parser.add_argument('--log-int', type=int, default=10, + help='log interval per training. Disabled if < 0.') + parser.add_argument('--verbose', + action=argparse.BooleanOptionalAction, + help='Print parsed arguments') + parser.add_argument('--nworker', type=int, default=0, + help=('number of workers in DataLoader ' + '(default: 0 - only main)')) + parser.add_argument('--prefetch', type=int, default=2, + help='prefetch data in DataLoader (default: 2)') + + # Model + parser.add_argument('--batch-size', type=int, default=64, metavar='N', + help='input batch size for training (default: 64)') + parser.add_argument('--epochs', type=int, default=10, metavar='N', + help='number of epochs to train (default: 10)') + parser.add_argument('--lr', type=float, default=0.01, metavar='LR', + help='learning rate (default: 0.01)') + parser.add_argument('--momentum', type=float, default=0.5, + help='momentum in SGD optimizer (default: 0.5)') + parser.add_argument('--shuff', action='store_true', default=False, + help='shuffle dataset (default: False)') + + # Reproducibility + parser.add_argument('--rnd-seed', type=Optional[int], default=None, + help='seed integer for reproducibility (default: 0)') + + # Distributed ML + parser.add_argument('--backend', type=str, default='nccl', metavar='N', + help='backend for parallelization (default: nccl)') + parser.add_argument('--no-cuda', action='store_true', default=False, + help='disables GPGPUs') + parser.add_argument('--local_rank', type=int, default=-1, + help='local rank passed from distributed launcher') + + # parse to deepspeed + parser = deepspeed.add_config_arguments(parser) + args = parser.parse_args() + if args.verbose: + args_list = [f"{key}: {val}" for key, val in args.items()] + print("PARSED ARGS:\n", '\n'.join(args_list)) + + return args + + +def train(args, model, train_loader, optimizer, epoch, grank, gwsize): + device = model.local_rank + t_list = [] + loss_acc = 0 + if grank == 0: + print("\n") + for batch_idx, (data, target) in enumerate(train_loader): + # if grank == 0: + # print(f"BS == DATA: {data.shape}, TARGET: {target.shape}") + t = timer() + data, target = data.to(device), target.to(device) + optimizer.zero_grad() + output = model(data) + loss = F.nll_loss(output, target) + loss.backward() + optimizer.step() + if args.log_int > 0 and batch_idx % args.log_int == 0 and grank == 0: + print( + f'Train epoch: {epoch} [{batch_idx * len(data)}/' + f'{len(train_loader.dataset) / gwsize} ' + f'({100.0 * batch_idx * len(data) / len(train_loader):.0f}%)]' + f'\t\tLoss: {loss.item():.6f}') + t_list.append(timer() - t) + loss_acc += loss.item() + if grank == 0: + print('TIMER: train time', sum(t_list) / len(t_list), 's') + return loss_acc + + +def main(): + # Parse CLI args + args = parse_params() + + # Check resources availability + use_cuda = not args.no_cuda and torch.cuda.is_available() + is_distributed = False + if use_cuda and torch.cuda.device_count() > 0: + is_distributed = True + + # Limit # of CPU threads to be used per worker + # torch.set_num_threads(1) + + # Start the timer for profiling + st = timer() + + # Initializes the distributed backend + if is_distributed: + deepspeed.init_distributed(dist_backend=args.backend) + + # Set random seed for reproducibility + torch_prng = set_seed(args.rnd_seed, use_cuda) + + if is_distributed: + # Get job rank info - rank==0 master gpu + gwsize = dist.get_world_size() # global world size - per run + lwsize = torch.cuda.device_count() # local world size - per node + grank = dist.get_rank() # global rank - assign per run + lrank = dist.get_rank() % lwsize # local rank - assign per node + else: + # Use a single worker (either on GPU or CPU) + lwsize = 1 + gwsize = 1 + grank = 0 + lrank = 0 + + if grank == 0: + print('TIMER: initialise:', timer()-st, 's') + print('DEBUG: local ranks:', lwsize, '/ global ranks:', gwsize) + print('DEBUG: sys.version:', sys.version) + print('DEBUG: args.data_dir:', args.data_dir) + print('DEBUG: args.log_int:', args.log_int) + print('DEBUG: args.nworker:', args.nworker) + print('DEBUG: args.prefetch:', args.prefetch) + print('DEBUG: args.batch_size:', args.batch_size) + print('DEBUG: args.epochs:', args.epochs) + print('DEBUG: args.lr:', args.lr) + print('DEBUG: args.momentum:', args.momentum) + print('DEBUG: args.shuff:', args.shuff) + print('DEBUG: args.rnd_seed:', args.rnd_seed) + print('DEBUG: args.backend:', args.backend) + print('DEBUG: args.local_rank:', args.local_rank) + print('DEBUG: args.no_cuda:', args.no_cuda, '\n') + + # Encapsulate the model on the GPU assigned to the current process + if use_cuda: + torch.cuda.set_device(lrank) + + # Read training dataset + train_dataset = imagenet_dataset(args.data_dir) + + if is_distributed: + # Distributed sampler restricts data loading to a subset of the dataset + # exclusive to the current process. + # `mun_replicas` and `rank` are automatically retrieved from + # the current distributed group. + train_sampler = DistributedSampler( + train_dataset, # num_replicas=gwsize, rank=grank, + shuffle=(args.shuff and args.rnd_seed is None) + ) + + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size, + sampler=train_sampler, num_workers=args.nworker, pin_memory=True, + persistent_workers=(args.nworker > 1), + prefetch_factor=args.prefetch, generator=torch_prng, + worker_init_fn=seed_worker + ) + else: + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size, generator=torch_prng, + worker_init_fn=seed_worker + ) + + # Create CNN model + model = torchvision.models.resnet152() + + # Initialize DeepSpeed and get: + # 1) Distributed model + # 2) DeepSpeed optimizer + # 3) Distributed data loader + deepspeed_config = { + "train_micro_batch_size_per_gpu": args.batch_size, # redundant + "optimizer": { + "type": "SGD", + "params": { + "lr": args.lr, + "momentum": args.momentum + } + }, + "fp16": { + "enabled": False + }, + "zero_optimization": False + } + distrib_model, optimizer, deepspeed_train_loader, _ = deepspeed.initialize( + args=args, model=model, model_parameters=model.parameters(), + training_data=train_dataset, config_params=deepspeed_config) + + # Start training loop + if grank == 0: + print('TIMER: broadcast:', timer()-st, 's') + print('\nDEBUG: start training') + print('--------------------------------------------------------') + nnod = os.environ.get('SLURM_NNODES', 'unk') + epoch_time_tracker = EpochTimeTracker( + series_name="deepspeed-bl", + csv_file=f"epochtime_deepspeed-bl_{nnod}N.csv" + ) + + et = timer() + start_epoch = 1 + for epoch in range(start_epoch, args.epochs + 1): + lt = timer() + if is_distributed: + # Inform the sampler that a new epoch started: shuffle + # may be needed + train_sampler.set_epoch(epoch) + + # Training + train(args, distrib_model, train_loader, + optimizer, epoch, grank, gwsize) + + # Save first epoch timer + if epoch == start_epoch: + first_ep_t = timer()-lt + + # Final epoch + if epoch + 1 == args.epochs: + train_loader.last_epoch = True + + if grank == 0: + print('TIMER: epoch time:', timer()-lt, 's') + epoch_time_tracker.add_epoch_time(epoch-1, timer()-lt) + + if torch.cuda.is_available(): + dist.barrier() + + if grank == 0: + print('\n--------------------------------------------------------') + print('DEBUG: results:\n') + print('TIMER: first epoch time:', first_ep_t, ' s') + print('TIMER: last epoch time:', timer()-lt, ' s') + print('TIMER: average epoch time:', (timer()-et)/args.epochs, ' s') + print('TIMER: total epoch time:', timer()-et, ' s') + if epoch > 1: + print('TIMER: total epoch-1 time:', + timer()-et-first_ep_t, ' s') + print('TIMER: average epoch-1 time:', + (timer()-et-first_ep_t)/(args.epochs-1), ' s') + if use_cuda: + print('DEBUG: memory req:', + int(torch.cuda.memory_reserved(lrank)/1024/1024), 'MB') + print('DEBUG: memory summary:\n\n', + torch.cuda.memory_summary(0)) + print(f'TIMER: final time: {timer()-st} s\n') + + time.sleep(1) + print(f" - TRAINING FINISHED") + + # Clean-up + if is_distributed: + deepspeed.sys.exit() + + +if __name__ == "__main__": + main() + sys.exit() diff --git a/tutorials/distributed-ml/torch-scaling-test/horovod_trainer.py b/tutorials/distributed-ml/torch-scaling-test/horovod_trainer.py new file mode 100755 index 00000000..501b545c --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/horovod_trainer.py @@ -0,0 +1,318 @@ +""" +Scaling test of Horovod on Imagenet using Resnet. +""" +from typing import Optional +import argparse +import os +import sys +from timeit import default_timer as timer +import time + +import torch +# import torch.multiprocessing as mp +import torch.nn.functional as F +import torch.optim as optim +from torch.utils.data import DataLoader +from torch.utils.data.distributed import DistributedSampler +import horovod.torch as hvd +import torchvision + +from itwinai.parser import ArgumentParser as ItAIArgumentParser +from itwinai.loggers import EpochTimeTracker + +from utils import imagenet_dataset, seed_worker, set_seed + + +def parse_params(): + parser = ItAIArgumentParser(description='PyTorch Imagenet Example') + + # Data and logging + parser.add_argument('--data-dir', default='./', + help=('location of the training dataset in the ' + 'local filesystem')) + parser.add_argument('--log-int', type=int, default=100, + help=('#batches to wait before logging training ' + 'status. Disabled if < 0.')) + parser.add_argument('--verbose', + action=argparse.BooleanOptionalAction, + help='Print parsed arguments') + parser.add_argument('--nworker', type=int, default=0, + help=('number of workers in DataLoader ' + '(default: 0 - only main)')) + parser.add_argument('--prefetch', type=int, default=2, + help='prefetch data in DataLoader (default: 2)') + + # Model + parser.add_argument('--batch-size', type=int, default=64, + help='input batch size for training (default: 64)') + parser.add_argument('--epochs', type=int, default=10, + help='number of epochs to train (default: 10)') + parser.add_argument('--lr', type=float, default=0.01, + help='learning rate (default: 0.01)') + parser.add_argument('--momentum', type=float, default=0.5, + help='SGD momentum (default: 0.5)') + parser.add_argument('--shuff', action='store_true', default=False, + help='shuffle dataset (default: False)') + + # Reproducibility + parser.add_argument('--rnd-seed', type=Optional[int], default=None, + help='seed integer for reproducibility (default: 0)') + + # Distributed ML + parser.add_argument('--no-cuda', action='store_true', default=False, + help='disables CUDA training') + parser.add_argument('--fp16-allreduce', action='store_true', default=False, + help='use fp16 compression during allreduce') + parser.add_argument('--use-adasum', action='store_true', default=False, + help='use adasum algorithm to do reduction') + parser.add_argument('--gradient-predivide-factor', type=float, default=1.0, + help=('apply gradient pre-divide factor in optimizer ' + '(default: 1.0)')) + + args = parser.parse_args() + if args.verbose: + args_list = [f"{key}: {val}" for key, val in args.items()] + print("PARSED ARGS:\n", '\n'.join(args_list)) + + return args + + +def train( + model, optimizer, train_sampler, train_loader, + args, use_cuda, epoch, grank +): + model.train() + t_list = [] + loss_acc = 0 + if grank == 0: + print("\n") + for batch_idx, (data, target) in enumerate(train_loader): + # if hvd.local_rank() == 0 and hvd.rank() == 0: + # print(f"BS == DATA: {data.shape}, TARGET: {target.shape}") + t = timer() + if use_cuda: + data, target = data.cuda(), target.cuda() + optimizer.zero_grad() + output = model(data) + loss = F.nll_loss(output, target) + loss.backward() + optimizer.step() + if grank == 0 and args.log_int > 0 and batch_idx % args.log_int == 0: + # Use train_sampler to determine the number of examples in + # this worker's partition + print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( + epoch, batch_idx * len(data), len(train_sampler), + 100. * batch_idx / len(train_loader), loss.item())) + t_list.append(timer() - t) + loss_acc += loss.item() + if grank == 0: + print('TIMER: train time', sum(t_list) / len(t_list), 's') + return loss_acc + + +def main(): + # Parse CLI args + args = parse_params() + + # Check resources availability + use_cuda = not args.no_cuda and torch.cuda.is_available() + is_distributed = False + if use_cuda and torch.cuda.device_count() > 0: + is_distributed = True + + # Start the time.time for profiling + st = timer() + + if is_distributed: + # Initializes the distributed backend which will + # take care of synchronizing the workers (nodes/GPUs) + hvd.init() + + # Set random seed for reproducibility + torch_prng = set_seed(args.rnd_seed, use_cuda) + + # is_main_worker = True + # if is_distributed and (hvd.rank() != 0 or hvd.local_rank() != 0): + # is_main_worker = False + + # Get local rank + if is_distributed: + lrank = hvd.local_rank() + grank = hvd.rank() + gwsize = hvd.size() + lwsize = torch.cuda.device_count() + else: + # Use a single worker (either on GPU or CPU) + lrank = 0 + grank = 0 + gwsize = 1 + lwsize = 1 + + if grank == 0: + print('TIMER: initialise:', timer()-st, 's') + print('DEBUG: local ranks:', lwsize, '/ global ranks:', gwsize) + print('DEBUG: sys.version:', sys.version) + print('DEBUG: args.data_dir:', args.data_dir) + print('DEBUG: args.log_int:', args.log_int) + print('DEBUG: args.nworker:', args.nworker) + print('DEBUG: args.prefetch:', args.prefetch) + print('DEBUG: args.batch_size:', args.batch_size) + print('DEBUG: args.epochs:', args.epochs) + print('DEBUG: args.lr:', args.lr) + print('DEBUG: args.momentum:', args.momentum) + print('DEBUG: args.shuff:', args.shuff) + print('DEBUG: args.rnd_seed:', args.rnd_seed) + print('DEBUG: args.no_cuda:', args.no_cuda) + print('DEBUG: args.fp16_allreduce:', args.fp16_allreduce) + print('DEBUG: args.use_adasum:', args.use_adasum) + print('DEBUG: args.gradient_predivide_factor:', + args.gradient_predivide_factor) + if use_cuda: + print('DEBUG: torch.cuda.is_available():', + torch.cuda.is_available()) + print('DEBUG: torch.cuda.current_device():', + torch.cuda.current_device()) + print('DEBUG: torch.cuda.device_count():', + torch.cuda.device_count()) + print('DEBUG: torch.cuda.get_device_properties(hvd.local_rank()):', + torch.cuda.get_device_properties(hvd.local_rank())) + + if use_cuda: + # Pin GPU to local rank + torch.cuda.set_device(lrank) + + # Limit # of CPU threads to be used per worker + # torch.set_num_threads(1) + + # Dataset + train_dataset = imagenet_dataset(args.data_dir) + + # kwargs = {} + # # When supported, use 'forkserver' to spawn dataloader workers instead... + # # issues with Infiniband implementations that are not fork-safe + # if (args.nworker > 0 and hasattr(mp, '_supports_context') + # and + # mp._supports_context and + # 'forkserver' in mp.get_all_start_methods()): + # kwargs['multiprocessing_context'] = 'forkserver' + + if is_distributed: + # Use DistributedSampler to partition the training data + # Since Horovod is not based on torch.distributed, + # `num_replicas` and `rank` cannot be retrieved from the + # current distributed group, thus they need to be provided explicitly. + train_sampler = DistributedSampler( + train_dataset, num_replicas=gwsize, rank=grank, + shuffle=(args.shuff and args.rnd_seed is None) + ) + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size, + sampler=train_sampler, num_workers=args.nworker, pin_memory=True, + persistent_workers=(args.nworker > 1), + prefetch_factor=args.prefetch, generator=torch_prng, + worker_init_fn=seed_worker + ) # , **kwargs) + else: + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size, generator=torch_prng, + worker_init_fn=seed_worker + ) # , **kwargs) + + # Create CNN model + model = torchvision.models.resnet152() + + if use_cuda: + model.cuda() + + if is_distributed: + # By default, Adasum doesn't need scaling up learning rate + lr_scaler = hvd.size() if not args.use_adasum else 1 + # If using GPU Adasum allreduce, scale learning rate by local_size + if args.use_adasum and hvd.nccl_built(): + lr_scaler = hvd.local_size() + # Scale learning rate by lr_scaler + args.lr *= lr_scaler + + optimizer = optim.SGD(model.parameters(), lr=args.lr, + momentum=args.momentum) + + if is_distributed: + # Broadcast parameters & optimizer state + hvd.broadcast_parameters(model.state_dict(), root_rank=0) + hvd.broadcast_optimizer_state(optimizer, root_rank=0) + + # Compression algorithm + compression = ( + hvd.Compression.fp16 if args.fp16_allreduce + else hvd.Compression.none + ) + + # Wrap optimizer with DistributedOptimizer + optimizer = hvd.DistributedOptimizer( + optimizer, + named_parameters=model.named_parameters(), + compression=compression, + op=hvd.Adasum if args.use_adasum else hvd.Average, + gradient_predivide_factor=args.gradient_predivide_factor) + + if grank == 0: + print('TIMER: broadcast:', timer()-st, 's') + print('\nDEBUG: start training') + print('--------------------------------------------------------') + nnod = os.environ.get('SLURM_NNODES', 'unk') + epoch_time_tracker = EpochTimeTracker( + series_name="horovod-bl", + csv_file=f"epochtime_horovod-bl_{nnod}N.csv" + ) + + et = timer() + start_epoch = 1 + for epoch in range(start_epoch, args.epochs + 1): + lt = timer() + if is_distributed: + # Inform the sampler that a new epoch started: shuffle + # may be needed + train_sampler.set_epoch(epoch) + + # Training + train(model, optimizer, train_sampler, + train_loader, args, use_cuda, epoch, grank) + + # Save first epoch timer + if epoch == start_epoch: + first_ep_t = timer()-lt + + # Final epoch + if epoch + 1 == args.epochs: + train_loader.last_epoch = True + + if grank == 0: + print('TIMER: epoch time:', timer()-lt, 's') + epoch_time_tracker.add_epoch_time(epoch-1, timer()-lt) + + if grank == 0: + print('\n--------------------------------------------------------') + print('DEBUG: training results:\n') + print('TIMER: first epoch time:', first_ep_t, ' s') + print('TIMER: last epoch time:', timer()-lt, 's') + print('TIMER: average epoch time:', (timer()-et)/args.epochs, ' s') + print('TIMER: total epoch time:', timer()-et, ' s') + if epoch > 1: + print('TIMER: total epoch-1 time:', + timer()-et-first_ep_t, ' s') + print('TIMER: average epoch-1 time:', + (timer()-et-first_ep_t)/(args.epochs-1), ' s') + if use_cuda: + print('DEBUG: memory req:', + int(torch.cuda.memory_reserved(lrank)/1024/1024), 'MB') + print('DEBUG: memory summary:\n\n', + torch.cuda.memory_summary(0)) + print(f'TIMER: final time: {timer()-st} s\n') + + time.sleep(1) + print(f" - TRAINING FINISHED") + + +if __name__ == "__main__": + main() + sys.exit() diff --git a/tutorials/distributed-ml/torch-scaling-test/img/report.png b/tutorials/distributed-ml/torch-scaling-test/img/report.png new file mode 100644 index 0000000000000000000000000000000000000000..53bb708ac3b94aaa63942b32742c84b1566a74b4 GIT binary patch literal 45730 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 BDf0jT literal 0 HcmV?d00001 diff --git a/tutorials/distributed-ml/torch-scaling-test/itwinai_trainer.py b/tutorials/distributed-ml/torch-scaling-test/itwinai_trainer.py new file mode 100644 index 00000000..a1eacc20 --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/itwinai_trainer.py @@ -0,0 +1,339 @@ +""" +Show how to use DDP, Horovod and DeepSpeed strategies interchangeably +with a large neural network trained on Imagenet dataset, showing how +to use checkpoints. +""" +from typing import Optional +import os +import argparse +import sys +from timeit import default_timer as timer +import time + +import torch +import torch.nn.functional as F +import torchvision +from torch.utils.data import DataLoader +from torch.utils.data.distributed import DistributedSampler + +import deepspeed +import horovod.torch as hvd + +from itwinai.torch.distributed import ( + TorchDistributedStrategy, + DDPDistributedStrategy, + HVDDistributedStrategy, + DSDistributedStrategy, +) +from itwinai.parser import ArgumentParser as ItAIArgumentParser +from itwinai.loggers import EpochTimeTracker + +from utils import seed_worker, imagenet_dataset, set_seed + + +def parse_params() -> argparse.Namespace: + """ + Parse CLI args, which can also be loaded from a configuration file + using the --config flag: + + >>> train.py --strategy ddp --config base-config.yaml --config foo.yaml + """ + parser = ItAIArgumentParser(description='PyTorch Imagenet Example') + + # Distributed ML strategy + parser.add_argument( + "--strategy", "-s", type=str, + choices=['ddp', 'horovod', 'deepspeed'], + default='ddp' + ) + + # Data and logging + parser.add_argument('--data-dir', default='./', + help=('location of the training dataset in the local ' + 'filesystem')) + parser.add_argument('--log-int', type=int, default=10, + help='log interval per training') + parser.add_argument('--verbose', + action=argparse.BooleanOptionalAction, + help='Print parsed arguments') + parser.add_argument('--nworker', type=int, default=0, + help=('number of workers in DataLoader (default: 0 -' + ' only main)')) + parser.add_argument('--prefetch', type=int, default=2, + help='prefetch data in DataLoader (default: 2)') + + # Model + parser.add_argument('--batch-size', type=int, default=64, + help='input batch size for training (default: 64)') + parser.add_argument('--epochs', type=int, default=10, + help='number of epochs to train (default: 10)') + parser.add_argument('--lr', type=float, default=0.01, + help='learning rate (default: 0.01)') + parser.add_argument('--momentum', type=float, default=0.5, + help='momentum in SGD optimizer (default: 0.5)') + parser.add_argument('--shuff', action='store_true', default=False, + help='shuffle dataset (default: False)') + + # Reproducibility + parser.add_argument('--rnd-seed', type=Optional[int], default=None, + help='seed integer for reproducibility (default: 0)') + + # Distributed ML + parser.add_argument('--backend', type=str, default='nccl', + help='backend for parrallelisation (default: nccl)') + parser.add_argument('--no-cuda', action='store_true', default=False, + help='disables GPGPUs') + parser.add_argument('--local_rank', type=int, default=-1, + help='local rank passed from distributed launcher') + + # Horovod + parser.add_argument('--fp16-allreduce', action='store_true', default=False, + help='use fp16 compression during allreduce') + parser.add_argument('--use-adasum', action='store_true', default=False, + help='use adasum algorithm to do reduction') + parser.add_argument('--gradient-predivide-factor', type=float, default=1.0, + help=('apply gradient pre-divide factor in optimizer ' + '(default: 1.0)')) + + # DeepSpeed + parser = deepspeed.add_config_arguments(parser) + args = parser.parse_args() + + if args.verbose: + args_list = [f"{key}: {val}" for key, val in args.items()] + print("PARSED ARGS:\n", '\n'.join(args_list)) + + return args + + +def train( + model, device, train_loader, optimizer, epoch, + strategy: TorchDistributedStrategy, args +): + """ + Training function, representing an epoch. + """ + model.train() + t_list = [] + loss_acc = 0 + gwsize = strategy.dist_gwsize() + if strategy.is_main_worker(): + print("\n") + for batch_idx, (data, target) in enumerate(train_loader): + t = timer() + data, target = data.to(device), target.to(device) + optimizer.zero_grad() + output = model(data) + loss = F.nll_loss(output, target) + loss.backward() + optimizer.step() + if (strategy.is_main_worker() and args.log_int > 0 + and batch_idx % args.log_int == 0): + print( + f'Train epoch: {epoch} ' + f'[{batch_idx * len(data)}/{len(train_loader.dataset)/gwsize} ' + f'({100.0 * batch_idx / len(train_loader):.0f}%)]\t\t' + f'Loss: {loss.item():.6f}') + t_list.append(timer() - t) + loss_acc += loss.item() + if strategy.is_main_worker(): + print('TIMER: train time', sum(t_list) / len(t_list), 's') + return loss_acc + + +def main(): + # Parse CLI args + args = parse_params() + + # Instantiate Strategy + if args.strategy == 'ddp': + if (not torch.cuda.is_available() + or not torch.cuda.device_count() > 1): + raise RuntimeError('Resources unavailable') + + strategy = DDPDistributedStrategy(backend=args.backend) + distribute_kwargs = {} + elif args.strategy == 'horovod': + strategy = HVDDistributedStrategy() + distribute_kwargs = dict( + compression=( + hvd.Compression.fp16 if args.fp16_allreduce + else hvd.Compression.none + ), + op=hvd.Adasum if args.use_adasum else hvd.Average, + gradient_predivide_factor=args.gradient_predivide_factor + ) + elif args.strategy == 'deepspeed': + strategy = DSDistributedStrategy(backend=args.backend) + distribute_kwargs = dict( + config_params=dict(train_micro_batch_size_per_gpu=args.batch_size) + ) + else: + raise NotImplementedError( + f"Strategy {args.strategy} is not recognized/implemented.") + strategy.init() + + # Check resources availability + use_cuda = not args.no_cuda and torch.cuda.is_available() + is_distributed = False + if use_cuda and torch.cuda.device_count() > 0: + is_distributed = True + + # Limit # of CPU threads to be used per worker + # torch.set_num_threads(1) + + # start the timer for profiling + st = timer() + + # Set random seed for reproducibility + torch_prng = set_seed(args.rnd_seed, use_cuda) + + # get job rank info - rank==0 master gpu + if is_distributed: + # local world size - per node + lwsize = strategy.dist_lwsize() # local world size - per run + gwsize = strategy.dist_gwsize() # global world size - per run + grank = strategy.dist_grank() # global rank - assign per run + lrank = strategy.dist_lrank() # local rank - assign per node + else: + # Use a single worker (either on GPU or CPU) + lwsize = 1 + gwsize = 1 + grank = 0 + lrank = 0 + + if strategy.is_main_worker(): + print('TIMER: initialise:', timer()-st, 's') + print('DEBUG: local ranks:', lwsize, '/ global ranks:', gwsize) + print('DEBUG: sys.version:', sys.version) + print('DEBUG: args.data_dir:', args.data_dir) + print('DEBUG: args.log_int:', args.log_int) + print('DEBUG: args.nworker:', args.nworker) + print('DEBUG: args.prefetch:', args.prefetch) + print('DEBUG: args.batch_size:', args.batch_size) + print('DEBUG: args.epochs:', args.epochs) + print('DEBUG: args.lr:', args.lr) + print('DEBUG: args.momentum:', args.momentum) + print('DEBUG: args.shuff:', args.shuff) + print('DEBUG: args.rnd_seed:', args.rnd_seed) + print('DEBUG: args.backend:', args.backend) + print('DEBUG: args.no_cuda:', args.no_cuda, '\n') + + # Encapsulate the model on the GPU assigned to the current process + device = torch.device( + strategy.dist_device() if use_cuda and torch.cuda.is_available() + else 'cpu') + if use_cuda: + torch.cuda.set_device(lrank) + + # Dataset + train_dataset = imagenet_dataset(args.data_dir) + + if is_distributed: + # Distributed sampler restricts data loading to a subset of the dataset + # exclusive to the current process. + train_sampler = DistributedSampler( + train_dataset, num_replicas=gwsize, rank=grank, + shuffle=(args.shuff and args.rnd_seed is None) + ) + + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size, + sampler=train_sampler, num_workers=args.nworker, pin_memory=True, + persistent_workers=(args.nworker > 1), + prefetch_factor=args.prefetch, generator=torch_prng, + worker_init_fn=seed_worker + ) + else: + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size, generator=torch_prng, + worker_init_fn=seed_worker + ) + + # Create CNN model: resnet 50, resnet101, resnet152 + model = torchvision.models.resnet152() + + # Optimizer + optimizer = torch.optim.SGD( + model.parameters(), lr=args.lr, momentum=args.momentum) + + if is_distributed: + distrib_model, optimizer, _ = strategy.distributed( + model, optimizer, lr_scheduler=None, **distribute_kwargs + ) + + # Start training loop + if strategy.is_main_worker(): + print('TIMER: broadcast:', timer()-st, 's') + print('\nDEBUG: start training') + print('--------------------------------------------------------') + nnod = os.environ.get('SLURM_NNODES', 'unk') + s_name = f"{args.strategy}-it" + epoch_time_tracker = EpochTimeTracker( + series_name=s_name, + csv_file=f"epochtime_{s_name}_{nnod}N.csv" + ) + + et = timer() + start_epoch = 1 + for epoch in range(start_epoch, args.epochs + 1): + lt = timer() + if is_distributed: + # Inform the sampler that a new epoch started: shuffle + # may be needed + train_sampler.set_epoch(epoch) + + # Training + train( + model=distrib_model, + device=device, + train_loader=train_loader, + optimizer=optimizer, + epoch=epoch, + strategy=strategy, + args=args + ) + + # Save first epoch timer + if epoch == start_epoch: + first_ep_t = timer()-lt + + # Final epoch + if epoch + 1 == args.epochs: + train_loader.last_epoch = True + + if strategy.is_main_worker(): + print('TIMER: epoch time:', timer()-lt, 's') + epoch_time_tracker.add_epoch_time(epoch-1, timer()-lt) + + if strategy.is_main_worker(): + print('\n--------------------------------------------------------') + print('DEBUG: training results:\n') + print('TIMER: first epoch time:', first_ep_t, ' s') + print('TIMER: last epoch time:', timer()-lt, ' s') + print('TIMER: average epoch time:', (timer()-et)/args.epochs, ' s') + print('TIMER: total epoch time:', timer()-et, ' s') + if epoch > 1: + print('TIMER: total epoch-1 time:', + timer()-et-first_ep_t, ' s') + print('TIMER: average epoch-1 time:', + (timer()-et-first_ep_t)/(args.epochs-1), ' s') + if use_cuda: + print('DEBUG: memory req:', + int(torch.cuda.memory_reserved(lrank)/1024/1024), 'MB') + print('DEBUG: memory summary:\n\n', + torch.cuda.memory_summary(0)) + + print(f'TIMER: final time: {timer()-st} s\n') + + time.sleep(1) + print(f" - TRAINING FINISHED") + + # Clean-up + if is_distributed: + strategy.clean_up() + + +if __name__ == "__main__": + main() + sys.exit() diff --git a/tutorials/distributed-ml/torch-scaling-test/runall.sh b/tutorials/distributed-ml/torch-scaling-test/runall.sh new file mode 100644 index 00000000..4f9efdcf --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/runall.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Run all versions of distributed ML version +# $1 (Optional[int]): number of nodes. Default: 2 +# $2 (Optional[str]): timeout. Default: "00:30:00" + +if [ -z "$1" ] ; then + N=2 +else + N=$1 +fi +if [ -z "$2" ] ; then + T="00:30:00" +else + T=$2 +fi + +# Common options +CMD="--nodes=$N --time=$T --account=atmo-rep --partition=booster slurm.sh" +PYTHON_VENV="../../../envAI_juwels" + +echo "Distributing training over $N nodes. Timeout set to: $T" + +rm -rf logs_slurm +mkdir logs_slurm +rm *.out *.err *.csv #*checkpoint.pth.tar + +# DDP baseline +DIST_MODE="ddp" +RUN_NAME="ddp-bl-imagenent" +TRAINING_CMD="ddp_trainer.py -c config/base.yaml -c config/ddp.yaml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD + +# DeepSpeed baseline +DIST_MODE="deepspeed" +RUN_NAME="deepspeed-bl-imagenent" +TRAINING_CMD="deepspeed_trainer.py -c config/base.yaml -c config/deepspeed.yaml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD + +# Horovod baseline +DIST_MODE="horovod" +RUN_NAME="horovod-bl-imagenent" +TRAINING_CMD="horovod_trainer.py -c config/base.yaml -c config/horovod.yaml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD + +# DDP itwinai +DIST_MODE="ddp" +RUN_NAME="ddp-itwinai-imagenent" +TRAINING_CMD="itwinai_trainer.py -c config/base.yaml -c config/ddp.yaml -s ddp" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD + +# DeepSpeed itwinai +DIST_MODE="deepspeed" +RUN_NAME="deepspeed-itwinai-imagenent" +TRAINING_CMD="itwinai_trainer.py -c config/base.yaml -c config/deepspeed.yaml -s deepspeed" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD + +# Horovod itwinai +DIST_MODE="horovod" +RUN_NAME="horovod-itwinai-imagenent" +TRAINING_CMD="itwinai_trainer.py -c config/base.yaml -c config/horovod.yaml -s horovod" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-scaling-test/scaling-test.sh b/tutorials/distributed-ml/torch-scaling-test/scaling-test.sh new file mode 100644 index 00000000..29a32705 --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/scaling-test.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +rm *checkpoint.pth.tar *.out *.err *.csv + +timeout="03:30:00" +for N in 1 2 4 8 16 32 64 128 +do + bash runall.sh $N $timeout + echo +done \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-scaling-test/slurm.sh b/tutorials/distributed-ml/torch-scaling-test/slurm.sh new file mode 100644 index 00000000..93dd4349 --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/slurm.sh @@ -0,0 +1,123 @@ +#!/bin/bash + +# SLURM jobscript for JSC systems + +# Job configuration +#SBATCH --job-name=distributed_training +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job.out +#SBATCH --error=job.err +#SBATCH --time=00:30:00 + +# Resources allocation +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --gpus-per-node=4 +#SBATCH --cpus-per-gpu=8 +#SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# Load environment modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# Job info +echo "DEBUG: TIME: $(date)" +sysN="$(uname -n | cut -f2- -d.)" +sysN="${sysN%%[0-9]*}" +echo "Running on system: $sysN" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$DEBUG" = true ] ; then + echo "DEBUG: NCCL_DEBUG=INFO" + export NCCL_DEBUG=INFO +fi +echo + +# Setup env for distributed ML +export CUDA_VISIBLE_DEVICES="0,1,2,3" +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_GPU" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_GPU +fi + +# Env vairables check +if [ -z "$DIST_MODE" ]; then + >&2 echo "ERROR: env variable DIST_MODE is not set. Allowed values are 'horovod', 'ddp' or 'deepspeed'" + exit 1 +fi +if [ -z "$RUN_NAME" ]; then + >&2 echo "WARNING: env variable RUN_NAME is not set. It's a way to identify some specific run of an experiment." + RUN_NAME=$DIST_MODE +fi +if [ -z "$TRAINING_CMD" ]; then + >&2 echo "ERROR: env variable TRAINING_CMD is not set. It's the python command to execute." + exit 1 +fi +if [ -z "$PYTHON_VENV" ]; then + >&2 echo "WARNING: env variable PYTHON_VENV is not set. It's the path to a python virtual environment." +else + # Activate Python virtual env + source $PYTHON_VENV/bin/activate +fi + +# Launch training +if [ "$DIST_MODE" == "ddp" ] ; then + echo "DDP training: $TRAINING_CMD" + srun --cpu-bind=none --ntasks-per-node=1 \ + --job-name="$RUN_NAME-n$SLURM_NNODES" \ + --output="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.out" \ + --error="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.err" \ + bash -c "torchrun \ + --log_dir='logs_torchrun' \ + --nnodes=$SLURM_NNODES \ + --nproc_per_node=$SLURM_GPUS_PER_NODE \ + --rdzv_id=$SLURM_JOB_ID \ + --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ + --rdzv_backend=c10d \ + --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ + $TRAINING_CMD" +elif [ "$DIST_MODE" == "deepspeed" ] ; then + echo "DEEPSPEED training: $TRAINING_CMD" + MASTER_ADDR=$(scontrol show hostnames "\$SLURM_JOB_NODELIST" | head -n 1)i + export MASTER_ADDR + export MASTER_PORT=29500 + + srun --cpu-bind=none --ntasks-per-node=$SLURM_GPUS_PER_NODE --cpus-per-task=$SLURM_CPUS_PER_GPU \ + --job-name="$RUN_NAME-n$SLURM_NNODES" \ + --output="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.out" \ + --error="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.err" \ + python -u $TRAINING_CMD --deepspeed + + # # Run with deepspeed launcher: set --ntasks-per-node=1 + # # https://www.deepspeed.ai/getting-started/#multi-node-environment-variables + # export NCCL_IB_DISABLE=1 + # export NCCL_SOCKET_IFNAME=eth0 + # nodelist=$(scontrol show hostname $SLURM_NODELIST) + # echo "$nodelist" | sed -e 's/$/ slots=4/' > .hostfile + # # Requires passwordless SSH access among compute node + # srun --cpu-bind=none deepspeed --hostfile=.hostfile $TRAINING_CMD --deepspeed + # rm .hostfile +elif [ "$DIST_MODE" == "horovod" ] ; then + echo "HOROVOD training: $TRAINING_CMD" + srun --cpu-bind=none --ntasks-per-node=$SLURM_GPUS_PER_NODE --cpus-per-task=$SLURM_CPUS_PER_GPU \ + --job-name="$RUN_NAME-imagenet-n$SLURM_NNODES" \ + --output="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.out" \ + --error="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.err" \ + python -u $TRAINING_CMD +else + >&2 echo "ERROR: unrecognized \$DIST_MODE env variable" + exit 1 +fi + diff --git a/tutorials/distributed-ml/torch-scaling-test/utils.py b/tutorials/distributed-ml/torch-scaling-test/utils.py new file mode 100644 index 00000000..cbd6aace --- /dev/null +++ b/tutorials/distributed-ml/torch-scaling-test/utils.py @@ -0,0 +1,55 @@ +from typing import Optional +import numpy as np +import random + +import torch +from torchvision import datasets, transforms + + +def seed_worker(worker_id): + worker_seed = torch.initial_seed() % 2**32 + np.random.seed(worker_seed) + random.seed(worker_seed) + + +def set_seed(rnd_seed: Optional[int], use_cuda: bool) -> torch.Generator: + """Set torch random seed and return a PRNG object. + + Args: + rnd_seed (Optional[int]): random seed. If None, the seed is not set. + use_cuda (bool): whether GPU is available. + + Returns: + torch.Generator: PRNG object. + """ + g = torch.Generator() + if rnd_seed is not None: + # Deterministic execution + np.random.seed(rnd_seed) + random.seed(rnd_seed) + torch.manual_seed(rnd_seed) + g.manual_seed(rnd_seed) + if use_cuda: + torch.cuda.manual_seed(rnd_seed) + torch.cuda.manual_seed_all(rnd_seed) + return g + + +def imagenet_dataset(data_root: str): + """Create a torch dataset object for Imagenet.""" + transform = transforms.Compose([ + transforms.Resize(256), + transforms.RandomHorizontalFlip(), + transforms.RandomVerticalFlip(), + transforms.RandomRotation(degrees=45), + transforms.ColorJitter( + brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5), + transforms.CenterCrop(224), + transforms.ToTensor(), + transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]) + imagenet = datasets.ImageFolder( + root=data_root, + transform=transform + ) + return imagenet diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/README.md b/tutorials/distributed-ml/torch-tutorial-0-basics/README.md new file mode 100644 index 00000000..5ddcd635 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-0-basics/README.md @@ -0,0 +1,45 @@ +# Tutorial: distributed strategies for PyTorch + +In this tutorial we show how to use torch `DistributedDataParallel` (DDP), Horovod and +DeepSpeed from the same client code. +Note that the environment is tested on the HDFML system at JSC. For other systems, +the module versions might need change accordingly. + +## Setup + +First, from the root of this repository, build the environment containing +pytorch, horovod and deepspeed. You can *try* with: + +```bash +# Creates a Python venv called envAI_hdfml +make torch-gpu-jsc +``` + +## Distributed training + +Each distributed strategy has its own SLURM job script, which +should be used to run it: + +If you want to distribute the code in `train.py` with **torch DDP**, run from terminal: + +```bash +sbatch ddp_slurm.sh +``` + +If you want to distribute the code in `train.py` with **DeepSpeed**, run from terminal: + +```bash +sbatch deepspeed_slurm.sh +``` + +If you want to distribute the code in `train.py` with **Horovod**, run from terminal: + +```bash +sbatch hvd_slurm.sh +``` + +You can run all of them with: + +```bash +bash runall.sh +``` diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/ddp_slurm.sh b/tutorials/distributed-ml/torch-tutorial-0-basics/ddp_slurm.sh new file mode 100644 index 00000000..1b53f04c --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-0-basics/ddp_slurm.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# general configuration of the job +#SBATCH --job-name=Torch_DDP_tutorial-0 +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job-ddp.out +#SBATCH --error=job-ddp.err +#SBATCH --time=00:15:00 + +# configure node and process count on the CM +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --cpus-per-task=32 +#SBATCH --gpus-per-node=4 +# SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# set modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# set env +source ../../../envAI_hdfml/bin/activate + +# job info +debug=false +echo "DEBUG: TIME: $(date)" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$debug" = true ] ; then + export NCCL_DEBUG=INFO +fi +echo + +# set comm +export CUDA_VISIBLE_DEVICES="0,1,2,3" +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK +fi + +# launch training +TRAINING_CMD="train.py -s ddp" + +srun --cpu-bind=none bash -c "torchrun \ + --log_dir='logs' \ + --nnodes=$SLURM_NNODES \ + --nproc_per_node=$SLURM_GPUS_PER_NODE \ + --rdzv_id=$SLURM_JOB_ID \ + --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ + --rdzv_backend=c10d \ + --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ + $TRAINING_CMD" + diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/deepspeed_slurm.sh b/tutorials/distributed-ml/torch-tutorial-0-basics/deepspeed_slurm.sh new file mode 100644 index 00000000..b12009de --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-0-basics/deepspeed_slurm.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +# general configuration of the job +#SBATCH --job-name=Torch_DeepSpeed_tutorial-0 +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job-ds.out +#SBATCH --error=job-ds.err +#SBATCH --time=00:15:00 + +# configure node and process count on the CM +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --ntasks-per-node=4 +#SBATCH --cpus-per-task=4 +#SBATCH --gpus-per-node=4 +# SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# set modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# set env +source ../../../envAI_hdfml/bin/activate + +# job info +debug=false +echo "DEBUG: TIME: $(date)" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$debug" = true ] ; then + export NCCL_DEBUG=INFO +fi +echo + +# set env vars +export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK +fi +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +# launch training +MASTER_ADDR=$(scontrol show hostnames "\$SLURM_JOB_NODELIST" | head -n 1)i +export MASTER_ADDR +export MASTER_PORT=29500 + +TRAINING_CMD="train.py -s deepspeed" + +# Run without launcher: set --ntasks-per-node=NUM_GPUS +srun --cpu-bind=none python -u $TRAINING_CMD #--deepspeed + +# srun pwd + +# # Run with deepspeed launcher: set --ntasks-per-node=1 +# # https://www.deepspeed.ai/getting-started/#multi-node-environment-variables +# export NCCL_IB_DISABLE=1 +# export NCCL_SOCKET_IFNAME=eth0 +# nodelist=$(scontrol show hostname $SLURM_NODELIST) +# echo "$nodelist" | sed -e 's/$/ slots=4/' > .hostfile +# # Requires passwordless SSH access among compute node +# srun --cpu-bind=none deepspeed --hostfile=.hostfile $TRAINING_CMD --deepspeed +# rm .hostfile \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/hvd_slurm.sh b/tutorials/distributed-ml/torch-tutorial-0-basics/hvd_slurm.sh new file mode 100644 index 00000000..a2a06e6c --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-0-basics/hvd_slurm.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# general configuration of the job +#SBATCH --job-name=Torch_HVD_tutorial-0 +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job-hvd.out +#SBATCH --error=job-hvd.err +#SBATCH --time=00:15:00 + +# configure node and process count on the CM +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --ntasks-per-node=4 +#SBATCH --cpus-per-task=8 +#SBATCH --gpus-per-node=4 +# SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# set modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# set env +source ../../../envAI_hdfml/bin/activate + +# job info +debug=false +echo "DEBUG: TIME: $(date)" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$debug" = true ] ; then + export NCCL_DEBUG=INFO +fi +echo + +# set vars +# export NCCL_DEBUG=INFO +export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK +fi +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +# launch training +TRAINING_CMD="train.py -s horovod" + +srun --cpu-bind=none python -u $TRAINING_CMD + diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/runall.sh b/tutorials/distributed-ml/torch-tutorial-0-basics/runall.sh new file mode 100644 index 00000000..17c0f190 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-0-basics/runall.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run all versions of distributed ML +rm *.out *.err +echo "Torch DDP training: $(sbatch ddp_slurm.sh)" +echo "DeepSpeed training: $(sbatch deepspeed_slurm.sh)" +echo "Horovod training: $(sbatch hvd_slurm.sh)" \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/train.py b/tutorials/distributed-ml/torch-tutorial-0-basics/train.py new file mode 100644 index 00000000..614b56e4 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-0-basics/train.py @@ -0,0 +1,143 @@ +""" +Show how to use DDP, Horovod and DeepSpeed strategies interchangeably +with an extremely simple neural network. +""" +from typing import Any +import os +import argparse + +import torch +from torch import nn +from torch.utils.data import DataLoader, Dataset, DistributedSampler + +from itwinai.torch.distributed import ( + TorchDistributedStrategy, + DDPDistributedStrategy, + HVDDistributedStrategy, + DSDistributedStrategy, +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "--strategy", "-s", type=str, + choices=['ddp', 'horovod', 'deepspeed'], + default='ddp' + ) + parser.add_argument( + "--shuffle_dataloader", + action=argparse.BooleanOptionalAction + ) + + # DeepSpeed: needs to be removed + import deepspeed + parser.add_argument('--local_rank', type=int, default=-1, + help='local rank passed from distributed launcher') + parser = deepspeed.add_config_arguments(parser) + args = parser.parse_args() + return args + + +class UniformRndDataset(Dataset): + """Dummy torch dataset.""" + + def __init__(self, x_size: int, y_size: int, len: int = 100): + super().__init__() + self.x_size = x_size + self.y_size = y_size + self.len = len + + def __len__(self): + return self.len + + def __getitem__(self, index): + return torch.rand(self.x_size), torch.rand(self.y_size) + + +def trainer_entrypoint_fn( + foo: Any, args: argparse.Namespace, strategy: TorchDistributedStrategy +) -> int: + """Dummy training function. This emulates custom code developed + by some use case. + """ + strategy.init() + print(f"{foo}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " + f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") + + # Local model + model = nn.Linear(3, 4) + optim = torch.optim.Adam(model.parameters(), lr=1e-3) + loss_fn = nn.MSELoss() + # Distributed model + deepspeed_config = dict(train_batch_size=32) + # 'config_params' key is ignored if strategy != DSDistributedStrategy + model, optim, lr_sched = strategy.distributed( + model, optim, lr_scheduler=None, config_params=deepspeed_config + ) + + # Data + train_set = UniformRndDataset(x_size=3, y_size=4) + # Distributed dataloader + train_loader = DataLoader( + train_set, batch_size=10, num_workers=1, + sampler=DistributedSampler( + train_set, + num_replicas=strategy.dist_gwsize(), + rank=strategy.dist_grank(), + shuffle=args.shuffle_dataloader + ) + ) + + # Device allocated for this worker + device = strategy.dist_device() + + for epoch in range(2): + for (x, y) in train_loader: + # print(f"tensor to cuda:{device}") + x = x.to(device) + y = y.to(device) + + optim.zero_grad() + + y_pred = model(x) + + loss = loss_fn(y_pred, y) + loss.backward() + + optim.step() + + if strategy.is_main_worker(): + print(f"Loss [epoch={epoch}]: {loss.item()}") + print(f"NNLoss [epoch={epoch}]: {loss.item()}") + + # Update scheduler + if lr_sched: + lr_sched.step() + + print(f" - TRAINING FINISHED") + strategy.clean_up() + return 123 + + +if __name__ == "__main__": + + args = parse_args() + + # Instantiate Strategy + if args.strategy == 'ddp': + if (not torch.cuda.is_available() + or not torch.cuda.device_count() > 1): + raise RuntimeError('Resources unavailable') + + strategy = DDPDistributedStrategy(backend='nccl') + elif args.strategy == 'horovod': + strategy = HVDDistributedStrategy() + elif args.strategy == 'deepspeed': + strategy = DSDistributedStrategy(backend='nccl') + else: + raise NotImplementedError( + f"Strategy {args.strategy} is not recognized/implemented.") + + # Launch distributed training + trainer_entrypoint_fn("foobar", args, strategy) diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/README.md b/tutorials/distributed-ml/torch-tutorial-1-mnist/README.md new file mode 100644 index 00000000..6f22d3ef --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/README.md @@ -0,0 +1,55 @@ +# Tutorial: distributed strategies for PyTorch model trained on MNIST dataset + +In this tutorial we show how to use torch `DistributedDataParallel` (DDP), Horovod and +DeepSpeed from the same client code. +Note that the environment is tested on the HDFML system at JSC. For other systems, +the module versions might need change accordingly. + +## Setup + +First, from the root of this repository, build the environment containing +pytorch, horovod and deepspeed. You can *try* with: + +```bash +# Creates a Python venv called envAI_hdfml +make torch-gpu-jsc +``` + +Before launching training, since on JSC's compute nodes there is not internet connection, +you need to download the dataset before while on the login lode: + +```bash +source ../../../envAI_hdfml/bin/activate +python train.py --download-only +``` + +This command creates a local folder called "MNIST" with the dataset. + +## Distributed training + +Each distributed strategy has its own SLURM job script, which +should be used to run it: + +If you want to distribute the code in `train.py` with **torch DDP**, run from terminal: + +```bash +sbatch ddp_slurm.sh +``` + +If you want to distribute the code in `train.py` with **DeepSpeed**, run from terminal: + +```bash +sbatch deepspeed_slurm.sh +``` + +If you want to distribute the code in `train.py` with **Horovod**, run from terminal: + +```bash +sbatch hvd_slurm.sh +``` + +You can run all of them with: + +```bash +bash runall.sh +``` diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/config.yaml b/tutorials/distributed-ml/torch-tutorial-1-mnist/config.yaml new file mode 100644 index 00000000..cb221dec --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/config.yaml @@ -0,0 +1,26 @@ +# I/O +data_dir: ./ +restart_int: 10 +download_only: False +verbose: True + +# Model +batch_size: 64 +epochs: 2 +lr: 0.001 +concM: 100 +momentum: 0.5 +shuff: False + +# Debugging +testrun: False +nseed: 10 +log_int: 10 + +# Distributed ML +backend: nccl +nworker: 4 # num workers dataloader +prefetch: 2 +no_cuda: False + + diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/ddp_slurm.sh b/tutorials/distributed-ml/torch-tutorial-1-mnist/ddp_slurm.sh new file mode 100644 index 00000000..3d5d4bb3 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/ddp_slurm.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# general configuration of the job +#SBATCH --job-name=Torch_DDP_tutorial-1 +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job-ddp.out +#SBATCH --error=job-ddp.err +#SBATCH --time=00:30:00 + +# configure node and process count on the CM +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --cpus-per-task=32 +#SBATCH --gpus-per-node=4 +# SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# set modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# set env +source ../../../envAI_hdfml/bin/activate + +# job info +debug=false +echo "DEBUG: TIME: $(date)" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$debug" = true ] ; then + export NCCL_DEBUG=INFO +fi +echo + +# set comm +export CUDA_VISIBLE_DEVICES="0,1,2,3" +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK +fi + +# launch training +TRAINING_CMD="train.py -s ddp -c config.yaml" + +srun --cpu-bind=none bash -c "torchrun \ + --log_dir='logs' \ + --nnodes=$SLURM_NNODES \ + --nproc_per_node=$SLURM_GPUS_PER_NODE \ + --rdzv_id=$SLURM_JOB_ID \ + --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ + --rdzv_backend=c10d \ + --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ + $TRAINING_CMD" + diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/deepspeed_slurm.sh b/tutorials/distributed-ml/torch-tutorial-1-mnist/deepspeed_slurm.sh new file mode 100644 index 00000000..8e5f7881 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/deepspeed_slurm.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# general configuration of the job +#SBATCH --job-name=Torch_DeepSpeed_tutorial-1 +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job-ds.out +#SBATCH --error=job-ds.err +#SBATCH --time=00:30:00 + +# configure node and process count on the CM +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --ntasks-per-node=4 +#SBATCH --cpus-per-task=4 +#SBATCH --gpus-per-node=4 +# SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# set modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# set env +source ../../../envAI_hdfml/bin/activate + +# job info +debug=false +echo "DEBUG: TIME: $(date)" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$debug" = true ] ; then + export NCCL_DEBUG=INFO +fi +echo + +# set env vars +export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK +fi +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +# launch training +MASTER_ADDR=$(scontrol show hostnames "\$SLURM_JOB_NODELIST" | head -n 1)i +export MASTER_ADDR +export MASTER_PORT=29500 + +TRAINING_CMD="train.py -s deepspeed -c config.yaml" + +# Run without launcher: set --ntasks-per-node=NUM_GPUS +srun --cpu-bind=none python -u $TRAINING_CMD --deepspeed + +# # Run with deepspeed launcher: set --ntasks-per-node=1 +# # https://www.deepspeed.ai/getting-started/#multi-node-environment-variables +# export NCCL_IB_DISABLE=1 +# export NCCL_SOCKET_IFNAME=eth0 +# nodelist=$(scontrol show hostname $SLURM_NODELIST) +# echo "$nodelist" | sed -e 's/$/ slots=4/' > .hostfile +# # Requires passwordless SSH access among compute node +# srun --cpu-bind=none deepspeed --hostfile=.hostfile $TRAINING_CMD --deepspeed +# rm .hostfile + diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/hvd_slurm.sh b/tutorials/distributed-ml/torch-tutorial-1-mnist/hvd_slurm.sh new file mode 100644 index 00000000..3774b6e1 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/hvd_slurm.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# general configuration of the job +#SBATCH --job-name=Torch_HVD_tutorial-1 +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job-hvd.out +#SBATCH --error=job-hvd.err +#SBATCH --time=00:30:00 + +# configure node and process count on the CM +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --ntasks-per-node=4 +#SBATCH --cpus-per-task=8 +#SBATCH --gpus-per-node=4 +# SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# set modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# set env +source ../../../envAI_hdfml/bin/activate + +# job info +debug=false +echo "DEBUG: TIME: $(date)" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$debug" = true ] ; then + export NCCL_DEBUG=INFO +fi +echo + +# set vars +# export NCCL_DEBUG=INFO +export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK +fi +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +# launch training +TRAINING_CMD="train.py -s horovod -c config.yaml" + +srun --cpu-bind=none python -u $TRAINING_CMD + diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/runall.sh b/tutorials/distributed-ml/torch-tutorial-1-mnist/runall.sh new file mode 100644 index 00000000..b1470d75 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/runall.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run all versions of distributed ML for MNIST +rm *checkpoint.pth.tar *.out *.err +echo "Torch DDP training: $(sbatch ddp_slurm.sh)" +echo "DeepSpeed training: $(sbatch deepspeed_slurm.sh)" +echo "Horovod training: $(sbatch hvd_slurm.sh)" \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/train.py b/tutorials/distributed-ml/torch-tutorial-1-mnist/train.py new file mode 100644 index 00000000..365a9048 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/train.py @@ -0,0 +1,546 @@ +""" +Show how to use DDP, Horovod and DeepSpeed strategies interchangeably +with a simple neural network trained on MNIST dataset, showing how +to use checkpoints. +""" +import os +import argparse +import sys +import time +import numpy as np +import random + +import torch +import torch.distributed as dist +import torch.nn as nn +import torch.nn.functional as F +from torchvision import datasets, transforms +from torch.utils.data import DataLoader, DistributedSampler + +import deepspeed + +from itwinai.torch.distributed import ( + TorchDistributedStrategy, + DDPDistributedStrategy, + HVDDistributedStrategy, + DSDistributedStrategy, +) +from itwinai.parser import ArgumentParser as ItAIArgumentParser + + +def parse_args() -> argparse.Namespace: + """ + Parse CLI args, which can also be loaded from a configuration file + using the --config flag: + + >>> train.py --strategy ddp --config config.yaml + """ + parser = ItAIArgumentParser(description='PyTorch MNIST Example') + + # Distributed ML strategy + parser.add_argument( + "--strategy", "-s", type=str, + choices=['ddp', 'horovod', 'deepspeed'], + default='ddp' + ) + + # IO parsers + parser.add_argument('--data-dir', default='./', + help=('location of the training dataset in the local ' + 'filesystem')) + parser.add_argument('--restart-int', type=int, default=10, + help='restart interval per epoch (default: 10)') + parser.add_argument('--download-only', + action=argparse.BooleanOptionalAction, + help='Download dataset and exit') + parser.add_argument('--verbose', + action=argparse.BooleanOptionalAction, + help='Print parsed arguments') + + # model parsers + parser.add_argument('--batch-size', type=int, default=64, + help='input batch size for training (default: 64)') + parser.add_argument('--epochs', type=int, default=10, + help='number of epochs to train (default: 10)') + parser.add_argument('--lr', type=float, default=0.01, + help='learning rate (default: 0.01)') + parser.add_argument('--concM', type=int, default=100, + help='concatenate MNIST to this factor (default: 100)') + parser.add_argument('--momentum', type=float, default=0.5, + help='momentum in SGD optimizer (default: 0.5)') + parser.add_argument('--shuff', action='store_true', default=False, + help='shuffle dataset (default: False)') + + # debug parsers + parser.add_argument('--testrun', action='store_true', default=False, + help='do a test run with seed (default: False)') + parser.add_argument('--nseed', type=int, default=0, + help='seed integer for reproducibility (default: 0)') + parser.add_argument('--log-int', type=int, default=10, + help='log interval per training') + + # parallel parsers + parser.add_argument('--backend', type=str, default='nccl', + help='backend for parrallelisation (default: nccl)') + parser.add_argument('--nworker', type=int, default=0, + help=('number of workers in DataLoader (default: 0 -' + ' only main)')) + parser.add_argument('--prefetch', type=int, default=2, + help='prefetch data in DataLoader (default: 2)') + parser.add_argument('--no-cuda', action='store_true', default=False, + help='disables GPGPUs') + parser.add_argument('--local_rank', type=int, default=-1, + help='local rank passed from distributed launcher') + + # DeepSpeed + parser = deepspeed.add_config_arguments(parser) + args = parser.parse_args() + + if args.verbose: + args_list = [f"{key}: {val}" for key, val in args.items()] + print("PARSED ARGS:\n", '\n'.join(args_list)) + + return args + + +class Net(nn.Module): + """ + Simple neural network classifier for MNIST images. + """ + + def __init__(self): + super(Net, self).__init__() + self.conv1 = nn.Conv2d(1, 10, kernel_size=5) + self.conv2 = nn.Conv2d(10, 20, kernel_size=5) + self.conv2_drop = nn.Dropout2d() + self.fc1 = nn.Linear(320, 50) + self.fc2 = nn.Linear(50, 10) + + def forward(self, x): + x = F.relu(F.max_pool2d(self.conv1(x), 2)) + x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) + x = x.view(-1, 320) + x = F.relu(self.fc1(x)) + x = F.dropout(x, training=self.training) + x = self.fc2(x) + return F.log_softmax(x, dim=-1) + + +def train( + model, device, train_loader, optimizer, epoch, + strategy: TorchDistributedStrategy, args +): + """ + Training function, representing an epoch. + """ + model.train() + t_list = [] + loss_acc = 0 + gwsize = strategy.dist_gwsize() + if strategy.is_main_worker(): + print("\n") + for batch_idx, (data, target) in enumerate(train_loader): + t = time.perf_counter() + data, target = data.to(device), target.to(device) + optimizer.zero_grad() + output = model(data) + loss = F.nll_loss(output, target) + loss.backward() + optimizer.step() + if batch_idx % args.log_int == 0 and strategy.is_main_worker(): + print( + f'Train epoch: {epoch} ' + f'[{batch_idx * len(data)}/{len(train_loader.dataset)/gwsize} ' + f'({100.0 * batch_idx / len(train_loader):.0f}%)]\t\t' + f'Loss: {loss.item():.6f}') + t_list.append(time.perf_counter() - t) + loss_acc += loss.item() + if strategy.is_main_worker(): + print('TIMER: train time', sum(t_list) / len(t_list), 's') + return loss_acc + + +def test(model, device, test_loader, strategy: TorchDistributedStrategy): + """ + Model validation. + """ + model.eval() + test_loss = 0 + correct = 0 + gwsize = strategy.dist_gwsize() + with torch.no_grad(): + for data, target in test_loader: + data, target = data.to(device), target.to(device) + output = model(data) + # sum up batch loss + test_loss += F.nll_loss(output, target, reduction="sum").item() + # get the index of the max log-probability + pred = output.argmax(dim=1, keepdim=True) + correct += pred.eq(target.view_as(pred)).sum().item() + test_loss /= len(test_loader.dataset) + if strategy.is_main_worker(): + print( + f'Test set: average loss: {test_loss:.4f}\t' + f'accurate samples: {correct}/{len(test_loader.dataset)/gwsize}') + acc_test = 100.0 * correct * gwsize / len(test_loader.dataset) + return acc_test + + +def save_state( + epoch, distrib_model, loss_acc, optimizer, + res_name, is_best, strategy: TorchDistributedStrategy +): + """ + Save training state. + """ + grank = strategy.dist_grank() + rt = time.time() + # find if is_best happened in any worker + if torch.cuda.is_available(): + is_best_m = strategy.par_allgather_obj(is_best) + + if torch.cuda.is_available(): + if any(is_best_m): + # find which rank is_best happened - select first rank if multiple + is_best_rank = np.where(np.array(is_best_m))[0][0] + + # collect state + state = {'epoch': epoch + 1, + 'state_dict': distrib_model.state_dict(), + 'best_acc': loss_acc, + 'optimizer': optimizer.state_dict()} + + # write on worker with is_best + if grank == is_best_rank: + torch.save(state, './'+res_name) + print( + f'DEBUG: state in {grank} is saved on epoch:{epoch} ' + f'in {time.time()-rt} s') + else: + # collect state + state = {'epoch': epoch + 1, + 'state_dict': distrib_model.state_dict(), + 'best_acc': loss_acc, + 'optimizer': optimizer.state_dict()} + + torch.save(state, './'+res_name) + print( + f'DEBUG: state in {grank} is saved on epoch:{epoch} in ' + f'{time.time()-rt} s') + + +def seed_worker(worker_id): + """ + Seed dataloader worker. + """ + worker_seed = torch.initial_seed() % 2**32 + np.random.seed(worker_seed) + random.seed(worker_seed) + + +def download_mnist(): + """ + Use built-in torch datasets functions to pull MNIST dataset. + """ + + _ = datasets.MNIST( + args.data_dir, train=True, download=True, + transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)) + ])) + _ = datasets.MNIST( + args.data_dir, train=False, download=True, + transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)) + ])) + + +if __name__ == "__main__": + + args = parse_args() + + if args.download_only: + # Download datasets and exit + download_mnist() + sys.exit() + + # Instantiate Strategy + if args.strategy == 'ddp': + if (not torch.cuda.is_available() + or not torch.cuda.device_count() > 1): + raise RuntimeError('Resources unavailable') + + strategy = DDPDistributedStrategy(backend=args.backend) + elif args.strategy == 'horovod': + strategy = HVDDistributedStrategy() + elif args.strategy == 'deepspeed': + strategy = DSDistributedStrategy(backend=args.backend) + else: + raise NotImplementedError( + f"Strategy {args.strategy} is not recognized/implemented.") + strategy.init() + + # check CUDA availability + args.cuda = not args.no_cuda and torch.cuda.is_available() + + # limit # of CPU threads to be used per worker + torch.set_num_threads(1) + + # get directory + program_dir = os.getcwd() + + # start the time.time for profiling + st = time.time() + + # deterministic testrun + if args.testrun: + torch.manual_seed(args.nseed) + g = torch.Generator() + g.manual_seed(args.nseed) + + # get job rank info - rank==0 master gpu + if torch.cuda.is_available(): + # local world size - per node + lwsize = strategy.dist_lwsize() if args.cuda else 0 + gwsize = strategy.dist_gwsize() # global world size - per run + grank = strategy.dist_grank() # global rank - assign per run + lrank = strategy.dist_lrank() # local rank - assign per node + else: + gwsize = 1 + grank = 0 + + # some debug + if strategy.is_main_worker(): + print('TIMER: initialise:', time.time()-st, 's') + + # move the model on the GPU assigned to the current process + device = torch.device( + strategy.dist_device() if args.cuda and torch.cuda.is_available() + else 'cpu') + if args.cuda: + torch.cuda.set_device(lrank) + # deterministic testrun + if args.testrun: + torch.cuda.manual_seed(args.nseed) + + # read data + mnist_scale = args.concM + largeData = [] + for i in range(mnist_scale): + largeData.append( + datasets.MNIST(args.data_dir, train=True, download=False, + transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)) + ])) + ) + + # concat data + train_dataset = torch.utils.data.ConcatDataset(largeData) + + mnist_scale = args.concM + largeData = [] + for i in range(mnist_scale): + largeData.append( + datasets.MNIST(args.data_dir, train=False, download=False, + transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)) + ])) + ) + + # concat data + test_dataset = torch.utils.data.ConcatDataset(largeData) + + # restricts data loading to a subset of the dataset exclusive to the + # current process + args.shuff = args.shuff and not args.testrun + if torch.cuda.is_available(): + train_sampler = DistributedSampler( + train_dataset, num_replicas=gwsize, rank=grank, shuffle=args.shuff) + test_sampler = DistributedSampler( + test_dataset, num_replicas=gwsize, rank=grank, shuffle=args.shuff) + # distribute dataset to workers + # persistent workers is not possible for nworker=0 + pers_w = True if args.nworker > 1 else False + + # deterministic testrun - the same dataset each run + kwargs = {'worker_init_fn': seed_worker, + 'generator': g} if args.testrun else {} + + if torch.cuda.is_available(): + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size, + sampler=train_sampler, num_workers=args.nworker, pin_memory=True, + persistent_workers=pers_w, prefetch_factor=args.prefetch, **kwargs + ) + test_loader = DataLoader( + test_dataset, batch_size=args.batch_size, + sampler=test_sampler, num_workers=args.nworker, pin_memory=True, + persistent_workers=pers_w, prefetch_factor=args.prefetch, **kwargs + ) + else: + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size) + test_loader = DataLoader( + test_dataset, batch_size=args.batch_size) + + if strategy.is_main_worker(): + print('TIMER: read and concat data:', time.time()-st, 's') + + # create CNN model + model = Net().to(device) + + # optimizer + optimizer = torch.optim.SGD( + model.parameters(), lr=args.lr, momentum=args.momentum) + + deepspeed_config = dict(train_batch_size=args.batch_size) + # 'config_params' key is ignored if strategy != DSDistributedStrategy + distrib_model, optimizer, _ = strategy.distributed( + model, optimizer, lr_scheduler=None, config_params=deepspeed_config + ) + + # resume state + start_epoch = 1 + best_acc = np.Inf + res_name = f'{args.strategy}-checkpoint.pth.tar' + if os.path.isfile(res_name): + try: + if torch.cuda.is_available(): + dist.barrier() + # Map model to be loaded to specified single gpu. + loc = {'cuda:%d' % 0: 'cuda:%d' % lrank} if args.cuda else { + 'cpu:%d' % 0: 'cpu:%d' % lrank} + checkpoint = torch.load( + program_dir+'/'+res_name, map_location=loc) + else: + checkpoint = torch.load(program_dir+'/'+res_name) + start_epoch = checkpoint['epoch'] + best_acc = checkpoint['best_acc'] + distrib_model.load_state_dict(checkpoint['state_dict']) + optimizer.load_state_dict(checkpoint['optimizer']) + if torch.cuda.is_available(): + if strategy.is_main_worker(): + print(f'WARNING: restarting from {start_epoch} epoch') + else: + print(f'WARNING: restarting from {start_epoch} epoch') + except Exception: + if torch.cuda.is_available(): + if strategy.is_main_worker(): + print('WARNING: restart file cannot be loaded, ' + 'restarting!') + else: + print('WARNING: restart file cannot be loaded, restarting!') + + if start_epoch > args.epochs: + if torch.cuda.is_available(): + if strategy.is_main_worker(): + print('WARNING: given epochs are less than the one in the ' + 'restart file!\n' + 'WARNING: SYS.EXIT is issued') + + strategy.clean_up() + sys.exit() + else: + print('WARNING: given epochs are less than the one in ' + 'the restart file!\n' + 'WARNING: SYS.EXIT is issued') + sys.exit() + + # start trainin/testing loop + if strategy.is_main_worker(): + print('TIMER: broadcast:', time.time()-st, 's') + print('\nDEBUG: start training') + print('--------------------------------------------------------') + + et = time.time() + for epoch in range(start_epoch, args.epochs + 1): + lt = time.time() + # training + loss_acc = train( + model=distrib_model, + device=device, + train_loader=train_loader, + optimizer=optimizer, + epoch=epoch, + strategy=strategy, + args=args + ) + + # testing + acc_test = test( + model=distrib_model, + device=device, + test_loader=test_loader, + strategy=strategy + ) + + # save first epoch timer + if epoch == start_epoch: + first_ep_t = time.time()-lt + + # final epoch + if epoch + 1 == args.epochs: + train_loader.last_epoch = True + test_loader.last_epoch = True + + if strategy.is_main_worker(): + print('TIMER: epoch time:', time.time()-lt, 's') + print('DEBUG: accuracy:', acc_test, '%') + + # save state if found a better state + is_best = loss_acc < best_acc + if epoch % args.restart_int == 0: + save_state( + epoch=epoch, + distrib_model=distrib_model, + loss_acc=loss_acc, + optimizer=optimizer, + res_name=res_name, + is_best=is_best, + strategy=strategy + ) + # reset best_acc + best_acc = min(loss_acc, best_acc) + + # finalise + # save final state + save_state( + epoch=epoch, + distrib_model=distrib_model, + loss_acc=loss_acc, + optimizer=optimizer, + res_name=res_name, + is_best=True, + strategy=strategy + ) + + # some debug + if strategy.is_main_worker(): + print('\n--------------------------------------------------------') + print('DEBUG: training results:\n') + print('TIMER: first epoch time:', first_ep_t, ' s') + print('TIMER: last epoch time:', time.time()-lt, ' s') + print('TIMER: average epoch time:', (time.time()-et)/args.epochs, ' s') + print('TIMER: total epoch time:', time.time()-et, ' s') + if epoch > 1: + print('TIMER: total epoch-1 time:', + time.time()-et-first_ep_t, ' s') + print('TIMER: average epoch-1 time:', + (time.time()-et-first_ep_t)/(args.epochs-1), ' s') + print('DEBUG: last accuracy:', acc_test, '%') + print('DEBUG: memory req:', + int(torch.cuda.memory_reserved(lrank)/1024/1024), 'MB') \ + if args.cuda else 'DEBUG: memory req: - MB' + print('DEBUG: memory summary:\n\n', + torch.cuda.memory_summary(0)) if args.cuda else '' + + if strategy.is_main_worker(): + print(f'TIMER: final time: {time.time()-st} s\n') + + print(f" - TRAINING FINISHED") + strategy.clean_up() + sys.exit() diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/README.md b/tutorials/distributed-ml/torch-tutorial-2-imagenet/README.md new file mode 100644 index 00000000..780eb278 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-2-imagenet/README.md @@ -0,0 +1,47 @@ +# Tutorial: distributed strategies for PyTorch model trained on MNIST dataset + +In this tutorial we show how to use torch `DistributedDataParallel` (DDP), Horovod and +DeepSpeed from the same client code. +Note that the environment is tested on the HDFML system at JSC. For other systems, +the module versions might need change accordingly. + +## Setup + +First, from the root of this repository, build the environment containing +pytorch, horovod and deepspeed. You can *try* with: + +```bash +# Creates a Python venv called envAI_hdfml +make torch-gpu-jsc +``` + +The Imagenet dataset is assumed to be already downloaded to some location. + +## Distributed training + +Each distributed strategy has its own SLURM job script, which +should be used to run it: + +If you want to distribute the code in `train.py` with **torch DDP**, run from terminal: + +```bash +sbatch ddp_slurm.sh +``` + +If you want to distribute the code in `train.py` with **DeepSpeed**, run from terminal: + +```bash +sbatch deepspeed_slurm.sh +``` + +If you want to distribute the code in `train.py` with **Horovod**, run from terminal: + +```bash +sbatch hvd_slurm.sh +``` + +You can run all of them with: + +```bash +bash runall.sh +``` diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/config.yaml b/tutorials/distributed-ml/torch-tutorial-2-imagenet/config.yaml new file mode 100644 index 00000000..2473d346 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-2-imagenet/config.yaml @@ -0,0 +1,25 @@ +# I/O +data_dir: /p/project/intertwin/datasets/Imagenet_sub/ImageNet_uncompressed/train/ #/p/project/intertwin/datasets/ImageNet_uncompressed/train +restart_int: 10 +verbose: True + +# Model +batch_size: 64 +epochs: 3 +lr: 0.001 +momentum: 0.5 +shuff: False +num_classes: 1000 + +# Debugging +testrun: False +nseed: 10 +log_int: 10 + +# Distributed ML +backend: nccl +nworker: 4 # num workers dataloader +prefetch: 2 +no_cuda: False + + diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/ddp_slurm.sh b/tutorials/distributed-ml/torch-tutorial-2-imagenet/ddp_slurm.sh new file mode 100644 index 00000000..4e9749c2 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-2-imagenet/ddp_slurm.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# general configuration of the job +#SBATCH --job-name=Torch_DDP_tutorial-1 +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job-ddp.out +#SBATCH --error=job-ddp.err +#SBATCH --time=00:30:00 + +# configure node and process count on the CM +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --cpus-per-task=32 +#SBATCH --gpus-per-node=4 +#SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# set modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# set env +source ../../../envAI_hdfml/bin/activate + +# job info +debug=false +echo "DEBUG: TIME: $(date)" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$debug" = true ] ; then + export NCCL_DEBUG=INFO +fi +echo + +# set comm +export CUDA_VISIBLE_DEVICES="0,1,2,3" +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK +fi + +# launch training +TRAINING_CMD="train.py -s ddp -c config.yaml" + +srun --cpu-bind=none bash -c "torchrun \ + --log_dir='logs' \ + --nnodes=$SLURM_NNODES \ + --nproc_per_node=$SLURM_GPUS_PER_NODE \ + --rdzv_id=$SLURM_JOB_ID \ + --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ + --rdzv_backend=c10d \ + --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ + $TRAINING_CMD" + diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/deepspeed_slurm.sh b/tutorials/distributed-ml/torch-tutorial-2-imagenet/deepspeed_slurm.sh new file mode 100644 index 00000000..8f1c2d2d --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-2-imagenet/deepspeed_slurm.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# general configuration of the job +#SBATCH --job-name=Torch_DeepSpeed_tutorial-1 +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job-ds.out +#SBATCH --error=job-ds.err +#SBATCH --time=00:30:00 + +# configure node and process count on the CM +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --ntasks-per-node=4 +#SBATCH --cpus-per-task=4 +#SBATCH --gpus-per-node=4 +#SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# set modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# set env +source ../../../envAI_hdfml/bin/activate + +# job info +debug=false +echo "DEBUG: TIME: $(date)" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$debug" = true ] ; then + export NCCL_DEBUG=INFO +fi +echo + +# set env vars +export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK +fi +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +# launch training +MASTER_ADDR=$(scontrol show hostnames "\$SLURM_JOB_NODELIST" | head -n 1)i +export MASTER_ADDR +export MASTER_PORT=29500 + +TRAINING_CMD="train.py -s deepspeed -c config.yaml" + +# Run without launcher: set --ntasks-per-node=NUM_GPUS +srun --cpu-bind=none python -u $TRAINING_CMD --deepspeed + +# # Run with deepspeed launcher: set --ntasks-per-node=1 +# # https://www.deepspeed.ai/getting-started/#multi-node-environment-variables +# export NCCL_IB_DISABLE=1 +# export NCCL_SOCKET_IFNAME=eth0 +# nodelist=$(scontrol show hostname $SLURM_NODELIST) +# echo "$nodelist" | sed -e 's/$/ slots=4/' > .hostfile +# # Requires passwordless SSH access among compute node +# srun --cpu-bind=none deepspeed --hostfile=.hostfile $TRAINING_CMD --deepspeed +# rm .hostfile + diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/hvd_slurm.sh b/tutorials/distributed-ml/torch-tutorial-2-imagenet/hvd_slurm.sh new file mode 100644 index 00000000..69b9d51e --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-2-imagenet/hvd_slurm.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# general configuration of the job +#SBATCH --job-name=Torch_HVD_tutorial-1 +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job-hvd.out +#SBATCH --error=job-hvd.err +#SBATCH --time=00:30:00 + +# configure node and process count on the CM +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --ntasks-per-node=4 +#SBATCH --cpus-per-task=8 +#SBATCH --gpus-per-node=4 +#SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# set modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# set env +source ../../../envAI_hdfml/bin/activate + +# job info +debug=false +echo "DEBUG: TIME: $(date)" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$debug" = true ] ; then + export NCCL_DEBUG=INFO +fi +echo + +# set vars +# export NCCL_DEBUG=INFO +export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK +fi +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +# launch training +TRAINING_CMD="train.py -s horovod -c config.yaml" + +srun --cpu-bind=none python -u $TRAINING_CMD + diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/runall.sh b/tutorials/distributed-ml/torch-tutorial-2-imagenet/runall.sh new file mode 100644 index 00000000..21c02a22 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-2-imagenet/runall.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Run all versions of distributed ML version +rm *checkpoint.pth.tar *.out *.err *.csv +echo "Torch DDP training: $(sbatch ddp_slurm.sh)" +echo "DeepSpeed training: $(sbatch deepspeed_slurm.sh)" +echo "Horovod training: $(sbatch hvd_slurm.sh)" \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/scaling-test.sh b/tutorials/distributed-ml/torch-tutorial-2-imagenet/scaling-test.sh new file mode 100644 index 00000000..275f7fb7 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-2-imagenet/scaling-test.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +rm *checkpoint.pth.tar *.out *.err *.csv + +timeout="01:01:00" +for N in 1 2 4 8 +do + sbatch --job-name="DDP-imagenet-n$N" --nodes=$N --output="job-ddp-n$N.out" --error="job-ddp-n$N.err" --time=$timeout ddp_slurm.sh + sbatch --job-name="DS-imagenet-n$N" --nodes=$N --output="job-ds-n$N.out" --error="job-ds-n$N.err" --time=$timeout deepspeed_slurm.sh + sbatch --job-name="HVD-imagenet-n$N" --nodes=$N --output="job-hvd-n$N.out" --error="job-hvd-n$N.err" --time=$timeout hvd_slurm.sh +done \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/train.py b/tutorials/distributed-ml/torch-tutorial-2-imagenet/train.py new file mode 100644 index 00000000..6bd71214 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-2-imagenet/train.py @@ -0,0 +1,499 @@ +""" +Show how to use DDP, Horovod and DeepSpeed strategies interchangeably +with a large neural network trained on Imagenet dataset, showing how +to use checkpoints. +""" +import os +import argparse +import sys +import time +import numpy as np +import random + +import torch +from torch import nn +import torch.distributed as dist +import torch.nn.functional as F +import torchvision +from torchvision import transforms +from torch.utils.data import DataLoader, DistributedSampler + +import deepspeed + +from itwinai.torch.distributed import ( + TorchDistributedStrategy, + DDPDistributedStrategy, + HVDDistributedStrategy, + DSDistributedStrategy, +) +from itwinai.parser import ArgumentParser as ItAIArgumentParser +from itwinai.loggers import EpochTimeTracker + + +def parse_args() -> argparse.Namespace: + """ + Parse CLI args, which can also be loaded from a configuration file + using the --config flag: + + >>> train.py --strategy ddp --config config.yaml + """ + parser = ItAIArgumentParser(description='PyTorch MNIST Example') + + # Distributed ML strategy + parser.add_argument( + "--strategy", "-s", type=str, + choices=['ddp', 'horovod', 'deepspeed'], + default='ddp' + ) + + # IO parsers + parser.add_argument('--data-dir', default='./', + help=('location of the training dataset in the local ' + 'filesystem')) + parser.add_argument('--restart-int', type=int, default=10, + help='restart interval per epoch (default: 10)') + parser.add_argument('--verbose', + action=argparse.BooleanOptionalAction, + help='Print parsed arguments') + + # model parsers + parser.add_argument('--batch-size', type=int, default=64, + help='input batch size for training (default: 64)') + parser.add_argument('--epochs', type=int, default=10, + help='number of epochs to train (default: 10)') + parser.add_argument('--lr', type=float, default=0.01, + help='learning rate (default: 0.01)') + parser.add_argument('--momentum', type=float, default=0.5, + help='momentum in SGD optimizer (default: 0.5)') + parser.add_argument('--shuff', action='store_true', default=False, + help='shuffle dataset (default: False)') + parser.add_argument('--num-classes', type=int, default=1000, + help='number of classes in dataset') + + # debug parsers + parser.add_argument('--testrun', action='store_true', default=False, + help='do a test run with seed (default: False)') + parser.add_argument('--nseed', type=int, default=0, + help='seed integer for reproducibility (default: 0)') + parser.add_argument('--log-int', type=int, default=10, + help='log interval per training') + + # parallel parsers + parser.add_argument('--backend', type=str, default='nccl', + help='backend for parrallelisation (default: nccl)') + parser.add_argument('--nworker', type=int, default=0, + help=('number of workers in DataLoader (default: 0 -' + ' only main)')) + parser.add_argument('--prefetch', type=int, default=2, + help='prefetch data in DataLoader (default: 2)') + parser.add_argument('--no-cuda', action='store_true', default=False, + help='disables GPGPUs') + parser.add_argument('--local_rank', type=int, default=-1, + help='local rank passed from distributed launcher') + + # DeepSpeed + parser = deepspeed.add_config_arguments(parser) + args = parser.parse_args() + + if args.verbose: + args_list = [f"{key}: {val}" for key, val in args.items()] + print("PARSED ARGS:\n", '\n'.join(args_list)) + + return args + + +def train( + model, device, train_loader, optimizer, epoch, + strategy: TorchDistributedStrategy, args +): + """ + Training function, representing an epoch. + """ + model.train() + t_list = [] + loss_acc = 0 + gwsize = strategy.dist_gwsize() + if strategy.is_main_worker(): + print("\n") + for batch_idx, (data, target) in enumerate(train_loader): + t = time.perf_counter() + data, target = data.to(device), target.to(device) + optimizer.zero_grad() + output = model(data) + loss = F.nll_loss(output, target) + loss.backward() + optimizer.step() + if batch_idx % args.log_int == 0 and strategy.is_main_worker(): + print( + f'Train epoch: {epoch} ' + f'[{batch_idx * len(data)}/{len(train_loader.dataset)/gwsize} ' + f'({100.0 * batch_idx / len(train_loader):.0f}%)]\t\t' + f'Loss: {loss.item():.6f}') + t_list.append(time.perf_counter() - t) + loss_acc += loss.item() + if strategy.is_main_worker(): + print('TIMER: train time', sum(t_list) / len(t_list), 's') + return loss_acc + + +def test(model, device, test_loader, strategy: TorchDistributedStrategy): + """ + Model validation. + """ + model.eval() + test_loss = 0 + correct = 0 + gwsize = strategy.dist_gwsize() + with torch.no_grad(): + for data, target in test_loader: + data, target = data.to(device), target.to(device) + output = model(data) + # sum up batch loss + test_loss += F.nll_loss(output, target, reduction="sum").item() + # get the index of the max log-probability + pred = output.argmax(dim=1, keepdim=True) + correct += pred.eq(target.view_as(pred)).sum().item() + test_loss /= len(test_loader.dataset) + if strategy.is_main_worker(): + print( + f'Test set: average loss: {test_loss:.4f}\t' + f'accurate samples: {correct}/{len(test_loader.dataset)/gwsize}') + acc_test = 100.0 * correct * gwsize / len(test_loader.dataset) + return acc_test + + +def save_state( + epoch, distrib_model, loss_acc, optimizer, + res_name, is_best, strategy: TorchDistributedStrategy +): + """ + Save training state. + """ + grank = strategy.dist_grank() + rt = time.time() + # find if is_best happened in any worker + if torch.cuda.is_available(): + is_best_m = strategy.par_allgather_obj(is_best) + + if torch.cuda.is_available(): + if any(is_best_m): + # find which rank is_best happened - select first rank if multiple + is_best_rank = np.where(np.array(is_best_m))[0][0] + + # collect state + state = {'epoch': epoch + 1, + 'state_dict': distrib_model.state_dict(), + 'best_acc': loss_acc, + 'optimizer': optimizer.state_dict()} + + # write on worker with is_best + if grank == is_best_rank: + torch.save(state, './'+res_name) + print( + f'DEBUG: state in {grank} is saved on epoch:{epoch} ' + f'in {time.time()-rt} s') + else: + # collect state + state = {'epoch': epoch + 1, + 'state_dict': distrib_model.state_dict(), + 'best_acc': loss_acc, + 'optimizer': optimizer.state_dict()} + + torch.save(state, './'+res_name) + print( + f'DEBUG: state in {grank} is saved on epoch:{epoch} in ' + f'{time.time()-rt} s') + + +def seed_worker(worker_id): + """ + Seed dataloader worker. + """ + worker_seed = torch.initial_seed() % 2**32 + np.random.seed(worker_seed) + random.seed(worker_seed) + + +if __name__ == "__main__": + + args = parse_args() + + # Instantiate Strategy + if args.strategy == 'ddp': + if (not torch.cuda.is_available() + or not torch.cuda.device_count() > 1): + raise RuntimeError('Resources unavailable') + + strategy = DDPDistributedStrategy(backend=args.backend) + elif args.strategy == 'horovod': + strategy = HVDDistributedStrategy() + elif args.strategy == 'deepspeed': + strategy = DSDistributedStrategy(backend=args.backend) + else: + raise NotImplementedError( + f"Strategy {args.strategy} is not recognized/implemented.") + strategy.init() + + # check CUDA availability + args.cuda = not args.no_cuda and torch.cuda.is_available() + + # limit # of CPU threads to be used per worker + torch.set_num_threads(1) + + # get directory + program_dir = os.getcwd() + + # start the time.time for profiling + st = time.time() + + # deterministic testrun + if args.testrun: + torch.manual_seed(args.nseed) + g = torch.Generator() + g.manual_seed(args.nseed) + + # get job rank info - rank==0 master gpu + if torch.cuda.is_available(): + # local world size - per node + lwsize = strategy.dist_lwsize() if args.cuda else 0 + gwsize = strategy.dist_gwsize() # global world size - per run + grank = strategy.dist_grank() # global rank - assign per run + lrank = strategy.dist_lrank() # local rank - assign per node + else: + gwsize = 1 + grank = 0 + + # some debug + if strategy.is_main_worker(): + print('TIMER: initialise:', time.time()-st, 's') + + # move the model on the GPU assigned to the current process + device = torch.device( + strategy.dist_device() if args.cuda and torch.cuda.is_available() + else 'cpu') + if args.cuda: + torch.cuda.set_device(lrank) + # deterministic testrun + if args.testrun: + torch.cuda.manual_seed(args.nseed) + + # dataset + # Initialize transformations for data augmentation + transform = transforms.Compose([ + transforms.Resize(256), + transforms.RandomHorizontalFlip(), + transforms.RandomVerticalFlip(), + transforms.RandomRotation(degrees=45), + transforms.ColorJitter( + brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5), + transforms.CenterCrop(224), + transforms.ToTensor(), + transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]) + + # Load the ImageNet Object Localization Challenge dataset + train_dataset = torchvision.datasets.ImageFolder( + root=args.data_dir, + transform=transform + ) + # test_dataset = ... + + # restricts data loading to a subset of the dataset exclusive to the + # current process + args.shuff = args.shuff and not args.testrun + if torch.cuda.is_available(): + train_sampler = DistributedSampler( + train_dataset, num_replicas=gwsize, rank=grank, shuffle=args.shuff) + # test_sampler = DistributedSampler( + # test_dataset, num_replicas=gwsize, rank=grank, + # shuffle=args.shuff) + # distribute dataset to workers + # persistent workers is not possible for nworker=0 + pers_w = True if args.nworker > 1 else False + + # deterministic testrun - the same dataset each run + kwargs = {'worker_init_fn': seed_worker, + 'generator': g} if args.testrun else {} + + if torch.cuda.is_available(): + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size, + sampler=train_sampler, num_workers=args.nworker, pin_memory=True, + persistent_workers=pers_w, prefetch_factor=args.prefetch, **kwargs + ) + # test_loader = DataLoader( + # test_dataset, batch_size=args.batch_size, + # sampler=test_sampler, num_workers=args.nworker, pin_memory=True, + # persistent_workers=pers_w, prefetch_factor=args.prefetch, + # **kwargs + # ) + else: + train_loader = DataLoader( + train_dataset, batch_size=args.batch_size) + # test_loader = DataLoader( + # test_dataset, batch_size=args.batch_size) + + if strategy.is_main_worker(): + print('TIMER: read and concat data:', time.time()-st, 's') + + # create CNN model: resnet 50, resnet101, resnet152 + model = torchvision.models.resnet152() + model.fc = nn.Linear(2048, args.num_classes) + + # optimizer + optimizer = torch.optim.SGD( + model.parameters(), lr=args.lr, momentum=args.momentum) + + deepspeed_config = dict(train_micro_batch_size_per_gpu=args.batch_size) + # 'config_params' key is ignored if strategy != DSDistributedStrategy + distrib_model, optimizer, _ = strategy.distributed( + model, optimizer, lr_scheduler=None, config_params=deepspeed_config + ) + + # resume state + start_epoch = 1 + best_acc = np.Inf + nnod = os.environ.get('SLURM_NNODES', 'unk') + res_name = f'{args.strategy}-{nnod}N-checkpoint.pth.tar' + if os.path.isfile(res_name): + try: + if torch.cuda.is_available(): + dist.barrier() + # Map model to be loaded to specified single gpu. + loc = {'cuda:%d' % 0: 'cuda:%d' % lrank} if args.cuda else { + 'cpu:%d' % 0: 'cpu:%d' % lrank} + checkpoint = torch.load( + program_dir+'/'+res_name, map_location=loc) + else: + checkpoint = torch.load(program_dir+'/'+res_name) + start_epoch = checkpoint['epoch'] + best_acc = checkpoint['best_acc'] + distrib_model.load_state_dict(checkpoint['state_dict']) + optimizer.load_state_dict(checkpoint['optimizer']) + if torch.cuda.is_available(): + if strategy.is_main_worker(): + print(f'WARNING: restarting from {start_epoch} epoch') + else: + print(f'WARNING: restarting from {start_epoch} epoch') + except Exception: + if torch.cuda.is_available(): + if strategy.is_main_worker(): + print('WARNING: restart file cannot be loaded, ' + 'restarting!') + else: + print('WARNING: restart file cannot be loaded, restarting!') + + if start_epoch > args.epochs: + if torch.cuda.is_available(): + if strategy.is_main_worker(): + print('WARNING: given epochs are less than the one in the ' + 'restart file!\n' + 'WARNING: SYS.EXIT is issued') + + strategy.clean_up() + sys.exit() + else: + print('WARNING: given epochs are less than the one in ' + 'the restart file!\n' + 'WARNING: SYS.EXIT is issued') + sys.exit() + + # start trainin/testing loop + if strategy.is_main_worker(): + print('TIMER: broadcast:', time.time()-st, 's') + print('\nDEBUG: start training') + print('--------------------------------------------------------') + epoch_time_tracker = EpochTimeTracker(series_name=args.strategy) + + et = time.time() + for epoch in range(start_epoch, args.epochs + 1): + lt = time.time() + # training + loss_acc = train( + model=distrib_model, + device=device, + train_loader=train_loader, + optimizer=optimizer, + epoch=epoch, + strategy=strategy, + args=args + ) + + # # testing + # acc_test = test( + # model=distrib_model, + # device=device, + # test_loader=test_loader, + # strategy=strategy + # ) + + # save first epoch timer + if epoch == start_epoch: + first_ep_t = time.time()-lt + + # final epoch + if epoch + 1 == args.epochs: + train_loader.last_epoch = True + # test_loader.last_epoch = True + + if strategy.is_main_worker(): + print('TIMER: epoch time:', time.time()-lt, 's') + epoch_time_tracker.add_epoch_time(epoch-1, time.time()-lt) + # print('DEBUG: accuracy:', acc_test, '%') + + # save state if found a better state + is_best = loss_acc < best_acc + if epoch % args.restart_int == 0: + save_state( + epoch=epoch, + distrib_model=distrib_model, + loss_acc=loss_acc, + optimizer=optimizer, + res_name=res_name, + is_best=is_best, + strategy=strategy + ) + # reset best_acc + best_acc = min(loss_acc, best_acc) + + # finalise + # save final state + save_state( + epoch=epoch, + distrib_model=distrib_model, + loss_acc=loss_acc, + optimizer=optimizer, + res_name=res_name, + is_best=True, + strategy=strategy + ) + + # some debug + if strategy.is_main_worker(): + print('\n--------------------------------------------------------') + print('DEBUG: training results:\n') + print('TIMER: first epoch time:', first_ep_t, ' s') + print('TIMER: last epoch time:', time.time()-lt, ' s') + print('TIMER: average epoch time:', (time.time()-et)/args.epochs, ' s') + print('TIMER: total epoch time:', time.time()-et, ' s') + if epoch > 1: + print('TIMER: total epoch-1 time:', + time.time()-et-first_ep_t, ' s') + print('TIMER: average epoch-1 time:', + (time.time()-et-first_ep_t)/(args.epochs-1), ' s') + # print('DEBUG: last accuracy:', acc_test, '%') + print('DEBUG: memory req:', + int(torch.cuda.memory_reserved(lrank)/1024/1024), 'MB') \ + if args.cuda else 'DEBUG: memory req: - MB' + print('DEBUG: memory summary:\n\n', + torch.cuda.memory_summary(0)) if args.cuda else '' + + if strategy.is_main_worker(): + print(f'TIMER: final time: {time.time()-st} s\n') + nnod = os.environ.get('SLURM_NNODES', 'unk') + epoch_time_tracker.save( + csv_file=f"epochtime_{args.strategy}_{nnod}N.csv") + + print(f" - TRAINING FINISHED") + strategy.clean_up() + sys.exit() diff --git a/use-cases/zebra2horse/cyclegan.py b/use-cases/zebra2horse/cyclegan.py deleted file mode 100644 index 5dc6b7a6..00000000 --- a/use-cases/zebra2horse/cyclegan.py +++ /dev/null @@ -1,507 +0,0 @@ -import tensorflow.keras as keras -import tensorflow as tf -import tensorflow_addons as tfa - -from tensorflow.keras import layers - - -class ReflectionPadding2D(layers.Layer): - """Implements Reflection Padding as a layer. - - Args: - padding(tuple): Amount of padding for the - spatial dimensions. - - Returns: - A padded tensor with the same type as the input tensor. - """ - - def __init__(self, padding=(1, 1), **kwargs): - self.padding = tuple(padding) - super().__init__(**kwargs) - - def call(self, input_tensor, mask=None): - padding_width, padding_height = self.padding - padding_tensor = [ - [0, 0], - [padding_height, padding_height], - [padding_width, padding_width], - [0, 0], - ] - return tf.pad(input_tensor, padding_tensor, mode="REFLECT") - - def get_config(self): - config = super().get_config().copy() - config.update({ - 'padding': self.padding, - }) - return config - - -def residual_block( - x, - activation, - kernel_initializer=keras.initializers.RandomNormal( - mean=0.0, stddev=0.02), - kernel_size=(3, 3), - strides=(1, 1), - padding="valid", - gamma_initializer=keras.initializers.RandomNormal( - mean=0.0, stddev=0.02), - use_bias=False, -): - dim = x.shape[-1] - input_tensor = x - - x = ReflectionPadding2D()(input_tensor) - x = layers.Conv2D( - dim, - kernel_size, - strides=strides, - kernel_initializer=kernel_initializer, - padding=padding, - use_bias=use_bias, - )(x) - x = tfa.layers.InstanceNormalization( - gamma_initializer=gamma_initializer)(x) - x = activation(x) - - x = ReflectionPadding2D()(x) - x = layers.Conv2D( - dim, - kernel_size, - strides=strides, - kernel_initializer=kernel_initializer, - padding=padding, - use_bias=use_bias, - )(x) - x = tfa.layers.InstanceNormalization( - gamma_initializer=gamma_initializer)(x) - x = layers.add([input_tensor, x]) - return x - - -def downsample( - x, - filters, - activation, - kernel_initializer=keras.initializers.RandomNormal( - mean=0.0, stddev=0.02), - kernel_size=(3, 3), - strides=(2, 2), - padding="same", - gamma_initializer=keras.initializers.RandomNormal( - mean=0.0, stddev=0.02), - use_bias=False, -): - x = layers.Conv2D( - filters, - kernel_size, - strides=strides, - kernel_initializer=kernel_initializer, - padding=padding, - use_bias=use_bias, - )(x) - x = tfa.layers.InstanceNormalization( - gamma_initializer=gamma_initializer)(x) - if activation: - x = activation(x) - return x - - -def upsample( - x, - filters, - activation, - kernel_size=(3, 3), - strides=(2, 2), - padding="same", - kernel_initializer=keras.initializers.RandomNormal( - mean=0.0, stddev=0.02), - gamma_initializer=keras.initializers.RandomNormal( - mean=0.0, stddev=0.02), - use_bias=False, -): - x = layers.Conv2DTranspose( - filters, - kernel_size, - strides=strides, - padding=padding, - kernel_initializer=kernel_initializer, - use_bias=use_bias, - )(x) - x = tfa.layers.InstanceNormalization( - gamma_initializer=gamma_initializer)(x) - if activation: - x = activation(x) - return x - - -class Generator(keras.Model): - def __init__( - self, - filters=64, - num_downsampling_blocks=2, - num_residual_blocks=9, - num_upsample_blocks=2, - gamma_initializer=keras.initializers.RandomNormal( - mean=0.0, stddev=0.02), - input_img_size=(256, 256, 3) - ): - super().__init__() - - name = 'gen' - - self.filters = filters - self.num_downsampling_blocks = num_downsampling_blocks - self.num_residual_blocks = num_residual_blocks - self.num_upsample_blocks = num_upsample_blocks - self.gamma_initializer = gamma_initializer - self.input_img_size = input_img_size - - img_input = layers.Input(shape=input_img_size, - name=name + "_img_input") - x = ReflectionPadding2D(padding=(3, 3))(img_input) - x = layers.Conv2D( - filters, (7, 7), - kernel_initializer=keras.initializers.RandomNormal( - mean=0.0, stddev=0.02), - use_bias=False - )(x) - x = tfa.layers.InstanceNormalization( - gamma_initializer=gamma_initializer)(x) - x = layers.Activation("relu")(x) - - # Downsampling - for _ in range(num_downsampling_blocks): - filters *= 2 - x = downsample(x, filters=filters, - activation=layers.Activation("relu")) - - # Residual blocks - for _ in range(num_residual_blocks): - x = residual_block(x, activation=layers.Activation("relu")) - - # Upsampling - for _ in range(num_upsample_blocks): - filters //= 2 - x = upsample(x, filters, activation=layers.Activation("relu")) - - # Final block - x = ReflectionPadding2D(padding=(3, 3))(x) - x = layers.Conv2D(3, (7, 7), padding="valid")(x) - x = layers.Activation("tanh")(x) - - self.model = keras.models.Model(img_input, x, name=name) - - def call(self, inputs, training=False): - return self.model(inputs) - - def get_config(self): - config = super().get_config().copy() - config.update({ - 'filters': self.filters, - 'num_downsampling_blocks': self.num_downsampling_blocks, - 'num_residual_blocks': self.num_residual_blocks, - 'num_upsample_blocks': self.num_upsample_blocks, - 'gamma_initializer': self.gamma_initializer, - 'input_img_size': self.input_img_size, - }) - return config - - -class Discriminator(keras.Model): - def __init__( - self, - filters=64, - kernel_initializer=keras.initializers.RandomNormal( - mean=0.0, stddev=0.02), - num_downsampling=3, - input_img_size=(256, 256, 3) - ): - super().__init__() - - name = 'disc' - self.filters = filters - self.kernel_initializer = kernel_initializer - self.num_downsampling = num_downsampling - self.input_img_size = input_img_size - - img_input = layers.Input(shape=input_img_size, - name=name + "_img_input") - x = layers.Conv2D( - filters, - (4, 4), - strides=(2, 2), - padding="same", - kernel_initializer=kernel_initializer, - )(img_input) - x = layers.LeakyReLU(0.2)(x) - - num_filters = filters - for num_downsample_block in range(3): - num_filters *= 2 - if num_downsample_block < 2: - x = downsample( - x, - filters=num_filters, - activation=layers.LeakyReLU(0.2), - kernel_size=(4, 4), - strides=(2, 2), - ) - else: - x = downsample( - x, - filters=num_filters, - activation=layers.LeakyReLU(0.2), - kernel_size=(4, 4), - strides=(1, 1), - ) - - x = layers.Conv2D( - 1, (4, 4), strides=(1, 1), - padding="same", kernel_initializer=kernel_initializer - )(x) - self.model = keras.models.Model(inputs=img_input, outputs=x, name=name) - - def call(self, inputs, training=False): - return self.model(inputs) - - def get_config(self): - config = super().get_config().copy() - config.update({ - 'filters': self.filters, - 'kernel_initializer': self.kernel_initializer, - 'num_downsampling': self.num_downsampling, - 'input_img_size': self.input_img_size, - }) - return config - - -class CycleGAN(keras.Model): - def __init__( - self, - generator_G: keras.Model, - generator_F: keras.Model, - discriminator_X: keras.Model, - discriminator_Y: keras.Model, - lambda_cycle=10.0, - lambda_identity=0.5, - ): - super().__init__() - self.gen_G = generator_G - self.gen_F = generator_F - self.disc_X = discriminator_X - self.disc_Y = discriminator_Y - self.lambda_cycle = lambda_cycle - self.lambda_identity = lambda_identity - - def compile(self, config: dict): - super().compile() - self.gen_G_optimizer = config['gen_G_optimizer'] - self.gen_F_optimizer = config['gen_F_optimizer'] - self.disc_X_optimizer = config['disc_X_optimizer'] - self.disc_Y_optimizer = config['disc_Y_optimizer'] - - # TODO: Define losses in config file - # Loss function for evaluating adversarial loss - adv_loss_fn = keras.losses.MeanSquaredError( - reduction=tf.keras.losses.Reduction.SUM) - - # Define the loss function for the generators - def generator_loss_fn(fake): - fake_loss = adv_loss_fn(tf.ones_like(fake), fake) - return fake_loss - - # Define the loss function for the discriminators - def discriminator_loss_fn(real, fake): - real_loss = adv_loss_fn(tf.ones_like(real), real) - fake_loss = adv_loss_fn(tf.zeros_like(fake), fake) - return (real_loss + fake_loss) * 0.5 - - self.generator_loss_fn = generator_loss_fn - self.discriminator_loss_fn = discriminator_loss_fn - - self.cycle_loss_fn = keras.losses.MeanAbsoluteError( - reduction=tf.keras.losses.Reduction.SUM) - self.identity_loss_fn = keras.losses.MeanAbsoluteError( - reduction=tf.keras.losses.Reduction.SUM) - - def train_step(self, batch_data): - # x is Horse and y is zebra - real_x, real_y = batch_data - - # For CycleGAN, we need to calculate different - # kinds of losses for the generators and discriminators. - # We will perform the following steps here: - # - # 1. Pass real images through the generators and get the generated - # images - # 2. Pass the generated images back to the generators to check if we - # can predict the original image from the generated image. - # 3. Do an identity mapping of the real images using the - # generators. - # 4. Pass the generated images in 1) to the corresponding - # discriminators. - # 5. Calculate the generators total loss (adversarial + cycle + - # identity) - # 6. Calculate the discriminators loss - # 7. Update the weights of the generators - # 8. Update the weights of the discriminators - # 9. Return the losses in a dictionary - - with tf.GradientTape(persistent=True) as tape: - # Horse to fake zebra - fake_y = self.gen_G(real_x, training=True) - # Zebra to fake horse -> y2x - fake_x = self.gen_F(real_y, training=True) - - # Cycle (Horse to fake zebra to fake horse): x -> y -> x - cycled_x = self.gen_F(fake_y, training=True) - # Cycle (Zebra to fake horse to fake zebra) y -> x -> y - cycled_y = self.gen_G(fake_x, training=True) - - # Identity mapping - same_x = self.gen_F(real_x, training=True) - same_y = self.gen_G(real_y, training=True) - - # Discriminator output - disc_real_x = self.disc_X(real_x, training=True) - disc_fake_x = self.disc_X(fake_x, training=True) - - disc_real_y = self.disc_Y(real_y, training=True) - disc_fake_y = self.disc_Y(fake_y, training=True) - - # Generator adversarial loss - gen_G_loss = self.generator_loss_fn(disc_fake_y) - gen_F_loss = self.generator_loss_fn(disc_fake_x) - - # Generator cycle loss - cycle_loss_G = self.cycle_loss_fn( - real_y, cycled_y) * self.lambda_cycle - cycle_loss_F = self.cycle_loss_fn( - real_x, cycled_x) * self.lambda_cycle - - # Generator identity loss - id_loss_G = ( - self.identity_loss_fn(real_y, same_y) - * self.lambda_cycle - * self.lambda_identity - ) - id_loss_F = ( - self.identity_loss_fn(real_x, same_x) - * self.lambda_cycle - * self.lambda_identity - ) - - # Total generator loss - total_loss_G = gen_G_loss + cycle_loss_G + id_loss_G - total_loss_F = gen_F_loss + cycle_loss_F + id_loss_F - - # Discriminator loss - disc_X_loss = self.discriminator_loss_fn(disc_real_x, disc_fake_x) - disc_Y_loss = self.discriminator_loss_fn(disc_real_y, disc_fake_y) - - # Get the gradients for the generators - grads_G = tape.gradient(total_loss_G, self.gen_G.trainable_variables) - grads_F = tape.gradient(total_loss_F, self.gen_F.trainable_variables) - - # Get the gradients for the discriminators - disc_X_grads = tape.gradient( - disc_X_loss, self.disc_X.trainable_variables) - disc_Y_grads = tape.gradient( - disc_Y_loss, self.disc_Y.trainable_variables) - - # Update the weights of the generators - self.gen_G_optimizer.apply_gradients( - zip(grads_G, self.gen_G.trainable_variables) - ) - self.gen_F_optimizer.apply_gradients( - zip(grads_F, self.gen_F.trainable_variables) - ) - - # Update the weights of the discriminators - self.disc_X_optimizer.apply_gradients( - zip(disc_X_grads, self.disc_X.trainable_variables) - ) - self.disc_Y_optimizer.apply_gradients( - zip(disc_Y_grads, self.disc_Y.trainable_variables) - ) - - return { - "G_loss": total_loss_G, - "F_loss": total_loss_F, - "D_X_loss": disc_X_loss, - "D_Y_loss": disc_Y_loss, - } - - def test_step(self, inputs): - real_x, real_y = inputs - - # Horse to fake zebra - fake_y = self.gen_G(real_x, training=False) - # Zebra to fake horse -> y2x - fake_x = self.gen_F(real_y, training=False) - - # Cycle (Horse to fake zebra to fake horse): x -> y -> x - cycled_x = self.gen_F(fake_y, training=False) - # Cycle (Zebra to fake horse to fake zebra) y -> x -> y - cycled_y = self.gen_G(fake_x, training=False) - - # Identity mapping - same_x = self.gen_F(real_x, training=False) - same_y = self.gen_G(real_y, training=False) - - # Discriminator output - disc_real_x = self.disc_X(real_x, training=False) - disc_fake_x = self.disc_X(fake_x, training=False) - - disc_real_y = self.disc_Y(real_y, training=False) - disc_fake_y = self.disc_Y(fake_y, training=False) - - # Generator adversarial loss - gen_G_loss = self.generator_loss_fn(disc_fake_y) - gen_F_loss = self.generator_loss_fn(disc_fake_x) - - # Generator cycle loss - cycle_loss_G = self.cycle_loss_fn(real_y, cycled_y) * self.lambda_cycle - cycle_loss_F = self.cycle_loss_fn(real_x, cycled_x) * self.lambda_cycle - - # Generator identity loss - id_loss_G = ( - self.identity_loss_fn(real_y, same_y) - * self.lambda_cycle - * self.lambda_identity - ) - id_loss_F = ( - self.identity_loss_fn(real_x, same_x) - * self.lambda_cycle - * self.lambda_identity - ) - - # Total generator loss - total_loss_G = gen_G_loss + cycle_loss_G + id_loss_G - total_loss_F = gen_F_loss + cycle_loss_F + id_loss_F - - # Discriminator loss - disc_X_loss = self.discriminator_loss_fn(disc_real_x, disc_fake_x) - disc_Y_loss = self.discriminator_loss_fn(disc_real_y, disc_fake_y) - - return { - "G_loss": total_loss_G, - "F_loss": total_loss_F, - "D_X_loss": disc_X_loss, - "D_Y_loss": disc_Y_loss, - } - - def get_config(self): - config = super().get_config().copy() - config.update({ - 'generator_G': self.gen_G, - 'generator_F': self.gen_F, - 'discriminator_X': self.disc_X, - 'discriminator_Y': self.disc_Y, - 'lambda_cycle': self.lambda_cycle, - 'lambda_identity': self.lambda_identity, - }) - return config diff --git a/use-cases/zebra2horse/dataloader.py b/use-cases/zebra2horse/dataloader.py deleted file mode 100644 index 0970d270..00000000 --- a/use-cases/zebra2horse/dataloader.py +++ /dev/null @@ -1,85 +0,0 @@ -from typing import Tuple, Dict, Optional - -# import tensorflow.keras as keras -import tensorflow as tf -import tensorflow_datasets as tfds - -from itwinai.components import DataGetter - - -class Zebra2HorseDataLoader(DataGetter): - def __init__(self, buffer_size: int): - super().__init__() - self.buffer_size = buffer_size - - def load(self): - # Load the horse-zebra dataset using tensorflow-datasets. - dataset, _ = tfds.load("cycle_gan/horse2zebra", - with_info=True, as_supervised=True) - train_horses, train_zebras = dataset["trainA"], dataset["trainB"] - test_horses, test_zebras = dataset["testA"], dataset["testB"] - - # Image sizes - orig_img_size = (286, 286) - input_img_size = (256, 256, 3) - - def normalize_img(img): - img = tf.cast(img, dtype=tf.float32) - # Map values in the range [-1, 1] - return (img / 127.5) - 1.0 - - def preproc_train_fn(img, label): - # Random flip - img = tf.image.random_flip_left_right(img) - # Resize to the original size first - img = tf.image.resize(img, [*orig_img_size]) - # Random crop to 256X256 - img = tf.image.random_crop(img, size=[*input_img_size]) - # Normalize the pixel values in the range [-1, 1] - img = normalize_img(img) - return img - - def preproc_test_fn(img, label): - # Only resizing and normalization for the test images. - img = tf.image.resize(img, [input_img_size[0], input_img_size[1]]) - img = normalize_img(img) - return img - - # TODO: Add shuffle? - # Apply the preprocessing operations to the training data - train_horses = ( - train_horses.map(preproc_train_fn, - num_parallel_calls=tf.data.AUTOTUNE) - .cache() - ) - train_zebras = ( - train_zebras.map(preproc_train_fn, - num_parallel_calls=tf.data.AUTOTUNE) - .cache() - ) - - # Apply the preprocessing operations to the test data - test_horses = ( - test_horses.map(preproc_test_fn, - num_parallel_calls=tf.data.AUTOTUNE) - .cache() - ) - test_zebras = ( - test_zebras.map(preproc_test_fn, - num_parallel_calls=tf.data.AUTOTUNE) - .cache() - ) - - return ( - tf.data.Dataset.zip((train_horses, train_zebras) - ).shuffle(self.buffer_size), - tf.data.Dataset.zip((test_horses, test_zebras) - ).shuffle(self.buffer_size) - ) - - def execute( - self, - config: Optional[Dict] = None - ) -> Tuple[Optional[Tuple], Optional[Dict]]: - train, test = self.load() - return ([train, test],), config diff --git a/use-cases/zebra2horse/pipeline.yaml b/use-cases/zebra2horse/pipeline.yaml deleted file mode 100644 index ff00ef28..00000000 --- a/use-cases/zebra2horse/pipeline.yaml +++ /dev/null @@ -1,47 +0,0 @@ -loader: - class_path: dataloader.Zebra2HorseDataLoader - init_args: - buffer_size: 256 - -trainer: - class_path: trainer.Zebra2HorseTrainer - init_args: - epochs: 10 - batch_size: 1 - model: - class_path: cyclegan.CycleGAN - init_args: - generator_G: - class_path: cyclegan.Generator - generator_F: - class_path: cyclegan.Generator - discriminator_X: - class_path: cyclegan.Discriminator - discriminator_Y: - class_path: cyclegan.Discriminator - compile_conf: - gen_G_optimizer: { - class_name: "Adam", - config: { - learning_rate: 0.001 - } - } - gen_F_optimizer: { - class_name: "Adam", - config: { - learning_rate: 0.001 - } - } - disc_X_optimizer: { - class_name: "Adam", - config: { - learning_rate: 0.001 - } - } - disc_Y_optimizer: { - class_name: "Adam", - config: { - learning_rate: 0.001 - } - } - loggers: [] diff --git a/use-cases/zebra2horse/pix2pix.py b/use-cases/zebra2horse/pix2pix.py deleted file mode 100644 index 0c94dd40..00000000 --- a/use-cases/zebra2horse/pix2pix.py +++ /dev/null @@ -1,111 +0,0 @@ -import torch -import torch.nn as nn - -OUTPUT_CHANNELS = 3 - - -def downsample(in_c, out_c, apply_batchnorm=True): - result = nn.Sequential() - result.add_module(name="Conv2d", module=nn.Conv2d( - in_c, out_c, 4, 2, 1, bias=False)) - if apply_batchnorm: - result.add_module(name="BatchNorm2d", module=nn.BatchNorm2d(out_c)) - result.add_module(name="LeakyReLU", module=nn.LeakyReLU(inplace=True)) - - return result - - -def upsample(in_c, out_c, apply_dropout=False): - result = nn.Sequential() - result.add_module(name="ConvTranspose2d", module=nn.ConvTranspose2d( - in_c, out_c, 4, 2, 1, bias=False)) - result.add_module(name="BatchNorm2d", module=nn.BatchNorm2d(out_c)) - if apply_dropout: - result.add_module(name="Dropout", module=nn.Dropout(0.5, inplace=True)) - result.add_module(name="ReLU", module=nn.ReLU(inplace=True)) - - return result - - -class Generator(nn.Module): - def __init__(self): - super(Generator, self).__init__() - self.down1 = downsample(3, 64, apply_batchnorm=False) - self.down2 = downsample(64, 128) - self.down3 = downsample(128, 256) - self.down4 = downsample(256, 512) - self.down5_7 = downsample(512, 512) - self.down8 = downsample(512, 512, apply_batchnorm=False) - - self.up1 = upsample(512, 512, apply_dropout=True) - self.up2_3 = upsample(1024, 512, apply_dropout=True) - self.up4 = upsample(1024, 512) - self.up5 = upsample(1024, 256) - self.up6 = upsample(512, 128) - self.up7 = upsample(256, 64) - - self.last = nn.Sequential() - self.last.add_module(name="ConvTranspose2d", module=nn.ConvTranspose2d( - 128, OUTPUT_CHANNELS, 4, 2, 1)) - self.last.add_module(name="tanh", module=nn.Tanh()) - - def forward(self, image): - # Encoder - x1 = self.down1(image) - x2 = self.down2(x1) - x3 = self.down3(x2) - x4 = self.down4(x3) - x5 = self.down5_7(x4) - x6 = self.down5_7(x5) - x7 = self.down5_7(x6) - x8 = self.down8(x7) - - # Decoder - x = self.up1(x8) - x = torch.cat([x7, x], dim=1) - x = self.up2_3(x) - x = torch.cat([x6, x], dim=1) - x = self.up2_3(x) - x = torch.cat([x5, x], dim=1) - x = self.up4(x) - x = torch.cat([x4, x], dim=1) - x = self.up5(x) - x = torch.cat([x3, x], dim=1) - x = self.up6(x) - x = torch.cat([x2, x], dim=1) - x = self.up7(x) - x = torch.cat([x1, x], dim=1) - - x = self.last(x) - - return x - - -class Discriminator(nn.Module): - def __init__(self): - super(Discriminator, self).__init__() - self.down1 = downsample(6, 64, apply_batchnorm=False) - self.down2 = downsample(64, 128) - self.down3 = downsample(128, 256) - - self.conv1 = nn.Conv2d(256, 512, 4, 1, 1, bias=False) - self.batchnorm = nn.BatchNorm2d(512) - self.leakyrelu = nn.LeakyReLU(inplace=True) - - self.last = nn.Sequential() - self.last.add_module(name="Conv2d", module=nn.Conv2d(512, 1, 4, 1, 1)) - # self.last.add_module(name="sigmoid", module=nn.Sigmoid()) - - def forward(self, inp, tar): - x = torch.cat([inp, tar], dim=1) - x = self.down1(x) - x = self.down2(x) - x = self.down3(x) - - x = self.conv1(x) - x = self.batchnorm(x) - x = self.leakyrelu(x) - - x = self.last(x) - - return x diff --git a/use-cases/zebra2horse/requriements.txt b/use-cases/zebra2horse/requriements.txt deleted file mode 100644 index 79f66f84..00000000 --- a/use-cases/zebra2horse/requriements.txt +++ /dev/null @@ -1 +0,0 @@ -ray[tune] \ No newline at end of file diff --git a/use-cases/zebra2horse/startscript b/use-cases/zebra2horse/startscript deleted file mode 100644 index d4ccb4a5..00000000 --- a/use-cases/zebra2horse/startscript +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -# general configuration of the job -#SBATCH --job-name=PrototypeTest -#SBATCH --account=intertwin -#SBATCH --mail-user= -#SBATCH --mail-type=ALL -#SBATCH --output=job.out -#SBATCH --error=job.err -#SBATCH --time=00:30:00 - -# configure node and process count on the CM -#SBATCH --partition=batch -#SBATCH --nodes=1 -#SBATCH --ntasks-per-node=1 -#SBATCH --cpus-per-task=4 -#SBATCH --gpus-per-node=4 - -#SBATCH --exclusive - -# gres options have to be disabled for deepv -#SBATCH --gres=gpu:4 - -# load modules -ml --force purge -ml Stages/2023 StdEnv/2023 NVHPC/23.1 OpenMPI/4.1.4 cuDNN/8.6.0.163-CUDA-11.7 Python/3.10.4 HDF5 libaio/0.3.112 GCC/11.3.0 - -# shellcheck source=/dev/null -source ~/.bashrc - -# TODO: test on HPC -srun micromamba run -p ../../.venv-tf python train.py -p pipeline.yaml \ No newline at end of file diff --git a/use-cases/zebra2horse/train.py b/use-cases/zebra2horse/train.py deleted file mode 100644 index c33b9402..00000000 --- a/use-cases/zebra2horse/train.py +++ /dev/null @@ -1,22 +0,0 @@ -import argparse - -from trainer import Zebra2HorseTrainer -from dataloader import Zebra2HorseDataLoader -from itwinai.experimental.executors import LocalExecutor # , RayExecutor - - -if __name__ == "__main__": - # Create CLI Parser - parser = argparse.ArgumentParser() - parser.add_argument("-p", "--pipeline", type=str) - args = parser.parse_args() - - # Execute pipe - executor = LocalExecutor( - steps=args.pipeline, - class_dict={ - "loader": Zebra2HorseDataLoader, - "trainer": Zebra2HorseTrainer - }) - executor.setup(None) - executor.execute(None) diff --git a/use-cases/zebra2horse/trainer.py b/use-cases/zebra2horse/trainer.py deleted file mode 100644 index 1ae74896..00000000 --- a/use-cases/zebra2horse/trainer.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import List, Dict, Tuple, Optional -import tensorflow as tf -import tensorflow.keras as keras - -from itwinai.tensorflow.trainer import TensorflowTrainer -from itwinai.loggers import Logger - - -class Zebra2HorseTrainer(TensorflowTrainer): - def __init__( - self, - epochs: int, - batch_size: int, - compile_conf: Dict, - model: Dict, - logger: List[Logger], - ): - super().__init__() - # Configurable - self.logger = logger - - # Parse down the optimizers - for key in compile_conf.keys(): - compile_conf[key] = keras.optimizers.get(compile_conf[key]) - - print(model) - - super().__init__( - epochs=epochs, - batch_size=batch_size, - callbacks=[], - model_dict=model, - compile_conf=compile_conf, - strategy=tf.distribute.MirroredStrategy() - ) - - def train(self, train_dataset, validation_dataset): - super().train(train_dataset, validation_dataset) - - def execute( - self, - train_dataset, - validation_dataset, - config: Optional[Dict] = None, - ) -> Tuple[Optional[Tuple], Optional[Dict]]: - train_result = self.train(train_dataset, validation_dataset) - return (train_result,), config diff --git a/workflows/README.md b/workflows/README.md deleted file mode 100644 index 900dd048..00000000 --- a/workflows/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Workflow manager integration - -It is possible that `itwinai` is a step in a greater workflow. -This folder contains examples on how to execute an `itwinai` use case -from an external workflow manager, using its own workflow definition language. diff --git a/workflows/cwl/README.md b/workflows/cwl/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/workflows/snakemake/README.md b/workflows/snakemake/README.md deleted file mode 100644 index e69de29b..00000000 From bb84d13080b8c25574f4fb78ae554d33c7ab3168 Mon Sep 17 00:00:00 2001 From: Matteo Bunino <48362942+matbun@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:57:39 +0200 Subject: [PATCH 5/8] Distributed strategy launcher (#127) Update ParseConfig --- src/itwinai/parser.py | 177 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/src/itwinai/parser.py b/src/itwinai/parser.py index 24c521cd..0001627b 100644 --- a/src/itwinai/parser.py +++ b/src/itwinai/parser.py @@ -5,11 +5,186 @@ import logging import os -from typing import List, Type, Union, Optional +from typing import Dict, Any, List, Type, Union, Optional from jsonargparse import ArgumentParser as JAPArgumentParser from jsonargparse import ActionConfigFile from jsonargparse._formatters import DefaultHelpFormatter +import json +from omegaconf import OmegaConf +from pathlib import Path + +from .components import BaseComponent +from .pipeline import Pipeline +from .utils import load_yaml + + +def add_replace_field( + config: Dict, + key_chain: str, + value: Any +) -> None: + """Replace or add (if not present) a field in a dictionary, following a + path of dot-separated keys. Adding is not supported for list items. + Inplace operation. + Args: + config (Dict): dictionary to be modified. + key_chain (str): path of nested (dot-separated) keys to specify the + location + of the new value (e.g., 'foo.bar.line' adds/overwrites the value + located at config['foo']['bar']['line']). + value (Any): the value to insert. + """ + sub_config = config + for idx, k in enumerate(key_chain.split('.')): + if idx >= len(key_chain.split('.')) - 1: + # Last key reached + break + + if isinstance(sub_config, (list, tuple)): + k = int(k) + next_elem = sub_config[k] + else: + next_elem = sub_config.get(k) + + if not isinstance(next_elem, (dict, list, tuple)): + sub_config[k] = dict() + + sub_config = sub_config[k] + if isinstance(sub_config, (list, tuple)): + k = int(k) + sub_config[k] = value + + +class ConfigParser: + """ + Parses a pipeline from a configuration file. + It also provides functionalities for dynamic override + of fields by means of nested key notation. + Args: + config (Union[str, Dict]): path to YAML configuration file + or dict storing a configuration. + override_keys (Optional[Dict[str, Any]], optional): dict mapping + nested keys to the value to override. Defaults to None. + Example: + >>> # pipeline.yaml file + >>> pipeline: + >>> class_path: itwinai.pipeline.Pipeline + >>> init_args: + >>> steps: + >>> - class_path: dataloader.MNISTDataModuleTorch + >>> init_args: + >>> save_path: .tmp/ + >>> + >>> - class_path: itwinai.torch.trainer.TorchTrainerMG + >>> init_args: + >>> model: + >>> class_path: model.Net + >>> loss: + >>> class_path: torch.nn.NLLLoss + >>> init_args: + >>> reduction: mean + >>> from itwinai.parser import ConfigParser + >>> + >>> parser = ConfigParser( + >>> config='pipeline.yaml', + >>> override_keys={ + >>> 'pipeline.init_args.steps.0.init_args.save_path': /save/path + >>> } + >>> ) + >>> pipeline = parser.parse_pipeline() + >>> print(pipeline) + >>> print(pipeline.steps) + >>> + >>> dataloader = parser.parse_step(0) + >>> print(dataloader) + >>> print(dataloader.save_path) + """ + + config: Dict + pipeline: Pipeline + + def __init__( + self, + config: Union[str, Dict], + override_keys: Optional[Dict[str, Any]] = None + ) -> None: + self.config = config + self.override_keys = override_keys + if isinstance(self.config, (str, Path)): + self.config = load_yaml(self.config) + self._dynamic_override_keys() + self._omegaconf_interpolate() + + def _dynamic_override_keys(self): + if self.override_keys is not None: + for key_chain, value in self.override_keys.items(): + add_replace_field(self.config, key_chain, value) + + def _omegaconf_interpolate(self) -> None: + """Performs variable interpolation with OmegaConf on internal + configuration file. + """ + conf = OmegaConf.create(self.config) + self.config = OmegaConf.to_container(conf, resolve=True) + + def parse_pipeline( + self, + pipeline_nested_key: str = "pipeline", + verbose: bool = False + ) -> Pipeline: + """Merges steps into pipeline and parses it. + Args: + pipeline_nested_key (str, optional): nested key in the + configuration file identifying the pipeline object. + Defaults to "pipeline". + verbose (bool): if True, prints the assembled pipeline + to console formatted as JSON. + Returns: + Pipeline: instantiated pipeline. + """ + pipe_parser = JAPArgumentParser() + pipe_parser.add_subclass_arguments(Pipeline, "pipeline") + + pipe_dict = self.config + for key in pipeline_nested_key.split('.'): + pipe_dict = pipe_dict[key] + # pipe_dict = self.config[pipeline_nested_key] + pipe_dict = {"pipeline": pipe_dict} + + if verbose: + print("Assembled pipeline:") + print(json.dumps(pipe_dict, indent=4)) + + # Parse pipeline dict once merged with steps + conf = pipe_parser.parse_object(pipe_dict) + pipe = pipe_parser.instantiate_classes(conf) + self.pipeline = pipe["pipeline"] + return self.pipeline + + def parse_step( + self, + step_idx: Union[str, int], + pipeline_nested_key: str = "pipeline", + verbose: bool = False + ) -> BaseComponent: + pipeline_dict = self.config + for key in pipeline_nested_key.split('.'): + pipeline_dict = pipeline_dict[key] + + step_dict_config = pipeline_dict['init_args']['steps'][step_idx] + + if verbose: + print(f"STEP '{step_idx}' CONFIG:") + print(json.dumps(step_dict_config, indent=4)) + + # Wrap config under "step" field and parse it + step_dict_config = {'step': step_dict_config} + step_parser = JAPArgumentParser() + step_parser.add_subclass_arguments(BaseComponent, "step") + parsed_namespace = step_parser.parse_object(step_dict_config) + return step_parser.instantiate_classes(parsed_namespace)["step"] + class ArgumentParser(JAPArgumentParser): def __init__( From 6180b96521b3b90e78bff67063de3fe4e064af07 Mon Sep 17 00:00:00 2001 From: Matteo Bunino <48362942+matbun@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:45:20 +0200 Subject: [PATCH 6/8] Distributed strategy launcher (#128) Remove experimental files --- Makefile | 2 - env-files/torch/createEnvJSC.sh | 2 +- experimental/cli/example.yaml | 9 - experimental/cli/itwinai-conf.yaml | 14 - experimental/cli/itwinaicli.py | 29 -- experimental/cli/mycode.py | 35 -- experimental/cli/parser-bk.py | 46 --- experimental/cli/parser.py | 29 -- experimental/cluster.py | 97 ----- experimental/distrib_launcher.py | 117 ------ experimental/distributed_tools.py | 68 ---- experimental/example_0.py | 125 ------ experimental/example_1.py | 106 ----- experimental/example_2.py | 107 ----- experimental/example_3.py | 77 ---- experimental/launcher.py | 295 -------------- experimental/launcher_factory.py | 144 ------- experimental/strategy.py | 150 ------- experimental/trainer/DS_config.json | 15 - experimental/trainer/general_startscript | 135 ------- experimental/trainer/general_trainer.py | 482 ----------------------- experimental/workflow/train.yaml | 53 --- src/itwinai/experimental/executors.py | 127 ------ 23 files changed, 1 insertion(+), 2263 deletions(-) delete mode 100644 experimental/cli/example.yaml delete mode 100644 experimental/cli/itwinai-conf.yaml delete mode 100644 experimental/cli/itwinaicli.py delete mode 100644 experimental/cli/mycode.py delete mode 100644 experimental/cli/parser-bk.py delete mode 100644 experimental/cli/parser.py delete mode 100644 experimental/cluster.py delete mode 100644 experimental/distrib_launcher.py delete mode 100644 experimental/distributed_tools.py delete mode 100644 experimental/example_0.py delete mode 100644 experimental/example_1.py delete mode 100644 experimental/example_2.py delete mode 100644 experimental/example_3.py delete mode 100644 experimental/launcher.py delete mode 100644 experimental/launcher_factory.py delete mode 100644 experimental/strategy.py delete mode 100644 experimental/trainer/DS_config.json delete mode 100755 experimental/trainer/general_startscript delete mode 100755 experimental/trainer/general_trainer.py delete mode 100644 experimental/workflow/train.yaml delete mode 100644 src/itwinai/experimental/executors.py diff --git a/Makefile b/Makefile index 52183fd2..9883659d 100644 --- a/Makefile +++ b/Makefile @@ -11,13 +11,11 @@ torch-gpu-jsc: env-files/torch/createEnvJSC.sh tf-gpu-jsc: env-files/tensorflow/createEnvJSCTF.sh sh env-files/tensorflow/createEnvJSCTF.sh - # Install PyTorch env (CPU only) torch-cpu: env-files/torch/pytorch-env-cpu.yml micromamba env create -p ./.venv-pytorch --file env-files/torch/pytorch-env-cpu.yml -y micromamba run -p ./.venv-pytorch python -m pip install -e .[dev] - # Install TensorFlow 2.13. Creates ./.venv-tf folder. # Ref: https://www.tensorflow.org/install/pip#step-by-step_instructions tf-2.13: env-files/tensorflow/tensorflow-2.13.yml diff --git a/env-files/torch/createEnvJSC.sh b/env-files/torch/createEnvJSC.sh index 6b0fa226..68cf292c 100644 --- a/env-files/torch/createEnvJSC.sh +++ b/env-files/torch/createEnvJSC.sh @@ -185,7 +185,7 @@ done # Install itwinai pip install --upgrade pip -pip install -e . +pip install -e .[dev] # cleanup rm -rf horovod *.tar.gz diff --git a/experimental/cli/example.yaml b/experimental/cli/example.yaml deleted file mode 100644 index ef6a342e..00000000 --- a/experimental/cli/example.yaml +++ /dev/null @@ -1,9 +0,0 @@ -server: - class_path: mycode.ServerOptions - init_args: - host: localhost - port: 80 -client: - class_path: mycode.ClientOptions - init_args: - url: http://${server.init_args.host}:${server.init_args.port}/ \ No newline at end of file diff --git a/experimental/cli/itwinai-conf.yaml b/experimental/cli/itwinai-conf.yaml deleted file mode 100644 index 0cb662df..00000000 --- a/experimental/cli/itwinai-conf.yaml +++ /dev/null @@ -1,14 +0,0 @@ -pipeline: - class_path: itwinai.pipeline.Pipeline - steps: [server, client] - -server: - class_path: mycode.ServerOptions - init_args: - host: localhost - port: 80 - -client: - class_path: mycode.ClientOptions - init_args: - url: http://${server.init_args.host}:${server.init_args.port}/ \ No newline at end of file diff --git a/experimental/cli/itwinaicli.py b/experimental/cli/itwinaicli.py deleted file mode 100644 index 6a22bfb1..00000000 --- a/experimental/cli/itwinaicli.py +++ /dev/null @@ -1,29 +0,0 @@ -""" ->>> python itwinaicli.py --config itwinai-conf.yaml --help ->>> python itwinaicli.py --config itwinai-conf.yaml --server.port 333 -""" - - -from itwinai.parser import ConfigParser2 -from itwinai.parser import ItwinaiCLI - -cli = ItwinaiCLI() -print(cli.pipeline) -print(cli.pipeline.steps) -print(cli.pipeline.steps['server'].port) - - -parser = ConfigParser2( - config='itwinai-conf.yaml', - override_keys={ - 'server.init_args.port': 777 - } -) -pipeline = parser.parse_pipeline() -print(pipeline) -print(pipeline.steps) -print(pipeline.steps['server'].port) - -server = parser.parse_step('server') -print(server) -print(server.port) diff --git a/experimental/cli/mycode.py b/experimental/cli/mycode.py deleted file mode 100644 index 5da07624..00000000 --- a/experimental/cli/mycode.py +++ /dev/null @@ -1,35 +0,0 @@ -# from dataclasses import dataclass -from itwinai.components import BaseComponent - - -class ServerOptions(BaseComponent): - host: str - port: int - - def __init__(self, host: str, port: int) -> None: - self.host = host - self.port = port - - def execute(): - ... - - -class ClientOptions(BaseComponent): - url: str - - def __init__(self, url: str) -> None: - self.url = url - - def execute(): - ... - - -class ServerOptions2(BaseComponent): - host: str - port: int - - def __init__(self, client: ClientOptions) -> None: - self.client = client - - def execute(): - ... diff --git a/experimental/cli/parser-bk.py b/experimental/cli/parser-bk.py deleted file mode 100644 index 8f87bf37..00000000 --- a/experimental/cli/parser-bk.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Provide functionalities to manage configuration files, including parsing, -execution, and dynamic override of fields. -""" - -from typing import Any -from jsonargparse import ArgumentParser, ActionConfigFile, Namespace - -from .components import BaseComponent - - -class ItwinaiCLI: - _parser: ArgumentParser - pipeline: BaseComponent - - def __init__( - self, - pipeline_nested_key: str = "pipeline", - args: Any = None, - parser_mode: str = "omegaconf" - ) -> None: - self.pipeline_nested_key = pipeline_nested_key - self.args = args - self.parser_mode = parser_mode - self._init_parser() - self._parse_args() - pipeline_inst = self._parser.instantiate_classes(self._config) - self.pipeline = pipeline_inst[self.pipeline_nested_key] - - def _init_parser(self): - self._parser = ArgumentParser(parser_mode=self.parser_mode) - self._parser.add_argument( - "-c", "--config", action=ActionConfigFile, - required=True, - help="Path to a configuration file in json or yaml format." - ) - self._parser.add_subclass_arguments( - baseclass=BaseComponent, - nested_key=self.pipeline_nested_key - ) - - def _parse_args(self): - if isinstance(self.args, (dict, Namespace)): - self._config = self._parser.parse_object(self.args) - else: - self._config = self._parser.parse_args(self.args) diff --git a/experimental/cli/parser.py b/experimental/cli/parser.py deleted file mode 100644 index f400466f..00000000 --- a/experimental/cli/parser.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Example of dynamic override of config files with (sub)class arguments, -and variable interpolation with omegaconf. - -Run with: ->>> python parser.py - -Or (after clearing the arguments in parse_args(...)): ->>> python parser.py --config example.yaml --server.port 212 -See the help page of each class: ->>> python parser.py --server.help mycode.ServerOptions -""" - -from jsonargparse import ArgumentParser, ActionConfigFile -from mycode import ServerOptions, ClientOptions - -if __name__ == "__main__": - parser = ArgumentParser(parser_mode="omegaconf") - parser.add_subclass_arguments(ServerOptions, "server") - parser.add_subclass_arguments(ClientOptions, "client") - parser.add_argument("--config", action=ActionConfigFile) - - # Example of dynamic CLI override - # cfg = parser.parse_args(["--config=example.yaml", "--server.port=212"]) - cfg = parser.parse_args() - cfg = parser.instantiate_classes(cfg) - print(cfg.client) - print(cfg.client.url) - print(cfg.server.port) diff --git a/experimental/cluster.py b/experimental/cluster.py deleted file mode 100644 index 78ae8ead..00000000 --- a/experimental/cluster.py +++ /dev/null @@ -1,97 +0,0 @@ -import abc -import os -import time - -from lightning.pytorch.plugins.environments import ( - ClusterEnvironment as LightningClusterEnvironment, - SLURMEnvironment as LightningSLURMEnvironment, - TorchElasticEnvironment as LightningTorchElasticEnvironment, - LightningEnvironment -) - - -class ClusterEnvironment(LightningClusterEnvironment): - @abc.abstractmethod - def num_nodes(self) -> int: - """Returns the number of nodes allocated for the current job.""" - - @abc.abstractmethod - def job_id(self) -> str: - """Returns the current job ID inferred from the cluster.""" - - -class SLURMEnvironment(LightningSLURMEnvironment): - def num_nodes(self) -> int: - """Returns the number of nodes allocated for the current job.""" - if os.environ.get('SLURM_JOB_NUM_NODES'): - return int(os.environ['SLURM_JOB_NUM_NODES']) - if os.environ.get('SLURM_NNODES'): - return int(os.environ['SLURM_NNODES']) - raise RuntimeError('Number of nodes not found in SLURM env variables') - - def job_id(self) -> str: - """Returns the current job ID inferred from the cluster.""" - return os.environ['SLURM_JOB_ID'] - - -class TorchElasticEnvironment(LightningTorchElasticEnvironment): - def num_nodes(self) -> int: - """Returns the number of nodes allocated for the current job.""" - gwsize = int(os.environ['WORLD_SIZE']) - lwsize = int(os.environ['LOCAL_WORLD_SIZE']) - return gwsize//lwsize - - def job_id(self) -> str: - """Returns the current job ID inferred from the cluster.""" - return os.environ['TORCHELASTIC_RUN_ID'] - - -class LocalEnvironment(LightningEnvironment): - - _job_id: str = None - - def world_size(self) -> int: - # if os.environ.get('WORLD_SIZE'): - # return int(os.environ.get('WORLD_SIZE')) - print( - "WARNING: world_size() method in 'LocalEnvironment' returns " - f"a fixed-value placeholder world_size={self._world_size}. " - "Use it carefully!" - ) - return self._world_size - - def global_rank(self) -> int: - # if os.environ.get('RANK'): - # return int(os.environ.get('RANK')) - print( - "WARNING: global_rank() method in 'LocalEnvironment' returns " - f"a fixed-value placeholder global_rank={self._global_rank}. " - "Use it carefully!" - ) - return self._global_rank - - def num_nodes(self) -> int: - """Returns the number of nodes allocated for the current job.""" - return 1 - - def job_id(self) -> str: - """Returns the current job ID inferred from the cluster.""" - if self._job_id is None: - self._job_id = str(time.time()) - return self._job_id - - -def detect_cluster() -> ClusterEnvironment: - """Defines a protocol to select the ClusterEnvironment - depending on availability and priority. - """ - - if SLURMEnvironment.detect(): - cluster = SLURMEnvironment() - elif TorchElasticEnvironment.detect(): - cluster = TorchElasticEnvironment() - elif LocalEnvironment.detect(): - cluster = LocalEnvironment() - else: - raise NotImplementedError("Unrecognized cluster env") - return cluster diff --git a/experimental/distrib_launcher.py b/experimental/distrib_launcher.py deleted file mode 100644 index d8f4e881..00000000 --- a/experimental/distrib_launcher.py +++ /dev/null @@ -1,117 +0,0 @@ -import os - -import torch -from torch import nn -from torch.utils.data import DataLoader, Dataset - -from strategy import Strategy, DDPStrategy -from launcher import DummyTorchElasticLauncher, TorchElasticLauncher -from launcher_factory import ( - LauncherFactory, - SimpleLauncherFactory, - TorchElasticLauncherFactory -) -from distributed_tools import DistributedTooling - - -class UniformRndDataset(Dataset): - def __init__(self, x_size: int, y_size: int, len: int = 100): - super().__init__() - self.x_size = x_size - self.y_size = y_size - self.len = len - - def __len__(self): - return self.len - - def __getitem__(self, index): - return torch.rand(self.x_size), torch.rand(self.y_size) - - -def trainer_entrypoint_fn(a, strategy: Strategy): - """Dummy training function.""" - strategy.setup() - print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " - f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") - - # Local model - model = nn.Linear(3, 4) - optim = torch.optim.Adam(model.parameters(), lr=1e-3) - loss_fn = nn.MSELoss() - # Distributed model - model: nn.Module = strategy.distribute_model(model) - optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) - - # Data - train_set = UniformRndDataset(x_size=3, y_size=4) - train_loader = DataLoader(train_set, batch_size=10, num_workers=1) - # Distributed dataloader - train_loader: DataLoader = strategy.distribute_dataloader(train_loader) - - for epoch in range(2): - for (x, y) in train_loader: - # print(f"tensor to cuda:{strategy.device}") - x = x.to(strategy.device) - y = y.to(strategy.device) - - optim.zero_grad() - y_pred = model(x) - loss = loss_fn(y_pred, y) - loss.backward() - optim.step() - - if strategy.is_main_worker(): - print(f"Loss [epoch={epoch}]: {loss.item()}") - - strategy.teardown() - return 123 - - -LAUNCHER = 'torch-elastic-no' -STRATEGY = 'ddp' - -RUN_ID = "my_run_id" -MIN_NODES = 1 -MAX_NODES = 1 -NPROC_PRE_NODE = 4 -MAX_RESTARTS = 2 - -if __name__ == "__main__": - # # STRATEGY BUILDER - - # # Instantiate Launcher Factory - # # launcher = DummyTorchElasticLauncher( - # # n_workers_per_node=NPROC_PRE_NODE, - # # min_nodes=MIN_NODES, - # # max_nodes=MAX_NODES - # # ) - # # launcher = TorchElasticLauncher( - # # rdzv_id=RUN_ID, - # # nproc_per_node=NPROC_PRE_NODE, - # # nnodes=f"{MIN_NODES}:{MAX_NODES}", - # # max_restarts=MAX_RESTARTS - # # ) - # if LAUNCHER == 'torch-elastic': - # launcher_builder: LauncherFactory = TorchElasticLauncherFactory() - # else: - # launcher_builder: LauncherFactory = SimpleLauncherFactory() - - # # Instantiate launcher - # launcher = launcher_builder.createLauncher( - # n_workers_per_node=NPROC_PRE_NODE - # ) - - # # Instantiate Strategy - # if (STRATEGY == 'ddp' - # and torch.cuda.is_available() - # and torch.cuda.device_count() > 1): - # strategy = DDPStrategy(cluster=None, backend='nccl') - # else: - # raise NotImplementedError - - dist_tools = DistributedTooling(n_workers_per_node=NPROC_PRE_NODE) - launcher, strategy = dist_tools.getTools('ddp') - - # CLIENT CODE - # Launch training from launcher - launcher.run(func=trainer_entrypoint_fn, args=("foobar", strategy)) diff --git a/experimental/distributed_tools.py b/experimental/distributed_tools.py deleted file mode 100644 index 83bf241f..00000000 --- a/experimental/distributed_tools.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Tuple -import abc - -from launcher import Launcher -from strategy import Strategy, DDPStrategy -from launcher_factory import TorchElasticLauncherFactory - - -class Assembler(abc.ABC): - """Abstract Assembler class.""" - - -class DistributedTooling(Assembler): - """ - Assembles a set of objects used to enable distributed ML. - Suggests working presets of Launcher and Strategy, providing - an easy entry point for the end user. - """ - - def __init__(self, n_workers_per_node: int = 1) -> None: - super().__init__() - self.n_workers_per_node = n_workers_per_node - - def getTools(self, strategy: str) -> Tuple[Launcher, Strategy]: - if strategy == 'ddp': - return self.getTorchDDPTools() - if strategy == 'deepspeed': - return self.getDeepSpeedTools() - if strategy == 'horovod': - return self.getHorovodTools() - raise ValueError(f"Unrecognized strategy={strategy}") - - def getTorchDDPTools(self) -> Tuple[Launcher, Strategy]: - """ - Returns a suggested preset of Launcher + Strategy - for torch distributed data parallel. - """ - import torch - if not torch.cuda.is_available(): - raise RuntimeError( - "Torch DDP cannot be used. GPUs not available." - ) - if not torch.cuda.device_count() > 1: - raise RuntimeError( - "Torch DDP cannot be used. Only one GPU is available." - ) - launcher_builder = TorchElasticLauncherFactory() - elastic_launcher = launcher_builder.createLauncher( - n_workers_per_node=self.n_workers_per_node - ) - strategy = DDPStrategy(backend='nccl') - return elastic_launcher, strategy - - def getDeepSpeedTools(self) -> Tuple[Launcher, Strategy]: - """ - Returns a suggested preset of Launcher + Strategy - for DeepSpeed distributed ML. - """ - # TODO: complete - raise NotImplementedError - - def getHorovodTools(self) -> Tuple[Launcher, Strategy]: - """ - Returns a suggested preset of Launcher + Strategy - for Horovod distributed ML. - """ - # TODO: complete - raise NotImplementedError diff --git a/experimental/example_0.py b/experimental/example_0.py deleted file mode 100644 index 5a67cfd8..00000000 --- a/experimental/example_0.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -Run this with torchrun -""" - -import os - -import torch -from torch import nn -from torch.utils.data import DataLoader, Dataset - -from strategy import Strategy, DDPStrategy, HorovodStrategy - - -class UniformRndDataset(Dataset): - def __init__(self, x_size: int, y_size: int, len: int = 100): - super().__init__() - self.x_size = x_size - self.y_size = y_size - self.len = len - - def __len__(self): - return self.len - - def __getitem__(self, index): - return torch.rand(self.x_size), torch.rand(self.y_size) - - -def trainer_entrypoint_fn(a, strategy: Strategy): - """Dummy training function.""" - strategy.setup() - print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " - f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") - - # Local model - model = nn.Linear(3, 4) - optim = torch.optim.Adam(model.parameters(), lr=1e-3) - loss_fn = nn.MSELoss() - # Distributed model - model: nn.Module = strategy.distribute_model(model) - optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) - - # Data - train_set = UniformRndDataset(x_size=3, y_size=4) - train_loader = DataLoader(train_set, batch_size=10, num_workers=1) - # Distributed dataloader - train_loader: DataLoader = strategy.distribute_dataloader(train_loader) - - for epoch in range(2): - for (x, y) in train_loader: - # print(f"tensor to cuda:{strategy.device}") - x = x.to(strategy.device) - y = y.to(strategy.device) - - optim.zero_grad() - y_pred = model(x) - loss = loss_fn(y_pred, y) - loss.backward() - optim.step() - - if strategy.is_main_worker(): - print(f"Loss [epoch={epoch}]: {loss.item()}") - - strategy.teardown() - return 123 - - -def trainer_entrypoint_fn_mario(a, strategy: Strategy): - """Dummy training function.""" - - print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " - f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") - - # Local model - model = nn.Linear(3, 4) - optim = torch.optim.Adam(model.parameters(), lr=1e-3) - loss_fn = nn.MSELoss() - # Data - train_set = UniformRndDataset(x_size=3, y_size=4) - train_loader = DataLoader(train_set, batch_size=10, num_workers=1) - - strategy.setup(model, train_set, optim) - # Distributed model - model: nn.Module = strategy.distribute_model(model) - optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) - # Distributed dataloader - train_loader: DataLoader = strategy.distribute_dataloader(train_loader) - - for epoch in range(2): - for (x, y) in train_loader: - # print(f"tensor to cuda:{strategy.device}") - x = x.to(strategy.device) - y = y.to(strategy.device) - - optim.zero_grad() - y_pred = model(x) - loss = loss_fn(y_pred, y) - loss.backward() - optim.step() - - if strategy.is_main_worker(): - print(f"Loss [epoch={epoch}]: {loss.item()}") - - strategy.teardown() - return 123 - - -STRATEGY = 'ddp' - - -if __name__ == "__main__": - - # Instantiate Strategy - if STRATEGY == 'ddp': - if (not torch.cuda.is_available() - or not torch.cuda.device_count() > 1): - raise RuntimeError('Resources unavailable') - - strategy = DDPStrategy(cluster=None, backend='nccl') - elif STRATEGY == 'horovod': - strategy = HorovodStrategy() - else: - raise NotImplementedError - - # Launch distributed training - trainer_entrypoint_fn("foobar", strategy) diff --git a/experimental/example_1.py b/experimental/example_1.py deleted file mode 100644 index 3cc2e452..00000000 --- a/experimental/example_1.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -Introduction of launcher. Torchrun is not needed anymore. -""" -import os - -import torch -from torch import nn -from torch.utils.data import DataLoader, Dataset - -from strategy import Strategy, DDPStrategy, HorovodStrategy -from launcher import TorchElasticLauncher, SimpleLauncher - - -class UniformRndDataset(Dataset): - def __init__(self, x_size: int, y_size: int, len: int = 100): - super().__init__() - self.x_size = x_size - self.y_size = y_size - self.len = len - - def __len__(self): - return self.len - - def __getitem__(self, index): - return torch.rand(self.x_size), torch.rand(self.y_size) - - -def trainer_entrypoint_fn(a, strategy: Strategy): - """Dummy training function.""" - strategy.setup() - print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " - f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") - - # Local model - model = nn.Linear(3, 4) - optim = torch.optim.Adam(model.parameters(), lr=1e-3) - loss_fn = nn.MSELoss() - # Distributed model - model: nn.Module = strategy.distribute_model(model) - optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) - - # Data - train_set = UniformRndDataset(x_size=3, y_size=4) - train_loader = DataLoader(train_set, batch_size=10, num_workers=1) - # Distributed dataloader - train_loader: DataLoader = strategy.distribute_dataloader(train_loader) - - for epoch in range(2): - for (x, y) in train_loader: - # print(f"tensor to cuda:{strategy.device}") - x = x.to(strategy.device) - y = y.to(strategy.device) - - optim.zero_grad() - y_pred = model(x) - loss = loss_fn(y_pred, y) - loss.backward() - optim.step() - - if strategy.is_main_worker(): - print(f"Loss [epoch={epoch}]: {loss.item()}") - - strategy.teardown() - return 123 - - -LAUNCHER = 'torch-elastic' -STRATEGY = 'ddp' -RUN_ID = "my_run_id" -MIN_NODES = 1 -MAX_NODES = 1 -NPROC_PRE_NODE = 4 -MAX_RESTARTS = 2 - -if __name__ == "__main__": - - # Instantiate Launcher Factory - if LAUNCHER == 'torch-elastic': - launcher = TorchElasticLauncher( - rdzv_id=RUN_ID, - nproc_per_node=NPROC_PRE_NODE, - nnodes=f"{MIN_NODES}:{MAX_NODES}", - max_restarts=MAX_RESTARTS - ) - elif LAUNCHER == 'simple-launcher': - launcher = SimpleLauncher( - nproc_per_node=NPROC_PRE_NODE - ) - else: - raise NotImplementedError - - # Instantiate Strategy - if STRATEGY == 'ddp': - if (not torch.cuda.is_available() - or not torch.cuda.device_count() > 1): - raise RuntimeError('Resources unavailable') - - strategy = DDPStrategy(cluster=None, backend='nccl') - elif STRATEGY == 'horovod': - strategy = HorovodStrategy() - else: - raise NotImplementedError - - # CLIENT CODE - # Launch training from launcher - launcher.run(func=trainer_entrypoint_fn, args=("foobar", strategy)) diff --git a/experimental/example_2.py b/experimental/example_2.py deleted file mode 100644 index 14685753..00000000 --- a/experimental/example_2.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -Unified interface for launchers. -Most of the complexity is hidden inside "factory" classes. -""" - -import os - -import torch -from torch import nn -from torch.utils.data import DataLoader, Dataset - -from strategy import Strategy, DDPStrategy, HorovodStrategy -from launcher_factory import ( - LauncherFactory, - SimpleLauncherFactory, - TorchElasticLauncherFactory -) - - -class UniformRndDataset(Dataset): - def __init__(self, x_size: int, y_size: int, len: int = 100): - super().__init__() - self.x_size = x_size - self.y_size = y_size - self.len = len - - def __len__(self): - return self.len - - def __getitem__(self, index): - return torch.rand(self.x_size), torch.rand(self.y_size) - - -def trainer_entrypoint_fn(a, strategy: Strategy): - """Dummy training function.""" - strategy.setup() - print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " - f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") - - # Local model - model = nn.Linear(3, 4) - optim = torch.optim.Adam(model.parameters(), lr=1e-3) - loss_fn = nn.MSELoss() - # Distributed model - model: nn.Module = strategy.distribute_model(model) - optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) - - # Data - train_set = UniformRndDataset(x_size=3, y_size=4) - train_loader = DataLoader(train_set, batch_size=10, num_workers=1) - # Distributed dataloader - train_loader: DataLoader = strategy.distribute_dataloader(train_loader) - - for epoch in range(2): - for (x, y) in train_loader: - # print(f"tensor to cuda:{strategy.device}") - x = x.to(strategy.device) - y = y.to(strategy.device) - - optim.zero_grad() - y_pred = model(x) - loss = loss_fn(y_pred, y) - loss.backward() - optim.step() - - if strategy.is_main_worker(): - print(f"Loss [epoch={epoch}]: {loss.item()}") - - strategy.teardown() - return 123 - - -LAUNCHER = 'torch-elastic' -STRATEGY = 'ddp' -NPROC_PRE_NODE = 4 - -if __name__ == "__main__": - # STRATEGY BUILDER - - # Instantiate Launcher Factory - if LAUNCHER == 'torch-elastic': - launcher_builder: LauncherFactory = TorchElasticLauncherFactory() - elif LAUNCHER == 'simple-launcher': - launcher_builder: LauncherFactory = SimpleLauncherFactory() - else: - raise NotImplementedError - - # Instantiate launcher - launcher = launcher_builder.createLauncher( - n_workers_per_node=NPROC_PRE_NODE - ) - - # Instantiate Strategy - if STRATEGY == 'ddp': - if (not torch.cuda.is_available() - or not torch.cuda.device_count() > 1): - raise RuntimeError('Resources unavailable') - - strategy = DDPStrategy(cluster=None, backend='nccl') - elif STRATEGY == 'horovod': - strategy = HorovodStrategy() - else: - raise NotImplementedError - - # CLIENT CODE - # Launch training from launcher - launcher.run(func=trainer_entrypoint_fn, args=("foobar", strategy)) diff --git a/experimental/example_3.py b/experimental/example_3.py deleted file mode 100644 index d38dd78c..00000000 --- a/experimental/example_3.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Hide the selection of launcher and strategy inside a class. -""" -import os - -import torch -from torch import nn -from torch.utils.data import DataLoader, Dataset - -from strategy import Strategy -from distributed_tools import DistributedTooling - - -class UniformRndDataset(Dataset): - def __init__(self, x_size: int, y_size: int, len: int = 100): - super().__init__() - self.x_size = x_size - self.y_size = y_size - self.len = len - - def __len__(self): - return self.len - - def __getitem__(self, index): - return torch.rand(self.x_size), torch.rand(self.y_size) - - -def trainer_entrypoint_fn(a, strategy: Strategy): - """Dummy training function.""" - strategy.setup() - print(f"{a}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " - f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") - - # Local model - model = nn.Linear(3, 4) - optim = torch.optim.Adam(model.parameters(), lr=1e-3) - loss_fn = nn.MSELoss() - # Distributed model - model: nn.Module = strategy.distribute_model(model) - optim: torch.optim.Optimizer = strategy.distribute_optimizer(optim) - - # Data - train_set = UniformRndDataset(x_size=3, y_size=4) - train_loader = DataLoader(train_set, batch_size=10, num_workers=1) - # Distributed dataloader - train_loader: DataLoader = strategy.distribute_dataloader(train_loader) - - for epoch in range(2): - for (x, y) in train_loader: - # print(f"tensor to cuda:{strategy.device}") - x = x.to(strategy.device) - y = y.to(strategy.device) - - optim.zero_grad() - y_pred = model(x) - loss = loss_fn(y_pred, y) - loss.backward() - optim.step() - - if strategy.is_main_worker(): - print(f"Loss [epoch={epoch}]: {loss.item()}") - - strategy.teardown() - return 123 - - -STRATEGY = 'ddp' -NPROC_PRE_NODE = 4 - - -if __name__ == "__main__": - dist_tools = DistributedTooling(n_workers_per_node=NPROC_PRE_NODE) - launcher, strategy = dist_tools.getTools('ddp') - - # CLIENT CODE - # Launch training from launcher - launcher.run(func=trainer_entrypoint_fn, args=("foobar", strategy)) diff --git a/experimental/launcher.py b/experimental/launcher.py deleted file mode 100644 index d9733b8f..00000000 --- a/experimental/launcher.py +++ /dev/null @@ -1,295 +0,0 @@ -import datetime -import os -import shutil -import abc -import time -import uuid -from typing import Callable, Tuple, Any, Union, List, Optional - -from torch.distributed.elastic.agent.server.local_elastic_agent import ( - LocalElasticAgent -) -from torch.distributed.elastic.agent.server import WorkerSpec -from torch.distributed.elastic.rendezvous.dynamic_rendezvous import ( - DynamicRendezvousHandler -) -from torch.distributed.elastic.rendezvous.c10d_rendezvous_backend import ( - C10dRendezvousBackend -) -from torch.distributed import TCPStore -from torch.distributed.elastic.multiprocessing import Std, start_processes - -from torch.distributed.launcher.api import LaunchConfig, elastic_launch -from torch.distributed.run import config_from_args - -from cluster import ClusterEnvironment, detect_cluster - - -class Launcher(abc.ABC): - cluster: ClusterEnvironment - - @abc.abstractmethod - def run(self, *args) -> Any: - """Launches the distributed execution.""" - - -class DummyTorchElasticLauncher(Launcher): - """Simplified Torch Elastic launcher.""" - - def __init__( - self, - cluster: Optional[ClusterEnvironment] = None, - n_workers_per_node: int = 1, - min_nodes: int = 1, - max_nodes: int = 1, - max_restarts: int = 1 - ) -> None: - super().__init__() - # detect_cluster() is preferred - self.cluster = cluster if cluster is not None else detect_cluster() - print(f"DummyTorchElasticLauncher with cluster '{self.cluster}'") - self.n_workers_per_node = n_workers_per_node - self.min_nodes = min_nodes - self.max_nodes = max_nodes - self.max_restarts = max_restarts - self.run_id = str(time.time()) - - if cluster.creates_processes_externally and n_workers_per_node > 1: - print("WARNING: the cluster may already spawn worker " - "processes for you... Consider setting " - "'n_workers_per_node=1'") - - g_world_size = cluster.num_nodes() * self.n_workers_per_node - - store = TCPStore( - host_name=cluster.main_address, - port=cluster.main_port, # could conflict! - world_size=g_world_size, - is_master=cluster.global_rank() == 0, - timeout=datetime.timedelta(seconds=3) - ) - backend = C10dRendezvousBackend(store, self.run_id) - self.rdzv_handler = DynamicRendezvousHandler.from_backend( - run_id=self.run_id, - store=store, - backend=backend, - min_nodes=self.min_nodes, - max_nodes=self.max_nodes - ) - - def run( - self, - func: Callable, - args: Tuple = (), - redirect: bool = False, - log_dir: str = 'launcher_logs', - tee_ranks: Union[str, int, List[int]] = None - ) -> List[Any]: - """Launches the distributed execution with Torch Elastic.""" - # Suppress all printing to console: - # redirects={0: Std.ALL} # do no print, but save to file. - # linked to Agent's log_dir - redirects = Std.ALL if redirect else Std.NONE - - # Fore back printing to console, while redirecting to file - # tee={0: Std.ALL} reactivates print to console + save to - # log file for RANK 0 - if tee_ranks == 'all': - tee = Std.ALL - elif tee_ranks is None: - tee = Std.NONE - elif isinstance(tee_ranks, int): - tee = {tee_ranks: Std.ALL} - elif isinstance(tee_ranks, list): - # tee_ranks is a list of int - tee = {rnk: Std.ALL for rnk in tee_ranks} - else: - raise ValueError(f"unrecognized 'tee_ranks={tee_ranks}'") - - spec = WorkerSpec( - role="worker", - local_world_size=self.n_workers_per_node, - entrypoint=func, - args=args, - rdzv_handler=self.rdzv_handler, - max_restarts=self.max_restarts, - # monitor_interval=monitor_interval, - redirects=redirects, - tee=tee - ) - - agent = LocalElasticAgent(spec, start_method="spawn", log_dir=log_dir) - # try: - run_result = agent.run() - if run_result.is_failed(): - print(f"worker 0 failed with: {run_result.failures[0]}") - result = None - else: - print(f"worker 0 return value is: {run_result.return_values[0]}") - result = run_result.return_values - # except Exception ex: - # # handle exception - return result - - -class TorchElasticLauncher(Launcher): - """ - Official Torch Elastic launcher. - Does NOT support passing values as environment variables. - - Adapted from: - https://github.com/pytorch/pytorch/blob/main/torch/distributed/run.py - """ - - def __init__( - self, - nnodes: str = '1:1', - nproc_per_node: str = '1', - rdzv_backend: str = 'static', - rdzv_endpoint: str = '', - rdzv_id: str = 'none', - rdzv_conf: str = '', - standalone: bool = False, - max_restarts: int = 0, - monitor_interval: float = 5, - start_method: str = 'spawn', - role: str = 'default', - module: bool = False, - no_python: bool = False, - run_path: bool = False, - log_dir: Optional[str] = None, - redirects: str = '0', - tee: str = '0', - node_rank: int = 0, - master_addr: str = "127.0.0.1", - master_port: int = 29500, - local_addr: Optional[str] = None - ) -> None: - super().__init__() - # emulate CLI args - # TODO: include logic for 'action=check_env' or 'action=env' - self.nnodes = nnodes - self.nproc_per_node = nproc_per_node - self.rdzv_backend = rdzv_backend - self.rdzv_endpoint = rdzv_endpoint - self.rdzv_id = rdzv_id - self.rdzv_conf = rdzv_conf - self.standalone = standalone - self.max_restarts = max_restarts - self.monitor_interval = monitor_interval - self.start_method = start_method - self.role = role - self.module = module - self.no_python = no_python - self.run_path = run_path - self.log_dir = log_dir - self.redirects = redirects - self.tee = tee - self.node_rank = node_rank - self.master_addr = master_addr - self.master_port = master_port - self.local_addr = local_addr - # Placeholders - self.training_script = "placeholder.py" - self.training_script_args = [] - - def config_from_args( - self - ) -> Tuple[LaunchConfig, Union[Callable, str], List[str]]: - return config_from_args(self) - - def run( - self, - func: Callable, - args: Tuple = () - ) -> Any: - if self.standalone: - self.rdzv_backend = "c10d" - self.rdzv_endpoint = "localhost:29400" - self.rdzv_id = str(uuid.uuid4()) - # log.info( - # f"\n**************************************\n" - # f"Rendezvous info:\n" - # f"--rdzv_backend={self.rdzv_backend} " - # f"--rdzv_endpoint={self.rdzv_endpoint} " - # f"--rdzv_id={self.rdzv_id}\n" - # f"**************************************\n" - # ) - - config, _, _ = self.config_from_args() - elastic_launch( - config=config, - entrypoint=func, - )(*args) - - -class SimpleLauncher(Launcher): - """Simple launcher based on multiprocessing. - Use ONLY for single node applications. - """ - - def __init__( - self, - nproc_per_node: int, - run_id: Optional[str] = None, - master_addr: str = "127.0.0.1", - master_port: int = 29500 - ) -> None: - super().__init__() - self.nproc_per_node = nproc_per_node - self.run_id = run_id if run_id is not None else f"RunID:{time.time()}" - self.master_addr = master_addr - self.master_port = master_port - self.log_dir = f'{self.__class__.__name__}_logs' - if os.path.exists(self.log_dir): - shutil.rmtree(self.log_dir) - os.makedirs(self.log_dir) - - def run( - self, - func: Callable, - args: Tuple = () - ) -> Any: - # Adapted from: - # https://pytorch.org/docs/stable/elastic/multiprocessing.html - w_args = {i: args for i in range(self.nproc_per_node)} - # Emulates the env variables set by torch Elastic - w_envs = { - i: dict( - RANK=str(i), - LOCAL_RANK=str(i), - GROUP_RANK=str(0), - ROLE_RANK=str(i), - WORLD_SIZE=str(self.nproc_per_node), - LOCAL_WORLD_SIZE=str(self.nproc_per_node), - ROLE_WORLD_SIZE=str(self.nproc_per_node), - TORCHELASTIC_RUN_ID=str(self.run_id), - MASTER_ADDR=str(self.master_addr), - MASTER_PORT=str(self.master_port) - ) - for i in range(self.nproc_per_node) - } - ctx = start_processes( - name=self.__class__.__name__, - entrypoint=func, - args=w_args, - envs=w_envs, - log_dir=self.log_dir - ) - ctx.wait() - return ctx.return_values - - -class DeepSpeedLauncher(Launcher): - """Official DeepSpeed launcher.""" - - def __init__(self) -> None: - super().__init__() - - def run( - self, - func: Callable, - args: Tuple = () - ) -> Any: - # TODO: complete - raise NotImplementedError diff --git a/experimental/launcher_factory.py b/experimental/launcher_factory.py deleted file mode 100644 index fce12a0c..00000000 --- a/experimental/launcher_factory.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Factories to instantiate Launcher classes. -They introduce a level of indirection to provide a unified interface -for all the launchers. The common interface is provided by the -`createLauncher` factory method. -""" - -from typing import Optional, Dict, Any -import abc - -from launcher import ( - Launcher, - TorchElasticLauncher, - SimpleLauncher, - DeepSpeedLauncher -) -from cluster import detect_cluster - - -class LauncherFactory(abc.ABC): - """ - Factory class to instantiate a Launcher classes. - It introduces a level of indirection to provide a unified interface - for all the launchers. The common interface is provided by the - `createLauncher` factory method. - """ - - def createLauncher( - self, - n_workers_per_node: int, - run_id: Optional[str] = None, - master_addr: Optional[str] = None, - master_port: Optional[int] = None, - **kwargs - ) -> Launcher: - """ - Simplifies the instantiation of a Launcher. - Advanced configuration is pre-computed in the body - of this method, leaving few parameters to the end user. - """ - - -class TorchElasticLauncherFactory(LauncherFactory): - """Factory class to instantiate a TorchElasticLauncher class.""" - - def createLauncher( - self, - n_workers_per_node: int, - run_id: Optional[str] = None, - master_addr: Optional[str] = None, - master_port: Optional[int] = None, - **kwargs - ) -> Launcher: - """ - Simplifies the instantiation of a TorchElasticLauncher. - Advanced configuration is pre-computed in the body - of this method, leaving few parameters to the end user. - """ - cluster = detect_cluster() - - kwargs['nproc_per_node'] = n_workers_per_node - # If given, propagate the args - if run_id: - kwargs['rdzv_id'] = run_id - if master_addr: - kwargs['master_addr'] = master_addr - if master_port: - kwargs['master_port'] = master_port - - # Compute and add TorchElastic specific args, if not - # provided as **kwargs - n_nodes = cluster.num_nodes() - safe_add(kwargs, 'nnodes', f"{n_nodes}:{n_nodes}") - safe_add(kwargs, 'rdzv_id', cluster.job_id()) - is_host_flag = '1' if cluster.node_rank() == 0 else '0' - safe_add(kwargs, 'rdzv_conf', f'is_host={is_host_flag}') - safe_add(kwargs, 'rdzv_backend', 'c10d') - safe_add( - kwargs, - 'rdzv_endpoint', - f'{cluster.main_address}:{cluster.main_port}' - ) - safe_add(kwargs, 'max_restarts', 3) - - return TorchElasticLauncher(**kwargs) - - -class SimpleLauncherFactory(LauncherFactory): - """Factory class to instantiate a SimpleLauncherFactory class.""" - - def createLauncher( - self, - n_workers_per_node: int, - run_id: Optional[str] = None, - master_addr: Optional[str] = None, - master_port: Optional[int] = None, - **kwargs - ) -> Launcher: - """ - Simplifies the instantiation of a SimpleLauncher. - Advanced configuration is pre-computed in the body - of this method, leaving few parameters to the end user. - """ - - kwargs['nproc_per_node'] = n_workers_per_node - # If given, propagate the args - if run_id: - kwargs['run_id'] = run_id - if master_addr: - kwargs['master_addr'] = master_addr - if master_port: - kwargs['master_port'] = master_port - - return SimpleLauncher(**kwargs) - - -class DeepSpeedLauncherFactory(LauncherFactory): - """Factory class to instantiate a DeepSpeedLauncher class.""" - - def createLauncher( - self, - n_workers_per_node: int, - run_id: Optional[str] = None, - master_addr: Optional[str] = None, - master_port: Optional[int] = None, - **kwargs - ) -> Launcher: - """ - Simplifies the instantiation of a DeepSpeedLauncher. - Advanced configuration is pre-computed in the body - of this method, leaving few parameters to the end user. - """ - # TODO: complete - raise NotImplementedError - return DeepSpeedLauncher(...) - - -def safe_add(map: Dict, key: str, value: Any) -> None: - """ - Add a key-value pair to a dict if the key - is not already present. - """ - if map.get(key) is None: - map[key] = value diff --git a/experimental/strategy.py b/experimental/strategy.py deleted file mode 100644 index 59dd7a4f..00000000 --- a/experimental/strategy.py +++ /dev/null @@ -1,150 +0,0 @@ -import os -import abc -from typing import Any, Optional - -import torch -from torch import nn -from torch.nn.parallel import DistributedDataParallel -from torch import optim -from torch.utils.data import DataLoader, DistributedSampler -from torch.distributed import init_process_group - -# from lightning.pytorch.plugins.environments import ClusterEnvironment -from cluster import ClusterEnvironment, detect_cluster - - -class Strategy(abc.ABC): - cluster: ClusterEnvironment - - @property - @abc.abstractmethod - def device(self) -> int: - """Device used by this worker""" - - @abc.abstractmethod - def setup(self) -> None: - """Setup the strategy once in a distributed environment.""" - - @abc.abstractmethod - def teardown(self) -> None: - """Frees the distributed strategy resources.""" - - @abc.abstractmethod - def is_main_worker(self) -> bool: - """Returns True if called from the main process of the pool.""" - - @abc.abstractmethod - def _is_env_setup(self) -> bool: - """Checks whether the distributed environment is correctly setup.""" - - @abc.abstractmethod - def distribute_model(self, model: Any) -> Any: - """Distributes a neural network.""" - - @abc.abstractmethod - def distribute_optimizer(self, optimizer: Any) -> Any: - """Distributes an optimizer.""" - - @abc.abstractmethod - def distribute_dataloader(self, dataloader: Any) -> Any: - """Distributes a dataloader.""" - - -class DDPStrategy(Strategy): - def __init__( - self, - backend: str = 'nccl', - cluster: Optional[ClusterEnvironment] = None - ) -> None: - super().__init__() - self.cluster = cluster - self.backend = backend - - @property - def device(self) -> int: - """Returns the local rank. Assumes one worker per GPU.""" - return self.cluster.local_rank() - - def setup(self, **kwargs) -> None: - """Setup the strategy in a distributed context.""" - if not self._is_env_setup(): - raise RuntimeError( - "Distributed environment not setup correctly. Use a launcher.") - - # detect_cluster() is preferred - if self.cluster is None: - self.cluster = detect_cluster() - print(f"DDPStrategy executed on '{self.cluster}' cluster") - - # Initializes the default distributed process group - # and the distributed package - init_process_group(backend=self.backend) - - def teardown(self) -> None: - torch.distributed.barrier() - torch.distributed.destroy_process_group() - - def _is_env_setup(self) -> bool: - if (os.environ.get('RANK') is not None): - # and torch.distributed.is_available()): - return True - return False - - def is_main_worker(self) -> bool: - return self.cluster.global_rank() == 0 - - def distribute_model(self, model: nn.Module) -> nn.Module: - model = model.to(f"cuda:{self.device}") - return DistributedDataParallel( - model, - device_ids=[self.device], - output_device=self.device - ) - - def distribute_optimizer( - self, - optimizer: optim.Optimizer - ) -> optim.Optimizer: - return optimizer - - def distribute_dataloader( - self, - dataloader: DataLoader, - shuffle: bool = True - ) -> DataLoader: - """Makes a torch DataLoader distributed by substituting its sampler.""" - sampler = DistributedSampler( - dataloader.dataset, - num_replicas=self.cluster.world_size(), - rank=self.cluster.global_rank(), - shuffle=shuffle - ) - # Recreate dataloader, with updated sampler - return DataLoader( - dataloader.dataset, - batch_size=dataloader.batch_size, - sampler=sampler, - num_workers=dataloader.num_workers, - collate_fn=dataloader.collate_fn, - pin_memory=dataloader.pin_memory, - drop_last=dataloader.drop_last, - timeout=dataloader.timeout, - worker_init_fn=dataloader.worker_init_fn, - multiprocessing_context=dataloader.multiprocessing_context, - generator=dataloader.generator, - prefetch_factor=dataloader.prefetch_factor, - persistent_workers=dataloader.persistent_workers, - pin_memory_device=dataloader.pin_memory_device - ) - - -class LocalStrategy(Strategy): - ... - - -class HorovodStrategy(Strategy): - ... - - -class DeepSpeedStrategy(Strategy): - ... diff --git a/experimental/trainer/DS_config.json b/experimental/trainer/DS_config.json deleted file mode 100644 index 544cab17..00000000 --- a/experimental/trainer/DS_config.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "train_micro_batch_size_per_gpu": 32, - "gradient_accumulation_steps": 1, - "optimizer": { - "type": "Adam", - "params": { - "lr": 0.01 - } - }, - "fp16": { - "enabled": false - }, - "zero_optimization": false -} - diff --git a/experimental/trainer/general_startscript b/experimental/trainer/general_startscript deleted file mode 100755 index 455466b4..00000000 --- a/experimental/trainer/general_startscript +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash - -# general configuration of the job -#SBATCH --job-name=TorchTest -#SBATCH --account=intertwin -#SBATCH --mail-user= -#SBATCH --mail-type=ALL -#SBATCH --output=job.out -#SBATCH --error=job.err -#SBATCH --time=00:15:00 - -# configure node and process count on the CM -#SBATCH --partition=batch -#SBATCH --nodes=4 -#SBATCH --ntasks-per-node=1 -#SBATCH --cpus-per-task=32 -#SBATCH --gpus-per-node=4 -#SBATCH --exclusive - -# gres options have to be disabled for deepv -#SBATCH --gres=gpu:4 - -# parallelization strategy (DDP, HVD, DS) -strategy='DS' - -# parameters -debug=false # do debug -bs=32 # batch-size -epochs=1 # epochs -lr=0.01 # learning rate - -# AT -dataDir="/p/scratch/raise-ctp2/data_MNIST/" - -# set modules -ml --force purge - -ml Stages/2022 NVHPC/22.1 ParaStationMPI/5.5.0-1-mt NCCL/2.11.4-CUDA-11.5 cuDNN/8.3.1.22-CUDA-11.5 -ml Python/3.9.6 CMake HDF5 PnetCDF libaio/0.3.112 mpi-settings/CUDA - -# set env -source /p/project/intertwin/rakesh/T6.5-AI-and-ML/dist_trainer/envAI_hdfml/bin/activate - -# sleep a sec -sleep 1 - -# job info -echo "DEBUG: TIME: $(date)" -echo "DEBUG: EXECUTE: $EXEC" -echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" -echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" -echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" -echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" -echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" -echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" -echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" -echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" -echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" -if [ "$debug" = true ] ; then - export NCCL_DEBUG=INFO -fi -echo - -# set comm -export CUDA_VISIBLE_DEVICES="0,1,2,3" -export OMP_NUM_THREADS=1 -if [ "$SLURM_CPUS_PER_TASK" > 0 ] ; then - export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK -fi - -COMMAND="general_trainer_v2.py" - -#launch -if [[ $strategy == *"HVD"* ]]; -then - EXEC="$COMMAND \ - --strategy $strategy \ - --batch-size $bs \ - --epochs $epochs \ - --lr $lr \ - --data-dir $dataDir" - - srun --cpu-bind=none python3 -u $EXEC - -elif [[ $strategy == *"DDP"* ]]; -then - EXEC="$COMMAND \ - --strategy $strategy \ - --batch-size $bs \ - --epochs $epochs \ - --lr $lr \ - --nworker $SLURM_CPUS_PER_TASK \ - --data-dir $dataDir" - - srun --cpu-bind=none bash -c "torchrun \ - --log_dir='logs' \ - --nnodes=$SLURM_NNODES \ - --nproc_per_node=$SLURM_GPUS_PER_NODE \ - --rdzv_id=$SLURM_JOB_ID \ - --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ - --rdzv_backend=c10d \ - --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ - $EXEC" - -else - EXEC="$COMMAND \ - --strategy $strategy \ - --batch-size $bs \ - --epochs $epochs \ - --lr $lr \ - --nworker $SLURM_CPUS_PER_TASK \ - --data-dir $dataDir" - - #### do not change this part - # create node-list - sysN=$(eval "scontrol show hostnames") - for i in $sysN; do - x+=\"$i\":[$CUDA_VISIBLE_DEVICES], - done - WID=`echo {${x::-1}} | base64 -w 0` - - # modify config file with parameters - sed -i "2s|.*| \"train_micro_batch_size_per_gpu\": ${bs},|" DS_config.json - sed -i "7s|.*| \"lr\": ${lr}|" DS_config.json - #### - - # launch - srun python -m deepspeed.launcher.launch \ - --node_rank $SLURM_PROCID \ - --master_addr ${SLURMD_NODENAME}i \ - --master_port 29500 \ - --world_info $WID \ - $EXEC --deepspeed_mpi --deepspeed_config DS_config.json - -fi diff --git a/experimental/trainer/general_trainer.py b/experimental/trainer/general_trainer.py deleted file mode 100755 index 33c21ced..00000000 --- a/experimental/trainer/general_trainer.py +++ /dev/null @@ -1,482 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# author: RS, adapted from https://gitlab.jsc.fz-juelich.de/CoE-RAISE/FZJ/ai4hpc -# version: 211029a - -# std libs -from typing import Any, Union -import argparse -import sys -import os -import time -import numpy as np -import random -import abc - -# ml libs -import deepspeed -import torch -import torch.distributed as dist -import torch.nn as nn -import torch.nn.functional as F -from torchvision import datasets, transforms - -from itwinai.torch.distributed import ( - DDPDistributedStrategy_old, - DSDistributedStrategy_old, - HVDDistributedStrategy_old -) - -# parsed settings - - -def pars_ini(): - global args - parser = argparse.ArgumentParser(description='PyTorch MNIST Example') - - # IO parsers - parser.add_argument('--data-dir', default='./', - help='location of the training dataset in the local filesystem') - parser.add_argument('--restart-int', type=int, default=10, - help='restart interval per epoch (default: 10)') - - # model parsers - parser.add_argument('--strategy', type=str, default='DDP', - help='strategy for parallelization (DDP, HVD, DS)') - parser.add_argument('--batch-size', type=int, default=64, - help='input batch size for training (default: 64)') - parser.add_argument('--epochs', type=int, default=10, - help='number of epochs to train (default: 10)') - parser.add_argument('--lr', type=float, default=0.01, - help='learning rate (default: 0.01)') - parser.add_argument('--concM', type=int, default=100, - help='conc MNIST to this factor (default: 100)') - parser.add_argument('--momentum', type=float, default=0.5, - help='momentum in SGD optimizer (default: 0.5)') - parser.add_argument('--shuff', action='store_true', default=False, - help='shuffle dataset (default: False)') - - # debug parsers - parser.add_argument('--testrun', action='store_true', default=False, - help='do a test run with seed (default: False)') - parser.add_argument('--nseed', type=int, default=0, - help='seed integer for reproducibility (default: 0)') - parser.add_argument('--log-int', type=int, default=10, - help='log interval per training') - - # parallel parsers - parser.add_argument('--backend', type=str, default='nccl', - help='backend for parrallelisation (default: nccl)') - parser.add_argument('--nworker', type=int, default=0, - help='number of workers in DataLoader (default: 0 - only main)') - parser.add_argument('--prefetch', type=int, default=2, - help='prefetch data in DataLoader (default: 2)') - parser.add_argument('--no-cuda', action='store_true', default=False, - help='disables GPGPUs') - parser.add_argument('--local_rank', type=int, default=-1, - help='local rank passed from distributed launcher') - - try: - parser = deepspeed.add_config_arguments(parser) - except: - pass - - args = parser.parse_args() - - -class Net(nn.Module): - def __init__(self): - super(Net, self).__init__() - self.conv1 = nn.Conv2d(1, 10, kernel_size=5) - self.conv2 = nn.Conv2d(10, 20, kernel_size=5) - self.conv2_drop = nn.Dropout2d() - self.fc1 = nn.Linear(320, 50) - self.fc2 = nn.Linear(50, 10) - - def forward(self, x): - x = F.relu(F.max_pool2d(self.conv1(x), 2)) - x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) - x = x.view(-1, 320) - x = F.relu(self.fc1(x)) - x = F.dropout(x, training=self.training) - x = self.fc2(x) - return F.log_softmax(x) - -# train loop - - -def train(model, device, train_loader, optimizer, epoch, grank, gwsize, args): - model.train() - t_list = [] - loss_acc = 0 - if grank == 0: - print("\n") - for batch_idx, (data, target) in enumerate(train_loader): - t = time.perf_counter() - data, target = data.to(device), target.to(device) - optimizer.zero_grad() - output = model(data) - loss = F.nll_loss(output, target) - loss.backward() - optimizer.step() - if batch_idx % args.log_int == 0 and grank == 0: - print( - f'Train epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)/gwsize} ' - f'({100.0 * batch_idx / len(train_loader):.0f}%)]\t\tLoss: {loss.item():.6f}') - t_list.append(time.perf_counter() - t) - loss_acc += loss.item() - if grank == 0: - print('TIMER: train time', sum(t_list) / len(t_list), 's') - return loss_acc - -# test loop - - -def test(model, device, test_loader, grank, gwsize, args): - model.eval() - test_loss = 0 - correct = 0 - with torch.no_grad(): - for data, target in test_loader: - data, target = data.to(device), target.to(device) - output = model(data) - # sum up batch loss - test_loss += F.nll_loss(output, target, reduction="sum").item() - # get the index of the max log-probability - pred = output.argmax(dim=1, keepdim=True) - correct += pred.eq(target.view_as(pred)).sum().item() - test_loss /= len(test_loader.dataset) - if grank == 0: - print( - f'Test set: average loss: {test_loss:.4f}\t' - f'accurate samples: {correct}/{len(test_loader.dataset)/gwsize}') - acc_test = 100.0 * correct * gwsize / len(test_loader.dataset) - return acc_test - - -# save state of the training -def save_state(epoch, distrib_model, loss_acc, optimizer, res_name, grank, gwsize, is_best, my_trainer): - rt = time.time() - # find if is_best happened in any worker - if torch.cuda.is_available(): - is_best_m = my_trainer.par_allgather_obj(is_best, gwsize) - - if torch.cuda.is_available(): - if any(is_best_m): - # find which rank is_best happened - select first rank if multiple - is_best_rank = np.where(np.array(is_best_m) == True)[0][0] - - # collect state - state = {'epoch': epoch + 1, - 'state_dict': distrib_model.state_dict(), - 'best_acc': loss_acc, - 'optimizer': optimizer.state_dict()} - - # write on worker with is_best - if grank == is_best_rank: - torch.save(state, './'+res_name) - print( - f'DEBUG: state in {grank} is saved on epoch:{epoch} in {time.time()-rt} s') - else: - # collect state - state = {'epoch': epoch + 1, - 'state_dict': distrib_model.state_dict(), - 'best_acc': loss_acc, - 'optimizer': optimizer.state_dict()} - - torch.save(state, './'+res_name) - print( - f'DEBUG: state in {grank} is saved on epoch:{epoch} in {time.time()-rt} s') - - -# deterministic dataloader -def seed_worker(worker_id): - worker_seed = torch.initial_seed() % 2**32 - np.random.seed(worker_seed) - random.seed(worker_seed) - - -# -# -# MAIN -# -# -def main(): - # get parse args - pars_ini() - - # check CUDA availibility - args.cuda = not args.no_cuda and torch.cuda.is_available() - - # Strategy for distributed training - if args.strategy == 'DDP': - my_trainer = DDPDistributedStrategy_old() - - elif args.strategy == 'DS': - my_trainer = DSDistributedStrategy_old() - - elif args.strategy == 'HVD': - my_trainer = HVDDistributedStrategy_old() - - # limit # of CPU threads to be used per worker - torch.set_num_threads(1) - - # get directory - program_dir = os.getcwd() - - # start the time.time for profiling - st = time.time() - - # initializes the distributed backend which will take care of sychronizing nodes/GPUs - my_trainer.init_backend(backend=args.backend) - - # deterministic testrun - if args.testrun: - torch.manual_seed(args.nseed) - g = torch.Generator() - g.manual_seed(args.nseed) - - # get job rank info - rank==0 master gpu - if torch.cuda.is_available(): - # local world size - per node - lwsize = my_trainer.local_world_size() if args.cuda else 0 - gwsize = my_trainer.global_world_size() # global world size - per run - grank = my_trainer.dist_grank() # global rank - assign per run - lrank = my_trainer.dist_lrank() # local rank - assign per node - else: - gwsize = 1 - grank = 0 - - # some debug - if grank == 0: - print('TIMER: initialise:', time.time()-st, 's') - print('DEBUG: local ranks:', lwsize, '/ global ranks:', gwsize) - print('DEBUG: sys.version:', sys.version, '\n') - - print('DEBUG: IO parsers:') - print('DEBUG: args.data_dir:', args.data_dir) - print('DEBUG: args.restart_int:', args.restart_int, '\n') - - print('DEBUG: model parsers:') - print('DEBUG: args.batch_size:', args.batch_size) - print('DEBUG: args.epochs:', args.epochs) - print('DEBUG: args.lr:', args.lr) - print('DEBUG: args.concM:', args.concM) - print('DEBUG: args.momentum:', args.momentum) - print('DEBUG: args.shuff:', args.shuff, '\n') - - print('DEBUG: debug parsers:') - print('DEBUG: args.testrun:', args.testrun) - print('DEBUG: args.nseed:', args.nseed) - print('DEBUG: args.log_int:', args.log_int, '\n') - - print('DEBUG: parallel parsers:') - print('DEBUG: args.backend:', args.backend) - print('DEBUG: args.nworker:', args.nworker) - print('DEBUG: args.prefetch:', args.prefetch) - print('DEBUG: args.cuda:', args.cuda, '\n') - - # encapsulate the model on the GPU assigned to the current process - device = torch.device( - 'cuda' if args.cuda and torch.cuda.is_available() else 'cpu', lrank) - if args.cuda: - torch.cuda.set_device(lrank) - # deterministic testrun - if args.testrun: - torch.cuda.manual_seed(args.nseed) - -# read data - data_dir = args.data_dir - mnist_scale = args.concM - largeData = [] - for i in range(mnist_scale): - largeData.append( - datasets.MNIST(data_dir, train=True, download=False, - transform=transforms.Compose([ - transforms.ToTensor(), - transforms.Normalize((0.1307,), (0.3081,)) - ])) - ) - - # concat data - train_dataset = torch.utils.data.ConcatDataset(largeData) - - mnist_scale = args.concM - largeData = [] - for i in range(mnist_scale): - largeData.append( - datasets.MNIST(data_dir, train=False, download=False, - transform=transforms.Compose([ - transforms.ToTensor(), - transforms.Normalize((0.1307,), (0.3081,)) - ])) - ) - - # concat data - test_dataset = torch.utils.data.ConcatDataset(largeData) - - # restricts data loading to a subset of the dataset exclusive to the current process - args.shuff = args.shuff and not args.testrun - if torch.cuda.is_available(): - train_sampler = torch.utils.data.distributed.DistributedSampler( - train_dataset, num_replicas=gwsize, rank=grank, shuffle=args.shuff) - test_sampler = torch.utils.data.distributed.DistributedSampler( - test_dataset, num_replicas=gwsize, rank=grank, shuffle=args.shuff) - -# distribute dataset to workers - # persistent workers is not possible for nworker=0 - pers_w = True if args.nworker > 1 else False - - # deterministic testrun - the same dataset each run - kwargs = {'worker_init_fn': seed_worker, - 'generator': g} if args.testrun else {} - - if torch.cuda.is_available(): - train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=args.batch_size, - sampler=train_sampler, num_workers=args.nworker, pin_memory=True, - persistent_workers=pers_w, prefetch_factor=args.prefetch, **kwargs) - test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=args.batch_size, - sampler=test_sampler, num_workers=args.nworker, pin_memory=True, - persistent_workers=pers_w, prefetch_factor=args.prefetch, **kwargs) - else: - train_loader = torch.utils.data.DataLoader( - train_dataset, batch_size=args.batch_size) - test_loader = torch.utils.data.DataLoader( - test_dataset, batch_size=args.batch_size) - - if grank == 0: - print('TIMER: read and concat data:', time.time()-st, 's') - - # create CNN model - model = Net().to(device) - - # distribute model to workers - distrib_model = my_trainer.distribute_model(model, device) - - # optimizer - optimizer = torch.optim.SGD( - distrib_model.parameters(), lr=args.lr, momentum=args.momentum) - - my_trainer.broadcast_params(distrib_model, optimizer) - - optimizer = my_trainer.distribute_optimizer(optimizer, distrib_model) - -# resume state - start_epoch = 1 - best_acc = np.Inf - res_name = 'checkpoint.pth.tar' - if os.path.isfile(res_name): - try: - if torch.cuda.is_available(): - dist.barrier() - # Map model to be loaded to specified single gpu. - loc = {'cuda:%d' % 0: 'cuda:%d' % lrank} if args.cuda else { - 'cpu:%d' % 0: 'cpu:%d' % lrank} - checkpoint = torch.load( - program_dir+'/'+res_name, map_location=loc) - else: - checkpoint = torch.load(program_dir+'/'+res_name) - start_epoch = checkpoint['epoch'] - best_acc = checkpoint['best_acc'] - distrib_model.load_state_dict(checkpoint['state_dict']) - optimizer.load_state_dict(checkpoint['optimizer']) - if torch.cuda.is_available(): - if grank == 0: - print(f'WARNING: restarting from {start_epoch} epoch') - else: - print(f'WARNING: restarting from {start_epoch} epoch') - except: - if torch.cuda.is_available(): - if grank == 0: - print(f'WARNING: restart file cannot be loaded, restarting!') - else: - print(f'WARNING: restart file cannot be loaded, restarting!') - - if start_epoch > args.epochs: - if torch.cuda.is_available(): - if grank == 0: - print(f'WARNING: given epochs are less than the one in the restart file!\n' - f'WARNING: SYS.EXIT is issued') - - my_trainer.clean_up() - sys.exit() - else: - print(f'WARNING: given epochs are less than the one in the restart file!\n' - f'WARNING: SYS.EXIT is issued') - sys.exit() - -# start trainin/testing loop - if grank == 0: - print('TIMER: broadcast:', time.time()-st, 's') - print(f'\nDEBUG: start training') - print(f'--------------------------------------------------------') - - et = time.time() - for epoch in range(start_epoch, args.epochs + 1): - lt = time.time() - # training - loss_acc = train(distrib_model, device, train_loader, - optimizer, epoch, grank, gwsize, args) - - # testing - acc_test = test(distrib_model, device, - test_loader, grank, gwsize, args) - - # save first epoch timer - if epoch == start_epoch: - first_ep_t = time.time()-lt - - # final epoch - if epoch + 1 == args.epochs: - train_loader.last_epoch = True - test_loader.last_epoch = True - - if grank == 0: - print('TIMER: epoch time:', time.time()-lt, 's') - print('DEBUG: accuracy:', acc_test, '%') - - # save state if found a better state - is_best = loss_acc < best_acc - if epoch % args.restart_int == 0: - save_state(epoch, distrib_model, loss_acc, optimizer, - res_name, grank, gwsize, is_best, my_trainer) - # reset best_acc - best_acc = min(loss_acc, best_acc) - -# finalise - # save final state - save_state(epoch, distrib_model, loss_acc, optimizer, - res_name, grank, gwsize, True, my_trainer) - # if torch.cuda.is_available(): - # dist.barrier() - - # some debug - if grank == 0: - print(f'\n--------------------------------------------------------') - print('DEBUG: training results:\n') - print('TIMER: first epoch time:', first_ep_t, ' s') - print('TIMER: last epoch time:', time.time()-lt, ' s') - print('TIMER: average epoch time:', (time.time()-et)/args.epochs, ' s') - print('TIMER: total epoch time:', time.time()-et, ' s') - if epoch > 1: - print('TIMER: total epoch-1 time:', - time.time()-et-first_ep_t, ' s') - print('TIMER: average epoch-1 time:', - (time.time()-et-first_ep_t)/(args.epochs-1), ' s') - print('DEBUG: last accuracy:', acc_test, '%') - print('DEBUG: memory req:', int(torch.cuda.memory_reserved(lrank)/1024/1024), 'MB') \ - if args.cuda else 'DEBUG: memory req: - MB' - print('DEBUG: memory summary:\n\n', - torch.cuda.memory_summary(0)) if args.cuda else '' - - if grank == 0: - print(f'TIMER: final time: {time.time()-st} s\n') - - my_trainer.clean_up() - - -if __name__ == "__main__": - main() - sys.exit() - -# eof diff --git a/experimental/workflow/train.yaml b/experimental/workflow/train.yaml deleted file mode 100644 index c21d4141..00000000 --- a/experimental/workflow/train.yaml +++ /dev/null @@ -1,53 +0,0 @@ -# AI workflow metadata/header. -# They are optional and easily extensible in the future. -version: 0.0.1 -name: Experiment name -description: This is a textual description -credits: - - author1 - - author2 - -# Provide a unified place where this *template* can be configured. -# Variables which can be overridden at runtime as env vars, e.g.: -# - Execution environment details (e.g., path in container vs. in laptop, MLFlow tracking URI) -# - Tunable parameters (e.g., learning rate) -# - Intrinsically dynamic values (e.g., MLFLow run ID is a random value) -# These variables are interpolated with OmegaConf. -vars: - images_dataset_path: some/path/disk - mlflow_tracking_uri: http://localhost:5000 - training_lr: 0.001 - -# Runner-independent workflow steps. -# Each step is designed to be minimal, but easily extensible -# to accommodate future needs by adding new fields. -# The only required field is 'command'. New fields can be added -# to support future workflow executors. -steps: - preprocessing-step: - command: - class_path: itwinai.torch.Preprocessor - init_args: - save_path: ${vars.images_dataset_path} - after: null - env: null - - training-step: - command: - class_path: itwinai.torch.Trainer - init_args: - lr: ${vars.training_lr} - tracking_uri: ${vars.mlflow_tracking_uri} - after: preprocessing-step - env: null - - sth_step: - command: python inference.py -p pipeline.yaml - after: [preprocessing-step, training-step] - env: docker+ghcr.io/intertwin-eu/itwinai:training-0.0.1 - - sth_step2: - command: python train.py -p pipeline.yaml - after: null - env: conda+path/to/my/local/env - diff --git a/src/itwinai/experimental/executors.py b/src/itwinai/experimental/executors.py deleted file mode 100644 index 2c89f1c3..00000000 --- a/src/itwinai/experimental/executors.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Executors to execute a sequence of executable steps.""" - -from typing import Any, Dict, Iterable -from abc import abstractmethod - -import yaml -import ray -from ray import air, tune -from jsonargparse import ArgumentParser - -from ..components import Pipeline, BaseComponent -from ..utils import parse_pipe_config - - -class LocalExecutor(Pipeline): - def __init__(self, pipeline, class_dict): - # Create parser for the pipeline (ordered) - pipe_parser = ArgumentParser() - for k, v in class_dict.items(): - pipe_parser.add_subclass_arguments(v, k) - - # Parse, Instantiate pipe - if isinstance(pipeline, str): - parsed = parse_pipe_config(pipeline, pipe_parser) - elif isinstance(pipeline, dict): - parsed = pipe_parser.parse_object(pipeline) - else: - raise "Type of pipeline is not supported" - - pipe = pipe_parser.instantiate_classes(parsed) - # Make pipe as a list - self.pipe = [getattr(pipe, arg) for arg in vars(pipe)] - - def execute(self, args): - for executable in self.pipe: - args = executable.execute(args) - - def setup(self, args): - for executable in self.pipe: - args = executable.setup(args) - - -class RayExecutor(Pipeline): - def __init__(self, pipeline, class_dict, param_space): - self.class_dict = class_dict - self.param_space = param_space - - # Read pipeline as yaml - with open(pipeline, 'r') as f: - self.pipeline = yaml.safe_load(f) - - # Init ray - ray.init(ignore_reinit_error=True) - print('Ray is initialized') - - def worker_fn(self, config, pipeline, class_dict): - # Should have same structure pipe and params - def replace(pipe, params): - for param in params: - if not isinstance(pipe[param], dict): - pipe[param] = params[param] - else: - replace(pipe[param], params[param]) - return pipe - - doc = replace(pipeline, config) - - executor = LocalExecutor(doc, class_dict) - executor.setup(None) - executor.execute(None) - - def execute(self, args): - print('Execute') - tuner = tune.Tuner( - trainable=tune.with_parameters( - self.worker_fn, - pipeline=self.pipeline, - class_dict=self.class_dict - ), - param_space=self.param_space, - run_config=air.RunConfig(name="tune_run") - ) - results = tuner.fit() - print( - "Best hyperparameters found were: " - f"{results.get_best_result().config}" - ) - - # Setup is done per worker via Tune execution - def setup(self, args): - pass - - -class ParallelExecutor(Pipeline): - """Execute a pipeline in parallel: multiprocessing and multi-node.""" - - def __init__(self, steps: Iterable[BaseComponent]): - super().__init__(steps) - - def setup(self, config: Dict = None): - return super().setup(config) - - def execute(self, args: Any = None): - return super().execute(args) - - -class HPCExecutor(ParallelExecutor): - """Execute a pipeline on an HPC system. - This executor provides as additional `setup_on_login` method - to allow for specific setup operations to be carried out on - the login node of a GPU cluster, being the only one with - network access. - """ - - def __init__(self, steps: Iterable[BaseComponent]): - super().__init__(steps) - - def setup(self, config: Dict = None): - return super().setup(config) - - @abstractmethod - def setup_on_login(self, config: Dict = None): - """Access the network to download datasets and misc.""" - raise NotImplementedError - - def execute(self, args: Any = None): - return super().execute(args) From fb12fca682905c614f90ab208448f615283a50ca Mon Sep 17 00:00:00 2001 From: Matteo Bunino <48362942+matbun@users.noreply.github.com> Date: Mon, 29 Apr 2024 22:05:53 +0200 Subject: [PATCH 7/8] Docs dev (#132) * commiting docs functionality for testing deployment * adding documentation deployment relevant files * updating readthedocs.yaml * changing directory of requirements.txt * updating reqs file * commiting changes and adding pages for tutorials * fixed distributed trainer in cyclones use case * adding installation instructions in docs * adding latest changes to docs * adding new pages for itwinai modules and other modifications * modified src/itwinai/torch directory name to solve namespace conflict * fixing tutorial sections * fixes in pages appearance * fixing rendering bugs * fixing pages appearance bugs * adding latest modifications * Deleted duplicate folder after renaming src/itwinai/torch * adding documentation.yml file for automatic updating on github pages * modifying documentation.yml file * updating reqs file to solve bug in deployment * commiting docs functionality for testing deployment * adding documentation deployment relevant files * updating readthedocs.yaml * changing directory of requirements.txt * updating reqs file * commiting changes and adding pages for tutorials * adding installation instructions in docs * adding latest changes to docs * adding new pages for itwinai modules and other modifications * modified src/itwinai/torch directory name to solve namespace conflict * fixing tutorial sections * fixes in pages appearance * fixing rendering bugs * fixing pages appearance bugs * adding latest modifications * Deleted duplicate folder after renaming src/itwinai/torch * adding documentation.yml file for automatic updating on github pages * modifying documentation.yml file * updating reqs file to solve bug in deployment * testing automated docs update * updating getting started page * fixing pages and adding new content * bug fixes * fixing content rendering * latest fixes in rendering * Add version feature to docs * Update .readthedocs.yaml * fixing display structure in getting started page * new fixes similar to previous commit * Update index.rst * Update index.rst Text re-edit index * Update index.rst change 1 word * Update .readthedocs.yaml * Update .readthedocs.yaml * fixing getting started page * Text review getting_started_with_itwinai.rst * Update 3dgan_doc.rst * Update getting_started_with_itwinai.rst punctuation * Fix torch naming problem --------- Co-authored-by: KalliopiTsolaki Co-authored-by: zoechbauer1 Co-authored-by: VerderK <167095399+VerderK@users.noreply.github.com> --- .github/workflows/documentation.yml | 38 ++ .readthedocs.yaml | 32 ++ docs/.gitignore | 10 - docs/3dgan_doc.rst | 124 ++++ docs/404.html | 12 - docs/Gemfile | 7 - docs/Gemfile.lock | 79 --- docs/LICENSE | 21 - docs/Makefile | 20 + docs/README.md | 181 ------ docs/_config.yml | 26 - docs/advanced_workflow.rst | 19 + docs/basic_comp.rst | 16 + docs/basic_workflow.rst | 19 + docs/conf.py | 73 +++ docs/docs/CLI.md | 53 -- docs/docs/Concepts.md | 35 -- docs/docs/How-to-use-this-software.md | 540 ------------------ docs/docs/img/Workflow DAG concept.png | Bin 224511 -> 0 bytes docs/docs/img/cwl-workflow.png | Bin 375527 -> 0 bytes .../img/user-platform interaction full.png | Bin 211421 -> 0 bytes docs/docs/img/user-platform interaction.png | Bin 30005 -> 0 bytes docs/docs/use-cases/index.md | 12 - docs/docs/use-cases/mnist.md | 185 ------ docs/favicon.ico | Bin 15406 -> 0 bytes docs/getting_started_with_itwinai.rst | 190 ++++++ docs/hpc_setup.rst | 69 +++ docs/index.md | 64 --- docs/index.rst | 70 +++ docs/intermediate_workflow.rst | 20 + docs/itwinai.cli.rst | 7 + docs/itwinai.cluster.rst | 7 + docs/itwinai.components.rst | 8 + docs/itwinai.loggers.rst | 8 + docs/itwinai.parser.rst | 8 + docs/itwinai.pipeline.rst | 8 + docs/itwinai.serialization.rst | 8 + docs/itwinai.tf.modules.rst | 26 + docs/itwinai.torch.modules.rst | 72 +++ docs/itwinai.types.rst | 8 + docs/itwinai.utils.rst | 8 + docs/local_setup.rst | 210 +++++++ docs/make.bat | 35 ++ docs/mnist_doc.rst | 151 +++++ docs/modules.rst | 23 + docs/requirements.txt | 185 ++++++ docs/tutorials.rst | 19 + docs/use_cases.rst | 32 ++ use-cases/3dgan/__init__.py | 1 + 49 files changed, 1514 insertions(+), 1225 deletions(-) create mode 100644 .github/workflows/documentation.yml create mode 100644 .readthedocs.yaml delete mode 100644 docs/.gitignore create mode 100644 docs/3dgan_doc.rst delete mode 100644 docs/404.html delete mode 100644 docs/Gemfile delete mode 100644 docs/Gemfile.lock delete mode 100644 docs/LICENSE create mode 100644 docs/Makefile delete mode 100644 docs/README.md delete mode 100644 docs/_config.yml create mode 100644 docs/advanced_workflow.rst create mode 100644 docs/basic_comp.rst create mode 100644 docs/basic_workflow.rst create mode 100644 docs/conf.py delete mode 100644 docs/docs/CLI.md delete mode 100644 docs/docs/Concepts.md delete mode 100644 docs/docs/How-to-use-this-software.md delete mode 100644 docs/docs/img/Workflow DAG concept.png delete mode 100644 docs/docs/img/cwl-workflow.png delete mode 100644 docs/docs/img/user-platform interaction full.png delete mode 100644 docs/docs/img/user-platform interaction.png delete mode 100644 docs/docs/use-cases/index.md delete mode 100644 docs/docs/use-cases/mnist.md delete mode 100644 docs/favicon.ico create mode 100644 docs/getting_started_with_itwinai.rst create mode 100644 docs/hpc_setup.rst delete mode 100644 docs/index.md create mode 100644 docs/index.rst create mode 100644 docs/intermediate_workflow.rst create mode 100644 docs/itwinai.cli.rst create mode 100644 docs/itwinai.cluster.rst create mode 100644 docs/itwinai.components.rst create mode 100644 docs/itwinai.loggers.rst create mode 100644 docs/itwinai.parser.rst create mode 100644 docs/itwinai.pipeline.rst create mode 100644 docs/itwinai.serialization.rst create mode 100644 docs/itwinai.tf.modules.rst create mode 100644 docs/itwinai.torch.modules.rst create mode 100644 docs/itwinai.types.rst create mode 100644 docs/itwinai.utils.rst create mode 100644 docs/local_setup.rst create mode 100644 docs/make.bat create mode 100644 docs/mnist_doc.rst create mode 100644 docs/modules.rst create mode 100644 docs/requirements.txt create mode 100644 docs/tutorials.rst create mode 100644 docs/use_cases.rst create mode 100644 use-cases/3dgan/__init__.py diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 00000000..aeb26d82 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,38 @@ +name: Build and Deploy Sphinx Documentation + +# on: +# push: +# branches: +# - docs_dev +# tags: +# - 'v*' + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Install dependencies + run: | + pip install -r docs/requirements.txt + pip install sphinx sphinx-rtd-theme + + - name: Build documentation + run: | + cd docs + make html + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/_build/html diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..790af042 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,32 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.10" + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index f55f6395..00000000 --- a/docs/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Copied from https://github.com/github/gitignore/blob/main/Jekyll.gitignore -# Ignore metadata generated by Jekyll -_site/ -.sass-cache/ -.jekyll-cache/ -.jekyll-metadata - -# Ignore folders generated by Bundler -.bundle/ -vendor/ diff --git a/docs/3dgan_doc.rst b/docs/3dgan_doc.rst new file mode 100644 index 00000000..1b8added --- /dev/null +++ b/docs/3dgan_doc.rst @@ -0,0 +1,124 @@ +3DGAN +===== + +This section covers the CERN use case that utilizes the `torch-lightning` framework for training and evaluation. The following files are integral to this use case: + +itwinai x 3DGAN +--------------- + + +.. toctree:: + :maxdepth: 5 + + +model.py +++++++++ + +.. literalinclude:: ../use-cases/3dgan/model.py + :language: python + + +trainer.py +++++++++++ +.. literalinclude:: ../use-cases/3dgan/trainer.py + :language: python + + +saver.py +++++++++ + +.. literalinclude:: ../use-cases/3dgan/saver.py + :language: python + + +dataloader.py ++++++++++++++ + +.. literalinclude:: ../use-cases/3dgan/dataloader.py + :language: python + + +cern-pipeline.yaml +++++++++++++++++++ + +This YAML file defines the pipeline configuration for the CERN use case. + +.. literalinclude:: ../use-cases/3dgan/cern-pipeline.yaml + :language: yaml + + +inference-pipeline.yaml ++++++++++++++++++++++++ + +This YAML file defines the pipeline configuration for the CERN use case inference. + +.. literalinclude:: ../use-cases/3dgan/inference-pipeline.yaml + :language: yaml + + +Dockerfile +++++++++++ + +.. literalinclude:: ../use-cases/3dgan/Dockerfile + :language: bash + + +pipeline.yaml ++++++++++++++ + +This YAML file defines the pipeline configuration for the CERN use case. It includes settings for the model, training, and evaluation. + +.. literalinclude:: ../use-cases/3dgan/pipeline.yaml + :language: yaml + + + +This section covers the CERN use case integration with `interLink `_ using ``itwinai``. The following files are integral to this use case: + +interLink x 3DGAN +----------------- + +.. toctree:: + :maxdepth: 5 + + +3dgan-inference-cpu.yaml +++++++++++++++++++++++++ + +.. literalinclude:: ../use-cases/3dgan/interLink/3dgan-inference-cpu.yaml + :language: yaml + + +3dgan-inference.yaml +++++++++++++++++++++++++ + +.. literalinclude:: ../use-cases/3dgan/interLink/3dgan-inference.yaml + :language: yaml + + + + +.. .. automodule:: 3dgan.model +.. :members: +.. :undoc-members: +.. :show-inheritance: + +.. .. automodule:: 3dgan.train +.. :members: +.. :undoc-members: +.. :show-inheritance: + +.. .. automodule:: 3dgan.trainer +.. :members: +.. :undoc-members: +.. :show-inheritance: + +.. .. automodule:: 3dgan.saver +.. :members: +.. :undoc-members: +.. :show-inheritance: + +.. .. automodule:: 3dgan.dataloader +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/docs/404.html b/docs/404.html deleted file mode 100644 index b8547546..00000000 --- a/docs/404.html +++ /dev/null @@ -1,12 +0,0 @@ ---- -layout: default -title: 404 -permalink: /404 -nav_exclude: true -search_exclude: true ---- - -

Page not found

- -

The page you requested could not be found. Try using the navigation {% if site.search_enabled != false %}or search {% - endif %}to find what you're looking for or go to this site's home page.

\ No newline at end of file diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index 387154f8..00000000 --- a/docs/Gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source 'https://rubygems.org' - -gem "jekyll", "~> 4.3" # installed by `gem jekyll` -# gem "webrick" # required when using Ruby >= 3 and Jekyll <= 4.2.2 - -gem "just-the-docs", "0.5.0" # pinned to the current release -# gem "just-the-docs" # always download the latest release diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock deleted file mode 100644 index 81efc419..00000000 --- a/docs/Gemfile.lock +++ /dev/null @@ -1,79 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) - colorator (1.1.0) - concurrent-ruby (1.1.10) - em-websocket (0.5.3) - eventmachine (>= 0.12.9) - http_parser.rb (~> 0) - eventmachine (1.2.7) - ffi (1.15.5) - forwardable-extended (2.6.0) - http_parser.rb (0.8.0) - i18n (1.12.0) - concurrent-ruby (~> 1.0) - jekyll (4.3.0) - addressable (~> 2.4) - colorator (~> 1.0) - em-websocket (~> 0.5) - i18n (~> 1.0) - jekyll-sass-converter (>= 2.0, < 4.0) - jekyll-watch (~> 2.0) - kramdown (~> 2.3, >= 2.3.1) - kramdown-parser-gfm (~> 1.0) - liquid (~> 4.0) - mercenary (>= 0.3.6, < 0.5) - pathutil (~> 0.9) - rouge (>= 3.0, < 5.0) - safe_yaml (~> 1.0) - terminal-table (>= 1.8, < 4.0) - webrick (~> 1.7) - jekyll-sass-converter (2.2.0) - sassc (> 2.0.1, < 3.0) - jekyll-seo-tag (2.8.0) - jekyll (>= 3.8, < 5.0) - jekyll-watch (2.2.1) - listen (~> 3.0) - just-the-docs (0.5.0) - jekyll (>= 3.8.5) - jekyll-seo-tag (>= 2.0) - rake (>= 12.3.1) - kramdown (2.4.0) - rexml - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - liquid (4.0.3) - listen (3.7.1) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) - mercenary (0.4.0) - pathutil (0.16.2) - forwardable-extended (~> 2.6) - public_suffix (5.0.0) - rake (13.0.6) - rb-fsevent (0.11.2) - rb-inotify (0.10.1) - ffi (~> 1.0) - rexml (3.2.5) - rouge (4.0.0) - safe_yaml (1.0.5) - sassc (2.4.0) - ffi (~> 1.9) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - unicode-display_width (2.3.0) - webrick (1.7.0) - -PLATFORMS - arm64-darwin-21 - x86_64-darwin-19 - x86_64-linux - -DEPENDENCIES - jekyll (~> 4.3) - just-the-docs (= 0.5.0) - -BUNDLED WITH - 2.3.9 diff --git a/docs/LICENSE b/docs/LICENSE deleted file mode 100644 index 7d510d02..00000000 --- a/docs/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 just-the-docs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 4a405acd..00000000 --- a/docs/README.md +++ /dev/null @@ -1,181 +0,0 @@ -# just-the-docs-template - -This is a *bare-minimum* template to create a [Jekyll] site that: - -- uses the [Just the Docs] theme; -- can be built and published on [GitHub Pages]; -- can be built and previewed locally, and published on other platforms. - -More specifically, the created site: - -- uses a gem-based approach, i.e. uses a `Gemfile` and loads the `just-the-docs` gem; -- uses the [GitHub Pages / Actions workflow] to build and publish the site on GitHub Pages. - -To get started with creating a site, just click "[use this template]"! - -If you want to maintain your docs in the `docs` directory of an existing project repository, see -[Hosting your docs from an existing project repository](#hosting-your-docs-from-an-existing-project-repository). - -After completing the creation of your new site on GitHub, update it as needed: - -## Replace the content of the template pages - -Update the following files to your own content: - -- `index.md` (your new home page) -- `README.md` (information for those who access your site repository on GitHub) - -## Changing the version of the theme and/or Jekyll - -Simply edit the relevant line(s) in the `Gemfile`. - -## Adding a plugin - -The Just the Docs theme automatically includes the [`jekyll-seo-tag`] plugin. - -To add an extra plugin, you need to add it in the `Gemfile` *and* in `_config.yml`. For example, to add [`jekyll-default-layout`]: - -- Add the following to your site's `Gemfile`: - - ```ruby - gem "jekyll-default-layout" - ``` - -- And add the following to your site's `_config.yml`: - - ```yaml - plugins: - - jekyll-default-layout - ``` - -Note: If you are using a Jekyll version less than 3.5.0, use the `gems` key instead of `plugins`. - -## Publishing your site on GitHub Pages - -1. If your created site is `YOUR-USERNAME/YOUR-SITE-NAME`, update `_config.yml` to: - - ```yaml - title: YOUR TITLE - description: YOUR DESCRIPTION - theme: just-the-docs - - url: https://YOUR-USERNAME.github.io/YOUR-SITE-NAME - - aux_links: # remove if you don't want this link to appear on your pages - Template Repository: https://github.com/YOUR-USERNAME/YOUR-SITE-NAME - ``` - -2. Push your updated `_config.yml` to your site on GitHub. - -3. In your newly created repository on GitHub: - - go to the `Settings` tab -> `Pages` -> `Build and deployment`, then select `Source`: `GitHub Actions`. - - if there were any failed Actions, go to the `Actions` tab and click on `Re-run jobs`. - -## Building and previewing your site locally - -Assuming [Jekyll] and [Bundler] are installed on your computer: - -1. Change your working directory to the root directory of your site. - -2. Run `bundle install`. - -3. Run `bundle exec jekyll serve` to build your site and preview it at `localhost:4000`. - - The built site is stored in the directory `_site`. - -## Publishing your built site on a different platform - -Just upload all the files in the directory `_site`. - -## Customization - -You're free to customize sites that you create with this template, however you like! - -[Browse our documentation][Just the Docs] to learn more about how to use this theme. - -## Hosting your docs from an existing project repository - -You might want to maintain your docs in an existing project repository. Instead of creating a new repository using -the [just-the-docs template](https://github.com/just-the-docs/just-the-docs-template), you can copy the template -files into your existing repository and configure the template's GitHub Actions workflow to -build from a `docs` directory. You can clone the template to your local machine or download the `.zip` file -to access the files. - -### Copy the template files - -1. Create a `.github/workflows` directory at your project root if your repository doesn't already have one. -Copy the `pages.yml` file into this directory. GitHub Actions searches this directory for workflow files. - -2. Create a `docs` directory at your project root and copy all remaining template files into this directory. - -### Modify the GitHub Actions workflow - -The GitHub Actions workflow that builds and deploys your site to GitHub Pages is defined by the `pages.yml` file. -You'll need to edit this file to that so that your build and deploy steps look to your `docs` directory, -rather than the project root. - -1. Set the default `working-directory` param for the build job. - - ```yaml - build: - runs-on: ubuntu-latest - defaults: - run: - working-directory: docs - ``` - -2. Set the `working-directory` param for the Setup Ruby step. - - ```yaml - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.1' - bundler-cache: true - cache-version: 0 - working-directory: '${{ github.workspace }}/docs' - ``` - -3. Set the path param for the Upload artifact step: - - ```yaml - - name: Upload artifact - uses: actions/upload-pages-artifact@v1 - with: - path: "docs/_site/" - ``` - -4. Modify the trigger so that only changes within the `docs` directory start the workflow. -Otherwise, every change to your project (even those that don't affect the docs) would trigger a new site build and deploy. - - ```yaml - on: - push: - branches: - - "main" - paths: - - "docs/**" - ``` - -## Licensing and Attribution - -This repository is licensed under the [MIT License]. You are generally free to reuse or extend upon this code as you -see fit; just include the original copy of the license (which is preserved when you "make a template"). -While it's not necessary, we'd love to hear from you if you do use this template, and how we can improve it for future use! - -The deployment GitHub Actions workflow is heavily based on GitHub's mixed-party [starter workflows]. -A copy of their MIT License is available in [actions/starter-workflows]. - ----- - -[Jekyll]: https://jekyllrb.com -[Just the Docs]: https://just-the-docs.github.io/just-the-docs/ -[GitHub Pages]: https://docs.github.com/en/pages -[GitHub Pages / Actions workflow]: https://github.blog/changelog/2022-07-27-github-pages-custom-github-actions-workflows-beta/ -[Bundler]: https://bundler.io -[use this template]: https://github.com/just-the-docs/just-the-docs-template -[`jekyll-default-layout`]: https://github.com/benbalter/jekyll-default-layout -[`jekyll-seo-tag`]: https://jekyll.github.io/jekyll-seo-tag -[MIT License]: https://en.wikipedia.org/wiki/MIT_License -[starter workflows]: https://github.com/actions/starter-workflows/blob/main/pages/jekyll.yml -[actions/starter-workflows]: https://github.com/actions/starter-workflows/blob/main/LICENSE diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index ba7498ac..00000000 --- a/docs/_config.yml +++ /dev/null @@ -1,26 +0,0 @@ -title: itwinai -description: Docs for task T6.5 prototype of interTwin project -theme: just-the-docs - -url: https://interTwin-eu.github.io/T6.5-AI-and-ML - -aux_links: - Template Repository: https://github.com/interTwin-eu/T6.5-AI-and-ML - -favicon_ico: "favicon.ico" - -nav_external_links: - - title: itwinai on GitHub - url: https://github.com/interTwin-eu/T6.5-AI-and-ML - hide_icon: false # set to true to hide the external link icon - defaults to false - - title: interTwin on GitHub - url: https://github.com/interTwin-eu/ - hide_icon: false # set to true to hide the external link icon - defaults to false - - title: interTwin project - url: https://www.intertwin.eu/ - hide_icon: false # set to true to hide the external link icon - defaults to false - -mermaid: - # Version of mermaid library - # Pick an available version from https://cdn.jsdelivr.net/npm/mermaid/ - version: "10.1.0" diff --git a/docs/advanced_workflow.rst b/docs/advanced_workflow.rst new file mode 100644 index 00000000..121d3acc --- /dev/null +++ b/docs/advanced_workflow.rst @@ -0,0 +1,19 @@ +Advanced workflow +================= + +tutorial_2_advanced_workflow.py ++++++++++++++++++++++++++++++++ + +The `tutorial_2_advanced_workflow.py` script is ... + +.. .. literalinclude:: ../use-cases/mnist/torch-lightning/dataloader.py +.. :language: python + +.. automodule:: tutorial_2_advanced_workflow + :members: + :undoc-members: + :show-inheritance: + + +.. literalinclude:: ../tutorials/ml-workflows/tutorial_2_advanced_workflow.py + :language: python diff --git a/docs/basic_comp.rst b/docs/basic_comp.rst new file mode 100644 index 00000000..5acfe66d --- /dev/null +++ b/docs/basic_comp.rst @@ -0,0 +1,16 @@ +Basic components +================ + +basic_components.py ++++++++++++++++++++ + +The `basic_components.py` script is ... + +.. .. literalinclude:: ../use-cases/mnist/torch-lightning/dataloader.py +.. :language: python + +.. automodule:: basic_components + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/basic_workflow.rst b/docs/basic_workflow.rst new file mode 100644 index 00000000..cd77d328 --- /dev/null +++ b/docs/basic_workflow.rst @@ -0,0 +1,19 @@ +Basic workflow +============== + +tutorial_0_basic_workflow.py +++++++++++++++++++++++++++++ + +The `tutorial_0_basic_workflow.py` script is ... + +.. .. literalinclude:: ../use-cases/mnist/torch-lightning/dataloader.py +.. :language: python + +.. .. automodule:: tutorial_0_basic_workflow +.. :members: +.. :undoc-members: +.. :show-inheritance: + +.. literalinclude:: ../tutorials/ml-workflows/tutorial_0_basic_workflow.py + :language: python + diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..f4c9b297 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,73 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +import os +import sys +import subprocess + +sys.path.insert(0, os.path.abspath('../use-cases/')) +sys.path.insert(0, os.path.abspath('../use-cases/3dgan/')) +sys.path.insert(0, os.path.abspath('../use-cases/mnist/torch-lightning/')) +sys.path.insert(0, os.path.abspath('../use-cases/mnist/torch/')) +sys.path.insert(0, os.path.abspath('../tutorials/ml-workflows/')) +sys.path.insert(0, os.path.abspath('../src/itwinai')) +sys.path.insert(0, os.path.abspath('../src/itwinai/tensorflow')) +sys.path.insert(0, os.path.abspath('../src/itwinai/torch')) +sys.path.insert(0, os.path.abspath('../')) + +project = 'itwinai' +copyright = '2024, Matteo Bunino, Alexander Zoechbauer, Kalliopi Tsolaki, Rakesh Sarma on behalf of CERN & JSC' +author = 'Matteo Bunino, Alexander Zoechbauer, Kalliopi Tsolaki' +# version = '0.0' # short version +# release = '0.0.2' # full version + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', + 'sphinx.ext.viewcode'] # 'myst_parser' + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +autodoc_mock_imports = ["mlflow"] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + + +def get_git_tag(): + try: + return subprocess.check_output(['git', 'describe', '--tags', '--abbrev=0']).decode('utf-8').strip() + except subprocess.CalledProcessError: + return 'unknown' + + +# Set the version to the latest tag +version = get_git_tag() +release = version + +html_theme = 'sphinx_rtd_theme' # 'alabaster' +html_static_path = ['_static'] + +html_context = { + 'display_version': True, + 'release': release +} + +html_footer = """ + +""" + +html_sidebars = { + '**': [ + html_footer # Adds the custom footer with version information + ] +} diff --git a/docs/docs/CLI.md b/docs/docs/CLI.md deleted file mode 100644 index a2383b46..00000000 --- a/docs/docs/CLI.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -layout: default -title: CLI -nav_order: 3 ---- - -# Command-line interface (CLI) - - -The `itwinai` package provides a custom CLI, which can be accessed, for instance -from the development environment: - -```bash -# Activate development environment -micromamba activate ./.venv-dev - -# Access itwinai CLI -itwinai --help -``` - -## Visualization - -Some visualization functionalities offered by `itwinai` CLI. - -```bash -# Datasets registry -itwinai datasets --help - -# Workflows (any file '*-workflow.yml') -itwinai workflows --help -``` - -## Machine learning - -```bash -# Training -itwinai train --help - -# Launch MLFlow UI to visualize ML logs -itwinai mlflow-ui --help - -# Inference -itwinai predict --help -``` diff --git a/docs/docs/Concepts.md b/docs/docs/Concepts.md deleted file mode 100644 index 5830fcfe..00000000 --- a/docs/docs/Concepts.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -layout: default -title: Concepts -nav_order: 4 ---- - -# Concepts - - -Here we presents the key concepts on which `itwinai` is based. - -## Workflow - -We define a workflow as a directed acyclic graph (DAG) of data processing -operations, in which each step can have multiple inputs and outputs, and -each input or output is a dataset. - -![image](img/Workflow%20DAG%20concept.png) - -In the picture above, the yellow boxes with numbers represent the steps -in the example workflow, whereas the blue cylinders represent data -(e.g., dataset, configuration file). - -Each step runs in *isolation* from the others, and data is the *interface*. -Isolation can be guaranteed by executing each step in a Docker container or -in a separate Python virtual environment. diff --git a/docs/docs/How-to-use-this-software.md b/docs/docs/How-to-use-this-software.md deleted file mode 100644 index 15a36b44..00000000 --- a/docs/docs/How-to-use-this-software.md +++ /dev/null @@ -1,540 +0,0 @@ ---- -layout: default -title: How to use this software -nav_order: 2 ---- - -# How to use this software -{: .no_toc } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -This guide provides a detailed explanation on how to use the AI/ML workflow -tool, developed in the context of [interTwin](https://github.com/interTwin-eu/). - -**Target audience**: anyone aiming to simplify MLOps for their digital twin (DT) -use case/project. Use cases from interTwin project. - -## Clone this repo - -```bash -git clone git@github.com:interTwin-eu/T6.5-AI-and-ML.git -``` - -A new use case/project can be added under `./use-cases` folder. - -Build the workflow runner environment and development environment -following the instructions on the README file. - -## Define a DT workflow - -Before delving into workflow definition rules, make sure to have -understood *what is* a [workflow](./Concepts#workflow) in this context. - -You can define one or more workflows for your DT use case (e.g., ML training, -ML inference, other). A workflow is -defined through configuration files in the use case subfolder. -For the same use case, a DT developer can define multiple workflows, -in which multiple datasets are involved. - -Currently, each step is executed in an isolated Python virtual environment, -built according to [conda](https://docs.conda.io/en/latest/) standards. -In practice, it is built with -[Micromamba](https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html), -which is faster. - -To begin with, you can start by looking at an example of the -[MNIST toy use case](use-cases/mnist), located at `use-cases/mnist` -in the code repository. - -### Use case metadata - -The main configuration file of an use case is `meta.yml`, which stores -the metadata of it. When creating a new use case, you need to update the -`root` field with the path to the use case folder, with respect to the -repo root. - -The datasets registry is a field in this configuration file, -which stores the metadata -for all datasets involved in a use case. This configuration provides a -unified place where datasets can be maintained, making it easy to access -them from other configuration files. - -The dataset registry has the format: - -```yaml -datasets: - some-dataset-name: - doc: Documentation string for this dataset - location: path/to/dataset/disk/location -``` - -Example of `meta.yml` from [MNIST use case](use-cases/mnist): - -```yaml -# Use case root location. End without path '/' char! -root: ./use-cases/mnist - -# AI folder location. End without path '/' char! -ai-root: ./ai - -# Datasets registry -datasets: - preproc-images: - doc: Preprocessed MNIST images - location: ${root}/data/preproc-images - ml-logs: - doc: MLflow tracking URI for local logging - location: ${root}/data/ml-logs - ml-predictions: - doc: predictions on unseen data - location: ${root}/data/ml-predictions -``` - -Datasets are imported from the datasets registry to other files by means -of [OmegaConf](https://omegaconf.readthedocs.io/)'s -[variable interpolation](https://omegaconf.readthedocs.io/en/2.3_branch/usage.html#variable-interpolation). -This way, you can easily import datasets metadata (e.g., location on -file system) from datasets registry. - -Dataset registry of an use case can be visualized using [itwinai CLI](./CLI#visualization): - -```bash -USE_CASE_ROOT='use-cases/mnist/' -micromamba activate ./.venv-dev && \ - itwinai datasets --use-case $USE_CASE_ROOT -``` - -### Workflow configuration - -Use case workflows are defined with `*-workflow.yml` files in the use case root, -and there are two ways to define a workflow: - -- "Custom" format for workflow definition is an intuitive standard we created -for this prototype, for easy prototyping. -- [Common Workflow Language](https://www.commonwl.org/) (CWL), which is -currently under development, and not ready to be used. - -Which of the two is used is defined by setting the `--cwl` flag (explained -[below](#run-the-workflow)). - -#### Custom workflow definition - -To define a workflow with the custom format, the DT developer must follow -the structure provided below. - -The `steps` section defines the steps of the workflow, in the order in which -they have to be executed: - -```yaml -steps: - - some-step-name: - doc: Documentation string for this step - env: # micromamba environment metadata - file: some-conda-env.yml - prefix: path/to/conda/env/ - command: Command to execute inside micromamba env - args: # Command arguments. - # Note interpolation with datasets registry here! - some-arg: ${datasets.my-dataset.location} - some-other-arg: 42 - - next-step-name: - ... -``` - -Example workflow from [MNIST use case](use-cases/mnist), defined in -`training-workflow.yml`: - -```yaml -steps: - - preprocessing: - doc: Download and split MNIST dataset into train and test sets - command: python ${root}/mnist-preproc.py - env: - file: ${root}/env-files/preproc-env.yml - prefix: ${root}/.venv-preproc - args: - output: ${datasets.preproc-images.location} - stage: train - - ml-training: - doc: Train a neural network to classify MNIST images - command: itwinai train - env: - file: ${ai-root}/env-files/pytorch-lock.yml - prefix: ${ai-root}/.venv-pytorch - source: ${ai-root} - args: - train-dataset: ${datasets.preproc-images.location} - ml-logs: ${datasets.ml-logs.location} - config: ${root}/mnist-ai-train.yml -``` - -Step 1 is named `preprocessing` and uses the `mnist-preproc.py` script to pre-process the MNIST dataset. It takes no -input, generates an output dataset named `preproc-images`, and uses an environment defined in a YAML file named -`preproc-env.yml` located in the `./use-cases/mnist` directory. -Step 2 is named `ml-training` and trains a machine learning model using the preprocessed image data generated in -the first step. The training is performed using the train command from the `itwinai` tool. The input dataset is -`preproc-images`, and the output is `ml-logs`. The step uses an environment defined in a YAML file named -`pytorch-env.yml` located in the `./ai` directory. The machine learning model is configured using the `mnist-ai-train.yml` -file located in the `./use-cases/mnist` directory. - -#### CWL: under development and not ready to be used yet - -**NOTE**. At the moment, support for CWL is under development, -and is not available. - - - -## Implement workflow steps - -Implement use case-specific steps. -Note that the implementation of steps involving AI/ML are addressed in the next -step, and they can be implemented a bit more easily. - -Each step of a workflow is characterized by its python virtual environment -and a command to be executed in that -environment. A command can be implemented by providing, for instance, a python script to be executed in some environment. - -To execute a step, the workflow engine will run something like: - -```bash -micromamba run -p PATH_TO_STEP_ENV CMD --arg1 ARG_1_VAL ... --argN ARG_N_VAL -``` - -Where: - -- `PATH_TO_STEP_ENV` is the path to the micromamba environment for this step. -- `CMD` is the command to execute in that environment. -- The developer can use additional parameters which are automatically appended -to the command. - -*Example*: in the [MNIST use case](use-cases/mnist), -the preprocessing step is implemented by a python script, which downloads and -splits the MNIST dataset in a specific location. Using a command similar to: - -```bash -micromamba run -p ./use-cases/mnist/.venv-preproc \ - python ./use-cases/mnist/mnist-preproc.py \ - --output ./data/mnist/preproc-images-inference \ - --stage train -``` - -## Define AI/ML workflow - -AI/ML workflows are implemented by the `itwinai` module. -The DT developer, who wants to include a new use case, needs to provide -only a reduced amount of code to describe a neural network, plus some -configuration files. - -The developer must implement the neural network to train and include it inside -`itwinai` python package, under `ai/src/itwinai`. For instance, under -`ai/src/itwinai/plmodels` when using PyTorch Lightning. - -For instance, `LitMNIST` neural network used in [MNIST use case](use-cases/mnist) -has been added under `ai/src/itwinai/plmodels/mnist.py` - -Once a model has been included inside the `itwinai` python module, it can be imported during training. -In the future, `itwinai` will support also neural networks not provided out-of-the-box by `itwinai`. - -The developer must define two configuration files to access `itwinai` -functionalities. -First, ML training configuration, associated with `$ itwinai train` [CLI](./CLI) command. -Second, ML inference configuration, associated with `$ itwinai predict` [CLI](./CLI) command. - -MLOps heavily relies on commands provided by [itwinai CLI](./CLI). -Therefore, before continuing, make sure to have understood how -[itwinai CLI](./CLI) works. - -### ML training configuration - -ML training configuration is provided in a with naming convention `*-ai-train.yml` -under the use case root directory. - -An example configuration file is provided below, where the fields have been replaced with their respective description: - -```yaml -# Configuration file of AI workflows for X use case - -# Training configuration -train: - type: > - can be 'lightning' or 'tf', depending whether the neural network is defined - using PyTorch Lightning or TensorFlow. - At the moment, only 'lightning' is supported. - - # Configuration format defined by PyTorch Lightning CLI - # https://pytorch-lightning.readthedocs.io/en/1.6.5/common/lightning_cli.html - conf: - # See discussion below - ... - -# MLFlow logger configuration -logger: - experiment_name: > - Unique name for an experiment, to group all similar - runs under the same experiment - description: Description for this specific run. - log_every_n_epoch: how often to log (epochs) - log_every_n_steps: how often to log (steps, i.e., batches) - registered_model_name: > - Unique name used in Models Registry to identify an ML model. - If given, it is automatically registered in the Models Registry. -``` - -When using PyTorch Lightning (PL) ML framework, the training configuration is easy to define, as it follows the schema -pre-defined by lightning authors for the PL CLI. See its documentation -[here](https://pytorch-lightning.readthedocs.io/en/1.6.5/common/lightning_cli.html#trainer-callbacks-and-arguments-with-class-type), -[here](https://pytorch-lightning.readthedocs.io/en/1.6.5/common/lightning_cli.html#trainer-callbacks-and-arguments-with-class-type), -[here](https://pytorch-lightning.readthedocs.io/en/1.6.5/common/lightning_cli.html#multiple-models-and-or-datasets), and -[here](https://pytorch-lightning.readthedocs.io/en/1.6.5/common/lightning_cli.html#optimizers-and-learning-rate-schedulers). - -An example taken from -[MNIST use case](use-cases/mnist) located at `use-cases/mnist/mnist-ai-training.yml`: - -```yaml -# Pytorch lightning config for training -train: - type: lightning - # Follows lightning config file format: - # https://pytorch-lightning.readthedocs.io/en/1.6.5/common/lightning_cli.html#multiple-models-and-or-datasets - conf: - seed_everything: 4231162351 - - # Lightning Trainer configuration - trainer: - accelerator: auto - strategy: auto - devices: auto - num_nodes: 1 - precision: 32-true - - # MLFlow logger (initial) configuration. - # To be completed with run details, later on - logger: - class_path: lightning.pytorch.loggers.MLFlowLogger - init_args: - experiment_name: ${logger.experiment_name} - save_dir: ./mlruns - - # Callbacks - callbacks: - - class_path: lightning.pytorch.callbacks.early_stopping.EarlyStopping - init_args: - monitor: val_loss - patience: 2 - - class_path: lightning.pytorch.callbacks.lr_monitor.LearningRateMonitor - init_args: - logging_interval: step - - class_path: lightning.pytorch.callbacks.ModelCheckpoint - init_args: - dirpath: checkpoints - filename: best-checkpoint - save_top_k: 1 - verbose: true - monitor: val_loss - mode: min - - max_epochs: 1 - - # Lightning Model configuration - model: - class_path: itwinai.plmodels.mnist.LitMNIST - init_args: - hidden_size: 64 - - # Lightning data module configuration - data: - class_path: itwinai.plmodels.mnist.MNISTDataModule - init_args: - data_dir: ${cli.train_dataset} - batch_size: 32 - - # Torch Optimizer configuration - optimizer: - class_path: torch.optim.AdamW - init_args: - lr: 0.001 - - # Torch LR scheduler configuration - lr_scheduler: - class_path: torch.optim.lr_scheduler.ExponentialLR - init_args: - gamma: 0.1 - -# Mlflow -logger: - experiment_name: MNIST classification lite - description: A MLP classifier for MNIST dataset. - log_every_n_epoch: 1 - log_every_n_steps: 1 - # Name used in Models Registry. If given, it is automatically - # registered in the Models Registry. - registered_model_name: MNIST-clf-lite -``` - -Note the field `data_dir: ${cli.train_dataset}` in the above configuration. -More on this later. - -### ML inference configuration - -ML training configuration is provided in a with naming convention -`*-ai-inference.yml` under the use case root directory. - -An example configuration file is provided below, where the fields have been replaced with their respective description: - -```yaml -inference: - experiment_name: > - Unique name for an experiment, to group all similar - runs under the same experiment - run_id: Run ID in MLFlow server of pre-trained model - ckpt_path: model/checkpoints/best-checkpoint/best-checkpoint.ckpt - train_config_artifact_path: name of training config saved to MLFlow artifacts folder - type: > - can be 'lightning' or 'tf', depending whether the neural network is defined - using PyTorch Lightning or TensorFlow. - At the moment, only 'lightning' is supported. - - # Configuration format defined by PyTorch Lightning CLI - # https://pytorch-lightning.readthedocs.io/en/1.6.5/common/lightning_cli.html - conf: - # See discussion below - ... -``` - -Regarding the `inference.conf` field, same considerations hold as for `train.conf` field of ML training configuration. - -An example taken from -[MNIST use case](use-cases/mnist) located at `use-cases/mnist/mnist-ai-training.yml`: - -```yaml -inference: - type: lightning - experiment_name: MNIST classification lite - # Run ID in MLFlow server: pre-trained model - run_id: 54f790100be646e0a7ccbb1235729d00 - ckpt_path: model/checkpoints/best-checkpoint/best-checkpoint.ckpt - train_config_artifact_path: pl-training.yml - conf: - # Lightning data module configuration - data: - class_path: itwinai.plmodels.mnist.MNISTDataModule - init_args: - data_dir: ${cli.input_dataset} - batch_size: 32 -``` - -### Accessing CLI args from config file - -As explained above, train and predict commands in itwinai CLI receive as input -specific configuration files: - -- The `train` command receives `*-ai-train.yml` as configuration. -- The `predict` command receives `*-ai-inference.yml` as configuration. - -With [OmegaConf](https://omegaconf.readthedocs.io/)'s -[variable interpolation](https://omegaconf.readthedocs.io/en/2.3_branch/usage.html#variable-interpolation) -you can access the args from the itwinai CLI command from the configuration file -associated with this command. - -Example: the field `data_dir: ${cli.input_dataset}` in the above configuration -accesses the value of `--input-dataset` argument of `itwinai predict` command. - -### ML framework: PyTorch vs. TensorFlow - -At the moment, only PyTorch are supported. TensorFlow support is planned for -future releases. - -## Run the workflow - -Once a workflow has been configured, it can be run by executing `run-workflow.py` in the root of this repo: - -```bash -micromamba run -p ./.venv python run-workflow.py -f WORKFLOW_DEFINITION_FILE -``` - -This script performs two main actions: - -1. Deploy ste steps of a workflow as python environments, managed with Conda. -2. Run a workflow step-by-step, following the directives given in the config file. - -See some examples of workflow executions in `examples.sh`, for instance: - -```bash -# Run workflow for MNIST toy use case -micromamba run -p ./.venv python run-workflow.py -f ./use-cases/mnist/training-workflow.yml -``` - - - -## Write tests cases - -Integrating an new use case means defining new workflows for it. -It is strongly suggested to define "integration" test cases for -those workflows. This way, every time `itwinai` -framework is updated, integration tests automatically verify that -the use case integrates well with the new changes introduced in the -main framework. -Moreover, integration tests verify that an use case case is stable, -and is not hiding some "bug". - -Add test for your use case under the `test/` folder. You can take -inspiration from other use cases' tests. diff --git a/docs/docs/img/Workflow DAG concept.png b/docs/docs/img/Workflow DAG concept.png deleted file mode 100644 index f09d8146dfe9d5a1c53588430d214b1a0b779b21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 224511 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

Eg&*2Z{> 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

i@NV+o>U;V6}xe^^j4-P-(iDe!6g2X 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 diff --git a/docs/docs/img/cwl-workflow.png b/docs/docs/img/cwl-workflow.png deleted file mode 100644 index 9c434e3fe5a68bb38f5243021afad62e682fb1d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 375527 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

  • eMMd>z=z&BZIqdC@O|1rGr~r7j`%& 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 <}Sa%Ascn?j^Re)@k{yE%vZsbL& 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⪙1#T)U9)N%*qYfh6bOdQA_l3XGNxO8 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 z�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%0ARH(2W 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 diff --git a/docs/docs/img/user-platform interaction.png b/docs/docs/img/user-platform interaction.png deleted file mode 100644 index 19dc3eeb9d67edf86b6a3bdb011651764be1e550..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30005 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 diff --git a/docs/docs/use-cases/index.md b/docs/docs/use-cases/index.md deleted file mode 100644 index 681caeb6..00000000 --- a/docs/docs/use-cases/index.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -layout: default -title: Use cases -nav_order: 4 -has_children: true ---- - -# Use cases - -Digital twin use cases employing machine learning workflows, currently integrated -in this prototype. -To integrate a new use case, please refer to [this page](../How-to-use-this-software). diff --git a/docs/docs/use-cases/mnist.md b/docs/docs/use-cases/mnist.md deleted file mode 100644 index e22f7ab1..00000000 --- a/docs/docs/use-cases/mnist.md +++ /dev/null @@ -1,185 +0,0 @@ ---- -layout: default -title: MNIST -parent: Use cases -nav_order: 1 ---- - -# MNIST: toy example for DT workflows -{: .no_toc } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -Of course MNIST images classification is not a digital twin. Still, it is useful to -provide an example on how to define an end-to-end digital twin workflow with the -software provided in this repository. - -The MNIST use case implements two workflows: - -1. Training workflow: train a neural network to classify MNIST images, and save the trained -neural network to the Models Registry. - - ```mermaid - flowchart LR - %% Nodes - preproc(Pre-processing) - ai(ML training) - reg[(Models Registry:\npre-trained ML models)] - - %% Workflow - preproc --> ai - - %% Connections - ai -.-> |Saves to| reg - ``` - - This workflow is executed by running the command: - - ```bash - micromamba run -p ./.venv python run-workflow.py -f ./use-cases/mnist/training-workflow.yml - ``` - -1. Inference workflow: use the pre-trained neural network to classify unseen images (the test set, in this case). - - ```mermaid - flowchart LR - %% Nodes - preproc(Pre-processing) - ai_depl(ML inference) - pred[(Predictions)] - - %% Workflow - preproc --> ai_depl - - %% Connections - ai_depl -.-> |Saves to| pred - ``` - - This workflow is executed by running the command: - - ```bash - micromamba run -p ./.venv python run-workflow.py -f ./use-cases/mnist/inference-workflow.yml - ``` - -The interactions among workflows and their steps can be described in more details as the following, where conceptual ordering -among different workflow steps is represented by solid arrows: - -```mermaid -graph TD - %% Nodes - remote_repo[(Remote repo)] - preproc(Pre-processing) - ai(ML training) - ai_depl(ML inference) - train_set[(Train dataset)] - test_set[(Test dataset)] - ml_logs[(ML logs)] - reg[(Models Registry:\npre-trained ML models)] - pred[(Predictions)] - - %% Workflow - preproc --> ai ---> ai_depl - - %% Connections - preproc -.-> |Fetches| remote_repo - preproc -.-> |Stores| train_set - preproc -.-> |Stores| test_set - ai -.-> |Trains/validates model| train_set - ai -.-> |Tests model| test_set - ai -.-> |Stores model to| reg - ai -.-> |Logs| ml_logs - ai_depl -.-> |Loads from| reg - ai_depl -.-> |Predict from| test_set - ai_depl -.-> |Stores| pred -``` - -## Workflow steps - -Here we explain in more details how the workflow steps have been configured. -Configuration files and Python scripts are organized under `use-cases/mnist/` -folder, in the core repository. - -### Pre-processing - -This step is implemented by executing `mnist-preproc.py` script in its dedicated micromamba environment, defined by -`preproc-env.yml`. This solution gives full freedom to the DT developer to implement any preprocessing logic, adaptable -to any custom dataset format. - -### ML training - -Is the step in which a neural network is trained on the training dataset, and -validated on the validation dataset. -The mentioned datasets are a result from a split of the pre-processed training -dataset, produced by the pre-processing step. -This step completes the **training workflow**, and results into ML logs and a -trained neural network, which is saved to -the Models Registry. The training workflow can be re-run multiple times with different (hyper)parameters, with the goal -of optimizing some ML validation metric. The neural network with the best validation performances is used to make -predictions on unseen data, in the inference step. - -ML training logic is implemented by the `itwinai` library, requiring the DT developer to produce only a set of YAML -configuration files. For the moment, we assume that the neural network and the training code is already present in -`itwinai` library. - -Both ML training and inference are implemented by commands executed in the same virtual environment. At the moment, -only PyTorch is supported. The corresponding virtual environment definition, used by the `itwinai` library, -is located at `ai/pytorch-env.yml`. - -The ML training logic provided by `itwinai` library is accessed via the -[itwinai CLI](../CLI). - -The DT developer must provide a training configuration file, following some -rules explained in [this section](../How-to-use-this-software#ml-training-configuration). For MNIST use case, the -training configuration is provided by `mnist-ai-train.yml` file. - -Training command is automatically managed by the workflow runner, but it can also -be triggered from withing the ai environment running the following command: - -```bash -micromamba activate ./ai/.venv-pytorch && \ - itwinai train --train-dataset $TRAINING_DATASET_PATH \ - --ml-logs $MLFLOW_TRACKING_URI \ - --config ./use-cases/mnist/mnist-ai-train.yml -``` - -While training is running, the produced ML logs can be inspected in real-time from MLFlow UI by running the command in -the training virtual environment (Conda): - -```bash -micromamba activate ./ai/.venv-pytorch && \ - itwinai mlflow-ui --path $PATH_TO_ML_LOGS -``` - -### ML inference - -A pre-trained neural network is applied to a set of data which was not used to train it. In fact, this is defined as -"unseen" data, from the neural network perspective. An example of ML inference is the application of a trained neural -network to make predictions on new data, to support decision making. *Example*: forecast fire risk maps in Sicily in -August 2023, starting from newly-collected satellite images, to alert local authorities in case of elevated fire risk. - -To select a pre-trained ML model, the DT developer must retrieve the `RUN ID` of -the training run created by MLFLow for some specific training. - -The, the DT developer can update the inference configuration file -`mnist-ai-inference.yml` and run inference workflow. - -Inference/prediction command is automatically managed by the workflow runner, but it can also be triggered from within -the ai environment running the following command: - -```bash -micromamba activate ./ai/.venv-pytorch && \ - itwinai predict --input-dataset $UNSEEN_EXAMPLES_DATASET_PATH \ - --predictions-location $PREDICTIONS_LOCATION \ - --ml-logs $PATH_TO_ML_LOGS \ - --config ./use-cases/mnist/mnist-ai-inference.yml -``` - -## References - -To learn more on how to use this software, e.g., to deploy a new use case, please refer to [this guide](../How-to-use-this-software). diff --git a/docs/favicon.ico b/docs/favicon.ico deleted file mode 100644 index 1665919207a78e8012a58e4425fd3fb90f51c3f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 zcmeHNU1(fI6rR~MB$33l52nTjS+R)~MN*bZkd`ipL`oD96-sF-rKz?rqDAaO!TL}e z4Q*Al6#q6=@Q)&GL{P+neUQFbL8`GTt)eqv`yT1xmezvd+#-eTPzCU8mF%w1EgD z(&b2k(o!_s5B?E6nPEP&jCEerfQr|_(_ryn*0BxST8a6iU@tjmIon!wP^YtvU!F1~ z_Gt#Q)}rR$AgZ<@E8o!2AZ=}JqFFa%hcafY(&gP>zk6U$^aTe9oBI!!N{BWEk>Rgiowq@C}#YMoj;gQ`J@?>psDd6$`|XuMhA7y_@Wt?nbhn3 zp}ZmElg2kV$9(pCe~o-zDc6}OI1c9eR03*-jy2#nz{kNOVD_8LXO^*!ZP>O{bPE(E zz!Tu0KESq=p=_aa4Fvaqr@%q&I@?i}I;blj!fv5XfSvjZ%~Kb32B8=lod*|c%S(T z?csXIccPbX6a|}+NJKVo-Yln2pU&>%d>9=amHz&IX=-Zn!a z_#ySY@m=}g;72yQ&dkuzkStoXC|7@a4AuYx*KB)zq(=1CBeUDi#6eFUz3McXlRtLs znAFtN*lnb3n!UtUr(+KXvduA=_sW?3a^>$Fo6dsOt5@4>Wcqv@Dfcd0o$h@vuzh0K zef&?Sxt#o=Lx*fOGB(W~YOmY=O?BDjf%G$5?_|pe${jae9 zRr~$p8er>xDqp$vr`qc;x|@m>bH~7pOL_Mn+OPKhOAOVFKWM+&@fR^vGybG~QZ?i6 zG=lUvlrk31=ljE8pg+(ji-}7qOC4_UHi`og-^~QrJw`NZggq_A_oow_i4{%EWMYSM ze}6l1{HyXjC&3Y|S`RMKISDl7Py{6lz&C??zF)(#jf}$`o06qieSzA!NUhSxZx(2{ea5+drDQ*eW zQ{W`n3rn%()J+?-MVqCfI23&bF2#8J^^rDcD-JG}I0lb`&w;s~`q?g3J#En@Z5KKZ z^^ko6Tuy)S>o08+1F_^o>@(#qg8giiyPg<`C7*fVyj64Dont~Q#NnX8X<0ZyVj6nk9*7etlzV&X=cdZw_qE{lf-M)~Y&V0ESc#d$_?0oBzu(WR6Mf@S(Q9ABef=M~XQ6vWB(mv4(YOy#b&Xj} zjg5`6a^*@H930Hv?OP9G$H&Lzz<~o2i^Zg_t}ZJFOkU5#O3cK*0L(bnt8Kcx`Fhd! z?-1QLoQpeiW^T^$b^kfhkL*qTuYvz#*tc(=jE#+1A-g`0jEu;xUAv^UwRLtZX{^Ld z?EH>6J1hPM_{y67qV`_&#cukbUc4mg@y}%N^Dkv;YRVO_TjAv7q$CoFAU;LR=DW$e zzd9oNo~@!@x)L0_Wj-GIWI~emzjV04wv|m!Ps_G#+fsKxvySV`H=if|$W6;c_n$2Y zJLjTle^TE0VM+igFQUJ7b#-~-%Ji!bDZfeG+ul+rx(s z`^|y*)^^38x>J1i`=ar;zL}JtNB!@joD(!WJS-g@9j@3iW9~dBu(j2J+3aUSSP=fIV`6DY;~owdJlWVkN*pOF^7&(Q{L#lfW_ z&I8w0?hobkC+emR+M>;JP!viBz>JqlF(1@HT?61KxDv$JBL)uR|2M4TW6s~rIWWIP Y&6%F{T!YVZ>Hjm`_, build the environment containing pytorch, horovod, and deepspeed. You can try with: + +.. code-block:: bash + + # Creates a Python environment called envAI_hdfml + make torch-gpu-jsc + + +Distributed training +++++++++++++++++++++ + + Each distributed strategy is described with a SLURM job script used to run that strategy. + +So if you want to distribute the code in `train.py` with, for example, **torch DDP**, run from terminal: + +.. code-block:: bash + + sbatch ddp_slurm.sh + +Similarly, if you want to distribute the code in `train.py` with **DeepSpeed**, run from terminal: + +.. code-block:: bash + + sbatch deepspeed_slurm.sh + +To distribute the code in `train.py` with **Horovod**, run from terminal: + +.. code-block:: bash + + sbatch hvd_slurm.sh + +Finally, you can run all of them with: + +.. code-block:: bash + + bash runall.sh + + + + + +💻 Local systems +----------------- + +**Requirements** + +* Linux environment. + +Windows and macOS were never tested. + + +Micromamba installation ++++++++++++++++++++++++ + +To manage Conda environments we use micromamba, a lightweight version of Conda. + +In order to install micromamba, please refer to the `Manual installation guide `_. + +Consider that Micromamba can eat a lot of space when building environments because packages are cached on the local filesystem after being downloaded. To clear cache, you can use `micromamba clean -a`. +Micromamba data are kept under the `$HOME` location. However, in some systems, `$HOME` has a limited storage space so it is recommended to install Micromamba in another location with more storage space by changing the `$MAMBA_ROOT_PREFIX` variable. +Below is a complete installation example where the default `$MAMBA_ROOT_PREFIX` is overridden for Linux: + + +.. code-block:: bash + + cd $HOME + + # Download micromamba (This command is for Linux Intel (x86_64) systems. Find the right one for your system!) + curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -xvj bin/micromamba + + # Install micromamba in a custom directory + MAMBA_ROOT_PREFIX='my-mamba-root' + ./bin/micromamba shell init $MAMBA_ROOT_PREFIX + + # To invoke micromamba from Makefile, you need to add explicitly to $PATH + echo 'PATH="$(dirname $MAMBA_EXE):$PATH"' >> ~/.bashrc + +**Reference**: `Micromamba installation guide `_. + + +Environment setup ++++++++++++++++++ + +**Requirements:** + +* Linux environment. Windows and macOS were never tested. +* Micromamba: see the installation instructions above. +* VS Code, for development. + +Tensorflow +++++++++++ + +Installation: + +.. code-block:: bash + + # Install TensorFlow 2.13 + make tf-2.13 + + # Activate env + micromamba activate ./.venv-tf + +Other TensorFlow versions are available, using the following targets `tf-2.10`, and `tf-2.11`. + + +PyTorch (+ Lightning) ++++++++++++++++++++++ + +Installation: + +.. code-block:: bash + + # Install PyTorch + lightning + make torch-gpu + + # Activate env + micromamba activate ./.venv-pytorch + +Other similarly CPU-only version is available at the target `torch-cpu`. + + +Development environment ++++++++++++++++++++++++ + +This is for developers only. To have it, update the installed `itwinai` package adding the `dev` extra: + +.. code-block:: bash + + pip install -e .[dev] + + +**Test with `pytest`** +To run tests on itwinai package: + +.. code-block:: bash + + # Activate env + micromamba activate ./.venv-pytorch # or ./.venv-tf + + pytest -v -m "not slurm" tests/ + + +However, some tests are intended to be executed only on HPC systems, where SLURM is available. They are marked with "slurm" tags. To run these tests, use the dedicated job script: + +.. code-block:: bash + + sbatch tests/slurm_tests_startscript + + # Upon completion, check the output: + cat job.err + cat job.out + + + + diff --git a/docs/hpc_setup.rst b/docs/hpc_setup.rst new file mode 100644 index 00000000..607c876c --- /dev/null +++ b/docs/hpc_setup.rst @@ -0,0 +1,69 @@ +.. 🌐 HPC systems +.. --------------- +How to use torch `DistributedDataParallel` (DDP), Horovod and DeepSpeed from the same client code. +Note that the environment is tested on the HDFML system at JSC. For other systems, the module versions might need change accordingly. + + +.. toctree:: + :maxdepth: 5 + + +Environments +++++++++++++ + +Install PyTorch env (GPU support) on Juelich Super Computer (tested on HDFML system) + +.. code-block:: bash + + torch-gpu-jsc: env-files/torch/createEnvJSC.sh + sh env-files/torch/createEnvJSC.sh + + +Install Tensorflow env (GPU support) on Juelich Super Computer (tested on HDFML system) + +.. code-block:: bash + + tf-gpu-jsc: env-files/tensorflow/createEnvJSCTF.sh + sh env-files/tensorflow/createEnvJSCTF.sh + + + +Setup ++++++ + +First, from the root of this `repository `_, build the environment containing pytorch, horovod and deepspeed. You can try with: + +.. code-block:: bash + + # Creates a Python venv called envAI_hdfml + make torch-gpu-jsc + + +Distributed training +++++++++++++++++++++ + +Each distributed strategy has its own SLURM job script, which should be used to run it: + +If you want to distribute the code in `train.py` with **torch DDP**, run from terminal: + +.. code-block:: bash + + sbatch ddp_slurm.sh + +If you want to distribute the code in `train.py` with **DeepSpeed**, run from terminal: + +.. code-block:: bash + + sbatch deepspeed_slurm.sh + +If you want to distribute the code in `train.py` with **Horovod**, run from terminal: + +.. code-block:: bash + + sbatch hvd_slurm.sh + +You can run all of them with: + +.. code-block:: bash + + bash runall.sh \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index ae78ffa7..00000000 --- a/docs/index.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Overview -layout: home -nav_order: 1 ---- - -# Overview - -Welcome to the `itwinai` docs! A framework for advanced AI/ML workflows in digital twins (DTs). - -Below we you are going to find an overview of interTwin's AI/ML workflows component. This platform -is intended to support general-purpose MLOps for digital twin use cases in [interTwin](https://www.intertwin.eu/) project. - -> Beware! As the code is frequently changed, the docs are unstable and may not reflect the actual state of the code base. -> Therefore, if you are looking for a more stable version, check out our -> [releases](https://github.com/interTwin-eu/T6.5-AI-and-ML/releases). - -Additional resources include: - -- Detailed instructions on [How to use this software](docs/How-to-use-this-software). -- Roadmap towards a prototype for T6.5 AI workflows for -digital twins here: [Prototype for T6.5](https://github.com/interTwin-eu/T6.5-AI-and-ML/wiki/Prototype-for-T6.5). - -## Platform for machine learning workflows in digital twins - -The goal of this platform is to provide ML researchers with an easy-to-use endpoint -to manage general-purpose machine learning (ML) workflows, with limited engineering overhead, -while providing state-of-the-art MLOps best practices. - -We call this platform `itwinai`. - -The user is going to provide as input a set of configuration files, to fully -describe ML workflows, in the context of digital twin (DT) applications. -`itwinai` platform instantiates ML workflows with the configurations -provided by the DT developer. The execution of ML workflows produces as output a -set of ML metrics, which are visualized by `itwinai` via -[MLFlow](https://mlflow.org/). -As a result of ML training, the best model (on validation dataset) -is saved to the *Models Registry* for future predictions. - -![image](docs/img/user-platform%20interaction%20full.png) - -### Simulating a whole DT workflow - -A DT workflow is more than ML. Generally speaking, MLOps -(i.e., ML model lifecycle management), -can be considered just as a step of a larger DT workflow. - -![image](docs/img/cwl-workflow.png) - -In `itwinai` platform, we focus mainly on the MLOps step, simulating or oversimplifying all the rest -(e.g., pre-processing, authentication, workflow execution). - -For further details on how to define a DT workflow in `itwinai`, follow [this guide](docs/How-to-use-this-software#2-define-a-dt-workflow). - -### How to integrate a new use case - -To integrate an existing use case in this platform, the ML engineer rewrites -the ML experiments according to a format supported by `itwinai`. -Some examples can be found by looking at the use cases -already integrated [here](docs/use-cases/index), located under `use-cases/` -in the code repository. - -A detailed guide on how to integrate a new use case in `itwinai` can be found [here](docs/How-to-use-this-software). diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..82b192f8 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,70 @@ +.. itwinai documentation master file, created by + sphinx-quickstart on Fri Feb 9 13:58:30 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +🚧 UNDER CONSTRUCTION 🚧 +========================= + +Welcome to itwinai's documentation! +=================================== + +``itwinai`` is a framework for advanced AI/ML workflows in Digital Twins (DTs). + +This platform is intended to support general-purpose MLOps for Digital Twin use cases in the `interTwin `_ project. + +Platform for machine learning workflows in digital twins +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +The goal of this platform is to provide ML researchers with an easy-to-use endpoint to manage general-purpose ML workflows, +with limited engineering overhead, while providing state-of-the-art MLOps best practices. + +The user can fully describe ML workflows for DT applications by providing a set of configuration files as input. +The ``itwinai`` platform instantiates ML workflows with the configurations provided by the DT developer. +The execution of ML workflows outputs a set of ML metrix, which are visualised by ``itwinai`` via `MLFlow `_. +The trained ML model that performed best on the validation dataset is saved to the Models Registry for future predictions. + +In ``itwinai`` platform, we focus mainly on the MLOps step, simulating or oversimplifying the rest (e.g., pre-processing, authentication, workflow execution). + + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: 💡 Installation + + getting_started_with_itwinai + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: 🪄 itwinai Modules + + modules + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: 📚 Integrated Use-cases + + use_cases + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: 🚀 Tutorials + + tutorials + + +`interTwin Demo: itwinai integration with other DTE modules `_ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` + +.. * :ref:`search` + diff --git a/docs/intermediate_workflow.rst b/docs/intermediate_workflow.rst new file mode 100644 index 00000000..860b12e5 --- /dev/null +++ b/docs/intermediate_workflow.rst @@ -0,0 +1,20 @@ +Intermediate workflow +===================== + +tutorial_1_intermediate_workflow.py ++++++++++++++++++++++++++++++++++++ + +The `tutorial_1_intermediate_workflow.py` script is ... + +.. .. literalinclude:: ../use-cases/mnist/torch-lightning/dataloader.py +.. :language: python + +.. .. automodule:: tutorial_1_intermediate_workflow +.. :members: +.. :undoc-members: +.. :show-inheritance: + + +.. literalinclude:: ../tutorials/ml-workflows/tutorial_1_intermediate_workflow.py + :language: python + diff --git a/docs/itwinai.cli.rst b/docs/itwinai.cli.rst new file mode 100644 index 00000000..18e0d0ef --- /dev/null +++ b/docs/itwinai.cli.rst @@ -0,0 +1,7 @@ +itwinai.cli +=========== + +.. automodule:: itwinai.cli + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/itwinai.cluster.rst b/docs/itwinai.cluster.rst new file mode 100644 index 00000000..7360c981 --- /dev/null +++ b/docs/itwinai.cluster.rst @@ -0,0 +1,7 @@ +itwinai.cluster +=============== + +.. automodule:: itwinai.cluster + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/itwinai.components.rst b/docs/itwinai.components.rst new file mode 100644 index 00000000..db3b0956 --- /dev/null +++ b/docs/itwinai.components.rst @@ -0,0 +1,8 @@ +itwinai.components +================== + +.. automodule:: itwinai.components + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/itwinai.loggers.rst b/docs/itwinai.loggers.rst new file mode 100644 index 00000000..513e3942 --- /dev/null +++ b/docs/itwinai.loggers.rst @@ -0,0 +1,8 @@ +itwinai.loggers +=============== + +.. automodule:: itwinai.loggers + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/itwinai.parser.rst b/docs/itwinai.parser.rst new file mode 100644 index 00000000..f9c7d930 --- /dev/null +++ b/docs/itwinai.parser.rst @@ -0,0 +1,8 @@ +itwinai.parser +============== + +.. automodule:: itwinai.parser + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/itwinai.pipeline.rst b/docs/itwinai.pipeline.rst new file mode 100644 index 00000000..b849240e --- /dev/null +++ b/docs/itwinai.pipeline.rst @@ -0,0 +1,8 @@ +itwinai.pipeline +================ + +.. automodule:: itwinai.pipeline + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/itwinai.serialization.rst b/docs/itwinai.serialization.rst new file mode 100644 index 00000000..691a3721 --- /dev/null +++ b/docs/itwinai.serialization.rst @@ -0,0 +1,8 @@ +itwinai.serialization +===================== + +.. automodule:: itwinai.serialization + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/itwinai.tf.modules.rst b/docs/itwinai.tf.modules.rst new file mode 100644 index 00000000..8b923bea --- /dev/null +++ b/docs/itwinai.tf.modules.rst @@ -0,0 +1,26 @@ +itwinai Tensorflow Modules +========================== + +trainer.py +++++++++++ + +.. literalinclude:: ../src/itwinai/tensorflow/trainer.py + :language: python + +utils.py +++++++++ + +.. literalinclude:: ../src/itwinai/tensorflow/utils.py + :language: python + + +.. .. automodule:: itwinai.tensorflow.trainer +.. :members: +.. :undoc-members: +.. :show-inheritance: + +.. .. automodule:: itwinai.tensorflow.utils +.. :members: +.. :undoc-members: +.. :show-inheritance: + diff --git a/docs/itwinai.torch.modules.rst b/docs/itwinai.torch.modules.rst new file mode 100644 index 00000000..d551af40 --- /dev/null +++ b/docs/itwinai.torch.modules.rst @@ -0,0 +1,72 @@ +itwinai PyTorch Modules +======================= + +cluster.py +++++++++++ + +.. literalinclude:: ../src/itwinai/torch/cluster.py + :language: python + +inference.py +++++++++++++ + +.. literalinclude:: ../src/itwinai/torch/inference.py + :language: python + +mlflow.py ++++++++++ + +.. literalinclude:: ../src/itwinai/torch/mlflow.py + :language: python + +trainer.py +++++++++++ + +.. literalinclude:: ../src/itwinai/torch/trainer.py + :language: python + +types.py +++++++++ + +.. literalinclude:: ../src/itwinai/torch/types.py + :language: python + +utils.py +++++++++ + +.. literalinclude:: ../src/itwinai/torch/utils.py + :language: python + + + +.. .. automodule:: itwinai.torch.cluster +.. :members: +.. :undoc-members: +.. :show-inheritance: + +.. .. automodule:: itwinai.torch.inference +.. :members: +.. :undoc-members: +.. :show-inheritance: + +.. .. automodule:: itwinai.torch.mlflow +.. :members: +.. :undoc-members: +.. :show-inheritance: + +.. .. automodule:: itwinai.torch.trainer +.. :members: +.. :undoc-members: +.. :show-inheritance: + +.. .. automodule:: itwinai.torch.types +.. :members: +.. :undoc-members: +.. :show-inheritance: + +.. .. automodule:: itwinai.torch.utils +.. :members: +.. :undoc-members: +.. :show-inheritance: + + diff --git a/docs/itwinai.types.rst b/docs/itwinai.types.rst new file mode 100644 index 00000000..20367596 --- /dev/null +++ b/docs/itwinai.types.rst @@ -0,0 +1,8 @@ +itwinai.types +============= + +.. automodule:: itwinai.types + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/itwinai.utils.rst b/docs/itwinai.utils.rst new file mode 100644 index 00000000..b487da7f --- /dev/null +++ b/docs/itwinai.utils.rst @@ -0,0 +1,8 @@ +itwinai.utils +============= + +.. automodule:: itwinai.utils + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/local_setup.rst b/docs/local_setup.rst new file mode 100644 index 00000000..72b5f377 --- /dev/null +++ b/docs/local_setup.rst @@ -0,0 +1,210 @@ +.. 💻 Local systems +.. ----------------- + +**Requirements** + +* Linux environment. +* Windows and macOS were never tested. + + +.. toctree:: + :maxdepth: 5 + + +Micromamba installation ++++++++++++++++++++++++ + +To manage Conda environments we use micromamba, a light weight version of conda. + +It is suggested to refer to the `Manual installation guide `_. + +Consider that Micromamba can eat a lot of space when building environments because packages are cached on +the local filesystem after being downloaded. To clear cache you can use `micromamba clean -a`. +Micromamba data are kept under the `$HOME` location. However, in some systems, `$HOME` has a limited storage +space and it would be cleverer to install Micromamba in another location with more storage space. +Thus by changing the `$MAMBA_ROOT_PREFIX` variable. See a complete installation example for Linux below, where the +default `$MAMBA_ROOT_PREFIX` is overridden: + + +.. code-block:: bash + + cd $HOME + + # Download micromamba (This command is for Linux Intel (x86_64) systems. Find the right one for your system!) + curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -xvj bin/micromamba + + # Install micromamba in a custom directory + MAMBA_ROOT_PREFIX='my-mamba-root' + ./bin/micromamba shell init $MAMBA_ROOT_PREFIX + + # To invoke micromamba from Makefile, you need to add explicitly to $PATH + echo 'PATH="$(dirname $MAMBA_EXE):$PATH"' >> ~/.bashrc + +**Reference**: `Micromamba installation guide `_. + + +Environment setup ++++++++++++++++++ + +**Requirements:** + +* Linux environment. Windows and macOS were never tested. +* Micromamba: see the installation instructions above. +* VS Code, for development. + +Tensorflow +++++++++++ + +Installation: + +.. code-block:: bash + + # Install TensorFlow 2.13 + make tf-2.13 + + # Activate env + micromamba activate ./.venv-tf + +Other TF versions are available, using the following targets `tf-2.10`, and `tf-2.11`. + + +PyTorch (+ Lightning) ++++++++++++++++++++++ + +Installation: + +.. code-block:: bash + + # Install PyTorch + lightning + make torch-gpu + + # Activate env + micromamba activate ./.venv-pytorch + +Other also CPU-only version is available at the target `torch-cpu`. + + +Development environment ++++++++++++++++++++++++ + +This is for developers only. To have it, update the installed `itwinai` package adding the `dev` extra: + +.. code-block:: bash + + pip install -e .[dev] + + +**Test with `pytest`** +To run tests on itwinai package: + +.. code-block:: bash + + # Activate env + micromamba activate ./.venv-pytorch # or ./.venv-tf + + pytest -v -m "not slurm" tests/ + + +However, some tests are intended to be executed only on an HPC system, where SLURM is available. They are marked with "slurm" tag. To run also those tests, use the dedicated job script: + +.. code-block:: bash + + sbatch tests/slurm_tests_startscript + + # Upon completion, check the output: + cat job.err + cat job.out + + + + +.. Workflow orchestrator +.. +++++++++++++++++++++ + +.. Install the (custom) orchestrator virtual environment. + +.. .. code-block:: bash + +.. source ~/.bashrc +.. # Create local env +.. make + +.. # Activate env +.. micromamba activate ./.venv + +.. To run tests on workflows use: + +.. .. code-block:: bash + +.. # Activate env +.. micromamba activate ./.venv + +.. pytest tests/ + + +.. Development env setup +.. --------------------- + +.. Requirements: + +.. * Linux, macOS environment. Windows was never tested. +.. * Micromamba: see the installation instructions above. +.. * VS Code, for development. + +.. Installation: + +.. .. code-block:: bash + +.. make dev-env + +.. # Activate env +.. micromamba activate ./.venv-dev + +.. To run tests on itwinai package: + +.. .. code-block:: bash + +.. # Activate env +.. micromamba activate ./.venv-dev + +.. pytest tests/ai/ + + +.. AI environment setup +.. -------------------- + +.. Requirements: + +.. * Linux, macOS environment. Windows was never tested. +.. * Micromamba: see the installation instructions above. +.. * VS Code, for development. + +.. **NOTE**: this environment gets automatically setup when a workflow is executed! + +.. However, you can also set it up explicitly with: + +.. .. code-block:: bash + +.. make ai-env + +.. # Activate env +.. micromamba activate ./ai/.venv-pytorch + +.. Updating the environment files +.. ++++++++++++++++++++++++++++++ + +.. The files under `ai/env-files/` are of two categories: + +.. * Simple environment definition, such as `pytorch-env.yml` and `pytorch-env-gpu.yml` +.. * Lockfiles, such as `pytorch-lock.yml` and `pytorch-gpu-lock.yml`, generated by `conda-lock `_. + +.. **When you install the ai environment, install it from the lock file!** + +.. When the "simple" environment file (e.g., `pytorch-env.yml`) changes, lock it with `conda-lock `_: + +.. .. code-block:: bash + +.. micromamba activate ./.venv + +.. make lock-ai + diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..32bb2452 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/mnist_doc.rst b/docs/mnist_doc.rst new file mode 100644 index 00000000..fd26c4e7 --- /dev/null +++ b/docs/mnist_doc.rst @@ -0,0 +1,151 @@ +MNIST +===== + +This section covers the MNIST use case, which utilizes the `torch-lightning` framework for training and evaluation. The following files are integral to this use case: + +torch-lightning +--------------- + +.. toctree:: + :maxdepth: 5 + +dataloader.py ++++++++++++++ + +The `dataloader.py` script is responsible for loading the MNIST dataset and preparing it for training. + +.. literalinclude:: ../use-cases/mnist/torch-lightning/dataloader.py + :language: python + +.. .. automodule:: torch-lightning.dataloader +.. :members: +.. :undoc-members: +.. :show-inheritance: + +pipeline.yaml ++++++++++++++ + +This YAML file defines the pipeline configuration for the MNIST use case. It includes settings for the model, training, and evaluation. + +.. literalinclude:: ../use-cases/mnist/torch-lightning/pipeline.yaml + :language: yaml + +startscript ++++++++++++ + +The `startscript` is a shell script to initiate the training process. It sets up the environment and starts the training using the `train.py` script. + +.. literalinclude:: ../use-cases/mnist/torch-lightning/startscript + :language: bash + +train.py +++++++++ + +This script contains the training loop and is where the model is trained using the data prepared by `dataloader.py`. + +.. literalinclude:: ../use-cases/mnist/torch-lightning/train.py + :language: python + +.. .. automodule:: torch-lightning.train +.. :members: +.. :undoc-members: +.. :show-inheritance: + +trainer.py +++++++++++ + +The `trainer.py` file defines the `Trainer` class which sets up the training parameters and the training process. + +.. literalinclude:: ../use-cases/mnist/torch-lightning/trainer.py + :language: python + +.. .. automodule:: torch-lightning.trainer +.. :members: +.. :undoc-members: +.. :show-inheritance: + +utils.py +++++++++ + +The `utils.py` script includes utility functions and classes that are used across the MNIST use case. + +.. literalinclude:: ../use-cases/mnist/torch-lightning/utils.py + :language: python + +.. .. automodule:: torch-lightning.utils +.. :members: +.. :undoc-members: +.. :show-inheritance: + + +This section covers the MNIST use case, which utilizes the `torch` framework for training and evaluation. The following files are integral to this use case: + +torch +----- + +.. toctree:: + :maxdepth: 5 + +dataloader.py ++++++++++++++ + +The `dataloader.py` script is responsible for loading the MNIST dataset and preparing it for training. + +.. literalinclude:: ../use-cases/mnist/torch/dataloader.py + :language: python + + +Dockerfile +++++++++++ + +.. literalinclude:: ../use-cases/mnist/torch/Dockerfile + :language: bash + + +inference-pipeline.yaml ++++++++++++++++++++++++ + +This YAML file defines the pipeline configuration for the MNIST use case inference. + +.. literalinclude:: ../use-cases/mnist/torch/inference-pipeline.yaml + :language: yaml + +model.py +++++++++ + +The `model.py` script is responsible for loading a simple model. + +.. literalinclude:: ../use-cases/mnist/torch/model.py + :language: python + +pipeline.yaml ++++++++++++++ + +This YAML file defines the pipeline configuration for the MNIST use case. It includes settings for the model, training, and evaluation. + +.. literalinclude:: ../use-cases/mnist/torch/pipeline.yaml + :language: yaml + +startscript ++++++++++++ + +The `startscript` is a shell script to initiate the training process. It sets up the environment and starts the training using the `train.py` script. + +.. literalinclude:: ../use-cases/mnist/torch/startscript + :language: bash + +train.py +++++++++ + +This script contains the training loop and is where the model is trained using the data prepared by `dataloader.py`. + +.. literalinclude:: ../use-cases/mnist/torch/train.py + :language: python + +saver.py +++++++++ +... + +.. literalinclude:: ../use-cases/mnist/torch/saver.py + :language: python + diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 00000000..a9e96c97 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,23 @@ +itwinai +======= + +.. toctree:: + :maxdepth: 4 + + itwinai.cli + itwinai.cluster + itwinai.components + itwinai.loggers + itwinai.parser + itwinai.pipeline + itwinai.serialization + itwinai.types + itwinai.utils + + +.. toctree:: + :maxdepth: 4 + + itwinai.tf.modules + itwinai.torch.modules + diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..31776555 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,185 @@ +absl-py==1.2.0 +alabaster==0.7.16 +alembic==1.8.1 +antlr4-python3-runtime==4.9.3 +anyio +appdirs==1.4.4 +arrow +attrs +autopage==0.5.1 +Babel==2.14.0 +backports.functools-lru-cache +beautifulsoup4 +blessed +blinker==1.6.3 +Brotli +build +CacheControl +certifi==2023.7.22 +cffi +charset-normalizer +cleo +click +cliff==4.0.0 +cloudpickle==2.2.1 +cmaes==0.8.2 +cmd2==2.4.2 +colorama +colorlog==6.7.0 +crashtest +croniter +cryptography +cycler==0.12.1 +databricks-cli==0.18.0 +dateutils +deepdiff +distlib +docker==6.1.3 +docker-pycreds==0.4.0 +docstring-parser==0.15 +docutils==0.20.1 +dulwich +einops==0.4.1 +entrypoints==0.4 +exceptiongroup +fastapi +filelock +Flask==2.3.3 +fonttools==4.37.4 +fsspec +gast==0.4.0 +gitdb==4.0.9 +GitPython==3.1.27 +google==3.0.0 +greenlet==1.1.3 +gunicorn==21.2.0 +h11 +h5py==3.7.0 +idna +imagesize==1.4.1 +importlib-metadata==5.0.0 +importlib-resources +inquirer +installer +itsdangerous +git+https://github.com/interTwin-eu/itwinai.git@a8f9ccb035c7736553eaafb12e06fd7b3fc73fb6#egg=itwinai +jaraco.classes +jeepney +Jinja2 +joblib==1.3.2 +jsonargparse==4.26.1 +jsonschema +keyring +kiwisolver==1.4.5 +libclang==14.0.6 +lightning +lightning-cloud +lightning-utilities +Mako==1.2.3 +Markdown==3.5 +markdown-it-py +MarkupSafe==2.1.1 +matplotlib==3.5.2 +mdurl +mlflow==2.7.1 +more-itertools +msgpack +mysqlclient==2.1.1 +numpy +oauthlib==3.2.2 +omegaconf==2.3.0 +optuna==2.10.1 +ordered-set +orjson +packaging +pandas==2.1.1 +pathtools==0.1.2 +pexpect +Pillow +pkginfo +pkgutil_resolve_name +platformdirs +plotly==5.10.0 +poetry +poetry-core +poetry-plugin-export +promise==2.3 +protobuf==4.24.4 +psutil +ptyprocess +pyarrow==13.0.0 +pycparser +pydantic +Pygments +PyJWT +PyMySQL==1.0.2 +pyparsing==3.1.1 +pyperclip==1.8.2 +pyproject_hooks +pyrsistent +PySocks +python-dateutil +python-editor==1.0.4 +python-multipart +pytorch-lightning +pytz +PyYAML +querystring-parser==1.2.4 +rapidfuzz +readchar +requests +requests-toolbelt +rich +scikit-learn==1.3.2 +scipy==1.12.0 +SecretStorage +sentry-sdk==1.9.10 +setproctitle==1.3.2 +shellingham +shortuuid==1.0.9 +six +smmap==5.0.0 +sniffio +snowballstemmer==2.2.0 +soupsieve +Sphinx==7.2.6 +sphinx-rtd-theme==2.0.0 +sphinxcontrib-applehelp==1.0.8 +sphinxcontrib-devhelp==1.0.6 +sphinxcontrib-htmlhelp==2.0.5 +sphinxcontrib-jquery==4.1 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.7 +sphinxcontrib-serializinghtml==1.1.10 +SQLAlchemy==1.4.41 +sqlparse==0.4.4 +starlette +starsessions +submitit==1.5.0 +tabulate==0.9.0 +tenacity==8.1.0 +tensorflow-io-gcs-filesystem==0.27.0 +threadpoolctl==3.2.0 +tomli +tomlkit +torch==1.13.1 +torchaudio==0.13.1 +torchmetrics +torchvision==0.14.1 +tqdm +traitlets +trove-classifiers +typer==0.9.0 +types-python-dateutil +typeshed-client==2.4.0 +typing_extensions==4.5.0 +tzdata==2023.3 +urllib3 +uvicorn +virtualenv +wandb==0.15.12 +wcwidth +websocket-client +websockets +Werkzeug==3.0.0 +zipp diff --git a/docs/tutorials.rst b/docs/tutorials.rst new file mode 100644 index 00000000..623582f2 --- /dev/null +++ b/docs/tutorials.rst @@ -0,0 +1,19 @@ +.. _tutorials: + +ML workflow tutorials +===================== + +Here you can find a collection of tutorials for various complexity ML workflows. + +Tutorials +--------- + +.. toctree:: + :maxdepth: 2 + + basic_comp + basic_workflow + intermediate_workflow + advanced_workflow + + \ No newline at end of file diff --git a/docs/use_cases.rst b/docs/use_cases.rst new file mode 100644 index 00000000..4a584702 --- /dev/null +++ b/docs/use_cases.rst @@ -0,0 +1,32 @@ +Integrated Use Cases +==================== + +Here you can find a collection of use cases including various projects. + +3DGAN CERN use case +------------------- + +The first ``interTwin`` use case integrated with ``itwinai`` framework is the DT for fast particle detector simulation. +3D Generative Adversarial Network (3DGAN) for generation of images of calorimeter depositions. +This project is based on the prototype `3DGAN `_ model developed at CERN and is implemented on PyTorch Lightning framework. + +.. toctree:: + :maxdepth: 2 + + 3dgan_doc + + +MNIST use case +-------------- + +MNIST image classification is used to provide an example on +how to define an end-to-end digital twin workflow with the ``itwinai`` software. + +.. toctree:: + :maxdepth: 2 + + mnist_doc + + + + diff --git a/use-cases/3dgan/__init__.py b/use-cases/3dgan/__init__.py new file mode 100644 index 00000000..079a8283 --- /dev/null +++ b/use-cases/3dgan/__init__.py @@ -0,0 +1 @@ +## This file can be empty but must be present \ No newline at end of file From 2479d0dbe4db578eaacb0b559e62ec2559796675 Mon Sep 17 00:00:00 2001 From: Matteo Bunino <48362942+matbun@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:19:49 +0200 Subject: [PATCH 8/8] Distributed strategy launcher (#131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ADD: distrib launcher mockup * REFACTOR: cluster env, strategy and launcher * ADD: Torch Elastic Launcher * ADD: info on env vars * ADD: distributed tooling and examples * new folder * UPDATE: distributed strategy setup * generalized for DDP and DS * add config file * UPDATE: kwargs * Update general_trainer.py * Update general_startscript * Update general_trainer.py * UPDATE .gitignore * Update distrib strategy * UPDATE torch distributed strategy classes * Updated docstrings * Small fixes * UPDATE docstrings * ADD deepespeed config loader * ADD first deepspeed tutorial draft * UPDATE DDP Dp distrib strategy * UPDATE horovod strategy * UPDATE tutorial on torch distributed strategies * UPDATE torch strategies tutorial * Update createEnvJSC.sh * Update hvd_slurm.sh * Update README.md * UPDATE distributed tutorial * Delete tutorials/distributed-ml/torch-ddp-deepspeed-horovod/0 * Fixes to deepspeed startscript * Update distributed.py * Update trainer.py * UPDATE tutorial * ADD draft MNIST tutorial * UPDATE DDP tutorial for MNIST * FIX small details * Update distributed.py * Added TF tutorials * Fixes to tutorials * Add files via upload * Update Makefile * Update README.md * UPDATE tutorials * UPDATE documentation and improve explainability * UPDATE SLURM scripts * FIX local rank mismatch * fixed distributed trainer in cyclones use case * UPDATE launcher * UPDATE linter * UPDATE format * FIX linter * FIX linter * Update workflow * UPDATE workflow * update * Update workflow * UPDATE super linter to v6 * UPDATE super linter to v6.3.0 * UPDATE super linter to slim * Cleanup * Update tfmirrored_slurm.sh * Update tfmirrored_slurm.sh * REMOVE workflows legacy * DELETE cyclegan use case * UPDATE dist training tutorials torch * RENAME folders with torch * DRAFT torch imagenet tutorial * UPDATE configuration * UPDATE imagenet tutorial * DRAFT scaling test * ADD scaling analysis report * FIX deepspeed micro batchsize * UPDATE data path * UPDATE checkpoint to avoid race conditions * UPDATE scalability report * UPDATE dataset path * Update createEnvJSC.sh * Update createEnvJSC.sh * Update createEnvJSC.sh * Update createEnvJSC.sh * Update createEnvJSC.sh * Update createEnvJSCTF.sh * Update README.md * Update README.md * JUBE benchmarks * Update createEnvJSC.sh * Update createEnvJSCTF.sh * ADD logy scale option * Extract JUBE tutorial * CLEANUP baselines * Log epoch time in real-time * FIX deepspeed dataloader for potential performances improvement * UPDATE SC bash severity * FIX deepspeed and horovod trainers * FIX some code checks * Unify redundant SLURM job scripts and configuration files * CLEANUP unused configuration * Reorg configurations * Refactor configurations and add documentation * Update README * ADD report image * Improve plot resolution * UPDATE scaling test * UPDATE launcher scripts * FIX linter * REMOVE jube tutorial * Restore ConfigParser * FIX type hinting * ADD dev dependencies * REMOVE experimental scripts * UPDATE scaling report * Add SLURM logs * Refactor log scale * Update scalability report * Unify SLURM logs per job * Update README.md * Update README.md * Update README.md * ADD itwinai installation * UPDATE torch distributed tutorial 0 * UPDATE torch distributed tutorials * REMOVE imagenet tutorial * ADD NonDistributedStrategy and create_dataloader method * CLEANUP older classes * Rename strategies * Simplify structure * ADD draft new torch trainer class * UPDATED torch trainer draft * UPDATE MNIST use case * INtegrate new trainer into MNIST use case * UPDATE structure: remove unused files and refactor tests * Tmp disable unused tests * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * Update action * FIX failing inference * Functiona tests (#133) * UPDATE tests * FIX errors * CLEANUP * Remove unused workflow --------- Co-authored-by: Mario Rüttgers Co-authored-by: r-sarma <126173968+r-sarma@users.noreply.github.com> Co-authored-by: r-sarma Co-authored-by: zoechbauer1 --- .github/linters/.jscpd.json | 3 +- .../{workflows-dt.yml => pytest.yml} | 4 +- README.md | 30 +- env-files/tensorflow/createEnvJSCTF.sh | 3 + src/itwinai/cli.py | 82 +- src/itwinai/cluster.py | 72 -- src/itwinai/components.py | 8 - src/itwinai/loggers.py | 31 +- src/itwinai/parser.py | 245 +--- src/itwinai/tensorflow/distributed.py | 16 +- src/itwinai/tensorflow/trainer.py | 27 +- src/itwinai/torch/cluster.py | 225 ---- src/itwinai/torch/distributed.py | 924 ++++++------- src/itwinai/torch/engine.py | 276 ---- src/itwinai/torch/inference.py | 3 +- src/itwinai/torch/mlflow.py | 2 + src/itwinai/torch/reproducibility.py | 48 + src/itwinai/torch/trainer.py | 1148 ++++++----------- src/itwinai/torch/types.py | 8 + src/itwinai/torch/utils.py | 84 -- src/itwinai/utils.py | 82 +- tests/components/test_components.py | 5 - tests/test_cli.py | 26 - tests/use-cases/conftest.py | 53 +- tests/use-cases/test_3dgan.py | 65 +- tests/use-cases/test_cyclones.py | 15 +- tests/use-cases/test_mnist.py | 88 +- .../torch-scaling-test/README.md | 15 +- .../torch-scaling-test/ddp_trainer.py | 7 +- .../torch-scaling-test/deepspeed_trainer.py | 9 +- .../torch-scaling-test/horovod_trainer.py | 7 +- .../torch-scaling-test/img/report.png | Bin 45730 -> 198864 bytes .../torch-scaling-test/itwinai_trainer.py | 51 +- .../torch-scaling-test/runall.sh | 46 +- .../torch-scaling-test/slurm.sh | 14 +- .../torch-scaling-test/utils.py | 34 - .../torch-tutorial-0-basics/README.md | 30 +- .../torch-tutorial-0-basics/ddp_slurm.sh | 66 - .../deepspeed_slurm.sh | 75 -- .../torch-tutorial-0-basics/hvd_slurm.sh | 60 - .../torch-tutorial-0-basics/runall.sh | 43 +- .../torch-tutorial-0-basics/slurm.sh | 117 ++ .../torch-tutorial-0-basics/train.py | 81 +- .../torch-tutorial-1-mnist/README.md | 30 +- .../torch-tutorial-1-mnist/config.yaml | 26 +- .../torch-tutorial-1-mnist/ddp_slurm.sh | 66 - .../torch-tutorial-1-mnist/deepspeed_slurm.sh | 74 -- .../torch-tutorial-1-mnist/hvd_slurm.sh | 60 - .../torch-tutorial-1-mnist/runall.sh | 43 +- .../torch-tutorial-1-mnist/slurm.sh | 116 ++ .../torch-tutorial-1-mnist/train.py | 542 +++----- .../torch-tutorial-2-imagenet/README.md | 47 - .../torch-tutorial-2-imagenet/config.yaml | 25 - .../torch-tutorial-2-imagenet/ddp_slurm.sh | 66 - .../deepspeed_slurm.sh | 74 -- .../torch-tutorial-2-imagenet/hvd_slurm.sh | 60 - .../torch-tutorial-2-imagenet/runall.sh | 6 - .../torch-tutorial-2-imagenet/scaling-test.sh | 11 - .../torch-tutorial-2-imagenet/train.py | 499 ------- tutorials/ml-workflows/basic_components.py | 6 - use-cases/3dgan/create_inference_sample.py | 23 + use-cases/3dgan/dataloader.py | 3 +- use-cases/3dgan/trainer.py | 6 - use-cases/cyclones/README.md | 12 + use-cases/cyclones/dataloader.py | 1 + use-cases/cyclones/trainer.py | 6 - use-cases/mnist/tensorflow/pipeline.yaml | 12 +- use-cases/mnist/tensorflow/trainer.py | 6 - use-cases/mnist/torch-lightning/README.md | 17 + .../{pipeline.yaml => config.yaml} | 4 +- use-cases/mnist/torch-lightning/dataloader.py | 2 +- use-cases/mnist/torch-lightning/train.py | 44 - use-cases/mnist/torch-lightning/trainer.py | 40 - use-cases/mnist/torch/Dockerfile | 6 +- use-cases/mnist/torch/README.md | 44 +- use-cases/mnist/torch/config.yaml | 99 ++ .../mnist/torch/create_inference_sample.py | 42 + use-cases/mnist/torch/dataloader.py | 2 +- use-cases/mnist/torch/inference-pipeline.yaml | 22 - use-cases/mnist/torch/pipeline.yaml | 56 - use-cases/mnist/torch/runall.sh | 39 + use-cases/mnist/torch/slurm.sh | 116 ++ use-cases/mnist/torch/train.py | 44 - 83 files changed, 2344 insertions(+), 4281 deletions(-) rename .github/workflows/{workflows-dt.yml => pytest.yml} (88%) delete mode 100644 src/itwinai/cluster.py delete mode 100644 src/itwinai/torch/cluster.py delete mode 100644 src/itwinai/torch/engine.py create mode 100644 src/itwinai/torch/reproducibility.py delete mode 100644 src/itwinai/torch/utils.py delete mode 100644 tests/test_cli.py delete mode 100644 tutorials/distributed-ml/torch-tutorial-0-basics/ddp_slurm.sh delete mode 100644 tutorials/distributed-ml/torch-tutorial-0-basics/deepspeed_slurm.sh delete mode 100644 tutorials/distributed-ml/torch-tutorial-0-basics/hvd_slurm.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-0-basics/slurm.sh delete mode 100644 tutorials/distributed-ml/torch-tutorial-1-mnist/ddp_slurm.sh delete mode 100644 tutorials/distributed-ml/torch-tutorial-1-mnist/deepspeed_slurm.sh delete mode 100644 tutorials/distributed-ml/torch-tutorial-1-mnist/hvd_slurm.sh create mode 100644 tutorials/distributed-ml/torch-tutorial-1-mnist/slurm.sh delete mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/README.md delete mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/config.yaml delete mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/ddp_slurm.sh delete mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/deepspeed_slurm.sh delete mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/hvd_slurm.sh delete mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/runall.sh delete mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/scaling-test.sh delete mode 100644 tutorials/distributed-ml/torch-tutorial-2-imagenet/train.py create mode 100644 use-cases/3dgan/create_inference_sample.py create mode 100644 use-cases/cyclones/README.md create mode 100644 use-cases/mnist/torch-lightning/README.md rename use-cases/mnist/torch-lightning/{pipeline.yaml => config.yaml} (96%) delete mode 100644 use-cases/mnist/torch-lightning/train.py delete mode 100644 use-cases/mnist/torch-lightning/trainer.py create mode 100644 use-cases/mnist/torch/config.yaml create mode 100644 use-cases/mnist/torch/create_inference_sample.py delete mode 100644 use-cases/mnist/torch/inference-pipeline.yaml delete mode 100644 use-cases/mnist/torch/pipeline.yaml create mode 100644 use-cases/mnist/torch/runall.sh create mode 100644 use-cases/mnist/torch/slurm.sh delete mode 100644 use-cases/mnist/torch/train.py diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json index 1a035770..8a003c54 100644 --- a/.github/linters/.jscpd.json +++ b/.github/linters/.jscpd.json @@ -1,7 +1,6 @@ { "threshold": 2.0, "ignore": [ - "**/itwinai/loggers.py", - "**/itwinai/torch/engine.py" + "**/itwinai/loggers.py" ] } \ No newline at end of file diff --git a/.github/workflows/workflows-dt.yml b/.github/workflows/pytest.yml similarity index 88% rename from .github/workflows/workflows-dt.yml rename to .github/workflows/pytest.yml index 53a72e43..ecee2bc1 100644 --- a/.github/workflows/workflows-dt.yml +++ b/.github/workflows/pytest.yml @@ -1,10 +1,12 @@ --- -name: Test workflows +name: Unit and integration tests on: pull_request: branches: [main, dev] +# TODO: use container and set custom TORCH_ENV and TF_ENV env variables + jobs: test-itwinai: name: Test itwinai with pytest diff --git a/README.md b/README.md index dc9a60dc..ce8b6684 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,35 @@ pip install -e .[dev] #### Test with `pytest` -To run tests on itwinai package: +Do this only if you are a developer wanting to test your code with pytest. + +First, you need to create virtual environments both for torch and tensorflow. +For instance, you can use: + +```bash +make torch-cpu +make make tf-2.13-cpu +``` + +To select the name of the torch and tf environments you can set the following +environment variables, which allow to run the tests in environments with +custom names which are different from `.venv-pytorch` and `.venv-tf`. + +```bash +export TORCH_ENV="my_torch_env" +export TF_ENV="my_tf_env" +``` + +Functional tests (marked with `pytest.mark.functional`) will be executed under +`/tmp/pytest` location to guarantee they are run in a clean environment. + +To run functional tests use: + +```bash +pytest -v tests/ -m "functional" +``` + +To run all tests on itwinai package: ```bash # Activate env diff --git a/env-files/tensorflow/createEnvJSCTF.sh b/env-files/tensorflow/createEnvJSCTF.sh index 8838347c..377940d4 100644 --- a/env-files/tensorflow/createEnvJSCTF.sh +++ b/env-files/tensorflow/createEnvJSCTF.sh @@ -104,5 +104,8 @@ if [ "$cont1" = true ] ; then pip3 install -r reqs_TF.txt --ignore-installed fi +# Install itwinai +pip install --upgrade pip +pip install -e .[dev] # eof diff --git a/src/itwinai/cli.py b/src/itwinai/cli.py index 275d853a..6c27d069 100644 --- a/src/itwinai/cli.py +++ b/src/itwinai/cli.py @@ -16,7 +16,7 @@ import typer -app = typer.Typer() +app = typer.Typer(pretty_exceptions_enable=False) @app.command() @@ -27,9 +27,6 @@ def scalability_report( plot_title: Annotated[Optional[str], typer.Option( help=("Plot name.") )] = None, - logy: Annotated[bool, typer.Option( - help=("Log scale on y axis.") - )] = False, skip_id: Annotated[Optional[int], typer.Option( help=("Skip epoch ID.") )] = None, @@ -43,15 +40,17 @@ def scalability_report( Example: >>> itwinai scalability-report --pattern="^epoch.+\\.csv$" --skip-id 0 \\ - >>> --plot-title "Some title" --logy --archive archive_name + >>> --plot-title "Some title" --archive archive_name """ # TODO: add max depth and path different from CWD import os import re + import glob import shutil import pandas as pd + import matplotlib import matplotlib.pyplot as plt - # import numpy as np + import numpy as np regex = re.compile(r'{}'.format(pattern)) combined_df = pd.DataFrame() @@ -83,7 +82,13 @@ def scalability_report( if plot_title is not None: fig.suptitle(plot_title) - for name in set(avg_times.name.values): + sp_up_ax.set_yscale("log") + sp_up_ax.set_xscale("log") + + markers = iter("ov^s*dXpD.+12348") + + series_names = sorted(set(avg_times.name.values)) + for name in series_names: df = avg_times[avg_times.name == name].drop(columns='name') # Debug @@ -104,32 +109,27 @@ def scalability_report( df["Efficiency"] = df["Threadscaled Sim. Time / s"].iloc[0] / \ df["Threadscaled Sim. Time / s"] - # Plot - # when lines are very close to each other - if logy: - sp_up_ax.semilogy( - df["NGPUs"].values, df["Speedup"].values, - marker='*', lw=1.0, label=name) - else: - sp_up_ax.plot( - df["NGPUs"].values, df["Speedup"].values, - marker='*', lw=1.0, label=name) - - if logy: - sp_up_ax.semilogy(df["NGPUs"].values, df["Speedup - ideal"].values, - ls='dashed', lw=1.0, c='k', label="ideal") - else: - sp_up_ax.plot(df["NGPUs"].values, df["Speedup - ideal"].values, - ls='dashed', lw=1.0, c='k', label="ideal") + sp_up_ax.plot( + df["NGPUs"].values, df["Speedup"].values, + marker=next(markers), lw=1.0, label=name, alpha=0.7) + + sp_up_ax.plot(df["NGPUs"].values, df["Speedup - ideal"].values, + ls='dashed', lw=1.0, c='k', label="ideal") sp_up_ax.legend(ncol=1) sp_up_ax.set_xticks(df["NGPUs"].values) - # sp_up_ax.set_yticks( - # np.arange(1, np.max(df["Speedup - ideal"].values) + 2, 1)) + sp_up_ax.get_xaxis().set_major_formatter( + matplotlib.ticker.ScalarFormatter()) sp_up_ax.set_ylabel('Speedup') sp_up_ax.set_xlabel('NGPUs (4 per node)') sp_up_ax.grid() + + # Sort legend + handles, labels = sp_up_ax.get_legend_handles_labels() + order = np.argsort(labels) + plt.legend([handles[idx] for idx in order], [labels[idx] for idx in order]) + plot_png = f"scaling_plot_{plot_title}.png" plt.tight_layout() plt.savefig(plot_png, bbox_inches='tight', format='png', dpi=300) @@ -151,6 +151,18 @@ def scalability_report( os.path.basename(csvfile))) shutil.copyfile(plot_png, os.path.join(archive, plot_png)) avg_times.to_csv(os.path.join(archive, "avg_times.csv"), index=False) + print("Archived AVG epoch times CSV") + + # Copy SLURM logs: *.err *.out files + if os.path.exists('logs_slurm'): + print("Archived SLURM logs") + shutil.copytree('logs_slurm', os.path.join(archive, 'logs_slurm')) + # Copy other SLURM logs + for ext in ['*.out', '*.err']: + for file in glob.glob(ext): + shutil.copyfile(file, os.path.join(archive, file)) + + # Create archive archive_name = shutil.make_archive( base_name=archive, # archive file name format='gztar', @@ -170,6 +182,11 @@ def exec_pipeline( help=("Key in the configuration file identifying " "the pipeline object to execute.") )] = "pipeline", + steps: Annotated[Optional[str], typer.Option( + help=("Run only some steps of the pipeline. Accepted values are " + "indices, python slices (e.g., 0:3 or 2:10:100), and " + "string names of steps.") + )] = None, print_config: Annotated[bool, typer.Option( help=("Print config to be executed after overrides.") )] = False, @@ -195,11 +212,14 @@ def exec_pipeline( # to find the local python files imported from the pipeline file import os import sys + import re + from .utils import str_to_slice sys.path.append(os.path.dirname(config)) sys.path.append(os.getcwd()) # Parse and execute pipeline from itwinai.parser import ConfigParser + overrides_list = overrides_list if overrides_list is not None else [] overrides = { k: v for k, v in map(lambda x: (x.split('=')[0], x.split('=')[1]), overrides_list) @@ -213,8 +233,18 @@ def exec_pipeline( print("#="*50) print() pipeline = parser.parse_pipeline(pipeline_nested_key=pipe_key) + if steps: + if not re.match(r"\d+(:\d+)?(:\d+)?", steps): + print(f"Looking for step name '{steps}'") + else: + steps = str_to_slice(steps) + pipeline = pipeline[steps] pipeline.execute() + # Cleanup PYTHONPATH + sys.path.pop() + sys.path.pop() + @app.command() def mlflow_ui( diff --git a/src/itwinai/cluster.py b/src/itwinai/cluster.py deleted file mode 100644 index 7b9f57e0..00000000 --- a/src/itwinai/cluster.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Cluster environments where to run AI workflows.""" - -from __future__ import annotations -from abc import ABCMeta, abstractmethod -import os -from contextlib import contextmanager - - -def setup_for_distributed(is_main): - """ - This function disables printing when not in master process - """ - import builtins as __builtin__ - builtin_print = __builtin__.print - - def print(*args, **kwself): - force = kwself.pop('force', False) - if is_main or force: - builtin_print(*args, **kwself) - - __builtin__.print = print - - -def handle_sigusr1(signum, frame): - os.system(f'scontrol requeue {os.getenv("SLURM_JOB_ID")}') - exit() - - -def handle_sigterm(signum, frame): - pass - - -class ClusterEnvironment(metaclass=ABCMeta): - port: int = -1 - ngpus_per_node: int = -1 - global_world_size: int = -1 - global_rank: int = -1 - local_world_size: int = -1 - local_rank: int = -1 - rnd_seed: int = None - distributed: bool = False - # This flag tells whether the user wants to use the GPU(s) - use_cuda: bool = False - - @property - def backend(self) -> str: - return self._backend - - @backend.setter - def backend(self, backend_name: str) -> None: - self._set_backend(backend_name) - - def _set_backend(self, backend_name: str) -> None: - # Override to implement sanitization - self._backend = backend_name - - @abstractmethod - def is_main_worker(self) -> bool: - """Tells if the current process is the main/master process.""" - pass - - @abstractmethod - def is_cuda_available(self) -> bool: - pass - - @abstractmethod - @contextmanager - def init_dist_gpu(self, *args, **kwargs): - pass - - def cleanup_resources(self): - pass diff --git a/src/itwinai/components.py b/src/itwinai/components.py index 1f41bacd..eca2e570 100644 --- a/src/itwinai/components.py +++ b/src/itwinai/components.py @@ -216,14 +216,6 @@ def execute( validation dataset, test dataset, trained model. """ - @abstractmethod - def save_state(self): - pass - - @abstractmethod - def load_state(self): - pass - class Predictor(BaseComponent): """Applies a pre-trained machine learning model to unseen data.""" diff --git a/src/itwinai/loggers.py b/src/itwinai/loggers.py index d5ed0008..7f86ffcb 100644 --- a/src/itwinai/loggers.py +++ b/src/itwinai/loggers.py @@ -4,13 +4,12 @@ import csv from abc import ABCMeta, abstractmethod from contextlib import contextmanager -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union, Literal import pickle import pathlib import wandb import mlflow -# import mlflow.keras BASE_EXP_NAME: str = 'unk_experiment' @@ -38,12 +37,12 @@ class Logger(LogMixin, metaclass=ABCMeta): """ savedir: str = None supported_types: List[str] # Supported logging 'kinds' - _log_freq: Union[int, str] + _log_freq: Union[int, Literal['epoch', 'batch']] def __init__( self, savedir: str = 'mllogs', - log_freq: Union[int, str] = 'epoch' + log_freq: Union[int, Literal['epoch', 'batch']] = 'epoch' ) -> None: self.savedir = savedir self.log_freq = log_freq @@ -120,7 +119,7 @@ class ConsoleLogger(Logger): def __init__( self, savedir: str = 'mllogs', - log_freq: Union[int, str] = 'epoch' + log_freq: Union[int, Literal['epoch', 'batch']] = 'epoch' ) -> None: savedir = os.path.join(savedir, 'simple-logger') super().__init__(savedir=savedir, log_freq=log_freq) @@ -190,7 +189,7 @@ def __init__( experiment_name: str = BASE_EXP_NAME, tracking_uri: Optional[str] = None, run_description: Optional[str] = None, - log_freq: Union[int, str] = 'epoch' + log_freq: Union[int, Literal['epoch', 'batch']] = 'epoch' ): savedir = os.path.join(savedir, 'mlflow') super().__init__(savedir=savedir, log_freq=log_freq) @@ -203,7 +202,7 @@ def __init__( saved_abs_path = os.path.abspath(self.savedir) self.tracking_uri = pathlib.Path(saved_abs_path).as_uri() # self.tracking_uri = "file://" + self.savedir - print(f'MLFLOW URI: {self.tracking_uri}') + # print(f'MLFLOW URI: {self.tracking_uri}') # TODO: for pytorch lightning: # mlflow.pytorch.autolog() @@ -317,7 +316,7 @@ def __init__( self, savedir: str = 'mllogs', project_name: str = BASE_EXP_NAME, - log_freq: Union[int, str] = 'epoch' + log_freq: Union[int, Literal['epoch', 'batch']] = 'epoch' ) -> None: savedir = os.path.join(savedir, 'wandb') super().__init__(savedir=savedir, log_freq=log_freq) @@ -376,7 +375,7 @@ class TensorBoardLogger(Logger): def __init__( self, savedir: str = 'mllogs', - log_freq: Union[int, str] = 'epoch' + log_freq: Union[int, Literal['epoch', 'batch']] = 'epoch' ) -> None: savedir = os.path.join(savedir, 'tensorboard') super().__init__(savedir=savedir, log_freq=log_freq) @@ -425,7 +424,7 @@ def __init__( self, loggers: List[Logger] ) -> None: - super().__init__(savedir='/.tmp_mllogs_LoggersCollection', log_freq=0) + super().__init__(savedir='/.tmp_mllogs_LoggersCollection', log_freq=1) self.loggers = loggers def should_log(self, batch_idx: int = None) -> bool: @@ -450,6 +449,18 @@ def log( **kwargs ) + def create_logger_context(self): + for logger in self.loggers: + logger.create_logger_context() + + def destroy_logger_context(self): + for logger in self.loggers: + logger.destroy_logger_context() + + def save_hyperparameters(self, params: Dict[str, Any]) -> None: + for logger in self.loggers: + logger.save_hyperparameters(params=params) + class EpochTimeTracker: def __init__(self, series_name: str, csv_file: str) -> None: diff --git a/src/itwinai/parser.py b/src/itwinai/parser.py index 0001627b..254e91a9 100644 --- a/src/itwinai/parser.py +++ b/src/itwinai/parser.py @@ -76,14 +76,11 @@ class ConfigParser: >>> init_args: >>> save_path: .tmp/ >>> - >>> - class_path: itwinai.torch.trainer.TorchTrainerMG + >>> - class_path: itwinai.torch.trainer.TorchTrainer >>> init_args: >>> model: >>> class_path: model.Net - >>> loss: - >>> class_path: torch.nn.NLLLoss - >>> init_args: - >>> reduction: mean + >>> >>> from itwinai.parser import ConfigParser >>> >>> parser = ConfigParser( @@ -244,241 +241,3 @@ def __init__( "-c", "--config", action=ActionConfigFile, help="Path to a configuration file in json or yaml format." ) - - -# class ConfigParser2: -# """ -# Deprecated: this pipeline structure does not allow for -# nested pipelines. However, it is more readable and the linking -# from name to step data could be achieved with OmegaConf. This -# could be reused in the future: left as example. - -# Parses a configuration file, merging the steps into -# the pipeline and returning a pipeline object. -# It also provides functionalities for dynamic override -# of fields by means of nested key notation. - -# Example: - -# >>> # pipeline.yaml -# >>> pipeline: -# >>> class_path: itwinai.pipeline.Pipeline -# >>> steps: [server, client] -# >>> -# >>> server: -# >>> class_path: mycode.ServerOptions -# >>> init_args: -# >>> host: localhost -# >>> port: 80 -# >>> -# >>> client: -# >>> class_path: mycode.ClientOptions -# >>> init_args: -# >>> url: http://${server.init_args.host}:${server.init_args.port}/ - -# >>> from itwinai.parser import ConfigParser2 -# >>> -# >>> parser = ConfigParser2( -# >>> config='pipeline.yaml', -# >>> override_keys={ -# >>> 'server.init_args.port': 777 -# >>> } -# >>> ) -# >>> pipeline = parser.parse_pipeline() -# >>> print(pipeline) -# >>> print(pipeline.steps) -# >>> print(pipeline.steps['server'].port) -# >>> -# >>> server = parser.parse_step('server') -# >>> print(server) -# >>> print(server.port) -# """ - -# config: Dict -# pipeline: Pipeline - -# def __init__( -# self, -# config: Union[str, Dict], -# override_keys: Optional[Dict[str, Any]] = None -# ) -> None: -# self.config = config -# self.override_keys = override_keys -# if isinstance(self.config, str): -# self.config = load_yaml(self.config) -# self._dynamic_override_keys() -# self._omegaconf_interpolate() - -# def _dynamic_override_keys(self): -# if self.override_keys is not None: -# for key_chain, value in self.override_keys.items(): -# add_replace_field(self.config, key_chain, value) - -# def _omegaconf_interpolate(self) -> None: -# """Performs variable interpolation with OmegaConf on internal -# configuration file. -# """ -# conf = OmegaConf.create(self.config) -# self.config = OmegaConf.to_container(conf, resolve=True) - -# def parse_pipeline( -# self, -# pipeline_nested_key: str = "pipeline", -# verbose: bool = False -# ) -> Pipeline: -# """Merges steps into pipeline and parses it. - -# Args: -# pipeline_nested_key (str, optional): nested key in the -# configuration file identifying the pipeline object. -# Defaults to "pipeline". -# verbose (bool): if True, prints the assembled pipeline -# to console formatted as JSON. - -# Returns: -# Pipeline: instantiated pipeline. -# """ -# pipe_parser = JAPArgumentParser() -# pipe_parser.add_subclass_arguments(Pipeline, pipeline_nested_key) -# pipe_dict = self.config[pipeline_nested_key] - -# # Pop steps list from pipeline dictionary -# steps_list = pipe_dict['steps'] -# del pipe_dict['steps'] - -# # Link steps with respective dictionaries -# if not pipe_dict.get('init_args'): -# pipe_dict['init_args'] = {} -# steps_dict = pipe_dict['init_args']['steps'] = {} -# for step_name in steps_list: -# steps_dict[step_name] = self.config[step_name] -# pipe_dict = {pipeline_nested_key: pipe_dict} - -# if verbose: -# print("Assembled pipeline:") -# print(json.dumps(pipe_dict, indent=4)) - -# # Parse pipeline dict once merged with steps -# conf = pipe_parser.parse_object(pipe_dict) -# pipe = pipe_parser.instantiate_classes(conf) -# self.pipeline = pipe[pipeline_nested_key] -# return self.pipeline - -# def parse_step( -# self, -# step_name: str, -# verbose: bool = False -# ) -> BaseComponent: -# step_dict_config = self.config[step_name] - -# if verbose: -# print(f"STEP '{step_name}' CONFIG:") -# print(json.dumps(step_dict_config, indent=4)) - -# # Wrap config under "step" field and parse it -# step_dict_config = {'step': step_dict_config} -# step_parser = JAPArgumentParser() -# step_parser.add_subclass_arguments(BaseComponent, "step") -# parsed_namespace = step_parser.parse_object(step_dict_config) -# return step_parser.instantiate_classes(parsed_namespace)["step"] - - -# class ItwinaiCLI2: -# """ -# Deprecated: the dynamic override does not work with nested parameters -# and may be confusing. - -# CLI tool for executing a configuration file, with dynamic -# override of fields and variable interpolation with Omegaconf. - -# Example: - -# >>> # train.py -# >>> from itwinai.parser import ItwinaiCLI -# >>> cli = ItwinaiCLI() -# >>> cli.pipeline.execute() - -# >>> # pipeline.yaml -# >>> pipeline: -# >>> class_path: itwinai.pipeline.Pipeline -# >>> steps: [server, client] -# >>> -# >>> server: -# >>> class_path: mycode.ServerOptions -# >>> init_args: -# >>> host: localhost -# >>> port: 80 -# >>> -# >>> client: -# >>> class_path: mycode.ClientOptions -# >>> init_args: -# >>> url: http://${server.init_args.host}:${server.init_args.port}/ - -# From command line: - -# >>> python train.py --config itwinai-conf.yaml --help -# >>> python train.py --config itwinai-conf.yaml -# >>> python train.py --config itwinai-conf.yaml --server.port 8080 -# """ -# _parser: JAPArgumentParser -# _config: Dict -# pipeline: Pipeline - -# def __init__( -# self, -# pipeline_nested_key: str = "pipeline", -# parser_mode: str = "omegaconf" -# ) -> None: -# self.pipeline_nested_key = pipeline_nested_key -# self.parser_mode = parser_mode -# self._init_parser() -# self._parser.add_argument(f"--{self.pipeline_nested_key}", type=dict) -# self._add_steps_arguments() -# self._config = self._parser.parse_args() - -# # Merge steps into pipeline and parse it -# del self._config['config'] -# pipe_parser = ConfigParser2(config=self._config.as_dict()) -# self.pipeline = pipe_parser.parse_pipeline( -# pipeline_nested_key=self.pipeline_nested_key -# ) - -# def _init_parser(self): -# self._parser = JAPArgumentParser(parser_mode=self.parser_mode) -# self._parser.add_argument( -# "-c", "--config", action=ActionConfigFile, -# required=True, -# help="Path to a configuration file in json or yaml format." -# ) - -# def _add_steps_arguments(self): -# """Pre-parses the configuration file, dynamically adding all the -# component classes under 'steps' as arguments of the parser. -# """ -# if "--config" not in sys.argv: -# raise ValueError( -# "--config parameter has to be specified with a " -# "valid path to a configuration file." -# ) -# config_path = sys.argv.index("--config") + 1 -# config_path = sys.argv[config_path] -# config = load_yaml(config_path) - -# # Add steps to parser -# steps = filter( -# lambda itm: itm[0] != self.pipeline_nested_key, -# config.items() -# ) -# steps = { -# step_name: step_data['class_path'] -# for step_name, step_data in steps -# } - -# for st_nested_key, step_class_str in steps.items(): -# step_class = dynamically_import_class(step_class_str) -# self._add_step_arguments( -# step_class=step_class, nested_key=st_nested_key) - -# def _add_step_arguments(self, step_class, nested_key): -# self._parser.add_subclass_arguments( -# baseclass=step_class, nested_key=nested_key) diff --git a/src/itwinai/tensorflow/distributed.py b/src/itwinai/tensorflow/distributed.py index e6c5f28a..64945ca8 100644 --- a/src/itwinai/tensorflow/distributed.py +++ b/src/itwinai/tensorflow/distributed.py @@ -1,17 +1,23 @@ -import tensorflow as tf import os +import tensorflow as tf +import tensorflow.distribute as dist def get_strategy(): """Strategy for distributed TensorFlow training""" - cluster_resolver = tf.distribute.cluster_resolver.SlurmClusterResolver( + if not os.environ.get('SLURM_JOB_ID'): + # TODO: improve + print('not in SLURM env!') + tf_dist_strategy = dist.MirroredStrategy() + return tf_dist_strategy, tf_dist_strategy.num_replicas_in_sync + cluster_resolver = dist.cluster_resolver.SlurmClusterResolver( port_base=12345) - implementation = tf.distribute.experimental.CommunicationImplementation.NCCL - communication_options = tf.distribute.experimental.CommunicationOptions( + implementation = dist.experimental.CommunicationImplementation.NCCL + communication_options = dist.experimental.CommunicationOptions( implementation=implementation) # declare distribution strategy - tf_dist_strategy = tf.distribute.MultiWorkerMirroredStrategy( + tf_dist_strategy = dist.MultiWorkerMirroredStrategy( cluster_resolver=cluster_resolver, communication_options=communication_options ) diff --git a/src/itwinai/tensorflow/trainer.py b/src/itwinai/tensorflow/trainer.py index d8c40012..51bfb97c 100644 --- a/src/itwinai/tensorflow/trainer.py +++ b/src/itwinai/tensorflow/trainer.py @@ -28,12 +28,19 @@ def instance_from_dict(obj_dict: Any) -> Any: return obj_dict +# TODO: the TF trainer is incomplete: +# - strategy is not received from constructor argument: if not needed, +# remove it +# - dataset is not distributed +# - much commented code that has to be removed or included + + class TensorflowTrainer(Trainer): def __init__( self, epochs, - train_dataset, - validation_dataset, + # train_dataset, + # validation_dataset, batch_size, callbacks, model_dict: Dict, @@ -61,14 +68,14 @@ def __init__( # get total number of workers print("Number of devices: {}".format(n_devices)) # distribute datasets among MirroredStrategy's replicas - dist_train_dataset = ( - tf_dist_strategy.experimental_distribute_dataset( - train_dataset - )) - dist_validation_dataset = ( - tf_dist_strategy.experimental_distribute_dataset( - validation_dataset - )) + # dist_train_dataset = ( + # tf_dist_strategy.experimental_distribute_dataset( + # train_dataset + # )) + # dist_validation_dataset = ( + # tf_dist_strategy.experimental_distribute_dataset( + # validation_dataset + # )) with self.strategy.scope(): # TODO: move loss, optimizer and metrics instantiation under # here diff --git a/src/itwinai/torch/cluster.py b/src/itwinai/torch/cluster.py deleted file mode 100644 index aece16e2..00000000 --- a/src/itwinai/torch/cluster.py +++ /dev/null @@ -1,225 +0,0 @@ -"""Cluster environments where to run AI workflows. Partially adapted from: -https://github.com/facebookresearch/detr/blob/master/util/misc.py and -https://github.com/ramyamounir/Template/blob/main/lib/utils/distributed.py -""" - -from __future__ import annotations -from typing import Optional -import os -import signal -import subprocess -from pathlib import Path -from contextlib import contextmanager - -import numpy as np - -import torch -import torch.distributed as dist -import torch.backends.cudnn as cudnn - -from ..cluster import ( - ClusterEnvironment, - setup_for_distributed, - handle_sigusr1, - handle_sigterm -) -from .types import TorchDistributedBackend as BackendT - - -def fix_random_seeds(seed=31): - """ - Fix random seeds. - """ - torch.manual_seed(seed) - torch.cuda.manual_seed_all(seed) - np.random.seed(seed) - - -class TorchCluster(ClusterEnvironment): - def __init__(self) -> None: - super().__init__() - - def _set_backend(self, backend_name: str) -> None: - if backend_name not in BackendT: - raise ValueError( - "Unrecognized 'backend' field. Allowed values " - f"are: {BackendT.list()}. Received '{backend_name}'") - self._backend = backend_name - - def is_cuda_available(self) -> bool: - return self.use_cuda and torch.cuda.is_available() - - def is_main_worker(self) -> bool: - """Checks if the current process is the main/master process - in the whole job. - """ - return self.global_rank == 0 - - def cleanup_resources(self): - dist.barrier() - dist.destroy_process_group() - - -class LocalCluster(TorchCluster): - """Simple single node cluster with optional access to multiple GPUs.""" - - def __init__( - self, - backend: Optional[str] = None, - gpus: Optional[str] = '', - port: int = 49153, - rnd_seed: Optional[int] = 42 - ) -> None: - """Initialize local cluster for multi-GPU access. - - Args: - backend (Optional[str], optional): supported PyTorch backends. - If None, workload is not distributed. Defaults to None. - gpus (Optional[str], optional): list of visible GPU devices - (e.g., '1,2,3'). If empty string uses all available GPUs. - If None, CPU is used. Defaults to ''. - port (int, optional): TCP port used by the master process. - Defaults to 49153. - rnd_seed (Optional[int], optional): random seed to be setup after - all processes are setup. Defaults to 42. - """ - super().__init__() - self.backend = backend - self.gpus = gpus - self.port = port - self.dist_url = f'tcp://127.0.0.1:{self.port}' - self.rnd_seed = rnd_seed - - if self.gpus != '' and self.gpus is not None: - # Restrict the number of GPUs visible according to user needs - os.environ['CUDA_VISIBLE_DEVICES'] = self.gpus - - self.ngpus_per_node = torch.cuda.device_count() - self.global_rank = 0 - self.global_world_size = self.ngpus_per_node - - print(f"{self.ngpus_per_node} GPUs are available.") - self.distributed = True - # This flag tells whether the user wants to use the GPU(s) - self.use_cuda = ( - self.gpus is not None # GPU is not manually disabled - and torch.cuda.device_count() >= 1 # At least one GPU is selected - ) - if self.backend is None or self.ngpus_per_node <= 1: - print("Distributed has been disabled.") - self.distributed = False - self.dist_url = None - self.global_world_size = 1 - self.global_rank = 0 - if not self.is_cuda_available(): - print("CUDA disabled... Running on single CPU.") - self.use_cuda = False - self.distributed = False - self.dist_url = None - self.global_world_size = 1 - self.global_rank = 0 - - # Since single node case - self.local_world_size = self.global_world_size - - @contextmanager - def init_dist_gpu(self, worker_id) -> torch.device: - if self.distributed: - torch.cuda.set_device(worker_id) - self.global_rank += worker_id - # print(f'GLOBAL RANK: {self.global_rank}') - # Since single node case - self.local_rank = self.global_rank - # Simplification: worker ID mapped to GPU ID - self.gpu_id = worker_id - - try: - dist.init_process_group( - backend=self.backend, - init_method=self.dist_url, - world_size=self.global_world_size, - rank=self.global_rank - ) - fix_random_seeds(self.rnd_seed) - torch.cuda.set_device(self.gpu_id) - cudnn.benchmark = True - dist.barrier() - - setup_for_distributed(self.is_main_worker()) - print("SETUP DISTRIBUTED COMPLETE") - yield torch.device('cuda', worker_id) - finally: - self.cleanup_resources() - else: - # Distributed is disabled - # Since single node case - self.global_rank = 0 - self.local_rank = self.global_rank - if self.use_cuda: - torch.cuda.set_device(worker_id) - yield torch.device('cuda', worker_id) - else: - yield torch.device('cpu') - - -class SLURMCluster(TorchCluster): - """SLURM cluster with access to multi-node multi-GPU.""" - - def __init__( - self, - port: int = 49153, - backend: str = 'gloo', - rnd_seed: Optional[int] = 42 - ) -> None: - super().__init__() - self.port = port - self.backend = backend - self.rnd_seed = rnd_seed - if 'SLURM_JOB_ID' not in os.environ: - raise RuntimeError( - "'SLURM_JOB_ID' environment variable is not set. " - "Perhaps you are not running in a slurm cluster?" - ) - - self.ngpus_per_node = torch.cuda.device_count() - - # requeue job on SLURM preemption - signal.signal(signal.SIGUSR1, handle_sigusr1) - signal.signal(signal.SIGTERM, handle_sigterm) - - # find a common host name on all nodes - cmd = 'scontrol show hostnames ' + os.getenv('SLURM_JOB_NODELIST') - stdout = subprocess.check_output(cmd.split()) - host_name = stdout.decode().splitlines()[0] - self.dist_url = f'tcp://{host_name}:{self.port}' - - # distributed parameters - self.global_rank = int(os.getenv('SLURM_NODEID')) * self.ngpus_per_node - self.global_world_size = int( - os.getenv('SLURM_NNODES')) * self.ngpus_per_node - - @contextmanager - def init_dist_gpu(self): - import submitit - try: - job_env = submitit.JobEnvironment() - self.output_dir = Path( - str(self.output_dir).replace("%j", str(job_env.job_id))) - self.gpu = job_env.local_rank - self.global_rank = job_env.global_rank - - dist.init_process_group( - backend=self.backend, - init_method=self.dist_url, - world_size=self.global_world_size, - rank=self.global_rank - ) - fix_random_seeds(self.rnd_seed) - torch.cuda.set_device(self.gpu) - cudnn.benchmark = True - dist.barrier() - - setup_for_distributed(self.is_main_worker()) - yield - finally: - self.cleanup_resources() diff --git a/src/itwinai/torch/distributed.py b/src/itwinai/torch/distributed.py index 34174346..3bb48647 100644 --- a/src/itwinai/torch/distributed.py +++ b/src/itwinai/torch/distributed.py @@ -1,5 +1,5 @@ import abc -from typing import Any, List, Optional, Tuple +from typing import Any, List, Optional, Tuple, Union, Iterable from pathlib import Path import json import os @@ -12,18 +12,47 @@ import torch.optim as optim from torch.optim.lr_scheduler import _LRScheduler as LRScheduler from torch.optim.optimizer import Optimizer +from torch.utils.data import Dataset, Sampler, DistributedSampler, DataLoader +from torch.utils.data.dataloader import T_co, _worker_init_fn_t, _collate_fn_t from ..distributed import DistributedStrategy +from .types import UninitializedStrategyError, DistributedStrategyError + + +def distributed_resources_available() -> bool: + """Check if the current execution environment + has (enough) GPUs available to allow for distributed ML. + + Returns: + bool: env can support distributed ML. + """ + if torch.cuda.is_available() and torch.cuda.device_count() > 1: + return True + return False class TorchDistributedStrategy(DistributedStrategy): """Abstract class to define the distributed backend methods for PyTorch models. """ + is_distributed: bool = True + is_initialized: bool = False + + @property + def is_main_worker(self) -> bool: + """Checks if local worker has global rank equal to zero. + + Returns: + bool: True if main worker. + """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") + return self.global_rank() == 0 + @abc.abstractmethod def init(self) -> None: """Initializes the chosen distributed backend""" - # @abc.abstractmethod # def distributed_engine( # self, model: nn.Module, optimizer: Optimizer, @@ -39,7 +68,7 @@ def distributed( """Setup model, optimizer and scheduler for distributed.""" @abc.abstractmethod - def dist_gwsize(self) -> int: + def global_world_size(self) -> int: """Returns the total number of processes (global world size). Returns: @@ -47,7 +76,7 @@ def dist_gwsize(self) -> int: """ @abc.abstractmethod - def dist_lwsize(self) -> int: + def local_world_size(self) -> int: """Returns the number of local workers available on a node (local world size). Usually it is equal to the number of available GPUs. @@ -57,7 +86,7 @@ def dist_lwsize(self) -> int: """ @abc.abstractmethod - def dist_grank(self) -> int: + def global_rank(self) -> int: """Returns the global rank of the current process. Rank ranges from 0 to world_size. @@ -66,28 +95,182 @@ def dist_grank(self) -> int: """ @abc.abstractmethod - def dist_lrank(self) -> int: + def local_rank(self) -> int: """Returns the local rank of the current process. Returns: int: local rank. """ - def is_main_worker(self) -> bool: - """Checks if local worker has global rank equal to zero. - - Returns: - bool: True if main worker. - """ - return self.dist_grank() == 0 - - def dist_device(self) -> str: + def device(self) -> str: """Device used by local worker. Returns: str: torch device in the form 'cuda:N'. """ - return f"cuda:{self.dist_lrank()}" + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") + return f"cuda:{self.local_rank()}" + + def create_dataloader( + self, dataset: Dataset[T_co], batch_size: Optional[int] = 1, + shuffle: Optional[bool] = None, + sampler: Union[Sampler, Iterable, None] = None, + batch_sampler: Union[Sampler[List], Iterable[List], None] = None, + num_workers: int = 0, collate_fn: Optional[_collate_fn_t] = None, + pin_memory: bool = False, drop_last: bool = False, + timeout: float = 0, + worker_init_fn: Optional[_worker_init_fn_t] = None, + multiprocessing_context=None, generator=None, + *, prefetch_factor: Optional[int] = None, + persistent_workers: bool = False, + pin_memory_device: str = "" + ): + """Create a distributed DataLoader by using ``DistributedSampler`` as + random sampler. + + Args: + dataset (Dataset): dataset from which to load the data. + batch_size (int, optional): how many samples per batch to load + (default: ``1``). + shuffle (bool, optional): set to ``True`` to have the data + reshuffled at every epoch (default: ``False``). + sampler (Sampler or Iterable, optional): defines the strategy to + draw + samples from the dataset. Can be any ``Iterable`` with + ``__len__`` + implemented. If specified, :attr:`shuffle` must not be + specified. + batch_sampler (Sampler or Iterable, optional): like + :attr:`sampler`, but + returns a batch of indices at a time. Mutually exclusive with + :attr:`batch_size`, :attr:`shuffle`, :attr:`sampler`, + and :attr:`drop_last`. + num_workers (int, optional): how many subprocesses to use for data + loading. ``0`` means that the data will be loaded in the main + process. (default: ``0``) + collate_fn (Callable, optional): merges a list of samples to form a + mini-batch of Tensor(s). Used when using batched loading from + a map-style dataset. + pin_memory (bool, optional): If ``True``, the data loader will + copy Tensors + into device/CUDA pinned memory before returning them. If your + data elements + are a custom type, or your :attr:`collate_fn` returns a batch + that is a custom type, + see the example below. + drop_last (bool, optional): set to ``True`` to drop the last + incomplete batch, + if the dataset size is not divisible by the batch size. + If ``False`` and + the size of dataset is not divisible by the batch size, then + the last batch + will be smaller. (default: ``False``) + timeout (numeric, optional): if positive, the timeout value for + collecting a batch + from workers. Should always be non-negative. (default: ``0``) + worker_init_fn (Callable, optional): If not ``None``, + this will be called on each + worker subprocess with the worker id (an int in + ``[0, num_workers - 1]``) as + input, after seeding and before data loading. + (default: ``None``) + multiprocessing_context (str or + multiprocessing.context.BaseContext, optional): If + ``None``, the default `multiprocessing context`_ of + your operating system will + be used. (default: ``None``) + generator (torch.Generator, optional): If not ``None``, + this RNG will be used + by RandomSampler to generate random indexes and + multiprocessing to generate + ``base_seed`` for workers. (default: ``None``) + prefetch_factor (int, optional, keyword-only arg): Number of + batches loaded + in advance by each worker. ``2`` means there will be a total of + 2 * num_workers batches prefetched across all workers. + (default value depends + on the set value for num_workers. If value of num_workers=0 + default is ``None``. + Otherwise, if value of ``num_workers > 0`` default is ``2``). + persistent_workers (bool, optional): If ``True``, the data loader + will not shut down + the worker processes after a dataset has been consumed once. + This allows to + maintain the workers `Dataset` instances alive. + (default: ``False``) + pin_memory_device (str, optional): the device to + :attr:`pin_memory` to if ``pin_memory`` is ``True``. + + + .. warning:: If the ``spawn`` start method is used, + :attr:`worker_init_fn` + cannot be an unpicklable object, e.g., a lambda function. + See :ref:`multiprocessing-best-practices` on more + details related to multiprocessing in PyTorch. + + .. warning:: ``len(dataloader)`` heuristic is based on the length of + the sampler used. + When :attr:`dataset` is an + :class:`~torch.utils.data.IterableDataset`, + it instead returns an estimate based on + ``len(dataset) / batch_size``, with proper + rounding depending on :attr:`drop_last`, regardless + of multi-process loading + configurations. This represents the best guess PyTorch + can make because PyTorch + trusts user :attr:`dataset` code in correctly handling + multi-process + loading to avoid duplicate data. + + However, if sharding results in multiple workers having + incomplete last batches, + this estimate can still be inaccurate, because (1) an + otherwise complete batch can + be broken into multiple ones and (2) more than one batch + worth of samples can be + dropped when :attr:`drop_last` is set. Unfortunately, + PyTorch can not detect such cases in general. + + See `Dataset Types`_ for more details on these two + types of datasets and how + :class:`~torch.utils.data.IterableDataset` interacts with + `Multi-process data loading`_. + + .. warning:: See :ref:`reproducibility`, and + :ref:`dataloader-workers-random-seed`, and + :ref:`data-loading-randomness` notes for random + seed related questions. + + .. _multiprocessing context: + https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods + """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") + + if self.is_distributed: + if sampler is not None: + raise RuntimeError( + "User-provided sampler is not supported." + ) + sampler = DistributedSampler( + dataset, num_replicas=self.global_world_size(), + rank=self.global_rank(), + shuffle=shuffle + ) + # shuffle and batch_sampler must be unset + return DataLoader( + dataset=dataset, batch_size=batch_size, sampler=sampler, + num_workers=num_workers, collate_fn=collate_fn, + pin_memory=pin_memory, drop_last=drop_last, timeout=timeout, + worker_init_fn=worker_init_fn, + multiprocessing_context=multiprocessing_context, + generator=generator, prefetch_factor=prefetch_factor, + persistent_workers=persistent_workers, + pin_memory_device=pin_memory_device + ) @abc.abstractmethod def clean_up(self) -> None: @@ -105,8 +288,8 @@ def par_allgather_obj(self, obj: Any) -> List[Any]: """ -class DDPDistributedStrategy(TorchDistributedStrategy): - """PyTorch DDP distributed strategy class. +class TorchDDPStrategy(TorchDistributedStrategy): + """PyTorch ``DistributedDataParallel`` distributed strategy class. Args: backend (str): Name of the communication backend to employ. @@ -121,12 +304,21 @@ def __init__(self, backend: str) -> None: def init(self) -> None: """Initializes the distributed process group and the distributed package. + + Raises: + RuntimeError: when there are not (enough) GPUs available. + DistributedStrategyError: when trying to initialize a strategy + already initialized. """ - if torch.cuda.is_available() and torch.cuda.device_count() > 1: - dist.init_process_group(backend=self.backend) - else: - print("WARNING: trying to run distributed on insufficient" - " resources. Skipping distributed process group setup.") + if not distributed_resources_available(): + raise RuntimeError( + "Trying to run distributed on insufficient resources.") + if self.is_initialized: + raise DistributedStrategyError("Strategy was already initialized") + dist.init_process_group(backend=self.backend) + self.is_initialized = True + + torch.cuda.device(self.local_rank()) # def distributed_engine( # self, model: nn.Module, optimizer: Optimizer, @@ -158,55 +350,73 @@ def distributed( **kwargs ) -> Tuple[nn.Module, Optimizer, Optional[LRScheduler]]: """Setup model, optimizer and scheduler for distributed.""" + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") if torch.cuda.is_available(): # device = self.dist_lrank() - model = model.to(self.dist_device()) + model = model.to(self.device()) dist_model = torch.nn.parallel.DistributedDataParallel( model, - device_ids=[self.dist_device()], - output_device=self.dist_device() + device_ids=[self.device()], + output_device=self.device() ) else: dist_model = model return dist_model, optimizer, lr_scheduler - def dist_gwsize(self) -> int: + def global_world_size(self) -> int: """Returns the total number of processes (global world size). Returns: int: global world size. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return dist.get_world_size() - def dist_lwsize(self) -> int: + def local_world_size(self) -> int: """Returns the local number of workers available per node, which is usually the number of GPUs available. Returns: int: local world size. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return torch.cuda.device_count() - def dist_grank(self) -> int: + def global_rank(self) -> int: """Returns the global rank of the current process, where rank ranges from 0 to world_size. Returns: int: global rank. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return dist.get_rank() - def dist_lrank(self) -> int: + def local_rank(self) -> int: """Returns the local rank of the current process. Returns: int: local rank. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return dist.get_rank() % torch.cuda.device_count() def clean_up(self) -> None: """Destroys the current process group.""" + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") if torch.cuda.is_available(): dist.barrier() dist.destroy_process_group() @@ -221,12 +431,15 @@ def par_allgather_obj(self, obj: Any) -> List[Any]: Returns: List[Any]: List of gathered objects. """ - res = [None] * self.dist_gwsize() + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") + res = [None] * self.global_world_size() dist.all_gather_object(res, obj) return res -class DSDistributedStrategy(TorchDistributedStrategy): +class DeepSpeedStrategy(TorchDistributedStrategy): """DeepSpeed distributed strategy class. Args: @@ -256,7 +469,19 @@ def _load_config(self, ds_config) -> None: def init(self) -> None: """Initializes the distributed process group and the distributed package. + + Raises: + RuntimeError: when there are not (enough) GPUs available. + DistributedStrategyError: when trying to initialize a strategy + already initialized. """ + if not distributed_resources_available(): + raise RuntimeError( + "Trying to run distributed on insufficient resources.") + + if self.is_initialized: + raise DistributedStrategyError("Strategy was already initialized") + # https://github.com/Lightning-AI/pytorch-lightning/issues/13567 ompi_lrank = os.environ.get('OMPI_COMM_WORLD_LOCAL_RANK') os.environ['OMPI_COMM_WORLD_LOCAL_RANK'] = os.environ.get( @@ -264,6 +489,9 @@ def init(self) -> None: # https://deepspeed.readthedocs.io/en/latest/initialize.html#training-initialization deepspeed.init_distributed(dist_backend=self.backend) + self.is_initialized = True + + torch.cuda.device(self.local_rank()) def distributed( self, model: nn.Module, optimizer: Optional[Optimizer] = None, @@ -272,6 +500,10 @@ def distributed( **init_kwargs ) -> Tuple[nn.Module, Optimizer, Optional[LRScheduler]]: """Setup model, optimizer and scheduler for distributed.""" + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") + if init_kwargs.get("config"): self._load_config(init_kwargs.get("config")) # https://deepspeed.readthedocs.io/en/latest/initialize.html#training-initialization @@ -286,42 +518,57 @@ def distributed( ) return distrib_model, optimizer, lr_scheduler - def dist_gwsize(self) -> int: + def global_world_size(self) -> int: """Returns the total number of processes (global world size). Returns: int: global world size. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return dist.get_world_size() - def dist_lwsize(self) -> int: + def local_world_size(self) -> int: """Returns the local number of workers available per node, which is usually the number of GPUs available. Returns: int: local world size. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return torch.cuda.device_count() - def dist_grank(self) -> int: + def global_rank(self) -> int: """Returns the global rank of the current process, where rank ranges from 0 to world_size. Returns: int: global rank. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return dist.get_rank() - def dist_lrank(self) -> int: + def local_rank(self) -> int: """Returns the local rank of the current process. Returns: int: local rank. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return dist.get_rank() % torch.cuda.device_count() def clean_up(self) -> None: """Destroys the current process group.""" + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") deepspeed.sys.exit() def par_allgather_obj(self, obj: Any) -> list[Any]: @@ -334,18 +581,34 @@ def par_allgather_obj(self, obj: Any) -> list[Any]: Returns: List[Any]: List of gathered objects. """ - res = [None] * self.dist_gwsize() + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") + res = [None] * self.global_world_size() dist.all_gather_object(res, obj) return res -class HVDDistributedStrategy(TorchDistributedStrategy): +class HorovodStrategy(TorchDistributedStrategy): """Horovod distributed strategy class.""" def init(self) -> None: - """Initializes the Horovod distributed backend.""" + """Initializes the Horovod distributed backend. + + Raises: + RuntimeError: when there are not (enough) GPUs available. + DistributedStrategyError: when trying to initialize a strategy + already initialized. + """ + if not distributed_resources_available(): + raise RuntimeError( + "Trying to run distributed on insufficient resources.") + if self.is_initialized: + raise DistributedStrategyError("Strategy was already initialized") hvd.init() - torch.cuda.set_device(hvd.local_rank()) + self.is_initialized = True + + torch.cuda.device(self.local_rank()) def distributed( self, model: nn.Module, optimizer: Optional[Optimizer] = None, @@ -353,8 +616,11 @@ def distributed( **optim_kwargs ) -> Tuple[nn.Module, Optimizer, Optional[LRScheduler]]: """Setup model, optimizer and scheduler for distributed.""" + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") - model.to(self.dist_device()) + model.to(self.device()) # Scale learning rate # https://github.com/horovod/horovod/issues/1653#issuecomment-574764452 @@ -389,42 +655,57 @@ def _broadcast_params( hvd.broadcast_parameters(model.state_dict(), root_rank=0) hvd.broadcast_optimizer_state(optimizer, root_rank=-0) - def dist_gwsize(self) -> int: + def global_world_size(self) -> int: """Returns the total number of processes (global world size). Returns: int: global world size. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return hvd.size() - def dist_lwsize(self) -> int: + def local_world_size(self) -> int: """Returns the local number of workers available per node, which is usually the number of GPUs available. Returns: int: local world size. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return hvd.local_size() - def dist_grank(self) -> int: + def global_rank(self) -> int: """Returns the global rank of the current process, where rank ranges from 0 to world_size. Returns: int: global rank. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return hvd.rank() - def dist_lrank(self) -> int: + def local_rank(self) -> int: """Returns the local rank of the current process. Returns: int: local rank. """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return hvd.local_rank() def clean_up(self) -> None: """Shuts Horovod down.""" + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") hvd.shutdown() def par_allgather_obj(self, obj: Any) -> list[Any]: @@ -437,484 +718,99 @@ def par_allgather_obj(self, obj: Any) -> list[Any]: Returns: list: gathered list with size(#worker). """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") return hvd.allgather_object(obj) -# class TorchDistributedStrategy_old(DistributedStrategy): -# """Abstract class to define the distributed backend methods for -# PyTorch models. -# """ -# @abc.abstractmethod -# def init_backend(self) -> None: -# """Initializes the chosen distributed backend""" +class NonDistributedStrategy(TorchDistributedStrategy): + """Dummy class for non-distributed environments.""" -# @abc.abstractmethod -# def distribute_model(self, model: Any) -> Any: -# """Distributes a machine learning model. + is_distributed: bool = False -# Args: -# model (Any): a generic ML model to be distributed. + def init(self) -> None: + """If CUDA is available set CUDA device, and do nothing more. -# Returns: -# Any: distributed model instance. -# """ + Raises: + DistributedStrategyError: when trying to initialize a strategy + already initialized. + """ + if self.is_initialized: + raise DistributedStrategyError("Strategy was already initialized") + if torch.cuda.is_available(): + torch.cuda.device(self.local_rank()) + self.is_initialized = True -# @abc.abstractmethod -# def broadcast_params(self, model: Any, optimizer: Any) -> None: -# """Broadcasts variables from root rank to all other processes/ + def device(self) -> str: + """Device used by local worker. -# Args: -# model (Any): distributed model. -# optimizer (Any): optimizer. -# """ + Returns: + str: cpu device if CUDA is not available. + """ + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") + if torch.cuda.is_available(): + return super().device() + return "cpu" + + def distributed( + self, model: nn.Module, optimizer: Optional[Optimizer] = None, + lr_scheduler: Optional[LRScheduler] = None, + **kwargs + ) -> Tuple[nn.Module, Optimizer, Optional[LRScheduler]]: + """Do nothing and return model, optimizer and scheduler.""" + if not self.is_initialized: + raise UninitializedStrategyError( + "Strategy has not been initialized. Use the init method.") + if torch.cuda.is_available(): + model = model.cuda() + return model, optimizer, lr_scheduler + + def global_world_size(self) -> int: + """Returns the total number of processes (global world size). + + Returns: + int: global world size. + """ + return 1 + + def local_world_size(self) -> int: + """Returns the local number of workers available per node, + which is usually the number of GPUs available. + + Returns: + int: local world size. + """ + return 1 + + def global_rank(self) -> int: + """Returns the global rank of the current process, where + rank ranges from 0 to world_size. + + Returns: + int: global rank. + """ + return 0 + + def local_rank(self) -> int: + """Returns the local rank of the current process. -# @abc.abstractmethod -# def distribute_optimizer(self, optimizer: Any, model: Any) -> Any: -# """Distribute optimizer. + Returns: + int: local rank. + """ + return 0 -# Args: -# optimizer (Any): optimizer. -# model (Any): distributed model. + def clean_up(self) -> None: + """Do nothing.""" -# Returns: -# Any: distributed optimizer. -# """ + def par_allgather_obj(self, obj: Any) -> list[Any]: + """Raise error as this operation is not available. -# @abc.abstractmethod -# def dist_gwsize(self) -> int: -# """Returns the total number of processes (global world size). - -# Returns: -# int: global world size. -# """ - -# @abc.abstractmethod -# def dist_lwsize(self) -> int: -# """Returns the number of local workers available on a node -# (local world size). -# Usually it is equal to the number of available GPUs. - -# Returns: -# int: local world size. -# """ - -# @abc.abstractmethod -# def dist_grank(self) -> int: -# """Returns the global rank of the current process. -# Rank ranges from 0 to world_size. - -# Returns: -# int: global rank. -# """ - -# @abc.abstractmethod -# def dist_lrank(self) -> int: -# """Returns the local rank of the current process. - -# Returns: -# int: local rank. -# """ - -# def is_main_worker(self) -> bool: -# """Checks if local worker has global rank equal to zero. - -# Returns: -# bool: True if main worker. -# """ -# return self.dist_grank() == 0 - -# def dist_device(self) -> str: -# """Device used by local worker. - -# Returns: -# str: torch device in the form 'cuda:N'. -# """ -# return f"cuda:{self.dist_lrank()}" - -# @abc.abstractmethod -# def clean_up(self) -> None: -# """Cleans up resources allocated by distributed strategy.""" - -# @abc.abstractmethod -# def par_allgather_obj(self, obj: Any) -> List[Any]: -# """Gathers any object from the whole group in a list -# (to all workers). - -# Args: -# obj (Any): object to gather from all workers. - -# Returns: -# List[Any]: list of objects gathered from all workers. -# """ - - -# class DDPDistributedStrategy_old(TorchDistributedStrategy_old): -# """PyTorch DDP distributed strategy class. - -# Args: -# backend (str): Name of the communication backend to employ. -# """ - -# backend: str - -# def __init__(self, backend: str) -> None: -# super().__init__() -# self.backend = backend - -# def init_backend(self) -> None: -# """Initializes the distributed process group and the distributed -# package. -# """ -# if torch.cuda.is_available(): -# dist.init_process_group(backend=self.backend) - -# def distribute_model(self, model: nn.Module) -> nn.Module: -# """Achieves data parallelism by synchronizing the gradients -# across each model replica located in each available -# computing device. - -# Args: -# model (nn.Module): ML model to be distributed. - -# Returns: -# nn.Module: Distributed model replicas across all devices. -# that are to be synchronized. -# """ -# if torch.cuda.is_available(): -# # device = self.dist_lrank() -# model = model.to(self.dist_device()) -# dist_model = torch.nn.parallel.DistributedDataParallel( -# model, -# device_ids=[self.dist_device()], -# output_device=self.dist_device() -# ) -# else: -# dist_model = model - -# return dist_model - -# def broadcast_params( -# self, -# model: nn.Module, -# optimizer: optim.Optimizer -# ) -> None: -# """Do nothing. Only applicable for Horovod. - -# Args: -# model (nn.Module): ML model -# optimizer (optim.Optimizer): Optimizer -# """ -# pass - -# def distribute_optimizer( -# self, -# optimizer: optim.Optimizer, -# model: nn.Module = None -# ) -> optim.Optimizer: -# """Returns the optimizer from argument. - -# Args: -# optimizer (optim.Optimizer): optimizer. -# model (nn.Module): ML model. Unused here. - -# Returns: -# optim.Optimizer: Distributed optimizer. -# """ -# return optimizer - -# def dist_gwsize(self) -> int: -# """Returns the total number of processes (global world size). - -# Returns: -# int: global world size. -# """ -# return dist.get_world_size() - -# def dist_lwsize(self) -> int: -# """Returns the local number of workers available per node, -# which is usually the number of GPUs available. - -# Returns: -# int: local world size. -# """ -# return torch.cuda.device_count() - -# def dist_grank(self) -> int: -# """Returns the global rank of the current process, where -# rank ranges from 0 to world_size. - -# Returns: -# int: global rank. -# """ -# return dist.get_rank() - -# def dist_lrank(self) -> int: -# """Returns the local rank of the current process. - -# Returns: -# int: local rank. -# """ -# return dist.get_rank() % torch.cuda.device_count() - -# def clean_up(self) -> None: -# """Destroys the current process group.""" -# if torch.cuda.is_available(): -# dist.barrier() -# dist.destroy_process_group() - -# def par_allgather_obj(self, obj: Any) -> List[Any]: -# """Gathers any object from the whole group -# in a list (to all workers). - -# Args: -# obj (Any): Object to gather from all workers. - -# Returns: -# List[Any]: List of gathered objects. -# """ -# res = [None] * self.dist_gwsize() -# dist.all_gather_object(res, obj) -# return res - - -# class DSDistributedStrategy_old(TorchDistributedStrategy_old): -# """DeepSpeed distributed strategy class. - -# Args: -# backend (str): Name of the communication backend to employ. -# config (Union[dict, Path, str]): DeepSpeed config. Either a -# dictionary or a path to a JSON file. -# """ - -# config: Dict = None -# backend: str - -# def __init__( -# self, -# backend: str, -# config: Union[Dict, Path, str] -# ) -> None: -# super().__init__() -# self.backend = backend -# self._load_config(config) - -# def _load_config(self, ds_config): -# if isinstance(ds_config, (str, Path)): -# with open(ds_config) as fp: -# self.config = json.load(fp) -# elif isinstance(ds_config, dict): -# self.config = ds_config -# else: -# raise ValueError("ds_config is not a dictionary not a path.") - -# def init_backend(self) -> None: -# """Initializes the distributed process group and the distributed -# package. -# """ -# deepspeed.init_distributed(dist_backend=self.backend) - -# def distribute_model(self, model: nn.Module) -> nn.Module: -# """Achieves data parallelism by synchronizing the gradients -# across each model replica located in each available -# computing device. - -# Args: -# model (nn.Module): ML model to be distributed. - -# Returns: -# nn.Module: Distributed model replicas across all devices -# that are to be synchronized. -# """ -# distrib_model, __, __, __ = deepspeed.initialize( -# model=model, -# model_parameters=model.parameters(), -# dist_init_required=True, -# config=self.config -# ) -# return distrib_model - -# def broadcast_params( -# self, model: nn.Module, optimizer: optim.Optimizer -# ) -> None: -# """Only applicable for Horovod. Does nothing. - -# Args: -# model (nn.Module): ML model. -# optimizer (optim.Optimizer): optimizer. -# """ -# pass - -# def distribute_optimizer( -# self, -# optimizer: optim.Optimizer, -# model: nn.Module = None -# ) -> optim.Optimizer: -# """Returns the optimizer from argument. - -# Args: -# optimizer (optim.Optimizer): torch optimizer. -# model (nn.Module): torch neural network. - -# Returns: -# optim.Optimizer: distributed optimizer. -# """ -# return optimizer - -# def dist_gwsize(self) -> int: -# """Returns the total number of processes (global world size). - -# Returns: -# int: global world size. -# """ -# return dist.get_world_size() - -# def dist_lwsize(self) -> int: -# """Returns the local number of workers available per node, -# which is usually the number of GPUs available. - -# Returns: -# int: local world size. -# """ -# return torch.cuda.device_count() - -# def dist_grank(self) -> int: -# """Returns the global rank of the current process, where -# rank ranges from 0 to world_size. - -# Returns: -# int: global rank. -# """ -# return dist.get_rank() - -# def dist_lrank(self) -> int: -# """Returns the local rank of the current process. - -# Returns: -# int: local rank. -# """ -# return dist.get_rank() % torch.cuda.device_count() - -# def clean_up(self) -> None: -# """Destroys the current process group.""" -# deepspeed.sys.exit() - -# def par_allgather_obj(self, obj: Any) -> list[Any]: -# """Gathers any object from the whole group -# in a list (to all workers). - -# Args: -# obj (Any): Object to gather from all workers. - -# Returns: -# List[Any]: List of gathered objects. -# """ -# res = [None] * self.dist_gwsize() -# dist.all_gather_object(res, obj) -# return res - - -# class HVDDistributedStrategy_old(TorchDistributedStrategy_old): -# """Horovod distributed strategy class.""" - -# def init_backend(self) -> None: -# """Initializes the Horovod distributed backend.""" -# hvd.init() - -# def distribute_model(self, model: nn.Module) -> nn.Module: -# """Only applicable for DDP and DeepSpeed. -# For Horovod, returns the same model passed as argument. - -# Args: -# model (nn.Module): ML model to be distributed. - -# Returns: -# nn.Module: ML model passed in the argument. -# """ -# return model - -# def broadcast_params( -# self, model: nn.Module, optimizer: optim.Optimizer -# ) -> None: -# """Broadcasts variables from root rank to all other processes. - -# Args: -# model (nn.Module): ML model that is to be broadcasted -# across processes. -# optimizer (optim.Optimizer): Optimizer that is to be broadcasted -# across processes. -# """ -# hvd.broadcast_parameters(model.state_dict(), root_rank=0) -# hvd.broadcast_optimizer_state(optimizer, root_rank=-0) - -# def distribute_optimizer( -# self, -# optimizer: optim.Optimizer, -# model: nn.Module -# ) -> optim.Optimizer: -# """Constructs a DistributedOptimizer, for computing single-process -# gradient values and applying gradient updates after the gradients -# have been combined across all the Horovod ranks. - -# Args: -# optimizer (optim.Optimizer): Optimizer to be distributed. -# model (nn.Module): ML model to be trained. - -# Returns: -# optim.Optimizer: Distributed optimizer across all ranks. -# """ -# distOptimizer = hvd.DistributedOptimizer( -# optimizer, -# named_parameters=model.named_parameters(), -# op=hvd.Average -# ) -# return distOptimizer - -# def dist_gwsize(self) -> int: -# """Returns the total number of processes (global world size). - -# Returns: -# int: global world size. -# """ -# return hvd.size() - -# def dist_lwsize(self) -> int: -# """Returns the local number of workers available per node, -# which is usually the number of GPUs available. - -# Returns: -# int: local world size. -# """ -# return hvd.local_size() - -# def dist_grank(self) -> int: -# """Returns the global rank of the current process, where -# rank ranges from 0 to world_size. - -# Returns: -# int: global rank. -# """ -# return hvd.rank() - -# def dist_lrank(self) -> int: -# """Returns the local rank of the current process. - -# Returns: -# int: local rank. -# """ -# return hvd.local_rank() - -# def clean_up(self) -> None: -# """Shuts Horovod down.""" -# hvd.shutdown() - -# def par_allgather_obj(self, obj: Any) -> list[Any]: -# """Gathers scalar objects across all workers to a -# list with size(#worker), uses horovod communicator - -# Args: -# obj (Any): object in a worker. - -# Returns: -# list: gathered list with size(#worker). -# """ -# return hvd.allgather_object(obj) + Args: + obj (Any): object in a worker. + """ + raise RuntimeError( + f"{self.__class__.__name__} does not support this operation." + ) diff --git a/src/itwinai/torch/engine.py b/src/itwinai/torch/engine.py deleted file mode 100644 index 7084d6ec..00000000 --- a/src/itwinai/torch/engine.py +++ /dev/null @@ -1,276 +0,0 @@ -""" -Model engine which wraps a torch NN. Still under development. May be removed... -""" - -import abc -from typing import Any, Union, Optional, Callable - -from pydantic import BaseModel - -import torch -import torch.nn as nn -import torch.optim as optim -from torch.optim.lr_scheduler import _LRScheduler as LRScheduler -from torch.cuda import amp -from torch import autocast - - -class OptimizerConfig: - def __init__(self, optim_class, **kwargs) -> None: - self.optim_class = optim_class - self.kwargs = kwargs - - def to_optim(self, parameters) -> optim.Optimizer: - return self.optim_class(parameters, **self.kwargs) - - -class LRSchedulerConfig: - def __init__(self, scheduler_class, **kwargs) -> None: - self.scheduler_class = scheduler_class - self.kwargs = kwargs - - def to_scheduler(self, optim) -> LRScheduler: - return self.scheduler_class(optim, **self.kwargs) - - -class ModelEngineConfig(BaseModel): - mixed_precision: bool = False - - -class ModelEngine(abc.ABC): - """Wrapper around ML model, which abstracts from distributed and - mixed-precision models. - """ - - model: nn.Module - _model_parameters: Any - optimizer: optim.Optimizer - lr_scheduler: LRScheduler - # config: ModelEngineConfig - mixed_precision: bool = False - grad_scaler: amp.GradScaler = None - - def __init__( - self, - model: nn.Module, - # model_parameters: Any, - optimizer: Union[optim.Optimizer, OptimizerConfig], - lr_scheduler: Optional[Union[LRScheduler, LRSchedulerConfig]] = None, - mixed_precision: bool = False - # config: Optional[ModelEngineConfig] = None - ) -> None: - super().__init__() - self.model = model - self.optimizer = optimizer - self.lr_scheduler = lr_scheduler - # self._model_parameters = model_parameters - # if isinstance(optimizer, OptimizerConfig): - # self.optimizer = optimizer.to_optim(model_parameters) - # else: - # self.optimizer = optimizer - - # if isinstance(lr_scheduler, LRSchedulerConfig): - # self.lr_scheduler = lr_scheduler.to_scheduler(self.optimizer) - # else: - # self.lr_scheduler = lr_scheduler - - # if not config: - # self.config = ModelEngineConfig() - self.mixed_precision = mixed_precision - if mixed_precision: - self.grad_scaler = amp.GradScaler() - - def __call__(self, *args: Any, **kwds: Any) -> Any: - """Performs the forward operation.""" - # Wrapper of self.forward() - return self.forward(*args, **kwds) - - def forward(self, *args: Any, **kwds: Any) -> Any: - """Performs the forward operation.""" - return self.model(*args, **kwds) - - def train(self, mode: bool = True) -> nn.Module: - """Set model in training mode.""" - self.model.train(mode=mode) - return self.model - - def eval(self) -> nn.Module: - """Set model in inference mode.""" - self.model.eval() - return self.model - - def to(self, device) -> nn.Module: - """Move model to specified device.""" - self.model.to(device) - return self.model - - @abc.abstractmethod - def zero_grad(): - """Set gradients to zero for the optimizer.""" - - @abc.abstractmethod - def backward(self, loss_fn: Callable, *loss_args) -> torch.Tensor: - """Perform backward pass and return the loss. - - Args: - loss_fn (Callable): computes the loss. - *loss_args: are the arguments to be passed to ``loss_fn``. - - Returns: - torch.Tensor: computed loss. - """ - - @abc.abstractmethod - def optimizer_step(self): - """Perform optimizer step.""" - - @abc.abstractmethod - def lr_scheduler_step(self): - """Perform lr scheduler step, if present.""" - # This should be incorporated in the optim step: - # https://deepspeed.readthedocs.io/en/latest/schedulers.html - # scheduler is updated automatically at each training step - - @abc.abstractmethod - def save_checkpoint(self): - """Save checkpoint to persistent storage.""" - - -class DDPModelEngine(ModelEngine): - """Model engine for torch DDP distributed strategy.""" - - def forward(self, *args: Any, **kwds: Any) -> Any: - """Performs the forward operation.""" - if self.mixed_precision: - # https://pytorch.org/docs/stable/notes/amp_examples.html - # Runs the forward pass with autocasting. - with autocast(device_type='cuda', dtype=torch.float16): - return self.model(*args, **kwds) - else: - return self.model(*args, **kwds) - - def zero_grad(self): - """Set gradients to zero for the optimizer.""" - self.optimizer.zero_grad() - - def backward(self, loss_fn: Callable, *loss_args) -> torch.Tensor: - """Perform backward pass and return the loss. - - Args: - loss_fn (Callable): computes the loss. - *loss_args: are the arguments to be passed to ``loss_fn``. - - Returns: - torch.Tensor: computed loss. - """ - if self.mixed_precision: - # https://pytorch.org/docs/stable/notes/amp_examples.html - # Runs the forward pass with autocasting. - with autocast(device_type='cuda', dtype=torch.float16): - loss = loss_fn(*loss_args) - - # Scales loss. Calls backward() on scaled loss to create scaled - # gradients. - # Backward passes under autocast are not recommended. - # Backward ops run in the same dtype autocast chose for - # corresponding forward ops. - loss = self.grad_scaler.scale(loss) - else: - loss = loss_fn(*loss_args) - loss.backward() - return loss - - def optimizer_step(self): - """Perform optimizer step.""" - if self.mixed_precision: - # https://pytorch.org/docs/stable/notes/amp_examples.html#typical-mixed-precision-training - # scaler.step() first unscales the gradients of the optimizer's - # assigned params. - # If these gradients do not contain infs or NaNs, optimizer.step() - # is then called, - # otherwise, optimizer.step() is skipped. - self.grad_scaler.step(self.optimizer) - - # Updates the scale for next iteration. - self.grad_scaler.update() - else: - self.optimizer.step() - - def lr_scheduler_step(self): - """Perform lr scheduler step, if present.""" - if self.lr_scheduler: - self.lr_scheduler.step() - - def save_checkpoint(self): - """Save checkpoint to persistent storage.""" - raise NotImplementedError - - -class DSModelEngine(ModelEngine): - """Model engine for DeeSpeed distributed strategy.""" - - def forward(self, *args: Any, **kwds: Any) -> Any: - """Performs the forward operation.""" - if self.mixed_precision: - # https://pytorch.org/docs/stable/notes/amp_examples.html - # Runs the forward pass with autocasting. - with autocast(device_type='cuda', dtype=torch.float16): - return self.model(*args, **kwds) - else: - return self.model(*args, **kwds) - - def zero_grad(self): - """Set gradients to zero for the optimizer.""" - self.optimizer.zero_grad() - - def backward(self, loss_fn: Callable, *loss_args) -> torch.Tensor: - """Perform backward pass and return the loss. - - Args: - loss_fn (Callable): computes the loss. - *loss_args: are the arguments to be passed to ``loss_fn``. - - Returns: - torch.Tensor: computed loss. - """ - if self.mixed_precision: - # https://pytorch.org/docs/stable/notes/amp_examples.html - # Runs the forward pass with autocasting. - with autocast(device_type='cuda', dtype=torch.float16): - loss = loss_fn(*loss_args) - - # Scales loss. Calls backward() on scaled loss to create scaled - # gradients. - # Backward passes under autocast are not recommended. - # Backward ops run in the same dtype autocast chose for - # corresponding forward ops. - loss = self.grad_scaler.scale(loss) - else: - loss = loss_fn(*loss_args) - loss.backward() - return loss - - def optimizer_step(self): - """Perform optimizer step.""" - if self.mixed_precision: - # https://pytorch.org/docs/stable/notes/amp_examples.html#typical-mixed-precision-training - # scaler.step() first unscales the gradients of the optimizer's - # assigned params. - # If these gradients do not contain infs or NaNs, optimizer.step() - # is then called, - # otherwise, optimizer.step() is skipped. - self.grad_scaler.step(self.optimizer) - - # Updates the scale for next iteration. - self.grad_scaler.update() - else: - self.optimizer.step() - - def lr_scheduler_step(self): - """Perform lr scheduler step, if present.""" - if self.lr_scheduler: - self.lr_scheduler.step() - - def save_checkpoint(self): - """Save checkpoint to persistent storage.""" - raise NotImplementedError diff --git a/src/itwinai/torch/inference.py b/src/itwinai/torch/inference.py index 02882f06..bb9af300 100644 --- a/src/itwinai/torch/inference.py +++ b/src/itwinai/torch/inference.py @@ -6,8 +6,7 @@ from torch import nn from torch.utils.data import DataLoader, Dataset -from ..utils import dynamically_import_class -from .utils import clear_key +from ..utils import dynamically_import_class, clear_key from ..components import Predictor, monitor_exec from .types import TorchDistributedStrategy as StrategyT from .types import Metric, Batch diff --git a/src/itwinai/torch/mlflow.py b/src/itwinai/torch/mlflow.py index 18a014ff..8bc854d4 100644 --- a/src/itwinai/torch/mlflow.py +++ b/src/itwinai/torch/mlflow.py @@ -16,6 +16,8 @@ def _get_mlflow_logger_conf(pl_config: Dict) -> Optional[Dict]: Optional[Dict]: if present, MLFLowLogger constructor arguments (under 'init_args' key). """ + if not pl_config['trainer'].get('logger'): + return None if isinstance(pl_config['trainer']['logger'], list): # If multiple loggers are provided for logger_conf in pl_config['trainer']['logger']: diff --git a/src/itwinai/torch/reproducibility.py b/src/itwinai/torch/reproducibility.py new file mode 100644 index 00000000..1513c82a --- /dev/null +++ b/src/itwinai/torch/reproducibility.py @@ -0,0 +1,48 @@ +""" +This module provides the tools to support reproducible execution of +torch scripts. +""" + +from typing import Optional +import numpy as np +import random + +import torch + + +def seed_worker(worker_id): + """Seed DataLoader worker.""" + worker_seed = torch.initial_seed() % 2**32 + np.random.seed(worker_seed) + random.seed(worker_seed) + + +def set_seed( + rnd_seed: Optional[int], + deterministic_cudnn: bool = True +) -> torch.Generator: + """Set torch random seed and return a PRNG object. + + Args: + rnd_seed (Optional[int]): random seed. If None, the seed is not set. + deterministic_cudnn (bool): if True, sets + ``torch.backends.cudnn.benchmark = False``, which may affect + performances. + + Returns: + torch.Generator: PRNG object. + """ + g = torch.Generator() + if rnd_seed is not None: + # Deterministic execution + np.random.seed(rnd_seed) + random.seed(rnd_seed) + torch.manual_seed(rnd_seed) + g.manual_seed(rnd_seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed(rnd_seed) + torch.cuda.manual_seed_all(rnd_seed) + if deterministic_cudnn: + torch.backends.cudnn.benchmark = False + torch.backends.cudnn.deterministic = True + return g diff --git a/src/itwinai/torch/trainer.py b/src/itwinai/torch/trainer.py index f0ad1c03..4e7a108f 100644 --- a/src/itwinai/torch/trainer.py +++ b/src/itwinai/torch/trainer.py @@ -1,15 +1,12 @@ """Provides training logic for PyTorch models via Trainer classes.""" from typing import ( - Optional, Dict, Union, Tuple, Type, List, Any + Optional, Dict, Union, Tuple, List, Any, Literal ) -import time import os import sys -import numpy as np import torch -import torch.multiprocessing as mp from torch.utils.data import DataLoader, Dataset from torch.utils.data.distributed import DistributedSampler import torch.distributed as dist @@ -17,494 +14,319 @@ import torch.nn as nn from torch.optim.optimizer import Optimizer +import lightning as L +from lightning.pytorch.cli import LightningCLI + +import horovod.torch as hvd + from ..components import Trainer, monitor_exec -from .utils import seed_worker, par_allgather_obj, clear_key from .types import ( Batch, Loss, LrScheduler, Metric ) -from .types import TorchDistributedStrategy as StrategyT -from ..loggers import LogMixin, Logger, ConsoleLogger -from ..utils import dynamically_import_class -from ..cluster import ClusterEnvironment -# from .distributed import ( -# TorchDistributedStrategy, -# DDPDistributedStrategy, -# DSDistributedStrategy, -# HVDDistributedStrategy -# ) +from ..loggers import LogMixin, Logger +from .reproducibility import seed_worker, set_seed +from .distributed import ( + TorchDistributedStrategy, + TorchDDPStrategy, + HorovodStrategy, + DeepSpeedStrategy, + NonDistributedStrategy, + distributed_resources_available +) +from ..utils import load_yaml +from .mlflow import ( + init_lightning_mlflow, + teardown_lightning_mlflow +) -def preproc_dataloader(dataloader: DataLoader, gwsize, grank): - """Makes a Dataloader distributed.""" - sampler = DistributedSampler( - dataloader.dataset, - num_replicas=gwsize, - rank=grank, - shuffle=True - ) - # Recreate dataloader, with updated sampler - return DataLoader( - dataloader.dataset, - batch_size=dataloader.batch_size, - sampler=sampler, - num_workers=dataloader.num_workers, - collate_fn=dataloader.collate_fn, - pin_memory=dataloader.pin_memory, - drop_last=dataloader.drop_last, - timeout=dataloader.timeout, - worker_init_fn=seed_worker, # dataloader.worker_init_fn, - multiprocessing_context=dataloader.multiprocessing_context, - generator=dataloader.generator, - prefetch_factor=dataloader.prefetch_factor, - persistent_workers=dataloader.persistent_workers, - pin_memory_device=dataloader.pin_memory_device - ) +class Config: + def __init__(self, my_dict: Optional[Dict] = None): + my_dict = my_dict if my_dict is not None else {} + self.__dict__.update(my_dict) -def distributed(func): - """The decorated function must have a standard signature. - Its first arguments must be: - model, train_dataloader, validation_dataloader, device (in this order). +class TorchTrainer(Trainer, LogMixin): + """Trainer class for torch training algorithms. - Additional args or kwargs are allowed consistently with the signature - of the decorated function. + Args: + config (Dict): training configuration containing hyperparameters. + epochs (int): number of training epochs. + model (Optional[nn.Module], optional): model to train. + Defaults to None. + strategy (Literal["ddp", "deepspeed", + "horovod"], optional): distributed strategy. + Defaults to 'ddp'. + validation_every (Optional[int], optional): run a validation epoch + every ``validation_every`` epochs. Disabled if None. Defaults to 1. + test_every (Optional[int], optional): run a test epoch + every ``test_every`` epochs. Disabled if None. Defaults to None. + random_seed (Optional[int], optional): set random seed for + reproducibility. If None, the seed is not set. Defaults to None. + logger (Optional[Logger], optional): logger for ML tracking. + Defaults to None. + log_all_workers (bool, optional): if True, the ``log`` method is + called on all workers in the distributed context. Defaults to False. + metrics (Optional[Dict[str, Metric]], optional): map of torchmetrics + metrics. Defaults to None. + name (Optional[str], optional): trainer custom name. Defaults to None. """ - def dist_train( - model, train_dataloader, validation_dataloader=None, device='cpu', - *args, **kwargs - ): - if torch.cuda.is_available(): - dist.init_process_group(backend='nccl') - - if torch.cuda.is_available(): - lwsize = torch.cuda.device_count() # local world size - per node - gwsize = dist.get_world_size() # global world size - per run - grank = dist.get_rank() # global rank - assign per run - lrank = dist.get_rank() % lwsize # local rank - assign per node - else: - gwsize = 1 - grank = 0 - lrank = 0 - - device = torch.device( - 'cuda' if torch.cuda.is_available() else 'cpu', lrank) - if torch.cuda.is_available(): - torch.cuda.set_device(lrank) - - model = model.to(device) - model = DDP(model, device_ids=[device], output_device=device) - - train_dataloader = preproc_dataloader(train_dataloader, gwsize, grank) - if validation_dataloader is not None: - validation_dataloader = preproc_dataloader( - validation_dataloader, gwsize, grank) - - try: - func(model, train_dataloader, validation_dataloader, device, - *args, **kwargs) - finally: - if torch.cuda.is_available(): - dist.barrier() - dist.destroy_process_group() - return dist_train + # TODO: + # - add checkpointing. + # - extract BaseTorchTrainer and extend it creating a set of trainer + # templates (e.g.. GAN, Classifier, Transformer) allowing scientists + # to reuse ML algos. + # - improve get from configuration object + _strategy: TorchDistributedStrategy = None -class TorchTrainerMG(Trainer, LogMixin): - """ - Torch trainer for optionally distributed data-parallel (DDP) workload. - Multi-GPU distribution. - - Args: - model (nn.Module): neural network instance. - loss (Loss): torch loss function instance. - optimizer_class (str): path to optimizer class - (e.g., 'torch.optim.SGD') - optimizer_kwargs (Optional[Dict], optional): optimizer constructor - arguments (except from parameters). Defaults to None. - lr_scheduler_class (Optional[str], optional): path to learning - rate scheduler class. Defaults to None. - lr_scheduler_kwargs (Optional[Dict], optional): constructor arguments - of the learning rate scheduler, except for the optimizer. - Defaults to None. - train_dataloader_class (str, optional): train dataloader class path. - Defaults to 'torch.utils.data.DataLoader'. - train_dataloader_kwargs (Optional[Dict], optional): constructor - arguments of the train dataloader, except for the dataset - instance. Defaults to None. - validation_dataloader_class (str, optional): validation dataloader - class path. Defaults to 'torch.utils.data.DataLoader'. - validation_dataloader_kwargs (Optional[Dict], optional): constructor - arguments of the validation dataloader, except for the dataset - instance. If None, it replicates `train_dataloader_kwargs`. - Defaults to None. - epochs (int, optional): number of training epochs. Defaults to 1. - strategy (Optional[TorchDistributedStrategy], optional): distributed - strategy. Defaults to StrategyT.NONE.value. - backend (TorchDistributedBackend, optional): computing backend. - Defaults to BackendT.NCCL.value. - shuffle_dataset (bool, optional): whether shuffle dataset before - sampling batches from dataloader. Defaults to False. - use_cuda (bool, optional): whether to use GPU. Defaults to True. - benchrun (bool, optional): sets up a debug run. Defaults to False. - testrun (bool, optional): deterministic training seeding everything. - Defaults to False. - seed (Optional[int], optional): random seed. Defaults to None. - logger (Optional[List[Logger]], optional): logger. Defaults to None. - checkpoint_every (int, optional): how often (epochs) to checkpoint the - best model. Defaults to 10. - cluster (Optional[ClusterEnvironment], optional): cluster environment - object describing the context in which the trainer is executed. - Defaults to None. - train_metrics (Optional[Dict[str, Metric]], optional): - list of metrics computed in the training step on the predictions. - It's a dictionary with the form - ``{'metric_unique_name': CallableMetric}``. Defaults to None. - validation_metrics (Optional[Dict[str, Metric]], optional): same - as ``training_metrics``. If not given, it mirrors the training - metrics. Defaults to None. - - Raises: - RuntimeError: When trying to use DDP without CUDA support. - NotImplementedError: when trying to use a strategy different from the - ones provided by TorchDistributedStrategy. - """ + train_dataloader: DataLoader = None + validation_dataloader: DataLoader = None + test_dataloader: DataLoader = None model: nn.Module = None loss: Loss = None optimizer: Optimizer = None - lr_scheduler = None - _strategy: StrategyT = StrategyT.NONE.value - train_dataset: Dataset - validation_dataset: Dataset - train_dataloader: DataLoader = None - validation_dataloader: DataLoader = None - epoch_idx: int = 0 + lr_scheduler: LrScheduler = None + + torch_rng: torch.Generator = None + logger: Logger = None train_glob_step: int = 0 validation_glob_step: int = 0 - train_metrics: Dict[str, Metric] - validation_metrics: Dict[str, Metric] + test_glob_step: int = 0 + metrics: Dict[str, Metric] def __init__( self, - model: nn.Module, - loss: Loss, - optimizer_class: str, - optimizer_kwargs: Optional[Dict] = None, - lr_scheduler_class: Optional[str] = None, - lr_scheduler_kwargs: Optional[Dict] = None, - train_dataloader_class: str = 'torch.utils.data.DataLoader', - train_dataloader_kwargs: Optional[Dict] = None, - validation_dataloader_class: str = 'torch.utils.data.DataLoader', - validation_dataloader_kwargs: Optional[Dict] = None, - epochs: int = 1, - strategy: str = StrategyT.NONE.value, - benchrun: bool = False, - testrun: bool = False, - seed: Optional[int] = None, - logger: Optional[List[Logger]] = None, - checkpoint_every: int = 10, - cluster: Optional[ClusterEnvironment] = None, - train_metrics: Optional[Dict[str, Metric]] = None, - validation_metrics: Optional[Dict[str, Metric]] = None + config: Dict, + epochs: int, + model: Optional[nn.Module] = None, + strategy: Literal["ddp", "deepspeed", "horovod"] = 'ddp', + validation_every: Optional[int] = 1, + test_every: Optional[int] = None, + random_seed: Optional[int] = None, + logger: Optional[Logger] = None, + log_all_workers: bool = False, + metrics: Optional[Dict[str, Metric]] = None, + name: Optional[str] = None ) -> None: - """Sets up the distributed backend and loggers. - Makes the model a DDP model. - """ - super().__init__() + super().__init__(name) self.save_parameters(**self.locals2params(locals())) - self.model = model - self.loss = loss + + # config is mean to store all hyperparameters, which can very from use + # case to use case + # and include learning_rate, batch_size.... + self.config = Config(config) self.epochs = epochs - self.testrun = testrun - self.seed = seed + self.model = model self.strategy = strategy - self.benchrun = benchrun - self.cluster = cluster - # Checkpoint every n epochs - self.checkpoint_every = checkpoint_every - - # Train and validation dataloaders - self.train_dataloader_class = dynamically_import_class( - train_dataloader_class - ) - self.validation_dataloader_class = dynamically_import_class( - validation_dataloader_class - ) - train_dataloader_kwargs = ( - train_dataloader_kwargs - if train_dataloader_kwargs is not None else {} - ) - self.train_dataloader_kwargs = clear_key( - train_dataloader_kwargs, 'train_dataloader_kwargs', 'dataset' - ) - # If validation_dataloader_kwargs is not given, - # copy train_dataloader_kwargs - validation_dataloader_kwargs = ( - validation_dataloader_kwargs if validation_dataloader_kwargs - is not None else train_dataloader_kwargs - ) - self.validation_dataloader_kwargs = clear_key( - validation_dataloader_kwargs, 'validation_dataloader_kwargs', - 'dataset' - ) - - # Optimizer and scheduler - optim_class = dynamically_import_class(optimizer_class) - optimizer_kwargs = ( - optimizer_kwargs if optimizer_kwargs is not None else {} - ) - optimizer_kwargs = clear_key( - optimizer_kwargs, 'optimizer_kwargs', 'parameters' - ) - self.optimizer: Optimizer = optim_class( - self.model.parameters(), **optimizer_kwargs - ) - if lr_scheduler_class is not None: - scheduler_class = dynamically_import_class(lr_scheduler_class) - lr_scheduler_kwargs = ( - lr_scheduler_kwargs if lr_scheduler_kwargs is not None else {} - ) - lr_scheduler_kwargs = clear_key( - lr_scheduler_kwargs, 'lr_scheduler_kwargs', 'optimizer' - ) - self.lr_scheduler: LrScheduler = scheduler_class( - self.optimizer, **lr_scheduler_kwargs - ) - - # Loggers - self.logger = logger if logger is not None else ConsoleLogger() - - # Metrics - self.train_metrics = ( - {} if train_metrics is None else train_metrics - ) - self.validation_metrics = ( - self.train_metrics if validation_metrics is None - else validation_metrics - ) + self.validation_every = validation_every + self.test_every = test_every + self.random_seed = random_seed + self.logger = logger + self.log_all_workers = log_all_workers + self.metrics = metrics if metrics is not None else {} @property - def strategy(self) -> Optional[str]: + def strategy(self) -> TorchDistributedStrategy: return self._strategy @strategy.setter - def strategy(self, strategy_name) -> None: - if strategy_name not in StrategyT: - raise ValueError( - "Unrecognized 'strategy' field. Allowed values " - f"are: {StrategyT.list()}. Received '{strategy_name}'") - self._strategy = strategy_name + def strategy(self, strategy: Union[str, TorchDistributedStrategy]) -> None: + if isinstance(strategy, TorchDistributedStrategy): + self._strategy = strategy + else: + self._strategy = self._detect_strategy(strategy) @property - def global_step(self) -> int: - return self.train_glob_step + self.validation_glob_step + def device(self) -> str: + return self.strategy.device() + + def _detect_strategy(self, strategy: str) -> TorchDistributedStrategy: + if not distributed_resources_available(): + print("WARNING: falling back to non-distributed strategy.") + dist_str = NonDistributedStrategy() + elif strategy == 'ddp': + dist_str = TorchDDPStrategy(backend='nccl') + elif strategy == 'horovod': + dist_str = HorovodStrategy() + elif strategy == 'deepspeed': + dist_str = DeepSpeedStrategy(backend='nccl') + else: + raise NotImplementedError( + f"Strategy '{strategy}' is not recognized/implemented.") + return dist_str - def set_seed(self, seed: Optional[int] = None): - """Deterministic operations for reproducibility. - Sets the random seed. + def _init_distributed_strategy(self) -> None: + if not self.strategy.is_initialized: + self.strategy.init() - Args: - seed (Optional[int], optional): if not None, overrides - `self.seed`. Defaults to None. + def create_model_loss_optimizer(self) -> None: + """ + Instantiate a torch model, loss, optimizer, and LR scheduler using the + configuration provided in the Trainer constructor. + Generally a user-define method. """ - seed = seed if seed is not None else self.seed - np.random.seed(seed) - self.torch_rng = torch.Generator() - if seed is not None: - torch.manual_seed(seed) - self.torch_rng.manual_seed(seed) - if self.cluster.is_cuda_available(): - torch.cuda.manual_seed(seed) + ################################### + # Dear user, this is a method you # + # may be interested to override! # + ################################### + + if self.model is None: + # Model was not passed to the constructor. + # Create a model here + raise ValueError( + "self.model is None! Either pass it to the constructor or " + "override this method." + ) - @monitor_exec - def execute( - self, - train_dataset: Dataset, - validation_dataset: Dataset, - model: nn.Module = None, - optimizer: Optimizer = None, - lr_scheduler: LrScheduler = None, - ) -> Any: - self.train_dataset = train_dataset - self.validation_dataset = validation_dataset - - # Update parameters passed for "interactive" use - if model is not None: - self.model = model - if optimizer is not None: - self.optimizer = optimizer - if lr_scheduler is not None: - self.lr_scheduler = lr_scheduler - - # Start training - if self.cluster.distributed: - # Make training distributed - result = mp.spawn(self._train, nprocs=self.cluster.ngpus_per_node) - else: - result = self._train(0) + # A simple NLLLoss + self.loss = nn.functional.nll_loss - # Return value compliant with Executable.execute format - return result + # TODO: improve robustness of getting from config + self.optimizer = torch.optim.SGD( + self.model.parameters(), + lr=self.config.lr, + momentum=self.config.momentum + ) + # Create self.lr_scheduler if needed - def _train( - self, - worker_id: int - ): - # Each worker has a different deterministic seed - # Here, 'worker' = replica of the training function - worker_seed = ( - self.seed + worker_id if self.seed is not None else self.seed + # IMPORTANT: model, optimizer, and scheduler need to be distributed + + # First, define strategy-wise optional configurations + # TODO: improve robustness of getting from config + if isinstance(self.strategy, DeepSpeedStrategy): + # Batch size definition is not optional for DeepSpeedStrategy! + distribute_kwargs = dict( + config_params=dict( + train_micro_batch_size_per_gpu=self.config.batch_size + ) + ) + elif isinstance(self.strategy, HorovodStrategy): + distribute_kwargs = dict( + compression=( + hvd.Compression.fp16 if self.config.fp16_allreduce + else hvd.Compression.none + ), + op=hvd.Adasum if self.config.use_adasum else hvd.Average, + gradient_predivide_factor=self.config.gradient_predivide_factor + ) + else: + distribute_kwargs = {} + + # Distributed model, optimizer, and scheduler + ( + self.model, + self.optimizer, + self.lr_scheduler + ) = self.strategy.distributed( + self.model, self.optimizer, self.lr_scheduler, **distribute_kwargs ) - self.set_seed(worker_seed) - # Instantiate dataloaders - self.train_dataloader = self._instantiate_dataloader( - dataloader_class=self.train_dataloader_class, - dataset=self.train_dataset, - init_kwargs=self.train_dataloader_kwargs + def create_dataloaders( + self, + train_dataset: Dataset, + validation_dataset: Optional[Dataset] = None, + test_dataset: Optional[Dataset] = None + ) -> None: + """ + Create train, validation and test dataloaders using the + configuration provided in the Trainer constructor. + Generally a user-define method. + + Args: + train_dataset (Dataset): training dataset object. + validation_dataset (Optional[Dataset]): validation dataset object. + Default None. + test_dataset (Optional[Dataset]): test dataset object. + Default None. + """ + + ################################### + # Dear user, this is a method you # + # may be interested to override! # + ################################### + + # TODO: improve robustness of getting from config + self.train_dataloader = self.strategy.create_dataloader( + dataset=train_dataset, + batch_size=self.config.batch_size, + num_workers=self.config.num_workers, + pin_memory=self.config.pin_memory, + generator=self.torch_rng ) - if self.validation_dataset is not None: - self.validation_dataloader = self._instantiate_dataloader( - dataloader_class=self.validation_dataloader_class, - dataset=self.validation_dataset, - init_kwargs=self.validation_dataloader_kwargs + if validation_dataset is not None: + self.validation_dataloader = self.strategy.create_dataloader( + dataset=train_dataset, + batch_size=self.config.batch_size, + num_workers=self.config.num_workers, + pin_memory=self.config.pin_memory, + generator=self.torch_rng + ) + if test_dataset is not None: + self.test_dataloader = self.strategy.create_dataloader( + dataset=train_dataset, + batch_size=self.config.batch_size, + num_workers=self.config.num_workers, + pin_memory=self.config.pin_memory, + generator=self.torch_rng ) - # Launch actual training: - - # Single worker case - if not self.cluster.distributed: - with self.cluster.init_dist_gpu(worker_id) as device: - self.device: torch.device = device - self.model = self.model.to(self.device) - self.setup_logger() - self._setup_metrics() - try: - train_result = self.train() - except Exception as exc: - print(exc) - raise exc - finally: - print("INFO: Training ended") - self.destroy_logger() - train_result = None - return train_result - - # Init / connect to distributed backend - with self.cluster.init_dist_gpu(worker_id) as device: - self.device: torch.device = device - self._distribute_model() - self.setup_logger() - self._setup_metrics() - try: - train_result = self.train() - except Exception as exc: - print(exc) - raise exc - finally: - print("INFO: Training ended") - self.destroy_logger() - train_result = None - return train_result - - def _instantiate_dataloader( + def _setup_metrics(self): + """Move metrics to current device.""" + for m_name, metric in self.metrics.items(): + self.metrics[m_name] = metric.to(self.device) + + @monitor_exec + def execute( self, - dataloader_class: Type, - dataset: Dataset, - init_kwargs: Dict - ) -> DataLoader: - """Make dataloader distributed if using distributed training strategy. + train_dataset: Dataset, + validation_dataset: Dataset, + test_dataset: Dataset + ) -> Tuple[Dataset, Dataset, Dataset, Any]: + """Prepares distributed environment and data structures + for the actual training. Args: - dataloader_class (Type): some torch DataLoader type. - dataset (Dataset): torch dataset instance. - init_kwargs (Dict): constructor args. + train_dataset (Dataset): training dataset. + validation_dataset (Dataset): validation dataset. + test_dataset (Dataset): test dataset. + + Returns: + Tuple[Dataset, Dataset, Dataset, Any]: training dataset, + validation dataset, test dataset, trained model. """ - init_kwargs['generator'] = init_kwargs.get( - 'generator', self.torch_rng - ) - init_kwargs['worker_init_fn'] = init_kwargs.get( - 'worker_init_fn', seed_worker + self.torch_rng = set_seed(self.random_seed) + self._init_distributed_strategy() + self._setup_metrics() + + self.create_dataloaders( + train_dataset=train_dataset, + validation_dataset=validation_dataset, + test_dataset=test_dataset ) + self.create_model_loss_optimizer() - if self.strategy == StrategyT.DDP.value and self.cluster.distributed: - sampler = DistributedSampler( - dataset=dataset, - num_replicas=self.cluster.global_world_size, - rank=self.cluster.global_rank, - shuffle=init_kwargs.get( - 'shuffle', False - ) - ) - # Overwrite existing sampler, if given. - # TODO: improve using wrapper: - # https://discuss.pytorch.org/t/how-to-use-my-own-sampler-when-i-already-use-distributedsampler/62143?page=2 - init_kwargs['sampler'] = sampler - if init_kwargs.get('shuffle') is not None: - # sampler option is mutually exclusive with shuffle - del init_kwargs['shuffle'] + if self.strategy.is_main_worker: + self.logger.create_logger_context() - return dataloader_class(dataset, **init_kwargs) + self.train() - def _setup_metrics(self): - for m_name, metric in self.train_metrics.items(): - self.train_metrics[m_name] = metric.to(self.device) - for m_name, metric in self.validation_metrics.items(): - self.validation_metrics[m_name] = metric.to(self.device) - - def _distribute_model(self): - if self.cluster.distributed: - # Distribute model - self.model = self.model.to(self.device) - if self.strategy == StrategyT.NONE.value: - print( - "WARNING: A GPU cluster is available but no distributed " - "strategy was given... Falling back to single worker...") - if not self.cluster.is_main_worker(): - # Use only GPU:0 for single worker - sys.exit(0) - elif self.strategy == StrategyT.DDP.value: - self.model = DDP( - self.model, - device_ids=[self.device.index], - output_device=self.device - ) - else: - raise NotImplementedError("Only DDP strategy is implemented.") - else: - raise RuntimeError( - "Trying to distribute a model when a " - "distributed cluster is not available." - ) + if self.strategy.is_main_worker: + self.logger.destroy_logger_context() + self.strategy.clean_up() + return train_dataset, validation_dataset, test_dataset, self.model - def setup_logger(self): - if self.cluster.is_main_worker(): - # Only setup loggers on main worker - if isinstance(self.logger, list): - for logger in self.logger: - logger.create_logger_context() - elif isinstance(self.logger, Logger): - self.logger.create_logger_context() - else: - raise TypeError( - "Unrecognized self.logger. Allowed types are 'list' and " - f"'Logger'. Received {type(self.logger)}" - ) - else: - self.logger = [] - - def destroy_logger(self): - if self.cluster.is_main_worker(): - if isinstance(self.logger, list): - for logger in self.logger: - logger.destroy_logger_context() - elif isinstance(self.logger, Logger): - self.logger.destroy_logger_context() - else: - raise TypeError( - "Unrecognized self.logger. Allowed types are 'list' and " - f"'Logger'. Received {type(self.logger)}" - ) + def _set_epoch_dataloaders(self, epoch: int): + """ + Sets epoch in the distributed sampler of a dataloader when using it. + """ + if self.strategy.is_distributed: + self.train_dataloader.sampler.set_epoch(epoch) + if self.validation_dataloader is not None: + self.validation_dataloader.sampler.set_epoch(epoch) + if self.test_dataloader is not None: + self.test_dataloader.sampler.set_epoch(epoch) def log( self, @@ -513,39 +335,44 @@ def log( kind: str = 'metric', step: Optional[int] = None, batch_idx: Optional[int] = None, - every_worker: bool = False, **kwargs ) -> None: - if self.cluster.is_main_worker() or every_worker: - # Only log on main worker if not specified otherwise - if isinstance(self.logger, list): - for logger in self.logger: - logger.log( - item=item, - identifier=identifier, - kind=kind, - step=step, - batch_idx=batch_idx, - **kwargs - ) - elif isinstance(self.logger, Logger): - self.logger.log( - item=item, - identifier=identifier, - kind=kind, - step=step, - batch_idx=batch_idx, - **kwargs - ) - else: - raise TypeError( - "Unrecognized self.logger. Allowed types are 'list' and " - f"'Logger'. Received {type(self.logger)}" - ) + if self.logger and ( + self.strategy.is_main_worker or self.log_all_workers): + self.logger.log( + item=item, + identifier=identifier, + kind=kind, + step=step, + batch_idx=batch_idx, + **kwargs + ) + + def train(self): + """Trains a machine learning model. + Main training loop/logic. + + Args: + train_dataset (Dataset): training dataset. + validation_dataset (Dataset): validation dataset. + test_dataset (Dataset): test dataset. + + Returns: + Tuple[Dataset, Dataset, Dataset, Any]: training dataset, + validation dataset, test dataset, trained model. + """ + # start_time = time.perf_counter() + for epoch in range(self.epochs): + epoch_n = epoch + 1 + self._set_epoch_dataloaders(epoch) + self.train_epoch() + if self.validation_every and self.validation_every % epoch_n == 0: + self.validation_epoch() + if self.test_every and self.test_every % epoch_n == 0: + self.test_epoch() def compute_metrics( self, - metrics: Dict[str, Metric], true: Batch, pred: Batch, logger_step: int, @@ -566,7 +393,7 @@ def compute_metrics( Dict[str, Any]: metric values. """ m_values = {} - for m_name, metric in metrics.items(): + for m_name, metric in self.metrics.items(): # metric = metric.to(self.device) m_val = metric(pred, true).detach().cpu().numpy() self.log( @@ -596,7 +423,6 @@ def training_step( batch_idx=batch_idx ) metrics: Dict[str, Any] = self.compute_metrics( - metrics=self.train_metrics, true=y, pred=pred_y, logger_step=self.train_glob_step, @@ -612,8 +438,9 @@ def validation_step( ) -> Tuple[Loss, Dict[str, Any]]: x, y = batch x, y = x.to(self.device), y.to(self.device) - pred_y = self.model(x) - loss: Loss = self.loss(pred_y, y) + with torch.no_grad(): + pred_y = self.model(x) + loss: Loss = self.loss(pred_y, y) self.log( item=loss.item(), identifier='validation_loss', @@ -622,7 +449,6 @@ def validation_step( batch_idx=batch_idx ) metrics: Dict[str, Any] = self.compute_metrics( - metrics=self.validation_metrics, true=y, pred=pred_y, logger_step=self.validation_glob_step, @@ -631,7 +457,7 @@ def validation_step( ) return loss, metrics - def training_epoch(self) -> Loss: + def train_epoch(self) -> Loss: self.model.train() train_losses = [] for batch_idx, train_batch in enumerate(self.train_dataloader): @@ -684,264 +510,130 @@ def validation_epoch(self) -> Loss: ) return avg_loss - def train(self): + def test_epoch(self): + # TODO: implement test epoch + raise NotImplementedError() - if self.optimizer is None: - raise ValueError("Undefined optimizer!") - - if self.loss is None: - raise ValueError("Undefined loss function!") - - st = time.time() - - # Resume state - self.start_epoch = 1 - self.best_loss = np.Inf - self.load_state() - - # start training/testing loop - if self.cluster.is_main_worker(): - print(f'TIMER: broadcast: {time.time()-st}s') - print('DEBUG: start training') - print('-'*56) - - ############################## - # Start training: run epochs # - ############################## - - et = time.time() - for self.epoch_idx in range(self.start_epoch, self.epochs + 1): - lt = time.time() - - ####################################################### - # Perform one training epoch and one validation epoch # - ####################################################### - - if self.benchrun and self.epoch_idx == self.epochs: - # TODO: move profiler into cluster environment - # profiling (done on last epoch - slower!) - with torch.autograd.profiler.profile( - use_cuda=self.cluster.is_cuda_available(), - profile_memory=True - ) as prof: - train_loss = self.training_epoch() - else: - train_loss = self.training_epoch() - val_loss = self.validation_epoch() - - ##################################### - # Save checkpoint if model improved # - ##################################### - - ref_loss = val_loss if val_loss is not None else train_loss - is_best = ref_loss < self.best_loss - if (self.epoch_idx % self.checkpoint_every == 0 - and not self.benchrun): - self.save_state( - loss_val=ref_loss, - is_best=is_best - ) - self.best_loss = min(ref_loss, self.best_loss) - - ########################### - # End of epoch operations # - ########################### - - # save first epoch timer - if self.epoch_idx == self.start_epoch: - first_ep_t = time.time()-lt - - # Final epoch - if self.epoch_idx + 1 == self.epochs: - self.train_dataloader.last_epoch = True - self.validation_dataloader.last_epoch = True - - if self.cluster.is_main_worker(): - print(f'TIMER: epoch time: {time.time()-lt}s') - if self.benchrun and self.epoch_idx == self.epochs: - print('-'*56) - print('benchmark of last epoch:') - what1 = ( - 'cuda' if self.cluster.is_cuda_available() else 'cpu' - ) - print( - prof.key_averages().table( - sort_by='self_'+str(what1)+'_time_total' - ) - ) - - ########################## - # Training has completed # - ########################## - - # save final state - if not self.benchrun: - self.save_state( - loss_val=ref_loss, - is_best=is_best - ) - if self.cluster.is_cuda_available() and self.cluster.distributed: - dist.barrier() - - ######################## - # Print training stats # - ######################## - - if self.cluster.is_main_worker(): - print('-'*56) - print('training results:') - print(f'TIMER: first epoch time: {first_ep_t}s') - print(f'TIMER: last epoch time: {time.time()-lt}s') - print( - f'TIMER: average epoch time: {(time.time()-et)/self.epochs}s') - print(f'TIMER: total epoch time: {time.time()-et}s') - if self.epoch_idx > 1: - print( - f'TIMER: total epoch-1 time: {time.time()-et-first_ep_t}s' - ) - print( - 'TIMER: average epoch-1 time: ' - f'{(time.time()-et-first_ep_t)/(self.epochs-1)}s') - if self.benchrun: - print( - f'TIMER: total epoch-2 time: {lt-first_ep_t}s') - print('TIMER: average epoch-2 time: ' - f'{(lt-first_ep_t)/(self.epochs-2)}s') - mem = int(torch.cuda.memory_reserved( - self.cluster.local_rank)/1024/1024) - print( - f'memory req: {mem} MB' - if self.cluster.is_cuda_available() - and self.cluster.distributed else 'memory req: - MB' - ) - if self.cluster.is_cuda_available(): - print( - f'memory summary:\n {torch.cuda.memory_summary(0)}') - - if self.cluster.is_main_worker(): - print(f'TIMER: final time: {time.time()-st} s') - - def save_state(self, loss_val: Any, is_best: bool): - """Save training state.""" - res_name = 'checkpoint.pth.tar' - rt = time.time() - - if (self.cluster.is_cuda_available() and self.cluster.distributed): - # find if is_best happened in any worker - is_best_m = par_allgather_obj( - is_best, self.cluster.global_world_size - ) - if any(is_best_m): - # TODO: is this strategy really good? Checkpointing when - # at least one worker improves the loss on their local - # data split is prone to overfitting, especially when - # the dataset in unbalanced! - - # find which rank is_best happened - select first rank - # if multiple - best_rank = np.where(np.array(is_best_m))[0][0] - if self.cluster.global_rank == best_rank: - self._save_sate( - epoch=self.epoch_idx+1, - loss_val=loss_val, - save_path=res_name - ) - print( - f'DEBUG: state in {self.cluster.global_rank} is ' - f'saved on epoch:{self.epoch_idx} ' - f'in {time.time()-rt} s') - else: - self._save_sate( - epoch=self.epoch_idx+1, - loss_val=loss_val, - save_path=res_name - ) - print( - f'DEBUG: state in {self.cluster.global_rank} ' - f'is saved on epoch:{self.epoch_idx} in {time.time()-rt} s') - def _save_sate( +class TorchLightningTrainer(Trainer): + """Generic trainer for torch Lightning workflows. + + Args: + config (Union[Dict, str]): (path to a) Lightning configuration + https://pytorch-lightning.readthedocs.io/en/1.6.5/common/lightning_cli.html + mlflow_saved_model (str, optional): name of the model created in + MLFlow. Defaults to 'my_model'. + """ + + def __init__( self, - epoch: int, - loss_val: Any, - save_path: str + config: Union[Dict, str], + mlflow_saved_model: str = 'my_model' ): - """Save state on disk.""" - sched = ( - self.lr_scheduler.state_dict() - if self.lr_scheduler is not None else None + self.save_parameters(**self.locals2params(locals())) + super().__init__() + if isinstance(config, str) and os.path.isfile(config): + # Load from YAML + config = load_yaml(config) + self.conf = config + self.mlflow_saved_model = mlflow_saved_model + + @monitor_exec + def execute(self) -> Any: + init_lightning_mlflow( + self.conf, + tmp_dir='/tmp', + registered_model_name=self.mlflow_saved_model ) - state = { - 'epoch': epoch, - 'state_dict': self.model.state_dict(), - 'best_loss': loss_val, - 'optimizer': self.optimizer.state_dict(), - 'lr_scheduler': sched - } - self.log( - item=state, - identifier=save_path, - kind='torch', - epoch_step=self.epoch_idx, - batch_step=0 + old_argv = sys.argv + sys.argv = ['some_script_placeholder.py'] + cli = LightningCLI( + args=self.conf, + model_class=L.LightningModule, + datamodule_class=L.LightningDataModule, + run=False, + save_config_kwargs={ + "overwrite": True, + "config_filename": "pl-training.yml", + }, + subclass_mode_model=True, + subclass_mode_data=True, ) + sys.argv = old_argv + cli.trainer.fit(cli.model, datamodule=cli.datamodule) + teardown_lightning_mlflow() - def load_state(self): - """Load training state.""" - res_name = 'checkpoint.pth.tar' - if os.path.isfile(res_name) and not self.benchrun: - try: - if (self.cluster.is_cuda_available() - and self.cluster.distributed): - dist.barrier() - # Map model to be loaded to specified single gpu. - # loc = ( - # {'cuda:%d' % 0: 'cuda:%d' % self.cluster.local_rank} - # if self.cluster.is_cuda_available() - # else {'cpu:%d' % 0: 'cpu:%d' % self.cluster.local_rank} - # ) - # checkpoint = torch.load(res_name, map_location=loc) - checkpoint = torch.load( - res_name, map_location=self.device - ) - else: - checkpoint = torch.load(res_name, map_location='cpu') - self.start_epoch = checkpoint['epoch'] - self.best_loss = checkpoint['best_loss'] - self.model.load_state_dict(checkpoint['state_dict']) - self.optimizer.load_state_dict(checkpoint['optimizer']) - if self.lr_scheduler is not None: - self.lr_scheduler.load_state_dict( - checkpoint['lr_scheduler'] - ) - if self.cluster.is_cuda_available(): - if self.cluster.is_main_worker(): - print( - f'WARNING: restarting from {self.start_epoch} ' - 'epoch') - else: - print( - f'WARNING: restarting from {self.start_epoch} epoch') - except Exception: - if self.cluster.is_cuda_available(): - if self.cluster.is_main_worker(): - print( - 'restart file cannot be loaded, restarting!') - else: - print( - 'WARNING: restart file cannot be loaded, restarting!') - - if self.start_epoch >= self.epochs + 1: - if self.cluster.is_cuda_available() and self.cluster.distributed: - if self.cluster.is_main_worker(): - print( - 'WARNING: given epochs are less than the ' - 'one in the restart file!') - print('WARNING: SYS.EXIT is issued') - sys.exit() - else: - print( - 'WARNING: given epochs are less than the ' - 'one in the restart file!') - print('WARNING: SYS.EXIT is issued') - sys.exit() + +def preproc_dataloader(dataloader: DataLoader, gwsize, grank): + """Makes a Dataloader distributed.""" + sampler = DistributedSampler( + dataloader.dataset, + num_replicas=gwsize, + rank=grank, + shuffle=True + ) + # Recreate dataloader, with updated sampler + return DataLoader( + dataloader.dataset, + batch_size=dataloader.batch_size, + sampler=sampler, + num_workers=dataloader.num_workers, + collate_fn=dataloader.collate_fn, + pin_memory=dataloader.pin_memory, + drop_last=dataloader.drop_last, + timeout=dataloader.timeout, + worker_init_fn=seed_worker, # dataloader.worker_init_fn, + multiprocessing_context=dataloader.multiprocessing_context, + generator=dataloader.generator, + prefetch_factor=dataloader.prefetch_factor, + persistent_workers=dataloader.persistent_workers, + pin_memory_device=dataloader.pin_memory_device + ) + + +def distributed(func): + """The decorated function must have a standard signature. + Its first arguments must be: + model, train_dataloader, validation_dataloader, device (in this order). + + Additional args or kwargs are allowed consistently with the signature + of the decorated function. + """ + def dist_train( + model, train_dataloader, validation_dataloader=None, device='cpu', + *args, **kwargs + ): + if torch.cuda.is_available(): + dist.init_process_group(backend='nccl') + + if torch.cuda.is_available(): + lwsize = torch.cuda.device_count() # local world size - per node + gwsize = dist.get_world_size() # global world size - per run + grank = dist.get_rank() # global rank - assign per run + lrank = dist.get_rank() % lwsize # local rank - assign per node + else: + gwsize = 1 + grank = 0 + lrank = 0 + + device = torch.device( + 'cuda' if torch.cuda.is_available() else 'cpu', lrank) + if torch.cuda.is_available(): + torch.cuda.set_device(lrank) + + model = model.to(device) + model = DDP(model, device_ids=[device], output_device=device) + + train_dataloader = preproc_dataloader(train_dataloader, gwsize, grank) + if validation_dataloader is not None: + validation_dataloader = preproc_dataloader( + validation_dataloader, gwsize, grank) + + try: + func(model, train_dataloader, validation_dataloader, device, + *args, **kwargs) + finally: + if torch.cuda.is_available(): + dist.barrier() + dist.destroy_process_group() + return dist_train diff --git a/src/itwinai/torch/types.py b/src/itwinai/torch/types.py index 614462ad..0b6f88ad 100644 --- a/src/itwinai/torch/types.py +++ b/src/itwinai/torch/types.py @@ -64,3 +64,11 @@ class TorchOptimizer(BaseEnum): """ SGD = 'SGD' ADAM = 'Adam' + + +class UninitializedStrategyError(Exception): + """Error raised when a strategy has not been initialized.""" + + +class DistributedStrategyError(Exception): + """Error raised when a strategy has already been initialized.""" diff --git a/src/itwinai/torch/utils.py b/src/itwinai/torch/utils.py deleted file mode 100644 index 99bcd246..00000000 --- a/src/itwinai/torch/utils.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import Hashable, Dict -import time -import numpy as np -import random - -import torch -import torch.distributed as dist - - -def save_state( - epoch, distrib_model, loss_val, optimizer, res_name, grank, gwsize, - is_best, distributed: bool = True -): - """Save training state""" - rt = time.time() - # find if is_best happened in any worker - if torch.cuda.is_available() and distributed: - is_best_m = par_allgather_obj(is_best, gwsize) - - if torch.cuda.is_available() and distributed: - if any(is_best_m): - # find which rank is_best happened - select first rank if multiple - is_best_rank = np.where(np.array(is_best_m))[0][0] - - # collect state - state = {'epoch': epoch + 1, - 'state_dict': distrib_model.state_dict(), - 'best_loss': loss_val, - 'optimizer': optimizer.state_dict()} - - # write on worker with is_best - if grank == is_best_rank: - torch.save(state, './'+res_name) - print(f'DEBUG: state in {grank} is saved on ' - f'epoch:{epoch} in {time.time()-rt} s') - else: - # collect state - state = {'epoch': epoch + 1, - 'state_dict': distrib_model.state_dict(), - 'best_loss': loss_val, - 'optimizer': optimizer.state_dict()} - - torch.save(state, './'+res_name) - print( - f'DEBUG: state in {grank} is saved on epoch:{epoch} ' - f'in {time.time()-rt} s') - - -def seed_worker(worker_id): - """deterministic dataloader""" - worker_seed = torch.initial_seed() % 2**32 - np.random.seed(worker_seed) - random.seed(worker_seed) - - -def par_allgather_obj(obj, gwsize): - """gathers any object from the whole group in a list (to all workers)""" - res = [None]*gwsize - dist.all_gather_object(res, obj, group=None) - # print(f'ALLGATHER: {res}') - return res - - -def clear_key( - my_dict: Dict, - dict_name: str, - key: Hashable, - complain: bool = True -) -> Dict: - """Remove key from dictionary if present and complain. - - Args: - my_dict (Dict): Dictionary. - dict_name (str): name of the dictionary. - key (Hashable): Key to remove. - """ - if key in my_dict: - if complain: - print( - f"Field '{key}' should not be present " - f"in dictionary '{dict_name}'" - ) - del my_dict[key] - return my_dict diff --git a/src/itwinai/utils.py b/src/itwinai/utils.py index 52279aeb..280de5d3 100644 --- a/src/itwinai/utils.py +++ b/src/itwinai/utils.py @@ -1,14 +1,11 @@ """ Utilities for itwinai package. """ -from typing import Dict, Type, Callable, Tuple -import os +from typing import Dict, Type, Callable, Tuple, Hashable import sys import inspect from collections.abc import MutableMapping import yaml -from omegaconf import OmegaConf -from omegaconf.dictconfig import DictConfig def load_yaml(path: str) -> Dict: @@ -32,32 +29,6 @@ def load_yaml(path: str) -> Dict: return loaded_config -def load_yaml_with_deps(path: str) -> DictConfig: - """ - Load YAML file with OmegaConf and merge it with its dependencies - specified in the `conf-dependencies` field. - Assume that the dependencies live in the same folder of the - YAML file which is importing them. - - Args: - path (str): path to YAML file. - - Raises: - exc: yaml.YAMLError for loading/parsing errors. - - Returns: - DictConfig: nested representation of parsed YAML file. - """ - yaml_conf = load_yaml(path) - use_case_dir = os.path.dirname(path) - deps = [] - if yaml_conf.get("conf-dependencies"): - for dependency in yaml_conf["conf-dependencies"]: - deps.append(load_yaml(os.path.join(use_case_dir, dependency))) - - return OmegaConf.merge(yaml_conf, *deps) - - def dynamically_import_class(name: str) -> Type: """ Dynamically import class by module path. @@ -115,18 +86,6 @@ def flatten_dict( return dict(items) -# Parse (part of) YAML loaded in memory -def parse_pipe_config(yaml_file, parser): - with open(yaml_file, "r", encoding="utf-8") as f: - try: - config = yaml.safe_load(f) - except yaml.YAMLError as exc: - print(exc) - raise exc - - return parser.parse_object(config) - - class SignatureInspector: """Provides the functionalities to inspect the signature of a function or a method. @@ -181,3 +140,42 @@ def max_params_num(self) -> int: if self.has_kwargs or self.has_varargs: return self.INFTY return len(self.func_params) + + +def str_to_slice(interval: str) -> slice: + import re + # TODO: add support for slices starting with empty index + # e.g., :20:3 + if not re.match(r"\d+(:\d+)?(:\d+)?", interval): + raise ValueError( + f"Received invalid interval for slice: '{interval}'" + ) + if ":" in interval: + return slice(*map( + lambda x: int(x.strip()) if x.strip() else None, + interval.split(':') + )) + return int(interval) + + +def clear_key( + my_dict: Dict, + dict_name: str, + key: Hashable, + complain: bool = True +) -> Dict: + """Remove key from dictionary if present and complain. + + Args: + my_dict (Dict): Dictionary. + dict_name (str): name of the dictionary. + key (Hashable): Key to remove. + """ + if key in my_dict: + if complain: + print( + f"Field '{key}' should not be present " + f"in dictionary '{dict_name}'" + ) + del my_dict[key] + return my_dict diff --git a/tests/components/test_components.py b/tests/components/test_components.py index 3ec55453..890188d7 100644 --- a/tests/components/test_components.py +++ b/tests/components/test_components.py @@ -74,11 +74,6 @@ class MyTrainer(Trainer): def execute(self): ... - def save_state(self): - ... - - def load_state(self): - ... comp = MyTrainer() with pytest.raises(SerializationError) as exc_info: dict_serializ = comp.to_dict() diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index 26b57cb0..00000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Test itwinai CLI. -""" - -import subprocess -import pytest - - -@pytest.mark.skip(reason="cli deprecated") -def test_datasets_viz(): - """ - Test visualization of use case's dataset registry. - """ - USE_CASE = "use-cases/mnist/" - subprocess.run( - f"itwinai datasets --use-case {USE_CASE}".split(), check=True) - - -@pytest.mark.skip(reason="cli deprecated") -def test_workflows_viz(): - """ - Test visualization of use case's workflows. - """ - USE_CASE = "./use-cases/mnist/" - subprocess.run( - f"itwinai workflows --use-case {USE_CASE}".split(), check=True) diff --git a/tests/use-cases/conftest.py b/tests/use-cases/conftest.py index d080e0a8..69229db6 100644 --- a/tests/use-cases/conftest.py +++ b/tests/use-cases/conftest.py @@ -2,9 +2,9 @@ from typing import Callable import pytest import subprocess +import random +import string -pytest.TORCH_PREFIX = './.venv-pytorch' -pytest.TF_PREFIX = './.venv-tf' FNAMES = [ 'pipeline.yaml', @@ -12,6 +12,52 @@ ] +def rnd_string(len: int = 26): + return ''.join(random.sample(string.ascii_lowercase, len)) + + +@pytest.fixture +def tmp_test_dir(): + root = '/tmp/pytest' + os.makedirs(root, exist_ok=True) + test_dir = os.path.join(root, rnd_string()) + while os.path.exists(test_dir): + test_dir = os.path.join(root, rnd_string()) + os.makedirs(test_dir, exist_ok=True) + + yield test_dir + + # Optional: remove dir here... + + +@pytest.fixture +def torch_env() -> str: + """ + Return absolute path to torch virtual environment parsing it + from environment variables, if provided, otherwise fall back + to ``./.venv-pytorch``. + """ + if os.environ.get('TORCH_ENV') is None: + env_p = './.venv-pytorch' + else: + env_p = os.environ.get('TORCH_ENV') + return os.path.abspath(env_p) + + +@pytest.fixture +def tf_env() -> str: + """ + Return absolute path to tensorflow virtual environment parsing it + from environment variables, if provided, otherwise fall back + to ``./.venv-tf``. + """ + if os.environ.get('TF_ENV') is None: + env_p = './.venv-tf' + else: + env_p = os.environ.get('TF_ENV') + return os.path.abspath(env_p) + + @pytest.fixture def check_folder_structure() -> Callable: """ @@ -31,7 +77,6 @@ def install_requirements() -> Callable: def _install_reqs(root: str, env_prefix: str): req_path = os.path.join(root, 'requirements.txt') if os.path.isfile(req_path): - cmd = (f"micromamba run -p {env_prefix} " - f"pip install -r {req_path}") + cmd = f"{env_prefix}/bin/pip install -r {req_path}" subprocess.run(cmd.split(), check=True) return _install_reqs diff --git a/tests/use-cases/test_3dgan.py b/tests/use-cases/test_3dgan.py index c57e21ff..9d19d1f3 100644 --- a/tests/use-cases/test_3dgan.py +++ b/tests/use-cases/test_3dgan.py @@ -3,73 +3,70 @@ """ import pytest import subprocess -# from itwinai.utils import dynamically_import_class +import os CERN_PATH = "use-cases/3dgan" -CKPT_PATH = "3dgan-inference.pth" - - -@pytest.fixture(scope="module") -def fake_model_checkpoint() -> None: - """ - Create a dummy model checkpoint for inference. - """ - import sys - import torch - sys.path.append(CERN_PATH) - from model import ThreeDGAN - # ThreeDGAN = dynamically_import_class('model.ThreeDGAN') - net = ThreeDGAN() - torch.save(net, CKPT_PATH) +CKPT_NAME = "3dgan-inference.pth" +@pytest.mark.skip("deprecated") def test_structure_3dgan(check_folder_structure): """Test 3DGAN folder structure.""" check_folder_structure(CERN_PATH) @pytest.mark.functional -def test_3dgan_train(install_requirements): +def test_3dgan_train(torch_env, tmp_test_dir, install_requirements): """ Test 3DGAN torch lightning trainer by running it end-to-end. """ - install_requirements(CERN_PATH, pytest.TORCH_PREFIX) - # cmd = (f"micromamba run -p {pytest.TORCH_PREFIX} python " - # f"{CERN_PATH}/train.py -p {CERN_PATH}/pipeline.yaml") + install_requirements(CERN_PATH, torch_env) + conf = os.path.join(os.path.abspath(CERN_PATH), 'pipeline.yaml') trainer_params = "pipeline.init_args.steps.training_step.init_args" - cmd = (f"micromamba run -p {pytest.TORCH_PREFIX} itwinai exec-pipeline " - f"--config {CERN_PATH}/pipeline.yaml " + cmd = (f"{torch_env}/bin/itwinai exec-pipeline " + f"--config {conf} " f'-o {trainer_params}.config.trainer.accelerator=cpu ' f'-o {trainer_params}.config.trainer.strategy=auto ' ) - subprocess.run(cmd.split(), check=True) + subprocess.run(cmd.split(), check=True, cwd=tmp_test_dir) @pytest.mark.functional -def test_3dgan_inference(install_requirements, fake_model_checkpoint): +def test_3dgan_inference( + torch_env, + tmp_test_dir, + install_requirements, + # fake_model_checkpoint +): """ Test 3DGAN torch lightning trainer by running it end-to-end. """ - install_requirements(CERN_PATH, pytest.TORCH_PREFIX) - # cmd = (f"micromamba run -p {pytest.TORCH_PREFIX} python " - # f"{CERN_PATH}/train.py -p {CERN_PATH}/pipeline.yaml") - # cmd = (f"micromamba run -p {pytest.TORCH_PREFIX} itwinai exec-pipeline " - # f"--config {CERN_PATH}/inference-pipeline.yaml") + install_requirements(CERN_PATH, torch_env) + + # Create fake inference dataset and checkpoint + exec = os.path.join(os.path.abspath(CERN_PATH), + 'create_inference_sample.py') + cmd = (f"{torch_env}/bin/python {exec} " + f"--root {tmp_test_dir} " + f"--ckpt-name {CKPT_NAME}") + subprocess.run(cmd.split(), check=True, cwd=tmp_test_dir) + # Test inference + conf = os.path.join(os.path.abspath(CERN_PATH), 'inference-pipeline.yaml') getter_params = "pipeline.init_args.steps.dataloading_step.init_args" trainer_params = "pipeline.init_args.steps.inference_step.init_args" logger_params = trainer_params + ".config.trainer.logger.init_args" data_params = trainer_params + ".config.data.init_args" saver_params = "pipeline.init_args.steps.saver_step.init_args" cmd = ( - 'itwinai exec-pipeline ' - '--config use-cases/3dgan/inference-pipeline.yaml ' + f'{torch_env}/bin/itwinai exec-pipeline ' + f'--config {conf} ' f'-o {getter_params}.data_path=exp_data ' - f'-o {trainer_params}.model.init_args.model_uri={CKPT_PATH} ' - f'-o {trainer_params}.config.trainer.accelerator=cpu ' + f'-o {trainer_params}.model.init_args.model_uri={CKPT_NAME} ' + f'-o {trainer_params}.config.trainer.accelerator=auto ' f'-o {trainer_params}.config.trainer.strategy=auto ' f'-o {logger_params}.save_dir=ml_logs/mlflow_logs ' f'-o {data_params}.datapath=exp_data/*/*.h5 ' f'-o {saver_params}.save_dir=3dgan-generated-data ' ) - subprocess.run(cmd.split(), check=True) + subprocess.run(cmd.split(), check=True, cwd=CERN_PATH) diff --git a/tests/use-cases/test_cyclones.py b/tests/use-cases/test_cyclones.py index 1a5ebb3f..d6a1ea2c 100644 --- a/tests/use-cases/test_cyclones.py +++ b/tests/use-cases/test_cyclones.py @@ -7,10 +7,12 @@ import pytest import subprocess +import os CYCLONES_PATH = "use-cases/cyclones" +@pytest.mark.skip("deprecated") def test_structure_cyclones(check_folder_structure): """Test cyclones folder structure.""" check_folder_structure(CYCLONES_PATH) @@ -18,11 +20,14 @@ def test_structure_cyclones(check_folder_structure): @pytest.mark.functional @pytest.mark.memory_heavy -def test_cyclones_train_tf(install_requirements): +def test_cyclones_train_tf(tf_env, tmp_test_dir, install_requirements): """ Test Cyclones tensorflow trainer by running it end-to-end. """ - install_requirements(CYCLONES_PATH, pytest.TF_PREFIX) - cmd = (f"micromamba run -p {pytest.TF_PREFIX} python " - f"{CYCLONES_PATH}/train.py -p {CYCLONES_PATH}/pipeline.yaml") - subprocess.run(cmd.split(), check=True) + # TODO: create a small sample dataset for tests only + install_requirements(CYCLONES_PATH, tf_env) + pipe = os.path.join(os.path.abspath(CYCLONES_PATH), 'pipeline.yaml') + train = os.path.join(os.path.abspath(CYCLONES_PATH), 'train.py') + cmd = (f"{tf_env}/bin/python {train} " + f"-p {pipe}") + subprocess.run(cmd.split(), check=True, cwd=tmp_test_dir) diff --git a/tests/use-cases/test_mnist.py b/tests/use-cases/test_mnist.py index d32aab1c..1f18a8e6 100644 --- a/tests/use-cases/test_mnist.py +++ b/tests/use-cases/test_mnist.py @@ -7,72 +7,100 @@ import pytest import subprocess +import os +# from itwinai.cli import exec_pipeline TORCH_PATH = "use-cases/mnist/torch" LIGHTNING_PATH = "use-cases/mnist/torch-lightning" TF_PATH = "use-cases/mnist/tensorflow" +@pytest.mark.skip(reason="structure changed") def test_structure_mnist_torch(check_folder_structure): """Test MNIST folder structure for torch native trainer.""" check_folder_structure(TORCH_PATH) +@pytest.mark.skip(reason="structure changed") def test_structure_mnist_lightning(check_folder_structure): """Test MNIST folder structure for torch lightning trainer.""" check_folder_structure(LIGHTNING_PATH) +@pytest.mark.skip(reason="structure changed") def test_structure_mnist_tf(check_folder_structure): """Test MNIST folder structure for tensorflow trainer.""" check_folder_structure(TF_PATH) @pytest.mark.functional -def test_mnist_train_torch(install_requirements): +def test_mnist_train_torch(torch_env, tmp_test_dir, install_requirements): """ Test MNIST torch native trainer by running it end-to-end. + + To set the torch env path set the ``TORCH_ENV`` env variable: + + >>> export TORCH_ENV="my_env" """ - install_requirements(TORCH_PATH, pytest.TORCH_PREFIX) - cmd = (f"micromamba run -p {pytest.TORCH_PREFIX} python " - f"{TORCH_PATH}/train.py -p {TORCH_PATH}/pipeline.yaml") - subprocess.run(cmd.split(), check=True) + install_requirements(TORCH_PATH, torch_env) + conf = os.path.join(os.path.abspath(TORCH_PATH), 'config.yaml') + cmd = (f"{torch_env}/bin/itwinai exec-pipeline " + f"--config {conf} --pipe-key training_pipeline") + subprocess.run(cmd.split(), check=True, cwd=tmp_test_dir) @pytest.mark.functional -def test_mnist_train_lightning(install_requirements): +def test_mnist_inference_torch(torch_env, tmp_test_dir, install_requirements): """ - Test MNIST torch lightning trainer by running it end-to-end. + Test MNIST torch native inference by running it end-to-end. + + To set the torch env path set the ``TORCH_ENV`` env variable: + + >>> export TORCH_ENV="my_env" """ - install_requirements(TORCH_PATH, pytest.TORCH_PREFIX) - cmd = (f"micromamba run -p {pytest.TORCH_PREFIX} python " - f"{LIGHTNING_PATH}/train.py -p {LIGHTNING_PATH}/pipeline.yaml") - subprocess.run(cmd.split(), check=True) + install_requirements(TORCH_PATH, torch_env) + + # Create fake inference dataset and checkpoint + exec = os.path.join(os.path.abspath(TORCH_PATH), + 'create_inference_sample.py') + cmd = (f"{torch_env}/bin/python {exec} " + f"--root {tmp_test_dir}") + subprocess.run(cmd.split(), check=True, cwd=tmp_test_dir) + + # Test inference + conf = os.path.join(os.path.abspath(TORCH_PATH), 'config.yaml') + cmd = (f"{torch_env}/bin/itwinai exec-pipeline " + f"--config {conf} --pipe-key inference_pipeline") + subprocess.run(cmd.split(), check=True, cwd=tmp_test_dir) @pytest.mark.functional -def test_mnist_train_tf(install_requirements): +def test_mnist_train_torch_lightning( + torch_env, + tmp_test_dir, + install_requirements +): """ - Test MNIST tensorflow trainer by running it end-to-end. + Test MNIST torch lightning trainer by running it end-to-end. + + To set the torch env path set the ``TORCH_ENV`` env variable: + + >>> export TORCH_ENV="my_env" """ - install_requirements(TF_PATH, pytest.TF_PREFIX) - cmd = (f"micromamba run -p {pytest.TF_PREFIX} python " - f"{TF_PATH}/train.py -p {TF_PATH}/pipeline.yaml") - subprocess.run(cmd.split(), check=True) + install_requirements(LIGHTNING_PATH, torch_env) + conf = os.path.join(os.path.abspath(LIGHTNING_PATH), 'config.yaml') + cmd = (f"{torch_env}/bin/itwinai exec-pipeline " + f"--config {conf} --pipe-key training_pipeline") + subprocess.run(cmd.split(), check=True, cwd=tmp_test_dir) -@pytest.mark.skip(reason="workflow changed. Left as example") -@pytest.mark.integration -def test_mnist_train_legacy(): +@pytest.mark.functional +def test_mnist_train_tf(tf_env, tmp_test_dir, install_requirements): """ - Test MNIST training workflow(s) by running it end-to-end. + Test MNIST tensorflow trainer by running it end-to-end. """ - workflows = [ - "./use-cases/mnist/torch/workflows/training-workflow.yml", - "./use-cases/mnist/tensorflow/workflows/training-workflow.yml", - ] - - for workflow in workflows: - cmd = f"micromamba run -p ./.venv python run-workflow.py -f {workflow}" - subprocess.run(cmd.split(), check=True) - subprocess.run(cmd.split() + ["--cwl"], check=True) + install_requirements(TF_PATH, tf_env) + conf = os.path.join(os.path.abspath(TF_PATH), 'pipeline.yaml') + cmd = (f"{tf_env}/bin/itwinai exec-pipeline " + f"--config {conf} --pipe-key pipeline") + subprocess.run(cmd.split(), check=True, cwd=tmp_test_dir) diff --git a/tutorials/distributed-ml/torch-scaling-test/README.md b/tutorials/distributed-ml/torch-scaling-test/README.md index 74e316c0..1344504e 100644 --- a/tutorials/distributed-ml/torch-scaling-test/README.md +++ b/tutorials/distributed-ml/torch-scaling-test/README.md @@ -38,11 +38,16 @@ setting SLURM environment variables using the `--export` option: ```bash # Launch a distributed training setup with Torch DDP -DIST_MODE="ddp" -RUN_NAME="ddp-bl-imagenent" -TRAINING_CMD="ddp_trainer.py -c config/base.yaml -c config/ddp.yaml" -sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD" \ - --job-name="$RUN_NAME" slurm.sh +export DIST_MODE="ddp" +export RUN_NAME="ddp-bl-imagenent" +export TRAINING_CMD="ddp_trainer.py -c config/base.yaml -c config/ddp.yaml" +export PYTHON_VENV="../../../envAI_hdfml" +export N=2 # Number of nodes +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + --nodes=$N slurm.sh ``` ## Run all training configurations diff --git a/tutorials/distributed-ml/torch-scaling-test/ddp_trainer.py b/tutorials/distributed-ml/torch-scaling-test/ddp_trainer.py index 54f64fef..0a25ae5b 100755 --- a/tutorials/distributed-ml/torch-scaling-test/ddp_trainer.py +++ b/tutorials/distributed-ml/torch-scaling-test/ddp_trainer.py @@ -18,8 +18,11 @@ from itwinai.parser import ArgumentParser as ItAIArgumentParser from itwinai.loggers import EpochTimeTracker +from itwinai.torch.reproducibility import ( + seed_worker, set_seed +) -from utils import seed_worker, imagenet_dataset, set_seed +from utils import imagenet_dataset def parse_params(): @@ -121,7 +124,7 @@ def main(): dist.init_process_group(backend=args.backend) # Set random seed for reproducibility - torch_prng = set_seed(args.rnd_seed, use_cuda) + torch_prng = set_seed(args.rnd_seed, deterministic_cudnn=False) if is_distributed: # get job rank info - rank==0 master gpu diff --git a/tutorials/distributed-ml/torch-scaling-test/deepspeed_trainer.py b/tutorials/distributed-ml/torch-scaling-test/deepspeed_trainer.py index 691712e8..e6022021 100644 --- a/tutorials/distributed-ml/torch-scaling-test/deepspeed_trainer.py +++ b/tutorials/distributed-ml/torch-scaling-test/deepspeed_trainer.py @@ -18,8 +18,11 @@ from itwinai.parser import ArgumentParser as ItAIArgumentParser from itwinai.loggers import EpochTimeTracker +from itwinai.torch.reproducibility import ( + seed_worker, set_seed +) -from utils import seed_worker, set_seed, imagenet_dataset +from utils import imagenet_dataset def parse_params(): @@ -124,7 +127,7 @@ def main(): deepspeed.init_distributed(dist_backend=args.backend) # Set random seed for reproducibility - torch_prng = set_seed(args.rnd_seed, use_cuda) + torch_prng = set_seed(args.rnd_seed, deterministic_cudnn=False) if is_distributed: # Get job rank info - rank==0 master gpu @@ -248,7 +251,7 @@ def main(): print('TIMER: epoch time:', timer()-lt, 's') epoch_time_tracker.add_epoch_time(epoch-1, timer()-lt) - if torch.cuda.is_available(): + if is_distributed: dist.barrier() if grank == 0: diff --git a/tutorials/distributed-ml/torch-scaling-test/horovod_trainer.py b/tutorials/distributed-ml/torch-scaling-test/horovod_trainer.py index 501b545c..a4c3eaa4 100755 --- a/tutorials/distributed-ml/torch-scaling-test/horovod_trainer.py +++ b/tutorials/distributed-ml/torch-scaling-test/horovod_trainer.py @@ -19,8 +19,11 @@ from itwinai.parser import ArgumentParser as ItAIArgumentParser from itwinai.loggers import EpochTimeTracker +from itwinai.torch.reproducibility import ( + seed_worker, set_seed +) -from utils import imagenet_dataset, seed_worker, set_seed +from utils import imagenet_dataset def parse_params(): @@ -129,7 +132,7 @@ def main(): hvd.init() # Set random seed for reproducibility - torch_prng = set_seed(args.rnd_seed, use_cuda) + torch_prng = set_seed(args.rnd_seed, deterministic_cudnn=False) # is_main_worker = True # if is_distributed and (hvd.rank() != 0 or hvd.local_rank() != 0): diff --git a/tutorials/distributed-ml/torch-scaling-test/img/report.png b/tutorials/distributed-ml/torch-scaling-test/img/report.png index 53bb708ac3b94aaa63942b32742c84b1566a74b4..4e81996e1e22505410cb7b19852a7a9558e3c0d5 100644 GIT binary patch literal 198864 zcmeFZWmHyc+cm6P-EMmu7)Tg^lprlg*homjB`s3Y(hY7~L{L(Ylx_tPX}3s&DB&eY zh=7!Ubi+GO^uE9E`ToB@zA>IPhGV1qVqI&UaUAoQa~|h?1v$wr8)-MLTD5A6)P=K3 zt5*Hlvuf4agunj8zl_D$AH;uz?ap1YQ?@d)bJVjjTy;^;?wYxkow>=C0}h5ZwkB4V zd|b!5csUPTwX?fsE5glf@!t<{S=ktKADCZyhnsA;c0t{C)he#Ta( zI(tgRDeOm^qmzp2Qu%0IjY0j14TX-64sE<)r}Fo%{m(K5pX+H3%I~Jx_3w>7_Sb4E zyZ0C#y>#v9U+m|0t&P1u;rBsi<3X{8;St}*6$UJNER{?r+cSDA{abD2XWMxk{~QtO zrSsR@dk0VO`&#=jH*d{<{<3P-dpjn)@bByTfx~R;|MQnsYxhYB|MxEkDlSyvfq!48 z&MR&G&(Ev=;`Oun?_W+ndK~co-2R4lEZu+q*WU*J`&IuLjQ=@+nU4m34d;zgqb!rb zPiZX$&RO*h4fa1i^Qfn(F*7l-3Ja_EM!2Ta@?TYG)BA(m#&p%nuzN~hyr`OGRJ)MQ zxO;c}=g*f)OG}f^J=i@^ANSzB#IoBK2GL8=(b1>8Cni3*?=Ylv z`t<4C`K=o^JZ|Xc6d$H$MDFI+(HdTQzgAdSSR>2mp~w7C&bN&46OL8KHv77;v9X!9 zyfnF8KG;!Q9K@-v)ak+0|Ng{Cqo!3Mjl%x?w)D~mdj`uHJsY2$d+^ezmR6X~hxZG= z`DX>HmhHpgqIt_~)28G@2V-bw=E74_JB-O&B+9NVimIf<#0w@SCeku84L6ro^e^3J z#|F0cz8q^Y%P<*fFKnVOGqD(M&2{OEzRT}AWp2@ymwle8o2h@iPJ&vzyf|`wr_B-G z&1d+GY81+S=(8=pyps0*HQh#JO9im8vr8YJ0p+bQ2yINhEyte_xOm3LPzUNPo84TA(Hr)*oEy>eZIm};?y zdsDi$rd)#Z`fUu2nTFN;R_&rRG&IgHo3o64xn6($hV>kIvIez%!x7VQP@^6k5KgEgkV{(F;_ z&&b=TE@DWr$aC4F&1Eo6=$fobL>c)9Hg3key*p} z>p0w+)ZdUmIb)D;B)B?GlF|KTUdi#a5LuRNlZNv`t=DeGUwj%L>A9rd+}zyqT<@mQ z`>-UIi=irx_fBo!zCHRbgUbci>EWheU5_M=jDpOXTVYI1DHM4|x1ayuW8L0% z!-wcIg;mD2p{n~jS7zfR39C`O7_Gq<uWUT4VhRNuX7Fbr9bhC z_L=Y^^N}MQEG+SPC5bj-E)%R2-r=)5kH}51Yd*jHkGY`c?YWgfJ$Xf*Ot7>NW+8(Y zgNLQohx>3l7GsH+eMB}xCs8(57iCr3)XV-tozf$G#2QdR9qk-uT zPwM;d6M^C3X$1wMx9{9x=jBzxpr|#(Uu?>=H!CeKr#Pjay7_1Svj<%D+awkgv|e2) z-RM5opFk__=5%{LBQZ@g#}tuG#HRZmA{z~2Rd=_6fNA4}wtPp4{vG#^i-p;~7f#m9 zN#9B@qN1gC=~Cj&_1n}2{UYz^T`jq@rKQL{zrMacAS5K|_3Ptgh1Z4(Gzj;G9nKP- z@w9Tr&@avnW{{5|fW-IpUB≠+~3#1vkY#?Ae^#Iy*Z}KSce5u()vDgL^C8@hH_4 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 TdYwP8-z#a=2!sCq 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 BDf0jT diff --git a/tutorials/distributed-ml/torch-scaling-test/itwinai_trainer.py b/tutorials/distributed-ml/torch-scaling-test/itwinai_trainer.py index a1eacc20..cded83af 100644 --- a/tutorials/distributed-ml/torch-scaling-test/itwinai_trainer.py +++ b/tutorials/distributed-ml/torch-scaling-test/itwinai_trainer.py @@ -21,14 +21,17 @@ from itwinai.torch.distributed import ( TorchDistributedStrategy, - DDPDistributedStrategy, - HVDDistributedStrategy, - DSDistributedStrategy, + TorchDDPStrategy, + HorovodStrategy, + DeepSpeedStrategy, ) from itwinai.parser import ArgumentParser as ItAIArgumentParser from itwinai.loggers import EpochTimeTracker +from itwinai.torch.reproducibility import ( + seed_worker, set_seed +) -from utils import seed_worker, imagenet_dataset, set_seed +from utils import imagenet_dataset def parse_params() -> argparse.Namespace: @@ -116,8 +119,8 @@ def train( model.train() t_list = [] loss_acc = 0 - gwsize = strategy.dist_gwsize() - if strategy.is_main_worker(): + gwsize = strategy.global_world_size() + if strategy.is_main_worker: print("\n") for batch_idx, (data, target) in enumerate(train_loader): t = timer() @@ -127,7 +130,7 @@ def train( loss = F.nll_loss(output, target) loss.backward() optimizer.step() - if (strategy.is_main_worker() and args.log_int > 0 + if (strategy.is_main_worker and args.log_int > 0 and batch_idx % args.log_int == 0): print( f'Train epoch: {epoch} ' @@ -136,7 +139,7 @@ def train( f'Loss: {loss.item():.6f}') t_list.append(timer() - t) loss_acc += loss.item() - if strategy.is_main_worker(): + if strategy.is_main_worker: print('TIMER: train time', sum(t_list) / len(t_list), 's') return loss_acc @@ -151,10 +154,10 @@ def main(): or not torch.cuda.device_count() > 1): raise RuntimeError('Resources unavailable') - strategy = DDPDistributedStrategy(backend=args.backend) + strategy = TorchDDPStrategy(backend=args.backend) distribute_kwargs = {} elif args.strategy == 'horovod': - strategy = HVDDistributedStrategy() + strategy = HorovodStrategy() distribute_kwargs = dict( compression=( hvd.Compression.fp16 if args.fp16_allreduce @@ -164,7 +167,7 @@ def main(): gradient_predivide_factor=args.gradient_predivide_factor ) elif args.strategy == 'deepspeed': - strategy = DSDistributedStrategy(backend=args.backend) + strategy = DeepSpeedStrategy(backend=args.backend) distribute_kwargs = dict( config_params=dict(train_micro_batch_size_per_gpu=args.batch_size) ) @@ -182,19 +185,19 @@ def main(): # Limit # of CPU threads to be used per worker # torch.set_num_threads(1) - # start the timer for profiling + # Start the timer for profiling st = timer() # Set random seed for reproducibility - torch_prng = set_seed(args.rnd_seed, use_cuda) + torch_prng = set_seed(args.rnd_seed, deterministic_cudnn=False) - # get job rank info - rank==0 master gpu + # Get job rank info - rank==0 master gpu if is_distributed: # local world size - per node - lwsize = strategy.dist_lwsize() # local world size - per run - gwsize = strategy.dist_gwsize() # global world size - per run - grank = strategy.dist_grank() # global rank - assign per run - lrank = strategy.dist_lrank() # local rank - assign per node + lwsize = strategy.local_world_size() # local world size - per run + gwsize = strategy.global_world_size() # global world size - per run + grank = strategy.global_rank() # global rank - assign per run + lrank = strategy.local_rank() # local rank - assign per node else: # Use a single worker (either on GPU or CPU) lwsize = 1 @@ -202,7 +205,7 @@ def main(): grank = 0 lrank = 0 - if strategy.is_main_worker(): + if strategy.is_main_worker: print('TIMER: initialise:', timer()-st, 's') print('DEBUG: local ranks:', lwsize, '/ global ranks:', gwsize) print('DEBUG: sys.version:', sys.version) @@ -221,7 +224,7 @@ def main(): # Encapsulate the model on the GPU assigned to the current process device = torch.device( - strategy.dist_device() if use_cuda and torch.cuda.is_available() + strategy.device() if use_cuda else 'cpu') if use_cuda: torch.cuda.set_device(lrank) @@ -263,7 +266,7 @@ def main(): ) # Start training loop - if strategy.is_main_worker(): + if strategy.is_main_worker: print('TIMER: broadcast:', timer()-st, 's') print('\nDEBUG: start training') print('--------------------------------------------------------') @@ -302,11 +305,11 @@ def main(): if epoch + 1 == args.epochs: train_loader.last_epoch = True - if strategy.is_main_worker(): + if strategy.is_main_worker: print('TIMER: epoch time:', timer()-lt, 's') epoch_time_tracker.add_epoch_time(epoch-1, timer()-lt) - if strategy.is_main_worker(): + if strategy.is_main_worker: print('\n--------------------------------------------------------') print('DEBUG: training results:\n') print('TIMER: first epoch time:', first_ep_t, ' s') @@ -327,7 +330,7 @@ def main(): print(f'TIMER: final time: {timer()-st} s\n') time.sleep(1) - print(f" - TRAINING FINISHED") + print(f" - TRAINING FINISHED") # Clean-up if is_distributed: diff --git a/tutorials/distributed-ml/torch-scaling-test/runall.sh b/tutorials/distributed-ml/torch-scaling-test/runall.sh index 4f9efdcf..22958c16 100644 --- a/tutorials/distributed-ml/torch-scaling-test/runall.sh +++ b/tutorials/distributed-ml/torch-scaling-test/runall.sh @@ -15,47 +15,75 @@ else fi # Common options -CMD="--nodes=$N --time=$T --account=atmo-rep --partition=booster slurm.sh" -PYTHON_VENV="../../../envAI_juwels" +CMD="--nodes=$N --time=$T --account=intertwin --partition=batch slurm.sh" +PYTHON_VENV="../../../envAI_hdfml" echo "Distributing training over $N nodes. Timeout set to: $T" +# Clear SLURM logs (*.out and *.err files) rm -rf logs_slurm mkdir logs_slurm -rm *.out *.err *.csv #*checkpoint.pth.tar +rm -rf logs_torchrun + +# Clear scaling test logs +rm *.csv # *checkpoint.pth.tar # DDP baseline DIST_MODE="ddp" RUN_NAME="ddp-bl-imagenent" TRAINING_CMD="ddp_trainer.py -c config/base.yaml -c config/ddp.yaml" -sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + $CMD # DeepSpeed baseline DIST_MODE="deepspeed" RUN_NAME="deepspeed-bl-imagenent" TRAINING_CMD="deepspeed_trainer.py -c config/base.yaml -c config/deepspeed.yaml" -sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + $CMD # Horovod baseline DIST_MODE="horovod" RUN_NAME="horovod-bl-imagenent" TRAINING_CMD="horovod_trainer.py -c config/base.yaml -c config/horovod.yaml" -sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + $CMD # DDP itwinai DIST_MODE="ddp" RUN_NAME="ddp-itwinai-imagenent" TRAINING_CMD="itwinai_trainer.py -c config/base.yaml -c config/ddp.yaml -s ddp" -sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + $CMD # DeepSpeed itwinai DIST_MODE="deepspeed" RUN_NAME="deepspeed-itwinai-imagenent" TRAINING_CMD="itwinai_trainer.py -c config/base.yaml -c config/deepspeed.yaml -s deepspeed" -sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + $CMD # Horovod itwinai DIST_MODE="horovod" RUN_NAME="horovod-itwinai-imagenent" TRAINING_CMD="itwinai_trainer.py -c config/base.yaml -c config/horovod.yaml -s horovod" -sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" --job-name="$RUN_NAME-n$N" $CMD \ No newline at end of file +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + $CMD \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-scaling-test/slurm.sh b/tutorials/distributed-ml/torch-scaling-test/slurm.sh index 93dd4349..c53e3da5 100644 --- a/tutorials/distributed-ml/torch-scaling-test/slurm.sh +++ b/tutorials/distributed-ml/torch-scaling-test/slurm.sh @@ -15,7 +15,7 @@ #SBATCH --partition=batch #SBATCH --nodes=2 #SBATCH --gpus-per-node=4 -#SBATCH --cpus-per-gpu=8 +#SBATCH --cpus-per-gpu=4 #SBATCH --exclusive # gres options have to be disabled for deepv @@ -72,13 +72,13 @@ else source $PYTHON_VENV/bin/activate fi +# Get GPUs info per node +srun --cpu-bind=none --ntasks-per-node=1 bash -c 'echo -e "NODE hostname: $(hostname)\n$(nvidia-smi)\n\n"' + # Launch training if [ "$DIST_MODE" == "ddp" ] ; then echo "DDP training: $TRAINING_CMD" srun --cpu-bind=none --ntasks-per-node=1 \ - --job-name="$RUN_NAME-n$SLURM_NNODES" \ - --output="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.out" \ - --error="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.err" \ bash -c "torchrun \ --log_dir='logs_torchrun' \ --nnodes=$SLURM_NNODES \ @@ -95,9 +95,6 @@ elif [ "$DIST_MODE" == "deepspeed" ] ; then export MASTER_PORT=29500 srun --cpu-bind=none --ntasks-per-node=$SLURM_GPUS_PER_NODE --cpus-per-task=$SLURM_CPUS_PER_GPU \ - --job-name="$RUN_NAME-n$SLURM_NNODES" \ - --output="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.out" \ - --error="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.err" \ python -u $TRAINING_CMD --deepspeed # # Run with deepspeed launcher: set --ntasks-per-node=1 @@ -112,9 +109,6 @@ elif [ "$DIST_MODE" == "deepspeed" ] ; then elif [ "$DIST_MODE" == "horovod" ] ; then echo "HOROVOD training: $TRAINING_CMD" srun --cpu-bind=none --ntasks-per-node=$SLURM_GPUS_PER_NODE --cpus-per-task=$SLURM_CPUS_PER_GPU \ - --job-name="$RUN_NAME-imagenet-n$SLURM_NNODES" \ - --output="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.out" \ - --error="logs_slurm/job-$RUN_NAME-n$SLURM_NNODES.err" \ python -u $TRAINING_CMD else >&2 echo "ERROR: unrecognized \$DIST_MODE env variable" diff --git a/tutorials/distributed-ml/torch-scaling-test/utils.py b/tutorials/distributed-ml/torch-scaling-test/utils.py index cbd6aace..a5dc591e 100644 --- a/tutorials/distributed-ml/torch-scaling-test/utils.py +++ b/tutorials/distributed-ml/torch-scaling-test/utils.py @@ -1,40 +1,6 @@ -from typing import Optional -import numpy as np -import random - -import torch from torchvision import datasets, transforms -def seed_worker(worker_id): - worker_seed = torch.initial_seed() % 2**32 - np.random.seed(worker_seed) - random.seed(worker_seed) - - -def set_seed(rnd_seed: Optional[int], use_cuda: bool) -> torch.Generator: - """Set torch random seed and return a PRNG object. - - Args: - rnd_seed (Optional[int]): random seed. If None, the seed is not set. - use_cuda (bool): whether GPU is available. - - Returns: - torch.Generator: PRNG object. - """ - g = torch.Generator() - if rnd_seed is not None: - # Deterministic execution - np.random.seed(rnd_seed) - random.seed(rnd_seed) - torch.manual_seed(rnd_seed) - g.manual_seed(rnd_seed) - if use_cuda: - torch.cuda.manual_seed(rnd_seed) - torch.cuda.manual_seed_all(rnd_seed) - return g - - def imagenet_dataset(data_root: str): """Create a torch dataset object for Imagenet.""" transform = transforms.Compose([ diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/README.md b/tutorials/distributed-ml/torch-tutorial-0-basics/README.md index 5ddcd635..43d42565 100644 --- a/tutorials/distributed-ml/torch-tutorial-0-basics/README.md +++ b/tutorials/distributed-ml/torch-tutorial-0-basics/README.md @@ -23,19 +23,43 @@ should be used to run it: If you want to distribute the code in `train.py` with **torch DDP**, run from terminal: ```bash -sbatch ddp_slurm.sh +export DIST_MODE="ddp" +export RUN_NAME="ddp-itwinai" +export TRAINING_CMD="train.py -s ddp" +export PYTHON_VENV="../../../envAI_hdfml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh ``` If you want to distribute the code in `train.py` with **DeepSpeed**, run from terminal: ```bash -sbatch deepspeed_slurm.sh +export DIST_MODE="deepspeed" +export RUN_NAME="deepspeed-itwinai" +export TRAINING_CMD="train.py -s deepspeed" +export PYTHON_VENV="../../../envAI_hdfml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh ``` If you want to distribute the code in `train.py` with **Horovod**, run from terminal: ```bash -sbatch hvd_slurm.sh +export DIST_MODE="deepspeed" +export RUN_NAME="deepspeed-itwinai" +export TRAINING_CMD="train.py -s deepspeed" +export PYTHON_VENV="../../../envAI_hdfml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh ``` You can run all of them with: diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/ddp_slurm.sh b/tutorials/distributed-ml/torch-tutorial-0-basics/ddp_slurm.sh deleted file mode 100644 index 1b53f04c..00000000 --- a/tutorials/distributed-ml/torch-tutorial-0-basics/ddp_slurm.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -# general configuration of the job -#SBATCH --job-name=Torch_DDP_tutorial-0 -#SBATCH --account=intertwin -#SBATCH --mail-user= -#SBATCH --mail-type=ALL -#SBATCH --output=job-ddp.out -#SBATCH --error=job-ddp.err -#SBATCH --time=00:15:00 - -# configure node and process count on the CM -#SBATCH --partition=batch -#SBATCH --nodes=2 -#SBATCH --ntasks-per-node=1 -#SBATCH --cpus-per-task=32 -#SBATCH --gpus-per-node=4 -# SBATCH --exclusive - -# gres options have to be disabled for deepv -#SBATCH --gres=gpu:4 - -# set modules -ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py - -# set env -source ../../../envAI_hdfml/bin/activate - -# job info -debug=false -echo "DEBUG: TIME: $(date)" -echo "DEBUG: EXECUTE: $EXEC" -echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" -echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" -echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" -echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" -echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" -echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" -echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" -echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" -echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" -if [ "$debug" = true ] ; then - export NCCL_DEBUG=INFO -fi -echo - -# set comm -export CUDA_VISIBLE_DEVICES="0,1,2,3" -export OMP_NUM_THREADS=1 -if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then - export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK -fi - -# launch training -TRAINING_CMD="train.py -s ddp" - -srun --cpu-bind=none bash -c "torchrun \ - --log_dir='logs' \ - --nnodes=$SLURM_NNODES \ - --nproc_per_node=$SLURM_GPUS_PER_NODE \ - --rdzv_id=$SLURM_JOB_ID \ - --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ - --rdzv_backend=c10d \ - --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ - $TRAINING_CMD" - diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/deepspeed_slurm.sh b/tutorials/distributed-ml/torch-tutorial-0-basics/deepspeed_slurm.sh deleted file mode 100644 index b12009de..00000000 --- a/tutorials/distributed-ml/torch-tutorial-0-basics/deepspeed_slurm.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash - -# general configuration of the job -#SBATCH --job-name=Torch_DeepSpeed_tutorial-0 -#SBATCH --account=intertwin -#SBATCH --mail-user= -#SBATCH --mail-type=ALL -#SBATCH --output=job-ds.out -#SBATCH --error=job-ds.err -#SBATCH --time=00:15:00 - -# configure node and process count on the CM -#SBATCH --partition=batch -#SBATCH --nodes=2 -#SBATCH --ntasks-per-node=4 -#SBATCH --cpus-per-task=4 -#SBATCH --gpus-per-node=4 -# SBATCH --exclusive - -# gres options have to be disabled for deepv -#SBATCH --gres=gpu:4 - -# set modules -ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py - -# set env -source ../../../envAI_hdfml/bin/activate - -# job info -debug=false -echo "DEBUG: TIME: $(date)" -echo "DEBUG: EXECUTE: $EXEC" -echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" -echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" -echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" -echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" -echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" -echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" -echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" -echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" -echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" -if [ "$debug" = true ] ; then - export NCCL_DEBUG=INFO -fi -echo - -# set env vars -export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} -export OMP_NUM_THREADS=1 -if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then - export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK -fi -export CUDA_VISIBLE_DEVICES="0,1,2,3" - -# launch training -MASTER_ADDR=$(scontrol show hostnames "\$SLURM_JOB_NODELIST" | head -n 1)i -export MASTER_ADDR -export MASTER_PORT=29500 - -TRAINING_CMD="train.py -s deepspeed" - -# Run without launcher: set --ntasks-per-node=NUM_GPUS -srun --cpu-bind=none python -u $TRAINING_CMD #--deepspeed - -# srun pwd - -# # Run with deepspeed launcher: set --ntasks-per-node=1 -# # https://www.deepspeed.ai/getting-started/#multi-node-environment-variables -# export NCCL_IB_DISABLE=1 -# export NCCL_SOCKET_IFNAME=eth0 -# nodelist=$(scontrol show hostname $SLURM_NODELIST) -# echo "$nodelist" | sed -e 's/$/ slots=4/' > .hostfile -# # Requires passwordless SSH access among compute node -# srun --cpu-bind=none deepspeed --hostfile=.hostfile $TRAINING_CMD --deepspeed -# rm .hostfile \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/hvd_slurm.sh b/tutorials/distributed-ml/torch-tutorial-0-basics/hvd_slurm.sh deleted file mode 100644 index a2a06e6c..00000000 --- a/tutorials/distributed-ml/torch-tutorial-0-basics/hvd_slurm.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# general configuration of the job -#SBATCH --job-name=Torch_HVD_tutorial-0 -#SBATCH --account=intertwin -#SBATCH --mail-user= -#SBATCH --mail-type=ALL -#SBATCH --output=job-hvd.out -#SBATCH --error=job-hvd.err -#SBATCH --time=00:15:00 - -# configure node and process count on the CM -#SBATCH --partition=batch -#SBATCH --nodes=2 -#SBATCH --ntasks-per-node=4 -#SBATCH --cpus-per-task=8 -#SBATCH --gpus-per-node=4 -# SBATCH --exclusive - -# gres options have to be disabled for deepv -#SBATCH --gres=gpu:4 - -# set modules -ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py - -# set env -source ../../../envAI_hdfml/bin/activate - -# job info -debug=false -echo "DEBUG: TIME: $(date)" -echo "DEBUG: EXECUTE: $EXEC" -echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" -echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" -echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" -echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" -echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" -echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" -echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" -echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" -echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" -if [ "$debug" = true ] ; then - export NCCL_DEBUG=INFO -fi -echo - -# set vars -# export NCCL_DEBUG=INFO -export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} -export OMP_NUM_THREADS=1 -if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then - export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK -fi -export CUDA_VISIBLE_DEVICES="0,1,2,3" - -# launch training -TRAINING_CMD="train.py -s horovod" - -srun --cpu-bind=none python -u $TRAINING_CMD - diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/runall.sh b/tutorials/distributed-ml/torch-tutorial-0-basics/runall.sh index 17c0f190..48a8f1e0 100644 --- a/tutorials/distributed-ml/torch-tutorial-0-basics/runall.sh +++ b/tutorials/distributed-ml/torch-tutorial-0-basics/runall.sh @@ -1,6 +1,39 @@ #!/bin/bash -# Run all versions of distributed ML -rm *.out *.err -echo "Torch DDP training: $(sbatch ddp_slurm.sh)" -echo "DeepSpeed training: $(sbatch deepspeed_slurm.sh)" -echo "Horovod training: $(sbatch hvd_slurm.sh)" \ No newline at end of file + +# Python virtual environment +PYTHON_VENV="../../../envAI_hdfml" + +# Clear SLURM logs (*.out and *.err files) +rm -rf logs_slurm +mkdir logs_slurm +rm -rf logs_torchrun + +# DDP itwinai +DIST_MODE="ddp" +RUN_NAME="ddp-itwinai" +TRAINING_CMD="train.py -s ddp" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh + +# DeepSpeed itwinai +DIST_MODE="deepspeed" +RUN_NAME="deepspeed-itwinai" +TRAINING_CMD="train.py -s deepspeed" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh + +# Horovod itwinai +DIST_MODE="horovod" +RUN_NAME="horovod-itwinai" +TRAINING_CMD="train.py -s horovod" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/slurm.sh b/tutorials/distributed-ml/torch-tutorial-0-basics/slurm.sh new file mode 100644 index 00000000..c53e3da5 --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-0-basics/slurm.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +# SLURM jobscript for JSC systems + +# Job configuration +#SBATCH --job-name=distributed_training +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job.out +#SBATCH --error=job.err +#SBATCH --time=00:30:00 + +# Resources allocation +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --gpus-per-node=4 +#SBATCH --cpus-per-gpu=4 +#SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# Load environment modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# Job info +echo "DEBUG: TIME: $(date)" +sysN="$(uname -n | cut -f2- -d.)" +sysN="${sysN%%[0-9]*}" +echo "Running on system: $sysN" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$DEBUG" = true ] ; then + echo "DEBUG: NCCL_DEBUG=INFO" + export NCCL_DEBUG=INFO +fi +echo + +# Setup env for distributed ML +export CUDA_VISIBLE_DEVICES="0,1,2,3" +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_GPU" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_GPU +fi + +# Env vairables check +if [ -z "$DIST_MODE" ]; then + >&2 echo "ERROR: env variable DIST_MODE is not set. Allowed values are 'horovod', 'ddp' or 'deepspeed'" + exit 1 +fi +if [ -z "$RUN_NAME" ]; then + >&2 echo "WARNING: env variable RUN_NAME is not set. It's a way to identify some specific run of an experiment." + RUN_NAME=$DIST_MODE +fi +if [ -z "$TRAINING_CMD" ]; then + >&2 echo "ERROR: env variable TRAINING_CMD is not set. It's the python command to execute." + exit 1 +fi +if [ -z "$PYTHON_VENV" ]; then + >&2 echo "WARNING: env variable PYTHON_VENV is not set. It's the path to a python virtual environment." +else + # Activate Python virtual env + source $PYTHON_VENV/bin/activate +fi + +# Get GPUs info per node +srun --cpu-bind=none --ntasks-per-node=1 bash -c 'echo -e "NODE hostname: $(hostname)\n$(nvidia-smi)\n\n"' + +# Launch training +if [ "$DIST_MODE" == "ddp" ] ; then + echo "DDP training: $TRAINING_CMD" + srun --cpu-bind=none --ntasks-per-node=1 \ + bash -c "torchrun \ + --log_dir='logs_torchrun' \ + --nnodes=$SLURM_NNODES \ + --nproc_per_node=$SLURM_GPUS_PER_NODE \ + --rdzv_id=$SLURM_JOB_ID \ + --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ + --rdzv_backend=c10d \ + --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ + $TRAINING_CMD" +elif [ "$DIST_MODE" == "deepspeed" ] ; then + echo "DEEPSPEED training: $TRAINING_CMD" + MASTER_ADDR=$(scontrol show hostnames "\$SLURM_JOB_NODELIST" | head -n 1)i + export MASTER_ADDR + export MASTER_PORT=29500 + + srun --cpu-bind=none --ntasks-per-node=$SLURM_GPUS_PER_NODE --cpus-per-task=$SLURM_CPUS_PER_GPU \ + python -u $TRAINING_CMD --deepspeed + + # # Run with deepspeed launcher: set --ntasks-per-node=1 + # # https://www.deepspeed.ai/getting-started/#multi-node-environment-variables + # export NCCL_IB_DISABLE=1 + # export NCCL_SOCKET_IFNAME=eth0 + # nodelist=$(scontrol show hostname $SLURM_NODELIST) + # echo "$nodelist" | sed -e 's/$/ slots=4/' > .hostfile + # # Requires passwordless SSH access among compute node + # srun --cpu-bind=none deepspeed --hostfile=.hostfile $TRAINING_CMD --deepspeed + # rm .hostfile +elif [ "$DIST_MODE" == "horovod" ] ; then + echo "HOROVOD training: $TRAINING_CMD" + srun --cpu-bind=none --ntasks-per-node=$SLURM_GPUS_PER_NODE --cpus-per-task=$SLURM_CPUS_PER_GPU \ + python -u $TRAINING_CMD +else + >&2 echo "ERROR: unrecognized \$DIST_MODE env variable" + exit 1 +fi + diff --git a/tutorials/distributed-ml/torch-tutorial-0-basics/train.py b/tutorials/distributed-ml/torch-tutorial-0-basics/train.py index 614b56e4..29c0d272 100644 --- a/tutorials/distributed-ml/torch-tutorial-0-basics/train.py +++ b/tutorials/distributed-ml/torch-tutorial-0-basics/train.py @@ -2,19 +2,23 @@ Show how to use DDP, Horovod and DeepSpeed strategies interchangeably with an extremely simple neural network. """ -from typing import Any -import os +from typing import Dict import argparse +import time import torch from torch import nn -from torch.utils.data import DataLoader, Dataset, DistributedSampler +from torch.utils.data import Dataset + +import horovod.torch as hvd from itwinai.torch.distributed import ( + distributed_resources_available, TorchDistributedStrategy, - DDPDistributedStrategy, - HVDDistributedStrategy, - DSDistributedStrategy, + TorchDDPStrategy, + HorovodStrategy, + DeepSpeedStrategy, + NonDistributedStrategy ) @@ -29,6 +33,9 @@ def parse_args() -> argparse.Namespace: "--shuffle_dataloader", action=argparse.BooleanOptionalAction ) + parser.add_argument( + '--batch-size', type=int, default=10, + help='input batch size for training (default: 10)') # DeepSpeed: needs to be removed import deepspeed @@ -55,42 +62,31 @@ def __getitem__(self, index): return torch.rand(self.x_size), torch.rand(self.y_size) -def trainer_entrypoint_fn( - foo: Any, args: argparse.Namespace, strategy: TorchDistributedStrategy +def training_fn( + args: argparse.Namespace, + strategy: TorchDistributedStrategy, + distribute_kwargs: Dict ) -> int: - """Dummy training function. This emulates custom code developed - by some use case. - """ + """Dummy training function.""" strategy.init() - print(f"{foo}: {os.environ.get('RANK')} {os.environ.get('LOCAL_RANK')} " - f"{os.environ.get('MASTER_ADDR')} {os.environ.get('MASTER_PORT')}") # Local model model = nn.Linear(3, 4) optim = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.MSELoss() # Distributed model - deepspeed_config = dict(train_batch_size=32) - # 'config_params' key is ignored if strategy != DSDistributedStrategy model, optim, lr_sched = strategy.distributed( - model, optim, lr_scheduler=None, config_params=deepspeed_config + model, optim, lr_scheduler=None, **distribute_kwargs ) # Data train_set = UniformRndDataset(x_size=3, y_size=4) # Distributed dataloader - train_loader = DataLoader( - train_set, batch_size=10, num_workers=1, - sampler=DistributedSampler( - train_set, - num_replicas=strategy.dist_gwsize(), - rank=strategy.dist_grank(), - shuffle=args.shuffle_dataloader - ) - ) + train_loader = strategy.create_dataloader( + train_set, batch_size=args.batch_size, num_workers=1) # Device allocated for this worker - device = strategy.dist_device() + device = strategy.device() for epoch in range(2): for (x, y) in train_loader: @@ -107,7 +103,7 @@ def trainer_entrypoint_fn( optim.step() - if strategy.is_main_worker(): + if strategy.is_main_worker: print(f"Loss [epoch={epoch}]: {loss.item()}") print(f"NNLoss [epoch={epoch}]: {loss.item()}") @@ -115,7 +111,8 @@ def trainer_entrypoint_fn( if lr_sched: lr_sched.step() - print(f" - TRAINING FINISHED") + time.sleep(1) + print(f" - TRAINING FINISHED") strategy.clean_up() return 123 @@ -125,19 +122,27 @@ def trainer_entrypoint_fn( args = parse_args() # Instantiate Strategy - if args.strategy == 'ddp': - if (not torch.cuda.is_available() - or not torch.cuda.device_count() > 1): - raise RuntimeError('Resources unavailable') - - strategy = DDPDistributedStrategy(backend='nccl') + if not distributed_resources_available(): + print("WARNING: falling back to non-distributed strategy.") + strategy = NonDistributedStrategy() + distribute_kwargs = {} + elif args.strategy == 'ddp': + strategy = TorchDDPStrategy(backend='nccl') + distribute_kwargs = {} elif args.strategy == 'horovod': - strategy = HVDDistributedStrategy() + strategy = HorovodStrategy() + distribute_kwargs = dict( + compression=hvd.Compression.none, + op=hvd.Average, + gradient_predivide_factor=1.0 + ) elif args.strategy == 'deepspeed': - strategy = DSDistributedStrategy(backend='nccl') + strategy = DeepSpeedStrategy(backend='nccl') + distribute_kwargs = dict( + config_params=dict(train_micro_batch_size_per_gpu=args.batch_size) + ) else: raise NotImplementedError( f"Strategy {args.strategy} is not recognized/implemented.") - # Launch distributed training - trainer_entrypoint_fn("foobar", args, strategy) + training_fn(args, strategy, distribute_kwargs) diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/README.md b/tutorials/distributed-ml/torch-tutorial-1-mnist/README.md index 6f22d3ef..70178f0d 100644 --- a/tutorials/distributed-ml/torch-tutorial-1-mnist/README.md +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/README.md @@ -33,19 +33,43 @@ should be used to run it: If you want to distribute the code in `train.py` with **torch DDP**, run from terminal: ```bash -sbatch ddp_slurm.sh +export DIST_MODE="ddp" +export RUN_NAME="ddp-itwinai" +export TRAINING_CMD="train.py -s ddp -c config.yaml" +export PYTHON_VENV="../../../envAI_hdfml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh ``` If you want to distribute the code in `train.py` with **DeepSpeed**, run from terminal: ```bash -sbatch deepspeed_slurm.sh +export DIST_MODE="deepspeed" +export RUN_NAME="deepspeed-itwinai" +export TRAINING_CMD="train.py -s deepspeed -c config.yaml" +export PYTHON_VENV="../../../envAI_hdfml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh ``` If you want to distribute the code in `train.py` with **Horovod**, run from terminal: ```bash -sbatch hvd_slurm.sh +export DIST_MODE="horovod" +export RUN_NAME="horovod-itwinai" +export TRAINING_CMD="train.py -s horovod -c config.yaml" +export PYTHON_VENV="../../../envAI_hdfml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh ``` You can run all of them with: diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/config.yaml b/tutorials/distributed-ml/torch-tutorial-1-mnist/config.yaml index cb221dec..331d6d04 100644 --- a/tutorials/distributed-ml/torch-tutorial-1-mnist/config.yaml +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/config.yaml @@ -1,26 +1,28 @@ -# I/O +# Data and logging data_dir: ./ +log_int: 10 +verbose: True restart_int: 10 download_only: False -verbose: True +dataset_replication: 10 +shuff: False +nworker: 4 # num workers dataloader +prefetch: 2 # Model batch_size: 64 epochs: 2 lr: 0.001 -concM: 100 momentum: 0.5 -shuff: False -# Debugging -testrun: False -nseed: 10 -log_int: 10 +# Reproducibility +rnd_seed: 10 # Distributed ML -backend: nccl -nworker: 4 # num workers dataloader -prefetch: 2 -no_cuda: False +backend: nccl # ignored when using Horovod +# Horovod: ignored when NOT using Horovod +fp16_allreduce: False +use_adasum: False +gradient_predivide_factor: 1.0 diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/ddp_slurm.sh b/tutorials/distributed-ml/torch-tutorial-1-mnist/ddp_slurm.sh deleted file mode 100644 index 3d5d4bb3..00000000 --- a/tutorials/distributed-ml/torch-tutorial-1-mnist/ddp_slurm.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -# general configuration of the job -#SBATCH --job-name=Torch_DDP_tutorial-1 -#SBATCH --account=intertwin -#SBATCH --mail-user= -#SBATCH --mail-type=ALL -#SBATCH --output=job-ddp.out -#SBATCH --error=job-ddp.err -#SBATCH --time=00:30:00 - -# configure node and process count on the CM -#SBATCH --partition=batch -#SBATCH --nodes=2 -#SBATCH --ntasks-per-node=1 -#SBATCH --cpus-per-task=32 -#SBATCH --gpus-per-node=4 -# SBATCH --exclusive - -# gres options have to be disabled for deepv -#SBATCH --gres=gpu:4 - -# set modules -ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py - -# set env -source ../../../envAI_hdfml/bin/activate - -# job info -debug=false -echo "DEBUG: TIME: $(date)" -echo "DEBUG: EXECUTE: $EXEC" -echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" -echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" -echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" -echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" -echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" -echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" -echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" -echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" -echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" -if [ "$debug" = true ] ; then - export NCCL_DEBUG=INFO -fi -echo - -# set comm -export CUDA_VISIBLE_DEVICES="0,1,2,3" -export OMP_NUM_THREADS=1 -if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then - export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK -fi - -# launch training -TRAINING_CMD="train.py -s ddp -c config.yaml" - -srun --cpu-bind=none bash -c "torchrun \ - --log_dir='logs' \ - --nnodes=$SLURM_NNODES \ - --nproc_per_node=$SLURM_GPUS_PER_NODE \ - --rdzv_id=$SLURM_JOB_ID \ - --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ - --rdzv_backend=c10d \ - --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ - $TRAINING_CMD" - diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/deepspeed_slurm.sh b/tutorials/distributed-ml/torch-tutorial-1-mnist/deepspeed_slurm.sh deleted file mode 100644 index 8e5f7881..00000000 --- a/tutorials/distributed-ml/torch-tutorial-1-mnist/deepspeed_slurm.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash - -# general configuration of the job -#SBATCH --job-name=Torch_DeepSpeed_tutorial-1 -#SBATCH --account=intertwin -#SBATCH --mail-user= -#SBATCH --mail-type=ALL -#SBATCH --output=job-ds.out -#SBATCH --error=job-ds.err -#SBATCH --time=00:30:00 - -# configure node and process count on the CM -#SBATCH --partition=batch -#SBATCH --nodes=2 -#SBATCH --ntasks-per-node=4 -#SBATCH --cpus-per-task=4 -#SBATCH --gpus-per-node=4 -# SBATCH --exclusive - -# gres options have to be disabled for deepv -#SBATCH --gres=gpu:4 - -# set modules -ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py - -# set env -source ../../../envAI_hdfml/bin/activate - -# job info -debug=false -echo "DEBUG: TIME: $(date)" -echo "DEBUG: EXECUTE: $EXEC" -echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" -echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" -echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" -echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" -echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" -echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" -echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" -echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" -echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" -if [ "$debug" = true ] ; then - export NCCL_DEBUG=INFO -fi -echo - -# set env vars -export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} -export OMP_NUM_THREADS=1 -if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then - export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK -fi -export CUDA_VISIBLE_DEVICES="0,1,2,3" - -# launch training -MASTER_ADDR=$(scontrol show hostnames "\$SLURM_JOB_NODELIST" | head -n 1)i -export MASTER_ADDR -export MASTER_PORT=29500 - -TRAINING_CMD="train.py -s deepspeed -c config.yaml" - -# Run without launcher: set --ntasks-per-node=NUM_GPUS -srun --cpu-bind=none python -u $TRAINING_CMD --deepspeed - -# # Run with deepspeed launcher: set --ntasks-per-node=1 -# # https://www.deepspeed.ai/getting-started/#multi-node-environment-variables -# export NCCL_IB_DISABLE=1 -# export NCCL_SOCKET_IFNAME=eth0 -# nodelist=$(scontrol show hostname $SLURM_NODELIST) -# echo "$nodelist" | sed -e 's/$/ slots=4/' > .hostfile -# # Requires passwordless SSH access among compute node -# srun --cpu-bind=none deepspeed --hostfile=.hostfile $TRAINING_CMD --deepspeed -# rm .hostfile - diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/hvd_slurm.sh b/tutorials/distributed-ml/torch-tutorial-1-mnist/hvd_slurm.sh deleted file mode 100644 index 3774b6e1..00000000 --- a/tutorials/distributed-ml/torch-tutorial-1-mnist/hvd_slurm.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# general configuration of the job -#SBATCH --job-name=Torch_HVD_tutorial-1 -#SBATCH --account=intertwin -#SBATCH --mail-user= -#SBATCH --mail-type=ALL -#SBATCH --output=job-hvd.out -#SBATCH --error=job-hvd.err -#SBATCH --time=00:30:00 - -# configure node and process count on the CM -#SBATCH --partition=batch -#SBATCH --nodes=2 -#SBATCH --ntasks-per-node=4 -#SBATCH --cpus-per-task=8 -#SBATCH --gpus-per-node=4 -# SBATCH --exclusive - -# gres options have to be disabled for deepv -#SBATCH --gres=gpu:4 - -# set modules -ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py - -# set env -source ../../../envAI_hdfml/bin/activate - -# job info -debug=false -echo "DEBUG: TIME: $(date)" -echo "DEBUG: EXECUTE: $EXEC" -echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" -echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" -echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" -echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" -echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" -echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" -echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" -echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" -echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" -if [ "$debug" = true ] ; then - export NCCL_DEBUG=INFO -fi -echo - -# set vars -# export NCCL_DEBUG=INFO -export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} -export OMP_NUM_THREADS=1 -if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then - export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK -fi -export CUDA_VISIBLE_DEVICES="0,1,2,3" - -# launch training -TRAINING_CMD="train.py -s horovod -c config.yaml" - -srun --cpu-bind=none python -u $TRAINING_CMD - diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/runall.sh b/tutorials/distributed-ml/torch-tutorial-1-mnist/runall.sh index b1470d75..5a89b4fe 100644 --- a/tutorials/distributed-ml/torch-tutorial-1-mnist/runall.sh +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/runall.sh @@ -1,6 +1,39 @@ #!/bin/bash -# Run all versions of distributed ML for MNIST -rm *checkpoint.pth.tar *.out *.err -echo "Torch DDP training: $(sbatch ddp_slurm.sh)" -echo "DeepSpeed training: $(sbatch deepspeed_slurm.sh)" -echo "Horovod training: $(sbatch hvd_slurm.sh)" \ No newline at end of file + +# Python virtual environment +PYTHON_VENV="../../../envAI_hdfml" + +# Clear SLURM logs (*.out and *.err files) +rm -rf logs_slurm +mkdir logs_slurm +rm -rf logs_torchrun + +# DDP itwinai +DIST_MODE="ddp" +RUN_NAME="ddp-itwinai" +TRAINING_CMD="train.py -s ddp -c config.yaml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh + +# DeepSpeed itwinai +DIST_MODE="deepspeed" +RUN_NAME="deepspeed-itwinai" +TRAINING_CMD="train.py -s deepspeed -c config.yaml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh + +# Horovod itwinai +DIST_MODE="horovod" +RUN_NAME="horovod-itwinai" +TRAINING_CMD="train.py -s horovod -c config.yaml" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/slurm.sh b/tutorials/distributed-ml/torch-tutorial-1-mnist/slurm.sh new file mode 100644 index 00000000..3eef38ae --- /dev/null +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/slurm.sh @@ -0,0 +1,116 @@ +#!/bin/bash + +# SLURM jobscript for JSC systems + +# Job configuration +#SBATCH --job-name=distributed_training +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job.out +#SBATCH --error=job.err +#SBATCH --time=00:30:00 + +# Resources allocation +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --gpus-per-node=4 +#SBATCH --cpus-per-gpu=4 +#SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# Load environment modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# Job info +echo "DEBUG: TIME: $(date)" +sysN="$(uname -n | cut -f2- -d.)" +sysN="${sysN%%[0-9]*}" +echo "Running on system: $sysN" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$DEBUG" = true ] ; then + echo "DEBUG: NCCL_DEBUG=INFO" + export NCCL_DEBUG=INFO +fi +echo + +# Setup env for distributed ML +export CUDA_VISIBLE_DEVICES="0,1,2,3" +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_GPU" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_GPU +fi + +# Env vairables check +if [ -z "$DIST_MODE" ]; then + >&2 echo "ERROR: env variable DIST_MODE is not set. Allowed values are 'horovod', 'ddp' or 'deepspeed'" + exit 1 +fi +if [ -z "$RUN_NAME" ]; then + >&2 echo "WARNING: env variable RUN_NAME is not set. It's a way to identify some specific run of an experiment." + RUN_NAME=$DIST_MODE +fi +if [ -z "$TRAINING_CMD" ]; then + >&2 echo "ERROR: env variable TRAINING_CMD is not set. It's the python command to execute." + exit 1 +fi +if [ -z "$PYTHON_VENV" ]; then + >&2 echo "WARNING: env variable PYTHON_VENV is not set. It's the path to a python virtual environment." +else + # Activate Python virtual env + source $PYTHON_VENV/bin/activate +fi + +# Get GPUs info per node +srun --cpu-bind=none --ntasks-per-node=1 bash -c 'echo -e "NODE hostname: $(hostname)\n$(nvidia-smi)\n\n"' + +# Launch training +if [ "$DIST_MODE" == "ddp" ] ; then + echo "DDP training: $TRAINING_CMD" + srun --cpu-bind=none --ntasks-per-node=1 \ + bash -c "torchrun \ + --log_dir='logs_torchrun' \ + --nnodes=$SLURM_NNODES \ + --nproc_per_node=$SLURM_GPUS_PER_NODE \ + --rdzv_id=$SLURM_JOB_ID \ + --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ + --rdzv_backend=c10d \ + --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ + $TRAINING_CMD" +elif [ "$DIST_MODE" == "deepspeed" ] ; then + echo "DEEPSPEED training: $TRAINING_CMD" + MASTER_ADDR=$(scontrol show hostnames "\$SLURM_JOB_NODELIST" | head -n 1)i + export MASTER_ADDR + export MASTER_PORT=29500 + + srun --cpu-bind=none --ntasks-per-node=$SLURM_GPUS_PER_NODE --cpus-per-task=$SLURM_CPUS_PER_GPU \ + python -u $TRAINING_CMD --deepspeed + + # # Run with deepspeed launcher: set --ntasks-per-node=1 + # # https://www.deepspeed.ai/getting-started/#multi-node-environment-variables + # export NCCL_IB_DISABLE=1 + # export NCCL_SOCKET_IFNAME=eth0 + # nodelist=$(scontrol show hostname $SLURM_NODELIST) + # echo "$nodelist" | sed -e 's/$/ slots=4/' > .hostfile + # # Requires passwordless SSH access among compute node + # srun --cpu-bind=none deepspeed --hostfile=.hostfile $TRAINING_CMD --deepspeed + # rm .hostfile +elif [ "$DIST_MODE" == "horovod" ] ; then + echo "HOROVOD training: $TRAINING_CMD" + srun --cpu-bind=none --ntasks-per-node=$SLURM_GPUS_PER_NODE --cpus-per-task=$SLURM_CPUS_PER_GPU \ + python -u $TRAINING_CMD +else + >&2 echo "ERROR: unrecognized \$DIST_MODE env variable" + exit 1 +fi diff --git a/tutorials/distributed-ml/torch-tutorial-1-mnist/train.py b/tutorials/distributed-ml/torch-tutorial-1-mnist/train.py index 365a9048..809480dd 100644 --- a/tutorials/distributed-ml/torch-tutorial-1-mnist/train.py +++ b/tutorials/distributed-ml/torch-tutorial-1-mnist/train.py @@ -1,34 +1,38 @@ """ Show how to use DDP, Horovod and DeepSpeed strategies interchangeably -with a simple neural network trained on MNIST dataset, showing how -to use checkpoints. +with a simple neural network trained on MNIST dataset. """ -import os +from typing import Tuple import argparse import sys import time -import numpy as np -import random +from timeit import default_timer as timer import torch -import torch.distributed as dist import torch.nn as nn import torch.nn.functional as F from torchvision import datasets, transforms -from torch.utils.data import DataLoader, DistributedSampler +from torch.utils.data import Dataset + +import horovod.torch as hvd import deepspeed from itwinai.torch.distributed import ( + distributed_resources_available, TorchDistributedStrategy, - DDPDistributedStrategy, - HVDDistributedStrategy, - DSDistributedStrategy, + TorchDDPStrategy, + HorovodStrategy, + DeepSpeedStrategy, + NonDistributedStrategy ) from itwinai.parser import ArgumentParser as ItAIArgumentParser +from itwinai.torch.reproducibility import ( + seed_worker, set_seed +) -def parse_args() -> argparse.Namespace: +def parse_params() -> argparse.Namespace: """ Parse CLI args, which can also be loaded from a configuration file using the --config flag: @@ -44,54 +48,59 @@ def parse_args() -> argparse.Namespace: default='ddp' ) - # IO parsers + # Data and logging parser.add_argument('--data-dir', default='./', help=('location of the training dataset in the local ' 'filesystem')) + parser.add_argument('--log-int', type=int, default=10, + help='log interval per training') + parser.add_argument('--verbose', + action=argparse.BooleanOptionalAction, + help='Print parsed arguments') parser.add_argument('--restart-int', type=int, default=10, help='restart interval per epoch (default: 10)') parser.add_argument('--download-only', action=argparse.BooleanOptionalAction, help='Download dataset and exit') - parser.add_argument('--verbose', - action=argparse.BooleanOptionalAction, - help='Print parsed arguments') + parser.add_argument('--dataset-replication', type=int, default=100, + help='concatenate MNIST to this factor (default: 100)') + parser.add_argument('--shuff', action='store_true', default=False, + help='shuffle dataset (default: False)') + parser.add_argument('--nworker', type=int, default=0, + help=('number of workers in DataLoader (default: 0 -' + ' only main)')) + parser.add_argument('--prefetch', type=int, default=2, + help='prefetch data in DataLoader (default: 2)') - # model parsers + # Model parser.add_argument('--batch-size', type=int, default=64, help='input batch size for training (default: 64)') parser.add_argument('--epochs', type=int, default=10, help='number of epochs to train (default: 10)') parser.add_argument('--lr', type=float, default=0.01, help='learning rate (default: 0.01)') - parser.add_argument('--concM', type=int, default=100, - help='concatenate MNIST to this factor (default: 100)') parser.add_argument('--momentum', type=float, default=0.5, help='momentum in SGD optimizer (default: 0.5)') - parser.add_argument('--shuff', action='store_true', default=False, - help='shuffle dataset (default: False)') - # debug parsers - parser.add_argument('--testrun', action='store_true', default=False, - help='do a test run with seed (default: False)') - parser.add_argument('--nseed', type=int, default=0, + # Reproducibility + parser.add_argument('--rnd-seed', type=int, default=0, help='seed integer for reproducibility (default: 0)') - parser.add_argument('--log-int', type=int, default=10, - help='log interval per training') - # parallel parsers + # Distributed ML parser.add_argument('--backend', type=str, default='nccl', help='backend for parrallelisation (default: nccl)') - parser.add_argument('--nworker', type=int, default=0, - help=('number of workers in DataLoader (default: 0 -' - ' only main)')) - parser.add_argument('--prefetch', type=int, default=2, - help='prefetch data in DataLoader (default: 2)') - parser.add_argument('--no-cuda', action='store_true', default=False, - help='disables GPGPUs') parser.add_argument('--local_rank', type=int, default=-1, help='local rank passed from distributed launcher') + # Horovod: ignored when not using Horovod + parser.add_argument('--fp16-allreduce', action='store_true', default=False, + help='use fp16 compression during allreduce') + parser.add_argument('--use-adasum', action='store_true', default=False, + help='use adasum algorithm to do reduction') + parser.add_argument('--gradient-predivide-factor', type=float, default=1.0, + help=('apply gradient pre-divide factor in optimizer ' + '(default: 1.0)')) + # DeepSpeed parser = deepspeed.add_config_arguments(parser) args = parser.parse_args() @@ -127,7 +136,7 @@ def forward(self, x): def train( - model, device, train_loader, optimizer, epoch, + model, train_loader, optimizer, epoch, strategy: TorchDistributedStrategy, args ): """ @@ -136,108 +145,62 @@ def train( model.train() t_list = [] loss_acc = 0 - gwsize = strategy.dist_gwsize() - if strategy.is_main_worker(): + if strategy.is_main_worker: print("\n") for batch_idx, (data, target) in enumerate(train_loader): - t = time.perf_counter() - data, target = data.to(device), target.to(device) + t = timer() + data = data.to(strategy.device()) + target = target.to(strategy.device()) optimizer.zero_grad() output = model(data) loss = F.nll_loss(output, target) loss.backward() optimizer.step() - if batch_idx % args.log_int == 0 and strategy.is_main_worker(): + if (strategy.is_main_worker and args.log_int > 0 + and batch_idx % args.log_int == 0): + dl_size = len(train_loader.dataset)//strategy.global_world_size() print( f'Train epoch: {epoch} ' - f'[{batch_idx * len(data)}/{len(train_loader.dataset)/gwsize} ' + f'[{batch_idx * len(data)}/{dl_size} ' f'({100.0 * batch_idx / len(train_loader):.0f}%)]\t\t' f'Loss: {loss.item():.6f}') - t_list.append(time.perf_counter() - t) + t_list.append(timer() - t) loss_acc += loss.item() - if strategy.is_main_worker(): + if strategy.is_main_worker: print('TIMER: train time', sum(t_list) / len(t_list), 's') return loss_acc -def test(model, device, test_loader, strategy: TorchDistributedStrategy): +def test(model, test_loader, strategy: TorchDistributedStrategy): """ Model validation. """ model.eval() test_loss = 0 correct = 0 - gwsize = strategy.dist_gwsize() with torch.no_grad(): for data, target in test_loader: - data, target = data.to(device), target.to(device) + data = data.to(strategy.device()) + target = target.to(strategy.device()) output = model(data) - # sum up batch loss + # Sum up batch loss test_loss += F.nll_loss(output, target, reduction="sum").item() - # get the index of the max log-probability + # Get the index of the max log-probability pred = output.argmax(dim=1, keepdim=True) correct += pred.eq(target.view_as(pred)).sum().item() test_loss /= len(test_loader.dataset) - if strategy.is_main_worker(): + if strategy.is_main_worker: + dl_size = len(test_loader.dataset)//strategy.global_world_size() print( f'Test set: average loss: {test_loss:.4f}\t' - f'accurate samples: {correct}/{len(test_loader.dataset)/gwsize}') - acc_test = 100.0 * correct * gwsize / len(test_loader.dataset) + f'accurate samples: {correct}/{dl_size}') + acc_test = ( + 100.0 * correct * strategy.global_world_size() + / len(test_loader.dataset) + ) return acc_test -def save_state( - epoch, distrib_model, loss_acc, optimizer, - res_name, is_best, strategy: TorchDistributedStrategy -): - """ - Save training state. - """ - grank = strategy.dist_grank() - rt = time.time() - # find if is_best happened in any worker - if torch.cuda.is_available(): - is_best_m = strategy.par_allgather_obj(is_best) - - if torch.cuda.is_available(): - if any(is_best_m): - # find which rank is_best happened - select first rank if multiple - is_best_rank = np.where(np.array(is_best_m))[0][0] - - # collect state - state = {'epoch': epoch + 1, - 'state_dict': distrib_model.state_dict(), - 'best_acc': loss_acc, - 'optimizer': optimizer.state_dict()} - - # write on worker with is_best - if grank == is_best_rank: - torch.save(state, './'+res_name) - print( - f'DEBUG: state in {grank} is saved on epoch:{epoch} ' - f'in {time.time()-rt} s') - else: - # collect state - state = {'epoch': epoch + 1, - 'state_dict': distrib_model.state_dict(), - 'best_acc': loss_acc, - 'optimizer': optimizer.state_dict()} - - torch.save(state, './'+res_name) - print( - f'DEBUG: state in {grank} is saved on epoch:{epoch} in ' - f'{time.time()-rt} s') - - -def seed_worker(worker_id): - """ - Seed dataloader worker. - """ - worker_seed = torch.initial_seed() % 2**32 - np.random.seed(worker_seed) - random.seed(worker_seed) - - def download_mnist(): """ Use built-in torch datasets functions to pull MNIST dataset. @@ -257,212 +220,154 @@ def download_mnist(): ])) +def mnist_dataset(dataset_replication: int = 1) -> Tuple[Dataset, Dataset]: + """Load MNIST train and test datasets, replicating them. + + Args: + dataset_replication (int): dataset replication factor. Default 1. + + Returns: + Tuple[Dataset, Dataset]: train dataset and test dataset. + """ + replicated_data = [ + datasets.MNIST(args.data_dir, train=True, download=False, + transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)) + ])) + for _ in range(dataset_replication) + ] + train_dataset = torch.utils.data.ConcatDataset(replicated_data) + + replicated_data = [ + datasets.MNIST(args.data_dir, train=False, download=False, + transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)) + ])) + for _ in range(dataset_replication) + ] + test_dataset = torch.utils.data.ConcatDataset(replicated_data) + return train_dataset, test_dataset + + if __name__ == "__main__": - args = parse_args() + args = parse_params() if args.download_only: - # Download datasets and exit + # Download datasets from a location with internet access and exit. + # This is convenient when submitting training jobs to + # a batch system where worker nodes have no internet + # access, like in some HPCs. download_mnist() sys.exit() # Instantiate Strategy - if args.strategy == 'ddp': - if (not torch.cuda.is_available() - or not torch.cuda.device_count() > 1): - raise RuntimeError('Resources unavailable') - - strategy = DDPDistributedStrategy(backend=args.backend) + if not distributed_resources_available(): + print("WARNING: falling back to non-distributed strategy.") + strategy = NonDistributedStrategy() + distribute_kwargs = {} + elif args.strategy == 'ddp': + strategy = TorchDDPStrategy(backend=args.backend) + distribute_kwargs = {} elif args.strategy == 'horovod': - strategy = HVDDistributedStrategy() + strategy = HorovodStrategy() + distribute_kwargs = dict( + compression=( + hvd.Compression.fp16 if args.fp16_allreduce + else hvd.Compression.none + ), + op=hvd.Adasum if args.use_adasum else hvd.Average, + gradient_predivide_factor=args.gradient_predivide_factor + ) elif args.strategy == 'deepspeed': - strategy = DSDistributedStrategy(backend=args.backend) + strategy = DeepSpeedStrategy(backend=args.backend) + distribute_kwargs = dict( + config_params=dict(train_micro_batch_size_per_gpu=args.batch_size) + ) else: raise NotImplementedError( f"Strategy {args.strategy} is not recognized/implemented.") - strategy.init() - - # check CUDA availability - args.cuda = not args.no_cuda and torch.cuda.is_available() - - # limit # of CPU threads to be used per worker - torch.set_num_threads(1) - - # get directory - program_dir = os.getcwd() - - # start the time.time for profiling - st = time.time() - # deterministic testrun - if args.testrun: - torch.manual_seed(args.nseed) - g = torch.Generator() - g.manual_seed(args.nseed) - - # get job rank info - rank==0 master gpu - if torch.cuda.is_available(): - # local world size - per node - lwsize = strategy.dist_lwsize() if args.cuda else 0 - gwsize = strategy.dist_gwsize() # global world size - per run - grank = strategy.dist_grank() # global rank - assign per run - lrank = strategy.dist_lrank() # local rank - assign per node - else: - gwsize = 1 - grank = 0 - - # some debug - if strategy.is_main_worker(): - print('TIMER: initialise:', time.time()-st, 's') - - # move the model on the GPU assigned to the current process - device = torch.device( - strategy.dist_device() if args.cuda and torch.cuda.is_available() - else 'cpu') - if args.cuda: - torch.cuda.set_device(lrank) - # deterministic testrun - if args.testrun: - torch.cuda.manual_seed(args.nseed) - - # read data - mnist_scale = args.concM - largeData = [] - for i in range(mnist_scale): - largeData.append( - datasets.MNIST(args.data_dir, train=True, download=False, - transform=transforms.Compose([ - transforms.ToTensor(), - transforms.Normalize((0.1307,), (0.3081,)) - ])) - ) - - # concat data - train_dataset = torch.utils.data.ConcatDataset(largeData) - - mnist_scale = args.concM - largeData = [] - for i in range(mnist_scale): - largeData.append( - datasets.MNIST(args.data_dir, train=False, download=False, - transform=transforms.Compose([ - transforms.ToTensor(), - transforms.Normalize((0.1307,), (0.3081,)) - ])) - ) + # Initialize strategy + strategy.init() - # concat data - test_dataset = torch.utils.data.ConcatDataset(largeData) - - # restricts data loading to a subset of the dataset exclusive to the - # current process - args.shuff = args.shuff and not args.testrun - if torch.cuda.is_available(): - train_sampler = DistributedSampler( - train_dataset, num_replicas=gwsize, rank=grank, shuffle=args.shuff) - test_sampler = DistributedSampler( - test_dataset, num_replicas=gwsize, rank=grank, shuffle=args.shuff) - # distribute dataset to workers - # persistent workers is not possible for nworker=0 - pers_w = True if args.nworker > 1 else False - - # deterministic testrun - the same dataset each run - kwargs = {'worker_init_fn': seed_worker, - 'generator': g} if args.testrun else {} - - if torch.cuda.is_available(): - train_loader = DataLoader( - train_dataset, batch_size=args.batch_size, - sampler=train_sampler, num_workers=args.nworker, pin_memory=True, - persistent_workers=pers_w, prefetch_factor=args.prefetch, **kwargs - ) - test_loader = DataLoader( - test_dataset, batch_size=args.batch_size, - sampler=test_sampler, num_workers=args.nworker, pin_memory=True, - persistent_workers=pers_w, prefetch_factor=args.prefetch, **kwargs - ) - else: - train_loader = DataLoader( - train_dataset, batch_size=args.batch_size) - test_loader = DataLoader( - test_dataset, batch_size=args.batch_size) + # Start the timer for profiling + st = timer() + + # Set random seed for reproducibility + torch_prng = set_seed(args.rnd_seed) + + if strategy.is_main_worker: + print('TIMER: initialise:', timer()-st, 's') + print('DEBUG: local ranks:', strategy.local_world_size(), + '/ global ranks:', strategy.global_world_size()) + print('DEBUG: sys.version:', sys.version) + print('DEBUG: args.data_dir:', args.data_dir) + print('DEBUG: args.log_int:', args.log_int) + print('DEBUG: args.nworker:', args.nworker) + print('DEBUG: args.prefetch:', args.prefetch) + print('DEBUG: args.batch_size:', args.batch_size) + print('DEBUG: args.epochs:', args.epochs) + print('DEBUG: args.lr:', args.lr) + print('DEBUG: args.momentum:', args.momentum) + print('DEBUG: args.shuff:', args.shuff) + print('DEBUG: args.rnd_seed:', args.rnd_seed) + print('DEBUG: args.backend:', args.backend) + + # Dataset + train_dataset, test_dataset = mnist_dataset(args.dataset_replication) + # Distributed dataloaders + train_loader = strategy.create_dataloader( + train_dataset, batch_size=args.batch_size, + num_workers=args.nworker, pin_memory=True, + persistent_workers=(args.nworker > 1), + prefetch_factor=args.prefetch, generator=torch_prng, + worker_init_fn=seed_worker + ) + test_loader = strategy.create_dataloader( + test_dataset, batch_size=args.batch_size, + num_workers=args.nworker, pin_memory=True, + persistent_workers=(args.nworker > 1), + prefetch_factor=args.prefetch, generator=torch_prng, + worker_init_fn=seed_worker + ) - if strategy.is_main_worker(): - print('TIMER: read and concat data:', time.time()-st, 's') + if strategy.is_main_worker: + print('TIMER: read and concat data:', timer()-st, 's') - # create CNN model - model = Net().to(device) + # Create CNN model + model = Net().to(strategy.device()) - # optimizer + # Optimizer optimizer = torch.optim.SGD( model.parameters(), lr=args.lr, momentum=args.momentum) - deepspeed_config = dict(train_batch_size=args.batch_size) - # 'config_params' key is ignored if strategy != DSDistributedStrategy - distrib_model, optimizer, _ = strategy.distributed( - model, optimizer, lr_scheduler=None, config_params=deepspeed_config + # Distributed model, optimizer, and scheduler + model, optimizer, _ = strategy.distributed( + model, optimizer, lr_scheduler=None, **distribute_kwargs ) - # resume state - start_epoch = 1 - best_acc = np.Inf - res_name = f'{args.strategy}-checkpoint.pth.tar' - if os.path.isfile(res_name): - try: - if torch.cuda.is_available(): - dist.barrier() - # Map model to be loaded to specified single gpu. - loc = {'cuda:%d' % 0: 'cuda:%d' % lrank} if args.cuda else { - 'cpu:%d' % 0: 'cpu:%d' % lrank} - checkpoint = torch.load( - program_dir+'/'+res_name, map_location=loc) - else: - checkpoint = torch.load(program_dir+'/'+res_name) - start_epoch = checkpoint['epoch'] - best_acc = checkpoint['best_acc'] - distrib_model.load_state_dict(checkpoint['state_dict']) - optimizer.load_state_dict(checkpoint['optimizer']) - if torch.cuda.is_available(): - if strategy.is_main_worker(): - print(f'WARNING: restarting from {start_epoch} epoch') - else: - print(f'WARNING: restarting from {start_epoch} epoch') - except Exception: - if torch.cuda.is_available(): - if strategy.is_main_worker(): - print('WARNING: restart file cannot be loaded, ' - 'restarting!') - else: - print('WARNING: restart file cannot be loaded, restarting!') - - if start_epoch > args.epochs: - if torch.cuda.is_available(): - if strategy.is_main_worker(): - print('WARNING: given epochs are less than the one in the ' - 'restart file!\n' - 'WARNING: SYS.EXIT is issued') - - strategy.clean_up() - sys.exit() - else: - print('WARNING: given epochs are less than the one in ' - 'the restart file!\n' - 'WARNING: SYS.EXIT is issued') - sys.exit() - - # start trainin/testing loop - if strategy.is_main_worker(): - print('TIMER: broadcast:', time.time()-st, 's') + # Start training and test loop + if strategy.is_main_worker: + print('TIMER: broadcast:', timer()-st, 's') print('\nDEBUG: start training') print('--------------------------------------------------------') - et = time.time() + et = timer() + start_epoch = 1 for epoch in range(start_epoch, args.epochs + 1): - lt = time.time() - # training + lt = timer() + if strategy.is_distributed: + # Inform the sampler that a new epoch started: shuffle + # may be needed + train_loader.sampler.set_epoch(epoch) + test_loader.sampler.set_epoch(epoch) + + # Training loss_acc = train( - model=distrib_model, - device=device, + model=model, train_loader=train_loader, optimizer=optimizer, epoch=epoch, @@ -470,77 +375,52 @@ def download_mnist(): args=args ) - # testing + # Testing acc_test = test( - model=distrib_model, - device=device, + model=model, test_loader=test_loader, strategy=strategy ) - # save first epoch timer + # Save first epoch timer if epoch == start_epoch: - first_ep_t = time.time()-lt + first_ep_t = timer()-lt - # final epoch + # Final epoch if epoch + 1 == args.epochs: train_loader.last_epoch = True test_loader.last_epoch = True - if strategy.is_main_worker(): - print('TIMER: epoch time:', time.time()-lt, 's') + if strategy.is_main_worker: + print('TIMER: epoch time:', timer()-lt, 's') print('DEBUG: accuracy:', acc_test, '%') - # save state if found a better state - is_best = loss_acc < best_acc - if epoch % args.restart_int == 0: - save_state( - epoch=epoch, - distrib_model=distrib_model, - loss_acc=loss_acc, - optimizer=optimizer, - res_name=res_name, - is_best=is_best, - strategy=strategy - ) - # reset best_acc - best_acc = min(loss_acc, best_acc) - - # finalise - # save final state - save_state( - epoch=epoch, - distrib_model=distrib_model, - loss_acc=loss_acc, - optimizer=optimizer, - res_name=res_name, - is_best=True, - strategy=strategy - ) - - # some debug - if strategy.is_main_worker(): + if strategy.is_main_worker: print('\n--------------------------------------------------------') print('DEBUG: training results:\n') print('TIMER: first epoch time:', first_ep_t, ' s') - print('TIMER: last epoch time:', time.time()-lt, ' s') - print('TIMER: average epoch time:', (time.time()-et)/args.epochs, ' s') - print('TIMER: total epoch time:', time.time()-et, ' s') + print('TIMER: last epoch time:', timer()-lt, ' s') + print('TIMER: average epoch time:', (timer()-et)/args.epochs, ' s') + print('TIMER: total epoch time:', timer()-et, ' s') if epoch > 1: print('TIMER: total epoch-1 time:', - time.time()-et-first_ep_t, ' s') + timer()-et-first_ep_t, ' s') print('TIMER: average epoch-1 time:', - (time.time()-et-first_ep_t)/(args.epochs-1), ' s') + (timer()-et-first_ep_t)/(args.epochs-1), ' s') print('DEBUG: last accuracy:', acc_test, '%') - print('DEBUG: memory req:', - int(torch.cuda.memory_reserved(lrank)/1024/1024), 'MB') \ - if args.cuda else 'DEBUG: memory req: - MB' - print('DEBUG: memory summary:\n\n', - torch.cuda.memory_summary(0)) if args.cuda else '' + if torch.cuda.is_available(): + print('DEBUG: memory req:', + int(torch.cuda.memory_reserved( + strategy.local_rank())/1024/1024), + 'MB') + print('DEBUG: memory summary:\n\n', + torch.cuda.memory_summary(0)) + + print(f'TIMER: final time: {timer()-st} s\n') - if strategy.is_main_worker(): - print(f'TIMER: final time: {time.time()-st} s\n') + time.sleep(1) + print(f" - TRAINING FINISHED") - print(f" - TRAINING FINISHED") + # Clean-up strategy.clean_up() sys.exit() diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/README.md b/tutorials/distributed-ml/torch-tutorial-2-imagenet/README.md deleted file mode 100644 index 780eb278..00000000 --- a/tutorials/distributed-ml/torch-tutorial-2-imagenet/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Tutorial: distributed strategies for PyTorch model trained on MNIST dataset - -In this tutorial we show how to use torch `DistributedDataParallel` (DDP), Horovod and -DeepSpeed from the same client code. -Note that the environment is tested on the HDFML system at JSC. For other systems, -the module versions might need change accordingly. - -## Setup - -First, from the root of this repository, build the environment containing -pytorch, horovod and deepspeed. You can *try* with: - -```bash -# Creates a Python venv called envAI_hdfml -make torch-gpu-jsc -``` - -The Imagenet dataset is assumed to be already downloaded to some location. - -## Distributed training - -Each distributed strategy has its own SLURM job script, which -should be used to run it: - -If you want to distribute the code in `train.py` with **torch DDP**, run from terminal: - -```bash -sbatch ddp_slurm.sh -``` - -If you want to distribute the code in `train.py` with **DeepSpeed**, run from terminal: - -```bash -sbatch deepspeed_slurm.sh -``` - -If you want to distribute the code in `train.py` with **Horovod**, run from terminal: - -```bash -sbatch hvd_slurm.sh -``` - -You can run all of them with: - -```bash -bash runall.sh -``` diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/config.yaml b/tutorials/distributed-ml/torch-tutorial-2-imagenet/config.yaml deleted file mode 100644 index 2473d346..00000000 --- a/tutorials/distributed-ml/torch-tutorial-2-imagenet/config.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# I/O -data_dir: /p/project/intertwin/datasets/Imagenet_sub/ImageNet_uncompressed/train/ #/p/project/intertwin/datasets/ImageNet_uncompressed/train -restart_int: 10 -verbose: True - -# Model -batch_size: 64 -epochs: 3 -lr: 0.001 -momentum: 0.5 -shuff: False -num_classes: 1000 - -# Debugging -testrun: False -nseed: 10 -log_int: 10 - -# Distributed ML -backend: nccl -nworker: 4 # num workers dataloader -prefetch: 2 -no_cuda: False - - diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/ddp_slurm.sh b/tutorials/distributed-ml/torch-tutorial-2-imagenet/ddp_slurm.sh deleted file mode 100644 index 4e9749c2..00000000 --- a/tutorials/distributed-ml/torch-tutorial-2-imagenet/ddp_slurm.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -# general configuration of the job -#SBATCH --job-name=Torch_DDP_tutorial-1 -#SBATCH --account=intertwin -#SBATCH --mail-user= -#SBATCH --mail-type=ALL -#SBATCH --output=job-ddp.out -#SBATCH --error=job-ddp.err -#SBATCH --time=00:30:00 - -# configure node and process count on the CM -#SBATCH --partition=batch -#SBATCH --nodes=2 -#SBATCH --ntasks-per-node=1 -#SBATCH --cpus-per-task=32 -#SBATCH --gpus-per-node=4 -#SBATCH --exclusive - -# gres options have to be disabled for deepv -#SBATCH --gres=gpu:4 - -# set modules -ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py - -# set env -source ../../../envAI_hdfml/bin/activate - -# job info -debug=false -echo "DEBUG: TIME: $(date)" -echo "DEBUG: EXECUTE: $EXEC" -echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" -echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" -echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" -echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" -echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" -echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" -echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" -echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" -echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" -if [ "$debug" = true ] ; then - export NCCL_DEBUG=INFO -fi -echo - -# set comm -export CUDA_VISIBLE_DEVICES="0,1,2,3" -export OMP_NUM_THREADS=1 -if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then - export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK -fi - -# launch training -TRAINING_CMD="train.py -s ddp -c config.yaml" - -srun --cpu-bind=none bash -c "torchrun \ - --log_dir='logs' \ - --nnodes=$SLURM_NNODES \ - --nproc_per_node=$SLURM_GPUS_PER_NODE \ - --rdzv_id=$SLURM_JOB_ID \ - --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ - --rdzv_backend=c10d \ - --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ - $TRAINING_CMD" - diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/deepspeed_slurm.sh b/tutorials/distributed-ml/torch-tutorial-2-imagenet/deepspeed_slurm.sh deleted file mode 100644 index 8f1c2d2d..00000000 --- a/tutorials/distributed-ml/torch-tutorial-2-imagenet/deepspeed_slurm.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash - -# general configuration of the job -#SBATCH --job-name=Torch_DeepSpeed_tutorial-1 -#SBATCH --account=intertwin -#SBATCH --mail-user= -#SBATCH --mail-type=ALL -#SBATCH --output=job-ds.out -#SBATCH --error=job-ds.err -#SBATCH --time=00:30:00 - -# configure node and process count on the CM -#SBATCH --partition=batch -#SBATCH --nodes=2 -#SBATCH --ntasks-per-node=4 -#SBATCH --cpus-per-task=4 -#SBATCH --gpus-per-node=4 -#SBATCH --exclusive - -# gres options have to be disabled for deepv -#SBATCH --gres=gpu:4 - -# set modules -ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py - -# set env -source ../../../envAI_hdfml/bin/activate - -# job info -debug=false -echo "DEBUG: TIME: $(date)" -echo "DEBUG: EXECUTE: $EXEC" -echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" -echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" -echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" -echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" -echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" -echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" -echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" -echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" -echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" -if [ "$debug" = true ] ; then - export NCCL_DEBUG=INFO -fi -echo - -# set env vars -export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} -export OMP_NUM_THREADS=1 -if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then - export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK -fi -export CUDA_VISIBLE_DEVICES="0,1,2,3" - -# launch training -MASTER_ADDR=$(scontrol show hostnames "\$SLURM_JOB_NODELIST" | head -n 1)i -export MASTER_ADDR -export MASTER_PORT=29500 - -TRAINING_CMD="train.py -s deepspeed -c config.yaml" - -# Run without launcher: set --ntasks-per-node=NUM_GPUS -srun --cpu-bind=none python -u $TRAINING_CMD --deepspeed - -# # Run with deepspeed launcher: set --ntasks-per-node=1 -# # https://www.deepspeed.ai/getting-started/#multi-node-environment-variables -# export NCCL_IB_DISABLE=1 -# export NCCL_SOCKET_IFNAME=eth0 -# nodelist=$(scontrol show hostname $SLURM_NODELIST) -# echo "$nodelist" | sed -e 's/$/ slots=4/' > .hostfile -# # Requires passwordless SSH access among compute node -# srun --cpu-bind=none deepspeed --hostfile=.hostfile $TRAINING_CMD --deepspeed -# rm .hostfile - diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/hvd_slurm.sh b/tutorials/distributed-ml/torch-tutorial-2-imagenet/hvd_slurm.sh deleted file mode 100644 index 69b9d51e..00000000 --- a/tutorials/distributed-ml/torch-tutorial-2-imagenet/hvd_slurm.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# general configuration of the job -#SBATCH --job-name=Torch_HVD_tutorial-1 -#SBATCH --account=intertwin -#SBATCH --mail-user= -#SBATCH --mail-type=ALL -#SBATCH --output=job-hvd.out -#SBATCH --error=job-hvd.err -#SBATCH --time=00:30:00 - -# configure node and process count on the CM -#SBATCH --partition=batch -#SBATCH --nodes=2 -#SBATCH --ntasks-per-node=4 -#SBATCH --cpus-per-task=8 -#SBATCH --gpus-per-node=4 -#SBATCH --exclusive - -# gres options have to be disabled for deepv -#SBATCH --gres=gpu:4 - -# set modules -ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py - -# set env -source ../../../envAI_hdfml/bin/activate - -# job info -debug=false -echo "DEBUG: TIME: $(date)" -echo "DEBUG: EXECUTE: $EXEC" -echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" -echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" -echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" -echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" -echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" -echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" -echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" -echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" -echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" -if [ "$debug" = true ] ; then - export NCCL_DEBUG=INFO -fi -echo - -# set vars -# export NCCL_DEBUG=INFO -export SRUN_CPUS_PER_TASK=${SLURM_CPUS_PER_TASK} -export OMP_NUM_THREADS=1 -if [ "$SLURM_CPUS_PER_TASK" -gt 0 ] ; then - export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK -fi -export CUDA_VISIBLE_DEVICES="0,1,2,3" - -# launch training -TRAINING_CMD="train.py -s horovod -c config.yaml" - -srun --cpu-bind=none python -u $TRAINING_CMD - diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/runall.sh b/tutorials/distributed-ml/torch-tutorial-2-imagenet/runall.sh deleted file mode 100644 index 21c02a22..00000000 --- a/tutorials/distributed-ml/torch-tutorial-2-imagenet/runall.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -# Run all versions of distributed ML version -rm *checkpoint.pth.tar *.out *.err *.csv -echo "Torch DDP training: $(sbatch ddp_slurm.sh)" -echo "DeepSpeed training: $(sbatch deepspeed_slurm.sh)" -echo "Horovod training: $(sbatch hvd_slurm.sh)" \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/scaling-test.sh b/tutorials/distributed-ml/torch-tutorial-2-imagenet/scaling-test.sh deleted file mode 100644 index 275f7fb7..00000000 --- a/tutorials/distributed-ml/torch-tutorial-2-imagenet/scaling-test.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -rm *checkpoint.pth.tar *.out *.err *.csv - -timeout="01:01:00" -for N in 1 2 4 8 -do - sbatch --job-name="DDP-imagenet-n$N" --nodes=$N --output="job-ddp-n$N.out" --error="job-ddp-n$N.err" --time=$timeout ddp_slurm.sh - sbatch --job-name="DS-imagenet-n$N" --nodes=$N --output="job-ds-n$N.out" --error="job-ds-n$N.err" --time=$timeout deepspeed_slurm.sh - sbatch --job-name="HVD-imagenet-n$N" --nodes=$N --output="job-hvd-n$N.out" --error="job-hvd-n$N.err" --time=$timeout hvd_slurm.sh -done \ No newline at end of file diff --git a/tutorials/distributed-ml/torch-tutorial-2-imagenet/train.py b/tutorials/distributed-ml/torch-tutorial-2-imagenet/train.py deleted file mode 100644 index 6bd71214..00000000 --- a/tutorials/distributed-ml/torch-tutorial-2-imagenet/train.py +++ /dev/null @@ -1,499 +0,0 @@ -""" -Show how to use DDP, Horovod and DeepSpeed strategies interchangeably -with a large neural network trained on Imagenet dataset, showing how -to use checkpoints. -""" -import os -import argparse -import sys -import time -import numpy as np -import random - -import torch -from torch import nn -import torch.distributed as dist -import torch.nn.functional as F -import torchvision -from torchvision import transforms -from torch.utils.data import DataLoader, DistributedSampler - -import deepspeed - -from itwinai.torch.distributed import ( - TorchDistributedStrategy, - DDPDistributedStrategy, - HVDDistributedStrategy, - DSDistributedStrategy, -) -from itwinai.parser import ArgumentParser as ItAIArgumentParser -from itwinai.loggers import EpochTimeTracker - - -def parse_args() -> argparse.Namespace: - """ - Parse CLI args, which can also be loaded from a configuration file - using the --config flag: - - >>> train.py --strategy ddp --config config.yaml - """ - parser = ItAIArgumentParser(description='PyTorch MNIST Example') - - # Distributed ML strategy - parser.add_argument( - "--strategy", "-s", type=str, - choices=['ddp', 'horovod', 'deepspeed'], - default='ddp' - ) - - # IO parsers - parser.add_argument('--data-dir', default='./', - help=('location of the training dataset in the local ' - 'filesystem')) - parser.add_argument('--restart-int', type=int, default=10, - help='restart interval per epoch (default: 10)') - parser.add_argument('--verbose', - action=argparse.BooleanOptionalAction, - help='Print parsed arguments') - - # model parsers - parser.add_argument('--batch-size', type=int, default=64, - help='input batch size for training (default: 64)') - parser.add_argument('--epochs', type=int, default=10, - help='number of epochs to train (default: 10)') - parser.add_argument('--lr', type=float, default=0.01, - help='learning rate (default: 0.01)') - parser.add_argument('--momentum', type=float, default=0.5, - help='momentum in SGD optimizer (default: 0.5)') - parser.add_argument('--shuff', action='store_true', default=False, - help='shuffle dataset (default: False)') - parser.add_argument('--num-classes', type=int, default=1000, - help='number of classes in dataset') - - # debug parsers - parser.add_argument('--testrun', action='store_true', default=False, - help='do a test run with seed (default: False)') - parser.add_argument('--nseed', type=int, default=0, - help='seed integer for reproducibility (default: 0)') - parser.add_argument('--log-int', type=int, default=10, - help='log interval per training') - - # parallel parsers - parser.add_argument('--backend', type=str, default='nccl', - help='backend for parrallelisation (default: nccl)') - parser.add_argument('--nworker', type=int, default=0, - help=('number of workers in DataLoader (default: 0 -' - ' only main)')) - parser.add_argument('--prefetch', type=int, default=2, - help='prefetch data in DataLoader (default: 2)') - parser.add_argument('--no-cuda', action='store_true', default=False, - help='disables GPGPUs') - parser.add_argument('--local_rank', type=int, default=-1, - help='local rank passed from distributed launcher') - - # DeepSpeed - parser = deepspeed.add_config_arguments(parser) - args = parser.parse_args() - - if args.verbose: - args_list = [f"{key}: {val}" for key, val in args.items()] - print("PARSED ARGS:\n", '\n'.join(args_list)) - - return args - - -def train( - model, device, train_loader, optimizer, epoch, - strategy: TorchDistributedStrategy, args -): - """ - Training function, representing an epoch. - """ - model.train() - t_list = [] - loss_acc = 0 - gwsize = strategy.dist_gwsize() - if strategy.is_main_worker(): - print("\n") - for batch_idx, (data, target) in enumerate(train_loader): - t = time.perf_counter() - data, target = data.to(device), target.to(device) - optimizer.zero_grad() - output = model(data) - loss = F.nll_loss(output, target) - loss.backward() - optimizer.step() - if batch_idx % args.log_int == 0 and strategy.is_main_worker(): - print( - f'Train epoch: {epoch} ' - f'[{batch_idx * len(data)}/{len(train_loader.dataset)/gwsize} ' - f'({100.0 * batch_idx / len(train_loader):.0f}%)]\t\t' - f'Loss: {loss.item():.6f}') - t_list.append(time.perf_counter() - t) - loss_acc += loss.item() - if strategy.is_main_worker(): - print('TIMER: train time', sum(t_list) / len(t_list), 's') - return loss_acc - - -def test(model, device, test_loader, strategy: TorchDistributedStrategy): - """ - Model validation. - """ - model.eval() - test_loss = 0 - correct = 0 - gwsize = strategy.dist_gwsize() - with torch.no_grad(): - for data, target in test_loader: - data, target = data.to(device), target.to(device) - output = model(data) - # sum up batch loss - test_loss += F.nll_loss(output, target, reduction="sum").item() - # get the index of the max log-probability - pred = output.argmax(dim=1, keepdim=True) - correct += pred.eq(target.view_as(pred)).sum().item() - test_loss /= len(test_loader.dataset) - if strategy.is_main_worker(): - print( - f'Test set: average loss: {test_loss:.4f}\t' - f'accurate samples: {correct}/{len(test_loader.dataset)/gwsize}') - acc_test = 100.0 * correct * gwsize / len(test_loader.dataset) - return acc_test - - -def save_state( - epoch, distrib_model, loss_acc, optimizer, - res_name, is_best, strategy: TorchDistributedStrategy -): - """ - Save training state. - """ - grank = strategy.dist_grank() - rt = time.time() - # find if is_best happened in any worker - if torch.cuda.is_available(): - is_best_m = strategy.par_allgather_obj(is_best) - - if torch.cuda.is_available(): - if any(is_best_m): - # find which rank is_best happened - select first rank if multiple - is_best_rank = np.where(np.array(is_best_m))[0][0] - - # collect state - state = {'epoch': epoch + 1, - 'state_dict': distrib_model.state_dict(), - 'best_acc': loss_acc, - 'optimizer': optimizer.state_dict()} - - # write on worker with is_best - if grank == is_best_rank: - torch.save(state, './'+res_name) - print( - f'DEBUG: state in {grank} is saved on epoch:{epoch} ' - f'in {time.time()-rt} s') - else: - # collect state - state = {'epoch': epoch + 1, - 'state_dict': distrib_model.state_dict(), - 'best_acc': loss_acc, - 'optimizer': optimizer.state_dict()} - - torch.save(state, './'+res_name) - print( - f'DEBUG: state in {grank} is saved on epoch:{epoch} in ' - f'{time.time()-rt} s') - - -def seed_worker(worker_id): - """ - Seed dataloader worker. - """ - worker_seed = torch.initial_seed() % 2**32 - np.random.seed(worker_seed) - random.seed(worker_seed) - - -if __name__ == "__main__": - - args = parse_args() - - # Instantiate Strategy - if args.strategy == 'ddp': - if (not torch.cuda.is_available() - or not torch.cuda.device_count() > 1): - raise RuntimeError('Resources unavailable') - - strategy = DDPDistributedStrategy(backend=args.backend) - elif args.strategy == 'horovod': - strategy = HVDDistributedStrategy() - elif args.strategy == 'deepspeed': - strategy = DSDistributedStrategy(backend=args.backend) - else: - raise NotImplementedError( - f"Strategy {args.strategy} is not recognized/implemented.") - strategy.init() - - # check CUDA availability - args.cuda = not args.no_cuda and torch.cuda.is_available() - - # limit # of CPU threads to be used per worker - torch.set_num_threads(1) - - # get directory - program_dir = os.getcwd() - - # start the time.time for profiling - st = time.time() - - # deterministic testrun - if args.testrun: - torch.manual_seed(args.nseed) - g = torch.Generator() - g.manual_seed(args.nseed) - - # get job rank info - rank==0 master gpu - if torch.cuda.is_available(): - # local world size - per node - lwsize = strategy.dist_lwsize() if args.cuda else 0 - gwsize = strategy.dist_gwsize() # global world size - per run - grank = strategy.dist_grank() # global rank - assign per run - lrank = strategy.dist_lrank() # local rank - assign per node - else: - gwsize = 1 - grank = 0 - - # some debug - if strategy.is_main_worker(): - print('TIMER: initialise:', time.time()-st, 's') - - # move the model on the GPU assigned to the current process - device = torch.device( - strategy.dist_device() if args.cuda and torch.cuda.is_available() - else 'cpu') - if args.cuda: - torch.cuda.set_device(lrank) - # deterministic testrun - if args.testrun: - torch.cuda.manual_seed(args.nseed) - - # dataset - # Initialize transformations for data augmentation - transform = transforms.Compose([ - transforms.Resize(256), - transforms.RandomHorizontalFlip(), - transforms.RandomVerticalFlip(), - transforms.RandomRotation(degrees=45), - transforms.ColorJitter( - brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5), - transforms.CenterCrop(224), - transforms.ToTensor(), - transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) - ]) - - # Load the ImageNet Object Localization Challenge dataset - train_dataset = torchvision.datasets.ImageFolder( - root=args.data_dir, - transform=transform - ) - # test_dataset = ... - - # restricts data loading to a subset of the dataset exclusive to the - # current process - args.shuff = args.shuff and not args.testrun - if torch.cuda.is_available(): - train_sampler = DistributedSampler( - train_dataset, num_replicas=gwsize, rank=grank, shuffle=args.shuff) - # test_sampler = DistributedSampler( - # test_dataset, num_replicas=gwsize, rank=grank, - # shuffle=args.shuff) - # distribute dataset to workers - # persistent workers is not possible for nworker=0 - pers_w = True if args.nworker > 1 else False - - # deterministic testrun - the same dataset each run - kwargs = {'worker_init_fn': seed_worker, - 'generator': g} if args.testrun else {} - - if torch.cuda.is_available(): - train_loader = DataLoader( - train_dataset, batch_size=args.batch_size, - sampler=train_sampler, num_workers=args.nworker, pin_memory=True, - persistent_workers=pers_w, prefetch_factor=args.prefetch, **kwargs - ) - # test_loader = DataLoader( - # test_dataset, batch_size=args.batch_size, - # sampler=test_sampler, num_workers=args.nworker, pin_memory=True, - # persistent_workers=pers_w, prefetch_factor=args.prefetch, - # **kwargs - # ) - else: - train_loader = DataLoader( - train_dataset, batch_size=args.batch_size) - # test_loader = DataLoader( - # test_dataset, batch_size=args.batch_size) - - if strategy.is_main_worker(): - print('TIMER: read and concat data:', time.time()-st, 's') - - # create CNN model: resnet 50, resnet101, resnet152 - model = torchvision.models.resnet152() - model.fc = nn.Linear(2048, args.num_classes) - - # optimizer - optimizer = torch.optim.SGD( - model.parameters(), lr=args.lr, momentum=args.momentum) - - deepspeed_config = dict(train_micro_batch_size_per_gpu=args.batch_size) - # 'config_params' key is ignored if strategy != DSDistributedStrategy - distrib_model, optimizer, _ = strategy.distributed( - model, optimizer, lr_scheduler=None, config_params=deepspeed_config - ) - - # resume state - start_epoch = 1 - best_acc = np.Inf - nnod = os.environ.get('SLURM_NNODES', 'unk') - res_name = f'{args.strategy}-{nnod}N-checkpoint.pth.tar' - if os.path.isfile(res_name): - try: - if torch.cuda.is_available(): - dist.barrier() - # Map model to be loaded to specified single gpu. - loc = {'cuda:%d' % 0: 'cuda:%d' % lrank} if args.cuda else { - 'cpu:%d' % 0: 'cpu:%d' % lrank} - checkpoint = torch.load( - program_dir+'/'+res_name, map_location=loc) - else: - checkpoint = torch.load(program_dir+'/'+res_name) - start_epoch = checkpoint['epoch'] - best_acc = checkpoint['best_acc'] - distrib_model.load_state_dict(checkpoint['state_dict']) - optimizer.load_state_dict(checkpoint['optimizer']) - if torch.cuda.is_available(): - if strategy.is_main_worker(): - print(f'WARNING: restarting from {start_epoch} epoch') - else: - print(f'WARNING: restarting from {start_epoch} epoch') - except Exception: - if torch.cuda.is_available(): - if strategy.is_main_worker(): - print('WARNING: restart file cannot be loaded, ' - 'restarting!') - else: - print('WARNING: restart file cannot be loaded, restarting!') - - if start_epoch > args.epochs: - if torch.cuda.is_available(): - if strategy.is_main_worker(): - print('WARNING: given epochs are less than the one in the ' - 'restart file!\n' - 'WARNING: SYS.EXIT is issued') - - strategy.clean_up() - sys.exit() - else: - print('WARNING: given epochs are less than the one in ' - 'the restart file!\n' - 'WARNING: SYS.EXIT is issued') - sys.exit() - - # start trainin/testing loop - if strategy.is_main_worker(): - print('TIMER: broadcast:', time.time()-st, 's') - print('\nDEBUG: start training') - print('--------------------------------------------------------') - epoch_time_tracker = EpochTimeTracker(series_name=args.strategy) - - et = time.time() - for epoch in range(start_epoch, args.epochs + 1): - lt = time.time() - # training - loss_acc = train( - model=distrib_model, - device=device, - train_loader=train_loader, - optimizer=optimizer, - epoch=epoch, - strategy=strategy, - args=args - ) - - # # testing - # acc_test = test( - # model=distrib_model, - # device=device, - # test_loader=test_loader, - # strategy=strategy - # ) - - # save first epoch timer - if epoch == start_epoch: - first_ep_t = time.time()-lt - - # final epoch - if epoch + 1 == args.epochs: - train_loader.last_epoch = True - # test_loader.last_epoch = True - - if strategy.is_main_worker(): - print('TIMER: epoch time:', time.time()-lt, 's') - epoch_time_tracker.add_epoch_time(epoch-1, time.time()-lt) - # print('DEBUG: accuracy:', acc_test, '%') - - # save state if found a better state - is_best = loss_acc < best_acc - if epoch % args.restart_int == 0: - save_state( - epoch=epoch, - distrib_model=distrib_model, - loss_acc=loss_acc, - optimizer=optimizer, - res_name=res_name, - is_best=is_best, - strategy=strategy - ) - # reset best_acc - best_acc = min(loss_acc, best_acc) - - # finalise - # save final state - save_state( - epoch=epoch, - distrib_model=distrib_model, - loss_acc=loss_acc, - optimizer=optimizer, - res_name=res_name, - is_best=True, - strategy=strategy - ) - - # some debug - if strategy.is_main_worker(): - print('\n--------------------------------------------------------') - print('DEBUG: training results:\n') - print('TIMER: first epoch time:', first_ep_t, ' s') - print('TIMER: last epoch time:', time.time()-lt, ' s') - print('TIMER: average epoch time:', (time.time()-et)/args.epochs, ' s') - print('TIMER: total epoch time:', time.time()-et, ' s') - if epoch > 1: - print('TIMER: total epoch-1 time:', - time.time()-et-first_ep_t, ' s') - print('TIMER: average epoch-1 time:', - (time.time()-et-first_ep_t)/(args.epochs-1), ' s') - # print('DEBUG: last accuracy:', acc_test, '%') - print('DEBUG: memory req:', - int(torch.cuda.memory_reserved(lrank)/1024/1024), 'MB') \ - if args.cuda else 'DEBUG: memory req: - MB' - print('DEBUG: memory summary:\n\n', - torch.cuda.memory_summary(0)) if args.cuda else '' - - if strategy.is_main_worker(): - print(f'TIMER: final time: {time.time()-st} s\n') - nnod = os.environ.get('SLURM_NNODES', 'unk') - epoch_time_tracker.save( - csv_file=f"epochtime_{args.strategy}_{nnod}N.csv") - - print(f" - TRAINING FINISHED") - strategy.clean_up() - sys.exit() diff --git a/tutorials/ml-workflows/basic_components.py b/tutorials/ml-workflows/basic_components.py index 49e74180..1fca03d8 100644 --- a/tutorials/ml-workflows/basic_components.py +++ b/tutorials/ml-workflows/basic_components.py @@ -70,12 +70,6 @@ def execute( """ return train_set, vaild_set, test_set, "my_trained_model" - def save_state(self): - return super().save_state() - - def load_state(self): - return super().load_state() - class MySaver(Saver): @monitor_exec diff --git a/use-cases/3dgan/create_inference_sample.py b/use-cases/3dgan/create_inference_sample.py new file mode 100644 index 00000000..14b88870 --- /dev/null +++ b/use-cases/3dgan/create_inference_sample.py @@ -0,0 +1,23 @@ +"""Create a simple inference dataset sample and a checkpoint.""" + +import argparse +import os +import torch +from model import ThreeDGAN + + +def create_checkpoint( + root: str = '.', + ckpt_name: str = "3dgan-inference.pth" +): + ckpt_path = os.path.join(root, ckpt_name) + net = ThreeDGAN() + torch.save(net, ckpt_path) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--root", type=str, default='.') + parser.add_argument("--ckpt-name", type=str, default="3dgan-inference.pth") + args = parser.parse_args() + create_checkpoint(**vars(args)) diff --git a/use-cases/3dgan/dataloader.py b/use-cases/3dgan/dataloader.py index 89234895..b4dcc096 100644 --- a/use-cases/3dgan/dataloader.py +++ b/use-cases/3dgan/dataloader.py @@ -35,7 +35,8 @@ def execute(self): gdown.download_folder( url=self.data_url, quiet=False, - output=self.data_path + output=self.data_path, + verify=False ) diff --git a/use-cases/3dgan/trainer.py b/use-cases/3dgan/trainer.py index 3bb5a1fd..8e022bc9 100644 --- a/use-cases/3dgan/trainer.py +++ b/use-cases/3dgan/trainer.py @@ -52,12 +52,6 @@ def execute(self) -> Any: cli.trainer.fit(cli.model, datamodule=cli.datamodule) teardown_lightning_mlflow() - def save_state(self): - return super().save_state() - - def load_state(self): - return super().load_state() - class LightningModelLoader(TorchModelLoader): """Loads a torch lightning model from somewhere. diff --git a/use-cases/cyclones/README.md b/use-cases/cyclones/README.md new file mode 100644 index 00000000..6b504fb0 --- /dev/null +++ b/use-cases/cyclones/README.md @@ -0,0 +1,12 @@ +# Tropical cyclone detection + +## Dataset + +If the automatic download from python does not work, try from the command line from +within the virtual environment: + +```bash +gdown https://drive.google.com/drive/folders/1TnmujO4T-8_j4bCxqNe5HEw9njJIIBQD -O data/tmp_data/trainval --folder +``` + +For more info visit the [gdown](https://github.com/wkentaro/gdown) repository. diff --git a/use-cases/cyclones/dataloader.py b/use-cases/cyclones/dataloader.py index 7f224157..3cf1d97b 100644 --- a/use-cases/cyclones/dataloader.py +++ b/use-cases/cyclones/dataloader.py @@ -180,6 +180,7 @@ def setup_config(self, config: Dict) -> None: if not exists(join(root_dir, self.data_path)): gdown.download_folder( url=self.data_url, quiet=False, + verify=False, output=join(root_dir, self.data_path) ) diff --git a/use-cases/cyclones/trainer.py b/use-cases/cyclones/trainer.py index 1c47819b..054f772b 100644 --- a/use-cases/cyclones/trainer.py +++ b/use-cases/cyclones/trainer.py @@ -155,9 +155,3 @@ def setup_config(self, config: Dict) -> None: if self.model_backup: self.best_model_name = join(self.model_backup, "best_model.h5") self.last_model_name = join(self.run_dir, "last_model.h5") - - def load_state(self): - return super().load_state() - - def save_state(self): - return super().save_state() diff --git a/use-cases/mnist/tensorflow/pipeline.yaml b/use-cases/mnist/tensorflow/pipeline.yaml index 9fced327..314f78b1 100644 --- a/use-cases/mnist/tensorflow/pipeline.yaml +++ b/use-cases/mnist/tensorflow/pipeline.yaml @@ -32,9 +32,9 @@ pipeline: strategy: class_path: tensorflow.python.distribute.mirrored_strategy.MirroredStrategy - logger: - - class_path: itwinai.loggers.ConsoleLogger - - class_path: itwinai.loggers.MLFlowLogger - init_args: - experiment_name: MNIST classifier - log_freq: batch + # logger: + # - class_path: itwinai.loggers.ConsoleLogger + # - class_path: itwinai.loggers.MLFlowLogger + # init_args: + # experiment_name: MNIST classifier + # log_freq: batch diff --git a/use-cases/mnist/tensorflow/trainer.py b/use-cases/mnist/tensorflow/trainer.py index 17ef19a5..435f79f4 100644 --- a/use-cases/mnist/tensorflow/trainer.py +++ b/use-cases/mnist/tensorflow/trainer.py @@ -35,9 +35,3 @@ def __init__( @monitor_exec def execute(self, train_dataset, validation_dataset) -> Any: return super().execute(train_dataset, validation_dataset) - - def load_state(self): - return super().load_state() - - def save_state(self): - return super().save_state() diff --git a/use-cases/mnist/torch-lightning/README.md b/use-cases/mnist/torch-lightning/README.md new file mode 100644 index 00000000..bd769c70 --- /dev/null +++ b/use-cases/mnist/torch-lightning/README.md @@ -0,0 +1,17 @@ +# Torch Lightning example on MNIST dataset + +## Training + +```bash +# Download dataset and exit: only run first step in the pipeline (index=0) +itwinai exec-pipeline --config config.yaml --pipe-key training_pipeline --steps 0 + +# Run the whole training pipeline +itwinai exec-pipeline --config config.yaml --pipe-key training_pipeline +``` + +View training logs on MLFLow server (if activated from the configuration): + +```bash +mlflow ui --backend-store-uri mllogs/mlflow/ +``` diff --git a/use-cases/mnist/torch-lightning/pipeline.yaml b/use-cases/mnist/torch-lightning/config.yaml similarity index 96% rename from use-cases/mnist/torch-lightning/pipeline.yaml rename to use-cases/mnist/torch-lightning/config.yaml index cf754b2f..23fde03d 100644 --- a/use-cases/mnist/torch-lightning/pipeline.yaml +++ b/use-cases/mnist/torch-lightning/config.yaml @@ -1,4 +1,4 @@ -pipeline: +training_pipeline: class_path: itwinai.pipeline.Pipeline init_args: steps: @@ -6,7 +6,7 @@ pipeline: init_args: data_path: data/ - - class_path: trainer.LightningMNISTTrainer + - class_path: itwinai.torch.trainer.TorchLightningTrainer #trainer.LightningMNISTTrainer init_args: # Pytorch lightning config for training config: diff --git a/use-cases/mnist/torch-lightning/dataloader.py b/use-cases/mnist/torch-lightning/dataloader.py index 1f062fe5..b7e8d46e 100644 --- a/use-cases/mnist/torch-lightning/dataloader.py +++ b/use-cases/mnist/torch-lightning/dataloader.py @@ -31,7 +31,7 @@ def execute(self) -> None: self._downloader.setup(stage='predict') -class MNISTDataModule(L.LightningModule): +class MNISTDataModule(L.LightningDataModule): def __init__( self, data_path: str, diff --git a/use-cases/mnist/torch-lightning/train.py b/use-cases/mnist/torch-lightning/train.py deleted file mode 100644 index 97f53093..00000000 --- a/use-cases/mnist/torch-lightning/train.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Training pipeline. To run this script, use the following commands. - -On login node: - ->>> micromamba run -p ../../../.venv-pytorch/ \ - python train.py -p pipeline.yaml -d - -On compute nodes: - ->>> micromamba run -p ../../../.venv-pytorch/ \ - python train.py -p pipeline.yaml - -""" - -import argparse - -from itwinai.parser import ConfigParser - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "-p", "--pipeline", type=str, required=True, - help='Configuration file to the pipeline to execute.' - ) - parser.add_argument( - '-d', '--download-only', - action=argparse.BooleanOptionalAction, - default=False, - help=('Whether to download only the dataset and exit execution ' - '(suggested on login nodes of HPC systems)') - ) - args = parser.parse_args() - - # Create parser for the pipeline - pipe_parser = ConfigParser(config=args.pipeline) - pipeline = pipe_parser.parse_pipeline() - - if args.download_only: - print('Downloading datasets and exiting...') - pipeline = pipeline[:1] - - pipeline.execute() diff --git a/use-cases/mnist/torch-lightning/trainer.py b/use-cases/mnist/torch-lightning/trainer.py deleted file mode 100644 index 128cf5c6..00000000 --- a/use-cases/mnist/torch-lightning/trainer.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -from typing import Union, Dict, Any - -from itwinai.components import Trainer, monitor_exec -from itwinai.torch.models.mnist import MNISTModel -from dataloader import MNISTDataModule -from lightning.pytorch.cli import LightningCLI -from utils import load_yaml - - -class LightningMNISTTrainer(Trainer): - def __init__(self, config: Union[Dict, str]): - super().__init__() - self.save_parameters(**self.locals2params(locals())) - if isinstance(config, str) and os.path.isfile(config): - # Load from YAML - config = load_yaml(config) - self.conf = config - - @monitor_exec - def execute(self) -> Any: - cli = LightningCLI( - args=self.conf, - model_class=MNISTModel, - datamodule_class=MNISTDataModule, - run=False, - save_config_kwargs={ - "overwrite": True, - "config_filename": "pl-training.yml", - }, - subclass_mode_model=True, - subclass_mode_data=True, - ) - cli.trainer.fit(cli.model, datamodule=cli.datamodule) - - def save_state(self): - return super().save_state() - - def load_state(self): - return super().load_state() diff --git a/use-cases/mnist/torch/Dockerfile b/use-cases/mnist/torch/Dockerfile index b4cf3654..5b96feb5 100644 --- a/use-cases/mnist/torch/Dockerfile +++ b/use-cases/mnist/torch/Dockerfile @@ -1,4 +1,5 @@ -FROM python:3.9.12 +# FROM python:3.9 +FROM nvcr.io/nvidia/pytorch:23.09-py3 WORKDIR /usr/src/app @@ -13,6 +14,3 @@ RUN pip install --no-cache-dir . # Add torch MNIST use case COPY use-cases/mnist/torch/* ./ - -# Run inference -CMD [ "python", "train.py", "-p", "inference-pipeline.yaml"] \ No newline at end of file diff --git a/use-cases/mnist/torch/README.md b/use-cases/mnist/torch/README.md index c953671f..e333f14b 100644 --- a/use-cases/mnist/torch/README.md +++ b/use-cases/mnist/torch/README.md @@ -3,10 +3,18 @@ ## Training ```bash -python train.py -p pipeline.yaml [-d] +# Download dataset and exit +itwinai exec-pipeline --config config.yaml --pipe-key training_pipeline --steps dataloading_step + +# Run the whole training pipeline +itwinai exec-pipeline --config config.yaml --pipe-key training_pipeline ``` -Use `-d` flag to run only the fist step in the pipeline. +View training logs on MLFLow server (if activated from the configuration): + +```bash +mlflow ui --backend-store-uri mllogs/mlflow/ +``` ## Inference @@ -30,24 +38,37 @@ Use `-d` flag to run only the fist step in the pipeline. folder containing a CSV file with the predictions as rows. ```bash - python train.py -p inference-pipeline.yaml + itwinai exec-pipeline --config config.yaml --pipe-key inference_pipeline ``` Note the same entry point as for training. -### Docker image +## Docker image Build from project root with ```bash # Local -docker buildx build -t itwinai-mnist-torch-inference -f use-cases/mnist/torch/Dockerfile . +docker buildx build -t itwinai:0.0.1-mnist-torch-0.1 -f use-cases/mnist/torch/Dockerfile . # Ghcr.io -docker buildx build -t ghcr.io/intertwin-eu/itwinai-mnist-torch-inference:0.0.1 -f use-cases/mnist/torch/Dockerfile . -docker push ghcr.io/intertwin-eu/itwinai-mnist-torch-inference:0.0.1 +docker buildx build -t ghcr.io/intertwin-eu/itwinai:0.0.1-mnist-torch-0.1 -f use-cases/mnist/torch/Dockerfile . +docker push ghcr.io/intertwin-eu/itwinai:0.0.1-mnist-torch-0.1 ``` +### Training with Docker container + +```bash +docker run -it --rm --name running-inference \ + -v "$PWD":/usr/data ghcr.io/intertwin-eu/itwinai:0.01-mnist-torch-0.1 \ + /bin/bash -c "itwinai exec-pipeline --print-config \ + --config /usr/src/app/config.yaml \ + --pipe-key training_pipeline \ + -o dataset_root=/usr/data/mnist-dataset " +``` + +### Inference with Docker container + From wherever a sample of MNIST jpg images is available (folder called 'mnist-sample-data/'): @@ -62,7 +83,14 @@ From wherever a sample of MNIST jpg images is available ``` ```bash -docker run -it --rm --name running-inference -v "$PWD":/usr/data ghcr.io/intertwin-eu/itwinai-mnist-torch-inference:0.0.1 +docker run -it --rm --name running-inference \ + -v "$PWD":/usr/data ghcr.io/intertwin-eu/itwinai:0.01-mnist-torch-0.1 \ + /bin/bash -c "itwinai exec-pipeline --print-config \ + --config /usr/src/app/config.yaml \ + --pipe-key inference_pipeline \ + -o test_data_path=/usr/data/mnist-sample-data \ + -o inference_model_mlflow_uri=/usr/src/app/mnist-pre-trained.pth \ + -o predictions_dir=/usr/data/mnist-predictions " ``` This command will store the results in a folder called "mnist-predictions": diff --git a/use-cases/mnist/torch/config.yaml b/use-cases/mnist/torch/config.yaml new file mode 100644 index 00000000..c5d71204 --- /dev/null +++ b/use-cases/mnist/torch/config.yaml @@ -0,0 +1,99 @@ +# General config +dataset_root: .tmp/ +num_classes: 10 +batch_size: 64 +num_workers_dataloader: 4 +pin_memory: False +lr: 0.001 +momentum: 0.9 +fp16_allreduce: False +use_adasum: False +gradient_predivide_factor: 1.0 +epochs: 2 +strategy: ddp +test_data_path: mnist-sample-data +inference_model_mlflow_uri: mnist-pre-trained.pth +predictions_dir: mnist-predictions +predictions_file: predictions.csv +class_labels: null + +# Workflows configuration +training_pipeline: + class_path: itwinai.pipeline.Pipeline + init_args: + steps: + dataloading_step: + class_path: dataloader.MNISTDataModuleTorch + init_args: + save_path: ${dataset_root} + + training_step: + class_path: itwinai.torch.trainer.TorchTrainer + init_args: + config: + batch_size: ${batch_size} + num_workers: ${num_workers_dataloader} + pin_memory: ${pin_memory} + lr: ${lr} + momentum: ${momentum} + fp16_allreduce: ${fp16_allreduce} + use_adasum: ${use_adasum} + gradient_predivide_factor: ${gradient_predivide_factor} + + model: + class_path: model.Net + epochs: ${epochs} + metrics: + accuracy: + class_path: torchmetrics.classification.MulticlassAccuracy + init_args: + num_classes: ${num_classes} + precision: + class_path: torchmetrics.classification.MulticlassPrecision + init_args: + num_classes: ${num_classes} + recall: + class_path: torchmetrics.classification.MulticlassRecall + init_args: + num_classes: ${num_classes} + logger: + class_path: itwinai.loggers.LoggersCollection + init_args: + loggers: + - class_path: itwinai.loggers.ConsoleLogger + init_args: + log_freq: 100 + - class_path: itwinai.loggers.MLFlowLogger + init_args: + experiment_name: MNIST classifier + log_freq: batch + strategy: ${strategy} + # checkpoint_every: 1 + # cluster: + # class_path: itwinai.torch.cluster.LocalCluster + # init_args: + # gpus: '0,1,2' + # backend: nccl + +inference_pipeline: + class_path: itwinai.pipeline.Pipeline + init_args: + steps: + - class_path: dataloader.MNISTPredictLoader + init_args: + test_data_path: ${test_data_path} + + - class_path: itwinai.torch.inference.MulticlassTorchPredictor + init_args: + model: + class_path: itwinai.torch.inference.TorchModelLoader + init_args: + model_uri: ${inference_model_mlflow_uri} + test_dataloader_kwargs: + batch_size: ${batch_size} + + - class_path: saver.TorchMNISTLabelSaver + init_args: + save_dir: ${predictions_dir} + predictions_file: ${predictions_file} + class_labels: ${class_labels} \ No newline at end of file diff --git a/use-cases/mnist/torch/create_inference_sample.py b/use-cases/mnist/torch/create_inference_sample.py new file mode 100644 index 00000000..1c588c48 --- /dev/null +++ b/use-cases/mnist/torch/create_inference_sample.py @@ -0,0 +1,42 @@ +"""Create a simple inference dataset sample and a checkpoint.""" + +import torch +import os +import argparse + +from model import Net +from dataloader import InferenceMNIST + + +def mnist_torch_inference_files( + root: str = '.', + samples_path: str = 'mnist-sample-data/', + model_name: str = 'mnist-pre-trained.pth' +): + """Create sample dataset and fake model to test mnist + inference workflow. Assumes to be run from + the use case folder. + + Args: + root (str, optional): where to create the files. + Defaults to '.'. + """ + + sample = os.path.join(root, samples_path) + InferenceMNIST.generate_jpg_sample(sample, 10) + + # Fake checkpoint + dummy_nn = Net() + mdl_ckpt = os.path.join(root, model_name) + torch.save(dummy_nn, mdl_ckpt) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--root", type=str, default='.') + parser.add_argument("--samples-path", type=str, + default='mnist-sample-data') + parser.add_argument("--model-name", type=str, + default='mnist-pre-trained.pth') + args = parser.parse_args() + mnist_torch_inference_files(**vars(args)) diff --git a/use-cases/mnist/torch/dataloader.py b/use-cases/mnist/torch/dataloader.py index e4243763..a19c647e 100644 --- a/use-cases/mnist/torch/dataloader.py +++ b/use-cases/mnist/torch/dataloader.py @@ -34,7 +34,7 @@ def execute(self) -> Tuple[Dataset, Dataset]: transforms.Normalize((0.1307,), (0.3081,)) ])) print("Train and validation datasets loaded.") - return train_dataset, validation_dataset + return train_dataset, validation_dataset, None class InferenceMNIST(Dataset): diff --git a/use-cases/mnist/torch/inference-pipeline.yaml b/use-cases/mnist/torch/inference-pipeline.yaml deleted file mode 100644 index 5edf6ce9..00000000 --- a/use-cases/mnist/torch/inference-pipeline.yaml +++ /dev/null @@ -1,22 +0,0 @@ -pipeline: - class_path: itwinai.pipeline.Pipeline - init_args: - steps: - - class_path: dataloader.MNISTPredictLoader - init_args: - test_data_path: /usr/data/mnist-sample-data - - - class_path: itwinai.torch.inference.MulticlassTorchPredictor - init_args: - model: - class_path: itwinai.torch.inference.TorchModelLoader - init_args: - model_uri: mnist-pre-trained.pth - test_dataloader_kwargs: - batch_size: 3 - - - class_path: saver.TorchMNISTLabelSaver - init_args: - save_dir: /usr/data/mnist-predictions - predictions_file: predictions.csv - class_labels: null \ No newline at end of file diff --git a/use-cases/mnist/torch/pipeline.yaml b/use-cases/mnist/torch/pipeline.yaml deleted file mode 100644 index 99f35c73..00000000 --- a/use-cases/mnist/torch/pipeline.yaml +++ /dev/null @@ -1,56 +0,0 @@ -pipeline: - class_path: itwinai.pipeline.Pipeline - init_args: - steps: - dataloading_step: - class_path: dataloader.MNISTDataModuleTorch - init_args: - save_path: .tmp/ - - training_step: - class_path: itwinai.torch.trainer.TorchTrainerMG - init_args: - model: - class_path: model.Net - loss: - class_path: torch.nn.NLLLoss - init_args: - reduction: mean - optimizer_class: torch.optim.SGD - optimizer_kwargs: - lr: 0.001 - train_dataloader_kwargs: - batch_size: 32 - pin_memory: True - shuffle: True - validation_dataloader_kwargs: - batch_size: 32 - pin_memory: True - shuffle: False - epochs: 2 - train_metrics: - accuracy: - class_path: torchmetrics.classification.MulticlassAccuracy - init_args: - num_classes: 10 - precision: - class_path: torchmetrics.classification.MulticlassPrecision - init_args: - num_classes: 10 - recall: - class_path: torchmetrics.classification.MulticlassRecall - init_args: - num_classes: 10 - logger: - - class_path: itwinai.loggers.ConsoleLogger - - class_path: itwinai.loggers.MLFlowLogger - init_args: - experiment_name: MNIST classifier - log_freq: batch - strategy: ddp - checkpoint_every: 1 - cluster: - class_path: itwinai.torch.cluster.LocalCluster - init_args: - gpus: '0,1,2' - backend: nccl diff --git a/use-cases/mnist/torch/runall.sh b/use-cases/mnist/torch/runall.sh new file mode 100644 index 00000000..e81ed74d --- /dev/null +++ b/use-cases/mnist/torch/runall.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Python virtual environment (no conda/micromamba) +PYTHON_VENV="../../../envAI_hdfml" + +# Clear SLURM logs (*.out and *.err files) +rm -rf logs_slurm +mkdir logs_slurm +rm -rf logs_torchrun + +# DDP itwinai +DIST_MODE="ddp" +RUN_NAME="ddp-itwinai" +TRAINING_CMD="$PYTHON_VENV/bin/itwinai exec-pipeline --config config.yaml --pipe-key training_pipeline -o strategy=ddp" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh + +# DeepSpeed itwinai +DIST_MODE="deepspeed" +RUN_NAME="deepspeed-itwinai" +TRAINING_CMD="$PYTHON_VENV/bin/itwinai exec-pipeline --config config.yaml --pipe-key training_pipeline -o strategy=deepspeed" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh + +# Horovod itwinai +DIST_MODE="horovod" +RUN_NAME="horovod-itwinai" +TRAINING_CMD="$PYTHON_VENV/bin/itwinai exec-pipeline --config config.yaml --pipe-key training_pipeline -o strategy=horovod" +sbatch --export=ALL,DIST_MODE="$DIST_MODE",RUN_NAME="$RUN_NAME",TRAINING_CMD="$TRAINING_CMD",PYTHON_VENV="$PYTHON_VENV" \ + --job-name="$RUN_NAME-n$N" \ + --output="logs_slurm/job-$RUN_NAME-n$N.out" \ + --error="logs_slurm/job-$RUN_NAME-n$N.err" \ + slurm.sh \ No newline at end of file diff --git a/use-cases/mnist/torch/slurm.sh b/use-cases/mnist/torch/slurm.sh new file mode 100644 index 00000000..2a2a15d8 --- /dev/null +++ b/use-cases/mnist/torch/slurm.sh @@ -0,0 +1,116 @@ +#!/bin/bash + +# SLURM jobscript for JSC systems + +# Job configuration +#SBATCH --job-name=distributed_training +#SBATCH --account=intertwin +#SBATCH --mail-user= +#SBATCH --mail-type=ALL +#SBATCH --output=job.out +#SBATCH --error=job.err +#SBATCH --time=00:30:00 + +# Resources allocation +#SBATCH --partition=batch +#SBATCH --nodes=2 +#SBATCH --gpus-per-node=4 +#SBATCH --cpus-per-gpu=4 +#SBATCH --exclusive + +# gres options have to be disabled for deepv +#SBATCH --gres=gpu:4 + +# Load environment modules +ml Stages/2024 GCC OpenMPI CUDA/12 MPI-settings/CUDA Python HDF5 PnetCDF libaio mpi4py + +# Job info +echo "DEBUG: TIME: $(date)" +sysN="$(uname -n | cut -f2- -d.)" +sysN="${sysN%%[0-9]*}" +echo "Running on system: $sysN" +echo "DEBUG: EXECUTE: $EXEC" +echo "DEBUG: SLURM_SUBMIT_DIR: $SLURM_SUBMIT_DIR" +echo "DEBUG: SLURM_JOB_ID: $SLURM_JOB_ID" +echo "DEBUG: SLURM_JOB_NODELIST: $SLURM_JOB_NODELIST" +echo "DEBUG: SLURM_NNODES: $SLURM_NNODES" +echo "DEBUG: SLURM_NTASKS: $SLURM_NTASKS" +echo "DEBUG: SLURM_TASKS_PER_NODE: $SLURM_TASKS_PER_NODE" +echo "DEBUG: SLURM_SUBMIT_HOST: $SLURM_SUBMIT_HOST" +echo "DEBUG: SLURMD_NODENAME: $SLURMD_NODENAME" +echo "DEBUG: CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES" +if [ "$DEBUG" = true ] ; then + echo "DEBUG: NCCL_DEBUG=INFO" + export NCCL_DEBUG=INFO +fi +echo + +# Setup env for distributed ML +export CUDA_VISIBLE_DEVICES="0,1,2,3" +export OMP_NUM_THREADS=1 +if [ "$SLURM_CPUS_PER_GPU" -gt 0 ] ; then + export OMP_NUM_THREADS=$SLURM_CPUS_PER_GPU +fi + +# Env vairables check +if [ -z "$DIST_MODE" ]; then + >&2 echo "ERROR: env variable DIST_MODE is not set. Allowed values are 'horovod', 'ddp' or 'deepspeed'" + exit 1 +fi +if [ -z "$RUN_NAME" ]; then + >&2 echo "WARNING: env variable RUN_NAME is not set. It's a way to identify some specific run of an experiment." + RUN_NAME=$DIST_MODE +fi +if [ -z "$TRAINING_CMD" ]; then + >&2 echo "ERROR: env variable TRAINING_CMD is not set. It's the python command to execute." + exit 1 +fi +if [ -z "$PYTHON_VENV" ]; then + >&2 echo "WARNING: env variable PYTHON_VENV is not set. It's the path to a python virtual environment." +else + # Activate Python virtual env + source $PYTHON_VENV/bin/activate +fi + +# Get GPUs info per node +srun --cpu-bind=none --ntasks-per-node=1 bash -c 'echo -e "NODE hostname: $(hostname)\n$(nvidia-smi)\n\n"' + +# Launch training +if [ "$DIST_MODE" == "ddp" ] ; then + echo "DDP training: $TRAINING_CMD" + srun --cpu-bind=none --ntasks-per-node=1 \ + bash -c "torchrun \ + --log_dir='logs_torchrun' \ + --nnodes=$SLURM_NNODES \ + --nproc_per_node=$SLURM_GPUS_PER_NODE \ + --rdzv_id=$SLURM_JOB_ID \ + --rdzv_conf=is_host=\$(((SLURM_NODEID)) && echo 0 || echo 1) \ + --rdzv_backend=c10d \ + --rdzv_endpoint='$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)'i:29500 \ + $TRAINING_CMD" +elif [ "$DIST_MODE" == "deepspeed" ] ; then + echo "DEEPSPEED training: $TRAINING_CMD" + MASTER_ADDR=$(scontrol show hostnames "\$SLURM_JOB_NODELIST" | head -n 1)i + export MASTER_ADDR + export MASTER_PORT=29500 + + srun --cpu-bind=none --ntasks-per-node=$SLURM_GPUS_PER_NODE --cpus-per-task=$SLURM_CPUS_PER_GPU \ + $TRAINING_CMD + + # # Run with deepspeed launcher: set --ntasks-per-node=1 + # # https://www.deepspeed.ai/getting-started/#multi-node-environment-variables + # export NCCL_IB_DISABLE=1 + # export NCCL_SOCKET_IFNAME=eth0 + # nodelist=$(scontrol show hostname $SLURM_NODELIST) + # echo "$nodelist" | sed -e 's/$/ slots=4/' > .hostfile + # # Requires passwordless SSH access among compute node + # srun --cpu-bind=none deepspeed --hostfile=.hostfile $TRAINING_CMD --deepspeed + # rm .hostfile +elif [ "$DIST_MODE" == "horovod" ] ; then + echo "HOROVOD training: $TRAINING_CMD" + srun --cpu-bind=none --ntasks-per-node=$SLURM_GPUS_PER_NODE --cpus-per-task=$SLURM_CPUS_PER_GPU \ + $TRAINING_CMD +else + >&2 echo "ERROR: unrecognized \$DIST_MODE env variable" + exit 1 +fi diff --git a/use-cases/mnist/torch/train.py b/use-cases/mnist/torch/train.py deleted file mode 100644 index 97f53093..00000000 --- a/use-cases/mnist/torch/train.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Training pipeline. To run this script, use the following commands. - -On login node: - ->>> micromamba run -p ../../../.venv-pytorch/ \ - python train.py -p pipeline.yaml -d - -On compute nodes: - ->>> micromamba run -p ../../../.venv-pytorch/ \ - python train.py -p pipeline.yaml - -""" - -import argparse - -from itwinai.parser import ConfigParser - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "-p", "--pipeline", type=str, required=True, - help='Configuration file to the pipeline to execute.' - ) - parser.add_argument( - '-d', '--download-only', - action=argparse.BooleanOptionalAction, - default=False, - help=('Whether to download only the dataset and exit execution ' - '(suggested on login nodes of HPC systems)') - ) - args = parser.parse_args() - - # Create parser for the pipeline - pipe_parser = ConfigParser(config=args.pipeline) - pipeline = pipe_parser.parse_pipeline() - - if args.download_only: - print('Downloading datasets and exiting...') - pipeline = pipeline[:1] - - pipeline.execute()