Image-to-Image
PyTorch
ONNX
TensorRT
English
super-resolution
image-restoration
sisr
real-world-restoration
spandrel
chainner
transformer
attention
Instructions to use Phips/HEART with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- TensorRT
How to use Phips/HEART with TensorRT:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
HEART release package: arch, configs, ONNX, README, license
Browse files- HEART.md +583 -0
- LICENSE +202 -0
- README.md +86 -0
- configs/4x_HEART_OTF_gan.yml +165 -0
- configs/4x_HEART_OTF_v2.yml +155 -0
- configs/4x_HEART_release.yml +91 -0
- heart_arch.py +645 -0
- models/.gitignore +1 -0
- onnx/heart_2x_fp32_op17.onnx +3 -0
- onnx/heart_4x_otf_fp32_op17.onnx +3 -0
- onnx/heart_4x_otf_gan_fp32_op17.onnx +3 -0
- onnx/heart_4x_pretrain_fp32_op17.onnx +3 -0
- scripts/export_heart.py +27 -0
HEART.md
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HEART — Efficient Attention with Rank-factorized bias Transformer
|
| 2 |
+
|
| 3 |
+
**TL;DR:** window-attention SR transformer. HAT-iLN quality, FlashAttention
|
| 4 |
+
speed, none of the complexity. **One architecture, one config. Trains in bf16.
|
| 5 |
+
Exports to dynamic ONNX with a single command** — no fused/unfused checkpoint
|
| 6 |
+
pairs, no custom CUDA kernels, no relative-position tables.
|
| 7 |
+
|
| 8 |
+
## What is HEART?
|
| 9 |
+
|
| 10 |
+
HEART is what HAT-iLN becomes when you strip out the parts nobody actually
|
| 11 |
+
deploys:
|
| 12 |
+
|
| 13 |
+
- **RIB** replaces the relative-position-bias *table* — learned low-rank
|
| 14 |
+
position features concatenated onto Q/K, so attention runs on standard
|
| 15 |
+
FlashAttention/SDPA kernels. No table, no index gather, no mask.
|
| 16 |
+
- **OCAB removed** — HAT's most complex, most memory-hungry block. Our
|
| 17 |
+
ablations showed it isn't needed at window 32.
|
| 18 |
+
- **Keeps the proven bits:** i-LN (input-adaptive normalization, bf16-stable),
|
| 19 |
+
window attention, a convolutional branch (CAB).
|
| 20 |
+
|
| 21 |
+
The result: a ~16.7M single-image SR architecture that **ties HAT on PSNR,
|
| 22 |
+
trains in stable bf16 AMP, and exports to ONNX that runs in TensorRT / ONNX
|
| 23 |
+
Runtime / DirectML.**
|
| 24 |
+
|
| 25 |
+
## Why HEART exists
|
| 26 |
+
|
| 27 |
+
Most SR papers chase benchmark numbers by adding modules that are annoying to
|
| 28 |
+
train and painful to deploy — dynamic routing, deformable attention, blocks
|
| 29 |
+
that only work with a specific resolution, or a "fused" checkpoint for
|
| 30 |
+
inference and an "unfused" one for fine-tuning. The result looks good on a
|
| 31 |
+
leaderboard and gathers dust in practice.
|
| 32 |
+
|
| 33 |
+
HEART has a different goal: **real-world simplicity.** Train it anywhere,
|
| 34 |
+
convert it to dynamic-shape ONNX, run it everywhere. No fusion step. No second
|
| 35 |
+
model file. No custom kernels.
|
| 36 |
+
|
| 37 |
+
## When to use HEART
|
| 38 |
+
|
| 39 |
+
- You want HAT-class quality on real images (denoise, deblur, upscale) without
|
| 40 |
+
babysitting the training.
|
| 41 |
+
- You need weights you can convert to ONNX and ship — no fusion step, no "use
|
| 42 |
+
variant B for inference."
|
| 43 |
+
- You are tired of benchmark-chasing networks that add modules no one can
|
| 44 |
+
actually deploy.
|
| 45 |
+
- You need desktop quality at reasonable inference cost. For mobile/edge, use
|
| 46 |
+
NERVE (coming in the BODY suite).
|
| 47 |
+
|
| 48 |
+
## Quickstart
|
| 49 |
+
|
| 50 |
+
1. Copy `traiNNer/archs/heart_arch.py` into your traiNNer-redux clone
|
| 51 |
+
(`traiNNer/archs/`).
|
| 52 |
+
2. Copy the release config from `options/train/RELEASE/4x_HEART_release.yml`
|
| 53 |
+
and point `dataroot_gt` / `dataroot_lq` at your data.
|
| 54 |
+
3. Train:
|
| 55 |
+
```bash
|
| 56 |
+
python train.py -opt options/train/RELEASE/4x_HEART_release.yml --auto_resume
|
| 57 |
+
```
|
| 58 |
+
(`--auto_resume` is **mandatory** — without it an interrupted run restarts
|
| 59 |
+
from iter 0 and archives the experiment dir.)
|
| 60 |
+
4. Convert to dynamic ONNX (each model, one at a time):
|
| 61 |
+
```bash
|
| 62 |
+
ulimit -v 14000000
|
| 63 |
+
./venv/bin/python scripts/heart/export_minimal.py \
|
| 64 |
+
experiments/4x_HEART_pretrain600k/models/net_g_ema_615000.safetensors \
|
| 65 |
+
onnx/4x_HEART_1x3xHxW_fp32_op17.onnx 4
|
| 66 |
+
```
|
| 67 |
+
5. Run inference anywhere ONNX Runtime or TensorRT runs.
|
| 68 |
+
|
| 69 |
+
Full architecture docs, ablation results, and training recipes are below.
|
| 70 |
+
|
| 71 |
+
> **Training crop size: use `lq_size: 96` (a multiple of the 32 px window).**
|
| 72 |
+
> HEART partitions the image into 32×32 windows. Training at a crop size that
|
| 73 |
+
> is *not* a multiple of 32 (e.g. 80) makes the model reflect-pad its own
|
| 74 |
+
> bottom/right edge every batch, so it learns a false "edge = mirrored
|
| 75 |
+
> content" response — visible as a bright bottom band and a tile-grid pattern,
|
| 76 |
+
> and strongly amplified by a GAN loss. All HEART ablations and release runs
|
| 77 |
+
> used `lq_size: 96` (= 3×32); a larger multiple (128) also works. This
|
| 78 |
+
> matters most for GAN/OTF finetunes, which are most sensitive to boundary
|
| 79 |
+
> statistics.
|
| 80 |
+
|
| 81 |
+
## BODY suite status
|
| 82 |
+
|
| 83 |
+
| Model | Role | Params | Status |
|
| 84 |
+
|---|---|---|---|
|
| 85 |
+
| HEART | desktop quality tier | 16.7M | trained + ONNX |
|
| 86 |
+
| NERVE | mobile/edge tier | ~270K | trained |
|
| 87 |
+
|
| 88 |
+
## Design
|
| 89 |
+
|
| 90 |
+
```
|
| 91 |
+
conv_first → [6× RHAG-RIB] → conv_after_body (+ global residual)
|
| 92 |
+
→ pixelshuffle upsampler
|
| 93 |
+
|
| 94 |
+
RHAG-RIB (residual group):
|
| 95 |
+
[6× HAB-RIB blocks] → 3x3 conv → group residual
|
| 96 |
+
|
| 97 |
+
HAB-RIB block:
|
| 98 |
+
i-LN → RIB window attention (window 32, FlashAttention/SDPA)
|
| 99 |
+
+ CAB conv-attention branch (scale 0.01)
|
| 100 |
+
i-LN → MLP (mlp_ratio 2)
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
- **i-LN** (Image Restoration tailored LayerNorm, arXiv:2504.06629) — stable
|
| 104 |
+
training in bf16/AMP, no divergence.
|
| 105 |
+
- **RIB window attention** — FlashAttention-compatible, window 32, shifted
|
| 106 |
+
windows via non-wrapping pad-and-partition (no masks, no position tables).
|
| 107 |
+
- **CAB conv branch** — HAT's proven local inductive bias, kept.
|
| 108 |
+
- **OCAB removed** — the most complex and memory-hungry part of HAT is gone.
|
| 109 |
+
|
| 110 |
+
## Goals
|
| 111 |
+
|
| 112 |
+
| Goal | How |
|
| 113 |
+
|---|---|
|
| 114 |
+
| Great results | HAT's proven hybrid block (window + conv) + large-window global context |
|
| 115 |
+
| Fast train & inference | FlashAttention via RIB (SST measured 2.1x train / 2.9x inference vs RPB) |
|
| 116 |
+
| Stable training | i-LN + EMA std clamping, bf16/AMP safe |
|
| 117 |
+
| Simple code | ~640 lines, reuses existing blocks, no masks/tables/OCAB |
|
| 118 |
+
| Easy deployment | Pure PyTorch, ONNX-exportable, no custom CUDA kernels |
|
| 119 |
+
|
| 120 |
+
Note on speed: the FlashAttention win only exists in bf16/fp16 (measured
|
| 121 |
+
~1.6x faster than HAT-iLN-M at bf16 inference). In fp32, HEART is slightly
|
| 122 |
+
slower than HAT-iLN — fp32 has no flash kernel. Use reduced precision.
|
| 123 |
+
|
| 124 |
+
## Status
|
| 125 |
+
|
| 126 |
+
Implemented, verified, and releasing. Architecture is frozen; all weights and
|
| 127 |
+
ONNX files below are measured, not extrapolated.
|
| 128 |
+
|
| 129 |
+
Final architecture (post-ablation, 2026-08-31): RIB window-32 attention + CAB,
|
| 130 |
+
`attention_freq=2`, rank 8, linear MLP — the 20k sweep winner. RoPE,
|
| 131 |
+
no-positional, GDFN, SwiGLU, LayerScale, window 48/64, and rank 18/32 were all
|
| 132 |
+
measured and rejected; the losing code paths were removed from `heart_arch.py`
|
| 133 |
+
(646 lines, 16/16 tests, lint + typecheck clean).
|
| 134 |
+
|
| 135 |
+
Release artifacts (all exist, paths verified):
|
| 136 |
+
|
| 137 |
+
| Artifact | Checkpoint | ONNX |
|
| 138 |
+
|---|---|---|
|
| 139 |
+
| 4x bicubic | `experiments/4x_HEART_pretrain600k/models/net_g_ema_615000.safetensors` | `onnx/4x_HEART_1x3xHxW_fp32_op17.onnx` |
|
| 140 |
+
| 2x | `experiments/2x_HEART/models/net_g_ema_200000.safetensors` | `onnx/2x_HEART_1x3xHxW_fp32_op17.onnx` |
|
| 141 |
+
| 4x OTF fidelity (v1.1) | `experiments/backup_otf_v2/net_g_ema_150000.safetensors` | `onnx/4x_HEART_OTF_1x3xHxW_fp32_op17.onnx` |
|
| 142 |
+
| 4x OTF perceptual (GAN) | training | — |
|
| 143 |
+
|
| 144 |
+
All checkpoints backed up to a private HF repo
|
| 145 |
+
(`Phips/heart-sisr-pretrain`). ONNX files pass `onnx.checker` and match
|
| 146 |
+
PyTorch numerically (ORT diff < 1e-2).
|
| 147 |
+
|
| 148 |
+
In flight: **4x OTF perceptual (GAN) finetune** (~3 days remaining). When it
|
| 149 |
+
finishes, its weights + ONNX join the release set.
|
| 150 |
+
|
| 151 |
+
Known limitation: a faint **window-32 grid artifact** (128 px period in 4x
|
| 152 |
+
output) on smooth gradients and fine lines in OTF outputs. Diagnosed via FFT;
|
| 153 |
+
root cause is window boundaries (not JPEG blocks or PixelShuffle). Overlap
|
| 154 |
+
attention contradicts the frozen arch, so it is documented here, not fixed in
|
| 155 |
+
1.0.
|
| 156 |
+
|
| 157 |
+
Release-gate checks passed: `--auto_resume` (CLI flag) resume verified,
|
| 158 |
+
tiled inference seam-free, ONNX legacy-tracer opset 17 + onnxruntime,
|
| 159 |
+
fp16/bf16 inference, bf16 AMP training stability, torch.compile validated.
|
| 160 |
+
the bare model, so checkpoints and val are unaffected. Kernel profile of
|
| 161 |
+
one step: ~23% flash attention, ~30% small elementwise ops (what compile
|
| 162 |
+
fuses), ~12% convs — no architectural hotspot left for custom kernels.
|
| 163 |
+
|
| 164 |
+
Dev notes for anyone touching the attention code:
|
| 165 |
+
|
| 166 |
+
- Shifted windows are non-wrapping: the feature map is padded top/left by half
|
| 167 |
+
a window (reflect) and windows are partitioned from the padded origin. Border
|
| 168 |
+
windows see reflected padding, never content from the opposite image border.
|
| 169 |
+
Do NOT reintroduce `torch.roll` cyclic shifting — without an attention mask
|
| 170 |
+
it couples opposite borders, and adding a mask would force SDPA off the flash
|
| 171 |
+
kernel (the whole point of RIB).
|
| 172 |
+
- Logits are pre-scaled per SST eq. 5 (`q/√D`, `pos/√R`) and SDPA is called
|
| 173 |
+
with `scale=1.0`. Re-adding default SDPA scaling silently divides logits
|
| 174 |
+
by √48 ≈ 7 and shifts attention toward uniform — shape-only tests will
|
| 175 |
+
not catch it. Without this fix the model trains around it and loses
|
| 176 |
+
quality for no reason.
|
| 177 |
+
- RIB features are computed in fp32 then cast to the input dtype. Skipping
|
| 178 |
+
the cast breaks pure fp16 inference (buffer/param dtype mismatch).
|
| 179 |
+
|
| 180 |
+
Next step is a short ablation against the baselines before the full training
|
| 181 |
+
schedule:
|
| 182 |
+
|
| 183 |
+
- `heart` (this network, 19.2M params, RIB window 32)
|
| 184 |
+
- `atd` (20.3M, the efficiency champion)
|
| 185 |
+
- `hat_m` (20.8M, classic HAT)
|
| 186 |
+
|
| 187 |
+
The ablation trains each for ~20-50k iterations on DIV2K and compares
|
| 188 |
+
Urban100 PSNR and wall-clock speed. HEART proceeds to full DF2K training only
|
| 189 |
+
if it wins.
|
| 190 |
+
|
| 191 |
+
## Ablation results (2026-08-15, post shifted-window fix)
|
| 192 |
+
|
| 193 |
+
30k iterations each, identical settings (post-fix HEART code). RTX 3060 12 GB,
|
| 194 |
+
DIV2K → Urban100:
|
| 195 |
+
|
| 196 |
+
| arch | best Urban100 PSNR | @iter | final SSIM | train it/s | peak VRAM |
|
| 197 |
+
|-------|--------------------|-------|------------|------------|-----------|
|
| 198 |
+
| HEART | 25.0235 | 30000 | 0.7497 | 0.86 | 1.44 GB |
|
| 199 |
+
| HAT_M | 25.0524 | 30000 | 0.7517 | 0.95 | 2.84 GB |
|
| 200 |
+
| ATD | OOM at iter 1 — did not run on the same config |
|
| 201 |
+
|
| 202 |
+
Reading the table honestly:
|
| 203 |
+
|
| 204 |
+
- **Quality:** HAT_M leads by +0.03 dB / +0.002 SSIM at the 30k cutoff —
|
| 205 |
+
within noise for a short schedule, and both were still improving on their
|
| 206 |
+
final validation. Effectively a tie; only a full-length run separates them.
|
| 207 |
+
- **VRAM:** HEART uses ~half the peak memory of HAT_M (1.44 vs 2.84 GB).
|
| 208 |
+
- **Train speed:** HAT_M trains ~10% faster (0.95 vs 0.86 it/s). Window 32
|
| 209 |
+
attention is 4x the per-pixel attention work of window 16; the flash kernel
|
| 210 |
+
makes it feasible, not free. HEART's speed advantage is at *inference*
|
| 211 |
+
(~1.6x vs HAT-iLN-M bf16), not training throughput.
|
| 212 |
+
- **ATD OOM caveat:** HEART/HAT_M ran with `use_checkpoint: true`; ATD's
|
| 213 |
+
factory has no such option. The OOM is a real deployment-relevant result
|
| 214 |
+
(ATD does not train on a 12 GB card at lq_size 64 / batch 4 as configured),
|
| 215 |
+
but the memory comparison is not fully symmetric because of that.
|
| 216 |
+
|
| 217 |
+
### Inference benchmark (same trained EMA checkpoints, RTX 3060)
|
| 218 |
+
|
| 219 |
+
Median of 5 runs after warmup, `cudnn.benchmark=False`,
|
| 220 |
+
`/tmp/kilo/inference_bench.py`:
|
| 221 |
+
|
| 222 |
+
| arch | dtype | input | time | peak VRAM |
|
| 223 |
+
|-------|-------|----------|---------|-----------|
|
| 224 |
+
| HEART | bf16 | 320x180 | 0.884s | 0.45 GB |
|
| 225 |
+
| HAT_M | bf16 | 320x180 | 1.092s | 1.20 GB |
|
| 226 |
+
| HEART | bf16 | 480x270 | 2.024s | 0.90 GB |
|
| 227 |
+
| HAT_M | bf16 | 480x270 | 2.351s | 2.47 GB |
|
| 228 |
+
| HEART | fp32 | 320x180 | 2.350s | 0.89 GB |
|
| 229 |
+
| HAT_M | fp32 | 320x180 | 1.816s | 2.37 GB |
|
| 230 |
+
| HEART | fp32 | 480x270 | 5.347s | 1.78 GB |
|
| 231 |
+
| HAT_M | fp32 | 480x270 | 3.973s | 4.94 GB |
|
| 232 |
+
|
| 233 |
+
In bf16 (the deployment-relevant precision) HEART wins both speed (1.16-1.24x)
|
| 234 |
+
and VRAM (~2.7x less). In fp32 HAT_M is faster (no flash kernel for fp32), but
|
| 235 |
+
HEART still uses 2.7-2.8x less VRAM.
|
| 236 |
+
|
| 237 |
+
### Training speed benchmarks (RTX 3060, bf16 AMP, channels-last)
|
| 238 |
+
|
| 239 |
+
torch.compile (`use_compile: true`) validated in the training harness:
|
| 240 |
+
~1.5x training speed, quality-neutral (300-iter smoke: 21.88 dB vs 21.80 dB
|
| 241 |
+
uncompiled).
|
| 242 |
+
|
| 243 |
+
| config | it/s | 800k iters | peak VRAM |
|
| 244 |
+
|---|---|---|---|
|
| 245 |
+
| lq64 bs4, no compile (ablation) | 0.86 | ~10.7 days | 1.44 GB |
|
| 246 |
+
| lq64 bs4 + compile | 1.26 | ~7.4 days | 1.44 GB |
|
| 247 |
+
| lq96 bs2 + compile | 1.19 | ~7.8 days | 0.98 GB |
|
| 248 |
+
| lq96 bs4 + compile | ~0.63 | ~14.7 days | 1.70 GB |
|
| 249 |
+
| lq96 bs4 / lq128, no checkpoint | OOM | — | — |
|
| 250 |
+
|
| 251 |
+
compile_mode comparison (@ lq96 bs2, 3060): `reduce-overhead` 1.23 it/s >
|
| 252 |
+
`default` 1.18 > `dynamic=False` 1.19 ≈ `default+TF32` 1.19 > no compile 0.77.
|
| 253 |
+
`max-autotune` is not worth it on small GPUs (inductor warns: not enough SMs).
|
| 254 |
+
At the ablation config (lq64 bs4), `max-autotune-no-cudagraphs` was measured
|
| 255 |
+
equal to `reduce-overhead` (both 1.28 it/s) with 2x the compile time and
|
| 256 |
+
slightly more VRAM — it gains nothing on a 28-SM card because the tuned GEMM
|
| 257 |
+
search falls back. `fast_matmul` (TF32) is a no-op under bf16 AMP — measured.
|
| 258 |
+
|
| 259 |
+
Kernel profile of one training step (uncompiled): flash attention ~23%,
|
| 260 |
+
small elementwise ops ~30% (what compile fuses — the source of the 1.5x),
|
| 261 |
+
convs ~12%, MLP gemms ~4%. No hotspot remains that a custom Triton kernel
|
| 262 |
+
could profitably target.
|
| 263 |
+
|
| 264 |
+
### Scoreboard vs HAT_M (30k ablation)
|
| 265 |
+
|
| 266 |
+
| Metric | Winner |
|
| 267 |
+
|---|---|
|
| 268 |
+
| Quality | tie (HAT_M +0.03 dB, within noise) |
|
| 269 |
+
| Inference speed bf16 | HEART (1.16-1.24x) |
|
| 270 |
+
| Inference VRAM | HEART (~2.7x less) |
|
| 271 |
+
| Training VRAM | HEART (1.44 vs 2.84 GB) |
|
| 272 |
+
| Training speed (no compile) | HAT_M (+10%) |
|
| 273 |
+
| ATD on 12 GB | does not run (OOM) |
|
| 274 |
+
|
| 275 |
+
### Rank ablation (done, 2026-08-17)
|
| 276 |
+
|
| 277 |
+
RIB's per-head positional-bias matrix has rank <= `rank`. Three 20k-iteration
|
| 278 |
+
runs compared rank 8 / 18 / 32 on the identical ablation config **with
|
| 279 |
+
torch.compile reduce-overhead** (`HEART_rank{8,18,32}_ablation.yml`, lr decay
|
| 280 |
+
at 16k). All three used compile so the comparison is not confounded (compile
|
| 281 |
+
shifts short-run PSNR ~0.08 dB, larger than the 0.03 dB decision threshold).
|
| 282 |
+
|
| 283 |
+
| rank | 20k PSNR |
|
| 284 |
+
|---|---|
|
| 285 |
+
| **8** | **24.7820** |
|
| 286 |
+
| 32 | 24.7586 |
|
| 287 |
+
| 18 | 24.7346 |
|
| 288 |
+
|
| 289 |
+
**Winner: rank 8** (smallest within 0.03 dB of the best). Rank 8 beating
|
| 290 |
+
rank 18 means positional capacity is *not* the bottleneck at rank 8 — a
|
| 291 |
+
lower-rank positional mechanism is enough, and it is leaner (concat dim 40
|
| 292 |
+
vs 48) and slightly faster. The canonical default in `heart_arch.py` is now
|
| 293 |
+
`rank=8`. The rank-18 uncompiled 30k reference (25.0235) was historical only.
|
| 294 |
+
The release configs are patched to rank 8.
|
| 295 |
+
|
| 296 |
+
### Architecture ablation: window size & attention frequency (2026-08-19)
|
| 297 |
+
|
| 298 |
+
Ran because an 8-day release was at stake: if window 48/64 or sparser
|
| 299 |
+
attention changed the quality-per-second picture, it had to be found before
|
| 300 |
+
committing. Setup: lq96 bs2 (the release crop), 20k iters, compile
|
| 301 |
+
reduce-overhead, rank 8, DIV2K, seed 1024 — identical settings except the
|
| 302 |
+
variable (`HEART_{w32,w48,w64,attn2}_ablation.yml`). The release run was
|
| 303 |
+
halted at 75k iters (checkpointed, resumable) while this ran.
|
| 304 |
+
|
| 305 |
+
| config | 20k PSNR | it/s (compiled) | params |
|
| 306 |
+
|---|---|---|---|
|
| 307 |
+
| **attn2** (`attention_freq=2`, RIB) | **24.6282** | **2.02** | 16.68M |
|
| 308 |
+
| attn3 (`attention_freq=3`) | 24.5606 | ~2.3 | 15.87M |
|
| 309 |
+
| w32 (baseline, RIB rank 8) | 24.5063 | 1.29 | 19.10M |
|
| 310 |
+
| none (no positional, freq=1) | 24.4965 | ~1.4 | 18.94M |
|
| 311 |
+
| w48 | 24.4347 | 0.80 | 19.10M |
|
| 312 |
+
| rope (RoPE, freq=1) | 24.3615 | ~1.4 | 18.94M |
|
| 313 |
+
| gdfn (Gated-Dconv FFN, ratio 2.0, freq=2) | 24.2546 | ~1.9 | 19.28M |
|
| 314 |
+
| swiglu (SwiGLU FFN, ratio 4/3 param-matched, freq=2) | 23.7403 (best @12k; collapsed to 23.21 @20k) | ~1.9 | ~16.7M |
|
| 315 |
+
| qknorm (per-head QK LayerNorm, freq=2) | 24.6334 (tie with attn2, +0.005) | ~2.0 | ~16.7M |
|
| 316 |
+
| swiglu + qknorm | 23.9576 | ~1.9 | ~16.7M |
|
| 317 |
+
| layerscale (per-block residual scaling, init 1e-6) | 24.0288 | ~2.0 | ~16.7M |
|
| 318 |
+
| w64 | skipped (w48 already lost; 4.2x slower) | 0.31 | 19.10M |
|
| 319 |
+
|
| 320 |
+
Reading:
|
| 321 |
+
|
| 322 |
+
- **attention_freq=2 wins on both axes**: +0.12 dB over the baseline AND
|
| 323 |
+
1.56x faster training, with 2.4M fewer params. HEART has more attention
|
| 324 |
+
than it needs at 20M scale.
|
| 325 |
+
- **GDFN rejected** (-0.37 dB vs attn2): doubling the per-block 3x3 conv
|
| 326 |
+
density (CAB already provides spatial convs) hurt. The FFN stays a linear
|
| 327 |
+
MLP.
|
| 328 |
+
- **Positional barely matters**: no-positional ties RIB at freq=1 (-0.01 dB,
|
| 329 |
+
noise), while RoPE's fixed rotation actively hurt (-0.15 dB). RIB stays as
|
| 330 |
+
cheap insurance; it is not a quality driver.
|
| 331 |
+
- **Window 48/64 closed**: window size is not a quality bottleneck at this
|
| 332 |
+
crop size (w48 -0.07 dB, 38% slower). SST's large-window gains needed
|
| 333 |
+
scaled crops+datasets this regime doesn't have.
|
| 334 |
+
- **Phase 2 confirmation** (in progress): w32 vs attn2 head-to-head at 100k
|
| 335 |
+
iters (`HEART_{w32,attn2}_confirm.yml`, lr decay at 50k). The 20k ordering
|
| 336 |
+
must survive to 100k before the release config changes.
|
| 337 |
+
|
| 338 |
+
### Release training (actual pipeline, 2026-08-25..28)
|
| 339 |
+
|
| 340 |
+
The release trained in five chained phases, all unattended with
|
| 341 |
+
`--auto_resume`. This is the pipeline that actually ran (not the earlier
|
| 342 |
+
3-phase design, which was superseded).
|
| 343 |
+
|
| 344 |
+
| Phase | Config | Iters | Result |
|
| 345 |
+
|---|---|---|---|
|
| 346 |
+
| 1. 4x pretrain (bicubic) | `options/train/RELEASE/4x_HEART_release.yml` | 615k (early-stopped) | 26.2139 dB |
|
| 347 |
+
| 2. 2x finetune | `options/train/RELEASE/2x_HEART_release.yml` | 200k | 33.9959 dB |
|
| 348 |
+
| 3. 4x OTF v1.0 (real-world) | `options/train/RELEASE/4x_HEART_OTF.yml` | 300k | 22.8473 dB |
|
| 349 |
+
| 4. 4x OTF v1.1 (anti-alias) | `options/train/RELEASE/4x_HEART_OTF_v11_finetune.yml` | 150k | 22.9529 dB |
|
| 350 |
+
| 5. 4x OTF perceptual (GAN) | `options/train/RELEASE/4x_HEART_OTF_gan.yml` | 150k | in progress |
|
| 351 |
+
|
| 352 |
+
Key operational lessons learned the hard way:
|
| 353 |
+
|
| 354 |
+
- **`--auto_resume` is mandatory** (CLI flag, not YAML). Without it an
|
| 355 |
+
interrupted run restarts from iter 0 and archives the experiment dir.
|
| 356 |
+
- **Dataset:** LUCID CC0 v2 HC-512 (100,866 tiles, 512x512, CC0). LR generated
|
| 357 |
+
with chainner CubicCatrom bicubic (the chaiNNer ecosystem standard).
|
| 358 |
+
- **OTF degradation:** Real-ESRGAN-style two-pass blur + noise + JPEG, applied
|
| 359 |
+
on-the-fly by `realesrgandataset`.
|
| 360 |
+
- **ONNX export froze the desktop twice** (see Part 1 of this plan) — the fix
|
| 361 |
+
is a per-process memory cap, not `nice`/`ionice`.
|
| 362 |
+
|
| 363 |
+
Pipeline scripts (durable, in `scripts/heart/`): `download_lucid.py`,
|
| 364 |
+
`prepare_lucid.py`, `run_release_sequence.sh`, `watch_download_then_prepare.sh`,
|
| 365 |
+
`export_one.py`, `export_minimal.py`, `infer_otf_comparison.py`,
|
| 366 |
+
`grid_diagnosis.py`. Logs in `/tmp/kilo/`.
|
| 367 |
+
|
| 368 |
+
## Variants
|
| 369 |
+
|
| 370 |
+
HEART 1.0 ships as a single architecture (`heart`, 19.2M params, embed 180,
|
| 371 |
+
6×6 blocks, window 32, CAB compress/squeeze 3/30). A principled S/M/L scaling
|
| 372 |
+
family (width, depth, heads, rank, CAB) is a HEART 2.0 item — deliberately
|
| 373 |
+
not added before the release model proves the architecture.
|
| 374 |
+
|
| 375 |
+
## Positioning
|
| 376 |
+
|
| 377 |
+
HEART deliberately does **not** compete on benchmark PSNR. Research
|
| 378 |
+
architectures (HAT, DAT, RGT, Restormer, DRCT, Mamba-based SSMs) exist to
|
| 379 |
+
move leaderboard numbers, and they will out-score HEART on some val sets —
|
| 380 |
+
that is expected and accepted.
|
| 381 |
+
|
| 382 |
+
HEART competes on everything that happens *after* the paper: training
|
| 383 |
+
without babysitting, exporting without a fuse step, running in chaiNNer
|
| 384 |
+
tomorrow, and being understood by a new maintainer in 2028.
|
| 385 |
+
|
| 386 |
+
The rules that define the position:
|
| 387 |
+
|
| 388 |
+
- **One configuration.** No S/M/L variants, no fused/unfused checkpoint
|
| 389 |
+
pairs, no "lite" forks. One architecture, one way to run it.
|
| 390 |
+
- **Zero custom kernels.** Everything a user needs ships in vanilla
|
| 391 |
+
PyTorch; ONNX opset-17 export is verified, not promised.
|
| 392 |
+
- **Anything that loses the ablation protocol is removed from the code.**
|
| 393 |
+
14 variants were measured; the winners stayed, the losers were deleted —
|
| 394 |
+
not kept behind flags.
|
| 395 |
+
- **The architecture is boring on purpose.** The block is i-LN -> [RIB
|
| 396 |
+
window attention | CAB] -> MLP. No dynamic routing, no deformable
|
| 397 |
+
anything, no state-space branches.
|
| 398 |
+
|
| 399 |
+
If a technique requires a fuse step, a custom kernel, a shape assumption,
|
| 400 |
+
or a "use variant B only for inference" note, it does not belong in HEART.
|
| 401 |
+
|
| 402 |
+
## Deployment notes
|
| 403 |
+
|
| 404 |
+
- **Arbitrary input sizes:** `check_image_size` pads the input to a window
|
| 405 |
+
multiple (reflect, replicate fallback for tiny images) and crops the
|
| 406 |
+
output back. Verified at 32x32, 64x64, 128x128, 323x711, 777x65,
|
| 407 |
+
720x1280. Users never handle padding themselves.
|
| 408 |
+
- **ONNX (verified, 2026-08-21, trained 20k checkpoint):** legacy tracer
|
| 409 |
+
(`dynamo=False`), opset 17, dynamic H/W + batch. `onnx.checker` passes;
|
| 410 |
+
onnxruntime output matches PyTorch (max abs diff 0.00026 at 128x128).
|
| 411 |
+
Export pipeline for releases: export -> onnxsim -> ORT optimizer ->
|
| 412 |
+
validate at 64x64 / 256x256 / 720x1280. Note: ORT *CPU* inference of
|
| 413 |
+
large images needs several GB of RAM (a 16.7M transformer is not a CPU
|
| 414 |
+
workload) — deploy on GPU providers (CUDA/TensorRT/DirectML).
|
| 415 |
+
- **NCNN: not supported, by design.** The fused SDPA node does not convert
|
| 416 |
+
to NCNN, and window attention is not an NCNN-style workload anyway.
|
| 417 |
+
HEART targets ONNX Runtime / TensorRT / DirectML; a mobile-conv sibling
|
| 418 |
+
(NERVE) is the future NCNN member of the suite.
|
| 419 |
+
- **Large images — single pass vs tiling:** single pass fits up to ~720x1280
|
| 420 |
+
LR on a 12 GB card (5.55 GB measured, bf16). 1080x1920 LR single-pass
|
| 421 |
+
OOMs (~10.6 GB); tiled inference (tile 256, overlap 32, weighted blend)
|
| 422 |
+
runs it at 0.86 GB and is seam-free (measured). Use chaiNNer seamless
|
| 423 |
+
tiling / the repo's `tile_size` option for anything bigger than 720x1280.
|
| 424 |
+
- **Real-world degradations:** the backbone is degradation-agnostic. For
|
| 425 |
+
denoising/JPEG cleanup train at `scale=1` (verified working) with a
|
| 426 |
+
Real-ESRGAN-style degradation pipeline, same as HAT-R relates to HAT.
|
| 427 |
+
- **spandrel:** registered in-repo via `SPANDREL_REGISTRY` +
|
| 428 |
+
`store_hyperparameters`, so traiNNer-redux/chainer forks that carry this
|
| 429 |
+
repo load it directly. **Upstream-PR decision (made):** at PR time,
|
| 430 |
+
`heart_arch.py`'s imports from `arch_util` (iLN) and `hat_iln_arch`
|
| 431 |
+
(CAB, AffineTransform, DropPath, Mlp, PatchEmbed, PatchUnEmbed, Upsample)
|
| 432 |
+
get vendored into the self-contained spandrel copy (~200 lines) rather
|
| 433 |
+
than landing HAT-iLN in spandrel first. No code change in this repo until
|
| 434 |
+
then.
|
| 435 |
+
|
| 436 |
+
## Experiment log (everything that shaped HEART)
|
| 437 |
+
|
| 438 |
+
Chronological record of every experiment, success and failure. The failures
|
| 439 |
+
matter as much as the wins — each one closed a direction.
|
| 440 |
+
|
| 441 |
+
| When | Experiment | Result | Decision / lesson |
|
| 442 |
+
|---|---|---|---|
|
| 443 |
+
| design | HAT-iLN base: RPB table -> RIB, OCAB removed, window 32 | — | Attention runs on FlashAttention/SDPA; simpler than HAT |
|
| 444 |
+
| 08-14 | Shifted windows via `torch.roll` | BUG: cyclic wrap coupled opposite borders (no mask) | Fixed: non-wrapping pad-and-partition |
|
| 445 |
+
| 08-14 | `F.pad` for V concat | minor | cleanup |
|
| 446 |
+
| 08-15 | 30k arch comparison HEART vs HAT_M vs ATD | 25.02 vs 25.05 vs OOM | tie with HAT_M at equal settings -> proceeded |
|
| 447 |
+
| 08-15 | Inference bench vs HAT_M | bf16: 1.16-1.24x faster, ~2.7x less VRAM; fp32 slower | deployment story is bf16 |
|
| 448 |
+
| 08-15 | torch.compile (smoke + harness) | +46-54% training, quality-neutral | adopted (`use_compile: true`) |
|
| 449 |
+
| 08-15 | compile_mode sweep | reduce-overhead 1.23 > default 1.18 > dynamic=False/TF32 1.19; max-autotune-no-cudagraphs == reduce-overhead with 2x compile time | reduce-overhead adopted; max-autotune rejected (28 SMs) |
|
| 450 |
+
| 08-15 | `fast_matmul` (TF32) | no-op under bf16 AMP | rejected |
|
| 451 |
+
| 08-15 | Crop/batch probes | lq96 bs4 OOM without checkpoint; lq96 bs2 fits, 9 windows | release crop = lq96 bs2 |
|
| 452 |
+
| 08-15 | `heart_light` (8.7M variant) | created, then removed | S/M/L family deferred to v2; 1.0 = one architecture |
|
| 453 |
+
| 08-17 | Rank ablation 8/18/32 @20k | 8 wins (24.782) > 32 (24.759) > 18 (24.735) | rank 8 default — positional capacity is NOT the bottleneck |
|
| 454 |
+
| 08-18 | Window ablation 48/64 @20k | w48 loses 0.07 dB, 38% slower; w64 skipped | spatial context is NOT the bottleneck at lq96 |
|
| 455 |
+
| 08-18 | Attention frequency 1/2/3 @20k | freq=2 wins: 24.628 (+0.12 dB) at 1.56x speed; freq=3 drops | attention density sweet spot = every 2nd block |
|
| 456 |
+
| 08-19 | RoPE instead of RIB @20k | loses 0.15 dB (24.36 vs 24.51) | learned low-rank positional beats fixed rotation |
|
| 457 |
+
| 08-19 | No positional at all @20k | ties RIB at freq=1 (24.497 vs 24.506) | RIB stays as cheap insurance; not a quality driver |
|
| 458 |
+
| 08-19 | GDFN (gated depthwise FFN) @20k | loses 0.37 dB (24.25 vs 24.63) | more per-block conv density hurts (CAB already convs) |
|
| 459 |
+
| 08-20 | SwiGLU FFN @20k | loses decisively: best 23.74 @12k, declined to 23.21 @20k | gated FFN destabilizes the i-LN-rescaled block — MLP stays |
|
| 460 |
+
| 08-20 | QK-Norm @20k | 24.6334 — tie with attn2 (+0.005) | tie-breaker: plain (simplest) — redundant with i-LN + RIB |
|
| 461 |
+
| 08-20 | SwiGLU + QK-Norm @20k | 23.9576 | gate instability again |
|
| 462 |
+
| 08-20 | LayerScale @20k | 24.0288 (−0.60) | 1e-6 init over-damps the branches; existing i-LN+conv_scale damping is right |
|
| 463 |
+
| 08-20 | Final cleanup | all test switches (ffn_type/qk_norm/layer_scale) stripped; canonical default attention_freq=2 | the file is the single-path measured-best architecture |
|
| 464 |
+
| 08-16 | Resume test | `--auto_resume` is a CLI flag, not YAML | all training commands pass it |
|
| 465 |
+
| 08-16 | Tiled inference | seam-free (seam diff == interior diff) | large-image tiling safe |
|
| 466 |
+
| 08-14..20 | ONNX export | legacy tracer opset 17 + checker + onnxruntime pass | deployment path verified |
|
| 467 |
+
|
| 468 |
+
Process lessons (drivers/pipeline): /tmp gets wiped on reboot — logs restored
|
| 469 |
+
from durable experiment copies; `ls dir/*.png | wc -l` breaks at ~100k files
|
| 470 |
+
(ARG_MAX) — use `find`; drivers are idempotent/self-healing and training runs
|
| 471 |
+
deprioritized (nice/ionice) so the machine stays usable.
|
| 472 |
+
|
| 473 |
+
## Considered-and-decided register
|
| 474 |
+
|
| 475 |
+
Every idea that was considered, with the decision and reason. If someone asks
|
| 476 |
+
"why didn't you use X" — the answer is here. Status: ✅ integrated (and
|
| 477 |
+
measured), ❌ rejected by measurement, 🚫 discarded by analysis (never run),
|
| 478 |
+
⏳ pending, 📌 deferred (not forgotten).
|
| 479 |
+
|
| 480 |
+
### Integrated
|
| 481 |
+
|
| 482 |
+
| Idea | Why |
|
| 483 |
+
|---|---|
|
| 484 |
+
| ✅ RIB instead of RPB table | Concatenated position features make attention FlashAttention/SDPA-compatible; measured (ties HAT_M, rank 8 suffices) |
|
| 485 |
+
| ✅ i-LN normalization | Input-adaptive holistic norm; bf16 stability is the paper's core claim, verified in training |
|
| 486 |
+
| ✅ CAB conv branch | Local inductive bias; the measured carrier of quality at this scale |
|
| 487 |
+
| ✅ Non-wrapping shifted windows | Fixes the cyclic-wrap border bug without the Swin mask (which would kill the flash kernel) |
|
| 488 |
+
| ✅ `attention_freq=2` | Measured winner: +0.12 dB at 1.56x speed vs attention-every-block |
|
| 489 |
+
| ✅ rank 8 RIB | Measured winner of 8/18/32 sweep |
|
| 490 |
+
| ✅ Window 32 | w48/64 measured losers at lq96 |
|
| 491 |
+
| ✅ torch.compile reduce-overhead | +46% training speed, quality-neutral; best of the mode sweep |
|
| 492 |
+
| ✅ bf16 AMP + gradient checkpointing | Verified stable; 1.4-1.7 GB training VRAM |
|
| 493 |
+
| ✅ Chainer CubicCatrom LR (release data) | The chaiNNer ecosystem standard — models behave as users generate LR |
|
| 494 |
+
|
| 495 |
+
### Rejected by measurement (ran, lost)
|
| 496 |
+
|
| 497 |
+
| Idea | Result | Why rejected |
|
| 498 |
+
|---|---|---|
|
| 499 |
+
| ❌ RoPE positional | −0.15 dB @20k | fixed rotation worse than learned low-rank positional |
|
| 500 |
+
| ❌ No positional | ties RIB at freq=1, but attn2+RIB wins overall | RIB stays as cheap insurance |
|
| 501 |
+
| ❌ GDFN gated-FFN | −0.37 dB @20k | more per-block conv density hurts (CAB already provides spatial convs) |
|
| 502 |
+
| ❌ SwiGLU FFN (param-matched 4/3) | best 23.74 @12k, declined to 23.21 @20k (−0.9 to −1.4 dB vs attn2) | the multiplicative gate destabilizes the i-LN-rescaled block (loss degraded after 12k) |
|
| 503 |
+
| ❌ SwiGLU + QK-Norm | 23.9576 @20k | same gate instability |
|
| 504 |
+
| ❌ LayerScale (init 1e-6) | 24.0288 @20k (−0.60 dB) | the near-zero init suppresses the branches through warmup; i-LN rescale can't compensate — the block's existing damping (i-LN + conv_scale 0.01) is the right amount |
|
| 505 |
+
| ⚖️ QK-Norm | 24.6334 — tie with attn2 (24.6282, +0.005 dB) | tie-breaker: plain wins (no component for zero measured gain) |
|
| 506 |
+
| ❌ Window 48 / 64 | w48 −0.07 dB + 38% slower; w64 skipped | spatial context is not the bottleneck at lq96 |
|
| 507 |
+
| ❌ `attention_freq=3` | −0.07 dB vs freq=2 | density sweet spot is every 2nd block |
|
| 508 |
+
| ❌ rank 18 / 32 | rank 8 wins | positional capacity is not the bottleneck |
|
| 509 |
+
| ❌ max-autotune compile modes | == reduce-overhead, 2x compile time | tuned GEMM search falls back on 28-SM GPUs |
|
| 510 |
+
| ❌ `fast_matmul` (TF32) | no-op | everything already runs bf16 on tensor cores |
|
| 511 |
+
| ❌ `compile(dynamic=False)` | no gain | shapes were effectively static anyway |
|
| 512 |
+
| ❌ `heart_light` 8.7M variant | created, then removed | hand-picked config, not a principled family; S/M/L is a v2 item |
|
| 513 |
+
|
| 514 |
+
### Discarded by analysis (never run — the reason is the evidence)
|
| 515 |
+
|
| 516 |
+
| Idea | Why discarded |
|
| 517 |
+
|---|---|
|
| 518 |
+
| 🚫 OCAB (HAT's overlapping cross-attention) | RPB tables + gather construction + memory are exactly what RIB/HEART removed; the 30k ablation already ties HAT_M *with* OCAB — the missing piece isn't needed |
|
| 519 |
+
| 🚫 Swin-style attention mask (shift fix alternative) | an additive mask forces SDPA off the flash kernel — the whole point of RIB |
|
| 520 |
+
| 🚫 RepCAB (reparameterized CAB) | `fuse()`-before-export creates two weight formats and breaks the drop-in property, for ~2-3% inference; quality hypothesis contradicted by the GDFN result |
|
| 521 |
+
| 🚫 MDTA transposed channel attention | global channel-covariance context — falsified direction (see w48) |
|
| 522 |
+
| 🚫 Stripe/multi-axis attention (HMA) | full-row/column context — same falsified hypothesis, plus partitioning complexity |
|
| 523 |
+
| 🚫 Dense residuals (DRCT) | groups and blocks already have residuals; dense skips grow activation memory |
|
| 524 |
+
| 🚫 Mamba/SSM backbones | custom CUDA kernels (`selective_scan`) violate the zero-custom-kernel constraint |
|
| 525 |
+
| 🚫 IET adaptive token selection | dynamic gather/indexing breaks dynamic ONNX tracing |
|
| 526 |
+
| 🚫 UCAN / SAT / FPLIA | sub-1M-parameter tier techniques; irrelevant at ~16.7M |
|
| 527 |
+
| 🚫 Muon optimizer | its gains are LLM-pretraining-scale; the RIB net is ~4k params |
|
| 528 |
+
| 🚫 FlashBias Triton kernel | HEART already *is* the SDPA variant of FlashBias; no bias-add attention exists to accelerate |
|
| 529 |
+
| 🚫 LAformer swap-in | different architecture, not an optimization; the deployability premise is already satisfied by HEART |
|
| 530 |
+
| 🚫 Triton-fused RIB concat | ~2-4% of a step at the cost of custom kernels; torch.compile already fuses the elementwise ops |
|
| 531 |
+
| 🚫 4D-native block refactor | single-digit % after compile's 46% landed; complexity not justified |
|
| 532 |
+
| 🚫 Hybrid window schedule (16/32 mix) | smaller windows contradict the measured landscape (w48 lost; context isn't the lever) |
|
| 533 |
+
| 🚫 lq128 crops | OOM without checkpointing and pathological compile on this GPU |
|
| 534 |
+
| 🚫 Progressive patch training (LQ64 -> LQ96) | measured apples-to-apples (bs2, compiled): lq64 3.86 it/s vs lq96 1.52 it/s (2.5x per iteration), but pixel throughput only 31.6 vs 28.0 kpx/s (+13%) — and lq64 crops see 4 windows vs lq96's 9, weakening cross-window mixing. ~8% total wall-time for real pipeline complexity and quality risk |
|
| 535 |
+
|
| 536 |
+
### Pending
|
| 537 |
+
|
| 538 |
+
None — the ablation program is complete. 14 variants measured; the core
|
| 539 |
+
(RIB rank-8 + attention_freq=2 + window 32 + CAB + plain MLP + i-LN) beat or
|
| 540 |
+
tied all of them. The architecture is at a measured local optimum.
|
| 541 |
+
|
| 542 |
+
### Deferred (not forgotten — see the 2.0 backlog)
|
| 543 |
+
|
| 544 |
+
📌 INT8 quantization, TensorRT export CI, RIB positional caching in eval, RIB
|
| 545 |
+
dtype sweep, ENAF-style early exits for 4K+ images, S/M/L family, larger
|
| 546 |
+
training crops (bigger GPU), video adaptation, rank 2/4 micro-sweep,
|
| 547 |
+
attention-freq bs4 batch check, MLP ratio sweep.
|
| 548 |
+
|
| 549 |
+
## HEART 2.0 backlog
|
| 550 |
+
|
| 551 |
+
Frozen for 1.0 (do not touch the architecture while the release model trains).
|
| 552 |
+
Ideas that need a fully trained 1.0 baseline before they can be judged
|
| 553 |
+
properly — the rule is: only adopt what wins on *quality per second* against
|
| 554 |
+
the 1.0 model, not what moves a benchmark number.
|
| 555 |
+
|
| 556 |
+
| Idea | Why it's on the list | Effort |
|
| 557 |
+
|---|---|---|
|
| 558 |
+
| Gated-Dconv FFN (GDFN, Restormer) | MEASURED (20k, ratio 2.0, freq=2): lost 0.37 dB (24.25 vs 24.63) — doubling per-block 3x3 conv density (CAB already provides spatial convs) hurt. Code removed. | — |
|
| 559 |
+
| SwiGLU FFN + QK-Norm | External review (2026-08-20): modernize the one remaining vanilla component (the MLP) + per-head QK LayerNorm for logit stability. TESTING: three 20k runs on the attn2 base (swiglu-only, qknorm-only, both). SwiGLU param-matched at ratio 4/3 (their 8/3 would have doubled params). | Low-Med |
|
| 560 |
+
| RepCAB (reparameterized CAB) | Train-time multi-branch conv fused at export. REJECTED for 1.0: the fuse()-before-export step conflicts with the trivial-export property that is part of the maintainability goal. | — |
|
| 561 |
+
| Transposed channel attention (MDTA) | Global channel-covariance attention — chases global context, which the w48 result already falsified as the lever. Backlog only if GDFN-style spatial enrichment proves the direction. | Medium |
|
| 562 |
+
| Multi-axis stripe attention (HMA) | Full-row/column context — same global-context rationale, contradicted by w48 losing at 20k. Skip unless a crop-size scaling study revives the context hypothesis. | Medium |
|
| 563 |
+
| Dense residual connections (DRCT) | Zero-param dense skips; NTIRE-2024 proven, but our blocks/groups already have residuals, and dense skips grow activation memory. Low expected value; cheap to test one day. | Low |
|
| 564 |
+
| ENAF early-exit routing | Inference-time wrapper for huge images, not an architecture change; post-release option only. | High |
|
| 565 |
+
| IET adaptive token selection | Dynamic gather/indexing risks breaking dynamic ONNX tracing — rejected on deployability grounds. | — |
|
| 566 |
+
| Mamba/SSM backbones | Custom CUDA kernels (selective_scan) violate the zero-custom-kernel constraint — rejected. | — |
|
| 567 |
+
| Larger windows (48 / 64) | MEASURED (20k, lq96): window 48 lost to 32 (-0.07 dB) and cost 38% more time. Closed for this crop/GPU regime unless crops scale too. | — |
|
| 568 |
+
| Attention frequency (H C H C) | MEASURED (20k, lq96): `attention_freq=2` beat the baseline (+0.12 dB) at 1.56x speed. Live candidate for the release config; longer-run confirmation pending. | Low-Med |
|
| 569 |
+
| RoPE instead of RIB | MEASURED (20k, lq96): RoPE lost 0.15 dB to RIB (24.36 vs 24.51) — learned low-rank positional wins; fixed rotations can't substitute. Closed. | — |
|
| 570 |
+
| Rank beyond release range (4/12/24/…) | The release gate only tests 8/18/32. Fine-grained rank scaling could shave the positional capacity further. | Low |
|
| 571 |
+
| RIB positional caching in eval | q_pos/k_pos recomputed every forward in every block; tiny but free to cache for inference. Micro-opt, measure before doing. | Low |
|
| 572 |
+
| RIB dtype sweep (bf16/fp16 vs fp32) | fp32 cast is for fp16-inference stability; whether bf16 RIB changes quality/speed is unmeasured. | Low |
|
| 573 |
+
| CAB variants / S/M/L family | An earlier hand-picked `heart_light` (8.7M) was removed for 1.0; a principled S/M/L scaling rule (width, depth, heads, rank, CAB) is the v2 way to extend the family. | Low-Med |
|
| 574 |
+
| TensorRT + export CI | ONNX verified; TensorRT untested. A CI export test (PyTorch→ONNX→ORT→TensorRT, compare max-abs-error/PSNR/runtime) would harden the deployment claim. | Medium |
|
| 575 |
+
| Quantization (INT8) | Untested; a 3060-class GPU model could get much faster. | Medium |
|
| 576 |
+
| Larger training crops | lq 128+ needs checkpointing + slower it/s; only test with a bigger GPU. | Medium |
|
| 577 |
+
| Video adaptation | temporal stability (like TSPAN) — separate scope. | High |
|
| 578 |
+
| Cyclic vs reflect shift | Deliberately closed for 1.0 (cyclic + mask kills the flash kernel). Revisit only if a flash-compatible exact-cyclic scheme emerges. | — |
|
| 579 |
+
|
| 580 |
+
Priority order after 1.0: confirm attention_freq=2 on a longer run (if the
|
| 581 |
+
release adopts it, this is moot) -> export hardening (TensorRT/CI) ->
|
| 582 |
+
RoPE swap-in benchmark -> rank/capacity scaling. Everything is measured
|
| 583 |
+
against the frozen 1.0 model, not against papers.
|
LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
tags:
|
| 6 |
+
- super-resolution
|
| 7 |
+
- image-restoration
|
| 8 |
+
- pytorch
|
| 9 |
+
- onnx
|
| 10 |
+
- spandrel
|
| 11 |
+
- chaiNNer
|
| 12 |
+
pipeline_tag: image-to-image
|
| 13 |
+
base_model:
|
| 14 |
+
- Phips/heart-sisr-pretrain
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
# HEART — Efficient Attention with Rank-factorized bias Transformer
|
| 18 |
+
|
| 19 |
+
Single-image super-resolution (SISR) architecture for traiNNer-redux and
|
| 20 |
+
spandrel. Simple to train, fast to run, and ships as a dynamic ONNX model that
|
| 21 |
+
works in ONNX Runtime, TensorRT, and chaiNNer.
|
| 22 |
+
|
| 23 |
+
## Highlights
|
| 24 |
+
|
| 25 |
+
- **16.7M params, 4x upscale**, window attention with Rank-factorized
|
| 26 |
+
Implicit Bias (RIB) — FlashAttention/SDPA friendly, no attention masks.
|
| 27 |
+
- **Pure PyTorch, single file** — drop `heart_arch.py` into any traiNNer-redux
|
| 28 |
+
clone and train.
|
| 29 |
+
- **Stable in reduced precision** — i-LN + bf16 AMP validated, no divergence.
|
| 30 |
+
- **Fast** — beats HAT-M in speed and VRAM (see HEART.md in the source repo).
|
| 31 |
+
- **Dynamic ONNX** — one export, any input size, runs anywhere.
|
| 32 |
+
|
| 33 |
+
## Quick start (train)
|
| 34 |
+
|
| 35 |
+
1. Copy `heart_arch.py` into `traiNNer/archs/`.
|
| 36 |
+
2. Use one of the example configs in `configs/` (start with
|
| 37 |
+
`4x_HEART_release.yml`).
|
| 38 |
+
3. Train:
|
| 39 |
+
```bash
|
| 40 |
+
python train.py -opt options/train/RELEASE/4x_HEART_release.yml --auto_resume
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
> **Important**: keep the training crop (`lq_size`) a multiple of the 32 px
|
| 44 |
+
> attention window — `96` (= 3×32) is recommended and used for all released
|
| 45 |
+
> models. A non-multiple size (e.g. 80) teaches the model a false "edges are
|
| 46 |
+
> mirrored" response that shows up as a bright bottom band / tile grid,
|
| 47 |
+
> especially with a GAN loss.
|
| 48 |
+
|
| 49 |
+
## Quick start (infer)
|
| 50 |
+
|
| 51 |
+
Load the `.safetensors` in chaiNNer via spandrel, or run the `.onnx` with any
|
| 52 |
+
ONNX Runtime / TensorRT engine. Inputs are RGB, dynamic H×W.
|
| 53 |
+
|
| 54 |
+
## Models
|
| 55 |
+
|
| 56 |
+
| File | Role |
|
| 57 |
+
|---|---|
|
| 58 |
+
| `models/heart_4x_pretrain.safetensors` | 4x pretrain (DF2K-style) |
|
| 59 |
+
| `models/heart_2x_finetune.safetensors` | 2x finetune |
|
| 60 |
+
| `models/heart_4x_otf_v2.safetensors` | 4x OTF fidelity (real-world, clean) |
|
| 61 |
+
| `models/heart_4x_otf_gan.safetensors` | 4x OTF GAN (real-world, sharp) |
|
| 62 |
+
| `onnx/heart_4x_otf_gan_1x3xHxW_fp32_op17.onnx` | dynamic ONNX, onnxslim-optimized |
|
| 63 |
+
|
| 64 |
+
## Contents
|
| 65 |
+
|
| 66 |
+
- `heart_arch.py` — the architecture (spandrel-registered).
|
| 67 |
+
- `configs/` — example training configs (release, OTF, OTF GAN).
|
| 68 |
+
- `models/` — pretrained checkpoints.
|
| 69 |
+
- `onnx/` — dynamic fp32 ONNX (opset 17), onnxslim-slimmed.
|
| 70 |
+
- `HEART.md` — full architecture docs, ablations, recipes.
|
| 71 |
+
- `export_heart.py` — export a checkpoint to dynamic ONNX.
|
| 72 |
+
|
| 73 |
+
## License & credits
|
| 74 |
+
|
| 75 |
+
Apache-2.0 (this repository's code and pretrained weights).
|
| 76 |
+
|
| 77 |
+
HEART is a re-implementation/simplification inspired by:
|
| 78 |
+
- **HAT** (arXiv:2205.04437) and **HAT-iLN** (arXiv:2504.06629) — architecture
|
| 79 |
+
family and i-LN.
|
| 80 |
+
- **SST / RIB** (arXiv:2603.06738) — Rank-factorized Implicit Bias, replacing
|
| 81 |
+
HAT's relative-position-bias table + OCAB with a FlashAttention-friendly
|
| 82 |
+
dot-product position bias.
|
| 83 |
+
|
| 84 |
+
The architecture file is original code; verify attribution/compatibility
|
| 85 |
+
against those papers before commercial redistribution. Full design rationale
|
| 86 |
+
and ablation data are in `HEART.md`.
|
configs/4x_HEART_OTF_gan.yml
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/the-database/traiNNer-redux/refs/heads/master/schemas/redux-config.schema.json
|
| 2 |
+
#########################################################################################
|
| 3 |
+
# HEART 4x OTF GAN finetune v2 (lq 96) — removes learned edge boundary response
|
| 4 |
+
#
|
| 5 |
+
# Root-cause fix for the bottom/right edge brightening artifact seen in the
|
| 6 |
+
# lq-80 GAN run: an lq crop of 80px is NOT a multiple of the 32px attention
|
| 7 |
+
# window, so every batch the model reflect-pads its own bottom/right edge and
|
| 8 |
+
# learns "edges mirror content", which the GAN loss amplifies into a visible
|
| 9 |
+
# band + tile grid. Training at lq_size 96 (= 3 x 32) keeps every window on
|
| 10 |
+
# real content, so the boundary response is never learned.
|
| 11 |
+
#
|
| 12 |
+
# Starts from the finished lq-80 GAN weights (150k). Degradation + losses
|
| 13 |
+
# unchanged from 4x_HEART_OTF_gan.yml. ~50-100k iters is enough.
|
| 14 |
+
#########################################################################################
|
| 15 |
+
name: 4x_HEART_OTF_gan_lq96
|
| 16 |
+
scale: 4
|
| 17 |
+
use_amp: true
|
| 18 |
+
amp_bf16: true
|
| 19 |
+
use_channels_last: true
|
| 20 |
+
fast_matmul: false
|
| 21 |
+
use_compile: false # inductor errors on discriminator step — keep default
|
| 22 |
+
compile_mode: default
|
| 23 |
+
num_gpu: auto
|
| 24 |
+
manual_seed: 1024
|
| 25 |
+
|
| 26 |
+
high_order_degradation: true
|
| 27 |
+
high_order_degradations_debug: false
|
| 28 |
+
high_order_degradations_debug_limit: 100
|
| 29 |
+
|
| 30 |
+
lq_usm: true
|
| 31 |
+
lq_usm_radius_range: [1, 25]
|
| 32 |
+
|
| 33 |
+
# Degradation matches the v2 fidelity pretrain (mild noise)
|
| 34 |
+
blur_prob: 0.6
|
| 35 |
+
resize_prob: [0.2, 0.7, 0.1]
|
| 36 |
+
resize_mode_list: ['bilinear', 'bicubic', 'nearest-exact', 'lanczos']
|
| 37 |
+
resize_mode_prob: [0.25, 0.25, 0.25, 0.25]
|
| 38 |
+
resize_range: [0.4, 1.5]
|
| 39 |
+
gaussian_noise_prob: 0.4
|
| 40 |
+
noise_range: [1, 15]
|
| 41 |
+
poisson_scale_range: [0.05, 2]
|
| 42 |
+
gray_noise_prob: 0.1
|
| 43 |
+
jpeg_prob: 0.4
|
| 44 |
+
jpeg_range: [40, 95]
|
| 45 |
+
|
| 46 |
+
blur_prob2: 0.3
|
| 47 |
+
resize_prob2: [0.3, 0.4, 0.3]
|
| 48 |
+
resize_mode_list2: ['bilinear', 'bicubic', 'nearest-exact', 'lanczos']
|
| 49 |
+
resize_mode_prob2: [0.25, 0.25, 0.25, 0.25]
|
| 50 |
+
resize_range2: [0.6, 1.2]
|
| 51 |
+
gaussian_noise_prob2: 0.3
|
| 52 |
+
noise_range2: [1, 10]
|
| 53 |
+
poisson_scale_range2: [0.05, 1.5]
|
| 54 |
+
gray_noise_prob2: 0.05
|
| 55 |
+
jpeg_prob2: 0.3
|
| 56 |
+
jpeg_range2: [40, 95]
|
| 57 |
+
|
| 58 |
+
resize_mode_list3: ['bilinear', 'bicubic', 'nearest-exact', 'lanczos']
|
| 59 |
+
resize_mode_prob3: [0.25, 0.25, 0.25, 0.25]
|
| 60 |
+
|
| 61 |
+
queue_size: 120
|
| 62 |
+
|
| 63 |
+
datasets:
|
| 64 |
+
train:
|
| 65 |
+
name: LUCID OTF Train
|
| 66 |
+
type: realesrgandataset
|
| 67 |
+
dataroot_gt: [datasets/train/lucid_hc512/hr]
|
| 68 |
+
|
| 69 |
+
blur_kernel_size: 12
|
| 70 |
+
kernel_list: ['iso', 'aniso', 'generalized_iso', 'generalized_aniso', 'plateau_iso', 'plateau_aniso']
|
| 71 |
+
kernel_prob: [0.45, 0.25, 0.12, 0.03, 0.12, 0.03]
|
| 72 |
+
kernel_range: [5, 17]
|
| 73 |
+
sinc_prob: 0.0
|
| 74 |
+
blur_sigma: [0.2, 2]
|
| 75 |
+
betag_range: [0.5, 4]
|
| 76 |
+
betap_range: [1, 2]
|
| 77 |
+
|
| 78 |
+
blur_kernel_size2: 12
|
| 79 |
+
kernel_list2: ['iso', 'aniso', 'generalized_iso', 'generalized_aniso', 'plateau_iso', 'plateau_aniso']
|
| 80 |
+
kernel_prob2: [0.45, 0.25, 0.12, 0.03, 0.12, 0.03]
|
| 81 |
+
kernel_range2: [5, 17]
|
| 82 |
+
sinc_prob2: 0.0
|
| 83 |
+
blur_sigma2: [0.2, 1]
|
| 84 |
+
betag_range2: [0.5, 4]
|
| 85 |
+
betap_range2: [1, 2]
|
| 86 |
+
|
| 87 |
+
final_sinc_prob: 0.0
|
| 88 |
+
final_kernel_range: [5, 17]
|
| 89 |
+
|
| 90 |
+
lq_size: 96 # = 3 x 32px window — window-aligned, no learned edge response
|
| 91 |
+
use_hflip: true
|
| 92 |
+
use_rot: true
|
| 93 |
+
num_worker_per_gpu: 8
|
| 94 |
+
batch_size_per_gpu: 2
|
| 95 |
+
accum_iter: 1
|
| 96 |
+
|
| 97 |
+
val:
|
| 98 |
+
name: Urban100 OTF Val
|
| 99 |
+
type: pairedimagedataset
|
| 100 |
+
dataroot_gt: [datasets/val/urban100/hr]
|
| 101 |
+
dataroot_lq: [datasets/val/urban100_otf_x4]
|
| 102 |
+
|
| 103 |
+
network_g:
|
| 104 |
+
type: heart
|
| 105 |
+
use_checkpoint: false
|
| 106 |
+
|
| 107 |
+
network_d:
|
| 108 |
+
type: dunet
|
| 109 |
+
|
| 110 |
+
path:
|
| 111 |
+
pretrain_network_g: experiments/4x_HEART_OTF_gan/models/net_g_ema_latest.safetensors
|
| 112 |
+
param_key_g: ~
|
| 113 |
+
strict_load_g: true
|
| 114 |
+
resume_state: ~
|
| 115 |
+
|
| 116 |
+
train:
|
| 117 |
+
ema_decay: 0.999
|
| 118 |
+
ema_power: 0.75
|
| 119 |
+
grad_clip: false
|
| 120 |
+
grad_clip_max_norm: 100
|
| 121 |
+
optim_g:
|
| 122 |
+
type: AdamW
|
| 123 |
+
lr: !!float 1e-4
|
| 124 |
+
weight_decay: 0
|
| 125 |
+
betas: [0.9, 0.99]
|
| 126 |
+
optim_d:
|
| 127 |
+
type: AdamW
|
| 128 |
+
lr: !!float 1e-4
|
| 129 |
+
weight_decay: 0
|
| 130 |
+
betas: [0.9, 0.99]
|
| 131 |
+
scheduler:
|
| 132 |
+
type: MultiStepLR
|
| 133 |
+
milestones: [75000, 120000]
|
| 134 |
+
gamma: 0.5
|
| 135 |
+
total_iter: 150000
|
| 136 |
+
warmup_iter: -1
|
| 137 |
+
losses:
|
| 138 |
+
- type: mssimloss
|
| 139 |
+
loss_weight: 0.5
|
| 140 |
+
- type: perceptualloss
|
| 141 |
+
criterion: charbonnier
|
| 142 |
+
loss_weight: 0.01
|
| 143 |
+
- type: hsluvloss
|
| 144 |
+
criterion: charbonnier
|
| 145 |
+
loss_weight: 1.0
|
| 146 |
+
- type: ganloss
|
| 147 |
+
gan_type: vanilla
|
| 148 |
+
loss_weight: 0.1
|
| 149 |
+
|
| 150 |
+
val:
|
| 151 |
+
val_enabled: true
|
| 152 |
+
val_freq: 5000
|
| 153 |
+
save_img: true
|
| 154 |
+
tile_size: 0
|
| 155 |
+
tile_overlap: 8
|
| 156 |
+
metrics_enabled: true
|
| 157 |
+
metrics:
|
| 158 |
+
topiq:
|
| 159 |
+
type: calculate_topiq
|
| 160 |
+
|
| 161 |
+
logger:
|
| 162 |
+
print_freq: 100
|
| 163 |
+
save_checkpoint_freq: 5000
|
| 164 |
+
save_checkpoint_format: safetensors
|
| 165 |
+
use_tb_logger: true
|
configs/4x_HEART_OTF_v2.yml
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/the-database/traiNNer-redux/refs/heads/master/schemas/redux-config.schema.json
|
| 2 |
+
#########################################################################################
|
| 3 |
+
# HEART 4x OTF v2 (lq 96) — window-aligned fidelity pretrain for GAN finetunes
|
| 4 |
+
#
|
| 5 |
+
# Replaces the v1.1 lq-80 run as the canonical OTF fidelity pretrain. Root-cause
|
| 6 |
+
# fix: lq_size 96 (= 3 x 32px window) keeps every attention window on real
|
| 7 |
+
# content during training. The lq-80 crop forced the model to reflect-pad its
|
| 8 |
+
# own bottom/right edge every batch, teaching a "mirror at edge" response that
|
| 9 |
+
# shows up as a bright band / tile grid (and that the GAN loss amplifies).
|
| 10 |
+
#
|
| 11 |
+
# Starts from the finished v1.1 weights (backup_otf_v2, 150k). Same degradation
|
| 12 |
+
# + losses as v1.1, only the crop size changes.
|
| 13 |
+
#########################################################################################
|
| 14 |
+
name: 4x_HEART_OTF_v2
|
| 15 |
+
scale: 4
|
| 16 |
+
use_amp: true
|
| 17 |
+
amp_bf16: true
|
| 18 |
+
use_channels_last: true
|
| 19 |
+
fast_matmul: false
|
| 20 |
+
use_compile: true
|
| 21 |
+
compile_mode: reduce-overhead
|
| 22 |
+
num_gpu: auto
|
| 23 |
+
manual_seed: 1024
|
| 24 |
+
|
| 25 |
+
high_order_degradation: true
|
| 26 |
+
high_order_degradations_debug: false
|
| 27 |
+
high_order_degradations_debug_limit: 100
|
| 28 |
+
|
| 29 |
+
lq_usm: true
|
| 30 |
+
lq_usm_radius_range: [1, 25]
|
| 31 |
+
|
| 32 |
+
# Degradation identical to v1.1
|
| 33 |
+
blur_prob: 0.6
|
| 34 |
+
resize_prob: [0.2, 0.7, 0.1]
|
| 35 |
+
resize_mode_list: ['bilinear', 'bicubic', 'nearest-exact', 'lanczos']
|
| 36 |
+
resize_mode_prob: [0.25, 0.25, 0.25, 0.25]
|
| 37 |
+
resize_range: [0.4, 1.5]
|
| 38 |
+
gaussian_noise_prob: 0.4
|
| 39 |
+
noise_range: [1, 15]
|
| 40 |
+
poisson_scale_range: [0.05, 2]
|
| 41 |
+
gray_noise_prob: 0.1
|
| 42 |
+
jpeg_prob: 0.4
|
| 43 |
+
jpeg_range: [40, 95]
|
| 44 |
+
|
| 45 |
+
blur_prob2: 0.3
|
| 46 |
+
resize_prob2: [0.3, 0.4, 0.3]
|
| 47 |
+
resize_mode_list2: ['bilinear', 'bicubic', 'nearest-exact', 'lanczos']
|
| 48 |
+
resize_mode_prob2: [0.25, 0.25, 0.25, 0.25]
|
| 49 |
+
resize_range2: [0.6, 1.2]
|
| 50 |
+
gaussian_noise_prob2: 0.3
|
| 51 |
+
noise_range2: [1, 10]
|
| 52 |
+
poisson_scale_range2: [0.05, 1.5]
|
| 53 |
+
gray_noise_prob2: 0.05
|
| 54 |
+
jpeg_prob2: 0.3
|
| 55 |
+
jpeg_range2: [40, 95]
|
| 56 |
+
|
| 57 |
+
resize_mode_list3: ['bilinear', 'bicubic', 'nearest-exact', 'lanczos']
|
| 58 |
+
resize_mode_prob3: [0.25, 0.25, 0.25, 0.25]
|
| 59 |
+
|
| 60 |
+
queue_size: 120
|
| 61 |
+
|
| 62 |
+
datasets:
|
| 63 |
+
train:
|
| 64 |
+
name: LUCID OTF Train
|
| 65 |
+
type: realesrgandataset
|
| 66 |
+
dataroot_gt: [datasets/train/lucid_hc512/hr]
|
| 67 |
+
|
| 68 |
+
blur_kernel_size: 12
|
| 69 |
+
kernel_list: ['iso', 'aniso', 'generalized_iso', 'generalized_aniso', 'plateau_iso', 'plateau_aniso']
|
| 70 |
+
kernel_prob: [0.45, 0.25, 0.12, 0.03, 0.12, 0.03]
|
| 71 |
+
kernel_range: [5, 17]
|
| 72 |
+
blur_sigma: [0.2, 2]
|
| 73 |
+
betag_range: [0.5, 4]
|
| 74 |
+
betap_range: [1, 2]
|
| 75 |
+
|
| 76 |
+
blur_kernel_size2: 12
|
| 77 |
+
kernel_list2: ['iso', 'aniso', 'generalized_iso', 'generalized_aniso', 'plateau_iso', 'plateau_aniso']
|
| 78 |
+
kernel_prob2: [0.45, 0.25, 0.12, 0.03, 0.12, 0.03]
|
| 79 |
+
kernel_range2: [5, 17]
|
| 80 |
+
blur_sigma2: [0.2, 1]
|
| 81 |
+
betag_range2: [0.5, 4]
|
| 82 |
+
betap_range2: [1, 2]
|
| 83 |
+
|
| 84 |
+
final_kernel_range: [5, 17]
|
| 85 |
+
|
| 86 |
+
lq_size: 96 # = 3 x 32px window — window-aligned, no learned edge response
|
| 87 |
+
use_hflip: true
|
| 88 |
+
use_rot: true
|
| 89 |
+
num_worker_per_gpu: 8
|
| 90 |
+
batch_size_per_gpu: 2
|
| 91 |
+
accum_iter: 1
|
| 92 |
+
|
| 93 |
+
val:
|
| 94 |
+
name: Urban100 OTF Val
|
| 95 |
+
type: pairedimagedataset
|
| 96 |
+
dataroot_gt: [datasets/val/urban100/hr]
|
| 97 |
+
dataroot_lq: [datasets/val/urban100_otf_x4]
|
| 98 |
+
|
| 99 |
+
network_g:
|
| 100 |
+
type: heart
|
| 101 |
+
use_checkpoint: false
|
| 102 |
+
|
| 103 |
+
path:
|
| 104 |
+
pretrain_network_g: experiments/backup_otf_v2/net_g_ema_150000.safetensors
|
| 105 |
+
param_key_g: ~
|
| 106 |
+
strict_load_g: true
|
| 107 |
+
resume_state: ~
|
| 108 |
+
|
| 109 |
+
train:
|
| 110 |
+
ema_decay: 0.999
|
| 111 |
+
ema_power: 0.75
|
| 112 |
+
grad_clip: false
|
| 113 |
+
grad_clip_max_norm: 100
|
| 114 |
+
optim_g:
|
| 115 |
+
type: AdamW
|
| 116 |
+
lr: !!float 5e-5
|
| 117 |
+
weight_decay: 0
|
| 118 |
+
betas: [0.9, 0.99]
|
| 119 |
+
scheduler:
|
| 120 |
+
type: MultiStepLR
|
| 121 |
+
milestones: [50000, 100000]
|
| 122 |
+
gamma: 0.5
|
| 123 |
+
total_iter: 150000
|
| 124 |
+
warmup_iter: -1
|
| 125 |
+
losses:
|
| 126 |
+
- type: charbonnierloss
|
| 127 |
+
loss_weight: 1.0
|
| 128 |
+
- type: perceptualloss
|
| 129 |
+
criterion: charbonnier
|
| 130 |
+
loss_weight: 0.01
|
| 131 |
+
- type: mssimloss
|
| 132 |
+
loss_weight: 0.5
|
| 133 |
+
|
| 134 |
+
val:
|
| 135 |
+
val_enabled: true
|
| 136 |
+
val_freq: 5000
|
| 137 |
+
save_img: true
|
| 138 |
+
tile_size: 0
|
| 139 |
+
tile_overlap: 8
|
| 140 |
+
metrics_enabled: true
|
| 141 |
+
metrics:
|
| 142 |
+
psnr:
|
| 143 |
+
type: calculate_psnr
|
| 144 |
+
crop_border: 4
|
| 145 |
+
test_y_channel: true
|
| 146 |
+
ssim:
|
| 147 |
+
type: calculate_ssim
|
| 148 |
+
crop_border: 4
|
| 149 |
+
test_y_channel: true
|
| 150 |
+
|
| 151 |
+
logger:
|
| 152 |
+
print_freq: 100
|
| 153 |
+
save_checkpoint_freq: 5000
|
| 154 |
+
save_checkpoint_format: safetensors
|
| 155 |
+
use_tb_logger: true
|
configs/4x_HEART_release.yml
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/the-database/traiNNer-redux/refs/heads/master/schemas/redux-config.schema.json
|
| 2 |
+
#########################################################################################
|
| 3 |
+
# HEART 4x release training
|
| 4 |
+
# LUCID CC0 v2 HC-512 (100,866 tiles, 512x512, CC0) - license-clean pretrain.
|
| 5 |
+
# lq 96 / batch 2: 9 windows per crop so the shifted-window mixing actually works.
|
| 6 |
+
# torch.compile reduce-overhead: +46% training speed, quality-neutral (validated).
|
| 7 |
+
# rank is set by the release driver after the rank ablation picks a winner.
|
| 8 |
+
#########################################################################################
|
| 9 |
+
name: 4x_HEART
|
| 10 |
+
scale: 4
|
| 11 |
+
use_amp: true
|
| 12 |
+
amp_bf16: true
|
| 13 |
+
use_channels_last: true
|
| 14 |
+
fast_matmul: false
|
| 15 |
+
use_compile: true
|
| 16 |
+
compile_mode: reduce-overhead
|
| 17 |
+
num_gpu: auto
|
| 18 |
+
manual_seed: 1024
|
| 19 |
+
|
| 20 |
+
datasets:
|
| 21 |
+
train:
|
| 22 |
+
name: LUCID Train
|
| 23 |
+
type: pairedimagedataset
|
| 24 |
+
dataroot_gt: [datasets/train/lucid_hc512/hr]
|
| 25 |
+
dataroot_lq: [datasets/train/lucid_hc512/lr_x4]
|
| 26 |
+
lq_size: 96
|
| 27 |
+
use_hflip: true
|
| 28 |
+
use_rot: true
|
| 29 |
+
num_worker_per_gpu: 8
|
| 30 |
+
batch_size_per_gpu: 2
|
| 31 |
+
accum_iter: 1
|
| 32 |
+
val:
|
| 33 |
+
name: Urban100 Val
|
| 34 |
+
type: pairedimagedataset
|
| 35 |
+
dataroot_gt: [datasets/val/urban100/hr]
|
| 36 |
+
dataroot_lq: [datasets/val/urban100/lr_x4]
|
| 37 |
+
filename_tmpl: "{}x4"
|
| 38 |
+
|
| 39 |
+
network_g:
|
| 40 |
+
type: heart
|
| 41 |
+
use_checkpoint: false
|
| 42 |
+
rank: 8
|
| 43 |
+
attention_freq: 2
|
| 44 |
+
|
| 45 |
+
path:
|
| 46 |
+
param_key_g: ~
|
| 47 |
+
strict_load_g: true
|
| 48 |
+
resume_state: ~
|
| 49 |
+
|
| 50 |
+
train:
|
| 51 |
+
ema_decay: 0.999
|
| 52 |
+
ema_power: 0.75
|
| 53 |
+
grad_clip: false
|
| 54 |
+
grad_clip_max_norm: 100
|
| 55 |
+
optim_g:
|
| 56 |
+
type: AdamW
|
| 57 |
+
lr: !!float 2e-4
|
| 58 |
+
weight_decay: 0
|
| 59 |
+
betas: [0.9, 0.99]
|
| 60 |
+
scheduler:
|
| 61 |
+
type: MultiStepLR
|
| 62 |
+
milestones: [300000, 500000, 650000, 700000, 750000]
|
| 63 |
+
gamma: 0.5
|
| 64 |
+
total_iter: 800000
|
| 65 |
+
warmup_iter: -1
|
| 66 |
+
losses:
|
| 67 |
+
- type: charbonnierloss
|
| 68 |
+
loss_weight: 1.0
|
| 69 |
+
|
| 70 |
+
val:
|
| 71 |
+
val_enabled: true
|
| 72 |
+
val_freq: 5000
|
| 73 |
+
save_img: true
|
| 74 |
+
tile_size: 0
|
| 75 |
+
tile_overlap: 8
|
| 76 |
+
metrics_enabled: true
|
| 77 |
+
metrics:
|
| 78 |
+
psnr:
|
| 79 |
+
type: calculate_psnr
|
| 80 |
+
crop_border: 4
|
| 81 |
+
test_y_channel: true
|
| 82 |
+
ssim:
|
| 83 |
+
type: calculate_ssim
|
| 84 |
+
crop_border: 4
|
| 85 |
+
test_y_channel: true
|
| 86 |
+
|
| 87 |
+
logger:
|
| 88 |
+
print_freq: 100
|
| 89 |
+
save_checkpoint_freq: 5000
|
| 90 |
+
save_checkpoint_format: safetensors
|
| 91 |
+
use_tb_logger: true
|
heart_arch.py
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ruff: noqa
|
| 2 |
+
# type: ignore
|
| 3 |
+
# HEART - Hybrid Efficient Attention with Rank-factorized bias Transformer
|
| 4 |
+
# based on HAT-iLN (arXiv:2504.06629) and SST's RIB (arXiv:2603.06738)
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from collections.abc import Sequence
|
| 8 |
+
from typing import Literal
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from einops import rearrange
|
| 12 |
+
from spandrel.util import store_hyperparameters
|
| 13 |
+
from spandrel.util.timm import trunc_normal_
|
| 14 |
+
from torch import nn
|
| 15 |
+
from torch.nn import functional as F # noqa: N812
|
| 16 |
+
from torch.utils import checkpoint
|
| 17 |
+
|
| 18 |
+
from traiNNer.archs.arch_util import iLN
|
| 19 |
+
from traiNNer.archs.hat_iln_arch import (
|
| 20 |
+
CAB,
|
| 21 |
+
AffineTransform,
|
| 22 |
+
DropPath,
|
| 23 |
+
Mlp,
|
| 24 |
+
PatchEmbed,
|
| 25 |
+
PatchUnEmbed,
|
| 26 |
+
Upsample,
|
| 27 |
+
)
|
| 28 |
+
from traiNNer.utils.registry import ARCH_REGISTRY, SPANDREL_REGISTRY
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class RIBWindowAttention(nn.Module):
|
| 32 |
+
"""Window attention with Rank-factorized Implicit Neural Bias (RIB).
|
| 33 |
+
|
| 34 |
+
RIB (SST, arXiv 2603.06738) replaces the relative position bias table:
|
| 35 |
+
position features from a small implicit net are concatenated onto Q/K,
|
| 36 |
+
turning bias addition into a dot product, so attention runs on
|
| 37 |
+
FlashAttention/SDPA kernels with no bias table and no masks.
|
| 38 |
+
Channels-first (B, C, H, W); shifted windows use non-wrapping
|
| 39 |
+
pad-and-partition (no cyclic wrap).
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
def __init__(
|
| 43 |
+
self,
|
| 44 |
+
dim: int,
|
| 45 |
+
window_size: int,
|
| 46 |
+
num_heads: int,
|
| 47 |
+
rank: int = 8,
|
| 48 |
+
rib_hidden_dim: int = 32,
|
| 49 |
+
rib_n_freqs: int = 10,
|
| 50 |
+
shift: bool = False,
|
| 51 |
+
) -> None:
|
| 52 |
+
super().__init__()
|
| 53 |
+
self.window_size = (window_size, window_size)
|
| 54 |
+
self.num_heads = num_heads
|
| 55 |
+
assert dim % num_heads == 0, "dim must be divisible by num_heads"
|
| 56 |
+
|
| 57 |
+
self.to_qkv = nn.Conv2d(dim, dim * 3, 1, 1, 0)
|
| 58 |
+
self.to_out = nn.Conv2d(dim, dim, 1, 1, 0)
|
| 59 |
+
|
| 60 |
+
self.rank = rank
|
| 61 |
+
|
| 62 |
+
# normalized window coordinates
|
| 63 |
+
wh, ww = self.window_size
|
| 64 |
+
yy, xx = torch.meshgrid(torch.arange(wh), torch.arange(ww), indexing="ij")
|
| 65 |
+
coords = torch.stack([xx, yy], dim=-1).reshape(-1, 2).float() # (N, 2)
|
| 66 |
+
coords[:, 0] = (2.0 * (coords[:, 0] + 0.5) / ww) - 1.0
|
| 67 |
+
coords[:, 1] = (2.0 * (coords[:, 1] + 0.5) / wh) - 1.0
|
| 68 |
+
|
| 69 |
+
self.n_freqs = rib_n_freqs
|
| 70 |
+
if self.n_freqs > 0:
|
| 71 |
+
base_coords = coords.clone()
|
| 72 |
+
for i in range(self.n_freqs):
|
| 73 |
+
freq = 2**i
|
| 74 |
+
coords = torch.cat(
|
| 75 |
+
[
|
| 76 |
+
coords,
|
| 77 |
+
torch.sin(base_coords * freq),
|
| 78 |
+
torch.cos(base_coords * freq),
|
| 79 |
+
],
|
| 80 |
+
dim=-1,
|
| 81 |
+
)
|
| 82 |
+
self.register_buffer("rib_coords", coords, persistent=False)
|
| 83 |
+
|
| 84 |
+
n_input = 2 + 4 * self.n_freqs
|
| 85 |
+
hidden_d = rib_hidden_dim
|
| 86 |
+
self.to_hidden = nn.Parameter(torch.empty(n_input, hidden_d))
|
| 87 |
+
self.hidden_b = nn.Parameter(torch.zeros(1, hidden_d))
|
| 88 |
+
self.to_q = nn.Parameter(torch.empty(num_heads, hidden_d, self.rank))
|
| 89 |
+
self.to_k = nn.Parameter(torch.empty(num_heads, hidden_d, self.rank))
|
| 90 |
+
|
| 91 |
+
nn.init.normal_(self.to_hidden, mean=0.0, std=0.05)
|
| 92 |
+
nn.init.normal_(self.to_q, mean=0.0, std=0.05)
|
| 93 |
+
nn.init.normal_(self.to_k, mean=0.0, std=0.05)
|
| 94 |
+
|
| 95 |
+
self.shift = shift
|
| 96 |
+
|
| 97 |
+
def pad_to_win(self, x: torch.Tensor, h: int, w: int) -> torch.Tensor:
|
| 98 |
+
# inputs are pre-aligned to window multiples by HEART.check_image_size
|
| 99 |
+
# (which owns the replicate fallback), so reflect padding is safe here
|
| 100 |
+
pad_h = (self.window_size[0] - h % self.window_size[0]) % self.window_size[0]
|
| 101 |
+
pad_w = (self.window_size[1] - w % self.window_size[1]) % self.window_size[1]
|
| 102 |
+
return F.pad(x, (0, pad_w, 0, pad_h), mode="reflect")
|
| 103 |
+
|
| 104 |
+
def _flash_cat_attn(
|
| 105 |
+
self,
|
| 106 |
+
q: torch.Tensor,
|
| 107 |
+
k: torch.Tensor,
|
| 108 |
+
v: torch.Tensor,
|
| 109 |
+
pos_q: torch.Tensor,
|
| 110 |
+
pos_k: torch.Tensor,
|
| 111 |
+
) -> torch.Tensor:
|
| 112 |
+
# q, k, v: (Bwin, N, heads, D); pos_q, pos_k: (Bwin, heads, N, R)
|
| 113 |
+
d = q.shape[-1]
|
| 114 |
+
pos_q = pos_q.transpose(1, 2) # (Bwin, N, heads, R)
|
| 115 |
+
pos_k = pos_k.transpose(1, 2)
|
| 116 |
+
r = pos_q.shape[-1]
|
| 117 |
+
|
| 118 |
+
q = q * (d**-0.5)
|
| 119 |
+
pos_q = pos_q * (r**-0.5)
|
| 120 |
+
q_cat = torch.cat([q, pos_q], dim=-1)
|
| 121 |
+
k_cat = torch.cat([k, pos_k], dim=-1)
|
| 122 |
+
|
| 123 |
+
d_total = q_cat.shape[-1]
|
| 124 |
+
pad = (8 - (d_total % 8)) % 8
|
| 125 |
+
if pad:
|
| 126 |
+
q_cat = F.pad(q_cat, (0, pad))
|
| 127 |
+
k_cat = F.pad(k_cat, (0, pad))
|
| 128 |
+
v_cat = F.pad(v, (0, r + pad))
|
| 129 |
+
|
| 130 |
+
# q and pos_q are pre-scaled (SST eq. 5); scale=1.0 keeps SDPA from
|
| 131 |
+
# rescaling them again
|
| 132 |
+
out = F.scaled_dot_product_attention(
|
| 133 |
+
q_cat.transpose(1, 2),
|
| 134 |
+
k_cat.transpose(1, 2),
|
| 135 |
+
v_cat.transpose(1, 2),
|
| 136 |
+
scale=1.0,
|
| 137 |
+
).transpose(1, 2)[:, :, :, :d]
|
| 138 |
+
return out
|
| 139 |
+
|
| 140 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 141 |
+
# x: (B, C, H, W)
|
| 142 |
+
_, _, h, w = x.shape
|
| 143 |
+
x = self.pad_to_win(x, h, w)
|
| 144 |
+
|
| 145 |
+
# shifted windows: pad top/left by half a window, partition from the
|
| 146 |
+
# padded origin. x is window-aligned here, so padding (sh, sh) per
|
| 147 |
+
# axis keeps alignment (one extra window row/col).
|
| 148 |
+
sh = sw = 0
|
| 149 |
+
if self.shift:
|
| 150 |
+
sh, sw = self.window_size[0] // 2, self.window_size[1] // 2
|
| 151 |
+
x = F.pad(x, (sw, sw, sh, sh), mode="reflect")
|
| 152 |
+
|
| 153 |
+
h_div, w_div = (
|
| 154 |
+
x.shape[2] // self.window_size[0],
|
| 155 |
+
x.shape[3] // self.window_size[1],
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
qkv = self.to_qkv(x)
|
| 159 |
+
qkv = rearrange(
|
| 160 |
+
qkv,
|
| 161 |
+
"b (qkv heads c) (h wh) (w ww) -> qkv (b h w) (wh ww) heads c",
|
| 162 |
+
heads=self.num_heads,
|
| 163 |
+
wh=self.window_size[0],
|
| 164 |
+
ww=self.window_size[1],
|
| 165 |
+
qkv=3,
|
| 166 |
+
)
|
| 167 |
+
q, k, v = qkv[0], qkv[1], qkv[2] # (Bwin, N, heads, D)
|
| 168 |
+
|
| 169 |
+
# RIB position features (fp32 for fp16-inference stability)
|
| 170 |
+
intermediate = F.relu(
|
| 171 |
+
self.rib_coords.to(dtype=torch.float32)
|
| 172 |
+
@ self.to_hidden.to(dtype=torch.float32)
|
| 173 |
+
+ self.hidden_b.to(dtype=torch.float32)
|
| 174 |
+
) # (N, hidden_d)
|
| 175 |
+
q_pos = torch.einsum(
|
| 176 |
+
"nd,hdr->hnr", intermediate, self.to_q.to(dtype=torch.float32)
|
| 177 |
+
) # (heads, N, R)
|
| 178 |
+
k_pos = torch.einsum(
|
| 179 |
+
"nd,hdr->hnr", intermediate, self.to_k.to(dtype=torch.float32)
|
| 180 |
+
)
|
| 181 |
+
bwin = q.shape[0]
|
| 182 |
+
q_pos = (
|
| 183 |
+
q_pos.to(q.dtype).unsqueeze(0).expand(bwin, -1, -1, -1)
|
| 184 |
+
) # (Bwin, heads, N, R)
|
| 185 |
+
k_pos = k_pos.to(k.dtype).unsqueeze(0).expand(bwin, -1, -1, -1)
|
| 186 |
+
|
| 187 |
+
out = self._flash_cat_attn(q, k, v, q_pos, k_pos) # (Bwin, N, heads, D)
|
| 188 |
+
|
| 189 |
+
out = rearrange(
|
| 190 |
+
out,
|
| 191 |
+
"(b h w) (wh ww) heads c -> b (heads c) (h wh) (w ww)",
|
| 192 |
+
h=h_div,
|
| 193 |
+
w=w_div,
|
| 194 |
+
wh=self.window_size[0],
|
| 195 |
+
ww=self.window_size[1],
|
| 196 |
+
)
|
| 197 |
+
out = out[:, :, sh : sh + h, sw : sw + w]
|
| 198 |
+
return self.to_out(out)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
class HAB_RIB(nn.Module):
|
| 202 |
+
"""Hybrid Attention Block: RIB window attention + CAB conv branch + i-LN.
|
| 203 |
+
|
| 204 |
+
The block unpacks ``(x, std)`` from norm1/norm2, so norm_layer must be
|
| 205 |
+
i-LN-compatible; the parameter exists for HAT symmetry, not as a knob.
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
def __init__(
|
| 209 |
+
self,
|
| 210 |
+
dim: int,
|
| 211 |
+
num_heads: int,
|
| 212 |
+
window_size: int = 32,
|
| 213 |
+
compress_ratio: int = 3,
|
| 214 |
+
squeeze_factor: int = 30,
|
| 215 |
+
conv_scale: float = 0.01,
|
| 216 |
+
mlp_ratio: float = 2.0,
|
| 217 |
+
drop_path: float = 0.0,
|
| 218 |
+
norm_layer: type[nn.Module] = iLN,
|
| 219 |
+
rank: int = 8,
|
| 220 |
+
rib_hidden_dim: int = 32,
|
| 221 |
+
rib_n_freqs: int = 10,
|
| 222 |
+
shift: bool = False,
|
| 223 |
+
use_attention: bool = True,
|
| 224 |
+
) -> None:
|
| 225 |
+
super().__init__()
|
| 226 |
+
self.conv_scale = conv_scale
|
| 227 |
+
self.use_attention = use_attention
|
| 228 |
+
|
| 229 |
+
self.norm1 = norm_layer(dim)
|
| 230 |
+
if use_attention:
|
| 231 |
+
self.attn = RIBWindowAttention(
|
| 232 |
+
dim=dim,
|
| 233 |
+
window_size=window_size,
|
| 234 |
+
num_heads=num_heads,
|
| 235 |
+
rank=rank,
|
| 236 |
+
rib_hidden_dim=rib_hidden_dim,
|
| 237 |
+
rib_n_freqs=rib_n_freqs,
|
| 238 |
+
shift=shift,
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
self.conv_block = CAB(
|
| 242 |
+
num_feat=dim, compress_ratio=compress_ratio, squeeze_factor=squeeze_factor
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
|
| 246 |
+
self.norm2 = norm_layer(dim)
|
| 247 |
+
mlp_hidden_dim = int(dim * mlp_ratio)
|
| 248 |
+
self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim)
|
| 249 |
+
|
| 250 |
+
def forward(self, x: torch.Tensor, x_size: tuple[int, int]) -> torch.Tensor:
|
| 251 |
+
h, w = x_size
|
| 252 |
+
b, _, c = x.shape
|
| 253 |
+
|
| 254 |
+
shortcut = x
|
| 255 |
+
x, std1 = self.norm1(x)
|
| 256 |
+
x_2d = x.transpose(1, 2).contiguous().view(b, c, h, w)
|
| 257 |
+
|
| 258 |
+
# conv branch
|
| 259 |
+
conv_x = self.conv_block(x_2d).view(b, c, -1).transpose(1, 2)
|
| 260 |
+
residual = conv_x * self.conv_scale
|
| 261 |
+
|
| 262 |
+
# RIB window attention (channels-first), optional per attention_freq
|
| 263 |
+
if self.use_attention:
|
| 264 |
+
attn_out = self.attn(x_2d)
|
| 265 |
+
attn_x = attn_out.view(b, c, -1).transpose(1, 2)
|
| 266 |
+
residual = residual + self.drop_path(attn_x)
|
| 267 |
+
|
| 268 |
+
# i-LN: rescale by std from norm1
|
| 269 |
+
x = shortcut + std1.view(b, 1, 1) * residual
|
| 270 |
+
|
| 271 |
+
# FFN with i-LN rescaling
|
| 272 |
+
x_normed, std2 = self.norm2(x)
|
| 273 |
+
ffn_out = self.mlp(x_normed)
|
| 274 |
+
x = x + std2.view(b, 1, 1) * self.drop_path(ffn_out)
|
| 275 |
+
|
| 276 |
+
return x
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
class AttenBlocksRIB(nn.Module):
|
| 280 |
+
"""A stack of HAB_RIB blocks with optional gradient checkpointing."""
|
| 281 |
+
|
| 282 |
+
def __init__(
|
| 283 |
+
self,
|
| 284 |
+
dim: int,
|
| 285 |
+
depth: int,
|
| 286 |
+
num_heads: int,
|
| 287 |
+
window_size: int,
|
| 288 |
+
compress_ratio: int,
|
| 289 |
+
squeeze_factor: int,
|
| 290 |
+
conv_scale: float,
|
| 291 |
+
mlp_ratio: float,
|
| 292 |
+
drop_path: float | list[float],
|
| 293 |
+
norm_layer: type[nn.Module] = iLN,
|
| 294 |
+
use_checkpoint: bool = False,
|
| 295 |
+
rank: int = 8,
|
| 296 |
+
rib_hidden_dim: int = 32,
|
| 297 |
+
rib_n_freqs: int = 10,
|
| 298 |
+
attention_freq: int = 2,
|
| 299 |
+
) -> None:
|
| 300 |
+
super().__init__()
|
| 301 |
+
self.use_checkpoint = use_checkpoint
|
| 302 |
+
blocks: list[HAB_RIB] = []
|
| 303 |
+
attn_idx = 0
|
| 304 |
+
for i in range(depth):
|
| 305 |
+
use_attention = i % attention_freq == 0
|
| 306 |
+
blocks.append(
|
| 307 |
+
HAB_RIB(
|
| 308 |
+
dim=dim,
|
| 309 |
+
num_heads=num_heads,
|
| 310 |
+
window_size=window_size,
|
| 311 |
+
compress_ratio=compress_ratio,
|
| 312 |
+
squeeze_factor=squeeze_factor,
|
| 313 |
+
conv_scale=conv_scale,
|
| 314 |
+
mlp_ratio=mlp_ratio,
|
| 315 |
+
drop_path=drop_path[i]
|
| 316 |
+
if isinstance(drop_path, list)
|
| 317 |
+
else drop_path,
|
| 318 |
+
norm_layer=norm_layer,
|
| 319 |
+
rank=rank,
|
| 320 |
+
rib_hidden_dim=rib_hidden_dim,
|
| 321 |
+
rib_n_freqs=rib_n_freqs,
|
| 322 |
+
shift=(attn_idx % 2 == 1) if use_attention else False,
|
| 323 |
+
use_attention=use_attention,
|
| 324 |
+
)
|
| 325 |
+
)
|
| 326 |
+
if use_attention:
|
| 327 |
+
attn_idx += 1
|
| 328 |
+
self.blocks = nn.ModuleList(blocks)
|
| 329 |
+
|
| 330 |
+
def forward(self, x: torch.Tensor, x_size: tuple[int, int]) -> torch.Tensor:
|
| 331 |
+
use_chk = self.use_checkpoint and self.training
|
| 332 |
+
for blk in self.blocks:
|
| 333 |
+
if use_chk:
|
| 334 |
+
x = checkpoint.checkpoint(blk, x, x_size, use_reentrant=False)
|
| 335 |
+
else:
|
| 336 |
+
x = blk(x, x_size)
|
| 337 |
+
return x
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class RHAG_RIB(nn.Module):
|
| 341 |
+
"""Residual Hybrid Attention Group: AttenBlocksRIB -> 3x3 conv -> residual."""
|
| 342 |
+
|
| 343 |
+
def __init__(
|
| 344 |
+
self,
|
| 345 |
+
dim: int,
|
| 346 |
+
depth: int,
|
| 347 |
+
num_heads: int,
|
| 348 |
+
window_size: int,
|
| 349 |
+
compress_ratio: int,
|
| 350 |
+
squeeze_factor: int,
|
| 351 |
+
conv_scale: float,
|
| 352 |
+
mlp_ratio: float,
|
| 353 |
+
drop_path: float | list[float],
|
| 354 |
+
norm_layer: type[nn.Module] = iLN,
|
| 355 |
+
use_checkpoint: bool = False,
|
| 356 |
+
img_size: int = 64,
|
| 357 |
+
patch_size: int = 1,
|
| 358 |
+
resi_connection: str = "1conv",
|
| 359 |
+
rank: int = 8,
|
| 360 |
+
rib_hidden_dim: int = 32,
|
| 361 |
+
rib_n_freqs: int = 10,
|
| 362 |
+
attention_freq: int = 2,
|
| 363 |
+
) -> None:
|
| 364 |
+
super().__init__()
|
| 365 |
+
self.dim = dim
|
| 366 |
+
|
| 367 |
+
self.residual_group = AttenBlocksRIB(
|
| 368 |
+
dim=dim,
|
| 369 |
+
depth=depth,
|
| 370 |
+
num_heads=num_heads,
|
| 371 |
+
window_size=window_size,
|
| 372 |
+
compress_ratio=compress_ratio,
|
| 373 |
+
squeeze_factor=squeeze_factor,
|
| 374 |
+
conv_scale=conv_scale,
|
| 375 |
+
mlp_ratio=mlp_ratio,
|
| 376 |
+
drop_path=drop_path,
|
| 377 |
+
norm_layer=norm_layer,
|
| 378 |
+
use_checkpoint=use_checkpoint,
|
| 379 |
+
rank=rank,
|
| 380 |
+
rib_hidden_dim=rib_hidden_dim,
|
| 381 |
+
rib_n_freqs=rib_n_freqs,
|
| 382 |
+
attention_freq=attention_freq,
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
if resi_connection == "1conv":
|
| 386 |
+
self.conv = nn.Conv2d(dim, dim, 3, 1, 1)
|
| 387 |
+
elif resi_connection == "identity":
|
| 388 |
+
self.conv = nn.Identity()
|
| 389 |
+
else:
|
| 390 |
+
raise ValueError(f"unsupported resi_connection: {resi_connection}")
|
| 391 |
+
|
| 392 |
+
self.patch_embed = PatchEmbed(
|
| 393 |
+
img_size=img_size,
|
| 394 |
+
patch_size=patch_size,
|
| 395 |
+
in_chans=0,
|
| 396 |
+
embed_dim=dim,
|
| 397 |
+
norm_layer=None,
|
| 398 |
+
)
|
| 399 |
+
self.patch_unembed = PatchUnEmbed(
|
| 400 |
+
img_size=img_size,
|
| 401 |
+
patch_size=patch_size,
|
| 402 |
+
in_chans=0,
|
| 403 |
+
embed_dim=dim,
|
| 404 |
+
norm_layer=None,
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
+
def forward(self, x: torch.Tensor, x_size: tuple[int, int]) -> torch.Tensor:
|
| 408 |
+
return (
|
| 409 |
+
self.patch_embed(
|
| 410 |
+
self.conv(self.patch_unembed(self.residual_group(x, x_size), x_size))
|
| 411 |
+
)
|
| 412 |
+
+ x
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
@store_hyperparameters()
|
| 417 |
+
class HEART(nn.Module):
|
| 418 |
+
"""HEART - Hybrid Efficient Attention with Rank-factorized bias Transformer.
|
| 419 |
+
|
| 420 |
+
Simplified HAT-iLN: RIB window attention (FlashAttention-friendly) replaces
|
| 421 |
+
the relative position bias table and OCAB is removed. i-LN keeps training
|
| 422 |
+
stable in reduced precision. Pure PyTorch, ONNX-exportable.
|
| 423 |
+
"""
|
| 424 |
+
|
| 425 |
+
hyperparameters = {}
|
| 426 |
+
|
| 427 |
+
def __init__(
|
| 428 |
+
self,
|
| 429 |
+
*,
|
| 430 |
+
img_size: int = 64,
|
| 431 |
+
patch_size: int = 1,
|
| 432 |
+
in_chans: int = 3,
|
| 433 |
+
embed_dim: int = 180,
|
| 434 |
+
depths: Sequence[int] = (6, 6, 6, 6, 6, 6),
|
| 435 |
+
num_heads: Sequence[int] = (6, 6, 6, 6, 6, 6),
|
| 436 |
+
window_size: int = 32,
|
| 437 |
+
compress_ratio: int = 3,
|
| 438 |
+
squeeze_factor: int = 30,
|
| 439 |
+
conv_scale: float = 0.01,
|
| 440 |
+
mlp_ratio: float = 2.0,
|
| 441 |
+
drop_rate: float = 0.0,
|
| 442 |
+
drop_path_rate: float = 0.1,
|
| 443 |
+
norm_layer: type[nn.Module] = iLN,
|
| 444 |
+
ape: bool = False,
|
| 445 |
+
patch_norm: bool = True,
|
| 446 |
+
use_checkpoint: bool = False,
|
| 447 |
+
upscale: int = 1,
|
| 448 |
+
upsampler: Literal["pixelshuffle"] = "pixelshuffle",
|
| 449 |
+
resi_connection: str = "1conv",
|
| 450 |
+
num_feat: int = 64,
|
| 451 |
+
rank: int = 8,
|
| 452 |
+
rib_hidden_dim: int = 32,
|
| 453 |
+
rib_n_freqs: int = 10,
|
| 454 |
+
attention_freq: int = 2,
|
| 455 |
+
) -> None:
|
| 456 |
+
super().__init__()
|
| 457 |
+
|
| 458 |
+
self.window_size = window_size
|
| 459 |
+
|
| 460 |
+
num_in_ch = in_chans
|
| 461 |
+
num_out_ch = in_chans
|
| 462 |
+
self.upscale = upscale
|
| 463 |
+
self.upsampler = upsampler
|
| 464 |
+
|
| 465 |
+
# ------------------------- 1, shallow feature extraction ------------------------- #
|
| 466 |
+
self.conv_first = nn.Conv2d(num_in_ch, embed_dim, 3, 1, 1)
|
| 467 |
+
|
| 468 |
+
# ------------------------- 2, deep feature extraction ------------------------- #
|
| 469 |
+
self.num_layers = len(depths)
|
| 470 |
+
self.embed_dim = embed_dim
|
| 471 |
+
self.ape = ape
|
| 472 |
+
self.patch_norm = patch_norm
|
| 473 |
+
self.num_features = embed_dim
|
| 474 |
+
|
| 475 |
+
self.patch_embed = PatchEmbed(
|
| 476 |
+
img_size=img_size,
|
| 477 |
+
patch_size=patch_size,
|
| 478 |
+
in_chans=embed_dim,
|
| 479 |
+
embed_dim=embed_dim,
|
| 480 |
+
norm_layer=AffineTransform if self.patch_norm else None,
|
| 481 |
+
)
|
| 482 |
+
num_patches = self.patch_embed.num_patches
|
| 483 |
+
patches_resolution = self.patch_embed.patches_resolution
|
| 484 |
+
self.patches_resolution = patches_resolution
|
| 485 |
+
|
| 486 |
+
self.patch_unembed = PatchUnEmbed(
|
| 487 |
+
img_size=img_size,
|
| 488 |
+
patch_size=patch_size,
|
| 489 |
+
in_chans=embed_dim,
|
| 490 |
+
embed_dim=embed_dim,
|
| 491 |
+
norm_layer=AffineTransform if self.patch_norm else None,
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
if self.ape:
|
| 495 |
+
self.absolute_pos_embed = nn.Parameter(
|
| 496 |
+
torch.zeros(1, num_patches, embed_dim)
|
| 497 |
+
)
|
| 498 |
+
trunc_normal_(self.absolute_pos_embed, std=0.02)
|
| 499 |
+
|
| 500 |
+
self.pos_drop = nn.Dropout(p=drop_rate)
|
| 501 |
+
|
| 502 |
+
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))]
|
| 503 |
+
|
| 504 |
+
self.layers = nn.ModuleList()
|
| 505 |
+
for i_layer in range(self.num_layers):
|
| 506 |
+
layer = RHAG_RIB(
|
| 507 |
+
dim=embed_dim,
|
| 508 |
+
depth=depths[i_layer],
|
| 509 |
+
num_heads=num_heads[i_layer],
|
| 510 |
+
window_size=window_size,
|
| 511 |
+
compress_ratio=compress_ratio,
|
| 512 |
+
squeeze_factor=squeeze_factor,
|
| 513 |
+
conv_scale=conv_scale,
|
| 514 |
+
mlp_ratio=mlp_ratio,
|
| 515 |
+
drop_path=dpr[sum(depths[:i_layer]) : sum(depths[: i_layer + 1])],
|
| 516 |
+
norm_layer=norm_layer,
|
| 517 |
+
use_checkpoint=use_checkpoint,
|
| 518 |
+
img_size=img_size,
|
| 519 |
+
patch_size=patch_size,
|
| 520 |
+
resi_connection=resi_connection,
|
| 521 |
+
rank=rank,
|
| 522 |
+
rib_hidden_dim=rib_hidden_dim,
|
| 523 |
+
rib_n_freqs=rib_n_freqs,
|
| 524 |
+
attention_freq=attention_freq,
|
| 525 |
+
)
|
| 526 |
+
self.layers.append(layer)
|
| 527 |
+
self.norm = AffineTransform(self.num_features)
|
| 528 |
+
|
| 529 |
+
if resi_connection == "1conv":
|
| 530 |
+
self.conv_after_body = nn.Conv2d(embed_dim, embed_dim, 3, 1, 1)
|
| 531 |
+
elif resi_connection == "identity":
|
| 532 |
+
self.conv_after_body = nn.Identity()
|
| 533 |
+
else:
|
| 534 |
+
raise ValueError(f"unsupported resi_connection: {resi_connection}")
|
| 535 |
+
|
| 536 |
+
# ------------------------- 3, high quality image reconstruction ------------------------- #
|
| 537 |
+
if self.upsampler == "pixelshuffle":
|
| 538 |
+
self.conv_before_upsample = nn.Sequential(
|
| 539 |
+
nn.Conv2d(embed_dim, num_feat, 3, 1, 1), nn.LeakyReLU(inplace=True)
|
| 540 |
+
)
|
| 541 |
+
self.upsample = Upsample(upscale, num_feat)
|
| 542 |
+
self.conv_last = nn.Conv2d(num_feat, num_out_ch, 3, 1, 1)
|
| 543 |
+
|
| 544 |
+
self.apply(self._init_weights)
|
| 545 |
+
|
| 546 |
+
def _init_weights(self, m: nn.Module) -> None:
|
| 547 |
+
if isinstance(m, nn.Linear):
|
| 548 |
+
trunc_normal_(m.weight, std=0.02)
|
| 549 |
+
if m.bias is not None:
|
| 550 |
+
nn.init.constant_(m.bias, 0)
|
| 551 |
+
elif isinstance(m, (nn.LayerNorm, iLN)):
|
| 552 |
+
nn.init.constant_(m.bias, 0)
|
| 553 |
+
nn.init.constant_(m.weight, 1.0)
|
| 554 |
+
|
| 555 |
+
def check_image_size(self, x: torch.Tensor) -> torch.Tensor:
|
| 556 |
+
_, _, h, w = x.shape
|
| 557 |
+
pad_h = (self.window_size - h % self.window_size) % self.window_size
|
| 558 |
+
pad_w = (self.window_size - w % self.window_size) % self.window_size
|
| 559 |
+
can_reflect = (pad_h == 0 or pad_h < h) and (pad_w == 0 or pad_w < w)
|
| 560 |
+
mode = "reflect" if can_reflect else "replicate"
|
| 561 |
+
return F.pad(x, (0, pad_w, 0, pad_h), mode=mode)
|
| 562 |
+
|
| 563 |
+
def forward_features(self, x: torch.Tensor) -> torch.Tensor:
|
| 564 |
+
x_size = (x.shape[2], x.shape[3])
|
| 565 |
+
|
| 566 |
+
x = self.patch_embed(x)
|
| 567 |
+
if self.ape:
|
| 568 |
+
x = x + self.absolute_pos_embed
|
| 569 |
+
x = self.pos_drop(x)
|
| 570 |
+
|
| 571 |
+
for layer in self.layers:
|
| 572 |
+
x = layer(x, x_size)
|
| 573 |
+
|
| 574 |
+
x = self.norm(x)
|
| 575 |
+
x = self.patch_unembed(x, x_size)
|
| 576 |
+
|
| 577 |
+
return x
|
| 578 |
+
|
| 579 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 580 |
+
h, w = x.shape[2:]
|
| 581 |
+
x = self.check_image_size(x)
|
| 582 |
+
|
| 583 |
+
if self.upsampler == "pixelshuffle":
|
| 584 |
+
x = self.conv_first(x)
|
| 585 |
+
x = self.conv_after_body(self.forward_features(x)) + x
|
| 586 |
+
x = self.conv_before_upsample(x)
|
| 587 |
+
x = self.conv_last(self.upsample(x))
|
| 588 |
+
|
| 589 |
+
return x[:, :, : h * self.upscale, : w * self.upscale]
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
@SPANDREL_REGISTRY.register()
|
| 593 |
+
@ARCH_REGISTRY.register()
|
| 594 |
+
def heart(
|
| 595 |
+
scale: int = 4,
|
| 596 |
+
img_size: int = 64,
|
| 597 |
+
patch_size: int = 1,
|
| 598 |
+
in_chans: int = 3,
|
| 599 |
+
embed_dim: int = 180,
|
| 600 |
+
depths: Sequence[int] = (6, 6, 6, 6, 6, 6),
|
| 601 |
+
num_heads: Sequence[int] = (6, 6, 6, 6, 6, 6),
|
| 602 |
+
window_size: int = 32,
|
| 603 |
+
compress_ratio: int = 3,
|
| 604 |
+
squeeze_factor: int = 30,
|
| 605 |
+
conv_scale: float = 0.01,
|
| 606 |
+
mlp_ratio: float = 2.0,
|
| 607 |
+
drop_rate: float = 0.0,
|
| 608 |
+
drop_path_rate: float = 0.1,
|
| 609 |
+
ape: bool = False,
|
| 610 |
+
patch_norm: bool = True,
|
| 611 |
+
use_checkpoint: bool = False,
|
| 612 |
+
upsampler: Literal["pixelshuffle"] = "pixelshuffle",
|
| 613 |
+
resi_connection: str = "1conv",
|
| 614 |
+
num_feat: int = 64,
|
| 615 |
+
rank: int = 8,
|
| 616 |
+
rib_hidden_dim: int = 32,
|
| 617 |
+
rib_n_freqs: int = 10,
|
| 618 |
+
attention_freq: int = 2,
|
| 619 |
+
) -> HEART:
|
| 620 |
+
return HEART(
|
| 621 |
+
upscale=scale,
|
| 622 |
+
img_size=img_size,
|
| 623 |
+
patch_size=patch_size,
|
| 624 |
+
in_chans=in_chans,
|
| 625 |
+
embed_dim=embed_dim,
|
| 626 |
+
depths=depths,
|
| 627 |
+
num_heads=num_heads,
|
| 628 |
+
window_size=window_size,
|
| 629 |
+
compress_ratio=compress_ratio,
|
| 630 |
+
squeeze_factor=squeeze_factor,
|
| 631 |
+
conv_scale=conv_scale,
|
| 632 |
+
mlp_ratio=mlp_ratio,
|
| 633 |
+
drop_rate=drop_rate,
|
| 634 |
+
drop_path_rate=drop_path_rate,
|
| 635 |
+
ape=ape,
|
| 636 |
+
patch_norm=patch_norm,
|
| 637 |
+
use_checkpoint=use_checkpoint,
|
| 638 |
+
upsampler=upsampler,
|
| 639 |
+
resi_connection=resi_connection,
|
| 640 |
+
num_feat=num_feat,
|
| 641 |
+
rank=rank,
|
| 642 |
+
rib_hidden_dim=rib_hidden_dim,
|
| 643 |
+
rib_n_freqs=rib_n_freqs,
|
| 644 |
+
attention_freq=attention_freq,
|
| 645 |
+
)
|
models/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*.safetensors
|
onnx/heart_2x_fp32_op17.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:08ee34f60303ac086c9b50012fdc3d261086d907756640623035cec1b6030102
|
| 3 |
+
size 71690367
|
onnx/heart_4x_otf_fp32_op17.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c2fd8a518b96d987dabaf7bbe284194fa26f14d2f972e34c8a4370c1d00df8ab
|
| 3 |
+
size 72281693
|
onnx/heart_4x_otf_gan_fp32_op17.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cf494433c0873c6893d324baff4d843afb2b96efcf54e4efca07a033a8308427
|
| 3 |
+
size 72281667
|
onnx/heart_4x_pretrain_fp32_op17.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c3b58cce1b18ba65c602f8937df40da75dfe1dbd208d661f48eb85f8380c4369
|
| 3 |
+
size 72281693
|
scripts/export_heart.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Minimal memory ONNX export. No validation, no onnxsim, tiny dummy shape.
|
| 3 |
+
If this fits in RAM, great. Otherwise the export must run on another machine."""
|
| 4 |
+
import gc, os, sys
|
| 5 |
+
import torch
|
| 6 |
+
from safetensors.torch import load_file
|
| 7 |
+
from traiNNer.archs.heart_arch import heart
|
| 8 |
+
|
| 9 |
+
ckpt, out, scale = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
| 10 |
+
torch.set_num_threads(2)
|
| 11 |
+
torch.set_grad_enabled(False)
|
| 12 |
+
|
| 13 |
+
sd = load_file(ckpt, device="cpu")
|
| 14 |
+
m = heart(scale=scale, ape=False, use_checkpoint=False).eval()
|
| 15 |
+
m.load_state_dict(sd, strict=True)
|
| 16 |
+
del sd; gc.collect()
|
| 17 |
+
|
| 18 |
+
# tiny dummy to minimize activation memory during trace
|
| 19 |
+
x = torch.randn(1, 3, 64, 64)
|
| 20 |
+
torch.onnx.export(
|
| 21 |
+
m, x, out,
|
| 22 |
+
input_names=["input"], output_names=["output"],
|
| 23 |
+
dynamic_axes={"input": {0:"batch",2:"height",3:"width"}, "output": {0:"batch",2:"height_out",3:"width_out"}},
|
| 24 |
+
opset_version=17, do_constant_folding=True, dynamo=False,
|
| 25 |
+
)
|
| 26 |
+
del m, x; gc.collect()
|
| 27 |
+
print(f"OK -> {out}")
|