pixr 0.1.0.dev0__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.
Files changed (69) hide show
  1. pixr/__init__.py +3 -0
  2. pixr/app/__init__.py +0 -0
  3. pixr/app/eval.py +95 -0
  4. pixr/app/train.py +192 -0
  5. pixr/data/__init__.py +2 -0
  6. pixr/data/loader.py +26 -0
  7. pixr/data/transforms.py +52 -0
  8. pixr/framework/__init__.py +7 -0
  9. pixr/framework/config.py +79 -0
  10. pixr/framework/device.py +176 -0
  11. pixr/framework/ema.py +85 -0
  12. pixr/framework/logger.py +32 -0
  13. pixr/framework/monitor.py +276 -0
  14. pixr/framework/random.py +15 -0
  15. pixr/framework/task.py +369 -0
  16. pixr/framework/trainer.py +397 -0
  17. pixr/generation.py +242 -0
  18. pixr/loss.py +128 -0
  19. pixr/models/__init__.py +11 -0
  20. pixr/models/attention.py +178 -0
  21. pixr/models/config.py +239 -0
  22. pixr/models/configs/cronut_b16_tiktoken.json +34 -0
  23. pixr/models/configs/cronut_s16_tiktoken.json +34 -0
  24. pixr/models/configs/cruller_hybrid_r50_base_1280.json +17 -0
  25. pixr/models/configs/cruller_naflex_clip_base.json +30 -0
  26. pixr/models/configs/cruller_naflex_clip_base_8k.json +30 -0
  27. pixr/models/configs/cruller_naflex_clip_base_ettin150m.json +31 -0
  28. pixr/models/configs/cruller_naflex_clip_large.json +30 -0
  29. pixr/models/configs/cruller_naflex_clip_large_ettin400m.json +31 -0
  30. pixr/models/configs/cruller_naflex_siglip2_base.json +14 -0
  31. pixr/models/configs/cruller_naflex_siglip2_base_ettin150m.json +15 -0
  32. pixr/models/configs/cruller_naflex_siglip2_base_qwen3_0.6b.json +16 -0
  33. pixr/models/configs/cruller_naflex_siglip2_so400m.json +14 -0
  34. pixr/models/configs/cruller_vit_clip_base_1280.json +20 -0
  35. pixr/models/configs/fritter_naflex_siglip2_base_ettin150m.json +18 -0
  36. pixr/models/configs/fritter_naflex_siglip2_base_ettin400m.json +18 -0
  37. pixr/models/configs/fritter_naflex_siglip2_base_qwen3_0.6b.json +18 -0
  38. pixr/models/configs/fritter_naflex_siglip2_so400m_ettin1b.json +18 -0
  39. pixr/models/cronut.py +389 -0
  40. pixr/models/cruller.py +124 -0
  41. pixr/models/factory.py +79 -0
  42. pixr/models/fritter.py +198 -0
  43. pixr/models/image_encoder_timm.py +181 -0
  44. pixr/models/kv_cache.py +112 -0
  45. pixr/models/param_groups.py +76 -0
  46. pixr/models/text_decoder.py +433 -0
  47. pixr/models/text_decoder_ettin.py +436 -0
  48. pixr/models/text_decoder_qwen3.py +352 -0
  49. pixr/task/__init__.py +4 -0
  50. pixr/task/common.py +212 -0
  51. pixr/task/docvqa.py +94 -0
  52. pixr/task/read.py +140 -0
  53. pixr/task/task_registry.py +19 -0
  54. pixr/tokenizers/__init__.py +3 -0
  55. pixr/tokenizers/config.py +8 -0
  56. pixr/tokenizers/tiktoken_tokenizer.py +152 -0
  57. pixr/tokenizers/tokenizer.py +45 -0
  58. pixr/utils/__init__.py +3 -0
  59. pixr/utils/cli.py +110 -0
  60. pixr/utils/file_utils.py +36 -0
  61. pixr/utils/json_utils.py +317 -0
  62. pixr/utils/metrics.py +65 -0
  63. pixr/utils/name_utils.py +11 -0
  64. pixr/version.py +1 -0
  65. pixr-0.1.0.dev0.dist-info/METADATA +349 -0
  66. pixr-0.1.0.dev0.dist-info/RECORD +69 -0
  67. pixr-0.1.0.dev0.dist-info/WHEEL +4 -0
  68. pixr-0.1.0.dev0.dist-info/entry_points.txt +6 -0
  69. pixr-0.1.0.dev0.dist-info/licenses/LICENSE +201 -0
pixr/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .models import Cruller, ModelCfg, ImageEncoderCfg, TextDecoderCfg, get_model_config, list_models
2
+ from .tokenizers import TokenizerCfg
3
+ from .version import __version__
pixr/app/__init__.py ADDED
File without changes
pixr/app/eval.py ADDED
@@ -0,0 +1,95 @@
1
+ """ pixr evaluation app
2
+
3
+ Example (DocVQA validation ANLS):
4
+ python -m pixr.app.eval \\
5
+ --task docvqa \\
6
+ --eval-data.source pixparse/docvqa-single-page-questions --eval-data.split validation \\
7
+ --eval-data.format hfds --eval-data.batch-size 32 \\
8
+ --checkpoint-path output/<experiment>/checkpoints/checkpoint-latest.pt
9
+ """
10
+ import json
11
+ import logging
12
+ import os
13
+ import re
14
+ import sys
15
+ from dataclasses import dataclass, field
16
+ from typing import Any, Optional
17
+
18
+ from chugr.common import DataArg, Serializable
19
+
20
+ from pixr.data import create_loader
21
+ from pixr.framework import DeviceEnv, destroy_distributed, setup_logging, random_seed
22
+ from pixr.task import get_task, get_task_cfgs
23
+ from pixr.utils import load_checkpoint
24
+ from pixr.utils.cli import parse_cfg
25
+
26
+ _logger = logging.getLogger('eval')
27
+
28
+
29
+ @dataclass
30
+ class EvalCfg(Serializable):
31
+ eval_data: DataArg = field(default_factory=lambda: DataArg(source=''))
32
+ task_name: str = 'docvqa' # selected via --task <name> (or task_name in a --config file)
33
+ task: Any = None # task config, type set by the selected task
34
+ checkpoint_path: str = ''
35
+ use_ema: bool = False # evaluate EMA weights if present in the checkpoint
36
+ output_dir: Optional[str] = None # default: <checkpoint dir>/eval
37
+ seed: int = 42
38
+
39
+
40
+ def _run(argv=None):
41
+ eval_cfg: EvalCfg = parse_cfg(
42
+ EvalCfg,
43
+ argv=argv,
44
+ task_cfgs=get_task_cfgs(),
45
+ default_task='docvqa',
46
+ description='pixr eval',
47
+ )
48
+ task_name = eval_cfg.task_name
49
+
50
+ device_env = DeviceEnv()
51
+ random_seed(eval_cfg.seed, rank=device_env.global_rank)
52
+ setup_logging(None)
53
+ if device_env.is_primary():
54
+ _logger.info('Eval config:\n' + eval_cfg.dumps_yaml())
55
+
56
+ assert eval_cfg.checkpoint_path, 'A checkpoint is required for evaluation (--checkpoint-path).'
57
+ task = get_task(task_name)(eval_cfg.task, device_env=device_env, checkpoint_path=eval_cfg.checkpoint_path)
58
+ if eval_cfg.use_ema:
59
+ checkpoint = load_checkpoint(eval_cfg.checkpoint_path)
60
+ assert 'model_ema' in checkpoint, 'Checkpoint has no EMA weights.'
61
+ task.trainable_module.load_state_dict(checkpoint['model_ema'])
62
+ task.to(device_env.device).eval()
63
+
64
+ loader = create_loader(
65
+ eval_cfg.eval_data,
66
+ task.eval_data_cfg(),
67
+ is_training=False,
68
+ world_size=device_env.world_size,
69
+ global_rank=device_env.global_rank,
70
+ local_rank=device_env.local_rank,
71
+ )
72
+ metrics = task.evaluate(loader.loader)
73
+
74
+ if device_env.is_primary():
75
+ _logger.info(f'{task_name} results: {metrics}')
76
+ output_dir = eval_cfg.output_dir or os.path.join(
77
+ os.path.dirname(os.path.abspath(eval_cfg.checkpoint_path)), 'eval')
78
+ split = re.sub(r'[^A-Za-z0-9_.+-]+', '_', eval_cfg.eval_data.split or 'eval').strip('_')
79
+ name = f'{task_name}-{split}'
80
+ task.save_results(output_dir, name=name)
81
+ with open(os.path.join(output_dir, f'{name}_metrics.json'), 'w') as f:
82
+ json.dump(metrics, f, indent=2)
83
+ _logger.info(f'Results written to {output_dir}')
84
+
85
+
86
+ def main(argv=None):
87
+ """ Entry point for both launch forms (`pixr-eval` console script, `python -m pixr.app.eval`). """
88
+ code = _run(argv)
89
+ # NOTE only after a normal return, w/ an exception other ranks may still be in collectives, the launcher tears down
90
+ destroy_distributed()
91
+ return code
92
+
93
+
94
+ if __name__ == '__main__':
95
+ sys.exit(main())
pixr/app/train.py ADDED
@@ -0,0 +1,192 @@
1
+ """ pixr training app
2
+
3
+ Example (read pretraining on IDL streamed from the HF hub, 2 GPUs):
4
+ python -m torch.distributed.run --nproc_per_node 2 -m pixr.app.train \\
5
+ --task read --task.model.name cruller_naflex_siglip2_base \\
6
+ --train-data.source "pipe:curl -s -f -L https://huggingface.co/datasets/pixparse/idl-wds/resolve/main/idl-train-0{0000..2999}.tar" \\
7
+ --train-data.num-samples 1000000 --train-data.batch-size 16 --train-data.num-workers 16 \\
8
+ --trainer.opt.learning-rate 2e-4
9
+ """
10
+ import logging
11
+ import os
12
+ import sys
13
+ from dataclasses import dataclass, field, replace
14
+ from datetime import datetime
15
+ from typing import Any, Dict, Optional
16
+
17
+ import torch
18
+
19
+ from chugr.common import DataArg, Serializable
20
+
21
+ from pixr.data import create_loader
22
+ from pixr.framework import DeviceEnv, destroy_distributed, Monitor, Trainer, TrainerCfg, setup_logging, random_seed
23
+ from pixr.task import get_task, get_task_cfgs
24
+ from pixr.utils import clean_name, get_latest_checkpoint
25
+ from pixr.utils.cli import parse_cfg
26
+
27
+ _logger = logging.getLogger('train')
28
+
29
+
30
+ @dataclass
31
+ class TrainCfg(Serializable):
32
+ train_data: DataArg = field(default_factory=lambda: DataArg(source=''))
33
+ eval_data: Optional[DataArg] = None
34
+ task_name: str = 'read' # selected via --task <name> (or task_name in a --config file)
35
+ task: Any = None # task config, type set by the selected task
36
+ trainer: TrainerCfg = field(default_factory=TrainerCfg)
37
+
38
+ experiment: Optional[str] = None # experiment name, auto-generated if None
39
+ output_dir: str = './output'
40
+ log_filename: str = 'out.log'
41
+ resume: str = '' # resume training from checkpoint w/ full train state ('latest' for most recent)
42
+ checkpoint_path: str = '' # initialize model weights (and tokens / arch) from this checkpoint
43
+ eval_frequency: int = 1 # evaluate every N intervals (if eval_data set)
44
+ eval_ema: bool = False # evaluate the EMA weights (if trainer.ema_decay set)
45
+ checkpoint_frequency: int = 1 # save checkpoint every N intervals (last interval always saved)
46
+ checkpoint_keep: int = 3 # number of most recent interval checkpoints to keep (0 == keep all)
47
+ seed: int = 42
48
+
49
+ wandb: bool = False
50
+ wandb_project: str = 'pixr'
51
+ tensorboard: bool = False
52
+ log_eval_data: bool = False
53
+
54
+
55
+ def _save_checkpoint(state: Dict[str, Any], checkpoint_dir: str, interval: int, keep: int):
56
+ path = os.path.join(checkpoint_dir, f'checkpoint-{interval}.pt')
57
+ tmp_path = path + '.tmp'
58
+ torch.save(state, tmp_path)
59
+ os.replace(tmp_path, path)
60
+ latest = os.path.join(checkpoint_dir, 'checkpoint-latest.pt')
61
+ if os.path.lexists(latest):
62
+ os.remove(latest)
63
+ os.symlink(os.path.basename(path), latest)
64
+ if keep > 0:
65
+ for f in os.listdir(checkpoint_dir):
66
+ stem = f[len('checkpoint-'):-3]
67
+ if f.startswith('checkpoint-') and stem.isdigit() and int(stem) <= interval - keep:
68
+ os.remove(os.path.join(checkpoint_dir, f))
69
+ return path
70
+
71
+
72
+ def _run(argv=None):
73
+ train_cfg: TrainCfg = parse_cfg(
74
+ TrainCfg,
75
+ argv=argv,
76
+ task_cfgs=get_task_cfgs(),
77
+ default_task='read',
78
+ description='pixr train',
79
+ )
80
+ task_name = train_cfg.task_name
81
+
82
+ device_env = DeviceEnv()
83
+ random_seed(train_cfg.seed, rank=device_env.global_rank)
84
+
85
+ if train_cfg.experiment is None:
86
+ date_str = datetime.now().strftime("%Y%m%d-%H%M%S")
87
+ if device_env.world_size > 1:
88
+ date_str = device_env.broadcast_object(date_str)
89
+ experiment = '-'.join([
90
+ date_str,
91
+ f"task_{task_name}",
92
+ f"model_{clean_name(train_cfg.task.model.name)}",
93
+ f"lr_{'{:.1e}'.format(train_cfg.trainer.opt.learning_rate)}",
94
+ f"b_{train_cfg.train_data.batch_size}",
95
+ ])
96
+ train_cfg = replace(train_cfg, experiment=experiment)
97
+
98
+ experiment_path = os.path.join(train_cfg.output_dir, train_cfg.experiment)
99
+ checkpoint_dir = os.path.join(experiment_path, 'checkpoints')
100
+ resume = train_cfg.resume
101
+ if resume == 'latest':
102
+ resume = get_latest_checkpoint(checkpoint_dir) if os.path.isdir(checkpoint_dir) else ''
103
+ resume = resume or ''
104
+ log_path = None
105
+ exists = False
106
+ if device_env.is_primary():
107
+ log_path = os.path.join(experiment_path, train_cfg.log_filename)
108
+ exists = os.path.exists(log_path) and not resume
109
+ if exists:
110
+ print(f"Experiment {train_cfg.experiment} already exists, set --resume or a new --experiment.")
111
+ else:
112
+ os.makedirs(checkpoint_dir, exist_ok=True)
113
+ train_cfg.save(os.path.join(experiment_path, 'config.yaml'))
114
+ if device_env.world_size > 1:
115
+ exists = device_env.broadcast_object(exists) # all ranks exit together
116
+ if exists:
117
+ return -1
118
+
119
+ setup_logging(log_path if device_env.is_primary() else None)
120
+ if not device_env.is_primary():
121
+ logging.root.setLevel(logging.WARNING)
122
+ _logger.info(f"Device env is {device_env}")
123
+ if device_env.is_primary():
124
+ _logger.info('Train config:\n' + train_cfg.dumps_yaml())
125
+
126
+ monitor = Monitor(
127
+ train_cfg.experiment,
128
+ output_dir=experiment_path,
129
+ wandb=train_cfg.wandb,
130
+ wandb_project=train_cfg.wandb_project,
131
+ tensorboard=train_cfg.tensorboard,
132
+ output_enabled=device_env.is_primary(),
133
+ log_eval_data=train_cfg.log_eval_data,
134
+ hparams=train_cfg.to_dict(),
135
+ )
136
+
137
+ task = get_task(task_name)(train_cfg.task, device_env=device_env, checkpoint_path=train_cfg.checkpoint_path)
138
+ trainer = Trainer(task, train_cfg.trainer, device_env=device_env, monitor=monitor)
139
+
140
+ assert train_cfg.train_data.source, 'Train dataset (--train-data.source) must be set.'
141
+ dist_kwargs = dict(
142
+ world_size=device_env.world_size,
143
+ global_rank=device_env.global_rank,
144
+ local_rank=device_env.local_rank,
145
+ )
146
+ train_loader = create_loader(
147
+ train_cfg.train_data, task.train_data_cfg(), is_training=True, seed=train_cfg.seed, **dist_kwargs)
148
+ eval_loader = None
149
+ if train_cfg.eval_data is not None and train_cfg.eval_data.source:
150
+ eval_loader = create_loader(train_cfg.eval_data, task.eval_data_cfg(), is_training=False, **dist_kwargs)
151
+
152
+ trainer.setup(
153
+ num_batches_per_interval=train_loader.num_batches,
154
+ resume=resume,
155
+ mean_batch_size=train_loader.mean_batch_size,
156
+ )
157
+ if device_env.is_primary():
158
+ num_params = sum(p.numel() for p in task.trainable_module.parameters())
159
+ _logger.info(f'Model params: {num_params / 1e6:.1f}M, steps per interval: {trainer.num_steps_per_interval}')
160
+
161
+ for interval in range(trainer.interval_idx, train_cfg.trainer.num_intervals):
162
+ train_loader.set_interval(interval)
163
+ results = {'train': trainer.train_interval(train_loader)}
164
+
165
+ last = interval + 1 == train_cfg.trainer.num_intervals
166
+ if eval_loader is not None and ((interval + 1) % train_cfg.eval_frequency == 0 or last):
167
+ # w/ FSDP, params are gathered for eval so each rank decodes its eval shard independently
168
+ with task.full_params(use_ema=train_cfg.eval_ema):
169
+ eval_metrics = task.evaluate(eval_loader.loader, use_ema=train_cfg.eval_ema)
170
+ results['eval'] = eval_metrics
171
+ monitor.log_phase('eval', interval=interval, **eval_metrics)
172
+
173
+ if device_env.is_primary():
174
+ monitor.write_summary(results, index=interval)
175
+ if (interval + 1) % train_cfg.checkpoint_frequency == 0 or last:
176
+ state = trainer.state_dict() # collective w/ FSDP
177
+ if device_env.is_primary():
178
+ path = _save_checkpoint(state, checkpoint_dir, interval, train_cfg.checkpoint_keep)
179
+ _logger.info(f'Saved checkpoint {path}')
180
+ del state
181
+
182
+
183
+ def main(argv=None):
184
+ """ Entry point for both launch forms (`pixr-train` console script, `python -m pixr.app.train`). """
185
+ code = _run(argv)
186
+ # NOTE only after a normal return, w/ an exception other ranks may still be in collectives, the launcher tears down
187
+ destroy_distributed()
188
+ return code
189
+
190
+
191
+ if __name__ == '__main__':
192
+ sys.exit(main())
pixr/data/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .loader import create_loader
2
+ from .transforms import create_image_preprocessor, ImagePreprocessorFactory
pixr/data/loader.py ADDED
@@ -0,0 +1,26 @@
1
+ from typing import Optional
2
+
3
+ import chugr
4
+ from chugr.common import DataArg, DataCfg, DataTaskCfg, DistributedCfg, LoaderBundle
5
+
6
+
7
+ def create_loader(
8
+ data: DataArg,
9
+ task_cfg: DataTaskCfg,
10
+ is_training: bool = False,
11
+ start_interval: int = 0,
12
+ seed: int = 0,
13
+ world_size: int = 1,
14
+ global_rank: int = 0,
15
+ local_rank: int = 0,
16
+ ) -> LoaderBundle:
17
+ """ Create a chugr loader (webdataset or HF datasets) for the given task data pipeline config. """
18
+ data_cfg = DataCfg.from_arg(data)
19
+ return chugr.create_loader(
20
+ data_cfg,
21
+ task_cfg,
22
+ is_training=is_training,
23
+ start_interval=start_interval,
24
+ seed=seed,
25
+ distributed=DistributedCfg(world_size=world_size, local_rank=local_rank, global_rank=global_rank),
26
+ )
@@ -0,0 +1,52 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+ import chugr
4
+ from chugr.common import ImageInputCfg
5
+
6
+
7
+ def create_image_preprocessor(
8
+ input_info: Dict[str, Any],
9
+ is_training: bool,
10
+ transform_type: Optional[str] = None,
11
+ max_seq_len: Optional[int] = None,
12
+ ):
13
+ """ Create image preprocessing for a model's image input requirements (see ImageEncoderTimm.input_info).
14
+
15
+ The same transform type is used for train and eval (eval disables augmentation), so input geometry
16
+ (resize, padding, patch sequence) always matches between training and evaluation.
17
+
18
+ Args:
19
+ input_info: model image input info
20
+ is_training: enable train augmentations
21
+ transform_type: chugr transform type, default 'doc_naflex' for NaFlex models, else 'doc_better'
22
+ """
23
+ naflex = input_info['naflex']
24
+ if transform_type is None:
25
+ transform_type = 'doc_naflex' if naflex else 'doc_better'
26
+ if naflex:
27
+ assert transform_type == 'doc_naflex', 'NaFlex models require doc_naflex transforms.'
28
+ cfg = ImageInputCfg(
29
+ size=input_info['image_size'],
30
+ mode=input_info['image_fmt'],
31
+ mean=input_info['mean'],
32
+ std=input_info['std'],
33
+ interpolation='bicubic',
34
+ fill_color=255,
35
+ transform_type=transform_type,
36
+ patch_size=input_info['patch_size'],
37
+ max_seq_len=max_seq_len or input_info['max_seq_len'],
38
+ )
39
+ return chugr.create_image_preprocessor(cfg, is_training=is_training)
40
+
41
+
42
+ class ImagePreprocessorFactory:
43
+ """ seq_len -> image preprocessor, for NaFlex bucketed batching (picklable for dataloader workers). """
44
+
45
+ def __init__(self, input_info: Dict[str, Any], is_training: bool, transform_type: Optional[str] = None):
46
+ self.input_info = dict(input_info)
47
+ self.is_training = is_training
48
+ self.transform_type = transform_type
49
+
50
+ def __call__(self, seq_len: int):
51
+ return create_image_preprocessor(
52
+ self.input_info, self.is_training, transform_type=self.transform_type, max_seq_len=seq_len)
@@ -0,0 +1,7 @@
1
+ from .config import OptimizationCfg, TaskCfg, TrainerCfg
2
+ from .device import DeviceEnv, DeviceEnvType, destroy_distributed
3
+ from .logger import setup_logging
4
+ from .monitor import Monitor
5
+ from .random import random_seed
6
+ from .task import TrainingTask, move_to_device
7
+ from .trainer import Trainer
@@ -0,0 +1,79 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, Optional, Tuple
3
+
4
+ from pixr.models import ModelArgs
5
+ from pixr.tokenizers import TokenizerCfg
6
+
7
+
8
+ @dataclass
9
+ class TaskCfg:
10
+ """ Base task config: model, tokenizer, preprocessing, and precision (used for training and eval). """
11
+ model: ModelArgs = field(default_factory=ModelArgs)
12
+ tokenizer: Optional[TokenizerCfg] = None # default == text decoder name (or checkpoint tokenizer)
13
+ image_transforms: Optional[str] = None # chugr transform type, default 'doc_naflex' or 'doc_better'
14
+ dtype: Optional[str] = 'bfloat16' # low precision dtype for autocast ('bfloat16' or 'float16')
15
+ amp: bool = True
16
+ label_smoothing: float = 0.
17
+ z_loss_weight: float = 0. # weight of the LM z-loss (mean logsumexp(logits) ** 2), e.g. 1e-4
18
+ # train text is trimmed to the longest sequence of the batch rounded up to this multiple, bounds the number of
19
+ # distinct text lengths (compile / FlexAttention mask shapes) for a few extra pad tokens, 1 disables rounding
20
+ text_pad_multiple: int = 64
21
+ loss_chunk_size: int = 4096 # fused (chunked) LM head + CE, tokens per chunk, 0 to materialize full logits
22
+
23
+
24
+ @dataclass
25
+ class OptimizationCfg:
26
+ optimizer: str = 'adamw'
27
+ scheduler: str = 'cosine'
28
+ learning_rate: float = 1e-4
29
+ warmup_learning_rate: float = 0.
30
+ min_learning_rate: float = 0.
31
+ weight_decay: float = .05
32
+ eps: float = 1e-8
33
+ clip_grad_value: Optional[float] = 1.0 # Gradient clipping value.
34
+ clip_grad_mode: Optional[str] = 'norm' # Gradient clipping mode, one of ('norm', 'value', 'agc').
35
+ grad_accum_steps: int = 1
36
+ momentum: Optional[float] = None
37
+ betas: Optional[Tuple[float, float]] = (0.9, 0.98)
38
+ # per component LR multipliers (components defined by the model's param_layer_map(): 'encoder' == image
39
+ # encoder, 'bridge' == cross-attention / prefix projector, 'decoder' == text decoder / LM trunk)
40
+ encoder_lr_scale: float = 1.0
41
+ bridge_lr_scale: float = 1.0
42
+ decoder_lr_scale: float = 1.0
43
+ # layer-wise LR decay within the encoder / decoder (scale = decay ** (top layer - layer id)), None disables
44
+ encoder_layer_decay: Optional[float] = None
45
+ decoder_layer_decay: Optional[float] = None
46
+ # extra multipliers by param name regex (re.search, first match wins), e.g. {'embed_tokens': 2.0}
47
+ lr_scale_overrides: Optional[Dict[str, float]] = None
48
+ # scale loss by batch size relative to the mean for variable batch size (NaFlex schedule) loaders,
49
+ # 'none', 'linear' (per-sample gradient weight constant) or 'sqrt'
50
+ loss_batch_scale: str = 'none'
51
+
52
+
53
+ @dataclass
54
+ class TrainerCfg:
55
+ """ Optimization loop config, independent of the task. """
56
+ num_intervals: int = 100
57
+ num_warmup_intervals: float = 1.
58
+ log_frequency: int = 100 # log every n steps
59
+ metrics_frequency: int = 1000 # calculate (expensive) task train metrics every n steps, 0 disables
60
+ opt: OptimizationCfg = field(default_factory=OptimizationCfg)
61
+ grad_checkpointing: bool = False
62
+ ema_decay: Optional[float] = None # enable EMA of model weights w/ this decay
63
+
64
+ # torch.compile, strategy (as per OpenCLIP --torchcompile-strategy):
65
+ # 'task': compile the task training forward (model + loss), after distributed wrapping
66
+ # 'model': compile the model before DDP wrapping
67
+ # 'blocks': compile blocks in place before DDP wrapping, grad checkpoint recompute stays in eager autograd
68
+ # (bounds compiled backward memory when whole-graph compile schedules checkpointed recomputes poorly)
69
+ # w/ FSDP, blocks are always compiled per block inside prepare_fsdp() (compile -> composable AC -> FSDP)
70
+ compile: bool = False
71
+ compile_strategy: str = 'task'
72
+ compile_backend: Optional[str] = None
73
+ compile_mode: Optional[str] = None
74
+ compile_dynamic: Optional[bool] = None
75
+
76
+ # FSDP2 (fully_shard) instead of DDP for distributed training (params / grads / optimizer state sharded)
77
+ fsdp: bool = False
78
+ fsdp_reshard_after_forward: bool = True # False keeps block params gathered from forward to backward
79
+ fsdp_offload_cpu: bool = False
@@ -0,0 +1,176 @@
1
+ """ Distributed training/validation utils
2
+
3
+ """
4
+ import os
5
+ from dataclasses import dataclass, field, InitVar
6
+ from enum import Enum
7
+ from typing import Union, Optional, List, Tuple
8
+
9
+ import torch
10
+ import torch.distributed as dist
11
+
12
+
13
+ def is_distributed_env():
14
+ if "WORLD_SIZE" in os.environ:
15
+ return int(os.environ["WORLD_SIZE"]) > 1
16
+ if "SLURM_NTASKS" in os.environ:
17
+ return int(os.environ["SLURM_NTASKS"]) > 1
18
+ return False
19
+
20
+
21
+ def world_info_from_env():
22
+ local_rank = 0
23
+ for v in (
24
+ "LOCAL_RANK",
25
+ "MPI_LOCALRANKID",
26
+ "SLURM_LOCALID",
27
+ "OMPI_COMM_WORLD_LOCAL_RANK",
28
+ ):
29
+ if v in os.environ:
30
+ local_rank = int(os.environ[v])
31
+ break
32
+
33
+ global_rank = 0
34
+ for v in ("RANK", "PMI_RANK", "SLURM_PROCID", "OMPI_COMM_WORLD_RANK"):
35
+ if v in os.environ:
36
+ global_rank = int(os.environ[v])
37
+ break
38
+
39
+ world_size = 1
40
+ for v in ("WORLD_SIZE", "PMI_SIZE", "SLURM_NTASKS", "OMPI_COMM_WORLD_SIZE"):
41
+ if v in os.environ:
42
+ world_size = int(os.environ[v])
43
+ break
44
+
45
+ return local_rank, global_rank, world_size
46
+
47
+
48
+ class DeviceEnvType(Enum):
49
+ """Device Environment Types"""
50
+
51
+ CPU = "cpu"
52
+ CUDA = "cuda"
53
+ XLA = "xla"
54
+
55
+
56
+ @dataclass
57
+ class DeviceEnv:
58
+ """
59
+ Represents the environment for device setup and distributed processing.
60
+
61
+ Attributes:
62
+ device (torch.device): Current device (e.g., CUDA GPU) that the model/data should reside in.
63
+ world_size (Optional[int]): Total number of processes involved in the distributed setup.
64
+ local_rank (Optional[int]): Rank of the current process on the current node.
65
+ global_rank (Optional[int]): Rank of the current process globally across all nodes.
66
+
67
+ Init Arguments:
68
+ init_device_type (Optional[str]): The type of device to initialize (e.g., 'cuda', 'cpu').
69
+ Defaults to None, which means it will be inferred.
70
+ init_device_index (Optional[int]): Index of the CUDA device to initialize if multiple are present.
71
+ Defaults to None, indicating the first available device.
72
+ init_dist_backend (str): Backend for distributed training. Default is 'nccl'.
73
+ init_dist_url (str): URL for initializing distributed training. Default is 'env://'.
74
+
75
+ Methods:
76
+ is_global_primary: Checks if current process has the global rank 0.
77
+ is_local_primary: Checks if current process has the local rank 0.
78
+ is_primary: Checks if current process is either a local or global primary.
79
+ broadcast_object: Broadcasts an object from a source process to all other processes.
80
+ all_gather_object: Gathers objects from all processes and returns them.
81
+ """
82
+
83
+ init_device_type: InitVar[Optional[str]] = None
84
+ init_device_index: InitVar[Optional[int]] = None
85
+ init_dist_backend: InitVar[str] = "nccl"
86
+ init_dist_url: InitVar[str] = "env://"
87
+
88
+ device: torch.device = field(
89
+ init=False
90
+ ) # set from device_type + device_index or post_init logic
91
+ world_size: Optional[int] = None # set by post_init from env when None
92
+ local_rank: Optional[int] = None # set by post_init from env when None
93
+ global_rank: Optional[int] = None # set by post_init from env when None
94
+
95
+ def is_global_primary(self):
96
+ return self.global_rank == 0
97
+
98
+ def is_local_primary(self):
99
+ return self.local_rank == 0
100
+
101
+ def is_primary(self, local=False):
102
+ return self.is_local_primary() if local else self.is_global_primary()
103
+
104
+ def __post_init__(
105
+ self,
106
+ init_device_type: Optional[str],
107
+ init_device_index: Optional[int],
108
+ init_dist_backend: str,
109
+ init_dist_url: str,
110
+ ):
111
+ # FIXME support different device types, just using cuda to start
112
+ assert torch.cuda.device_count()
113
+ torch.backends.cudnn.benchmark = True
114
+ torch.backends.cuda.matmul.allow_tf32 = True
115
+ init_local_rank, init_global_rank, init_world_size = world_info_from_env()
116
+ if init_world_size > 1:
117
+ # setup distributed
118
+ assert init_device_index is None
119
+ self.local_rank = int(init_local_rank)
120
+ is_slurm = "SLURM_PROCID" in os.environ
121
+ if "SLURM_PROCID" in os.environ:
122
+ # os.environ['LOCAL_RANK'] = str(init_local_rank)
123
+ # os.environ['RANK'] = str(init_global_rank)
124
+ # os.environ['WORLD_SIZE'] = str(init_world_size)
125
+ torch.distributed.init_process_group(
126
+ backend=init_dist_backend,
127
+ init_method=init_dist_url,
128
+ world_size=init_world_size,
129
+ rank=init_global_rank,
130
+ )
131
+ else:
132
+ torch.distributed.init_process_group(
133
+ backend=init_dist_backend,
134
+ init_method=init_dist_url,
135
+ )
136
+
137
+ self.world_size = torch.distributed.get_world_size()
138
+ self.global_rank = torch.distributed.get_rank()
139
+ if is_slurm:
140
+ assert self.world_size == init_world_size
141
+ assert self.global_rank == init_global_rank
142
+
143
+ self.device = torch.device("cuda:%d" % self.local_rank)
144
+ torch.cuda.set_device(self.local_rank)
145
+ else:
146
+ self.device = torch.device(
147
+ "cuda" if init_device_index is None else f"cuda:{init_device_index}"
148
+ )
149
+ self.local_rank = 0
150
+ self.world_size = 1
151
+ self.global_rank = 0
152
+
153
+ def broadcast_object(self, obj, src=0):
154
+ # broadcast a pickle-able python object from rank-0 to all ranks
155
+ if self.global_rank == src:
156
+ objects = [obj]
157
+ else:
158
+ objects = [None]
159
+ dist.broadcast_object_list(objects, src=src)
160
+ return objects[0]
161
+
162
+ def all_gather_object(self, obj, dst=0):
163
+ # gather a pickle-able python object across all ranks
164
+ objects = [None for _ in range(self.world_size)]
165
+ dist.all_gather_object(objects, obj)
166
+ return objects
167
+
168
+
169
+ def destroy_distributed():
170
+ """ Tear down the default process group (if any) before interpreter exit.
171
+
172
+ W/o this, the NCCL watchdog thread can still be querying events while CUDA unloads at exit, crashing a rank
173
+ ('driver shutting down', SIGSEGV) after a successful run.
174
+ """
175
+ if dist.is_available() and dist.is_initialized():
176
+ dist.destroy_process_group()