AlirezaSalehi99 commited on
Commit
1954c27
·
verified ·
1 Parent(s): 5f5f5ee

Add files using upload-large-folder tool

Browse files
Files changed (50) hide show
  1. Tipsomaly/LICENSE +21 -0
  2. Tipsomaly/README.md +131 -0
  3. Tipsomaly/__init__.py +0 -0
  4. Tipsomaly/datasets/__init__.py +0 -0
  5. Tipsomaly/datasets/dataset.py +115 -0
  6. Tipsomaly/datasets/input_transforms.py +23 -0
  7. Tipsomaly/model/__init__.py +0 -0
  8. Tipsomaly/model/big_vision/__init__.py +0 -0
  9. Tipsomaly/model/big_vision/configs/proj/paligemma/transfers/vatex_cap.py +210 -0
  10. Tipsomaly/model/big_vision/configs/proj/paligemma/transfers/vizwizvqa.py +160 -0
  11. Tipsomaly/model/big_vision/configs/proj/paligemma/transfers/widgetcap.py +180 -0
  12. Tipsomaly/model/big_vision/input_pipeline.py +357 -0
  13. Tipsomaly/model/big_vision/load_siglip.py +157 -0
  14. Tipsomaly/model/big_vision/optax.py +225 -0
  15. Tipsomaly/model/big_vision/optax_test.py +341 -0
  16. Tipsomaly/model/big_vision/requirements.txt +19 -0
  17. Tipsomaly/model/big_vision/run_tpu.sh +35 -0
  18. Tipsomaly/model/big_vision/sharding.py +197 -0
  19. Tipsomaly/model/big_vision/train.py +518 -0
  20. Tipsomaly/model/big_vision/utils.py +1478 -0
  21. Tipsomaly/model/big_vision/utils_test.py +360 -0
  22. Tipsomaly/model/omaly/__init__.py +2 -0
  23. Tipsomaly/model/omaly/fixed_prompts.py +54 -0
  24. Tipsomaly/model/omaly/text_encoder.py +164 -0
  25. Tipsomaly/model/omaly/vision_encoder.py +48 -0
  26. Tipsomaly/model/siglip2/__init__.py +0 -0
  27. Tipsomaly/model/siglip2/siglip2_prompt_learnable.py +183 -0
  28. Tipsomaly/model/tips/__init__.py +35 -0
  29. Tipsomaly/model/tips/__pycache__/image_encoder.cpython-39.pyc +0 -0
  30. Tipsomaly/model/tips/__pycache__/load_model.cpython-39.pyc +0 -0
  31. Tipsomaly/model/tips/__pycache__/text_encoder.cpython-39.pyc +0 -0
  32. Tipsomaly/model/tips/checkpoints/checkpoint.py +98 -0
  33. Tipsomaly/model/tips/checkpoints/download_checkpoints.sh +36 -0
  34. Tipsomaly/model/tips/image_encoder.py +1002 -0
  35. Tipsomaly/model/tips/load_model.py +112 -0
  36. Tipsomaly/model/tips/text_encoder.py +519 -0
  37. Tipsomaly/reproduce.sh +22 -0
  38. Tipsomaly/requirements.txt +42 -0
  39. Tipsomaly/test.py +336 -0
  40. Tipsomaly/train.py +353 -0
  41. Tipsomaly/train_test.sh +34 -0
  42. Tipsomaly/utils/logger.py +72 -0
  43. Tipsomaly/utils/loss.py +107 -0
  44. Tipsomaly/utils/metrics.py +84 -0
  45. Tipsomaly/utils/visualize.py +55 -0
  46. Tipsomaly/workspaces/trained_on_mvtec_default/vegan-arkansas/checkpoints/args.txt +46 -0
  47. Tipsomaly/workspaces/trained_on_mvtec_default/vegan-arkansas/log.txt +2 -0
  48. Tipsomaly/workspaces/trained_on_visa_default/vegan-arkansas/checkpoints/args.txt +23 -0
  49. Tipsomaly/workspaces/trained_on_visa_default/vegan-arkansas/log.txt +2 -0
  50. requirements.txt +8 -0
Tipsomaly/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alireza Salehi et al.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
Tipsomaly/README.md ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tipsomaly (ICASSP 2026)
2
+
3
+ Official PyTorch implementation of [TIPS Over Tricks: Simple Prompts for Effective Zero-shot Anomaly Detection](https://arxiv.org/abs/2602.03594) — a spatially-aware zero-shot anomaly detection pipeline built on the [TIPS](https://arxiv.org/abs/2410.16512) vision-language model, using decoupled prompts and local evidence injection to improve image-level and pixel-level performance.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [📖 Introduction](#-introduction)
10
+ - [📊 Results](#-results)
11
+ - [🚀 Quickstart](#-quickstart)
12
+ - [📍 Checkpoints](#-checkpoints)
13
+ - [🔧 Setup](#-setup)
14
+ - [🗂️ Datasets](#️-datasets)
15
+ - [🛠️ Training](#️-training)
16
+ - [⚖️ License](#️-license)
17
+ - [🔗 Citation](#-citation)
18
+ <!-- - [🙏 Acknowledgements](#-acknowledgements) -->
19
+
20
+ ---
21
+
22
+ ## 📖 Introduction
23
+ Anomaly detection identifies departures from expected behavior in safety-critical settings. When target-domain normal data are unavailable, zero-shot anomaly detection (ZSAD) leverages vision-language models (VLMs). However, CLIP's coarse image-text alignment limits both localization and detection due to (i) spatial misalignment and (ii) weak sensitivity to fine-grained anomalies; prior work compensates with complex auxiliary modules yet largely overlooks the choice of backbone. We revisit the backbone and use TIPS-a VLM trained with spatially aware objectives. While TIPS alleviates CLIP's issues, it exposes a distributional gap between global and local features. We address this with decoupled prompts-fixed for image-level detection and learnable for pixel-level localization-and by injecting local evidence into the global score. Without CLIP-specific tricks, our TIPS-based pipeline improves image-level performance by 1.1-3.9% and pixel-level by 1.5-6.9% across seven industrial datasets, delivering strong generalization with a lean architecture.
24
+
25
+ ![Architecture](imgs/Models_Architecture_page-0001.jpg)
26
+
27
+ ---
28
+
29
+ ## 📊 Results
30
+ Across 14 industrial and medical benchmarks, Tipsomaly consistently outperforms prior CLIP-based zero-shot methods while remaining lightweight. The figure below summarizes its performance across datasets.
31
+
32
+ ![Results](imgs/results-table.png)
33
+
34
+ We compare pixel-level anomaly maps from Tipsomaly with prior CLIP-based methods (AdaCLIP and AnomalyCLIP) on industrial and medical samples. As shown in the following figure, Tipsomaly more accurately localizes anomalous regions across both domains.
35
+
36
+ ![Qualitative-results](imgs/Qualitative_results_page-0001.jpg)
37
+
38
+ ---
39
+
40
+ ## 🚀 Quickstart
41
+ **Quick start (recommended):**
42
+ Use our ready-to-run [Kaggle notebook](https://www.kaggle.com/code/sepehrnoey/tipsomaly-reproduction) to reproduce the results with the provided checkpoints.
43
+
44
+ ## 📍 Checkpoints
45
+ You can find our checkpoints trained on MVTec and VisA in the directory [workspaces](./workspaces/) or on google drive at [this link](https://drive.google.com/file/d/1yvgZYHFljFkGwTD0DYOdrZ_95ZbmjDcC/view?usp=sharing).
46
+
47
+ ---
48
+
49
+
50
+
51
+
52
+ ## 🔧 Setup
53
+ To setup the environment and run experiments, follow these steps.
54
+ 1. Install the required dependencies with the following command.
55
+ ```
56
+ pip install -r requirements.txt
57
+ ```
58
+ 2. Download your desired checkpoints of the TIPS model. For this purpose, edit and run the mentioned script to download model components. After downloading the components, place them in a desired directory.
59
+ ```
60
+ bash model/tips/checkpoints/download_checkpoints.sh
61
+ ```
62
+ ```
63
+ mkdir tips && mv tips_oss_l14_highres_distilled_text.npz tips_oss_l14_highres_distilled_vision.npz tokenizer.model tips/
64
+ ```
65
+ 3. Prepare your datasets in the format described in the [Datasets](#️-datasets) section and place them in a desired directory.
66
+ ```
67
+ mkdir data-root && mv mvtec visa data-root/
68
+ ```
69
+ 4. Now, by setting the correct paths for the model, checkpoints, and the data root path, you can edit the runnable script `reproduce.sh` to run experiments. Here is a sample:
70
+ ```
71
+ models_dir="path/to/tips"
72
+ data_root_dir="path/to/data-root"
73
+ model_version='l14h'
74
+ checkpoint_path="./workspaces/trained_on_mvtec_default/vegan-arkansas/checkpoints"
75
+
76
+ python test.py --models_dir $models_dir --checkpoint_path $checkpoint_path --data_root_dir $data_root_dir --dataset visa --epoch 2 --model_version $model_version --fixed_prompt_type industrial
77
+
78
+ ```
79
+
80
+ ## 🗂️ Datasets
81
+ The datasets should be set in the following format. Each dataset, must contain a file named `meta.json` which contains information about the dataset. Common datasets for anomaly detection and instructions to generate `meta.json` files can be found in the [AdaCLIP GitHub repository](https://github.com/caoyunkang/AdaCLIP). The general expected structure of the datasets is like the following:
82
+ ```
83
+ data-root/
84
+ ├── mvtec/
85
+ │ ├── meta.json
86
+ │ ├── bottle/
87
+ │ │ ├── train/
88
+ │ │ │ └── good/
89
+ │ │ │ └── 000.png
90
+ │ │ ├── test/
91
+ │ ��� │ ├── good/
92
+ │ │ │ │ └── 000.png
93
+ │ │ │ ├── broken_large/
94
+ │ │ │ │ └── 000.png
95
+ │ │ │ └── broken_small/
96
+ │ │ │ └── 000.png
97
+ │ │ └── ground_truth/
98
+ │ │ ├── broken_large/
99
+ │ │ │ └── 000_mask.png
100
+ │ │ └── broken_small/
101
+ │ │ └── 000_mask.png
102
+
103
+ ├── visa/
104
+ ├── mpdd/
105
+ ├── ....
106
+ ....
107
+ ```
108
+
109
+ ## 🛠️ Training
110
+ You can use our ready-to-run [Kaggle notebook](https://www.kaggle.com/code/sepehrnoey/tipsomaly-train) to train your model and test on your desired dataset. Steps to prepare the datasets and downloading the base model components can be found at the [Setup](#-setup) section. You can also find the commands to train the model and test multiple datasets in a loop in the [train_test.sh](train_test.sh)
111
+
112
+ ## 🔗 Citation
113
+ If you find this project helpful for your research, please consider citing the following BibTeX entry.
114
+
115
+
116
+ <!-- 📚 [Paper Link](https://arxiv.org/pdf/2504.11055) -->
117
+
118
+ **BibTeX:**
119
+ ```bibtex
120
+ @article{salehi2026tips,
121
+ title={TIPS Over Tricks: Simple Prompts for Effective Zero-shot Anomaly Detection},
122
+ author={Salehi, Alireza and Karami, Ehsan and Noey, Sepehr and Noey, Sahand and Yamada, Makoto and Hosseini, Reshad and Sabokrou, Mohammad},
123
+ journal={arXiv preprint arXiv:2602.03594},
124
+ year={2026}
125
+ }
126
+ ```
127
+
128
+ <!-- ## 🙏 Acknowledgements -->
129
+
130
+ ## ⚖️ License
131
+ This project is licensed under the MIT License. Please refer to the [LICENSE](LICENSE) file for more information.
Tipsomaly/__init__.py ADDED
File without changes
Tipsomaly/datasets/__init__.py ADDED
File without changes
Tipsomaly/datasets/dataset.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.utils.data as data
2
+ import json
3
+ import random
4
+ from PIL import Image
5
+ import numpy as np
6
+ import os
7
+
8
+ def sample_data(meta_info, k_shot):
9
+ sampled_data = []
10
+ complement_data = []
11
+
12
+ for cls_name, data_list in meta_info.items():
13
+ nrm_smpls = [item for item in data_list if item['anomaly'] == 0]
14
+ anm_smpls = [item for item in data_list if item['anomaly'] == 1]
15
+
16
+ if k_shot > 0:
17
+ n_samples = k_shot
18
+ n_nrm_smpls = min(int(n_samples / 2), len(nrm_smpls))
19
+ n_anm_smpls = min(int(n_samples / 2), len(anm_smpls))
20
+
21
+ cls_data = []
22
+ cls_data.extend(random.sample(nrm_smpls, n_nrm_smpls))
23
+ cls_data.extend(random.sample(anm_smpls, n_anm_smpls))
24
+ sampled_data.extend(cls_data)
25
+
26
+ complement_class_data = [item for item in data_list if item not in cls_data]
27
+ complement_data.extend(complement_class_data)
28
+
29
+ print(f'num samples for cls {cls_name}, norm: {n_nrm_smpls}, anom: {n_anm_smpls}')
30
+
31
+ return sampled_data, complement_data
32
+
33
+ class Dataset(data.Dataset):
34
+ def __init__(self, roots, transform, target_transform, kwargs=None):
35
+ self.roots = roots
36
+ self.transform = transform
37
+ self.target_transform = target_transform
38
+ split='test'
39
+
40
+ meta_infos = {}
41
+ for root in roots:
42
+ with open(f'{root}/meta.json', 'r') as f:
43
+ meta_info = json.load(f)
44
+ for cls in meta_info[split]:
45
+ meta_info[split][cls] = [{**s, 'root': root} for s in meta_info[split][cls]]
46
+
47
+ if cls in meta_infos:
48
+ meta_infos[cls].extend(meta_info[split][cls])
49
+ meta_infos[cls].extend(meta_info[split][cls])
50
+ else:
51
+ meta_infos[cls] = meta_info[split][cls]
52
+
53
+ meta_info_classes = list(meta_infos.keys())
54
+
55
+ self.selected_class = kwargs.class_name
56
+ self.cls_names = meta_info_classes if self.selected_class == ['all'] else self.selected_class
57
+
58
+ self.data_all = []
59
+ for cls_name in self.cls_names:
60
+ self.data_all.extend(meta_infos[cls_name])
61
+
62
+ self.dataset_name = kwargs.dataset
63
+ self.class_ids = list(range(len(self.cls_names)))
64
+ self.class_name_map_class_id = {k: index for k, index in zip(self.cls_names, self.class_ids)}
65
+
66
+ # Few-shot dataset (splitting...)
67
+ self.k_shot = kwargs.k_shot
68
+ if not self.k_shot == 0:
69
+ sampled_sets = sample_data(meta_infos, self.k_shot)
70
+
71
+ if kwargs.type == 'train':
72
+ self.data_all = sampled_sets[0]
73
+
74
+ elif kwargs.type == 'test' and kwargs.train_dataset == self.dataset_name:
75
+ self.data_all = sampled_sets[1]
76
+
77
+ self.length = len(self.data_all)
78
+ print(f"number of dataset samples: {self.length}")
79
+
80
+ def _process_image(self, data):
81
+ img_path, mask_path, cls_name, specie_name, anomaly = data['img_path'], data['mask_path'], \
82
+ data['cls_name'], data['specie_name'], data['anomaly']
83
+
84
+ root = data['root']
85
+ full_img_path = os.path.join(root, img_path)
86
+ img = Image.open(full_img_path).convert('RGB')
87
+
88
+ if anomaly == 0 or (not os.path.isfile(os.path.join(root, mask_path))):
89
+ img_mask = Image.fromarray(np.zeros((img.size[1], img.size[0])), mode='L')
90
+ else:
91
+ img_mask = np.array(Image.open(os.path.join(root, mask_path)).convert('L')) > 0
92
+ img_mask = Image.fromarray(img_mask.astype(np.uint8) * 255, mode='L')
93
+
94
+ img = self.transform(img) if self.transform is not None else img
95
+ img_mask = self.target_transform(img_mask)
96
+ img_mask = np.where(img_mask > 0.5, 1, 0)
97
+
98
+ result = {
99
+ 'img': img,
100
+ 'abnorm_mask': img_mask,
101
+ 'cls_name': cls_name,
102
+ 'anomaly': anomaly,
103
+ 'img_path': os.path.join(root, img_path),
104
+ "cls_id": self.class_name_map_class_id[cls_name]
105
+ }
106
+
107
+ return result
108
+
109
+ def __len__(self):
110
+ return self.length
111
+
112
+ def __getitem__(self, index):
113
+ data = self.data_all[index]
114
+ result = self._process_image(data)
115
+ return result
Tipsomaly/datasets/input_transforms.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from torchvision import transforms
2
+
3
+ IMAGE_MEAN = (0, 0, 0)
4
+ IMAGE_STD = (1.0, 1.0, 1.0)
5
+
6
+ class Ensure3Channels:
7
+ def __call__(self, img):
8
+ return img.convert('RGB')
9
+
10
+ def create_transforms_tips(image_size):
11
+ transform = transforms.Compose([
12
+ Ensure3Channels(),
13
+ transforms.Resize((image_size, image_size)),
14
+ transforms.ToTensor(),
15
+ transforms.Normalize(IMAGE_MEAN, IMAGE_STD),
16
+ ])
17
+
18
+ target_transform = transforms.Compose([
19
+ transforms.Resize((image_size, image_size)),
20
+ transforms.ToTensor(),
21
+ ])
22
+
23
+ return transform, target_transform
Tipsomaly/model/__init__.py ADDED
File without changes
Tipsomaly/model/big_vision/__init__.py ADDED
File without changes
Tipsomaly/model/big_vision/configs/proj/paligemma/transfers/vatex_cap.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # pylint: disable=line-too-long
16
+ r"""PaliGemma transfer to VATEX Video captioning.
17
+
18
+ IMPORTANT: This config is based on an unreleased version of DeepMind Video
19
+ Readers (DMVR). Users can either set up DMVR using the open source code from
20
+ GitHub (see below for details), or add their own data loader of choice.
21
+ """
22
+
23
+ import big_vision.configs.common as bvcc
24
+ from big_vision.configs.proj.paligemma.transfers.common import combine_and_keep_train, combine_and_keep_eval, TOKENIZER
25
+
26
+ TEXT_LEN = 64
27
+ DATASET_NAME = 'vatex'
28
+ # Numbers might need to be updated due to wipeout. Current from 2024-04-28
29
+ SPLIT_SIZE = {'train': 22315, 'valid': 2584, 'test': 5135}
30
+
31
+
32
+ def training_data(res, *, final_split, num_frames=8, stride=None):
33
+ """Creates training data config.
34
+
35
+ Args:
36
+ res: The requested image resolution (eg 224).
37
+ final_split: Train on all train+valid data.
38
+ num_frames: number of sampled frames per video.
39
+ stride: stride at which the frames are sampled.
40
+
41
+ Returns:
42
+ The ConfigDict for the input section.
43
+ """
44
+ pp = '|'.join([
45
+ # prepare the frames by decoding, resizing, replicating, sampling:
46
+ f'video_decode({res})|video_replicate_img({num_frames},{num_frames})',
47
+ f'video_ensure_shape("image", {(num_frames, res, res, 3)})',
48
+ # pick one caption at random during training
49
+ 'strfmt("caption en", outkey="prefix")',
50
+ 'video_choice(inkey="caption/string", outkey="suffix")',
51
+ combine_and_keep_train(TEXT_LEN),
52
+ ])
53
+
54
+ c = bvcc.parse_arg('')
55
+ c.data = {}
56
+ splits = ['train', 'valid'] if final_split else ['train']
57
+ raise NotImplementedError('Please implement a video reader of choice!')
58
+ # For example DMVR https://github.com/google-deepmind/dmvr
59
+ # The reader should support the following arguments:
60
+ # - name: Name of the reader.
61
+ # - dataset_name: Name of the data set.
62
+ # - split: Data set split.
63
+ # - num_frames: Number of frames sampled from the video.
64
+ # - stride: Stride at which the video frames are sampled.
65
+ # - deterministic_fs: Whether to sample the frames starting at the first
66
+ # frame or whether an offest should be chosen at random (if there are more
67
+ # frames than num_frames * stride)
68
+ # - first_k_shards: Whether to only use the first k shards of the data
69
+ # (optional but useful for speeding up intermediate evaluations).
70
+ for split in splits:
71
+ c.data[split] = SPLIT_SIZE[split]
72
+ c[split] = {'pp': pp}
73
+ c[split].data = dict(
74
+ # PLEASE ADD YOUR READER HERE:
75
+ name='<add_your_data_loader_here>',
76
+ dataset_name=DATASET_NAME, split=split,
77
+ num_frames=num_frames, stride=stride,
78
+ deterministic_fs=False)
79
+ return c
80
+
81
+
82
+ def add_eval(c, res, num_frames=8, stride=None):
83
+ """Captioning evaluator."""
84
+ c_train = training_data(res, final_split=True, num_frames=num_frames, stride=stride)
85
+
86
+ pp = '|'.join([
87
+ # prepare the frames by decoding, resizing, replicating, sampling:
88
+ f'video_decode({res})|video_replicate_img({num_frames},{num_frames})',
89
+ f'video_ensure_shape("image", {(num_frames, res, res, 3)})',
90
+ 'strfmt("caption en", outkey="prefix")',
91
+ 'copy("example/video_id", "image/id")',
92
+ 'copy("caption/string", "captions")',
93
+ combine_and_keep_eval(TEXT_LEN, keep=('image/id', 'captions')),
94
+ ])
95
+
96
+ for freq, name, split, first_k_shards, skip_first_eval in [
97
+ (1/8, 'minitrain', 'train', 2, False), # To gauge memorization.
98
+ (1/4, 'minival', 'valid', 2, False), # To monitor val progress.
99
+ (1, 'val', 'valid', None, False), # To tune hparams.
100
+ (1, 'eval', 'test', None, False), # final metric
101
+ ]:
102
+ c.evals[f'{DATASET_NAME}/{name}'] = dict(
103
+ type='proj.paligemma.transfers.coco_caption',
104
+ pred='decode', pred_kw={'max_decode_len': TEXT_LEN},
105
+ data={**c_train.train.data, 'split': split,
106
+ 'first_k_shards': first_k_shards,
107
+ 'deterministic_fs': True},
108
+ log_percent=freq, tokenizer=TOKENIZER,
109
+ pp_fn=pp, skip_first=skip_first_eval)
110
+
111
+
112
+ def add_eval_pplx(c, res, num_frames=8, stride=None):
113
+ """Perplexity evaluator to test runs before implementing the real deal."""
114
+ c_train = training_data(res, final_split=True, num_frames=num_frames, stride=stride)
115
+
116
+ for name, split, first_k_shards in [
117
+ ('minitrain', 'train', 2), # To gauge memorization.
118
+ ]:
119
+ c.evals[f'{DATASET_NAME}/{name}/pplx'] = dict(
120
+ type='proj.paligemma.perplexity', pred='logits',
121
+ key='text', shift_labels=True,
122
+ log_percent=1/8, # Not too cheap, do 10x per run.
123
+ data={**c_train.train.data, 'split': split,
124
+ 'first_k_shards': first_k_shards,
125
+ 'deterministic_fs': True},
126
+ pp_fn=c_train.train.pp,
127
+ )
128
+
129
+
130
+ def sweep_best(add, arg=None):
131
+ """Train with best hyper-params."""
132
+ c = bvcc.parse_arg(arg, final_split=False)
133
+ add(lr=3e-6, wd=3e-7, total_epochs=10, **bvcc.arg(res=224, **c))
134
+
135
+
136
+ sweep = sweep_best
137
+
138
+
139
+ def get_config(arg=None):
140
+ """Config for training."""
141
+ c = bvcc.parse_arg(arg, mode='xm', num_frames=16, stride=7, res=224,
142
+ freeze_vit=False, freeze_llm=False, final_split=False)
143
+
144
+ c.input = training_data(
145
+ c.res, final_split=c.final_split,
146
+ num_frames=c.num_frames, stride=c.stride)
147
+
148
+ c.total_epochs = 3
149
+ c.input.batch_size = 128
150
+ c.optax_name = 'scale_by_adam'
151
+ c.optax = dict(b2=0.999)
152
+ c.lr = 3e-6
153
+ c.wd = 3e-7
154
+ c.grad_clip_norm = 1.0
155
+ c.label_smoothing = 0.0
156
+
157
+ # Learning-rate schedule.
158
+ sched = dict(decay_type='cosine', warmup_percent=0.05)
159
+ c.schedule = [
160
+ ('img/.*', None if c.freeze_vit else sched),
161
+ ('llm/.*', None if c.freeze_llm else sched),
162
+ ]
163
+
164
+ # Add evaluators.
165
+ c.evals = {}
166
+ add_eval(c, c.res, c.num_frames, c.stride)
167
+ add_eval_pplx(c, c.res, c.num_frames, c.stride)
168
+
169
+ # Model section.
170
+ c.model_name = 'proj.paligemma.paligemma'
171
+ c.model = {}
172
+ c.model.img = dict(variant='So400m/14', pool_type='none', scan=True)
173
+ c.model.llm = dict(vocab_size=256_000 + 1024 + 128, dropout=0.0)
174
+ c.model_init = f'pt_{c.res}'
175
+
176
+ # FSDP strategy.
177
+ c.mesh = [('data', -1)]
178
+ c.sharding_strategy = [('.*', 'fsdp(axis="data")')]
179
+ c.sharding_rules = [('act_batch', ('data',))]
180
+
181
+ for split in c.input.data.keys():
182
+ c.input[split].shuffle_buffer_size = 10_000
183
+ c.log_training_steps = 50
184
+ c.ckpt_steps = 1_000
185
+ c.pp_modules = ['ops_general', 'ops_image', 'ops_text', 'proj.paligemma.ops',
186
+ 'proj.paligemma.video']
187
+
188
+ # Update configs for quicker local runs and avoid swapping.
189
+ if c.mode in ('runlocal', 'mock'):
190
+ for split in c.input.data.keys():
191
+ c.input[split].shuffle_buffer_size = None
192
+ for ev in c.evals.values():
193
+ ev.data.first_k_shards = 1
194
+
195
+ if c.mode == 'runlocal':
196
+ c.log_training_steps = 1
197
+ c.input.batch_size = 2
198
+
199
+ c.seed = 0
200
+ return c
201
+
202
+
203
+ def metrics(arg=None): # pylint: disable=unused-argument
204
+ m = ['training_loss']
205
+ for split in ('minitrain', 'minival', 'val', 'eval'):
206
+ m.append((f'{DATASET_NAME}/{split}/cider'))
207
+ for split in ('minitrain', 'minival'):
208
+ m.append((f'{DATASET_NAME}/{split}/pplx/avg'))
209
+ return m
210
+
Tipsomaly/model/big_vision/configs/proj/paligemma/transfers/vizwizvqa.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # pylint: disable=line-too-long
16
+ r"""PaliGemma transfer to VQAv2.
17
+ """
18
+
19
+ import big_vision.configs.common as bvcc
20
+ from big_vision.configs.proj.paligemma.transfers.common import combine_and_keep_train, combine_and_keep_eval, TOKENIZER
21
+
22
+
23
+ def training_data(res, final_split, text_len=48):
24
+ """Creates training data config.
25
+
26
+ See (internal link)
27
+ You can add more arguments beside `res`, but give them good defaults.
28
+
29
+ Args:
30
+ res: The requested image resolution (eg 224)
31
+ final_split: Train on combined train+val
32
+ text_len: sequence length
33
+
34
+ Returns:
35
+ The ConfigDict for the input section.
36
+ """
37
+ c = bvcc.parse_arg('') # Just make a configdict without extra import.
38
+ c.data = dict(
39
+ name='vizwizvqa',
40
+ split='train+val' if final_split else 'train',
41
+ )
42
+ c.pp = '|'.join([
43
+ f'decode|resize({res}, antialias=True)|value_range(-1, 1)',
44
+ 'strfmt("answer en {question}", outkey="prefix")',
45
+ 'choice_no_replacement(inkey="answers", outkey="suffix")',
46
+ combine_and_keep_train(text_len),
47
+ ])
48
+ return c
49
+
50
+
51
+ def add_eval(c, res, text_len=48, **kw):
52
+ """VQAv2 evaluators."""
53
+ pp = '|'.join([
54
+ f'decode|resize({res})|value_range(-1, 1)',
55
+ 'strfmt("answer en {question}", outkey="prefix")',
56
+ 'copy("image/filename", "question_id")',
57
+ combine_and_keep_eval(text_len, keep=('answers', 'question_id')),
58
+ ])
59
+
60
+ for freq, name, split in [
61
+ (1/8, 'minitrain', 'train[:5120]'), # To gauge memorization. 400s on 32v2
62
+ (0.1, 'minival', 'val'), # To tune hparams. 4k samples, full eval is fine.
63
+ (1.0, 'test', 'test'), # For the test-server. SLOW.
64
+ ]:
65
+ c.evals[f'vizwizvqa/{name}'] = dict(
66
+ type='proj.paligemma.transfers.vqa',
67
+ pred='decode', pred_kw={'max_decode_len': text_len},
68
+ outfile=f'{{workdir}}/vizwiz_{name}.json',
69
+ out_question_key='image',
70
+ data={**training_data(res, True, text_len).data, 'split': split},
71
+ log_percent=freq, tokenizer=TOKENIZER, pp_fn=pp)
72
+ c.evals[f'vizwizvqa/{name}'].update(kw)
73
+
74
+
75
+ def add_eval_pplx(c, res, text_len=48):
76
+ """Perplexity evaluator to test runs before implementing the real deal."""
77
+ c_train = training_data(res, True, text_len) # Use mostly same settings as training.
78
+ for name, split in [
79
+ ('minitrain', 'train'), # To gauge memorization.
80
+ ('minival', 'val'), # To tune hparams
81
+ ]:
82
+ c.evals[f'vizwizvqa/{name}/pplx'] = dict(
83
+ type='proj.paligemma.perplexity', pred='logits',
84
+ key='text', shift_labels=True,
85
+ log_percent=1/8,
86
+ data={**c_train.data, 'split': split},
87
+ pp_fn=c_train.pp,
88
+ )
89
+
90
+
91
+ def sweep_best(add, arg=None):
92
+ """Train with best hyper-params."""
93
+ c = bvcc.parse_arg(arg, final_split=False)
94
+ add(**bvcc.arg(res=224, **c))
95
+ add(**bvcc.arg(res=448, **c))
96
+
97
+
98
+ sweep = sweep_best # Choose which sweep to run.
99
+
100
+
101
+ def get_config(arg=None):
102
+ """Config for training."""
103
+ c = bvcc.parse_arg(arg, mode='xm', res=224, final_split=False)
104
+
105
+ c.input = training_data(c.res, c.final_split)
106
+
107
+ # Instead of epochs, you can also use `total_examples` or `total_steps`.
108
+ c.total_epochs = 10
109
+ c.input.batch_size = 256
110
+ c.optax_name = 'scale_by_adam'
111
+ c.optax = dict(b2=0.999)
112
+ c.lr = 0.00001
113
+ c.wd = 0.0
114
+ c.grad_clip_norm = 1.0
115
+ c.label_smoothing = 0.0
116
+ c.schedule = dict(decay_type='cosine', warmup_percent=0.05)
117
+
118
+ # Add evaluators.
119
+ c.evals = {}
120
+ add_eval(c, c.res, batch_size=1024)
121
+ add_eval_pplx(c, c.res)
122
+
123
+ # Model section.
124
+ c.model_name = 'proj.paligemma.paligemma'
125
+ c.model = {}
126
+ c.model.img = dict(variant='So400m/14', pool_type='none', scan=True)
127
+ c.model.llm = dict(vocab_size=256_000 + 1024 + 128, dropout=0.0)
128
+ c.model_init = f'pt_{c.res}'
129
+
130
+ # FSDP strategy.
131
+ c.mesh = [('data', -1)]
132
+ c.sharding_strategy = [('.*', 'fsdp(axis="data")')]
133
+ c.sharding_rules = [('act_batch', ('data',))]
134
+
135
+ # These probably do not need any change/tuning
136
+ c.input.shuffle_buffer_size = 25_000
137
+ c.log_training_steps = 50
138
+ c.ckpt_steps = 1_000
139
+ c.pp_modules = ['ops_general', 'ops_image', 'ops_text', 'proj.paligemma.ops']
140
+
141
+ # Update configs for quicker local runs and avoid swapping.
142
+ if c.mode in ('runlocal', 'mock'):
143
+ c.input.shuffle_buffer_size = None
144
+ for ev in c.evals.values():
145
+ ev.data.split = ev.data.split.split('[')[0] + '[:16]'
146
+
147
+ if c.mode == 'runlocal':
148
+ c.log_training_steps = 1
149
+ c.input.batch_size = 2
150
+
151
+ c.seed = 0
152
+ return c
153
+
154
+
155
+ def metrics(arg=None): # pylint: disable=unused-argument
156
+ m = ['training_loss']
157
+ for split in ('minival', 'minitrain'):
158
+ m.append(f'vizwizvqa/{split}/acc')
159
+ m.append(f'vizwizvqa/{split}/pplx/avg')
160
+ return m
Tipsomaly/model/big_vision/configs/proj/paligemma/transfers/widgetcap.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # pylint: disable=line-too-long
16
+ r"""PaliGemma transfer to widgetcap (bbox drawn in the picture).
17
+ """
18
+
19
+ import big_vision.configs.common as bvcc
20
+ from big_vision.configs.proj.paligemma.transfers.common import combine_and_keep_train, combine_and_keep_eval, TOKENIZER
21
+
22
+
23
+ def training_data(res, *, final_split, text_len=32):
24
+ """Creates training data config.
25
+
26
+ See (internal link)
27
+ You can add more arguments beside `res`, but give them good defaults.
28
+
29
+ Args:
30
+ res: The requested image resolution (eg 224).
31
+ final_split: Train on all train+dev data.
32
+ text_len: The max text length.
33
+
34
+ Returns:
35
+ The ConfigDict for the input section.
36
+ """
37
+ c = bvcc.parse_arg('') # Just make a configdict without extra import.
38
+ c.data = dict(
39
+ name='widgetcap',
40
+ split='train+dev' if final_split else 'train',
41
+ )
42
+ c.pp = '|'.join([
43
+ 'decode',
44
+ f'resize({res}, antialias=True)',
45
+ 'draw_bbox',
46
+ 'value_range(-1, 1)',
47
+ 'strfmt("caption en", outkey="prefix")',
48
+ 'choice_no_replacement(inkey="texts", outkey="suffix")',
49
+ combine_and_keep_train(text_len),
50
+ ])
51
+ return c
52
+
53
+
54
+ def add_eval(c, res, text_len=32, **kw):
55
+ """Captioning evaluator with cider/bleu-4/meteor/rouge/spice metrics."""
56
+ # Input eval pp without ground truth text and random crop.
57
+ pp_eval = '|'.join([
58
+ 'copy("texts", "captions")', # GT for evaluator.
59
+ 'decode',
60
+ f'resize({res}, antialias=True)',
61
+ 'draw_bbox',
62
+ 'value_range(-1, 1)',
63
+ 'strfmt("caption en", outkey="prefix")',
64
+ combine_and_keep_eval(text_len, keep=('image/id', 'captions')),
65
+ ])
66
+
67
+ for name, split in [
68
+ ('val', 'dev'),
69
+ ('eval', 'test'),
70
+ ]:
71
+ c.evals[f'widgetcap/{name}'] = dict(
72
+ type='proj.paligemma.transfers.coco_caption',
73
+ pred='decode', pred_kw={'max_decode_len': text_len},
74
+ data=dict(
75
+ name='widgetcap',
76
+ split=split,
77
+ ),
78
+ log_percent=0.1, tokenizer=TOKENIZER, pp_fn=pp_eval)
79
+ c.evals[f'widgetcap/{name}'].update(kw)
80
+
81
+
82
+ def add_eval_pplx(c, res, text_len=32):
83
+ """Perplexity evaluator to test runs before implementing the real deal."""
84
+ c_train = training_data(res, final_split=True, text_len=text_len) # Use mostly same settings as training.
85
+ for name, split in [
86
+ ('minitrain', 'train[:5%]'), # To gauge memorization.
87
+ ('minival', 'dev'), # To tune hparams.
88
+ ('eval', 'test'), # To compute final publishable scores.
89
+ ]:
90
+ c.evals[f'widgetcap/{name}/pplx'] = dict(
91
+ type='proj.paligemma.perplexity', pred='logits',
92
+ key='text', shift_labels=True,
93
+ log_percent=0.05, # Eval ~20x per run; it's cheap.
94
+ data={**c_train.data, 'split': split},
95
+ pp_fn=c_train.pp,
96
+ )
97
+
98
+
99
+ def sweep_best(add, arg=None):
100
+ """Train with best hyper-params."""
101
+ c = bvcc.parse_arg(arg, final_split=False)
102
+ # Based on sweeps (internal link) (widgetcap/val/cider).
103
+ # NOTE: dropout always on, see get_config.
104
+ add(lr=3e-6, wd=3e-7, total_epochs=4, **bvcc.arg(res=224, **c))
105
+ add(lr=3e-6, wd=3e-7, total_epochs=4, **bvcc.arg(res=448, **c))
106
+ # Not better: add(lr=3e-6, wd=3e-7, total_epochs=4, **bvcc.arg(res=896, **c))
107
+
108
+
109
+ sweep = sweep_best # Choose which sweep to run.
110
+
111
+
112
+ def get_config(arg=None):
113
+ """Config for training."""
114
+ c = bvcc.parse_arg(arg, mode='xm', res=224, final_split=False)
115
+
116
+ c.input = training_data(c.res, final_split=c.final_split)
117
+
118
+ # Instead of epochs, you can also use `total_examples` or `total_steps`.
119
+ c.total_epochs = 4
120
+ c.input.batch_size = 64
121
+ c.optax_name = 'scale_by_adam'
122
+ c.optax = dict(b2=0.999)
123
+ c.lr = 3e-6
124
+ c.wd = 3e-7
125
+ c.grad_clip_norm = 1.0
126
+ c.label_smoothing = 0.1
127
+ c.schedule = dict(decay_type='cosine', warmup_percent=0.05)
128
+
129
+ # Add evaluators.
130
+ c.evals = {}
131
+ add_eval(c, c.res, batch_size=1024)
132
+ add_eval_pplx(c, c.res)
133
+
134
+ # Model section.
135
+ c.model_name = 'proj.paligemma.paligemma'
136
+ c.model = {}
137
+ c.model.img = dict(variant='So400m/14', pool_type='none', scan=True)
138
+ c.model.llm = dict(vocab_size=256_000 + 1024 + 128, dropout=0.1)
139
+ c.model_init = f'pt_{c.res}'
140
+
141
+ # FSDP strategy.
142
+ c.mesh = [('data', -1)]
143
+ c.sharding_strategy = [('.*', 'fsdp(axis="data")')]
144
+ c.sharding_rules = [('act_batch', ('data',))]
145
+
146
+ # These probably do not need any change/tuning
147
+ c.input.shuffle_buffer_size = 50_000
148
+ c.log_training_steps = 50
149
+ c.ckpt_steps = 1_000
150
+ c.pp_modules = [
151
+ 'ops_general',
152
+ 'ops_image',
153
+ 'ops_text',
154
+ 'proj.paligemma.ops',
155
+ 'proj.paligemma.widgetcap',
156
+ ]
157
+
158
+ # Update configs for quicker local runs and avoid swapping.
159
+ if c.mode in ('runlocal', 'mock'):
160
+ c.input.shuffle_buffer_size = None
161
+ for ev in c.evals.values():
162
+ ev.data.split = ev.data.split.split('[')[0] + '[:16]'
163
+
164
+ if c.mode == 'runlocal':
165
+ c.log_training_steps = 1
166
+ c.input.batch_size = 2
167
+
168
+ c.seed = 0
169
+ return c
170
+
171
+
172
+ def metrics(arg=None): # pylint: disable=unused-argument
173
+ # This function defines the default flatboard. If you want, it can be a lot
174
+ # fancier too, but the simplest way is a list of metric names.
175
+ m = ['training_loss']
176
+ for split in ('eval', 'minival', 'minitrain'):
177
+ m.append(f'widgetcap/{split}/pplx/avg')
178
+ for split in ('val', 'eval'):
179
+ m.append(f'widgetcap/{split}/cider')
180
+ return m
Tipsomaly/model/big_vision/input_pipeline.py ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """ImageNet input pipeline."""
16
+ import collections
17
+ import functools
18
+ import itertools
19
+ import math
20
+ import multiprocessing.pool
21
+
22
+ from absl import logging
23
+ from big_vision.datasets import sequence_packing
24
+ import big_vision.datasets.core as ds_core
25
+ import big_vision.pp.builder as pp_builder
26
+ import big_vision.utils as u
27
+ import einops
28
+ import jax
29
+ import numpy as np
30
+ import tensorflow as tf
31
+
32
+
33
+ DEFAULT_NUM_PARALLEL_CALLS = 100
34
+
35
+
36
+ def make_for_train(
37
+ data, preprocess_fn, batch_size,
38
+ shuffle_buffer_size=None, cache_raw=False,
39
+ num_parallel_calls=DEFAULT_NUM_PARALLEL_CALLS, prefetch=2,
40
+ *,
41
+ pre_filter_fn=None, post_filter_fn=None,
42
+ pack=None, skip_errors=False,
43
+ ):
44
+ """Makes an input pipeline for training."""
45
+ # Use data filtering at your own risk: the actual split sizes won't be known
46
+ # in advance, so epoch-based things won't work correctly.
47
+
48
+ data = _add_tpu_host_options(data)
49
+
50
+ data = data.filter(pre_filter_fn) if pre_filter_fn else data
51
+ data = data.cache() if cache_raw else data
52
+
53
+ # First shuffle and then repeat (each with a different shuffle). This way
54
+ # the data for one epoch is all seen before the next one is processed and
55
+ # significantly affects the number of times each example is seen when
56
+ # processing for small number of epochs.
57
+ if shuffle_buffer_size:
58
+ data = data.shuffle(shuffle_buffer_size, reshuffle_each_iteration=True)
59
+ data = data.repeat(None)
60
+
61
+ data = data.map(preprocess_fn, num_parallel_calls=num_parallel_calls)
62
+ data = data.filter(post_filter_fn) if post_filter_fn else data
63
+
64
+ data = data.ignore_errors(log_warning=True) if skip_errors else data
65
+
66
+ if pack:
67
+ data = sequence_packing.pack_dataset(
68
+ data,
69
+ batch_size // jax.process_count() if batch_size else None,
70
+ pack.to_dict())
71
+
72
+ # Drop remainder makes shape fully static, so we can later use it if needed.
73
+ if batch_size:
74
+ data = data.batch(batch_size // jax.process_count(), drop_remainder=True)
75
+ if prefetch: # None means autotune, but we never want that.
76
+ data = data.prefetch(prefetch)
77
+ return data
78
+
79
+
80
+ def training(input_config):
81
+ """Reads the data from a single dataset, or mixes it from multiple.
82
+
83
+ The data is read either from one or mixed from multiple datasets, depending
84
+ on the `input_config`.
85
+
86
+ Args:
87
+ input_config: Configures the input pipeline. See input_pipeline_test for
88
+ examples.
89
+
90
+ Returns:
91
+ A tuple containing (possibly mixed) tf.data.Dataset and a total number of
92
+ training examples.
93
+ """
94
+ per_pipeline_configs = (
95
+ "shuffle_buffer_size", "cache_raw", "num_parallel_calls",
96
+ "pre_filter_fn", "post_filter_fn", "pack", "skip_errors")
97
+ def config_to_kw(config):
98
+ assert "filter_fn" not in config, "Deprecated; use `pre_filter_fn` instead."
99
+ return {k: config[k] for k in per_pipeline_configs if k in config}
100
+
101
+ batch_size = input_config.batch_size
102
+ # Handle separately the common case when no mixing happens.
103
+ if isinstance(input_config.data.get("name"), str):
104
+ train_data = ds_core.get(**input_config.data)
105
+ train_ds = make_for_train(
106
+ data=train_data.get_tfdata(ordered=False,
107
+ **input_config.get("tfdata", {})),
108
+ batch_size=batch_size,
109
+ preprocess_fn=pp_builder.get_preprocess_fn(input_config.get("pp")),
110
+ prefetch=input_config.get("prefetch", 2), # Default 2 for bwd compat.
111
+ **config_to_kw(input_config)
112
+ )
113
+ return train_ds, train_data.total_examples
114
+
115
+ # A helpful error instead of silent ignore:
116
+ for k in per_pipeline_configs:
117
+ assert k not in input_config, f"{k} is per-dataset in multi-input."
118
+
119
+ # Parallelize the loading of datasets when doing data mixture.
120
+ # For larger mixes, we sometimes spend >5min when doing sequentially.
121
+ # NOTE: functools.cache is thread-safe.
122
+ def _make(name_and_weight):
123
+ name, weight = name_and_weight
124
+ dataset = input_config[name]
125
+ train_data = ds_core.get(**dataset.data)
126
+ dataset = make_for_train(
127
+ data=train_data.get_tfdata(ordered=False, **dataset.get("tfdata", {})),
128
+ # Don't batch the data just yet, it will be done after
129
+ # mixing the different datasets below.
130
+ batch_size=None,
131
+ preprocess_fn=pp_builder.get_preprocess_fn(dataset.get("pp"), name),
132
+ prefetch=0, # Prefetching each pipeline leads to huge OOMs.
133
+ **config_to_kw(dataset)
134
+ )
135
+ if keys := input_config.get("keep_only"):
136
+ dataset = dataset.map(lambda d, keys=keys: {k: d[k] for k in keys})
137
+ return name, dataset, weight, train_data.total_examples
138
+
139
+ names, datasets, weights, totals = [], [], [], []
140
+ pool = multiprocessing.pool.ThreadPool(
141
+ input_config.get("thread_pool_size", len(input_config.data))
142
+ )
143
+ for name, dataset, weight, total in pool.map(
144
+ # Skip weight=0 datasets as a convenient optimization in sweeps.
145
+ _make, ((name, w) for name, w in input_config.data.items() if w)):
146
+ names.append(name)
147
+ datasets.append(dataset)
148
+ weights.append(weight)
149
+ totals.append(total)
150
+
151
+ # Normalize the weights such that they sum up to 1.
152
+ weights = [x / sum(weights) for x in weights]
153
+
154
+ logging.info(
155
+ "NOTE: Total dataset mix size: %d\nContributions:\n%s", sum(totals),
156
+ "\n".join(f"{ds}: {n} ({w * 100:.2g}%)"
157
+ for ds, n, w in zip(names, totals, weights))
158
+ )
159
+
160
+ train_ds = tf.data.Dataset.sample_from_datasets(
161
+ datasets, weights, stop_on_empty_dataset=True)
162
+ if input_config.get("pack"):
163
+ train_ds = sequence_packing.pack_dataset(
164
+ train_ds,
165
+ input_config["batch_size"] // jax.process_count(),
166
+ input_config.pack.to_dict())
167
+
168
+ train_ds = train_ds.batch(
169
+ input_config["batch_size"] // jax.process_count(), drop_remainder=True)
170
+ if (pf := input_config.get("prefetch", 2)):
171
+ train_ds = train_ds.prefetch(pf)
172
+
173
+ return train_ds, sum(totals)
174
+
175
+
176
+ # The pipeline below is used for evals in multi-{G,T}PU and multi-host settings.
177
+ # As the total number of examples may not be evenly divisible accross all
178
+ # devices, we use the `infinite tf.data padding` trick, which was suggested by
179
+ # Andreas Steiner and also implemented by him in the clu library:
180
+ # https://github.com/google/CommonLoopUtils/blob/84b777c42dfd3fb6685537138433bfeb5241a006/clu/deterministic_data.py#L304.
181
+ def make_for_inference(
182
+ data, preprocess_fn, batch_size, num_ex_per_process,
183
+ cache_raw=False, cache_final=False,
184
+ num_parallel_calls=DEFAULT_NUM_PARALLEL_CALLS, prefetch=1,
185
+ ):
186
+ """Makes an input pipeline for inference."""
187
+
188
+ data = _add_tpu_host_options(data)
189
+ data = data.cache() if cache_raw else data
190
+ data = data.map(_add_internal_fields(preprocess_fn),
191
+ num_parallel_calls=num_parallel_calls)
192
+ data = data.concatenate(_get_pad_data(data))
193
+
194
+ local_batch_size = batch_size // jax.process_count()
195
+ # This is just like `batch`, but allows batching elements of different shapes
196
+ # into a tf.RaggedTensor. Elements of the same fixed shape remain tf.Tensors.
197
+ # Since we do 'infinite' padding it is safe to drop the remainder.
198
+ data = data.ragged_batch(batch_size=local_batch_size, drop_remainder=True)
199
+
200
+ # We need to make sure that all hosts process all data and exactly the same
201
+ # number of batches. Below we take max per-host num examples and use it on all
202
+ # hosts to derive the number of batches.
203
+ num_batches = math.ceil(max(num_ex_per_process) / local_batch_size)
204
+ data = data.take(num_batches)
205
+
206
+ # Note we cache data after a finite number of batches is taken.
207
+ data = data.cache() if cache_final else data
208
+ data = data.repeat()
209
+ data = data.prefetch(prefetch) if prefetch else data
210
+ return data, num_batches
211
+
212
+
213
+ def _get_pad_data(data):
214
+ def zeros_like_spec(spec):
215
+ # For unknown/flexible dimensions (None), just use 0 instead.
216
+ return tf.zeros([x or 0 for x in spec.shape], spec.dtype)
217
+
218
+ zero = jax.tree.map(zeros_like_spec, data.element_spec)
219
+ return tf.data.Dataset.from_tensors(zero).repeat()
220
+
221
+
222
+ def _add_internal_fields(pp_fn):
223
+ """Wraps pp_fn to add _mask and _id keys."""
224
+ # Adds internal keys, that we either, in this order of preference:
225
+ # 1. keep from result of pp_fn,
226
+ # 2. carry over from raw (not pp_fn'd) example, or
227
+ # 3. add, if that makes sense.
228
+ def _pp_fn(example):
229
+ result = pp_fn(example)
230
+ # _mask will be False on padded examples (see _get_pad_data).
231
+ result.setdefault("_mask", example.get("_mask", tf.constant(True)))
232
+ # Not all data-sources can provide an ID. Only carry-over if it can:
233
+ if "_id" in example and "_id" not in result:
234
+ result["_id"] = example["_id"]
235
+ return result
236
+ return _pp_fn
237
+
238
+
239
+ def _add_tpu_host_options(data):
240
+ options = tf.data.Options()
241
+ options.threading.private_threadpool_size = 48
242
+ options.threading.max_intra_op_parallelism = 1
243
+
244
+ # Stop a whole bunch of magic stuff that eats up all RAM:
245
+ options.experimental_optimization.inject_prefetch = False
246
+
247
+ return data.with_options(options)
248
+
249
+
250
+ def prefetch_iterator(it, n):
251
+ """Runs iterator `it` ahead for `n` steps. Adapted from flax."""
252
+ if not n:
253
+ yield from it
254
+ return
255
+ queue = collections.deque()
256
+
257
+ def enqueue(n_steps): # Enqueues *up to* `n` elements from the iterator.
258
+ for data in itertools.islice(it, n_steps):
259
+ # Prefetching will parallelize any processing that happens in a different
260
+ # thread (like `jax.device_put()`), but it will be of no use for
261
+ # processing that happens in the same thread.
262
+ queue.append(data)
263
+
264
+ enqueue(n) # Fill up the buffer.
265
+ while queue:
266
+ yield queue.popleft()
267
+ enqueue(1)
268
+
269
+
270
+ def threadstart_iterator(it):
271
+ """Starts an iterator right away in a background thread."""
272
+ # We already want to "start" the iterator in order to start the underlying
273
+ # dataset prefetch mechanisms, so here we get the first element. But we don't
274
+ # want to lose it from training, so we yield that one afterwards.
275
+ # (internal link)
276
+ pool = multiprocessing.pool.ThreadPool(processes=1)
277
+ first_ex_promise = pool.apply_async(lambda: next(it))
278
+
279
+ yield first_ex_promise.get()
280
+ yield from it
281
+
282
+
283
+ def tf_to_numpy(x):
284
+ """Convert any TF types to numpy."""
285
+ if isinstance(x, tf.Tensor):
286
+ if x.dtype != tf.string: # Dense, non-string tensor? Easy!
287
+ return x.numpy()
288
+ else: # A dense string tensor? Turn into actual strings, not bytes.
289
+ return np.vectorize(bytes.decode, otypes=[str])(x.numpy())
290
+
291
+ # The rest deals with RaggedTensors, for two main reasons:
292
+ # - For strings, recursively apply the above conversion
293
+ # - For common cases (eg batch of images), return more reasonable shapes.
294
+
295
+ # Replace all None's in the shape by a fixed number, in the (somewhat common)
296
+ # case that they are marked ragged, but really all have the same shape.
297
+ real_shape = list(x.shape)
298
+ for i, s in enumerate(real_shape[1:]):
299
+ if s is not None: continue
300
+ rowlens = np.diff(x.nested_row_splits[i])
301
+ if len(set(rowlens)) == 1:
302
+ real_shape[i + 1] = rowlens[0]
303
+
304
+ if None not in real_shape:
305
+ return tf_to_numpy(x.flat_values).reshape(real_shape)
306
+
307
+ # It's actually ragged, reconstruct the array from the variable length pieces.
308
+ splits = x.row_splits.numpy()
309
+ rows = [tf_to_numpy(x.values[splits[i]:splits[i + 1]])
310
+ for i in range(len(splits) - 1)]
311
+ return np.fromiter(rows, dtype=object)
312
+
313
+
314
+ # Note that the order of global devices for sharding data is important and
315
+ # should be compatible with device order used for models params, state, etc.
316
+ def start_global(
317
+ data, global_devices, n_prefetch=1, keep_on_cpu=frozenset(), warmup=False):
318
+ """Starts the global input pipeline."""
319
+ def maybe_shard(name, x):
320
+ if name in keep_on_cpu:
321
+ return tf_to_numpy(x)
322
+ return u.make_fsarray_from_local_slice(x, global_devices)
323
+
324
+ it = iter(data)
325
+ if warmup: # actually pre-fill shuffle buffers etc.
326
+ it = threadstart_iterator(it)
327
+
328
+ it = (u.tree_map_with_names(maybe_shard, elem) for elem in it)
329
+ return prefetch_iterator(it, n_prefetch)
330
+
331
+
332
+ ##########################################################################
333
+ # The code below is pmap-specific and is deprecated, please switch to jit.
334
+ ##########################################################################
335
+
336
+
337
+ def shard_and_put(x, shard=True, put=True):
338
+ x = np.asarray(memoryview(x)) # No-copy conversion: http://(internal link)
339
+ if shard:
340
+ x = einops.rearrange(x, "(d l) ... -> d l ...", d=jax.local_device_count())
341
+ if shard and put: # Only works for pmap (for now).
342
+ x = jax.device_put_sharded(list(x), jax.local_devices())
343
+ return x
344
+
345
+
346
+ def start_input_pipeline(data, n_prefetch=1, shard=True):
347
+ fn = functools.partial(shard_and_put, shard=shard, put=n_prefetch)
348
+ it = (jax.tree.map(fn, elem) for elem in iter(data))
349
+ return prefetch_iterator(it, n_prefetch)
350
+
351
+
352
+ def start_ragged_input_pipeline(data, n_prefetch=1, shard=True, ragged=None):
353
+ def maybe_shard_and_put(name, x):
354
+ return x if name in (ragged or {}) else shard_and_put(x, shard)
355
+
356
+ it = (u.tree_map_with_names(maybe_shard_and_put, elem) for elem in iter(data))
357
+ return prefetch_iterator(it, n_prefetch)
Tipsomaly/model/big_vision/load_siglip.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from big_vision.models.proj.image_text import two_towers as model_mod
2
+
3
+ # Remember that the each module like pp should be imported under the same name
4
+ # if some other internal files use big_vision.pp and i use .pp then it run through the
5
+ # module pp twice
6
+ from big_vision.pp import builder as pp_builder
7
+ from big_vision.pp import ops_general
8
+ from big_vision.pp import ops_image
9
+ from big_vision.pp import ops_text
10
+ from big_vision.pp.proj.image_text import ops_naflex
11
+ from big_vision.pp.proj.paligemma import ops
12
+ import PIL
13
+
14
+ import jax
15
+ import jax.numpy as jnp
16
+ import ml_collections
17
+ import numpy as np
18
+
19
+ # images = [PIL.Image.open(fname) for fname in [
20
+ # 'apple-ipod.jpg',
21
+ # 'apple-blank.jpg',
22
+ # 'cold_drink.jpg',
23
+ # 'hot_drink.jpg',
24
+ # 'caffeine.jpg',
25
+ # 'siglip.jpg',
26
+ # 'authors.jpg',
27
+ # 'robosign.jpg',
28
+ # 'cow_beach.jpg',
29
+ # 'cow_beach2.jpg',
30
+ # 'mountain_view.jpg',
31
+ # ]]
32
+ # pp_img = pp_builder.get_preprocess_fn(f'resize({RES})|value_range(-1, 1)')
33
+ # imgs = np.array([pp_img({'image': np.array(image)})['image'] for image in images])
34
+ # print('imgs', imgs.shape)
35
+
36
+ class InputTransform:
37
+ def __init__(self, RES):
38
+ self.transform = pp_builder.get_preprocess_fn(f'resize({RES})|value_range(-1, 1)')
39
+
40
+ def __call__(self, img):
41
+ # print(type(img))
42
+ if np.array(img).size < 3:
43
+ raise ValueError("invalid image: fewer than 3 elements")
44
+ return np.array(self.transform({'image': np.array(img)})['image'])
45
+
46
+ class TargetTransform:
47
+ def __init__(self, RES):
48
+ self.transform = pp_builder.get_preprocess_fn(f'resize({RES})|value_range(0, 1)')
49
+
50
+ def __call__(self, img):
51
+ # print(type(img))
52
+ # print(np.expand_dims(np.array(img), axis=-1).shape)
53
+
54
+ out = np.array(self.transform({'image': np.expand_dims(np.array(img), axis=-1)})['image'])
55
+ # print(out.shape)
56
+ return np.squeeze(out, axis=-1)
57
+
58
+ def create_preprocessors_siglip2(RES):
59
+ # transform = transforms.Compose([
60
+ # Ensure3Channels(),
61
+ # transforms.Resize((image_size, image_size)),
62
+ # transforms.ToTensor(),
63
+ # transforms.Normalize(IMAGE_MEAN, IMAGE_STD),
64
+ # ])
65
+
66
+ # target_transform = transforms.Compose([
67
+ # transforms.Resize((image_size, image_size)),
68
+ # transforms.ToTensor(),
69
+ # ])
70
+ pp_input_img = InputTransform(RES)
71
+ pp_target_img = TargetTransform(RES)
72
+ return pp_input_img, pp_target_img
73
+
74
+ def input_transforms(images, RES):
75
+ pp_img = pp_builder.get_preprocess_fn(f'resize({RES})|value_range(-1, 1)')
76
+ imgs = np.array([pp_img({'image': np.array(image)})['image'] for image in images])
77
+ return imgs
78
+
79
+ def target_transforms(images, RES):
80
+ pp_img = pp_builder.get_preprocess_fn(f'resize({RES})|value_range(0, 1)')
81
+ imgs = np.array([pp_img({'image': np.array(image)})['image'] for image in images])
82
+ return imgs
83
+
84
+ def load(VARIANT, RES, ROOT_PATH='/kaggle/working/cpt/'):
85
+ CKPT = f'siglip2_{VARIANT.lower().replace("/", "")}_{RES}.npz'
86
+ TXTVARIANT, PATCH_SIZE = VARIANT.split('/')
87
+ EMBDIM = {'B': 768, 'L': 1024, 'So400m': 1152, 'g-opt': 1536}[TXTVARIANT]
88
+ # Note: The g-opt vision encoder is paired with a So400m text encoder
89
+ TXTVARIANT = 'So400m' if TXTVARIANT == 'g-opt' else TXTVARIANT
90
+ PATCH_SIZE = int(PATCH_SIZE)
91
+ VOCAB = 256_000
92
+ SEQLEN = 64
93
+
94
+ # It is significantly faster to first copy the checkpoint (30s vs 8m30 for B and 1m vs ??? for L)
95
+ # !test -f {ROOT_PATH}/{CKPT} || gsutil cp gs://big_vision/siglip2/{CKPT} {ROOT_PATH}
96
+ # print(f'{ROOT_PATH}/{CKPT} ', f'gs://big_vision/siglip2/{CKPT} ')
97
+
98
+ model_cfg = ml_collections.ConfigDict(dict(
99
+ image_model='vit',
100
+ image=dict(
101
+ pool_type='map',
102
+ scan=True,
103
+ variant=VARIANT,
104
+ ),
105
+ text_model='proj.image_text.text_transformer',
106
+ text=dict(
107
+ scan=True,
108
+ variant=TXTVARIANT,
109
+ vocab_size=256_000,
110
+ ),
111
+ out_dim=[None, EMBDIM],
112
+ bias_init=-10, # without this arg, no "b" param is added
113
+ ))
114
+ model = model_mod.Model(**model_cfg)
115
+
116
+ # Using `init_params` is slower but will lead to `load` below performing sanity-checks.
117
+ # init_params = jax.jit(model.init, backend="cpu")(jax.random.PRNGKey(42), jnp.zeros([1, RES, RES, 3], jnp.float32), jnp.zeros([1, SEQLEN], jnp.int32))['params']
118
+ init_params = None # Faster but bypasses loading sanity-checks.
119
+ params = model_mod.load(init_params, f'/{ROOT_PATH}/{CKPT}', model_cfg)
120
+
121
+ return model, params
122
+
123
+
124
+ class SigLIPTokenizer:
125
+ def __init__(self, SEQLEN):
126
+ self.pp_txt = pp_builder.get_preprocess_fn(f'lower(key="text")|tok(length={SEQLEN}, model="gemma", bos="no", eos="sticky", key="text")')
127
+
128
+ def __call__(self, texts):
129
+ """texts: str | list[str] -> np.ndarray[int] (B, L)"""
130
+ if isinstance(texts, str):
131
+ texts = [texts]
132
+ return np.array([self.pp_txt({'text': t})['text'] for t in texts])
133
+
134
+
135
+ class SigLIPImageEncoder:
136
+ def __init__(self, model, params):
137
+ self.model = model
138
+ self.params = params
139
+
140
+ def __call__(self, imgs):
141
+ return self.model.apply({'params': self.params}, imgs, None)
142
+
143
+
144
+ class SigLIPTextEncoder:
145
+ def __init__(self, model, params):
146
+ self.model = model
147
+ self.params = params
148
+
149
+ def __call__(self, text_ids, learnable_prompts=None, learning_method=None):
150
+ return self.model.apply({'params': self.params}, None, text_ids, learnable_prompts=learnable_prompts, learning_method=learning_method)
151
+
152
+ def build_siglip_modules(model_version, image_size, SEQLEN=64):
153
+ model, params = load(model_version, image_size)
154
+ tok = SigLIPTokenizer(SEQLEN)
155
+ img_enc = SigLIPImageEncoder(model, params)
156
+ txt_enc = SigLIPTextEncoder(model, params)
157
+ return img_enc, txt_enc, tok
Tipsomaly/model/big_vision/optax.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Gradient transformations and other optax utilities."""
16
+
17
+ import operator
18
+ import big_vision.utils as u
19
+ import jax
20
+ import jax.numpy as jnp
21
+ import optax
22
+
23
+
24
+ def find_states(opt_state, cls):
25
+ leaves = jax.tree.leaves(
26
+ opt_state, is_leaf=lambda node: isinstance(node, cls))
27
+ return [leaf for leaf in leaves if isinstance(leaf, cls)]
28
+
29
+
30
+ def get_count(opt_state, jittable=False):
31
+ """Returns `ScaleByScheduleState.count` from `opt_state` as an integer."""
32
+ counts = [
33
+ state.count
34
+ for state in find_states(opt_state, optax.ScaleByScheduleState)
35
+ ]
36
+ if jittable:
37
+ return counts[0]
38
+ else:
39
+ counts = {int(c) for c in counts}
40
+ assert len(counts) == 1, f"Expected exactly 1 ScaleByScheduleState:{counts}"
41
+ return next(iter(counts))
42
+
43
+
44
+ def replace_frozen(schedule, pytree, replacement, log=None):
45
+ """Replaces values matching frozen params in `pytree` with `replacement`."""
46
+ if not isinstance(schedule, (list, tuple)):
47
+ return pytree
48
+ masks, scheds = _make_mask_trees(pytree, schedule, log=log)
49
+ frozen_mask, _, _ = _split_frozen(masks, scheds)
50
+ return jax.tree.map(
51
+ lambda v, f: replacement if f else v, pytree, frozen_mask)
52
+
53
+
54
+ def clip_by_per_example_global_norm(
55
+ max_norm: float,
56
+ ) -> optax.GradientTransformation:
57
+ """Clips the norm of per-example gradients."""
58
+
59
+ def init_fn(params):
60
+ del params
61
+ return optax.EmptyState()
62
+
63
+ def update_fn(updates, state, params=None):
64
+ del params
65
+ grads_flat, grads_treedef = jax.tree_util.tree_flatten(updates)
66
+ batch_size = grads_flat[0].shape[0]
67
+ clipped, _ = optax.per_example_global_norm_clip(grads_flat, max_norm)
68
+ grads_sum = jax.tree_util.tree_unflatten(grads_treedef, clipped)
69
+ grads_mean = jax.tree_util.tree_map(lambda x: x / batch_size, grads_sum)
70
+ return grads_mean, state
71
+
72
+ return optax.GradientTransformation(init_fn, update_fn)
73
+
74
+
75
+ def make(config, params, *, sched_kw):
76
+ """Returns gradient transform and learning rate functions."""
77
+
78
+ # Global schedule. No schedule means frozen.
79
+ schedule = config.get("schedule", {})
80
+ if not isinstance(schedule, (tuple, list)):
81
+ schedule = [(".*", schedule)]
82
+ masks, scheds = _make_mask_trees(params, schedule, "config.schedule")
83
+ frozen_mask, masks, scheds = _split_frozen(masks, scheds)
84
+ not_frozen_mask = jax.tree.map(operator.not_, frozen_mask)
85
+ def create_schedule(mult=1.0, **kw):
86
+ assert "base" not in kw, kw
87
+ return u.create_learning_rate_schedule(base=mult, **kw)
88
+ schedule_fns = [create_schedule(**sched_kw, **sched) for sched in scheds]
89
+ schedule_txs = [
90
+ optax.masked(optax.scale_by_schedule(schedule_fn), mask)
91
+ for schedule_fn, mask in zip(schedule_fns, masks)
92
+ ] + [
93
+ # Removes weight decay updates. Note that weight decay already has an
94
+ # independent mask (which cannot be combined easily with a second mask),
95
+ # so instead we multiply updates for frozen params with zero.
96
+ optax.masked(optax.set_to_zero(), frozen_mask)
97
+ ]
98
+
99
+ # Gradient clipping.
100
+ if clip_norm := config.get("grad_clip_norm"):
101
+ if config.get("grad_clip_per_example"):
102
+ clip_tx = clip_by_per_example_global_norm(clip_norm)
103
+ else:
104
+ clip_tx = optax.clip_by_global_norm(clip_norm)
105
+ grad_clip_norm_tx = optax.masked(clip_tx, not_frozen_mask)
106
+ else:
107
+ grad_clip_norm_tx = optax.identity()
108
+
109
+ # Optimizer updates.
110
+ tx_func = operator.attrgetter(config.optax_name)(optax)
111
+ opt_txs = [optax.masked(tx_func(**config.get("optax", {})), not_frozen_mask)]
112
+ assert "optim" not in config, "Deprecated option, use config.optax."
113
+
114
+ # Learning rate multipliers. Defaults to 1.0.
115
+ lr_mult_txs = [optax.scale(config.lr)]
116
+ if config.get("lr_mults"):
117
+ masks, mults = _make_mask_trees(params, config.lr_mults, "config.lr_mults")
118
+ assert all(mult > 0 for mult in mults), (
119
+ f"Use schedule=None for parameter freezing instead of lr_mults={mults}")
120
+ lr_mult_txs += [
121
+ optax.masked(optax.scale(mult), mask)
122
+ for mult, mask in zip(mults, masks)
123
+ ]
124
+
125
+ # Weight decay. Defaults to 0.0.
126
+ # Weight decay is not gradient-based but instead uses "params side-input".
127
+ # Hence, weight decay is additive and independent of previous gradient-based
128
+ # updates.
129
+ assert "weight_decay" not in config, "Deprecated option. Use wd and schedule."
130
+ assert config.get("weight_decay_decouple", True), (
131
+ "Coupled weight decay not supported anymore.")
132
+ if config.get("wd"):
133
+ wd_mults = config.get("wd_mults", [(".*/kernel$", 1.0)])
134
+ masks, mults = _make_mask_trees(params, wd_mults, "config.wd_mults")
135
+ weight_decay_txs = [
136
+ optax.add_decayed_weights(config.wd * mult, mask)
137
+ for mult, mask in zip(mults, masks)
138
+ ]
139
+ else:
140
+ weight_decay_txs = []
141
+
142
+ # Combine gradient updates and learning rate schedules.
143
+ return optax.chain(
144
+ grad_clip_norm_tx,
145
+ *opt_txs,
146
+ *lr_mult_txs,
147
+ *weight_decay_txs,
148
+ *schedule_txs,
149
+ optax.scale(-1.0)), schedule_fns
150
+
151
+
152
+ def _make_mask_trees(params, patterns_values, log):
153
+ patterns, values = zip(*patterns_values)
154
+ masks = u.make_mask_trees(params, patterns, log=log)
155
+ return masks, values
156
+
157
+
158
+ def _split_frozen(masks, scheds):
159
+ """Computes `frozen_mask` and updates `masks` and `scheds`."""
160
+ # Specifying `None` as a scheduler freezes params.
161
+ all_false = jax.tree.map(lambda *bools: not any(bools), *masks)
162
+ not_covered = [k for k, v in u.tree_flatten_with_names(all_false)[0] if v]
163
+ assert not not_covered, (
164
+ f"All params must be covered (use `None` for freezing): {not_covered}")
165
+ frozen_masks = [
166
+ mask for mask, sched in zip(masks, scheds) if sched is None]
167
+ frozen_mask = jax.tree.map(
168
+ lambda *bools: any(bools), *frozen_masks,
169
+ all_false) # `all_false` is required when `frozen_masks==[]`.
170
+ masks, scheds = zip(*(
171
+ (mask, sched) for mask, sched in zip(masks, scheds) if sched is not None))
172
+ return frozen_mask, masks, scheds
173
+
174
+
175
+ ############ Custom BigVision optimizers #######################################
176
+ # Currently there's only one custom optimizer and we don't foresee new ones in
177
+ # the near future, we opt not to create a new optimizer folder/module for just
178
+ # one isolated case. If there will be more optimizers, we can consider moving
179
+ # them into individual files in a subfolder.
180
+
181
+
182
+ # A dummy object to allow for foo.bar access syntax, see
183
+ # https://stackoverflow.com/a/19476841/2366315
184
+ optax.big_vision = type("", (), {})()
185
+
186
+
187
+ def scale_by_adafactor(min_dim_size_to_factor=32,
188
+ decay_rate=0.8, decay_offset=0,
189
+ beta2_cap=0.999,
190
+ clipping_threshold=None,
191
+ momentum=0.9, dtype_momentum=jnp.bfloat16,
192
+ eps=1e-30):
193
+ """The BigVision variant of Adafactor optimizer."""
194
+
195
+ def _decay_rate_pow(i, exponent):
196
+ """Second-order moment decay schedule."""
197
+ t = jnp.array(i, jnp.float32) + 1.0
198
+ return jnp.minimum(beta2_cap, 1.0 - t**(-exponent))
199
+
200
+ scale_by_rms = optax.scale_by_factored_rms(
201
+ factored=True,
202
+ decay_rate=decay_rate,
203
+ step_offset=decay_offset,
204
+ min_dim_size_to_factor=min_dim_size_to_factor,
205
+ epsilon=eps,
206
+ decay_rate_fn=_decay_rate_pow)
207
+
208
+ clip = (optax.clip_by_block_rms(clipping_threshold) if clipping_threshold
209
+ else optax.identity())
210
+
211
+ mom = (optax.ema(momentum, debias=False, accumulator_dtype=dtype_momentum)
212
+ if momentum else optax.identity())
213
+
214
+ return optax.chain(scale_by_rms, clip, mom)
215
+
216
+ optax.big_vision.scale_by_adafactor = scale_by_adafactor # pytype: disable=module-attr
217
+
218
+
219
+ # A few more aliases we use frequently:
220
+ def momentum_hp(momentum=0.9, dtype=jnp.bfloat16, nesterov=False):
221
+ """SGD-Momentum with half-precision accumulator."""
222
+ return optax.trace(decay=momentum, accumulator_dtype=dtype, nesterov=nesterov)
223
+
224
+ optax.big_vision.momentum_hp = momentum_hp # pytype: disable=module-attr
225
+ optax.big_vision.sgd = optax.identity # pytype: disable=module-attr
Tipsomaly/model/big_vision/optax_test.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Tests for optax."""
16
+
17
+ from absl.testing import absltest
18
+ from absl.testing import parameterized
19
+ from big_vision import optax as bv_optax
20
+ import chex
21
+ import jax
22
+ import jax.numpy as jnp
23
+ import ml_collections
24
+ import numpy as np
25
+ import optax
26
+
27
+
28
+ class OptaxTest(parameterized.TestCase):
29
+
30
+ def test_get_count(self):
31
+ params = jax.tree.map(jnp.array, {"a": 1.})
32
+ tx = optax.masked(
33
+ optax.scale_by_schedule(lambda step: step),
34
+ {"a": True},
35
+ )
36
+ opt_state = tx.init(params)
37
+ self.assertEqual(bv_optax.get_count(opt_state), 0)
38
+ _, opt_state = tx.update(params, opt_state)
39
+ self.assertEqual(bv_optax.get_count(opt_state), 1)
40
+
41
+ def test_split_frozen(self):
42
+ params = jax.tree.map(jnp.array, {
43
+ "Dense_0": {"kernel": 1., "bias": 2.},
44
+ }) # pyformat: disable
45
+ sched1 = dict(decay_type="cosine")
46
+ sched2 = dict(decay_type="linear")
47
+ schedule = [
48
+ (".*/kernel", sched1),
49
+ (".*/bias", sched2),
50
+ ]
51
+ masks, scheds = bv_optax._make_mask_trees(params, schedule, log="schedule")
52
+ frozen_mask, masks, scheds = bv_optax._split_frozen(masks, scheds)
53
+ chex.assert_trees_all_equal(
54
+ frozen_mask,
55
+ {"Dense_0": {"kernel": False, "bias": False}},
56
+ ) # pyformat: disable
57
+ chex.assert_trees_all_equal(
58
+ masks,
59
+ (
60
+ {"Dense_0": {"kernel": True, "bias": False}},
61
+ {"Dense_0": {"kernel": False, "bias": True}},
62
+ ),
63
+ ) # pyformat: disable
64
+ self.assertEqual(scheds, (sched1, sched2))
65
+ # freeze some
66
+ schedule = [
67
+ (".*/bias", None),
68
+ ("Dense_0/.*", sched1),
69
+ (".*", None),
70
+ ]
71
+ masks, scheds = bv_optax._make_mask_trees(params, schedule, log="schedule")
72
+ frozen_mask, masks, scheds = bv_optax._split_frozen(masks, scheds)
73
+ chex.assert_trees_all_equal(
74
+ frozen_mask,
75
+ {"Dense_0": {"kernel": False, "bias": True}},
76
+ ) # pyformat: disable
77
+ chex.assert_trees_all_equal(
78
+ masks,
79
+ ({"Dense_0": {"kernel": True, "bias": False}},),
80
+ ) # pyformat: disable
81
+ self.assertEqual(scheds, (sched1,))
82
+ # does not cover all params - fails
83
+ schedule = [
84
+ (".*/kernel", None),
85
+ ]
86
+ masks, scheds = bv_optax._make_mask_trees(params, schedule, log="schedule")
87
+ with self.assertRaisesRegex(AssertionError, "All params must be covered"):
88
+ _ = bv_optax._split_frozen(masks, scheds)
89
+
90
+ def test_replace_frozen(self):
91
+ params = jax.tree.map(jnp.array, {
92
+ "Dense_0": {"kernel": 1., "bias": 2.},
93
+ }) # pyformat: disable
94
+ schedule = [
95
+ (".*/kernel", {}),
96
+ (".*", None),
97
+ ]
98
+ chex.assert_trees_all_equal(
99
+ bv_optax.replace_frozen(schedule, params, 0.),
100
+ {"Dense_0": {"kernel": 1., "bias": 0.}},
101
+ ) # pyformat: disable
102
+
103
+ def test_make_simple(self):
104
+ params = jax.tree.map(jnp.array, {
105
+ "Dense_0": {"kernel": 1., "bias": 2.},
106
+ }) # pyformat: disable
107
+
108
+ config = ml_collections.ConfigDict()
109
+ config.lr = 0.01
110
+ config.schedule = dict(decay_type="linear")
111
+ config.optax_name = "scale"
112
+ config.optax = ml_collections.ConfigDict()
113
+ g_scale = 0.5
114
+ config.optax.step_size = g_scale
115
+
116
+ total_steps = 10
117
+ sched_kw = dict(global_batch_size=1, total_steps=total_steps)
118
+ tx, (schedule_fn,) = bv_optax.make(config, params, sched_kw=sched_kw)
119
+ opt_state = tx.init(params)
120
+ grads = jax.tree.map(jnp.ones_like, params)
121
+ for step in range(total_steps):
122
+ updates, opt_state = tx.update(grads, opt_state)
123
+ self.assertEqual(bv_optax.get_count(opt_state), step + 1)
124
+ sched = schedule_fn(step)
125
+ np.testing.assert_almost_equal(
126
+ sched, 1.0 / total_steps * (total_steps - step))
127
+ make_tx = lambda sched: lambda g: -sched * config.lr * g_scale * g
128
+ chex.assert_trees_all_close(updates, jax.tree.map(make_tx(sched), grads))
129
+
130
+ def test_make_wd(self):
131
+ params = jax.tree.map(jnp.array, {
132
+ "Dense_0": {"kernel": 1., "bias": 2., "other": 3.},
133
+ }) # pyformat: disable
134
+ wds = jax.tree.map(jnp.array, {
135
+ "Dense_0": {"kernel": 2e-3, "bias": 5e-4, "other": 0.},
136
+ }) # pyformat: disable
137
+
138
+ config = ml_collections.ConfigDict()
139
+ config.lr = 0.01
140
+ config.wd = 1e-3
141
+ config.wd_mults = [
142
+ (".*/kernel", 2.0),
143
+ (".*/bias", 0.5),
144
+ ]
145
+ config.schedule = dict(decay_type="linear")
146
+ config.optax_name = "scale"
147
+ config.optax = ml_collections.ConfigDict()
148
+ g_scale = 0.5
149
+ config.optax.step_size = g_scale
150
+
151
+ total_steps = 10
152
+ sched_kw = dict(global_batch_size=1, total_steps=total_steps)
153
+ tx, (sched_fn,) = bv_optax.make(config, params, sched_kw=sched_kw)
154
+ opt_state = tx.init(params)
155
+ grads = jax.tree.map(jnp.ones_like, params)
156
+ for step in range(total_steps):
157
+ updates, opt_state = tx.update(grads, opt_state, params)
158
+ self.assertEqual(bv_optax.get_count(opt_state), step + 1)
159
+ sched = sched_fn(step)
160
+ np.testing.assert_almost_equal(
161
+ sched, 1.0 / total_steps * (total_steps - step))
162
+
163
+ def make_tx(sched):
164
+ def inner(p, g, wd):
165
+ return -sched * (config.lr * g_scale * g + p * wd)
166
+ return inner
167
+
168
+ chex.assert_trees_all_close(
169
+ updates, jax.tree.map(make_tx(sched), params, grads, wds))
170
+
171
+ def test_make_clip_norm(self):
172
+ params = jax.tree.map(jnp.array, {
173
+ "Dense_0": {"kernel": 1., "bias": 2., "other": 3.},
174
+ }) # pyformat: disable
175
+
176
+ config = ml_collections.ConfigDict()
177
+ config.lr = 0.01
178
+ config.schedule = dict(decay_type="linear")
179
+ config.optax_name = "scale"
180
+ config.grad_clip_norm = 1.0
181
+ config.optax = ml_collections.ConfigDict()
182
+ g_scale = 0.5
183
+ config.optax.step_size = g_scale
184
+
185
+ total_steps = 10
186
+ sched_kw = dict(global_batch_size=1, total_steps=total_steps)
187
+ tx, (sched_fn,) = bv_optax.make(config, params, sched_kw=sched_kw)
188
+ opt_state = tx.init(params)
189
+
190
+ grads = jax.tree.map(jnp.ones_like, params)
191
+ gflat = jax.tree.leaves(grads)
192
+ l2_g = jnp.sqrt(sum([jnp.vdot(p, p) for p in gflat]))
193
+ grad_clip_factor = jnp.minimum(1.0, config.grad_clip_norm / l2_g)
194
+ grads_scaled = jax.tree.map(lambda p: grad_clip_factor * p, grads)
195
+
196
+ for step in range(total_steps):
197
+ updates, opt_state = tx.update(grads, opt_state)
198
+ self.assertEqual(bv_optax.get_count(opt_state), step + 1)
199
+ sched = sched_fn(step)
200
+ np.testing.assert_almost_equal(
201
+ sched, 1.0 / total_steps * (total_steps - step))
202
+ make_tx = lambda sched: lambda g: -sched * config.lr * g_scale * g
203
+ chex.assert_trees_all_close(updates,
204
+ jax.tree.map(make_tx(sched), grads_scaled))
205
+
206
+ def test_make_multi(self):
207
+ params = jax.tree.map(
208
+ jnp.array, {
209
+ "Dense_0": {"kernel": 1.0, "bias": 2.0, "other": 3.0},
210
+ "Dense_1": {"kernel": 4.0, "bias": 5.0, "other": 6.0},
211
+ "Dense_2": {"kernel": 7.0, "bias": 8.0, "other": 9.0},
212
+ "Dense_3": {"kernel": 10., "bias": 11., "other": 12.},
213
+ }) # pyformat: disable
214
+
215
+ # Manually specify lr + wd for computing expected values.
216
+ lrb = 0.01
217
+ lr1 = 2.0
218
+ lr2 = 0.5
219
+ lr_mults = {
220
+ "Dense_0": {"kernel": lr1, "bias": lr1, "other": lr1},
221
+ "Dense_1": {"kernel": lr2, "bias": lr2, "other": lr2},
222
+ "Dense_2": {"kernel": 1.0, "bias": 1.0, "other": 1.0},
223
+ "Dense_3": {"kernel": 1.0, "bias": 1.0, "other": 1.0},
224
+ } # pyformat: disable
225
+ wdb = 1e-3
226
+ wd1 = 10.0
227
+ wd2 = 0.1
228
+ wds = jax.tree.map(
229
+ jnp.array, {
230
+ "Dense_0": {"kernel": wd1 * wdb, "bias": wd2 * wdb, "other": 0.},
231
+ "Dense_1": {"kernel": wd1 * wdb, "bias": wd2 * wdb, "other": 0.},
232
+ "Dense_2": {"kernel": wd1 * wdb, "bias": wd2 * wdb, "other": 0.},
233
+ "Dense_3": {"kernel": 0.0 * wdb, "bias": 0.0 * wdb, "other": 0.},
234
+ }) # pyformat: disable
235
+
236
+ config = ml_collections.ConfigDict()
237
+ config.lr = lrb
238
+ config.lr_mults = [
239
+ ("Dense_0/.*", lr1),
240
+ ("Dense_1/.*", lr2),
241
+ ]
242
+ config.wd = wdb
243
+ config.wd_mults = [
244
+ (".*/kernel", wd1),
245
+ (".*/bias", wd2),
246
+ ]
247
+ mult1 = 1.0
248
+ mult2 = 0.1
249
+ config.schedule = [
250
+ ("Dense_0/.*", dict(decay_type="linear", mult=mult1, linear_end=mult1)),
251
+ ("Dense_[12]/.*", dict(decay_type="linear", mult=mult2)),
252
+ (".*", None),
253
+ ]
254
+ config.optax_name = "scale"
255
+ config.grad_clip_norm = 1.0
256
+ config.optax = ml_collections.ConfigDict()
257
+ g_scale = 0.5
258
+ config.optax.step_size = g_scale
259
+
260
+ total_steps = 10
261
+ sched_kw = dict(global_batch_size=1, total_steps=total_steps)
262
+ tx, (sched_fn1,
263
+ sched_fn2) = bv_optax.make(config, params, sched_kw=sched_kw)
264
+ opt_state = tx.init(params)
265
+
266
+ # Manually specify schedules for computing expected values.
267
+ frozen_fn = lambda _: jnp.array(0.)
268
+ sched_fns = {
269
+ "Dense_0": {"kernel": sched_fn1, "bias": sched_fn1, "other": sched_fn1},
270
+ "Dense_1": {"kernel": sched_fn2, "bias": sched_fn2, "other": sched_fn2},
271
+ "Dense_2": {"kernel": sched_fn2, "bias": sched_fn2, "other": sched_fn2},
272
+ "Dense_3": {"kernel": frozen_fn, "bias": frozen_fn, "other": frozen_fn},
273
+ } # pyformat: disable
274
+
275
+ grads = jax.tree.map(jnp.ones_like, params)
276
+ gflat, _ = jax.tree.flatten(
277
+ # Don't count frozen params towards gradient norm.
278
+ jax.tree.map(lambda g, sched_fn: {frozen_fn: 0}.get(sched_fn, g),
279
+ grads, sched_fns))
280
+ l2_g = jnp.sqrt(sum([jnp.vdot(p, p) for p in gflat]))
281
+ grad_clip_factor = jnp.minimum(1.0, config.grad_clip_norm / l2_g)
282
+ grads_scaled = jax.tree.map(lambda p: grad_clip_factor * p, grads)
283
+
284
+ def make_tx(step):
285
+ def get_update(p, g, wd, sched_fn, lr_mult):
286
+ return -sched_fn(step) * (lrb * lr_mult * g_scale * g + p * wd)
287
+ return get_update
288
+
289
+ for step in range(total_steps):
290
+ updates, opt_state = tx.update(grads, opt_state, params)
291
+ self.assertEqual(bv_optax.get_count(opt_state), step + 1)
292
+ sched1, sched2 = sched_fn1(step), sched_fn2(step)
293
+ np.testing.assert_almost_equal(sched1, mult1)
294
+ np.testing.assert_almost_equal(sched2,
295
+ mult2 * (total_steps - step) / total_steps)
296
+ chex.assert_trees_all_close(
297
+ updates,
298
+ jax.tree.map(
299
+ make_tx(step), params, grads_scaled, wds, sched_fns, lr_mults))
300
+
301
+ def test_frozen_no_state(self):
302
+ params = {"small": jnp.zeros([1]), "large": jnp.zeros([1000])}
303
+ config = ml_collections.ConfigDict()
304
+ config.lr = 0.01
305
+ config.schedule = [
306
+ ("small", dict(decay_type="cosine")),
307
+ ("large", None),
308
+ ]
309
+ config.optax_name = "scale_by_adam"
310
+
311
+ sched_kw = dict(global_batch_size=1, total_steps=1)
312
+ tx, _ = bv_optax.make(config, params, sched_kw=sched_kw)
313
+
314
+ opt_state = tx.init(params)
315
+ adam_state = bv_optax.find_states(opt_state, optax.ScaleByAdamState)
316
+ nbytes = sum(
317
+ jax.tree.flatten(jax.tree.map(lambda x: x.nbytes, adam_state))[0])
318
+ self.assertLess(nbytes, 1_000)
319
+
320
+ def test_adafactor(self):
321
+ params = {"Dense_0": {"kernel": jnp.zeros([1024, 1024])}}
322
+
323
+ config = ml_collections.ConfigDict()
324
+ config.optax_name = "big_vision.scale_by_adafactor"
325
+ config.lr = 0.01
326
+ config.schedule = dict(decay_type="linear")
327
+ sched_kw = dict(global_batch_size=1, total_steps=1)
328
+
329
+ tx, _ = bv_optax.make(config, params, sched_kw=sched_kw)
330
+
331
+ opt_state = tx.init(params)
332
+ adafactor_state = bv_optax.find_states(opt_state, optax.FactoredState)
333
+ n_state_params = sum(
334
+ jax.tree.flatten(
335
+ jax.tree.map(lambda x: np.prod(
336
+ x.shape if hasattr(x, "shape") else 0), adafactor_state))[0])
337
+ self.assertEqual(n_state_params, 2 * 1024 + 2)
338
+
339
+
340
+ if __name__ == "__main__":
341
+ absltest.main()
Tipsomaly/model/big_vision/requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy>=1.26
2
+ absl-py
3
+ git+https://github.com/google/CommonLoopUtils
4
+ distrax
5
+ editdistance
6
+ einops
7
+ flax
8
+ optax
9
+ git+https://github.com/google/flaxformer
10
+ git+https://github.com/akolesnikoff/panopticapi.git@mute
11
+ overrides
12
+ protobuf
13
+ sentencepiece
14
+ tensorflow-cpu
15
+ tfds-nightly
16
+ tensorflow-text
17
+ tensorflow-gan
18
+ psutil
19
+ pycocoevalcap
Tipsomaly/model/big_vision/run_tpu.sh ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ #!/bin/bash
16
+
17
+ if [ ! -d "bv_venv" ]
18
+ then
19
+ sudo apt-get update
20
+ sudo apt install -y python3-venv
21
+ python3 -m venv bv_venv
22
+ . bv_venv/bin/activate
23
+
24
+ pip install -U pip # Yes, really needed.
25
+ # NOTE: doesn't work when in requirements.txt -> cyclic dep
26
+ pip install "jax[tpu]>=0.4.25" -f https://storage.googleapis.com/jax-releases/libtpu_releases.html
27
+ pip install -r big_vision/requirements.txt
28
+ else
29
+ . bv_venv/bin/activate
30
+ fi
31
+
32
+ if [ $# -ne 0 ]
33
+ then
34
+ env TFDS_DATA_DIR=$TFDS_DATA_DIR BV_JAX_INIT=1 python3 -m "$@"
35
+ fi
Tipsomaly/model/big_vision/sharding.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Big vision sharding utilities."""
16
+
17
+ from absl import logging
18
+
19
+ from big_vision.pp.registry import Registry
20
+ import big_vision.utils as u
21
+ import flax.linen as nn
22
+ import jax
23
+ import numpy as np
24
+
25
+
26
+ NamedSharding = jax.sharding.NamedSharding
27
+ P = jax.sharding.PartitionSpec
28
+
29
+
30
+ def _replicated(mesh):
31
+ return NamedSharding(mesh, P())
32
+
33
+
34
+ def _shard_along_axis(mesh, i, axis_name):
35
+ return NamedSharding(mesh, P(*((None,) * i + (axis_name,))))
36
+
37
+
38
+ def infer_sharding(params, strategy, mesh):
39
+ """Infers `params` sharding based on strategy.
40
+
41
+ Args:
42
+ params: a pytree of arrays.
43
+ strategy: sharding strategy.
44
+ mesh: jax device mesh.
45
+
46
+ Returns:
47
+ A pytree with shardings, that has the same shape as the `tree` argument.
48
+ """
49
+ patterns, tactics = zip(*strategy)
50
+
51
+ x_with_names, tree_def = u.tree_flatten_with_names(params)
52
+ names = tree_def.unflatten(list(zip(*x_with_names))[0])
53
+
54
+ # Follows big_vision conventions: each variable is matched at most once,
55
+ # early patterns get matching priority.
56
+ mask_trees = u.make_mask_trees(params, patterns)
57
+
58
+ specs = jax.tree.map(lambda x: (None,) * x.ndim, params)
59
+
60
+ for mask_tree, tactic in zip(mask_trees, tactics):
61
+ for op_str in tactic.split("|"):
62
+ op = Registry.lookup(f"shardings.{op_str}")()
63
+ specs = jax.tree.map(
64
+ lambda x, n, match, spec, op=op: op(spec, mesh, n, x)
65
+ if match else spec,
66
+ params, names, mask_tree, specs,
67
+ is_leaf=lambda v: isinstance(v, nn.Partitioned))
68
+
69
+ # Two-level tree_map to prevent it from doing traversal inside the spec.
70
+ specs = jax.tree.map(lambda _, spec: P(*spec), nn.unbox(params), specs)
71
+ return jax.tree.map(lambda spec: NamedSharding(mesh, spec), specs)
72
+
73
+
74
+ # Sharding rules
75
+ #
76
+ # Each rule needs to be added to the registry, can accept custom args, and
77
+ # returns a function that updates the current spec. The arguments are:
78
+ # 1. Variable name
79
+ # 2. Variable itself (or placeholder with .shape and .dtype properties)
80
+ # 3. The current sharing spec.
81
+
82
+
83
+ @Registry.register("shardings.replicate")
84
+ def replicate():
85
+ """Full replication sharding rule.
86
+
87
+ Note full replication is deafult, so this can be skipped and useful to
88
+ explicitly state in the config that certrain parameters are replicated.
89
+ TODO: can be generalized to support replication over a sub-mesh.
90
+
91
+ Returns:
92
+ A function that updates the sharding spec.
93
+ """
94
+ def _update_spec(cur_spec, mesh, name, x):
95
+ del x, mesh
96
+ if not all(axis is None for axis in cur_spec):
97
+ raise ValueError(f"Inconsistent sharding instructions: "
98
+ f"parameter {name} has spec {cur_spec}, "
99
+ f"so it can't be fully replicated.")
100
+ return cur_spec
101
+ return _update_spec
102
+
103
+
104
+ @Registry.register("shardings.fsdp")
105
+ def fsdp(axis, min_size_to_shard_mb=4):
106
+ """FSDP sharding rule.
107
+
108
+ Shards the largest dimension that is not sharded already and is divisible
109
+ by the total device count.
110
+
111
+ Args:
112
+ axis: mesh axis name for FSDP, or a collection of names.
113
+ min_size_to_shard_mb: minimal tensor size to bother with sharding.
114
+
115
+ Returns:
116
+ A function that updates the sharding spec.
117
+ """
118
+ axis = axis if isinstance(axis, str) else tuple(axis)
119
+ axis_tuple = axis if isinstance(axis, tuple) else (axis,)
120
+ def _update_spec(cur_spec, mesh, name, x):
121
+ shape = x.shape
122
+ axis_size = np.prod([mesh.shape[a] for a in axis_tuple])
123
+
124
+ if np.prod(shape) * x.dtype.itemsize <= min_size_to_shard_mb * (2 ** 20):
125
+ return cur_spec
126
+
127
+ # Partition along largest axis that is divisible and not taken.
128
+ idx = np.argsort(shape)[::-1]
129
+ for i in idx:
130
+ if shape[i] % axis_size == 0:
131
+ if cur_spec[i] is None:
132
+ return cur_spec[:i] + (axis,) + cur_spec[i+1:]
133
+
134
+ logging.info("Failed to apply `fsdp` rule to the parameter %s:%s, as all "
135
+ "its dimensions are not divisible by the requested axis: "
136
+ "%s:%i, or already occupied by other sharding rules: %s",
137
+ name, shape, axis, axis_size, cur_spec)
138
+ return cur_spec
139
+ return _update_spec
140
+
141
+
142
+ @Registry.register("shardings.logical_partitioning")
143
+ def logical_partitioning():
144
+ """Manual sharding based on Flax's logical partitioning annotations.
145
+
146
+ Uses logical sharding annotations added in model code with
147
+ `nn.with_logical_partitioning`. Respects logical to mesh name mapping rules
148
+ (typically defined in the dynamic context using
149
+ `with nn.logical_axis_rules(rules): ...`).
150
+
151
+ Returns:
152
+ A function that outputs the sharding spec of `nn.LogicallyPartitioned` boxed
153
+ specs.
154
+ """
155
+ def _update_spec(cur_spec, mesh, name, x):
156
+ del x, name, mesh
157
+ if isinstance(cur_spec, nn.LogicallyPartitioned):
158
+ return nn.logical_to_mesh_axes(cur_spec.names)
159
+ return cur_spec
160
+ return _update_spec
161
+
162
+
163
+ @Registry.register("shardings.shard_dim")
164
+ def shard_dim(axis, dim, ignore_ndim_error=False):
165
+ """Shards the given dimension along the given axis.
166
+
167
+ Args:
168
+ axis: mesh axis name for sharding.
169
+ dim: dimension to shard (can be negative).
170
+ ignore_ndim_error: if True, a warning error is logged instead of raising an
171
+ exception when the given dimension is not compatible with the number of
172
+ dimensions of the array.
173
+
174
+ Returns:
175
+ A function that updates the sharding spec.
176
+ """
177
+ def _update_spec(cur_spec, mesh, name, x):
178
+ del mesh, x
179
+ if np.abs(dim) >= len(cur_spec):
180
+ msg = f"Cannot shard_dim({axis}, {dim}): name={name} cur_spec={cur_spec}"
181
+ if ignore_ndim_error:
182
+ logging.warning(msg)
183
+ return cur_spec
184
+ else:
185
+ raise ValueError(msg)
186
+ pos_dim = dim
187
+ if pos_dim < 0:
188
+ pos_dim += len(cur_spec)
189
+ if cur_spec[pos_dim] is not None:
190
+ raise ValueError(
191
+ f"Already sharded: shard_dim({axis}, {dim}):"
192
+ f" name={name} cur_spec={cur_spec}"
193
+ )
194
+ new_spec = cur_spec[:pos_dim] + (axis,) + cur_spec[pos_dim + 1 :]
195
+ return new_spec
196
+
197
+ return _update_spec
Tipsomaly/model/big_vision/train.py ADDED
@@ -0,0 +1,518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Training loop example.
16
+
17
+ This is a basic variant of a training loop, good starting point for fancy ones.
18
+ """
19
+ # pylint: disable=consider-using-from-import
20
+ # pylint: disable=logging-fstring-interpolation
21
+
22
+ import functools
23
+ import importlib
24
+ import multiprocessing.pool
25
+ import os
26
+
27
+ from absl import app
28
+ from absl import flags
29
+ from absl import logging
30
+ import big_vision.evaluators.common as eval_common
31
+ import big_vision.input_pipeline as input_pipeline
32
+ import big_vision.optax as bv_optax
33
+ import big_vision.sharding as bv_sharding
34
+ import big_vision.utils as u
35
+ from clu import parameter_overview
36
+ import flax.linen as nn
37
+ import jax
38
+ from jax.experimental import multihost_utils
39
+ from jax.experimental.array_serialization import serialization as array_serial
40
+ from jax.experimental.shard_map import shard_map
41
+ import jax.numpy as jnp
42
+ from ml_collections import config_flags
43
+ import numpy as np
44
+ import optax
45
+ import tensorflow as tf
46
+
47
+ from tensorflow.io import gfile
48
+
49
+
50
+ config_flags.DEFINE_config_file(
51
+ "config", None, "Training configuration.", lock_config=True)
52
+
53
+ flags.DEFINE_string("workdir", default=None, help="Work unit directory.")
54
+ flags.DEFINE_boolean("cleanup", default=False,
55
+ help="Delete workdir (only) after successful completion.")
56
+
57
+ # Adds jax flags to the program.
58
+ jax.config.parse_flags_with_absl()
59
+ # Transfer guard will fail the program whenever that data between a host and
60
+ # a device is transferred implicitly. This often catches subtle bugs that
61
+ # cause slowdowns and memory fragmentation. Explicit transfers are done
62
+ # with jax.device_put and jax.device_get.
63
+ jax.config.update("jax_transfer_guard", "disallow")
64
+ # Fixes design flaw in jax.random that may cause unnecessary d2d comms.
65
+ jax.config.update("jax_threefry_partitionable", True)
66
+
67
+
68
+ NamedSharding = jax.sharding.NamedSharding
69
+ P = jax.sharding.PartitionSpec
70
+
71
+
72
+ def main(argv):
73
+ del argv
74
+
75
+ # This is needed on multihost systems, but crashes on non-TPU single-host.
76
+ if os.environ.get("BV_JAX_INIT"):
77
+ jax.distributed.initialize()
78
+
79
+ # Make sure TF does not touch GPUs.
80
+ tf.config.set_visible_devices([], "GPU")
81
+
82
+ config = flags.FLAGS.config
83
+
84
+ ################################################################################
85
+ # #
86
+ # Set up logging #
87
+ # #
88
+ ################################################################################
89
+
90
+ # Set up work directory and print welcome message.
91
+ workdir = flags.FLAGS.workdir
92
+ logging.info(
93
+ f"\u001b[33mHello from process {jax.process_index()} holding "
94
+ f"{jax.local_device_count()}/{jax.device_count()} devices and "
95
+ f"writing to workdir {workdir}.\u001b[0m")
96
+ logging.info(f"The config:\n{config}")
97
+
98
+ save_ckpt_path = None
99
+ if workdir: # Always create if requested, even if we may not write into it.
100
+ gfile.makedirs(workdir)
101
+ save_ckpt_path = os.path.join(workdir, "checkpoint.bv")
102
+
103
+ # The pool is used to perform misc operations such as logging in async way.
104
+ pool = multiprocessing.pool.ThreadPool(1)
105
+
106
+ # Here we register preprocessing ops from modules listed on `pp_modules`.
107
+ for m in config.get("pp_modules", ["ops_general", "ops_image", "ops_text"]):
108
+ importlib.import_module(f"big_vision.pp.{m}")
109
+
110
+ # Setup up logging and experiment manager.
111
+ xid, wid = -1, -1
112
+ fillin = lambda s: s
113
+ def info(s, *a):
114
+ logging.info("\u001b[33mNOTE\u001b[0m: " + s, *a)
115
+ def write_note(note):
116
+ if jax.process_index() == 0:
117
+ info("%s", note)
118
+
119
+ mw = u.BigVisionMetricWriter(xid, wid, workdir, config)
120
+
121
+ # Allow for things like timings as early as possible!
122
+ u.chrono.inform(measure=mw.measure, write_note=write_note)
123
+
124
+ ################################################################################
125
+ # #
126
+ # Set up Mesh #
127
+ # #
128
+ ################################################################################
129
+
130
+ # We rely on jax mesh_utils to organize devices, such that communication
131
+ # speed is the fastest for the last dimension, second fastest for the
132
+ # penultimate dimension, etc.
133
+ config_mesh = config.get("mesh", [("data", jax.device_count())])
134
+
135
+ # Sharding rules with default
136
+ sharding_rules = config.get("sharding_rules", [("act_batch", "data")])
137
+
138
+ write_note("Creating device mesh...")
139
+ mesh = u.create_device_mesh(
140
+ config_mesh,
141
+ allow_split_physical_axes=config.get("mesh_allow_split_physical_axes",
142
+ False))
143
+ repl_sharding = jax.sharding.NamedSharding(mesh, P())
144
+
145
+ # Consistent device order is important to ensure correctness of various train
146
+ # loop components, such as input pipeline, update step, evaluators. The
147
+ # order presribed by the `devices_flat` variable should be used throughout
148
+ # the program.
149
+ devices_flat = mesh.devices.flatten()
150
+
151
+ ################################################################################
152
+ # #
153
+ # Input Pipeline #
154
+ # #
155
+ ################################################################################
156
+
157
+ write_note("Initializing train dataset...")
158
+ batch_size = config.input.batch_size
159
+ if batch_size % jax.device_count() != 0:
160
+ raise ValueError(f"Batch size ({batch_size}) must "
161
+ f"be divisible by device number ({jax.device_count()})")
162
+ info("Global batch size %d on %d hosts results in %d local batch size. With "
163
+ "%d dev per host (%d dev total), that's a %d per-device batch size.",
164
+ batch_size, jax.process_count(), batch_size // jax.process_count(),
165
+ jax.local_device_count(), jax.device_count(),
166
+ batch_size // jax.device_count())
167
+
168
+ train_ds, ntrain_img = input_pipeline.training(config.input)
169
+
170
+ total_steps = u.steps("total", config, ntrain_img, batch_size)
171
+ def get_steps(name, default=ValueError, cfg=config):
172
+ return u.steps(name, cfg, ntrain_img, batch_size, total_steps, default)
173
+
174
+ u.chrono.inform(total_steps=total_steps, global_bs=batch_size,
175
+ steps_per_epoch=ntrain_img / batch_size)
176
+
177
+ info("Running for %d steps, that means %f epochs",
178
+ total_steps, total_steps * batch_size / ntrain_img)
179
+
180
+ # Start input pipeline as early as possible.
181
+ n_prefetch = config.get("prefetch_to_device", 1)
182
+ train_iter = input_pipeline.start_global(train_ds, devices_flat, n_prefetch)
183
+
184
+ ################################################################################
185
+ # #
186
+ # Create Model & Optimizer #
187
+ # #
188
+ ################################################################################
189
+
190
+ write_note("Creating model...")
191
+ model_mod = importlib.import_module(f"big_vision.models.{config.model_name}")
192
+ model = model_mod.Model(
193
+ num_classes=config.num_classes, **config.get("model", {}))
194
+
195
+ def init(rng):
196
+ batch = jax.tree.map(lambda x: jnp.zeros(x.shape, x.dtype.as_numpy_dtype),
197
+ train_ds.element_spec)
198
+ params = model.init(rng, batch["image"])["params"]
199
+
200
+ # Set bias in the head to a low value, such that loss is small initially.
201
+ if "init_head_bias" in config:
202
+ params["head"]["bias"] = jnp.full_like(params["head"]["bias"],
203
+ config["init_head_bias"])
204
+
205
+ return params
206
+
207
+ # This seed makes the Jax part of things (like model init) deterministic.
208
+ # However, full training still won't be deterministic, for example due to the
209
+ # tf.data pipeline not being deterministic even if we would set TF seed.
210
+ # See (internal link) for a fun read on what it takes.
211
+ rng = jax.random.PRNGKey(u.put_cpu(config.get("seed", 0)))
212
+
213
+ write_note("Inferring parameter shapes...")
214
+ rng, rng_init = jax.random.split(rng)
215
+ params_shape = jax.eval_shape(init, rng_init)
216
+
217
+ write_note("Inferring optimizer state shapes...")
218
+ tx, sched_fns = bv_optax.make(config, nn.unbox(params_shape), sched_kw=dict(
219
+ total_steps=total_steps, batch_size=batch_size, data_size=ntrain_img))
220
+ opt_shape = jax.eval_shape(tx.init, params_shape)
221
+ # We jit this, such that the arrays are created on the CPU, not device[0].
222
+ sched_fns_cpu = [u.jit_cpu()(sched_fn) for sched_fn in sched_fns]
223
+
224
+ if jax.process_index() == 0:
225
+ num_params = sum(np.prod(p.shape) for p in jax.tree.leaves(params_shape))
226
+ mw.measure("num_params", num_params)
227
+
228
+ ################################################################################
229
+ # #
230
+ # Shard & Transfer #
231
+ # #
232
+ ################################################################################
233
+
234
+ write_note("Inferring shardings...")
235
+ train_state_shape = {"params": params_shape, "opt": opt_shape}
236
+
237
+ strategy = config.get("sharding_strategy", [(".*", "replicate")])
238
+ with nn.logical_axis_rules(sharding_rules):
239
+ train_state_sharding = bv_sharding.infer_sharding(
240
+ train_state_shape, strategy=strategy, mesh=mesh)
241
+
242
+ write_note("Transferring train_state to devices...")
243
+ # RNG is always replicated
244
+ rng_init = u.reshard(rng_init, repl_sharding)
245
+
246
+ # Parameters and the optimizer are now global (distributed) jax arrays.
247
+ params = jax.jit(init, out_shardings=train_state_sharding["params"])(rng_init)
248
+ opt = jax.jit(tx.init, out_shardings=train_state_sharding["opt"])(params)
249
+
250
+ rng, rng_loop = jax.random.split(rng, 2)
251
+ rng_loop = u.reshard(rng_loop, repl_sharding)
252
+ del rng # not used anymore, so delete it.
253
+
254
+ # At this point we have everything we need to form a train state. It contains
255
+ # all the parameters that are passed and updated by the main training step.
256
+ # From here on, we have no need for Flax AxisMetadata (such as partitioning).
257
+ train_state = nn.unbox({"params": params, "opt": opt})
258
+ del params, opt # Delete to avoid memory leak or accidental reuse.
259
+
260
+ write_note("Logging parameter overview...")
261
+ parameter_overview.log_parameter_overview(
262
+ train_state["params"], msg="Init params",
263
+ include_stats="global", jax_logging_process=0)
264
+
265
+ ################################################################################
266
+ # #
267
+ # Update Step #
268
+ # #
269
+ ################################################################################
270
+
271
+ @functools.partial(
272
+ jax.jit,
273
+ donate_argnums=(0,),
274
+ out_shardings=(train_state_sharding, repl_sharding))
275
+ def update_fn(train_state, rng, batch):
276
+ """Update step."""
277
+
278
+ images, labels = batch["image"], batch["labels"]
279
+
280
+ step_count = bv_optax.get_count(train_state["opt"], jittable=True)
281
+ rng = jax.random.fold_in(rng, step_count)
282
+
283
+ if config.get("mixup") and config.mixup.p:
284
+ # The shard_map below makes mixup run on every device independently and
285
+ # thus avoids unnecessary communication.
286
+ sharded_mixup_fn = shard_map(
287
+ u.get_mixup(rng, config.mixup.p),
288
+ mesh=jax.sharding.Mesh(devices_flat, ("data",)),
289
+ in_specs=P("data"), out_specs=(P(), P("data"), P("data")))
290
+ rng, (images, labels), _ = sharded_mixup_fn(images, labels)
291
+
292
+ # Get device-specific loss rng.
293
+ rng, rng_model = jax.random.split(rng, 2)
294
+
295
+ def loss_fn(params):
296
+ logits, _ = model.apply(
297
+ {"params": params}, images,
298
+ train=True, rngs={"dropout": rng_model})
299
+ return getattr(u, config.get("loss", "sigmoid_xent"))(
300
+ logits=logits, labels=labels)
301
+
302
+ params, opt = train_state["params"], train_state["opt"]
303
+ loss, grads = jax.value_and_grad(loss_fn)(params)
304
+ updates, opt = tx.update(grads, opt, params)
305
+ params = optax.apply_updates(params, updates)
306
+
307
+ measurements = {"training_loss": loss}
308
+ gs = jax.tree.leaves(bv_optax.replace_frozen(config.schedule, grads, 0.))
309
+ measurements["l2_grads"] = jnp.sqrt(sum([jnp.sum(g * g) for g in gs]))
310
+ ps = jax.tree.leaves(params)
311
+ measurements["l2_params"] = jnp.sqrt(sum([jnp.sum(p * p) for p in ps]))
312
+ us = jax.tree.leaves(updates)
313
+ measurements["l2_updates"] = jnp.sqrt(sum([jnp.sum(u * u) for u in us]))
314
+
315
+ return {"params": params, "opt": opt}, measurements
316
+
317
+ ################################################################################
318
+ # #
319
+ # Load Checkpoint #
320
+ # #
321
+ ################################################################################
322
+
323
+ # Decide how to initialize training. The order is important.
324
+ # 1. Always resumes from the existing checkpoint, e.g. resumes a finetune job.
325
+ # 2. Resume from a previous checkpoint, e.g. start a cooldown training job.
326
+ # 3. Initialize model from something, e,g, start a fine-tuning job.
327
+ # 4. Train from scratch.
328
+ resume_ckpt_path = None
329
+ if save_ckpt_path and gfile.exists(f"{save_ckpt_path}-LAST"):
330
+ resume_ckpt_path = save_ckpt_path
331
+ elif config.get("resume"):
332
+ resume_ckpt_path = fillin(config.resume)
333
+
334
+ ckpt_mngr = None
335
+ if save_ckpt_path or resume_ckpt_path:
336
+ ckpt_mngr = array_serial.GlobalAsyncCheckpointManager()
337
+
338
+ if resume_ckpt_path:
339
+ write_note(f"Resuming training from checkpoint {resume_ckpt_path}...")
340
+ jax.tree.map(lambda x: x.delete(), train_state)
341
+ del train_state
342
+ shardings = {
343
+ **train_state_sharding,
344
+ "chrono": jax.tree.map(lambda _: repl_sharding,
345
+ u.chrono.save()),
346
+ }
347
+ loaded = u.load_checkpoint_ts(
348
+ resume_ckpt_path, tree=shardings, shardings=shardings)
349
+ train_state = {key: loaded[key] for key in train_state_sharding.keys()}
350
+
351
+ u.chrono.load(jax.device_get(loaded["chrono"]))
352
+ del loaded
353
+ elif config.get("model_init"):
354
+ write_note(f"Initialize model from {config.model_init}...")
355
+ # TODO: when updating the `load` API soon, do pass and request the
356
+ # full `train_state` from it. Examples where useful: VQVAE, BN.
357
+ train_state["params"] = model_mod.load(
358
+ train_state["params"], config.model_init, config.get("model"),
359
+ **config.get("model_load", {}))
360
+
361
+ # load has the freedom to return params not correctly sharded. Think of for
362
+ # example ViT resampling position embedings on CPU as numpy arrays.
363
+ train_state["params"] = u.reshard(
364
+ train_state["params"], train_state_sharding["params"])
365
+
366
+ parameter_overview.log_parameter_overview(
367
+ train_state["params"], msg="restored params",
368
+ include_stats="global", jax_logging_process=0)
369
+
370
+
371
+ ################################################################################
372
+ # #
373
+ # Setup Evals #
374
+ # #
375
+ ################################################################################
376
+
377
+ # We do not jit/pmap this function, because it is passed to evaluator that
378
+ # does it later. We output as many intermediate tensors as possible for
379
+ # maximal flexibility. Later `jit` will prune out things that are not needed.
380
+ def eval_logits_fn(train_state, batch):
381
+ logits, out = model.apply({"params": train_state["params"]}, batch["image"])
382
+ return logits, out
383
+
384
+ def eval_loss_fn(train_state, batch):
385
+ logits, _ = model.apply({"params": train_state["params"]}, batch["image"])
386
+ loss_fn = getattr(u, config.get("loss", "sigmoid_xent"))
387
+ return {
388
+ "loss": loss_fn(logits=logits, labels=batch["labels"], reduction=False)
389
+ }
390
+
391
+ eval_fns = {
392
+ "predict": eval_logits_fn,
393
+ "loss": eval_loss_fn,
394
+ }
395
+
396
+ # Only initialize evaluators when they are first needed.
397
+ @functools.lru_cache(maxsize=None)
398
+ def evaluators():
399
+ return eval_common.from_config(
400
+ config, eval_fns,
401
+ lambda s: write_note(f"Init evaluator: {s}…\n{u.chrono.note}"),
402
+ lambda key, cfg: get_steps(key, default=None, cfg=cfg),
403
+ devices_flat,
404
+ )
405
+
406
+ # At this point we need to know the current step to see whether to run evals.
407
+ write_note("Inferring the first step number...")
408
+ first_step_device = bv_optax.get_count(train_state["opt"], jittable=True)
409
+ first_step = int(jax.device_get(first_step_device))
410
+ u.chrono.inform(first_step=first_step)
411
+
412
+ # Note that training can be pre-empted during the final evaluation (i.e.
413
+ # just after the final checkpoint has been written to disc), in which case we
414
+ # want to run the evals.
415
+ if first_step in (total_steps, 0):
416
+ write_note("Running initial or final evals...")
417
+ mw.step_start(first_step)
418
+ for (name, evaluator, _, prefix) in evaluators():
419
+ if config.evals[name].get("skip_first") and first_step != total_steps:
420
+ continue
421
+ write_note(f"{name} evaluation...\n{u.chrono.note}")
422
+ with u.chrono.log_timing(f"z/secs/eval/{name}"):
423
+ with mesh, nn.logical_axis_rules(sharding_rules):
424
+ for key, value in evaluator.run(train_state):
425
+ mw.measure(f"{prefix}{key}", value)
426
+
427
+ ################################################################################
428
+ # #
429
+ # Train Loop #
430
+ # #
431
+ ################################################################################
432
+
433
+ prof = None # Keeps track of start/stop of profiler state.
434
+
435
+ write_note("Starting training loop, compiling the first step...")
436
+ for step, batch in zip(range(first_step + 1, total_steps + 1), train_iter):
437
+ mw.step_start(step)
438
+
439
+ with jax.profiler.StepTraceAnnotation("train_step", step_num=step):
440
+ with u.chrono.log_timing("z/secs/update0", noop=step > first_step + 1):
441
+ with mesh, nn.logical_axis_rules(sharding_rules):
442
+ train_state, measurements = update_fn(train_state, rng_loop, batch)
443
+
444
+ # On the first host, let's always profile a handful of early steps.
445
+ if jax.process_index() == 0:
446
+ prof = u.startstop_prof(prof, step, first_step, get_steps("log_training"))
447
+
448
+ # Report training progress
449
+ if (u.itstime(step, get_steps("log_training"), total_steps, host=0)
450
+ or u.chrono.warmup and jax.process_index() == 0):
451
+ for i, sched_fn_cpu in enumerate(sched_fns_cpu):
452
+ mw.measure(f"global_schedule{i if i else ''}",
453
+ sched_fn_cpu(u.put_cpu(step - 1)))
454
+ measurements = jax.device_get(measurements)
455
+ for name, value in measurements.items():
456
+ mw.measure(name, value)
457
+ u.chrono.tick(step)
458
+ for k in ("training_loss", "l2_grads", "l2_updates", "l2_params"):
459
+ if not np.isfinite(measurements.get(k, 0.0)):
460
+ raise RuntimeError(f"{k} became nan or inf somewhere within steps "
461
+ f"[{step - get_steps('log_training')}, {step}]")
462
+
463
+ # Checkpoint saving
464
+ keep_last = total_steps if get_steps("ckpt", None) else None
465
+ keep_ckpt_steps = get_steps("keep_ckpt", None) or keep_last
466
+ if save_ckpt_path and (
467
+ (keep := u.itstime(step, keep_ckpt_steps, total_steps, first=False))
468
+ or u.itstime(step, get_steps("ckpt", None), total_steps, first=True)
469
+ ):
470
+ u.chrono.pause(wait_for=train_state)
471
+
472
+ # Copy because we add extra stuff to the checkpoint.
473
+ ckpt = {**train_state}
474
+
475
+ # To save chrono state correctly and safely in a multihost setup, we
476
+ # broadcast the state to all hosts and convert it to a global array.
477
+ with jax.transfer_guard("allow"):
478
+ chrono_ckpt = multihost_utils.broadcast_one_to_all(u.chrono.save())
479
+ chrono_shardings = jax.tree.map(lambda _: repl_sharding, chrono_ckpt)
480
+ ckpt = ckpt | {"chrono": u.reshard(chrono_ckpt, chrono_shardings)}
481
+
482
+ u.save_checkpoint_ts(ckpt_mngr, ckpt, save_ckpt_path, step, keep)
483
+ u.chrono.resume()
484
+
485
+ for (name, evaluator, log_steps, prefix) in evaluators():
486
+ if u.itstime(step, log_steps, total_steps, first=False, last=True):
487
+ u.chrono.pause(wait_for=train_state)
488
+ u.chrono.tick(step) # Record things like epoch number, core hours etc.
489
+ write_note(f"{name} evaluation...\n{u.chrono.note}")
490
+ with u.chrono.log_timing(f"z/secs/eval/{name}"):
491
+ with mesh, nn.logical_axis_rules(sharding_rules):
492
+ for key, value in evaluator.run(train_state):
493
+ mw.measure(f"{prefix}{key}", jax.device_get(value))
494
+ u.chrono.resume()
495
+ mw.step_end()
496
+
497
+ # Always give a chance to stop the profiler, no matter how things ended.
498
+ # TODO: can we also do this when dying of an exception like OOM?
499
+ if jax.process_index() == 0 and prof is not None:
500
+ u.startstop_prof(prof)
501
+
502
+ # Last note needs to happen before the pool's closed =)
503
+ write_note(f"Done!\n{u.chrono.note}")
504
+
505
+ pool.close()
506
+ pool.join()
507
+ mw.close()
508
+ if ckpt_mngr:
509
+ ckpt_mngr.wait_until_finished()
510
+
511
+ # Make sure all hosts stay up until the end of main.
512
+ u.sync()
513
+
514
+ u.maybe_cleanup_workdir(workdir, flags.FLAGS.cleanup, info)
515
+
516
+
517
+ if __name__ == "__main__":
518
+ app.run(main)
Tipsomaly/model/big_vision/utils.py ADDED
@@ -0,0 +1,1478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Utils very specific to this project, not generic."""
16
+
17
+ import collections
18
+ import contextlib
19
+ import dataclasses
20
+ import functools
21
+ import io
22
+ import json
23
+ import multiprocessing
24
+ import multiprocessing.pool
25
+ import os
26
+ import re
27
+ import sys
28
+ import time
29
+ from typing import Mapping
30
+
31
+ from absl import flags
32
+ from absl import logging
33
+ from big_vision.pp import registry as pp_registry
34
+ import einops
35
+ import flax
36
+ import flax.jax_utils as flax_utils
37
+ import jax
38
+ from jax.experimental import mesh_utils
39
+ from jax.experimental.array_serialization import serialization as array_serial
40
+ import jax.numpy as jnp
41
+ import ml_collections as mlc
42
+ import numpy as np
43
+
44
+ import tensorflow.io.gfile as gfile # pylint: disable=consider-using-from-import
45
+
46
+
47
+ Registry = pp_registry.Registry
48
+
49
+
50
+ # pylint: disable=logging-fstring-interpolation
51
+
52
+
53
+ def pad_shard_unpad(wrapped, static_argnums=(0,), static_argnames=()):
54
+ """Wraps a function with code that pads, shards, then un-shards, un-pads.
55
+
56
+ Args:
57
+ wrapped: the function to be wrapped. Signature is `params, *args, *kwargs`.
58
+ static_argnums: indices of arguments to `wrapped` that should _not_ be
59
+ padded and sharded, but instead be forwarded as-is. The default is (0,)
60
+ because by far the most common use-case is to pass `params` first.
61
+ static_argnames: names of kwargs to `wrapped` that should _not_ be padded
62
+ and sharded, but instead be forwarded as-is.
63
+
64
+ Returns:
65
+ A new function that pads and shards its arguments before passing them to
66
+ the wrapped function, and un-shards and un-pads the returned pytree.
67
+
68
+ This is useful for calling a pmap'ed function with inputs that aren't
69
+ divisible by the number of devices. A typical use is:
70
+ @pad_shard_unpad
71
+ @jax.pmap
72
+ def forward(params, x): ...
73
+
74
+ Notes:
75
+ The padding is done in host-memory before being passed to the function, and
76
+ the values returned by the function are transferred back to host memory.
77
+
78
+ The returned function is augmented with a new keyword-only argument
79
+ `min_device_batch` that, if specified, forces padding inputs to at least
80
+ this size per device. This can be useful to avoid recompiles for the last
81
+ batch and reduce memory fragmentation.
82
+ """
83
+
84
+ def pad_shard_unpad_wrapper(*args, min_device_batch=None, **kw):
85
+ d = jax.local_device_count() # d = devices, b = batch
86
+
87
+ # Find the batch-sizes of all non-static arguments.
88
+ def get_bs(x):
89
+ batch_sizes = jax.tree.map(lambda y: y.shape[0], x)
90
+ return jax.tree.flatten(batch_sizes)[0]
91
+
92
+ bs_a = [get_bs(a) for i, a in enumerate(args) if i not in static_argnums]
93
+ bs_kw = [get_bs(v) for k, v in kw.items() if k not in static_argnames]
94
+ bs = set([n for b in (bs_a + bs_kw) for n in b])
95
+ assert len(bs) == 1, f"Inconsistent batch-sizes: {bs}"
96
+ b = bs.pop()
97
+
98
+ def pad(x):
99
+ _, *shape = x.shape
100
+ db, rest = divmod(b, d)
101
+ if rest:
102
+ x = np.concatenate([x, np.zeros((d - rest, *shape), x.dtype)], axis=0)
103
+ db += 1
104
+ if min_device_batch and db < min_device_batch:
105
+ x = np.concatenate(
106
+ [x, np.zeros((d * (min_device_batch - db), *shape), x.dtype)])
107
+ db = min_device_batch
108
+ return x.reshape(d, db, *shape)
109
+
110
+ def maybe_pad(x, actually_pad=True):
111
+ if not actually_pad: return x # For call-site convenience below.
112
+ return jax.tree.map(pad, x)
113
+
114
+ args = [maybe_pad(a, i not in static_argnums) for i, a in enumerate(args)]
115
+ kw = {k: maybe_pad(v, k not in static_argnames) for k, v in kw.items()}
116
+ out = wrapped(*args, **kw)
117
+
118
+ def unpad(x):
119
+ # Transfer back before cutting, to reduce on-device shape diversity.
120
+ return einops.rearrange(jax.device_get(x), "d b ... -> (d b) ...")[:b]
121
+ return jax.tree.map(unpad, out)
122
+
123
+ return pad_shard_unpad_wrapper
124
+
125
+
126
+ def onehot(labels, num_classes, on_value=1.0, off_value=0.0):
127
+ x = (labels[..., None] == jnp.arange(num_classes)[None])
128
+ x = jax.lax.select(x, jnp.full(x.shape, on_value),
129
+ jnp.full(x.shape, off_value))
130
+ return x.astype(jnp.float32)
131
+
132
+
133
+ def npload(fname):
134
+ """Loads `fname` and returns an np.ndarray or dict thereof."""
135
+ # Load the data; use local paths directly if possible:
136
+ if os.path.exists(fname):
137
+ loaded = np.load(fname, allow_pickle=False)
138
+ else:
139
+ # For other (remote) paths go via gfile+BytesIO as np.load requires seeks.
140
+ with gfile.GFile(fname, "rb") as f:
141
+ data = f.read()
142
+ loaded = np.load(io.BytesIO(data), allow_pickle=False)
143
+
144
+ # Support loading both single-array files (np.save) and zips (np.savez).
145
+ if isinstance(loaded, np.ndarray):
146
+ return loaded
147
+ else:
148
+ return dict(loaded)
149
+
150
+
151
+ def load_checkpoint_np(npz, tree=None):
152
+ """Loads a jax pytree from a npz file.
153
+
154
+ Args:
155
+ npz: Either path to the checkpoint file (.npz), or a dict-like.
156
+ tree: deprecated, use None.
157
+ Bwd-compat for old format that only stored values: the pytree structure.
158
+
159
+ Returns:
160
+ A pytree that is the checkpoint.
161
+ """
162
+ if isinstance(npz, str): # If not already loaded, then load.
163
+ npz = npload(npz)
164
+ keys, values = zip(*list(npz.items()))
165
+ if tree:
166
+ checkpoint = tree.unflatten(values)
167
+ else:
168
+ checkpoint = recover_tree(keys, values)
169
+ return checkpoint
170
+
171
+
172
+ def load_params(ckpt, **kw):
173
+ """Loads the parameters of a big_vision checkpoint, both old or new format.
174
+
175
+ Args:
176
+ ckpt: Path to the checkpoint (.npz, .ts) or dict-like.
177
+ **kw: forwarded to the underlying load function (_np or _ts).
178
+
179
+ Returns:
180
+ A pytree that is the checkpoint, potentially sharded.
181
+
182
+ Notes:
183
+ The `ckpt` string can contain an colon-separated "submodel" indicator, like
184
+ `img` in the example `/path/to/file.npz:img`.
185
+ This is used to load sub-parts of a model, for example the image load the
186
+ image encoder out of a two_tower (SigLIP) checkpoint, or distillation.
187
+ This way, ANY model that uses this function can load itself from a
188
+ checkpoint that contains multiple sub-models.
189
+ """
190
+ key = None # Whether we want to extract only a sub-key of the model.
191
+
192
+ if isinstance(ckpt, str): # Most common case of passing a checkpoint path.
193
+ # Potentially read out the sub-part to load from after the colon
194
+ # '/path/to/file:img/head' => '/path/to/file', 'img/head'
195
+ # 'gs://path/to/file' => 'gs://path/to/file', None
196
+ if match := re.match(r"^(.*?/.*?)(?::([\w/]+))?$", ckpt):
197
+ ckpt, key = match.groups()
198
+ else:
199
+ raise ValueError(f"Weird ckpt path: {ckpt} ; Maybe prepend ./ ?")
200
+
201
+ # Use the checkpoint filename to detect when we're loading old-style .npz
202
+ # checkpoints, as opposed to new-style tensorstore checkpoint folders.
203
+ if ".npz" in ckpt: # Not a perfect heuristic, but good enough.
204
+ checkpoint = load_checkpoint_np(ckpt, **kw)
205
+ checkpoint = jax.tree.map(recover_dtype, checkpoint)
206
+ if "params" in checkpoint:
207
+ # Checkpoint with optax state (after (internal link)).
208
+ params = checkpoint["params"]
209
+ elif "opt" in checkpoint:
210
+ # Checkpoint with Flax optimizer.
211
+ params = checkpoint["opt"]["target"]
212
+ else:
213
+ # When open-sourcing, we often shared only the params directly.
214
+ params = checkpoint
215
+ else:
216
+ # Here we're now loading new-style tensorstore checkpoints.
217
+ # We can be a more efficient and load params and `key` only right away.
218
+ regex = f"params/{key}($|/.*)" if key else "params/.*"
219
+ assert "regex" not in kw, "For a custom regex, use tsload directly."
220
+ kw["regex"] = regex
221
+ checkpoint = load_checkpoint_ts(ckpt, **kw)
222
+ params = checkpoint["params"]
223
+
224
+ if key is not None:
225
+ params = tree_get(params, key)
226
+
227
+ return params
228
+
229
+
230
+ def prefetch_scalar(it, nprefetch=1, devices=None):
231
+ n_loc_dev = len(devices) if devices else jax.local_device_count()
232
+ repl_iter = (np.ones(n_loc_dev) * i for i in it)
233
+ return flax_utils.prefetch_to_device(repl_iter, nprefetch, devices)
234
+
235
+
236
+ def sigmoid_xent(*, logits, labels, reduction=True):
237
+ # NOTE: This implementation is stable, see these two:
238
+ # (internal link)
239
+ # https://github.com/google/jax/issues/2140
240
+ log_p = jax.nn.log_sigmoid(logits)
241
+ log_not_p = jax.nn.log_sigmoid(-logits)
242
+ nll = -jnp.sum(labels * log_p + (1. - labels) * log_not_p, axis=-1)
243
+ return jnp.mean(nll) if reduction else nll
244
+
245
+
246
+ def bidirectional_contrastive_loss(zimg, ztxt, t, mask=None, reduction=False):
247
+ """Bidirectional contrastive loss (e.g. for contrastive trainer/evaluator)."""
248
+ # BF.FB = BB
249
+ logits = jnp.dot(zimg, ztxt.T) * t
250
+
251
+ if mask is not None:
252
+ # Set to negative infinity where mask = 0. Masked examples will disappear
253
+ # under softmax, and be ignored by ncorrect (NINF will never win argmax).
254
+ exclude = jnp.logical_not(mask) # Now 1 if we don't want to keep.
255
+ exclude = jnp.logical_or(exclude[:, None], exclude[None, :])
256
+ logits = jnp.where(exclude, -jnp.inf, logits)
257
+
258
+ # Note: assumed t is in a good range e.g. already passed through exp/softplus.
259
+ l1 = -jnp.diag(jax.nn.log_softmax(logits, axis=1)) # NLL img->txt
260
+ l2 = -jnp.diag(jax.nn.log_softmax(logits, axis=0)) # NLL txt->img
261
+ l = 0.5 * (l1 + l2)
262
+
263
+ if mask is not None:
264
+ l = jnp.where(mask, l, 0)
265
+
266
+ redux = jnp.mean if reduction else lambda x: x
267
+ if reduction and mask is not None:
268
+ redux = lambda x: jnp.sum(x * mask) / (jnp.sum(mask) + 1e-8)
269
+
270
+ # Also return extra measurements.
271
+ return redux(l), {
272
+ "ncorrect": redux(jnp.argmax(logits, axis=1) == jnp.arange(len(logits))),
273
+ }
274
+
275
+
276
+ def softmax_xent(*, logits, labels, reduction=True, kl=False, axis=-1):
277
+ log_p = jax.nn.log_softmax(logits, axis=axis)
278
+ nll = -jnp.sum(labels * log_p, axis=axis)
279
+ if kl:
280
+ nll += jnp.sum(labels * jnp.log(jnp.clip(labels, 1e-8)), axis=axis)
281
+ return jnp.mean(nll) if reduction else nll
282
+
283
+
284
+ def weighted_softmax_xent(*,
285
+ logits,
286
+ labels,
287
+ reduction=True,
288
+ weights=None,
289
+ label_smoothing=0.0,
290
+ normalize=True):
291
+ """Compute weighted cross entropy.
292
+
293
+ Args:
294
+ logits: [batch, length, num_classes] float array.
295
+ labels: categorical targets [batch, length] int array.
296
+ reduction: reduce across batch dim.
297
+ weights: None or array of shape [batch, length].
298
+ label_smoothing: label smoothing constant, used to determine the on and off
299
+ values.
300
+ normalize: normalize each "sentence" loss by the number of tokens in it.
301
+
302
+ Returns:
303
+ Tuple of scalar loss and batch normalizing factor.
304
+ """
305
+ if logits.ndim != labels.ndim + 1:
306
+ raise ValueError("Incorrect shapes. Got shape %s logits and %s targets" %
307
+ (str(logits.shape), str(labels.shape)))
308
+ vocab_size = logits.shape[-1]
309
+ confidence = 1.0 - label_smoothing
310
+ low_confidence = (1.0 - confidence) / (vocab_size - 1)
311
+ soft_targets = onehot(
312
+ labels, vocab_size, on_value=confidence, off_value=low_confidence)
313
+
314
+ loss = -jnp.sum(soft_targets * jax.nn.log_softmax(logits), axis=-1)
315
+
316
+ normalizing_factor = labels.shape[1]
317
+ if weights is not None:
318
+ loss = loss * weights
319
+ normalizing_factor = jnp.clip(weights.sum(axis=1), 2e-38)
320
+
321
+ loss = loss.sum(axis=1)
322
+ if normalize:
323
+ loss = loss / normalizing_factor
324
+
325
+ return loss.mean() if reduction else loss
326
+
327
+
328
+ def accumulate_gradient(loss_and_grad_fn, params, images, labels, accum_steps):
329
+ """Accumulate gradient over multiple steps to save on memory."""
330
+ # See (internal link) for details and experiments.
331
+ if accum_steps and accum_steps > 1:
332
+ assert images.shape[0] % accum_steps == 0, (
333
+ f"Bad accum_steps {accum_steps} for batch size {images.shape[0]}")
334
+ step_size = images.shape[0] // accum_steps
335
+ l, g = loss_and_grad_fn(params, images[:step_size], labels[:step_size])
336
+ def acc_grad_and_loss(i, l_and_g):
337
+ imgs = jax.lax.dynamic_slice(images, (i*step_size, 0, 0, 0),
338
+ (step_size,) + images.shape[1:])
339
+ lbls = jax.lax.dynamic_slice(labels, (i*step_size, 0),
340
+ (step_size, labels.shape[1]))
341
+ li, gi = loss_and_grad_fn(params, imgs, lbls)
342
+ l, g = l_and_g
343
+ return (l + li, jax.tree.map(lambda x, y: x + y, g, gi))
344
+ l, g = jax.lax.fori_loop(1, accum_steps, acc_grad_and_loss, (l, g))
345
+ return jax.tree.map(lambda x: x / accum_steps, (l, g))
346
+ else:
347
+ return loss_and_grad_fn(params, images, labels)
348
+
349
+
350
+ def itstime(step, every_n_steps, total_steps, host=None, last=True, first=True,
351
+ drop_close_to_last=0.25):
352
+ """Returns True if it's time to execute an action.
353
+
354
+ Args:
355
+ step: the current step representing "now".
356
+ every_n_steps: the action should run every this many steps.
357
+ total_steps: the step number of the last step of training.
358
+ host: host number. If provided, only run if we are this process.
359
+ last: whether to run on the last step or not.
360
+ first: whether to run on the first step or not.
361
+ drop_close_to_last: if a step would run, but is this close (in terms of
362
+ fraction of every_n_step) to the last one, skip.
363
+
364
+ Returns:
365
+ True if the action should be executed, False if not.
366
+ """
367
+
368
+ # This logic avoids running `itstime` "a few" steps before the last step.
369
+ # Canonical example: don't save checkpoint 2 steps before the last, and then
370
+ # at the last again; it's pointless and checkpoint timing will time out.
371
+ close_to_last = False
372
+ if drop_close_to_last and every_n_steps:
373
+ close_to_last = abs(step - total_steps) < drop_close_to_last * every_n_steps
374
+
375
+ is_host = host is None or jax.process_index() == host
376
+ is_step = every_n_steps and (step % every_n_steps == 0) and not close_to_last
377
+ is_last = every_n_steps and step == total_steps
378
+ is_first = every_n_steps and step == 1
379
+ return is_host and (is_step or (last and is_last) or (first and is_first))
380
+
381
+
382
+ def checkpointing_timeout(writer, timeout):
383
+ # Make sure checkpoint writing is not a bottleneck
384
+ if writer is not None:
385
+ try:
386
+ # Note: `writer` is a multiprocessing.AsyncResult, and
387
+ # timeout is in seconds.
388
+ writer.get(timeout=timeout)
389
+ except multiprocessing.TimeoutError as e:
390
+ raise TimeoutError(
391
+ "Checkpoint writing seems to be a bottleneck. Make sure you do "
392
+ "not do something wrong, like writing checkpoints to a distant "
393
+ "cell. In a case you are OK with checkpoint writing being a "
394
+ "bottleneck, you can configure `ckpt_timeout` parameter") from e
395
+
396
+
397
+ def hms(s):
398
+ """Format time in hours/minutes/seconds."""
399
+ if s < 60:
400
+ return f"{s:.0f}s"
401
+ m, s = divmod(s, 60)
402
+ if m < 60:
403
+ return f"{m:.0f}m{s:.0f}s"
404
+ h, m = divmod(m, 60)
405
+ if h < 25:
406
+ return f"{h:.0f}h{m:.0f}m" # Seconds intentionally omitted.
407
+ d, h = divmod(h, 24)
408
+ return f"{d:.0f}d{h:.0f}h{m:.0f}m" # Seconds intentionally omitted.
409
+
410
+
411
+ class Chrono:
412
+ """Measures time and reports progress, hyper-specific to our train loops.
413
+
414
+ Some concepts:
415
+ 1. This differentiates between three "types" of time:
416
+ - training time: the time spent on actual training (fprop/bprop/update)
417
+ - program time: overall time the program runs, including all overheads
418
+ - pause time: the chronometer can be paused (eg during evals).
419
+ 2. This handles a "warmup": the first step is skipped for training time
420
+ purposes, as it includes significant compilation overheads, which distort
421
+ estimates.
422
+ 3. `accum`ulates (i.e. integrates) timings, and save/load them across
423
+ restarts.
424
+ """
425
+
426
+ def __init__(self):
427
+ self._timing_history = collections.defaultdict(list)
428
+ self._measure = None
429
+ self._write_note = None
430
+
431
+ self.program_start_time = time.monotonic()
432
+ self.train_start_time = None
433
+ self.train_start_step = None # When we started timing (after warmup)
434
+
435
+ self.prev_time = None
436
+ self.prev_step = None
437
+
438
+ self.pause_start = None
439
+ self.paused_time = 0
440
+
441
+ self.total_steps = None
442
+ self.global_bs = None
443
+ self.steps_per_epoch = None
444
+
445
+ self.warmup = 2 # How many calls to `tick` to skip.
446
+ self.load() # Inits accum integrators.
447
+ self.note = "Chrono n/a"
448
+
449
+ def inform(self, *, first_step=None, total_steps=None, global_bs=None,
450
+ steps_per_epoch=None, measure=None, write_note=None):
451
+ """Provide some extra info that's only known later in the program."""
452
+ # The pattern of `self.x = x or self.x` allows one to call `inform` various
453
+ # times with various subset of information (args), as they become available.
454
+ # Except for `first_step` which can be 0 so is a bit more verbose.
455
+ self.prev_step = first_step if first_step is not None else self.prev_step
456
+ self.total_steps = total_steps or self.total_steps
457
+ self.steps_per_epoch = steps_per_epoch or self.steps_per_epoch
458
+ self.global_bs = global_bs or self.global_bs
459
+ self._measure = measure or self._measure
460
+ self._write_note = write_note or self._write_note
461
+ if self.total_steps and self.prev_step is not None:
462
+ self.note = (f"Steps:{self.prev_step}/{self.total_steps} "
463
+ f"[{self.prev_step/self.total_steps:.1%}]")
464
+
465
+ def tick(self, step, measure=None, write_note=None):
466
+ """A chronometer tick."""
467
+ if step == self.prev_step: return # Can happen from evals for example.
468
+
469
+ measure = measure or self._measure
470
+ write_note = write_note or self._write_note
471
+
472
+ now = time.monotonic()
473
+ measure("uptime", now - self.program_start_time)
474
+ self.flush_timings()
475
+
476
+ # We do always count examples, regardless of the timing-related warmup that
477
+ # happens a few lines below.
478
+ ds = step - self.prev_step # Steps between ticks
479
+ self.prev_step = step
480
+ self.accum_examples_seen += ds * self.global_bs
481
+ measure("examples_seen", self.accum_examples_seen)
482
+ measure("progress", step / self.total_steps)
483
+ if self.steps_per_epoch:
484
+ measure("epoch", step / self.steps_per_epoch)
485
+
486
+ # We take the start as the second time `tick` is called, so we avoid
487
+ # measuring the overhead of compilation and don't include it in time
488
+ # estimates.
489
+ if self.warmup > 1:
490
+ self.warmup -= 1
491
+ write_note(self.note) # This can help debugging.
492
+ return
493
+ if self.warmup == 1:
494
+ self.train_start_time = self.prev_time = now
495
+ self.train_start_step = step
496
+ self.accum_program_time += now - self.program_start_time
497
+ self.paused_time = 0 # Drop pauses that happened before timing starts.
498
+ self.warmup = 0
499
+ write_note(self.note) # This can help debugging.
500
+ return
501
+
502
+ # Measurement with micro-timings of current training steps speed.
503
+ # Time between ticks (ignoring pause)
504
+ dt = now - self.prev_time - self.paused_time
505
+ ncores = jax.device_count() # Global device count
506
+ measure("img/sec/core", self.global_bs * ds / dt / ncores)
507
+
508
+ # Accumulate (integrate) times, good for plots.
509
+ self.accum_train_time += dt
510
+ self.accum_pause_time += self.paused_time
511
+ self.accum_program_time += dt + self.paused_time
512
+
513
+ # Convert to, and log as, core hours.
514
+ core_hours = self.accum_train_time * ncores / 60 / 60
515
+ devtype = jax.devices()[0].device_kind
516
+ measure(f"core_hours_{devtype}", core_hours)
517
+ measure("core_hours", core_hours) # For convenience as x-axis in sweeps.
518
+
519
+ # Progress note with "global" full-program average timings
520
+ # (eg in program-time minus warmup)
521
+ dt = now - self.train_start_time # Time elapsed since end of warmup.
522
+ steps_timed = step - self.train_start_step
523
+ steps_todo = self.total_steps - step
524
+ self.note = f"Steps:{step}/{self.total_steps} [{step/self.total_steps:.1%}]"
525
+ self.note += f"\nWalltime:{hms(self.accum_program_time)}"
526
+ self.note += f" ({hms(self.accum_pause_time)} eval)"
527
+ self.note += f"\nETA:{hms(dt / steps_timed*steps_todo)}"
528
+ self.note += f"\nTotal train time:{hms(dt / steps_timed*self.total_steps)}"
529
+ write_note(self.note)
530
+
531
+ log_memory(measure)
532
+
533
+ self.prev_time = now
534
+ self.paused_time = 0
535
+
536
+ def pause(self, wait_for=()):
537
+ assert self.pause_start is None, "Don't pause twice."
538
+ jax.block_until_ready(wait_for)
539
+ self.pause_start = time.monotonic()
540
+
541
+ def resume(self):
542
+ self.paused_time += time.monotonic() - self.pause_start
543
+ self.pause_start = None
544
+
545
+ def save(self):
546
+ return dict(
547
+ accum_program_time=self.accum_program_time,
548
+ accum_train_time=self.accum_train_time,
549
+ accum_pause_time=self.accum_pause_time,
550
+ accum_examples_seen=self.accum_examples_seen,
551
+ )
552
+
553
+ def load(self, ckpt={}): # pylint: disable=dangerous-default-value
554
+ self.accum_program_time = float(ckpt.get("accum_program_time", 0.0))
555
+ self.accum_train_time = float(ckpt.get("accum_train_time", 0.0))
556
+ self.accum_pause_time = float(ckpt.get("accum_pause_time", 0.0))
557
+ self.accum_examples_seen = int(ckpt.get("accum_examples_seen", 0))
558
+
559
+ @contextlib.contextmanager
560
+ def log_timing(self, name, *, noop=False):
561
+ """Use this when you time sth once per step and want instant flushing."""
562
+ t0 = time.monotonic()
563
+ yield
564
+ dt = time.monotonic() - t0
565
+ if not noop:
566
+ if self._measure: # So that timed things still work in colab.
567
+ self._measure(name, dt)
568
+ logging.info("TIMING[%s]: %s", name, dt)
569
+ logging.flush()
570
+
571
+ @contextlib.contextmanager
572
+ def log_timing_avg(self, name, *, noop=False):
573
+ """Use this when you time sth multiple times per step (eg in a loop)."""
574
+ t0 = time.monotonic()
575
+ yield
576
+ dt = time.monotonic() - t0
577
+ if not noop:
578
+ self._timing_history[name].append(dt)
579
+ logging.info("TIMING[%s]: avg %s current %s",
580
+ name, np.mean(self._timing_history[name]), dt)
581
+ logging.flush()
582
+
583
+ def flush_timings(self):
584
+ assert self._measure is not None
585
+ for name, times in self._timing_history.items():
586
+ self._measure(name, np.mean(times))
587
+ self._timing_history.clear()
588
+
589
+
590
+ # Singleton to use from everywhere. https://stackoverflow.com/a/6760726/2366315
591
+ chrono = Chrono()
592
+
593
+
594
+ def log_memory(measure):
595
+ """Log a bunch of memory-related measurements."""
596
+ try:
597
+ import psutil
598
+ except ImportError:
599
+ psutil = None
600
+
601
+ if psutil is not None:
602
+ # Note that total != available + used, see psutil docs.
603
+ vmem = psutil.virtual_memory()
604
+ measure("y/hostmem/total", vmem.total)
605
+ measure("y/hostmem/available", vmem.available)
606
+ measure("y/hostmem/used", vmem.used)
607
+
608
+ # We show only device 0 and 1 to avoid spam. The reason to show two and not
609
+ # just one, if multiple are available, is because a frequent mistake is to
610
+ # create arrays on the default device, which is device 0.
611
+ for i, d in zip([0, 1], jax.local_devices()):
612
+ for k, v in (d.memory_stats() or {}).items():
613
+ measure(f"y/devmem/dev{i}/{k}", v)
614
+
615
+
616
+ def _traverse_with_names(tree, with_inner_nodes=False):
617
+ """Traverses nested dicts/dataclasses and emits (leaf_name, leaf_val)."""
618
+ if dataclasses.is_dataclass(tree):
619
+ tree = flax.serialization.to_state_dict(tree)
620
+ # Don't output the non-leaf nodes. If the optimizer doesn't have a state
621
+ # the tree leaves can be Nones which was interpreted as a leaf by this
622
+ # function but not by the other functions (like jax.tree.map).
623
+ if tree is None:
624
+ return
625
+ elif isinstance(tree, Mapping):
626
+ keys = sorted(tree.keys())
627
+ for key in keys:
628
+ for path, v in _traverse_with_names(tree[key], with_inner_nodes):
629
+ yield (key + "/" + path).rstrip("/"), v
630
+ if with_inner_nodes:
631
+ yield "", tree
632
+ elif isinstance(tree, (list, tuple)):
633
+ for idx in range(len(tree)):
634
+ for path, v in _traverse_with_names(tree[idx], with_inner_nodes):
635
+ yield (str(idx) + "/" + path).rstrip("/"), v
636
+ if with_inner_nodes:
637
+ yield "", tree
638
+ else:
639
+ yield "", tree
640
+
641
+
642
+ def tree_flatten_with_names(tree):
643
+ """Populates tree_flatten with leaf names.
644
+
645
+ This function populates output of tree_flatten with leaf names, using a
646
+ custom traversal that produces names is provided. The custom traversal does
647
+ NOT have to traverse tree in the same order as jax, as we take care of
648
+ automatically aligning jax' and custom traversals.
649
+
650
+ Args:
651
+ tree: python tree.
652
+
653
+ Returns:
654
+ A list of values with names: [(name, value), ...]
655
+ """
656
+ vals, tree_def = jax.tree.flatten(tree)
657
+
658
+ # "Fake" token tree that is use to track jax internal tree traversal and
659
+ # adjust our custom tree traversal to be compatible with it.
660
+ tokens = range(len(vals))
661
+ token_tree = tree_def.unflatten(tokens)
662
+ val_names, perm = zip(*_traverse_with_names(token_tree))
663
+ inv_perm = np.argsort(perm)
664
+
665
+ # Custom traverasal should visit the same number of leaves.
666
+ assert len(val_names) == len(vals)
667
+
668
+ return [(val_names[i], v) for i, v in zip(inv_perm, vals)], tree_def
669
+
670
+
671
+ def tree_unflatten(names_and_vals):
672
+ """Reverses `tree_flatten_with_names(tree)[0]`."""
673
+ return recover_tree(*zip(*names_and_vals))
674
+
675
+
676
+ def tree_map_with_names(f, tree, *rest):
677
+ """Like jax.tree.map but with a filter on the leaf path name.
678
+
679
+ Args:
680
+ f: A function with first parameter `name` (path-like "a/b/c") and remaining
681
+ parameters values of `tree` and `*rest` corresponding to the given `name`
682
+ Should return a new value for parameter `name`.
683
+ tree: The tree of parameters `f` should be applied to.
684
+ *rest: more trees of the exact same structure.
685
+
686
+ Returns:
687
+ A tree identical in structure to `tree` and `*rest` but with the leaves the
688
+ result of calling `f` on corresponding name/leaves in `tree` and `*rest`.
689
+ """
690
+ names_and_vals, tree_def = tree_flatten_with_names(tree)
691
+ names, vals = zip(*names_and_vals)
692
+ rest_vals = [list(zip(*tree_flatten_with_names(t)[0]))[1] for t in rest]
693
+ vals = [f(*name_and_vals) for name_and_vals in zip(names, vals, *rest_vals)]
694
+ return tree_def.unflatten(vals)
695
+
696
+
697
+ def tree_map_with_regex(f, tree, regex_rules, not_f=lambda x: x, name=None):
698
+ """Apply jax-style tree_map based on regex rules.
699
+
700
+ Args:
701
+ f: a function that is being applied to every variable.
702
+ tree: jax tree of arrays.
703
+ regex_rules: a list of tuples `(pattern, args)`, where `pattern` is a regex
704
+ which used for variable matching and `args` are positional arguments
705
+ passed to `f`. If some variable is not matched, we apply `not_f` transform
706
+ which is id by default. If multiple patterns match, then only the first
707
+ rule is applied.
708
+ not_f: optional function which is applied to variables that do not match any
709
+ pattern.
710
+ name: a name of transform for logging purposes.
711
+
712
+ Returns:
713
+ a tree, transformed by `f` according to the given rules.
714
+ """
715
+ def _f(vname, v):
716
+ for pattern, arg in regex_rules:
717
+ if re.fullmatch(pattern, vname):
718
+ if name and jax.process_index() == 0:
719
+ logging.info("Applying %s to %s with %s due to `%s`",
720
+ name, vname, arg, pattern)
721
+ return f(v, arg)
722
+ return not_f(v)
723
+ return tree_map_with_names(_f, tree)
724
+
725
+
726
+ def tree_get(tree, name):
727
+ """Get an entry of pytree by flattened key name, eg a/b/c, with nice error.
728
+
729
+ Args:
730
+ tree: the pytree to be queried.
731
+ name: the path to extract from the tree, see below for examples.
732
+
733
+ Returns:
734
+ A few examples:
735
+ tree = {'a': 1, 'b': {'c': 2, 'd': 3}}
736
+ tree_get(tree, 'a') == 1
737
+ tree_get(tree, 'b/c') == 2
738
+ tree_get(tree, 'b') == {'c': 2, 'd': 3}
739
+ """
740
+ flattened = dict(_traverse_with_names(tree, with_inner_nodes=True))
741
+ try:
742
+ return flattened[name]
743
+ except KeyError as e:
744
+ class Msg(str): # Reason: https://stackoverflow.com/a/70114007/2366315
745
+ def __repr__(self):
746
+ return str(self)
747
+ msg = "\n".join([name, "Available keys:", *flattened, ""])
748
+ # Turn into configdict to use its "did you mean?" error message!
749
+ msg = mlc.ConfigDict(flattened)._generate_did_you_mean_message(name, msg) # pylint: disable=protected-access
750
+ raise KeyError(Msg(msg)) from e
751
+
752
+
753
+ def tree_replace(tree, replacements):
754
+ """Renames/removes (nested) keys.
755
+
756
+ Example usage:
757
+
758
+ tree = {'a': {'b': 2, 'c': 3}, 'c': 4}
759
+ replacements = {
760
+ 'a/b': 'a/b/x', # replaces 'a/b' with 'a/b/x'
761
+ '.*c': 'C', # replaces 'c' with 'C' ('a/c' is removed)
762
+ 'C': 'D', # replaces 'C' (which was 'c') with 'D'
763
+ '.*/c': None, # removes 'a/c'
764
+ }
765
+ tree2 = rename_remove(tree, replacements)
766
+ assert tree2 == {'D': 4, 'a': {'b': {'x': 2}}}
767
+
768
+ Args:
769
+ tree: A nested dictionary.
770
+ replacements: Rules specifying `regex` as keys and `replacement` as values
771
+ to be used with `m = re.match(regex, key)` and `m.expand(replacement)`
772
+ for every `key` independently.
773
+
774
+ Note that:
775
+ 1. If any rule matches with `replacement=None`, then the key is removed.
776
+ 2. The rules are applied in order. It's possible to have multiple
777
+ transformations on a single key.
778
+
779
+ Returns:
780
+ Updated `tree` according to rules defined in `replacements`.
781
+ """
782
+ replacements = {
783
+ re.compile(kk): vv for kk, vv in replacements.items()
784
+ }
785
+
786
+ def rename(k):
787
+ for kk, vv in replacements.items():
788
+ m = kk.match(k)
789
+ if m:
790
+ k = k[:m.start()] + m.expand(vv) + k[m.end():]
791
+ return k
792
+
793
+ def should_remove(k):
794
+ return any(vv is None and kk.match(k) for kk, vv in replacements.items())
795
+
796
+ names_and_vals, _ = tree_flatten_with_names(tree)
797
+ names_and_vals = [
798
+ (rename(k), v) for k, v in names_and_vals if not should_remove(k)
799
+ ]
800
+ return tree_unflatten(names_and_vals)
801
+
802
+
803
+ def tree_compare(tree1, tree2):
804
+ """Returns `(tree1_only, tree2_only, dtype_shape_mismatch)`."""
805
+ tree1 = flax.traverse_util.flatten_dict(tree1, sep="/")
806
+ tree2 = flax.traverse_util.flatten_dict(tree2, sep="/")
807
+ return set(tree1) - set(tree2), set(tree2) - set(tree1), {
808
+ k: [(v.dtype, v.shape), (tree2[k].dtype, tree2[k].shape)]
809
+ for k, v in tree1.items()
810
+ if k in tree2 and (v.dtype != tree2[k].dtype or v.shape != tree2[k].shape)
811
+ }
812
+
813
+
814
+ def tree_filter(tree, mask):
815
+ """Returns nested dict structure with only a subset of children."""
816
+ # TODO: The code below only works for nested-dict and only when they
817
+ # have same structure. Consider relax this.
818
+ if not isinstance(tree, dict):
819
+ assert isinstance(mask, bool), f"Mask leaves must be boolean! {mask}"
820
+ return tree
821
+ assert sorted(tree.keys()) == sorted(mask.keys()), (
822
+ f"Keys in tree and mask are not equal! {tree.keys()} != {mask.keys()}")
823
+ return {k: tree_filter(v, mask[k]) for k, v in tree.items()
824
+ if mask[k] is not False}
825
+
826
+
827
+ def recover_dtype(a):
828
+ """Numpy's `save` stores bfloat16 type as "void" type, so we recover it."""
829
+ if hasattr(a, "dtype") and a.dtype.type is np.void:
830
+ assert a.itemsize == 2, "Unknown dtype!"
831
+ return a.view(jax.numpy.bfloat16)
832
+ else:
833
+ return a
834
+
835
+
836
+ def recover_tree(keys, values):
837
+ """Recovers a tree as a nested dict from flat names and values.
838
+
839
+ This function is useful to analyze checkpoints that are saved by our programs
840
+ without need to access the exact source code of the experiment. In particular,
841
+ it can be used to extract an reuse various subtrees of the scheckpoint, e.g.
842
+ subtree of parameters.
843
+
844
+ Args:
845
+ keys: a list of keys, where '/' is used as separator between nodes.
846
+ values: a list of leaf values.
847
+
848
+ Returns:
849
+ A nested tree-like dict.
850
+ """
851
+ tree = {}
852
+ sub_trees = collections.defaultdict(list)
853
+ for k, v in zip(keys, values):
854
+ if "/" not in k:
855
+ tree[k] = v
856
+ else:
857
+ k_left, k_right = k.split("/", 1)
858
+ sub_trees[k_left].append((k_right, v))
859
+ for k, kv_pairs in sub_trees.items():
860
+ k_subtree, v_subtree = zip(*kv_pairs)
861
+ tree[k] = recover_tree(k_subtree, v_subtree)
862
+ return tree
863
+
864
+
865
+ def tssave(mngr, pytree, path, on_commit=lambda *_, **__: None):
866
+ """Save pytree using jax tensorstore-based checkpoint manager.
867
+
868
+ NOTE: When overwriting an existing checkpoint with a different pytree, the
869
+ result is, counterintuitively, the union of both, not only the new one.
870
+
871
+ Args:
872
+ mngr: An instance of GlobalAsyncCheckpointManager.
873
+ pytree: What to store; any pytree of arrays.
874
+ path: Where to save the pytree. Creates subfolders as needed.
875
+ on_commit: A callback when writing is done, see `mngr.serialize`.
876
+ """
877
+ names, vals = zip(*tree_flatten_with_names(pytree)[0])
878
+
879
+ for name in names:
880
+ if "~" in name:
881
+ raise ValueError(f"Symbol '~' is not allowed in names. Found in {name}.")
882
+
883
+ gfile.makedirs(path)
884
+ with jax.transfer_guard("allow"):
885
+ names = [name.replace("/", "~") for name in names]
886
+ mngr.serialize_with_paths(
887
+ list(vals), [os.path.join(path, name) for name in names],
888
+ on_commit_callback=functools.partial(on_commit, array_names=names))
889
+
890
+
891
+ def save_checkpoint_ts(mngr, checkpoint, path, step, keep=True):
892
+ """Preemption-safe saving of checkpoints using tssave."""
893
+ # The tensorstore checkpoint format is a folder with (potentially) many files.
894
+ # On some file-systems, operations on these (copy, rename, delete) are slow,
895
+ # so we implement a flow that's both robust to pre-emptions/crashes during
896
+ # checkpointing and makes minimal use of these slow operations.
897
+
898
+ # The logic goes as follows. It's infaillible :)
899
+ # (...if file move is atomic, which it is.)
900
+ # We always write the current checkpoint to a new folder, which contains the
901
+ # step number in its name. If we don't need to keep it indefinitely, we append
902
+ # "-tmp" to its name.
903
+ # After writing the next checkpoint, we remove the previous one if it had
904
+ # "-tmp" in its name.
905
+ # We also have a -LAST file that contains a pointer to the latest complete
906
+ # checkpoint. File operations are cheap to make atomic, that's why.
907
+
908
+ def _on_commit_callback(array_names): # Runs after writing ckpt is done.
909
+ with gfile.GFile(f"{path}-CUR", "w") as f:
910
+ f.write(curr)
911
+
912
+ last = ""
913
+ if gfile.exists(f"{path}-LAST"):
914
+ with gfile.GFile(f"{path}-LAST", "r") as f:
915
+ last = f.read().strip()
916
+
917
+ gfile.rename(f"{path}-CUR", f"{path}-LAST", overwrite=True)
918
+
919
+ if last.endswith("-tmp"):
920
+ # If pre-emption happens here, some old checkpoints may not be deleted.
921
+ multiprocessing.pool.ThreadPool().map(
922
+ gfile.rmtree,
923
+ [f"{path}-{last}/{name}" for name in array_names])
924
+ gfile.rmtree(f"{path}-{last}")
925
+
926
+ # NOTE: The jax checkpoint manager automatically waits for the previous save
927
+ # to be finished before writing again, so we don't need to do it here.
928
+
929
+ # Always write to path with step number in it.
930
+ curr = f"{step:09d}{'-tmp' if not keep else ''}"
931
+ tssave(mngr, checkpoint, f"{path}-{curr}", _on_commit_callback)
932
+
933
+
934
+ def load_checkpoint_ts(path, **tsload_kw):
935
+ """Loads a big_vision checkpoint saved by `save_checkpoint_ts`."""
936
+ to_load = path
937
+
938
+ try:
939
+ # When passing a general path (not a specific step), get the last available.
940
+ with gfile.GFile(f"{path}-LAST", "r") as f:
941
+ to_load = f"{path}-{f.read().strip()}"
942
+ except Exception: # Differs based on backend, so blanket catch. pylint:disable=broad-exception-caught
943
+ pass
944
+
945
+ return tsload(to_load, **tsload_kw)
946
+
947
+
948
+ def tsload(path, *, tree=None, shardings=None, regex=None):
949
+ """Loads tensorstore-based array-tree from disk.
950
+
951
+ If `tree` argument is provided, then array names to load and target structure
952
+ is derived from the tree. If `tree` is None, then array names to load are
953
+ derived from array filenames on the disk, and, optionally, `regex` is applied
954
+ to filter these names. The`tree` argument is then automatically derived from
955
+ array names with `recover_tree` util.
956
+
957
+ Arrays are loaded to CPU/TPU/GPU memory as specified by the `shardings`
958
+ argument, which is a pytree of CPU/TPU/GPU shardings (can be mixed within a
959
+ single pytree). `shardings` should a prefix tree of the `tree` argument. We
960
+ automatically broadcast `shardings` to a full `tree`. For example, a user can
961
+ specify `shardings=jax.sharding.SingleDeviceSharing(jax.devices('cpu')[0])`,
962
+ which will be broadcasted to a full tree.
963
+
964
+ Args:
965
+ path: a directory where the checkpoint arrays are stored.
966
+ tree: a target pytree, which defines array names to load and the target tree
967
+ structure. If tree is None, then `tree` is inferred from the names of
968
+ arrays stored on the disk.
969
+ shardings: a prefix pytree (with respect to `tree`) of the target shardings.
970
+ regex: regex to filter array names from the disk, if `tree` is not provided.
971
+
972
+ Returns:
973
+ A pytree of loaded arrays that has the same structure as `shardings` arg.
974
+ """
975
+ if (tree is not None) and (regex is not None):
976
+ raise ValueError("If tree is specified, regex filtering is not allowed.")
977
+
978
+ if tree is None:
979
+ # Some file-systems (gs://) list folders with a trailing /, get rid of it.
980
+ path_names = set([p.rstrip("/").replace("~", "/")
981
+ for p in gfile.listdir(path)])
982
+ regex = re.compile(regex) if regex is not None else re.compile(".*")
983
+ path_names = [p for p in path_names if regex.match(p)]
984
+ tree = recover_tree(path_names, [0] * len(path_names))
985
+
986
+ names_and_vals, tree_def = tree_flatten_with_names(tree)
987
+ names_to_load, _ = zip(*names_and_vals)
988
+
989
+ if shardings is None:
990
+ shardings = jax.sharding.SingleDeviceSharding(
991
+ jax.local_devices(backend="cpu")[0]
992
+ )
993
+ shardings = list(jax.tree.leaves(tree_broadcast(shardings, tree)))
994
+
995
+ names_to_load = [os.path.join(path, name.replace("/", "~"))
996
+ for name in names_to_load]
997
+ specs = [array_serial.get_tensorstore_spec(n) for n in names_to_load]
998
+ arrays = array_serial.run_deserialization(shardings, specs, concurrent_gb=64)
999
+ return tree_def.unflatten(arrays)
1000
+
1001
+
1002
+ def steps(prefix, config, data_size=None, batch_size=None, total_steps=None,
1003
+ default=ValueError):
1004
+ """Gets duration named `prefix` out of `config` and converts it to steps.
1005
+
1006
+ Using this function to access a configuration value that denotes some kind
1007
+ of duration (eg training time, warmup, checkpoint frequency, ...) allows the
1008
+ duration to be specified in terms of steps, epochs, examples, or percent of
1009
+ training time, and converts any of these into steps, such that the training
1010
+ code only deals with steps.
1011
+ If the result is not an integer step number, it is rounded to the nearest one.
1012
+
1013
+ Args:
1014
+ prefix: The name of the duration to query. The actual config fields can
1015
+ then be one of `prefix_steps`, `prefix_examples`, or `prefix_epochs`.
1016
+ config: The dictionary (config) from which to read the duration.
1017
+ data_size: The total number of training examples in one epoch.
1018
+ batch_size: The number of examples processed per step.
1019
+ total_steps: The total number of training steps to run.
1020
+ default: The default value to return when no duration of the name `prefix`
1021
+ is found in the `config`. Set to `ValueError` (the default) to raise an
1022
+ error instead of returning a default value.
1023
+
1024
+ Returns:
1025
+ The number of steps from the config, or the default value.
1026
+
1027
+ Raises:
1028
+ ValueError if there is no such duration in the config and no default is set.
1029
+ """
1030
+ # Be helpful and make sure only match one of the following suffixes.
1031
+ suffixes = {"steps", "examples", "epochs", "percent"}
1032
+ matches = {
1033
+ f"{prefix}_{s}"
1034
+ for s in suffixes
1035
+ if (x := config.get(f"{prefix}_{s}")) is not None and x >= 0
1036
+ }
1037
+ # Note that steps=0 is also a valid value (e.g. to only run evaluators).
1038
+ assert len(matches) <= 1, f"Only one of '{matches}' should be defined."
1039
+
1040
+ if f"{prefix}_steps" in matches:
1041
+ return config[f"{prefix}_steps"]
1042
+
1043
+ def to_integer(x):
1044
+ # Round to nearest but always executed at least one step unless explictily
1045
+ # asked for 0. E.g. total_epochs=0 vs total_epochs=0.0001
1046
+ return max(1, round(x)) if x else 0
1047
+
1048
+ if batch_size and f"{prefix}_examples" in matches:
1049
+ return to_integer(config[f"{prefix}_examples"] / batch_size)
1050
+
1051
+ if batch_size and data_size and f"{prefix}_epochs" in matches:
1052
+ steps_per_epoch = data_size / batch_size
1053
+ return to_integer(config[f"{prefix}_epochs"] * steps_per_epoch)
1054
+
1055
+ if total_steps and f"{prefix}_percent" in matches:
1056
+ pct = config[f"{prefix}_percent"]
1057
+ assert 0.0 <= pct <= 1.0, ( # Be helpful, since it's not obvious.
1058
+ f"Percents should lie in [0.0, 1.0], but {prefix}_percent is {pct}")
1059
+ return to_integer(pct * total_steps)
1060
+
1061
+ if default is ValueError:
1062
+ raise ValueError(
1063
+ f"Cannot convert {prefix} to steps, due to missing batch_size "
1064
+ f"({batch_size}), data_size ({data_size}), total_steps ({total_steps})"
1065
+ ", or corresponding entry in config:\n" + "\n".join(config.keys()))
1066
+
1067
+ return default
1068
+
1069
+
1070
+ def create_learning_rate_schedule(
1071
+ total_steps, batch_size=None, data_size=None,
1072
+ base=1.0, decay_type="stair",
1073
+ scale_with_batchsize=False, **kw):
1074
+ """Creates learning rate schedule, see (internal link).
1075
+
1076
+ Args:
1077
+ total_steps: The total number of steps to run.
1078
+ batch_size: The global batch-size optionally used for scaling.
1079
+ data_size: Number of examples in the training data (for epoch conversion).
1080
+ base: The starting learning-rate (without warmup).
1081
+ decay_type: 'linear' or 'cosine', 'rsqrt', 'stair'.
1082
+ scale_with_batchsize: Whether or not to scale lr automatically.
1083
+ **kw: extra arguments specific to individual decay_types. Also contains
1084
+ declaration of `{warmup,cooldown}_{steps,epochs,examples}` that applies
1085
+ on top of any/all decay_type.
1086
+
1087
+ Returns:
1088
+ A function learning_rate(step): float -> {"learning_rate": float}.
1089
+ """
1090
+
1091
+ def to_steps(name, default=0):
1092
+ return steps(name, kw, data_size, batch_size, total_steps, default=default)
1093
+
1094
+ warmup_steps = to_steps("warmup")
1095
+ cooldown_steps = to_steps("cooldown")
1096
+
1097
+ # Early catch hard to backtrack errors due to warmup_steps >= total_steps,
1098
+ # but let it run for 0 and 1 steps used to eval and debug runs.
1099
+ assert (total_steps <= 1) or (warmup_steps < total_steps), (
1100
+ "warmup_steps is >= total_steps")
1101
+
1102
+ def step_fn(step):
1103
+ """Step to learning rate function."""
1104
+ lr = base
1105
+
1106
+ # This implements the linear scaling rule following
1107
+ # Goyal et al. at arxiv.org/abs/1706.02677.
1108
+ # The reference batch size in literature is 256, so we scale the lr to
1109
+ # adjust to the literature lr when bach_size changes.
1110
+ if scale_with_batchsize:
1111
+ lr = lr * batch_size / 256.0
1112
+
1113
+ progress = (step - warmup_steps) / float(total_steps - warmup_steps)
1114
+ progress = jnp.clip(progress, 0.0, 1.0)
1115
+ if decay_type in ("linear", "polynomial"):
1116
+ power = kw.get("power", 1)
1117
+ zero = kw.get("end", kw.get("linear_end", 0))
1118
+ lr = zero + (lr - zero) * (1.0 - progress) ** power
1119
+ elif decay_type == "cosine":
1120
+ lr = lr * 0.5 * (1. + jnp.cos(jnp.pi * progress))
1121
+ elif decay_type == "rsqrt":
1122
+ # See (internal link) for details, especially how to set timescale
1123
+ # and shift in order to continue smoothly when changing batch-size.
1124
+ t = to_steps("timescale", default=kw.get("timescale", 10_000))
1125
+ shift = to_steps("shift", default=kw.get("shift", 0))
1126
+ lr = jnp.where(
1127
+ warmup_steps <= step,
1128
+ lr / jnp.sqrt(1 + (step + shift - warmup_steps) / t), # In decay
1129
+ lr / jnp.sqrt(1 + shift / t)) # In warmup.
1130
+ elif decay_type == "stair":
1131
+ i = jnp.searchsorted(jnp.array(kw.get("steps", [])), step + 1)
1132
+ lr = lr * jnp.take(jnp.array([1.0] + list(kw.get("mults", []))), i)
1133
+ else:
1134
+ raise ValueError(f"Unknown lr type {decay_type}")
1135
+
1136
+ if warmup_steps:
1137
+ lr = lr * jnp.minimum(1., step / warmup_steps)
1138
+ if cooldown_steps:
1139
+ lr = lr * jnp.minimum(1., (total_steps - step) / cooldown_steps)
1140
+
1141
+ return jnp.asarray(lr, dtype=jnp.float32)
1142
+
1143
+ return step_fn
1144
+
1145
+
1146
+ def get_mixup(rng, p):
1147
+ """Perform mixup https://arxiv.org/abs/1710.09412."""
1148
+ rng, rng_mixup = jax.random.split(rng)
1149
+ a = jax.random.beta(rng_mixup, p, p)
1150
+ a = jnp.maximum(a, 1.0 - a) # see (internal link) for the context.
1151
+ def _mixup(*things, **more_things):
1152
+ mix = lambda thing: a * thing + (1 - a) * jnp.roll(thing, shift=1, axis=0)
1153
+ return rng, *jax.tree.map(mix, (things, more_things))
1154
+ return _mixup
1155
+
1156
+
1157
+ # For backwards compatability with legacy code.
1158
+ def mixup(rng, *things, p, **more_things):
1159
+ return get_mixup(rng, p)(*things, **more_things)
1160
+
1161
+
1162
+ def sync():
1163
+ """Syncs hosts and empties async computation queue."""
1164
+ x = reshard(np.ones(jax.device_count()),
1165
+ jax.sharding.PositionalSharding(jax.devices()))
1166
+ jax.jit(jnp.sum)(x).block_until_ready()
1167
+
1168
+
1169
+ def check_and_compile_patterns(patterns):
1170
+ """Validates and compiles a list of param-patterns.
1171
+
1172
+ The validation consists of checking for common mistakes, currently only that
1173
+ the pattern does not start with a slash, because unlike FLAX, our parameter
1174
+ names don't start with a slash.
1175
+
1176
+ Args:
1177
+ patterns: a single (string) pattern (regex), or a list of patterns.
1178
+
1179
+ Returns:
1180
+ A list of compiled and verified regexes.
1181
+ """
1182
+ if isinstance(patterns, str):
1183
+ patterns = [patterns]
1184
+
1185
+ assert isinstance(patterns, (list, tuple)), patterns
1186
+
1187
+ def check_and_compile(pattern):
1188
+ assert not pattern.startswith("/"), (
1189
+ f"Big vision parameter names never start with '/': '{pattern}")
1190
+ return re.compile(pattern)
1191
+
1192
+ return list(map(check_and_compile, patterns))
1193
+
1194
+
1195
+ def make_mask_trees(tree, patterns, *, log=None):
1196
+ """Returns a boolean mask tree for every pattern (only first match)."""
1197
+ compiled_patterns = check_and_compile_patterns(patterns)
1198
+
1199
+ def matchfirst(name, _):
1200
+ matches = []
1201
+ for pattern in compiled_patterns:
1202
+ matches.append(not any(matches) and bool(pattern.fullmatch(name)))
1203
+ if log is not None and True in matches and jax.process_index() == 0:
1204
+ logging.info("%s: %s - matched by %s", log, name,
1205
+ patterns[matches.index(True)])
1206
+ return np.array(matches)
1207
+
1208
+ multimask = tree_map_with_names(matchfirst, tree)
1209
+ return [
1210
+ jax.tree.map(lambda matches, i=idx: matches[i], multimask)
1211
+ for idx in range(len(patterns))
1212
+ ]
1213
+
1214
+
1215
+ @contextlib.contextmanager
1216
+ def profile(name, ttl=3 * 365 * 24 * 3600, noop=False):
1217
+ if not noop:
1218
+ sess = startstop_prof_at_steps(None, name=name, ttl=ttl)
1219
+ yield
1220
+ if not noop:
1221
+ startstop_prof_at_steps(sess, name=name, ttl=ttl)
1222
+
1223
+
1224
+ def startstop_prof(sess, step=None, first_step=0,
1225
+ log_steps=1, surround=10, **kw):
1226
+ """Runs the profiler for `surround` steps around the next `log_steps`."""
1227
+ first_log = first_step + log_steps - (first_step % log_steps)
1228
+ # don't start before first!
1229
+ start = max(first_log - surround//2, first_step + 1)
1230
+ return startstop_prof_at_steps(sess, step, start, start + surround, **kw)
1231
+
1232
+
1233
+ def startstop_prof_at_steps(
1234
+ sess, step=None, first_step=None, last_step=None,
1235
+ name="steps", ttl=3 * 365 * 24 * 3600):
1236
+ del sess, step, first_step, last_step, name, ttl
1237
+ pass # TODO: implement using `jax.profiler` API. Needs workdir.
1238
+
1239
+
1240
+ # This is a very minimal variant for open-sourcing. Our internal code makes use
1241
+ # of multiple internal logging tools instead.
1242
+ class BigVisionMetricWriter:
1243
+ """A class for logging metrics."""
1244
+
1245
+ def __init__(self, xid=-1, wid=-1, workdir=None, config=None):
1246
+ self.step_start(0)
1247
+ if jax.process_index() != 0: return # Only one host shall write stuff.
1248
+
1249
+ self.pool = multiprocessing.pool.ThreadPool(1) # 1 is important here.
1250
+ self.fname = None
1251
+ if workdir:
1252
+ if xid != -1 and wid != -1:
1253
+ self.fname = os.path.join(workdir,
1254
+ f"big_vision_{xid}_{wid}_metrics.txt")
1255
+ else:
1256
+ self.fname = os.path.join(workdir, "big_vision_metrics.txt")
1257
+ if config:
1258
+ with gfile.GFile(os.path.join(workdir, "config.json"), "w") as f:
1259
+ f.write(config.to_json())
1260
+
1261
+ def step_start(self, step):
1262
+ self.step = step
1263
+ self.step_metrics = {}
1264
+
1265
+ def measure(self, name, value):
1266
+ """Logs the metric value."""
1267
+ if jax.process_index() != 0: return # Only one host shall write stuff.
1268
+
1269
+ # Convenience for accepting scalar np/DeviceArrays, as well as N-d single
1270
+ # scalars, like [[[123]]] or similar, avoiding silly mistakes.
1271
+ value = np.array(value).squeeze()
1272
+
1273
+ # If the value is a scalar, we keep it in mind to append a line to the logs.
1274
+ # If it has any structure, we instead just log its shape.
1275
+ value = float(value) if value.ndim == 0 else value.shape
1276
+
1277
+ logging.info(f"\u001b[35m[{self.step}]\u001b[0m {name} = {value}")
1278
+ logging.flush()
1279
+ self.step_metrics[name] = value
1280
+
1281
+ return value # Just for convenience
1282
+
1283
+ def step_end(self):
1284
+ """Ends a training step, write its full row."""
1285
+ if not self.step_metrics: return
1286
+
1287
+ def write(metrics):
1288
+ with gfile.GFile(self.fname, "a") as f:
1289
+ f.write(json.dumps({"step": self.step, **metrics}) + "\n")
1290
+
1291
+ if self.fname:
1292
+ self.pool.apply(lambda: None) # Potentially wait for past writes.
1293
+ self.pool.apply_async(write, (self.step_metrics,))
1294
+
1295
+ def close(self):
1296
+ self.step_end()
1297
+ if jax.process_index() == 0:
1298
+ self.pool.close()
1299
+ self.pool.join()
1300
+
1301
+
1302
+ def maybe_cleanup_workdir(workdir, cleanup, info):
1303
+ """Potentially removes workdirs at end of run for cleanup."""
1304
+ if not workdir:
1305
+ return
1306
+
1307
+ if not cleanup:
1308
+ info("Logs/checkpoints are in %s", workdir)
1309
+ elif jax.process_index() == 0:
1310
+ gfile.rmtree(workdir)
1311
+ try: # Only need this on the last work-unit, if already empty.
1312
+ gfile.remove(os.path.join(workdir, ".."))
1313
+ except tf.errors.OpError:
1314
+ pass
1315
+
1316
+
1317
+ def tree_broadcast(prefix, target):
1318
+ """Broadcasts a prefix tree to a full tree.
1319
+
1320
+ Input-output examples:
1321
+ 1. prefix: {"x": 10, "y": 20}
1322
+ target: {"x": {"a": 1, "b": 2}, "y": 3}
1323
+
1324
+ Result: {"x": {"a": 10, "b": 10}, "y": 20}
1325
+
1326
+ 2. prefix: 100
1327
+ target: {"x": {"a": 1, "b": 2}, "y": 3}
1328
+
1329
+ Result: {"x": {"a": 100, "b": 100}, "y": 100}
1330
+
1331
+ 3. prefix: {"x": 10}
1332
+ target: {"x": {"a": 1, "b": 2}, "y": 3}
1333
+
1334
+ Result: ValueError
1335
+
1336
+ Args:
1337
+ prefix: prefix pytree.
1338
+ target: boradcast target for a prefix tree.
1339
+
1340
+ Returns:
1341
+ prefix tree broadcasted to a target tree.
1342
+ """
1343
+ def _broadcast(leaf, subtree):
1344
+ return jax.tree.map(lambda _: leaf, subtree)
1345
+ return jax.tree.map(_broadcast, prefix, target)
1346
+
1347
+
1348
+ def reshard(tree, shardings):
1349
+ """Take an arbitrarily* sharded pytree and shard it according to `shardings`.
1350
+
1351
+ This is a no-op for tree elements which are already sharded as requested.
1352
+
1353
+ *Arrays that are fully addressable (for example, CPU arrays) are assumed to be
1354
+ identical (i.e. replicated) across hosts.
1355
+
1356
+ *It does not work if an element of `tree` is not fully-addressable, unless its
1357
+ sharding is already consistent with the target sharding.
1358
+ If this is needed, please ping lbeyer@ or akolesnikov@.
1359
+
1360
+ Args:
1361
+ tree: a pytree of arrays.
1362
+ shardings: a (prefix) pytree of jax array shardings.
1363
+ Returns:
1364
+ A pytree of global jax arrays that follows provided shardings.
1365
+ """
1366
+ def _make_global_arr(x, shard, shape):
1367
+ # Avoid unnecessary copies and transfers:
1368
+ if hasattr(x, "sharding") and x.sharding.is_equivalent_to(shard, len(shape)): # pylint: disable=line-too-long
1369
+ return x
1370
+ if not getattr(x, "is_fully_addressable", True):
1371
+ raise RuntimeError("Trying to reshard a non-fully-addressable array. "
1372
+ "Please see the doc-comment for detailed explanation.")
1373
+ x = jax.device_get(x) # Might be on local devices.
1374
+ xs = [jax.device_put(x[s], device=d)
1375
+ for d, s in shard.addressable_devices_indices_map(shape).items()]
1376
+ return jax.make_array_from_single_device_arrays(shape, shard, xs)
1377
+
1378
+ shapes = jax.tree.map(np.shape, tree)
1379
+ shardings = tree_broadcast(shardings, tree)
1380
+ return jax.tree.map(_make_global_arr, tree, shardings, shapes)
1381
+
1382
+
1383
+ def put_cpu(x):
1384
+ """Places array/pytree on a CPU device."""
1385
+ return jax.device_put(x, jax.local_devices(backend="cpu")[0])
1386
+
1387
+
1388
+ def make_fsarray_from_local_slice(local_slice, global_devices):
1389
+ """Create a fully-sharded global device array from local host arrays.
1390
+
1391
+ Args:
1392
+ local_slice: Something convertible to a numpy array (eg also TF tensors)
1393
+ that is this host's slice of the global array.
1394
+ global_devices: The list of global devices. Needed for consistent ordering.
1395
+
1396
+ Returns:
1397
+ The global on-device array which consists of all local slices stacked
1398
+ together in the order consistent with the devices.
1399
+ """
1400
+ mesh = jax.sharding.Mesh(global_devices, ("devices",))
1401
+ sharding = jax.sharding.NamedSharding(
1402
+ mesh, jax.sharding.PartitionSpec("devices"))
1403
+ local_ds = mesh.local_devices
1404
+
1405
+ x = np.asarray(memoryview(local_slice)) # No-copy: http://(internal link)
1406
+ xs = jax.device_put(np.split(x, len(local_ds), axis=0), local_ds)
1407
+
1408
+ global_shape = (x.shape[0] * jax.process_count(), *x.shape[1:])
1409
+ return jax.make_array_from_single_device_arrays(global_shape, sharding, xs)
1410
+
1411
+
1412
+ def get_local_slice_from_fsarray(global_array):
1413
+ """Return numpy array for the host-local slice of fully-sharded array.
1414
+
1415
+ Args:
1416
+ global_array: JAX array, globally sharded on devices across hosts.
1417
+
1418
+ Returns:
1419
+ NumPy array that holds the part of `global_array` that is held by the
1420
+ devices on the host that calls this function.
1421
+ """
1422
+ # For now, for simplicity, we only implement slicing along the first axis.
1423
+ for shard in global_array.addressable_shards:
1424
+ assert all(idx == slice(None) for idx in shard.index[1:]), (
1425
+ f"global_array is sharded along non-first dimensions:\n{shard.index}")
1426
+
1427
+ # Get the shards back in the same order in which the global array was created
1428
+ # in the first place. This makes sure it's consistent with other things in the
1429
+ # batch, for example (assuming the whole batch is consistent).
1430
+ m = {s.device: s for s in global_array.addressable_shards}
1431
+ local_shards = [m[d] for d in global_array.sharding.mesh.local_devices]
1432
+ return np.concatenate([jax.device_get(s.data) for s in local_shards], axis=0)
1433
+
1434
+
1435
+ def assert_local_slices_same(*global_arrays):
1436
+ """Check whether all `global_arrays` have local slices at the same indices."""
1437
+ slices = [
1438
+ tuple(
1439
+ tuple((idx.start, idx.end, idx.step) for idx in s.index)
1440
+ for s in a.addressable_shards)
1441
+ for a in global_arrays]
1442
+ assert len(set(slices)) == 1, f"Not all slices are the same: {slices}"
1443
+
1444
+
1445
+ # TODO: remove this logic when the
1446
+ # issue is github fixed https://github.com/google/jax/issues/15600.
1447
+ def jit_cpu(**extra_kwargs):
1448
+ def _decorator(fun):
1449
+ def _wrapped(*args, **kwargs):
1450
+ sh = jax.sharding.SingleDeviceSharding(
1451
+ jax.local_devices(backend="cpu")[0]
1452
+ )
1453
+ return jax.jit(fun, **extra_kwargs, out_shardings=sh)(*args, **kwargs)
1454
+ return _wrapped
1455
+ return _decorator
1456
+
1457
+
1458
+ def create_device_mesh(
1459
+ config_mesh,
1460
+ *,
1461
+ allow_split_physical_axes=False,
1462
+ ):
1463
+ """Returns a JAX device mesh.
1464
+
1465
+ Args:
1466
+ config_mesh: A list of tuples of (axis_name, axis_size). It is advised to
1467
+ sort the axis in increasing order of network communication intensity.
1468
+ allow_split_physical_axes: Whether to allow splitting physical axes.
1469
+ """
1470
+ devices = jax.devices()
1471
+ mesh_axes, mesh_size = tuple(zip(*config_mesh))
1472
+ # Because jax.utils do not support `-1` shape size.
1473
+ mesh_size = np.array(devices).reshape(mesh_size).shape
1474
+ device_mesh = mesh_utils.create_device_mesh(
1475
+ mesh_size,
1476
+ devices=devices,
1477
+ allow_split_physical_axes=allow_split_physical_axes)
1478
+ return jax.sharding.Mesh(device_mesh, mesh_axes)
Tipsomaly/model/big_vision/utils_test.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Tests for utils."""
16
+
17
+ from functools import partial
18
+ import os
19
+
20
+ from absl.testing import parameterized
21
+ from big_vision import utils
22
+ import chex
23
+ import flax
24
+ import jax
25
+ from jax.experimental.array_serialization import serialization as array_serial
26
+ import jax.numpy as jnp
27
+ import numpy as np
28
+ import tensorflow as tf
29
+
30
+ from tensorflow.io import gfile
31
+
32
+
33
+ NDEV = 4
34
+
35
+
36
+ def setUpModule():
37
+ chex.set_n_cpu_devices(NDEV)
38
+
39
+
40
+ class PadShardUnpadTest(chex.TestCase, tf.test.TestCase):
41
+ BATCH_SIZES = [NDEV, NDEV + 1, NDEV - 1, 5 * NDEV, 5 * NDEV + 1, 5 * NDEV - 1]
42
+ DTYPES = [np.float32, np.uint8, jax.numpy.bfloat16, np.int32]
43
+
44
+ def tearDown(self):
45
+ chex.clear_trace_counter()
46
+ super().tearDown()
47
+
48
+ @parameterized.product(dtype=DTYPES, bs=BATCH_SIZES)
49
+ def test_basics(self, dtype, bs):
50
+ # Just tests that basic calling works without exploring caveats.
51
+ @partial(utils.pad_shard_unpad, static_argnums=())
52
+ def add(a, b):
53
+ return a + b
54
+
55
+ x = jnp.arange(bs, dtype=dtype)
56
+ y = add(x, 10 * x)
57
+ chex.assert_type(y.dtype, x.dtype)
58
+ np.testing.assert_allclose(np.float64(y), np.float64(x + 10*x))
59
+
60
+ @parameterized.parameters(DTYPES)
61
+ def test_min_device_batch_avoids_recompile(self, dtype):
62
+ @partial(utils.pad_shard_unpad, static_argnums=())
63
+ @jax.jit
64
+ @chex.assert_max_traces(n=1)
65
+ def add(a, b):
66
+ return a + b
67
+
68
+ chex.clear_trace_counter()
69
+
70
+ for bs in self.BATCH_SIZES:
71
+ x = jnp.arange(bs, dtype=dtype)
72
+ y = add(x, 10 * x, min_device_batch=9) # pylint: disable=unexpected-keyword-arg
73
+ chex.assert_type(y.dtype, x.dtype)
74
+ np.testing.assert_allclose(np.float64(y), np.float64(x + 10*x))
75
+
76
+ @parameterized.product(dtype=DTYPES, bs=BATCH_SIZES)
77
+ def test_static_argnum(self, dtype, bs):
78
+ @partial(utils.pad_shard_unpad, static_argnums=(1,))
79
+ def add(a, b):
80
+ return a + b
81
+
82
+ x = jnp.arange(bs, dtype=dtype)
83
+ y = add(x, dtype(10))
84
+ chex.assert_type(y.dtype, x.dtype)
85
+ np.testing.assert_allclose(np.float64(y), np.float64(x + 10))
86
+
87
+ @parameterized.product(dtype=DTYPES, bs=BATCH_SIZES)
88
+ def test_static_argnames(self, dtype, bs):
89
+ # In this test, leave static_argnums at the default value too, in order to
90
+ # test the default/most canonical path where `params` are the first arg.
91
+ @partial(utils.pad_shard_unpad, static_argnames=('b',))
92
+ def add(params, a, *, b):
93
+ return params * a + b
94
+
95
+ x = jnp.arange(bs, dtype=dtype)
96
+ y = add(dtype(5), x, b=dtype(10))
97
+ chex.assert_type(y.dtype, x.dtype)
98
+ np.testing.assert_allclose(np.float64(y), np.float64(5 * x + 10))
99
+
100
+
101
+ class TreeTest(tf.test.TestCase):
102
+
103
+ def setUp(self):
104
+ super().setUp()
105
+
106
+ self.d1 = {'w1': 1, 'w2': 2, 'w34': (3, 4)}
107
+ self.d1_flat = [1, 2]
108
+ self.d1_flat_jax = jax.tree.flatten(self.d1)[0]
109
+ self.d1_named_flat = [('w1', 1), ('w2', 2), ('w34/0', 3), ('w34/1', 4)]
110
+ self.d1_named_flat_jax = [('w1', 1), ('w2', 2), ('w34/0', 3), ('w34/1', 4)]
111
+
112
+ self.d2 = {'conv1': {'kernel': 0, 'bias': 1},
113
+ 'conv2': {'kernel': 2, 'bias': 3}}
114
+ self.d2_flat = [1, 0, 3, 2]
115
+ self.d2_flat_jax = jax.tree.flatten(self.d2)[0]
116
+ self.d2_named_flat = [('conv1/bias', 1), ('conv1/kernel', 0),
117
+ ('conv2/bias', 3), ('conv2/kernel', 2)]
118
+ self.d2_named_flat_jax = [('conv1/bias', 1), ('conv1/kernel', 0),
119
+ ('conv2/bias', 3), ('conv2/kernel', 2)]
120
+ self.d2_named_flat_inner = [
121
+ ('conv1/bias', 1), ('conv1/kernel', 0), ('conv1', self.d2['conv1']),
122
+ ('conv2/bias', 3), ('conv2/kernel', 2), ('conv2', self.d2['conv2']),
123
+ ('', self.d2),
124
+ ]
125
+
126
+ # This is a very important testcase that checks whether we correctly
127
+ # recover jax' traversal order, even though our custom traversal may not
128
+ # be consistent with jax' traversal order. In particular, jax traverses
129
+ # FlaxStruct in the order of attribute definition, while our custom
130
+ # traversal is alphabetical.
131
+ @flax.struct.dataclass
132
+ class FlaxStruct():
133
+ v3: float
134
+ v2: int
135
+ v1: str
136
+ self.d3 = {'a': 0, 'flax': FlaxStruct(2.0, 1, 's')}
137
+ self.d3_flat = [0, 1, 2.0, 's']
138
+ self.d3_flat_jax = jax.tree.flatten(self.d3)[0]
139
+ self.d3_named_flat = [
140
+ ('a', 0), ('flax/v1', 's'), ('flax/v2', 1), ('flax/v3', 2.0)]
141
+ self.d3_named_flat_jax = [
142
+ ('a', 0), ('flax/v3', 2.0), ('flax/v2', 1), ('flax/v1', 's')]
143
+
144
+ def test_traverse_with_names(self):
145
+ names_and_vals = list(utils._traverse_with_names(self.d1))
146
+ self.assertEqual(names_and_vals, self.d1_named_flat)
147
+
148
+ names_and_vals = list(utils._traverse_with_names(self.d2))
149
+ self.assertEqual(names_and_vals, self.d2_named_flat)
150
+
151
+ names_and_vals = list(utils._traverse_with_names(
152
+ self.d2, with_inner_nodes=True))
153
+ self.assertEqual(names_and_vals, self.d2_named_flat_inner)
154
+
155
+ names_and_vals = list(utils._traverse_with_names(self.d3))
156
+ self.assertEqual(names_and_vals, self.d3_named_flat)
157
+
158
+ def test_tree_flatten_with_names(self):
159
+ names_and_vals = utils.tree_flatten_with_names(self.d1)[0]
160
+ self.assertEqual(names_and_vals, self.d1_named_flat_jax)
161
+ self.assertEqual([x for _, x in names_and_vals], self.d1_flat_jax)
162
+
163
+ names_and_vals = utils.tree_flatten_with_names(self.d2)[0]
164
+ self.assertEqual(names_and_vals, self.d2_named_flat_jax)
165
+ self.assertEqual([x for _, x in names_and_vals], self.d2_flat_jax)
166
+
167
+ names_and_vals = utils.tree_flatten_with_names(self.d3)[0]
168
+ self.assertEqual(names_and_vals, self.d3_named_flat_jax)
169
+ self.assertEqual([x for _, x in names_and_vals], self.d3_flat_jax)
170
+
171
+ def test_tree_map_with_names(self):
172
+ d1 = utils.tree_map_with_names(
173
+ lambda name, x: -x if 'w2' in name else x, self.d1)
174
+ self.assertEqual(d1, {'w1': 1, 'w2': -2, 'w34': (3, 4)})
175
+
176
+ d1 = utils.tree_map_with_names(
177
+ lambda name, x1, x2: x1 + x2 if 'w2' in name else x1, self.d1, self.d1)
178
+ self.assertEqual(d1, {'w1': 1, 'w2': 4, 'w34': (3, 4)})
179
+
180
+ def test_recover_tree(self):
181
+ keys = ['a/b', 'a/c/x', 'a/c/y', 'd']
182
+ values = [0, 1, 2, 3]
183
+ self.assertEqual(utils.recover_tree(keys, values),
184
+ {'a': {'b': 0, 'c': {'x': 1, 'y': 2}}, 'd': 3})
185
+
186
+ def test_make_mask_trees(self):
187
+ F, T = False, True # pylint: disable=invalid-name
188
+ tree = {'a': {'b': 0, 'x': 1}, 'b': {'x': 2, 'y': 3}}
189
+ msk1 = {'a': {'b': F, 'x': T}, 'b': {'x': T, 'y': F}}
190
+ msk2 = {'a': {'b': F, 'x': F}, 'b': {'x': F, 'y': T}}
191
+ # Note that 'b' matches '^b' only and not '.*/b'.
192
+ # Also note that "b/x" is matched by rule 1 only (because it comes first).
193
+ self.assertEqual(
194
+ utils.make_mask_trees(tree, ('.*/x', 'b/.*')), [msk1, msk2])
195
+
196
+ def test_tree_get(self):
197
+ tree = {'a': {'b': 0, 'x': 1}, 'b': {'x': 2, 'y': 3}}
198
+ self.assertEqual(utils.tree_get(tree, 'a/b'), 0)
199
+ self.assertEqual(utils.tree_get(tree, 'a/x'), 1)
200
+ self.assertEqual(utils.tree_get(tree, 'b/x'), 2)
201
+ self.assertEqual(utils.tree_get(tree, 'b/y'), 3)
202
+ self.assertEqual(utils.tree_get(tree, 'a'), tree['a'])
203
+ self.assertEqual(utils.tree_get(tree, 'b'), tree['b'])
204
+
205
+ def test_tree_replace(self):
206
+ tree = {'a': {'b': 2, 'c': 3}, 'c': 4}
207
+ replacements = {
208
+ 'a/b': 'a/b/x', # replaces 'a/b' with 'a/b/x'
209
+ '.*c': 'C', # replaces 'c' with 'C' ('a/c' is removed)
210
+ 'C': 'D', # replaces 'C' (which was 'c') with 'D'
211
+ '.*/c': None, # removes 'a/c'
212
+ }
213
+ tree2 = utils.tree_replace(tree, replacements)
214
+ self.assertEqual(tree2, {'D': 4, 'a': {'b': {'x': 2}}})
215
+
216
+ def test_tree_compare(self):
217
+ tree1_only, tree2_only, dtype_shape_mismatch = utils.tree_compare(
218
+ {'a': {'b': jnp.array(2), 'c': jnp.array(3)}},
219
+ {'a': {'B': jnp.array(2), 'c': jnp.array(3.)}},
220
+ )
221
+ self.assertEqual(tree1_only, {'a/b'})
222
+ self.assertEqual(tree2_only, {'a/B'})
223
+ self.assertEqual(
224
+ dtype_shape_mismatch,
225
+ {'a/c': [(jnp.dtype('int32'), ()), (jnp.dtype('float32'), ())]})
226
+
227
+
228
+ class StepConversionTest(parameterized.TestCase, tf.test.TestCase):
229
+
230
+ @parameterized.named_parameters(
231
+ ('nice_steps', 1000, None, None, dict(foo_steps=3), 3),
232
+ ('nice_epochs', 1000, 100, None, dict(foo_epochs=3), 30),
233
+ ('nice_examples', None, 100, None, dict(foo_examples=300), 3),
234
+ ('nice_percent', None, None, 10, dict(foo_percent=0.30), 3),
235
+ ('ignore_neg', 1000, 100, 10, dict(foo_steps=-1, foo_epochs=-1,
236
+ foo_examples=-1, foo_percent=0.30), 3),
237
+ ('zero_steps', None, None, 10, dict(foo_percent=0.0), 0),
238
+ ('offbyone_steps', 1001, None, None, dict(foo_steps=3), 3),
239
+ ('offbyone_epochs', 1001, 100, None, dict(foo_epochs=3), 30),
240
+ ('offbyone_examples', None, 101, None, dict(foo_examples=300), 3),
241
+ ('offbyone_percent', None, None, 11, dict(foo_percent=0.30), 3),
242
+ )
243
+ def test_steps(self, data_size, batch_size, total, cfg, expected):
244
+ # Correct default usage:
245
+ step = utils.steps('foo', cfg, data_size=data_size, batch_size=batch_size,
246
+ total_steps=total)
247
+ self.assertEqual(step, expected)
248
+
249
+ # Inexitent entry:
250
+ with self.assertRaises(ValueError):
251
+ step = utils.steps('bar', cfg, data_size=data_size, batch_size=batch_size,
252
+ total_steps=total)
253
+ step = utils.steps('bar', cfg, data_size=data_size, batch_size=batch_size,
254
+ total_steps=total, default=1234)
255
+ self.assertEqual(step, 1234)
256
+
257
+
258
+ class CreateLearningRateScheduleTest(parameterized.TestCase, tf.test.TestCase):
259
+
260
+ @parameterized.named_parameters(
261
+ ('linear', 'linear', {}, 13, .5),
262
+ ('polynomial', 'polynomial', {'end': .1, 'power': 2}, 13, .325),
263
+ ('cosine', 'cosine', {}, 13, .5),
264
+ ('rsqrt', 'rsqrt', {'timescale': 1}, 13, 0.3333333),
265
+ ('stair_5', 'stair', {'steps': [10], 'mults': [.5]}, 5, 1.),
266
+ ('stair_10', 'stair', {'steps': [10], 'mults': [.5]}, 10, .5),
267
+ ('warmup_before', 'rsqrt', {'timescale': 1}, 3, .6),
268
+ ('cooldown_after', 'rsqrt', {'timescale': 1}, 20, .05),
269
+ )
270
+ def test_schedule(self, decay_type, extra_kwargs, step, expected_lr):
271
+ lr_fn = utils.create_learning_rate_schedule(
272
+ total_steps=21,
273
+ batch_size=512,
274
+ base=.5,
275
+ decay_type=decay_type,
276
+ scale_with_batchsize=True,
277
+ warmup_steps=5,
278
+ cooldown_steps=5,
279
+ **extra_kwargs)
280
+ lr = lr_fn(step)
281
+ self.assertAlmostEqual(lr, expected_lr)
282
+
283
+
284
+ class CheckpointTest(tf.test.TestCase):
285
+
286
+ def setup(self):
287
+ gacm = array_serial.GlobalAsyncCheckpointManager()
288
+
289
+ save_path = os.path.join(self.create_tempdir('workdir'), 'checkpoint.bv')
290
+ x = utils.put_cpu(np.array([1, 2, 3, 4]))
291
+ y = utils.put_cpu(np.array([5, 6, 7, 8]))
292
+ ckpt = {'x': x, 'y': {'z': y}}
293
+
294
+ sharding = jax.sharding.SingleDeviceSharding(
295
+ jax.local_devices(backend='cpu')[0]
296
+ )
297
+ shardings = jax.tree.map(lambda _: sharding, ckpt)
298
+
299
+ return gacm, save_path, ckpt, shardings
300
+
301
+ def test_save_and_load(self):
302
+ gacm, save_path, ckpt, shardings = self.setup()
303
+ step = 100
304
+ utils.save_checkpoint_ts(gacm, ckpt, save_path, step, keep=True)
305
+ gacm.wait_until_finished()
306
+ ckpt_loaded = utils.load_checkpoint_ts(save_path,
307
+ tree=ckpt, shardings=shardings)
308
+ chex.assert_trees_all_equal(ckpt_loaded, ckpt)
309
+
310
+ save_path_step = f'{save_path}-{step:09d}'
311
+ ckpt_loaded_step = utils.tsload(save_path_step, shardings=shardings)
312
+ chex.assert_trees_all_equal(ckpt_loaded_step, ckpt)
313
+
314
+ def test_save_and_partial_load(self):
315
+ gacm, save_path, ckpt, shardings = self.setup()
316
+ utils.save_checkpoint_ts(gacm, ckpt, save_path, step=100)
317
+ gacm.wait_until_finished()
318
+ _ = shardings.pop('x'), ckpt.pop('x')
319
+ ckpt_loaded = utils.load_checkpoint_ts(save_path,
320
+ tree=ckpt, shardings=shardings)
321
+ chex.assert_trees_all_equal(ckpt_loaded, ckpt)
322
+
323
+ def test_save_and_cpu_load(self):
324
+ gacm, save_path, ckpt, _ = self.setup()
325
+ utils.save_checkpoint_ts(gacm, ckpt, save_path, step=100)
326
+ gacm.wait_until_finished()
327
+ ckpt_loaded = utils.load_checkpoint_ts(save_path)
328
+ chex.assert_trees_all_equal(ckpt_loaded, ckpt)
329
+
330
+ def test_save_and_partial_cpu_load(self):
331
+ gacm, save_path, ckpt, _ = self.setup()
332
+ utils.save_checkpoint_ts(gacm, ckpt, save_path, step=100)
333
+ gacm.wait_until_finished()
334
+ ckpt.pop('y')
335
+ ckpt_loaded = utils.load_checkpoint_ts(save_path, regex='x.*')
336
+ chex.assert_trees_all_equal(ckpt_loaded, ckpt)
337
+
338
+ def test_keep_deletes(self):
339
+ def x(tree, factor): # x as in "times" for multiplying.
340
+ return jax.tree.map(lambda a: a * factor, tree)
341
+
342
+ gacm, save_path, ckpt, _ = self.setup()
343
+ utils.save_checkpoint_ts(gacm, ckpt, save_path, step=100, keep=False)
344
+ utils.save_checkpoint_ts(gacm, x(ckpt, 2), save_path, step=200, keep=True)
345
+ utils.save_checkpoint_ts(gacm, x(ckpt, 3), save_path, step=300, keep=False)
346
+ gacm.wait_until_finished()
347
+ ckpt_loaded_200 = utils.tsload(f'{save_path}-{200:09d}')
348
+ chex.assert_trees_all_equal(ckpt_loaded_200, x(ckpt, 2))
349
+ ckpt_loaded_300 = utils.tsload(f'{save_path}-{300:09d}-tmp')
350
+ chex.assert_trees_all_equal(ckpt_loaded_300, x(ckpt, 3))
351
+ ckpt_loaded_last = utils.load_checkpoint_ts(save_path)
352
+ chex.assert_trees_all_equal(ckpt_loaded_last, x(ckpt, 3))
353
+ with self.assertRaises(Exception): # Can different types depending on fs.
354
+ _ = utils.tsload(f'{save_path}-{100:09d}')
355
+ # Test that ckpt@100 was deleted
356
+ self.assertFalse(gfile.exists(f'{save_path}-{100:09d}-tmp'))
357
+
358
+
359
+ if __name__ == '__main__':
360
+ tf.test.main()
Tipsomaly/model/omaly/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .text_encoder import text_encoder
2
+ from .vision_encoder import vision_encoder
Tipsomaly/model/omaly/fixed_prompts.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def generate_prompt_templates(prompt_type):
2
+ if prompt_type == 'medical':
3
+ prompt_normal = [
4
+ 'normal {}',
5
+ 'intact {}',
6
+ '{} with uniform structure',
7
+ '{} showing clear tissue',
8
+ '{} with normal anatomy',
9
+ '{} showing no distortion',
10
+ '{} with symmetric appearance',
11
+ '{} looking normal',
12
+ '{} with even texture',
13
+ '{} with regular shape',
14
+ ]
15
+ prompt_abnormal = [
16
+ 'abnormal {}',
17
+ '{} with spot',
18
+ '{} with abnormality',
19
+ 'diseased {}',
20
+ '{} showing distortion',
21
+ '{} with irregular area',
22
+ '{} with irregular shape',
23
+ '{} with uneven texture',
24
+ ]
25
+ # Prompt templates adapted for medical images
26
+ prompt_templates = [
27
+ 'a medical image of a {}.',
28
+ 'a medical image of the {}.',
29
+ 'a diagnostic scan of a {}.',
30
+ 'a diagnostic scan of the {}.',
31
+ 'a slice showing a {}.',
32
+ 'a slice showing the {}.',
33
+ 'a scan of the {}.',
34
+ 'a clinical brain scan of a {}.'
35
+ ]
36
+
37
+ elif prompt_type == 'object_agnostic':
38
+ prompt_normal = ['{}']
39
+ # prompt_normal = ['normal {}']
40
+ prompt_abnormal = ['damaged {}']
41
+ prompt_templates = ['{}']
42
+ # prompt_templates = ['a photo of a {}']
43
+
44
+ elif prompt_type == 'industrial':
45
+ prompt_normal = ['{}', 'flawless {}', 'perfect {}', 'unblemished {}', '{} without flaw',
46
+ '{} without defect',
47
+ '{} without damage']
48
+ prompt_abnormal = ['damaged {}', 'broken {}', '{} with flaw', '{} with defect', '{} with damage']
49
+ prompt_templates = ['a bad photo of a {}.',
50
+ 'a low resolution photo of the {}.',
51
+ 'a bad photo of the {}.',
52
+ 'a cropped photo of the {}.',
53
+ ]
54
+ return prompt_normal, prompt_abnormal, prompt_templates
Tipsomaly/model/omaly/text_encoder.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torch.nn import functional as F
4
+
5
+ # NOTE: Since tips is inside model/, your import in model/omaly/text_encoder.py should be:
6
+ from model.tips.text_encoder import TextEncoder as BaseTextEncoder
7
+ from .fixed_prompts import generate_prompt_templates
8
+ import jax.numpy as jnp
9
+ import numpy as np
10
+ from tqdm import tqdm
11
+ import math
12
+
13
+ def jax_to_torch(x):
14
+ return torch.from_numpy(np.array(x))
15
+
16
+ class text_encoder(nn.Module):
17
+ def __init__(self, tokenizer, bb_text_encoder, bb_type, text_embd_dim, MAX_LEN, prompt_learn_method='none', prompt_type='industrial', n_prompt=8, n_deep=0, d_deep=0):
18
+ super(text_encoder, self).__init__()
19
+ self.tokenizer = tokenizer
20
+ self._encoder = bb_text_encoder
21
+ self.model = bb_type
22
+ self.MAX_LEN = MAX_LEN
23
+ self.prompt_learn_method = prompt_learn_method
24
+ self.n_deep_tokens = n_deep
25
+ self.d_deep_tokens = d_deep
26
+ self.n_prompt = n_prompt
27
+ self.prompt_type = prompt_type
28
+ self.text_embd_dim = text_embd_dim
29
+
30
+ self.prompt_normal, self.prompt_abnormal, self.prompt_templates = generate_prompt_templates(self.prompt_type)
31
+
32
+ self.prompt_state = [self.prompt_normal, self.prompt_abnormal]
33
+
34
+ if self.n_deep_tokens > 0 and self.d_deep_tokens > 0:
35
+ self.deep_parameters = torch.nn.ParameterList([torch.nn.Parameter(\
36
+ torch.randn(self.n_deep_tokens, text_embd_dim) * 0.02) \
37
+ for _ in range(self.d_deep_tokens)])
38
+ else:
39
+ self.deep_parameters = None
40
+
41
+ if not self.prompt_learn_method == 'none':
42
+ self.normal_prompt = torch.nn.Parameter(torch.randn(self.n_prompt, text_embd_dim) * 0.02) # Learnable prompt for normal text description with std of 0.02
43
+ self.abnormal_prompt = torch.nn.Parameter(torch.randn(self.n_prompt, text_embd_dim) * 0.02) # Learnable prompt for abnormal text description with std of 0.02
44
+ self.learnable_prompts = torch.nn.ParameterList([self.normal_prompt, self.abnormal_prompt])
45
+
46
+
47
+ def forward(self, texts, device, learned=False):
48
+ text_feature_list = []
49
+
50
+ for indx, text in enumerate(texts):
51
+ text_features = self.encode_text(text, device, learned)
52
+ text_feature_list.append(text_features)
53
+
54
+ text_features = torch.stack(text_feature_list, dim=0)
55
+ text_features = F.normalize(text_features, dim=2)
56
+ return text_features
57
+
58
+ def encode_text(self, text, device, learned=False):
59
+ text_features = []
60
+ for i in range(len(self.prompt_state)):
61
+ learnables = self.learnable_prompts[i] if learned and not self.prompt_learn_method == 'none' else None
62
+ deep_parameters = self.deep_parameters if learned else None
63
+
64
+ text = text.replace('-', ' ')
65
+ prompted_state = [state.format(text) for state in self.prompt_state[i]]
66
+ prompted_sentence = []
67
+ for s in prompted_state:
68
+ for template in self.prompt_templates:
69
+ prompted_sentence.append(template.format(s))
70
+
71
+ if self.model == 'tips':
72
+ # NOTE: replace the class based prompt learning concatenated to the templates with only 2 sentences
73
+ text_ids, text_paddings = self.tokenizer.tokenize(prompted_sentence, max_len=self.MAX_LEN)
74
+ class_embeddings = self._encoder(text_ids.to(device), text_paddings.to(device), learnables, self.prompt_learn_method, deep_parameters, device)
75
+
76
+ # NOTE: Avoid in-place /=, +=, _add() on tensor which are actively gradiented
77
+ class_embeddings = class_embeddings / class_embeddings.norm(dim=-1, keepdim=True).clamp(min=1e-3)
78
+ class_embedding = class_embeddings.mean(dim=0)
79
+ class_embedding = class_embedding / class_embedding.norm(dim=-1, keepdim=True)
80
+
81
+ elif self.model == 'siglip2':
82
+ num_prompts = len(prompted_sentence)
83
+ if num_prompts == 0:
84
+ class_embedding = torch.zeros(getattr(self, "expected_text_dim", 512), device=device)
85
+ text_features.append(class_embedding)
86
+ continue
87
+
88
+ batch_size = 20
89
+ batch_ranges = range(0, num_prompts, batch_size)
90
+ total_batches = math.ceil(num_prompts / batch_size)
91
+
92
+ # accumulator for sum in JAX (None -> initialize on first batch)
93
+ sum_jax = None
94
+
95
+ # optional tqdm over batches
96
+ batch_iter = tqdm(batch_ranges, total=total_batches,
97
+ desc=f"Encoding text {i+1}/{len(self.prompt_state)} ({self.model})",
98
+ leave=False)
99
+
100
+ for start in batch_iter:
101
+ batch_sentences = prompted_sentence[start:start + batch_size]
102
+ # tokenizer expects a list of strings (batch)
103
+ txts = self.tokenizer(batch_sentences)
104
+
105
+ # encoder returns jax arrays; ztxt shape -> (batch, dim)
106
+ _, ztxt, out = self._encoder(txts)
107
+
108
+ # sum along batch axis in JAX to accumulate raw (unnormalized) vectors
109
+ batch_sum = jnp.sum(ztxt, axis=0) # shape (dim,)
110
+
111
+ if sum_jax is None:
112
+ sum_jax = batch_sum
113
+ else:
114
+ sum_jax = sum_jax + batch_sum
115
+
116
+ # now compute mean in JAX
117
+ mean_jax = sum_jax / float(num_prompts)
118
+ mean_jax = mean_jax / (jnp.linalg.norm(mean_jax, axis=-1, keepdims=True) + 1e-8)
119
+
120
+ # convert final normalized mean to torch once
121
+ class_embedding = jax_to_torch(mean_jax) # shape: (dim,)
122
+ elif self.model == 'siglip2-hf':
123
+ ids = self.tokenizer(text=prompted_sentence, padding="max_length", max_length=self.MAX_LEN, return_tensors="pt")
124
+ ztxt = self._encoder(ids['input_ids'].to(device), learnable_prompts=learnables, learning_method=self.prompt_learn_method).pooler_output
125
+ ztxt = ztxt / ztxt.norm(dim=-1, keepdim=True).clamp(min=1e-3)
126
+ ztxt = ztxt.mean(dim=0)
127
+ class_embedding = ztxt / ztxt.norm(dim=-1, keepdim=True)
128
+ # elif self.model == 'siglip2-hf': # NOTE: Use this block if you lack enough memory for encoding text input
129
+ # num_prompts = len(prompted_sentence)
130
+ # if num_prompts == 0:
131
+ # class_embedding = torch.zeros(getattr(self, "expected_text_dim", 512), device=device)
132
+ # text_features.append(class_embedding)
133
+ # continue
134
+
135
+ # batch_size = 20
136
+ # batch_ranges = range(0, num_prompts, batch_size)
137
+ # total_batches = math.ceil(num_prompts / batch_size)
138
+
139
+ # sum_torch = None
140
+
141
+ # # optional tqdm over batches
142
+ # batch_iter = tqdm(batch_ranges, total=total_batches,
143
+ # desc=f"Encoding text {i+1}/{len(self.prompt_state)} ({self.model})",
144
+ # leave=False)
145
+
146
+ # for start in batch_iter:
147
+ # batch_sentences = prompted_sentence[start:start + batch_size]
148
+ # ids = self.tokenizer(text=batch_sentences, padding="max_length", max_length=self.MAX_LEN, return_tensors="pt")
149
+ # ztxt = self._encoder(ids['input_ids'].to(device), learnable_prompts=learnables, learning_method=self.prompt_learn_method).pooler_output
150
+ # batch_sum = torch.sum(ztxt, dim=0) # shape (dim,)
151
+
152
+ # if sum_torch is None:
153
+ # sum_torch = batch_sum
154
+ # else:
155
+ # sum_torch = sum_torch + batch_sum
156
+
157
+ # mean_torch = sum_torch / float(num_prompts)
158
+ # mean_torch = mean_torch / (torch.norm(mean_torch, dim=-1, keepdim=True) + 1e-8)
159
+ # class_embedding = mean_torch
160
+
161
+ text_features.append(class_embedding)
162
+
163
+ text_features = torch.stack(text_features, dim=0)
164
+ return text_features
Tipsomaly/model/omaly/vision_encoder.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torch.nn import functional as F
4
+ import jax.numpy as jnp
5
+ import numpy as np
6
+
7
+ def jax_to_torch(x):
8
+ return torch.from_numpy(np.array(x))
9
+
10
+ # The backbone can be SigLIP2 or TIPS
11
+ class vision_encoder(nn.Module):
12
+ def __init__(self, bb_vision_encoder, bb_type):
13
+ super(vision_encoder, self).__init__()
14
+ self._encoder = bb_vision_encoder
15
+ self.model = bb_type
16
+
17
+ def _forward_tips(_encoder, images):
18
+ outputs = _encoder(images)
19
+
20
+ first_cls_token = outputs[0]
21
+ first_cls_token = first_cls_token / first_cls_token.norm(dim=-1, keepdim=True).clamp(min=1e-3)
22
+
23
+ second_cls_token = outputs[1]
24
+ second_cls_token = second_cls_token / second_cls_token.norm(dim=-1, keepdim=True).clamp(min=1e-3)
25
+
26
+ spatial_tokens = outputs[2]
27
+ spatial_tokens = spatial_tokens / spatial_tokens.norm(dim=-1, keepdim=True).clamp(min=1e-3)
28
+
29
+ return first_cls_token, second_cls_token, spatial_tokens
30
+
31
+ def _forward_siglip2(_encoder, images):
32
+ _, _, out = _encoder(images)
33
+ return jax_to_torch(out['img/normalized']), jax_to_torch(out['img/normalized']), jax_to_torch(out['img/2d_normalized'])
34
+
35
+ def _forward_siglip2_hf(_encoder, images):
36
+ image_features = _encoder(images)
37
+ return image_features.pooler_output, image_features.pooler_output, image_features.last_hidden_state
38
+
39
+ def forward(self, images):
40
+ if self.model == 'siglip2':
41
+ return vision_encoder._forward_siglip2(self._encoder, images)
42
+ elif self.model == 'tips':
43
+ return vision_encoder._forward_tips(self._encoder, images)
44
+ elif self.model == "siglip2-hf":
45
+ return vision_encoder._forward_siglip2_hf(self._encoder, images)
46
+
47
+ # outputs /= outputs.norm(dim=-1, keepdim=True).clamp(min=1e-3)
48
+ # first_cls_token, second_cls_token, spatial_tokens = outputs[:, 0], outputs[:, 1], outputs[:, 2:]
Tipsomaly/model/siglip2/__init__.py ADDED
File without changes
Tipsomaly/model/siglip2/siglip2_prompt_learnable.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ import torch
3
+ import torch.nn as nn
4
+ from transformers.models.siglip.modeling_siglip import SiglipTextConfig, SiglipTextEmbeddings, SiglipTextTransformer, SiglipTextModel, SiglipPreTrainedModel
5
+ from transformers.utils import (
6
+ auto_docstring,
7
+ TransformersKwargs,
8
+ can_return_tuple,
9
+ )
10
+
11
+ from transformers.utils.generic import check_model_inputs
12
+ from transformers.processing_utils import Unpack
13
+ from transformers.modeling_outputs import (
14
+ BaseModelOutput,
15
+ BaseModelOutputWithPooling,
16
+ )
17
+
18
+ from transformers.modeling_attn_mask_utils import _prepare_4d_attention_mask
19
+
20
+ class SiglipTextEmbeddingsWithPromptLearning(SiglipTextEmbeddings):
21
+ """
22
+ Extends SiglipTextEmbeddings with learnable prompt tokens.
23
+ Prompts are added BEFORE the position embeddings are applied (so they
24
+ receive position embeddings as part of the combined sequence).
25
+ """
26
+ def __init__(self, config: SiglipTextConfig):
27
+ super().__init__(config)
28
+
29
+ def forward(
30
+ self,
31
+ input_ids: Optional[torch.LongTensor] = None,
32
+ position_ids: Optional[torch.LongTensor] = None,
33
+ inputs_embeds: Optional[torch.FloatTensor] = None,
34
+ learnable_prompts: torch.Tensor = None,
35
+ learning_method: str = None,
36
+ ) -> torch.Tensor:
37
+ seq_length = input_ids.shape[-1] if input_ids is not None else inputs_embeds.shape[-2]
38
+ max_position_embedding = self.position_embedding.weight.shape[0]
39
+
40
+ if seq_length > max_position_embedding:
41
+ raise ValueError(
42
+ f"Sequence length must be less than max_position_embeddings (got `sequence length`: "
43
+ f"{seq_length} and max_position_embeddings: {max_position_embedding}"
44
+ )
45
+
46
+ if position_ids is None:
47
+ position_ids = self.position_ids[:, :seq_length]
48
+
49
+ if inputs_embeds is None:
50
+ inputs_embeds = self.token_embedding(input_ids) # [B, L, D]
51
+
52
+ batch_size = inputs_embeds.shape[0]
53
+ device = inputs_embeds.device
54
+
55
+ if learnable_prompts is not None:
56
+ if learning_method == 'concat':
57
+ prompts = learnable_prompts.unsqueeze(0).expand(batch_size, -1, -1).to(device) # [B, P, D]
58
+ inputs_embeds = torch.cat([prompts, inputs_embeds], dim=1) # [B, P+L, D]
59
+ elif learning_method == 'sumate':
60
+ prompt_len = learnable_prompts.size(0)
61
+ inputs_embeds[:, :prompt_len, :] += learnable_prompts.unsqueeze(0)
62
+ elif learning_method == 'entire_learnable':
63
+ inputs_embeds = learnable_prompts.unsqueeze(0).expand(batch_size, -1, -1)
64
+
65
+ current_len = inputs_embeds.size(1)
66
+ if current_len > seq_length:
67
+ inputs_embeds = inputs_embeds[:, :seq_length, :]
68
+ elif current_len < seq_length: # must check this case if happens or not
69
+ pad_len = seq_length - current_len
70
+ pad_embed = torch.zeros((batch_size, pad_len, inputs_embeds.size(2)))
71
+ inputs_embeds = torch.cat([inputs_embeds, pad_embed], dim=1)
72
+
73
+ position_embeddings = self.position_embedding(position_ids)
74
+ embeddings = inputs_embeds + position_embeddings
75
+
76
+ return embeddings
77
+
78
+
79
+ class SiglipTextTransformerWithPromptLearning(SiglipTextTransformer):
80
+ def __init__(self, config: SiglipTextConfig):
81
+ super().__init__(config)
82
+ self.embeddings = SiglipTextEmbeddingsWithPromptLearning(config)
83
+
84
+ @can_return_tuple
85
+ # @auto_docstring
86
+ def forward(
87
+ self,
88
+ input_ids: Optional[torch.Tensor] = None,
89
+ attention_mask: Optional[torch.Tensor] = None,
90
+ position_ids: Optional[torch.Tensor] = None,
91
+ learnable_prompts: torch.Tensor = None,
92
+ learning_method: str = None,
93
+ **kwargs: Unpack[TransformersKwargs],
94
+ ) -> BaseModelOutputWithPooling:
95
+ if input_ids is None:
96
+ raise ValueError("You have to specify input_ids")
97
+
98
+ input_shape = input_ids.size()
99
+ input_ids = input_ids.view(-1, input_shape[-1])
100
+
101
+ hidden_states = self.embeddings(input_ids=input_ids, position_ids=position_ids,
102
+ learnable_prompts=learnable_prompts, learning_method=learning_method)
103
+
104
+ # note: SigLIP's text model does not use a causal mask, unlike the original CLIP model.
105
+ # expand attention_mask
106
+ uses_flash_attention = "flash" in self.config._attn_implementation
107
+ if uses_flash_attention:
108
+ attention_mask = None
109
+ elif attention_mask is not None and not uses_flash_attention:
110
+ # [batch_size, seq_len] -> [batch_size, 1, tgt_seq_len, src_seq_len]
111
+ attention_mask = _prepare_4d_attention_mask(attention_mask, hidden_states.dtype)
112
+
113
+ encoder_outputs: BaseModelOutput = self.encoder(
114
+ inputs_embeds=hidden_states,
115
+ attention_mask=attention_mask,
116
+ **kwargs,
117
+ )
118
+
119
+ last_hidden_state = encoder_outputs.last_hidden_state
120
+ last_hidden_state = self.final_layer_norm(last_hidden_state)
121
+
122
+ # The model uses the last token's hidden state, which may be padding.
123
+ pooled_output = last_hidden_state[:, -1, :]
124
+ pooled_output = self.head(pooled_output)
125
+
126
+ return BaseModelOutputWithPooling(
127
+ last_hidden_state=last_hidden_state,
128
+ pooler_output=pooled_output,
129
+ )
130
+
131
+
132
+ class SiglipTextModelWithPromptLearning(SiglipPreTrainedModel):
133
+ config: SiglipTextConfig
134
+ input_modalities = ("text",)
135
+
136
+ def __init__(self, config: SiglipTextConfig):
137
+ super().__init__(config)
138
+ self.text_model = SiglipTextTransformerWithPromptLearning(config)
139
+ # Initialize weights and apply final processing
140
+ self.post_init()
141
+
142
+ def get_input_embeddings(self) -> nn.Module:
143
+ return self.text_model.embeddings.token_embedding
144
+
145
+ def set_input_embeddings(self, value):
146
+ self.text_model.embeddings.token_embedding = value
147
+
148
+ @check_model_inputs(tie_last_hidden_states=False)
149
+ # @auto_docstring
150
+ def forward(
151
+ self,
152
+ input_ids: Optional[torch.Tensor] = None,
153
+ attention_mask: Optional[torch.Tensor] = None,
154
+ position_ids: Optional[torch.Tensor] = None,
155
+ learnable_prompts: torch.Tensor = None,
156
+ learning_method: str = None,
157
+ **kwargs: Unpack[TransformersKwargs],
158
+ ) -> BaseModelOutputWithPooling:
159
+ r"""
160
+ Examples:
161
+
162
+ ```python
163
+ >>> from transformers import AutoTokenizer, SiglipTextModel
164
+
165
+ >>> model = SiglipTextModel.from_pretrained("google/siglip-base-patch16-224")
166
+ >>> tokenizer = AutoTokenizer.from_pretrained("google/siglip-base-patch16-224")
167
+
168
+ >>> # important: make sure to set padding="max_length" as that's how the model was trained
169
+ >>> inputs = tokenizer(["a photo of a cat", "a photo of a dog"], padding="max_length", return_tensors="pt")
170
+
171
+ >>> outputs = model(**inputs)
172
+ >>> last_hidden_state = outputs.last_hidden_state
173
+ >>> pooled_output = outputs.pooler_output # pooled (EOS token) states
174
+ ```"""
175
+
176
+ return self.text_model(
177
+ input_ids=input_ids,
178
+ attention_mask=attention_mask,
179
+ position_ids=position_ids,
180
+ learnable_prompts=learnable_prompts,
181
+ learning_method=learning_method,
182
+ **kwargs,
183
+ )
Tipsomaly/model/tips/__init__.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ # All software is licensed under the Apache License, Version 2.0 (Apache 2.0);
17
+ # you may not use this file except in compliance with the Apache 2.0 license.
18
+ # You may obtain a copy of the Apache 2.0 license at:
19
+ # https://www.apache.org/licenses/LICENSE-2.0
20
+
21
+ # All other materials are licensed under the Creative Commons Attribution 4.0
22
+ # International License (CC-BY). You may obtain a copy of the CC-BY license at:
23
+ # https://creativecommons.org/licenses/by/4.0/legalcode
24
+
25
+ # Unless required by applicable law or agreed to in writing, all software and
26
+ # materials distributed here under the Apache 2.0 or CC-BY licenses are
27
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
28
+ # either express or implied. See the licenses for the specific language
29
+ # governing permissions and limitations under those licenses.
30
+
31
+ # This is not an official Google product.
32
+ """Import all files."""
33
+ # from . import text_encoder
34
+ # from . import image_encoder
35
+ from . import load_model
Tipsomaly/model/tips/__pycache__/image_encoder.cpython-39.pyc ADDED
Binary file (27.5 kB). View file
 
Tipsomaly/model/tips/__pycache__/load_model.cpython-39.pyc ADDED
Binary file (3.55 kB). View file
 
Tipsomaly/model/tips/__pycache__/text_encoder.cpython-39.pyc ADDED
Binary file (13.7 kB). View file
 
Tipsomaly/model/tips/checkpoints/checkpoint.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Dict, List
7
+
8
+ # ---- Model registry ----
9
+ # Short unique IDs -> full checkpoint base name on GCS
10
+ # (all files are under: https://storage.googleapis.com/tips_data/v1_0/checkpoints/pytorch/)
11
+ MODEL_REGISTRY: Dict[str, str] = {
12
+ # id # full checkpoint basename
13
+ "s14h": "tips_oss_s14_highres_distilled",
14
+ "b14h": "tips_oss_b14_highres_distilled",
15
+ "l14h": "tips_oss_l14_highres_distilled",
16
+ "so4h": "tips_oss_so400m14_highres_largetext_distilled",
17
+ "g14l": "tips_oss_g14_lowres",
18
+ "g14h": "tips_oss_g14_highres",
19
+ }
20
+
21
+ TOKENIZER_FILENAME = "tokenizer.model"
22
+ TOKENIZER_URL = "https://storage.googleapis.com/tips_data/v1_0/checkpoints/tokenizer.model"
23
+
24
+ BASE_URL = "https://storage.googleapis.com/tips_data/v1_0/checkpoints/pytorch"
25
+
26
+ def _require_wget() -> None:
27
+ from shutil import which
28
+ if which("wget") is None:
29
+ raise EnvironmentError(
30
+ "wget is required but was not found on PATH. "
31
+ "Please install wget or add it to your PATH."
32
+ )
33
+
34
+ def _wget(url: str, dest: Path) -> None:
35
+ dest.parent.mkdir(parents=True, exist_ok=True)
36
+ # -q for quiet except errors; --show-progress if attached to TTY would be nice,
37
+ # but we keep it simple and quiet here.
38
+ result = subprocess.run(
39
+ ["wget", "-q", "-O", str(dest), url],
40
+ stdout=subprocess.PIPE,
41
+ stderr=subprocess.PIPE,
42
+ text=True,
43
+ )
44
+ if result.returncode != 0:
45
+ # Clean up partial file if any
46
+ if dest.exists() and dest.stat().st_size == 0:
47
+ try:
48
+ dest.unlink()
49
+ except OSError:
50
+ pass
51
+ raise RuntimeError(
52
+ f"Failed to download {url} -> {dest}\n"
53
+ f"wget stderr:\n{result.stderr.strip()}"
54
+ )
55
+
56
+ def _model_files_for_basename(base: str) -> List[str]:
57
+ return [f"{base}_vision.npz", f"{base}_text.npz"]
58
+
59
+ def list_models() -> Dict[str, str]:
60
+ return dict(MODEL_REGISTRY)
61
+
62
+ def ensure_model_files(model_id: str, save_dir: str) -> Dict[str, str]:
63
+ if model_id not in MODEL_REGISTRY:
64
+ raise KeyError(
65
+ f"Unknown model_id '{model_id}'. "
66
+ f"Valid options: {', '.join(sorted(MODEL_REGISTRY.keys()))}"
67
+ )
68
+
69
+ _require_wget()
70
+
71
+ save_path = Path(save_dir).expanduser().resolve()
72
+ save_path.mkdir(parents=True, exist_ok=True)
73
+
74
+ # 1) Tokenizer
75
+ tokenizer_path = save_path / TOKENIZER_FILENAME
76
+ if not tokenizer_path.exists():
77
+ _wget(TOKENIZER_URL, tokenizer_path)
78
+
79
+ # 2) Model files
80
+ base = MODEL_REGISTRY[model_id]
81
+ required_files = _model_files_for_basename(base)
82
+
83
+ out_paths = {"tokenizer": str(tokenizer_path)}
84
+
85
+ for fname in required_files:
86
+ local_path = save_path / fname
87
+ if not local_path.exists():
88
+ # Compose the correct URL for this file
89
+ url = f"{BASE_URL}/{fname}"
90
+ _wget(url, local_path)
91
+
92
+ # record
93
+ if fname.endswith("_vision.npz"):
94
+ out_paths["vision"] = str(local_path)
95
+ elif fname.endswith("_text.npz"):
96
+ out_paths["text"] = str(local_path)
97
+
98
+ return out_paths
Tipsomaly/model/tips/checkpoints/download_checkpoints.sh ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Copyright 2025 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ==============================================================================
16
+
17
+
18
+ # The model weights can be found in https://console.cloud.google.com/storage/browser/tips_data
19
+ ALL_CHECKPOINTS=(
20
+ "tips_oss_s14_highres_distilled"
21
+ "tips_oss_b14_highres_distilled"
22
+ "tips_oss_l14_highres_distilled"
23
+ "tips_oss_so400m14_highres_largetext_distilled"
24
+ "tips_oss_g14_lowres"
25
+ "tips_oss_g14_highres"
26
+ )
27
+
28
+ echo "Downloading the tokenizer."
29
+ wget https://storage.googleapis.com/tips_data/v1_0/checkpoints/tokenizer.model
30
+
31
+ for CHECKPOINT in "${ALL_CHECKPOINTS[@]}"; do
32
+ echo "Downloading ${CHECKPOINT} (vision encoder weights)"
33
+ wget https://storage.googleapis.com/tips_data/v1_0/checkpoints/pytorch/${CHECKPOINT}_vision.npz
34
+ echo "Downloading ${CHECKPOINT} (text encoder weights)"
35
+ wget https://storage.googleapis.com/tips_data/v1_0/checkpoints/pytorch/${CHECKPOINT}_text.npz
36
+ done
Tipsomaly/model/tips/image_encoder.py ADDED
@@ -0,0 +1,1002 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Vision encoder implementation in PyTorch."""
17
+
18
+ import functools
19
+ import math
20
+ import os
21
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
22
+ import warnings
23
+ import torch
24
+ from torch import nn
25
+ import torch.nn.functional as F
26
+ import torch.utils.checkpoint
27
+
28
+
29
+ class Mlp(nn.Module):
30
+ """Transformer MLP, following DINOv2 implementation."""
31
+
32
+ def __init__(
33
+ self,
34
+ in_features: int,
35
+ hidden_features: Optional[int] = None,
36
+ out_features: Optional[int] = None,
37
+ act_layer: Callable[..., nn.Module] = nn.GELU,
38
+ drop: float = 0.0,
39
+ bias: bool = True,
40
+ ) -> None:
41
+ super().__init__()
42
+ out_features = out_features or in_features
43
+ hidden_features = hidden_features or in_features
44
+ self.fc1 = nn.Linear(in_features, hidden_features, bias=bias)
45
+ self.act = act_layer()
46
+ self.fc2 = nn.Linear(hidden_features, out_features, bias=bias)
47
+ self.drop = nn.Dropout(drop)
48
+
49
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
50
+ x = self.fc1(x)
51
+ x = self.act(x)
52
+ x = self.drop(x)
53
+ x = self.fc2(x)
54
+ x = self.drop(x)
55
+ return x
56
+
57
+
58
+ def make_2tuple(x):
59
+ if isinstance(x, tuple):
60
+ assert len(x) == 2
61
+ return x
62
+
63
+ assert isinstance(x, int)
64
+ return (x, x)
65
+
66
+
67
+ class PatchEmbed(nn.Module):
68
+ """2D image to patch embedding: (B,C,H,W) -> (B,N,D)."""
69
+
70
+ def __init__(
71
+ self,
72
+ img_size: Union[int, Tuple[int, int]] = 224,
73
+ patch_size: Union[int, Tuple[int, int]] = 16,
74
+ in_chans: int = 3,
75
+ embed_dim: int = 768,
76
+ norm_layer: Optional[Callable] = None, # pylint: disable=g-bare-generic
77
+ flatten_embedding: bool = True,
78
+ ) -> None:
79
+ super().__init__()
80
+
81
+ image_hw = make_2tuple(img_size)
82
+ patch_hw = make_2tuple(patch_size)
83
+ patch_grid_size = (
84
+ image_hw[0] // patch_hw[0],
85
+ image_hw[1] // patch_hw[1],
86
+ )
87
+
88
+ self.img_size = image_hw
89
+ self.patch_size = patch_hw
90
+ self.patches_resolution = patch_grid_size
91
+ self.num_patches = patch_grid_size[0] * patch_grid_size[1]
92
+
93
+ self.in_chans = in_chans
94
+ self.embed_dim = embed_dim
95
+
96
+ self.flatten_embedding = flatten_embedding
97
+
98
+ self.proj = nn.Conv2d(
99
+ in_chans, embed_dim, kernel_size=patch_hw, stride=patch_hw
100
+ )
101
+ self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity()
102
+
103
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
104
+ _, _, h, w = x.shape
105
+ patch_h, patch_w = self.patch_size
106
+
107
+ assert (
108
+ h % patch_h == 0
109
+ ), f"Input image height {h} is not a multiple of patch height {patch_h}"
110
+ assert (
111
+ w % patch_w == 0
112
+ ), f"Input image width {w} is not a multiple of patch width: {patch_w}"
113
+
114
+ x = self.proj(x) # B C H W
115
+ h, w = x.size(2), x.size(3)
116
+ x = x.flatten(2).transpose(1, 2) # B HW C
117
+ x = self.norm(x)
118
+ if not self.flatten_embedding:
119
+ x = x.reshape(-1, h, w, self.embed_dim) # B H W C
120
+ return x
121
+
122
+ def flops(self) -> float:
123
+ ho, wo = self.patches_resolution
124
+ flops = (
125
+ ho
126
+ * wo
127
+ * self.embed_dim
128
+ * self.in_chans
129
+ * (self.patch_size[0] * self.patch_size[1])
130
+ )
131
+ if self.norm is not None:
132
+ flops += ho * wo * self.embed_dim
133
+ return flops
134
+
135
+
136
+ class SwiGLUFFN(nn.Module):
137
+ """SwiGLU FFN layer, following DINOv2 implementation."""
138
+
139
+ def __init__(
140
+ self,
141
+ in_features: int,
142
+ hidden_features: Optional[int] = None,
143
+ out_features: Optional[int] = None,
144
+ act_layer: Callable[..., nn.Module] = None,
145
+ drop: float = 0.0,
146
+ bias: bool = True,
147
+ ) -> None:
148
+ super().__init__()
149
+ out_features = out_features or in_features
150
+ hidden_features = hidden_features or in_features
151
+ self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias)
152
+ self.w3 = nn.Linear(hidden_features, out_features, bias=bias)
153
+
154
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
155
+ x12 = self.w12(x)
156
+ x1, x2 = x12.chunk(2, dim=-1)
157
+ hidden = F.silu(x1) * x2
158
+ return self.w3(hidden)
159
+
160
+
161
+ XFORMERS_ENABLED = os.environ.get("XFORMERS_DISABLED") is None
162
+ try:
163
+ if XFORMERS_ENABLED:
164
+ from xformers.ops import SwiGLU, memory_efficient_attention, unbind, fmha, scaled_index_add, index_select_cat # pylint: disable=g-multiple-import, g-import-not-at-top
165
+
166
+ XFORMERS_AVAILABLE = True
167
+ warnings.warn("xFormers is available (SwiGLU)")
168
+ else:
169
+ warnings.warn("xFormers is disabled (SwiGLU)")
170
+ raise ImportError
171
+ except ImportError:
172
+ SwiGLU = SwiGLUFFN
173
+ XFORMERS_AVAILABLE = False
174
+
175
+ warnings.warn("xFormers is not available (SwiGLU)")
176
+
177
+
178
+ class SwiGLUFFNFused(SwiGLU):
179
+ """SwiGLU FFN layer, following DINOv2 implementation."""
180
+
181
+ def __init__(
182
+ self,
183
+ in_features: int,
184
+ hidden_features: Optional[int] = None,
185
+ out_features: Optional[int] = None,
186
+ act_layer: Callable[..., nn.Module] = None, # pylint: disable=unused-argument
187
+ drop: float = 0.0, # pylint: disable=unused-argument
188
+ bias: bool = True,
189
+ ) -> None:
190
+ out_features = out_features or in_features
191
+ hidden_features = hidden_features or in_features
192
+ hidden_features = (int(hidden_features * 2 / 3) + 7) // 8 * 8
193
+ super().__init__(
194
+ in_features=in_features,
195
+ hidden_features=hidden_features,
196
+ out_features=out_features,
197
+ bias=bias,
198
+ )
199
+
200
+
201
+ class Attention(nn.Module):
202
+ """Attention layer, following DINOv2 implementation."""
203
+
204
+ def __init__(
205
+ self,
206
+ dim: int,
207
+ num_heads: int = 8,
208
+ qkv_bias: bool = False,
209
+ proj_bias: bool = True,
210
+ attn_drop: float = 0.0,
211
+ proj_drop: float = 0.0,
212
+ ) -> None:
213
+ super().__init__()
214
+ self.num_heads = num_heads
215
+ head_dim = dim // num_heads
216
+ self.scale = head_dim**-0.5
217
+
218
+ self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
219
+ self.attn_drop = nn.Dropout(attn_drop)
220
+ self.proj = nn.Linear(dim, dim, bias=proj_bias)
221
+ self.proj_drop = nn.Dropout(proj_drop)
222
+
223
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
224
+ b_dim, n_dim, c_dim = x.shape
225
+ qkv = (
226
+ self.qkv(x)
227
+ .reshape(b_dim, n_dim, 3, self.num_heads, c_dim // self.num_heads)
228
+ .permute(2, 0, 3, 1, 4)
229
+ )
230
+
231
+ q, k, v = qkv[0] * self.scale, qkv[1], qkv[2]
232
+ attn = q @ k.transpose(-2, -1)
233
+
234
+ attn = attn.softmax(dim=-1)
235
+ attn = self.attn_drop(attn)
236
+
237
+ x = (attn @ v).transpose(1, 2).reshape(b_dim, n_dim, c_dim)
238
+ x = self.proj(x)
239
+ x = self.proj_drop(x)
240
+ return x
241
+
242
+
243
+ class MemEffAttention(Attention):
244
+ """Memory Efficient Attention layer, following DINOv2 implementation."""
245
+
246
+ def forward(self, x: torch.Tensor, attn_bias=None) -> torch.Tensor:
247
+ if not XFORMERS_AVAILABLE:
248
+ if attn_bias is not None:
249
+ raise AssertionError("xFormers is required for using nested tensors")
250
+ return super().forward(x)
251
+
252
+ b_dim, n_dim, c_dim = x.shape
253
+ qkv = self.qkv(x).reshape(
254
+ b_dim, n_dim, 3, self.num_heads, c_dim // self.num_heads
255
+ )
256
+
257
+ q, k, v = unbind(qkv, 2)
258
+
259
+ x = memory_efficient_attention(q, k, v, attn_bias=attn_bias)
260
+ x = x.reshape([b_dim, n_dim, c_dim])
261
+
262
+ x = self.proj(x)
263
+ x = self.proj_drop(x)
264
+ return x
265
+
266
+
267
+ class LayerScale(nn.Module):
268
+ """Layer scale, following DINOv2 implementation."""
269
+
270
+ def __init__(
271
+ self,
272
+ dim: int,
273
+ init_values: Union[float, torch.Tensor] = 1e-5,
274
+ inplace: bool = False,
275
+ ) -> None:
276
+ super().__init__()
277
+ self.inplace = inplace
278
+ self.gamma = nn.Parameter(init_values * torch.ones(dim))
279
+
280
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
281
+ return x.mul_(self.gamma) if self.inplace else x * self.gamma
282
+
283
+
284
+ def drop_path_impl(x, drop_prob: float = 0.0, training: bool = False):
285
+ if drop_prob == 0.0 or not training:
286
+ return x
287
+ keep_prob = 1 - drop_prob
288
+ shape = (x.shape[0],) + (1,) * (
289
+ x.ndim - 1
290
+ ) # work with diff dim tensors, not just 2D ConvNets
291
+ random_tensor = x.new_empty(shape).bernoulli_(keep_prob)
292
+ if keep_prob > 0.0:
293
+ random_tensor.div_(keep_prob)
294
+ output = x * random_tensor
295
+ return output
296
+
297
+
298
+ class DropPath(nn.Module):
299
+ """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks)."""
300
+
301
+ def __init__(self, drop_prob=None):
302
+ super(DropPath, self).__init__()
303
+ self.drop_prob = drop_prob
304
+
305
+ def forward(self, x):
306
+ return drop_path_impl(x, self.drop_prob, self.training)
307
+
308
+
309
+ class Block(nn.Module):
310
+ """Transformer Block Implementation, following DINOv2 implementation."""
311
+
312
+ def __init__(
313
+ self,
314
+ dim: int,
315
+ num_heads: int,
316
+ mlp_ratio: float = 4.0,
317
+ qkv_bias: bool = False,
318
+ proj_bias: bool = True,
319
+ ffn_bias: bool = True,
320
+ drop: float = 0.0,
321
+ attn_drop: float = 0.0,
322
+ init_values=None,
323
+ drop_path: float = 0.0,
324
+ act_layer: Callable[..., nn.Module] = nn.GELU,
325
+ norm_layer: Callable[..., nn.Module] = nn.LayerNorm,
326
+ attn_class: Callable[..., nn.Module] = Attention,
327
+ ffn_layer: Callable[..., nn.Module] = Mlp,
328
+ ) -> None:
329
+ super().__init__()
330
+ self.norm1 = norm_layer(dim)
331
+ self.attn = attn_class(
332
+ dim,
333
+ num_heads=num_heads,
334
+ qkv_bias=qkv_bias,
335
+ proj_bias=proj_bias,
336
+ attn_drop=attn_drop,
337
+ proj_drop=drop,
338
+ )
339
+ self.ls1 = (
340
+ LayerScale(dim, init_values=init_values)
341
+ if init_values
342
+ else nn.Identity()
343
+ )
344
+ self.drop_path1 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
345
+
346
+ self.norm2 = norm_layer(dim)
347
+ mlp_hidden_dim = int(dim * mlp_ratio)
348
+ self.mlp = ffn_layer(
349
+ in_features=dim,
350
+ hidden_features=mlp_hidden_dim,
351
+ act_layer=act_layer,
352
+ drop=drop,
353
+ bias=ffn_bias,
354
+ )
355
+ self.ls2 = (
356
+ LayerScale(dim, init_values=init_values)
357
+ if init_values
358
+ else nn.Identity()
359
+ )
360
+ self.drop_path2 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
361
+
362
+ self.sample_drop_ratio = drop_path
363
+
364
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
365
+ def attn_residual_func(x: torch.Tensor) -> torch.Tensor:
366
+ return self.ls1(self.attn(self.norm1(x)))
367
+
368
+ def ffn_residual_func(x: torch.Tensor) -> torch.Tensor:
369
+ return self.ls2(self.mlp(self.norm2(x)))
370
+
371
+ if self.training and self.sample_drop_ratio > 0.1:
372
+ # the overhead is compensated only for a drop path rate larger than 0.1
373
+ x = drop_add_residual_stochastic_depth(
374
+ x,
375
+ residual_func=attn_residual_func,
376
+ sample_drop_ratio=self.sample_drop_ratio,
377
+ )
378
+ x = drop_add_residual_stochastic_depth(
379
+ x,
380
+ residual_func=ffn_residual_func,
381
+ sample_drop_ratio=self.sample_drop_ratio,
382
+ )
383
+ elif self.training and self.sample_drop_ratio > 0.0:
384
+ x = x + self.drop_path1(attn_residual_func(x))
385
+ x = x + self.drop_path1(ffn_residual_func(x))
386
+ else:
387
+ x = x + attn_residual_func(x)
388
+ x = x + ffn_residual_func(x)
389
+ return x
390
+
391
+
392
+ def drop_add_residual_stochastic_depth(
393
+ x: torch.Tensor,
394
+ residual_func: Callable[[torch.Tensor], torch.Tensor],
395
+ sample_drop_ratio: float = 0.0,
396
+ ) -> torch.Tensor:
397
+ """This function is taken from the original implementation in DINOv2 to implement stochastic depth in the image encoder."""
398
+ # 1) extract subset using permutation
399
+ b, _, _ = x.shape
400
+ sample_subset_size = max(int(b * (1 - sample_drop_ratio)), 1)
401
+ brange = (torch.randperm(b, device=x.device))[:sample_subset_size]
402
+ x_subset = x[brange]
403
+
404
+ # 2) apply residual_func to get residual
405
+ residual = residual_func(x_subset)
406
+
407
+ x_flat = x.flatten(1)
408
+ residual = residual.flatten(1)
409
+
410
+ residual_scale_factor = b / sample_subset_size
411
+
412
+ # 3) add the residual
413
+ x_plus_residual = torch.index_add(
414
+ x_flat, 0, brange, residual.to(dtype=x.dtype), alpha=residual_scale_factor
415
+ )
416
+ return x_plus_residual.view_as(x)
417
+
418
+
419
+ def get_branges_scales(x, sample_drop_ratio=0.0):
420
+ b, _, _ = x.shape
421
+ sample_subset_size = max(int(b * (1 - sample_drop_ratio)), 1)
422
+ brange = (torch.randperm(b, device=x.device))[:sample_subset_size]
423
+ residual_scale_factor = b / sample_subset_size
424
+ return brange, residual_scale_factor
425
+
426
+
427
+ def add_residual(
428
+ x, brange, residual, residual_scale_factor, scaling_vector=None
429
+ ):
430
+ """Implement residual addition in the image encoder."""
431
+ if scaling_vector is None:
432
+ x_flat = x.flatten(1)
433
+ residual = residual.flatten(1)
434
+ x_plus_residual = torch.index_add(
435
+ x_flat,
436
+ 0,
437
+ brange,
438
+ residual.to(dtype=x.dtype),
439
+ alpha=residual_scale_factor,
440
+ )
441
+ else:
442
+ x_plus_residual = scaled_index_add(
443
+ x,
444
+ brange,
445
+ residual.to(dtype=x.dtype),
446
+ scaling=scaling_vector,
447
+ alpha=residual_scale_factor,
448
+ )
449
+ return x_plus_residual
450
+
451
+
452
+ attn_bias_cache: Dict[Tuple, Any] = {} # pylint: disable=g-bare-generic
453
+
454
+
455
+ def get_attn_bias_and_cat(x_list, branges=None):
456
+ """this will perform the index select, cat the tensors, and provide the attn_bias from cache."""
457
+ batch_sizes = (
458
+ [b.shape[0] for b in branges]
459
+ if branges is not None
460
+ else [x.shape[0] for x in x_list]
461
+ )
462
+ all_shapes = tuple((b, x.shape[1]) for b, x in zip(batch_sizes, x_list))
463
+ if all_shapes not in attn_bias_cache.keys():
464
+ seqlens = []
465
+ for b, x in zip(batch_sizes, x_list):
466
+ for _ in range(b):
467
+ seqlens.append(x.shape[1])
468
+ attn_bias = fmha.BlockDiagonalMask.from_seqlens(seqlens)
469
+ attn_bias._batch_sizes = batch_sizes # pylint: disable=protected-access
470
+ attn_bias_cache[all_shapes] = attn_bias
471
+
472
+ if branges is not None:
473
+ cat_tensors = index_select_cat(
474
+ [x.flatten(1) for x in x_list], branges
475
+ ).view(1, -1, x_list[0].shape[-1])
476
+ else:
477
+ tensors_bs1 = tuple(x.reshape([1, -1, *x.shape[2:]]) for x in x_list)
478
+ cat_tensors = torch.cat(tensors_bs1, dim=1)
479
+
480
+ return attn_bias_cache[all_shapes], cat_tensors
481
+
482
+
483
+ def drop_add_residual_stochastic_depth_list(
484
+ x_list: List[torch.Tensor],
485
+ residual_func: Callable[[torch.Tensor, Any], torch.Tensor],
486
+ sample_drop_ratio: float = 0.0,
487
+ scaling_vector=None,
488
+ ) -> torch.Tensor:
489
+ """Add residual to a list of tensors."""
490
+ # 1) generate random set of indices for dropping samples in the batch.
491
+ branges_scales = [
492
+ get_branges_scales(x, sample_drop_ratio=sample_drop_ratio) for x in x_list
493
+ ]
494
+ branges = [s[0] for s in branges_scales]
495
+ residual_scale_factors = [s[1] for s in branges_scales]
496
+
497
+ # 2) get attention bias and index+concat the tensors.
498
+ attn_bias, x_cat = get_attn_bias_and_cat(x_list, branges)
499
+
500
+ # 3) apply residual_func to get residual, and split the result.
501
+ residual_list = attn_bias.split(residual_func(x_cat, attn_bias=attn_bias)) # type: ignore
502
+
503
+ outputs = []
504
+ for x, brange, residual, residual_scale_factor in zip(
505
+ x_list, branges, residual_list, residual_scale_factors
506
+ ):
507
+ outputs.append(
508
+ add_residual(
509
+ x, brange, residual, residual_scale_factor, scaling_vector
510
+ ).view_as(x)
511
+ )
512
+ return outputs
513
+
514
+
515
+ class NestedTensorBlock(Block):
516
+ """Nested tensor block implementation."""
517
+
518
+ def forward_nested(self, x_list: List[torch.Tensor]) -> List[torch.Tensor]:
519
+ """x_list contains a list of tensors to nest together and run."""
520
+ assert isinstance(self.attn, MemEffAttention)
521
+
522
+ if self.training and self.sample_drop_ratio > 0.0:
523
+
524
+ def attn_residual_func(x: torch.Tensor, attn_bias=None) -> torch.Tensor:
525
+ return self.attn(self.norm1(x), attn_bias=attn_bias)
526
+
527
+ def ffn_residual_func(x: torch.Tensor, attn_bias=None) -> torch.Tensor:
528
+ del attn_bias
529
+ return self.mlp(self.norm2(x))
530
+
531
+ x_list = drop_add_residual_stochastic_depth_list(
532
+ x_list,
533
+ residual_func=attn_residual_func,
534
+ sample_drop_ratio=self.sample_drop_ratio,
535
+ scaling_vector=self.ls1.gamma
536
+ if isinstance(self.ls1, LayerScale)
537
+ else None,
538
+ )
539
+ x_list = drop_add_residual_stochastic_depth_list(
540
+ x_list,
541
+ residual_func=ffn_residual_func,
542
+ sample_drop_ratio=self.sample_drop_ratio,
543
+ scaling_vector=self.ls2.gamma
544
+ if isinstance(self.ls1, LayerScale)
545
+ else None,
546
+ )
547
+ return x_list
548
+ else:
549
+
550
+ def attn_residual_func(x: torch.Tensor, attn_bias=None) -> torch.Tensor:
551
+ return self.ls1(self.attn(self.norm1(x), attn_bias=attn_bias))
552
+
553
+ def ffn_residual_func(x: torch.Tensor, attn_bias=None) -> torch.Tensor:
554
+ del attn_bias
555
+ return self.ls2(self.mlp(self.norm2(x)))
556
+
557
+ attn_bias, x = get_attn_bias_and_cat(x_list)
558
+ x = x + attn_residual_func(x, attn_bias=attn_bias)
559
+ x = x + ffn_residual_func(x)
560
+ return attn_bias.split(x)
561
+
562
+ def forward(self, x):
563
+ if isinstance(x, torch.Tensor):
564
+ return super().forward(x)
565
+ elif isinstance(x, list):
566
+ if not XFORMERS_AVAILABLE:
567
+ raise AssertionError("xFormers is required for using nested tensors")
568
+ return self.forward_nested(x)
569
+ else:
570
+ raise AssertionError
571
+
572
+
573
+ def named_apply(
574
+ fn: Callable, # pylint: disable=g-bare-generic
575
+ module: nn.Module,
576
+ name="",
577
+ depth_first=True,
578
+ include_root=False,
579
+ ) -> nn.Module:
580
+ """Apply a function to a module and its children."""
581
+ if not depth_first and include_root:
582
+ fn(module=module, name=name)
583
+ for child_name, child_module in module.named_children():
584
+ child_name = ".".join((name, child_name)) if name else child_name
585
+ named_apply(
586
+ fn=fn,
587
+ module=child_module,
588
+ name=child_name,
589
+ depth_first=depth_first,
590
+ include_root=True,
591
+ )
592
+ if depth_first and include_root:
593
+ fn(module=module, name=name)
594
+ return module
595
+
596
+
597
+ class BlockChunk(nn.ModuleList):
598
+
599
+ def forward(self, x):
600
+ for b in self:
601
+ x = b(x)
602
+ return x
603
+
604
+
605
+ class VisionTransformer(nn.Module):
606
+ """Vision Transformer implementation."""
607
+
608
+ def __init__(
609
+ self,
610
+ img_size=224,
611
+ patch_size=16,
612
+ in_chans=3,
613
+ embed_dim=768,
614
+ depth=12,
615
+ num_heads=12,
616
+ mlp_ratio=4.0,
617
+ qkv_bias=True,
618
+ ffn_bias=True,
619
+ proj_bias=True,
620
+ drop_path_rate=0.0,
621
+ drop_path_uniform=False,
622
+ init_values=None, # for layerscale: None or 0 => no layerscale
623
+ embed_layer=PatchEmbed,
624
+ act_layer=nn.GELU,
625
+ block_fn=Block,
626
+ ffn_layer="mlp",
627
+ block_chunks=1,
628
+ num_register_tokens=0,
629
+ interpolate_antialias=False,
630
+ interpolate_offset=0.1,
631
+ ):
632
+ """Defines the Vision Transformer model.
633
+
634
+ Args:
635
+ img_size (int, tuple): input image size
636
+ patch_size (int, tuple): patch size
637
+ in_chans (int): number of input channels
638
+ embed_dim (int): embedding dimension
639
+ depth (int): depth of transformer
640
+ num_heads (int): number of attention heads
641
+ mlp_ratio (int): ratio of mlp hidden dim to embedding dim
642
+ qkv_bias (bool): enable bias for qkv if True
643
+ ffn_bias (bool): enable bias for ffn if True
644
+ proj_bias (bool): enable bias for proj in attn if True
645
+ drop_path_rate (float): stochastic depth rate
646
+ drop_path_uniform (bool): apply uniform drop rate across blocks
647
+ init_values (float): layer-scale init values
648
+ embed_layer (nn.Module): patch embedding layer
649
+ act_layer (nn.Module): MLP activation layer
650
+ block_fn (nn.Module): transformer block class
651
+ ffn_layer (str): "mlp", "swiglu", "swiglufused" or "identity"
652
+ block_chunks: (int) split block sequence into block_chunks units for FSDP
653
+ wrap
654
+ num_register_tokens: (int) number of extra cls tokens (so-called
655
+ "registers")
656
+ interpolate_antialias: (str) flag to apply anti-aliasing when
657
+ interpolating positional embeddings
658
+ interpolate_offset: (float) work-around offset to apply when interpolating
659
+ positional embeddings
660
+ """
661
+ super().__init__()
662
+ norm_layer = functools.partial(nn.LayerNorm, eps=1e-6)
663
+
664
+ self.num_features = self.embed_dim = (
665
+ embed_dim # num_features for consistency with other models
666
+ )
667
+ self.num_tokens = 1
668
+ self.n_blocks = depth
669
+ self.num_heads = num_heads
670
+ self.patch_size = patch_size
671
+ self.num_register_tokens = num_register_tokens
672
+ self.interpolate_antialias = interpolate_antialias
673
+ self.interpolate_offset = interpolate_offset
674
+
675
+ self.patch_embed = embed_layer(
676
+ img_size=img_size,
677
+ patch_size=patch_size,
678
+ in_chans=in_chans,
679
+ embed_dim=embed_dim,
680
+ )
681
+ num_patches = self.patch_embed.num_patches
682
+
683
+ self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
684
+ self.pos_embed = nn.Parameter(
685
+ torch.zeros(1, num_patches + self.num_tokens, embed_dim)
686
+ )
687
+ assert num_register_tokens >= 0
688
+ self.register_tokens = (
689
+ nn.Parameter(torch.zeros(1, num_register_tokens, embed_dim))
690
+ if num_register_tokens
691
+ else None
692
+ )
693
+
694
+ if drop_path_uniform:
695
+ dpr = [drop_path_rate] * depth
696
+ else:
697
+ dpr = [
698
+ x.item() for x in torch.linspace(0, drop_path_rate, depth)
699
+ ] # stochastic depth decay rule
700
+
701
+ if ffn_layer == "mlp":
702
+ ffn_layer = Mlp
703
+ elif ffn_layer == "swiglufused" or ffn_layer == "swiglu":
704
+ ffn_layer = SwiGLUFFNFused
705
+ else:
706
+ raise NotImplementedError
707
+
708
+ blocks_list = [
709
+ block_fn(
710
+ dim=embed_dim,
711
+ num_heads=num_heads,
712
+ mlp_ratio=mlp_ratio,
713
+ qkv_bias=qkv_bias,
714
+ proj_bias=proj_bias,
715
+ ffn_bias=ffn_bias,
716
+ drop_path=dpr[i],
717
+ norm_layer=norm_layer,
718
+ act_layer=act_layer,
719
+ ffn_layer=ffn_layer,
720
+ init_values=init_values,
721
+ )
722
+ for i in range(depth)
723
+ ]
724
+ if block_chunks > 0:
725
+ self.chunked_blocks = True
726
+ chunked_blocks = []
727
+ chunksize = depth // block_chunks
728
+ for i in range(0, depth, chunksize):
729
+ # this is to keep the block index consistent if we chunk the block list
730
+ chunked_blocks.append(
731
+ [nn.Identity()] * i + blocks_list[i : i + chunksize]
732
+ )
733
+ self.blocks = nn.ModuleList([BlockChunk(p) for p in chunked_blocks])
734
+ else:
735
+ self.chunked_blocks = False
736
+ self.blocks = nn.ModuleList(blocks_list)
737
+
738
+ self.norm = norm_layer(embed_dim)
739
+ self.head = nn.Identity()
740
+
741
+ self.mask_token = nn.Parameter(torch.zeros(1, embed_dim))
742
+
743
+ self.init_weights()
744
+
745
+ def init_weights(self):
746
+ nn.init.trunc_normal_(self.pos_embed, std=0.02)
747
+ nn.init.normal_(self.cls_token, std=1e-6)
748
+ if self.register_tokens is not None:
749
+ nn.init.normal_(self.register_tokens, std=1e-6)
750
+ named_apply(init_weights_vit_timm, self)
751
+
752
+ def interpolate_pos_encoding(self, x, w, h):
753
+ previous_dtype = x.dtype
754
+ npatch = x.shape[1] - 1
755
+ num_patches = self.pos_embed.shape[1] - 1
756
+ if npatch == num_patches and w == h:
757
+ return self.pos_embed
758
+ pos_embed = self.pos_embed.float()
759
+ class_pos_embed = pos_embed[:, 0]
760
+ patch_pos_embed = pos_embed[:, 1:]
761
+ dim = x.shape[-1]
762
+ w0 = w // self.patch_size
763
+ h0 = h // self.patch_size
764
+ num_patches_dim = int(
765
+ math.sqrt(num_patches)
766
+ ) # Recover the number of patches in each dimension
767
+ assert num_patches == num_patches_dim * num_patches_dim
768
+ kwargs = {}
769
+ if self.interpolate_offset:
770
+ sx = float(w0 + self.interpolate_offset) / num_patches_dim
771
+ sy = float(h0 + self.interpolate_offset) / num_patches_dim
772
+ kwargs["scale_factor"] = (sx, sy)
773
+ else:
774
+ # Simply specify an output size instead of a scale factor
775
+ kwargs["size"] = (w0, h0)
776
+ patch_pos_embed = nn.functional.interpolate(
777
+ patch_pos_embed.reshape(
778
+ 1, num_patches_dim, num_patches_dim, dim
779
+ ).permute(0, 3, 1, 2),
780
+ mode="bilinear",
781
+ antialias=self.interpolate_antialias,
782
+ **kwargs,
783
+ )
784
+ assert (w0, h0) == patch_pos_embed.shape[-2:]
785
+ patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(1, -1, dim)
786
+ return torch.cat((class_pos_embed.unsqueeze(0), patch_pos_embed), dim=1).to(
787
+ previous_dtype
788
+ )
789
+
790
+ def prepare_tokens_with_masks(self, x, masks=None):
791
+ _, _, w, h = x.shape
792
+ x = self.patch_embed(x)
793
+ if masks is not None:
794
+ x = torch.where(
795
+ masks.unsqueeze(-1), self.mask_token.to(x.dtype).unsqueeze(0), x
796
+ )
797
+
798
+ x = torch.cat((self.cls_token.expand(x.shape[0], -1, -1), x), dim=1)
799
+ x = x + self.interpolate_pos_encoding(x, w, h)
800
+
801
+ if self.register_tokens is not None:
802
+ x = torch.cat(
803
+ (
804
+ x[:, :1],
805
+ self.register_tokens.expand(x.shape[0], -1, -1),
806
+ x[:, 1:],
807
+ ),
808
+ dim=1,
809
+ )
810
+
811
+ return x
812
+
813
+ def forward_features_list(self, x_list, masks_list):
814
+ x = [
815
+ self.prepare_tokens_with_masks(x, masks)
816
+ for x, masks in zip(x_list, masks_list)
817
+ ]
818
+ for blk in self.blocks:
819
+ x = blk(x)
820
+
821
+ all_x = x
822
+ output = []
823
+ for x, masks in zip(all_x, masks_list):
824
+ x_norm = self.norm(x)
825
+ output.append({
826
+ "x_norm_1st_clstoken": x_norm[:, :1],
827
+ "x_norm_2nd_clstoken": x_norm[:, 1 : self.num_register_tokens + 1],
828
+ "x_norm_patchtokens": x_norm[:, self.num_register_tokens + 1 :],
829
+ "x_prenorm": x,
830
+ "masks": masks,
831
+ })
832
+ return output
833
+
834
+ def forward_features(self, x, masks=None):
835
+ if isinstance(x, list):
836
+ return self.forward_features_list(x, masks)
837
+
838
+ x = self.prepare_tokens_with_masks(x, masks)
839
+
840
+ for blk in self.blocks:
841
+ x = blk(x)
842
+
843
+ x_norm = self.norm(x)
844
+ return {
845
+ "x_norm_1st_clstoken": x_norm[:, :1],
846
+ "x_norm_2nd_clstoken": x_norm[:, 1 : self.num_register_tokens + 1],
847
+ "x_norm_patchtokens": x_norm[:, self.num_register_tokens + 1 :],
848
+ "x_prenorm": x,
849
+ "masks": masks,
850
+ }
851
+
852
+ def _get_intermediate_layers_not_chunked(self, x, n=1):
853
+ x = self.prepare_tokens_with_masks(x)
854
+ # If n is an int, take the n last blocks. If it's a list, take them
855
+ output, total_block_len = [], len(self.blocks)
856
+ blocks_to_take = (
857
+ range(total_block_len - n, total_block_len) if isinstance(n, int) else n
858
+ )
859
+ for i, blk in enumerate(self.blocks):
860
+ x = blk(x)
861
+ if i in blocks_to_take:
862
+ output.append(x)
863
+ assert len(output) == len(
864
+ blocks_to_take
865
+ ), f"only {len(output)} / {len(blocks_to_take)} blocks found"
866
+ return output
867
+
868
+ def _get_intermediate_layers_chunked(self, x, n=1):
869
+ x = self.prepare_tokens_with_masks(x)
870
+ output, i, total_block_len = [], 0, len(self.blocks[-1])
871
+ # If n is an int, take the n last blocks. If it's a list, take them
872
+ blocks_to_take = (
873
+ range(total_block_len - n, total_block_len) if isinstance(n, int) else n
874
+ )
875
+ for block_chunk in self.blocks:
876
+ for blk in block_chunk[i:]: # Passing the nn.Identity()
877
+ x = blk(x)
878
+ if i in blocks_to_take:
879
+ output.append(x)
880
+ i += 1
881
+ assert len(output) == len(
882
+ blocks_to_take
883
+ ), f"only {len(output)} / {len(blocks_to_take)} blocks found"
884
+ return output
885
+
886
+ def get_intermediate_layers(
887
+ self,
888
+ x: torch.torch.Tensor,
889
+ n: Union[int, Sequence] = 1, # Layers or n last layers to take # pylint: disable=g-bare-generic
890
+ reshape: bool = False,
891
+ return_class_token: bool = False,
892
+ norm=True,
893
+ ) -> Tuple[Union[torch.torch.Tensor, Tuple[torch.torch.Tensor]]]: # pylint: disable=g-one-element-tuple
894
+ if self.chunked_blocks:
895
+ outputs = self._get_intermediate_layers_chunked(x, n)
896
+ else:
897
+ outputs = self._get_intermediate_layers_not_chunked(x, n)
898
+ if norm:
899
+ outputs = [self.norm(out) for out in outputs]
900
+ class_tokens = [out[:, 0] for out in outputs]
901
+ outputs = [out[:, 1 + self.num_register_tokens :] for out in outputs]
902
+ if reshape:
903
+ batch_size, _, w, h = x.shape
904
+ outputs = [
905
+ out.reshape(
906
+ batch_size, w // self.patch_size, h // self.patch_size, -1
907
+ )
908
+ .permute(0, 3, 1, 2)
909
+ .contiguous()
910
+ for out in outputs
911
+ ]
912
+ if return_class_token:
913
+ return tuple(zip(outputs, class_tokens))
914
+ return tuple(outputs)
915
+
916
+ def forward(self, *args, is_training=False, **kwargs):
917
+ ret = self.forward_features(*args, **kwargs)
918
+ if is_training:
919
+ return ret
920
+ else:
921
+ return self.head(ret["x_norm_1st_clstoken"]), self.head(
922
+ ret["x_norm_2nd_clstoken"]
923
+ ), ret["x_norm_patchtokens"]
924
+
925
+
926
+ def init_weights_vit_timm(module: nn.Module, name: str = ""): # pylint: disable=unused-argument
927
+ """ViT weight initialization, original timm impl (for reproducibility)."""
928
+ if isinstance(module, nn.Linear):
929
+ nn.init.trunc_normal_(module.weight, std=0.02)
930
+ if module.bias is not None:
931
+ nn.init.zeros_(module.bias)
932
+
933
+
934
+ def vit_small(patch_size=14, **kwargs):
935
+ model = VisionTransformer(
936
+ patch_size=patch_size,
937
+ embed_dim=384,
938
+ depth=12,
939
+ num_heads=6,
940
+ mlp_ratio=4,
941
+ block_fn=functools.partial(Block, attn_class=MemEffAttention),
942
+ num_register_tokens=1,
943
+ **kwargs,
944
+ )
945
+ return model
946
+
947
+
948
+ def vit_base(patch_size=14, **kwargs):
949
+ model = VisionTransformer(
950
+ patch_size=patch_size,
951
+ embed_dim=768,
952
+ depth=12,
953
+ num_heads=12,
954
+ mlp_ratio=4,
955
+ block_fn=functools.partial(Block, attn_class=MemEffAttention),
956
+ num_register_tokens=1,
957
+ **kwargs,
958
+ )
959
+ return model
960
+
961
+
962
+ def vit_large(patch_size=14, **kwargs):
963
+ model = VisionTransformer(
964
+ patch_size=patch_size,
965
+ embed_dim=1024,
966
+ depth=24,
967
+ num_heads=16,
968
+ mlp_ratio=4,
969
+ block_fn=functools.partial(Block, attn_class=MemEffAttention),
970
+ num_register_tokens=1,
971
+ **kwargs,
972
+ )
973
+ return model
974
+
975
+
976
+ def vit_so400m(patch_size=14, **kwargs):
977
+ """SoViT 400M model (https://arxiv.org/abs/2305.13035)."""
978
+ model = VisionTransformer(
979
+ patch_size=patch_size,
980
+ embed_dim=1152,
981
+ depth=27,
982
+ num_heads=16,
983
+ mlp_ratio=4304 / 1152,
984
+ block_fn=functools.partial(Block, attn_class=MemEffAttention),
985
+ num_register_tokens=1,
986
+ **kwargs,
987
+ )
988
+ return model
989
+
990
+
991
+ def vit_giant2(patch_size=14, **kwargs):
992
+ model = VisionTransformer(
993
+ patch_size=patch_size,
994
+ embed_dim=1536,
995
+ depth=40,
996
+ num_heads=24,
997
+ mlp_ratio=4,
998
+ block_fn=functools.partial(Block, attn_class=MemEffAttention),
999
+ num_register_tokens=1,
1000
+ **kwargs,
1001
+ )
1002
+ return model
Tipsomaly/model/tips/load_model.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import numpy as np
3
+ import torch
4
+ import os
5
+
6
+ from .text_encoder import TextEncoder, Tokenizer
7
+ # from .image_encoder import vit_small, vit_base, vit_large, vit_so400m, vit_giant2
8
+ from .image_encoder import Block, VisionTransformer, MemEffAttention
9
+ import functools
10
+
11
+ from .checkpoints import checkpoint
12
+
13
+ MAX_LEN = 64
14
+ VOCAB_SIZE = 32000
15
+ PATCH_SIZE = 14
16
+
17
+ vision_models = {
18
+ 'S': {'embed_dim': 384, 'depth': 12, 'num_heads': 6, 'mlp_ratio': 4.0},
19
+ 'B': {'embed_dim': 768, 'depth': 12, 'num_heads': 12, 'mlp_ratio': 4.0},
20
+ 'L': {'embed_dim': 1024, 'depth': 24, 'num_heads': 16, 'mlp_ratio': 4.0},
21
+ 'So400m': {'embed_dim': 1152, 'depth': 27, 'num_heads': 16, 'mlp_ratio': 4304/1152},
22
+ 'G': {'embed_dim': 1536, 'depth': 40, 'num_heads': 24, 'mlp_ratio': 4.0},
23
+ }
24
+ def build_vision_encoder(cfg: dict, *, img_size, patch_size, ffn_layer):
25
+ return VisionTransformer(
26
+ patch_size=patch_size,
27
+ embed_dim=cfg['embed_dim'],
28
+ depth=cfg['depth'],
29
+ num_heads=cfg['num_heads'],
30
+ mlp_ratio=cfg['mlp_ratio'],
31
+ block_fn=functools.partial(Block, attn_class=MemEffAttention),
32
+ num_register_tokens=1,
33
+ img_size=img_size,
34
+ ffn_layer=ffn_layer,
35
+ block_chunks=0,
36
+ init_values=1.0,
37
+ interpolate_antialias=True,
38
+ interpolate_offset=0.0,
39
+ )
40
+
41
+ def load_image_encoder(model_weights_path, model_variant, is_low_res, patch_size = 14):
42
+ img_size = 224 if is_low_res else 448
43
+ ffn_layer = 'swiglu' if model_variant == 'G' else 'mlp'
44
+ cfg = vision_models[model_variant]
45
+
46
+ checkpoint_np = dict(np.load(model_weights_path, allow_pickle=False))
47
+ checkpoint = {k: torch.tensor(v) for k, v in checkpoint_np.items()}
48
+
49
+ with torch.no_grad():
50
+ model = build_vision_encoder(cfg, img_size=img_size, patch_size=patch_size, ffn_layer=ffn_layer)
51
+ missing, unexpected = model.load_state_dict(checkpoint, strict=False)
52
+ # Optional: sanity logs
53
+ if missing:
54
+ print(f"[vision:{model_variant}] Missing keys: {len(missing)} (e.g. {missing[:3]})")
55
+ if unexpected:
56
+ print(f"[vision:{model_variant}] Unexpected keys: {len(unexpected)} (e.g. {unexpected[:3]})")
57
+
58
+ return model
59
+
60
+ text_models = {
61
+ 'S': {'hidden_size': 384, 'mlp_dim': 1536, 'num_heads': 6, 'num_layers': 12},
62
+ 'B': {'hidden_size': 768, 'mlp_dim': 3072, 'num_heads': 12, 'num_layers': 12},
63
+ 'L': {'hidden_size': 1024, 'mlp_dim': 4096, 'num_heads': 16, 'num_layers': 12},
64
+ 'So400m': {'hidden_size': 1152, 'mlp_dim': 4304, 'num_heads': 16, 'num_layers': 27},
65
+ 'G': {'hidden_size': 1536, 'mlp_dim': 6144, 'num_heads': 24, 'num_layers': 12},
66
+ }
67
+ def load_text_encoder(model_path, model_variant, tokenizer_path):
68
+ with open(model_path, 'rb') as fin:
69
+ inbuffer = io.BytesIO(fin.read())
70
+ np_weights_text = np.load(inbuffer, allow_pickle=False)
71
+
72
+ weights_text = {}
73
+ for key, value in np_weights_text.items():
74
+ weights_text[key] = torch.from_numpy(value)
75
+ temperature = weights_text.pop('temperature')
76
+
77
+ with torch.no_grad():
78
+ # Define the text model.
79
+ model_text = TextEncoder(
80
+ text_models[model_variant],
81
+ vocab_size=VOCAB_SIZE,
82
+ )
83
+ model_text.load_state_dict(weights_text)
84
+
85
+ tokenizer_obj = Tokenizer(tokenizer_path=tokenizer_path)
86
+ return model_text, tokenizer_obj, temperature
87
+
88
+
89
+ CHECKPOINT_TO_VARIANT = {
90
+ "s14h": "S",
91
+ "b14h": "B",
92
+ "l14h": "L",
93
+ "so4h": "So400m", # 400m = big SoViT model
94
+ "g14l": "G", # Giant low-res
95
+ "g14h": "G", # Giant high-res
96
+ }
97
+ def get_model(model_path, model_checkpoint):
98
+ paths = checkpoint.ensure_model_files(model_checkpoint, model_path)
99
+ for key, path in paths.items():
100
+ print(f"{key}: {path}")
101
+
102
+ tokenizer_path = os.path.join(model_path, 'tokenizer.model')
103
+ image_enc_name, text_enc_name = checkpoint._model_files_for_basename(checkpoint.MODEL_REGISTRY[model_checkpoint])
104
+ image_encoder_path = os.path.join(model_path, image_enc_name)
105
+ text_encoder_path = os.path.join(model_path, text_enc_name)
106
+ is_low_res = model_checkpoint.endswith('l')
107
+
108
+ model_variant = CHECKPOINT_TO_VARIANT[model_checkpoint]
109
+ image_encoder = load_image_encoder(image_encoder_path, model_variant, is_low_res)
110
+ text_encoder, tokenizer, temperature = load_text_encoder(text_encoder_path, model_variant, tokenizer_path)
111
+
112
+ return image_encoder, text_encoder, tokenizer, temperature
Tipsomaly/model/tips/text_encoder.py ADDED
@@ -0,0 +1,519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Text encoder implementation in PyTorch."""
17
+
18
+ import typing as t
19
+
20
+ # import tensorflow as tf
21
+ # import tensorflow_text
22
+ import torch
23
+ from torch import nn
24
+ import torch.nn.functional as F
25
+
26
+ import torch
27
+ import sentencepiece as spm
28
+
29
+ class Tokenizer:
30
+ """A simple tokenizer using SentencePiece with batch support."""
31
+
32
+ def __init__(self, tokenizer_path: str):
33
+ """Initializes the tokenizer."""
34
+ self.tokenizer = spm.SentencePieceProcessor(model_file=tokenizer_path)
35
+
36
+ def tokenize(self, input_texts, max_len=64):
37
+ """Tokenizes a batch of input texts and pads/clips them to max_len."""
38
+ if isinstance(input_texts, str):
39
+ input_texts = [input_texts] # Convert single string to list
40
+
41
+ input_texts = [text.lower() for text in input_texts] # Lowercasing batch
42
+ tokenized = [self.tokenizer.encode(text) for text in input_texts] # Tokenize batch
43
+
44
+ # Pad or truncate to max_len
45
+ padded_tokens = []
46
+ padding_masks = []
47
+ for tokens in tokenized:
48
+ curr_len = len(tokens)
49
+ if curr_len > max_len:
50
+ tokens = tokens[:max_len] # Truncate
51
+ else:
52
+ tokens += [0] * (max_len - curr_len) # Pad
53
+
54
+ padded_tokens.append(tokens)
55
+ padding_masks.append([0] * curr_len + [1] * (max_len - curr_len)) # Mask (1 for real tokens, 0 for padding)
56
+
57
+ # Convert to PyTorch tensors
58
+ tokens_tensor = torch.tensor(padded_tokens, dtype=torch.int32)
59
+ padding_mask_tensor = torch.tensor(padding_masks, dtype=torch.int32)
60
+
61
+ return tokens_tensor, padding_mask_tensor
62
+
63
+
64
+ # class Tokenizer(object):
65
+ # """A simple tokenizer."""
66
+
67
+ # def __init__(self, tokenizer_path: str):
68
+ # """Initializes the tokenizer."""
69
+ # with open(tokenizer_path, 'rb') as f:
70
+ # model = f.read()
71
+ # self.tokenizer = tensorflow_text.SentencepieceTokenizer(
72
+ # model=model, add_eos=False, add_bos=False
73
+ # )
74
+
75
+ # def tokenize(self, input_text, max_len=64):
76
+ # tokens = self.tokenizer.tokenize(tf.strings.lower(input_text)).to_tensor()
77
+ # curr_len = tokens.shape[1]
78
+ # is_padding = tf.zeros((tokens.shape[0], max_len))
79
+ # if curr_len > max_len:
80
+ # tokens = tokens[:, :max_len]
81
+ # else:
82
+ # padding_len = max_len - curr_len
83
+ # tokens = tf.pad(tokens, [[0, 0], [0, padding_len]], constant_values=0)
84
+ # is_padding = tf.cast(tokens == 0, tf.int32)
85
+ # return tokens.numpy(), is_padding.numpy()
86
+
87
+
88
+ class PositionalEmbedding(nn.Module):
89
+ """Generates position embedding for a given 1-d sequence.
90
+
91
+ Attributes:
92
+ min_timescale: Start of the geometric index. Determines the periodicity of
93
+ the added signal.
94
+ max_timescale: End of the geometric index. Determines the frequency of the
95
+ added signal.
96
+ embedding_dim: Dimension of the embedding to be generated.
97
+ """
98
+
99
+ min_timescale: int = 1
100
+ max_timescale: int = 10_000
101
+ embedding_dim: int = 0
102
+
103
+ def __init__(self, embedding_dim: int):
104
+ super().__init__()
105
+ self.embedding_dim = embedding_dim
106
+
107
+ def __call__(self, seq_length: int = None, position: torch.tensor = None):
108
+ """Generates a torch.tensor of sinusoids with different frequencies.
109
+
110
+ Args:
111
+ seq_length: an optional Python int defining the output sequence length.
112
+ if the `position` argument is specified.
113
+ position: [B, seq_length], optional position for each token in the
114
+ sequence, only required when the sequence is packed.
115
+
116
+ Returns:
117
+ [B, seqlen, D] if `position` is specified, else [1, seqlen, D]
118
+ """
119
+ if position is None:
120
+ assert seq_length is not None
121
+ # [1, seqlen]
122
+ position = torch.arange(seq_length, dtype=torch.float32)[None, :]
123
+ else:
124
+ assert position.ndim == 2, position.shape
125
+
126
+ num_timescales = self.embedding_dim // 2
127
+ log_timescale_increment = torch.log(
128
+ torch.tensor(float(self.max_timescale) / float(self.min_timescale))
129
+ ) / torch.maximum(
130
+ torch.tensor(num_timescales, dtype=torch.float32) - 1, torch.tensor(1)
131
+ )
132
+ inv_timescales = self.min_timescale * torch.exp(
133
+ torch.arange(num_timescales, dtype=torch.float32)
134
+ * -log_timescale_increment
135
+ )
136
+ scaled_time = position[:, :, None] * inv_timescales[None, None, :]
137
+ signal = torch.cat((torch.sin(scaled_time), torch.cos(scaled_time)), dim=2)
138
+ # Force usage of `np` rather than `jnp` to compute static values at trace
139
+ # time.
140
+ signal = F.pad(signal, (0, self.embedding_dim % 2, 0, 0, 0, 0))
141
+ return signal
142
+
143
+
144
+ class MlpBlockWithMask(nn.Module):
145
+ """Transformer MLP / feed-forward block that supports masking."""
146
+
147
+ def __init__(
148
+ self,
149
+ mlp_dim: int,
150
+ d_model: int,
151
+ use_bias: bool = True,
152
+ dtype: torch.dtype = torch.float32,
153
+ activation_fn: nn.Module = nn.GELU,
154
+ ):
155
+ super().__init__()
156
+
157
+ self.mlp_dim = mlp_dim
158
+ self.d_model = d_model
159
+ self.use_bias = use_bias
160
+ self.dtype = dtype
161
+ self.activation_fn = activation_fn
162
+
163
+ self.c_fc = nn.Linear(
164
+ in_features=self.d_model,
165
+ out_features=self.mlp_dim,
166
+ dtype=self.dtype,
167
+ bias=self.use_bias,
168
+ )
169
+ self.c_proj = nn.Linear(
170
+ in_features=self.mlp_dim,
171
+ out_features=self.d_model,
172
+ dtype=self.dtype,
173
+ bias=self.use_bias,
174
+ )
175
+
176
+ def __call__(
177
+ self, inputs: torch.Tensor, mlp_mask: torch.Tensor
178
+ ) -> torch.Tensor:
179
+ """Applies Transformer MlpBlock with mask module."""
180
+ x = self.c_fc(inputs)
181
+ x = self.activation_fn()(x)
182
+ x = x * mlp_mask[..., None] # First masking.
183
+ x = self.c_proj(x)
184
+ x = x * mlp_mask[..., None] # Second masking.
185
+ return x
186
+
187
+
188
+ class ResidualAttentionBlock(nn.Module):
189
+ """Transformer residual attention block."""
190
+
191
+ def __init__(
192
+ self,
193
+ d_model: int,
194
+ n_head: int,
195
+ mlp_dim: int,
196
+ dtype: torch.dtype = torch.float32,
197
+ ):
198
+ super().__init__()
199
+ self.d_model = d_model
200
+ self.n_head = n_head
201
+ self.mlp_dim = mlp_dim
202
+ self.dtype = dtype
203
+
204
+ self.attn = nn.MultiheadAttention(d_model, n_head, dtype=self.dtype)
205
+ self.ln_1 = nn.LayerNorm(d_model, dtype=self.dtype)
206
+ self.mlp = MlpBlockWithMask(
207
+ self.mlp_dim,
208
+ d_model,
209
+ use_bias=True,
210
+ dtype=self.dtype,
211
+ activation_fn=nn.ReLU,
212
+ )
213
+ self.ln_2 = nn.LayerNorm(d_model, dtype=self.dtype)
214
+
215
+ def attention(self, x: torch.Tensor, mask: torch.Tensor):
216
+ attn_mask = (
217
+ mask[:, None, None, :]
218
+ .repeat(1, self.n_head, x.shape[0], 1)
219
+ .flatten(0, 1)
220
+ )
221
+ attn_mask[attn_mask == 0] = float('-inf')
222
+ attn_mask[attn_mask == 1] = 0
223
+ return self.attn(x, x, x, need_weights=False, attn_mask=attn_mask)[0]
224
+
225
+ def forward(self, x: torch.Tensor, mask: torch.Tensor):
226
+ x = x + self.attention(self.ln_1(x), mask.permute(1, 0))
227
+ x = x + self.mlp(self.ln_2(x), mask)
228
+ return x, mask
229
+
230
+
231
+ class SequentialMultiInput(nn.Sequential):
232
+ """Sequential module that can take multiple inputs."""
233
+
234
+ def forward(self, *inputs):
235
+ for module in self._modules.values():
236
+ if isinstance(inputs, tuple):
237
+ inputs = module(*inputs)
238
+ else:
239
+ inputs = module(inputs)
240
+ return inputs
241
+
242
+
243
+ class Transformer(nn.Module):
244
+ """Transformer implementation."""
245
+
246
+ def __init__(
247
+ self,
248
+ width: int,
249
+ layers: int,
250
+ heads: int,
251
+ mlp_dim: int,
252
+ dtype: torch.dtype = torch.float32,
253
+ ):
254
+ super().__init__()
255
+ self.width = width
256
+ self.layers = layers
257
+ self.heads = heads
258
+ self.mlp_dim = mlp_dim
259
+ self.dtype = dtype
260
+
261
+ self.resblocks = SequentialMultiInput(*[
262
+ ResidualAttentionBlock(self.width, self.heads, self.mlp_dim, self.dtype)
263
+ for _ in range(self.layers)
264
+ ])
265
+
266
+
267
+ def _concat_mask(self, mask: torch.Tensor, n_deep: int, after_bos: bool) -> torch.Tensor:
268
+ if mask is None:
269
+ return None
270
+ if mask.dim() == 2: # (B, L), 1/True = keep
271
+ B, L = mask.shape
272
+ deep_vis = torch.ones(B, n_deep, dtype=mask.dtype, device=mask.device)
273
+ if after_bos and L >= 1:
274
+ return torch.cat([mask[:, :1], deep_vis, mask[:, 1:]], dim=1)
275
+ else:
276
+ return torch.cat([deep_vis, mask], dim=1)
277
+ elif mask.dim() == 4: # (B, 1, 1, L), additive mask (0 keep)
278
+ B, _, _, L = mask.shape
279
+ deep_vis = torch.zeros(B, 1, 1, n_deep, dtype=mask.dtype, device=mask.device)
280
+ if after_bos and L >= 1:
281
+ return torch.cat([mask[..., :1], deep_vis, mask[..., 1:]], dim=-1)
282
+ else:
283
+ return torch.cat([deep_vis, mask], dim=-1)
284
+ else:
285
+ raise ValueError(f"Unsupported mask shape: {mask.shape}")
286
+
287
+ def forward(
288
+ self,
289
+ x: torch.Tensor,
290
+ mask: torch.Tensor,
291
+ deep_tokens: nn.ParameterList = None,
292
+ *,
293
+ after_bos: bool = False # place prompts after BOS/CLS at position 0
294
+ ) -> torch.Tensor:
295
+ """
296
+ If deep_tokens is provided, insert n_deep tokens at the input of layers
297
+ [insert_start_layer, insert_start_layer + len(deep_tokens)) and strip them after each block.
298
+ """
299
+ # fast path: no deep tokens
300
+ if not deep_tokens:
301
+ for block in self.resblocks:
302
+ x, mask = block(x, mask)
303
+ return x
304
+
305
+ # shapes/sizes
306
+ B, L, D = x.shape
307
+ n_deep = deep_tokens[0].shape[0] # number of deep tokens per tuned layer
308
+ d_deep = len(deep_tokens) # number of layers to tune
309
+ start = 1 # max(0, int(self.insert_start_layer))
310
+ end = min(start + d_deep, len(self.resblocks))
311
+
312
+ # sanity checks
313
+ for t in deep_tokens:
314
+ assert t.shape == (n_deep, D), f"Each deep token tensor must be (n_deep={n_deep}, D={D}), got {t.shape}"
315
+
316
+ for i, block in enumerate(self.resblocks):
317
+ if start <= i < end:
318
+ # select layer-specific deep tokens and expand across batch
319
+ pt = deep_tokens[i - start].unsqueeze(0).expand(B, -1, -1) # (B, n_deep, D)
320
+
321
+ # place after BOS/CLS if present, otherwise at pure prefix
322
+ if after_bos and L >= 1:
323
+ x_ext = torch.cat([x[:, :1], pt, x[:, 1:]], dim=1) # [BOS] + deep + tokens
324
+ else:
325
+ x_ext = torch.cat([pt, x], dim=1) # deep + tokens
326
+
327
+ mask_ext = self._concat_mask(mask, n_deep, after_bos=(after_bos and L >= 1))
328
+ y, _ = block(x_ext, mask_ext)
329
+ if after_bos and L >= 1:
330
+ x = torch.cat([y[:, :1], y[:, 1 + n_deep:]], dim=1)
331
+ else:
332
+ x = y[:, n_deep:, :]
333
+ else:
334
+ x, _ = block(x, mask)
335
+
336
+ return x
337
+
338
+ def squeeze_multiple(tensor: torch.Tensor, dims: list) -> torch.Tensor:
339
+ """Dynamically squeezes specified singleton dimensions from a tensor.
340
+
341
+ Args:
342
+ tensor (torch.Tensor): Input tensor.
343
+ dims (list): List of dimensions to squeeze.
344
+
345
+ Returns:
346
+ torch.Tensor: Squeezed tensor.
347
+ """
348
+ for dim in sorted(dims, reverse=True): # Sort in reverse to avoid shifting indices
349
+ if tensor.shape[dim] == 1: # Only squeeze if dim is 1
350
+ tensor = tensor.squeeze(dim=dim)
351
+ return tensor
352
+
353
+ class GlobalAvgPooling(nn.Module):
354
+ """Performs a simple global pooling over the input with optional paddings.
355
+
356
+ Attributes:
357
+ pooling_dims: A list of dims to perform pooling over.
358
+ keepdims: If True, keep dimension of inputs after pooling.
359
+ """
360
+
361
+ pooling_dims: t.Sequence[int]
362
+ epsilon: float = 1e-8
363
+
364
+ def __init__(
365
+ self, pooling_dims: t.Sequence[int], epsilon: float = 1e-8
366
+ ):
367
+ super().__init__()
368
+ self.pooling_dims = pooling_dims
369
+ self.epsilon = epsilon
370
+
371
+ if not all([p_dims >= 0 for p_dims in self.pooling_dims]):
372
+ raise ValueError('pooling_dims must be non-negative integers.')
373
+
374
+ def __call__(
375
+ self,
376
+ inputs: torch.tensor,
377
+ compatible_paddings: torch.tensor,
378
+ ):
379
+ """Applies global average spatial pooling to inputs.
380
+
381
+ Args:
382
+ inputs: An input tensor.
383
+ compatible_paddings: paddings of inputs with shapes compatible with
384
+ inputs, e.g. compatible_paddings with shape [B, 1] for inputs with shape
385
+ [B, D].
386
+
387
+ Returns:
388
+ Output tensor with global pooling applied.
389
+ """
390
+ padded_value = torch.zeros_like(inputs)
391
+ padded_value = torch.ones_like(inputs) * padded_value
392
+ inputs = torch.where(compatible_paddings > 0, padded_value, inputs)
393
+ valid_inputs = (
394
+ torch.sum(
395
+ 1.0 - compatible_paddings,
396
+ self.pooling_dims,
397
+ keepdims=True,
398
+ dtype=inputs.dtype,
399
+ )
400
+ + self.epsilon
401
+ )
402
+ inputs_sum = torch.sum(inputs, self.pooling_dims, keepdims=True)
403
+ outputs = torch.divide(inputs_sum, valid_inputs).type(inputs.dtype)
404
+ outputs = torch.squeeze(outputs, axis=self.pooling_dims)
405
+ # outputs = squeeze_multiple(outputs, self.pooling_dims)
406
+ return outputs
407
+
408
+
409
+ class TextEncoder(nn.Module):
410
+ """Text encoder implementation."""
411
+
412
+ def __init__(
413
+ self,
414
+ config: t.Dict[str, int],
415
+ vocab_size: int,
416
+ dtype: torch.dtype = torch.float32,
417
+ scale_sqrt_depth: bool = True,
418
+ ):
419
+ super().__init__()
420
+ self.vocab_size = vocab_size
421
+ self.dtype = dtype
422
+ self.scale_sqrt_depth = scale_sqrt_depth
423
+
424
+ # The text tower layers are fixed independent of vision tower size.
425
+ self.transformer_layers = config['num_layers']
426
+ self.embedding_dim = config['hidden_size']
427
+ self.transformer_width = config['hidden_size']
428
+ self.mlp_dim = config['mlp_dim']
429
+ self.transformer_heads = config['num_heads']
430
+
431
+ self.token_embedding = nn.Embedding(
432
+ self.vocab_size, self.embedding_dim, dtype=self.dtype
433
+ )
434
+ self.pos_embedder = PositionalEmbedding(embedding_dim=self.embedding_dim)
435
+ self.transformer = Transformer(
436
+ width=self.transformer_width,
437
+ layers=self.transformer_layers,
438
+ heads=self.transformer_heads,
439
+ mlp_dim=self.mlp_dim,
440
+ dtype=self.dtype,
441
+ )
442
+ self.pooling = GlobalAvgPooling(pooling_dims=[1])
443
+ self.ln_final = nn.LayerNorm(self.transformer_width, dtype=self.dtype)
444
+
445
+ def __call__(
446
+ self,
447
+ ids: torch.tensor,
448
+ paddings: torch.tensor,
449
+ learnable_prompts: torch.Tensor = None, # New parameter for learnable prompts
450
+ learning_method: str = None, # But addition can also be used [concat, sumate, entire_learnable, None]
451
+ deep_parameters: torch.nn.ParameterList = None,
452
+ device = 'cuda',
453
+ ):
454
+ # """Applies TextEncoder module."""
455
+ # _, seq_length = ids.shape
456
+
457
+ # x = self.token_embedding(ids)
458
+ # if self.scale_sqrt_depth:
459
+ # x = x * (self.embedding_dim**0.5)
460
+
461
+ # mask = (paddings == 0).type(torch.float32)
462
+ # mask = mask.permute(1, 0) # NL -> LN
463
+
464
+ # x = x + self.pos_embedder(seq_length=seq_length).to(device)
465
+ # x = x.permute(1, 0, 2) # NLD -> LND
466
+ # x = self.transformer(x, mask)
467
+ # x = x.permute(1, 0, 2) # LND -> NLD
468
+ # x = self.ln_final(x)
469
+ # x = self.pooling(x, compatible_paddings=paddings[:, :, None])
470
+ # return x
471
+
472
+ """Applies TextEncoder module with optional prompt learning (fixed input length 64)."""
473
+ batch_size, original_seq_length = ids.shape
474
+
475
+ x = self.token_embedding(ids) # [B, L, D]
476
+
477
+ if self.scale_sqrt_depth:
478
+ x = x * (self.embedding_dim ** 0.5)
479
+
480
+ x = x.to(device)
481
+ if learnable_prompts is not None:
482
+ if learning_method == 'concat':
483
+ prompts = learnable_prompts.unsqueeze(0).expand(batch_size, -1, -1).to(x.device) # [B, P, D]
484
+ x = torch.cat([prompts, x], dim=1) # [B, P+L, D]
485
+ paddings = torch.cat([
486
+ torch.zeros((batch_size, prompts.size(1)), device=x.device),
487
+ paddings
488
+ ], dim=1)
489
+
490
+ elif learning_method == 'sumate':
491
+ prompt_len = learnable_prompts.size(0)
492
+ x[:, :prompt_len, :] += learnable_prompts.unsqueeze(0)
493
+
494
+ elif learning_method == 'entire_learnable':
495
+ x = learnable_prompts.unsqueeze(0).expand(batch_size, -1, -1)
496
+ paddings = torch.zeros((batch_size, x.size(1)), device=x.device)
497
+
498
+ # 🔒 Ensure fixed sequence length (truncate or pad)
499
+ current_len = x.size(1)
500
+ if current_len > original_seq_length:
501
+ x = x[:, :original_seq_length, :]
502
+ paddings = paddings[:, :original_seq_length]
503
+ elif current_len < original_seq_length:
504
+ pad_len = original_seq_length - current_len
505
+ pad_embed = torch.zeros((batch_size, pad_len, x.size(2)), device=x.device)
506
+ pad_mask = torch.ones((batch_size, pad_len), device=x.device) # masked-out padding
507
+ x = torch.cat([x, pad_embed], dim=1)
508
+ paddings = torch.cat([paddings, pad_mask], dim=1)
509
+
510
+ # Positional encoding and transformer
511
+ x = x + self.pos_embedder(seq_length=original_seq_length).to(device)
512
+ x = x.permute(1, 0, 2) # [L, B, D]
513
+ mask = (paddings == 0).float().permute(1, 0) # [L, B]
514
+ x = self.transformer(x, mask, deep_tokens=deep_parameters)
515
+ x = x.permute(1, 0, 2) # [B, L, D]
516
+ x = self.ln_final(x)
517
+ x = self.pooling(x, compatible_paddings=paddings[:, :, None])
518
+
519
+ return x
Tipsomaly/reproduce.sh ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This script provides a sample command to test given checkpoints on a dataset
2
+ # For testing multiple datasets in a loop, refer to script train_test.sh
3
+
4
+ models_dir="/kaggle/working/tips"
5
+ data_root_dir="/kaggle/working/datasets"
6
+ model_version='l14h' # model version name, like s14h, l14h, g14h for TIPS model and google/siglip2-large-patch16-512 for SigLIP2 model
7
+ checkpoint_path="./workspaces/trained_on_mvtec_default/vegan-arkansas/checkpoints"
8
+ # visa checkpoint at: './workspaces/trained_on_visa_default/vegan-arkansas/checkpoints'
9
+
10
+ ### Test using industrial fixed prompts
11
+
12
+ # test on VisA
13
+ python test.py --models_dir $models_dir --checkpoint_path $checkpoint_path --data_root_dir $data_root_dir --dataset visa --epoch 2 --model_version $model_version --fixed_prompt_type industrial
14
+
15
+ # test on MVTec
16
+ # python test.py --models_dir $models_dir --checkpoint_path $checkpoint_path --data_root_dir $data_root_dir --dataset mvtec --epoch 2 --model_version $model_version --fixed_prompt_type industrial
17
+
18
+ # test on a medical dataset with industrial prompts
19
+ # python test.py --models_dir $models_dir --checkpoint_path $checkpoint_path --data_root_dir $data_root_dir --dataset cvc-colondb --fixed_prompt_type industrial --epoch 2 --model_version $model_version
20
+
21
+ # test on a medical dataset with medical prompts
22
+ # python test.py --models_dir $models_dir --checkpoint_path $checkpoint_path --data_root_dir $data_root_dir --dataset headct --fixed_prompt_type medical --epoch 2 --model_version $model_version
Tipsomaly/requirements.txt ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate==1.11.0
2
+ humanhash3==0.0.6
3
+ humanize==4.14.0
4
+ torch==2.8.0+cu126
5
+ torchao==0.10.0
6
+ torchaudio==2.8.0+cu126
7
+ torchdata==0.11.0
8
+ torchinfo==1.8.0
9
+ torchmetrics==1.8.2
10
+ torchsummary==1.5.1
11
+ torchtune==0.6.1
12
+ torchvision==0.23.0+cu126
13
+ transformers==5.0.0 # NOTE: This version is required only for SigLIP2 experiments
14
+ opencv-contrib-python==4.12.0.88
15
+ opencv-python==4.12.0.88
16
+ opencv-python-headless==4.12.0.88
17
+ sentencepiece==0.2.1
18
+ timm==1.0.20
19
+ pillow==11.3.0
20
+ scikit-image==0.25.2
21
+ scikit-learn==1.6.1
22
+ scikit-learn-intelex==2025.10.0
23
+ scikit-multilearn==0.2.0
24
+ scikit-optimize==0.10.2
25
+ scikit-plot==0.3.7
26
+ scikit-surprise==1.1.4
27
+ scipy==1.15.3
28
+ sklearn-pandas==2.2.0
29
+ numpy==2.0.2
30
+ pandas==2.2.2
31
+ pandas-datareader==0.10.0
32
+ pandas-gbq==0.29.2
33
+ pandas-profiling==3.6.6
34
+ pandas-stubs==2.2.2.240909
35
+ pandasql==0.7.3
36
+ tqdm==4.67.1
37
+ PyYAML==6.0.3
38
+ einops==0.8.1
39
+ huggingface_hub==1.4.0
40
+
41
+ # this is a curated and condensed list of the result of pip freeze > requirements.txt
42
+ # most of our experiments was conducted on Kaggle on Python 3.11 and 3.12
Tipsomaly/test.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import humanhash
3
+ import os, sys
4
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "model"))
5
+
6
+ import torch
7
+ from torch.utils.data import DataLoader
8
+ import torch.nn.functional as F
9
+ from torchvision import transforms
10
+
11
+ import random
12
+ import numpy as np
13
+ import pandas as pd
14
+ from pathlib import Path
15
+
16
+ from tabulate import tabulate
17
+ from tqdm import tqdm
18
+ import subprocess
19
+ import argparse
20
+ from scipy.ndimage import gaussian_filter
21
+
22
+ from model import tips, omaly
23
+ from datasets import input_transforms, dataset
24
+ from utils.metrics import image_level_metrics, pixel_level_metrics
25
+ from utils.visualize import visualizer
26
+ from utils.logger import get_logger, read_train_args
27
+ from transformers import AutoProcessor, AutoModel, AutoTokenizer, SiglipTextModel, SiglipVisionModel
28
+ from model.siglip2.siglip2_prompt_learnable import SiglipTextModelWithPromptLearning
29
+
30
+ ####################
31
+
32
+ from model.big_vision import load_siglip
33
+
34
+ def setup_seed(seed):
35
+ torch.manual_seed(seed)
36
+ torch.cuda.manual_seed_all(seed)
37
+ np.random.seed(seed)
38
+ random.seed(seed)
39
+ torch.backends.cudnn.deterministic = True
40
+ torch.backends.cudnn.benchmark = False
41
+
42
+ def calc_soft_score(vis_feat, txt_feat, temp):
43
+ return F.softmax((vis_feat @ txt_feat.permute(0, 2, 1))/temp, dim=-1)
44
+
45
+ def calc_sigm_score(vis_feat, txt_feat, temp, bias):
46
+ if vis_feat.dim() < 3:
47
+ vis_feat = vis_feat.unsqueeze(dim=1)
48
+ tempered_logits = vis_feat @ txt_feat.permute(0, 2, 1) * temp
49
+ probs = 1 / (1 + np.exp(-tempered_logits - bias))
50
+ return F.softmax(probs, dim=-1)
51
+
52
+ def calc_sigm_score_hf(vis_feat, txt_feat, temp, bias):
53
+ if vis_feat.dim() < 3:
54
+ vis_feat = vis_feat.unsqueeze(dim=1)
55
+ logits = vis_feat @ txt_feat.permute(0, 2, 1) * temp + bias
56
+ probs = torch.sigmoid(logits)
57
+ return probs
58
+
59
+ def regrid_upsample_smooth(flat_scores, size, sigma):
60
+ h_w = int(flat_scores.shape[1] ** 0.5)
61
+ regrided = flat_scores.reshape(flat_scores.shape[0], h_w, h_w, -1).permute(0, 3, 1, 2)
62
+ upsampled = torch.nn.functional.interpolate(regrided, (size, size), mode='bilinear').permute(0, 2, 3, 1)
63
+ rough_maps = (1-upsampled[..., 0] + upsampled[..., 1])/2
64
+ assert (rough_maps >= 0).all() and (rough_maps <= 1).all(), "All elements of rough_maps must be between 0 and 1"
65
+ anomaly_map = torch.stack([torch.from_numpy(gaussian_filter(map, sigma=sigma)) for map in rough_maps.detach().cpu()], dim=0)
66
+ return anomaly_map
67
+
68
+ def create_tips(args, device):
69
+ # load dataset
70
+ transform, target_transform = input_transforms.create_transforms_tips(args.image_size)
71
+
72
+ # load model
73
+ vision_encoder, text_encoder, tokenizer, temperature = tips.load_model.get_model(args.models_dir, args.model_version)
74
+ return vision_encoder.to(device), text_encoder.to(device), text_encoder.transformer.width, tokenizer, transform, target_transform, temperature
75
+
76
+ # L/14, 512
77
+ def create_siglip2(args, device):
78
+ transform, target_transform = load_siglip.create_preprocessors_siglip2(args.image_size)
79
+ vision_encoder, text_encoder, tokenizer = load_siglip.build_siglip_modules(args.model_version, args.image_size)
80
+ # model.to(device)
81
+
82
+ temperature, bias = text_encoder.params['t'], text_encoder.params['b']
83
+ temperature = np.exp(torch.from_numpy(np.array(temperature)))
84
+ return vision_encoder, text_encoder, text_encoder.model.out_dim[1], tokenizer, transform, target_transform, temperature, bias
85
+
86
+ def create_siglip2_hf(args, device):
87
+ tokenizer = AutoTokenizer.from_pretrained(args.model_version)
88
+ model = AutoModel.from_pretrained(args.model_version)
89
+ text_encoder = SiglipTextModelWithPromptLearning.from_pretrained(args.model_version).to(device)
90
+ vision_encoder = SiglipVisionModel.from_pretrained(args.model_version).to(device)
91
+ processor = AutoProcessor.from_pretrained(args.model_version)
92
+ def transform(x):
93
+ d = processor(images=x, return_tensors="pt")
94
+ return d['pixel_values'].squeeze(0)
95
+ target_transform = transforms.Compose([
96
+ transforms.Resize((args.image_size, args.image_size)),
97
+ transforms.ToTensor(),
98
+ ])
99
+ bias = model.logit_bias.to(device)
100
+ temperature = model.logit_scale.to(device).exp()
101
+ return vision_encoder, text_encoder, model.text_model.embeddings.token_embedding.embedding_dim, tokenizer, transform, target_transform, temperature, bias
102
+
103
+ def test(args):
104
+ logger = get_logger(args.save_path)
105
+ # load dataset
106
+
107
+ device = args.device
108
+ if args.backbone_name == 'tips':
109
+ bb_vision_encoder, bb_text_encoder, text_embd_dim, tokenizer, transform, target_transform, temperature = create_tips(args, device)
110
+ calc_score = lambda vis_feat, txt_feat: calc_soft_score(vis_feat, txt_feat, temperature)
111
+
112
+ elif args.backbone_name == 'siglip2':
113
+ bb_vision_encoder, bb_text_encoder, text_embd_dim, tokenizer, transform, target_transform, temperature, bias = create_siglip2(args, device)
114
+ calc_score = lambda vis_feat, txt_feat: calc_sigm_score(vis_feat, txt_feat, temperature, bias)
115
+
116
+ elif args.backbone_name == 'siglip2-hf':
117
+ bb_vision_encoder, bb_text_encoder, text_embd_dim, tokenizer, transform, target_transform, temperature, bias = create_siglip2_hf(args, device)
118
+ calc_score = lambda vis_feat, txt_feat: calc_sigm_score_hf(vis_feat, txt_feat, temperature, bias)
119
+
120
+ text_encoder = omaly.text_encoder(tokenizer, bb_text_encoder, args.backbone_name, text_embd_dim, 64, args.prompt_learn_method, args.fixed_prompt_type, args.n_prompt, args.n_deep_tokens, args.d_deep_tokens)
121
+ vision_encoder = omaly.vision_encoder(bb_vision_encoder, args.backbone_name)
122
+
123
+ # class_names = desc.dataset_dict[args.dataset]
124
+ test_data = dataset.Dataset(args.data_path, transform, target_transform, args)
125
+ test_loader = DataLoader(test_data, batch_size=args.batch_size, num_workers=4, shuffle=False)
126
+ # test_loader = DataLoader(test_data, batch_size=8, shuffle=False, num_workers=1, prefetch_factor=2, pin_memory=True)
127
+ fixed_class_names = [clss.replace('_', ' ') for clss in test_data.cls_names]
128
+
129
+ # extract features
130
+ with torch.no_grad():
131
+ # Fixed prototypes
132
+ fixed_text_features = text_encoder(fixed_class_names, device, learned=False)
133
+ fixed_text_features = fixed_text_features / fixed_text_features.norm(dim=-1, keepdim=True) # NOTE: For test also
134
+ cls_text_features, seg_text_features = fixed_text_features, fixed_text_features
135
+
136
+ if args.checkpoint_path:
137
+ assert not args.prompt_learn_method == 'none', 'The prompt_learn_method should not be none'
138
+ checkpoint = torch.load(args.params_path, weights_only=False)
139
+ text_encoder.learnable_prompts = checkpoint["learnable_prompts"] if isinstance(checkpoint, dict) else checkpoint
140
+
141
+ # text_encoder.learnable_prompts = chekpoint
142
+ # text_encoder.deep_parameters = chekpoint["deep_parameters"]
143
+
144
+ learnable_class_names = ['object']
145
+ learnable_class_ids = torch.tensor([0])
146
+ print('The learnable prompts are read')
147
+
148
+ # extract features
149
+ with torch.no_grad():
150
+ # Learnable prototypes
151
+ learnable_text_features = text_encoder(learnable_class_names, device, learned=True) # NOTE: important learned=True
152
+ learnable_text_features = learnable_text_features / learnable_text_features.norm(dim=-1, keepdim=True) # NOTE: For test also
153
+ cls_text_features, seg_text_features = learnable_text_features, learnable_text_features
154
+
155
+ if args.checkpoint_path and args.decoupled_prompt:
156
+ cls_text_features, seg_text_features = fixed_text_features, learnable_text_features
157
+
158
+ dataset_preds = {cls_id: {'name': test_loader.dataset.cls_names[cls_id], 'img_scrs': [], 'img_lbls': [], 'pxl_scrs': [], 'pxl_lbls': [], 'paths': []} for cls_id in test_loader.dataset.class_ids}
159
+ for batch in tqdm(test_loader, desc="Extracting features", unit="batch"):
160
+ image = batch['img'].to(device)
161
+ label = batch['anomaly'].long().to(device)
162
+ abnorm_mask = batch['abnorm_mask'].squeeze(dim=1).to(device)
163
+ path = batch['img_path']
164
+
165
+ # Indecies
166
+ cls_class_ids, seg_class_ids = batch['cls_id'], batch['cls_id']
167
+ if args.checkpoint_path and args.decoupled_prompt:
168
+ seg_class_ids = learnable_class_ids
169
+ elif args.checkpoint_path and not args.decoupled_prompt:
170
+ cls_class_ids, seg_class_ids = learnable_class_ids, learnable_class_ids
171
+
172
+ with torch.no_grad():
173
+ vision_features = vision_encoder(image)
174
+ vision_features = [feature / feature.norm(dim=-1, keepdim=True) for feature in vision_features] # NOTE: for test also
175
+
176
+ # calculate normal/abnormal scores
177
+ img_scr0 = calc_score(vision_features[0], cls_text_features[cls_class_ids]).squeeze(dim=1).detach() # prompt_class_ids cls_ids
178
+ img_scr1 = calc_score(vision_features[1], cls_text_features[cls_class_ids]).squeeze(dim=1).detach()
179
+
180
+ img_map = calc_score(vision_features[2], seg_text_features[seg_class_ids])
181
+ if args.aggregate_local2global:
182
+ max_local = torch.max(img_map, dim=1)[0]
183
+ img_scr0 = img_scr0 + max_local
184
+ img_scr1 = img_scr1 + max_local
185
+
186
+ pxl_scr = regrid_upsample_smooth(img_map.detach(), args.image_size, args.sigma)
187
+
188
+ for idx, cls_id in enumerate(batch['cls_id'].cpu().numpy()):
189
+ dataset_preds[cls_id]['img_scrs'].append([img_scr0[idx][1].cpu(), img_scr1[idx][1].cpu()])
190
+ dataset_preds[cls_id]['img_lbls'].append(label[idx].cpu())
191
+ dataset_preds[cls_id]['pxl_scrs'].append(pxl_scr[idx].cpu())
192
+ dataset_preds[cls_id]['pxl_lbls'].append(abnorm_mask[idx].cpu())
193
+ dataset_preds[cls_id]['paths'].append(path[idx])
194
+
195
+ # calculate metrics
196
+ header = ['objects']+args.pixel_metrics+[mtr for mtr in args.image_metrics for _ in range(2)]
197
+ dataset_results = []
198
+ for cls_id in dataset_preds.keys():
199
+ cls_results = [dataset_preds[cls_id]['name']]
200
+ img_prds = np.array(dataset_preds[cls_id]['img_scrs'])
201
+ img_lbls = np.array(dataset_preds[cls_id]['img_lbls'])
202
+ pxl_prds = torch.stack(dataset_preds[cls_id]['pxl_scrs'], dim=0)
203
+ pxl_lbls = torch.stack(dataset_preds[cls_id]['pxl_lbls'], dim=0)
204
+ print(f'pxl_prds: ({pxl_prds.max()}, {pxl_prds.min()})')
205
+ print(f'img_prds: ({img_prds.max()}, {img_prds.min()})')
206
+
207
+ for px_mtr in args.pixel_metrics:
208
+ if not px_mtr == '':
209
+ cls_results.append(pixel_level_metrics(device, pxl_prds, pxl_lbls, px_mtr)*100)
210
+
211
+ for im_mtr in args.image_metrics:
212
+ for col in range(img_prds.shape[1]):
213
+ cls_results.append(image_level_metrics(img_prds[:, col], img_lbls, im_mtr)*100)
214
+
215
+ if args.visualize:
216
+ img_path = f"{args.dataset}/{dataset_preds[cls_id]['name']}"
217
+ visualizer(dataset_preds[cls_id]['paths'], pxl_prds.cpu().numpy(), pxl_lbls.cpu().numpy(), args.image_size, img_path, save_path=f'{args.save_path}/img/', draw_contours=True)
218
+
219
+ dataset_results.append(cls_results)
220
+
221
+ df = pd.DataFrame(dataset_results, columns=header)
222
+ mean_values = ['Mean'] + df.iloc[:, 1:].mean().tolist()
223
+ # df = df.append(mean_values)
224
+ df.loc[len(df)] = mean_values
225
+ df = df.round(2)
226
+
227
+ # store the results
228
+ results_text = tabulate(df, headers='keys', tablefmt='pretty')
229
+ logger.info(results_text)
230
+
231
+ def make_human_readable_name(args, exclude=['model_name', 'dataset', 'dataset_category', 'data_path',
232
+ 'checkpoint_path', 'training_path', "Timestamp",
233
+ "metrics", "device", "available_devices", "epochs", "visualize", 'help', None]):
234
+ args=vars(args)
235
+ name_value_pairs = [
236
+ f"{k}_{v}"
237
+ for k,v in args.items()
238
+ if k not in exclude # Exclude "help" or invalid arguments
239
+ ]
240
+ combined = ",".join(sorted(name_value_pairs)) # Sorting ensures consistent order
241
+ hash_value = hashlib.sha256(combined.encode()).hexdigest()
242
+ human_hash = humanhash.humanize(hash_value, words=2) # 'hash' #
243
+ return human_hash
244
+
245
+ def str2bool(v):
246
+ if isinstance(v, bool):
247
+ return v
248
+ if v.lower() in ('yes', 'true', 't', 'y', '1'):
249
+ return True
250
+ elif v.lower() in ('no', 'false', 'f', 'n', '0'):
251
+ return False
252
+ else:
253
+ raise argparse.ArgumentTypeError('Boolean value expected.')
254
+
255
+ if __name__ == '__main__':
256
+ parser = argparse.ArgumentParser("TIPSomaly", add_help=True)
257
+ # model
258
+ parser.add_argument("--image_size", type=int, default=518, help="image size") #224 if is_low_res else 448
259
+ parser.add_argument("--seed", type=int, default=111, help="random seed")
260
+
261
+ parser.add_argument("--metrics", type=str, default='image-pixel-level')
262
+ parser.add_argument("--device", type=str, default="cuda", help="type of device, can be cuda or cpu")
263
+ parser.add_argument("--available_devices", type=int, nargs='+', default=[0, 1, 2, 3, 4, 5, 6, 7], help="array of possible cuda devices")
264
+ parser.add_argument("--model_name", type=str, default="trained_on_visa_siglip2_both_mvtec", help="")
265
+ parser.add_argument("--models_dir", type=str, default="./tips", help="directory of the base model of tips")
266
+ parser.add_argument("--data_root_dir", type=str, default="./datasets", help="root directory for all datasets to be placed in")
267
+ parser.add_argument("--checkpoint_path", type=str, default='None', help="")
268
+ parser.add_argument("--epoch", type=int, default=1, help="")
269
+ parser.add_argument("--batch_size", type=int, default=8)
270
+
271
+ parser.add_argument("--sigma", type=int, default=4, help="zero shot")
272
+
273
+ parser.add_argument("--dataset", type=str, default="visa")
274
+ parser.add_argument("--dataset_category", type=str, default='', help="train dataset categories")
275
+
276
+ parser.add_argument("--class_name", type=str, nargs='+', default=['all'], help="train class name")
277
+
278
+ parser.add_argument("--image_metrics", type=str, nargs='+', default=['auroc', 'ap', 'f1-max'], help="")
279
+ parser.add_argument("--pixel_metrics", type=str, nargs='+', default=['auroc', 'aupro', 'f1-max'], help="")
280
+
281
+ parser.add_argument("--k_shot", type=int, default=0, help="number of samples per class for few-shot learning. 0 means use all data.")
282
+
283
+ parser.add_argument("--type", type=str, default='test')
284
+ parser.add_argument("--visualize", type=str2bool, default=False)
285
+ parser.add_argument("--log_dir", type=str, default="")
286
+
287
+ ##########################
288
+ ### Method Arguements ####
289
+ parser.add_argument("--backbone_name", type=str, default='tips', choices=["tips", "siglip2", "siglip2-hf"])
290
+ parser.add_argument("--model_version", type=str, default='l14h', choices=["s14h","b14h","l14h","so4h","g14l","g14h", \
291
+ "B/16", "L/16", "So400m/14", "So400m/16", "g-opt/16", \
292
+ "google/siglip2-so400m-patch16-256", "google/siglip2-large-patch16-512"])
293
+
294
+ parser.add_argument("--n_deep_tokens", type=int, default=0)
295
+ parser.add_argument("--d_deep_tokens", type=int, default=0)
296
+ parser.add_argument("--n_prompt", type=int, default=8)
297
+ parser.add_argument("--fixed_prompt_type", type=str, default='industrial', choices=['industrial', 'medical', 'object_agnostic'])
298
+
299
+ parser.add_argument("--prompt_learn_method", type=str, default='concat', choices=['concat', 'sumate', 'entire_learnable', 'none'])
300
+ parser.add_argument("--decoupled_prompt", type=str2bool, default=True)
301
+ parser.add_argument("--aggregate_local2global", type=str2bool, default=True)
302
+
303
+ args = parser.parse_args()
304
+
305
+ if 'CUDA_VISIBLE_DEVICES' not in os.environ:
306
+ os.environ['CUDA_VISIBLE_DEVICES'] = ','.join(map(str, args.available_devices)) if len(args.available_devices) > 1 else str(args.available_devices[0])
307
+ command = [sys.executable, __file__, ] + sys.argv[1:]
308
+ process = subprocess.Popen(command, env=os.environ)
309
+ process.wait()
310
+
311
+ else:
312
+ setup_seed(args.seed)
313
+
314
+ #### ONLY KAGGLE
315
+ # args.dataset = f'{args.dataset}-ad'
316
+ # base_paths = [Path(p) for p in [f'{DATA_ROOT_DIR}/{args.dataset_category}/{args.dataset}/']]
317
+ # args.data_path = [str(next(p.iterdir())) for p in base_paths]
318
+
319
+ args.data_path = [f'{args.data_root_dir}/{args.dataset_category}/{args.dataset}/']
320
+ if not args.checkpoint_path:
321
+ args.log_dir = make_human_readable_name(args)
322
+ args.save_path = f'./workspaces/{args.model_name}/{args.log_dir}/quantative/NoTrain/{args.dataset}'
323
+ else: # ./workspaces/test/blah-blah/checkpoints/
324
+ splits = args.checkpoint_path.split('/')
325
+ args.params_path = f'{args.checkpoint_path}/learnable_params_{args.epoch}.pth'
326
+ args.model_name = splits[-3]
327
+ args.log_dir = splits[-2]
328
+ args.save_path = f'{"/".join(splits[:-1])}/quantative/epoch_{args.epoch}/{args.dataset}'
329
+ ###
330
+ train_args = read_train_args(args.checkpoint_path) # ./workspaces/{args.model_name}/{args.log_dir}/args.txt
331
+ args.prompt_learn_method = train_args['prompt_learn_method']
332
+ assert not train_args['prompt_learn_method'] is None, 'prompt_learn_method should not be none'
333
+
334
+ print(args)
335
+ print(f"Data Path: {args.data_path}, Log Directory: {args.log_dir}, Save Path: {args.save_path}")
336
+ test(args)
Tipsomaly/train.py ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, sys
2
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "model"))
3
+ import subprocess
4
+ import argparse
5
+ import hashlib
6
+ import humanhash
7
+ from collections import defaultdict
8
+ from tqdm import tqdm
9
+
10
+ import random
11
+ import numpy as np
12
+ from scipy.ndimage import gaussian_filter
13
+
14
+ import torch
15
+ import torch.nn.functional as F
16
+ from torchvision import transforms
17
+ from torch.utils.data import DataLoader
18
+ from transformers import AutoProcessor, AutoModel, AutoTokenizer, SiglipTextModel, SiglipVisionModel
19
+
20
+ from datasets import input_transforms, dataset
21
+ from utils.loss import FocalLoss, BinaryDiceLoss
22
+ from utils.logger import save_args_to_file, get_logger
23
+ from torch.utils.tensorboard import SummaryWriter
24
+
25
+ from model import tips
26
+ from model import omaly
27
+ from model.big_vision import load_siglip
28
+ from model.siglip2.siglip2_prompt_learnable import SiglipTextModelWithPromptLearning
29
+
30
+ loss_names = {'img_ls_ce': 'LS CE', 'pxl_ls_fc': 'LS FC', \
31
+ 'plx_ls_dc_p': 'LS DC P', 'plx_ls_dc_n': 'LS DC N', \
32
+ 'emb_l1_nrm': 'LS L1 NRM', 'epc_ls': 'total'}
33
+
34
+ def setup_seed(seed):
35
+ torch.manual_seed(seed)
36
+ torch.cuda.manual_seed_all(seed)
37
+ np.random.seed(seed)
38
+ random.seed(seed)
39
+ torch.backends.cudnn.deterministic = True
40
+ torch.backends.cudnn.benchmark = False
41
+
42
+ def seed_worker(worker_id):
43
+ worker_seed = 111 + worker_id
44
+ np.random.seed(worker_seed)
45
+ random.seed(worker_seed)
46
+
47
+ def calc_soft_score(vis_feat, txt_feat, temp):
48
+ return F.softmax((vis_feat @ txt_feat.permute(0, 2, 1))/temp, dim=-1)
49
+
50
+ def calc_sigm_score(vis_feat, txt_feat, temp, bias):
51
+ if vis_feat.dim() < 3:
52
+ vis_feat = vis_feat.unsqueeze(dim=1)
53
+ tempered_logits = vis_feat @ txt_feat.permute(0, 2, 1) * temp
54
+ probs = 1 / (1 + np.exp(-tempered_logits - bias))
55
+ return F.softmax(probs, dim=-1)
56
+
57
+ def calc_sigm_score_hf(vis_feat, txt_feat, temp_non_exp, bias):
58
+ if vis_feat.dim() < 3:
59
+ vis_feat = vis_feat.unsqueeze(dim=1)
60
+ logits = vis_feat @ txt_feat.permute(0, 2, 1) * temp_non_exp.exp() + bias
61
+ probs = torch.sigmoid(logits)
62
+ return probs
63
+
64
+ def create_tips(args, device):
65
+ # load dataset
66
+ transform, target_transform = input_transforms.create_transforms_tips(args.image_size)
67
+
68
+ # load model
69
+ vision_encoder, text_encoder, tokenizer, temperature = tips.load_model.get_model(args.models_dir, args.model_version)
70
+ return vision_encoder.to(device), text_encoder.to(device), text_encoder.transformer.width, tokenizer, transform, target_transform, temperature
71
+
72
+ def create_siglip2(args, device):
73
+ transform, target_transform = load_siglip.create_preprocessors_siglip2(args.image_size)
74
+ vision_encoder, text_encoder, tokenizer = load_siglip.build_siglip_modules(args.model_version, args.image_size)
75
+ # model.to(device)
76
+
77
+ temperature, bias = text_encoder.params['t'], text_encoder.params['b']
78
+ temperature = np.exp(torch.from_numpy(np.array(temperature)))
79
+ return vision_encoder, text_encoder, text_encoder.model.out_dim[1], tokenizer, transform, target_transform, temperature, bias
80
+
81
+ def create_siglip2_hf(args, device):
82
+ tokenizer = AutoTokenizer.from_pretrained(args.model_version)
83
+ model = AutoModel.from_pretrained(args.model_version)
84
+ text_encoder = SiglipTextModelWithPromptLearning.from_pretrained(args.model_version).to(device)
85
+ vision_encoder = SiglipVisionModel.from_pretrained(args.model_version).to(device)
86
+ processor = AutoProcessor.from_pretrained(args.model_version)
87
+ def transform(x):
88
+ d = processor(images=x, return_tensors="pt")
89
+ return d['pixel_values'].squeeze(0)
90
+ target_transform = transforms.Compose([
91
+ transforms.Resize((args.image_size, args.image_size)),
92
+ transforms.ToTensor(),
93
+ ])
94
+ bias = model.logit_bias.to(device)
95
+ temp_non_exp = model.logit_scale.to(device)
96
+ return vision_encoder, text_encoder, model.text_model.embeddings.token_embedding.embedding_dim, tokenizer, transform, target_transform, temp_non_exp, bias
97
+
98
+ def regrid_upsample_smooth(flat_scores, size, sigma):
99
+ upsampled = regrid_upsample(flat_scores, size)
100
+ anomaly_map = torch.stack([torch.from_numpy(gaussian_filter(map, sigma=sigma)) for map in upsampled.detach().cpu()], dim=0)
101
+ return anomaly_map
102
+
103
+ def regrid_upsample(flat_scores, size):
104
+ h_w = int(flat_scores.shape[1] ** 0.5)
105
+ regrided = flat_scores.reshape(flat_scores.shape[0], h_w, h_w, -1).permute(0, 3, 1, 2)
106
+ upsampled = torch.nn.functional.interpolate(regrided, (size, size), mode='bilinear').permute(0, 2, 3, 1)
107
+ return upsampled
108
+
109
+ def turn_gradient_off(model):
110
+ print("Turning off gradients in both the image and the text encoder")
111
+ for _, param in model.named_parameters():
112
+ param.requires_grad_(False)
113
+
114
+ enabled = set()
115
+ for name, param in model.named_parameters():
116
+ if param.requires_grad:
117
+ enabled.add(name)
118
+ # print(f"Parameters to be updated: {enabled}")
119
+
120
+ model.eval()
121
+ return model
122
+
123
+ def train(args):
124
+ epochs = args.epoch
125
+ device = args.device
126
+
127
+ writer = SummaryWriter(log_dir=args.experiment_root)
128
+ logger = get_logger(args.experiment_root)
129
+
130
+ if args.backbone_name == 'tips':
131
+ bb_vision_encoder, bb_text_encoder, text_embd_dim, tokenizer, transform, target_transform, temperature = create_tips(args, device)
132
+ calc_score = lambda vis_feat, txt_feat: calc_soft_score(vis_feat, txt_feat, temperature)
133
+ elif args.backbone_name == "siglip2":
134
+ bb_vision_encoder, bb_text_encoder, text_embd_dim, tokenizer, transform, target_transform, temperature, bias = create_siglip2(args, device)
135
+ calc_score = lambda vis_feat, txt_feat: calc_sigm_score(vis_feat, txt_feat, temperature, bias)
136
+ elif args.backbone_name == 'siglip2-hf':
137
+ bb_vision_encoder, bb_text_encoder, text_embd_dim, tokenizer, transform, target_transform, temperature, bias = create_siglip2_hf(args, device)
138
+ calc_score = lambda vis_feat, txt_feat: calc_sigm_score_hf(vis_feat, txt_feat, temperature, bias)
139
+
140
+ bb_text_encoder = bb_text_encoder.to(device)
141
+ bb_vision_encoder = bb_vision_encoder.to(device)
142
+ bb_text_encoder = turn_gradient_off(bb_text_encoder)
143
+ bb_vision_encoder = turn_gradient_off(bb_vision_encoder)
144
+ text_encoder = omaly.text_encoder(tokenizer, bb_text_encoder, args.backbone_name, text_embd_dim, 64, args.prompt_learn_method, args.fixed_prompt_type, args.n_prompt, args.n_deep_tokens, args.d_deep_tokens)
145
+ vision_encoder = omaly.vision_encoder(bb_vision_encoder, args.backbone_name)
146
+
147
+
148
+ # load dataset
149
+ # class_names = desc.dataset_dict[args.dataset]
150
+ train_data = dataset.Dataset(args.data_path, transform, target_transform, args)
151
+
152
+ g = torch.Generator()
153
+ g.manual_seed(args.seed)
154
+ train_loader = DataLoader(train_data, batch_size=args.batch_size, num_workers=4, shuffle=False)
155
+
156
+ # class_names = [clss.replace('_', ' ') for clss in train_data.cls_names]
157
+ # class_ids = train_data.class_ids
158
+ class_names = ['object']
159
+ class_ids = torch.tensor([0])
160
+
161
+ # Define losses
162
+ bce_loss = torch.nn.CrossEntropyLoss()
163
+ loss_focal = FocalLoss()
164
+ loss_dice = BinaryDiceLoss()
165
+
166
+ # Define optimizer
167
+ optimizer = torch.optim.Adam(
168
+ list(text_encoder.learnable_prompts),# + list(text_encoder.deep_parameters),
169
+ lr=args.learning_rate,
170
+ betas=(0.5, 0.999)
171
+ )
172
+ train_stats = defaultdict(list)
173
+
174
+ torch.autograd.set_detect_anomaly(True)
175
+ train_loader_cpu = [bat for bat in train_loader]
176
+
177
+ global_step = 0
178
+
179
+ text_encoder.train()
180
+ text_encoder.to(device)
181
+ vision_encoder.train()
182
+ vision_encoder.to(device)
183
+ for epoch in range(epochs): # Add epoch loop
184
+ print(f"Epoch {epoch + 1}/{epochs}")
185
+ epoch_loss = defaultdict(int)
186
+
187
+ for batch in tqdm(train_loader_cpu, desc="Train", unit="batch"):
188
+ image = batch['img'].to(device)
189
+ # cls_ids = batch['cls_id']
190
+ label = batch['anomaly'].long().to(device)
191
+ abnorm_mask = batch['abnorm_mask'].squeeze(dim=1).to(device)
192
+
193
+ # extract features
194
+ text_features = text_encoder(class_names, device, learned=True)
195
+ text_features = text_features / text_features.norm(dim=-1, keepdim=True) # NOTE: For test also
196
+ with torch.no_grad():
197
+ vision_features = vision_encoder(image)
198
+ vision_features = [feature / feature.norm(dim=-1, keepdim=True) for feature in vision_features] # NOTE: for test also
199
+
200
+ # calculate normal/abnormal scores (since TIPS has two global visual embeddings we have two calculated image-level scores)
201
+ img_scr0 = calc_score(vision_features[0], text_features[class_ids]).squeeze(dim=1)
202
+ img_scr1 = calc_score(vision_features[1], text_features[class_ids]).squeeze(dim=1)
203
+
204
+ img_map = calc_score(vision_features[2], text_features[class_ids])
205
+ anomaly_map = regrid_upsample(img_map, args.image_size)
206
+ abnorm_mask[abnorm_mask > 0.5], abnorm_mask[abnorm_mask< 0.5] = 1, 0
207
+
208
+ # Calculate loss
209
+ anomaly_map = anomaly_map.permute(0, 3, 1, 2)
210
+ ls_fc = loss_focal(anomaly_map, abnorm_mask)
211
+ ls_dc_p = loss_dice(anomaly_map[:, 1, :, :], abnorm_mask)
212
+ ls_dc_n = loss_dice(anomaly_map[:, 0, :, :], 1-abnorm_mask)
213
+
214
+ ls_cls = bce_loss(img_scr0, label) + bce_loss(img_scr1, label)
215
+ ls_seg = ls_fc + ls_dc_p + ls_dc_n # (pixel loss)
216
+ if args.cls_seg_los == 'both': # (image loss)
217
+ loss_total = ls_cls + ls_seg
218
+ elif args.cls_seg_los == 'seg':
219
+ loss_total = ls_seg
220
+ elif args.cls_seg_los == 'cls':
221
+ loss_total = ls_cls
222
+
223
+ # L1 Regularization term
224
+ l1_norm = torch.sum(torch.abs(text_features))
225
+ loss_total = loss_total + l1_norm * args.l1_lambda
226
+
227
+ # Train
228
+ optimizer.zero_grad()
229
+ loss_total.backward()
230
+ optimizer.step()
231
+
232
+ # log
233
+ epoch_loss['img_ls_ce'] += ls_cls.item()
234
+ epoch_loss['pxl_ls_fc'] += ls_fc.item()
235
+ epoch_loss['plx_ls_dc_p'] += ls_dc_p.item()
236
+ epoch_loss['plx_ls_dc_n'] += ls_dc_n.item()
237
+ epoch_loss['epc_ls'] += loss_total.item()
238
+ epoch_loss['emb_l1_nrm'] += l1_norm.item()
239
+
240
+ # Tensorboard update for each batch
241
+ writer.add_scalar(f"Loss/img_ls_ce", ls_cls.item(), global_step)
242
+ writer.add_scalar(f"Loss/pxl_ls_fc", ls_fc.item(), global_step)
243
+ writer.add_scalar(f"Loss/plx_ls_dc_p", ls_dc_p.item(), global_step)
244
+ writer.add_scalar(f"Loss/plx_ls_dc_n", ls_dc_n.item(), global_step)
245
+ writer.add_scalar(f"Loss/epc_ls", loss_total.item(), global_step)
246
+ writer.add_scalar(f"Loss/emb_l1_nrm", l1_norm.item(), global_step)
247
+ global_step += 1
248
+
249
+ # Calc epoch mean loss
250
+ num_batches = len(train_loader)
251
+ for key, val in epoch_loss.items():
252
+ train_stats[key].append(val / num_batches)
253
+
254
+ # Print mean losses at the end of the epoch
255
+ epoch_details = f"Epoch {epoch + 1} Mean Losses: "
256
+ for key, val in epoch_loss.items():
257
+ epoch_details = epoch_details + f"{loss_names[key]}: {train_stats[key][-1]:.4f}, "
258
+ logger.info(epoch_details[:-2])
259
+
260
+ torch.save({"learnable_prompts":text_encoder.learnable_prompts},
261
+ f'{args.save_path}/learnable_params_{epoch+1}.pth')
262
+ # "deep_parameters":text_encoder.deep_parameters},
263
+ print(f'checkpoints saved for epoch {epoch+1}.')
264
+
265
+ def make_human_readable_name(args, exclude=['model_name', 'dataset', 'dataset_category', 'epoch', 'data_path',
266
+ 'checkpoint_path', 'training_path', "Timestamp",
267
+ "metrics", "device", "available_devices", "epochs", "visualize", 'help', None]):
268
+ args=vars(args)
269
+ name_value_pairs = [
270
+ f"{k}_{v}"
271
+ for k,v in args.items()
272
+ if k not in exclude # Exclude "help" or invalid arguments
273
+ ]
274
+ combined = ",".join(sorted(name_value_pairs)) # Sorting ensures consistent order
275
+ hash_value = hashlib.sha256(combined.encode()).hexdigest()
276
+ human_hash = humanhash.humanize(hash_value, words=2)
277
+ return human_hash.replace('-', '_')
278
+
279
+ def str2bool(v):
280
+ if isinstance(v, bool):
281
+ return v
282
+ if v.lower() in ('yes', 'true', 't', 'y', '1'):
283
+ return True
284
+ elif v.lower() in ('no', 'false', 'f', 'n', '0'):
285
+ return False
286
+ else:
287
+ raise argparse.ArgumentTypeError('Boolean value expected.')
288
+
289
+ if __name__ == '__main__':
290
+
291
+ dss = ['mvtec']
292
+
293
+ parser = argparse.ArgumentParser("TIPSomaly", add_help=True)
294
+ # model
295
+ parser.add_argument("--image_size", type=int, default=518, help="image size")
296
+ parser.add_argument("--seed", type=int, default=111, help="random seed")
297
+
298
+ parser.add_argument("--epoch", type=int, default=5, help="epochs")
299
+ parser.add_argument("--learning_rate", type=float, default=0.001)
300
+
301
+ parser.add_argument("--metrics", type=str, default='image-pixel-level')
302
+ parser.add_argument("--device", type=str, default="cuda", help="type of device, can be cuda or cpu")
303
+ parser.add_argument("--available_devices", type=int, nargs='+', default=[0, 1, 2, 3, 4, 5, 6, 7], help="array of possible cuda devices")
304
+ parser.add_argument("--model_name", type=str, default="tips_test", help="cuda device")
305
+ parser.add_argument("--models_dir", type=str, default="./tips", help="directory of the base model of tips")
306
+ parser.add_argument("--data_root_dir", type=str, default="./datasets", help="root directory for all datasets to be placed in")
307
+ parser.add_argument("--batch_size", type=int, default=8)
308
+
309
+ parser.add_argument("--sigma", type=int, default=4, help="zero shot")
310
+
311
+ parser.add_argument("--dataset", type=str, default="visa")
312
+ parser.add_argument("--dataset_category", type=str, default='', help="train dataset categories")
313
+
314
+ parser.add_argument("--type", type=str, default='train')
315
+ parser.add_argument("--class_name", type=str, nargs='+', default=['all'], help="train class name")
316
+ parser.add_argument("--k_shot", type=int, default=0, help="number of samples per class for few-shot learning. 0 means use all data.")
317
+
318
+ ##########################
319
+ ### Method Arguements ####
320
+ parser.add_argument("--model_version", type=str, default='l14h', choices=["s14h","b14h","l14h","so4h","g14l","g14h", \
321
+ "B/16", "L/16", "So400m/14", "So400m/16", "g-opt/16", \
322
+ "google/siglip2-so400m-patch16-256", "google/siglip2-large-patch16-512"])
323
+ parser.add_argument("--n_deep_tokens", type=int, default=0)
324
+ parser.add_argument("--d_deep_tokens", type=int, default=0)
325
+ parser.add_argument("--n_prompt", type=int, default=8)
326
+ parser.add_argument("--fixed_prompt_type", type=str, default='industrial')
327
+
328
+ parser.add_argument("--prompt_learn_method", type=str, default='concat', choices=['concat', 'sumate', 'entire_learnable', 'none'])
329
+ parser.add_argument("--cls_seg_los", type=str, default='seg', choices=['both', 'seg', 'cls'])
330
+ parser.add_argument("--l1_lambda", type=float, default=0.0)
331
+ parser.add_argument("--backbone_name", type=str, default='tips', choices=["tips", "siglip2", "siglip2-hf"])
332
+
333
+ args = parser.parse_args()
334
+
335
+ command = [sys.executable, __file__, ] + sys.argv[1:]
336
+ if 'CUDA_VISIBLE_DEVICES' not in os.environ:
337
+ os.environ['CUDA_VISIBLE_DEVICES'] = ','.join(map(str, args.available_devices)) if len(args.available_devices) > 1 else str(args.available_devices[0])
338
+ process = subprocess.Popen(command, env=os.environ)
339
+ process.wait()
340
+
341
+ else:
342
+ print(args)
343
+ setup_seed(args.seed)
344
+ args.log_dir = make_human_readable_name(args)
345
+ args.data_path = [f'{args.data_root_dir}/{args.dataset_category}/{args.dataset}/']
346
+ args.experiment_root = f'./workspaces/trained_on_{args.dataset}_{args.model_name}/{args.log_dir}'
347
+ args.save_path = f'{args.experiment_root}/checkpoints'
348
+ os.makedirs(args.save_path, exist_ok=True)
349
+
350
+ save_args_to_file(args, command) # ./workspaces/{args.model_name}/{args.log_dir}/args.txt
351
+
352
+ print(f"Data Path: {args.data_path}, Log Directory: {args.log_dir}, Save Path: {args.save_path}")
353
+ train(args)
Tipsomaly/train_test.sh ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This script provides the command to train your model
2
+
3
+ # By setting "model_name" and "log_dir" you define the path for the checkpoints to be saved and
4
+ # you can use the same values later to test on other datasets in a loop easily
5
+ model_version='l14h' # l14h or l14
6
+ model_name=$1
7
+ epoch=$2
8
+
9
+ models_dir="/kaggle/working/tips"
10
+ data_root_dir="/kaggle/working/datasets"
11
+ # checkpoint_path="/kaggle/working/checkpoints" # uncomment for testing results
12
+
13
+ # Train on MVTec
14
+ python train.py --models_dir $models_dir --model_name $model_name --data_root_dir $data_root_dir --dataset mvtec --cls_seg_los seg --l1_lambda 0.0 --d_deep_tokens 0 --n_deep_tokens 0 --epoch $epoch --model_version $model_version --fixed_prompt_type industrial
15
+
16
+ # Train on VisA
17
+ # python train.py --models_dir $models_dir --model_name $model_name --data_root_dir $data_root_dir --dataset visa --cls_seg_los seg --l1_lambda 0.0 --d_deep_tokens 0 --n_deep_tokens 0 --epoch $epoch --model_version $model_version --fixed_prompt_type industrial
18
+
19
+
20
+ # Test multiple datasets in a loop
21
+
22
+ # for dataset in visa mpdd btad sdd dagm dtd; do
23
+ # python test.py --models_dir $models_dir --checkpoint_path $checkpoint_path --data_root_dir $data_root_dir --dataset $dataset --epoch $epoch --model_version $model_version --fixed_prompt_type industrial
24
+ # done
25
+
26
+ # Medical segmentation datasets - using learned prompts
27
+ # for dataset in isic tn3k cvc-colondb cvc-clinicdb; do
28
+ # python test.py --models_dir $models_dir --checkpoint_path $checkpoint_path --data_root_dir $data_root_dir --dataset $dataset --fixed_prompt_type industrial --epoch $epoch --model_version $model_version
29
+ # done
30
+
31
+ # Medical classification datasets - using medical prompts
32
+ # for dataset in headct brainmri br35h; do
33
+ # python test.py --models_dir $models_dir --checkpoint_path $checkpoint_path --data_root_dir $data_root_dir --dataset $dataset --fixed_prompt_type medical --epoch $epoch --model_version $model_version
34
+ # done
Tipsomaly/utils/logger.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import logging
3
+ import os
4
+ import datetime
5
+
6
+ def read_train_args(training_path):
7
+ # Check if args.txt exists in the training_path
8
+ args_file_path = os.path.join(training_path, 'args.txt')
9
+ configurations_dict = {} # Dictionary to store configurations
10
+ last_config = {}
11
+ if os.path.exists(args_file_path):
12
+ with open(args_file_path, 'r') as f:
13
+ # Read the entire content of the file
14
+ file_content = f.read().strip()
15
+
16
+ # Split the content into different configurations based on the 'Timestamp' keyword
17
+ configurations = file_content.split('Timestamp:')
18
+
19
+ # Iterate over each configuration to populate the dictionary
20
+ for config in configurations:
21
+ if config.strip():
22
+ # Convert the configuration to a dictionary
23
+ file_args_dict = {}
24
+ for line in config.strip().split('\n'):
25
+ if ':' in line:
26
+ key, value = line.split(':', 1)
27
+ file_args_dict[key.strip()] = value.strip()
28
+
29
+ # Store the configuration in the dictionary with a unique key
30
+ timestamp = file_args_dict.get('Timestamp', 'Unknown')
31
+ configurations_dict[timestamp] = file_args_dict
32
+
33
+ if configurations_dict:
34
+ last_timestamp = max(configurations_dict.keys())
35
+ last_config = configurations_dict[last_timestamp]
36
+ else:
37
+ print('train args does not exists')
38
+ return last_config
39
+
40
+ def save_args_to_file(args, command, log_dir=''):
41
+ args_file_path = os.path.join(args.save_path, log_dir, 'args.txt')
42
+ os.makedirs(os.path.dirname(args_file_path), exist_ok=True)
43
+ if os.path.exists(args_file_path):
44
+ print(f"Warning: The file {args_file_path} already exists and will be overwritten.")
45
+ with open(args_file_path, 'a') as f: # Change 'w' to 'a' to append to the file
46
+ f.write("\n") # Add new line before writing to the file
47
+ f.write(f"Timestamp: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") # Add timestamp
48
+ for arg, value in vars(args).items():
49
+ f.write(f"{arg}: {value}\n")
50
+ f.write(f"Command arguments: {' '.join(command)}\n") # Add the command arguments to the file
51
+
52
+ def get_logger(save_path):
53
+ if not os.path.exists(save_path):
54
+ os.makedirs(save_path)
55
+
56
+ txt_path = os.path.join(save_path, 'log.txt')
57
+ # logger
58
+ root_logger = logging.getLogger()
59
+ for handler in root_logger.handlers[:]:
60
+ root_logger.removeHandler(handler)
61
+ root_logger.setLevel(logging.WARNING)
62
+ logger = logging.getLogger('test')
63
+ formatter = logging.Formatter('%(asctime)s.%(msecs)03d - %(levelname)s: %(message)s',
64
+ datefmt='%y-%m-%d %H:%M:%S')
65
+ logger.setLevel(logging.INFO)
66
+ file_handler = logging.FileHandler(txt_path, mode='a')
67
+ file_handler.setFormatter(formatter)
68
+ logger.addHandler(file_handler)
69
+ console_handler = logging.StreamHandler()
70
+ console_handler.setFormatter(formatter)
71
+ logger.addHandler(console_handler)
72
+ return logger
Tipsomaly/utils/loss.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ from math import exp
6
+
7
+ class FocalLoss(nn.Module):
8
+ """
9
+ copy from: https://github.com/Hsuxu/Loss_ToolBox-PyTorch/blob/master/FocalLoss/FocalLoss.py
10
+ This is a implementation of Focal Loss with smooth label cross entropy supported which is proposed in
11
+ 'Focal Loss for Dense Object Detection. (https://arxiv.org/abs/1708.02002)'
12
+ Focal_Loss= -1*alpha*(1-pt)*log(pt)
13
+ :param alpha: (tensor) 3D or 4D the scalar factor for this criterion
14
+ :param gamma: (float,double) gamma > 0 reduces the relative loss for well-classified examples (p>0.5) putting more
15
+ focus on hard misclassified example
16
+ :param smooth: (float,double) smooth value when cross entropy
17
+ :param balance_index: (int) balance class index, should be specific when alpha is float
18
+ :param size_average: (bool, optional) By default, the losses are averaged over each loss element in the batch.
19
+ """
20
+
21
+ def __init__(self, apply_nonlin=None, alpha= None, gamma=2, balance_index=1, smooth=1e-5, size_average=True):
22
+ super(FocalLoss, self).__init__()
23
+ self.apply_nonlin = apply_nonlin
24
+ self.alpha = alpha
25
+ self.gamma = gamma
26
+ self.balance_index = balance_index
27
+ self.smooth = smooth
28
+ self.size_average = size_average
29
+
30
+ if self.smooth is not None:
31
+ if self.smooth < 0 or self.smooth > 1.0:
32
+ raise ValueError('smooth value should be in [0,1]')
33
+
34
+ def forward(self, logit, target):
35
+ if self.apply_nonlin is not None:
36
+ logit = self.apply_nonlin(logit)
37
+ num_class = logit.shape[1]
38
+
39
+ if logit.dim() > 2:
40
+ # N,C,d1,d2 -> N,C,m (m=d1*d2*...)
41
+ logit = logit.view(logit.size(0), logit.size(1), -1)
42
+ logit = logit.permute(0, 2, 1).contiguous()
43
+ logit = logit.view(-1, logit.size(-1))
44
+ target = torch.squeeze(target, 1)
45
+ target = target.view(-1, 1)
46
+ alpha = self.alpha
47
+
48
+ if alpha is None:
49
+ alpha = torch.ones(num_class, 1)
50
+ elif isinstance(alpha, (list, np.ndarray)):
51
+ assert len(alpha) == num_class
52
+ alpha = torch.FloatTensor(alpha).view(num_class, 1)
53
+ alpha = alpha / alpha.sum()
54
+ elif isinstance(alpha, float):
55
+ alpha = torch.ones(num_class, 1)
56
+ alpha = alpha * (1 - self.alpha)
57
+ alpha[self.balance_index] = self.alpha
58
+
59
+ else:
60
+ raise TypeError('Not support alpha type')
61
+
62
+ if alpha.device != logit.device:
63
+ alpha = alpha.to(logit.device)
64
+
65
+ idx = target.cpu().long()
66
+
67
+ one_hot_key = torch.FloatTensor(target.size(0), num_class).zero_()
68
+ one_hot_key = one_hot_key.scatter_(1, idx, 1)
69
+ if one_hot_key.device != logit.device:
70
+ one_hot_key = one_hot_key.to(logit.device)
71
+
72
+ if self.smooth:
73
+ one_hot_key = torch.clamp(
74
+ one_hot_key, self.smooth / (num_class - 1), 1.0 - self.smooth)
75
+ pt = (one_hot_key * logit).sum(1) + self.smooth
76
+ logpt = pt.log()
77
+
78
+ gamma = self.gamma
79
+
80
+ alpha = alpha[idx]
81
+ alpha = torch.squeeze(alpha)
82
+ loss = -1 * alpha * torch.pow((1 - pt), gamma) * logpt
83
+
84
+ if self.size_average:
85
+ loss = loss.mean()
86
+ return loss
87
+
88
+
89
+ class BinaryDiceLoss(nn.Module):
90
+ def __init__(self):
91
+ super(BinaryDiceLoss, self).__init__()
92
+
93
+ def forward(self, input, targets):
94
+ # 获取每个批次的大小 N
95
+ N = targets.size()[0]
96
+ # 平滑变量
97
+ smooth = 1
98
+ # 将宽高 reshape 到同一纬度
99
+ input_flat = input.view(N, -1)
100
+ targets_flat = targets.view(N, -1)
101
+
102
+ # 计算交集
103
+ intersection = input_flat * targets_flat
104
+ N_dice_eff = (2 * intersection.sum(1) + smooth) / (input_flat.sum(1) + targets_flat.sum(1) + smooth)
105
+ # 计算一个批次中平均每张图的损失
106
+ loss = 1 - N_dice_eff.sum() / N
107
+ return loss
Tipsomaly/utils/metrics.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from skimage import measure
4
+ from torchmetrics import AUROC, AveragePrecision
5
+ from sklearn.metrics import auc, roc_auc_score, average_precision_score, precision_recall_curve
6
+
7
+ def calc_f1_max(gt, pr):
8
+ precisions, recalls, _ = precision_recall_curve(gt, pr)
9
+ f1_scores = (2 * precisions * recalls) / (precisions + recalls)
10
+ return np.max(f1_scores[np.isfinite(f1_scores)])
11
+
12
+
13
+ def cal_pro_score_gpu(device, masks, amaps, max_step=200, expect_fpr=0.3):
14
+ # GPU implementation using PyTorch
15
+ if not torch.is_tensor(amaps):
16
+ amaps = torch.tensor(amaps)
17
+ amaps = amaps.to(device)
18
+ masks = masks.to(device)
19
+
20
+ binary_amaps = torch.zeros_like(amaps, dtype=torch.bool, device=device)
21
+ min_th, max_th = amaps.min().item(), amaps.max().item()
22
+ delta = (max_th - min_th) / max_step
23
+ pros, fprs, ths = [], [], []
24
+
25
+ regionprops_list = [measure.regionprops(measure.label(mask.cpu().numpy())) for mask in masks]
26
+ coords_list = [[(region.coords[:, 0], region.coords[:, 1], len(region.coords)) for region in regionprops] for regionprops in regionprops_list]
27
+ inverse_masks = 1 - masks
28
+ tn_pixel = inverse_masks.sum().item() # Pixels that truly has the label of 0
29
+ for th in torch.arange(min_th, max_th, delta, device=device):
30
+ binary_amaps[amaps <= th], binary_amaps[amaps > th] = 0, 1
31
+ pro = []
32
+
33
+ for binary_amap, regions_coords in zip(binary_amaps, coords_list):
34
+ for coords in regions_coords:
35
+ tp_pixels = binary_amap[coords[0], coords[1]].sum().item()
36
+ pro.append(tp_pixels / coords[2])
37
+
38
+ fp_pixels = torch.logical_and(inverse_masks, binary_amaps).sum().item()
39
+ fpr = fp_pixels / tn_pixel
40
+ pros.append(np.mean(pro))
41
+ fprs.append(fpr)
42
+ ths.append(th.item())
43
+
44
+ pros, fprs, ths = torch.tensor(pros, device=device), torch.tensor(fprs, device=device), torch.tensor(ths, device=device)
45
+ idxes = fprs < expect_fpr
46
+ fprs = fprs[idxes]
47
+ fprs = (fprs - fprs.min()) / (fprs.max() - fprs.min())
48
+ pro_auc = auc(fprs.cpu().numpy(), pros[idxes].cpu().numpy())
49
+ return pro_auc
50
+
51
+ def image_level_metrics(prd, lbl, metric):
52
+ if len(np.unique(lbl)) < 2:
53
+ print("only one class present, can not calculate image metrics")
54
+ return 0
55
+
56
+ if metric == 'auroc':
57
+ performance = roc_auc_score(lbl, prd)
58
+ elif metric == 'ap':
59
+ performance = average_precision_score(lbl, prd)
60
+ elif metric == 'f1-max':
61
+ performance = calc_f1_max(lbl, prd)
62
+ return performance
63
+
64
+ def pixel_level_metrics(device, prd, lbl, metric):
65
+ if torch.unique(lbl).numel() < 2:
66
+ print("only one class present, can not calculate pixel metrics")
67
+ return 0
68
+
69
+ if metric == 'auroc':
70
+ performance = AUROC(task="binary")(prd, lbl.to(dtype=torch.long)).item()
71
+
72
+ elif metric == 'aupro':
73
+ if len(lbl.shape) == 4:
74
+ lbl = lbl.squeeze(1)
75
+ if len(prd.shape) == 4:
76
+ prd = prd.squeeze(1)
77
+ performance = cal_pro_score_gpu(device, lbl, prd)
78
+
79
+ elif metric == 'ap':
80
+ performance = AveragePrecision(task="binary")(prd, lbl.to(dtype=torch.long)).item()
81
+
82
+ elif metric == 'f1-max':
83
+ performance = calc_f1_max(lbl.cpu().ravel(), prd.cpu().ravel())
84
+ return performance
Tipsomaly/utils/visualize.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import os
3
+ import numpy as np
4
+ import torch
5
+ import matplotlib.pyplot as plt
6
+ from PIL import Image
7
+
8
+ def normalize(pred, max_value=None, min_value=None):
9
+ if max_value is None or min_value is None:
10
+ return (pred - pred.min()) / (pred.max() - pred.min())
11
+ else:
12
+ return (pred - min_value) / (max_value - min_value)
13
+
14
+ def apply_ad_scoremap(image, scoremap, alpha=0.5):
15
+ np_image = np.asarray(image, dtype=float)
16
+ # Convert scoremap from a PyTorch tensor to a NumPy array
17
+ if isinstance(scoremap, torch.Tensor):
18
+ scoremap = scoremap.detach().cpu().numpy() # Convert tensor to NumPy array
19
+ scoremap = (scoremap * 255).astype(np.uint8)
20
+ scoremap = cv2.applyColorMap(scoremap, cv2.COLORMAP_JET)
21
+ scoremap = cv2.cvtColor(scoremap, cv2.COLOR_BGR2RGB)
22
+ return (alpha * np_image + (1 - alpha) * scoremap).astype(np.uint8)
23
+
24
+ def visualizer(pathes, anomaly_map, masks, img_size, cls_name, save_path='./vis_img/', draw_contours=True):
25
+ for idx, path in enumerate(pathes):
26
+ cls = path.split('/')[-2]
27
+ filename = path.split('/')[-1]
28
+
29
+ # Modify filename if contours are enabled
30
+ if draw_contours:
31
+ filename_ctr = filename.split('.')[0] + "_cntr." + filename.split('.')[-1] # Append '_cntr' before file extension
32
+
33
+ # Load original image and resize
34
+ vis = cv2.cvtColor(cv2.resize(cv2.imread(path), (img_size, img_size)), cv2.COLOR_BGR2RGB)
35
+
36
+ # Use the provided mask (it's guaranteed to be available)
37
+ gt_mask = (masks[idx] > 0).astype(np.uint8) * 255 # Convert to binary (0 or 255)
38
+
39
+ # Normalize and apply anomaly map
40
+ mask = normalize(anomaly_map[idx])
41
+ vis = apply_ad_scoremap(vis, mask)
42
+
43
+ # Convert back to BGR for OpenCV
44
+ vis = cv2.cvtColor(vis, cv2.COLOR_RGB2BGR)
45
+
46
+ # Save the final visualization
47
+ save_vis = os.path.join(save_path, str(cls_name), str(cls))
48
+ os.makedirs(save_vis, exist_ok=True)
49
+ cv2.imwrite(os.path.join(save_vis, filename), vis)
50
+
51
+ # Find and overlay contours (only if draw_contours is True)
52
+ if draw_contours:
53
+ contours, _ = cv2.findContours(gt_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
54
+ cv2.drawContours(vis, contours, -1, (120, 251, 120), 2) # Pale green contours
55
+ cv2.imwrite(os.path.join(save_vis, filename_ctr), vis)
Tipsomaly/workspaces/trained_on_mvtec_default/vegan-arkansas/checkpoints/args.txt ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Timestamp: 2025-08-24 02:23:07
3
+ image_size: 518
4
+ seed: 111
5
+ metrics: image-pixel-level
6
+ devices: [3]
7
+ model_name: prompt_learning_segonly_518
8
+ sigma: 4
9
+ dataset: ['mvtec']
10
+ dataset_category:
11
+ class_name: ['all']
12
+ k_shot: 0
13
+ type: train
14
+ log_dir: vegan-arkansas
15
+ image_metrics: ['auroc', 'ap', 'f1-max']
16
+ pixel_metrics: ['auroc', 'aupro', 'f1-max']
17
+ visualize: True
18
+ prompt_learn_method: concat
19
+ cls_seg_los: seg
20
+ data_path: ['/data/alireza/datasets//mvtec/']
21
+ experiment_root: ./workspaces/trained_on_mvtec_prompt_learning_segonly_518/vegan-arkansas
22
+ save_path: ./workspaces/trained_on_mvtec_prompt_learning_segonly_518/vegan-arkansas/checkpoints
23
+ Command arguments: /home/alireza/miniconda3/envs/cuda11_8torch2_5/bin/python /home/alireza/KyotoServer/CLIP-AD/tipsomaly/train.py --model_name prompt_learning_segonly_518 --dataset mvtec --device 3 --cls_seg_los seg
24
+
25
+ Timestamp: 2025-08-24 02:23:50
26
+ image_size: 518
27
+ seed: 111
28
+ metrics: image-pixel-level
29
+ devices: [3]
30
+ model_name: prompt_learning_segonly_518
31
+ sigma: 4
32
+ dataset: ['mvtec']
33
+ dataset_category:
34
+ class_name: ['all']
35
+ k_shot: 0
36
+ type: train
37
+ log_dir: vegan-arkansas
38
+ image_metrics: ['auroc', 'ap', 'f1-max']
39
+ pixel_metrics: ['auroc', 'aupro', 'f1-max']
40
+ visualize: True
41
+ prompt_learn_method: concat
42
+ cls_seg_los: seg
43
+ data_path: ['/data/alireza/datasets//mvtec/']
44
+ experiment_root: ./workspaces/trained_on_mvtec_prompt_learning_segonly_518/vegan-arkansas
45
+ save_path: ./workspaces/trained_on_mvtec_prompt_learning_segonly_518/vegan-arkansas/checkpoints
46
+ Command arguments: /home/alireza/miniconda3/envs/cuda11_8torch2_5/bin/python /home/alireza/KyotoServer/CLIP-AD/tipsomaly/train.py --model_name prompt_learning_segonly_518 --dataset mvtec --device 3 --cls_seg_los seg
Tipsomaly/workspaces/trained_on_mvtec_default/vegan-arkansas/log.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ 25-08-24 02:27:58.439 - INFO: Epoch 1 Mean Losses: LS CE: 1.6996, LS FC: 0.0422, LS DC P: 0.8976, LS DC N: 0.0594, Loss: 0.9993
2
+ 25-08-24 02:31:43.275 - INFO: Epoch 2 Mean Losses: LS CE: 1.8427, LS FC: 0.0470, LS DC P: 0.7758, LS DC N: 0.0271, Loss: 0.8499
Tipsomaly/workspaces/trained_on_visa_default/vegan-arkansas/checkpoints/args.txt ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Timestamp: 2025-08-24 02:44:06
3
+ image_size: 518
4
+ seed: 111
5
+ metrics: image-pixel-level
6
+ devices: [3]
7
+ model_name: prompt_learning_segonly_518
8
+ sigma: 4
9
+ dataset: ['visa']
10
+ dataset_category:
11
+ class_name: ['all']
12
+ k_shot: 0
13
+ type: train
14
+ log_dir: vegan-arkansas
15
+ image_metrics: ['auroc', 'ap', 'f1-max']
16
+ pixel_metrics: ['auroc', 'aupro', 'f1-max']
17
+ visualize: True
18
+ prompt_learn_method: concat
19
+ cls_seg_los: seg
20
+ data_path: ['/data/alireza/datasets//visa/']
21
+ experiment_root: ./workspaces/trained_on_visa_prompt_learning_segonly_518/vegan-arkansas
22
+ save_path: ./workspaces/trained_on_visa_prompt_learning_segonly_518/vegan-arkansas/checkpoints
23
+ Command arguments: /home/alireza/miniconda3/envs/cuda11_8torch2_5/bin/python /home/alireza/KyotoServer/CLIP-AD/tipsomaly/train.py --model_name prompt_learning_segonly_518 --dataset visa --device 3 --cls_seg_los seg
Tipsomaly/workspaces/trained_on_visa_default/vegan-arkansas/log.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ 25-08-24 02:49:49.051 - INFO: Epoch 1 Mean Losses: LS CE: 1.6292, LS FC: 0.0135, LS DC P: 0.9782, LS DC N: 0.0234, Loss: 1.0151
2
+ 25-08-24 02:55:10.451 - INFO: Epoch 2 Mean Losses: LS CE: 1.7016, LS FC: 0.0139, LS DC P: 0.9251, LS DC N: 0.0069, Loss: 0.9459
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.44.0
2
+ numpy>=1.26
3
+ pillow>=10.0
4
+ scipy>=1.11
5
+ torch>=2.1
6
+ torchvision>=0.16
7
+ transformers>=4.45
8
+ sentencepiece>=0.1.99