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/framework/ema.py ADDED
@@ -0,0 +1,85 @@
1
+ """ Exponential moving average of model weights, for plain and FSDP2 sharded models.
2
+
3
+ Ported from OpenCLIP's FSDPModelEma (open_clip/task/fsdp_ema.py).
4
+ """
5
+ from typing import Iterable, List, Optional, Tuple
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+
10
+
11
+ def _clean_name(name: str) -> str:
12
+ return name.replace('._orig_mod.', '.').removeprefix('_orig_mod.')
13
+
14
+
15
+ def _to_local(tensor: torch.Tensor) -> torch.Tensor:
16
+ return tensor.to_local() if hasattr(tensor, 'to_local') else tensor
17
+
18
+
19
+ class ModelEma(nn.Module):
20
+ """ EMA of a model held in a separate copy of it.
21
+
22
+ Params are paired by name (aliased / tied params are updated once, torch.compile wrapped blocks in the trained
23
+ model are matched to the plain EMA blocks). When the trained model is FSDP2 sharded, the copy must be sharded the
24
+ same way (`TrainingTask.prepare_fsdp`): each rank then updates its local shards w/o communication, and evaluating
25
+ `module` is a collective like any FSDP forward.
26
+ """
27
+
28
+ def __init__(self, module: nn.Module, decay: float = 0.9999):
29
+ super().__init__()
30
+ self.module = module
31
+ self.decay = decay
32
+ self.module.eval()
33
+ self.module.requires_grad_(False)
34
+ # Non-persistent buffers are derived, not learned (lerping the -inf entries of an attn mask would give NaN).
35
+ persistent = {_clean_name(k) for k in self.module.state_dict()}
36
+ self._buffer_names = [n for n, _ in self._named_tensors(self.module.named_buffers()) if n in persistent]
37
+
38
+ @staticmethod
39
+ def _named_tensors(named: Iterable[Tuple[str, torch.Tensor]]) -> List[Tuple[str, torch.Tensor]]:
40
+ return [(_clean_name(n), t) for n, t in named]
41
+
42
+ @torch.no_grad()
43
+ def init_fsdp(self):
44
+ """ Run FSDP's lazy init and allocate its unsharded storage now (call after sharding `module`).
45
+
46
+ The first forward would otherwise happen inside eval, w/ inference_mode, FSDP would create the sharded params
47
+ and unsharded storage as inference tensors and the next `update()` would fail writing to them.
48
+ """
49
+ from torch.distributed.fsdp import FSDPModule
50
+ if not isinstance(self.module, FSDPModule):
51
+ return
52
+ self.module._get_fsdp_state()._lazy_init()
53
+ for mod in self.module.modules():
54
+ if isinstance(mod, FSDPModule):
55
+ mod.unshard()
56
+ mod.reshard()
57
+
58
+ def _param_pairs(self, model: nn.Module) -> Tuple[List[torch.Tensor], List[torch.Tensor]]:
59
+ model_params = dict(self._named_tensors(model.named_parameters()))
60
+ ema_params = self._named_tensors(self.module.named_parameters())
61
+ return [_to_local(p) for _, p in ema_params], [_to_local(model_params[n]) for n, _ in ema_params]
62
+
63
+ def _buffer_pairs(self, model: nn.Module) -> List[Tuple[torch.Tensor, torch.Tensor]]:
64
+ model_buffers = dict(self._named_tensors(model.named_buffers()))
65
+ ema_buffers = dict(self._named_tensors(self.module.named_buffers()))
66
+ return [(ema_buffers[n], model_buffers[n]) for n in self._buffer_names]
67
+
68
+ @torch.no_grad()
69
+ def update(self, model: nn.Module, decay: Optional[float] = None):
70
+ """ Update from `model` (unwrapped, must be sharded if FSDP, true after backward). """
71
+ decay = self.decay if decay is None else decay
72
+ ema_params, model_params = self._param_pairs(model)
73
+ torch._foreach_lerp_(ema_params, model_params, weight=1. - decay)
74
+ for ema_buffer, model_buffer in self._buffer_pairs(model):
75
+ if ema_buffer.is_floating_point():
76
+ ema_buffer.lerp_(model_buffer, 1. - decay)
77
+ else:
78
+ ema_buffer.copy_(model_buffer)
79
+
80
+ @torch.no_grad()
81
+ def set(self, model: nn.Module):
82
+ """ Copy `model`'s weights into the EMA. """
83
+ torch._foreach_copy_(*self._param_pairs(model))
84
+ for ema_buffer, model_buffer in self._buffer_pairs(model):
85
+ ema_buffer.copy_(model_buffer)
@@ -0,0 +1,32 @@
1
+ import logging
2
+
3
+
4
+ def setup_logging(
5
+ log_file,
6
+ debug=False,
7
+ include_host=False,
8
+ set_all_loggers=False,
9
+ ):
10
+ level = logging.DEBUG if debug else logging.INFO
11
+ if include_host:
12
+ import socket
13
+ hostname = socket.gethostname()
14
+ formatter = logging.Formatter(
15
+ f'%(asctime)s | {hostname} | %(levelname)s | %(message)s', datefmt='%Y-%m-%d,%H:%M:%S')
16
+ else:
17
+ formatter = logging.Formatter('%(asctime)s | %(levelname)s | %(message)s', datefmt='%Y-%m-%d,%H:%M:%S')
18
+
19
+ logging.root.setLevel(level)
20
+ if set_all_loggers:
21
+ loggers = [logging.getLogger(name) for name in logging.root.manager.loggerDict]
22
+ for logger in loggers:
23
+ logger.setLevel(level)
24
+
25
+ stream_handler = logging.StreamHandler()
26
+ stream_handler.setFormatter(formatter)
27
+ logging.root.addHandler(stream_handler)
28
+
29
+ if log_file:
30
+ file_handler = logging.FileHandler(filename=log_file)
31
+ file_handler.setFormatter(formatter)
32
+ logging.root.addHandler(file_handler)
@@ -0,0 +1,276 @@
1
+ import csv
2
+ import logging
3
+ import os
4
+ from collections import OrderedDict
5
+ from typing import Optional, Tuple, Dict, Union
6
+
7
+ import torch
8
+
9
+ _logger = logging.getLogger(__name__)
10
+
11
+ try:
12
+ from torch.utils.tensorboard import SummaryWriter
13
+ from torch.utils.tensorboard.summary import image
14
+ HAS_TB = True
15
+ except ImportError as e:
16
+ HAS_TB = False
17
+
18
+ try:
19
+ import wandb
20
+ HAS_WANDB = True
21
+ except ImportError:
22
+ HAS_WANDB = False
23
+
24
+
25
+ def summary_row_dict(results, index=None, index_name='epoch'):
26
+ assert isinstance(results, dict)
27
+ row_dict = OrderedDict()
28
+ if index is not None:
29
+ row_dict[index_name] = index
30
+ if not results:
31
+ return row_dict
32
+ if isinstance(next(iter(results.values())), dict):
33
+ # each key in results is a per-phase results dict, flatten by prefixing with phase name
34
+ for p, pr in results.items():
35
+ assert isinstance(pr, dict)
36
+ row_dict.update([('_'.join([p, k]), v) for k, v in pr.items()])
37
+ else:
38
+ row_dict.update(results)
39
+ return row_dict
40
+
41
+
42
+ class SummaryCsv:
43
+ def __init__(self, output_dir, filename='summary.csv'):
44
+ self.output_dir = output_dir
45
+ self.filename = os.path.join(output_dir, filename)
46
+ self.needs_header = not os.path.exists(self.filename)
47
+
48
+ def update(self, row_dict):
49
+ with open(self.filename, mode='a') as cf:
50
+ dw = csv.DictWriter(cf, fieldnames=row_dict.keys())
51
+ if self.needs_header: # first iteration (epoch == 1 can't be used)
52
+ dw.writeheader()
53
+ self.needs_header = False
54
+ dw.writerow(row_dict)
55
+
56
+
57
+ _sci_keys = {'lr'}
58
+
59
+
60
+ def _add_kwargs(text_update, name_map=None, **kwargs):
61
+ def _to_str(key, val):
62
+ if isinstance(val, float):
63
+ if key.lower() in _sci_keys:
64
+ return f'{key}: {val:.3e} '
65
+ else:
66
+ return f'{key}: {val:.4f}'
67
+ else:
68
+ return f'{key}: {val}'
69
+
70
+ def _map_name(key, name_map, capitalize=False):
71
+ if name_map is None:
72
+ if capitalize:
73
+ return key.capitalize() if not key.isupper() else key
74
+ else:
75
+ return key
76
+ return name_map.get(key, None)
77
+
78
+ for k, v in kwargs.items():
79
+ if isinstance(v, dict):
80
+ # log each k, v of a dict kwarg as separate items
81
+ for kk, vv in v.items():
82
+ name = _map_name(kk, name_map)
83
+ if not name:
84
+ continue
85
+ text_update += [_to_str(kk, vv)]
86
+ else:
87
+ name = _map_name(k, name_map)
88
+ if not name:
89
+ continue
90
+ text_update += [_to_str(name, v)]
91
+
92
+
93
+ class Monitor:
94
+
95
+ def __init__(
96
+ self,
97
+ experiment_name=None,
98
+ output_dir=None,
99
+ logger=None,
100
+ hparams=None,
101
+ wandb=False,
102
+ wandb_project='unknown',
103
+ wandb_dir='wandb',
104
+ tensorboard=False,
105
+ tensorboard_dir='tensorboard',
106
+ output_enabled=True,
107
+ log_eval_data=False,
108
+ ):
109
+ """
110
+ A monitoring utility for logging experimental metrics and results to various destinations, such as CSV files,
111
+ Tensorboard, or Weights & Biases (wandb). Allows logging image data and associated text for OCR generation.
112
+
113
+ Args:
114
+ experiment_name (str, optional): The name of the experiment. Used as the run name in wandb. Defaults to None.
115
+ output_dir (str, optional): The directory for output files (CSV logs, Tensorboard logs, etc). Defaults to None.
116
+ logger (Logger, optional): A custom logger instance. Defaults to None.
117
+ hparams (dict, optional): Hyperparameters for the experiment. Used as the config in wandb. Defaults to an empty dict.
118
+ wandb (bool, optional): Flag to enable wandb logging. Defaults to False.
119
+ wandb_project (str, optional): The name of the wandb project. Defaults to 'unknown'.
120
+ wandb_dir (str, optional): The directory for wandb output. Relative to `output_dir`. Defaults to 'wandb'.
121
+ tensorboard (bool, optional): Flag to enable Tensorboard logging. Defaults to False.
122
+ tensorboard_dir (str, optional): The directory for Tensorboard output. Relative to `output_dir`. Defaults to 'tensorboard'.
123
+ output_enabled (bool, optional): Flag to enable output. If False, disables all output. Defaults to True.
124
+ log_eval_data (bool, optional): Flag to log evaluation data, can grow quite large in size. Defaults to False.
125
+ """
126
+ self.output_dir = output_dir # for tensorboard, csv, text file (TODO) logging
127
+ self.logger = logger or logging.getLogger('log')
128
+ hparams = hparams or {}
129
+
130
+ # Setup CSV writer(s)
131
+ if output_dir is not None:
132
+ self.csv_writer = SummaryCsv(output_dir=output_dir)
133
+ else:
134
+ self.csv_writer = None
135
+
136
+ # Setup Tensorboard
137
+ self.tensorboard = None
138
+ if tensorboard:
139
+ assert HAS_TB
140
+ self.tensorboard = SummaryWriter(
141
+ log_dir=os.path.join(self.output_dir, tensorboard_dir)
142
+ )
143
+
144
+ # Setup W&B
145
+ self.wandb = None
146
+ if wandb:
147
+ if HAS_WANDB:
148
+ dir_ = os.path.join(self.output_dir, wandb_dir)
149
+ self.wandb = wandb.init(
150
+ project=wandb_project,
151
+ name=experiment_name,
152
+ config=hparams,
153
+ dir=dir_
154
+ )
155
+ _logger.info(f"Wandb found. Metrics are being logged to {dir_}")
156
+ else:
157
+ _logger.warning(
158
+ "You've requested to log metrics to wandb but package not found. "
159
+ "Metrics not being logged to wandb, try `pip install wandb`")
160
+
161
+ self.output_enabled = output_enabled
162
+ self.log_eval_data = log_eval_data
163
+
164
+ def log_step(
165
+ self,
166
+ phase: str,
167
+ step_idx: int,
168
+ step_end_idx: Optional[int] = None,
169
+ interval: Optional[int] = None,
170
+ loss: Optional[float] = None,
171
+ rate: Optional[Union[float, Tuple[float, float]]] = None,
172
+ lr: Optional[float] = None,
173
+ phase_suffix: str = '',
174
+ metrics: Optional[Dict[str, float]] = None,
175
+ eval_data: Optional[Dict[str, str]] = None,
176
+ **kwargs,
177
+ ):
178
+ """ log train/eval step
179
+
180
+ Args:
181
+ metrics: flat dict of scalar metrics (logged to all destinations)
182
+ eval_data: dict of text samples (e.g. predicted vs target text), logged to tensorboard if enabled
183
+ """
184
+ if not self.output_enabled:
185
+ return
186
+ phase_title = f'{phase.capitalize()} ({phase_suffix})' if phase_suffix else f'{phase.capitalize()}:'
187
+ progress = 100. * step_idx / step_end_idx if step_end_idx else 0.
188
+ rate_str = ''
189
+ if isinstance(rate, (tuple, list)):
190
+ rate_str = f'Rate: {rate[0]:.2f}/s ({rate[1]:.2f}/s)'
191
+ elif rate is not None:
192
+ rate_str = f'Rate: {rate:.2f}/s'
193
+ text_update = [
194
+ phase_title,
195
+ f'{interval}' if interval is not None else None,
196
+ f'[{step_idx}]' if step_end_idx is None else None,
197
+ f'[{step_idx}/{step_end_idx} ({progress:>3.0f}%)]' if step_end_idx is not None else None,
198
+ rate_str,
199
+ f'loss: {loss:.5f}' if loss is not None else None,
200
+ f'lr: {lr:.3e}' if lr is not None else None,
201
+ ]
202
+ _add_kwargs(text_update, **kwargs)
203
+ if metrics:
204
+ _add_kwargs(text_update, **metrics)
205
+ log_str = ' '.join(item for item in text_update if item)
206
+ self.logger.info(log_str)
207
+
208
+ scalars = dict(kwargs)
209
+ if loss is not None:
210
+ scalars['loss'] = loss
211
+ if lr is not None:
212
+ scalars['lr'] = lr
213
+ if metrics:
214
+ scalars.update(metrics)
215
+ scalars = {k: v for k, v in scalars.items() if isinstance(v, (int, float))}
216
+
217
+ if self.tensorboard is not None:
218
+ for k, v in scalars.items():
219
+ self.tensorboard.add_scalar(f'{phase}/{k}', v, step_idx)
220
+ if eval_data and self.log_eval_data:
221
+ for k, v in eval_data.items():
222
+ self.tensorboard.add_text(f'{phase}/{k}', str(v), step_idx)
223
+
224
+ if self.wandb is not None:
225
+ wandb.log({f'{phase}/{k}': v for k, v in scalars.items()}, step=step_idx)
226
+
227
+ def log_phase(
228
+ self,
229
+ phase: str = 'eval',
230
+ interval: Optional[int] = None,
231
+ name_map: Optional[dict] = None,
232
+ **kwargs,
233
+ ):
234
+ """log completion of evaluation or training phase
235
+ """
236
+ if not self.output_enabled:
237
+ return
238
+
239
+ title = [
240
+ f'{phase.capitalize()}',
241
+ f'interval {interval}' if interval is not None else None,
242
+ 'completed. ',
243
+ ]
244
+ title_str = ' '.join(i for i in title if i)
245
+ results = []
246
+ _add_kwargs(results, name_map=name_map, **kwargs)
247
+ log_str = title_str + ', '.join(item for item in results if item)
248
+ self.logger.info(log_str)
249
+
250
+ def write_summary(
251
+ self,
252
+ results: Dict, # Dict or Dict of Dict where first level keys are treated as per-phase results
253
+ index: Optional[Union[int, str]] = None,
254
+ index_name: str = 'interval',
255
+ ):
256
+ """ Log complete results for all phases (typically called at end of epoch)
257
+
258
+ Args:
259
+ results (dict or dict[dict]): dict of results to write, or multiple dicts where first level
260
+ key is the name of results dict for each phase
261
+ index: value for row index (typically epoch #)
262
+ index_name: name for row index header (typically 'interval' or 'epoch)
263
+ """
264
+ if not self.output_enabled:
265
+ return
266
+
267
+ row_dict = summary_row_dict(index=index, index_name=index_name, results=results)
268
+ if self.csv_writer:
269
+ self.csv_writer.update(row_dict)
270
+
271
+ if self.wandb is not None:
272
+ wandb.log(row_dict)
273
+
274
+ if self.tensorboard:
275
+ # FIXME log interval (epoch) summaries to tensorboard?
276
+ pass
@@ -0,0 +1,15 @@
1
+ import random
2
+
3
+ import numpy as np
4
+
5
+ import torch
6
+
7
+ from chugr.common import rank_seed
8
+
9
+
10
+ def random_seed(seed=42, rank=0):
11
+ # ranks spaced by a large stride (not seed + rank), so runs w/ nearby seeds don't share random streams
12
+ seed = rank_seed(seed, rank)
13
+ torch.manual_seed(seed)
14
+ np.random.seed(seed % 2 ** 32)
15
+ random.seed(seed)