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.
- pixr/__init__.py +3 -0
- pixr/app/__init__.py +0 -0
- pixr/app/eval.py +95 -0
- pixr/app/train.py +192 -0
- pixr/data/__init__.py +2 -0
- pixr/data/loader.py +26 -0
- pixr/data/transforms.py +52 -0
- pixr/framework/__init__.py +7 -0
- pixr/framework/config.py +79 -0
- pixr/framework/device.py +176 -0
- pixr/framework/ema.py +85 -0
- pixr/framework/logger.py +32 -0
- pixr/framework/monitor.py +276 -0
- pixr/framework/random.py +15 -0
- pixr/framework/task.py +369 -0
- pixr/framework/trainer.py +397 -0
- pixr/generation.py +242 -0
- pixr/loss.py +128 -0
- pixr/models/__init__.py +11 -0
- pixr/models/attention.py +178 -0
- pixr/models/config.py +239 -0
- pixr/models/configs/cronut_b16_tiktoken.json +34 -0
- pixr/models/configs/cronut_s16_tiktoken.json +34 -0
- pixr/models/configs/cruller_hybrid_r50_base_1280.json +17 -0
- pixr/models/configs/cruller_naflex_clip_base.json +30 -0
- pixr/models/configs/cruller_naflex_clip_base_8k.json +30 -0
- pixr/models/configs/cruller_naflex_clip_base_ettin150m.json +31 -0
- pixr/models/configs/cruller_naflex_clip_large.json +30 -0
- pixr/models/configs/cruller_naflex_clip_large_ettin400m.json +31 -0
- pixr/models/configs/cruller_naflex_siglip2_base.json +14 -0
- pixr/models/configs/cruller_naflex_siglip2_base_ettin150m.json +15 -0
- pixr/models/configs/cruller_naflex_siglip2_base_qwen3_0.6b.json +16 -0
- pixr/models/configs/cruller_naflex_siglip2_so400m.json +14 -0
- pixr/models/configs/cruller_vit_clip_base_1280.json +20 -0
- pixr/models/configs/fritter_naflex_siglip2_base_ettin150m.json +18 -0
- pixr/models/configs/fritter_naflex_siglip2_base_ettin400m.json +18 -0
- pixr/models/configs/fritter_naflex_siglip2_base_qwen3_0.6b.json +18 -0
- pixr/models/configs/fritter_naflex_siglip2_so400m_ettin1b.json +18 -0
- pixr/models/cronut.py +389 -0
- pixr/models/cruller.py +124 -0
- pixr/models/factory.py +79 -0
- pixr/models/fritter.py +198 -0
- pixr/models/image_encoder_timm.py +181 -0
- pixr/models/kv_cache.py +112 -0
- pixr/models/param_groups.py +76 -0
- pixr/models/text_decoder.py +433 -0
- pixr/models/text_decoder_ettin.py +436 -0
- pixr/models/text_decoder_qwen3.py +352 -0
- pixr/task/__init__.py +4 -0
- pixr/task/common.py +212 -0
- pixr/task/docvqa.py +94 -0
- pixr/task/read.py +140 -0
- pixr/task/task_registry.py +19 -0
- pixr/tokenizers/__init__.py +3 -0
- pixr/tokenizers/config.py +8 -0
- pixr/tokenizers/tiktoken_tokenizer.py +152 -0
- pixr/tokenizers/tokenizer.py +45 -0
- pixr/utils/__init__.py +3 -0
- pixr/utils/cli.py +110 -0
- pixr/utils/file_utils.py +36 -0
- pixr/utils/json_utils.py +317 -0
- pixr/utils/metrics.py +65 -0
- pixr/utils/name_utils.py +11 -0
- pixr/version.py +1 -0
- pixr-0.1.0.dev0.dist-info/METADATA +349 -0
- pixr-0.1.0.dev0.dist-info/RECORD +69 -0
- pixr-0.1.0.dev0.dist-info/WHEEL +4 -0
- pixr-0.1.0.dev0.dist-info/entry_points.txt +6 -0
- pixr-0.1.0.dev0.dist-info/licenses/LICENSE +201 -0
pixr/__init__.py
ADDED
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
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
|
+
)
|
pixr/data/transforms.py
ADDED
|
@@ -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
|
pixr/framework/config.py
ADDED
|
@@ -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
|
pixr/framework/device.py
ADDED
|
@@ -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()
|