Phips commited on
Commit
c77ff4a
Β·
verified Β·
1 Parent(s): 66c8d94

restore model card (accidental overwrite) + benchmark update

Browse files
Files changed (1) hide show
  1. README.md +381 -14
README.md CHANGED
@@ -1,20 +1,387 @@
1
  ---
2
- title: NERVE
3
- colorFrom: indigo
4
- colorTo: green
5
- sdk: gradio
6
- sdk_version: 5.25.2
7
- app_file: app.py
8
- pinned: false
9
- suggested_hardware: zero-a10g
 
 
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
- # NERVE β€” image upscaling & restoration
13
 
14
- A tiny demo for the released [NERVE](https://huggingface.co/Phips/NERVE) models.
15
 
16
- - **2x / 4x release** β€” for clean images.
17
- - **4x OTF GAN** β€” for degraded images (blurry, noisy, JPEG-compressed).
 
 
 
18
 
19
- Input: an image. Output: an uncompressed PNG you can download.
20
- Runs on ZeroGPU; large images are processed in tiles.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ library_name: pytorch
6
+ pipeline_tag: image-to-image
7
+ tags:
8
+ - super-resolution
9
+ - image-restoration
10
+ - image-to-image
11
+ - sisr
12
+ - real-world-restoration
13
+ - pytorch
14
+ - onnx
15
+ - ncnn
16
+ - spandrel
17
+ - chainner
18
+ - edge
19
+ - mobile
20
+ datasets:
21
+ - Phips/lucid-cc0-v2-hc-512
22
  ---
23
 
24
+ # NERVE
25
 
26
+ **Norm-free Efficient Restoration for Various Edge devices**
27
 
28
+ A lightweight super-resolution architecture that is simple to train and fast to
29
+ run. Released models are **verified** with **traiNNer-redux** (training) and
30
+ **ONNX Runtime** + **ncnn** (inference). The op set is standard, so other
31
+ runtimes (**CoreML**, **TensorRT**, DirectML, ...) are expected to work too β€”
32
+ but they are not tested here.
33
 
34
+ Part of the **BODY** suite by **Philip Hofmann** β€” networks built to be
35
+ real-world usable first, not benchmark-chasing. See
36
+ [Relation to HEART](#relation-to-heart) for which one to pick.
37
+
38
+ > Short version: copy `nerve_arch.py` into traiNNer-redux, train with the sample
39
+ > configs, then run the `.safetensors`, `.onnx`, or `.ncnn` models. One
40
+ > architecture, one size, no fused/unfused pairs. Details below.
41
+
42
+ ![NERVE 4x on anime β€” input | bicubic | release | OTF GAN](examples/ani_compare.png)
43
+ *Input (as-is) | bicubic x4 | NERVE 4x release | NERVE 4x OTF GAN.*
44
+
45
+ ![NERVE 4x on a noisy image](examples/noisy_compare.png)
46
+ ![NERVE 4x on a Stable-Diffusion image](examples/stablediffusion_compare.png)
47
+
48
+ ---
49
+
50
+ ## Why NERVE?
51
+
52
+ There are strong and established SR networks already. This is the honest case
53
+ for when NERVE is worth your time β€” and when it is not.
54
+
55
+ This project comes out of a trainer's frustration rather than a benchmark goal.
56
+ After training hundreds of SISR models (see
57
+ [Phhofm/models](https://github.com/Phhofm/models)), the recurring annoyance was
58
+ networks that felt like *paper* networks: they chase leaderboard metrics with
59
+ techniques that leave you juggling an unfused and a fused checkpoint, or whose
60
+ dynamic-ONNX conversion is fragile. This network is the opposite of that β€” built
61
+ so a trainer can pick it up, train it, and convert it to dynamic ONNX without
62
+ headaches.
63
+
64
+ **Strengths of NERVE**
65
+ - **Real-world usability over metric chasing.** Trained on a CC0 dataset with
66
+ the full Real-ESRGAN OTF degradation pipeline; the OTF GAN model restores
67
+ degraded images, not just clean bicubic downsamples.
68
+ - **One file, one variant.** A single readable architecture file and one model
69
+ size. No S/M/L matrix, no fused-vs-unfused checkpoints, nothing to fuse before
70
+ deploy β€” what you train is what you ship.
71
+ - **Export just works.** Dynamic-shape ONNX (verified, ORT parity ~1e-5) and a
72
+ clean ncnn conversion. Standard ops only (`Conv / Add / ReLU / DepthToSpace /
73
+ Resize`).
74
+ - **Boring, stable training.** Norm-free, bf16 AMP, no gradient clipping, a
75
+ single Charbonnier loss for the base stage.
76
+ - **Small and fast.** ~1.8M params β€” SPAN-class size but conv-only, suited to
77
+ mobile, edge, web and game engines.
78
+ - **Maintainable for years.** One op set, no exotic dependencies, ~85-line arch.
79
+
80
+ **When to pick something else.** If the goal is maximum PSNR/LPIPS on academic
81
+ benchmarks, or a large transformer, NERVE is not aiming there β€” use HEART, HAT,
82
+ ATD, DAT2, or one of the community networks below. NERVE aims at simplicity,
83
+ deployability and hassle-free training.
84
+
85
+ **Credit where it is due.** None of this exists in a vacuum. SISR has many
86
+ strong architectures and generous community members, for example HAT, ATD and
87
+ DAT2, the community network work of [umzi](https://github.com/umzi2) and
88
+ [Kim2091](https://github.com/Kim2091), fdat by
89
+ [sharekhan](https://github.com/stinkybread), and
90
+ [the-database](https://github.com/the-database) (traiNNer-redux, its benchmarks
91
+ and its models). This list is far from complete β€” apologies to anyone not named;
92
+ the SISR community has a great many people contributing a great deal.
93
+
94
+ ---
95
+
96
+ ## Quickstart
97
+
98
+ ### As a user (just want to upscale / restore images)
99
+
100
+ Pick a model and run it:
101
+
102
+ | I want to... | Use |
103
+ |---|---|
104
+ | upscale a clean image 4x | `models/4x_NERVE_release.safetensors` |
105
+ | upscale a clean image 2x | `models/2x_NERVE_release.safetensors` |
106
+ | restore a blurry / noisy / JPEG-y image 4x | `models/4x_NERVE_OTF_gan.safetensors` |
107
+ | run on-device / inside an engine | the `onnx/` or `ncnn/` files |
108
+
109
+ - **ONNX Runtime** (verified): use `onnx/*.onnx` (dynamic H/W). TensorRT /
110
+ DirectML should work too (same op set) but are untested here.
111
+ - **ncnn** (verified, fp16): `ncnn/*.ncnn.param` + `*.ncnn.bin`. Feed images with
112
+ `Mat.from_pixels`:
113
+ ```python
114
+ import ncnn
115
+ net = ncnn.Net(); net.load_param("4x_NERVE_fp16.ncnn.param"); net.load_model("4x_NERVE_fp16.ncnn.bin")
116
+ ex = net.create_extractor()
117
+ ex.input("in0", ncnn.Mat.from_pixels(bgr_uint8, ncnn.Mat.PixelType.PIXEL_BGR2RGB, w, h))
118
+ out = ex.extract("out0")[1] # CHW float, 4x
119
+ ```
120
+ (Use `from_pixels`; the ncnn Python binding's raw-array constructor misbehaves
121
+ on large inputs. Outputs are fp16, so expect small deviations at hard edges.)
122
+ - **chaiNNer**: load the `onnx/*.onnx` files directly. Loading the
123
+ `.safetensors` via spandrel needs NERVE registered in your chaiNNer/spandrel
124
+ build (not upstream yet).
125
+ - Input: RGB, any size. Output: 2x or 4x.
126
+
127
+ ### As a trainer (want to train your own)
128
+
129
+ NERVE is a normal [traiNNer-redux](https://github.com/the-database/traiNNer-redux)
130
+ architecture β€” no special steps.
131
+
132
+ ```bash
133
+ # 1. Add the architecture + its ICNR helper (auto-registers: traiNNer scans *_arch.py)
134
+ cp nerve_arch.py <traiNNer-redux>/traiNNer/archs/
135
+ cp icnr.py <traiNNer-redux>/traiNNer/utils/ # ICNR init helper (not in upstream yet)
136
+
137
+ # 2. Copy a training config and point it at your data
138
+ cp configs/4x_NERVE_release.yml <traiNNer-redux>/options/
139
+ # -> edit dataroot_gt / dataroot_lq in that file
140
+
141
+ # 3. Train
142
+ cd <traiNNer-redux>
143
+ python train.py -opt options/4x_NERVE_release.yml --auto_resume
144
+
145
+ # 4. Export to optimized dynamic ONNX (official converter)
146
+ cp configs/4x_NERVE_onnx.yml <traiNNer-redux>/options/
147
+ # -> set path.pretrain_network_g to your trained checkpoint
148
+ python convert_to_onnx.py -opt options/4x_NERVE_onnx.yml
149
+ ```
150
+
151
+ That is the whole workflow β€” 2 small files plus a config. The net is one file,
152
+ the config is one file, the checkpoint is one file β€” **no fusion step, no paired
153
+ fused/unfused weights.** (`icnr.py` is the only piece not in upstream
154
+ traiNNer-redux yet; it is the ICNR initializer for the PixelShuffle head.)
155
+
156
+ - `4x_NERVE_onnx.yml` exports dynamic-shape ONNX (opset 20) and can emit
157
+ `fp32`, `fp16`, or `bf16` (`onnx.dtype`), with OnnxSlim optimization and a
158
+ built-in PyTorch-vs-ONNX verify pass.
159
+ - Prefer no extra dependencies? `scripts/export_dynamic.py` is a tiny
160
+ standalone exporter (torch + onnxruntime only).
161
+
162
+ **Start from a released pretrain** (skip the expensive early training) by
163
+ setting `path.pretrain_network_g` in your training config to a file from
164
+ `models/`:
165
+
166
+ | training stage | warm-start from |
167
+ |---|---|
168
+ | `4x_NERVE_release.yml` | `models/4x_NERVE_release.safetensors` (or the 2x one with `strict_load_g: false`) |
169
+ | `4x_NERVE_OTF_fidelity.yml` | `models/4x_NERVE_release.safetensors` |
170
+ | `4x_NERVE_OTF_gan.yml` | your OTF fidelity checkpoint |
171
+
172
+ **Training tips** (all NERVE models follow these):
173
+ - bf16 AMP is fine; no gradient clipping needed (norm-free net).
174
+ - `--auto_resume` is recommended for long runs.
175
+ - For real-world input, follow the chain: clean release -> OTF fidelity -> OTF GAN.
176
+
177
+ ---
178
+
179
+ ## Models
180
+
181
+ | File | Role | Notes |
182
+ |---|---|---|
183
+ | `models/2x_NERVE_release.safetensors` | **2x official pretrain** | clean bicubic, 31.84 dB / 0.9255 SSIM (Urban100) |
184
+ | `models/4x_NERVE_release.safetensors` | **4x official pretrain** | clean bicubic, 25.09 dB / 0.7531 SSIM (Urban100) |
185
+ | `models/4x_NERVE_OTF_fidelity.safetensors` | 4x OTF fidelity | restore degraded input; the pretrain for OTF GAN finetunes |
186
+ | `models/4x_NERVE_OTF_gan.safetensors` | 4x OTF GAN | **sharpest real-world output** (topiq 0.430 / lpips 0.290, Urban100 OTF val) |
187
+ | `onnx/4x_NERVE_1x3xHxW_fp32_op20.onnx` | 4x release, dynamic ONNX | onnxslim-optimized, fp32 |
188
+ | `onnx/2x_NERVE_1x3xHxW_fp32_op20.onnx` | 2x release, dynamic ONNX | onnxslim-optimized, fp32 |
189
+ | `onnx/4x_NERVE_OTF_fidelity_1x3xHxW_fp32_op20.onnx` | 4x OTF fidelity, dynamic ONNX | onnxslim-optimized, fp32 |
190
+ | `onnx/4x_NERVE_OTF_gan_1x3xHxW_fp32_op20.onnx` | 4x OTF GAN, dynamic ONNX | onnxslim-optimized, fp32 |
191
+ | `ncnn/4x_NERVE_fp16.ncnn.param` + `.bin` | 4x release, ncnn | fp16 weights |
192
+ | `ncnn/2x_NERVE_fp16.ncnn.param` + `.bin` | 2x release, ncnn | fp16 weights |
193
+ | `ncnn/4x_NERVE_OTF_fidelity_fp16.ncnn.param` + `.bin` | 4x OTF fidelity, ncnn | fp16 weights |
194
+ | `ncnn/4x_NERVE_OTF_gan_fp16.ncnn.param` + `.bin` | 4x OTF GAN, ncnn | fp16 weights |
195
+
196
+ `models/*.safetensors` are plain EMA state dicts (no wrapper) β€” load them with
197
+ `nerve_arch.nerve(scale=N)`.
198
+
199
+ All released models (2x/4x release, 4x OTF fidelity, 4x OTF GAN) are final.
200
+
201
+ **Pretrain chain** (how the models relate):
202
+
203
+ ```
204
+ 2x_NERVE_release ──→ 4x_NERVE_release ──→ OTF fidelity ──→ OTF GAN
205
+ ```
206
+
207
+ The release models are the shared starting point for anyone training NERVE:
208
+ warm-start from them (2x β†’ 4x, or release β†’ OTF fidelity β†’ OTF GAN) to skip the
209
+ expensive early training.
210
+
211
+ ---
212
+
213
+ ## Examples
214
+
215
+ Real images are fed in **as-is** (not downscaled) and upscaled 4x. For each
216
+ image there are two comparisons:
217
+
218
+ - `examples/*_compare.png` β€” whole image at **full resolution**: **input** |
219
+ **bicubic x4** | **NERVE 4x release** | **NERVE 4x OTF GAN**.
220
+ - `examples/*_1to1.png` β€” a **1:1 native-pixel center crop**.
221
+
222
+ The bicubic column is the baseline; the OTF GAN column is the released showcase
223
+ model. The source inputs are in `examples/inputs/`.
224
+
225
+ - `examples/realphoto_compare.png` (street scene)
226
+ - `examples/ani_compare.png` (anime)
227
+ - `examples/manga_compare.png` (manga)
228
+ - `examples/text_compare.png` (text)
229
+ - `examples/noisy_compare.png` (noisy input)
230
+ - `examples/stablediffusion_compare.png` (AI-generated)
231
+
232
+ ## Architecture
233
+
234
+ ```
235
+ 5x5 conv stem β†’ 24 Γ— (Conv-ReLU-Conv + residual) β†’ PixelShuffle β†’ + bicubic(x)
236
+ ```
237
+
238
+ - **~1.8M params**, pure convolution β€” **no attention, no normalization, no
239
+ gating, no positional encoding, no reparameterization.**
240
+ - A **bicubic input residual** means the net only learns what bicubic upscaling
241
+ misses β€” that's why it stays small and trains fast.
242
+ - **ICNR-initialized** PixelShuffle head (checkerboard-free by construction).
243
+ - The op graph is trivial: `Conv, Add, ReLU, DepthToSpace, Resize`. That's the
244
+ entire reason it exports to dynamic ONNX and converts to ncnn (verified)
245
+ without custom ops or fused/unfused pairs; CoreML is expected but untested.
246
+
247
+ ## What we actually tested
248
+
249
+ Every design choice here came from a measured ablation, not a guess. The full
250
+ development log β€” including dead ends and raw numbers β€” is in
251
+ [`docs/ABLATIONS.md`](docs/ABLATIONS.md). Highlights:
252
+
253
+ - **Normalization**: removing LayerNorm beat the normed baseline by **+1.15 dB**
254
+ at ~2x the training speed -> norm-free.
255
+ - **Depth/width sweep + capacity scaling**: quality improved with size; the
256
+ sweep put the GAN quality knee at **1.80M** (0.69M -> 1.80M: lpips -0.015;
257
+ 1.80M -> 2.39M: only -0.002 more).
258
+ - **Dilation**: a dilated-conv A/B under the OTF recipe was **null** -> plain convs.
259
+ - **Checkerboard**: diagnosed a PixelShuffle phase-lock, fixed with ICNR plus
260
+ *keeping aliased (nearest-exact) LR*. An anti-aliased-LR + sub-pixel-jitter
261
+ variant made thin lines *worse* and was reverted.
262
+ - **OTF/GAN capability**: the small net trained productively under the full
263
+ Real-ESRGAN OTF GAN recipe (no collapse).
264
+ - **Runtime validation**: every released `.safetensors` loads (strict) and runs;
265
+ every `.onnx` loads in ONNX Runtime; the ncnn models were run at multiple input
266
+ sizes up to 720x720 (2880x2880 output).
267
+ - **Deployability**: dynamic-ONNX export + ORT parity and ncnn conversion were
268
+ verified at every stage (the numbers are in the log).
269
+ - **Not tested here** (community welcome): TensorRT / DirectML *speed* and
270
+ fixed-shape static exports, CoreML, and on-device benchmarks. Train more
271
+ variants, benchmark, and share β€” the arch is standard ops only.
272
+
273
+ ## Training data & license
274
+
275
+ All released NERVE models were trained **only** on
276
+ [`Phips/lucid-cc0-v2-hc-512`](https://huggingface.co/datasets/Phips/lucid-cc0-v2-hc-512),
277
+ a **CC0** dataset β€” the released weights are clean to use commercially.
278
+
279
+ We did **not** optimize for benchmark leaderboards. If you want higher
280
+ PSNR/LPIPS numbers, training on larger academic datasets (DIV2K, Flickr2K,
281
+ LSDIR, ...) is expected to score better; NERVE is a normal architecture, train
282
+ it on whatever you like.
283
+
284
+ ## Relation to HEART
285
+
286
+ NERVE and HEART are the two members of the **BODY** suite (by Philip Hofmann):
287
+ same philosophy (real-world usability > benchmark scores), different tiers.
288
+
289
+ | | **HEART** | **NERVE** |
290
+ |---|---|---|
291
+ | size | ~16.7M params | ~1.8M params |
292
+ | design | attention (FlashAttention-friendly) | pure convolution |
293
+ | use when | you want the **best quality** and have the compute (desktop, server, high-end device) | you want **speed, small size, and dead-simple deployment** (mobile, edge, web, game engines) |
294
+ | cost | heavier to run | a fraction of the cost |
295
+
296
+ Rule of thumb: **HEART for maximum quality, NERVE for maximum practicality.**
297
+ Both export to dynamic ONNX; NERVE additionally converts to ncnn cleanly
298
+ because it has no attention ops.
299
+
300
+ Side-by-side comparisons on the same inputs (`input | bicubic | both release
301
+ models | both OTF GAN models`), each with a 1:1 crop:
302
+ `examples/OST_009_nerve_vs_heart_*` and `examples/00003_nerve_vs_heart_*`
303
+ (Real-ESRGAN test set), plus `examples/realphoto_nerve_vs_heart_*` and
304
+ `examples/noisy_nerve_vs_heart_*`.
305
+
306
+ ---
307
+
308
+ ## Repository layout
309
+
310
+ ```
311
+ nerve_arch.py the architecture (copy to traiNNer/archs/)
312
+ icnr.py ICNR init helper (copy to traiNNer/utils/; not in upstream yet)
313
+ configs/ example training configs (+ 4x_NERVE_onnx.yml for ONNX export)
314
+ models/ pretrained checkpoints (see table above)
315
+ onnx/ dynamic fp32 ONNX (opset 20, onnxslim-optimized)
316
+ ncnn/ ncnn fp16 param/bin
317
+ docs/ABLATIONS.md the full development/ablation log (what we tested)
318
+ examples/ real-image comparisons (input vs bicubic vs release vs GAN)
319
+ scripts/export_dynamic.py checkpoint β†’ dynamic ONNX exporter (+ ORT verify)
320
+ LICENSE Apache-2.0
321
+ ```
322
+
323
+ ## Help wanted (community welcome)
324
+
325
+ The goal here is **real-world usability** β€” easy training, hassle-free dynamic
326
+ ONNX, simple and robust to maintain β€” **not** leaderboard chasing. So NERVE's
327
+ strength is *simplicity for trainers*; there's still plenty worth measuring.
328
+ Pick anything below and open an issue/PR/discussion with your numbers.
329
+
330
+ **Benchmarks** (deliberately not chased here)
331
+ - Train on an academic dataset (DIV2K / Flickr2K / LSDIR) and compare against
332
+ SPAN, SRVGGNetCompact, HAT, Real-ESRGAN (PSNR / SSIM / LPIPS).
333
+ - Perceptual metrics (LPIPS / TopIQ / MUSIQ) on real-world degraded images.
334
+
335
+ **Speed & deployment** (not benchmarked here)
336
+ - TensorRT / DirectML latency + peak VRAM on the dynamic ONNX.
337
+ - Static-shape ONNX (e.g. `1x3x256x256`) vs dynamic: speed + memory.
338
+ - fp16 vs fp32 ONNX.
339
+ - ncnn on-device (Android / iOS): latency + memory.
340
+ - CoreML conversion + on-device test.
341
+ - Mobile CPU (XNNPACK / ORT Mobile / ncnn).
342
+
343
+ **Models & code**
344
+ - More finetunes (illustration, manga, denoise, 1x restoration).
345
+ - Architecture experiments β€” see [`docs/ABLATIONS.md`](docs/ABLATIONS.md) for
346
+ what we already tested (and rejected).
347
+
348
+ Small, measured contributions are welcome.
349
+
350
+ ## License
351
+
352
+ Apache-2.0 (this repository's code and pretrained weights).
353
+
354
+ ## Update β€” 2026-09-22: speed benchmark (honest numbers)
355
+
356
+ We ran a head-to-head **inference benchmark on an RTX 3060** using the
357
+ traiNNer-redux benchmark protocol (input `1x3x480x640`, fp16, 50 runs), against
358
+ the closest networks by parameter count:
359
+
360
+ | model | params | PyTorch fp16 | TensorRT fp16 | ncnn (Vulkan GPU) |
361
+ |---|---|---|---|---|
362
+ | **NERVE 2x** | 1.78M | 90.0 ms | 35.3 ms | 117 ms |
363
+ | SPAN_S 2x | 2.22M | 40.4 ms | **16.8 ms** | β€” |
364
+ | PLKSR_tiny 2x | 2.35M | 110.1 ms | 380.6 ms | β€” |
365
+ | compact 2x | 0.60M | 35.7 ms | 19.1 ms | β€” |
366
+
367
+ **NERVE does not win on speed.** SPAN_S is ~2.1x faster at a similar parameter
368
+ count, and compact is faster still. (PLKSR_tiny is slower in PyTorch and falls
369
+ apart in ONNX/TensorRT because its large-kernel/CCM ops don't map, so NERVE does
370
+ beat it there.)
371
+
372
+ That is neither a surprise nor a problem for what NERVE is for. **NERVE is not a
373
+ benchmark-chasing network.** It exists for ease of use and real-world usability:
374
+ one variant, one file, no fused/unfused checkpoint pairs, and a checkpoint that
375
+ converts to dynamic-shape ONNX, TensorRT and ncnn with no special handling.
376
+ Speed is good enough, not the goal.
377
+
378
+ Practical notes from the benchmark:
379
+ - **TensorRT is the fastest runtime for NERVE** (~2.5x over PyTorch), and it
380
+ wants a **static shape** β€” that is where static exports actually matter.
381
+ - In ONNX Runtime, static vs dynamic made no measurable difference.
382
+ - fp16 β‰ˆ bf16.
383
+ - **ncnn runs on the GPU via Vulkan** (117 ms vs 5.4 s on CPU), but that is the
384
+ Python binding; native ncnn on a phone is the intended path, not a desktop race.
385
+
386
+ Choose SPAN if you want maximum speed. Choose NERVE if you want the simplest
387
+ thing to train, convert and ship.