Phips commited on
Commit
603edfb
·
verified ·
1 Parent(s): d7333f2

restore nerve_arch.py (was removed by mistake)

Browse files
Files changed (1) hide show
  1. nerve_arch.py +86 -0
nerve_arch.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NERVE — lightweight super-resolution for mobile/edge (single config).
2
+ #
3
+ # Conv-ReLU-Conv residual blocks, no normalization, no attention, no gating.
4
+ # A 5x5 stem opens the receptive field for real-world degradations; a global
5
+ # residual and a bicubic input residual let the net learn only what bicubic
6
+ # upscaling misses. dim=64, n_blocks=24, ~1.8M params at 4x.
7
+ #
8
+ # Norm-free is decided by ablation (NERVE_ABLATION_LOG.md §4): removing
9
+ # LayerNorm won +1.15 dB at ~2x the training speed. dim=64/n_blocks=24 is the
10
+ # capacity-sweep sweet spot (§15): 0.69M -> 1.80M bought a large GAN quality
11
+ # gain (lpips -0.015), while 1.80M -> 2.39M bought ~4x less gain per parameter.
12
+ # Every op in the graph is standard (Conv/Add/ReLU/PixelShuffle/bicubic
13
+ # Resize), so the checkpoint exports to dynamic-shape ONNX and converts to
14
+ # NCNN/CoreML without fused/unfused pairs.
15
+
16
+ from spandrel.util import store_hyperparameters
17
+ from torch import Tensor, nn
18
+ from torch.nn import functional as F # noqa: N812
19
+
20
+ from traiNNer.utils.icnr import icnr_reinit
21
+ from traiNNer.utils.registry import ARCH_REGISTRY, SPANDREL_REGISTRY
22
+
23
+
24
+ class NerveBlock(nn.Module):
25
+ """Conv-ReLU-Conv residual block. No normalization, no shortcuts."""
26
+
27
+ def __init__(self, dim: int) -> None:
28
+ super().__init__()
29
+ self.conv1 = nn.Conv2d(dim, dim, 3, padding=1, bias=False)
30
+ self.conv2 = nn.Conv2d(dim, dim, 3, padding=1, bias=False)
31
+
32
+ def forward(self, x: Tensor) -> Tensor:
33
+ out = self.conv2(F.relu(self.conv1(x)))
34
+ return x + out
35
+
36
+
37
+ @store_hyperparameters()
38
+ class NERVE(nn.Module):
39
+ """5x5 stem -> n_blocks residual blocks -> PixelShuffle head -> + bicubic(x)."""
40
+
41
+ hyperparameters = {} # noqa: RUF012
42
+
43
+ def __init__(
44
+ self,
45
+ *,
46
+ in_ch: int = 3,
47
+ out_ch: int = 3,
48
+ dim: int = 64,
49
+ n_blocks: int = 24,
50
+ upscale: int = 4,
51
+ ) -> None:
52
+ super().__init__()
53
+ self.upscale = upscale
54
+
55
+ self.stem = nn.Conv2d(in_ch, dim, 5, padding=2, bias=False)
56
+ self.blocks = nn.Sequential(*[NerveBlock(dim) for _ in range(n_blocks)])
57
+ self.upsampler = nn.Sequential(
58
+ nn.Conv2d(dim, out_ch * upscale**2, 3, padding=1, bias=False),
59
+ nn.PixelShuffle(upscale),
60
+ )
61
+ # ICNR: every sub-pixel phase starts from the same base kernel, so the
62
+ # head cannot train up a checkerboard. Init-only, zero graph/runtime cost.
63
+ icnr_reinit(self.upsampler)
64
+
65
+ def forward(self, x: Tensor) -> Tensor:
66
+ feat = self.stem(x)
67
+ out = feat + self.blocks(feat)
68
+ out = self.upsampler(out)
69
+
70
+ base = F.interpolate(
71
+ x, scale_factor=self.upscale, mode="bicubic", align_corners=False
72
+ )
73
+ return out + base
74
+
75
+
76
+ @SPANDREL_REGISTRY.register()
77
+ @ARCH_REGISTRY.register()
78
+ def nerve(
79
+ scale: int = 4,
80
+ *,
81
+ in_ch: int = 3,
82
+ out_ch: int = 3,
83
+ dim: int = 64,
84
+ n_blocks: int = 24,
85
+ ) -> NERVE:
86
+ return NERVE(in_ch=in_ch, out_ch=out_ch, dim=dim, n_blocks=n_blocks, upscale=scale)