Add files using upload-large-folder tool
Browse files- Tipsomaly/LICENSE +21 -0
- Tipsomaly/README.md +131 -0
- Tipsomaly/__init__.py +0 -0
- Tipsomaly/datasets/__init__.py +0 -0
- Tipsomaly/datasets/dataset.py +115 -0
- Tipsomaly/datasets/input_transforms.py +23 -0
- Tipsomaly/model/__init__.py +0 -0
- Tipsomaly/model/big_vision/__init__.py +0 -0
- Tipsomaly/model/big_vision/configs/proj/paligemma/transfers/vatex_cap.py +210 -0
- Tipsomaly/model/big_vision/configs/proj/paligemma/transfers/vizwizvqa.py +160 -0
- Tipsomaly/model/big_vision/configs/proj/paligemma/transfers/widgetcap.py +180 -0
- Tipsomaly/model/big_vision/input_pipeline.py +357 -0
- Tipsomaly/model/big_vision/load_siglip.py +157 -0
- Tipsomaly/model/big_vision/optax.py +225 -0
- Tipsomaly/model/big_vision/optax_test.py +341 -0
- Tipsomaly/model/big_vision/requirements.txt +19 -0
- Tipsomaly/model/big_vision/run_tpu.sh +35 -0
- Tipsomaly/model/big_vision/sharding.py +197 -0
- Tipsomaly/model/big_vision/train.py +518 -0
- Tipsomaly/model/big_vision/utils.py +1478 -0
- Tipsomaly/model/big_vision/utils_test.py +360 -0
- Tipsomaly/model/omaly/__init__.py +2 -0
- Tipsomaly/model/omaly/fixed_prompts.py +54 -0
- Tipsomaly/model/omaly/text_encoder.py +164 -0
- Tipsomaly/model/omaly/vision_encoder.py +48 -0
- Tipsomaly/model/siglip2/__init__.py +0 -0
- Tipsomaly/model/siglip2/siglip2_prompt_learnable.py +183 -0
- Tipsomaly/model/tips/__init__.py +35 -0
- Tipsomaly/model/tips/__pycache__/image_encoder.cpython-39.pyc +0 -0
- Tipsomaly/model/tips/__pycache__/load_model.cpython-39.pyc +0 -0
- Tipsomaly/model/tips/__pycache__/text_encoder.cpython-39.pyc +0 -0
- Tipsomaly/model/tips/checkpoints/checkpoint.py +98 -0
- Tipsomaly/model/tips/checkpoints/download_checkpoints.sh +36 -0
- Tipsomaly/model/tips/image_encoder.py +1002 -0
- Tipsomaly/model/tips/load_model.py +112 -0
- Tipsomaly/model/tips/text_encoder.py +519 -0
- Tipsomaly/reproduce.sh +22 -0
- Tipsomaly/requirements.txt +42 -0
- Tipsomaly/test.py +336 -0
- Tipsomaly/train.py +353 -0
- Tipsomaly/train_test.sh +34 -0
- Tipsomaly/utils/logger.py +72 -0
- Tipsomaly/utils/loss.py +107 -0
- Tipsomaly/utils/metrics.py +84 -0
- Tipsomaly/utils/visualize.py +55 -0
- Tipsomaly/workspaces/trained_on_mvtec_default/vegan-arkansas/checkpoints/args.txt +46 -0
- Tipsomaly/workspaces/trained_on_mvtec_default/vegan-arkansas/log.txt +2 -0
- Tipsomaly/workspaces/trained_on_visa_default/vegan-arkansas/checkpoints/args.txt +23 -0
- Tipsomaly/workspaces/trained_on_visa_default/vegan-arkansas/log.txt +2 -0
- 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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
|