Sync from GitHub FlexiBrain main
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- .gitignore +25 -0
- LICENSE +12 -0
- NOTICE +8 -0
- README.md +177 -180
- THIRD_PARTY.md +10 -0
- assets/pipeline.png +3 -0
- configs/downstream_example.yaml +45 -0
- configs/pretrain_example.yaml +34 -0
- data_process.py +535 -0
- flexibrain/__init__.py +0 -0
- flexibrain/__main__.py +4 -0
- flexibrain/cli.py +155 -0
- flexibrain/config.py +131 -0
- flexibrain/data/__init__.py +5 -0
- flexibrain/data/builders.py +57 -0
- flexibrain/data/classification.py +375 -0
- flexibrain/data/collate.py +119 -0
- flexibrain/data/nifti.py +386 -0
- flexibrain/distributed/__init__.py +20 -0
- flexibrain/engine/__init__.py +4 -0
- flexibrain/engine/downstream_trainer.py +170 -0
- flexibrain/engine/pretrainer.py +134 -0
- flexibrain/models/__init__.py +4 -0
- flexibrain/models/classifier.py +237 -0
- flexibrain/models/factory.py +104 -0
- flexibrain/models/layers/__init__.py +0 -0
- flexibrain/models/layers/moe.py +156 -0
- flexibrain/models/layers/pos_embed.py +62 -0
- flexibrain/models/layers/stape.py +290 -0
- flexibrain/models/mamba_blocks.py +115 -0
- flexibrain/models/mamba_jepa.py +440 -0
- flexibrain/models/transformer_block.py +92 -0
- flexibrain/utils/__init__.py +0 -0
- flexibrain/utils/checkpoint.py +108 -0
- flexibrain/utils/logging.py +19 -0
- flexibrain/utils/pinv_resize.py +55 -0
- flexibrain/utils/seed.py +11 -0
- flexibrain/utils/training.py +38 -0
- flexibrain/utils/weight_resize.py +131 -0
- licenses/causal_conv1d_LICENSE_BSD_3_Clause.txt +29 -0
- licenses/mamba2_LICENSE_Apache_2.0.txt +201 -0
- licenses/mamba_mae_LICENSE_CC_BY_NC_4.0.txt +400 -0
- mamba_ssm/__init__.py +6 -0
- mamba_ssm/distributed/__init__.py +0 -0
- mamba_ssm/distributed/distributed_utils.py +144 -0
- mamba_ssm/distributed/tensor_parallel.py +296 -0
- mamba_ssm/models/__init__.py +0 -0
- mamba_ssm/models/config_mamba.py +18 -0
- mamba_ssm/models/mixer_seq_simple.py +307 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/pipeline.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*.so
|
| 4 |
+
*.whl
|
| 5 |
+
*.egg-info/
|
| 6 |
+
build/
|
| 7 |
+
dist/
|
| 8 |
+
.eggs/
|
| 9 |
+
.pytest_cache/
|
| 10 |
+
.mypy_cache/
|
| 11 |
+
.ruff_cache/
|
| 12 |
+
/logs/
|
| 13 |
+
/checkpoints/
|
| 14 |
+
/resolution_runs/
|
| 15 |
+
/outputs/
|
| 16 |
+
/data/
|
| 17 |
+
*.nii
|
| 18 |
+
*.nii.gz
|
| 19 |
+
*.pt
|
| 20 |
+
*.pth
|
| 21 |
+
*.npy
|
| 22 |
+
*.npz
|
| 23 |
+
.DS_Store
|
| 24 |
+
Thumbs.db
|
| 25 |
+
.nfs*
|
LICENSE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
| 2 |
+
|
| 3 |
+
Flexibrain is provided for non-commercial research use under the Creative
|
| 4 |
+
Commons Attribution-NonCommercial-ShareAlike 4.0 International License
|
| 5 |
+
(CC BY-NC-SA 4.0).
|
| 6 |
+
|
| 7 |
+
You may share and adapt the material for non-commercial purposes, provided
|
| 8 |
+
that appropriate attribution is given and adaptations are distributed under
|
| 9 |
+
the same license terms.
|
| 10 |
+
|
| 11 |
+
Full legal code: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
|
| 12 |
+
Human-readable summary: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flexibrain Notice
|
| 2 |
+
|
| 3 |
+
This repository contains Flexibrain project code plus small source fragments
|
| 4 |
+
needed for the pretrain/downstream path. Third-party references and preserved
|
| 5 |
+
license files are listed in THIRD_PARTY.md and licenses/.
|
| 6 |
+
|
| 7 |
+
Selected files include reference comments at the top of the source file where
|
| 8 |
+
the implementation is derived from or adapted from prior public projects.
|
README.md
CHANGED
|
@@ -1,180 +1,177 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
--
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
```
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
--
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
`--
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
```
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
--
|
| 100 |
-
--
|
| 101 |
-
--
|
| 102 |
-
--
|
| 103 |
-
--
|
| 104 |
-
--
|
| 105 |
-
--
|
| 106 |
-
--
|
| 107 |
-
--
|
| 108 |
-
--
|
| 109 |
-
--epochs
|
| 110 |
-
--
|
| 111 |
-
--
|
| 112 |
-
--
|
| 113 |
-
--
|
| 114 |
-
--
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
--
|
| 141 |
-
--
|
| 142 |
-
--
|
| 143 |
-
--
|
| 144 |
-
--
|
| 145 |
-
--
|
| 146 |
-
--
|
| 147 |
-
--
|
| 148 |
-
--
|
| 149 |
-
--
|
| 150 |
-
--
|
| 151 |
-
--
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
```
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
## License
|
| 179 |
-
|
| 180 |
-
This repository is provided for non-commercial research use under CC BY-NC-SA 4.0. See `LICENSE` and `NOTICE` for license boundaries and preserved notices.
|
|
|
|
| 1 |
+
# Flexibrain
|
| 2 |
+
|
| 3 |
+
Flexibrain is a voxel-level fMRI representation learning framework for pretraining and downstream classification. It keeps fMRI volumes in a fixed 96 x 96 x 96 input grid, reads each sample's voxel spacing and TR from the NIfTI header, and resizes patch embedding kernels in physical spatial and temporal units before learning with a Mamba-JEPA backbone.
|
| 4 |
+
|
| 5 |
+
<p align="center">
|
| 6 |
+
<img src="assets/pipeline.png" width="900" alt="FlexiBrain framework pipeline">
|
| 7 |
+
</p>
|
| 8 |
+
|
| 9 |
+
## Installation
|
| 10 |
+
|
| 11 |
+
The code was tested on l40 with Python 3.10, PyTorch 2.1.2, CUDA 12.1, `causal-conv1d`, `mamba-ssm`, and `flash-attn`.
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
conda create -n flexibrain python=3.10
|
| 15 |
+
conda activate flexibrain
|
| 16 |
+
pip install -r requirements.txt
|
| 17 |
+
pip install -e .
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
Check the CLI:
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
python -m flexibrain --help
|
| 24 |
+
python -m flexibrain pretrain --help
|
| 25 |
+
python -m flexibrain downstream --help
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## Data Preparation
|
| 29 |
+
|
| 30 |
+
Each sample should be a 4D NIfTI file shaped as:
|
| 31 |
+
|
| 32 |
+
```text
|
| 33 |
+
96 x 96 x 96 x T
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
Flexibrain uses the NIfTI header to read voxel spacing and TR. If a dataset has missing TR metadata, fix the header before training or pass an explicit fallback with `--default-tr` / `data.default_tr`.
|
| 37 |
+
|
| 38 |
+
`T_prime` and `tau_seconds` control the selected temporal length:
|
| 39 |
+
|
| 40 |
+
```text
|
| 41 |
+
kt = round(tau_seconds / TR)
|
| 42 |
+
T_selected = T_prime * kt
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
The preprocessing script can convert native/T1/MNI-space inputs to 96 x 96 x 96, apply sample-wise global z-score normalization over foreground voxels, and write 4D NIfTI outputs:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
python data_process.py \
|
| 49 |
+
--input-root /path/to/input_root \
|
| 50 |
+
--output-root /path/to/output_root \
|
| 51 |
+
--spaces all \
|
| 52 |
+
--groups class0,class1,class2
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
Expected grouped input layout:
|
| 56 |
+
|
| 57 |
+
```text
|
| 58 |
+
input_root/
|
| 59 |
+
|-- nativespace/class0/*.nii.gz
|
| 60 |
+
|-- t1space/class0/*.nii.gz
|
| 61 |
+
`-- mnispace/class0/*.nii.gz
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
If files are not organized by group subfolders, omit `--groups`. For MNI-space inputs, provide `--template-mask` when the default mask is not available.
|
| 65 |
+
|
| 66 |
+
Pretraining list files contain one NIfTI path per line:
|
| 67 |
+
|
| 68 |
+
```text
|
| 69 |
+
/path/to/sub-0001_bold.nii.gz
|
| 70 |
+
/path/to/sub-0002_bold.nii.gz
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
Downstream classification uses the same list format plus a CSV label table:
|
| 74 |
+
|
| 75 |
+
```csv
|
| 76 |
+
Subject,Group_idx
|
| 77 |
+
003_S_0908,2
|
| 78 |
+
011_S_0002,1
|
| 79 |
+
1001,0
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
Default label fields are `Subject` and `Group_idx`. `path_id_mode=auto` supports ADNI-style IDs such as `003_S_0908`, ADHD-style filenames, and fallback digit IDs.
|
| 83 |
+
|
| 84 |
+
## Pretraining
|
| 85 |
+
|
| 86 |
+
Run from a config:
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
python -m flexibrain pretrain --config configs/pretrain_example.yaml
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
Or use CLI arguments:
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
python -m flexibrain pretrain \
|
| 96 |
+
--train-list /path/to/pretrain_train.txt \
|
| 97 |
+
--val-list /path/to/pretrain_val.txt \
|
| 98 |
+
--checkpoint-dir ./checkpoints/pretrain/example \
|
| 99 |
+
--log-dir ./logs/pretrain/example \
|
| 100 |
+
--embed-dim 512 \
|
| 101 |
+
--depth 24 \
|
| 102 |
+
--predictor-depth 2 \
|
| 103 |
+
--bimamba-type v2 \
|
| 104 |
+
--if-devide-out \
|
| 105 |
+
--batch-size 4 \
|
| 106 |
+
--epochs 30 \
|
| 107 |
+
--lr 5e-4 \
|
| 108 |
+
--weight-decay 0.05 \
|
| 109 |
+
--warmup-epochs 3 \
|
| 110 |
+
--mask-ratio 0.65 \
|
| 111 |
+
--grad-accumulation-steps 4 \
|
| 112 |
+
--t-prime 30 \
|
| 113 |
+
--tau-seconds 6.0 \
|
| 114 |
+
--use-amp
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Outputs:
|
| 118 |
+
|
| 119 |
+
```text
|
| 120 |
+
checkpoint_latest.pt
|
| 121 |
+
checkpoint_best.pt
|
| 122 |
+
pretrain_*.log
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
## Downstream Classification
|
| 126 |
+
|
| 127 |
+
Run from a config:
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
python -m flexibrain downstream --config configs/downstream_example.yaml
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
Or use CLI arguments:
|
| 134 |
+
|
| 135 |
+
```bash
|
| 136 |
+
python -m flexibrain downstream \
|
| 137 |
+
--train-list /path/to/downstream_train.txt \
|
| 138 |
+
--val-list /path/to/downstream_val.txt \
|
| 139 |
+
--test-list /path/to/downstream_test.txt \
|
| 140 |
+
--csv /path/to/labels.csv \
|
| 141 |
+
--pretrain-checkpoint /path/to/checkpoint_best.pt \
|
| 142 |
+
--num-classes 3 \
|
| 143 |
+
--head-type transformer \
|
| 144 |
+
--batch-size 8 \
|
| 145 |
+
--epochs 30 \
|
| 146 |
+
--lr 1e-5 \
|
| 147 |
+
--lr-backbone 6e-6 \
|
| 148 |
+
--lr-head 6e-5 \
|
| 149 |
+
--checkpoint-dir ./checkpoints/downstream/example \
|
| 150 |
+
--log-dir ./logs/downstream/example \
|
| 151 |
+
--use-amp
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
During downstream training, validation metrics select `downstream_best.pt`. The test set is evaluated once at the end after loading that best validation checkpoint, and the final metrics are written to `test_metrics.json`.
|
| 155 |
+
|
| 156 |
+
## Configuration
|
| 157 |
+
|
| 158 |
+
YAML config mirrors the CLI options. Keep private paths in local config files and leave shared configs as portable examples. The provided examples use placeholder paths under `data/`:
|
| 159 |
+
|
| 160 |
+
```text
|
| 161 |
+
configs/pretrain_example.yaml
|
| 162 |
+
configs/downstream_example.yaml
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
## Checkpoint Compatibility
|
| 166 |
+
|
| 167 |
+
The downstream loader can initialize from the original pretraining checkpoint path format:
|
| 168 |
+
|
| 169 |
+
```text
|
| 170 |
+
/path/to/checkpoint_best.pt
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
When `use_checkpoint_config: true`, model-shape settings stored in the checkpoint are applied before loading the backbone.
|
| 174 |
+
|
| 175 |
+
## License
|
| 176 |
+
|
| 177 |
+
This repository is provided for non-commercial research use under CC BY-NC-SA 4.0. See `LICENSE` and `NOTICE` for license boundaries and preserved notices.
|
|
|
|
|
|
|
|
|
THIRD_PARTY.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Third-Party References
|
| 2 |
+
|
| 3 |
+
Flexibrain keeps only the source fragments needed for the Mamba-JEPA pretrain/downstream path.
|
| 4 |
+
|
| 5 |
+
- Brain-Harmony / BrainHarmonix: https://github.com/hzlab/Brain-Harmony. The Transformer head block in `flexibrain/models/transformer_block.py` is derived from the official Brain-Harmony `libs/flex_transformer.py` design. The official README marks Brain-Harmony as CC BY-NC-SA 4.0. No standalone Brain-Harmony project directory is vendored.
|
| 6 |
+
- 3D Mamba MAE: referenced for the Mamba block factory design. The original project license file is preserved in `licenses/mamba_mae_LICENSE_CC_BY_NC_4.0.txt`.
|
| 7 |
+
- Mamba / mamba_ssm: the minimal Python source needed by the custom Mamba block is included under `mamba_ssm/`; the Apache-2.0 license is preserved in `licenses/mamba2_LICENSE_Apache_2.0.txt`.
|
| 8 |
+
- causal-conv1d: used as a CUDA extension dependency and installed via requirements; BSD-3-Clause license is preserved in `licenses/causal_conv1d_LICENSE_BSD_3_Clause.txt`.
|
| 9 |
+
|
| 10 |
+
Binary build artifacts (`*.so`, `*.whl`), checkpoints, logs, and datasets are not included in this repository.
|
assets/pipeline.png
ADDED
|
Git LFS Details
|
configs/downstream_example.yaml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pretrain_checkpoint: /path/to/checkpoint_best.pt
|
| 2 |
+
from_scratch: false
|
| 3 |
+
use_checkpoint_config: true
|
| 4 |
+
model:
|
| 5 |
+
model_type: mamba
|
| 6 |
+
num_classes: 3
|
| 7 |
+
head_type: transformer
|
| 8 |
+
head_depth: 2
|
| 9 |
+
head_num_heads: 8
|
| 10 |
+
head_mlp_ratio: 4.0
|
| 11 |
+
head_proj_drop: 0.1
|
| 12 |
+
head_drop_path: 0.1
|
| 13 |
+
mlp_hidden: 512
|
| 14 |
+
mlp_depth: 4
|
| 15 |
+
mlp_dropout: 0.1
|
| 16 |
+
freeze_backbone: false
|
| 17 |
+
data:
|
| 18 |
+
train_list: data/lists/downstream_train.txt
|
| 19 |
+
val_list: data/lists/downstream_val.txt
|
| 20 |
+
test_list: data/lists/downstream_test.txt
|
| 21 |
+
csv: data/labels.csv
|
| 22 |
+
id_column: Subject
|
| 23 |
+
label_column: Group_idx
|
| 24 |
+
label_mode: multiclass
|
| 25 |
+
path_id_mode: auto
|
| 26 |
+
batch_size: 8
|
| 27 |
+
num_workers: 8
|
| 28 |
+
T_prime: 30
|
| 29 |
+
tau_seconds: 6.0
|
| 30 |
+
default_tr: null
|
| 31 |
+
training:
|
| 32 |
+
epochs: 30
|
| 33 |
+
lr: 1.0e-5
|
| 34 |
+
lr_backbone: 6.0e-6
|
| 35 |
+
lr_head: 6.0e-5
|
| 36 |
+
weight_decay: 0.05
|
| 37 |
+
warmup_epochs: 2
|
| 38 |
+
grad_accumulation_steps: 2
|
| 39 |
+
grad_clip: 1.0
|
| 40 |
+
seed: 42
|
| 41 |
+
use_amp: true
|
| 42 |
+
logging:
|
| 43 |
+
log_interval: 20
|
| 44 |
+
checkpoint_dir: ./checkpoints/downstream/example
|
| 45 |
+
log_dir: ./logs/downstream/example
|
configs/pretrain_example.yaml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model:
|
| 2 |
+
model_type: mamba
|
| 3 |
+
embed_dim: 512
|
| 4 |
+
depth: 24
|
| 5 |
+
predictor_depth: 2
|
| 6 |
+
drop_path_rate: 0.1
|
| 7 |
+
bimamba_type: v2
|
| 8 |
+
if_devide_out: true
|
| 9 |
+
mixer_type: mamba
|
| 10 |
+
momentum: 0.996
|
| 11 |
+
final_momentum: 0.9999
|
| 12 |
+
norm_target: true
|
| 13 |
+
data:
|
| 14 |
+
train_list: data/lists/pretrain_train.txt
|
| 15 |
+
val_list: data/lists/pretrain_val.txt
|
| 16 |
+
batch_size: 4
|
| 17 |
+
num_workers: 8
|
| 18 |
+
T_prime: 30
|
| 19 |
+
tau_seconds: 6.0
|
| 20 |
+
default_tr: null
|
| 21 |
+
training:
|
| 22 |
+
epochs: 30
|
| 23 |
+
lr: 5.0e-4
|
| 24 |
+
weight_decay: 0.05
|
| 25 |
+
warmup_epochs: 3
|
| 26 |
+
mask_ratio: 0.65
|
| 27 |
+
grad_clip: 1.0
|
| 28 |
+
grad_accumulation_steps: 4
|
| 29 |
+
seed: 42
|
| 30 |
+
use_amp: true
|
| 31 |
+
logging:
|
| 32 |
+
log_interval: 20
|
| 33 |
+
checkpoint_dir: ./checkpoints/pretrain/example
|
| 34 |
+
log_dir: ./logs/pretrain/example
|
data_process.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generic three-space 4D voxel preprocessing with sample-wise global z-score."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import re
|
| 8 |
+
import sys
|
| 9 |
+
import warnings
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
| 13 |
+
|
| 14 |
+
import nibabel as nib
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
from nilearn.image import resample_to_img
|
| 19 |
+
except ImportError:
|
| 20 |
+
resample_to_img = None
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
from tqdm import tqdm
|
| 24 |
+
except ImportError:
|
| 25 |
+
tqdm = None
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
SPACE_ORDER = ("native", "t1", "mni")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class Counts:
|
| 33 |
+
seen: int = 0
|
| 34 |
+
processed: int = 0
|
| 35 |
+
dry_run: int = 0
|
| 36 |
+
skipped_existing: int = 0
|
| 37 |
+
skipped_input_dir: int = 0
|
| 38 |
+
skipped_non4d: int = 0
|
| 39 |
+
skipped_no_mask: int = 0
|
| 40 |
+
skipped_empty_foreground: int = 0
|
| 41 |
+
failed: int = 0
|
| 42 |
+
|
| 43 |
+
def add(self, other: "Counts") -> None:
|
| 44 |
+
for name in self.__dataclass_fields__:
|
| 45 |
+
setattr(self, name, getattr(self, name) + getattr(other, name))
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def parse_target_shape(value: str) -> Tuple[int, int, int]:
|
| 49 |
+
parts = [part.strip() for part in value.split(",")]
|
| 50 |
+
if len(parts) != 3:
|
| 51 |
+
raise argparse.ArgumentTypeError("target shape must be formatted as X,Y,Z")
|
| 52 |
+
try:
|
| 53 |
+
shape = tuple(int(part) for part in parts)
|
| 54 |
+
except ValueError as exc:
|
| 55 |
+
raise argparse.ArgumentTypeError("target shape values must be integers") from exc
|
| 56 |
+
if any(dim <= 0 for dim in shape):
|
| 57 |
+
raise argparse.ArgumentTypeError("target shape values must be positive")
|
| 58 |
+
return shape # type: ignore[return-value]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def parse_csv(value: Optional[str]) -> Optional[List[str]]:
|
| 62 |
+
if value is None:
|
| 63 |
+
return None
|
| 64 |
+
items = [item.strip() for item in value.split(",") if item.strip()]
|
| 65 |
+
return items or None
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def selected_spaces(value: str) -> List[str]:
|
| 69 |
+
if value == "all":
|
| 70 |
+
return list(SPACE_ORDER)
|
| 71 |
+
return [value]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def is_nifti_file(path: Path) -> bool:
|
| 75 |
+
name = path.name.lower()
|
| 76 |
+
return path.is_file() and (name.endswith(".nii") or name.endswith(".nii.gz"))
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def nifti_stem(path: Path) -> str:
|
| 80 |
+
name = path.name
|
| 81 |
+
if name.lower().endswith(".nii.gz"):
|
| 82 |
+
return name[:-7]
|
| 83 |
+
if name.lower().endswith(".nii"):
|
| 84 |
+
return name[:-4]
|
| 85 |
+
return path.stem
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def clean_output_name(in_file: Path) -> str:
|
| 89 |
+
return f"{nifti_stem(in_file)}_global_zscore.nii.gz"
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def iter_nifti_files(input_dir: Path) -> List[Path]:
|
| 93 |
+
if not input_dir.is_dir():
|
| 94 |
+
return []
|
| 95 |
+
return sorted((path for path in input_dir.iterdir() if is_nifti_file(path)), key=lambda p: p.name)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def progress(items: Sequence[Path], desc: str) -> Iterable[Path]:
|
| 99 |
+
if tqdm is None:
|
| 100 |
+
return items
|
| 101 |
+
return tqdm(items, desc=desc)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def pad_crop_keep_world(
|
| 105 |
+
img: nib.Nifti1Image,
|
| 106 |
+
target_xyz: Tuple[int, int, int] = (96, 96, 96),
|
| 107 |
+
fill_value: float = 0.0,
|
| 108 |
+
) -> Tuple[nib.Nifti1Image, Dict[str, Any]]:
|
| 109 |
+
"""Center pad/crop the first three axes and update affine translation."""
|
| 110 |
+
data = np.asarray(img.dataobj)
|
| 111 |
+
affine = img.affine.copy()
|
| 112 |
+
linear = affine[:3, :3].copy()
|
| 113 |
+
old_translation = affine[:3, 3].copy()
|
| 114 |
+
original_ndim = data.ndim
|
| 115 |
+
|
| 116 |
+
if data.ndim == 3:
|
| 117 |
+
x, y, z = data.shape
|
| 118 |
+
t = 1
|
| 119 |
+
data = data[..., None]
|
| 120 |
+
elif data.ndim == 4:
|
| 121 |
+
x, y, z, t = data.shape
|
| 122 |
+
else:
|
| 123 |
+
raise ValueError(f"Expect 3D/4D NIfTI, got shape {data.shape}")
|
| 124 |
+
|
| 125 |
+
def split_plan(old: int, new: int) -> Tuple[int, int, str]:
|
| 126 |
+
if old == new:
|
| 127 |
+
return 0, 0, "same"
|
| 128 |
+
if old < new:
|
| 129 |
+
total = new - old
|
| 130 |
+
left = total // 2
|
| 131 |
+
return left, total - left, "pad"
|
| 132 |
+
total = old - new
|
| 133 |
+
left = total // 2
|
| 134 |
+
return left, total - left, "crop"
|
| 135 |
+
|
| 136 |
+
px_l, px_r, mode_x = split_plan(x, target_xyz[0])
|
| 137 |
+
py_l, py_r, mode_y = split_plan(y, target_xyz[1])
|
| 138 |
+
pz_l, pz_r, mode_z = split_plan(z, target_xyz[2])
|
| 139 |
+
|
| 140 |
+
out_data = data
|
| 141 |
+
if mode_z == "crop":
|
| 142 |
+
out_data = out_data[:, :, pz_l : z - pz_r, :]
|
| 143 |
+
if mode_y == "crop":
|
| 144 |
+
out_data = out_data[:, py_l : y - py_r, :, :]
|
| 145 |
+
if mode_x == "crop":
|
| 146 |
+
out_data = out_data[px_l : x - px_r, :, :, :]
|
| 147 |
+
|
| 148 |
+
pad_width = (
|
| 149 |
+
(px_l if mode_x == "pad" else 0, px_r if mode_x == "pad" else 0),
|
| 150 |
+
(py_l if mode_y == "pad" else 0, py_r if mode_y == "pad" else 0),
|
| 151 |
+
(pz_l if mode_z == "pad" else 0, pz_r if mode_z == "pad" else 0),
|
| 152 |
+
(0, 0),
|
| 153 |
+
)
|
| 154 |
+
if any(width != (0, 0) for width in pad_width):
|
| 155 |
+
out_data = np.pad(out_data, pad_width=pad_width, mode="constant", constant_values=fill_value)
|
| 156 |
+
|
| 157 |
+
pad_left = np.array(
|
| 158 |
+
[
|
| 159 |
+
px_l if mode_x == "pad" else 0,
|
| 160 |
+
py_l if mode_y == "pad" else 0,
|
| 161 |
+
pz_l if mode_z == "pad" else 0,
|
| 162 |
+
],
|
| 163 |
+
dtype=float,
|
| 164 |
+
)
|
| 165 |
+
crop_left = np.array(
|
| 166 |
+
[
|
| 167 |
+
px_l if mode_x == "crop" else 0,
|
| 168 |
+
py_l if mode_y == "crop" else 0,
|
| 169 |
+
pz_l if mode_z == "crop" else 0,
|
| 170 |
+
],
|
| 171 |
+
dtype=float,
|
| 172 |
+
)
|
| 173 |
+
new_translation = old_translation + linear @ crop_left - linear @ pad_left
|
| 174 |
+
affine[:3, 3] = new_translation
|
| 175 |
+
|
| 176 |
+
if original_ndim == 3:
|
| 177 |
+
out_data = out_data[..., 0]
|
| 178 |
+
|
| 179 |
+
header = img.header.copy()
|
| 180 |
+
qcode = int(header.get("qform_code", 1)) or 1
|
| 181 |
+
scode = int(header.get("sform_code", 1)) or 1
|
| 182 |
+
out_img = nib.Nifti1Image(out_data, affine, header=header)
|
| 183 |
+
out_img.set_qform(affine, code=qcode)
|
| 184 |
+
out_img.set_sform(affine, code=scode)
|
| 185 |
+
|
| 186 |
+
info = {
|
| 187 |
+
"old_shape": (x, y, z, t),
|
| 188 |
+
"new_shape": out_img.shape if len(out_img.shape) == 4 else out_img.shape + (1,),
|
| 189 |
+
"plan": {
|
| 190 |
+
"x": (px_l, px_r, mode_x),
|
| 191 |
+
"y": (py_l, py_r, mode_y),
|
| 192 |
+
"z": (pz_l, pz_r, mode_z),
|
| 193 |
+
},
|
| 194 |
+
"affine_old_t": tuple(old_translation.tolist()),
|
| 195 |
+
"affine_new_t": tuple(new_translation.tolist()),
|
| 196 |
+
}
|
| 197 |
+
return out_img, info
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def extract_match_key(path: Path) -> str:
|
| 201 |
+
stem = nifti_stem(path)
|
| 202 |
+
bids_match = re.search(r"(sub-[A-Za-z0-9]+)", stem, flags=re.IGNORECASE)
|
| 203 |
+
if bids_match:
|
| 204 |
+
return bids_match.group(1).lower()
|
| 205 |
+
|
| 206 |
+
for token in ("_space-", "_desc-", "_task-", "_run-", "_bold", "_fmri"):
|
| 207 |
+
idx = stem.lower().find(token)
|
| 208 |
+
if idx > 0:
|
| 209 |
+
return stem[:idx].lower()
|
| 210 |
+
return stem.lower()
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def best_matching_file(source_file: Path, target_dir: Path) -> Optional[Path]:
|
| 214 |
+
if not target_dir.is_dir():
|
| 215 |
+
return None
|
| 216 |
+
|
| 217 |
+
key = extract_match_key(source_file)
|
| 218 |
+
candidates = [path for path in target_dir.rglob("*") if is_nifti_file(path)]
|
| 219 |
+
if not candidates:
|
| 220 |
+
return None
|
| 221 |
+
|
| 222 |
+
source_stem = nifti_stem(source_file).lower()
|
| 223 |
+
|
| 224 |
+
def score(path: Path) -> Tuple[int, int, int, str]:
|
| 225 |
+
stem = nifti_stem(path).lower()
|
| 226 |
+
exact = 3 if stem == source_stem else 0
|
| 227 |
+
key_match = 2 if key and key in stem else 0
|
| 228 |
+
short_path = -len(str(path))
|
| 229 |
+
return (exact, key_match, short_path, str(path))
|
| 230 |
+
|
| 231 |
+
best = max(candidates, key=score)
|
| 232 |
+
best_score = score(best)
|
| 233 |
+
if best_score[0] == 0 and best_score[1] == 0:
|
| 234 |
+
return None
|
| 235 |
+
return best
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def make_output_image(z_data: np.ndarray, template_img: nib.Nifti1Image) -> nib.Nifti1Image:
|
| 239 |
+
header = template_img.header.copy()
|
| 240 |
+
qcode = int(header.get("qform_code", 1)) or 1
|
| 241 |
+
scode = int(header.get("sform_code", 1)) or 1
|
| 242 |
+
out_img = nib.Nifti1Image(z_data.astype(np.float32), template_img.affine, header=header)
|
| 243 |
+
out_img.set_data_dtype(np.float32)
|
| 244 |
+
out_img.set_qform(template_img.affine, code=qcode)
|
| 245 |
+
out_img.set_sform(template_img.affine, code=scode)
|
| 246 |
+
return out_img
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def global_zscore(arr: np.ndarray, background_mask: np.ndarray) -> Optional[np.ndarray]:
|
| 250 |
+
foreground_mask = ~background_mask
|
| 251 |
+
if foreground_mask.shape != arr.shape[:3]:
|
| 252 |
+
raise ValueError(f"mask shape {foreground_mask.shape} does not match image shape {arr.shape[:3]}")
|
| 253 |
+
if not np.any(foreground_mask):
|
| 254 |
+
return None
|
| 255 |
+
|
| 256 |
+
vals = arr[foreground_mask, :]
|
| 257 |
+
if vals.size == 0:
|
| 258 |
+
return None
|
| 259 |
+
|
| 260 |
+
mu = float(vals.mean())
|
| 261 |
+
std = float(vals.std())
|
| 262 |
+
with np.errstate(invalid="ignore", divide="ignore"):
|
| 263 |
+
z = (arr - mu) / (std + 1e-6)
|
| 264 |
+
z[background_mask, :] = 0.0
|
| 265 |
+
return z.astype(np.float32, copy=False)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def load_4d_padded(path: Path, target_shape: Tuple[int, int, int]) -> Tuple[Optional[nib.Nifti1Image], Optional[Dict[str, Any]]]:
|
| 269 |
+
img = nib.load(str(path))
|
| 270 |
+
if len(img.shape) != 4:
|
| 271 |
+
return None, None
|
| 272 |
+
return pad_crop_keep_world(img, target_xyz=target_shape, fill_value=0.0)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def resolve_mask_path(mask_arg: Optional[str], script_dir: Path) -> Optional[Path]:
|
| 276 |
+
if not mask_arg:
|
| 277 |
+
return None
|
| 278 |
+
mask_path = Path(mask_arg)
|
| 279 |
+
if mask_path.exists():
|
| 280 |
+
return mask_path
|
| 281 |
+
script_relative = script_dir / mask_arg
|
| 282 |
+
if script_relative.exists():
|
| 283 |
+
return script_relative
|
| 284 |
+
return mask_path
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def resample_template_background(mask_img: nib.Nifti1Image, target_img: nib.Nifti1Image) -> np.ndarray:
|
| 288 |
+
if resample_to_img is None:
|
| 289 |
+
raise RuntimeError("nilearn is required for template mask resampling but is not installed")
|
| 290 |
+
try:
|
| 291 |
+
mask_res = resample_to_img(
|
| 292 |
+
mask_img,
|
| 293 |
+
target_img,
|
| 294 |
+
interpolation="nearest",
|
| 295 |
+
force_resample=True,
|
| 296 |
+
copy_header=True,
|
| 297 |
+
)
|
| 298 |
+
except TypeError:
|
| 299 |
+
mask_res = resample_to_img(mask_img, target_img, interpolation="nearest")
|
| 300 |
+
mask_arr = np.asarray(mask_res.dataobj)
|
| 301 |
+
if mask_arr.ndim == 4:
|
| 302 |
+
mask_arr = mask_arr[..., 0]
|
| 303 |
+
return mask_arr == 0
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def space_subdir(args: argparse.Namespace, space: str) -> str:
|
| 307 |
+
return {
|
| 308 |
+
"native": args.native_subdir,
|
| 309 |
+
"t1": args.t1_subdir,
|
| 310 |
+
"mni": args.mni_subdir,
|
| 311 |
+
}[space]
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def input_dir_for(args: argparse.Namespace, space: str, group: Optional[str]) -> Path:
|
| 315 |
+
base = args.input_root / space_subdir(args, space)
|
| 316 |
+
return base / group if group else base
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def output_dir_for(args: argparse.Namespace, space: str, group: Optional[str]) -> Path:
|
| 320 |
+
base = args.output_root / space_subdir(args, space)
|
| 321 |
+
return base / group if group else base
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def output_path_for(args: argparse.Namespace, space: str, group: Optional[str], in_file: Path) -> Path:
|
| 325 |
+
return output_dir_for(args, space, group) / clean_output_name(in_file)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def discover_groups(args: argparse.Namespace, space: str) -> List[Optional[str]]:
|
| 329 |
+
if args.groups is not None:
|
| 330 |
+
return args.groups
|
| 331 |
+
|
| 332 |
+
base = args.input_root / space_subdir(args, space)
|
| 333 |
+
if not base.is_dir():
|
| 334 |
+
return [None]
|
| 335 |
+
|
| 336 |
+
groups: List[Optional[str]] = []
|
| 337 |
+
if iter_nifti_files(base):
|
| 338 |
+
groups.append(None)
|
| 339 |
+
groups.extend(sorted(path.name for path in base.iterdir() if path.is_dir() and iter_nifti_files(path)))
|
| 340 |
+
return groups or [None]
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def process_one_file(
|
| 344 |
+
in_file: Path,
|
| 345 |
+
out_file: Path,
|
| 346 |
+
space: str,
|
| 347 |
+
group: Optional[str],
|
| 348 |
+
args: argparse.Namespace,
|
| 349 |
+
template_mask_img: Optional[nib.Nifti1Image] = None,
|
| 350 |
+
) -> str:
|
| 351 |
+
new_img, info = load_4d_padded(in_file, args.target_shape)
|
| 352 |
+
if new_img is None:
|
| 353 |
+
return "skipped_non4d"
|
| 354 |
+
|
| 355 |
+
arr = np.asarray(new_img.dataobj, dtype=np.float32)
|
| 356 |
+
if arr.ndim != 4:
|
| 357 |
+
return "skipped_non4d"
|
| 358 |
+
|
| 359 |
+
if space == "native":
|
| 360 |
+
background_mask = (arr == 0).all(axis=3)
|
| 361 |
+
elif space == "t1":
|
| 362 |
+
native_dir = input_dir_for(args, "native", group)
|
| 363 |
+
native_match = best_matching_file(in_file, native_dir)
|
| 364 |
+
if native_match is None:
|
| 365 |
+
print(f"[WARN] Native-space mask source not found for {in_file.name} in {native_dir}")
|
| 366 |
+
return "skipped_no_mask"
|
| 367 |
+
native_img, _ = load_4d_padded(native_match, args.target_shape)
|
| 368 |
+
if native_img is None:
|
| 369 |
+
print(f"[WARN] Native-space mask source is not 4D, skipping: {native_match}")
|
| 370 |
+
return "skipped_no_mask"
|
| 371 |
+
native_arr = np.asarray(native_img.dataobj, dtype=np.float32)
|
| 372 |
+
background_mask = (native_arr == 0).all(axis=3)
|
| 373 |
+
elif space == "mni":
|
| 374 |
+
if template_mask_img is None:
|
| 375 |
+
print(f"[WARN] Template mask unavailable, skipping: {in_file}")
|
| 376 |
+
return "skipped_no_mask"
|
| 377 |
+
background_mask = resample_template_background(template_mask_img, new_img)
|
| 378 |
+
else:
|
| 379 |
+
raise ValueError(f"Unsupported space: {space}")
|
| 380 |
+
|
| 381 |
+
z_data = global_zscore(arr, background_mask)
|
| 382 |
+
if z_data is None:
|
| 383 |
+
print(f"[WARN] Empty foreground after masking, skipping: {in_file}")
|
| 384 |
+
return "skipped_empty_foreground"
|
| 385 |
+
|
| 386 |
+
group_label = group if group is not None else "ungrouped"
|
| 387 |
+
print(
|
| 388 |
+
f"[AFT] {space}/{group_label} {in_file.name}: "
|
| 389 |
+
f"{info['old_shape']} -> {new_img.shape}, plan={info['plan']}"
|
| 390 |
+
)
|
| 391 |
+
if args.dry_run:
|
| 392 |
+
print(f"[DRY-RUN] Would save {out_file}")
|
| 393 |
+
return "dry_run"
|
| 394 |
+
|
| 395 |
+
out_file.parent.mkdir(parents=True, exist_ok=True)
|
| 396 |
+
out_img = make_output_image(z_data, new_img)
|
| 397 |
+
nib.save(out_img, str(out_file))
|
| 398 |
+
print(f"[SAVE] {out_file}")
|
| 399 |
+
return "processed"
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def increment(counts: Counts, status: str) -> None:
|
| 403 |
+
if not hasattr(counts, status):
|
| 404 |
+
raise ValueError(f"Unknown status: {status}")
|
| 405 |
+
setattr(counts, status, getattr(counts, status) + 1)
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def process_space_group(
|
| 409 |
+
space: str,
|
| 410 |
+
group: Optional[str],
|
| 411 |
+
args: argparse.Namespace,
|
| 412 |
+
template_mask_img: Optional[nib.Nifti1Image] = None,
|
| 413 |
+
) -> Counts:
|
| 414 |
+
counts = Counts()
|
| 415 |
+
input_dir = input_dir_for(args, space, group)
|
| 416 |
+
group_label = group if group is not None else "ungrouped"
|
| 417 |
+
|
| 418 |
+
if not input_dir.is_dir():
|
| 419 |
+
print(f"[WARN] Missing input dir, skipping {space}/{group_label}: {input_dir}")
|
| 420 |
+
counts.skipped_input_dir += 1
|
| 421 |
+
return counts
|
| 422 |
+
|
| 423 |
+
files = iter_nifti_files(input_dir)
|
| 424 |
+
print(f"[INFO] {space}/{group_label}: found {len(files)} NIfTI file(s) in {input_dir}")
|
| 425 |
+
for in_file in progress(files, desc=f"{space}/{group_label}"):
|
| 426 |
+
counts.seen += 1
|
| 427 |
+
out_file = output_path_for(args, space, group, in_file)
|
| 428 |
+
if out_file.exists() and not args.overwrite:
|
| 429 |
+
print(f"[SKIP] Exists: {out_file}")
|
| 430 |
+
counts.skipped_existing += 1
|
| 431 |
+
continue
|
| 432 |
+
|
| 433 |
+
try:
|
| 434 |
+
status = process_one_file(in_file, out_file, space, group, args, template_mask_img)
|
| 435 |
+
increment(counts, status)
|
| 436 |
+
except Exception as exc:
|
| 437 |
+
counts.failed += 1
|
| 438 |
+
print(f"[ERROR] Failed {in_file}: {exc}", file=sys.stderr)
|
| 439 |
+
|
| 440 |
+
return counts
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def build_arg_parser() -> argparse.ArgumentParser:
|
| 444 |
+
parser = argparse.ArgumentParser(
|
| 445 |
+
description="Process native/T1/MNI 4D voxel NIfTIs with sample-wise global z-score."
|
| 446 |
+
)
|
| 447 |
+
parser.add_argument(
|
| 448 |
+
"--input-root",
|
| 449 |
+
type=Path,
|
| 450 |
+
default=Path("."),
|
| 451 |
+
help="Root containing the native, T1, and MNI input subdirectories.",
|
| 452 |
+
)
|
| 453 |
+
parser.add_argument(
|
| 454 |
+
"--output-root",
|
| 455 |
+
type=Path,
|
| 456 |
+
default=Path("global_zscore_outputs"),
|
| 457 |
+
help="Output root for processed full 4D NIfTI files.",
|
| 458 |
+
)
|
| 459 |
+
parser.add_argument(
|
| 460 |
+
"--spaces",
|
| 461 |
+
choices=["native", "t1", "mni", "all"],
|
| 462 |
+
default="all",
|
| 463 |
+
help="Space to process.",
|
| 464 |
+
)
|
| 465 |
+
parser.add_argument(
|
| 466 |
+
"--groups",
|
| 467 |
+
type=parse_csv,
|
| 468 |
+
default=None,
|
| 469 |
+
help="Optional comma-separated group/subfolder list. If omitted, groups are auto-discovered.",
|
| 470 |
+
)
|
| 471 |
+
parser.add_argument("--native-subdir", default="nativespace", help="Native-space subdirectory under input/output roots.")
|
| 472 |
+
parser.add_argument("--t1-subdir", default="t1space", help="T1-space subdirectory under input/output roots.")
|
| 473 |
+
parser.add_argument("--mni-subdir", default="mnispace", help="MNI/template-space subdirectory under input/output roots.")
|
| 474 |
+
parser.add_argument(
|
| 475 |
+
"--target-shape",
|
| 476 |
+
type=parse_target_shape,
|
| 477 |
+
default=(96, 96, 96),
|
| 478 |
+
help="Spatial target shape formatted as X,Y,Z.",
|
| 479 |
+
)
|
| 480 |
+
parser.add_argument(
|
| 481 |
+
"--template-mask",
|
| 482 |
+
default="MNI152_T1_2mm_Brain_Mask.nii.gz",
|
| 483 |
+
help="Brain mask for MNI/template-space inputs. Relative paths are also checked next to this script.",
|
| 484 |
+
)
|
| 485 |
+
parser.add_argument("--overwrite", action="store_true", help="Overwrite existing outputs.")
|
| 486 |
+
parser.add_argument("--dry-run", action="store_true", help="Print planned work without writing outputs.")
|
| 487 |
+
return parser
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
def main(argv: Optional[List[str]] = None) -> int:
|
| 491 |
+
warnings.filterwarnings("default")
|
| 492 |
+
parser = build_arg_parser()
|
| 493 |
+
args = parser.parse_args(argv)
|
| 494 |
+
|
| 495 |
+
input_root = args.input_root.resolve()
|
| 496 |
+
output_root = args.output_root.resolve()
|
| 497 |
+
for space in SPACE_ORDER:
|
| 498 |
+
input_space_dir = (input_root / space_subdir(args, space)).resolve()
|
| 499 |
+
output_space_dir = (output_root / space_subdir(args, space)).resolve()
|
| 500 |
+
if output_space_dir == input_space_dir or input_space_dir in output_space_dir.parents:
|
| 501 |
+
print(
|
| 502 |
+
"[ERROR] Output space directory must not be the same as or inside "
|
| 503 |
+
f"the input space directory: {output_space_dir}",
|
| 504 |
+
file=sys.stderr,
|
| 505 |
+
)
|
| 506 |
+
return 2
|
| 507 |
+
|
| 508 |
+
spaces = selected_spaces(args.spaces)
|
| 509 |
+
template_mask_img = None
|
| 510 |
+
if "mni" in spaces:
|
| 511 |
+
mask_path = resolve_mask_path(args.template_mask, Path(__file__).resolve().parent)
|
| 512 |
+
if mask_path is None or not mask_path.exists():
|
| 513 |
+
print(f"[WARN] Template mask not found; MNI/template files will be skipped: {args.template_mask}", file=sys.stderr)
|
| 514 |
+
else:
|
| 515 |
+
template_mask_img = nib.load(str(mask_path))
|
| 516 |
+
print(f"[INFO] Loaded template mask: {mask_path}")
|
| 517 |
+
|
| 518 |
+
total = Counts()
|
| 519 |
+
for space in spaces:
|
| 520 |
+
for group in discover_groups(args, space):
|
| 521 |
+
counts = process_space_group(space, group, args, template_mask_img=template_mask_img)
|
| 522 |
+
total.add(counts)
|
| 523 |
+
|
| 524 |
+
print(
|
| 525 |
+
"[SUMMARY] "
|
| 526 |
+
f"seen={total.seen}, processed={total.processed}, dry_run={total.dry_run}, "
|
| 527 |
+
f"skipped_existing={total.skipped_existing}, skipped_input_dir={total.skipped_input_dir}, "
|
| 528 |
+
f"skipped_non4d={total.skipped_non4d}, skipped_no_mask={total.skipped_no_mask}, "
|
| 529 |
+
f"skipped_empty_foreground={total.skipped_empty_foreground}, failed={total.failed}"
|
| 530 |
+
)
|
| 531 |
+
return 1 if total.failed else 0
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
if __name__ == "__main__":
|
| 535 |
+
raise SystemExit(main())
|
flexibrain/__init__.py
ADDED
|
File without changes
|
flexibrain/__main__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flexibrain.cli import main
|
| 2 |
+
|
| 3 |
+
if __name__ == "__main__":
|
| 4 |
+
main()
|
flexibrain/cli.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from dataclasses import asdict
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
from flexibrain.config import load_config
|
| 10 |
+
from flexibrain.engine import DownstreamTrainer, Pretrainer
|
| 11 |
+
from flexibrain.models import build_downstream_model, build_pretrain_model
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _add_common(parser):
|
| 15 |
+
parser.add_argument("--config", default=None)
|
| 16 |
+
parser.add_argument("--train-list", default=None)
|
| 17 |
+
parser.add_argument("--val-list", default=None)
|
| 18 |
+
parser.add_argument("--test-list", default=None)
|
| 19 |
+
parser.add_argument("--batch-size", type=int, default=None)
|
| 20 |
+
parser.add_argument("--num-workers", type=int, default=None)
|
| 21 |
+
parser.add_argument("--t-prime", type=int, default=None)
|
| 22 |
+
parser.add_argument("--tau-seconds", type=float, default=None)
|
| 23 |
+
parser.add_argument("--default-tr", type=float, default=None, help="Fallback TR in seconds when a NIfTI header has no valid TR.")
|
| 24 |
+
parser.add_argument("--epochs", type=int, default=None)
|
| 25 |
+
parser.add_argument("--lr", type=float, default=None)
|
| 26 |
+
parser.add_argument("--weight-decay", type=float, default=None)
|
| 27 |
+
parser.add_argument("--warmup-epochs", type=int, default=None)
|
| 28 |
+
parser.add_argument("--grad-accumulation-steps", type=int, default=None)
|
| 29 |
+
parser.add_argument("--seed", type=int, default=None)
|
| 30 |
+
parser.add_argument("--log-interval", type=int, default=None)
|
| 31 |
+
parser.add_argument("--checkpoint-dir", default=None)
|
| 32 |
+
parser.add_argument("--log-dir", default=None)
|
| 33 |
+
parser.add_argument("--local-rank", type=int, default=None)
|
| 34 |
+
parser.add_argument("--world-size", type=int, default=None)
|
| 35 |
+
parser.add_argument("--use-amp", action="store_true", default=None)
|
| 36 |
+
parser.add_argument("--no-use-amp", dest="use_amp", action="store_false")
|
| 37 |
+
parser.add_argument("--dry-run", action="store_true")
|
| 38 |
+
parser.set_defaults(use_amp=None)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _add_model(parser):
|
| 42 |
+
parser.add_argument("--embed-dim", type=int, default=None)
|
| 43 |
+
parser.add_argument("--depth", type=int, default=None)
|
| 44 |
+
parser.add_argument("--predictor-depth", type=int, default=None)
|
| 45 |
+
parser.add_argument("--drop-path-rate", type=float, default=None)
|
| 46 |
+
parser.add_argument("--bimamba-type", default=None)
|
| 47 |
+
parser.add_argument("--if-bimamba", action="store_true", default=None)
|
| 48 |
+
parser.add_argument("--if-devide-out", action="store_true", default=None)
|
| 49 |
+
parser.add_argument("--no-if-devide-out", dest="if_devide_out", action="store_false")
|
| 50 |
+
parser.add_argument("--mixer-type", default=None)
|
| 51 |
+
parser.add_argument("--momentum", type=float, default=None)
|
| 52 |
+
parser.add_argument("--final-momentum", type=float, default=None)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def apply_common(cfg, args):
|
| 56 |
+
for key in ["train_list", "val_list", "test_list", "batch_size", "num_workers", "epochs", "lr", "weight_decay", "warmup_epochs", "grad_accumulation_steps", "seed", "local_rank", "world_size"]:
|
| 57 |
+
value = getattr(args, key, None)
|
| 58 |
+
if value is None:
|
| 59 |
+
continue
|
| 60 |
+
target = cfg.data if key in {"train_list", "val_list", "test_list", "batch_size", "num_workers"} else cfg.training
|
| 61 |
+
setattr(target, key, value)
|
| 62 |
+
if args.t_prime is not None:
|
| 63 |
+
cfg.data.T_prime = args.t_prime
|
| 64 |
+
if args.tau_seconds is not None:
|
| 65 |
+
cfg.data.tau_seconds = args.tau_seconds
|
| 66 |
+
if args.default_tr is not None:
|
| 67 |
+
cfg.data.default_tr = args.default_tr
|
| 68 |
+
if args.use_amp is not None:
|
| 69 |
+
cfg.training.use_amp = args.use_amp
|
| 70 |
+
if args.log_interval is not None:
|
| 71 |
+
cfg.logging.log_interval = args.log_interval
|
| 72 |
+
if args.checkpoint_dir is not None:
|
| 73 |
+
cfg.logging.checkpoint_dir = args.checkpoint_dir
|
| 74 |
+
if args.log_dir is not None:
|
| 75 |
+
cfg.logging.log_dir = args.log_dir
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def apply_model(cfg, args):
|
| 79 |
+
for key in ["embed_dim", "depth", "predictor_depth", "drop_path_rate", "bimamba_type", "if_bimamba", "if_devide_out", "mixer_type", "momentum", "final_momentum"]:
|
| 80 |
+
value = getattr(args, key, None)
|
| 81 |
+
if value is not None:
|
| 82 |
+
setattr(cfg.model, key, value)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def parse_args():
|
| 86 |
+
parser = argparse.ArgumentParser(prog="flexibrain")
|
| 87 |
+
sub = parser.add_subparsers(dest="command", required=True)
|
| 88 |
+
pretrain = sub.add_parser("pretrain")
|
| 89 |
+
_add_common(pretrain)
|
| 90 |
+
_add_model(pretrain)
|
| 91 |
+
pretrain.add_argument("--mask-ratio", type=float, default=None)
|
| 92 |
+
pretrain.add_argument("--grad-clip", type=float, default=None)
|
| 93 |
+
|
| 94 |
+
downstream = sub.add_parser("downstream")
|
| 95 |
+
_add_common(downstream)
|
| 96 |
+
_add_model(downstream)
|
| 97 |
+
downstream.add_argument("--csv", default=None)
|
| 98 |
+
downstream.add_argument("--id-column", default=None)
|
| 99 |
+
downstream.add_argument("--label-column", default=None)
|
| 100 |
+
downstream.add_argument("--label-mode", default=None)
|
| 101 |
+
downstream.add_argument("--path-id-mode", default=None)
|
| 102 |
+
downstream.add_argument("--pretrain-checkpoint", default=None)
|
| 103 |
+
downstream.add_argument("--from-scratch", action="store_true")
|
| 104 |
+
downstream.add_argument("--ignore-checkpoint-config", action="store_true")
|
| 105 |
+
downstream.add_argument("--num-classes", type=int, default=None)
|
| 106 |
+
downstream.add_argument("--head-type", choices=["transformer", "avgpool"], default=None)
|
| 107 |
+
downstream.add_argument("--freeze-backbone", action="store_true")
|
| 108 |
+
downstream.add_argument("--lr-backbone", type=float, default=None)
|
| 109 |
+
downstream.add_argument("--lr-head", type=float, default=None)
|
| 110 |
+
return parser.parse_args()
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def main():
|
| 114 |
+
args = parse_args()
|
| 115 |
+
cfg = load_config(args.config)
|
| 116 |
+
apply_common(cfg, args)
|
| 117 |
+
apply_model(cfg, args)
|
| 118 |
+
if args.command == "pretrain":
|
| 119 |
+
if args.mask_ratio is not None:
|
| 120 |
+
cfg.training.mask_ratio = args.mask_ratio
|
| 121 |
+
if args.grad_clip is not None:
|
| 122 |
+
cfg.training.grad_clip = args.grad_clip
|
| 123 |
+
if args.dry_run:
|
| 124 |
+
model = build_pretrain_model(cfg.model, torch.device("cpu"))
|
| 125 |
+
print(json.dumps({"config": asdict(cfg), "parameters": sum(p.numel() for p in model.parameters())}, indent=2))
|
| 126 |
+
return
|
| 127 |
+
Pretrainer(cfg).fit()
|
| 128 |
+
elif args.command == "downstream":
|
| 129 |
+
for key in ["csv", "id_column", "label_column", "label_mode", "path_id_mode"]:
|
| 130 |
+
value = getattr(args, key, None)
|
| 131 |
+
if value is not None:
|
| 132 |
+
setattr(cfg.data, key, value)
|
| 133 |
+
for key in ["num_classes", "head_type", "freeze_backbone"]:
|
| 134 |
+
value = getattr(args, key, None)
|
| 135 |
+
if value is not None:
|
| 136 |
+
setattr(cfg.model, key, value)
|
| 137 |
+
if args.lr_backbone is not None:
|
| 138 |
+
cfg.training.lr_backbone = args.lr_backbone
|
| 139 |
+
if args.lr_head is not None:
|
| 140 |
+
cfg.training.lr_head = args.lr_head
|
| 141 |
+
if args.pretrain_checkpoint is not None:
|
| 142 |
+
cfg.pretrain_checkpoint = args.pretrain_checkpoint
|
| 143 |
+
if args.from_scratch:
|
| 144 |
+
cfg.from_scratch = True
|
| 145 |
+
if args.ignore_checkpoint_config:
|
| 146 |
+
cfg.use_checkpoint_config = False
|
| 147 |
+
if args.dry_run:
|
| 148 |
+
model = build_downstream_model(cfg.model, torch.device("cpu"), checkpoint_path=cfg.pretrain_checkpoint, from_scratch=cfg.from_scratch, use_checkpoint_config=cfg.use_checkpoint_config)
|
| 149 |
+
print(json.dumps({"config": asdict(cfg), "parameters": sum(p.numel() for p in model.parameters())}, indent=2))
|
| 150 |
+
return
|
| 151 |
+
DownstreamTrainer(cfg).fit()
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
if __name__ == "__main__":
|
| 155 |
+
main()
|
flexibrain/config.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
import yaml
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class ModelConfig:
|
| 12 |
+
model_type: str = "mamba"
|
| 13 |
+
embed_dim: int = 512
|
| 14 |
+
depth: int = 24
|
| 15 |
+
predictor_depth: int = 2
|
| 16 |
+
drop_path_rate: float = 0.1
|
| 17 |
+
rms_norm: bool = False
|
| 18 |
+
fused_add_norm: bool = True
|
| 19 |
+
residual_in_fp32: bool = True
|
| 20 |
+
bimamba_type: str = "v2"
|
| 21 |
+
if_bimamba: bool = False
|
| 22 |
+
mixer_type: str = "mamba"
|
| 23 |
+
if_devide_out: bool = True
|
| 24 |
+
momentum: float = 0.996
|
| 25 |
+
final_momentum: float = 0.9999
|
| 26 |
+
norm_target: bool = True
|
| 27 |
+
num_heads: int = 8
|
| 28 |
+
mlp_ratio: float = 4.0
|
| 29 |
+
head_type: str = "transformer"
|
| 30 |
+
num_classes: int = 3
|
| 31 |
+
head_depth: int = 2
|
| 32 |
+
head_num_heads: int = 8
|
| 33 |
+
head_mlp_ratio: float = 4.0
|
| 34 |
+
head_proj_drop: float = 0.1
|
| 35 |
+
head_drop_path: float = 0.1
|
| 36 |
+
mlp_hidden: int = 512
|
| 37 |
+
mlp_depth: int = 4
|
| 38 |
+
mlp_dropout: float = 0.1
|
| 39 |
+
freeze_backbone: bool = False
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass
|
| 43 |
+
class DataConfig:
|
| 44 |
+
train_list: str = ""
|
| 45 |
+
val_list: str = ""
|
| 46 |
+
test_list: Optional[str] = None
|
| 47 |
+
csv: Optional[str] = None
|
| 48 |
+
id_column: str = "Subject"
|
| 49 |
+
label_column: str = "Group_idx"
|
| 50 |
+
label_mode: str = "multiclass"
|
| 51 |
+
path_id_mode: str = "auto"
|
| 52 |
+
normal_label: int = 2
|
| 53 |
+
batch_size: int = 8
|
| 54 |
+
num_workers: int = 8
|
| 55 |
+
memory_map: bool = True
|
| 56 |
+
T_prime: int = 30
|
| 57 |
+
tau_seconds: float = 6.0
|
| 58 |
+
default_tr: Optional[float] = None
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@dataclass
|
| 62 |
+
class TrainingConfig:
|
| 63 |
+
epochs: int = 30
|
| 64 |
+
lr: float = 5e-4
|
| 65 |
+
lr_backbone: Optional[float] = None
|
| 66 |
+
lr_head: Optional[float] = None
|
| 67 |
+
weight_decay: float = 0.05
|
| 68 |
+
warmup_epochs: int = 2
|
| 69 |
+
mask_ratio: float = 0.65
|
| 70 |
+
grad_clip: float = 1.0
|
| 71 |
+
grad_accumulation_steps: int = 1
|
| 72 |
+
seed: int = 42
|
| 73 |
+
use_amp: bool = False
|
| 74 |
+
local_rank: int = 0
|
| 75 |
+
world_size: int = 1
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@dataclass
|
| 79 |
+
class LoggingConfig:
|
| 80 |
+
log_interval: int = 20
|
| 81 |
+
checkpoint_dir: str = "./checkpoints"
|
| 82 |
+
log_dir: str = "./logs"
|
| 83 |
+
resume: Optional[str] = None
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@dataclass
|
| 87 |
+
class RunConfig:
|
| 88 |
+
model: ModelConfig = field(default_factory=ModelConfig)
|
| 89 |
+
data: DataConfig = field(default_factory=DataConfig)
|
| 90 |
+
training: TrainingConfig = field(default_factory=TrainingConfig)
|
| 91 |
+
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
| 92 |
+
pretrain_checkpoint: Optional[str] = None
|
| 93 |
+
from_scratch: bool = False
|
| 94 |
+
use_checkpoint_config: bool = True
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _update_dataclass(obj, values: dict):
|
| 98 |
+
for key, value in values.items():
|
| 99 |
+
if hasattr(obj, key):
|
| 100 |
+
setattr(obj, key, value)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def load_config(path: Optional[str]) -> RunConfig:
|
| 104 |
+
cfg = RunConfig()
|
| 105 |
+
if not path:
|
| 106 |
+
return cfg
|
| 107 |
+
data = yaml.safe_load(Path(path).read_text()) or {}
|
| 108 |
+
if "model" in data:
|
| 109 |
+
_update_dataclass(cfg.model, data["model"] or {})
|
| 110 |
+
if "data" in data:
|
| 111 |
+
_update_dataclass(cfg.data, data["data"] or {})
|
| 112 |
+
if "training" in data:
|
| 113 |
+
_update_dataclass(cfg.training, data["training"] or {})
|
| 114 |
+
if "logging" in data:
|
| 115 |
+
_update_dataclass(cfg.logging, data["logging"] or {})
|
| 116 |
+
for key in ["pretrain_checkpoint", "from_scratch", "use_checkpoint_config"]:
|
| 117 |
+
if key in data:
|
| 118 |
+
setattr(cfg, key, data[key])
|
| 119 |
+
return cfg
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def apply_checkpoint_config(model_cfg: ModelConfig, checkpoint_config: dict) -> None:
|
| 123 |
+
keys = [
|
| 124 |
+
"model_type", "embed_dim", "depth", "predictor_depth", "drop_path_rate",
|
| 125 |
+
"rms_norm", "fused_add_norm", "residual_in_fp32", "bimamba_type",
|
| 126 |
+
"if_bimamba", "mixer_type", "if_devide_out", "momentum", "norm_target",
|
| 127 |
+
"num_heads", "mlp_ratio",
|
| 128 |
+
]
|
| 129 |
+
for key in keys:
|
| 130 |
+
if key in checkpoint_config:
|
| 131 |
+
setattr(model_cfg, key, checkpoint_config[key])
|
flexibrain/data/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flexibrain.data.builders import build_downstream_dataloaders, build_pretrain_dataloaders
|
| 2 |
+
from flexibrain.data.nifti import NiftiTxtDataset
|
| 3 |
+
from flexibrain.data.classification import ClassificationDataset
|
| 4 |
+
|
| 5 |
+
__all__ = [build_downstream_dataloaders, build_pretrain_dataloaders, NiftiTxtDataset, ClassificationDataset]
|
flexibrain/data/builders.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Optional, Tuple
|
| 4 |
+
|
| 5 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 6 |
+
|
| 7 |
+
from flexibrain.config import DataConfig, TrainingConfig
|
| 8 |
+
from flexibrain.data.nifti import NiftiTxtDataset
|
| 9 |
+
from flexibrain.data.classification import ClassificationDataset, custom_collate_fn as downstream_collate
|
| 10 |
+
from flexibrain.data.collate import custom_collate_fn as pretrain_collate
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def build_pretrain_dataloaders(data: DataConfig, training: TrainingConfig, rank: int = 0, world_size: int = 1) -> Tuple[DataLoader, DataLoader]:
|
| 14 |
+
train_set = NiftiTxtDataset(data.train_list, return_torch=True, memory_map=data.memory_map, cache_meta=True, T_prime=data.T_prime, tau_seconds=data.tau_seconds, default_tr=data.default_tr)
|
| 15 |
+
val_set = NiftiTxtDataset(data.val_list, return_torch=True, memory_map=data.memory_map, cache_meta=True, T_prime=data.T_prime, tau_seconds=data.tau_seconds, default_tr=data.default_tr)
|
| 16 |
+
train_sampler = DistributedSampler(train_set, num_replicas=world_size, rank=rank, shuffle=True, seed=training.seed) if world_size > 1 else None
|
| 17 |
+
val_sampler = DistributedSampler(val_set, num_replicas=world_size, rank=rank, shuffle=False, seed=training.seed) if world_size > 1 else None
|
| 18 |
+
train_loader = DataLoader(train_set, batch_size=data.batch_size, sampler=train_sampler, shuffle=train_sampler is None, num_workers=data.num_workers, pin_memory=True, drop_last=True, collate_fn=pretrain_collate)
|
| 19 |
+
val_loader = DataLoader(val_set, batch_size=data.batch_size, sampler=val_sampler, shuffle=False, num_workers=data.num_workers, pin_memory=True, drop_last=False, collate_fn=pretrain_collate)
|
| 20 |
+
return train_loader, val_loader
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _classification_dataset(txt_file: Optional[str], data: DataConfig):
|
| 24 |
+
if not txt_file:
|
| 25 |
+
return None
|
| 26 |
+
if not data.csv:
|
| 27 |
+
raise ValueError("data.csv is required for downstream classification")
|
| 28 |
+
return ClassificationDataset(
|
| 29 |
+
txt_files=txt_file,
|
| 30 |
+
csv_path=data.csv,
|
| 31 |
+
id_column=data.id_column,
|
| 32 |
+
label_column=data.label_column,
|
| 33 |
+
label_mode=data.label_mode,
|
| 34 |
+
path_id_mode=data.path_id_mode,
|
| 35 |
+
normal_label=data.normal_label,
|
| 36 |
+
return_torch=True,
|
| 37 |
+
memory_map=data.memory_map,
|
| 38 |
+
cache_meta=True,
|
| 39 |
+
T_prime=data.T_prime,
|
| 40 |
+
tau_seconds=data.tau_seconds,
|
| 41 |
+
default_tr=data.default_tr,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def build_downstream_dataloaders(data: DataConfig, training: TrainingConfig, rank: int = 0, world_size: int = 1):
|
| 46 |
+
train_set = _classification_dataset(data.train_list, data)
|
| 47 |
+
val_set = _classification_dataset(data.val_list, data)
|
| 48 |
+
test_set = _classification_dataset(data.test_list, data)
|
| 49 |
+
train_sampler = DistributedSampler(train_set, num_replicas=world_size, rank=rank, shuffle=True, seed=training.seed) if world_size > 1 else None
|
| 50 |
+
val_sampler = DistributedSampler(val_set, num_replicas=world_size, rank=rank, shuffle=False, seed=training.seed) if world_size > 1 else None
|
| 51 |
+
test_sampler = DistributedSampler(test_set, num_replicas=world_size, rank=rank, shuffle=False, seed=training.seed) if world_size > 1 and test_set is not None else None
|
| 52 |
+
train_loader = DataLoader(train_set, batch_size=data.batch_size, sampler=train_sampler, shuffle=train_sampler is None, num_workers=data.num_workers, pin_memory=True, drop_last=True, collate_fn=downstream_collate)
|
| 53 |
+
val_loader = DataLoader(val_set, batch_size=data.batch_size, sampler=val_sampler, shuffle=False, num_workers=data.num_workers, pin_memory=True, drop_last=False, collate_fn=downstream_collate)
|
| 54 |
+
test_loader = None
|
| 55 |
+
if test_set is not None:
|
| 56 |
+
test_loader = DataLoader(test_set, batch_size=data.batch_size, sampler=test_sampler, shuffle=False, num_workers=data.num_workers, pin_memory=True, drop_last=False, collate_fn=downstream_collate)
|
| 57 |
+
return train_loader, val_loader, test_loader
|
flexibrain/data/classification.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from typing import Dict, Any, List, Optional, Tuple, Union
|
| 3 |
+
import torch
|
| 4 |
+
import re
|
| 5 |
+
import ast
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import pandas as pd
|
| 8 |
+
|
| 9 |
+
from flexibrain.data.nifti import NiftiTxtDataset, _read_list_files, _load_nifti, _space_time_units_to_mm_s
|
| 10 |
+
|
| 11 |
+
# class ClassificationDataset(NiftiTxtDataset):
|
| 12 |
+
# """
|
| 13 |
+
# Classification dataset that extends NiftiTxtDataset.
|
| 14 |
+
|
| 15 |
+
# Extracts binary labels from filenames:
|
| 16 |
+
# - Files containing 'control' -> label 0
|
| 17 |
+
# - Files containing 'patient' -> label 1
|
| 18 |
+
# """
|
| 19 |
+
|
| 20 |
+
# def __init__(self, *args, **kwargs):
|
| 21 |
+
# super().__init__(*args, **kwargs)
|
| 22 |
+
# self.labels = self._extract_labels()
|
| 23 |
+
|
| 24 |
+
# def _extract_labels(self) -> List[int]:
|
| 25 |
+
# """Extract binary labels from file paths."""
|
| 26 |
+
# labels = []
|
| 27 |
+
# for path in self.paths:
|
| 28 |
+
# path_str = str(path).lower()
|
| 29 |
+
# if 'cn' in path_str:
|
| 30 |
+
# labels.append(0)
|
| 31 |
+
# elif 'ad' in path_str:
|
| 32 |
+
# labels.append(1)
|
| 33 |
+
# else:
|
| 34 |
+
# raise ValueError(
|
| 35 |
+
# f"Cannot determine label for {path}. "
|
| 36 |
+
# f"Filename must contain 'control' or 'patient'."
|
| 37 |
+
# )
|
| 38 |
+
# return labels
|
| 39 |
+
|
| 40 |
+
# def __getitem__(self, idx: int) -> Dict:
|
| 41 |
+
# sample = super().__getitem__(idx)
|
| 42 |
+
# sample['label'] = self.labels[idx]
|
| 43 |
+
# return sample
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class ClassificationDataset(NiftiTxtDataset):
|
| 49 |
+
|
| 50 |
+
_seven_digits = re.compile(r'(\d{4,8})(?!\d)')
|
| 51 |
+
|
| 52 |
+
def __init__(
|
| 53 |
+
self,
|
| 54 |
+
*args,
|
| 55 |
+
csv_path: Union[str, Path],
|
| 56 |
+
id_column: str = 'Subject',
|
| 57 |
+
label_column: str = 'Group_idx',
|
| 58 |
+
label_mode: str = 'multiclass',
|
| 59 |
+
path_id_mode: str = 'auto',
|
| 60 |
+
normal_label: int = 2,
|
| 61 |
+
exclude_labels: Optional[List[int]] = None,
|
| 62 |
+
**kwargs
|
| 63 |
+
):
|
| 64 |
+
super().__init__(*args, **kwargs)
|
| 65 |
+
self.csv_path = Path(csv_path)
|
| 66 |
+
self.id_column = id_column
|
| 67 |
+
self.label_column = label_column
|
| 68 |
+
self.label_mode = str(label_mode)
|
| 69 |
+
self.path_id_mode = str(path_id_mode)
|
| 70 |
+
self.normal_label = int(normal_label)
|
| 71 |
+
self.exclude_labels = set(int(x) for x in (exclude_labels or []))
|
| 72 |
+
|
| 73 |
+
self._df = self._load_csv(self.csv_path, self.id_column, self.label_column)
|
| 74 |
+
self._id_to_label = self._build_id_to_label(self._df, self.id_column, self.label_column)
|
| 75 |
+
|
| 76 |
+
self.labels = self._extract_labels()
|
| 77 |
+
self.valid_indices = [i for i, label in enumerate(self.labels) if label is not None]
|
| 78 |
+
|
| 79 |
+
@staticmethod
|
| 80 |
+
def _normalize_id(x: Any) -> str:
|
| 81 |
+
s = str(x).strip()
|
| 82 |
+
if '_' in s:
|
| 83 |
+
return s.upper()
|
| 84 |
+
s = s.lstrip('0')
|
| 85 |
+
return s if s != '' else '0'
|
| 86 |
+
|
| 87 |
+
@classmethod
|
| 88 |
+
def _load_csv(cls, csv_path: Path, id_column: str, label_column: str) -> pd.DataFrame:
|
| 89 |
+
df = pd.read_csv(csv_path)
|
| 90 |
+
df = df.copy()
|
| 91 |
+
df['_norm_id'] = df[id_column].apply(cls._normalize_id)
|
| 92 |
+
return df
|
| 93 |
+
|
| 94 |
+
@staticmethod
|
| 95 |
+
def _parse_label(val: Any) -> Union[int, str, np.ndarray, None]:
|
| 96 |
+
|
| 97 |
+
if pd.isna(val):
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
if isinstance(val, (list, tuple, np.ndarray)):
|
| 101 |
+
return np.asarray(val, dtype=int)
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
return int(float(val))
|
| 105 |
+
except Exception:
|
| 106 |
+
pass
|
| 107 |
+
|
| 108 |
+
if isinstance(val, str):
|
| 109 |
+
s = val.strip()
|
| 110 |
+
try:
|
| 111 |
+
lit = ast.literal_eval(s)
|
| 112 |
+
if isinstance(lit, (list, tuple, np.ndarray)):
|
| 113 |
+
return np.asarray(lit, dtype=int)
|
| 114 |
+
if isinstance(lit, (int, float)):
|
| 115 |
+
return int(lit)
|
| 116 |
+
except Exception:
|
| 117 |
+
tokens = [t for t in re.split(r'[,\s]+', s) if t]
|
| 118 |
+
if all(t.isdigit() for t in tokens) and len(tokens) > 1:
|
| 119 |
+
return np.asarray([int(t) for t in tokens], dtype=int)
|
| 120 |
+
return s
|
| 121 |
+
|
| 122 |
+
raise ValueError(f"无法解析标签值: {val!r}")
|
| 123 |
+
|
| 124 |
+
@classmethod
|
| 125 |
+
def _build_id_to_label(cls, df: pd.DataFrame, id_column: str, label_column: str):
|
| 126 |
+
mapping = {}
|
| 127 |
+
for _, row in df.iterrows():
|
| 128 |
+
key = row['_norm_id']
|
| 129 |
+
lbl = cls._parse_label(row[label_column])
|
| 130 |
+
if lbl is None:
|
| 131 |
+
continue
|
| 132 |
+
if key in mapping:
|
| 133 |
+
a, b = np.asarray(mapping[key]), np.asarray(lbl)
|
| 134 |
+
else:
|
| 135 |
+
mapping[key] = lbl
|
| 136 |
+
return mapping
|
| 137 |
+
|
| 138 |
+
def _extract_path_id(self, name: str) -> str:
|
| 139 |
+
mode = self.path_id_mode.lower()
|
| 140 |
+
if mode == 'auto':
|
| 141 |
+
if 'ADNI_' in name or re.search(r'\d{3}_S_\d{4}', name) or re.search(r'sub-\d+', name, flags=re.IGNORECASE):
|
| 142 |
+
mode = 'adni'
|
| 143 |
+
elif 'ADHD_' in name:
|
| 144 |
+
mode = 'adhd'
|
| 145 |
+
else:
|
| 146 |
+
mode = 'digits'
|
| 147 |
+
|
| 148 |
+
if mode == 'adni':
|
| 149 |
+
match = re.search(r'(\d{3}_S_\d{4})', name)
|
| 150 |
+
if match:
|
| 151 |
+
return match.group(1).upper()
|
| 152 |
+
match = re.search(r'sub-(\d+)', name, flags=re.IGNORECASE)
|
| 153 |
+
if match:
|
| 154 |
+
return self._normalize_id(match.group(1))
|
| 155 |
+
raise ValueError(f"Cannot extract ADNI subject id from filename: {name}")
|
| 156 |
+
|
| 157 |
+
if mode == 'adhd':
|
| 158 |
+
match = re.search(r'ADHD_[^_]+_(\d+)_', name)
|
| 159 |
+
if not match:
|
| 160 |
+
raise ValueError(f"Cannot extract ADHD subject id from filename: {name}")
|
| 161 |
+
return self._normalize_id(match.group(1))
|
| 162 |
+
|
| 163 |
+
matches = self._seven_digits.findall(name)
|
| 164 |
+
if not matches:
|
| 165 |
+
raise ValueError(f"Cannot extract subject id from filename: {name}")
|
| 166 |
+
return self._normalize_id(matches[-1])
|
| 167 |
+
|
| 168 |
+
def _extract_labels(self) -> List[Union[int, str, np.ndarray]]:
|
| 169 |
+
|
| 170 |
+
labels: List[Union[int, str, np.ndarray]] = []
|
| 171 |
+
for path in self.paths:
|
| 172 |
+
name = Path(path).name
|
| 173 |
+
norm_id = self._extract_path_id(name)
|
| 174 |
+
|
| 175 |
+
label = self._id_to_label.get(norm_id)
|
| 176 |
+
if label is None:
|
| 177 |
+
labels.append(None)
|
| 178 |
+
continue
|
| 179 |
+
label = self._convert_label(label)
|
| 180 |
+
|
| 181 |
+
labels.append(label)
|
| 182 |
+
return labels
|
| 183 |
+
|
| 184 |
+
def _convert_label(self, label: Union[int, str, np.ndarray]) -> Union[int, np.ndarray]:
|
| 185 |
+
if self.label_mode in ('multiclass', 'raw', None):
|
| 186 |
+
if not isinstance(label, np.ndarray) and int(label) in self.exclude_labels:
|
| 187 |
+
return None
|
| 188 |
+
return label
|
| 189 |
+
if self.label_mode == 'binary_control_vs_disease':
|
| 190 |
+
if isinstance(label, np.ndarray):
|
| 191 |
+
return np.asarray([0 if int(x) == self.normal_label else 1 for x in label], dtype=int)
|
| 192 |
+
return 0 if int(label) == self.normal_label else 1
|
| 193 |
+
if self.label_mode == 'binary_pd_vs_prodromal':
|
| 194 |
+
if isinstance(label, np.ndarray):
|
| 195 |
+
converted = []
|
| 196 |
+
for x in label:
|
| 197 |
+
xi = int(x)
|
| 198 |
+
if xi in self.exclude_labels:
|
| 199 |
+
converted.append(-1)
|
| 200 |
+
else:
|
| 201 |
+
converted.append(1 if xi == 1 else 0)
|
| 202 |
+
return np.asarray(converted, dtype=int)
|
| 203 |
+
label_i = int(label)
|
| 204 |
+
if label_i in self.exclude_labels:
|
| 205 |
+
return None
|
| 206 |
+
# PPMI Group_idx: 0=Prodromal, 1=PD, 2=Control.
|
| 207 |
+
return 1 if label_i == 1 else 0
|
| 208 |
+
if self.label_mode == 'binary_gender':
|
| 209 |
+
def to_gender(v: Any) -> int:
|
| 210 |
+
s = str(v).strip().upper()
|
| 211 |
+
if s in {'M', 'MALE', '1'}:
|
| 212 |
+
return 1
|
| 213 |
+
if s in {'F', 'FEMALE', '0'}:
|
| 214 |
+
return 0
|
| 215 |
+
raise ValueError(f"Unknown gender label: {v!r}")
|
| 216 |
+
|
| 217 |
+
if isinstance(label, np.ndarray):
|
| 218 |
+
return np.asarray([to_gender(x) for x in label], dtype=int)
|
| 219 |
+
return to_gender(label)
|
| 220 |
+
if self.label_mode == 'binary_gender_abide':
|
| 221 |
+
def to_gender_abide(v: Any) -> int:
|
| 222 |
+
s = str(v).strip().upper()
|
| 223 |
+
if s in {'0', 'M', 'MALE'}:
|
| 224 |
+
return 1
|
| 225 |
+
if s in {'1', 'F', 'FEMALE'}:
|
| 226 |
+
return 0
|
| 227 |
+
raise ValueError(f"Unknown ABIDE gender label: {v!r}")
|
| 228 |
+
|
| 229 |
+
if isinstance(label, np.ndarray):
|
| 230 |
+
return np.asarray([to_gender_abide(x) for x in label], dtype=int)
|
| 231 |
+
return to_gender_abide(label)
|
| 232 |
+
raise ValueError(f"Unknown label_mode: {self.label_mode}")
|
| 233 |
+
|
| 234 |
+
def __len__(self) -> int:
|
| 235 |
+
return len(self.valid_indices)
|
| 236 |
+
|
| 237 |
+
def __getitem__(self, idx: int) -> Dict:
|
| 238 |
+
raw_idx = self.valid_indices[idx]
|
| 239 |
+
sample = super().__getitem__(raw_idx)
|
| 240 |
+
sample['label'] = self.labels[raw_idx]
|
| 241 |
+
return sample
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def custom_collate_fn(batch: List[Dict[str, Any]]) -> Dict[str, Any]:
|
| 245 |
+
"""
|
| 246 |
+
Custom collate function to handle:
|
| 247 |
+
1. nibabel headers and other non-tensor objects
|
| 248 |
+
2. Variable-length time dimensions (due to different TR values)
|
| 249 |
+
|
| 250 |
+
For variable-length data, we pad to the maximum length in the batch.
|
| 251 |
+
"""
|
| 252 |
+
# Separate tensor/array fields from non-collatable fields
|
| 253 |
+
tensor_fields = ['data', 'affine']
|
| 254 |
+
scalar_fields = ['tr', 'subject_idx', 'T_selected', 'T_prime', 'tau_seconds', 'label']
|
| 255 |
+
tuple_fields = ['voxel']
|
| 256 |
+
object_fields = ['header', 'path'] # These won't be collated
|
| 257 |
+
|
| 258 |
+
collated = {}
|
| 259 |
+
|
| 260 |
+
# Handle tensor/array fields with padding for variable-length data
|
| 261 |
+
for field in tensor_fields:
|
| 262 |
+
if field in batch[0]:
|
| 263 |
+
values = [item[field] for item in batch]
|
| 264 |
+
|
| 265 |
+
if field == 'data':
|
| 266 |
+
# Data has variable time dimension due to different TR values
|
| 267 |
+
# Pad all to the maximum time length
|
| 268 |
+
max_t = max(v.shape[-1] if len(v.shape) >= 4 else 1 for v in values)
|
| 269 |
+
|
| 270 |
+
padded_values = []
|
| 271 |
+
for v in values:
|
| 272 |
+
if len(v.shape) >= 4 and v.shape[-1] < max_t:
|
| 273 |
+
# Pad in time dimension (last dimension)
|
| 274 |
+
pad_amount = max_t - v.shape[-1]
|
| 275 |
+
if isinstance(v, torch.Tensor):
|
| 276 |
+
v = torch.nn.functional.pad(v, (0, pad_amount), mode='constant', value=0)
|
| 277 |
+
else:
|
| 278 |
+
v = np.pad(v, ((0, 0), (0, 0), (0, 0), (0, pad_amount)), mode='constant', constant_values=0)
|
| 279 |
+
padded_values.append(v)
|
| 280 |
+
|
| 281 |
+
# Convert to tensor and stack
|
| 282 |
+
if isinstance(padded_values[0], torch.Tensor):
|
| 283 |
+
collated[field] = torch.stack(padded_values)
|
| 284 |
+
else:
|
| 285 |
+
collated[field] = torch.from_numpy(np.stack(padded_values))
|
| 286 |
+
else:
|
| 287 |
+
# Affine matrices should all be the same size (4x4)
|
| 288 |
+
if isinstance(values[0], torch.Tensor):
|
| 289 |
+
collated[field] = torch.stack(values)
|
| 290 |
+
else:
|
| 291 |
+
collated[field] = torch.from_numpy(np.stack(values))
|
| 292 |
+
|
| 293 |
+
# Handle scalar fields
|
| 294 |
+
for field in scalar_fields:
|
| 295 |
+
if field in batch[0]:
|
| 296 |
+
values = [item[field] for item in batch]
|
| 297 |
+
if isinstance(values[0], (int, float)):
|
| 298 |
+
collated[field] = torch.tensor(values)
|
| 299 |
+
else:
|
| 300 |
+
collated[field] = values
|
| 301 |
+
|
| 302 |
+
# Handle tuple fields (like voxel sizes)
|
| 303 |
+
for field in tuple_fields:
|
| 304 |
+
if field in batch[0]:
|
| 305 |
+
collated[field] = [item[field] for item in batch]
|
| 306 |
+
|
| 307 |
+
# Handle object fields (keep as lists)
|
| 308 |
+
for field in object_fields:
|
| 309 |
+
if field in batch[0]:
|
| 310 |
+
collated[field] = [item[field] for item in batch]
|
| 311 |
+
|
| 312 |
+
return collated
|
| 313 |
+
|
| 314 |
+
def prepare_batch_data(batch: Dict, device: torch.device) -> Tuple[torch.Tensor, Dict, np.ndarray, torch.Tensor, Optional[torch.Tensor]]:
|
| 315 |
+
"""Prepare batch data for model forward pass.
|
| 316 |
+
|
| 317 |
+
Returns:
|
| 318 |
+
x: Input tensor (B, 96, 96, 96, T_max)
|
| 319 |
+
meta: Dict {batch_index: {"voxel": (vx, vy, vz), "tr": float}}
|
| 320 |
+
orig_Ts: Array of original time steps
|
| 321 |
+
labels: Classification labels
|
| 322 |
+
affines: Affine matrices or None
|
| 323 |
+
"""
|
| 324 |
+
# Data is already padded and stacked by custom_collate_fn
|
| 325 |
+
x = batch['data'].to(device, dtype=torch.float32)
|
| 326 |
+
|
| 327 |
+
# Build meta dict - use batch index as key
|
| 328 |
+
batch_size = x.shape[0]
|
| 329 |
+
voxels = batch['voxel']
|
| 330 |
+
trs = batch['tr']
|
| 331 |
+
|
| 332 |
+
# Convert trs to numpy if needed
|
| 333 |
+
if isinstance(trs, torch.Tensor):
|
| 334 |
+
trs = trs.cpu().numpy()
|
| 335 |
+
elif isinstance(trs, list):
|
| 336 |
+
trs = np.array(trs)
|
| 337 |
+
|
| 338 |
+
meta = {}
|
| 339 |
+
for i in range(batch_size):
|
| 340 |
+
# Get voxel
|
| 341 |
+
if isinstance(voxels, list):
|
| 342 |
+
voxel = voxels[i] if isinstance(voxels[i], tuple) else tuple(voxels[i])
|
| 343 |
+
else:
|
| 344 |
+
print("voxels is empty")
|
| 345 |
+
voxel = (2.0, 2.0, 2.0) # Default voxel size
|
| 346 |
+
|
| 347 |
+
# Get TR
|
| 348 |
+
if isinstance(trs, np.ndarray):
|
| 349 |
+
tr = float(trs[i])
|
| 350 |
+
elif isinstance(trs, list):
|
| 351 |
+
tr = float(trs[i])
|
| 352 |
+
else:
|
| 353 |
+
print("trs is empty")
|
| 354 |
+
tr = 2.0 # Default TR
|
| 355 |
+
|
| 356 |
+
meta[i] = {"voxel": voxel, "tr": tr}
|
| 357 |
+
|
| 358 |
+
# Get original time steps (T_selected from dataset)
|
| 359 |
+
orig_Ts = batch.get('T_selected', x.shape[-1])
|
| 360 |
+
if isinstance(orig_Ts, torch.Tensor):
|
| 361 |
+
orig_Ts = orig_Ts.cpu().numpy()
|
| 362 |
+
elif isinstance(orig_Ts, list):
|
| 363 |
+
orig_Ts = np.array(orig_Ts)
|
| 364 |
+
|
| 365 |
+
# Handle labels
|
| 366 |
+
labels = batch['label']
|
| 367 |
+
if isinstance(labels, torch.Tensor):
|
| 368 |
+
labels = labels.to(device, dtype=torch.long)
|
| 369 |
+
else:
|
| 370 |
+
labels = torch.tensor(labels, dtype=torch.long, device=device)
|
| 371 |
+
|
| 372 |
+
# Get affines if available
|
| 373 |
+
affines = batch['affine'].to(device, dtype=torch.float32) if 'affine' in batch else None
|
| 374 |
+
|
| 375 |
+
return x, meta, orig_Ts, labels, affines
|
flexibrain/data/collate.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, List, Any, Tuple, Optional
|
| 2 |
+
import torch
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def custom_collate_fn(batch: List[Dict[str, Any]]) -> Dict[str, Any]:
|
| 7 |
+
"""
|
| 8 |
+
Custom collate function to handle:
|
| 9 |
+
1. nibabel headers and other non-tensor objects
|
| 10 |
+
2. Variable-length time dimensions (due to different TR values)
|
| 11 |
+
|
| 12 |
+
For variable-length data, we pad to the maximum length in the batch.
|
| 13 |
+
"""
|
| 14 |
+
# Separate tensor/array fields from non-collatable fields
|
| 15 |
+
tensor_fields = ['data', 'affine']
|
| 16 |
+
scalar_fields = ['tr', 'subject_idx', 'T_selected', 'T_prime', 'tau_seconds']
|
| 17 |
+
tuple_fields = ['voxel']
|
| 18 |
+
object_fields = ['header', 'path'] # These won't be collated
|
| 19 |
+
|
| 20 |
+
collated = {}
|
| 21 |
+
|
| 22 |
+
# Handle tensor/array fields with padding for variable-length data
|
| 23 |
+
for field in tensor_fields:
|
| 24 |
+
if field in batch[0]:
|
| 25 |
+
values = [item[field] for item in batch]
|
| 26 |
+
|
| 27 |
+
if field == 'data':
|
| 28 |
+
# Data has variable time dimension due to different TR values
|
| 29 |
+
# Pad all to the maximum time length
|
| 30 |
+
max_t = max(v.shape[-1] if len(v.shape) >= 4 else 1 for v in values)
|
| 31 |
+
|
| 32 |
+
padded_values = []
|
| 33 |
+
for v in values:
|
| 34 |
+
if len(v.shape) >= 4 and v.shape[-1] < max_t:
|
| 35 |
+
# Pad in time dimension (last dimension)
|
| 36 |
+
pad_amount = max_t - v.shape[-1]
|
| 37 |
+
if isinstance(v, torch.Tensor):
|
| 38 |
+
v = torch.nn.functional.pad(v, (0, pad_amount), mode='constant', value=0)
|
| 39 |
+
else:
|
| 40 |
+
v = np.pad(v, ((0, 0), (0, 0), (0, 0), (0, pad_amount)), mode='constant', value=0)
|
| 41 |
+
padded_values.append(v)
|
| 42 |
+
|
| 43 |
+
# Convert to tensor and stack
|
| 44 |
+
if isinstance(padded_values[0], torch.Tensor):
|
| 45 |
+
collated[field] = torch.stack(padded_values)
|
| 46 |
+
else:
|
| 47 |
+
collated[field] = torch.from_numpy(np.stack(padded_values))
|
| 48 |
+
else:
|
| 49 |
+
# Affine matrices should all be the same size (4x4)
|
| 50 |
+
if isinstance(values[0], torch.Tensor):
|
| 51 |
+
collated[field] = torch.stack(values)
|
| 52 |
+
else:
|
| 53 |
+
collated[field] = torch.from_numpy(np.stack(values))
|
| 54 |
+
|
| 55 |
+
# Handle scalar fields
|
| 56 |
+
for field in scalar_fields:
|
| 57 |
+
if field in batch[0]:
|
| 58 |
+
values = [item[field] for item in batch]
|
| 59 |
+
if isinstance(values[0], (int, float)):
|
| 60 |
+
collated[field] = torch.tensor(values)
|
| 61 |
+
else:
|
| 62 |
+
collated[field] = values
|
| 63 |
+
|
| 64 |
+
# Handle tuple fields (like voxel sizes)
|
| 65 |
+
for field in tuple_fields:
|
| 66 |
+
if field in batch[0]:
|
| 67 |
+
collated[field] = [item[field] for item in batch]
|
| 68 |
+
|
| 69 |
+
# Handle object fields (keep as lists)
|
| 70 |
+
for field in object_fields:
|
| 71 |
+
if field in batch[0]:
|
| 72 |
+
collated[field] = [item[field] for item in batch]
|
| 73 |
+
|
| 74 |
+
return collated
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def prepare_batch_data(batch: Dict, device: torch.device) -> Tuple[torch.Tensor, Dict, np.ndarray, Optional[torch.Tensor]]:
|
| 78 |
+
"""
|
| 79 |
+
Prepare batch data for model forward pass.
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
x: Input tensor (B, 96, 96, 96, T_max)
|
| 83 |
+
meta: Dict {subject_idx: {"voxel": (vx, vy, vz), "tr": float}}
|
| 84 |
+
orig_Ts: Array of original time steps
|
| 85 |
+
affines: Affine matrices or None
|
| 86 |
+
"""
|
| 87 |
+
# Move data to device
|
| 88 |
+
x = batch['data'].to(device, dtype=torch.float32)
|
| 89 |
+
|
| 90 |
+
# Build meta dict: {batch_index: {"voxel": (vx, vy, vz), "tr": float}}
|
| 91 |
+
subject_idxs = batch['subject_idx'].cpu().numpy()
|
| 92 |
+
voxels = batch['voxel'] # List of tuples or tensor
|
| 93 |
+
trs = batch['tr'].cpu().numpy() if isinstance(batch['tr'], torch.Tensor) else batch['tr']
|
| 94 |
+
|
| 95 |
+
meta = {}
|
| 96 |
+
for i, subject_idx in enumerate(subject_idxs):
|
| 97 |
+
# Handle voxel format
|
| 98 |
+
if isinstance(voxels, (list, tuple)):
|
| 99 |
+
voxel = voxels[i]
|
| 100 |
+
else:
|
| 101 |
+
voxel = tuple(voxels[i].cpu().numpy()) if isinstance(voxels[i], torch.Tensor) else voxels[i]
|
| 102 |
+
|
| 103 |
+
tr = float(trs[i])
|
| 104 |
+
# Use batch index (i) as key, not subject_idx
|
| 105 |
+
meta[i] = {"voxel": voxel, "tr": tr}
|
| 106 |
+
|
| 107 |
+
# Get original time steps (number of frames, not TR)
|
| 108 |
+
# T_selected is the actual number of time frames selected by the dataset
|
| 109 |
+
# Do NOT use 'tr' (time resolution in seconds) as it will cause incorrect T_pad calculation
|
| 110 |
+
if 'T_selected' in batch:
|
| 111 |
+
orig_Ts = batch['T_selected'].cpu().numpy() if isinstance(batch['T_selected'], torch.Tensor) else batch['T_selected']
|
| 112 |
+
else:
|
| 113 |
+
# Fallback: use actual data time dimension if T_selected is not available
|
| 114 |
+
orig_Ts = np.array([x.shape[-1] for x in batch['data']])
|
| 115 |
+
|
| 116 |
+
# Get affines if available
|
| 117 |
+
affines = batch['affine'].to(device, dtype=torch.float32) if 'affine' in batch else None
|
| 118 |
+
|
| 119 |
+
return x, meta, orig_Ts, affines
|
flexibrain/data/nifti.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import os
|
| 3 |
+
import glob
|
| 4 |
+
import random
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Dict, List, Optional, Sequence, Tuple, Union
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import nibabel as nib
|
| 10 |
+
import torch
|
| 11 |
+
from torch.utils.data import Dataset
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class fMRIDataset(Dataset):
|
| 15 |
+
def __init__(self,
|
| 16 |
+
data_root, datasets, split_suffixes, crop_length=40, downstream=False):
|
| 17 |
+
|
| 18 |
+
self.file_paths = []
|
| 19 |
+
self.crop_length = crop_length
|
| 20 |
+
self.downstream = downstream
|
| 21 |
+
|
| 22 |
+
for dataset_name in datasets:
|
| 23 |
+
for suffix in split_suffixes:
|
| 24 |
+
folder_name = f"{dataset_name}_{suffix}"
|
| 25 |
+
folder_path = os.path.join(data_root, folder_name)
|
| 26 |
+
if not os.path.exists(folder_path):
|
| 27 |
+
print(f"Warning: Folder not found: {folder_path}")
|
| 28 |
+
continue
|
| 29 |
+
|
| 30 |
+
for root, dirs, files in os.walk(folder_path):
|
| 31 |
+
npz_files = glob.glob(os.path.join(root, "*.npz"))
|
| 32 |
+
if len(npz_files) > 1:
|
| 33 |
+
sample_size = max(1, int(len(npz_files)))
|
| 34 |
+
npz_files = random.sample(npz_files, sample_size)
|
| 35 |
+
self.file_paths.extend(npz_files)
|
| 36 |
+
|
| 37 |
+
print(f"Dataset loaded. Total files found: {len(self.file_paths)}")
|
| 38 |
+
|
| 39 |
+
def __len__(self):
|
| 40 |
+
return len(self.file_paths)
|
| 41 |
+
|
| 42 |
+
def __getitem__(self, idx):
|
| 43 |
+
file_path = self.file_paths[idx]
|
| 44 |
+
try:
|
| 45 |
+
with np.load(file_path) as data_file:
|
| 46 |
+
key = list(data_file.keys())[0]
|
| 47 |
+
fmri_data = data_file[key]
|
| 48 |
+
fmri_data = fmri_data.astype(np.float32)
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"Error loading file {file_path}: {e}")
|
| 51 |
+
return None
|
| 52 |
+
|
| 53 |
+
total_time_frames = fmri_data.shape[-1]
|
| 54 |
+
if total_time_frames > self.crop_length:
|
| 55 |
+
start_idx = np.random.randint(0, total_time_frames - self.crop_length + 1)
|
| 56 |
+
end_idx = start_idx + self.crop_length
|
| 57 |
+
cropped_data = fmri_data[..., start_idx:end_idx]
|
| 58 |
+
else:
|
| 59 |
+
cropped_data = fmri_data[..., :self.crop_length]
|
| 60 |
+
|
| 61 |
+
data_tensor = torch.from_numpy(cropped_data)
|
| 62 |
+
data_tensor = data_tensor.permute(3, 0, 1, 2)
|
| 63 |
+
|
| 64 |
+
return data_tensor
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _read_list_files(txt_files: Union[str, Path, Sequence[Union[str, Path]]]) -> List[Path]:
|
| 68 |
+
"""Read one or many .txt files and collect absolute paths listed in them.
|
| 69 |
+
|
| 70 |
+
Each line should contain a path to a .nii or .nii.gz file. Empty lines and lines
|
| 71 |
+
starting with '#' are ignored. Paths are expanded and normalized to absolute Paths.
|
| 72 |
+
"""
|
| 73 |
+
if isinstance(txt_files, (str, Path)):
|
| 74 |
+
txt_files = [txt_files]
|
| 75 |
+
paths: List[Path] = []
|
| 76 |
+
for f in txt_files: # type: ignore[assignment]
|
| 77 |
+
f = Path(f)
|
| 78 |
+
if not f.exists():
|
| 79 |
+
raise FileNotFoundError(f"List file not found: {f}")
|
| 80 |
+
for line in f.read_text().splitlines():
|
| 81 |
+
line = line.strip()
|
| 82 |
+
if not line or line.startswith("#"):
|
| 83 |
+
continue
|
| 84 |
+
p = Path(os.path.expanduser(line)).resolve()
|
| 85 |
+
# allow relative paths inside list files (relative to the list file dir)
|
| 86 |
+
if not p.exists():
|
| 87 |
+
p = (f.parent / line).resolve()
|
| 88 |
+
if not p.exists():
|
| 89 |
+
raise FileNotFoundError(f"Path from list file does not exist: {line} (resolved: {p})")
|
| 90 |
+
if p.suffix not in {".nii", ".gz"} and not str(p).endswith(".nii.gz"):
|
| 91 |
+
raise ValueError(f"Not a NIfTI file: {p}")
|
| 92 |
+
paths.append(p)
|
| 93 |
+
# deduplicate while preserving order
|
| 94 |
+
seen = set()
|
| 95 |
+
deduped = []
|
| 96 |
+
for p in paths:
|
| 97 |
+
if p not in seen:
|
| 98 |
+
deduped.append(p)
|
| 99 |
+
seen.add(p)
|
| 100 |
+
return deduped
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _space_time_units_to_mm_s(header: nib.nifti1.Nifti1Header) -> Tuple[Tuple[float, float, float], float]:
|
| 104 |
+
"""Return (vx, vy, vz) in millimeters and TR in seconds from a NIfTI header.
|
| 105 |
+
|
| 106 |
+
Uses header.get_zooms() and header.get_xyzt_units(). Safely handles cases with
|
| 107 |
+
missing time dimension or unusual units.
|
| 108 |
+
"""
|
| 109 |
+
zooms = header.get_zooms()
|
| 110 |
+
# space-time units, e.g. ("mm", "sec")
|
| 111 |
+
space_u, time_u = header.get_xyzt_units()
|
| 112 |
+
|
| 113 |
+
# Spatial voxel sizes
|
| 114 |
+
vx, vy, vz = (zooms + (1.0, 1.0, 1.0, 1.0))[:3]
|
| 115 |
+
# Convert to mm if needed
|
| 116 |
+
if space_u == "m":
|
| 117 |
+
vx, vy, vz = vx * 1000.0, vy * 1000.0, vz * 1000.0
|
| 118 |
+
elif space_u in ("mm", None, "unknown"):
|
| 119 |
+
pass
|
| 120 |
+
else:
|
| 121 |
+
# Fallback: assume values already in mm
|
| 122 |
+
pass
|
| 123 |
+
|
| 124 |
+
# Temporal resolution (TR)
|
| 125 |
+
tr = 0.0
|
| 126 |
+
if len(zooms) >= 4:
|
| 127 |
+
tr = float(zooms[3])
|
| 128 |
+
if time_u == "msec":
|
| 129 |
+
tr = tr / 1000.0
|
| 130 |
+
elif time_u in ("usec", "microsec"):
|
| 131 |
+
tr = tr / 1e6
|
| 132 |
+
elif time_u in ("sec", None, "unknown"):
|
| 133 |
+
pass
|
| 134 |
+
else:
|
| 135 |
+
# Unknown -> leave as-is
|
| 136 |
+
pass
|
| 137 |
+
return (float(vx), float(vy), float(vz)), float(tr)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _load_nifti(path: Union[str, Path], mmap: bool = True) -> Tuple[np.ndarray, np.ndarray, nib.nifti1.Nifti1Header]:
|
| 141 |
+
try:
|
| 142 |
+
img = nib.load(str(path), mmap=mmap)
|
| 143 |
+
data = img.get_fdata(dtype=np.float32)
|
| 144 |
+
affine = img.affine.copy()
|
| 145 |
+
header = img.header.copy()
|
| 146 |
+
return data, affine, header
|
| 147 |
+
except Exception as e:
|
| 148 |
+
# Return None to signal invalid file
|
| 149 |
+
return None, None, None
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class NiftiTxtDataset(Dataset):
|
| 153 |
+
"""Dataset that loads NIfTI volumes listed in one or more .txt files.
|
| 154 |
+
|
| 155 |
+
Each item returns a dict with:
|
| 156 |
+
- 'data': np.ndarray (from get_fdata())
|
| 157 |
+
- 'affine': np.ndarray (4x4)
|
| 158 |
+
- 'header': nibabel header
|
| 159 |
+
- 'voxel': (vx, vy, vz) in millimeters
|
| 160 |
+
- 'tr': float, seconds (0.0 if not present)
|
| 161 |
+
- 'path': pathlib.Path to the NIfTI file
|
| 162 |
+
- 'subject_idx': integer index inside this dataset
|
| 163 |
+
- 'T_selected': int, number of time frames selected based on T_prime and tau_seconds
|
| 164 |
+
|
| 165 |
+
Parameters
|
| 166 |
+
----------
|
| 167 |
+
txt_files: str | Path | Sequence[str|Path]
|
| 168 |
+
One or more text files containing absolute (or relative) paths to NIfTI files.
|
| 169 |
+
transform: Optional[callable]
|
| 170 |
+
Optional transform applied to the sample dict (after loading).
|
| 171 |
+
return_torch: bool
|
| 172 |
+
If True, converts 'data' and 'affine' to torch tensors.
|
| 173 |
+
memory_map: bool
|
| 174 |
+
If True, enables nibabel's memory mapping. Disable to force full load into RAM.
|
| 175 |
+
cache_meta: bool
|
| 176 |
+
If True, caches voxel/TR in memory to avoid recomputing for repeated access.
|
| 177 |
+
T_prime: Optional[int]
|
| 178 |
+
Target number of time patches after TAPE (Time-to-space patch embedding).
|
| 179 |
+
If provided, dataset will automatically select appropriate time frames to ensure
|
| 180 |
+
all samples have the same T_prime after TAPE processing.
|
| 181 |
+
Formula: T_selected = T_prime * tau_seconds / TR
|
| 182 |
+
tau_seconds: float
|
| 183 |
+
Time window in seconds for TAPE kernel (default: 6.0).
|
| 184 |
+
Used to calculate T_selected when T_prime is specified.
|
| 185 |
+
"""
|
| 186 |
+
|
| 187 |
+
def __init__(
|
| 188 |
+
self,
|
| 189 |
+
txt_files: Union[str, Path, Sequence[Union[str, Path]]],
|
| 190 |
+
transform: Optional[callable] = None,
|
| 191 |
+
return_torch: bool = False,
|
| 192 |
+
memory_map: bool = True,
|
| 193 |
+
cache_meta: bool = True,
|
| 194 |
+
T_prime: Optional[int] = None,
|
| 195 |
+
tau_seconds: float = 6.0,
|
| 196 |
+
default_tr: Optional[float] = None,
|
| 197 |
+
) -> None:
|
| 198 |
+
super().__init__()
|
| 199 |
+
self.paths: List[Path] = _read_list_files(txt_files)
|
| 200 |
+
if len(self.paths) == 0:
|
| 201 |
+
raise ValueError("No NIfTI paths found in the provided list files.")
|
| 202 |
+
self.transform = transform
|
| 203 |
+
self.return_torch = bool(return_torch)
|
| 204 |
+
self.memory_map = bool(memory_map)
|
| 205 |
+
self.cache_meta = bool(cache_meta)
|
| 206 |
+
self.T_prime = T_prime
|
| 207 |
+
self.tau_seconds = float(tau_seconds)
|
| 208 |
+
self.default_tr = float(default_tr) if default_tr is not None else None
|
| 209 |
+
if self.default_tr is not None and self.default_tr <= 0:
|
| 210 |
+
raise ValueError("default_tr must be positive when provided")
|
| 211 |
+
self._meta_cache: Dict[int, Tuple[Tuple[float, float, float], float]] = {}
|
| 212 |
+
|
| 213 |
+
def __len__(self) -> int:
|
| 214 |
+
return len(self.paths)
|
| 215 |
+
|
| 216 |
+
def _get_meta(self, idx: int, header: Optional[nib.nifti1.Nifti1Header] = None) -> Tuple[Tuple[float, float, float], float]:
|
| 217 |
+
if self.cache_meta and idx in self._meta_cache:
|
| 218 |
+
return self._meta_cache[idx]
|
| 219 |
+
if header is None:
|
| 220 |
+
_, _, header = _load_nifti(self.paths[idx], mmap=self.memory_map)
|
| 221 |
+
voxel, tr = _space_time_units_to_mm_s(header)
|
| 222 |
+
voxel = tuple(float(v) for v in voxel)
|
| 223 |
+
if any((not np.isfinite(v)) or v <= 0 for v in voxel):
|
| 224 |
+
raise ValueError(f"Invalid voxel spacing for {self.paths[idx]}: {voxel}")
|
| 225 |
+
tr = float(tr)
|
| 226 |
+
if (not np.isfinite(tr)) or tr <= 0:
|
| 227 |
+
if self.default_tr is None:
|
| 228 |
+
raise ValueError(
|
| 229 |
+
f"Invalid or missing TR for {self.paths[idx]}: {tr}. "
|
| 230 |
+
"Set data.default_tr or pass --default-tr to use an explicit fallback."
|
| 231 |
+
)
|
| 232 |
+
tr = self.default_tr
|
| 233 |
+
if self.cache_meta:
|
| 234 |
+
self._meta_cache[idx] = (voxel, tr)
|
| 235 |
+
return voxel, tr
|
| 236 |
+
|
| 237 |
+
def _calculate_T_selected(self, tr: float, T_total: int) -> int:
|
| 238 |
+
"""
|
| 239 |
+
Calculate the number of time frames to select based on T_prime and tau_seconds.
|
| 240 |
+
|
| 241 |
+
Formula:
|
| 242 |
+
kt = round(tau_seconds / tr) # kernel size in time dimension
|
| 243 |
+
T_selected = T_prime * kt
|
| 244 |
+
|
| 245 |
+
This ensures that after TAPE (Time-to-space patch embedding), all samples
|
| 246 |
+
will have the same number of time patches (T_prime).
|
| 247 |
+
|
| 248 |
+
Args:
|
| 249 |
+
tr: Temporal resolution (TR) in seconds
|
| 250 |
+
T_total: Total number of time frames available in the data
|
| 251 |
+
|
| 252 |
+
Returns:
|
| 253 |
+
T_selected: Number of time frames to use (min with T_total)
|
| 254 |
+
"""
|
| 255 |
+
if self.T_prime is None:
|
| 256 |
+
return T_total
|
| 257 |
+
if tr <= 0:
|
| 258 |
+
raise ValueError("TR must be positive when T_prime is set")
|
| 259 |
+
|
| 260 |
+
# Calculate kernel size in time dimension
|
| 261 |
+
kt = max(1, round(self.tau_seconds / tr))
|
| 262 |
+
|
| 263 |
+
# Calculate required time frames to get T_prime patches
|
| 264 |
+
T_selected = self.T_prime * kt
|
| 265 |
+
|
| 266 |
+
# Ensure we don't exceed available data
|
| 267 |
+
T_selected = min(T_selected, T_total)
|
| 268 |
+
|
| 269 |
+
return T_selected
|
| 270 |
+
|
| 271 |
+
def __getitem__(self, idx: int) -> Dict:
|
| 272 |
+
# Try to load file, skip to next valid file if current is invalid
|
| 273 |
+
attempt = 0
|
| 274 |
+
max_attempts = len(self.paths)
|
| 275 |
+
|
| 276 |
+
while attempt < max_attempts:
|
| 277 |
+
current_idx = (idx + attempt) % len(self.paths)
|
| 278 |
+
p = self.paths[current_idx]
|
| 279 |
+
data, affine, header = _load_nifti(p, mmap=self.memory_map)
|
| 280 |
+
|
| 281 |
+
# If file is valid, process it
|
| 282 |
+
if data is not None:
|
| 283 |
+
voxel, tr = self._get_meta(current_idx, header)
|
| 284 |
+
|
| 285 |
+
# # 检测到tr=2或者1.96
|
| 286 |
+
# if not (np.isclose(tr, 2.0, atol=1e-2) or np.isclose(tr, 1.96, atol=1e-2)):
|
| 287 |
+
# attempt += 1
|
| 288 |
+
# continue
|
| 289 |
+
# print(f"TR is {tr} for {p}")
|
| 290 |
+
|
| 291 |
+
# Calculate T_selected based on T_prime and tau_seconds
|
| 292 |
+
T_total = data.shape[3] if len(data.shape) >= 4 else 1
|
| 293 |
+
T_selected = self._calculate_T_selected(tr, T_total)
|
| 294 |
+
|
| 295 |
+
# Slice data to T_selected frames
|
| 296 |
+
if len(data.shape) >= 4 and T_selected < T_total:
|
| 297 |
+
data = data[..., :T_selected]
|
| 298 |
+
|
| 299 |
+
sample = {
|
| 300 |
+
"data": torch.from_numpy(data) if self.return_torch else data,
|
| 301 |
+
"affine": torch.from_numpy(affine) if self.return_torch else affine,
|
| 302 |
+
"header": header,
|
| 303 |
+
"voxel": voxel,
|
| 304 |
+
"tr": tr,
|
| 305 |
+
"path": p,
|
| 306 |
+
"subject_idx": current_idx,
|
| 307 |
+
"T_selected": T_selected,
|
| 308 |
+
"T_prime": self.T_prime,
|
| 309 |
+
"tau_seconds": self.tau_seconds,
|
| 310 |
+
}
|
| 311 |
+
if self.transform is not None:
|
| 312 |
+
sample = self.transform(sample)
|
| 313 |
+
return sample
|
| 314 |
+
|
| 315 |
+
# Try next file if current one is invalid
|
| 316 |
+
attempt += 1
|
| 317 |
+
|
| 318 |
+
# If all files are invalid, raise error
|
| 319 |
+
raise RuntimeError(f"Could not find any valid file starting from index {idx}")
|
| 320 |
+
|
| 321 |
+
def meta_dict(self) -> Dict[int, Dict[str, Union[Tuple[float, float, float], float]]]:
|
| 322 |
+
"""Return {subject_idx: {"voxel": (vx,vy,vz), "tr": tr}} for the whole dataset."""
|
| 323 |
+
meta: Dict[int, Dict[str, Union[Tuple[float, float, float], float]]] = {}
|
| 324 |
+
for i, p in enumerate(self.paths):
|
| 325 |
+
if self.cache_meta and i in self._meta_cache:
|
| 326 |
+
voxel, tr = self._meta_cache[i]
|
| 327 |
+
else:
|
| 328 |
+
# read header cheaply without loading full data
|
| 329 |
+
img = nib.load(str(p), mmap=True)
|
| 330 |
+
voxel, tr = _space_time_units_to_mm_s(img.header)
|
| 331 |
+
if self.cache_meta:
|
| 332 |
+
self._meta_cache[i] = (voxel, tr)
|
| 333 |
+
meta[i] = {"voxel": voxel, "tr": tr}
|
| 334 |
+
return meta
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def build_train_val_from_lists(
|
| 340 |
+
train_txts: Union[str, Path, Sequence[Union[str, Path]]],
|
| 341 |
+
val_txts: Union[str, Path, Sequence[Union[str, Path]]],
|
| 342 |
+
*,
|
| 343 |
+
return_torch: bool = False,
|
| 344 |
+
memory_map: bool = True,
|
| 345 |
+
T_prime: Optional[int] = None,
|
| 346 |
+
tau_seconds: float = 6.0,
|
| 347 |
+
) -> Tuple[NiftiTxtDataset, NiftiTxtDataset, Dict[str, Dict[int, Dict[str, Union[Tuple[float, float, float], float]]]]]:
|
| 348 |
+
"""Convenience helper to create train/val datasets and collect their meta dicts.
|
| 349 |
+
|
| 350 |
+
Parameters
|
| 351 |
+
----------
|
| 352 |
+
train_txts, val_txts: str | Path | Sequence[str|Path]
|
| 353 |
+
Text files containing paths to NIfTI files
|
| 354 |
+
return_torch: bool
|
| 355 |
+
If True, converts data and affine to torch tensors
|
| 356 |
+
memory_map: bool
|
| 357 |
+
If True, enables nibabel's memory mapping
|
| 358 |
+
T_prime: Optional[int]
|
| 359 |
+
Target number of time patches after TAPE. If provided, dataset will automatically
|
| 360 |
+
select appropriate time frames to ensure all samples have the same T_prime.
|
| 361 |
+
tau_seconds: float
|
| 362 |
+
Time window in seconds for TAPE kernel (default: 6.0)
|
| 363 |
+
|
| 364 |
+
Returns
|
| 365 |
+
-------
|
| 366 |
+
train_set, val_set, meta_all
|
| 367 |
+
where meta_all = {"train": {...}, "val": {...}}
|
| 368 |
+
"""
|
| 369 |
+
train_set = NiftiTxtDataset(
|
| 370 |
+
train_txts,
|
| 371 |
+
return_torch=return_torch,
|
| 372 |
+
memory_map=memory_map,
|
| 373 |
+
T_prime=T_prime,
|
| 374 |
+
tau_seconds=tau_seconds,
|
| 375 |
+
)
|
| 376 |
+
val_set = NiftiTxtDataset(
|
| 377 |
+
val_txts,
|
| 378 |
+
return_torch=return_torch,
|
| 379 |
+
memory_map=memory_map,
|
| 380 |
+
T_prime=T_prime,
|
| 381 |
+
tau_seconds=tau_seconds,
|
| 382 |
+
)
|
| 383 |
+
meta_all = {"train": train_set.meta_dict(), "val": val_set.meta_dict()}
|
| 384 |
+
return train_set, val_set, meta_all
|
| 385 |
+
|
| 386 |
+
|
flexibrain/distributed/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch.distributed as dist
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def setup_distributed(rank: int, world_size: int) -> None:
|
| 6 |
+
"""Setup distributed training."""
|
| 7 |
+
if world_size > 1:
|
| 8 |
+
os.environ['MASTER_ADDR'] = os.environ.get('MASTER_ADDR', 'localhost')
|
| 9 |
+
os.environ['MASTER_PORT'] = os.environ.get('MASTER_PORT', '12355')
|
| 10 |
+
dist.init_process_group(
|
| 11 |
+
backend='nccl',
|
| 12 |
+
rank=rank,
|
| 13 |
+
world_size=world_size
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def cleanup_distributed() -> None:
|
| 18 |
+
"""Cleanup distributed training."""
|
| 19 |
+
if dist.is_available() and dist.is_initialized():
|
| 20 |
+
dist.destroy_process_group()
|
flexibrain/engine/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flexibrain.engine.pretrainer import Pretrainer
|
| 2 |
+
from flexibrain.engine.downstream_trainer import DownstreamTrainer
|
| 3 |
+
|
| 4 |
+
__all__ = [Pretrainer, DownstreamTrainer]
|
flexibrain/engine/downstream_trainer.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from typing import Dict
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
import torch.optim as optim
|
| 10 |
+
from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score
|
| 11 |
+
|
| 12 |
+
from flexibrain.config import RunConfig
|
| 13 |
+
from flexibrain.data import build_downstream_dataloaders
|
| 14 |
+
from flexibrain.data.classification import prepare_batch_data
|
| 15 |
+
from flexibrain.models import build_downstream_model
|
| 16 |
+
from flexibrain.utils.logging import setup_logger
|
| 17 |
+
from flexibrain.utils.seed import set_seed
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class DownstreamTrainer:
|
| 21 |
+
def __init__(self, cfg: RunConfig):
|
| 22 |
+
self.cfg = cfg
|
| 23 |
+
self.rank = cfg.training.local_rank
|
| 24 |
+
self.device = torch.device(f"cuda:{self.rank}" if torch.cuda.is_available() else "cpu")
|
| 25 |
+
self.logger = setup_logger("downstream", cfg.logging.log_dir, rank=self.rank)
|
| 26 |
+
|
| 27 |
+
def build(self):
|
| 28 |
+
set_seed(self.cfg.training.seed)
|
| 29 |
+
self.model = build_downstream_model(
|
| 30 |
+
self.cfg.model,
|
| 31 |
+
self.device,
|
| 32 |
+
logger=self.logger,
|
| 33 |
+
checkpoint_path=self.cfg.pretrain_checkpoint,
|
| 34 |
+
from_scratch=self.cfg.from_scratch,
|
| 35 |
+
use_checkpoint_config=self.cfg.use_checkpoint_config,
|
| 36 |
+
)
|
| 37 |
+
self.train_loader, self.val_loader, self.test_loader = build_downstream_dataloaders(self.cfg.data, self.cfg.training, rank=self.rank, world_size=self.cfg.training.world_size)
|
| 38 |
+
if self.cfg.training.lr_backbone is not None or self.cfg.training.lr_head is not None:
|
| 39 |
+
backbone_params = list(self.model.backbone.parameters())
|
| 40 |
+
head_params = [p for n, p in self.model.named_parameters() if not n.startswith("backbone.")]
|
| 41 |
+
self.optimizer = optim.AdamW([
|
| 42 |
+
{"params": backbone_params, "lr": self.cfg.training.lr_backbone or self.cfg.training.lr},
|
| 43 |
+
{"params": head_params, "lr": self.cfg.training.lr_head or self.cfg.training.lr},
|
| 44 |
+
], weight_decay=self.cfg.training.weight_decay)
|
| 45 |
+
else:
|
| 46 |
+
self.optimizer = optim.AdamW(self.model.parameters(), lr=self.cfg.training.lr, weight_decay=self.cfg.training.weight_decay)
|
| 47 |
+
self.use_amp = bool(self.cfg.training.use_amp and self.device.type == "cuda")
|
| 48 |
+
self.scaler = torch.cuda.amp.GradScaler(enabled=self.use_amp)
|
| 49 |
+
total_steps = max(1, len(self.train_loader) * self.cfg.training.epochs)
|
| 50 |
+
warmup_steps = max(1, len(self.train_loader) * self.cfg.training.warmup_epochs)
|
| 51 |
+
|
| 52 |
+
def lr_lambda(step):
|
| 53 |
+
if step < warmup_steps:
|
| 54 |
+
return step / warmup_steps
|
| 55 |
+
return max(0.0, (total_steps - step) / max(1, total_steps - warmup_steps))
|
| 56 |
+
|
| 57 |
+
self.scheduler = optim.lr_scheduler.LambdaLR(self.optimizer, lr_lambda)
|
| 58 |
+
return self
|
| 59 |
+
|
| 60 |
+
def _optimizer_step(self) -> None:
|
| 61 |
+
if self.cfg.training.grad_clip > 0:
|
| 62 |
+
self.scaler.unscale_(self.optimizer)
|
| 63 |
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.cfg.training.grad_clip)
|
| 64 |
+
self.scaler.step(self.optimizer)
|
| 65 |
+
self.scaler.update()
|
| 66 |
+
self.scheduler.step()
|
| 67 |
+
self.optimizer.zero_grad(set_to_none=True)
|
| 68 |
+
|
| 69 |
+
def train_one_epoch(self, epoch: int) -> float:
|
| 70 |
+
self.model.train()
|
| 71 |
+
criterion = nn.CrossEntropyLoss()
|
| 72 |
+
total_loss = 0.0
|
| 73 |
+
num_batches = 0
|
| 74 |
+
accum = self.cfg.training.grad_accumulation_steps
|
| 75 |
+
self.optimizer.zero_grad(set_to_none=True)
|
| 76 |
+
for batch_idx, batch in enumerate(self.train_loader):
|
| 77 |
+
x, meta, orig_Ts, labels, affines = prepare_batch_data(batch, self.device)
|
| 78 |
+
with torch.autocast(device_type=self.device.type, dtype=torch.float16, enabled=self.use_amp):
|
| 79 |
+
logits = self.model(x, meta=meta, orig_Ts=orig_Ts, affines=affines)
|
| 80 |
+
loss = criterion(logits, labels)
|
| 81 |
+
self.scaler.scale(loss / accum).backward()
|
| 82 |
+
if (batch_idx + 1) % accum == 0:
|
| 83 |
+
self._optimizer_step()
|
| 84 |
+
total_loss += float(loss.item())
|
| 85 |
+
num_batches += 1
|
| 86 |
+
if self.rank == 0 and (batch_idx + 1) % self.cfg.logging.log_interval == 0:
|
| 87 |
+
self.logger.info("Epoch %d [%d/%d] loss=%.6f", epoch + 1, batch_idx + 1, len(self.train_loader), loss.item())
|
| 88 |
+
if num_batches % accum != 0:
|
| 89 |
+
self._optimizer_step()
|
| 90 |
+
return total_loss / max(1, num_batches)
|
| 91 |
+
|
| 92 |
+
@torch.no_grad()
|
| 93 |
+
def evaluate(self, loader, split_name: str) -> Dict[str, float]:
|
| 94 |
+
self.model.eval()
|
| 95 |
+
criterion = nn.CrossEntropyLoss()
|
| 96 |
+
preds, labels_all = [], []
|
| 97 |
+
total_loss = 0.0
|
| 98 |
+
num_batches = 0
|
| 99 |
+
for batch in loader:
|
| 100 |
+
x, meta, orig_Ts, labels, affines = prepare_batch_data(batch, self.device)
|
| 101 |
+
with torch.autocast(device_type=self.device.type, dtype=torch.float16, enabled=self.use_amp):
|
| 102 |
+
logits = self.model(x, meta=meta, orig_Ts=orig_Ts, affines=affines)
|
| 103 |
+
loss = criterion(logits, labels)
|
| 104 |
+
total_loss += float(loss.item())
|
| 105 |
+
num_batches += 1
|
| 106 |
+
preds.extend(torch.argmax(logits, dim=1).cpu().numpy())
|
| 107 |
+
labels_all.extend(labels.cpu().numpy())
|
| 108 |
+
metrics = {
|
| 109 |
+
"loss": total_loss / max(1, num_batches),
|
| 110 |
+
"accuracy": accuracy_score(labels_all, preds),
|
| 111 |
+
"precision_macro": precision_score(labels_all, preds, average="macro", zero_division=0),
|
| 112 |
+
"recall_macro": recall_score(labels_all, preds, average="macro", zero_division=0),
|
| 113 |
+
"f1_macro": f1_score(labels_all, preds, average="macro", zero_division=0),
|
| 114 |
+
"f1_weighted": f1_score(labels_all, preds, average="weighted", zero_division=0),
|
| 115 |
+
}
|
| 116 |
+
if self.rank == 0:
|
| 117 |
+
self.logger.info("%s metrics: %s", split_name, metrics)
|
| 118 |
+
return metrics
|
| 119 |
+
|
| 120 |
+
def save(self, epoch: int, metrics: dict, is_best: bool):
|
| 121 |
+
if self.rank != 0:
|
| 122 |
+
return
|
| 123 |
+
os.makedirs(self.cfg.logging.checkpoint_dir, exist_ok=True)
|
| 124 |
+
payload = {
|
| 125 |
+
"epoch": epoch,
|
| 126 |
+
"model": self.model.state_dict(),
|
| 127 |
+
"optimizer": self.optimizer.state_dict(),
|
| 128 |
+
"scheduler": self.scheduler.state_dict(),
|
| 129 |
+
"metrics": metrics,
|
| 130 |
+
"config": vars(self.cfg.model),
|
| 131 |
+
}
|
| 132 |
+
torch.save(payload, os.path.join(self.cfg.logging.checkpoint_dir, "downstream_latest.pt"))
|
| 133 |
+
if is_best:
|
| 134 |
+
torch.save(payload, os.path.join(self.cfg.logging.checkpoint_dir, "downstream_best.pt"))
|
| 135 |
+
|
| 136 |
+
def _load_best_for_test(self) -> None:
|
| 137 |
+
best_path = os.path.join(self.cfg.logging.checkpoint_dir, "downstream_best.pt")
|
| 138 |
+
if not os.path.exists(best_path):
|
| 139 |
+
return
|
| 140 |
+
checkpoint = torch.load(best_path, map_location=self.device)
|
| 141 |
+
self.model.load_state_dict(checkpoint["model"])
|
| 142 |
+
|
| 143 |
+
def _save_test_metrics(self, metrics: Dict[str, float]) -> None:
|
| 144 |
+
if self.rank != 0:
|
| 145 |
+
return
|
| 146 |
+
os.makedirs(self.cfg.logging.checkpoint_dir, exist_ok=True)
|
| 147 |
+
path = os.path.join(self.cfg.logging.checkpoint_dir, "test_metrics.json")
|
| 148 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 149 |
+
json.dump(metrics, f, indent=2)
|
| 150 |
+
|
| 151 |
+
def fit(self):
|
| 152 |
+
self.build()
|
| 153 |
+
if self.rank == 0:
|
| 154 |
+
self.logger.info("Starting downstream on %s", self.device)
|
| 155 |
+
self.logger.info("Train size=%d Val size=%d", len(self.train_loader.dataset), len(self.val_loader.dataset))
|
| 156 |
+
best_f1 = -1.0
|
| 157 |
+
for epoch in range(self.cfg.training.epochs):
|
| 158 |
+
train_loss = self.train_one_epoch(epoch)
|
| 159 |
+
val_metrics = self.evaluate(self.val_loader, "Validation")
|
| 160 |
+
metrics = {"val": val_metrics, "train_loss": train_loss}
|
| 161 |
+
is_best = val_metrics["f1_macro"] > best_f1
|
| 162 |
+
if is_best:
|
| 163 |
+
best_f1 = val_metrics["f1_macro"]
|
| 164 |
+
self.save(epoch, metrics, is_best=is_best)
|
| 165 |
+
if self.rank == 0:
|
| 166 |
+
self.logger.info("Epoch %d done train=%.6f best_f1=%.6f", epoch + 1, train_loss, best_f1)
|
| 167 |
+
if self.test_loader is not None:
|
| 168 |
+
self._load_best_for_test()
|
| 169 |
+
test_metrics = self.evaluate(self.test_loader, "Test")
|
| 170 |
+
self._save_test_metrics(test_metrics)
|
flexibrain/engine/pretrainer.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.optim as optim
|
| 8 |
+
|
| 9 |
+
from flexibrain.config import RunConfig
|
| 10 |
+
from flexibrain.data import build_pretrain_dataloaders
|
| 11 |
+
from flexibrain.data.collate import prepare_batch_data
|
| 12 |
+
from flexibrain.distributed import cleanup_distributed, setup_distributed
|
| 13 |
+
from flexibrain.models import build_pretrain_model
|
| 14 |
+
from flexibrain.utils.logging import setup_logger
|
| 15 |
+
from flexibrain.utils.seed import set_seed
|
| 16 |
+
from flexibrain.utils.training import get_dynamic_momentum, update_ema
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Pretrainer:
|
| 20 |
+
def __init__(self, cfg: RunConfig):
|
| 21 |
+
self.cfg = cfg
|
| 22 |
+
self.rank = cfg.training.local_rank
|
| 23 |
+
self.world_size = cfg.training.world_size
|
| 24 |
+
if self.world_size > 1:
|
| 25 |
+
setup_distributed(self.rank, self.world_size)
|
| 26 |
+
self.device = torch.device(f"cuda:{self.rank}" if torch.cuda.is_available() else "cpu")
|
| 27 |
+
self.logger = setup_logger("pretrain", cfg.logging.log_dir, rank=self.rank)
|
| 28 |
+
|
| 29 |
+
def build(self):
|
| 30 |
+
set_seed(self.cfg.training.seed)
|
| 31 |
+
self.model = build_pretrain_model(self.cfg.model, self.device)
|
| 32 |
+
self.train_loader, self.val_loader = build_pretrain_dataloaders(self.cfg.data, self.cfg.training, rank=self.rank, world_size=self.world_size)
|
| 33 |
+
self.optimizer = optim.AdamW(self.model.parameters(), lr=self.cfg.training.lr, weight_decay=self.cfg.training.weight_decay)
|
| 34 |
+
self.use_amp = bool(self.cfg.training.use_amp and self.device.type == "cuda")
|
| 35 |
+
self.scaler = torch.cuda.amp.GradScaler(enabled=self.use_amp)
|
| 36 |
+
total_steps = max(1, len(self.train_loader) * self.cfg.training.epochs)
|
| 37 |
+
warmup_steps = max(1, len(self.train_loader) * self.cfg.training.warmup_epochs)
|
| 38 |
+
|
| 39 |
+
def lr_lambda(step):
|
| 40 |
+
if step < warmup_steps:
|
| 41 |
+
return step / warmup_steps
|
| 42 |
+
progress = (step - warmup_steps) / max(1, total_steps - warmup_steps)
|
| 43 |
+
cycle_progress = (progress * 4) % 1.0
|
| 44 |
+
if cycle_progress < 0.8:
|
| 45 |
+
return 0.5 * (1 + math.cos(math.pi * cycle_progress / 0.8))
|
| 46 |
+
return 0.0
|
| 47 |
+
|
| 48 |
+
self.scheduler = optim.lr_scheduler.LambdaLR(self.optimizer, lr_lambda)
|
| 49 |
+
return self
|
| 50 |
+
|
| 51 |
+
def _optimizer_step(self, momentum: float) -> None:
|
| 52 |
+
if self.cfg.training.grad_clip > 0:
|
| 53 |
+
self.scaler.unscale_(self.optimizer)
|
| 54 |
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.cfg.training.grad_clip)
|
| 55 |
+
self.scaler.step(self.optimizer)
|
| 56 |
+
self.scaler.update()
|
| 57 |
+
self.optimizer.zero_grad(set_to_none=True)
|
| 58 |
+
update_ema(self.model, momentum)
|
| 59 |
+
self.scheduler.step()
|
| 60 |
+
|
| 61 |
+
def train_one_epoch(self, epoch: int) -> float:
|
| 62 |
+
self.model.train()
|
| 63 |
+
total_loss = 0.0
|
| 64 |
+
num_batches = 0
|
| 65 |
+
accumulation_steps = self.cfg.training.grad_accumulation_steps
|
| 66 |
+
momentum = get_dynamic_momentum(epoch, self.cfg.training.epochs, self.cfg.model.momentum, self.cfg.model.final_momentum)
|
| 67 |
+
self.optimizer.zero_grad(set_to_none=True)
|
| 68 |
+
for batch_idx, batch in enumerate(self.train_loader):
|
| 69 |
+
x, meta, orig_Ts, affines = prepare_batch_data(batch, self.device)
|
| 70 |
+
with torch.autocast(device_type=self.device.type, dtype=torch.float16, enabled=self.use_amp):
|
| 71 |
+
loss, _, _, _ = self.model(x, mask_ratio=self.cfg.training.mask_ratio, meta=meta, orig_Ts=orig_Ts, affines=affines)
|
| 72 |
+
self.scaler.scale(loss / accumulation_steps).backward()
|
| 73 |
+
total_loss += float(loss.item())
|
| 74 |
+
num_batches += 1
|
| 75 |
+
if (batch_idx + 1) % accumulation_steps == 0:
|
| 76 |
+
self._optimizer_step(momentum)
|
| 77 |
+
if self.rank == 0 and (batch_idx + 1) % self.cfg.logging.log_interval == 0:
|
| 78 |
+
self.logger.info("Epoch %d [%d/%d] loss=%.6f avg=%.6f momentum=%.6f", epoch + 1, batch_idx + 1, len(self.train_loader), loss.item(), total_loss / num_batches, momentum)
|
| 79 |
+
if num_batches % accumulation_steps != 0:
|
| 80 |
+
self._optimizer_step(momentum)
|
| 81 |
+
return total_loss / max(1, num_batches)
|
| 82 |
+
|
| 83 |
+
@torch.no_grad()
|
| 84 |
+
def validate(self, epoch: int) -> float:
|
| 85 |
+
self.model.eval()
|
| 86 |
+
total_loss = 0.0
|
| 87 |
+
num_batches = 0
|
| 88 |
+
for batch in self.val_loader:
|
| 89 |
+
x, meta, orig_Ts, affines = prepare_batch_data(batch, self.device)
|
| 90 |
+
with torch.autocast(device_type=self.device.type, dtype=torch.float16, enabled=self.use_amp):
|
| 91 |
+
loss, _, _, _ = self.model(x, mask_ratio=self.cfg.training.mask_ratio, meta=meta, orig_Ts=orig_Ts, affines=affines)
|
| 92 |
+
total_loss += float(loss.item())
|
| 93 |
+
num_batches += 1
|
| 94 |
+
avg = total_loss / max(1, num_batches)
|
| 95 |
+
if self.rank == 0:
|
| 96 |
+
self.logger.info("Epoch %d validation loss=%.6f", epoch + 1, avg)
|
| 97 |
+
return avg
|
| 98 |
+
|
| 99 |
+
def save(self, epoch: int, val_loss: float, best_loss: float, is_best: bool):
|
| 100 |
+
if self.rank != 0:
|
| 101 |
+
return
|
| 102 |
+
os.makedirs(self.cfg.logging.checkpoint_dir, exist_ok=True)
|
| 103 |
+
payload = {
|
| 104 |
+
"epoch": epoch,
|
| 105 |
+
"model_state_dict": self.model.state_dict(),
|
| 106 |
+
"optimizer_state_dict": self.optimizer.state_dict(),
|
| 107 |
+
"scheduler_state_dict": self.scheduler.state_dict(),
|
| 108 |
+
"val_loss": val_loss,
|
| 109 |
+
"best_loss": best_loss,
|
| 110 |
+
"config": vars(self.cfg.model),
|
| 111 |
+
}
|
| 112 |
+
torch.save(payload, os.path.join(self.cfg.logging.checkpoint_dir, "checkpoint_latest.pt"))
|
| 113 |
+
if is_best:
|
| 114 |
+
torch.save(payload, os.path.join(self.cfg.logging.checkpoint_dir, "checkpoint_best.pt"))
|
| 115 |
+
|
| 116 |
+
def fit(self):
|
| 117 |
+
self.build()
|
| 118 |
+
if self.rank == 0:
|
| 119 |
+
self.logger.info("Starting pretrain on %s", self.device)
|
| 120 |
+
self.logger.info("Train size=%d Val size=%d", len(self.train_loader.dataset), len(self.val_loader.dataset))
|
| 121 |
+
best_loss = float("inf")
|
| 122 |
+
for epoch in range(self.cfg.training.epochs):
|
| 123 |
+
if hasattr(self.train_loader.sampler, "set_epoch"):
|
| 124 |
+
self.train_loader.sampler.set_epoch(epoch)
|
| 125 |
+
train_loss = self.train_one_epoch(epoch)
|
| 126 |
+
val_loss = self.validate(epoch)
|
| 127 |
+
is_best = val_loss < best_loss
|
| 128 |
+
if is_best:
|
| 129 |
+
best_loss = val_loss
|
| 130 |
+
self.save(epoch, val_loss, best_loss, is_best=is_best)
|
| 131 |
+
if self.rank == 0:
|
| 132 |
+
self.logger.info("Epoch %d done train=%.6f val=%.6f best=%.6f", epoch + 1, train_loss, val_loss, best_loss)
|
| 133 |
+
if self.world_size > 1:
|
| 134 |
+
cleanup_distributed()
|
flexibrain/models/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flexibrain.models.factory import build_downstream_model, build_pretrain_model
|
| 2 |
+
from flexibrain.models.mamba_jepa import VolumeMambaJEPA
|
| 3 |
+
|
| 4 |
+
__all__ = [build_downstream_model, build_pretrain_model, VolumeMambaJEPA]
|
flexibrain/models/classifier.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from flexibrain.models.transformer_block import Block
|
| 4 |
+
|
| 5 |
+
from flexibrain.models.mamba_jepa import VolumeMambaJEPA
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class MambaJEPAClassifier(nn.Module):
|
| 9 |
+
|
| 10 |
+
def __init__(
|
| 11 |
+
self,
|
| 12 |
+
backbone: 'VolumeMambaJEPA',
|
| 13 |
+
num_classes: int,
|
| 14 |
+
head_depth: int = 2,
|
| 15 |
+
head_num_heads: int = 8,
|
| 16 |
+
head_mlp_ratio: float = 4.0,
|
| 17 |
+
head_qkv_bias: bool = True,
|
| 18 |
+
head_attn_drop: float = 0.0,
|
| 19 |
+
head_proj_drop: float = 0.0,
|
| 20 |
+
head_drop_path: float = 0.0,
|
| 21 |
+
head_norm_epsilon: float = 1e-5,
|
| 22 |
+
mlp_hidden: int = 1024,
|
| 23 |
+
mlp_depth: int = 2,
|
| 24 |
+
mlp_dropout: float = 0.1,
|
| 25 |
+
freeze_backbone: bool = False,
|
| 26 |
+
device=None,
|
| 27 |
+
dtype=None,
|
| 28 |
+
):
|
| 29 |
+
super().__init__()
|
| 30 |
+
self.backbone = backbone
|
| 31 |
+
self.embed_dim = backbone.embed_dim
|
| 32 |
+
|
| 33 |
+
if freeze_backbone:
|
| 34 |
+
for p in self.backbone.parameters():
|
| 35 |
+
p.requires_grad = False
|
| 36 |
+
else:
|
| 37 |
+
for p in self.backbone.parameters():
|
| 38 |
+
p.requires_grad = True
|
| 39 |
+
|
| 40 |
+
if dtype is None:
|
| 41 |
+
dtype = next(self.backbone.parameters()).dtype
|
| 42 |
+
if device is None:
|
| 43 |
+
device = next(self.backbone.parameters()).device
|
| 44 |
+
factory = dict(device=device, dtype=dtype)
|
| 45 |
+
|
| 46 |
+
self.cls_token = nn.Parameter(torch.zeros(1, 1, self.embed_dim, **factory))
|
| 47 |
+
nn.init.normal_(self.cls_token, std=0.02)
|
| 48 |
+
|
| 49 |
+
dpr = [x.item() for x in torch.linspace(0, head_drop_path, head_depth)] if head_depth > 0 else []
|
| 50 |
+
def _norm_layer_with_dtype(dim):
|
| 51 |
+
ln = nn.LayerNorm(dim, eps=head_norm_epsilon)
|
| 52 |
+
if device is not None:
|
| 53 |
+
ln = ln.to(device=device)
|
| 54 |
+
if dtype is not None:
|
| 55 |
+
ln = ln.to(dtype=dtype)
|
| 56 |
+
return ln
|
| 57 |
+
self.head_blocks = nn.ModuleList([
|
| 58 |
+
Block(
|
| 59 |
+
dim=self.embed_dim,
|
| 60 |
+
num_heads=head_num_heads,
|
| 61 |
+
mlp_ratio=head_mlp_ratio,
|
| 62 |
+
qkv_bias=head_qkv_bias,
|
| 63 |
+
attn_drop=head_attn_drop,
|
| 64 |
+
drop=head_proj_drop,
|
| 65 |
+
drop_path=dpr[i] if head_depth > 0 else 0.0,
|
| 66 |
+
norm_layer=_norm_layer_with_dtype,
|
| 67 |
+
) for i in range(head_depth)
|
| 68 |
+
])
|
| 69 |
+
self.head_norm = nn.LayerNorm(self.embed_dim, eps=head_norm_epsilon, **factory)
|
| 70 |
+
|
| 71 |
+
layers = []
|
| 72 |
+
in_dim = self.embed_dim
|
| 73 |
+
for _ in range(max(mlp_depth - 1, 0)):
|
| 74 |
+
layers += [nn.Linear(in_dim, mlp_hidden, **factory), nn.GELU(), nn.Dropout(mlp_dropout)]
|
| 75 |
+
in_dim = mlp_hidden
|
| 76 |
+
layers += [nn.Linear(in_dim, num_classes, **factory)]
|
| 77 |
+
self.classifier = nn.Sequential(*layers)
|
| 78 |
+
|
| 79 |
+
@torch.no_grad()
|
| 80 |
+
def _encode_backbone_nograd(self, x, meta=None, orig_Ts=None, affines=None, inference_params=None):
|
| 81 |
+
xf, attn_pad, _, _ = self.backbone.patch_embed(x, meta, orig_Ts, affines)
|
| 82 |
+
feat = self.backbone._run_blocks(
|
| 83 |
+
xf, attn_pad,
|
| 84 |
+
blocks=self.backbone.blocks,
|
| 85 |
+
norm_layer=self.backbone.norm_f,
|
| 86 |
+
inference_params=inference_params
|
| 87 |
+
)
|
| 88 |
+
return feat, attn_pad
|
| 89 |
+
|
| 90 |
+
def _encode_backbone(self, x, meta=None, orig_Ts=None, affines=None, inference_params=None):
|
| 91 |
+
xf, attn_pad, _, _ = self.backbone.patch_embed(x, meta, orig_Ts, affines)
|
| 92 |
+
|
| 93 |
+
feat = self.backbone._run_blocks(
|
| 94 |
+
xf, attn_pad,
|
| 95 |
+
blocks=self.backbone.blocks,
|
| 96 |
+
norm_layer=self.backbone.norm_f,
|
| 97 |
+
inference_params=inference_params
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
return feat, attn_pad
|
| 101 |
+
|
| 102 |
+
def forward_from_tokens(self, tokens, attn_pad, inference_params=None):
|
| 103 |
+
|
| 104 |
+
B, L, D = tokens.shape
|
| 105 |
+
device = tokens.device
|
| 106 |
+
|
| 107 |
+
cls_tok = self.cls_token.to(dtype=tokens.dtype).expand(B, -1, -1) # [B,1,D]
|
| 108 |
+
x_cat = torch.cat([cls_tok, tokens], dim=1) # [B,1+L,D]
|
| 109 |
+
|
| 110 |
+
cls_pad = torch.zeros(B, 1, dtype=torch.bool, device=device) # False=valid
|
| 111 |
+
attn_cat = torch.cat([cls_pad, attn_pad], dim=1) # [B,1+L]
|
| 112 |
+
|
| 113 |
+
attn_cat_for_flash = ~attn_cat
|
| 114 |
+
|
| 115 |
+
h = x_cat
|
| 116 |
+
for blk in self.head_blocks:
|
| 117 |
+
h = blk(h, attention_mask=attn_cat_for_flash)
|
| 118 |
+
h = self.head_norm(h)
|
| 119 |
+
|
| 120 |
+
cls_feat = h[:, 0, :] # [B,D]
|
| 121 |
+
logits = self.classifier(cls_feat) # [B,C]
|
| 122 |
+
return logits
|
| 123 |
+
|
| 124 |
+
def forward(self, x, meta=None, orig_Ts=None, affines=None, inference_params=None):
|
| 125 |
+
|
| 126 |
+
feat, attn_pad = self._encode_backbone(x, meta=meta, orig_Ts=orig_Ts, affines=affines,
|
| 127 |
+
inference_params=inference_params)
|
| 128 |
+
B, L, D = feat.shape
|
| 129 |
+
device = feat.device
|
| 130 |
+
|
| 131 |
+
cls_tok = self.cls_token.to(dtype=feat.dtype).expand(B, -1, -1) # [B,1,D]
|
| 132 |
+
x_cat = torch.cat([cls_tok, feat], dim=1) # [B,1+L,D]
|
| 133 |
+
|
| 134 |
+
cls_pad = torch.zeros(B, 1, dtype=torch.bool, device=device) # False=valid
|
| 135 |
+
attn_cat = torch.cat([cls_pad, attn_pad], dim=1) # [B,1+L]
|
| 136 |
+
|
| 137 |
+
attn_cat_for_flash = ~attn_cat
|
| 138 |
+
|
| 139 |
+
h = x_cat
|
| 140 |
+
for blk in self.head_blocks:
|
| 141 |
+
h = blk(h, attention_mask=attn_cat_for_flash)
|
| 142 |
+
h = self.head_norm(h)
|
| 143 |
+
|
| 144 |
+
cls_feat = h[:, 0, :] # [B,D]
|
| 145 |
+
logits = self.classifier(cls_feat) # [B,C]
|
| 146 |
+
return logits
|
| 147 |
+
|
| 148 |
+
class MambaJEPAClassifierAvgPool(nn.Module):
|
| 149 |
+
|
| 150 |
+
def __init__(
|
| 151 |
+
self,
|
| 152 |
+
backbone: 'VolumeMambaJEPA',
|
| 153 |
+
num_classes: int,
|
| 154 |
+
mlp_hidden: int = 1024,
|
| 155 |
+
mlp_depth: int = 3,
|
| 156 |
+
mlp_dropout: float = 0.1,
|
| 157 |
+
freeze_backbone: bool = False,
|
| 158 |
+
device=None,
|
| 159 |
+
dtype=None,
|
| 160 |
+
):
|
| 161 |
+
super().__init__()
|
| 162 |
+
self.backbone = backbone
|
| 163 |
+
self.embed_dim = backbone.embed_dim
|
| 164 |
+
|
| 165 |
+
if freeze_backbone:
|
| 166 |
+
for p in self.backbone.parameters():
|
| 167 |
+
p.requires_grad = False
|
| 168 |
+
else:
|
| 169 |
+
for p in self.backbone.parameters():
|
| 170 |
+
p.requires_grad = True
|
| 171 |
+
|
| 172 |
+
if dtype is None:
|
| 173 |
+
dtype = next(self.backbone.parameters()).dtype
|
| 174 |
+
if device is None:
|
| 175 |
+
device = next(self.backbone.parameters()).device
|
| 176 |
+
factory = dict(device=device, dtype=dtype)
|
| 177 |
+
|
| 178 |
+
layers = []
|
| 179 |
+
in_dim = self.embed_dim
|
| 180 |
+
for _ in range(max(mlp_depth - 1, 0)):
|
| 181 |
+
layers += [nn.Linear(in_dim, mlp_hidden, **factory), nn.GELU(), nn.Dropout(mlp_dropout)]
|
| 182 |
+
in_dim = mlp_hidden
|
| 183 |
+
layers += [nn.Linear(in_dim, num_classes, **factory)]
|
| 184 |
+
self.classifier = nn.Sequential(*layers)
|
| 185 |
+
|
| 186 |
+
def _encode_backbone(self, x, meta=None, orig_Ts=None, affines=None, inference_params=None):
|
| 187 |
+
xf, attn_pad, _, _ = self.backbone.patch_embed(x, meta, orig_Ts, affines)
|
| 188 |
+
|
| 189 |
+
feat = self.backbone._run_blocks(
|
| 190 |
+
xf, attn_pad,
|
| 191 |
+
blocks=self.backbone.blocks,
|
| 192 |
+
norm_layer=self.backbone.norm_f,
|
| 193 |
+
inference_params=inference_params
|
| 194 |
+
)
|
| 195 |
+
return feat, attn_pad
|
| 196 |
+
|
| 197 |
+
def forward_from_tokens(self, tokens, attn_pad, inference_params=None):
|
| 198 |
+
"""
|
| 199 |
+
Args:
|
| 200 |
+
tokens: [B, L, D] backbone token
|
| 201 |
+
attn_pad: [B, L] attention mask (True=padding, False=valid)
|
| 202 |
+
inference_params
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
logits: [B, num_classes]
|
| 206 |
+
"""
|
| 207 |
+
B, L, D = tokens.shape
|
| 208 |
+
|
| 209 |
+
valid_mask = ~attn_pad # [B, L] -> True=valid
|
| 210 |
+
|
| 211 |
+
valid_counts = valid_mask.sum(dim=1, keepdim=True).clamp(min=1) # [B, 1]
|
| 212 |
+
|
| 213 |
+
feat_masked = tokens * valid_mask.unsqueeze(-1).float() # [B, L, D]
|
| 214 |
+
feat_sum = feat_masked.sum(dim=1) # [B, D]
|
| 215 |
+
|
| 216 |
+
feat_avg = feat_sum / valid_counts.float() # [B, D]
|
| 217 |
+
|
| 218 |
+
logits = self.classifier(feat_avg) # [B, num_classes]
|
| 219 |
+
return logits
|
| 220 |
+
|
| 221 |
+
def forward(self, x, meta=None, orig_Ts=None, affines=None, inference_params=None):
|
| 222 |
+
|
| 223 |
+
feat, attn_pad = self._encode_backbone(x, meta=meta, orig_Ts=orig_Ts, affines=affines,
|
| 224 |
+
inference_params=inference_params)
|
| 225 |
+
B, L, D = feat.shape
|
| 226 |
+
|
| 227 |
+
valid_mask = ~attn_pad # [B, L] -> True=valid
|
| 228 |
+
|
| 229 |
+
valid_counts = valid_mask.sum(dim=1, keepdim=True).clamp(min=1) # [B, 1]
|
| 230 |
+
|
| 231 |
+
feat_masked = feat * valid_mask.unsqueeze(-1).float() # [B, L, D]
|
| 232 |
+
feat_sum = feat_masked.sum(dim=1) # [B, D]
|
| 233 |
+
|
| 234 |
+
feat_avg = feat_sum / valid_counts.float() # [B, D]
|
| 235 |
+
|
| 236 |
+
logits = self.classifier(feat_avg) # [B, num_classes]
|
| 237 |
+
return logits
|
flexibrain/models/factory.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
|
| 9 |
+
from flexibrain.config import ModelConfig, apply_checkpoint_config
|
| 10 |
+
from flexibrain.models.mamba_jepa import VolumeMambaJEPA
|
| 11 |
+
from flexibrain.models.classifier import MambaJEPAClassifier, MambaJEPAClassifierAvgPool
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def build_mamba_backbone(cfg: ModelConfig, device: torch.device, dtype=torch.float32) -> VolumeMambaJEPA:
|
| 15 |
+
return VolumeMambaJEPA(
|
| 16 |
+
embed_dim=cfg.embed_dim,
|
| 17 |
+
depth=cfg.depth,
|
| 18 |
+
predictor_depth=cfg.predictor_depth,
|
| 19 |
+
ssm_cfg=None,
|
| 20 |
+
encoder_attn_layer_idx=None,
|
| 21 |
+
attn_cfg=None,
|
| 22 |
+
drop_path_rate=cfg.drop_path_rate,
|
| 23 |
+
norm_epsilon=1e-5,
|
| 24 |
+
rms_norm=cfg.rms_norm,
|
| 25 |
+
initializer_cfg=None,
|
| 26 |
+
fused_add_norm=cfg.fused_add_norm,
|
| 27 |
+
residual_in_fp32=cfg.residual_in_fp32,
|
| 28 |
+
device=device,
|
| 29 |
+
dtype=dtype,
|
| 30 |
+
bimamba_type=cfg.bimamba_type,
|
| 31 |
+
if_bimamba=cfg.if_bimamba,
|
| 32 |
+
mixer_type=cfg.mixer_type,
|
| 33 |
+
if_devide_out=cfg.if_devide_out,
|
| 34 |
+
momentum=cfg.momentum,
|
| 35 |
+
norm_target=cfg.norm_target,
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def build_pretrain_model(cfg: ModelConfig, device: torch.device) -> nn.Module:
|
| 40 |
+
if cfg.model_type != "mamba":
|
| 41 |
+
raise ValueError("This cleaned Flexibrain build currently keeps only the Mamba pretrain/downstream path")
|
| 42 |
+
return build_mamba_backbone(cfg, device=device, dtype=torch.float32).to(device)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def load_checkpoint(path: str, device: torch.device):
|
| 46 |
+
return torch.load(path, map_location=device)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def state_dict_from_checkpoint(checkpoint: dict):
|
| 50 |
+
if "model_state_dict" in checkpoint:
|
| 51 |
+
return checkpoint["model_state_dict"]
|
| 52 |
+
if "model" in checkpoint:
|
| 53 |
+
return checkpoint["model"]
|
| 54 |
+
raise KeyError("Checkpoint has neither model_state_dict nor model")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def build_downstream_model(cfg: ModelConfig, device: torch.device, logger: Optional[logging.Logger] = None, checkpoint_path: Optional[str] = None, from_scratch: bool = False, use_checkpoint_config: bool = True) -> nn.Module:
|
| 58 |
+
checkpoint = None
|
| 59 |
+
if checkpoint_path and not from_scratch:
|
| 60 |
+
checkpoint = load_checkpoint(checkpoint_path, device)
|
| 61 |
+
if use_checkpoint_config:
|
| 62 |
+
apply_checkpoint_config(cfg, checkpoint.get("config", {}))
|
| 63 |
+
if logger:
|
| 64 |
+
logger.info("Backbone config restored from checkpoint: %s", checkpoint.get("config", {}))
|
| 65 |
+
if cfg.model_type != "mamba":
|
| 66 |
+
raise ValueError("This cleaned Flexibrain build currently keeps only the Mamba downstream path")
|
| 67 |
+
backbone = build_mamba_backbone(cfg, device=device, dtype=torch.float32)
|
| 68 |
+
if checkpoint is not None:
|
| 69 |
+
state = state_dict_from_checkpoint(checkpoint)
|
| 70 |
+
try:
|
| 71 |
+
backbone.load_state_dict(state, strict=True)
|
| 72 |
+
if logger:
|
| 73 |
+
logger.info("Loaded pretrained backbone strictly from %s", checkpoint_path)
|
| 74 |
+
except RuntimeError as exc:
|
| 75 |
+
incompatible = backbone.load_state_dict(state, strict=False)
|
| 76 |
+
backward_markers = ["_b", "conv1d_b", "x_proj_b", "dt_proj_b", "A_b_log", "D_b"]
|
| 77 |
+
missing = list(incompatible.missing_keys)
|
| 78 |
+
only_backward = missing and all(any(marker in key for marker in backward_markers) for key in missing)
|
| 79 |
+
if not only_backward or incompatible.unexpected_keys:
|
| 80 |
+
raise exc
|
| 81 |
+
if logger:
|
| 82 |
+
logger.warning("Strict load missed %d backward-scan BiMamba keys; loaded checkpoint with strict=False compatibility", len(missing))
|
| 83 |
+
elif logger:
|
| 84 |
+
logger.info("Backbone initialized from scratch")
|
| 85 |
+
if cfg.head_type == "transformer":
|
| 86 |
+
model = MambaJEPAClassifier(
|
| 87 |
+
backbone=backbone,
|
| 88 |
+
num_classes=cfg.num_classes,
|
| 89 |
+
head_depth=cfg.head_depth,
|
| 90 |
+
head_num_heads=cfg.head_num_heads,
|
| 91 |
+
head_mlp_ratio=cfg.head_mlp_ratio,
|
| 92 |
+
head_proj_drop=cfg.head_proj_drop,
|
| 93 |
+
head_drop_path=cfg.head_drop_path,
|
| 94 |
+
mlp_hidden=cfg.mlp_hidden,
|
| 95 |
+
mlp_depth=cfg.mlp_depth,
|
| 96 |
+
mlp_dropout=cfg.mlp_dropout,
|
| 97 |
+
freeze_backbone=cfg.freeze_backbone,
|
| 98 |
+
device=device,
|
| 99 |
+
)
|
| 100 |
+
elif cfg.head_type == "avgpool":
|
| 101 |
+
model = MambaJEPAClassifierAvgPool(backbone=backbone, num_classes=cfg.num_classes, mlp_hidden=cfg.mlp_hidden, mlp_depth=cfg.mlp_depth, mlp_dropout=cfg.mlp_dropout, freeze_backbone=cfg.freeze_backbone, device=device)
|
| 102 |
+
else:
|
| 103 |
+
raise ValueError(f"Unknown head_type: {cfg.head_type}")
|
| 104 |
+
return model.to(device)
|
flexibrain/models/layers/__init__.py
ADDED
|
File without changes
|
flexibrain/models/layers/moe.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class CondLoRA(nn.Module):
|
| 6 |
+
def __init__(self, dim, cond_in, rank=8, cond_hidden=32, device=None, dtype=None):
|
| 7 |
+
super().__init__()
|
| 8 |
+
fk={}
|
| 9 |
+
if device is not None: fk["device"]=device
|
| 10 |
+
if dtype is not None: fk["dtype"]=dtype
|
| 11 |
+
self.U = nn.Parameter(torch.randn(dim, rank, **fk)*0.02)
|
| 12 |
+
self.V = nn.Parameter(torch.randn(dim, rank, **fk)*0.02)
|
| 13 |
+
self.cond = nn.Sequential(
|
| 14 |
+
nn.LayerNorm(cond_in),
|
| 15 |
+
nn.Linear(cond_in, cond_hidden, **fk), nn.GELU(),
|
| 16 |
+
nn.Linear(cond_hidden, rank, **fk)
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
def forward(self, x, cond_vec, attn_mask=None): # x:[B,L,D], cond_vec:[B,C]
|
| 20 |
+
B,L,D = x.shape
|
| 21 |
+
a = self.cond(cond_vec) # [B,r]
|
| 22 |
+
xU = torch.einsum('bld,dr->blr', x, self.U) # [B,L,r]
|
| 23 |
+
add = torch.einsum('blr,br,dr->bld', xU, a, self.V) # [B,L,D]
|
| 24 |
+
y = x + add
|
| 25 |
+
if attn_mask is not None:
|
| 26 |
+
valid = (~attn_mask).unsqueeze(-1)
|
| 27 |
+
y = torch.where(valid, y, x)
|
| 28 |
+
return y
|
| 29 |
+
|
| 30 |
+
class ResoPrior_E(nn.Module):
|
| 31 |
+
def __init__(self, k_space=0.5, k_time=0.5, gamma=0.5):
|
| 32 |
+
super().__init__()
|
| 33 |
+
self.ks, self.kt, self.g = k_space, k_time, gamma
|
| 34 |
+
def forward(self, rxyztr):
|
| 35 |
+
rx, ry, rz, tr = torch.clamp(rxyztr, 1e-6).unbind(-1)
|
| 36 |
+
sig = torch.stack([self.ks*rx, self.ks*ry, self.ks*rz, self.kt*tr], dim=-1) # [B,4]
|
| 37 |
+
snr = (rx*ry*rz) / (tr ** self.g + 1e-6) # [B]
|
| 38 |
+
z = torch.cat([torch.log(sig+1e-6), torch.log(snr+1e-6).unsqueeze(-1)], dim=-1) # [B,5]
|
| 39 |
+
return z
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class ExpertMLP(nn.Module):
|
| 43 |
+
def __init__(self, dim, hidden_dim=None, device=None, dtype=None):
|
| 44 |
+
super().__init__()
|
| 45 |
+
hidden_dim = hidden_dim or dim * 4
|
| 46 |
+
factory_kwargs = {"device": device, "dtype": dtype}
|
| 47 |
+
self.fc1 = nn.Linear(dim, hidden_dim, **factory_kwargs)
|
| 48 |
+
self.act = nn.GELU()
|
| 49 |
+
self.fc2 = nn.Linear(hidden_dim, dim, **factory_kwargs)
|
| 50 |
+
|
| 51 |
+
def forward(self, x):
|
| 52 |
+
return self.fc2(self.act(self.fc1(x)))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class MoE(nn.Module):
|
| 56 |
+
def __init__(self,
|
| 57 |
+
dim,
|
| 58 |
+
hidden_dim=None,
|
| 59 |
+
num_indep=3,
|
| 60 |
+
aux_loss_coef=0.00,
|
| 61 |
+
device=None,
|
| 62 |
+
dtype=None,
|
| 63 |
+
load_balance_coef: float = 0.01,
|
| 64 |
+
use_res_cond: bool = False,
|
| 65 |
+
cond_dim: int = 5,
|
| 66 |
+
cond_hidden_dim: int = 16,
|
| 67 |
+
cond_tanh_scale: float = 0.5):
|
| 68 |
+
super().__init__()
|
| 69 |
+
factory_kwargs = {"device": device, "dtype": dtype}
|
| 70 |
+
|
| 71 |
+
self.num_shared = 1
|
| 72 |
+
self.num_indep = int(num_indep)
|
| 73 |
+
self.num_experts = self.num_shared + self.num_indep
|
| 74 |
+
self.aux_loss_coef = float(aux_loss_coef)
|
| 75 |
+
self.load_balance_coef = load_balance_coef
|
| 76 |
+
|
| 77 |
+
experts = [ExpertMLP(dim, hidden_dim, **factory_kwargs)]
|
| 78 |
+
for _ in range(self.num_indep):
|
| 79 |
+
experts.append(ExpertMLP(dim, hidden_dim, **factory_kwargs))
|
| 80 |
+
self.experts = nn.ModuleList(experts)
|
| 81 |
+
|
| 82 |
+
self.router_token = nn.Linear(dim, self.num_experts, bias=False, **factory_kwargs)
|
| 83 |
+
|
| 84 |
+
self.use_res_cond = bool(use_res_cond)
|
| 85 |
+
self.cond_tanh_scale = float(cond_tanh_scale)
|
| 86 |
+
if self.use_res_cond:
|
| 87 |
+
self.cond_proj = nn.Sequential(
|
| 88 |
+
nn.LayerNorm(cond_dim, **factory_kwargs),
|
| 89 |
+
nn.Linear(cond_dim, cond_hidden_dim, **factory_kwargs),
|
| 90 |
+
nn.GELU(),
|
| 91 |
+
nn.LayerNorm(cond_hidden_dim, **factory_kwargs),
|
| 92 |
+
)
|
| 93 |
+
self.router_scale = nn.Linear(cond_hidden_dim, self.num_experts, bias=False, **factory_kwargs)
|
| 94 |
+
self.router_bias = nn.Linear(cond_hidden_dim, self.num_experts, bias=False, **factory_kwargs)
|
| 95 |
+
else:
|
| 96 |
+
self.router_scale = None
|
| 97 |
+
self.router_bias = None
|
| 98 |
+
|
| 99 |
+
self.use_router_film = False
|
| 100 |
+
if self.use_router_film and self.use_res_cond:
|
| 101 |
+
self.film_gamma = nn.Linear(cond_hidden_dim, dim, **factory_kwargs)
|
| 102 |
+
self.film_beta = nn.Linear(cond_hidden_dim, dim, **factory_kwargs)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def forward(self, x, attn_mask=None, cond_vec: torch.Tensor = None, return_gates: bool = False):
|
| 106 |
+
"""
|
| 107 |
+
x: [B, L, D]
|
| 108 |
+
attn_mask: [B, L] True=pad, False=valid
|
| 109 |
+
cond_venc: [B, 3]
|
| 110 |
+
return_gates
|
| 111 |
+
return: y: [B, L, D], aux: scalar, (gates: [B, L, E] if return_gates=True)
|
| 112 |
+
"""
|
| 113 |
+
B, L, D = x.shape
|
| 114 |
+
|
| 115 |
+
if self.use_res_cond:
|
| 116 |
+
cond = self.cond_proj(cond_vec) # [B,cond_dim]
|
| 117 |
+
if self.use_router_film:
|
| 118 |
+
gamma = torch.tanh(self.film_gamma(cond)) # [B,D]
|
| 119 |
+
beta = self.film_beta(cond)
|
| 120 |
+
x = x * (1 + 0.3 * gamma.unsqueeze(1)) + 0.3 * beta.unsqueeze(1)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
token_logits = self.router_token(x) # [B, L, E]
|
| 124 |
+
|
| 125 |
+
if self.use_res_cond:
|
| 126 |
+
scale = torch.tanh(self.router_scale(cond)) # [B,E]
|
| 127 |
+
bias = self.router_bias(cond) # [B,E]
|
| 128 |
+
token_logits = token_logits * (1 + self.cond_tanh_scale * scale.unsqueeze(1)) \
|
| 129 |
+
+ bias.unsqueeze(1) # [B,L,E]
|
| 130 |
+
|
| 131 |
+
gates = torch.softmax(token_logits, dim=-1) # [B, L, E]
|
| 132 |
+
|
| 133 |
+
if attn_mask is not None:
|
| 134 |
+
valid = ~attn_mask # [B, L]
|
| 135 |
+
gates = gates * valid.unsqueeze(-1)
|
| 136 |
+
gates = gates / gates.sum(dim=-1, keepdim=True).clamp_min(1e-6)
|
| 137 |
+
|
| 138 |
+
expert_outs = torch.stack([expert(x) for expert in self.experts], dim=-2) # [B, L, E, D]
|
| 139 |
+
y = (gates.unsqueeze(-1) * expert_outs).sum(dim=-2) # [B, L, D]
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
imp = gates.sum(dim=(0, 1)) # [E]
|
| 143 |
+
imp = imp / imp.sum().clamp_min(1e-6)
|
| 144 |
+
uniform = torch.full_like(imp, 1.0 / self.num_experts)
|
| 145 |
+
load_balance_loss = ((imp - uniform) ** 2).sum() * self.load_balance_coef
|
| 146 |
+
|
| 147 |
+
aux = x.new_zeros(())
|
| 148 |
+
if self.aux_loss_coef > 0.0:
|
| 149 |
+
imp = gates.sum(dim=(0, 1)) # [E]
|
| 150 |
+
imp = imp / imp.sum().clamp_min(1e-6)
|
| 151 |
+
uniform = torch.full_like(imp, 1.0 / self.num_experts)
|
| 152 |
+
aux = ((imp - uniform) ** 2).sum() * self.aux_loss_coef
|
| 153 |
+
|
| 154 |
+
if return_gates:
|
| 155 |
+
return y, load_balance_loss, gates
|
| 156 |
+
return y, load_balance_loss
|
flexibrain/models/layers/pos_embed.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from typing import Tuple
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def stape_patch_world_coords_physical(
|
| 9 |
+
X:int, Y:int, Z:int,
|
| 10 |
+
kx:int, ky:int, kz:int,
|
| 11 |
+
affine: torch.Tensor,
|
| 12 |
+
rho_mm: Tuple[float, float, float],
|
| 13 |
+
device=None, dtype=None
|
| 14 |
+
):
|
| 15 |
+
if device is None: device = affine.device
|
| 16 |
+
if dtype is None: dtype = torch.float32
|
| 17 |
+
|
| 18 |
+
A = affine[:3, :3].to(device=device, dtype=dtype) # [3,3]
|
| 19 |
+
t = affine[:3, 3].to(device=device, dtype=dtype) # [3]
|
| 20 |
+
|
| 21 |
+
Lx, Ly, Lz = X//kx, Y//ky, Z//kz
|
| 22 |
+
icx = torch.arange(Lx, device=device, dtype=dtype)*kx + (kx-1)*0.5
|
| 23 |
+
icy = torch.arange(Ly, device=device, dtype=dtype)*ky + (ky-1)*0.5
|
| 24 |
+
icz = torch.arange(Lz, device=device, dtype=dtype)*kz + (kz-1)*0.5
|
| 25 |
+
|
| 26 |
+
gx, gy, gz = torch.meshgrid(icx, icy, icz, indexing='ij') # [Lx,Ly,Lz]
|
| 27 |
+
idx = torch.stack([gx, gy, gz], dim=-1).reshape(-1, 3) # [N,3]
|
| 28 |
+
coords = idx @ A.T + t # [N,3]
|
| 29 |
+
return coords
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class FixedSinCos3DPE(nn.Module):
|
| 34 |
+
def __init__(self, embed_dim:int, num_freq:int=12,
|
| 35 |
+
space_scale:float=1.0, learnable_proj: bool=True):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.embed_dim = embed_dim
|
| 38 |
+
self.num_freq = num_freq
|
| 39 |
+
freq = torch.exp(torch.linspace(0, math.log(10000.0), num_freq)) / 10000.0
|
| 40 |
+
self.register_buffer('freq', freq) # [num_freq]
|
| 41 |
+
self.space_scale = space_scale
|
| 42 |
+
in_dim = 3 * 2 * num_freq
|
| 43 |
+
self.proj = nn.Linear(in_dim, embed_dim, bias=False) if learnable_proj else nn.Identity()
|
| 44 |
+
|
| 45 |
+
def forward(self, xyz: torch.Tensor):
|
| 46 |
+
"""
|
| 47 |
+
xyz: [B, L, 3]
|
| 48 |
+
return: [B, L, embed_dim]
|
| 49 |
+
"""
|
| 50 |
+
B, L, _ = xyz.shape
|
| 51 |
+
x = xyz[..., 0] * self.space_scale
|
| 52 |
+
y = xyz[..., 1] * self.space_scale
|
| 53 |
+
z = xyz[..., 2] * self.space_scale
|
| 54 |
+
|
| 55 |
+
freq = self.freq.to(device=xyz.device, dtype=xyz.dtype)
|
| 56 |
+
|
| 57 |
+
def enc(u):
|
| 58 |
+
u = u[..., None] * freq # [B,L,num_freq]
|
| 59 |
+
return torch.cat([torch.sin(u), torch.cos(u)], dim=-1) # [B,L,2*num_freq]
|
| 60 |
+
|
| 61 |
+
feats = torch.cat([enc(x), enc(y), enc(z)], dim=-1) # [B,L, 3*2*num_freq]
|
| 62 |
+
return self.proj(feats) # [B,L,C]
|
flexibrain/models/layers/stape.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# stape_time_to_space.py
|
| 2 |
+
import math
|
| 3 |
+
from collections import defaultdict
|
| 4 |
+
from typing import Dict, List, Sequence, Tuple
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
|
| 10 |
+
from flexibrain.models.layers.pos_embed import FixedSinCos3DPE, stape_patch_world_coords_physical
|
| 11 |
+
from flexibrain.utils.weight_resize import pi_resize_weight_1d, pi_resize_weight_3d
|
| 12 |
+
|
| 13 |
+
class STAPE4D_TimeToSpace(nn.Module):
|
| 14 |
+
"""
|
| 15 |
+
input:
|
| 16 |
+
x: (B, 96, 96, 96, T_max)
|
| 17 |
+
meta: {subject_idx: {"voxel": (vx,vy,vz) mm, "tr": float s}}
|
| 18 |
+
orig_T
|
| 19 |
+
affine
|
| 20 |
+
output:
|
| 21 |
+
tokens: (B, L_max, D_out)
|
| 22 |
+
attn_mask:(B, L_max) True=padding
|
| 23 |
+
lengths: List[int]
|
| 24 |
+
"""
|
| 25 |
+
def __init__(self,
|
| 26 |
+
d_mid: int = 128,
|
| 27 |
+
d_out: int = 256,
|
| 28 |
+
kt_base: int = 6,
|
| 29 |
+
kx_base: int = 6,
|
| 30 |
+
ky_base: int = 6,
|
| 31 |
+
kz_base: int = 6,
|
| 32 |
+
tau_seconds: float = 6.0,
|
| 33 |
+
rho_mm: Tuple[float, float, float] = (12., 12., 12.),
|
| 34 |
+
):
|
| 35 |
+
super().__init__()
|
| 36 |
+
self.Dm = d_mid
|
| 37 |
+
self.Do = d_out
|
| 38 |
+
self.kt0, self.kx0, self.ky0, self.kz0 = kt_base, kx_base, ky_base, kz_base
|
| 39 |
+
self.tau = float(tau_seconds)
|
| 40 |
+
self.rho = tuple(float(r) for r in rho_mm)
|
| 41 |
+
|
| 42 |
+
# time based kernal [Dm, 1, kt0]
|
| 43 |
+
self.w_t_first_base = nn.Parameter(
|
| 44 |
+
torch.randn(d_mid, 1, kt_base) * (1.0 / (1 * kt_base)) ** 0.5
|
| 45 |
+
)
|
| 46 |
+
self.b_t_first = nn.Parameter(torch.zeros(d_mid))
|
| 47 |
+
|
| 48 |
+
# space base kernal: [Do, Dm, kx0, ky0, kz0]
|
| 49 |
+
self.w_xyz_after_base = nn.Parameter(
|
| 50 |
+
torch.randn(d_out, d_mid, kx_base, ky_base, kz_base) *
|
| 51 |
+
(1.0 / (d_mid * kx_base * ky_base * kz_base)) ** 0.5
|
| 52 |
+
)
|
| 53 |
+
self.b_xyz_after = nn.Parameter(torch.zeros(d_out))
|
| 54 |
+
|
| 55 |
+
self._cache_t = {} # key=(kt,dtype) -> w_t_first
|
| 56 |
+
self._cache_xyz = {} # key=(kx,ky,kz,dtype) -> w_xyz_after
|
| 57 |
+
|
| 58 |
+
# physical pe
|
| 59 |
+
self.pos_embed = FixedSinCos3DPE(
|
| 60 |
+
embed_dim=d_out,
|
| 61 |
+
num_freq=12,
|
| 62 |
+
space_scale=0.01,
|
| 63 |
+
learnable_proj=True
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
@torch.no_grad()
|
| 67 |
+
def _k_from_meta(self, tr: float, voxel: Tuple[float, float, float]) -> Tuple[int, int, int, int]:
|
| 68 |
+
vx, vy, vz = voxel
|
| 69 |
+
if tr <= 0 or not math.isfinite(float(tr)):
|
| 70 |
+
raise ValueError(f"TR must be positive for STAPE kernel sizing, got {tr!r}")
|
| 71 |
+
if any(v <= 0 or not math.isfinite(float(v)) for v in (vx, vy, vz)):
|
| 72 |
+
raise ValueError(f"Voxel spacing must be positive for STAPE kernel sizing, got {voxel!r}")
|
| 73 |
+
kt = max(1, round(self.tau / tr))
|
| 74 |
+
kx = max(1, round(self.rho[0] / vx))
|
| 75 |
+
ky = max(1, round(self.rho[1] / vy))
|
| 76 |
+
kz = max(1, round(self.rho[2] / vz))
|
| 77 |
+
return int(kt), int(kx), int(ky), int(kz)
|
| 78 |
+
|
| 79 |
+
def _get_wt_first(self, kt:int, device, dtype):
|
| 80 |
+
wt = pi_resize_weight_1d(self.w_t_first_base.to(dtype), kt) # [Dm,1,kt]
|
| 81 |
+
return wt.to(device)
|
| 82 |
+
|
| 83 |
+
def _get_wxyz_after(self, kx:int, ky:int, kz:int, device, dtype):
|
| 84 |
+
w = pi_resize_weight_3d(self.w_xyz_after_base.to(dtype), kx, ky, kz) # [Do,Dm,kx,ky,kz]
|
| 85 |
+
return w.to(device)
|
| 86 |
+
|
| 87 |
+
@staticmethod
|
| 88 |
+
def _detect_true_T(x_b: torch.Tensor) -> int:
|
| 89 |
+
|
| 90 |
+
with torch.no_grad():
|
| 91 |
+
s = x_b.abs().sum(dim=(0,1,2))
|
| 92 |
+
nz = torch.nonzero(s > 0, as_tuple=False)
|
| 93 |
+
if nz.numel() == 0:
|
| 94 |
+
return 0
|
| 95 |
+
return int(nz.max().item() + 1)
|
| 96 |
+
|
| 97 |
+
@staticmethod
|
| 98 |
+
def _spatial_keep_mask_alltime(x_b: torch.Tensor, kx:int, ky:int, kz:int, T_true:int) -> torch.Tensor:
|
| 99 |
+
X=Y=Z=96
|
| 100 |
+
Lx, Ly, Lz = X//kx, Y//ky, Z//kz
|
| 101 |
+
if T_true == 0:
|
| 102 |
+
return torch.zeros(Lx*Ly*Lz, dtype=torch.bool, device=x_b.device)
|
| 103 |
+
vol = (x_b[:,:,:,:T_true] != 0).any(dim=-1).float() # [96,96,96] -> 1/0
|
| 104 |
+
vol = vol[:Lx*kx, :Ly*ky, :Lz*kz].unsqueeze(0).unsqueeze(0) # [1,1,X,Y,Z]
|
| 105 |
+
keep = F.max_pool3d(vol, kernel_size=(kx,ky,kz), stride=(kx,ky,kz)) > 0 # [1,1,Lx,Ly,Lz]
|
| 106 |
+
return keep.squeeze(0).squeeze(0).reshape(-1)
|
| 107 |
+
|
| 108 |
+
def _compute_spatial_coords_for_group(self,
|
| 109 |
+
group_idxs: List[int],
|
| 110 |
+
affines: List[torch.Tensor],
|
| 111 |
+
kx: int, ky: int, kz: int,
|
| 112 |
+
device: torch.device) -> torch.Tensor:
|
| 113 |
+
"""
|
| 114 |
+
compute patch physical coordinate
|
| 115 |
+
"""
|
| 116 |
+
G = len(group_idxs)
|
| 117 |
+
X, Y, Z = 96, 96, 96
|
| 118 |
+
Lx, Ly, Lz = X//kx, Y//ky, Z//kz
|
| 119 |
+
|
| 120 |
+
coords_list = []
|
| 121 |
+
for g, affine in enumerate(affines):
|
| 122 |
+
coords = stape_patch_world_coords_physical(
|
| 123 |
+
X=X, Y=Y, Z=Z,
|
| 124 |
+
kx=kx, ky=ky, kz=kz,
|
| 125 |
+
affine=affine,
|
| 126 |
+
rho_mm=self.rho
|
| 127 |
+
) # [Lx*Ly*Lz, 3]
|
| 128 |
+
coords_list.append(coords)
|
| 129 |
+
|
| 130 |
+
# [G, Lx*Ly*Lz, 3]
|
| 131 |
+
coords_batch = torch.stack(coords_list, dim=0)
|
| 132 |
+
return coords_batch.to(device)
|
| 133 |
+
|
| 134 |
+
def _add_positional_encoding(self,
|
| 135 |
+
tokens_all: torch.Tensor, # [G, Lx*Ly*Lz, Do]
|
| 136 |
+
group_idxs: List[int],
|
| 137 |
+
affines: List[torch.Tensor],
|
| 138 |
+
kx: int, ky: int, kz: int) -> torch.Tensor:
|
| 139 |
+
device = tokens_all.device
|
| 140 |
+
|
| 141 |
+
coords = self._compute_spatial_coords_for_group(
|
| 142 |
+
group_idxs, affines, kx, ky, kz, device
|
| 143 |
+
) # [G, Lx*Ly*Lz, 3]
|
| 144 |
+
|
| 145 |
+
pos_encoding = self.pos_embed(coords) # [G, Lx*Ly*Lz, Do]
|
| 146 |
+
pos_encoding = pos_encoding.to(tokens_all.dtype)
|
| 147 |
+
tokens_with_pos = tokens_all + pos_encoding
|
| 148 |
+
|
| 149 |
+
return tokens_with_pos, pos_encoding
|
| 150 |
+
|
| 151 |
+
def _run_group_time_first(self,
|
| 152 |
+
x_group: torch.Tensor, # [G,96,96,96,T_max]
|
| 153 |
+
orig_Ts: List[int],
|
| 154 |
+
kt:int, kx:int, ky:int, kz:int,
|
| 155 |
+
group_idxs: List[int],
|
| 156 |
+
affines: List[torch.Tensor],
|
| 157 |
+
return_grid_info: bool = False) -> Tuple[List[torch.Tensor], List[int], Dict]:
|
| 158 |
+
device, dtype = x_group.device, x_group.dtype
|
| 159 |
+
G, X, Y, Z, T_max = x_group.shape
|
| 160 |
+
assert X==96 and Y==96 and Z==96
|
| 161 |
+
|
| 162 |
+
T_true_max = max(orig_Ts) if len(orig_Ts)>0 else 0
|
| 163 |
+
T_pad = math.ceil(T_true_max / kt) * kt
|
| 164 |
+
T_prime = T_pad // kt
|
| 165 |
+
|
| 166 |
+
w_t = self._get_wt_first(kt, device, dtype) # [Dm,1,kt]
|
| 167 |
+
|
| 168 |
+
xg = x_group.clone()
|
| 169 |
+
if T_max < T_pad:
|
| 170 |
+
pad_len = T_pad - T_max
|
| 171 |
+
xg = F.pad(xg, (0, pad_len), mode='constant', value=0.0) # [G,96,96,96,T_pad]
|
| 172 |
+
xg = xg[..., :T_pad]
|
| 173 |
+
|
| 174 |
+
xlin = xg.permute(0,1,2,3,4).contiguous().view(G*X*Y*Z, 1, T_pad)
|
| 175 |
+
b_t_first = self.b_t_first.to(device=device, dtype=dtype)
|
| 176 |
+
tfeat = F.conv1d(xlin, w_t, bias=b_t_first, stride=kt) # [N, Dm, T′]
|
| 177 |
+
tfeat = tfeat.view(G, X, Y, Z, self.Dm, T_prime).permute(0,4,5,1,2,3).contiguous()
|
| 178 |
+
x_sp_in = tfeat.view(G, self.Dm*T_prime, X, Y, Z) # [G, C_in, X,Y,Z]
|
| 179 |
+
|
| 180 |
+
w_xyz = self._get_wxyz_after(kx,ky,kz, device, dtype) # [Do, Dm, kx,ky,kz]
|
| 181 |
+
w_xyz_rep = w_xyz.repeat(1, T_prime, 1, 1, 1) # [Do, Dm*T′, kx,ky,kz]
|
| 182 |
+
b_xyz_after = self.b_xyz_after.to(device=device, dtype=dtype)
|
| 183 |
+
sfeat = F.conv3d(x_sp_in, w_xyz_rep, bias=b_xyz_after, stride=(kx,ky,kz)) # [G, Do, Lx,Ly,Lz]
|
| 184 |
+
|
| 185 |
+
Lx, Ly, Lz = X//kx, Y//ky, Z//kz
|
| 186 |
+
tokens_all = sfeat.permute(0,2,3,4,1).contiguous().view(G, Lx*Ly*Lz, self.Do)
|
| 187 |
+
|
| 188 |
+
if affines is not None and len(affines) == len(group_idxs):
|
| 189 |
+
tokens_all, pos_group = self._add_positional_encoding(tokens_all, group_idxs, affines, kx, ky, kz)
|
| 190 |
+
|
| 191 |
+
tokens_list, lengths, pos_list = [], [], []
|
| 192 |
+
grid_data = {}
|
| 193 |
+
|
| 194 |
+
for g in range(G):
|
| 195 |
+
T_true = orig_Ts[g]
|
| 196 |
+
|
| 197 |
+
keep_mask = self._spatial_keep_mask_alltime(x_group[g], kx,ky,kz, T_true) # [Lx*Ly*Lz]
|
| 198 |
+
if keep_mask.any():
|
| 199 |
+
toks = tokens_all[g][keep_mask] # [N_valid, Do]
|
| 200 |
+
pe = pos_group[g][keep_mask]
|
| 201 |
+
else:
|
| 202 |
+
toks = tokens_all[g].new_zeros((0, self.Do))
|
| 203 |
+
pe = pos_group[g].new_zeros((0, self.Do))
|
| 204 |
+
|
| 205 |
+
tokens_list.append(toks)
|
| 206 |
+
pos_list.append(pe)
|
| 207 |
+
lengths.append(int(toks.size(0)))
|
| 208 |
+
|
| 209 |
+
if return_grid_info:
|
| 210 |
+
sample_idx = group_idxs[g]
|
| 211 |
+
grid_data[sample_idx] = {
|
| 212 |
+
'Lx': Lx,
|
| 213 |
+
'Ly': Ly,
|
| 214 |
+
'Lz': Lz,
|
| 215 |
+
'kx': kx,
|
| 216 |
+
'ky': ky,
|
| 217 |
+
'kz': kz,
|
| 218 |
+
'keep_mask': keep_mask.cpu(), # [Lx*Ly*Lz] bool
|
| 219 |
+
'grid_to_token_idx': torch.nonzero(keep_mask, as_tuple=False).squeeze(-1).cpu(),
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
return tokens_list, lengths, grid_data, pos_list
|
| 223 |
+
|
| 224 |
+
def forward(self,
|
| 225 |
+
x: torch.Tensor,
|
| 226 |
+
meta: Dict[int, Dict],
|
| 227 |
+
orig_Ts: Sequence[int] = None,
|
| 228 |
+
affines: Sequence[torch.Tensor] = None,
|
| 229 |
+
return_grid_info: bool = False):
|
| 230 |
+
B = x.size(0)
|
| 231 |
+
device, dtype = x.device, x.dtype
|
| 232 |
+
|
| 233 |
+
if orig_Ts is None:
|
| 234 |
+
orig_Ts = [self._detect_true_T(x[b]) for b in range(B)]
|
| 235 |
+
else:
|
| 236 |
+
orig_Ts = [int(t) for t in orig_Ts]
|
| 237 |
+
|
| 238 |
+
if affines is None:
|
| 239 |
+
print("WARNING: not provide affine")
|
| 240 |
+
affines = [torch.eye(4, device=device, dtype=dtype) for _ in range(B)]
|
| 241 |
+
else:
|
| 242 |
+
affines = [aff.to(device=device, dtype=dtype) for aff in affines]
|
| 243 |
+
|
| 244 |
+
groups = defaultdict(list)
|
| 245 |
+
for i in range(B):
|
| 246 |
+
voxel = tuple(meta[i]["voxel"])
|
| 247 |
+
tr = float(meta[i]["tr"])
|
| 248 |
+
group_key = (voxel, tr)
|
| 249 |
+
groups[group_key].append(i)
|
| 250 |
+
|
| 251 |
+
per_sample_tokens: List[torch.Tensor] = [None]*B
|
| 252 |
+
per_sample_pos: List[torch.Tensor] = [None]*B
|
| 253 |
+
lengths: List[int] = [0]*B
|
| 254 |
+
grid_info: Dict[int, Dict] = {}
|
| 255 |
+
|
| 256 |
+
for (voxel, tr), idxs in groups.items():
|
| 257 |
+
kt, kx, ky, kz = self._k_from_meta(tr, voxel)
|
| 258 |
+
|
| 259 |
+
x_group = x[idxs, ...] # [G,96,96,96,T_max]
|
| 260 |
+
Ts_group = [orig_Ts[i] for i in idxs]
|
| 261 |
+
affines_group = [affines[i] for i in idxs]
|
| 262 |
+
|
| 263 |
+
toks, lens, grid_data, pos_list = self._run_group_time_first(
|
| 264 |
+
x_group, Ts_group, kt, kx, ky, kz, idxs, affines_group,
|
| 265 |
+
return_grid_info=return_grid_info
|
| 266 |
+
)
|
| 267 |
+
for g_idx, (tok, ln) in enumerate(zip(toks, lens)):
|
| 268 |
+
loc = idxs[g_idx]
|
| 269 |
+
per_sample_tokens[loc] = tok
|
| 270 |
+
lengths[loc] = ln
|
| 271 |
+
per_sample_pos[loc] = pos_list[g_idx]
|
| 272 |
+
if return_grid_info and loc in grid_data:
|
| 273 |
+
grid_info[loc] = grid_data[loc]
|
| 274 |
+
|
| 275 |
+
L_max = max(lengths) if lengths else 0
|
| 276 |
+
out = x.new_zeros((B, L_max, self.Do))
|
| 277 |
+
pos_out = x.new_zeros((B, L_max, self.Do))
|
| 278 |
+
attn_mask = torch.ones((B, L_max), dtype=torch.bool, device=device)
|
| 279 |
+
|
| 280 |
+
for b, tok in enumerate(per_sample_tokens):
|
| 281 |
+
n = lengths[b]
|
| 282 |
+
if n > 0:
|
| 283 |
+
out[b, :n] = tok
|
| 284 |
+
pos_out[b, :n] = per_sample_pos[b]
|
| 285 |
+
attn_mask[b, :n] = False
|
| 286 |
+
|
| 287 |
+
if return_grid_info:
|
| 288 |
+
return out, attn_mask, lengths, grid_info, pos_out
|
| 289 |
+
else:
|
| 290 |
+
return out, attn_mask, lengths, pos_out
|
flexibrain/models/mamba_blocks.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Mamba sequence blocks used by Flexibrain.
|
| 2 |
+
|
| 3 |
+
References:
|
| 4 |
+
- Mamba: https://github.com/state-spaces/mamba
|
| 5 |
+
- 3D Mamba MAE: https://github.com/ydchen0806/TokenUnify
|
| 6 |
+
|
| 7 |
+
This file keeps only the block factory pieces needed by the Flexibrain
|
| 8 |
+
Mamba-JEPA backbone, instead of vendoring the full upstream training project.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from functools import partial
|
| 12 |
+
import inspect
|
| 13 |
+
import math
|
| 14 |
+
from typing import Optional
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
import torch.nn as nn
|
| 18 |
+
from torch import Tensor
|
| 19 |
+
from timm.models.layers import DropPath
|
| 20 |
+
|
| 21 |
+
from mamba_ssm.modules.mamba_simple import Mamba
|
| 22 |
+
from mamba_ssm.modules.mamba2 import Mamba2
|
| 23 |
+
from mamba_ssm.modules.mha import MHA
|
| 24 |
+
from mamba_ssm.ops.triton.layer_norm import RMSNorm, layer_norm_fn, rms_norm_fn
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class Block(nn.Module):
|
| 28 |
+
def __init__(self, dim, mixer_cls, mlp_cls, norm_cls=nn.LayerNorm, fused_add_norm=False, residual_in_fp32=False, drop_path=0.0):
|
| 29 |
+
super().__init__()
|
| 30 |
+
self.residual_in_fp32 = residual_in_fp32
|
| 31 |
+
self.fused_add_norm = fused_add_norm
|
| 32 |
+
self.norm = norm_cls(dim)
|
| 33 |
+
self.mixer = mixer_cls(dim)
|
| 34 |
+
try:
|
| 35 |
+
self._mixer_kwset = set(inspect.signature(self.mixer.forward).parameters.keys())
|
| 36 |
+
except Exception:
|
| 37 |
+
self._mixer_kwset = set()
|
| 38 |
+
if mlp_cls is not nn.Identity:
|
| 39 |
+
self.norm2 = norm_cls(dim)
|
| 40 |
+
self.mlp = mlp_cls(dim)
|
| 41 |
+
else:
|
| 42 |
+
self.mlp = None
|
| 43 |
+
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
|
| 44 |
+
if self.fused_add_norm:
|
| 45 |
+
assert RMSNorm is not None, "RMSNorm import failed"
|
| 46 |
+
assert isinstance(self.norm, (nn.LayerNorm, RMSNorm))
|
| 47 |
+
|
| 48 |
+
def forward(self, hidden_states: Tensor, residual: Optional[Tensor] = None, inference_params=None, **mixer_kwargs):
|
| 49 |
+
if not self.fused_add_norm:
|
| 50 |
+
residual = (self.drop_path(hidden_states) + residual) if residual is not None else hidden_states
|
| 51 |
+
hidden_states = self.norm(residual.to(dtype=self.norm.weight.dtype))
|
| 52 |
+
if self.residual_in_fp32:
|
| 53 |
+
residual = residual.to(torch.float32)
|
| 54 |
+
else:
|
| 55 |
+
hidden_states, residual = layer_norm_fn(
|
| 56 |
+
self.drop_path(hidden_states), self.norm.weight, self.norm.bias,
|
| 57 |
+
residual=residual, prenorm=True, residual_in_fp32=self.residual_in_fp32,
|
| 58 |
+
eps=self.norm.eps, is_rms_norm=isinstance(self.norm, RMSNorm),
|
| 59 |
+
)
|
| 60 |
+
filtered_kwargs = {k: v for k, v in mixer_kwargs.items() if k in self._mixer_kwset}
|
| 61 |
+
hidden_states = self.mixer(hidden_states, inference_params=inference_params, **filtered_kwargs)
|
| 62 |
+
if self.mlp is not None:
|
| 63 |
+
if not self.fused_add_norm:
|
| 64 |
+
residual = self.drop_path(hidden_states) + residual
|
| 65 |
+
residual = self.norm2(residual.to(dtype=self.norm2.weight.dtype))
|
| 66 |
+
if self.residual_in_fp32:
|
| 67 |
+
residual = residual.to(torch.float32)
|
| 68 |
+
else:
|
| 69 |
+
hidden_states, residual = layer_norm_fn(
|
| 70 |
+
self.drop_path(hidden_states), self.norm2.weight, self.norm2.bias,
|
| 71 |
+
residual=residual, prenorm=True, residual_in_fp32=self.residual_in_fp32,
|
| 72 |
+
eps=self.norm2.eps, is_rms_norm=isinstance(self.norm2, RMSNorm),
|
| 73 |
+
)
|
| 74 |
+
hidden_states = self.mlp(hidden_states)
|
| 75 |
+
return hidden_states, residual
|
| 76 |
+
|
| 77 |
+
def allocate_inference_cache(self, batch_size, max_seqlen, dtype=None, **kwargs):
|
| 78 |
+
return self.mixer.allocate_inference_cache(batch_size, max_seqlen, dtype=dtype, **kwargs)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def create_block(d_model, ssm_cfg=None, attn_layer_idx=None, attn_cfg=None, norm_epsilon=1e-5, drop_path=0.0, rms_norm=False, residual_in_fp32=False, fused_add_norm=False, layer_idx=None, device=None, dtype=None, if_bimamba=False, bimamba_type="none", if_devide_out=False, init_layer_scale=None, mixer_type="mamba"):
|
| 82 |
+
if if_bimamba and bimamba_type == "none":
|
| 83 |
+
bimamba_type = "v1"
|
| 84 |
+
if ssm_cfg is None:
|
| 85 |
+
ssm_cfg = {}
|
| 86 |
+
if attn_cfg is None:
|
| 87 |
+
attn_cfg = {}
|
| 88 |
+
factory_kwargs = {"device": device, "dtype": dtype}
|
| 89 |
+
if (attn_layer_idx is None) or (layer_idx not in attn_layer_idx):
|
| 90 |
+
if mixer_type == "mamba":
|
| 91 |
+
mixer_cls = partial(Mamba, layer_idx=layer_idx, init_layer_scale=init_layer_scale, bimamba_type=bimamba_type, if_devide_out=if_devide_out, **ssm_cfg, **factory_kwargs)
|
| 92 |
+
elif mixer_type == "mamba2":
|
| 93 |
+
mixer_cls = partial(Mamba2, layer_idx=layer_idx, **ssm_cfg, **factory_kwargs)
|
| 94 |
+
else:
|
| 95 |
+
raise ValueError(f"Unknown mixer_type: {mixer_type}")
|
| 96 |
+
else:
|
| 97 |
+
mixer_cls = partial(MHA, layer_idx=layer_idx, **attn_cfg, **factory_kwargs)
|
| 98 |
+
norm_cls = partial(nn.LayerNorm if not rms_norm else RMSNorm, eps=norm_epsilon, **factory_kwargs)
|
| 99 |
+
block = Block(d_model, mixer_cls, nn.Identity, norm_cls=norm_cls, drop_path=drop_path, fused_add_norm=fused_add_norm, residual_in_fp32=residual_in_fp32)
|
| 100 |
+
block.layer_idx = layer_idx
|
| 101 |
+
return block
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _init_weights(module, n_layer, initializer_range=0.02, rescale_prenorm_residual=True, n_residuals_per_layer=1):
|
| 105 |
+
if isinstance(module, nn.Linear):
|
| 106 |
+
if module.bias is not None and not getattr(module.bias, "_no_reinit", False):
|
| 107 |
+
nn.init.zeros_(module.bias)
|
| 108 |
+
elif isinstance(module, nn.Embedding):
|
| 109 |
+
nn.init.normal_(module.weight, std=initializer_range)
|
| 110 |
+
if rescale_prenorm_residual:
|
| 111 |
+
for name, p in module.named_parameters():
|
| 112 |
+
if name in ["out_proj.weight", "fc2.weight"]:
|
| 113 |
+
nn.init.kaiming_uniform_(p, a=math.sqrt(5))
|
| 114 |
+
with torch.no_grad():
|
| 115 |
+
p /= math.sqrt(n_residuals_per_layer * n_layer)
|
flexibrain/models/mamba_jepa.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
|
| 4 |
+
# This source code is licensed under the license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
# --------------------------------------------------------
|
| 7 |
+
# References:
|
| 8 |
+
# timm: https://github.com/rwightman/pytorch-image-models/tree/master/timm
|
| 9 |
+
# DeiT: https://github.com/facebookresearch/deit
|
| 10 |
+
# --------------------------------------------------------
|
| 11 |
+
|
| 12 |
+
import copy
|
| 13 |
+
from functools import partial
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
|
| 18 |
+
from timm.models.vision_transformer import DropPath
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
from flexibrain.models.mamba_blocks import RMSNorm, create_block, _init_weights, layer_norm_fn, rms_norm_fn
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
from flexibrain.models.layers.stape import STAPE4D_TimeToSpace
|
| 25 |
+
from flexibrain.models.layers.moe import MoE
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class VolumeMambaJEPA(nn.Module):
|
| 29 |
+
""" JEPA with VisionMamba backbone
|
| 30 |
+
"""
|
| 31 |
+
def __init__(self,
|
| 32 |
+
embed_dim=512,
|
| 33 |
+
depth=24,
|
| 34 |
+
predictor_depth=2,
|
| 35 |
+
ssm_cfg=None,
|
| 36 |
+
encoder_attn_layer_idx=None,
|
| 37 |
+
attn_cfg=None,
|
| 38 |
+
drop_path_rate=0.1,
|
| 39 |
+
norm_epsilon: float = 1e-5,
|
| 40 |
+
rms_norm: bool = False,
|
| 41 |
+
initializer_cfg=None,
|
| 42 |
+
fused_add_norm=True,
|
| 43 |
+
residual_in_fp32=True,
|
| 44 |
+
device=None,
|
| 45 |
+
dtype=None,
|
| 46 |
+
bimamba_type="none",
|
| 47 |
+
if_bimamba=False,
|
| 48 |
+
mixer_type="mamba",
|
| 49 |
+
if_devide_out=False,
|
| 50 |
+
momentum: float = 0.996,
|
| 51 |
+
norm_target: bool = True,
|
| 52 |
+
|
| 53 |
+
**kwargs
|
| 54 |
+
):
|
| 55 |
+
factory_kwargs = {"device": device, "dtype": dtype}
|
| 56 |
+
kwargs.update(factory_kwargs)
|
| 57 |
+
super().__init__()
|
| 58 |
+
|
| 59 |
+
self.embed_dim = embed_dim
|
| 60 |
+
self.residual_in_fp32 = residual_in_fp32
|
| 61 |
+
self.fused_add_norm = fused_add_norm
|
| 62 |
+
self.momentum = float(momentum)
|
| 63 |
+
self.norm_target = bool(norm_target)
|
| 64 |
+
|
| 65 |
+
self.patch_embed = STAPE4D_TimeToSpace(
|
| 66 |
+
d_mid=16,
|
| 67 |
+
d_out=embed_dim,
|
| 68 |
+
kt_base=6,
|
| 69 |
+
kx_base=6,
|
| 70 |
+
ky_base=6,
|
| 71 |
+
kz_base=6,
|
| 72 |
+
tau_seconds=6.0,
|
| 73 |
+
rho_mm=(12.0, 12.0, 12.0),
|
| 74 |
+
)
|
| 75 |
+
if device is not None:
|
| 76 |
+
self.patch_embed = self.patch_embed.to(device=device)
|
| 77 |
+
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)] # stochastic depth decay rule
|
| 78 |
+
inter_dpr = [0.0] + dpr
|
| 79 |
+
self.drop_path = DropPath(drop_path_rate) if drop_path_rate > 0. else nn.Identity()
|
| 80 |
+
self.blocks = nn.ModuleList()
|
| 81 |
+
block_idx = 0
|
| 82 |
+
for i in range(depth):
|
| 83 |
+
self.blocks.append(
|
| 84 |
+
create_block(
|
| 85 |
+
embed_dim,
|
| 86 |
+
ssm_cfg=ssm_cfg,
|
| 87 |
+
attn_layer_idx=encoder_attn_layer_idx,
|
| 88 |
+
attn_cfg=attn_cfg,
|
| 89 |
+
norm_epsilon=norm_epsilon,
|
| 90 |
+
rms_norm=rms_norm,
|
| 91 |
+
residual_in_fp32=residual_in_fp32,
|
| 92 |
+
fused_add_norm=fused_add_norm,
|
| 93 |
+
layer_idx=block_idx,
|
| 94 |
+
bimamba_type=bimamba_type,
|
| 95 |
+
if_bimamba=if_bimamba,
|
| 96 |
+
drop_path=inter_dpr[i],
|
| 97 |
+
if_devide_out=if_devide_out,
|
| 98 |
+
mixer_type=mixer_type,
|
| 99 |
+
**factory_kwargs,
|
| 100 |
+
)
|
| 101 |
+
)
|
| 102 |
+
block_idx += 1
|
| 103 |
+
|
| 104 |
+
self.norm_f = (nn.LayerNorm if not rms_norm else RMSNorm)(
|
| 105 |
+
embed_dim, eps=norm_epsilon, **factory_kwargs
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
self.target_blocks = copy.deepcopy(self.blocks)
|
| 109 |
+
self.target_norm = (nn.LayerNorm if not rms_norm else RMSNorm)(embed_dim, eps=norm_epsilon, **factory_kwargs)
|
| 110 |
+
for p in self.target_blocks.parameters():
|
| 111 |
+
p.requires_grad = False
|
| 112 |
+
for p in self.target_norm.parameters():
|
| 113 |
+
p.requires_grad = False
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# ------- MoE & ResolutionxTR embed-------
|
| 117 |
+
self.moe_aux_coef = float(kwargs.get("moe_aux_coef", 0.1))
|
| 118 |
+
self.moe = MoE(
|
| 119 |
+
dim=embed_dim,
|
| 120 |
+
hidden_dim=embed_dim * 4,
|
| 121 |
+
num_indep=3,
|
| 122 |
+
aux_loss_coef=self.moe_aux_coef,
|
| 123 |
+
device=device,
|
| 124 |
+
dtype=dtype,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
self.mask_token_ctx = nn.Parameter(torch.zeros(1, 1, embed_dim))
|
| 128 |
+
torch.nn.init.normal_(self.mask_token_ctx, std=0.02)
|
| 129 |
+
|
| 130 |
+
self.pred_depth = predictor_depth
|
| 131 |
+
self.pred_dpr = [0.0 for _ in range(self.pred_depth)]
|
| 132 |
+
self.predictor_blocks = nn.ModuleList()
|
| 133 |
+
for i in range(self.pred_depth):
|
| 134 |
+
self.predictor_blocks.append(
|
| 135 |
+
create_block(
|
| 136 |
+
embed_dim,
|
| 137 |
+
ssm_cfg=ssm_cfg,
|
| 138 |
+
attn_layer_idx=None,
|
| 139 |
+
attn_cfg=attn_cfg,
|
| 140 |
+
norm_epsilon=norm_epsilon,
|
| 141 |
+
rms_norm=rms_norm,
|
| 142 |
+
residual_in_fp32=residual_in_fp32,
|
| 143 |
+
fused_add_norm=fused_add_norm,
|
| 144 |
+
layer_idx=i,
|
| 145 |
+
bimamba_type=bimamba_type,
|
| 146 |
+
if_bimamba=if_bimamba,
|
| 147 |
+
drop_path=self.pred_dpr[i],
|
| 148 |
+
if_devide_out=if_devide_out,
|
| 149 |
+
mixer_type=mixer_type,
|
| 150 |
+
**factory_kwargs,
|
| 151 |
+
)
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
self.predictor_norm = (nn.LayerNorm if not rms_norm else RMSNorm)(
|
| 155 |
+
embed_dim, eps=norm_epsilon, **factory_kwargs)
|
| 156 |
+
|
| 157 |
+
self.apply(self._init_linear_ln)
|
| 158 |
+
self.initialize_jepa_weights()
|
| 159 |
+
self.apply(
|
| 160 |
+
partial(
|
| 161 |
+
_init_weights,
|
| 162 |
+
n_layer=depth,
|
| 163 |
+
**(initializer_cfg if initializer_cfg is not None else {}),
|
| 164 |
+
)
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
def _init_linear_ln(self, m):
|
| 168 |
+
if isinstance(m, nn.Linear):
|
| 169 |
+
torch.nn.init.xavier_uniform_(m.weight)
|
| 170 |
+
if m.bias is not None:
|
| 171 |
+
nn.init.constant_(m.bias, 0)
|
| 172 |
+
elif (isinstance(m, nn.LayerNorm) or m.__class__.__name__ == 'RMSNorm') and hasattr(m, 'weight'):
|
| 173 |
+
nn.init.constant_(m.weight, 1.0)
|
| 174 |
+
if hasattr(m, 'bias') and m.bias is not None:
|
| 175 |
+
nn.init.constant_(m.bias, 0.0)
|
| 176 |
+
|
| 177 |
+
def initialize_jepa_weights(self):
|
| 178 |
+
torch.nn.init.normal_(self.mask_token_ctx, std=0.02)
|
| 179 |
+
if hasattr(self, 'predictor_blocks'):
|
| 180 |
+
self.predictor_blocks.apply(self._init_linear_ln)
|
| 181 |
+
if hasattr(self, 'predictor_norm'):
|
| 182 |
+
self._init_linear_ln(self.predictor_norm)
|
| 183 |
+
self.blocks.apply(self._init_linear_ln)
|
| 184 |
+
self._init_linear_ln(self.norm_f)
|
| 185 |
+
|
| 186 |
+
if hasattr(self.patch_embed, 'reset_parameters'):
|
| 187 |
+
self.patch_embed.reset_parameters()
|
| 188 |
+
else:
|
| 189 |
+
for m in self.patch_embed.modules():
|
| 190 |
+
if isinstance(m, nn.Conv1d) or isinstance(m, nn.Conv2d) or isinstance(m, nn.Conv3d):
|
| 191 |
+
torch.nn.init.kaiming_normal_(m.weight, nonlinearity='linear')
|
| 192 |
+
if m.bias is not None:
|
| 193 |
+
nn.init.constant_(m.bias, 0)
|
| 194 |
+
elif isinstance(m, nn.Linear):
|
| 195 |
+
torch.nn.init.xavier_uniform_(m.weight)
|
| 196 |
+
if m.bias is not None:
|
| 197 |
+
nn.init.constant_(m.bias, 0)
|
| 198 |
+
|
| 199 |
+
with torch.no_grad():
|
| 200 |
+
for ps, pt in zip(self.blocks.parameters(), self.target_blocks.parameters()):
|
| 201 |
+
pt.copy_(ps)
|
| 202 |
+
if hasattr(self, 'target_norm'):
|
| 203 |
+
for ps, pt in zip(self.norm_f.parameters(), self.target_norm.parameters()):
|
| 204 |
+
pt.copy_(ps)
|
| 205 |
+
|
| 206 |
+
@torch.no_grad()
|
| 207 |
+
def update_target_encoder(self, m: float = None):
|
| 208 |
+
"""EMA update of the target encoder"""
|
| 209 |
+
m = float(m or self.momentum)
|
| 210 |
+
for ps, pt in zip(self.blocks.parameters(), self.target_blocks.parameters()):
|
| 211 |
+
pt.data.mul_(m).add_(ps.data, alpha=1 - m)
|
| 212 |
+
for ps, pt in zip(self.norm_f.parameters(), self.target_norm.parameters()):
|
| 213 |
+
pt.data.mul_(m).add_(ps.data, alpha=1 - m)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def random_masking(self, x, attn_mask, lengths, mask_ratio):
|
| 217 |
+
"""
|
| 218 |
+
x: [B, Lmax, D], attn_mask: [B, Lmax] (True=pad), lengths: list[int]
|
| 219 |
+
Return:
|
| 220 |
+
x_keep: [B, Lk_max, D]
|
| 221 |
+
mask_full: [B, Lmax] (0=keep, 1=remove; pad 仍为1)
|
| 222 |
+
ids_restore: [B, Lmax]
|
| 223 |
+
attn_keep: [B, Lk_max] (True=pad)
|
| 224 |
+
keep_lengths: list[int]
|
| 225 |
+
ids_keep_pad: [B, Lk_max]
|
| 226 |
+
"""
|
| 227 |
+
N, Lmax, D = x.shape
|
| 228 |
+
device = x.device
|
| 229 |
+
|
| 230 |
+
x_keep_list, mask_list, ids_restore_list = [], [], []
|
| 231 |
+
ids_keep_list, keep_lengths, Lk_max = [], [], 0
|
| 232 |
+
|
| 233 |
+
for i in range(N):
|
| 234 |
+
Li = lengths[i]
|
| 235 |
+
if Li == 0:
|
| 236 |
+
x_keep_list.append(torch.empty(0, D, device=device, dtype=x.dtype))
|
| 237 |
+
mask_list.append(torch.ones(Lmax, device=device))
|
| 238 |
+
ids_restore_list.append(torch.arange(Lmax, device=device))
|
| 239 |
+
ids_keep_list.append(torch.empty(0, dtype=torch.long, device=device))
|
| 240 |
+
keep_lengths.append(0)
|
| 241 |
+
continue
|
| 242 |
+
|
| 243 |
+
Lk = max(1, int(Li * (1 - mask_ratio)))
|
| 244 |
+
keep_lengths.append(Lk)
|
| 245 |
+
Lk_max = max(Lk_max, Lk)
|
| 246 |
+
|
| 247 |
+
noise = torch.rand(Li, device=device)
|
| 248 |
+
ids_shuffle = torch.argsort(noise) # [Li]
|
| 249 |
+
ids_restore_valid = torch.argsort(ids_shuffle)
|
| 250 |
+
ids_keep = ids_shuffle[:Lk]
|
| 251 |
+
|
| 252 |
+
x_keep_list.append(x[i, ids_keep])
|
| 253 |
+
ids_keep_list.append(ids_keep)
|
| 254 |
+
|
| 255 |
+
mask_i = torch.ones(Lmax, device=device)
|
| 256 |
+
valid_mask = torch.ones(Li, device=device)
|
| 257 |
+
valid_mask[:Lk] = 0
|
| 258 |
+
mask_i[:Li] = torch.gather(valid_mask, 0, ids_restore_valid)
|
| 259 |
+
mask_list.append(mask_i)
|
| 260 |
+
|
| 261 |
+
ids_restore_full = torch.arange(Lmax, device=device)
|
| 262 |
+
ids_restore_full[:Li] = ids_restore_valid
|
| 263 |
+
ids_restore_list.append(ids_restore_full)
|
| 264 |
+
|
| 265 |
+
# pad keep tensors
|
| 266 |
+
x_keep = x.new_zeros((N, max(1, Lk_max), D))
|
| 267 |
+
ids_keep_pad = torch.full((N, max(1, Lk_max)), -1, dtype=torch.long, device=device)
|
| 268 |
+
attn_keep = torch.ones(N, max(1, Lk_max), dtype=torch.bool, device=device)
|
| 269 |
+
|
| 270 |
+
for i, (xi, ik) in enumerate(zip(x_keep_list, ids_keep_list)):
|
| 271 |
+
if xi.numel() > 0:
|
| 272 |
+
Lk = xi.size(0)
|
| 273 |
+
x_keep[i, :Lk] = xi
|
| 274 |
+
ids_keep_pad[i, :Lk] = ik
|
| 275 |
+
attn_keep[i, :Lk] = False
|
| 276 |
+
|
| 277 |
+
mask_full = torch.stack(mask_list, dim=0)
|
| 278 |
+
ids_restore = torch.stack(ids_restore_list, dim=0)
|
| 279 |
+
|
| 280 |
+
return x_keep, mask_full, ids_restore, attn_keep, keep_lengths, ids_keep_pad
|
| 281 |
+
|
| 282 |
+
def _build_context_visible(self, x_keep, attn_keep):
|
| 283 |
+
|
| 284 |
+
return x_keep, attn_keep
|
| 285 |
+
|
| 286 |
+
def _build_target_masked(self, x_full, mask_full, lengths):
|
| 287 |
+
"""
|
| 288 |
+
x_tgt_pad: [B, Lt_max, D]
|
| 289 |
+
attn_tgt: [B, Lt_max] (True=padding)
|
| 290 |
+
tgt_lengths: list[int]
|
| 291 |
+
"""
|
| 292 |
+
B, Lmax, D = x_full.shape
|
| 293 |
+
device, dtype = x_full.device, x_full.dtype
|
| 294 |
+
per_sample, tgt_lengths, Lt_max = [], [], 0
|
| 295 |
+
for i in range(B):
|
| 296 |
+
Li = lengths[i]
|
| 297 |
+
if Li == 0:
|
| 298 |
+
per_sample.append(x_full.new_empty((0, D)))
|
| 299 |
+
tgt_lengths.append(0)
|
| 300 |
+
continue
|
| 301 |
+
sel = (mask_full[i, :Li] == 1) if mask_full.dtype != torch.bool else mask_full[i, :Li]
|
| 302 |
+
xi = x_full[i, :Li][sel]
|
| 303 |
+
per_sample.append(xi)
|
| 304 |
+
tgt_lengths.append(xi.size(0))
|
| 305 |
+
Lt_max = max(Lt_max, xi.size(0))
|
| 306 |
+
|
| 307 |
+
x_tgt_pad = x_full.new_zeros((B, Lt_max, D))
|
| 308 |
+
attn_tgt = torch.ones(B, Lt_max, dtype=torch.bool, device=device)
|
| 309 |
+
for i, xi in enumerate(per_sample):
|
| 310 |
+
if xi.numel() > 0:
|
| 311 |
+
Lti = xi.size(0)
|
| 312 |
+
x_tgt_pad[i, :Lti] = xi
|
| 313 |
+
attn_tgt[i, :Lti] = False
|
| 314 |
+
|
| 315 |
+
return x_tgt_pad, attn_tgt, tgt_lengths
|
| 316 |
+
|
| 317 |
+
def _run_blocks(self, x, attn_mask, blocks, norm_layer, inference_params=None, unpack_buffer=None):
|
| 318 |
+
residual = None
|
| 319 |
+
# x, seq_idx, idx_info = pack_batch(x, attn_mask)
|
| 320 |
+
hidden_states = x
|
| 321 |
+
for layer in blocks:
|
| 322 |
+
hidden_states, residual = layer(
|
| 323 |
+
hidden_states, residual, inference_params=inference_params,
|
| 324 |
+
attn_mask=attn_mask
|
| 325 |
+
)
|
| 326 |
+
# hidden_states, residual = layer(
|
| 327 |
+
# hidden_states, residual, inference_params=inference_params,
|
| 328 |
+
# seq_idx=seq_idx
|
| 329 |
+
# )
|
| 330 |
+
fused_norm_available = layer_norm_fn is not None and (
|
| 331 |
+
RMSNorm is None or not isinstance(norm_layer, RMSNorm) or rms_norm_fn is not None
|
| 332 |
+
)
|
| 333 |
+
if not self.fused_add_norm or not fused_norm_available:
|
| 334 |
+
if residual is None:
|
| 335 |
+
residual = hidden_states
|
| 336 |
+
else:
|
| 337 |
+
residual = residual + self.drop_path(hidden_states)
|
| 338 |
+
hidden_states = norm_layer(residual.to(dtype=norm_layer.weight.dtype))
|
| 339 |
+
else:
|
| 340 |
+
fused_add_norm_fn = rms_norm_fn if RMSNorm is not None and isinstance(norm_layer, RMSNorm) else layer_norm_fn
|
| 341 |
+
hidden_states = fused_add_norm_fn(
|
| 342 |
+
self.drop_path(hidden_states),
|
| 343 |
+
norm_layer.weight,
|
| 344 |
+
norm_layer.bias,
|
| 345 |
+
residual=residual,
|
| 346 |
+
prenorm=False,
|
| 347 |
+
residual_in_fp32=self.residual_in_fp32,
|
| 348 |
+
eps=norm_layer.eps,
|
| 349 |
+
)
|
| 350 |
+
# hidden_states = unpack_batch(hidden_states, idx_info, unpack_buffer)
|
| 351 |
+
return hidden_states
|
| 352 |
+
|
| 353 |
+
def forward(self, x, mask_ratio=0.6, meta=None, orig_Ts=None, affines=None, inference_params=None, return_moe_features=False):
|
| 354 |
+
|
| 355 |
+
x_full, attn_pad, lengths, _ = self.patch_embed(x, meta, orig_Ts, affines, return_grid_info=False)
|
| 356 |
+
|
| 357 |
+
# random mask
|
| 358 |
+
x_keep, mask_full, ids_restore, attn_keep, keep_lengths, ids_keep_pad = self.random_masking(x_full, attn_pad, lengths, mask_ratio)
|
| 359 |
+
|
| 360 |
+
# build ctx visible
|
| 361 |
+
x_ctx, attn_ctx = self._build_context_visible(x_keep, attn_keep)
|
| 362 |
+
ctx_keep_out = self._run_blocks(x_ctx, attn_ctx,
|
| 363 |
+
blocks=self.blocks,
|
| 364 |
+
norm_layer=self.norm_f,
|
| 365 |
+
) # [B, Lk_max, D]
|
| 366 |
+
|
| 367 |
+
device = x_full.device
|
| 368 |
+
B, Lmax, D = x_full.shape
|
| 369 |
+
ctx_keep_out, moe_aux, gates = self.moe(ctx_keep_out, attn_mask=attn_ctx, cond_vec=None, return_gates=True)
|
| 370 |
+
|
| 371 |
+
if return_moe_features:
|
| 372 |
+
return ctx_keep_out, attn_keep, keep_lengths, meta
|
| 373 |
+
|
| 374 |
+
ctx_full = x_full.new_zeros((B, Lmax, D))
|
| 375 |
+
attn_full = torch.ones(B, Lmax, dtype=torch.bool, device=device)
|
| 376 |
+
for i in range(B):
|
| 377 |
+
Li = lengths[i]
|
| 378 |
+
if Li == 0:
|
| 379 |
+
continue
|
| 380 |
+
|
| 381 |
+
Lk_i = int((~attn_keep[i]).sum().item())
|
| 382 |
+
if Lk_i > 0:
|
| 383 |
+
keep_idx = ids_keep_pad[i, :Lk_i].long() # [Lk_i]
|
| 384 |
+
ctx_full[i, keep_idx] = ctx_keep_out[i, :Lk_i] # [Lk_i, D]
|
| 385 |
+
|
| 386 |
+
masked_sel = (mask_full[i, :Li] == 1) if mask_full.dtype != torch.bool else mask_full[i, :Li]
|
| 387 |
+
if masked_sel.any():
|
| 388 |
+
idx = torch.nonzero(masked_sel, as_tuple=False).squeeze(1) # [n_mask]
|
| 389 |
+
token_rows = self.mask_token_ctx[0, 0].expand(idx.numel(), D) # [n_mask, D]
|
| 390 |
+
ctx_full[i, :Li].index_copy_(0, idx, token_rows)
|
| 391 |
+
|
| 392 |
+
attn_full[i, :Li] = False
|
| 393 |
+
|
| 394 |
+
pred_full = self._run_blocks(
|
| 395 |
+
ctx_full, attn_full,
|
| 396 |
+
blocks=self.predictor_blocks,
|
| 397 |
+
norm_layer=self.predictor_norm,
|
| 398 |
+
) # [B, Lmax, D]
|
| 399 |
+
|
| 400 |
+
x_tgt_pad, attn_tgt, tgt_lengths = self._build_target_masked(x_full, mask_full, lengths)
|
| 401 |
+
with torch.no_grad():
|
| 402 |
+
tgt_feat = self._run_blocks(
|
| 403 |
+
x_tgt_pad, attn_tgt,
|
| 404 |
+
blocks=self.target_blocks,
|
| 405 |
+
norm_layer=self.target_norm,
|
| 406 |
+
) # [B, Lt_max, D]
|
| 407 |
+
|
| 408 |
+
Lt_max = x_tgt_pad.size(1)
|
| 409 |
+
pred_masked = pred_full.new_zeros((B, Lt_max, D))
|
| 410 |
+
attn_pred = torch.ones(B, Lt_max, dtype=torch.bool, device=device)
|
| 411 |
+
for i in range(B):
|
| 412 |
+
Li = lengths[i]
|
| 413 |
+
if Li == 0:
|
| 414 |
+
continue
|
| 415 |
+
sel = (mask_full[i, :Li] == 1) if mask_full.dtype != torch.bool else mask_full[i, :Li]
|
| 416 |
+
vi = pred_full[i, :Li][sel]
|
| 417 |
+
if vi.numel() > 0:
|
| 418 |
+
Lti = vi.size(0)
|
| 419 |
+
pred_masked[i, :Lti] = vi
|
| 420 |
+
attn_pred[i, :Lti] = False
|
| 421 |
+
|
| 422 |
+
if self.norm_target:
|
| 423 |
+
# def norm_sg(x, eps=1e-6):
|
| 424 |
+
# return x / (x.norm(dim=-1, keepdim=True).clamp_min(eps).detach())
|
| 425 |
+
# tgt_feat = norm_sg(tgt_feat)
|
| 426 |
+
# pred_masked = norm_sg(pred_masked)
|
| 427 |
+
tgt_norm = torch.linalg.norm(tgt_feat, dim=-1, keepdim=True).clamp_min(1e-6)
|
| 428 |
+
tgt_feat = tgt_feat / tgt_norm
|
| 429 |
+
pred_norm = torch.linalg.norm(pred_masked, dim=-1, keepdim=True).clamp_min(1e-6)
|
| 430 |
+
pred_masked = pred_masked / pred_norm
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
valid = ~attn_pred
|
| 434 |
+
denom = valid.sum().clamp_min(1)
|
| 435 |
+
loss = (pred_masked[valid] - tgt_feat[valid]).pow(2).sum() / denom
|
| 436 |
+
|
| 437 |
+
loss = loss
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
return loss, pred_masked, tgt_feat, mask_full
|
flexibrain/models/transformer_block.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Transformer block used by Flexibrain downstream heads.
|
| 2 |
+
|
| 3 |
+
Reference:
|
| 4 |
+
- Brain-Harmony / BrainHarmonix official codebase: https://github.com/hzlab/Brain-Harmony
|
| 5 |
+
- The official README marks the project license as CC BY-NC-SA 4.0.
|
| 6 |
+
|
| 7 |
+
Only the small Block/Attention/MLP subset required by the downstream head is
|
| 8 |
+
kept here; the rest of the Brain-Harmony repository is intentionally not
|
| 9 |
+
vendored into Flexibrain.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
import torch.nn as nn
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def drop_path(x, drop_prob: float = 0.0, training: bool = False):
|
| 17 |
+
if drop_prob == 0.0 or not training:
|
| 18 |
+
return x
|
| 19 |
+
keep_prob = 1 - drop_prob
|
| 20 |
+
shape = (x.shape[0],) + (1,) * (x.ndim - 1)
|
| 21 |
+
random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype, device=x.device)
|
| 22 |
+
random_tensor.floor_()
|
| 23 |
+
return x.div(keep_prob) * random_tensor
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class DropPath(nn.Module):
|
| 27 |
+
def __init__(self, drop_prob=0.0):
|
| 28 |
+
super().__init__()
|
| 29 |
+
self.drop_prob = float(drop_prob)
|
| 30 |
+
|
| 31 |
+
def forward(self, x):
|
| 32 |
+
return drop_path(x, self.drop_prob, self.training)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class MLP(nn.Module):
|
| 36 |
+
def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.0):
|
| 37 |
+
super().__init__()
|
| 38 |
+
out_features = out_features or in_features
|
| 39 |
+
hidden_features = hidden_features or in_features
|
| 40 |
+
self.fc1 = nn.Linear(in_features, hidden_features)
|
| 41 |
+
self.act = act_layer()
|
| 42 |
+
self.fc2 = nn.Linear(hidden_features, out_features)
|
| 43 |
+
self.drop = nn.Dropout(drop)
|
| 44 |
+
|
| 45 |
+
def forward(self, x):
|
| 46 |
+
x = self.fc1(x)
|
| 47 |
+
x = self.act(x)
|
| 48 |
+
x = self.drop(x)
|
| 49 |
+
x = self.fc2(x)
|
| 50 |
+
return self.drop(x)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class Attention(nn.Module):
|
| 54 |
+
def __init__(self, dim, num_heads=8, qkv_bias=False, qk_scale=None, attn_drop=0.0, proj_drop=0.0):
|
| 55 |
+
super().__init__()
|
| 56 |
+
self.num_heads = num_heads
|
| 57 |
+
head_dim = dim // num_heads
|
| 58 |
+
self.scale = qk_scale or head_dim ** -0.5
|
| 59 |
+
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
|
| 60 |
+
self.attn_drop = nn.Dropout(attn_drop)
|
| 61 |
+
self.proj = nn.Linear(dim, dim)
|
| 62 |
+
self.proj_drop = nn.Dropout(proj_drop)
|
| 63 |
+
|
| 64 |
+
def forward(self, x, attention_mask=None, output_attentions=False):
|
| 65 |
+
B, N, C = x.shape
|
| 66 |
+
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
|
| 67 |
+
q, k, v = qkv[0], qkv[1], qkv[2]
|
| 68 |
+
attn = (q @ k.transpose(-2, -1)) * self.scale
|
| 69 |
+
if attention_mask is not None:
|
| 70 |
+
valid = attention_mask.bool()
|
| 71 |
+
key_mask = ~valid[:, None, None, :]
|
| 72 |
+
attn = attn.masked_fill(key_mask, torch.finfo(attn.dtype).min)
|
| 73 |
+
attn = self.attn_drop(attn.softmax(dim=-1))
|
| 74 |
+
x = (attn @ v).transpose(1, 2).reshape(B, N, C)
|
| 75 |
+
x = self.proj_drop(self.proj(x))
|
| 76 |
+
return (x, attn) if output_attentions else (x, None)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class Block(nn.Module):
|
| 80 |
+
def __init__(self, dim, num_heads, mlp_ratio=4.0, qkv_bias=False, qk_scale=None, drop=0.0, attn_drop=0.0, drop_path=0.0, act_layer=nn.GELU, norm_layer=nn.LayerNorm, attn_mode=None):
|
| 81 |
+
super().__init__()
|
| 82 |
+
self.norm1 = norm_layer(dim)
|
| 83 |
+
self.norm2 = norm_layer(dim)
|
| 84 |
+
self.attn = Attention(dim, num_heads=num_heads, qkv_bias=qkv_bias, qk_scale=qk_scale, attn_drop=attn_drop, proj_drop=drop)
|
| 85 |
+
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
|
| 86 |
+
self.mlp = MLP(in_features=dim, hidden_features=int(dim * mlp_ratio), act_layer=act_layer, drop=drop)
|
| 87 |
+
|
| 88 |
+
def forward(self, x, attention_mask=None, return_attention=False):
|
| 89 |
+
y, attn = self.attn(self.norm1(x), attention_mask=attention_mask, output_attentions=return_attention)
|
| 90 |
+
x = x + self.drop_path(y)
|
| 91 |
+
x = x + self.drop_path(self.mlp(self.norm2(x)))
|
| 92 |
+
return (x, attn) if return_attention else x
|
flexibrain/utils/__init__.py
ADDED
|
File without changes
|
flexibrain/utils/checkpoint.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
import torch
|
| 3 |
+
from typing import Tuple
|
| 4 |
+
import torch.optim as optim
|
| 5 |
+
from torch.nn.parallel import DistributedDataParallel as DDP
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
def load_checkpoint(
|
| 9 |
+
model: nn.Module,
|
| 10 |
+
optimizer: optim.Optimizer,
|
| 11 |
+
scheduler,
|
| 12 |
+
checkpoint_path: str,
|
| 13 |
+
device: torch.device,
|
| 14 |
+
) -> Tuple[int, float]:
|
| 15 |
+
"""Load model checkpoint."""
|
| 16 |
+
checkpoint = torch.load(checkpoint_path, map_location=device)
|
| 17 |
+
|
| 18 |
+
# Load model state dict (handle DDP wrapper)
|
| 19 |
+
if isinstance(model, DDP):
|
| 20 |
+
model.module.load_state_dict(checkpoint['model_state_dict'])
|
| 21 |
+
else:
|
| 22 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
| 23 |
+
|
| 24 |
+
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
|
| 25 |
+
|
| 26 |
+
if scheduler is not None and 'scheduler_state_dict' in checkpoint:
|
| 27 |
+
scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
|
| 28 |
+
|
| 29 |
+
epoch = checkpoint.get('epoch', 0)
|
| 30 |
+
best_loss = checkpoint.get('best_loss', float('inf'))
|
| 31 |
+
|
| 32 |
+
return epoch, best_loss
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def save_checkpoint(
|
| 36 |
+
model: nn.Module,
|
| 37 |
+
optimizer: optim.Optimizer,
|
| 38 |
+
scheduler,
|
| 39 |
+
epoch: int,
|
| 40 |
+
best_loss: float,
|
| 41 |
+
checkpoint_dir: str,
|
| 42 |
+
args=None,
|
| 43 |
+
rank: int = 0,
|
| 44 |
+
) -> None:
|
| 45 |
+
"""Save model checkpoint with configuration."""
|
| 46 |
+
if rank != 0:
|
| 47 |
+
return
|
| 48 |
+
|
| 49 |
+
os.makedirs(checkpoint_dir, exist_ok=True)
|
| 50 |
+
|
| 51 |
+
# Get model state dict (handle DDP wrapper)
|
| 52 |
+
model_state = model.module.state_dict() if isinstance(model, DDP) else model.state_dict()
|
| 53 |
+
|
| 54 |
+
checkpoint = {
|
| 55 |
+
'epoch': epoch,
|
| 56 |
+
'model_state_dict': model_state,
|
| 57 |
+
'optimizer_state_dict': optimizer.state_dict(),
|
| 58 |
+
'best_loss': best_loss,
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
if scheduler is not None:
|
| 62 |
+
checkpoint['scheduler_state_dict'] = scheduler.state_dict()
|
| 63 |
+
|
| 64 |
+
# Save model configuration for downstream tasks
|
| 65 |
+
if args is not None:
|
| 66 |
+
checkpoint['config'] = {
|
| 67 |
+
'model_type': args.model_type,
|
| 68 |
+
'embed_dim': args.embed_dim,
|
| 69 |
+
'depth': args.depth,
|
| 70 |
+
'predictor_depth': args.predictor_depth,
|
| 71 |
+
'drop_path_rate': args.drop_path_rate,
|
| 72 |
+
'rms_norm': args.rms_norm,
|
| 73 |
+
'fused_add_norm': args.fused_add_norm,
|
| 74 |
+
'residual_in_fp32': args.residual_in_fp32,
|
| 75 |
+
'bimamba_type': args.bimamba_type,
|
| 76 |
+
'if_bimamba': args.if_bimamba,
|
| 77 |
+
'mixer_type': args.mixer_type,
|
| 78 |
+
'if_devide_out': args.if_devide_out,
|
| 79 |
+
'predictor_hidden': args.predictor_hidden,
|
| 80 |
+
'momentum': args.momentum,
|
| 81 |
+
'norm_target': args.norm_target,
|
| 82 |
+
'num_heads': args.num_heads,
|
| 83 |
+
'mlp_ratio': args.mlp_ratio,
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# Save latest checkpoint
|
| 87 |
+
latest_path = os.path.join(checkpoint_dir, 'checkpoint_latest.pt')
|
| 88 |
+
torch.save(checkpoint, latest_path)
|
| 89 |
+
|
| 90 |
+
# Save best checkpoint
|
| 91 |
+
if best_loss is not None:
|
| 92 |
+
best_path = os.path.join(checkpoint_dir, 'checkpoint_best.pt')
|
| 93 |
+
torch.save(checkpoint, best_path)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def save_downstream_checkpoint(model, optimizer, scheduler, epoch, metrics, checkpoint_dir, rank=0):
|
| 97 |
+
"""Save downstream checkpoint."""
|
| 98 |
+
if rank == 0:
|
| 99 |
+
os.makedirs(checkpoint_dir, exist_ok=True)
|
| 100 |
+
checkpoint = {
|
| 101 |
+
'epoch': epoch,
|
| 102 |
+
'model': model.state_dict() if not isinstance(model, DDP) else model.module.state_dict(),
|
| 103 |
+
'optimizer': optimizer.state_dict(),
|
| 104 |
+
'scheduler': scheduler.state_dict(),
|
| 105 |
+
'metrics': metrics,
|
| 106 |
+
}
|
| 107 |
+
path = os.path.join(checkpoint_dir, f"downstream_epoch_{epoch:03d}.pt")
|
| 108 |
+
torch.save(checkpoint, path)
|
flexibrain/utils/logging.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def setup_logger(name: str, log_dir: str, rank: int = 0) -> logging.Logger:
|
| 7 |
+
os.makedirs(log_dir, exist_ok=True)
|
| 8 |
+
logger = logging.getLogger(name)
|
| 9 |
+
logger.setLevel(logging.INFO if rank == 0 else logging.WARNING)
|
| 10 |
+
logger.handlers.clear()
|
| 11 |
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
| 12 |
+
if rank == 0:
|
| 13 |
+
file_handler = logging.FileHandler(os.path.join(log_dir, f"{name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"))
|
| 14 |
+
file_handler.setFormatter(formatter)
|
| 15 |
+
console_handler = logging.StreamHandler()
|
| 16 |
+
console_handler.setFormatter(formatter)
|
| 17 |
+
logger.addHandler(file_handler)
|
| 18 |
+
logger.addHandler(console_handler)
|
| 19 |
+
return logger
|
flexibrain/utils/pinv_resize.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025
|
| 2 |
+
# Utilities to build resize operators and their pseudoinverses.
|
| 3 |
+
|
| 4 |
+
from typing import Tuple
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
from functools import lru_cache
|
| 10 |
+
|
| 11 |
+
@torch.no_grad()
|
| 12 |
+
def _resize_2d(x: Tensor, shape: Tuple[int, int],
|
| 13 |
+
interpolation: str = "bicubic",
|
| 14 |
+
antialias: bool = True) -> Tensor:
|
| 15 |
+
"""
|
| 16 |
+
Resize a 2D tensor x[h0,w0] -> shape[h,w] using torch interpolate.
|
| 17 |
+
Matches the "wrap with [None,None,...]" trick from your flex_patch_embed.py.
|
| 18 |
+
"""
|
| 19 |
+
x_resized = F.interpolate(
|
| 20 |
+
x[None, None, ...],
|
| 21 |
+
shape,
|
| 22 |
+
mode=interpolation,
|
| 23 |
+
antialias=antialias,
|
| 24 |
+
)
|
| 25 |
+
return x_resized[0, 0, ...]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@lru_cache(maxsize=256)
|
| 29 |
+
def _calculate_pinv_2d(old_shape: Tuple[int, int],
|
| 30 |
+
new_shape: Tuple[int, int],
|
| 31 |
+
interpolation: str = "bicubic",
|
| 32 |
+
antialias: bool = True,
|
| 33 |
+
device: torch.device = torch.device("cpu"),
|
| 34 |
+
dtype: torch.dtype = torch.float32) -> Tensor:
|
| 35 |
+
"""
|
| 36 |
+
Build the (flattened) resize matrix R s.t. vec(new) = R @ vec(old),
|
| 37 |
+
then return pinv(R). This mirrors your flex_patch_embed.py approach.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
old_shape: (h0, w0)
|
| 41 |
+
new_shape: (h, w)
|
| 42 |
+
Returns:
|
| 43 |
+
pinv(R): Tensor of shape [(h*w), (h0*w0)]
|
| 44 |
+
"""
|
| 45 |
+
# Construct R by sending basis vectors through the geometric resize op.
|
| 46 |
+
mat = []
|
| 47 |
+
h0, w0 = int(old_shape[0]), int(old_shape[1])
|
| 48 |
+
for i in range(int(np.prod(old_shape))):
|
| 49 |
+
basis = torch.zeros((h0, w0), dtype=dtype, device=device)
|
| 50 |
+
idx = np.unravel_index(i, (h0, w0))
|
| 51 |
+
basis[idx] = 1.0
|
| 52 |
+
mat.append(_resize_2d(basis, new_shape, interpolation, antialias).reshape(-1))
|
| 53 |
+
resize_matrix = torch.stack(mat) # [(h*w), (h0*w0)]
|
| 54 |
+
pinv = torch.linalg.pinv(resize_matrix)
|
| 55 |
+
return pinv # [(h*w), (h0*w0)]
|
flexibrain/utils/seed.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def set_seed(seed: int) -> None:
|
| 7 |
+
random.seed(seed)
|
| 8 |
+
np.random.seed(seed)
|
| 9 |
+
torch.manual_seed(seed)
|
| 10 |
+
if torch.cuda.is_available():
|
| 11 |
+
torch.cuda.manual_seed_all(seed)
|
flexibrain/utils/training.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from torch.nn.parallel import DistributedDataParallel as DDP
|
| 4 |
+
|
| 5 |
+
def meta_to_matrix(meta: dict, B: int) -> np.ndarray:
|
| 6 |
+
out = np.empty((B, 4), dtype=np.float32)
|
| 7 |
+
for i in range(B):
|
| 8 |
+
|
| 9 |
+
m = meta[i]
|
| 10 |
+
voxel = m.get("voxel", m.get("voxel_size", m.get("spacing")))
|
| 11 |
+
|
| 12 |
+
rx = float(voxel[0])
|
| 13 |
+
ry = float(voxel[1])
|
| 14 |
+
rt = float(m.get("rt", voxel[2]))
|
| 15 |
+
tr = float(m["tr"])
|
| 16 |
+
|
| 17 |
+
out[i] = (rx, ry, rt, tr)
|
| 18 |
+
return out
|
| 19 |
+
|
| 20 |
+
def update_ema(model: nn.Module, momentum: float) -> None:
|
| 21 |
+
"""Update target encoder with EMA."""
|
| 22 |
+
if hasattr(model, 'update_target_encoder'):
|
| 23 |
+
model.update_target_encoder(m=momentum)
|
| 24 |
+
elif isinstance(model, DDP) and hasattr(model.module, 'update_target_encoder'):
|
| 25 |
+
model.module.update_target_encoder(m=momentum)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def get_dynamic_momentum(epoch: int, total_epochs: int, base_momentum: float = 0.996, final_momentum: float = 0.9999) -> float:
|
| 29 |
+
"""
|
| 30 |
+
Calculate dynamic momentum for EMA.
|
| 31 |
+
|
| 32 |
+
Momentum increases from base_momentum to final_momentum over training.
|
| 33 |
+
This helps stabilize training in later epochs.
|
| 34 |
+
"""
|
| 35 |
+
progress = epoch / total_epochs
|
| 36 |
+
# Cosine annealing: start at base, end at final
|
| 37 |
+
momentum = final_momentum - (final_momentum - base_momentum) * 0.5 * (1 + np.cos(np.pi * progress))
|
| 38 |
+
return momentum
|
flexibrain/utils/weight_resize.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Weight (kernel) resizing using pseudoinverse-based geometric operators.
|
| 2 |
+
import torch
|
| 3 |
+
from torch import Tensor
|
| 4 |
+
from flexibrain.utils.pinv_resize import _calculate_pinv_2d
|
| 5 |
+
|
| 6 |
+
# ------------- 1D (e.g., time) -----------------
|
| 7 |
+
def resize_conv1d_weight_with_pinv(
|
| 8 |
+
w_star: Tensor, k_new: int,
|
| 9 |
+
interpolation: str = "bicubic",
|
| 10 |
+
antialias: bool = True,
|
| 11 |
+
) -> Tensor:
|
| 12 |
+
"""
|
| 13 |
+
Resample a Conv1d kernel from K_old -> k_new using pinv of a 2D operator
|
| 14 |
+
on a degenerate dimension (1,K). This keeps the math aligned with the 2D codepath.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
w_star: [Out, In, K_old]
|
| 18 |
+
k_new: new kernel length
|
| 19 |
+
Returns:
|
| 20 |
+
w_new: [Out, In, k_new]
|
| 21 |
+
"""
|
| 22 |
+
Out, In, K_old = w_star.shape
|
| 23 |
+
if k_new == K_old:
|
| 24 |
+
return w_star
|
| 25 |
+
|
| 26 |
+
dev, dt = w_star.device, w_star.dtype
|
| 27 |
+
requires_grad = w_star.requires_grad
|
| 28 |
+
|
| 29 |
+
# Build pinv((1,K_old)->(1,k_new)) - 这个操作不需要梯度
|
| 30 |
+
with torch.no_grad():
|
| 31 |
+
pinv = _calculate_pinv_2d(
|
| 32 |
+
(1, int(K_old)), (1, int(k_new)),
|
| 33 |
+
interpolation=interpolation,
|
| 34 |
+
antialias=antialias,
|
| 35 |
+
device=dev, dtype=dt
|
| 36 |
+
) # [(1*k_new), (1*K_old)] == [k_new, K_old]
|
| 37 |
+
|
| 38 |
+
W = w_star.reshape(Out * In, K_old) # [(Out*In), K_old]
|
| 39 |
+
W_new = (pinv @ W.T).T # [(Out*In), k_new]
|
| 40 |
+
W_new = W_new.reshape(Out, In, k_new)
|
| 41 |
+
|
| 42 |
+
# 恢复requires_grad状态
|
| 43 |
+
if requires_grad:
|
| 44 |
+
W_new = W_new.requires_grad_(True)
|
| 45 |
+
|
| 46 |
+
return W_new
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def pi_resize_weight_1d(
|
| 50 |
+
w_star: Tensor, k_new: int,
|
| 51 |
+
interpolation: str = "bicubic",
|
| 52 |
+
antialias: bool = True,
|
| 53 |
+
) -> Tensor:
|
| 54 |
+
"""
|
| 55 |
+
Alias kept for timetospace: same signature as your current helper.
|
| 56 |
+
"""
|
| 57 |
+
return resize_conv1d_weight_with_pinv(
|
| 58 |
+
w_star, k_new, interpolation=interpolation, antialias=antialias
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# ------------- 3D separable (x,y,z) ------------
|
| 62 |
+
def resize_conv3d_weight_separable_with_pinv(
|
| 63 |
+
w_star: Tensor, kx: int, ky: int, kz: int,
|
| 64 |
+
interpolation: str = "bicubic",
|
| 65 |
+
antialias: bool = True,
|
| 66 |
+
) -> Tensor:
|
| 67 |
+
"""
|
| 68 |
+
Separable 3-axis resize using three 1D pinv operators.
|
| 69 |
+
Mirrors your existing axis-by-axis path; only changes how we form each 1D pinv.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
w_star: [Out, In, Kx0, Ky0, Kz0]
|
| 73 |
+
kx, ky, kz: target sizes
|
| 74 |
+
Returns:
|
| 75 |
+
w_new: [Out, In, kx, ky, kz]
|
| 76 |
+
"""
|
| 77 |
+
Out, In, Kx0, Ky0, Kz0 = w_star.shape
|
| 78 |
+
if (kx, ky, kz) == (Kx0, Ky0, Kz0):
|
| 79 |
+
return w_star
|
| 80 |
+
|
| 81 |
+
dev, dt = w_star.device, w_star.dtype
|
| 82 |
+
requires_grad = w_star.requires_grad
|
| 83 |
+
W = w_star
|
| 84 |
+
|
| 85 |
+
# x-axis
|
| 86 |
+
with torch.no_grad():
|
| 87 |
+
Rx_p = _calculate_pinv_2d(
|
| 88 |
+
(int(Kx0), 1), (int(kx), 1),
|
| 89 |
+
device=dev, dtype=dt,
|
| 90 |
+
interpolation=interpolation, antialias=antialias
|
| 91 |
+
) # [kx, Kx0]
|
| 92 |
+
W = W.permute(2, 0, 1, 3, 4).reshape(Kx0, -1) # [Kx0, Out*In*Ky0*Kz0]
|
| 93 |
+
W = (Rx_p @ W).reshape(kx, Out, In, Ky0, Kz0).permute(1, 2, 0, 3, 4)
|
| 94 |
+
|
| 95 |
+
# y-axis
|
| 96 |
+
with torch.no_grad():
|
| 97 |
+
Ry_p = _calculate_pinv_2d(
|
| 98 |
+
(int(Ky0), 1), (int(ky), 1),
|
| 99 |
+
device=dev, dtype=dt,
|
| 100 |
+
interpolation=interpolation, antialias=antialias
|
| 101 |
+
) # [ky, Ky0]
|
| 102 |
+
W = W.permute(3, 0, 1, 2, 4).reshape(Ky0, -1)
|
| 103 |
+
W = (Ry_p @ W).reshape(ky, Out, In, kx, Kz0).permute(1, 2, 3, 0, 4)
|
| 104 |
+
|
| 105 |
+
# z-axis
|
| 106 |
+
with torch.no_grad():
|
| 107 |
+
Rz_p = _calculate_pinv_2d(
|
| 108 |
+
(int(Kz0), 1), (int(kz), 1),
|
| 109 |
+
device=dev, dtype=dt,
|
| 110 |
+
interpolation=interpolation, antialias=antialias
|
| 111 |
+
) # [kz, Kz0]
|
| 112 |
+
W = W.permute(4, 0, 1, 2, 3).reshape(Kz0, -1)
|
| 113 |
+
W = (Rz_p @ W).reshape(kz, Out, In, kx, ky).permute(1, 2, 3, 4, 0)
|
| 114 |
+
|
| 115 |
+
# 恢复requires_grad状态
|
| 116 |
+
if requires_grad:
|
| 117 |
+
W = W.requires_grad_(True)
|
| 118 |
+
|
| 119 |
+
return W
|
| 120 |
+
|
| 121 |
+
def pi_resize_weight_3d(
|
| 122 |
+
w_star: Tensor, kx: int, ky: int, kz: int,
|
| 123 |
+
interpolation: str = "bicubic",
|
| 124 |
+
antialias: bool = True,
|
| 125 |
+
) -> Tensor:
|
| 126 |
+
"""
|
| 127 |
+
Alias kept for timetospace: same signature as your current helper.
|
| 128 |
+
"""
|
| 129 |
+
return resize_conv3d_weight_separable_with_pinv(
|
| 130 |
+
w_star, kx, ky, kz, interpolation=interpolation, antialias=antialias
|
| 131 |
+
)
|
licenses/causal_conv1d_LICENSE_BSD_3_Clause.txt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BSD 3-Clause License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2022, the respective contributors, as shown by the AUTHORS file.
|
| 4 |
+
All rights reserved.
|
| 5 |
+
|
| 6 |
+
Redistribution and use in source and binary forms, with or without
|
| 7 |
+
modification, are permitted provided that the following conditions are met:
|
| 8 |
+
|
| 9 |
+
* Redistributions of source code must retain the above copyright notice, this
|
| 10 |
+
list of conditions and the following disclaimer.
|
| 11 |
+
|
| 12 |
+
* Redistributions in binary form must reproduce the above copyright notice,
|
| 13 |
+
this list of conditions and the following disclaimer in the documentation
|
| 14 |
+
and/or other materials provided with the distribution.
|
| 15 |
+
|
| 16 |
+
* Neither the name of the copyright holder nor the names of its
|
| 17 |
+
contributors may be used to endorse or promote products derived from
|
| 18 |
+
this software without specific prior written permission.
|
| 19 |
+
|
| 20 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
| 21 |
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
| 22 |
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
| 23 |
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
| 24 |
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
| 25 |
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
| 26 |
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
| 27 |
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
| 28 |
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| 29 |
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
licenses/mamba2_LICENSE_Apache_2.0.txt
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright 2023 Tri Dao, Albert Gu
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
licenses/mamba_mae_LICENSE_CC_BY_NC_4.0.txt
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Attribution-NonCommercial 4.0 International
|
| 3 |
+
|
| 4 |
+
=======================================================================
|
| 5 |
+
|
| 6 |
+
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
| 7 |
+
does not provide legal services or legal advice. Distribution of
|
| 8 |
+
Creative Commons public licenses does not create a lawyer-client or
|
| 9 |
+
other relationship. Creative Commons makes its licenses and related
|
| 10 |
+
information available on an "as-is" basis. Creative Commons gives no
|
| 11 |
+
warranties regarding its licenses, any material licensed under their
|
| 12 |
+
terms and conditions, or any related information. Creative Commons
|
| 13 |
+
disclaims all liability for damages resulting from their use to the
|
| 14 |
+
fullest extent possible.
|
| 15 |
+
|
| 16 |
+
Using Creative Commons Public Licenses
|
| 17 |
+
|
| 18 |
+
Creative Commons public licenses provide a standard set of terms and
|
| 19 |
+
conditions that creators and other rights holders may use to share
|
| 20 |
+
original works of authorship and other material subject to copyright
|
| 21 |
+
and certain other rights specified in the public license below. The
|
| 22 |
+
following considerations are for informational purposes only, are not
|
| 23 |
+
exhaustive, and do not form part of our licenses.
|
| 24 |
+
|
| 25 |
+
Considerations for licensors: Our public licenses are
|
| 26 |
+
intended for use by those authorized to give the public
|
| 27 |
+
permission to use material in ways otherwise restricted by
|
| 28 |
+
copyright and certain other rights. Our licenses are
|
| 29 |
+
irrevocable. Licensors should read and understand the terms
|
| 30 |
+
and conditions of the license they choose before applying it.
|
| 31 |
+
Licensors should also secure all rights necessary before
|
| 32 |
+
applying our licenses so that the public can reuse the
|
| 33 |
+
material as expected. Licensors should clearly mark any
|
| 34 |
+
material not subject to the license. This includes other CC-
|
| 35 |
+
licensed material, or material used under an exception or
|
| 36 |
+
limitation to copyright. More considerations for licensors:
|
| 37 |
+
wiki.creativecommons.org/Considerations_for_licensors
|
| 38 |
+
|
| 39 |
+
Considerations for the public: By using one of our public
|
| 40 |
+
licenses, a licensor grants the public permission to use the
|
| 41 |
+
licensed material under specified terms and conditions. If
|
| 42 |
+
the licensor's permission is not necessary for any reason--for
|
| 43 |
+
example, because of any applicable exception or limitation to
|
| 44 |
+
copyright--then that use is not regulated by the license. Our
|
| 45 |
+
licenses grant only permissions under copyright and certain
|
| 46 |
+
other rights that a licensor has authority to grant. Use of
|
| 47 |
+
the licensed material may still be restricted for other
|
| 48 |
+
reasons, including because others have copyright or other
|
| 49 |
+
rights in the material. A licensor may make special requests,
|
| 50 |
+
such as asking that all changes be marked or described.
|
| 51 |
+
Although not required by our licenses, you are encouraged to
|
| 52 |
+
respect those requests where reasonable. More_considerations
|
| 53 |
+
for the public:
|
| 54 |
+
wiki.creativecommons.org/Considerations_for_licensees
|
| 55 |
+
|
| 56 |
+
=======================================================================
|
| 57 |
+
|
| 58 |
+
Creative Commons Attribution-NonCommercial 4.0 International Public
|
| 59 |
+
License
|
| 60 |
+
|
| 61 |
+
By exercising the Licensed Rights (defined below), You accept and agree
|
| 62 |
+
to be bound by the terms and conditions of this Creative Commons
|
| 63 |
+
Attribution-NonCommercial 4.0 International Public License ("Public
|
| 64 |
+
License"). To the extent this Public License may be interpreted as a
|
| 65 |
+
contract, You are granted the Licensed Rights in consideration of Your
|
| 66 |
+
acceptance of these terms and conditions, and the Licensor grants You
|
| 67 |
+
such rights in consideration of benefits the Licensor receives from
|
| 68 |
+
making the Licensed Material available under these terms and
|
| 69 |
+
conditions.
|
| 70 |
+
|
| 71 |
+
Section 1 -- Definitions.
|
| 72 |
+
|
| 73 |
+
a. Adapted Material means material subject to Copyright and Similar
|
| 74 |
+
Rights that is derived from or based upon the Licensed Material
|
| 75 |
+
and in which the Licensed Material is translated, altered,
|
| 76 |
+
arranged, transformed, or otherwise modified in a manner requiring
|
| 77 |
+
permission under the Copyright and Similar Rights held by the
|
| 78 |
+
Licensor. For purposes of this Public License, where the Licensed
|
| 79 |
+
Material is a musical work, performance, or sound recording,
|
| 80 |
+
Adapted Material is always produced where the Licensed Material is
|
| 81 |
+
synched in timed relation with a moving image.
|
| 82 |
+
|
| 83 |
+
b. Adapter's License means the license You apply to Your Copyright
|
| 84 |
+
and Similar Rights in Your contributions to Adapted Material in
|
| 85 |
+
accordance with the terms and conditions of this Public License.
|
| 86 |
+
|
| 87 |
+
c. Copyright and Similar Rights means copyright and/or similar rights
|
| 88 |
+
closely related to copyright including, without limitation,
|
| 89 |
+
performance, broadcast, sound recording, and Sui Generis Database
|
| 90 |
+
Rights, without regard to how the rights are labeled or
|
| 91 |
+
categorized. For purposes of this Public License, the rights
|
| 92 |
+
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
| 93 |
+
Rights.
|
| 94 |
+
d. Effective Technological Measures means those measures that, in the
|
| 95 |
+
absence of proper authority, may not be circumvented under laws
|
| 96 |
+
fulfilling obligations under Article 11 of the WIPO Copyright
|
| 97 |
+
Treaty adopted on December 20, 1996, and/or similar international
|
| 98 |
+
agreements.
|
| 99 |
+
|
| 100 |
+
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
| 101 |
+
any other exception or limitation to Copyright and Similar Rights
|
| 102 |
+
that applies to Your use of the Licensed Material.
|
| 103 |
+
|
| 104 |
+
f. Licensed Material means the artistic or literary work, database,
|
| 105 |
+
or other material to which the Licensor applied this Public
|
| 106 |
+
License.
|
| 107 |
+
|
| 108 |
+
g. Licensed Rights means the rights granted to You subject to the
|
| 109 |
+
terms and conditions of this Public License, which are limited to
|
| 110 |
+
all Copyright and Similar Rights that apply to Your use of the
|
| 111 |
+
Licensed Material and that the Licensor has authority to license.
|
| 112 |
+
|
| 113 |
+
h. Licensor means the individual(s) or entity(ies) granting rights
|
| 114 |
+
under this Public License.
|
| 115 |
+
|
| 116 |
+
i. NonCommercial means not primarily intended for or directed towards
|
| 117 |
+
commercial advantage or monetary compensation. For purposes of
|
| 118 |
+
this Public License, the exchange of the Licensed Material for
|
| 119 |
+
other material subject to Copyright and Similar Rights by digital
|
| 120 |
+
file-sharing or similar means is NonCommercial provided there is
|
| 121 |
+
no payment of monetary compensation in connection with the
|
| 122 |
+
exchange.
|
| 123 |
+
|
| 124 |
+
j. Share means to provide material to the public by any means or
|
| 125 |
+
process that requires permission under the Licensed Rights, such
|
| 126 |
+
as reproduction, public display, public performance, distribution,
|
| 127 |
+
dissemination, communication, or importation, and to make material
|
| 128 |
+
available to the public including in ways that members of the
|
| 129 |
+
public may access the material from a place and at a time
|
| 130 |
+
individually chosen by them.
|
| 131 |
+
|
| 132 |
+
k. Sui Generis Database Rights means rights other than copyright
|
| 133 |
+
resulting from Directive 96/9/EC of the European Parliament and of
|
| 134 |
+
the Council of 11 March 1996 on the legal protection of databases,
|
| 135 |
+
as amended and/or succeeded, as well as other essentially
|
| 136 |
+
equivalent rights anywhere in the world.
|
| 137 |
+
|
| 138 |
+
l. You means the individual or entity exercising the Licensed Rights
|
| 139 |
+
under this Public License. Your has a corresponding meaning.
|
| 140 |
+
|
| 141 |
+
Section 2 -- Scope.
|
| 142 |
+
|
| 143 |
+
a. License grant.
|
| 144 |
+
|
| 145 |
+
1. Subject to the terms and conditions of this Public License,
|
| 146 |
+
the Licensor hereby grants You a worldwide, royalty-free,
|
| 147 |
+
non-sublicensable, non-exclusive, irrevocable license to
|
| 148 |
+
exercise the Licensed Rights in the Licensed Material to:
|
| 149 |
+
|
| 150 |
+
a. reproduce and Share the Licensed Material, in whole or
|
| 151 |
+
in part, for NonCommercial purposes only; and
|
| 152 |
+
|
| 153 |
+
b. produce, reproduce, and Share Adapted Material for
|
| 154 |
+
NonCommercial purposes only.
|
| 155 |
+
|
| 156 |
+
2. Exceptions and Limitations. For the avoidance of doubt, where
|
| 157 |
+
Exceptions and Limitations apply to Your use, this Public
|
| 158 |
+
License does not apply, and You do not need to comply with
|
| 159 |
+
its terms and conditions.
|
| 160 |
+
|
| 161 |
+
3. Term. The term of this Public License is specified in Section
|
| 162 |
+
6(a).
|
| 163 |
+
|
| 164 |
+
4. Media and formats; technical modifications allowed. The
|
| 165 |
+
Licensor authorizes You to exercise the Licensed Rights in
|
| 166 |
+
all media and formats whether now known or hereafter created,
|
| 167 |
+
and to make technical modifications necessary to do so. The
|
| 168 |
+
Licensor waives and/or agrees not to assert any right or
|
| 169 |
+
authority to forbid You from making technical modifications
|
| 170 |
+
necessary to exercise the Licensed Rights, including
|
| 171 |
+
technical modifications necessary to circumvent Effective
|
| 172 |
+
Technological Measures. For purposes of this Public License,
|
| 173 |
+
simply making modifications authorized by this Section 2(a)
|
| 174 |
+
(4) never produces Adapted Material.
|
| 175 |
+
|
| 176 |
+
5. Downstream recipients.
|
| 177 |
+
|
| 178 |
+
a. Offer from the Licensor -- Licensed Material. Every
|
| 179 |
+
recipient of the Licensed Material automatically
|
| 180 |
+
receives an offer from the Licensor to exercise the
|
| 181 |
+
Licensed Rights under the terms and conditions of this
|
| 182 |
+
Public License.
|
| 183 |
+
|
| 184 |
+
b. No downstream restrictions. You may not offer or impose
|
| 185 |
+
any additional or different terms or conditions on, or
|
| 186 |
+
apply any Effective Technological Measures to, the
|
| 187 |
+
Licensed Material if doing so restricts exercise of the
|
| 188 |
+
Licensed Rights by any recipient of the Licensed
|
| 189 |
+
Material.
|
| 190 |
+
|
| 191 |
+
6. No endorsement. Nothing in this Public License constitutes or
|
| 192 |
+
may be construed as permission to assert or imply that You
|
| 193 |
+
are, or that Your use of the Licensed Material is, connected
|
| 194 |
+
with, or sponsored, endorsed, or granted official status by,
|
| 195 |
+
the Licensor or others designated to receive attribution as
|
| 196 |
+
provided in Section 3(a)(1)(A)(i).
|
| 197 |
+
|
| 198 |
+
b. Other rights.
|
| 199 |
+
|
| 200 |
+
1. Moral rights, such as the right of integrity, are not
|
| 201 |
+
licensed under this Public License, nor are publicity,
|
| 202 |
+
privacy, and/or other similar personality rights; however, to
|
| 203 |
+
the extent possible, the Licensor waives and/or agrees not to
|
| 204 |
+
assert any such rights held by the Licensor to the limited
|
| 205 |
+
extent necessary to allow You to exercise the Licensed
|
| 206 |
+
Rights, but not otherwise.
|
| 207 |
+
|
| 208 |
+
2. Patent and trademark rights are not licensed under this
|
| 209 |
+
Public License.
|
| 210 |
+
|
| 211 |
+
3. To the extent possible, the Licensor waives any right to
|
| 212 |
+
collect royalties from You for the exercise of the Licensed
|
| 213 |
+
Rights, whether directly or through a collecting society
|
| 214 |
+
under any voluntary or waivable statutory or compulsory
|
| 215 |
+
licensing scheme. In all other cases the Licensor expressly
|
| 216 |
+
reserves any right to collect such royalties, including when
|
| 217 |
+
the Licensed Material is used other than for NonCommercial
|
| 218 |
+
purposes.
|
| 219 |
+
|
| 220 |
+
Section 3 -- License Conditions.
|
| 221 |
+
|
| 222 |
+
Your exercise of the Licensed Rights is expressly made subject to the
|
| 223 |
+
following conditions.
|
| 224 |
+
|
| 225 |
+
a. Attribution.
|
| 226 |
+
|
| 227 |
+
1. If You Share the Licensed Material (including in modified
|
| 228 |
+
form), You must:
|
| 229 |
+
|
| 230 |
+
a. retain the following if it is supplied by the Licensor
|
| 231 |
+
with the Licensed Material:
|
| 232 |
+
|
| 233 |
+
i. identification of the creator(s) of the Licensed
|
| 234 |
+
Material and any others designated to receive
|
| 235 |
+
attribution, in any reasonable manner requested by
|
| 236 |
+
the Licensor (including by pseudonym if
|
| 237 |
+
designated);
|
| 238 |
+
|
| 239 |
+
ii. a copyright notice;
|
| 240 |
+
|
| 241 |
+
iii. a notice that refers to this Public License;
|
| 242 |
+
|
| 243 |
+
iv. a notice that refers to the disclaimer of
|
| 244 |
+
warranties;
|
| 245 |
+
|
| 246 |
+
v. a URI or hyperlink to the Licensed Material to the
|
| 247 |
+
extent reasonably practicable;
|
| 248 |
+
|
| 249 |
+
b. indicate if You modified the Licensed Material and
|
| 250 |
+
retain an indication of any previous modifications; and
|
| 251 |
+
|
| 252 |
+
c. indicate the Licensed Material is licensed under this
|
| 253 |
+
Public License, and include the text of, or the URI or
|
| 254 |
+
hyperlink to, this Public License.
|
| 255 |
+
|
| 256 |
+
2. You may satisfy the conditions in Section 3(a)(1) in any
|
| 257 |
+
reasonable manner based on the medium, means, and context in
|
| 258 |
+
which You Share the Licensed Material. For example, it may be
|
| 259 |
+
reasonable to satisfy the conditions by providing a URI or
|
| 260 |
+
hyperlink to a resource that includes the required
|
| 261 |
+
information.
|
| 262 |
+
|
| 263 |
+
3. If requested by the Licensor, You must remove any of the
|
| 264 |
+
information required by Section 3(a)(1)(A) to the extent
|
| 265 |
+
reasonably practicable.
|
| 266 |
+
|
| 267 |
+
4. If You Share Adapted Material You produce, the Adapter's
|
| 268 |
+
License You apply must not prevent recipients of the Adapted
|
| 269 |
+
Material from complying with this Public License.
|
| 270 |
+
|
| 271 |
+
Section 4 -- Sui Generis Database Rights.
|
| 272 |
+
|
| 273 |
+
Where the Licensed Rights include Sui Generis Database Rights that
|
| 274 |
+
apply to Your use of the Licensed Material:
|
| 275 |
+
|
| 276 |
+
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
| 277 |
+
to extract, reuse, reproduce, and Share all or a substantial
|
| 278 |
+
portion of the contents of the database for NonCommercial purposes
|
| 279 |
+
only;
|
| 280 |
+
|
| 281 |
+
b. if You include all or a substantial portion of the database
|
| 282 |
+
contents in a database in which You have Sui Generis Database
|
| 283 |
+
Rights, then the database in which You have Sui Generis Database
|
| 284 |
+
Rights (but not its individual contents) is Adapted Material; and
|
| 285 |
+
|
| 286 |
+
c. You must comply with the conditions in Section 3(a) if You Share
|
| 287 |
+
all or a substantial portion of the contents of the database.
|
| 288 |
+
|
| 289 |
+
For the avoidance of doubt, this Section 4 supplements and does not
|
| 290 |
+
replace Your obligations under this Public License where the Licensed
|
| 291 |
+
Rights include other Copyright and Similar Rights.
|
| 292 |
+
|
| 293 |
+
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
| 294 |
+
|
| 295 |
+
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
| 296 |
+
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
| 297 |
+
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
| 298 |
+
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
| 299 |
+
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
| 300 |
+
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
| 301 |
+
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
| 302 |
+
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
| 303 |
+
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
| 304 |
+
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
| 305 |
+
|
| 306 |
+
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
| 307 |
+
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
| 308 |
+
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
| 309 |
+
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
| 310 |
+
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
| 311 |
+
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
| 312 |
+
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
| 313 |
+
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
| 314 |
+
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
| 315 |
+
|
| 316 |
+
c. The disclaimer of warranties and limitation of liability provided
|
| 317 |
+
above shall be interpreted in a manner that, to the extent
|
| 318 |
+
possible, most closely approximates an absolute disclaimer and
|
| 319 |
+
waiver of all liability.
|
| 320 |
+
|
| 321 |
+
Section 6 -- Term and Termination.
|
| 322 |
+
|
| 323 |
+
a. This Public License applies for the term of the Copyright and
|
| 324 |
+
Similar Rights licensed here. However, if You fail to comply with
|
| 325 |
+
this Public License, then Your rights under this Public License
|
| 326 |
+
terminate automatically.
|
| 327 |
+
|
| 328 |
+
b. Where Your right to use the Licensed Material has terminated under
|
| 329 |
+
Section 6(a), it reinstates:
|
| 330 |
+
|
| 331 |
+
1. automatically as of the date the violation is cured, provided
|
| 332 |
+
it is cured within 30 days of Your discovery of the
|
| 333 |
+
violation; or
|
| 334 |
+
|
| 335 |
+
2. upon express reinstatement by the Licensor.
|
| 336 |
+
|
| 337 |
+
For the avoidance of doubt, this Section 6(b) does not affect any
|
| 338 |
+
right the Licensor may have to seek remedies for Your violations
|
| 339 |
+
of this Public License.
|
| 340 |
+
|
| 341 |
+
c. For the avoidance of doubt, the Licensor may also offer the
|
| 342 |
+
Licensed Material under separate terms or conditions or stop
|
| 343 |
+
distributing the Licensed Material at any time; however, doing so
|
| 344 |
+
will not terminate this Public License.
|
| 345 |
+
|
| 346 |
+
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
| 347 |
+
License.
|
| 348 |
+
|
| 349 |
+
Section 7 -- Other Terms and Conditions.
|
| 350 |
+
|
| 351 |
+
a. The Licensor shall not be bound by any additional or different
|
| 352 |
+
terms or conditions communicated by You unless expressly agreed.
|
| 353 |
+
|
| 354 |
+
b. Any arrangements, understandings, or agreements regarding the
|
| 355 |
+
Licensed Material not stated herein are separate from and
|
| 356 |
+
independent of the terms and conditions of this Public License.
|
| 357 |
+
|
| 358 |
+
Section 8 -- Interpretation.
|
| 359 |
+
|
| 360 |
+
a. For the avoidance of doubt, this Public License does not, and
|
| 361 |
+
shall not be interpreted to, reduce, limit, restrict, or impose
|
| 362 |
+
conditions on any use of the Licensed Material that could lawfully
|
| 363 |
+
be made without permission under this Public License.
|
| 364 |
+
|
| 365 |
+
b. To the extent possible, if any provision of this Public License is
|
| 366 |
+
deemed unenforceable, it shall be automatically reformed to the
|
| 367 |
+
minimum extent necessary to make it enforceable. If the provision
|
| 368 |
+
cannot be reformed, it shall be severed from this Public License
|
| 369 |
+
without affecting the enforceability of the remaining terms and
|
| 370 |
+
conditions.
|
| 371 |
+
|
| 372 |
+
c. No term or condition of this Public License will be waived and no
|
| 373 |
+
failure to comply consented to unless expressly agreed to by the
|
| 374 |
+
Licensor.
|
| 375 |
+
|
| 376 |
+
d. Nothing in this Public License constitutes or may be interpreted
|
| 377 |
+
as a limitation upon, or waiver of, any privileges and immunities
|
| 378 |
+
that apply to the Licensor or You, including from the legal
|
| 379 |
+
processes of any jurisdiction or authority.
|
| 380 |
+
|
| 381 |
+
=======================================================================
|
| 382 |
+
|
| 383 |
+
Creative Commons is not a party to its public
|
| 384 |
+
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
| 385 |
+
its public licenses to material it publishes and in those instances
|
| 386 |
+
will be considered the “Licensor.” The text of the Creative Commons
|
| 387 |
+
public licenses is dedicated to the public domain under the CC0 Public
|
| 388 |
+
Domain Dedication. Except for the limited purpose of indicating that
|
| 389 |
+
material is shared under a Creative Commons public license or as
|
| 390 |
+
otherwise permitted by the Creative Commons policies published at
|
| 391 |
+
creativecommons.org/policies, Creative Commons does not authorize the
|
| 392 |
+
use of the trademark "Creative Commons" or any other trademark or logo
|
| 393 |
+
of Creative Commons without its prior written consent including,
|
| 394 |
+
without limitation, in connection with any unauthorized modifications
|
| 395 |
+
to any of its public licenses or any other arrangements,
|
| 396 |
+
understandings, or agreements concerning use of licensed material. For
|
| 397 |
+
the avoidance of doubt, this paragraph does not form part of the
|
| 398 |
+
public licenses.
|
| 399 |
+
|
| 400 |
+
Creative Commons may be contacted at creativecommons.org.
|
mamba_ssm/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__version__ = "2.0.3"
|
| 2 |
+
|
| 3 |
+
from .ops.selective_scan_interface import selective_scan_fn, mamba_inner_fn
|
| 4 |
+
from .modules.mamba_simple import Mamba
|
| 5 |
+
from .modules.mamba2 import Mamba2
|
| 6 |
+
from .models.mixer_seq_simple import MambaLMHeadModel
|
mamba_ssm/distributed/__init__.py
ADDED
|
File without changes
|
mamba_ssm/distributed/distributed_utils.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import Tensor
|
| 5 |
+
from torch.distributed import ProcessGroup
|
| 6 |
+
|
| 7 |
+
# `all_gather_into_tensor` and `reduce_scatter_tensor` are new placeholders for
|
| 8 |
+
# `_all_gather_base` and `_reduce_scatter_base`. They require the most recent
|
| 9 |
+
# version of PyTorch. The following 4 lines are for backward compatibility with
|
| 10 |
+
# older PyTorch.
|
| 11 |
+
if "all_gather_into_tensor" not in dir(torch.distributed):
|
| 12 |
+
torch.distributed.all_gather_into_tensor = torch.distributed._all_gather_base
|
| 13 |
+
if "reduce_scatter_tensor" not in dir(torch.distributed):
|
| 14 |
+
torch.distributed.reduce_scatter_tensor = torch.distributed._reduce_scatter_base
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Raw operation, does not support autograd, but does support async
|
| 18 |
+
def all_gather_raw(input_: Tensor, process_group: ProcessGroup, async_op: bool = False):
|
| 19 |
+
world_size = torch.distributed.get_world_size(process_group)
|
| 20 |
+
output = torch.empty(
|
| 21 |
+
world_size * input_.shape[0], *input_.shape[1:], dtype=input_.dtype, device=input_.device
|
| 22 |
+
)
|
| 23 |
+
handle = torch.distributed.all_gather_into_tensor(
|
| 24 |
+
output, input_.contiguous(), group=process_group, async_op=async_op
|
| 25 |
+
)
|
| 26 |
+
return output, handle
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# Raw operation, does not support autograd, but does support async
|
| 30 |
+
def reduce_scatter_raw(input_: Tensor, process_group: ProcessGroup, async_op: bool = False):
|
| 31 |
+
world_size = torch.distributed.get_world_size(process_group)
|
| 32 |
+
assert input_.shape[0] % world_size == 0
|
| 33 |
+
output = torch.empty(
|
| 34 |
+
input_.shape[0] // world_size, *input_.shape[1:], dtype=input_.dtype, device=input_.device
|
| 35 |
+
)
|
| 36 |
+
handle = torch.distributed.reduce_scatter_tensor(
|
| 37 |
+
output, input_.contiguous(), group=process_group, async_op=async_op
|
| 38 |
+
)
|
| 39 |
+
return output, handle
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# Raw operation, does not support autograd, but does support async
|
| 43 |
+
def all_reduce_raw(input_: Tensor, process_group: ProcessGroup, async_op: bool = False):
|
| 44 |
+
input_ = input_.contiguous()
|
| 45 |
+
handle = torch.distributed.all_reduce(input_, group=process_group, async_op=async_op)
|
| 46 |
+
return input_, handle
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class AllGatherFunc(torch.autograd.Function):
|
| 50 |
+
"""Gather the input from sequence parallel region and concatenate."""
|
| 51 |
+
|
| 52 |
+
@staticmethod
|
| 53 |
+
def forward(ctx, input_: Tensor, process_group: ProcessGroup) -> Tensor:
|
| 54 |
+
ctx.process_group = process_group
|
| 55 |
+
output, _ = all_gather_raw(input_, process_group)
|
| 56 |
+
return output
|
| 57 |
+
|
| 58 |
+
@staticmethod
|
| 59 |
+
def backward(ctx, grad_output: Tensor):
|
| 60 |
+
grad_input, _ = reduce_scatter_raw(grad_output, ctx.process_group)
|
| 61 |
+
return grad_input, None
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# Supports autograd, but does not support async
|
| 65 |
+
all_gather = AllGatherFunc.apply
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class ReduceScatterFunc(torch.autograd.Function):
|
| 69 |
+
"""Reduce scatter the input from the sequence parallel region and concatenate."""
|
| 70 |
+
|
| 71 |
+
@staticmethod
|
| 72 |
+
def forward(ctx, input_: Tensor, process_group: ProcessGroup) -> Tensor:
|
| 73 |
+
ctx.process_group = process_group
|
| 74 |
+
output, _ = reduce_scatter_raw(input_, process_group)
|
| 75 |
+
return output
|
| 76 |
+
|
| 77 |
+
@staticmethod
|
| 78 |
+
def backward(ctx, grad_output: Tensor):
|
| 79 |
+
grad_input, _ = all_gather_raw(grad_output, ctx.process_group)
|
| 80 |
+
return grad_input, None
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# Supports autograd, but does not support async
|
| 84 |
+
reduce_scatter = ReduceScatterFunc.apply
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class AllReduceFunc(torch.autograd.Function):
|
| 88 |
+
"""Gather the input from sequence parallel region and concatenate."""
|
| 89 |
+
|
| 90 |
+
@staticmethod
|
| 91 |
+
def forward(ctx, input_: Tensor, process_group: ProcessGroup) -> Tensor:
|
| 92 |
+
ctx.process_group = process_group
|
| 93 |
+
output, _ = all_reduce_raw(input_, process_group)
|
| 94 |
+
return output
|
| 95 |
+
|
| 96 |
+
@staticmethod
|
| 97 |
+
def backward(ctx, grad_output: Tensor):
|
| 98 |
+
return grad_output, None
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# Supports autograd, but does not support async
|
| 102 |
+
all_reduce = AllReduceFunc.apply
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def sync_shared_params(model: torch.nn.Module, process_group: ProcessGroup):
|
| 106 |
+
# We want to iterate over parameters with _shared_params=True in the same order,
|
| 107 |
+
# as different ranks might have different number of parameters (e.g., only rank 0 has bias).
|
| 108 |
+
pamams_shared = {
|
| 109 |
+
name: p for name, p in model.named_parameters() if getattr(p, "_shared_params", False)
|
| 110 |
+
}
|
| 111 |
+
for _, p in sorted(pamams_shared.items()):
|
| 112 |
+
with torch.no_grad():
|
| 113 |
+
# Broadcast needs src to be global rank, not group rank
|
| 114 |
+
torch.distributed.broadcast(
|
| 115 |
+
p, src=torch.distributed.get_global_rank(process_group, 0), group=process_group
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# Ref: https://github.com/NVIDIA/Megatron-LM/blob/52e636888cccc41e931251c417a7181fc36de926/megatron/optimizer/optimizer.py#L256
|
| 120 |
+
def allreduce_sequence_parallel_grad(model: torch.nn.Module, process_group: ProcessGroup):
|
| 121 |
+
# We want to iterate over parameters with _sequence_parallel=True in the same order,
|
| 122 |
+
# as different ranks might have different number of parameters (e.g., only rank 0 has bias).
|
| 123 |
+
params_seqparallel = {
|
| 124 |
+
name: p for name, p in model.named_parameters() if getattr(p, "_sequence_parallel", False)
|
| 125 |
+
}
|
| 126 |
+
grads = [p.grad for _, p in sorted(params_seqparallel.items())]
|
| 127 |
+
if grads:
|
| 128 |
+
with torch.no_grad():
|
| 129 |
+
coalesced = torch._utils._flatten_dense_tensors(grads)
|
| 130 |
+
torch.distributed.all_reduce(coalesced, group=process_group)
|
| 131 |
+
for buf, synced in zip(grads, torch._utils._unflatten_dense_tensors(coalesced, grads)):
|
| 132 |
+
buf.copy_(synced)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def get_dim_for_local_rank(dim: int, world_size: int, local_rank: int, multiple_of: int = 1) -> int:
|
| 136 |
+
"""Get the dim for the local rank derived from splitting dim on world_size processes.
|
| 137 |
+
|
| 138 |
+
The split may not be even across the world_size processes.
|
| 139 |
+
"""
|
| 140 |
+
multiple = dim // multiple_of
|
| 141 |
+
div = multiple // world_size
|
| 142 |
+
mod = multiple % world_size
|
| 143 |
+
local_multiple = div + int(local_rank < mod)
|
| 144 |
+
return local_multiple * multiple_of
|
mamba_ssm/distributed/tensor_parallel.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2024, Tri Dao.
|
| 2 |
+
# The TensorParallel linear modules are inspired by https://github.com/NVIDIA/apex/blob/master/apex/transformer/tensor_parallel/layers.py
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
from torch.cuda.amp import custom_bwd, custom_fwd
|
| 10 |
+
from torch.distributed import ProcessGroup
|
| 11 |
+
|
| 12 |
+
from einops import rearrange
|
| 13 |
+
|
| 14 |
+
from .distributed_utils import (
|
| 15 |
+
all_gather_raw,
|
| 16 |
+
all_reduce,
|
| 17 |
+
all_reduce_raw,
|
| 18 |
+
reduce_scatter,
|
| 19 |
+
reduce_scatter_raw,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ParallelLinearFunc(torch.autograd.Function):
|
| 24 |
+
@staticmethod
|
| 25 |
+
@custom_fwd
|
| 26 |
+
def forward(ctx, x, weight, bias, process_group=None, sequence_parallel=True):
|
| 27 |
+
"""
|
| 28 |
+
If process_group is not None and sequence_parallel=True, we're doing Tensor Parallel
|
| 29 |
+
with sequence parallelism: we do an all_gather_raw of x before doing the matmul.
|
| 30 |
+
"""
|
| 31 |
+
ctx.compute_weight_gradient = weight.requires_grad
|
| 32 |
+
ctx.process_group = process_group
|
| 33 |
+
ctx.sequence_parallel = sequence_parallel
|
| 34 |
+
|
| 35 |
+
if torch.is_autocast_enabled():
|
| 36 |
+
x = x.to(dtype=torch.get_autocast_gpu_dtype())
|
| 37 |
+
x = x.contiguous()
|
| 38 |
+
if process_group is not None and sequence_parallel:
|
| 39 |
+
# We want to kick off the all_gather early, before weight dtype conversion
|
| 40 |
+
total_x, handle_x = all_gather_raw(x, process_group, async_op=True)
|
| 41 |
+
else:
|
| 42 |
+
total_x = x
|
| 43 |
+
|
| 44 |
+
if torch.is_autocast_enabled():
|
| 45 |
+
weight = weight.to(dtype=torch.get_autocast_gpu_dtype())
|
| 46 |
+
bias = bias.to(dtype=torch.get_autocast_gpu_dtype()) if bias is not None else None
|
| 47 |
+
weight = weight.contiguous()
|
| 48 |
+
if process_group is not None and sequence_parallel:
|
| 49 |
+
handle_x.wait()
|
| 50 |
+
batch_shape, n = total_x.shape[:-1], total_x.shape[-1]
|
| 51 |
+
batch_dim = batch_shape.numel()
|
| 52 |
+
# https://github.com/pytorch/pytorch/blob/5b51849b48a7dbccd297286cc0110def4706f9e7/aten/src/ATen/native/cuda/Blas.cpp#L174
|
| 53 |
+
output = F.linear(total_x, weight, bias)
|
| 54 |
+
if ctx.compute_weight_gradient:
|
| 55 |
+
ctx.save_for_backward(x, weight)
|
| 56 |
+
else:
|
| 57 |
+
ctx.save_for_backward(weight)
|
| 58 |
+
return output
|
| 59 |
+
|
| 60 |
+
@staticmethod
|
| 61 |
+
@custom_bwd
|
| 62 |
+
def backward(ctx, grad_output):
|
| 63 |
+
grad_output = grad_output.contiguous()
|
| 64 |
+
process_group = ctx.process_group
|
| 65 |
+
sequence_parallel = ctx.sequence_parallel
|
| 66 |
+
if ctx.compute_weight_gradient:
|
| 67 |
+
x, weight = ctx.saved_tensors
|
| 68 |
+
if process_group is not None and sequence_parallel:
|
| 69 |
+
total_x, handle_x = all_gather_raw(x, process_group, async_op=True)
|
| 70 |
+
else:
|
| 71 |
+
total_x = x
|
| 72 |
+
else:
|
| 73 |
+
(weight,) = ctx.saved_tensors
|
| 74 |
+
total_x = None
|
| 75 |
+
batch_shape = grad_output.shape[:-1]
|
| 76 |
+
batch_dim = batch_shape.numel()
|
| 77 |
+
grad_output = grad_output.reshape(batch_dim, grad_output.shape[-1])
|
| 78 |
+
if ctx.needs_input_grad[0]:
|
| 79 |
+
grad_input = F.linear(grad_output, weight.t())
|
| 80 |
+
grad_input = grad_input.reshape(*batch_shape, grad_input.shape[-1])
|
| 81 |
+
if process_group is not None:
|
| 82 |
+
reduce_fn = reduce_scatter_raw if sequence_parallel else all_reduce_raw
|
| 83 |
+
grad_input, handle_grad_input = reduce_fn(grad_input, process_group, async_op=True)
|
| 84 |
+
else:
|
| 85 |
+
grad_input = None
|
| 86 |
+
if ctx.needs_input_grad[1]:
|
| 87 |
+
assert ctx.compute_weight_gradient
|
| 88 |
+
if process_group is not None and sequence_parallel:
|
| 89 |
+
handle_x.wait()
|
| 90 |
+
grad_weight = torch.einsum(
|
| 91 |
+
"bo,bi->oi", grad_output, total_x.reshape(batch_dim, total_x.shape[-1])
|
| 92 |
+
)
|
| 93 |
+
else:
|
| 94 |
+
grad_weight = None
|
| 95 |
+
grad_bias = grad_output.sum(dim=0) if ctx.needs_input_grad[2] else None
|
| 96 |
+
if process_group is not None and ctx.needs_input_grad[0]:
|
| 97 |
+
handle_grad_input.wait()
|
| 98 |
+
return grad_input, grad_weight, grad_bias, None, None
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def parallel_linear_func(
|
| 102 |
+
x: Tensor,
|
| 103 |
+
weight: Tensor,
|
| 104 |
+
bias: Optional[Tensor] = None,
|
| 105 |
+
process_group: Optional[ProcessGroup] = None,
|
| 106 |
+
sequence_parallel: bool = True,
|
| 107 |
+
):
|
| 108 |
+
return ParallelLinearFunc.apply(x, weight, bias, process_group, sequence_parallel)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class ColumnParallelLinear(nn.Linear):
|
| 112 |
+
def __init__(
|
| 113 |
+
self,
|
| 114 |
+
in_features: int,
|
| 115 |
+
out_features: int,
|
| 116 |
+
process_group: ProcessGroup,
|
| 117 |
+
bias: bool = True,
|
| 118 |
+
sequence_parallel=True,
|
| 119 |
+
multiple_of=1,
|
| 120 |
+
device=None,
|
| 121 |
+
dtype=None,
|
| 122 |
+
) -> None:
|
| 123 |
+
world_size = torch.distributed.get_world_size(process_group)
|
| 124 |
+
if out_features % multiple_of:
|
| 125 |
+
raise ValueError(f"out_features ({out_features}) must be a multiple of {multiple_of}")
|
| 126 |
+
multiple = out_features // multiple_of
|
| 127 |
+
# We want to split @multiple across world_size, but it could be an uneven split
|
| 128 |
+
div = multiple // world_size
|
| 129 |
+
mod = multiple % world_size
|
| 130 |
+
# The first @mod ranks get @div + 1 copies, the rest get @div copies
|
| 131 |
+
local_multiple = div + int(torch.distributed.get_rank(process_group) < mod)
|
| 132 |
+
super().__init__(
|
| 133 |
+
in_features, local_multiple * multiple_of, bias=bias, device=device, dtype=dtype
|
| 134 |
+
)
|
| 135 |
+
self.process_group = process_group
|
| 136 |
+
self.sequence_parallel = sequence_parallel
|
| 137 |
+
|
| 138 |
+
def forward(self, x):
|
| 139 |
+
# If self.sequence_parallel is True, we're doing Tensor Parallel with sequence parallelism:
|
| 140 |
+
# we do an all_gather of x before doing the matmul.
|
| 141 |
+
# If not, then the input is already gathered.
|
| 142 |
+
return parallel_linear_func(
|
| 143 |
+
x,
|
| 144 |
+
self.weight,
|
| 145 |
+
self.bias,
|
| 146 |
+
process_group=self.process_group,
|
| 147 |
+
sequence_parallel=self.sequence_parallel,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
class RowParallelLinear(nn.Linear):
|
| 152 |
+
def __init__(
|
| 153 |
+
self,
|
| 154 |
+
in_features: int,
|
| 155 |
+
out_features: int,
|
| 156 |
+
process_group: ProcessGroup,
|
| 157 |
+
bias: bool = True,
|
| 158 |
+
sequence_parallel=True,
|
| 159 |
+
multiple_of=1,
|
| 160 |
+
device=None,
|
| 161 |
+
dtype=None,
|
| 162 |
+
) -> None:
|
| 163 |
+
world_size = torch.distributed.get_world_size(process_group)
|
| 164 |
+
rank = torch.distributed.get_rank(process_group)
|
| 165 |
+
if in_features % multiple_of:
|
| 166 |
+
raise ValueError(f"in_features ({in_features}) must be a multiple of {multiple_of}")
|
| 167 |
+
multiple = in_features // multiple_of
|
| 168 |
+
# We want to split @multiple across world_size, but it could be an uneven split
|
| 169 |
+
div = multiple // world_size
|
| 170 |
+
mod = multiple % world_size
|
| 171 |
+
# The first @mod ranks get @div + 1 copies, the rest get @div copies
|
| 172 |
+
local_multiple = div + int(torch.distributed.get_rank(process_group) < mod)
|
| 173 |
+
# Only rank 0 will have bias
|
| 174 |
+
super().__init__(
|
| 175 |
+
local_multiple * multiple_of,
|
| 176 |
+
out_features,
|
| 177 |
+
bias=bias and rank == 0,
|
| 178 |
+
device=device,
|
| 179 |
+
dtype=dtype,
|
| 180 |
+
)
|
| 181 |
+
self.process_group = process_group
|
| 182 |
+
self.sequence_parallel = sequence_parallel
|
| 183 |
+
|
| 184 |
+
def forward(self, x):
|
| 185 |
+
"""
|
| 186 |
+
We're doing Tensor Parallel with sequence parallelism: we do the matmul and then
|
| 187 |
+
a reduce_scatter of the result.
|
| 188 |
+
"""
|
| 189 |
+
out = parallel_linear_func(x, self.weight, self.bias)
|
| 190 |
+
reduce_fn = reduce_scatter if self.sequence_parallel else all_reduce
|
| 191 |
+
return reduce_fn(out, self.process_group)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
class VocabParallelEmbedding(nn.Embedding):
|
| 195 |
+
def __init__(self, num_embeddings, *args, process_group=None, padding_idx=None, **kwargs):
|
| 196 |
+
self.process_group = process_group
|
| 197 |
+
if process_group is not None:
|
| 198 |
+
world_size = torch.distributed.get_world_size(process_group)
|
| 199 |
+
if num_embeddings % world_size != 0:
|
| 200 |
+
raise ValueError(
|
| 201 |
+
f"num_embeddings ({num_embeddings}) must be divisible by "
|
| 202 |
+
f"world_size ({world_size})"
|
| 203 |
+
)
|
| 204 |
+
if world_size > 1 and padding_idx is not None:
|
| 205 |
+
raise RuntimeError("ParallelEmbedding does not support padding_idx")
|
| 206 |
+
else:
|
| 207 |
+
world_size = 1
|
| 208 |
+
super().__init__(num_embeddings // world_size, *args, padding_idx=padding_idx, **kwargs)
|
| 209 |
+
|
| 210 |
+
def forward(self, input: Tensor) -> Tensor:
|
| 211 |
+
if self.process_group is None:
|
| 212 |
+
return super().forward(input)
|
| 213 |
+
else:
|
| 214 |
+
rank = torch.distributed.get_rank(self.process_group)
|
| 215 |
+
vocab_size = self.num_embeddings
|
| 216 |
+
vocab_start_index, vocab_end_index = rank * vocab_size, (rank + 1) * vocab_size
|
| 217 |
+
# Create a mask of valid vocab ids (1 means it needs to be masked).
|
| 218 |
+
input_ids_mask = (input < vocab_start_index) | (input >= vocab_end_index)
|
| 219 |
+
input = input - vocab_start_index
|
| 220 |
+
input[input_ids_mask] = 0
|
| 221 |
+
embeddings = super().forward(input)
|
| 222 |
+
embeddings[input_ids_mask] = 0.0
|
| 223 |
+
return embeddings
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
class ColumnParallelEmbedding(nn.Embedding):
|
| 227 |
+
def __init__(self, num_embeddings, embedding_dim, *args, process_group=None, **kwargs):
|
| 228 |
+
self.process_group = process_group
|
| 229 |
+
if process_group is not None:
|
| 230 |
+
world_size = torch.distributed.get_world_size(process_group)
|
| 231 |
+
if embedding_dim % world_size != 0:
|
| 232 |
+
raise ValueError(
|
| 233 |
+
f"embedding_dim ({embedding_dim}) must be divisible by "
|
| 234 |
+
f"world_size ({world_size})"
|
| 235 |
+
)
|
| 236 |
+
else:
|
| 237 |
+
world_size = 1
|
| 238 |
+
super().__init__(num_embeddings, embedding_dim // world_size, *args, **kwargs)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
class ParallelEmbeddings(nn.Module):
|
| 242 |
+
def __init__(
|
| 243 |
+
self,
|
| 244 |
+
embed_dim,
|
| 245 |
+
vocab_size,
|
| 246 |
+
max_position_embeddings,
|
| 247 |
+
process_group,
|
| 248 |
+
padding_idx=None,
|
| 249 |
+
sequence_parallel=True,
|
| 250 |
+
device=None,
|
| 251 |
+
dtype=None,
|
| 252 |
+
):
|
| 253 |
+
"""
|
| 254 |
+
If max_position_embeddings <= 0, there's no position embeddings
|
| 255 |
+
"""
|
| 256 |
+
factory_kwargs = {"device": device, "dtype": dtype}
|
| 257 |
+
super().__init__()
|
| 258 |
+
self.process_group = process_group
|
| 259 |
+
self.sequence_parallel = sequence_parallel
|
| 260 |
+
self.word_embeddings = VocabParallelEmbedding(
|
| 261 |
+
vocab_size,
|
| 262 |
+
embed_dim,
|
| 263 |
+
padding_idx=padding_idx,
|
| 264 |
+
process_group=process_group,
|
| 265 |
+
**factory_kwargs,
|
| 266 |
+
)
|
| 267 |
+
self.max_position_embeddings = max_position_embeddings
|
| 268 |
+
if self.max_position_embeddings > 0:
|
| 269 |
+
self.position_embeddings = ColumnParallelEmbedding(
|
| 270 |
+
max_position_embeddings, embed_dim, process_group=process_group, **factory_kwargs
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
def forward(self, input_ids, position_ids=None, combine_batch_seqlen_dim=False):
|
| 274 |
+
"""
|
| 275 |
+
input_ids: (batch, seqlen)
|
| 276 |
+
position_ids: (batch, seqlen)
|
| 277 |
+
"""
|
| 278 |
+
batch_size, seqlen = input_ids.shape
|
| 279 |
+
world_size = torch.distributed.get_world_size(self.process_group)
|
| 280 |
+
embeddings = self.word_embeddings(input_ids)
|
| 281 |
+
if self.max_position_embeddings > 0:
|
| 282 |
+
if position_ids is None:
|
| 283 |
+
position_ids = torch.arange(seqlen, dtype=torch.long, device=input_ids.device)
|
| 284 |
+
position_embeddings = self.position_embeddings(position_ids)
|
| 285 |
+
if world_size <= 1:
|
| 286 |
+
embeddings = embeddings + position_embeddings
|
| 287 |
+
else:
|
| 288 |
+
partition_dim = self.position_embeddings.embedding_dim
|
| 289 |
+
rank = torch.distributed.get_rank(self.process_group)
|
| 290 |
+
embeddings[
|
| 291 |
+
..., rank * partition_dim : (rank + 1) * partition_dim
|
| 292 |
+
] += position_embeddings
|
| 293 |
+
if combine_batch_seqlen_dim:
|
| 294 |
+
embeddings = rearrange(embeddings, "b s d -> (b s) d")
|
| 295 |
+
reduce_fn = reduce_scatter if self.sequence_parallel else all_reduce
|
| 296 |
+
return embeddings if world_size <= 1 else reduce_fn(embeddings, self.process_group)
|
mamba_ssm/models/__init__.py
ADDED
|
File without changes
|
mamba_ssm/models/config_mamba.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass, field
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
@dataclass
|
| 5 |
+
class MambaConfig:
|
| 6 |
+
|
| 7 |
+
d_model: int = 2560
|
| 8 |
+
d_intermediate: int = 0
|
| 9 |
+
n_layer: int = 64
|
| 10 |
+
vocab_size: int = 50277
|
| 11 |
+
ssm_cfg: dict = field(default_factory=dict)
|
| 12 |
+
attn_layer_idx: list = field(default_factory=list)
|
| 13 |
+
attn_cfg: dict = field(default_factory=dict)
|
| 14 |
+
rms_norm: bool = True
|
| 15 |
+
residual_in_fp32: bool = True
|
| 16 |
+
fused_add_norm: bool = True
|
| 17 |
+
pad_vocab_size_multiple: int = 8
|
| 18 |
+
tie_embeddings: bool = True
|
mamba_ssm/models/mixer_seq_simple.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2023, Albert Gu, Tri Dao.
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
from functools import partial
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import copy
|
| 8 |
+
|
| 9 |
+
from collections import namedtuple
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn as nn
|
| 13 |
+
|
| 14 |
+
from .config_mamba import MambaConfig
|
| 15 |
+
from ..modules.mamba_simple import Mamba
|
| 16 |
+
from ..modules.mamba2 import Mamba2
|
| 17 |
+
from ..modules.mha import MHA
|
| 18 |
+
from ..modules.mlp import GatedMLP
|
| 19 |
+
from ..modules.block import Block
|
| 20 |
+
from ..utils.generation import GenerationMixin
|
| 21 |
+
from ..utils.hf import load_config_hf, load_state_dict_hf
|
| 22 |
+
|
| 23 |
+
from ..ops.triton.layer_norm import RMSNorm, layer_norm_fn, rms_norm_fn
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def create_block(
|
| 28 |
+
d_model,
|
| 29 |
+
d_intermediate,
|
| 30 |
+
ssm_cfg=None,
|
| 31 |
+
attn_layer_idx=None,
|
| 32 |
+
attn_cfg=None,
|
| 33 |
+
norm_epsilon=1e-5,
|
| 34 |
+
rms_norm=False,
|
| 35 |
+
residual_in_fp32=False,
|
| 36 |
+
fused_add_norm=False,
|
| 37 |
+
layer_idx=None,
|
| 38 |
+
device=None,
|
| 39 |
+
dtype=None,
|
| 40 |
+
):
|
| 41 |
+
if ssm_cfg is None:
|
| 42 |
+
ssm_cfg = {}
|
| 43 |
+
if attn_layer_idx is None:
|
| 44 |
+
attn_layer_idx = []
|
| 45 |
+
if attn_cfg is None:
|
| 46 |
+
attn_cfg = {}
|
| 47 |
+
factory_kwargs = {"device": device, "dtype": dtype}
|
| 48 |
+
if layer_idx not in attn_layer_idx:
|
| 49 |
+
# Create a copy of the config to modify
|
| 50 |
+
ssm_cfg = copy.deepcopy(ssm_cfg) if ssm_cfg is not None else {}
|
| 51 |
+
ssm_layer = ssm_cfg.pop("layer", "Mamba1")
|
| 52 |
+
if ssm_layer not in ["Mamba1", "Mamba2"]:
|
| 53 |
+
raise ValueError(f"Invalid ssm_layer: {ssm_layer}, only support Mamba1 and Mamba2")
|
| 54 |
+
mixer_cls = partial(
|
| 55 |
+
Mamba2 if ssm_layer == "Mamba2" else Mamba,
|
| 56 |
+
layer_idx=layer_idx,
|
| 57 |
+
**ssm_cfg,
|
| 58 |
+
**factory_kwargs
|
| 59 |
+
)
|
| 60 |
+
else:
|
| 61 |
+
mixer_cls = partial(MHA, layer_idx=layer_idx, **attn_cfg, **factory_kwargs)
|
| 62 |
+
norm_cls = partial(
|
| 63 |
+
nn.LayerNorm if not rms_norm else RMSNorm, eps=norm_epsilon, **factory_kwargs
|
| 64 |
+
)
|
| 65 |
+
if d_intermediate == 0:
|
| 66 |
+
mlp_cls = nn.Identity
|
| 67 |
+
else:
|
| 68 |
+
mlp_cls = partial(
|
| 69 |
+
GatedMLP, hidden_features=d_intermediate, out_features=d_model, **factory_kwargs
|
| 70 |
+
)
|
| 71 |
+
block = Block(
|
| 72 |
+
d_model,
|
| 73 |
+
mixer_cls,
|
| 74 |
+
mlp_cls,
|
| 75 |
+
norm_cls=norm_cls,
|
| 76 |
+
fused_add_norm=fused_add_norm,
|
| 77 |
+
residual_in_fp32=residual_in_fp32,
|
| 78 |
+
)
|
| 79 |
+
block.layer_idx = layer_idx
|
| 80 |
+
return block
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# https://github.com/huggingface/transformers/blob/c28d04e9e252a1a099944e325685f14d242ecdcd/src/transformers/models/gpt2/modeling_gpt2.py#L454
|
| 84 |
+
def _init_weights(
|
| 85 |
+
module,
|
| 86 |
+
n_layer,
|
| 87 |
+
initializer_range=0.02, # Now only used for embedding layer.
|
| 88 |
+
rescale_prenorm_residual=True,
|
| 89 |
+
n_residuals_per_layer=1, # Change to 2 if we have MLP
|
| 90 |
+
):
|
| 91 |
+
if isinstance(module, nn.Linear):
|
| 92 |
+
if module.bias is not None:
|
| 93 |
+
if not getattr(module.bias, "_no_reinit", False):
|
| 94 |
+
nn.init.zeros_(module.bias)
|
| 95 |
+
elif isinstance(module, nn.Embedding):
|
| 96 |
+
nn.init.normal_(module.weight, std=initializer_range)
|
| 97 |
+
|
| 98 |
+
if rescale_prenorm_residual:
|
| 99 |
+
# Reinitialize selected weights subject to the OpenAI GPT-2 Paper Scheme:
|
| 100 |
+
# > A modified initialization which accounts for the accumulation on the residual path with model depth. Scale
|
| 101 |
+
# > the weights of residual layers at initialization by a factor of 1/√N where N is the # of residual layers.
|
| 102 |
+
# > -- GPT-2 :: https://openai.com/blog/better-language-models/
|
| 103 |
+
#
|
| 104 |
+
# Reference (Megatron-LM): https://github.com/NVIDIA/Megatron-LM/blob/main/megatron/model/gpt_model.py
|
| 105 |
+
for name, p in module.named_parameters():
|
| 106 |
+
if name in ["out_proj.weight", "fc2.weight"]:
|
| 107 |
+
# Special Scaled Initialization --> There are 2 Layer Norms per Transformer Block
|
| 108 |
+
# Following Pytorch init, except scale by 1/sqrt(2 * n_layer)
|
| 109 |
+
# We need to reinit p since this code could be called multiple times
|
| 110 |
+
# Having just p *= scale would repeatedly scale it down
|
| 111 |
+
nn.init.kaiming_uniform_(p, a=math.sqrt(5))
|
| 112 |
+
with torch.no_grad():
|
| 113 |
+
p /= math.sqrt(n_residuals_per_layer * n_layer)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class MixerModel(nn.Module):
|
| 117 |
+
def __init__(
|
| 118 |
+
self,
|
| 119 |
+
d_model: int,
|
| 120 |
+
n_layer: int,
|
| 121 |
+
d_intermediate: int,
|
| 122 |
+
vocab_size: int,
|
| 123 |
+
ssm_cfg=None,
|
| 124 |
+
attn_layer_idx=None,
|
| 125 |
+
attn_cfg=None,
|
| 126 |
+
norm_epsilon: float = 1e-5,
|
| 127 |
+
rms_norm: bool = False,
|
| 128 |
+
initializer_cfg=None,
|
| 129 |
+
fused_add_norm=False,
|
| 130 |
+
residual_in_fp32=False,
|
| 131 |
+
device=None,
|
| 132 |
+
dtype=None,
|
| 133 |
+
) -> None:
|
| 134 |
+
factory_kwargs = {"device": device, "dtype": dtype}
|
| 135 |
+
super().__init__()
|
| 136 |
+
self.residual_in_fp32 = residual_in_fp32
|
| 137 |
+
|
| 138 |
+
self.embedding = nn.Embedding(vocab_size, d_model, **factory_kwargs)
|
| 139 |
+
|
| 140 |
+
# We change the order of residual and layer norm:
|
| 141 |
+
# Instead of LN -> Attn / MLP -> Add, we do:
|
| 142 |
+
# Add -> LN -> Attn / MLP / Mixer, returning both the residual branch (output of Add) and
|
| 143 |
+
# the main branch (output of MLP / Mixer). The model definition is unchanged.
|
| 144 |
+
# This is for performance reason: we can fuse add + layer_norm.
|
| 145 |
+
self.fused_add_norm = fused_add_norm
|
| 146 |
+
if self.fused_add_norm:
|
| 147 |
+
if layer_norm_fn is None or rms_norm_fn is None:
|
| 148 |
+
raise ImportError("Failed to import Triton LayerNorm / RMSNorm kernels")
|
| 149 |
+
|
| 150 |
+
self.layers = nn.ModuleList(
|
| 151 |
+
[
|
| 152 |
+
create_block(
|
| 153 |
+
d_model,
|
| 154 |
+
d_intermediate=d_intermediate,
|
| 155 |
+
ssm_cfg=ssm_cfg,
|
| 156 |
+
attn_layer_idx=attn_layer_idx,
|
| 157 |
+
attn_cfg=attn_cfg,
|
| 158 |
+
norm_epsilon=norm_epsilon,
|
| 159 |
+
rms_norm=rms_norm,
|
| 160 |
+
residual_in_fp32=residual_in_fp32,
|
| 161 |
+
fused_add_norm=fused_add_norm,
|
| 162 |
+
layer_idx=i,
|
| 163 |
+
**factory_kwargs,
|
| 164 |
+
)
|
| 165 |
+
for i in range(n_layer)
|
| 166 |
+
]
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
self.norm_f = (nn.LayerNorm if not rms_norm else RMSNorm)(
|
| 170 |
+
d_model, eps=norm_epsilon, **factory_kwargs
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
self.apply(
|
| 174 |
+
partial(
|
| 175 |
+
_init_weights,
|
| 176 |
+
n_layer=n_layer,
|
| 177 |
+
**(initializer_cfg if initializer_cfg is not None else {}),
|
| 178 |
+
n_residuals_per_layer=1 if d_intermediate == 0 else 2, # 2 if we have MLP
|
| 179 |
+
)
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
def allocate_inference_cache(self, batch_size, max_seqlen, dtype=None, **kwargs):
|
| 183 |
+
return {
|
| 184 |
+
i: layer.allocate_inference_cache(batch_size, max_seqlen, dtype=dtype, **kwargs)
|
| 185 |
+
for i, layer in enumerate(self.layers)
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
def forward(self, input_ids, inference_params=None, **mixer_kwargs):
|
| 189 |
+
hidden_states = self.embedding(input_ids)
|
| 190 |
+
residual = None
|
| 191 |
+
for layer in self.layers:
|
| 192 |
+
hidden_states, residual = layer(
|
| 193 |
+
hidden_states, residual, inference_params=inference_params
|
| 194 |
+
)
|
| 195 |
+
if not self.fused_add_norm:
|
| 196 |
+
residual = (hidden_states + residual) if residual is not None else hidden_states
|
| 197 |
+
hidden_states = self.norm_f(residual.to(dtype=self.norm_f.weight.dtype))
|
| 198 |
+
else:
|
| 199 |
+
# Set prenorm=False here since we don't need the residual
|
| 200 |
+
hidden_states = layer_norm_fn(
|
| 201 |
+
hidden_states,
|
| 202 |
+
self.norm_f.weight,
|
| 203 |
+
self.norm_f.bias,
|
| 204 |
+
eps=self.norm_f.eps,
|
| 205 |
+
residual=residual,
|
| 206 |
+
prenorm=False,
|
| 207 |
+
residual_in_fp32=self.residual_in_fp32,
|
| 208 |
+
is_rms_norm=isinstance(self.norm_f, RMSNorm)
|
| 209 |
+
)
|
| 210 |
+
return hidden_states
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
class MambaLMHeadModel(nn.Module, GenerationMixin):
|
| 214 |
+
|
| 215 |
+
def __init__(
|
| 216 |
+
self,
|
| 217 |
+
config: MambaConfig,
|
| 218 |
+
initializer_cfg=None,
|
| 219 |
+
device=None,
|
| 220 |
+
dtype=None,
|
| 221 |
+
) -> None:
|
| 222 |
+
self.config = config
|
| 223 |
+
d_model = config.d_model
|
| 224 |
+
n_layer = config.n_layer
|
| 225 |
+
d_intermediate = config.d_intermediate
|
| 226 |
+
vocab_size = config.vocab_size
|
| 227 |
+
ssm_cfg = config.ssm_cfg
|
| 228 |
+
attn_layer_idx = config.attn_layer_idx
|
| 229 |
+
attn_cfg = config.attn_cfg
|
| 230 |
+
rms_norm = config.rms_norm
|
| 231 |
+
residual_in_fp32 = config.residual_in_fp32
|
| 232 |
+
fused_add_norm = config.fused_add_norm
|
| 233 |
+
pad_vocab_size_multiple = config.pad_vocab_size_multiple
|
| 234 |
+
factory_kwargs = {"device": device, "dtype": dtype}
|
| 235 |
+
|
| 236 |
+
super().__init__()
|
| 237 |
+
if vocab_size % pad_vocab_size_multiple != 0:
|
| 238 |
+
vocab_size += pad_vocab_size_multiple - (vocab_size % pad_vocab_size_multiple)
|
| 239 |
+
self.backbone = MixerModel(
|
| 240 |
+
d_model=d_model,
|
| 241 |
+
n_layer=n_layer,
|
| 242 |
+
d_intermediate=d_intermediate,
|
| 243 |
+
vocab_size=vocab_size,
|
| 244 |
+
ssm_cfg=ssm_cfg,
|
| 245 |
+
attn_layer_idx=attn_layer_idx,
|
| 246 |
+
attn_cfg=attn_cfg,
|
| 247 |
+
rms_norm=rms_norm,
|
| 248 |
+
initializer_cfg=initializer_cfg,
|
| 249 |
+
fused_add_norm=fused_add_norm,
|
| 250 |
+
residual_in_fp32=residual_in_fp32,
|
| 251 |
+
**factory_kwargs,
|
| 252 |
+
)
|
| 253 |
+
self.lm_head = nn.Linear(d_model, vocab_size, bias=False, **factory_kwargs)
|
| 254 |
+
|
| 255 |
+
# Initialize weights and apply final processing
|
| 256 |
+
self.apply(
|
| 257 |
+
partial(
|
| 258 |
+
_init_weights,
|
| 259 |
+
n_layer=n_layer,
|
| 260 |
+
**(initializer_cfg if initializer_cfg is not None else {}),
|
| 261 |
+
)
|
| 262 |
+
)
|
| 263 |
+
self.tie_weights()
|
| 264 |
+
|
| 265 |
+
def tie_weights(self):
|
| 266 |
+
if self.config.tie_embeddings:
|
| 267 |
+
self.lm_head.weight = self.backbone.embedding.weight
|
| 268 |
+
|
| 269 |
+
def allocate_inference_cache(self, batch_size, max_seqlen, dtype=None, **kwargs):
|
| 270 |
+
return self.backbone.allocate_inference_cache(batch_size, max_seqlen, dtype=dtype, **kwargs)
|
| 271 |
+
|
| 272 |
+
def forward(self, input_ids, position_ids=None, inference_params=None, num_last_tokens=0, **mixer_kwargs):
|
| 273 |
+
"""
|
| 274 |
+
"position_ids" is just to be compatible with Transformer generation. We don't use it.
|
| 275 |
+
num_last_tokens: if > 0, only return the logits for the last n tokens
|
| 276 |
+
"""
|
| 277 |
+
hidden_states = self.backbone(input_ids, inference_params=inference_params, **mixer_kwargs)
|
| 278 |
+
if num_last_tokens > 0:
|
| 279 |
+
hidden_states = hidden_states[:, -num_last_tokens:]
|
| 280 |
+
lm_logits = self.lm_head(hidden_states)
|
| 281 |
+
CausalLMOutput = namedtuple("CausalLMOutput", ["logits"])
|
| 282 |
+
return CausalLMOutput(logits=lm_logits)
|
| 283 |
+
|
| 284 |
+
@classmethod
|
| 285 |
+
def from_pretrained(cls, pretrained_model_name, device=None, dtype=None, **kwargs):
|
| 286 |
+
config_data = load_config_hf(pretrained_model_name)
|
| 287 |
+
config = MambaConfig(**config_data)
|
| 288 |
+
model = cls(config, device=device, dtype=dtype, **kwargs)
|
| 289 |
+
model.load_state_dict(load_state_dict_hf(pretrained_model_name, device=device, dtype=dtype))
|
| 290 |
+
return model
|
| 291 |
+
|
| 292 |
+
def save_pretrained(self, save_directory):
|
| 293 |
+
"""
|
| 294 |
+
Minimal implementation of save_pretrained for MambaLMHeadModel.
|
| 295 |
+
Save the model and its configuration file to a directory.
|
| 296 |
+
"""
|
| 297 |
+
# Ensure save_directory exists
|
| 298 |
+
os.makedirs(save_directory, exist_ok=True)
|
| 299 |
+
|
| 300 |
+
# Save the model's state_dict
|
| 301 |
+
model_path = os.path.join(save_directory, 'pytorch_model.bin')
|
| 302 |
+
torch.save(self.state_dict(), model_path)
|
| 303 |
+
|
| 304 |
+
# Save the configuration of the model
|
| 305 |
+
config_path = os.path.join(save_directory, 'config.json')
|
| 306 |
+
with open(config_path, 'w') as f:
|
| 307 |
+
json.dump(self.config.__dict__, f, indent=4)
|