Phips commited on
Commit
b415c47
·
1 Parent(s): 331cc92

HEART release package: arch, configs, ONNX, README, license

Browse files
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}")