lightray 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lightray/__init__.py +1 -0
- lightray/callbacks.py +63 -0
- lightray/fs.py +43 -0
- lightray/tune.py +110 -0
- lightray/utils.py +277 -0
- lightray-0.1.0.dist-info/METADATA +96 -0
- lightray-0.1.0.dist-info/RECORD +8 -0
- lightray-0.1.0.dist-info/WHEEL +4 -0
lightray/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .tune import run
|
lightray/callbacks.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from botocore.exceptions import ClientError, ConnectTimeoutError
|
|
7
|
+
from ray import train
|
|
8
|
+
from ray.train import Checkpoint
|
|
9
|
+
from ray.train.lightning import RayTrainReportCallback
|
|
10
|
+
|
|
11
|
+
BOTO_RETRY_EXCEPTIONS = (ClientError, ConnectTimeoutError)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def report_with_retries(metrics, checkpoint, retries: int = 10):
|
|
15
|
+
"""
|
|
16
|
+
Call `train.report`, which will persist checkpoints to s3,
|
|
17
|
+
retrying after any possible errors
|
|
18
|
+
"""
|
|
19
|
+
for _ in range(retries):
|
|
20
|
+
try:
|
|
21
|
+
train.report(metrics=metrics, checkpoint=checkpoint)
|
|
22
|
+
break
|
|
23
|
+
except BOTO_RETRY_EXCEPTIONS:
|
|
24
|
+
time.sleep(5)
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TrainReportCallback(RayTrainReportCallback):
|
|
29
|
+
"""
|
|
30
|
+
Equivalent of the RayTrainReportCallback
|
|
31
|
+
(https://docs.ray.io/en/latest/train/api/doc/ray.train.lightning.RayTrainReportCallback.html)
|
|
32
|
+
except adds a retry mechanism to the `train.report` call
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def on_train_epoch_end(self, trainer, pl_module) -> None:
|
|
36
|
+
# Creates a checkpoint dir with fixed name
|
|
37
|
+
tmpdir = Path(
|
|
38
|
+
self.tmpdir_prefix, str(trainer.current_epoch)
|
|
39
|
+
).as_posix()
|
|
40
|
+
os.makedirs(tmpdir, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
# Fetch metrics
|
|
43
|
+
metrics = trainer.callback_metrics
|
|
44
|
+
metrics = {k: v.item() for k, v in metrics.items()}
|
|
45
|
+
|
|
46
|
+
# (Optional) Add customized metrics
|
|
47
|
+
metrics["epoch"] = trainer.current_epoch
|
|
48
|
+
metrics["step"] = trainer.global_step
|
|
49
|
+
|
|
50
|
+
# Save checkpoint to local
|
|
51
|
+
ckpt_path = Path(tmpdir, self.CHECKPOINT_NAME).as_posix()
|
|
52
|
+
trainer.save_checkpoint(ckpt_path, weights_only=False)
|
|
53
|
+
|
|
54
|
+
# Report to train session
|
|
55
|
+
checkpoint = Checkpoint.from_directory(tmpdir)
|
|
56
|
+
report_with_retries(metrics=metrics, checkpoint=checkpoint)
|
|
57
|
+
|
|
58
|
+
# Add a barrier to ensure all workers finished reporting here
|
|
59
|
+
trainer.strategy.barrier()
|
|
60
|
+
|
|
61
|
+
if self.local_rank == 0:
|
|
62
|
+
shutil.rmtree(tmpdir)
|
|
63
|
+
shutil.rmtree(tmpdir)
|
lightray/fs.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import pyarrow
|
|
5
|
+
import s3fs
|
|
6
|
+
|
|
7
|
+
# s3 retry configuration
|
|
8
|
+
retry_config = {"retries": {"total_max_attempts": 10, "mode": "adaptive"}}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_filesystem(
|
|
12
|
+
storage_dir: Optional[str] = None,
|
|
13
|
+
) -> tuple[
|
|
14
|
+
Optional[pyarrow.fs.PyFileSystem], Optional[pyarrow.fs.PyFileSystem]
|
|
15
|
+
]:
|
|
16
|
+
internal_fs = external_fs = None
|
|
17
|
+
if storage_dir and storage_dir.startswith("s3://"):
|
|
18
|
+
storage_dir = storage_dir.removeprefix("s3://")
|
|
19
|
+
endpoint_url = os.getenv("AWS_ENDPOINT_URL")
|
|
20
|
+
# directly use s3 instead of rays pyarrow s3 default due to
|
|
21
|
+
# this issue https://github.com/ray-project/ray/issues/41137
|
|
22
|
+
internal_fs = s3fs.S3FileSystem(
|
|
23
|
+
key=os.getenv("AWS_ACCESS_KEY_ID"),
|
|
24
|
+
secret=os.getenv("AWS_SECRET_ACCESS_KEY"),
|
|
25
|
+
endpoint_url=endpoint_url,
|
|
26
|
+
config_kwargs=retry_config,
|
|
27
|
+
)
|
|
28
|
+
internal_fs = pyarrow.fs.PyFileSystem(
|
|
29
|
+
pyarrow.fs.FSSpecHandler(internal_fs)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
external_fs = s3fs.S3FileSystem(
|
|
33
|
+
key=os.getenv("AWS_ACCESS_KEY_ID"),
|
|
34
|
+
secret=os.getenv("AWS_SECRET_ACCESS_KEY"),
|
|
35
|
+
endpoint_url=os.getenv(
|
|
36
|
+
"AWS_EXTERNAL_ENDPOINT_URL", os.getenv("AWS_ENDPOINT_URL")
|
|
37
|
+
),
|
|
38
|
+
config_kwargs=retry_config,
|
|
39
|
+
)
|
|
40
|
+
external_fs = pyarrow.fs.PyFileSystem(
|
|
41
|
+
pyarrow.fs.FSSpecHandler(external_fs)
|
|
42
|
+
)
|
|
43
|
+
return internal_fs, external_fs
|
lightray/tune.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, Literal, Optional, Type, Union
|
|
5
|
+
|
|
6
|
+
import lightning.pytorch as pl
|
|
7
|
+
import ray
|
|
8
|
+
from ray import tune
|
|
9
|
+
from ray.tune.schedulers import TrialScheduler
|
|
10
|
+
|
|
11
|
+
from lightray import utils
|
|
12
|
+
from lightray.fs import setup_filesystem
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from lightning.pytorch.cli import LightningCLI
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run(
|
|
19
|
+
cli_cls: Type["LightningCLI"],
|
|
20
|
+
name: str,
|
|
21
|
+
metric_name: str,
|
|
22
|
+
objective: Literal["min", "max"],
|
|
23
|
+
search_space: Union[dict, Path],
|
|
24
|
+
scheduler: TrialScheduler,
|
|
25
|
+
storage_dir: Optional[str] = None,
|
|
26
|
+
address: Optional[str] = None,
|
|
27
|
+
num_samples: int = 10,
|
|
28
|
+
workers_per_trial: int = 1,
|
|
29
|
+
gpus_per_worker: float = 1.0,
|
|
30
|
+
cpus_per_gpu: float = 1.0,
|
|
31
|
+
callbacks: Optional[list[pl.callbacks.Callback]] = None,
|
|
32
|
+
temp_dir: Optional[str] = None,
|
|
33
|
+
args: Optional[list[str]] = None,
|
|
34
|
+
) -> tune.ResultGrid:
|
|
35
|
+
# parse the training configuration file, and
|
|
36
|
+
# any argument overrides
|
|
37
|
+
# using the user passed LightningCLI class;
|
|
38
|
+
config = utils.parse_args(cli_cls, args)
|
|
39
|
+
|
|
40
|
+
# if specified, connect to a running ray cluster
|
|
41
|
+
# otherwise, ray will assume one is running locally
|
|
42
|
+
logging.info("Initializing Ray")
|
|
43
|
+
if address is not None:
|
|
44
|
+
logging.info(f"Connecting to Ray cluster at {address}")
|
|
45
|
+
# ensure the ray adress starts with "ray://"
|
|
46
|
+
if not address.startswith("ray://"):
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Address must start with 'ray://', got {address}"
|
|
49
|
+
)
|
|
50
|
+
ray.init(address, _temp_dir=temp_dir)
|
|
51
|
+
|
|
52
|
+
logging.info("Initializing checkpoint storage filesystems")
|
|
53
|
+
internal_fs, external_fs = setup_filesystem(str(storage_dir))
|
|
54
|
+
|
|
55
|
+
# construct the function that will actually
|
|
56
|
+
# execute the training loop, and then set it
|
|
57
|
+
# up for Ray to distribute it over our cluster,
|
|
58
|
+
# with the desired number of resources allocated
|
|
59
|
+
# to each running version of the job
|
|
60
|
+
train_func = utils.configure_deployment(
|
|
61
|
+
utils.TrainFunc(cli_cls, name, config, callbacks),
|
|
62
|
+
metric_name=metric_name,
|
|
63
|
+
workers_per_trial=workers_per_trial,
|
|
64
|
+
gpus_per_worker=gpus_per_worker,
|
|
65
|
+
cpus_per_gpu=cpus_per_gpu,
|
|
66
|
+
objective=objective,
|
|
67
|
+
storage_dir=storage_dir or None,
|
|
68
|
+
fs=internal_fs,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
search_space = utils.get_search_space(search_space)
|
|
72
|
+
|
|
73
|
+
# restore from a previous tuning run
|
|
74
|
+
path = os.path.join(storage_dir, name)
|
|
75
|
+
if tune.Tuner.can_restore(path, storage_filesystem=external_fs):
|
|
76
|
+
logging.info(f"Restoring from previous tuning run at {path}")
|
|
77
|
+
tuner = tune.Tuner.restore(
|
|
78
|
+
path,
|
|
79
|
+
train_func,
|
|
80
|
+
resume_errored=True,
|
|
81
|
+
storage_filesystem=external_fs,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
else:
|
|
85
|
+
tuner = tune.Tuner(
|
|
86
|
+
train_func,
|
|
87
|
+
param_space={"train_loop_config": search_space},
|
|
88
|
+
tune_config=tune.TuneConfig(
|
|
89
|
+
metric=metric_name,
|
|
90
|
+
mode=objective,
|
|
91
|
+
num_samples=num_samples,
|
|
92
|
+
scheduler=scheduler,
|
|
93
|
+
reuse_actors=True,
|
|
94
|
+
trial_name_creator=lambda trial: f"{trial.trial_id}",
|
|
95
|
+
trial_dirname_creator=lambda trial: f"{trial.trial_id}",
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
logging.info("Starting tune job")
|
|
100
|
+
results = tuner.fit()
|
|
101
|
+
|
|
102
|
+
return results
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cli():
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
cli()
|
lightray/utils.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hyperparameter tuning utilities based largely on this tutorial
|
|
3
|
+
https://docs.ray.io/en/latest/tune/examples/tune-pytorch-lightning.html
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
import math
|
|
9
|
+
import os
|
|
10
|
+
from tempfile import NamedTemporaryFile
|
|
11
|
+
from typing import Any, List, Optional, Type, Union
|
|
12
|
+
|
|
13
|
+
import lightning.pytorch as pl
|
|
14
|
+
import pyarrow.fs
|
|
15
|
+
import yaml
|
|
16
|
+
from lightning.pytorch.cli import LightningCLI
|
|
17
|
+
from ray import train
|
|
18
|
+
from ray.train import CheckpointConfig, FailureConfig, RunConfig, ScalingConfig
|
|
19
|
+
from ray.train.lightning import (
|
|
20
|
+
RayDDPStrategy,
|
|
21
|
+
RayLightningEnvironment,
|
|
22
|
+
prepare_trainer,
|
|
23
|
+
)
|
|
24
|
+
from ray.train.torch import TorchTrainer
|
|
25
|
+
|
|
26
|
+
from lightray.callbacks import RayTrainReportCallback
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_host_cli(cli: Type[LightningCLI]):
|
|
30
|
+
"""
|
|
31
|
+
Return a LightningCLI class that will utilize
|
|
32
|
+
the `cli` class passed as a parent class
|
|
33
|
+
for parsing arguments.
|
|
34
|
+
|
|
35
|
+
Since this is run on the client,
|
|
36
|
+
we don't actually want to do anything with the arguments we parse,
|
|
37
|
+
just record them, so override the couple parent
|
|
38
|
+
methods responsible for instantiating classes and running
|
|
39
|
+
subcommands.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
class HostCLI(cli):
|
|
43
|
+
def instantiate_classes(self):
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
def _run_subcommand(self):
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
return HostCLI
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_worker_cli(
|
|
53
|
+
cli: Type[LightningCLI],
|
|
54
|
+
callbacks: Optional[List[pl.callbacks.Callback]] = None,
|
|
55
|
+
):
|
|
56
|
+
"""
|
|
57
|
+
Return a LightningCLI class that will actually execute
|
|
58
|
+
training runs on worker nodes
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
callbacks = callbacks or [RayTrainReportCallback()]
|
|
62
|
+
|
|
63
|
+
class WorkerCLI(cli):
|
|
64
|
+
def instantiate_trainer(self, **kwargs):
|
|
65
|
+
kwargs = kwargs | dict(
|
|
66
|
+
enable_progress_bar=False,
|
|
67
|
+
devices="auto",
|
|
68
|
+
accelerator="auto",
|
|
69
|
+
strategy=RayDDPStrategy(),
|
|
70
|
+
callbacks=callbacks,
|
|
71
|
+
plugins=[RayLightningEnvironment()],
|
|
72
|
+
)
|
|
73
|
+
return super().instantiate_trainer(**kwargs)
|
|
74
|
+
|
|
75
|
+
return WorkerCLI
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_search_space(search_space: Union[str, dict]) -> dict[str, callable]:
|
|
79
|
+
if isinstance(search_space, dict):
|
|
80
|
+
return search_space
|
|
81
|
+
|
|
82
|
+
# determine if the path is a file path or a module path
|
|
83
|
+
if os.path.isfile(search_space):
|
|
84
|
+
# load the module from the file
|
|
85
|
+
module_name = os.path.splitext(os.path.basename(search_space))[0]
|
|
86
|
+
spec = importlib.util.spec_from_file_location(
|
|
87
|
+
module_name, search_space
|
|
88
|
+
)
|
|
89
|
+
module = importlib.util.module_from_spec(spec)
|
|
90
|
+
spec.loader.exec_module(module)
|
|
91
|
+
else:
|
|
92
|
+
# load the module using importlib
|
|
93
|
+
module = importlib.import_module(search_space)
|
|
94
|
+
|
|
95
|
+
# try to get the 'space' attribute from the module
|
|
96
|
+
try:
|
|
97
|
+
space = module.space
|
|
98
|
+
except AttributeError:
|
|
99
|
+
raise ValueError(f"Module {module.__name__} has no space dictionary")
|
|
100
|
+
|
|
101
|
+
if not isinstance(space, dict):
|
|
102
|
+
raise TypeError(
|
|
103
|
+
"Expected search space in module {} to be "
|
|
104
|
+
"a dictionary, found {}".format(module.__name__, type(space))
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return space
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def stop_on_nan(trial_id: str, result: dict) -> bool:
|
|
111
|
+
return math.isnan(result["train_loss"])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TrainFunc:
|
|
115
|
+
"""
|
|
116
|
+
Callable wrapper that takes a `LightningCLI` and executes
|
|
117
|
+
it with the both the `config` passed here at initialization
|
|
118
|
+
time as well as the arguments supplied by a particular
|
|
119
|
+
hyperparameter config. Meant for execution on workers during
|
|
120
|
+
tuning run, which expect a callable that a particular
|
|
121
|
+
hyperparameter config as its only argument.
|
|
122
|
+
|
|
123
|
+
All runs of the function will be given the same
|
|
124
|
+
Weights & Biases group name of `name` for tracking.
|
|
125
|
+
The names of individual runs in this group will be
|
|
126
|
+
randomly chosen by W&B.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
cli: Type[LightningCLI],
|
|
132
|
+
name: str,
|
|
133
|
+
config: dict,
|
|
134
|
+
callbacks: Optional[list[pl.callbacks.Callback]] = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
self.cli = cli
|
|
137
|
+
self.name = name
|
|
138
|
+
self.config = config
|
|
139
|
+
self.callbacks = callbacks
|
|
140
|
+
|
|
141
|
+
def validate_logger(self, args):
|
|
142
|
+
try:
|
|
143
|
+
logger = self.config["trainer"]["logger"]
|
|
144
|
+
except KeyError:
|
|
145
|
+
logger = None
|
|
146
|
+
|
|
147
|
+
if logger is not None:
|
|
148
|
+
if "WandbLogger" not in logger:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
"Only W&B logging is supported for distributed tuning"
|
|
151
|
+
)
|
|
152
|
+
args.append(f"--trainer.logger.group={self.name}")
|
|
153
|
+
return args
|
|
154
|
+
|
|
155
|
+
def __call__(self, hparams: dict[str, Any]):
|
|
156
|
+
"""
|
|
157
|
+
Dump the config to a file, then parse it
|
|
158
|
+
along with the hyperparameter configuration
|
|
159
|
+
passed here using our CLI.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
with NamedTemporaryFile(mode="w") as f:
|
|
163
|
+
# dump the core config,
|
|
164
|
+
# then add the hyperparameters
|
|
165
|
+
yaml.dump(self.config, f)
|
|
166
|
+
args = ["-c", f.name]
|
|
167
|
+
for key, value in hparams.items():
|
|
168
|
+
args.append(f"--{key}={value}")
|
|
169
|
+
|
|
170
|
+
args = self.validate_logger(args)
|
|
171
|
+
|
|
172
|
+
cli_cls = get_worker_cli(self.cli, self.callbacks)
|
|
173
|
+
cli = cli_cls(
|
|
174
|
+
run=False, args=args, save_config_kwargs={"overwrite": True}
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
log_dir = cli.trainer.logger.log_dir or cli.trainer.logger.save_dir
|
|
178
|
+
if not log_dir.startswith("s3://"):
|
|
179
|
+
ckpt_prefix = ""
|
|
180
|
+
else:
|
|
181
|
+
ckpt_prefix = "s3://"
|
|
182
|
+
|
|
183
|
+
# restore from checkpoint if available
|
|
184
|
+
checkpoint = train.get_checkpoint()
|
|
185
|
+
ckpt_path = None
|
|
186
|
+
if checkpoint:
|
|
187
|
+
ckpt_path = os.path.join(
|
|
188
|
+
ckpt_prefix, checkpoint.path, "checkpoint.ckpt"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
trainer = prepare_trainer(cli.trainer)
|
|
192
|
+
trainer.fit(cli.model, cli.datamodule, ckpt_path=ckpt_path)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def configure_deployment(
|
|
196
|
+
train_func: TrainFunc,
|
|
197
|
+
metric_name: str,
|
|
198
|
+
workers_per_trial: int,
|
|
199
|
+
gpus_per_worker: int,
|
|
200
|
+
cpus_per_gpu: int,
|
|
201
|
+
objective: str = "max",
|
|
202
|
+
storage_dir: Optional[str] = None,
|
|
203
|
+
fs: Optional[pyarrow.fs.FileSystem] = None,
|
|
204
|
+
) -> TorchTrainer:
|
|
205
|
+
"""
|
|
206
|
+
Set up a training function that can be distributed
|
|
207
|
+
among the workers in a ray cluster.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
train_func:
|
|
211
|
+
Function that each worker will execute
|
|
212
|
+
with a config specifying the hyperparameter
|
|
213
|
+
configuration for that trial.
|
|
214
|
+
metric_name:
|
|
215
|
+
Name of the metric that will be optimized
|
|
216
|
+
during the hyperparameter search
|
|
217
|
+
workers_per_trial:
|
|
218
|
+
Number of training workers to deploy
|
|
219
|
+
gpus_per_worker:
|
|
220
|
+
Number of GPUs to train over within each worker
|
|
221
|
+
cpus_per_gpu:
|
|
222
|
+
Number of CPUs to attach to each GPU. If gpus_per_worker
|
|
223
|
+
is 0, this is interpretated as the number of CPUs per worker
|
|
224
|
+
objective:
|
|
225
|
+
`"max"` or `"min"`, indicating how the indicated
|
|
226
|
+
metric ought to be optimized
|
|
227
|
+
storage_dir:
|
|
228
|
+
Directory to save ray checkpoints and logs
|
|
229
|
+
during training.
|
|
230
|
+
fs: Filesystem to use for storage
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
if gpus_per_worker == 0:
|
|
234
|
+
cpus_per_worker = cpus_per_gpu
|
|
235
|
+
else:
|
|
236
|
+
cpus_per_worker = cpus_per_gpu * gpus_per_worker
|
|
237
|
+
|
|
238
|
+
use_gpu = gpus_per_worker > 0
|
|
239
|
+
scaling_config = ScalingConfig(
|
|
240
|
+
trainer_resources={"CPU": 0},
|
|
241
|
+
resources_per_worker={"CPU": cpus_per_worker, "GPU": gpus_per_worker},
|
|
242
|
+
num_workers=workers_per_trial,
|
|
243
|
+
use_gpu=use_gpu,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
run_config = RunConfig(
|
|
247
|
+
checkpoint_config=CheckpointConfig(
|
|
248
|
+
num_to_keep=2,
|
|
249
|
+
checkpoint_score_attribute=metric_name,
|
|
250
|
+
checkpoint_score_order=objective,
|
|
251
|
+
),
|
|
252
|
+
failure_config=FailureConfig(
|
|
253
|
+
max_failures=5,
|
|
254
|
+
),
|
|
255
|
+
storage_filesystem=fs,
|
|
256
|
+
storage_path=storage_dir,
|
|
257
|
+
name=train_func.name,
|
|
258
|
+
stop=stop_on_nan,
|
|
259
|
+
)
|
|
260
|
+
return TorchTrainer(
|
|
261
|
+
train_func,
|
|
262
|
+
scaling_config=scaling_config,
|
|
263
|
+
run_config=run_config,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def parse_args(
|
|
268
|
+
cli_cls: Type[LightningCLI], args: Optional[list[str]] = None
|
|
269
|
+
) -> dict:
|
|
270
|
+
"""
|
|
271
|
+
Use a `LightningCLI` class to parse command line arguments
|
|
272
|
+
"""
|
|
273
|
+
host_cli = get_host_cli(cli_cls)
|
|
274
|
+
host_cli = host_cli(run=False, args=args)
|
|
275
|
+
config = host_cli.parser.dump(host_cli.config, format="yaml")
|
|
276
|
+
config = yaml.safe_load(config)
|
|
277
|
+
return config
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: lightray
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Distribute a LightningCLI hyperparameter search with Ray Tune
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Ethan Jacob Marx
|
|
7
|
+
Author-email: ethan.marx@ligo.org
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Dist: jsonargparse[signatures] (>=4.29,<5.0)
|
|
16
|
+
Requires-Dist: lightning (>=2.4.0,<3.0.0)
|
|
17
|
+
Requires-Dist: pyarrow (>=17.0.0,<18.0.0)
|
|
18
|
+
Requires-Dist: ray[default,tune] (>=2.8.0,<3.0.0)
|
|
19
|
+
Requires-Dist: s3fs (>=2024.9.0,<2025.0.0)
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# lightray
|
|
23
|
+
Easily integrate a `LightningCLI` with `RayTune` hyperparameter optimization
|
|
24
|
+
|
|
25
|
+
## Getting Started
|
|
26
|
+
Extend your custom `LightningCLI` to a `Trainable` compatible with `RayTune`.
|
|
27
|
+
|
|
28
|
+
Imagine you have a `LightningCLI` parser for a
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from lightning.pytorch as pl
|
|
32
|
+
|
|
33
|
+
class CustomCLI(pl.cli.LightningCLI):
|
|
34
|
+
def add_arguments_to_parser(self, parser):
|
|
35
|
+
parser.link_arguments(
|
|
36
|
+
"data.init_args.parameter",
|
|
37
|
+
"model.init_args.parameter",
|
|
38
|
+
apply_on="parse"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
class DataModule(pl.LightningDataModule):
|
|
42
|
+
def __init__(self, hidden_dim: int, learning_rate: float, parameter: int):
|
|
43
|
+
self.parameter = parameter
|
|
44
|
+
self.hidden_dim = hidden_dim
|
|
45
|
+
self.learning_rate = learning_rate
|
|
46
|
+
|
|
47
|
+
def train_dataloader(self):
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
class DataModule(pl.LightningModule):
|
|
51
|
+
def __init__(self, parameter: int):
|
|
52
|
+
self.parameter = parameter
|
|
53
|
+
|
|
54
|
+
def training_step(self):
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Launching a hyperparameter tuning job with `RayTune` using this `LightningCLI` is as simple as
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from ray import tune
|
|
63
|
+
from raylightning import run
|
|
64
|
+
|
|
65
|
+
# define search space
|
|
66
|
+
search_space = {
|
|
67
|
+
"model.init_args.hidden_dim": tune.choice([32, 64])
|
|
68
|
+
"model.init_args.learning_rate": tune.loguniform(1e-4, 1e-2)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# define scheduler
|
|
72
|
+
scheduler = ASHAScheduler(max_t=50, grace_period=1, reduction_factor=2)
|
|
73
|
+
|
|
74
|
+
# pass command line style arguments as if you were
|
|
75
|
+
# running your `LightningCLI` from the command line
|
|
76
|
+
args = ["--config", "/path/to/config.yaml"]
|
|
77
|
+
results = run(
|
|
78
|
+
args=args
|
|
79
|
+
cli_cls=simple_cli,
|
|
80
|
+
name="tune-test",
|
|
81
|
+
metric="val_loss",
|
|
82
|
+
objective="min",
|
|
83
|
+
search_space=search_space,
|
|
84
|
+
scheduler=scheduler,
|
|
85
|
+
storage_dir=storage_dir,
|
|
86
|
+
address=None,
|
|
87
|
+
num_samples=num_samples,
|
|
88
|
+
workers_per_trial=1,
|
|
89
|
+
gpus_per_worker=1.0,
|
|
90
|
+
cpus_per_gpu=4.0,
|
|
91
|
+
temp_dir=None,
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
s3 storage works out of the box. Make sure you have set the `AWS_ENDPOINT_URL`, `AWS_ACCESS_KEY_ID`, and `AWS_SECRET_ACCESS_KEY` environment variables set. Then, simply pass an s3 path (e.g. `s3://{bucket}/{folder}` to the `storage_dir` argument.
|
|
96
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
lightray/__init__.py,sha256=JgTN3Hzek3t8ejv76E8ffQSuptc3PYtEHjTKinsc9ks,22
|
|
2
|
+
lightray/callbacks.py,sha256=YaTNoz-KP1hS2ZjWs09_0De1SCT-f10-u5NMN_Otivw,2024
|
|
3
|
+
lightray/fs.py,sha256=3WTWtWue8L1rFNusmtJTNrnRQWzFYXJHlf4yjN9Ewgc,1461
|
|
4
|
+
lightray/tune.py,sha256=xmDj6mdKe1xjOY6ya5USWsTJKx9_iSDlux56Z232734,3409
|
|
5
|
+
lightray/utils.py,sha256=qNtWoReHTVSCQJ5hUHJGcci_VmwJnBVyHJ7phHb6pOk,8407
|
|
6
|
+
lightray-0.1.0.dist-info/METADATA,sha256=6onMoD9_uafK11YnUctjFYetjRTF7uAv3Eo9YlPKLpI,2919
|
|
7
|
+
lightray-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
8
|
+
lightray-0.1.0.dist-info/RECORD,,
|